tryouts 2.4.1 → 3.0.0.pre

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,340 @@
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, true)
14
+ @current_indent = 0
15
+ end
16
+
17
+ # Phase-level output
18
+ def phase_header(message, _file_count = nil, level = 0)
19
+ separators = [
20
+ { char: '=', width: @line_width }, # Major phases
21
+ { char: '-', width: @line_width - 10 }, # Sub-phases
22
+ { char: '.', width: @line_width - 20 }, # Details
23
+ { char: '~', width: @line_width - 30 }, # Minor items
24
+ ]
25
+
26
+ config = separators[level] || separators.last
27
+
28
+ separator_line = config[:char] * config[:width]
29
+ header_line = message.center(config[:width])
30
+
31
+ output = case level
32
+ when 0, 1
33
+ ['', separator_line, header_line, separator_line]
34
+ else
35
+ ['', header_line, separator_line]
36
+ end
37
+
38
+ with_indent(level) do
39
+ puts output.join("\n")
40
+ end
41
+ puts
42
+ end
43
+
44
+ # File-level operations
45
+ def file_start(file_path, _context_info = {})
46
+ # framework = context_info[:framework] || :direct
47
+ # context = context_info[:context] || :fresh
48
+
49
+ # with_indent(1) do
50
+ # puts "Framework: #{framework}"
51
+ # puts "Context: #{context}"
52
+ # end
53
+
54
+ puts file_header_visual(file_path)
55
+ end
56
+
57
+ def file_parsed(file_path, test_count, setup_present: false, teardown_present: false)
58
+ pretty_path = Console.pretty_path(file_path)
59
+ message = "Parsed #{test_count} test cases from #{pretty_path}"
60
+
61
+ extras = []
62
+ extras << 'setup' if setup_present
63
+ extras << 'teardown' if teardown_present
64
+ message += " (#{extras.join(', ')})" unless extras.empty?
65
+
66
+ puts indent_text(message, 2)
67
+ end
68
+
69
+ def file_execution_start(_file_path, test_count, context_mode)
70
+ message = "Running #{test_count} tests with #{context_mode} context"
71
+ puts indent_text(message, 1)
72
+ end
73
+
74
+ def file_result(_file_path, total_tests, failed_count, error_count, elapsed_time)
75
+ issues_count = failed_count + error_count
76
+ details = []
77
+
78
+ status = if issues_count > 0
79
+ details << "#{failed_count} failed" if failed_count > 0
80
+ details << "#{error_count} errors" if error_count > 0
81
+ details_str = details.join(', ')
82
+ Console.color(:red, "✗ #{issues_count}/#{total_tests} tests had issues (#{details_str})")
83
+ else
84
+ Console.color(:green, "✓ #{total_tests} tests passed")
85
+ end
86
+
87
+ puts indent_text(status, 2)
88
+
89
+ if elapsed_time
90
+ time_msg = "Completed in #{elapsed_time.round(3)}s"
91
+ puts indent_text(Console.color(:dim, time_msg), 2)
92
+ end
93
+ end
94
+
95
+ # Test-level operations
96
+ def test_start(test_case, index, total)
97
+ desc = test_case.description.to_s
98
+ desc = 'Unnamed test' if desc.empty?
99
+ message = "Test #{index}/#{total}: #{desc}"
100
+ puts indent_text(Console.color(:dim, message), 2)
101
+ end
102
+
103
+ def test_result(test_case, result_status, actual_results = [], _elapsed_time = nil)
104
+ should_show = @show_passed || result_status != :passed
105
+
106
+ return unless should_show
107
+
108
+ status_line = case result_status
109
+ when :passed
110
+ Console.color(:green, 'PASSED')
111
+ when :failed
112
+ Console.color(:red, 'FAILED')
113
+ when :error
114
+ Console.color(:red, 'ERROR')
115
+ when :skipped
116
+ Console.color(:yellow, 'SKIPPED')
117
+ else
118
+ 'UNKNOWN'
119
+ end
120
+
121
+ location = "#{Console.pretty_path(test_case.path)}:#{test_case.line_range.first + 1}"
122
+ puts indent_text("#{status_line} #{test_case.description} @ #{location}", 2)
123
+
124
+ # Show source code for verbose mode
125
+ show_test_source_code(test_case)
126
+
127
+ # Show failure details for failed tests
128
+ if [:failed, :error].include?(result_status)
129
+ show_failure_details(test_case, actual_results)
130
+ end
131
+ end
132
+
133
+ def test_output(test_case, output_text)
134
+ return if output_text.nil? || output_text.strip.empty?
135
+
136
+ puts indent_text('Test Output:', 3)
137
+ puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 3)
138
+
139
+ output_text.lines.each do |line|
140
+ puts indent_text(line.chomp, 4)
141
+ end
142
+
143
+ puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 3)
144
+ puts
145
+ end
146
+
147
+ # Setup/teardown operations
148
+ def setup_start(line_range)
149
+ message = "Executing global setup (lines #{line_range.first}..#{line_range.last})"
150
+ puts indent_text(Console.color(:cyan, message), 2)
151
+ end
152
+
153
+ def setup_output(output_text)
154
+ return if output_text.strip.empty?
155
+
156
+ output_text.lines.each do |line|
157
+ puts indent_text(line.chomp, 0)
158
+ end
159
+ end
160
+
161
+ def teardown_start(line_range)
162
+ message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
163
+ puts indent_text(Console.color(:cyan, message), 2)
164
+ end
165
+
166
+ def teardown_output(output_text)
167
+ return if output_text.strip.empty?
168
+
169
+ output_text.lines.each do |line|
170
+ puts indent_text(line.chomp, 0)
171
+ end
172
+ end
173
+
174
+ # Summary operations
175
+ def batch_summary(total_tests, failed_count, elapsed_time)
176
+ if failed_count > 0
177
+ passed = total_tests - failed_count
178
+ message = "#{failed_count} failed, #{passed} passed"
179
+ color = :red
180
+ else
181
+ message = "#{total_tests} tests passed"
182
+ color = :green
183
+ end
184
+
185
+ time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
186
+ summary = Console.color(color, "#{message}#{time_str}")
187
+ puts summary
188
+ end
189
+
190
+ def grand_total(total_tests, failed_count, error_count, successful_files, total_files, elapsed_time)
191
+ puts
192
+ puts '=' * @line_width
193
+ puts 'Grand Total:'
194
+
195
+ issues_count = failed_count + error_count
196
+ if issues_count > 0
197
+ passed = total_tests - issues_count
198
+ details = []
199
+ details << "#{failed_count} failed" if failed_count > 0
200
+ details << "#{error_count} errors" if error_count > 0
201
+ puts "#{details.join(', ')}, #{passed} passed (#{elapsed_time.round(2)}s)"
202
+ else
203
+ puts "#{total_tests} tests passed (#{elapsed_time.round(2)}s)"
204
+ end
205
+
206
+ puts "Files processed: #{successful_files} of #{total_files} successful"
207
+ puts '=' * @line_width
208
+ end
209
+
210
+ # Debug and diagnostic output
211
+ def debug_info(message, level = 0)
212
+ return unless @show_debug
213
+
214
+ prefix = Console.color(:cyan, 'INFO ')
215
+ puts indent_text("#{prefix} #{message}", level + 1)
216
+ end
217
+
218
+ def trace_info(message, level = 0)
219
+ return unless @show_trace
220
+
221
+ prefix = Console.color(:dim, 'TRACE')
222
+ puts indent_text("#{prefix} #{message}", level + 1)
223
+ end
224
+
225
+ def error_message(message, backtrace = nil)
226
+ error_msg = Console.color(:red, "ERROR: #{message}")
227
+ puts indent_text(error_msg, 1)
228
+
229
+ return unless backtrace && @show_debug
230
+
231
+ puts indent_text('Details:', 2)
232
+ # Show first 10 lines of backtrace to avoid overwhelming output
233
+ backtrace.first(10).each do |line|
234
+ puts indent_text(line, 3)
235
+ end
236
+ puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
237
+ end
238
+
239
+ # Utility methods
240
+ def raw_output(text)
241
+ puts text
242
+ end
243
+
244
+ def separator(style = :light)
245
+ case style
246
+ when :heavy
247
+ puts '=' * @line_width
248
+ when :light
249
+ puts '-' * @line_width
250
+ when :dotted
251
+ puts '.' * @line_width
252
+ else # rubocop:disable Lint/DuplicateBranch
253
+ puts '-' * @line_width
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def show_test_source_code(test_case)
260
+ puts indent_text('Source code:', 3)
261
+
262
+ # Use pre-captured source lines from parsing
263
+ start_line = test_case.line_range.first
264
+
265
+ test_case.source_lines.each_with_index do |line_content, index|
266
+ line_num = start_line + index
267
+ line_display = format('%3d: %s', line_num + 1, line_content)
268
+
269
+ # Highlight expectation lines by checking if this line
270
+ # contains the expectation syntax
271
+ if line_content.match?(/^\s*#\s*=>\s*/)
272
+ line_display = Console.color(:yellow, line_display)
273
+ end
274
+
275
+ puts indent_text(line_display, 4)
276
+ end
277
+ puts
278
+ end
279
+
280
+ def show_failure_details(test_case, actual_results)
281
+ return if actual_results.empty?
282
+
283
+ puts indent_text('Expected vs Actual:', 3)
284
+
285
+ actual_results.each_with_index do |actual, idx|
286
+ expected_line = test_case.expectations[idx] if test_case.expectations
287
+
288
+ if expected_line
289
+ puts indent_text("Expected: #{Console.color(:green, expected_line)}", 4)
290
+ puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
291
+ else
292
+ puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
293
+ end
294
+
295
+ # Show difference if both are strings
296
+ if expected_line && actual.is_a?(String) && expected_line.is_a?(String)
297
+ show_string_diff(expected_line, actual)
298
+ end
299
+
300
+ puts
301
+ end
302
+ end
303
+
304
+ def show_string_diff(expected, actual)
305
+ return if expected == actual
306
+
307
+ puts indent_text('Difference:', 4)
308
+ puts indent_text("- #{Console.color(:red, actual)}", 5)
309
+ puts indent_text("+ #{Console.color(:green, expected)}", 5)
310
+ end
311
+
312
+ def file_header_visual(file_path)
313
+ pretty_path = Console.pretty_path(file_path)
314
+ header_content = ">>>>> #{pretty_path} "
315
+ padding_length = [@line_width - header_content.length, 0].max
316
+ padding = '<' * padding_length
317
+
318
+ [
319
+ '-' * @line_width,
320
+ header_content + padding,
321
+ '-' * @line_width,
322
+ ].join("\n")
323
+ end
324
+ end
325
+
326
+ # Verbose formatter that only shows failures and errors
327
+ class VerboseFailsFormatter < VerboseFormatter
328
+ def initialize(options = {})
329
+ super(options.merge(show_passed: false))
330
+ end
331
+
332
+ def test_result(test_case, result_status, actual_results = [], elapsed_time = nil)
333
+ # Only show failed/error tests, but with full source code
334
+ return if result_status == :passed
335
+
336
+ super
337
+ end
338
+ end
339
+ end
340
+ 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 application's
125
+ # base 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 = File.expand_path('..', TRYOUTS_LIB_HOME)
133
+ Pathname.new(file).relative_path_from(basepath).to_s
134
+ end
120
135
  end
121
136
  end
122
137
  end