tapout 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/tapout ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'tapout'
3
+ TapOut.cli(*ARGV)
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,6 @@
1
+ require 'tapout/reporters/abstract'
2
+ require 'tapout/reporters/dotprogress'
3
+ require 'tapout/reporters/verbose'
4
+ require 'tapout/reporters/tap'
5
+ require 'tapout/reporters/progressbar'
6
+ require 'tapout/reporters/breakdown'
@@ -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
+