tryouts 2.3.0 → 2.3.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 964b15141b2d2a8658bb5beee61ef9618ad478b8c29a4c91c5595dfb7ac92323
4
- data.tar.gz: b33744a232df6dd0020deef5ea682217481ba6fc17d6d1336ccb9dc314080052
3
+ metadata.gz: 8c9881b556aa0492a4e0cba92343efa2c9761664a9507ac3d123fee8143301b8
4
+ data.tar.gz: 245f405149281445c9bb902695919f6fbd4c5ec461303f16c7078ed6ddcbe111
5
5
  SHA512:
6
- metadata.gz: 4116e1c5918e226ec2232a55cd4dfdc8ff034948ceafa03ca3cdaecee6df4aa2f7d1dc5598eed66074f0c2e22c71492b7fab83c83b8869165897be69cabf4e9c
7
- data.tar.gz: 826761b6d51762fb214380d05d438ab70b029bab4c7a138b59909ac096bcdc5bc79d7aed6c6f2509d728b6ea59f8ea03201fb42e6e77002fef1cd47a7635ec7d
6
+ metadata.gz: ced6968dc159ab82ffcc8902b9835977bfdac9fbf1f8290ce20c216a09fb07fe7731235ac464827094cdebb422048ae963cf396fcb604c37d8945d73d148a619
7
+ data.tar.gz: b57ed46b023c084209153176fcbb05c4ec710cf1da0a77e61db1ac14e50b87f4bd1c5d35c73e2b9a950d855415e4728d5f229138bb6d8d51edab1bb3df8961f3
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Tryouts v2.3.0 (2024-04-04)
1
+ # Tryouts v2.3.1 (2024-06-18)
2
2
 
3
3
  **Ruby tests that read like documentation.**
4
4
 
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :MAJOR: 2
3
+ :MINOR: 3
4
+ :PATCH: 1
data/exe/try CHANGED
@@ -3,35 +3,49 @@
3
3
  require 'optparse'
4
4
  require_relative '../lib/tryouts'
5
5
 
6
- # Add local lib directories to the load path
7
- Dir.glob(File.join(Dir.pwd, '{lib,..,lib}')).each { |dir| $LOAD_PATH.unshift(dir) }
6
+ # Adding local lib directories allows the test files
7
+ # to require the ruby code they are testing without
8
+ # needing even if the files use regular requires. It
9
+ # skips the test files from having to muck with the
10
+ # load path or use `require_relative` which is easy
11
+ # to forget. This is a convenience feature.
12
+ #
13
+ # Here we're looking for directories relative to the
14
+ # current working directory (i.e. where this script
15
+ # is being run from.
16
+ lib_glob = File.join(Dir.pwd, '{lib,../lib,.}')
17
+ Tryouts.update_load_path(lib_glob)
8
18
 
9
19
  # Parse command-line arguments
10
- options = { quiet: false, noisy: false, fails: false }
11
20
  OptionParser.new do |opts|
12
- opts.on('-V', '--version', 'Display the version') { puts "Tryouts: #{Tryouts::VERSION}"; exit }
13
- opts.on('-q', '--quiet', 'Run in quiet mode') { options[:quiet] = true }
14
- opts.on('-v', '--verbose', 'Run in verbose mode') { options[:noisy] = true }
15
- opts.on('-f', '--fails', 'Show only failing tryouts') { options[:fails] = true }
21
+ opts.on('-V', '--version', 'Display the version') do
22
+ puts "Tryouts: #{Tryouts::VERSION}"
23
+ exit
24
+ end
25
+ opts.on('-q', '--quiet', 'Run in quiet mode') { Tryouts.quiet = true }
26
+ opts.on('-v', '--verbose', 'Run in verbose mode') { Tryouts.noisy = true }
27
+ opts.on('-f', '--fails', 'Show only failing tryouts') { Tryouts.fails = true }
16
28
  opts.on('-D', '--debug', 'Run in debug mode') { Tryouts.debug = true }
17
- opts.on('-h', '--help', 'Display this help') { puts opts; exit }
29
+ opts.on('-h', '--help', 'Display this help') do
30
+ puts opts
31
+ exit
32
+ end
18
33
  end.parse!
19
34
 
20
- # Set Tryouts options
21
- Tryouts.quiet = options[:quiet]
22
- Tryouts.noisy = options[:noisy]
23
- Tryouts.fails = options[:fails]
35
+ # Find tryouts path with a path glob unless thin
36
+ # script was called with arguments in which case
37
+ # we consume those as a list of paths.
38
+ paths = if ARGV.empty?
39
+ Dir.glob(
40
+ ['./{try,tryouts/,.}/*_{try,tryouts}.rb'],
41
+ base: Dir.pwd
42
+ ).sort # deterministic order
24
43
 
25
- # Find tryouts path
26
- if ARGV.empty?
27
- paths = Dir.glob(
28
- ['./{try,tryouts/,.}/*_{try,tryouts}.rb'],
29
- base: Dir.pwd,
30
- sort: true # deterministic order
31
- )
32
-
33
- else
34
- paths = ARGV
35
- end
44
+ else
45
+ ARGV
46
+ end
36
47
 
48
+ # Running the tryouts returns the number of
49
+ # test failures so here we pass that value
50
+ # through as the exit code for the script.
37
51
  exit Tryouts.run_all(*paths)
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tryouts
4
+ module Console
5
+ # ANSI escape sequence numbers for text attributes
6
+ unless defined? ATTRIBUTES
7
+ ATTRIBUTES = {
8
+ normal: 0,
9
+ bright: 1,
10
+ dim: 2,
11
+ underline: 4,
12
+ blink: 5,
13
+ reverse: 7,
14
+ hidden: 8,
15
+ default: 0
16
+ }.freeze
17
+ end
18
+
19
+ # ANSI escape sequence numbers for text colours
20
+ unless defined? COLOURS
21
+ COLOURS = {
22
+ black: 30,
23
+ red: 31,
24
+ green: 32,
25
+ yellow: 33,
26
+ blue: 34,
27
+ magenta: 35,
28
+ cyan: 36,
29
+ white: 37,
30
+ default: 39,
31
+ random: 30 + rand(10).to_i
32
+ }.freeze
33
+ end
34
+
35
+ # ANSI escape sequence numbers for background colours
36
+ unless defined? BGCOLOURS
37
+ BGCOLOURS = {
38
+ black: 40,
39
+ red: 41,
40
+ green: 42,
41
+ yellow: 43,
42
+ blue: 44,
43
+ magenta: 45,
44
+ cyan: 46,
45
+ white: 47,
46
+ default: 49,
47
+ random: 40 + rand(10).to_i
48
+ }.freeze
49
+ end
50
+
51
+ module InstanceMethods
52
+ def bright
53
+ Console.bright(self)
54
+ end
55
+
56
+ def underline
57
+ Console.underline(self)
58
+ end
59
+
60
+ def reverse
61
+ Console.reverse(self)
62
+ end
63
+
64
+ def color(col)
65
+ Console.color(col, self)
66
+ end
67
+
68
+ def att(col)
69
+ Console.att(col, self)
70
+ end
71
+
72
+ def bgcolor(col)
73
+ Console.bgcolor(col, self)
74
+ end
75
+ end
76
+
77
+ def self.bright(str)
78
+ str = [style(ATTRIBUTES[:bright]), str, default_style].join
79
+ str.extend Console::InstanceMethods
80
+ str
81
+ end
82
+
83
+ def self.underline(str)
84
+ str = [style(ATTRIBUTES[:underline]), str, default_style].join
85
+ str.extend Console::InstanceMethods
86
+ str
87
+ end
88
+
89
+ def self.reverse(str)
90
+ str = [style(ATTRIBUTES[:reverse]), str, default_style].join
91
+ str.extend Console::InstanceMethods
92
+ str
93
+ end
94
+
95
+ def self.color(col, str)
96
+ str = [style(COLOURS[col]), str, default_style].join
97
+ str.extend Console::InstanceMethods
98
+ str
99
+ end
100
+
101
+ def self.att(name, str)
102
+ str = [style(ATTRIBUTES[name]), str, default_style].join
103
+ str.extend Console::InstanceMethods
104
+ str
105
+ end
106
+
107
+ def self.bgcolor(col, str)
108
+ str = [style(ATTRIBUTES[col]), str, default_style].join
109
+ str.extend Console::InstanceMethods
110
+ str
111
+ end
112
+
113
+ def self.style(*att)
114
+ # => \e[8;34;42m
115
+ "\e[%sm" % att.join(';')
116
+ end
117
+
118
+ def self.default_style
119
+ style(ATTRIBUTES[:default], ATTRIBUTES[:COLOURS], ATTRIBUTES[:BGCOLOURS])
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ class Tryouts
4
+ class Section < Array
5
+ attr_accessor :path, :first, :last
6
+
7
+ def initialize(path, start = 0)
8
+ @path = path
9
+ @first = start
10
+ @last = start
11
+ end
12
+
13
+ def range
14
+ @first..@last
15
+ end
16
+
17
+ def inspect
18
+ range.to_a.zip(self).collect do |line|
19
+ "%-4d %s\n" % line
20
+ end.join
21
+ end
22
+
23
+ def to_s
24
+ join($/)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ class Tryouts
4
+ class TestBatch < Array
5
+ class Container
6
+ def metaclass
7
+ class << self; end
8
+ end
9
+ end
10
+ attr_reader :path, :failed, :lines
11
+
12
+ def initialize(p, l)
13
+ @path = p
14
+ @lines = l
15
+ @container = Container.new.metaclass
16
+ @run = false
17
+ end
18
+
19
+ def run(before_test, &after_test)
20
+ return if empty?
21
+
22
+ setup
23
+ ret = self.select do |tc|
24
+ before_test.call(tc) unless before_test.nil?
25
+ ret = !tc.run
26
+ after_test.call(tc)
27
+ ret # select failed tests
28
+ end
29
+ @failed = ret.size
30
+ @run = true
31
+ clean
32
+ !failed?
33
+ end
34
+
35
+ def failed?
36
+ !@failed.nil? && @failed > 0
37
+ end
38
+
39
+ def setup
40
+ return if empty?
41
+
42
+ start = first.desc.nil? ? first.test.first : first.desc.first - 1
43
+ Tryouts.eval lines[0..start - 1].join, path, 0 if start > 0
44
+ end
45
+
46
+ def clean
47
+ return if empty?
48
+
49
+ last_line = last.exps.last + 1
50
+ return unless last_line < lines.size
51
+
52
+ Tryouts.eval lines[last_line..-1].join, path, last_line
53
+ end
54
+
55
+ def run?
56
+ @run
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tryouts
4
+ class TestCase
5
+ attr_reader :desc, :test, :exps, :path, :outlines, :test_result
6
+
7
+ def initialize(d, t, e)
8
+ @desc, @test, @exps, @path = d, t, e
9
+ @outlines = []
10
+ end
11
+
12
+ def inspect
13
+ [@desc.inspect, @test.inspect, @exps.inspect].join
14
+ end
15
+
16
+ def to_s
17
+ [@desc.to_s, @test.to_s, @exps.to_s].join
18
+ end
19
+
20
+ def run
21
+ Tryouts.debug format('%s:%d', @test.path, @test.first)
22
+ Tryouts.debug inspect, $/
23
+ $stdout = StringIO.new
24
+ expectations = exps.collect do |exp, _idx|
25
+ exp =~ /\A\#?\s*=>\s*(.+)\Z/
26
+ ::Regexp.last_match(1) # this will be nil if the expectation is commented out
27
+ end
28
+
29
+ # Evaluate test block only if there are valid expectations
30
+ unless expectations.compact.empty? # TODO: fast-fail if no expectations
31
+ test_value = Tryouts.eval @test.to_s, @test.path, @test.first
32
+ @has_run = true
33
+ end
34
+ $stdout = STDOUT # restore stdout
35
+
36
+
37
+ expectations.each_with_index do |exp, idx|
38
+ if exp.nil?
39
+ @outlines << ' [skipped]'
40
+ @test_result = 0
41
+ else
42
+ # Evaluate expectation
43
+
44
+ exp_value = Tryouts.eval(exp, @exps.path, @exps.first + idx)
45
+
46
+ test_passed = test_value.eql?(exp_value)
47
+ @test_result = test_passed ? 1 : -1
48
+ @outlines << test_value.inspect
49
+ end
50
+ end
51
+ Tryouts.debug # extra newline
52
+ failed?
53
+
54
+ rescue Exception => e
55
+ Tryouts.debug e.message, e.backtrace.join($/), $/
56
+ $stdout = STDOUT # restore stdout
57
+ end
58
+
59
+ def run?
60
+ @has_run.eql?(true)
61
+ end
62
+
63
+ def skipped?
64
+ @test_result == 0
65
+ end
66
+
67
+ def passed?
68
+ @test_result == 1
69
+ end
70
+ def failed?
71
+ @test_result == -1
72
+ end
73
+
74
+ def color
75
+ case @test_result
76
+ when 1
77
+ :green
78
+ when 0
79
+ :white
80
+ else
81
+ :red
82
+ end
83
+ end
84
+
85
+ def adjective
86
+ case @test_result
87
+ when 1
88
+ 'PASSED'
89
+ when 0
90
+ 'SKIPPED'
91
+ else
92
+ 'FAILED'
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Tryouts
2
4
  module VERSION
3
5
  def self.to_s
4
- load_config
5
- [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.')
6
+ version_file
7
+ [@version_file[:MAJOR], @version_file[:MINOR], @version_file[:PATCH]].join('.')
6
8
  end
7
9
  alias_method :inspect, :to_s
8
- def self.load_config
10
+ def self.version_file
9
11
  require 'yaml'
10
- @version ||= YAML.load_file(File.join(TRYOUTS_LIB_HOME, '..', 'VERSION.yml'))
12
+ @version_file ||= YAML.load_file(File.join(TRYOUTS_LIB_HOME, '..', 'VERSION.yml'))
11
13
  end
12
14
  end
13
15
  end
data/lib/tryouts.rb CHANGED
@@ -1,9 +1,13 @@
1
- require 'ostruct'
1
+ # frozen_string_literal: true
2
2
 
3
- unless defined?(TRYOUTS_LIB_HOME)
4
- TRYOUTS_LIB_HOME = File.expand_path File.dirname(__FILE__)
5
- end
3
+ require 'stringio'
4
+
5
+ TRYOUTS_LIB_HOME = __dir__ unless defined?(TRYOUTS_LIB_HOME)
6
6
 
7
+ require_relative 'tryouts/console'
8
+ require_relative 'tryouts/section'
9
+ require_relative 'tryouts/testbatch'
10
+ require_relative 'tryouts/testcase'
7
11
  require_relative 'tryouts/version'
8
12
 
9
13
  class Tryouts
@@ -14,9 +18,12 @@ class Tryouts
14
18
  @container = Class.new
15
19
  @cases = []
16
20
  @sysinfo = nil
17
- class << self
18
- attr_accessor :debug, :container, :quiet, :noisy, :fails
19
- attr_reader :cases
21
+ @testcase_io = StringIO.new
22
+
23
+ module ClassMethods
24
+ attr_accessor :container, :quiet, :noisy, :fails
25
+ attr_writer :debug
26
+ attr_reader :cases, :testcase_io
20
27
 
21
28
  def sysinfo
22
29
  require 'sysinfo'
@@ -24,17 +31,26 @@ class Tryouts
24
31
  @sysinfo
25
32
  end
26
33
 
27
- def debug?() @debug == true end
34
+ def debug?
35
+ @debug == true
36
+ end
37
+
38
+ def update_load_path(lib_glob)
39
+ Dir.glob(lib_glob).each { |dir| $LOAD_PATH.unshift(dir) }
40
+ end
28
41
 
29
42
  def run_all *paths
30
43
  batches = paths.collect do |path|
31
44
  parse path
32
45
  end
33
46
 
34
- all, skipped_tests, failed_tests = 0, 0, 0
35
- skipped_batches, failed_batches = 0, 0
47
+ all = 0
48
+ skipped_tests = 0
49
+ failed_tests = 0
50
+ skipped_batches = 0
51
+ failed_batches = 0
36
52
 
37
- msg 'Ruby %s @ %-40s' % [RUBY_VERSION, Time.now], $/
53
+ msg format('Ruby %s @ %-60s', RUBY_VERSION, Time.now), $/
38
54
 
39
55
  if Tryouts.debug?
40
56
  Tryouts.debug "Found #{paths.size} files:"
@@ -43,415 +59,213 @@ class Tryouts
43
59
  end
44
60
 
45
61
  batches.each do |batch|
46
-
47
- path = batch.path.gsub(/#{Dir.pwd}\/?/, '')
48
-
49
- vmsg '%-60s %s' % [path, '']
50
-
51
- before_handler = Proc.new do |t|
52
- if Tryouts.noisy && !Tryouts.fails
53
- vmsg Console.reverse(' %-58s ' % [t.desc.to_s])
54
- vmsg t.test.inspect, t.exps.inspect
62
+ path = batch.path.gsub(%r{#{Dir.pwd}/?}, '')
63
+ divider = '-' * 70
64
+ path_pretty = format('>>>>> %-20s %s', path, '').ljust(70, '<')
65
+
66
+ msg $/
67
+ vmsg Console.reverse(divider)
68
+ vmsg Console.reverse(path_pretty)
69
+ vmsg Console.reverse(divider)
70
+ vmsg $/
71
+
72
+ before_handler = proc do |tc|
73
+ if Tryouts.noisy
74
+ tc_title = tc.desc.to_s
75
+ vmsg Console.underline(format('%-58s ', tc_title))
76
+ vmsg tc.test.inspect, tc.exps.inspect
55
77
  end
56
78
  end
57
79
 
58
- batch.run(before_handler) do |t|
59
- if t.failed?
60
- failed_tests += 1
61
- if Tryouts.noisy && Tryouts.fails
62
- vmsg Console.color(:red, t.failed.join($/)), $/
63
- else
64
- msg ' %s (%s:%s)' % [Console.color(:red, "FAIL"), path, t.exps.first]
65
- end
66
- elsif (t.skipped? || !t.run?) && !Tryouts.fails
67
- skipped_tests += 1
68
- if Tryouts.noisy
69
- vmsg Console.bright(t.skipped.join($/)), $/
70
- else
71
- msg ' SKIP (%s:%s)' % [path, t.exps.first]
72
- end
73
- elsif !Tryouts.fails
74
- if Tryouts.noisy
75
- vmsg Console.color(:green, t.passed.join($/)), $/
76
- else
77
- msg ' %s' % [Console.color(:green, 'PASS')]
78
- end
79
- end
80
+ batch.run(before_handler) do |tc|
80
81
  all += 1
82
+ failed_tests += 1 if tc.failed?
83
+ skipped_tests += 1 if tc.skipped?
84
+ codelines = tc.outlines.join($/)
85
+ first_exp_line = tc.exps.first
86
+ result_adjective = tc.failed? ? 'FAILED' : 'PASSED'
87
+
88
+ first_exp_line = tc.exps.first
89
+ location = format('%s:%d', tc.exps.path, first_exp_line)
90
+
91
+ expectation = Console.color(tc.color, codelines)
92
+ summary = Console.color(tc.color, "%s @ %s" % [tc.adjective, location])
93
+ vmsg ' %s' % expectation
94
+ if tc.failed?
95
+ msg Console.reverse(summary)
96
+ else
97
+ msg summary
98
+ end
99
+ vmsg
100
+
101
+ # Output buffered testcase_io to stdout
102
+ # and reset it for the next test case.
103
+ unless Tryouts.fails && !tc.failed?
104
+ $stdout.puts testcase_io.string unless Tryouts.quiet
105
+ end
106
+
107
+ # Reset the testcase IO buffer
108
+ testcase_io.truncate(0)
81
109
  end
82
110
  end
83
111
 
84
- msg
85
- if all > 0
86
- suffix = 'tests passed'
87
- suffix << " (and #{skipped_tests} skipped)" if skipped_tests > 0
88
- msg cformat(all-failed_tests-skipped_tests, all-skipped_tests, suffix) if all-skipped_tests > 0
89
- end
90
- if batches.size > 1
91
- if batches.size-skipped_batches > 0
92
- suffix = "batches passed"
93
- suffix << " (and #{skipped_batches} skipped)" if skipped_batches > 0
94
- msg cformat(batches.size-skipped_batches-failed_batches, batches.size-skipped_batches, suffix)
112
+ # Create a line of separation before the result summary
113
+ msg $INPUT_RECORD_SEPARATOR # newline
114
+
115
+ if all
116
+ suffix = "tests passed (#{skipped_tests} skipped)" if skipped_tests > 0
117
+ actual_test_size = all - skipped_tests
118
+ if actual_test_size > 0
119
+ msg cformat(all - failed_tests - skipped_tests, all - skipped_tests, suffix)
95
120
  end
96
121
  end
97
122
 
98
- failed_tests # 0 means success
123
+ actual_batch_size = (batches.size - skipped_batches)
124
+ if batches.size > 1 && actual_batch_size > 0
125
+ suffix = 'batches passed'
126
+ suffix << " (#{skipped_batches} skipped)" if skipped_batches > 0
127
+ msg cformat(batches.size - skipped_batches - failed_batches, batches.size - skipped_batches, suffix)
128
+ end
129
+
130
+ # Print out the buffered result summary
131
+ $stdout.puts testcase_io.string
132
+
133
+ failed_tests # returns the number of failed tests (0 if all passed)
99
134
  end
100
135
 
101
136
  def cformat(*args)
102
137
  Console.bright '%d of %d %s' % args
103
138
  end
104
139
 
105
- def run path
140
+ def run(path)
106
141
  batch = parse path
107
142
  batch.run
108
143
  batch
109
144
  end
110
145
 
111
- def parse path
112
- #debug "Loading #{path}"
146
+ def parse(path)
147
+ # debug "Loading #{path}"
113
148
  lines = File.readlines path
114
149
  skip_ahead = 0
115
150
  batch = TestBatch.new path, lines
116
151
  lines.size.times do |idx|
117
152
  skip_ahead -= 1 and next if skip_ahead > 0
153
+
118
154
  line = lines[idx].chomp
119
- #debug('%-4d %s' % [idx, line])
120
- if expectation? line
121
- offset = 0
122
- exps = Section.new(path, idx+1)
123
- exps << line.chomp
124
- while (idx+offset < lines.size)
125
- offset += 1
126
- this_line = lines[idx+offset]
127
- break if ignore?(this_line)
128
- if expectation?(this_line)
129
- exps << this_line.chomp
130
- skip_ahead += 1
131
- end
132
- exps.last += 1
155
+ # debug('%-4d %s' % [idx, line])
156
+ next unless expectation? line
157
+
158
+ offset = 0
159
+ exps = Section.new(path, idx + 1)
160
+ exps << line.chomp
161
+ while idx + offset < lines.size
162
+ offset += 1
163
+ this_line = lines[idx + offset]
164
+ break if ignore?(this_line)
165
+
166
+ if expectation?(this_line)
167
+ exps << this_line.chomp
168
+ skip_ahead += 1
133
169
  end
170
+ exps.last += 1
171
+ end
134
172
 
135
- offset = 0
136
- buffer, desc = Section.new(path), Section.new(path)
137
- test = Section.new(path, idx) # test start the line before the exp.
138
- blank_buffer = Section.new(path)
139
- while (idx-offset >= 0)
140
- offset += 1
141
- this_line = lines[idx-offset].chomp
142
- buffer.unshift this_line if ignore?(this_line)
143
- if comment?(this_line)
144
- buffer.unshift this_line
145
- end
146
- if test?(this_line)
147
- test.unshift(*buffer) && buffer.clear
148
- test.unshift this_line
149
- end
150
- if test_begin?(this_line)
151
- while test_begin?(lines[idx-(offset+1)].chomp)
152
- offset += 1
153
- buffer.unshift lines[idx-offset].chomp
154
- end
155
- end
156
- if test_begin?(this_line) || idx-offset == 0 || expectation?(this_line)
157
- adjust = expectation?(this_line) ? 2 : 1
158
- test.first = idx-offset+buffer.size+adjust
159
- desc.unshift *buffer
160
- desc.last = test.first-1
161
- desc.first = desc.last-desc.size+1
162
- # remove empty lines between the description
163
- # and the previous expectation
164
- while !desc.empty? && desc[0].empty?
165
- desc.shift
166
- desc.first += 1
167
- end
168
- break
173
+ offset = 0
174
+ buffer = Section.new(path)
175
+ desc = Section.new(path)
176
+ test = Section.new(path, idx) # test start the line before the exp.
177
+ while idx - offset >= 0
178
+ offset += 1
179
+ this_line = lines[idx - offset].chomp
180
+ buffer.unshift this_line if ignore?(this_line)
181
+ buffer.unshift this_line if comment?(this_line)
182
+ if test?(this_line)
183
+ test.unshift(*buffer) && buffer.clear
184
+ test.unshift this_line
185
+ end
186
+ if test_begin?(this_line)
187
+ while test_begin?(lines[idx - (offset + 1)].chomp)
188
+ offset += 1
189
+ buffer.unshift lines[idx - offset].chomp
169
190
  end
170
191
  end
171
-
172
- batch << TestCase.new(desc, test, exps)
192
+ next unless test_begin?(this_line) || idx - offset == 0 || expectation?(this_line)
193
+
194
+ adjust = expectation?(this_line) ? 2 : 1
195
+ test.first = idx - offset + buffer.size + adjust
196
+ desc.unshift(*buffer)
197
+ desc.last = test.first - 1
198
+ desc.first = desc.last - desc.size + 1
199
+ # remove empty lines between the description
200
+ # and the previous expectation
201
+ while !desc.empty? && desc[0].empty?
202
+ desc.shift
203
+ desc.first += 1
204
+ end
205
+ break
173
206
  end
207
+
208
+ batch << TestCase.new(desc, test, exps)
174
209
  end
175
210
 
176
211
  batch
177
212
  end
178
-
179
- def print str
213
+ def print(str)
180
214
  return if Tryouts.quiet
181
- STDOUT.print str
182
- STDOUT.flush
215
+
216
+ $stdout.print str
217
+ $stdout.flush
183
218
  end
184
219
 
185
- def vmsg *msg
186
- STDOUT.puts *msg if !Tryouts.quiet && Tryouts.noisy
220
+ def vmsg *msgs
221
+ msg(*msgs) if Tryouts.noisy
187
222
  end
188
223
 
189
- def msg *msg
190
- STDOUT.puts *msg unless Tryouts.quiet
224
+ def msg *msgs
225
+ testcase_io.puts(*msgs) unless Tryouts.quiet
191
226
  end
192
227
 
193
- def err *msg
228
+ def err *msgs
194
229
  msg.each do |line|
195
- STDERR.puts Console.color :red, line
230
+ $stderr.puts Console.color :red, line
196
231
  end
197
232
  end
198
233
 
199
- def debug *msg
200
- STDERR.puts *msg if @debug
234
+ def debug *msgs
235
+ $stderr.puts(*msgs) if debug?
201
236
  end
202
237
 
203
238
  def eval(str, path, line)
204
- begin
205
- Kernel.eval str, @container.send(:binding), path, line
206
- rescue SyntaxError, LoadError => ex
207
- Tryouts.err Console.color(:red, ex.message),
208
- Console.color(:red, ex.backtrace.first)
209
- nil
210
- end
239
+ Kernel.eval str, @container.send(:binding), path, line
240
+ rescue SyntaxError, LoadError => e
241
+ Tryouts.err Console.color(:red, e.message),
242
+ Console.color(:red, e.backtrace.first)
243
+ nil
211
244
  end
212
245
 
213
246
  private
214
247
 
215
- def expectation? str
248
+ def expectation?(str)
216
249
  !ignore?(str) && str.strip.match(/\A\#+\s*=>/)
217
250
  end
218
251
 
219
- def comment? str
252
+ def comment?(str)
220
253
  !str.strip.match(/^\#+/).nil? && !expectation?(str)
221
254
  end
222
255
 
223
- def test? str
256
+ def test?(str)
224
257
  !ignore?(str) && !expectation?(str) && !comment?(str)
225
258
  end
226
259
 
227
- def ignore? str
260
+ def ignore?(str)
228
261
  str.to_s.strip.chomp.empty?
229
262
  end
230
263
 
231
- def test_begin? str
232
- ret = !str.strip.match(/\#+\s*TEST/i).nil? ||
233
- !str.strip.match(/\A\#\#+[\s\w]+/i).nil?
234
- ret
235
- end
236
-
237
-
238
- end
239
-
240
- class TestBatch < Array
241
- class Container
242
- def metaclass
243
- class << self; end
244
- end
245
- end
246
- attr_reader :path
247
- attr_reader :failed
248
- attr_reader :lines
249
- def initialize(p,l)
250
- @path, @lines = p, l
251
- @container = Container.new.metaclass
252
- @run = false
253
- end
254
- def run(before_test, &after_test)
255
- return if empty?
256
- setup
257
- ret = self.select { |tc|
258
- before_test.call(tc) unless before_test.nil?
259
- ret = !tc.run
260
- after_test.call(tc)
261
- ret # select failed tests
262
- }
263
- @failed = ret.size
264
- @run = true
265
- clean
266
- !failed?
267
- end
268
- def failed?
269
- !@failed.nil? && @failed > 0
270
- end
271
- def setup
272
- return if empty?
273
- start = first.desc.nil? ? first.test.first : first.desc.first-1
274
- Tryouts.eval lines[0..start-1].join, path, 0 if start > 0
275
- end
276
- def clean
277
- return if empty?
278
- last_line = last.exps.last+1
279
- if last_line < lines.size
280
- Tryouts.eval lines[last_line..-1].join, path, last_line
281
- end
282
- end
283
- def run?
284
- @run
285
- end
286
- end
287
- class TestCase
288
- attr_reader :desc, :test, :exps, :failed, :passed, :skipped
289
- def initialize(d,t,e)
290
- @desc, @test, @exps, @path = d,t,e
291
- end
292
- def inspect
293
- [@desc.inspect, @test.inspect, @exps.inspect].join
294
- end
295
- def to_s
296
- [@desc.to_s, @test.to_s, @exps.to_s].join
297
- end
298
- def run
299
- Tryouts.debug '%s:%d' % [@test.path, @test.first]
300
- Tryouts.debug inspect, $/
301
- expectations = exps.collect { |exp,idx|
302
- exp =~ /\A\#?\s*=>\s*(.+)\Z/
303
- $1 # this will be nil if the expectation is commented out
304
- }
305
-
306
- # Evaluate test block only if there are valid expectations
307
- unless expectations.compact.empty?
308
- test_value = Tryouts.eval @test.to_s, @test.path, @test.first
309
- @has_run = true
310
- end
311
-
312
- @passed, @failed, @skipped = [], [], []
313
- expectations.each_with_index { |exp,idx|
314
- if exp.nil?
315
- @skipped << ' [skipped]'
316
- else
317
- exp_value = Tryouts.eval(exp, @exps.path, @exps.first+idx)
318
- if test_value == exp_value
319
- @passed << ' == %s' % [test_value.inspect]
320
- else
321
- @failed << ' != %s' % [test_value.inspect]
322
- end
323
- end
324
- }
325
- Tryouts.debug
326
- @failed.empty?
327
- end
328
- def skipped?
329
- !@skipped.nil? && !@skipped.empty?
330
- end
331
- def run?
332
- @has_run == true
333
- end
334
- def failed?
335
- !@failed.nil? && !@failed.empty?
336
- end
337
- private
338
- def create_proc str, path, line
339
- eval("Proc.new {\n #{str}\n}", binding, path, line)
340
- end
341
- end
342
- class Section < Array
343
- attr_accessor :path, :first, :last
344
- def initialize path, start=0
345
- @path = path
346
- @first, @last = start, start
347
- end
348
- def range
349
- @first..@last
350
- end
351
- def inspect
352
- range.to_a.zip(self).collect do |line|
353
- "%-4d %s\n" % line
354
- end.join
355
- end
356
- def to_s
357
- self.join($/)
358
- end
359
- end
360
-
361
-
362
- module Console
363
-
364
- # ANSI escape sequence numbers for text attributes
365
- ATTRIBUTES = {
366
- :normal => 0,
367
- :bright => 1,
368
- :dim => 2,
369
- :underline => 4,
370
- :blink => 5,
371
- :reverse => 7,
372
- :hidden => 8,
373
- :default => 0,
374
- }.freeze unless defined? ATTRIBUTES
375
-
376
- # ANSI escape sequence numbers for text colours
377
- COLOURS = {
378
- :black => 30,
379
- :red => 31,
380
- :green => 32,
381
- :yellow => 33,
382
- :blue => 34,
383
- :magenta => 35,
384
- :cyan => 36,
385
- :white => 37,
386
- :default => 39,
387
- :random => 30 + rand(10).to_i
388
- }.freeze unless defined? COLOURS
389
-
390
- # ANSI escape sequence numbers for background colours
391
- BGCOLOURS = {
392
- :black => 40,
393
- :red => 41,
394
- :green => 42,
395
- :yellow => 43,
396
- :blue => 44,
397
- :magenta => 45,
398
- :cyan => 46,
399
- :white => 47,
400
- :default => 49,
401
- :random => 40 + rand(10).to_i
402
- }.freeze unless defined? BGCOLOURS
403
-
404
- module InstanceMethods
405
- def bright
406
- Console.bright(self)
407
- end
408
- def reverse
409
- Console.reverse(self)
410
- end
411
- def color(col)
412
- Console.color(col, self)
413
- end
414
- def att(col)
415
- Console.att(col, self)
416
- end
417
- def bgcolor(col)
418
- Console.bgcolor(col, self)
419
- end
420
- end
421
-
422
- def self.bright(str)
423
- str = [style(ATTRIBUTES[:bright]), str, default_style].join
424
- str.extend Console::InstanceMethods
425
- str
426
- end
427
- def self.reverse(str)
428
- str = [style(ATTRIBUTES[:reverse]), str, default_style].join
429
- str.extend Console::InstanceMethods
430
- str
431
- end
432
- def self.color(col, str)
433
- str = [style(COLOURS[col]), str, default_style].join
434
- str.extend Console::InstanceMethods
435
- str
436
- end
437
- def self.att(name, str)
438
- str = [style(ATTRIBUTES[name]), str, default_style].join
439
- str.extend Console::InstanceMethods
440
- str
441
- end
442
- def self.bgcolor(col, str)
443
- str = [style(ATTRIBUTES[col]), str, default_style].join
444
- str.extend Console::InstanceMethods
445
- str
446
- end
447
- private
448
- def self.style(*att)
449
- # => \e[8;34;42m
450
- "\e[%sm" % att.join(';')
451
- end
452
- def self.default_style
453
- style(ATTRIBUTES[:default], ATTRIBUTES[:COLOURS], ATTRIBUTES[:BGCOLOURS])
264
+ def test_begin?(str)
265
+ !str.strip.match(/\#+\s*TEST/i).nil? ||
266
+ !str.strip.match(/\A\#\#+[\s\w]+/i).nil?
454
267
  end
455
268
  end
456
269
 
270
+ extend ClassMethods
457
271
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tryouts
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-18 00:00:00.000000000 Z
11
+ date: 2024-06-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A simple test framework for Ruby code that uses introspection to allow
14
14
  defining checks in comments.
@@ -21,9 +21,14 @@ extra_rdoc_files: []
21
21
  files:
22
22
  - LICENSE.txt
23
23
  - README.md
24
+ - VERSION.yml
24
25
  - exe/try
25
26
  - exe/tryouts
26
27
  - lib/tryouts.rb
28
+ - lib/tryouts/console.rb
29
+ - lib/tryouts/section.rb
30
+ - lib/tryouts/testbatch.rb
31
+ - lib/tryouts/testcase.rb
27
32
  - lib/tryouts/version.rb
28
33
  homepage: https://github.com/delano/tryouts
29
34
  licenses:
@@ -44,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
49
  - !ruby/object:Gem::Version
45
50
  version: '0'
46
51
  requirements: []
47
- rubygems_version: 3.5.7
52
+ rubygems_version: 3.4.19
48
53
  signing_key:
49
54
  specification_version: 4
50
55
  summary: Ruby tests that read like documentation.