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/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
+