probatio 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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.0.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