tryouts 2.4.1 → 3.0.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,344 @@
1
+ # lib/tryouts/cli/formatters/verbose.rb
2
+
3
+ class Tryouts
4
+ class CLI
5
+ # Detailed formatter with comprehensive output and clear visual hierarchy
6
+ class VerboseFormatter
7
+ include FormatterInterface
8
+
9
+ def initialize(options = {})
10
+ @line_width = options.fetch(:line_width, 70)
11
+ @show_passed = options.fetch(:show_passed, true)
12
+ @show_debug = options.fetch(:debug, false)
13
+ @show_trace = options.fetch(:trace, false)
14
+ @current_indent = 0
15
+ end
16
+
17
+ # Phase-level output
18
+ def phase_header(message, _file_count = nil, level = 0)
19
+ return if level.equal?(1)
20
+
21
+ separators = [
22
+ { char: '=', width: @line_width }, # Major phases
23
+ { char: '-', width: @line_width - 10 }, # Sub-phases
24
+ { char: '.', width: @line_width - 20 }, # Details
25
+ { char: '~', width: @line_width - 30 }, # Minor items
26
+ ]
27
+
28
+ config = separators[level] || separators.last
29
+
30
+ separator_line = config[:char] * config[:width]
31
+ header_line = message.center(config[:width])
32
+
33
+ output = case level
34
+ when 0, 1
35
+ ['', separator_line, header_line, separator_line]
36
+ else
37
+ ['', header_line, separator_line]
38
+ end
39
+
40
+ with_indent(level) do
41
+ puts output.join("\n")
42
+ end
43
+ puts
44
+ end
45
+
46
+ # File-level operations
47
+ def file_start(file_path, _context_info = {})
48
+ puts file_header_visual(file_path)
49
+ end
50
+
51
+ def file_parsed(_file_path, _test_count, setup_present: false, teardown_present: false)
52
+ message = ''
53
+
54
+ extras = []
55
+ extras << 'setup' if setup_present
56
+ extras << 'teardown' if teardown_present
57
+ message += " (#{extras.join(', ')})" unless extras.empty?
58
+
59
+ puts indent_text(message, 2)
60
+ end
61
+
62
+ def file_execution_start(_file_path, test_count, context_mode)
63
+ message = "Running #{test_count} tests with #{context_mode} context"
64
+ puts indent_text(message, 1)
65
+ end
66
+
67
+ def file_result(_file_path, total_tests, failed_count, error_count, elapsed_time)
68
+ issues_count = failed_count + error_count
69
+ details = []
70
+
71
+ status = if issues_count > 0
72
+ details << "#{failed_count} failed" if failed_count > 0
73
+ details << "#{error_count} errors" if error_count > 0
74
+ details_str = details.join(', ')
75
+ Console.color(:red, "✗ #{issues_count}/#{total_tests} tests had issues (#{details_str})")
76
+ else
77
+ Console.color(:green, "✓ #{total_tests} tests passed")
78
+ end
79
+
80
+ puts indent_text(status, 2)
81
+ return unless elapsed_time
82
+
83
+ time_msg =
84
+ if elapsed_time < 2.0
85
+ "Completed in #{(elapsed_time * 1000).round}ms"
86
+ else
87
+ "Completed in #{elapsed_time.round(3)}s"
88
+ end
89
+ puts indent_text(Console.color(:dim, time_msg), 2)
90
+ end
91
+
92
+ # Test-level operations
93
+ def test_start(test_case, index, total)
94
+ desc = test_case.description.to_s
95
+ desc = 'Unnamed test' if desc.empty?
96
+ message = "Test #{index}/#{total}: #{desc}"
97
+ puts indent_text(Console.color(:dim, message), 2)
98
+ end
99
+
100
+ def test_result(test_case, result_status, actual_results = [], _elapsed_time = nil)
101
+ should_show = @show_passed || result_status != :passed
102
+
103
+ return unless should_show
104
+
105
+ status_line = case result_status
106
+ when :passed
107
+ Console.color(:green, 'PASSED')
108
+ when :failed
109
+ Console.color(:red, 'FAILED')
110
+ when :error
111
+ Console.color(:red, 'ERROR')
112
+ when :skipped
113
+ Console.color(:yellow, 'SKIPPED')
114
+ else
115
+ 'UNKNOWN'
116
+ end
117
+
118
+ location = "#{Console.pretty_path(test_case.path)}:#{test_case.line_range.first + 1}"
119
+ puts indent_text("#{status_line} #{test_case.description} @ #{location}", 2)
120
+
121
+ # Show source code for verbose mode
122
+ show_test_source_code(test_case)
123
+
124
+ # Show failure details for failed tests
125
+ if [:failed, :error].include?(result_status)
126
+ show_failure_details(test_case, actual_results)
127
+ end
128
+ end
129
+
130
+ def test_output(_test_case, output_text)
131
+ return if output_text.nil? || output_text.strip.empty?
132
+
133
+ puts indent_text('Test Output:', 3)
134
+ puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 3)
135
+
136
+ output_text.lines.each do |line|
137
+ puts indent_text(line.chomp, 4)
138
+ end
139
+
140
+ puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 3)
141
+ puts
142
+ end
143
+
144
+ # Setup/teardown operations
145
+ def setup_start(line_range)
146
+ message = "Executing global setup (lines #{line_range.first}..#{line_range.last})"
147
+ puts indent_text(Console.color(:cyan, message), 2)
148
+ end
149
+
150
+ def setup_output(output_text)
151
+ return if output_text.strip.empty?
152
+
153
+ output_text.lines.each do |line|
154
+ puts indent_text(line.chomp, 0)
155
+ end
156
+ end
157
+
158
+ def teardown_start(line_range)
159
+ message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
160
+ puts indent_text(Console.color(:cyan, message), 2)
161
+ end
162
+
163
+ def teardown_output(output_text)
164
+ return if output_text.strip.empty?
165
+
166
+ output_text.lines.each do |line|
167
+ puts indent_text(line.chomp, 0)
168
+ end
169
+ end
170
+
171
+ # Summary operations
172
+ def batch_summary(total_tests, failed_count, elapsed_time)
173
+ if failed_count > 0
174
+ passed = total_tests - failed_count
175
+ message = "#{failed_count} failed, #{passed} passed"
176
+ color = :red
177
+ else
178
+ message = "#{total_tests} tests passed"
179
+ color = :green
180
+ end
181
+
182
+ time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
183
+ summary = Console.color(color, "#{message}#{time_str}")
184
+ puts summary
185
+ end
186
+
187
+ def grand_total(total_tests, failed_count, error_count, successful_files, total_files, elapsed_time)
188
+ puts
189
+ puts '=' * @line_width
190
+ puts 'Grand Total:'
191
+
192
+ issues_count = failed_count + error_count
193
+ time_str =
194
+ if elapsed_time < 2.0
195
+ " (#{(elapsed_time * 1000).round}ms)"
196
+ else
197
+ " (#{elapsed_time.round(2)}s)"
198
+ end
199
+
200
+ if issues_count > 0
201
+ passed = total_tests - issues_count
202
+ details = []
203
+ details << "#{failed_count} failed" if failed_count > 0
204
+ details << "#{error_count} errors" if error_count > 0
205
+ puts "#{details.join(', ')}, #{passed} passed#{time_str}"
206
+ else
207
+ puts "#{total_tests} tests passed#{time_str}"
208
+ end
209
+
210
+ puts "Files processed: #{successful_files} of #{total_files} successful"
211
+ puts '=' * @line_width
212
+ end
213
+
214
+ # Debug and diagnostic output
215
+ def debug_info(message, level = 0)
216
+ return unless @show_debug
217
+
218
+ prefix = Console.color(:cyan, 'INFO ')
219
+ puts indent_text("#{prefix} #{message}", level + 1)
220
+ end
221
+
222
+ def trace_info(message, level = 0)
223
+ return unless @show_trace
224
+
225
+ prefix = Console.color(:dim, 'TRACE')
226
+ puts indent_text("#{prefix} #{message}", level + 1)
227
+ end
228
+
229
+ def error_message(message, backtrace = nil)
230
+ error_msg = Console.color(:red, "ERROR: #{message}")
231
+ puts indent_text(error_msg, 1)
232
+
233
+ return unless backtrace && @show_debug
234
+
235
+ puts indent_text('Details:', 2)
236
+ # Show first 10 lines of backtrace to avoid overwhelming output
237
+ backtrace.first(10).each do |line|
238
+ puts indent_text(line, 3)
239
+ end
240
+ puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
241
+ end
242
+
243
+ # Utility methods
244
+ def raw_output(text)
245
+ puts text
246
+ end
247
+
248
+ def separator(style = :light)
249
+ case style
250
+ when :heavy
251
+ puts '=' * @line_width
252
+ when :light
253
+ puts '-' * @line_width
254
+ when :dotted
255
+ puts '.' * @line_width
256
+ else # rubocop:disable Lint/DuplicateBranch
257
+ puts '-' * @line_width
258
+ end
259
+ end
260
+
261
+ private
262
+
263
+ def show_test_source_code(test_case)
264
+ puts indent_text('Source code:', 3)
265
+
266
+ # Use pre-captured source lines from parsing
267
+ start_line = test_case.line_range.first
268
+
269
+ test_case.source_lines.each_with_index do |line_content, index|
270
+ line_num = start_line + index
271
+ line_display = format('%3d: %s', line_num + 1, line_content)
272
+
273
+ # Highlight expectation lines by checking if this line
274
+ # contains the expectation syntax
275
+ if line_content.match?(/^\s*#\s*=>\s*/)
276
+ line_display = Console.color(:yellow, line_display)
277
+ end
278
+
279
+ puts indent_text(line_display, 4)
280
+ end
281
+ puts
282
+ end
283
+
284
+ def show_failure_details(test_case, actual_results)
285
+ return if actual_results.empty?
286
+
287
+ puts indent_text('Expected vs Actual:', 3)
288
+
289
+ actual_results.each_with_index do |actual, idx|
290
+ expected_line = test_case.expectations[idx] if test_case.expectations
291
+
292
+ if expected_line
293
+ puts indent_text("Expected: #{Console.color(:green, expected_line)}", 4)
294
+ puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
295
+ else
296
+ puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
297
+ end
298
+
299
+ # Show difference if both are strings
300
+ if expected_line && actual.is_a?(String) && expected_line.is_a?(String)
301
+ show_string_diff(expected_line, actual)
302
+ end
303
+
304
+ puts
305
+ end
306
+ end
307
+
308
+ def show_string_diff(expected, actual)
309
+ return if expected == actual
310
+
311
+ puts indent_text('Difference:', 4)
312
+ puts indent_text("- #{Console.color(:red, actual)}", 5)
313
+ puts indent_text("+ #{Console.color(:green, expected)}", 5)
314
+ end
315
+
316
+ def file_header_visual(file_path)
317
+ pretty_path = Console.pretty_path(file_path)
318
+ header_content = ">>>>> #{pretty_path} "
319
+ padding_length = [@line_width - header_content.length, 0].max
320
+ padding = '<' * padding_length
321
+
322
+ [
323
+ '-' * @line_width,
324
+ header_content + padding,
325
+ '-' * @line_width,
326
+ ].join("\n")
327
+ end
328
+ end
329
+
330
+ # Verbose formatter that only shows failures and errors
331
+ class VerboseFailsFormatter < VerboseFormatter
332
+ def initialize(options = {})
333
+ super(options.merge(show_passed: false))
334
+ end
335
+
336
+ def test_result(test_case, result_status, actual_results = [], elapsed_time = nil)
337
+ # Only show failed/error tests, but with full source code
338
+ return if result_status == :passed
339
+
340
+ super
341
+ end
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,6 @@
1
+ # lib/tryouts/cli/formatters.rb
2
+
3
+ require_relative 'formatters/base'
4
+ require_relative 'formatters/compact'
5
+ require_relative 'formatters/quiet'
6
+ require_relative 'formatters/verbose'
@@ -0,0 +1,22 @@
1
+ # lib/tryouts/cli/modes/generate.rb
2
+
3
+ class Tryouts
4
+ class CLI
5
+ class GenerateMode
6
+ def initialize(file, testrun, options, output_manager, translator)
7
+ @file = file
8
+ @testrun = testrun
9
+ @options = options
10
+ @output_manager = output_manager
11
+ @translator = translator
12
+ end
13
+
14
+ def handle
15
+ @output_manager.raw("# Generated #{@options[:framework]} code for #{@file}")
16
+ @output_manager.raw("# Updated: #{Time.now}")
17
+ @output_manager.raw(@translator.generate_code(@testrun))
18
+ @output_manager.raw('')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # lib/tryouts/cli/modes/inspect.rb
2
+
3
+ class Tryouts
4
+ class CLI
5
+ class InspectMode
6
+ def initialize(file, testrun, options, output_manager, translator)
7
+ @file = file
8
+ @testrun = testrun
9
+ @options = options
10
+ @output_manager = output_manager
11
+ @translator = translator
12
+ end
13
+
14
+ def handle
15
+ @output_manager.raw("Inspecting: #{@file}")
16
+ @output_manager.separator(:heavy)
17
+ @output_manager.raw("Found #{@testrun.total_tests} test cases")
18
+ @output_manager.raw("Setup code: #{@testrun.setup.empty? ? 'None' : 'Present'}")
19
+ @output_manager.raw("Teardown code: #{@testrun.teardown.empty? ? 'None' : 'Present'}")
20
+ @output_manager.raw('')
21
+
22
+ @testrun.test_cases.each_with_index do |tc, i|
23
+ @output_manager.raw("Test #{i + 1}: #{tc.description}")
24
+ @output_manager.raw(" Code lines: #{tc.code.lines.count}")
25
+ @output_manager.raw(" Expectations: #{tc.expectations.size}")
26
+ @output_manager.raw(" Range: #{tc.line_range}")
27
+ @output_manager.raw('')
28
+ end
29
+
30
+ return unless @options[:framework] != :direct
31
+
32
+ @output_manager.raw("Testing #{@options[:framework]} translation...")
33
+ framework_klass = TestRunner::FRAMEWORKS[@options[:framework]]
34
+ inspect_translator = framework_klass.new
35
+
36
+ translated_code = inspect_translator.generate_code(@testrun)
37
+ @output_manager.raw("#{@options[:framework].to_s.capitalize} code generated (#{translated_code.lines.count} lines)")
38
+ @output_manager.raw('')
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,88 @@
1
+ # lib/tryouts/cli/opts.rb
2
+
3
+ class Tryouts
4
+ class CLI
5
+ HELP = <<~HELP
6
+
7
+ Framework Defaults:
8
+ Tryouts: Shared context (state persists across tests)
9
+ RSpec: Fresh context (each test isolated)
10
+ Minitest: Fresh context (each test isolated)
11
+
12
+ Examples:
13
+ try test_try.rb # Tryouts test runner with shared context
14
+ try --rspec test_try.rb # RSpec with fresh context
15
+ try --direct --shared-context test_try.rb # Explicit shared context
16
+ try --generate-rspec test_try.rb # Output RSpec code only
17
+ try --inspect test_try.rb # Inspect file structure and validation
18
+
19
+ File Format:
20
+ ## Test description # Test case marker
21
+ code_to_test # Ruby code
22
+ #=> expected_result # Expectation
23
+ HELP
24
+
25
+ class << self
26
+ def parse_args(args)
27
+ Tryouts.trace "Parsing arguments: #{args.inspect}"
28
+ options = {}
29
+
30
+ parser = OptionParser.new do |opts|
31
+ opts.banner = "Usage: try [OPTIONS] FILE...\n\nModern Tryouts test runner with framework translation"
32
+
33
+ opts.separator "\nFramework Options:"
34
+ opts.on('--direct', 'Direct execution with TestBatch (default)') { options[:framework] = :direct }
35
+ opts.on('--rspec', 'Use RSpec framework') { options[:framework] = :rspec }
36
+ opts.on('--minitest', 'Use Minitest framework') { options[:framework] = :minitest }
37
+
38
+ opts.separator "\nGeneration Options:"
39
+ opts.on('--generate-rspec', 'Generate RSpec code only') do
40
+ options[:framework] = :rspec
41
+ options[:generate_only] = true
42
+ end
43
+ opts.on('--generate-minitest', 'Generate Minitest code only') do
44
+ options[:framework] = :minitest
45
+ options[:generate_only] = true
46
+ end
47
+ opts.on('--generate', 'Generate code only (use with --rspec/--minitest)') do
48
+ options[:generate_only] = true
49
+ options[:framework] ||= :rspec
50
+ end
51
+
52
+ opts.separator "\nExecution Options:"
53
+ opts.on('--shared-context', 'Override default context mode') { options[:shared_context] = true }
54
+ opts.on('--no-shared-context', 'Override default context mode') { options[:shared_context] = false }
55
+ opts.on('-v', '--verbose', 'Show detailed test output with line numbers') { options[:verbose] = true }
56
+ opts.on('-f', '--fails', 'Show only failing tests (with --verbose)') { options[:fails_only] = true }
57
+ opts.on('-q', '--quiet', 'Minimal output (dots and summary only)') { options[:quiet] = true }
58
+ opts.on('-c', '--compact', 'Compact single-line output') { options[:compact] = true }
59
+
60
+ opts.separator "\nInspection Options:"
61
+ opts.on('-i', '--inspect', 'Inspect file structure without running tests') { options[:inspect] = true }
62
+
63
+ opts.separator "\nGeneral Options:"
64
+ opts.on('-V', '--version', 'Show version') { options[:version] = true }
65
+ opts.on('-D', '--debug', 'Enable debug mode') do
66
+ options[:debug] = true
67
+ Tryouts.debug = true
68
+ end
69
+ opts.on('-h', '--help', 'Show this help') do
70
+ puts opts
71
+ exit 0
72
+ end
73
+
74
+ opts.separator HELP.freeze
75
+ end
76
+
77
+ files = parser.parse(args)
78
+ Tryouts.trace "Parsed files: #{files.inspect}, options: #{options.inspect}"
79
+ [files, options]
80
+ rescue OptionParser::InvalidOption => ex
81
+ Tryouts.info Console.color(:red, "Invalid option error: #{ex.message}")
82
+ warn "Error: #{ex.message}"
83
+ warn "Try 'try --help' for more information."
84
+ exit 1
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,54 @@
1
+ # lib/tryouts/cli.rb
2
+
3
+ require 'optparse'
4
+
5
+ require_relative 'cli/opts'
6
+ require_relative 'cli/formatters'
7
+ require_relative 'test_runner'
8
+
9
+ class Tryouts
10
+ class CLI
11
+ def initialize
12
+ @options = {
13
+ framework: :direct,
14
+ verbose: false,
15
+ inspect: false,
16
+ }
17
+ end
18
+
19
+ def run(files, **options)
20
+ @options.merge!(options)
21
+
22
+ output_manager = FormatterFactory.create_output_manager(@options)
23
+
24
+ handle_version_flag(@options, output_manager)
25
+ validate_files_exist(files, output_manager)
26
+
27
+ runner = TestRunner.new(
28
+ files: files,
29
+ options: @options,
30
+ output_manager: output_manager,
31
+ )
32
+
33
+ runner.run
34
+ end
35
+
36
+ private
37
+
38
+ def handle_version_flag(options, output_manager)
39
+ return unless options[:version]
40
+
41
+ output_manager.raw("Tryouts version #{Tryouts::VERSION}")
42
+ exit 0
43
+ end
44
+
45
+ def validate_files_exist(files, output_manager)
46
+ missing_files = files.reject { |file| File.exist?(file) }
47
+
48
+ unless missing_files.empty?
49
+ missing_files.each { |file| output_manager.error("File not found: #{file}") }
50
+ exit 1
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,4 +1,6 @@
1
- # frozen_string_literal: true
1
+ # lib/tryouts/console.rb
2
+
3
+ require 'pathname'
2
4
 
3
5
  class Tryouts
4
6
  module Console
@@ -12,7 +14,7 @@ class Tryouts
12
14
  blink: 5,
13
15
  reverse: 7,
14
16
  hidden: 8,
15
- default: 0
17
+ default: 0,
16
18
  }.freeze
17
19
  end
18
20
 
@@ -28,7 +30,7 @@ class Tryouts
28
30
  cyan: 36,
29
31
  white: 37,
30
32
  default: 39,
31
- random: 30 + rand(10).to_i
33
+ random: 30 + rand(10).to_i,
32
34
  }.freeze
33
35
  end
34
36
 
@@ -44,7 +46,7 @@ class Tryouts
44
46
  cyan: 46,
45
47
  white: 47,
46
48
  default: 49,
47
- random: 40 + rand(10).to_i
49
+ random: 40 + rand(10).to_i,
48
50
  }.freeze
49
51
  end
50
52
 
@@ -73,50 +75,63 @@ class Tryouts
73
75
  Console.bgcolor(col, self)
74
76
  end
75
77
  end
78
+ class << self
79
+ def bright(str)
80
+ str = [style(ATTRIBUTES[:bright]), str, default_style].join
81
+ str.extend Console::InstanceMethods
82
+ str
83
+ end
76
84
 
77
- def self.bright(str)
78
- str = [style(ATTRIBUTES[:bright]), str, default_style].join
79
- str.extend Console::InstanceMethods
80
- str
81
- end
85
+ def underline(str)
86
+ str = [style(ATTRIBUTES[:underline]), str, default_style].join
87
+ str.extend Console::InstanceMethods
88
+ str
89
+ end
82
90
 
83
- def self.underline(str)
84
- str = [style(ATTRIBUTES[:underline]), str, default_style].join
85
- str.extend Console::InstanceMethods
86
- str
87
- end
91
+ def reverse(str)
92
+ str = [style(ATTRIBUTES[:reverse]), str, default_style].join
93
+ str.extend Console::InstanceMethods
94
+ str
95
+ end
88
96
 
89
- def self.reverse(str)
90
- str = [style(ATTRIBUTES[:reverse]), str, default_style].join
91
- str.extend Console::InstanceMethods
92
- str
93
- end
97
+ def color(col, str)
98
+ str = [style(COLOURS[col]), str, default_style].join
99
+ str.extend Console::InstanceMethods
100
+ str
101
+ end
94
102
 
95
- def self.color(col, str)
96
- str = [style(COLOURS[col]), str, default_style].join
97
- str.extend Console::InstanceMethods
98
- str
99
- end
103
+ def att(name, str)
104
+ str = [style(ATTRIBUTES[name]), str, default_style].join
105
+ str.extend Console::InstanceMethods
106
+ str
107
+ end
100
108
 
101
- def self.att(name, str)
102
- str = [style(ATTRIBUTES[name]), str, default_style].join
103
- str.extend Console::InstanceMethods
104
- str
105
- end
109
+ def bgcolor(col, str)
110
+ str = [style(ATTRIBUTES[col]), str, default_style].join
111
+ str.extend Console::InstanceMethods
112
+ str
113
+ end
106
114
 
107
- def self.bgcolor(col, str)
108
- str = [style(ATTRIBUTES[col]), str, default_style].join
109
- str.extend Console::InstanceMethods
110
- str
111
- end
115
+ def style(*att)
116
+ # => \e[8;34;42m
117
+ "\e[%sm" % att.join(';')
118
+ end
112
119
 
113
- def self.style(*att)
114
- # => \e[8;34;42m
115
- "\e[%sm" % att.join(';')
116
- end
120
+ def default_style
121
+ style(ATTRIBUTES[:default], COLOURS[:default], BGCOLOURS[:default])
122
+ end
117
123
 
118
- def self.default_style
119
- style(ATTRIBUTES[:default], ATTRIBUTES[:COLOURS], ATTRIBUTES[:BGCOLOURS])
124
+ # Converts an absolute file path to a path relative to the current working
125
+ # directory. This simplifies logging and error reporting by showing
126
+ # only the relevant parts of file paths instead of lengthy absolute paths.
127
+ #
128
+ def pretty_path(file)
129
+ return nil if file.nil?
130
+
131
+ file = File.expand_path(file) # be absolutely sure
132
+ basepath = Dir.pwd
133
+ Pathname.new(file).relative_path_from(basepath).to_s
134
+ end
120
135
  end
121
136
  end
122
137
  end