tapout 0.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/.ruby +44 -0
- data/APACHE2.txt +204 -0
- data/HISTORY.rdoc +11 -0
- data/NOTICE.rdoc +38 -0
- data/README.rdoc +73 -0
- data/TAP-YJ.rdoc +296 -0
- data/bin/tapout +3 -0
- data/lib/tapout.rb +88 -0
- data/lib/tapout/reporters.rb +6 -0
- data/lib/tapout/reporters/abstract.rb +266 -0
- data/lib/tapout/reporters/breakdown.rb +120 -0
- data/lib/tapout/reporters/dotprogress.rb +69 -0
- data/lib/tapout/reporters/progressbar.rb +89 -0
- data/lib/tapout/reporters/tap.rb +80 -0
- data/lib/tapout/reporters/verbose.rb +54 -0
- data/lib/tapout/tap_legacy_adapter.rb +168 -0
- data/lib/tapout/tap_legacy_parser.rb +25 -0
- data/lib/tapout/tapy_parser.rb +58 -0
- data/lib/tapout/version.rb +7 -0
- data/qed/applique/env.rb +5 -0
- data/qed/tap_adapter.rdoc +68 -0
- metadata +129 -0
data/bin/tapout
ADDED
data/lib/tapout.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'tapout/tapy_parser'
|
2
|
+
require 'tapout/tap_legacy_parser'
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
module TapOut
|
7
|
+
|
8
|
+
#
|
9
|
+
PARSERS = %w{breakdown dotprogress progressbar tap verbose}
|
10
|
+
|
11
|
+
#
|
12
|
+
def self.cli(*argv)
|
13
|
+
options = {}
|
14
|
+
type = :modern
|
15
|
+
|
16
|
+
parser = OptionParser.new do |opt|
|
17
|
+
opt.banner = "tapout [options] [format]"
|
18
|
+
|
19
|
+
opt.separator("\nOPTIONS:")
|
20
|
+
|
21
|
+
#opt.on('--format', '-f FORMAT', 'Report format') do |fmt|
|
22
|
+
# options[:format] = fmt
|
23
|
+
#end
|
24
|
+
|
25
|
+
#opt.on('-t', '--tap', 'Consume legacy TAP input') do |fmt|
|
26
|
+
# type = :legacy
|
27
|
+
#end
|
28
|
+
|
29
|
+
opt.on('--no-color', 'Supress ANSI color codes') do
|
30
|
+
# TODO
|
31
|
+
end
|
32
|
+
|
33
|
+
opt.on('--debug', 'Run with $DEBUG flag on') do |fmt|
|
34
|
+
$DEBUG = true
|
35
|
+
end
|
36
|
+
|
37
|
+
opt.separator("\nFORMATS:\n " + PARSERS.join("\n "))
|
38
|
+
end
|
39
|
+
|
40
|
+
parser.parse!(argv)
|
41
|
+
|
42
|
+
options[:format] = argv.first
|
43
|
+
|
44
|
+
# TODO: would be nice if it could automatically determine which
|
45
|
+
#c = $stdin.getc
|
46
|
+
# $stdin.pos = 0
|
47
|
+
#type = :legacy if c =~ /\d/
|
48
|
+
#type = :modern if c == '-'
|
49
|
+
|
50
|
+
stdin = Curmudgeon.new($stdin)
|
51
|
+
|
52
|
+
case stdin.line1
|
53
|
+
when /^\d/
|
54
|
+
type = :legacy
|
55
|
+
when /^\-/
|
56
|
+
type = :modern
|
57
|
+
else
|
58
|
+
raise "Not a recognized TAP stream!"
|
59
|
+
end
|
60
|
+
|
61
|
+
case type
|
62
|
+
when :legacy
|
63
|
+
stream_parser = TAPLegacyParser.new(options)
|
64
|
+
stream_parser.consume(stdin)
|
65
|
+
else
|
66
|
+
stream_parser = TAPYParser.new(options)
|
67
|
+
stream_parser.consume(stdin)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
class Curmudgeon #< IO
|
73
|
+
def initialize(input)
|
74
|
+
@input = input
|
75
|
+
@line1 = input.gets
|
76
|
+
end
|
77
|
+
def line1
|
78
|
+
@line1
|
79
|
+
end
|
80
|
+
def gets
|
81
|
+
(class << self; self; end).class_eval %{
|
82
|
+
def gets; @input.gets; end
|
83
|
+
}
|
84
|
+
return @line1
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
require 'ansi'
|
2
|
+
require 'abbrev'
|
3
|
+
|
4
|
+
module TapOut
|
5
|
+
|
6
|
+
# Namespace for Report Formats.
|
7
|
+
module Reporters
|
8
|
+
|
9
|
+
# Returns a Hash of name to reporter class.
|
10
|
+
def self.index
|
11
|
+
@index ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns a reporter class given it's name or a unique abbreviation of it.
|
15
|
+
def self.factory(name)
|
16
|
+
list = index.keys.abbrev
|
17
|
+
index[list[name]]
|
18
|
+
end
|
19
|
+
|
20
|
+
# The Abstract class serves as a base class for all reporters. Reporters
|
21
|
+
# must sublcass Abstract in order to be added the the Reporters Index.
|
22
|
+
#
|
23
|
+
# TODO: Simplify this class and have the sublcasses handle more of the load.
|
24
|
+
class Abstract
|
25
|
+
# When Abstract is inherited it saves a reference to it in `Reporters.index`.
|
26
|
+
def self.inherited(subclass)
|
27
|
+
name = subclass.name.split('::').last.downcase
|
28
|
+
Reporters.index[name] = subclass
|
29
|
+
end
|
30
|
+
|
31
|
+
# New reporter.
|
32
|
+
def initialize
|
33
|
+
@passed = []
|
34
|
+
@failed = []
|
35
|
+
@raised = []
|
36
|
+
@skipped = []
|
37
|
+
@omitted = []
|
38
|
+
|
39
|
+
@source = {}
|
40
|
+
@previous_case = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
def <<(entry)
|
45
|
+
handle(entry)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Handler method. This dispatches a given entry to the appropriate
|
49
|
+
# report methods.
|
50
|
+
def handle(entry)
|
51
|
+
case entry['type']
|
52
|
+
when 'header'
|
53
|
+
start_suite(entry)
|
54
|
+
when 'case'
|
55
|
+
finish_case(@previous_case) if @previous_case
|
56
|
+
@previous_case = entry
|
57
|
+
start_case(entry)
|
58
|
+
when 'note'
|
59
|
+
note(entry)
|
60
|
+
when 'test'
|
61
|
+
test(entry)
|
62
|
+
case entry['status']
|
63
|
+
when 'pass'
|
64
|
+
pass(entry)
|
65
|
+
when 'fail'
|
66
|
+
fail(entry)
|
67
|
+
when 'error'
|
68
|
+
err(entry)
|
69
|
+
when 'omit'
|
70
|
+
omit(entry)
|
71
|
+
when 'pending', 'skip'
|
72
|
+
skip(entry)
|
73
|
+
end
|
74
|
+
when 'footer'
|
75
|
+
finish_case(@previous_case) if @previous_case
|
76
|
+
finish_suite(entry)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Handle header.
|
81
|
+
def start_suite(entry)
|
82
|
+
end
|
83
|
+
|
84
|
+
# At the start of a new test case.
|
85
|
+
def start_case(entry)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Handle an arbitray note.
|
89
|
+
def note(entry)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Handle test. This is run before the status handlers.
|
93
|
+
def test(entry)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Handle test with pass status.
|
97
|
+
def pass(entry)
|
98
|
+
@passed << entry
|
99
|
+
end
|
100
|
+
|
101
|
+
# Handle test with fail status.
|
102
|
+
def fail(entry)
|
103
|
+
@failed << entry
|
104
|
+
end
|
105
|
+
|
106
|
+
# Handle test with error status.
|
107
|
+
def err(entry)
|
108
|
+
@raised << entry
|
109
|
+
end
|
110
|
+
|
111
|
+
# Handle test with omit status.
|
112
|
+
def omit(entry)
|
113
|
+
@omitted << entry
|
114
|
+
end
|
115
|
+
|
116
|
+
# Handle test with skip or pending status.
|
117
|
+
def skip(entry)
|
118
|
+
@skipped << entry
|
119
|
+
end
|
120
|
+
|
121
|
+
# When a test case is complete.
|
122
|
+
def finish_case(entry)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Handle footer.
|
126
|
+
def finish_suite(entry)
|
127
|
+
end
|
128
|
+
|
129
|
+
# TODO: get the tally's from the footer entry ?
|
130
|
+
def tally(entry)
|
131
|
+
total = entry['count'] || (@passed.size + @failed.size + @raised.size)
|
132
|
+
|
133
|
+
if entry['tally']
|
134
|
+
count_fail = entry['tally']['fail'] || 0
|
135
|
+
count_error = entry['tally']['error'] || 0
|
136
|
+
else
|
137
|
+
count_fail = @failed.size
|
138
|
+
count_error = @raised.size
|
139
|
+
end
|
140
|
+
|
141
|
+
if tally = entry['tally']
|
142
|
+
sums = %w{pass fail error skip}.map{ |e| tally[e] || 0 }
|
143
|
+
else
|
144
|
+
sums = [@passed, @failed, @raised, @skipped].map{ |e| e.size }
|
145
|
+
end
|
146
|
+
|
147
|
+
assertions = entry['assertions']
|
148
|
+
failures = entry['failures']
|
149
|
+
|
150
|
+
if assertions
|
151
|
+
text = "%s tests: %s pass, %s fail, %s err, %s pending (%s/%s assertions)"
|
152
|
+
text = text % [total, *sums] + [assertions - failures, assertions]
|
153
|
+
else
|
154
|
+
text = "%s tests: %s pass, %s fail, %s err, %s pending"
|
155
|
+
text = text % [total, *sums]
|
156
|
+
end
|
157
|
+
|
158
|
+
if count_fail > 0
|
159
|
+
text.ansi(:red)
|
160
|
+
elsif count_error > 0
|
161
|
+
text.ansi(:yellow)
|
162
|
+
else
|
163
|
+
text.ansi(:green)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
INTERNALS = /(lib|bin)#{Regexp.escape(File::SEPARATOR)}ko/
|
169
|
+
|
170
|
+
# Clean the backtrace of any reference to ko/ paths and code.
|
171
|
+
def clean_backtrace(backtrace)
|
172
|
+
trace = backtrace.reject{ |bt| bt =~ INTERNALS }
|
173
|
+
trace = trace.map do |bt|
|
174
|
+
if i = bt.index(':in')
|
175
|
+
bt[0...i]
|
176
|
+
else
|
177
|
+
bt
|
178
|
+
end
|
179
|
+
end
|
180
|
+
trace = backtrace if trace.empty?
|
181
|
+
trace = trace.map{ |bt| bt.sub(Dir.pwd+File::SEPARATOR,'') }
|
182
|
+
trace
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns a String of source code.
|
186
|
+
def code_snippet(entry)
|
187
|
+
file = entry['file']
|
188
|
+
line = entry['line']
|
189
|
+
snippet = entry['snippet']
|
190
|
+
|
191
|
+
s = []
|
192
|
+
|
193
|
+
case snippet
|
194
|
+
when String
|
195
|
+
lines = snippet.lines.to_a
|
196
|
+
index = line - ((lines.size - 1) / 2)
|
197
|
+
lines.each do |line|
|
198
|
+
s << [index, line]
|
199
|
+
index += 1
|
200
|
+
end
|
201
|
+
when Array
|
202
|
+
snippet.each do |h|
|
203
|
+
s << [h.key, h.value]
|
204
|
+
end
|
205
|
+
else
|
206
|
+
##backtrace = exception.backtrace.reject{ |bt| bt =~ INTERNALS }
|
207
|
+
##backtrace.first =~ /(.+?):(\d+(?=:|\z))/ or return ""
|
208
|
+
#caller =~ /(.+?):(\d+(?=:|\z))/ or return ""
|
209
|
+
#source_file, source_line = $1, $2.to_i
|
210
|
+
|
211
|
+
if File.file?(file)
|
212
|
+
source = source(file)
|
213
|
+
|
214
|
+
radius = 3 # number of surrounding lines to show
|
215
|
+
region = [source_line - radius, 1].max ..
|
216
|
+
[source_line + radius, source.length].min
|
217
|
+
|
218
|
+
#len = region.last.to_s.length
|
219
|
+
|
220
|
+
s = region.map do |n|
|
221
|
+
format % [n, source[n-1].chomp]
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
len = s.map{ |(n,t)| n }.max.to_s.length
|
227
|
+
|
228
|
+
# ensure proper alignment by zero-padding line numbers
|
229
|
+
format = " %5s %0#{len}d %s"
|
230
|
+
|
231
|
+
#s = s.map{|n,t|[n,t]}.sort{|a,b|a[0]<=>b[0]}
|
232
|
+
|
233
|
+
pretty = s.map do |(n,t)|
|
234
|
+
format % [('=>' if n == line), n, t.rstrip]
|
235
|
+
end #.unshift "[#{region.inspect}] in #{source_file}"
|
236
|
+
|
237
|
+
return pretty
|
238
|
+
end
|
239
|
+
|
240
|
+
# Cache source file text. This is only used if the TAP-Y stream
|
241
|
+
# doesn not provide a snippet and the test file is locatable.
|
242
|
+
def source(file)
|
243
|
+
@source[file] ||= (
|
244
|
+
File.readlines(file)
|
245
|
+
)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Parse source location from caller, caller[0] or an Exception object.
|
249
|
+
def parse_source_location(caller)
|
250
|
+
case caller
|
251
|
+
when Exception
|
252
|
+
trace = caller.backtrace.reject{ |bt| bt =~ INTERNALS }
|
253
|
+
caller = trace.first
|
254
|
+
when Array
|
255
|
+
caller = caller.first
|
256
|
+
end
|
257
|
+
caller =~ /(.+?):(\d+(?=:|\z))/ or return ""
|
258
|
+
source_file, source_line = $1, $2.to_i
|
259
|
+
returnf source_file, source_line
|
260
|
+
end
|
261
|
+
|
262
|
+
end#class Abstract
|
263
|
+
|
264
|
+
end#module Reporters
|
265
|
+
|
266
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'tapout/reporters/abstract'
|
2
|
+
|
3
|
+
module TapOut
|
4
|
+
|
5
|
+
module Reporters
|
6
|
+
|
7
|
+
# The Breakdown report format give a tally for each test case.
|
8
|
+
class Breakdown < Abstract
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
super
|
12
|
+
@case = {}
|
13
|
+
@case_entries = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_suite(entry)
|
17
|
+
headers = [ 'TESTCASE', 'TESTS', 'PASS', 'FAIL', 'ERR', 'SKIP' ]
|
18
|
+
puts "\n%-20s %8s %8s %8s %8s %8s\n" % headers
|
19
|
+
end
|
20
|
+
|
21
|
+
def start_case(entry)
|
22
|
+
@case = entry
|
23
|
+
@case_entries = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def test(entry)
|
27
|
+
@case_entries << entry
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
def finish_case(entry)
|
32
|
+
label = entry['label'][0,19]
|
33
|
+
groups = @case_entries.group_by{ |e| e['status'] }
|
34
|
+
|
35
|
+
total = @case_entries.size
|
36
|
+
sums = %w{pass fail error pending}.map{ |n| groups[n] ? groups[n].size : 0 }
|
37
|
+
|
38
|
+
result = sums[1] + sums[2] > 0 ? "FAIL".ansi(:red) : "PASS".ansi(:green)
|
39
|
+
|
40
|
+
puts "%-20s %8s %8s %8s %8s %8s [%s]" % ([label, total] + sums + [result])
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
def finish_suite(entry)
|
45
|
+
#@pbar.finish
|
46
|
+
post_report(entry)
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
def post_report(entry)
|
51
|
+
|
52
|
+
sums = %w{pass fail error pending}.map{ |n| entry['tally'][n] || 0 }
|
53
|
+
|
54
|
+
puts ("-" * 80)
|
55
|
+
|
56
|
+
tally_line = "%-20s " % "TOTAL"
|
57
|
+
tally_line << "%8s %8s %8s %8s %8s" % [entry['count'], *sums]
|
58
|
+
|
59
|
+
puts(tally_line + "\n")
|
60
|
+
|
61
|
+
=begin
|
62
|
+
tally = test_tally(entry)
|
63
|
+
|
64
|
+
width = suite.collect{ |tr| tr.name.size }.max
|
65
|
+
|
66
|
+
headers = [ 'TESTCASE ', ' TESTS ', 'ASSERTIONS', ' FAILURES ', ' ERRORS ' ]
|
67
|
+
io.puts "\n%-#{width}s %10s %10s %10s %10s\n" % headers
|
68
|
+
|
69
|
+
files = nil
|
70
|
+
|
71
|
+
suite.each do |testrun|
|
72
|
+
if testrun.files != [testrun.name] && testrun.files != files
|
73
|
+
label = testrun.files.join(' ')
|
74
|
+
label = Colorize.magenta(label)
|
75
|
+
io.puts(label + "\n")
|
76
|
+
files = testrun.files
|
77
|
+
end
|
78
|
+
io.puts paint_line(testrun, width)
|
79
|
+
end
|
80
|
+
|
81
|
+
#puts("\n%i tests, %i assertions, %i failures, %i errors\n\n" % tally)
|
82
|
+
|
83
|
+
tally_line = "-----\n"
|
84
|
+
tally_line << "%-#{width}s " % "TOTAL"
|
85
|
+
tally_line << "%10s %10s %10s %10s" % tally
|
86
|
+
|
87
|
+
io.puts(tally_line + "\n")
|
88
|
+
=end
|
89
|
+
|
90
|
+
bad = @failed + @raised
|
91
|
+
|
92
|
+
#fails = suite.select do |testrun|
|
93
|
+
# testrun.fail? || testrun.error?
|
94
|
+
#end
|
95
|
+
|
96
|
+
#if tally[2] != 0 or tally[3] != 0
|
97
|
+
unless bad.empty? # or verbose?
|
98
|
+
#puts "\n-- Failures and Errors --\n"
|
99
|
+
puts
|
100
|
+
bad.each do |e|
|
101
|
+
message = e['message'].strip
|
102
|
+
message = message.ansi(:red)
|
103
|
+
puts(message)
|
104
|
+
puts "#{e['file']}:#{e['line']}"
|
105
|
+
puts
|
106
|
+
puts code_snippet(e)
|
107
|
+
end
|
108
|
+
puts
|
109
|
+
end
|
110
|
+
#end
|
111
|
+
|
112
|
+
puts tally(entry)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|