probatio 0.9.0 → 1.1.0

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.
data/lib/probatio.rb CHANGED
@@ -2,196 +2,453 @@
2
2
  #
3
3
  # probatio.rb
4
4
 
5
+ require 'pp'
6
+ require 'set'
5
7
  require 'stringio'
8
+ require 'io/console'
9
+
10
+ require 'colorato'
11
+
12
+ require 'probatio/debug'
13
+ require 'probatio/more'
6
14
 
7
15
 
8
16
  module Probatio
9
17
 
10
- VERSION = '0.9.0'
18
+ VERSION = '1.1.0'
11
19
 
12
20
  class << self
13
21
 
22
+ attr_reader :seed, :rng
23
+ attr_reader :map
24
+
25
+ def c; $_PROBATIO_COLOURS; end
26
+
14
27
  def run(run_opts)
15
28
 
16
- Probatio.despatch(:start, run_opts)
29
+ @seed = run_opts[:seed]
30
+ @rng = Random.new(@seed)
31
+
32
+ Probatio.despatch(:pre, run_opts)
33
+
34
+ #
35
+ # construct tree
17
36
 
18
- root_group = Group.new(nil, '_', {}, __FILE__, nil)
37
+ root_group = Group.new(nil, __FILE__, '_', {}, nil)
38
+
39
+ run_opts[:dirs] ||= []
40
+ run_opts[:files] ||= []
41
+
42
+ run_opts[:filez] = []
43
+ run_opts[:filen] = []
44
+
45
+ helpers = locate(run_opts, '*_helper.rb', '*_helpers.rb')
46
+ setups = locate(run_opts, 'setup.rb', '*_setup.rb')
47
+
48
+ dbg_s do
49
+ " / dirs: #{run_opts[:dirs].inspect}\n" +
50
+ " / files: #{run_opts[:files].inspect}\n" +
51
+ " / helpers: #{helpers.inspect}\n" +
52
+ " / setups: #{setups.inspect}\n"
53
+ end
19
54
 
20
- dir = run_opts[:dir]
55
+ # helpers and setups...
21
56
 
22
- Dir[File.join(dir, '**', '*_helper.rb')].each do |path|
57
+ helpers.each do |path|
23
58
 
24
59
  read_helper_file(root_group, path)
25
60
  end
26
61
 
27
- Dir[File.join(dir, '**', '*_test.rb')].each do |path|
62
+ setups.each do |path|
28
63
 
64
+ run_opts[:filez] << path
29
65
  read_test_file(root_group, path)
30
66
  end
31
67
 
68
+ # tests from dirs...
69
+
70
+ run_opts[:dirs].each do |dir|
71
+
72
+ ( Dir[File.join(dir, '**', '*_test.rb')] +
73
+ Dir[File.join(dir, '**', '*_tests.rb')]
74
+
75
+ ).each do |path|
76
+
77
+ run_opts[:filez] << path
78
+ read_test_file(root_group, path)
79
+ end
80
+ end
81
+
82
+ # tests from files...
83
+
84
+ run_opts[:files].each do |path|
85
+
86
+ colons = path.split(':')
87
+ fpath = colons.shift
88
+
89
+ if colons.empty?
90
+ run_opts[:filez] << path
91
+ else
92
+ colons.each do |lnum|
93
+ lnum = lnum.match?(/^\d+$/) ? lnum.to_i : false
94
+ run_opts[:filen] << [ fpath, lnum ] if lnum
95
+ end
96
+ end
97
+
98
+ read_test_file(root_group, fpath)
99
+ end
100
+
101
+ run_opts[:filen] = rework_filen(root_group, run_opts)
102
+
103
+ dbg_s { Cerata.vertical_h_to_s(run_opts, ' run_opts| ') }
104
+
105
+ #
106
+ # print or map
107
+
108
+ if run_opts[:print]
109
+
110
+ puts root_group.to_s
111
+
112
+ exit 0
113
+ end
114
+
115
+ if run_opts[:map]
116
+
117
+ root_group.map.each do |path, groups|
118
+ puts ". #{Probatio.c.green(path)}"
119
+ groups.each do |l0, l1, g|
120
+ puts " #{Probatio.c.dark_grey('%4d %4d')} %s" % [ l0, l1, g.head ]
121
+ end
122
+ end
123
+
124
+ exit 0
125
+ end
126
+
127
+ #
128
+ # run
129
+
130
+ dbg_s { "---\n" + root_group.to_s + "\n---\n" }
131
+
132
+ Probatio.despatch(:start, root_group, run_opts)
133
+
32
134
  root_group.run(run_opts)
33
135
 
34
- Probatio.despatch(:over, run_opts)
136
+ Probatio.despatch(:over, root_group, run_opts)
137
+
138
+ Probatio.despatch(:exit, root_group, run_opts)
139
+ # some plugin will catch that and do `exit 0` or `exit 1`...
35
140
  end
36
141
 
37
142
  def init
38
143
 
144
+ @read = Set.new
145
+
39
146
  @plugins = []
147
+ @plugouts = nil
40
148
  end
41
149
 
42
- def plugins; @plugins; end
150
+ def despatch(event_name, *details)
43
151
 
44
- def plug(x)
152
+ #p [ :despatch, event_name ]
153
+ en = event_name.to_sym
154
+ me = "on_#{event_name}"
155
+ ev = Probatio::Event.new(en, details)
156
+ #p [ :despatch, event_name, ev.delta ]
45
157
 
46
- @plugins << x
47
- end
158
+ dbg_m { ' ' + [ :despatch, en, ev.node && ev.node.full_name ].inspect }
48
159
 
49
- def despatch(event_name, *details)
160
+ @plugouts ||= @plugins.reverse
50
161
 
51
- #p [ :despatch, event_name ]
52
- m = "on_#{event_name}"
53
- ev = Probatio::Event.new(event_name.to_sym, details)
162
+ (ev.leave? ? @plugouts : @plugins).each do |plugin|
54
163
 
55
- @plugins.each do |plugin|
56
- plugin.send(m, ev) if plugin.respond_to?(m)
164
+ plugin.record(ev) if plugin.respond_to?(:record)
165
+ plugin.send(me, ev) if plugin.respond_to?(me)
57
166
  end
58
167
  end
59
168
 
169
+ def epath; '.probatio-environments.rb'; end
170
+ def opath; '.probatio-output.rb'; end
171
+ def tpath; '.test-point'; end
172
+
60
173
  protected
61
174
 
62
175
  def read_helper_file(group, path)
63
176
 
177
+ return if @read.include?(path); @read.add(path)
178
+
64
179
  Kernel.load(path)
65
180
  end
66
181
 
67
182
  def read_test_file(group, path)
68
183
 
184
+ return if @read.include?(path); @read.add(path)
185
+
69
186
  group.add_file(path)
70
187
  end
71
- end
72
188
 
73
- self.init
189
+ def rework_filen(root_group, run_opts)
74
190
 
75
- class Group
191
+ run_opts[:filen]
192
+ .inject([]) { |a, fn| a.concat(rework_fn(root_group.map, fn)) }
193
+ end
76
194
 
77
- attr_reader :name
78
- attr_accessor :path
195
+ def rework_fn(map, fn)
79
196
 
80
- def initialize(parent_group, name, group_opts, path, block)
197
+ fmap = map[fn[0]]
198
+ fline = fn[1]
81
199
 
82
- @parent = parent_group
200
+ n = fmap.find { |l0, l1, n| fline >= l0 && (fline <= l1 || l1 < 1) }
83
201
 
84
- @name = name
85
- @group_opts = group_opts
202
+ return [ fn ] if n && n[2].is_a?(Probatio::Test)
203
+
204
+ # we don't have a test...
205
+
206
+ n2 = n[2]
207
+ n2 = n2.parent unless n2.is_a?(Probatio::Group)
208
+
209
+ # we have a group, lists all its tests located in the given file...
210
+
211
+ n2.all_tests
212
+ .select { |t| t.path == fn[0] }
213
+ .collect { |t| t.path_and_line }
214
+ end
215
+
216
+ def locate(run_opts, *suffixes)
217
+
218
+ (
219
+ run_opts[:dirs].inject([]) { |a, d|
220
+ a.concat(do_locate(d, suffixes)) } +
221
+ run_opts[:files].inject([]) { |a, f|
222
+ a.concat(do_locate(File.dirname(f), suffixes)) }
223
+ ).uniq.sort
224
+ end
225
+
226
+ def do_locate(dir, suffixes)
227
+
228
+ return [] if dir == '.'
229
+
230
+ fs = suffixes
231
+ .inject([]) { |a, suf| a.concat(Dir[File.join(dir, '**', suf)]) }
232
+
233
+ fs.any? ? fs : do_locate(File.dirname(dir), suffixes)
234
+ end
235
+ end
236
+
237
+ class Node
238
+
239
+ attr_reader :parent, :path, :opts, :block, :children
240
+
241
+ def root; @parent ? @parent.root : self; end
242
+ def map; @parent ? @parent.map : (@map ||= {}); end
243
+
244
+ def initialize(parent, path, name, opts, block)
245
+
246
+ @parent = parent
86
247
  @path = path
248
+ @name = name
249
+ @opts = opts
250
+ @block = block
87
251
 
88
252
  @children = []
89
253
 
90
- add_block(block)
254
+ map_self
91
255
  end
92
256
 
93
- def add_block(block)
257
+ def filename; @filename ||= File.basename(@path); end
94
258
 
95
- instance_eval(&block) if block
259
+ def type; self.class.name.split('::').last.downcase; end
260
+ def test?; type == 'test'; end
261
+
262
+ def depth; parent ? parent.depth + 1 : 0; end
263
+
264
+ def name; @name || type; end
265
+ def array_name; parent ? parent.array_name + [ name ] : [ name ]; end
266
+ def full_name; array_name.join(' '); end
267
+
268
+ def pending?; @opts[:pending]; end
269
+
270
+ def line
271
+
272
+ (@block.respond_to?(:source_location) ? @block.source_location : [])[1]
96
273
  end
97
274
 
98
- def add_file(path)
275
+ def last_line
99
276
 
100
- @path = path
277
+ lln = map[path].find { |l0, l1, n| n == self }
278
+
279
+ l = lln && lln[1]
280
+ l && l > 0 ? l : 9_999_999
281
+ end
282
+
283
+ def path_and_line
101
284
 
102
- instance_eval(File.read(path))
285
+ [ @path, line ]
286
+ end
287
+
288
+ def location
289
+
290
+ "#{@path}:#{line}"
103
291
  end
104
292
 
105
293
  def to_s(opts={})
106
294
 
295
+ col = Probatio.c
107
296
  out = opts[:out] || StringIO.new
108
- ind = opts[:indent] || ''
109
- gos = @group_opts.any? ? ' ' + @group_opts.inspect : ''
297
+ opts1 = opts.merge(out: out)
110
298
 
111
- out <<
112
- "#{ind}group #{@name.inspect}#{gos}\n"
299
+ pali = location; pali = pali.chop if pali.end_with?(':')
113
300
 
114
- @children.each do |c|
115
- c.to_s(opts.merge(out: out, indent: ind + ' '))
116
- end unless opts[:head]
301
+ out << ' ' * depth
302
+ out << col.yellow(type)
303
+ out << (@name ? ' ' + @name.inspect : '')
304
+ out << (@opts.any? ? ' ' + @opts.inspect : '')
305
+ out << ' ' << col.dark_grey(pali)
306
+ out << "\n"
117
307
 
118
- opts[:out] ? nil : out.string
308
+ @children.each { |c| c.to_s(opts1) } unless opts[:head]
309
+
310
+ opts[:out] ? nil : out.string.strip
119
311
  end
120
312
 
121
- def head; to_s(head: true); end
313
+ def head(opts={}); to_s(opts.merge(head: true)).strip; end
122
314
 
123
- def run(run_opts)
315
+ def trail(opts={})
124
316
 
125
- # on_group_enter, on_group_leave
126
- # on_test_leave
127
- # on_setup, on_teardown
128
- # on_before, on_after
129
- Probatio.despatch(:group_enter, self)
317
+ out = opts[:out] || StringIO.new
318
+ opts1 = opts.merge(out: out, head: true)
130
319
 
131
- setups.each { |s| s.run(run_opts) }
320
+ parent.trail(opts1) if parent
321
+ to_s(opts1)
132
322
 
133
- tests.each do |t|
323
+ opts[:out] ? nil : out.string.strip
324
+ end
134
325
 
135
- c = Probatio::Context.new(self)
326
+ def skip?(run_opts); false; end
136
327
 
137
- befores.each { |b| c.run(b, run_opts) }
328
+ def groups
138
329
 
139
- c.run(t, run_opts)
330
+ @children.select { |c| c.is_a?(Probatio::Group) }
331
+ end
140
332
 
141
- afters.each { |a| c.run(a, run_opts) }
142
- end
333
+ protected
334
+
335
+ def exclude?(run_opts); false; end
336
+
337
+ def map_self
338
+
339
+ l =
340
+ @block_source_location ? @block_source_location[1] :
341
+ @block ? @block.source_location[1] :
342
+ 0
343
+
344
+ f = (map[path] ||= [])
345
+
346
+ f0 = f.last
347
+ f0[1] = (l == 0 ? f0[0] : l - 1) if f0
348
+
349
+ f << [ l, 0, self ]
350
+ end
351
+ end
352
+
353
+ class Section < Node
354
+
355
+ def path=(pat)
356
+
357
+ @path0 ||= @path
358
+ @path = pat
359
+ end
360
+ # so it can be set when groups are "re-opened"...
361
+
362
+ def initialize(parent_group, path, name, opts, block)
363
+
364
+ @block_source_location = block && block.source_location
365
+
366
+ super(parent_group, path, name, opts, nil)
367
+
368
+ parent_group.add_section(self, block) \
369
+ if self.class == Probatio::Section
370
+ end
371
+
372
+ def add_block(block)
373
+
374
+ instance_eval(&block) if block
375
+ end
376
+
377
+ def add_file(path)
143
378
 
144
- groups.each { |g| g.run(run_opts) }
379
+ @path = path
380
+
381
+ instance_eval(File.read(path), path, 1)
382
+ end
383
+
384
+ def run(run_opts)
385
+
386
+ return Probatio.despatch(:group_excluded, self) \
387
+ if exclude?(run_opts)
145
388
 
146
- teardowns.each { |s| s.run(run_opts) }
389
+ return Probatio.despatch(:group_pending, self) \
390
+ if opts[:pending]
391
+
392
+ return Probatio.despatch(:group_skipped, self) \
393
+ if skip?(run_opts)
394
+
395
+ Probatio.despatch(:group_enter, self)
396
+
397
+ (
398
+ setups +
399
+ shuffle(tests_and_groups) +
400
+ teardowns
401
+ ).each { |c| c.run(run_opts) }
147
402
 
148
403
  Probatio.despatch(:group_leave, self)
149
404
  end
150
405
 
151
406
  def setup(opts={}, &block)
152
- @children << Probatio::Setup.new(self, nil, opts, @path, block)
407
+ @children << Probatio::Setup.new(self, @path, nil, opts, block)
153
408
  end
154
409
  def teardown(opts={}, &block)
155
- @children << Probatio::Teardown.new(self, nil, opts, @path, block)
410
+ @children << Probatio::Teardown.new(self, @path, nil, opts, block)
156
411
  end
157
412
  def before(opts={}, &block)
158
- @children << Probatio::Before.new(self, nil, opts, @path, block)
413
+ @children << Probatio::Before.new(self, @path, nil, opts, block)
159
414
  end
160
415
  def after(opts={}, &block)
161
- @children << Probatio::After.new(self, nil, opts, @path, block)
416
+ @children << Probatio::After.new(self, @path, nil, opts, block)
162
417
  end
163
-
164
- def group(name, opts={}, &block)
165
-
166
- if g = @children.find { |e| e.is_a?(Probatio::Group) && e.name == name }
167
- g.path = @path
168
- g.add_block(block)
169
- else
170
- @children << Probatio::Group.new(self, name, opts, @path, block)
171
- end
418
+ def around(opts={}, &block)
419
+ @children << Probatio::Around.new(self, @path, nil, opts, block)
172
420
  end
173
421
 
174
- def test(name, opts={}, &block)
422
+ def arounds
175
423
 
176
- @children << Probatio::Test.new(self, name, opts, @path, block)
424
+ (@parent ? @parent.arounds : []) +
425
+ group_sections.select { |s| s.is_a?(Probatio::Around) } +
426
+ @children.select { |c| c.is_a?(Probatio::Around) }
177
427
  end
178
428
 
179
429
  def befores
180
430
 
181
431
  (@parent ? @parent.befores : []) +
432
+ group_sections.select { |s| s.is_a?(Probatio::Before) } +
182
433
  @children.select { |c| c.is_a?(Probatio::Before) }
183
434
  end
184
435
 
185
436
  def afters
186
437
 
187
- (@parent ? @parent.afters : []) +
188
- @children.select { |c| c.is_a?(Probatio::After) }
438
+ (
439
+ (@parent ? @parent.afters : []) +
440
+ group_sections.select { |c| c.is_a?(Probatio::After) } +
441
+ @children.select { |c| c.is_a?(Probatio::After) }
442
+ ).reverse
189
443
  end
190
444
 
191
445
  ATTRS = %i[ @parent @name @group_opts @path @children ].freeze
192
446
 
193
447
  def context(h={})
194
448
 
449
+ fail ArgumentError.new('Probatio says "trailing RSpec context?"') \
450
+ unless h.is_a?(Hash)
451
+
195
452
  instance_variables
196
453
  .each { |k|
197
454
  h[k] = instance_variable_get(k) unless ATTRS.include?(k) }
@@ -200,49 +457,142 @@ module Probatio
200
457
  h
201
458
  end
202
459
 
460
+ METHS = %i[
461
+ _group _section
462
+ _setup _teardown _before _after
463
+ _test
464
+ ].freeze
465
+
466
+ def method_missing(name, *args, &block)
467
+
468
+ if METHS.include?(name)
469
+
470
+ opts = args.find { |a| a.is_a?(Hash) }
471
+ args << {} unless opts; opts = args.last
472
+ opts[:pending] = true
473
+
474
+ send(name.to_s[1..-1], *args, &block)
475
+
476
+ else
477
+
478
+ super
479
+ end
480
+ end
481
+
482
+ def skip?(run_opts)
483
+
484
+ opts[:pending] ||
485
+ tests_and_groups.all? { |n| n.skip?(run_opts) }
486
+ end
487
+
488
+ def all_tests
489
+
490
+ tests + groups.inject([]) { |a, g| a.concat(g.all_tests) }
491
+ end
492
+
493
+ def add_section(section, block)
494
+
495
+ s = ((@sections ||= {})[section.name] ||= section)
496
+ s.add_block(block)
497
+ end
498
+
203
499
  protected
204
500
 
205
- def setups; @children.select { |c| c.is_a?(Probatio::Setup) }; end
206
- def teardowns; @children.select { |c| c.is_a?(Probatio::Teardown) }; end
501
+ def setups
502
+
503
+ group_sections.select { |c| c.is_a?(Probatio::Setup) } +
504
+ @children.select { |c| c.is_a?(Probatio::Setup) }
505
+ end
506
+
507
+ def teardowns
508
+
509
+ group_sections.select { |c| c.is_a?(Probatio::Teardown) } +
510
+ @children.select { |c| c.is_a?(Probatio::Teardown) }
511
+ end
512
+
513
+ def tests_and_groups
207
514
 
208
- def test_and_groups
209
515
  @children.select { |c|
210
516
  c.is_a?(Probatio::Test) || c.is_a?(Probatio::Group) }
211
517
  end
518
+
212
519
  def tests; @children.select { |c| c.is_a?(Probatio::Test) }; end
213
520
  def groups; @children.select { |c| c.is_a?(Probatio::Group) }; end
214
- end
215
521
 
216
- class Child
522
+ def shuffle(a)
217
523
 
218
- attr_reader :name, :opts, :block
524
+ case Probatio.seed
525
+ when 0 then a
526
+ when -1 then a.reverse
527
+ else a.shuffle(random: Probatio.rng)
528
+ end
529
+ end
219
530
 
220
- def initialize(parent, name, opts, path, block)
531
+ def section_drill
221
532
 
222
- @parent = parent
223
- @name = name
224
- @opts = opts
225
- @path = path
226
- @block = block
533
+ (@parent ? @parent.section_drill : []) +
534
+ (@sections || {}).values
227
535
  end
228
536
 
229
- def type
537
+ def group_sections
230
538
 
231
- self.class.name.split('::').last.downcase
539
+ @_group_sections ||=
540
+ section_drill
541
+ .inject([]) { |a, s| a.concat(s.children) if s.name == name; a }
232
542
  end
543
+ end
233
544
 
234
- def to_s(opts={})
545
+ class Group < Section
235
546
 
236
- n = @name ? ' ' + @name.inspect : ''
237
- os = @opts.any? ? ' ' + @opts.inspect : ''
238
- _, l = block.source_location
547
+ def group(*names, &block)
239
548
 
240
- (opts[:out] || $stdout) <<
241
- "#{opts[:indent]}#{type}#{n}#{os} #{@path}:#{l}\n"
549
+ opts = names.last.is_a?(Hash) ? names.pop : {}
550
+
551
+ names = names
552
+ .collect { |s| s.to_s.split(/\s*(?:\||;|<|>)\s*/) }
553
+ .flatten(1)
554
+
555
+ last_name = names.last
556
+
557
+ names.inject(self) do |g, name|
558
+
559
+ gg = g.groups.find { |e| e.name == name }
560
+
561
+ if gg
562
+ gg.path = @path
563
+ else
564
+ gg = Probatio::Group.new(g, @path, name, opts, block)
565
+ g.children << gg
566
+ end
567
+
568
+ gg.add_block(block) if name == last_name
569
+
570
+ gg
571
+ end
242
572
  end
243
573
 
574
+ def section(name, opts={}, &block)
575
+
576
+ @children << Probatio::Section.new(self, @path, name.to_s, opts, block) \
577
+ unless opts[:pending]
578
+ end
579
+
580
+ def test(name, opts={}, &block)
581
+
582
+ @children << Probatio::Test.new(self, @path, name.to_s, opts, block)
583
+ end
584
+ end
585
+
586
+ class Leaf < Node
587
+
244
588
  def run(run_opts)
245
589
 
590
+ return Probatio.despatch("#{type}_excluded", self) \
591
+ if exclude?(run_opts)
592
+
593
+ return Probatio.despatch("#{type}_pending", self) \
594
+ if @opts[:pending]
595
+
246
596
  Probatio.despatch("#{type}_enter", self)
247
597
 
248
598
  @parent.instance_eval(&@block)
@@ -251,144 +601,410 @@ module Probatio
251
601
  end
252
602
  end
253
603
 
254
- class Setup < Child; end
255
- class Teardown < Child; end
604
+ class Setup < Leaf; end
605
+ class Teardown < Leaf; end
256
606
 
257
- class Before < Child; end
258
- class After < Child; end
607
+ class Before < Leaf; end
608
+ class After < Leaf; end
609
+ class Around < Leaf; end
259
610
 
260
- class Test < Child; end
611
+ class Test < Leaf
261
612
 
262
- class Context
613
+ alias group parent
263
614
 
264
- def initialize(group)
615
+ def run(run_opts)
265
616
 
266
- group.context.each { |k, v| instance_variable_set(k, v) }
617
+ return Probatio.despatch(:test_excluded, self) \
618
+ if exclude?(run_opts)
619
+
620
+ return Probatio.despatch(:test_pending, self) \
621
+ if opts[:pending]
622
+
623
+ Probatio::Context.new(group, self).run(run_opts)
624
+ end
625
+
626
+ def skip?(run_opts)
627
+
628
+ opts[:pending] ||
629
+ exclude?(run_opts)
630
+ end
631
+
632
+ protected
633
+
634
+ def exclude?(run_opts)
635
+
636
+ return true if in_setup?
637
+
638
+ if incs = run_opts[:includes]
639
+ return true unless incs.find { |e| do_match?(e) }
640
+ elsif exes = run_opts[:excludes]
641
+ return true if exes.find { |e| do_match?(e) }
642
+ end
643
+
644
+ fz = run_opts[:filez]
645
+ return false if fz && fz.include?(@path)
646
+
647
+ fns = run_opts[:filen]
648
+ return true if fns && ! fns.find { |pa, li| path_and_line_match?(pa, li) }
649
+
650
+ false
267
651
  end
268
652
 
269
- def run(child, run_opts)
653
+ def do_match?(pattern_or_string)
270
654
 
271
- @__child = child
655
+ full_name.match?(pattern_or_string)
656
+ end
272
657
 
273
- Probatio.despatch("#{child.type}_enter", self, child, run_opts)
658
+ def path_and_line_match?(fpath, fline)
659
+
660
+ #p [ path, line, last_line, '<-->', fpath, fline ]
661
+ line &&
662
+ path == fpath &&
663
+ fline >= line && fline <= last_line
664
+ end
274
665
 
275
- instance_eval(&child.block)
666
+ def in_setup?
276
667
 
277
- rescue AssertionError => aerr
278
- ensure
279
- Probatio.despatch("#{child.type}_leave", self, child, run_opts)
668
+ filename == 'setup.rb' || filename.end_with?('_setup.rb')
280
669
  end
670
+ end
281
671
 
282
- def assert(*as)
672
+ class Context
283
673
 
284
- do_assert do
674
+ attr_reader :__group
675
+ attr_reader :__test
285
676
 
286
- as.all? { |a| a == as[0] } ||
287
- "no equal"
288
- end
677
+ def initialize(group, test)
678
+
679
+ @__group = group
680
+ @__test = test
681
+
682
+ group.context.each { |k, v| instance_variable_set(k, v) }
289
683
  end
290
684
 
291
- def assert_match(*as)
685
+ def block; @__block; end
292
686
 
293
- do_assert do
687
+ def run(run_opts)
294
688
 
295
- strings, others = as.partition { |a| a.is_a?(String) }
296
- rex = others.find { |o| o.is_a?(Regexp) } || strings.pop
689
+ _run(@__group.arounds + [ :do_run ], run_opts)
690
+ end
297
691
 
298
- strings.all? { |s| s.match?(rex) } ||
299
- "no match"
692
+ def __test_name; @__test.name; end
693
+ def __group_name; @__group.name; end
694
+
695
+ protected
696
+
697
+ def _run(arounds, run_opts)
698
+
699
+ if (ar = arounds.shift).is_a?(Probatio::Around)
700
+ do_run(ar, run_opts) { _run(arounds, run_opts) }
701
+ else
702
+ @__group.befores.each { |bf| do_run(bf, run_opts) }
703
+ do_run(@__test, run_opts)
704
+ @__group.afters.each { |af| do_run(af, run_opts) }
300
705
  end
301
706
  end
302
707
 
303
- protected
708
+ def do_run(child, run_opts, &block)
304
709
 
305
- def do_assert(&block)
710
+ fail ArgumentError.new("invalid child opts #{child.opts.inspect}") \
711
+ unless child.opts.is_a?(Hash)
306
712
 
307
- r =
308
- begin
309
- block.call
310
- rescue => err
311
- err
312
- end
713
+ return Probatio.despatch("#{child.type}_pending", self, child, run_opts) \
714
+ if child.opts[:pending] || child.block.nil?
313
715
 
314
- if r.is_a?(StandardError) || r.is_a?(String)
716
+ begin
315
717
 
316
- Probatio.despatch(:test_succeed, self, @__child)
718
+ @__child = child
719
+ @__block = block
317
720
 
318
- fail AssertionError.new(r)
721
+ Probatio.despatch("#{child.type}_enter", self, child, run_opts)
319
722
 
320
- elsif r.is_a?(Exception)
723
+ r =
724
+ run_opts[:dry] ? nil :
725
+ instance_eval(&child.block)
321
726
 
322
- raise r
323
- end
727
+ Probatio.despatch(:test_succeed, self, child) \
728
+ if r != :pending && child.type == 'test'
729
+
730
+ rescue AssertionError
731
+
732
+ #Probatio.despatch(:test_fail, self, child)
733
+ # done in the assertion implementation...
734
+
735
+ rescue StandardError => serr
736
+
737
+ class << serr; include Probatio::ExtraErrorMethods; end
738
+ serr.test = child
324
739
 
325
- Probatio.despatch(:test_fail, self, @__child)
740
+ Probatio.despatch(:test_fail, self, child, serr)
326
741
 
327
- true # end on a positive note...
742
+ ensure
743
+
744
+ Probatio.despatch("#{child.type}_leave", self, child, run_opts)
745
+ end
328
746
  end
747
+
748
+ require 'probatio/assertions'
749
+ #
750
+ # where assert_* methods are defined...
751
+
752
+ require 'probatio/waiters'
753
+ #
754
+ # where wait_* methods are defined...
329
755
  end
330
756
 
331
757
  class AssertionError < StandardError
332
758
 
759
+ attr_reader :assertion, :arguments, :test, :file, :line
333
760
  attr_accessor :nested_error
334
761
 
335
- def initialize(error_or_message)
762
+ alias path file
763
+
764
+ def initialize(assertion, arguments, error_or_message, test, file, line)
765
+
766
+ @assertion = assertion
767
+ @arguments = arguments
768
+
769
+ @test = test
770
+
771
+ @file = file
772
+ @line = line
336
773
 
337
774
  if error_or_message.is_a?(String)
338
- super(message)
775
+ @msg = error_or_message
339
776
  else
340
- super("error while asserting: " + error_or_message.message)
777
+ @msg = "error while asserting: " + error_or_message.message
341
778
  @nested_error = error_or_message
342
779
  end
780
+
781
+ super(@msg)
782
+ end
783
+
784
+ def location
785
+
786
+ [ @file, @line ]
787
+ end
788
+
789
+ def loc
790
+
791
+ location.map(&:to_s).join(':')
792
+ end
793
+
794
+ def to_s
795
+
796
+ "#{self.class.name}: #{@msg}"
797
+ end
798
+
799
+ def trail
800
+
801
+ @test.trail + "\n" +
802
+ Probatio.c.red("#{' ' * (test.depth + 1)}#{loc} --> #{@msg}")
803
+ end
804
+
805
+ def source_line
806
+
807
+ @source_line ||=
808
+ File.readlines(@file)[@line - 1]
809
+ end
810
+
811
+ def source_lines
812
+
813
+ @source_lines ||=
814
+ Probatio::AssertionError.select_source_lines(@file, @line)
815
+ end
816
+
817
+ def summary(indent='')
818
+
819
+ tw = Probatio.term_width - 4 - indent.length
820
+
821
+ as =
822
+ @arguments.find { |a| a.inspect.length > tw } ?
823
+ @arguments.collect { |a|
824
+ if (s0 = a.inspect).length < tw
825
+ "\n#{indent} " + s0
826
+ else
827
+ s1 = StringIO.new; PP.pp(a, s1, tw)
828
+ qualify_argument(a) + "\n" +
829
+ indent + s1.string.gsub(/^(.*)$/) { " #{$1}" }
830
+ end } :
831
+ @arguments.collect(&:inspect)
832
+
833
+ s = StringIO.new
834
+ s << indent << @assertion << ':'
835
+ as.each_with_index { |a, i| s << "\n#{indent} %d: %s" % [ i, a ] }
836
+
837
+ s.string
838
+ end
839
+
840
+ class << self
841
+
842
+ def select_source_lines(path, line)
843
+
844
+ File.readlines(path).each_with_index.to_a[line - 1..-1]
845
+ .map { |l, i| [ i + 1, l.rstrip ] }
846
+ .take_while { |_, l|
847
+ l = l.strip
848
+ l.length > 0 && l != 'end' && l != '}' }
849
+ end
850
+ end
851
+
852
+ protected
853
+
854
+ def qualify_argument(a)
855
+
856
+ '<' +
857
+ a.class.to_s +
858
+ (a.respond_to?(:size) ? " size:#{a.size}" : '') +
859
+ '>'
860
+ end
861
+ end
862
+
863
+ module ExtraErrorMethods
864
+
865
+ attr_accessor :test
866
+
867
+ def path; test.path; end
868
+ def location; [ path, line ]; end
869
+ def loc; location.map(&:to_s).join(':'); end
870
+
871
+ def trail
872
+
873
+ msg = "#{self.class}: #{self.message.inspect}"
874
+
875
+ @test.trail + "\n" +
876
+ Probatio.c.red("#{' ' * (test.depth + 1)}#{loc} --> #{msg}")
877
+ end
878
+
879
+ def source_lines
880
+
881
+ @source_lines ||=
882
+ Probatio::AssertionError.select_source_lines(test.path, line)
883
+ end
884
+
885
+ def summary(indent='')
886
+
887
+ o = StringIO.new
888
+
889
+ o << self.class.name << ': ' << self.message.inspect << "\n"
890
+
891
+ i = backtrace.index { |l| l.match?(/\/lib\/probatio\.rb:/) } || -1
892
+
893
+ backtrace[0..i]
894
+ .inject(o) { |o, l| o << indent << l << "\n" }
895
+
896
+ o.string
897
+ end
898
+
899
+ def line
900
+
901
+ backtrace.each do |l|
902
+
903
+ ss = l.split(':')
904
+
905
+ next unless ss.find { |e| e == test.path }
906
+ return ss.find { |e| e.match?(/^\d+$/) }.to_i
907
+ end
908
+
909
+ -1
343
910
  end
344
911
  end
345
912
 
346
913
  class Event
347
914
 
348
- attr_reader :name, :opts, :context, :group, :child
915
+ attr_reader :tstamp, :delta
916
+ attr_reader :name, :opts, :context, :group, :leaf, :error
917
+ attr_accessor :leave_delta
349
918
 
350
919
  def initialize(name, details)
351
920
 
352
- @name = name
921
+ @tstamp, @delta = Probatio.monow_and_delta
922
+
923
+ @name = name.to_s
353
924
 
354
925
  details.each do |d|
355
926
  case d
356
927
  when Hash then @opts = d
928
+ when Exception then @error = d
929
+ when Probatio::Leaf then @leaf = d
357
930
  when Probatio::Group then @group = d
358
- when Probatio::Child then @child = d
359
931
  when Probatio::Context then @context = d
360
932
  else fail ArgumentError.new("cannot fathom #{d.class} #{d.inspect}")
361
933
  end
362
934
  end
363
935
  end
364
- end
365
- end
366
936
 
367
- module Probatio::DotReporter
937
+ def direction; @direction ||= name.split('_').last.to_sym; end
938
+ def node; @leaf || @group; end
939
+ def depth; node.depth rescue 0; end
368
940
 
369
- class << self
941
+ def type; @name.split('_').first; end
942
+ #
943
+ # which, in the case of assertion != self.node.type ...
944
+
945
+ def node_full_name; node && node.full_name; end
946
+
947
+ def enter?; direction == :enter; end
948
+ def leave?; direction == :leave; end
949
+
950
+ def determine_leave_delta
951
+
952
+ lev = Probatio.recorder_plugin.test_leave_event(node)
953
+
954
+ lev && lev.leave_delta
955
+ end
956
+
957
+ def delta_s
958
+
959
+ led = determine_leave_delta
960
+ led ? Probatio.to_time_s(led) : '?'
961
+ end
370
962
 
371
- def on_start(ev)
963
+ def location
372
964
 
373
- @successes = []
374
- @failures = []
965
+ (error && error.respond_to?(:location) && error.location) ||
966
+ (node && node.location)
375
967
  end
376
968
 
377
- def on_test_succeed(ev)
378
- print '.'
379
- @successes << ev
969
+ def path
970
+
971
+ node && node.path
380
972
  end
381
973
 
382
- def on_test_fail(ev)
383
- print 'x'
384
- @failures << ev
974
+ def to_s
975
+
976
+ led = determine_leave_delta
977
+
978
+ o = StringIO.new
979
+ o << "<event"
980
+ o << "\n name=#{name.inspect}"
981
+ o << "\n node=#{node.full_name.inspect}" if node
982
+ o << "\n node_type=#{node.type.inspect}" if node
983
+ o << "\n error=#{error.to_s.inspect}" if error
984
+ o << "\n location=#{location.map(&:to_s).join(':').inspect}" if node
985
+ o << "\n delta=\"#{Probatio.to_time_s(delta)}\"" if delta
986
+ o << "\n leave_delta=\"#{Probatio.to_time_s(led)}\"" if led
987
+ o << " />"
988
+
989
+ o.string
385
990
  end
386
991
 
387
- def on_over(ev)
388
- puts
992
+ def to_h
993
+
994
+ { n: name, p: location[0], l: location[1], t: delta_s }
389
995
  end
390
996
  end
391
997
  end
392
998
 
393
- Probatio.plug(Probatio::DotReporter)
999
+
1000
+ Probatio.init
1001
+
1002
+
1003
+ require 'probatio/plug'
1004
+ #
1005
+ # when Probatio.plug and friends are defined
1006
+
1007
+ require 'probatio/plugins'
1008
+ #
1009
+ # plugins that listen to dispatches, report, and summarize
394
1010