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