test-unit-runner-tap 1.0.0 → 1.1.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.
data/Gemfile.lock ADDED
@@ -0,0 +1,16 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ test-unit-runner-tap (0.0.1)
5
+ test-unit
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ test-unit (2.4.7)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ test-unit-runner-tap!
data/README.rdoc CHANGED
@@ -18,8 +18,25 @@ TAP-Y/J format.
18
18
 
19
19
  == USAGE:
20
20
 
21
+ It you test helper script use:
22
+
21
23
  require 'test/unit/runner/tap'
22
24
 
25
+ Then you can select the runner via the `--runner` command line option.
26
+
27
+ $ ruby test/run_tests.rb --runner yaml
28
+
29
+ Available runners are `tap`, `tapy`/`yaml` or `tapj`/`json`.
30
+
31
+ The runner can also be specified in code, if need be. See API documentation
32
+ for more information on how to do this.
33
+
34
+ To use TAP-Y/J formats with TAPOUT, just pipe results to tapout utility.
35
+
36
+ $ ruby test/run_tests.rb --runner yaml | tapout
37
+
38
+ See TAPOUT poject for more information on that.
39
+
23
40
  == LICENSE:
24
41
 
25
42
  (LGPL v3.0 License)
@@ -2,7 +2,7 @@ module Test
2
2
  module Unit
3
3
  module Runner
4
4
  module Tap
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.1"
6
6
  end
7
7
  end
8
8
  end
@@ -1,11 +1,40 @@
1
- require 'test/unit'
1
+ require 'test-unit'
2
2
  require 'test/unit/runner/tap-version'
3
3
 
4
4
  module Test
5
5
  module Unit
6
6
  AutoRunner.register_runner(:tap) do |auto_runner|
7
- require 'test/unit/ui/tap/testrunner'
8
- Test::Unit::UI::Tap::TestRunner
7
+ require 'test/unit/ui/tap/perl_testrunner'
8
+ Test::Unit::UI::Tap::PerlTestRunner
9
+ end
10
+
11
+ AutoRunner.register_runner(:tapj) do |auto_runner|
12
+ require 'test/unit/ui/tap/json_testrunner'
13
+ Test::Unit::UI::Tap::JSONTestRunner
14
+ end
15
+
16
+ # alias for tap-j
17
+ AutoRunner.register_runner(:json) do |auto_runner|
18
+ require 'test/unit/ui/tap/json_testrunner'
19
+ Test::Unit::UI::Tap::JSONTestRunner
20
+ end
21
+
22
+ AutoRunner.register_runner(:tapy) do |auto_runner|
23
+ require 'test/unit/ui/tap/yaml_testrunner'
24
+ Test::Unit::UI::Tap::YAMLTestRunner
25
+ end
26
+
27
+ # alias for tap-y
28
+ AutoRunner.register_runner(:yaml) do |auto_runner|
29
+ require 'test/unit/ui/tap/yaml_testrunner'
30
+ Test::Unit::UI::Tap::YAMLTestRunner
31
+ end
32
+
33
+ # temporaryily available as fallback to orignal tap runner
34
+ # just in case the new runner exhibits any issues
35
+ AutoRunner.register_runner(:tap0) do |auto_runner|
36
+ require 'test/unit/ui/tap/perl_testrunner'
37
+ Test::Unit::UI::Tap::OldTestRunner
9
38
  end
10
39
  end
11
40
  end
@@ -0,0 +1,550 @@
1
+ require 'test/unit/ui/testrunner'
2
+ require 'test/unit/ui/testrunnermediator'
3
+ require 'stringio'
4
+
5
+ module Test
6
+ module Unit
7
+ module UI
8
+ module Tap
9
+
10
+ # Base class for all TAP runners.
11
+ #
12
+ class BaseTestRunner < Test::Unit::UI::TestRunner
13
+
14
+ # TAP-Y/J Revision
15
+ REVISION = 4
16
+
17
+ #
18
+ def initialize(suite, options={})
19
+ super
20
+
21
+ @output = @options[:output] || STDOUT
22
+
23
+ @level = 0
24
+
25
+ @_source_cache = {}
26
+ @already_outputted = false
27
+ @top_level = true
28
+
29
+ @counts = Hash.new{ |h,k| h[k] = 0 }
30
+ end
31
+
32
+ private
33
+
34
+ #
35
+ def setup_mediator
36
+ super
37
+
38
+ #suite_name = @suite.to_s # file name
39
+ #suite_name = @suite.name if @suite.kind_of?(Module)
40
+
41
+ #reset_output # TODO: Should we do this up front?
42
+ end
43
+
44
+ #
45
+ def attach_to_mediator
46
+ @mediator.add_listener(Test::Unit::TestResult::FAULT, &method(:tapout_fault))
47
+ @mediator.add_listener(Test::Unit::UI::TestRunnerMediator::STARTED, &method(:tapout_before_suite))
48
+ @mediator.add_listener(Test::Unit::UI::TestRunnerMediator::FINISHED, &method(:tapout_after_suite))
49
+ @mediator.add_listener(Test::Unit::TestCase::STARTED_OBJECT, &method(:tapout_before_test))
50
+ @mediator.add_listener(Test::Unit::TestCase::FINISHED_OBJECT, &method(:tapout_pass))
51
+ @mediator.add_listener(Test::Unit::TestSuite::STARTED_OBJECT, &method(:tapout_before_case))
52
+ @mediator.add_listener(Test::Unit::TestSuite::FINISHED_OBJECT, &method(:tapout_after_case))
53
+ end
54
+
55
+ #
56
+ # Before everything else.
57
+ #
58
+ def tapout_before_suite(result)
59
+ @result = result
60
+ @suite_start = Time.now
61
+
62
+ doc = {
63
+ 'type' => 'suite',
64
+ 'start' => @suite_start.strftime('%Y-%m-%d %H:%M:%S'),
65
+ 'count' => @suite.size,
66
+ #'seed' => #@suite.seed, # no seed?
67
+ 'rev' => REVISION
68
+ }
69
+ return doc
70
+ end
71
+
72
+ #
73
+ # After everything else.
74
+ #
75
+ def tapout_after_suite(elapsed_time)
76
+ doc = {
77
+ 'type' => 'final',
78
+ 'time' => elapsed_time, #Time.now - @suite_start,
79
+ 'counts' => {
80
+ 'total' => @counts[:total],
81
+ 'pass' => @counts[:pass], #self.test_count - self.failures - self.errors - self.skips,
82
+ 'fail' => @counts[:fail],
83
+ 'error' => @counts[:error],
84
+ 'omit' => @counts[:omit],
85
+ 'todo' => @counts[:todo],
86
+ } #,
87
+ #'assertions' => {
88
+ # 'total' => @result.assertion_count + @counts[:fail],
89
+ # 'pass' => @result.assertion_count,
90
+ # 'fail' => @counts[:fail]
91
+ #}
92
+ }
93
+ return doc
94
+ end
95
+
96
+ #
97
+ #
98
+ #
99
+ def tapout_before_case(testcase)
100
+ return nil if testcase.test_case.nil?
101
+
102
+ @test_case = testcase
103
+
104
+ doc = {
105
+ 'type' => 'case',
106
+ #'subtype' => '',
107
+ 'label' => testcase.name,
108
+ 'level' => @level
109
+ }
110
+
111
+ @level += 1
112
+
113
+ return doc
114
+ end
115
+
116
+ #
117
+ # After each case, decrement the case level.
118
+ #
119
+ def tapout_after_case(testcase)
120
+ @level -= 1
121
+ end
122
+
123
+ #
124
+ def tapout_before_test(test)
125
+ @test_start = Time.now
126
+ # set up stdout and stderr to be captured
127
+ reset_output
128
+ end
129
+
130
+ #
131
+ def tapout_fault(fault)
132
+ case fault
133
+ when Test::Unit::Pending
134
+ tapout_todo(fault)
135
+ when Test::Unit::Omission
136
+ tapout_omit(fault)
137
+ when Test::Unit::Notification
138
+ tapout_note(fault)
139
+ when Test::Unit::Failure
140
+ tapout_fail(fault)
141
+ else
142
+ tapout_error(fault)
143
+ end
144
+
145
+ @already_outputted = true #if fault.critical?
146
+ end
147
+
148
+ #
149
+ def tapout_note(note)
150
+ doc = {
151
+ 'type' => 'note',
152
+ 'text' => note.message
153
+ }
154
+ return doc
155
+ end
156
+
157
+ #
158
+ def tapout_pass(test)
159
+ if @already_outputted
160
+ @already_outputted = false
161
+ return nil
162
+ end
163
+
164
+ @counts[:total] += 1
165
+ @counts[:pass] += 1
166
+
167
+ doc = {
168
+ 'type' => 'test',
169
+ #'subtype' => '',
170
+ 'status' => 'pass',
171
+ #'setup': foo instance
172
+ 'label' => clean_label(test.name),
173
+ #'expected' => 2
174
+ #'returned' => 2
175
+ #'file' => test_file
176
+ #'line' => test_line
177
+ #'source' => source(test_file)[test_line-1].strip,
178
+ #'snippet' => code_snippet(test_file, test_line),
179
+ 'time' => Time.now - @suite_start
180
+ }
181
+
182
+ doc.update(captured_output)
183
+
184
+ return doc
185
+ end
186
+
187
+ #
188
+ def tapout_todo(fault)
189
+ @counts[:total] += 1
190
+ @counts[:todo] += 1
191
+
192
+ file, line = location(fault.location)
193
+ rel_file = file.sub(Dir.pwd+'/', '')
194
+
195
+ doc = {
196
+ 'type' => 'test',
197
+ #'subtype' => '',
198
+ 'status' => 'todo',
199
+ 'label' => clean_label(fault.test_name),
200
+ #'setup' => "foo instance",
201
+ #'expected' => 2,
202
+ #'returned' => 1,
203
+ #'file' => test_file
204
+ #'line' => test_line
205
+ #'source' => source(test_file)[test_line-1].strip,
206
+ #'snippet' => code_snippet(test_file, test_line),
207
+ 'exception' => {
208
+ 'message' => clean_message(fault.message),
209
+ 'class' => fault.class.name,
210
+ 'file' => rel_file,
211
+ 'line' => line,
212
+ 'source' => source(file)[line-1].strip,
213
+ 'snippet' => code_snippet(file, line),
214
+ 'backtrace' => filter_backtrace(fault.location)
215
+ },
216
+ 'time' => Time.now - @suite_start
217
+ }
218
+
219
+ doc.update(captured_output)
220
+
221
+ return doc
222
+ end
223
+
224
+ #
225
+ def tapout_omit(fault)
226
+ @counts[:total] += 1
227
+ @counts[:omit] += 1
228
+
229
+ file, line = location(fault.location)
230
+ rel_file = file.sub(Dir.pwd+'/', '')
231
+
232
+ doc = {
233
+ 'type' => 'test',
234
+ #'subtype' => '',
235
+ 'status' => 'skip',
236
+ 'label' => clean_label(fault.test_name),
237
+ #'setup' => "foo instance",
238
+ #'expected' => 2,
239
+ #'returned' => 1,
240
+ #'file' => test_file
241
+ #'line' => test_line
242
+ #'source' => source(test_file)[test_line-1].strip,
243
+ #'snippet' => code_snippet(test_file, test_line),
244
+ 'exception' => {
245
+ 'message' => clean_message(fault.message),
246
+ 'class' => fault.class.name,
247
+ 'file' => rel_file,
248
+ 'line' => line,
249
+ 'source' => source(file)[line-1].strip,
250
+ 'snippet' => code_snippet(file, line),
251
+ 'backtrace' => filter_backtrace(fault.location)
252
+ },
253
+ 'time' => Time.now - @suite_start
254
+ }
255
+
256
+ doc.update(captured_output)
257
+
258
+ return doc
259
+ end
260
+
261
+ #
262
+ def tapout_fail(fault)
263
+ @counts[:total] += 1
264
+ @counts[:fail] += 1
265
+
266
+ file, line = location(fault.location)
267
+ rel_file = file.sub(Dir.pwd+'/', '')
268
+
269
+ doc = {
270
+ 'type' => 'test',
271
+ #'subtype' => '',
272
+ 'status' => 'fail',
273
+ 'label' => clean_label(fault.test_name),
274
+ #'setup' => "foo instance",
275
+ 'expected' => fault.inspected_expected,
276
+ 'returned' => fault.inspected_actual,
277
+ #'file' => test_file
278
+ #'line' => test_line
279
+ #'source' => ok 1, 2
280
+ #'snippet' =>
281
+ # - 44: ok 0,0
282
+ # - 45: ok 1,2
283
+ # - 46: ok 2,4
284
+ 'exception' => {
285
+ 'message' => clean_message(fault.user_message || fault.message),
286
+ 'class' => fault.class.name,
287
+ 'file' => rel_file,
288
+ 'line' => line,
289
+ 'source' => source(file)[line-1].strip,
290
+ 'snippet' => code_snippet(file, line),
291
+ 'backtrace' => filter_backtrace(fault.location)
292
+ },
293
+ 'time' => Time.now - @suite_start
294
+ }
295
+
296
+ doc.update(captured_output)
297
+
298
+ return doc
299
+ end
300
+
301
+ #
302
+ def tapout_error(fault)
303
+ @counts[:total] += 1
304
+ @counts[:error] += 1
305
+
306
+ file, line = location(fault.location)
307
+ rel_file = file.sub(Dir.pwd+'/', '')
308
+
309
+ doc = {
310
+ 'type' => 'test',
311
+ #'subtype' => '',
312
+ 'status' => 'error',
313
+ 'label' => clean_label(fault.test_name),
314
+ #'setup' => "foo instance",
315
+ #'expected' => fault.inspected_expected,
316
+ #'returned' => fault.inspected_actual,
317
+ #'file' => test_file
318
+ #'line' => test_line
319
+ #'source' => ok 1, 2
320
+ #'snippet' =>
321
+ # - 44: ok 0,0
322
+ # - 45: ok 1,2
323
+ # - 46: ok 2,4
324
+ 'exception' => {
325
+ 'message' => clean_message(fault.message),
326
+ 'class' => fault.class.name,
327
+ 'file' => rel_file,
328
+ 'line' => line,
329
+ 'source' => source(file)[line-1].strip,
330
+ 'snippet' => code_snippet(file, line),
331
+ 'backtrace' => filter_backtrace(fault.location)
332
+ },
333
+ 'time' => Time.now - @suite_start
334
+ }
335
+
336
+ doc.update(captured_output)
337
+
338
+ return doc
339
+ end
340
+
341
+ #
342
+ def clean_label(name)
343
+ name.sub(/\(.+?\)\z/, '').chomp('()')
344
+ end
345
+
346
+ # Clean the backtrace of any reference to test framework itself.
347
+ def filter_backtrace(backtrace)
348
+ trace = backtrace
349
+
350
+ ## remove backtraces that match any pattern in $RUBY_IGNORE_CALLERS
351
+ #trace = race.reject{|b| $RUBY_IGNORE_CALLERS.any?{|i| i=~b}}
352
+
353
+ ## remove `:in ...` portion of backtraces
354
+ trace = trace.map do |bt|
355
+ i = bt.index(':in')
356
+ i ? bt[0...i] : bt
357
+ end
358
+
359
+ # TODO: does TestUnit have a filter ?
360
+ ## now apply MiniTest's own filter (note: doesn't work if done first, why?)
361
+ #trace = MiniTest::filter_backtrace(trace)
362
+
363
+ ## if the backtrace is empty now then revert to the original
364
+ trace = backtrace if trace.empty?
365
+
366
+ ## simplify paths to be relative to current workding diectory
367
+ trace = trace.map{ |bt| bt.sub(Dir.pwd+File::SEPARATOR,'') }
368
+
369
+ return trace
370
+ end
371
+
372
+ # Returns a Hash of source code.
373
+ def code_snippet(file, line)
374
+ s = []
375
+ if File.file?(file)
376
+ source = source(file)
377
+ radius = 2 # TODO: make customizable (number of surrounding lines to show)
378
+ region = [line - radius, 1].max ..
379
+ [line + radius, source.length].min
380
+
381
+ s = region.map do |n|
382
+ {n => source[n-1].chomp}
383
+ end
384
+ end
385
+ return s
386
+ end
387
+
388
+ # Return nicely formated String of code lines.
389
+ def code_snippet_string(file, line)
390
+ str = []
391
+ snp = code_snippet_array(file, line)
392
+ max = snp.map{ |n, c| n.to_s.size }.max
393
+ snp.each do |n, c|
394
+ if n == line
395
+ str << "=> %#{max}d %s" % [n, c]
396
+ else
397
+ str << " %#{max}d %s" % [n, c]
398
+ end
399
+ end
400
+ str.join("\n")
401
+ end
402
+
403
+ # Return Array of source code line numbers and text.
404
+ def code_snippet_array(file, line)
405
+ snp = []
406
+ if File.file?(file)
407
+ source = source(file)
408
+ radius = 2 # TODO: make customizable (number of surrounding lines to show)
409
+ region = [line - radius, 1].max ..
410
+ [line + radius, source.length].min
411
+ snp = region.map do |n|
412
+ [n, source[n-1].chomp]
413
+ end
414
+ end
415
+ return snp
416
+ end
417
+
418
+ # Cache source file text. This is only used if the TAP-Y stream
419
+ # doesn not provide a snippet and the test file is locatable.
420
+ def source(file)
421
+ @_source_cache[file] ||= (
422
+ File.readlines(file)
423
+ )
424
+ end
425
+
426
+ # Parse source location from caller, caller[0] or an Exception object.
427
+ def parse_source_location(caller)
428
+ case caller
429
+ when Exception
430
+ trace = caller.backtrace.reject{ |bt| bt =~ INTERNALS }
431
+ caller = trace.first
432
+ when Array
433
+ caller = caller.first
434
+ end
435
+ caller =~ /(.+?):(\d+(?=:|\z))/ or return ""
436
+ source_file, source_line = $1, $2.to_i
437
+ return source_file, source_line
438
+ end
439
+
440
+ # Get location of exception.
441
+ def location(backtrace)
442
+ last_before_assertion = ""
443
+ backtrace.reverse_each do |s|
444
+ break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
445
+ last_before_assertion = s
446
+ end
447
+ file, line = last_before_assertion.sub(/:in .*$/, '').split(':')
448
+ line = line.to_i if line
449
+ return file, line
450
+ end
451
+
452
+ #
453
+ def clean_message(message)
454
+ message.strip.gsub(/\n+/, "\n")
455
+ end
456
+
457
+ #
458
+ def puts(string='')
459
+ @output.write(string.chomp+"\n")
460
+ @output.flush
461
+ end
462
+
463
+ #
464
+ def reset_output
465
+ @_oldout = $stdout
466
+ @_olderr = $stderr
467
+
468
+ @_newout = StringIO.new
469
+ @_newerr = StringIO.new
470
+
471
+ $stdout = @_newout
472
+ $stderr = @_newerr
473
+ end
474
+
475
+ #
476
+ def captured_output
477
+ stdout = @_newout.string.chomp("\n")
478
+ stderr = @_newerr.string.chomp("\n")
479
+
480
+ doc = {}
481
+ doc['stdout'] = stdout unless stdout.empty?
482
+ doc['stderr'] = stderr unless stderr.empty?
483
+
484
+ $stdout = @_oldout
485
+ $stderr = @_olderr
486
+
487
+ return doc
488
+ end
489
+
490
+ end
491
+
492
+ end #module Tap
493
+ end #module UI
494
+ end #module Unit
495
+ end #module Test
496
+
497
+
498
+
499
+
500
+ =begin
501
+ # TEMPORARILY LEAVING THIS FOR REFERENCE. What's this about encoding?
502
+
503
+ def output_fault_message(fault)
504
+ if fault.expected.respond_to?(:encoding) and
505
+ fault.actual.respond_to?(:encoding) and
506
+ fault.expected.encoding != fault.actual.encoding
507
+ need_encoding = true
508
+ else
509
+ need_encoding = false
510
+ end
511
+ output(fault.user_message) if fault.user_message
512
+ output_single("<")
513
+ output_single(fault.inspected_expected, color("pass"))
514
+ output_single(">")
515
+ if need_encoding
516
+ output_single("(")
517
+ output_single(fault.expected.encoding.name, color("pass"))
518
+ output_single(")")
519
+ end
520
+ output(" expected but was")
521
+ output_single("<")
522
+ output_single(fault.inspected_actual, color("failure"))
523
+ output_single(">")
524
+ if need_encoding
525
+ output_single("(")
526
+ output_single(fault.actual.encoding.name, color("failure"))
527
+ output_single(")")
528
+ end
529
+ output("")
530
+
531
+ from, to = prepare_for_diff(fault.expected, fault.actual)
532
+ if from and to
533
+ from_lines = from.split(/\r?\n/)
534
+ to_lines = to.split(/\r?\n/)
535
+ if need_encoding
536
+ from_lines << ""
537
+ to_lines << ""
538
+ from_lines << "Encoding: #{fault.expected.encoding.name}"
539
+ to_lines << "Encoding: #{fault.actual.encoding.name}"
540
+ end
541
+ differ = ColorizedReadableDiffer.new(from_lines, to_lines, self)
542
+ if differ.need_diff?
543
+ output("")
544
+ output("diff:")
545
+ differ.diff
546
+ end
547
+ end
548
+ end
549
+ =end
550
+