minitest_log 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,31 @@
1
+ require 'diff/lcs'
2
+
3
+ class ArrayHelper
4
+
5
+ # Compare two arrays.
6
+ def self.compare(expected, actual)
7
+ sdiff = Diff::LCS.sdiff(expected, actual)
8
+ changes = {}
9
+ action_words = {
10
+ '!' => 'changed',
11
+ '+' => 'unexpected',
12
+ '-' => 'missing',
13
+ '=' => 'unchanged'
14
+ }
15
+ sdiff.each_with_index do |change, i|
16
+ action_word = action_words.fetch(change.action)
17
+ key = "change_#{i}"
18
+ attrs = %W/
19
+ action=#{action_word}
20
+ old_pos=#{change.old_position}
21
+ old_ele=#{change.old_element}
22
+ new_pos=#{change.old_position}
23
+ new_ele=#{change.old_element}
24
+ /
25
+ value = attrs.join(' ')
26
+ changes.store(key, value)
27
+ end
28
+ {:sdiff => changes}
29
+ end
30
+
31
+ end
@@ -0,0 +1,32 @@
1
+ class HashHelper
2
+
3
+ # Compare two hashes.
4
+ # Returns a hash with keys +:ok+, +:missing+, +:unexpected+, +:changed+.
5
+ def self.compare(expected, actual)
6
+ result = {
7
+ :missing => {},
8
+ :unexpected => {},
9
+ :changed => {},
10
+ :ok => {},
11
+ }
12
+ expected.each_pair do |key_expected, value_expected|
13
+ if actual.include?(key_expected)
14
+ value_actual = actual[key_expected]
15
+ if value_actual == value_expected
16
+ result[:ok][key_expected] = value_expected
17
+ else
18
+ result[:changed][key_expected] = {:expected => value_expected, :actual => value_actual}
19
+ end
20
+ else
21
+ result[:missing][key_expected] = value_expected
22
+ end
23
+ end
24
+ actual.each_pair do |key_actual, value_actual|
25
+ next if expected.include?(key_actual)
26
+ result[:unexpected][key_actual] = value_actual
27
+ end
28
+ result
29
+ end
30
+
31
+ end
32
+
@@ -0,0 +1,14 @@
1
+ class SetHelper
2
+
3
+ # Compare two sets.
4
+ # Returns a hash with keys +:ok+, +:missing+, +:unexpected+.
5
+ def self.compare(expected, actual)
6
+ {
7
+ :missing => expected - actual,
8
+ :unexpected => actual - expected,
9
+ :ok => expected & actual,
10
+ }
11
+ end
12
+
13
+ end
14
+
@@ -0,0 +1,498 @@
1
+ require 'rexml/document'
2
+ require 'minitest/autorun'
3
+ require 'minitest/assertions'
4
+ require 'diff/lcs'
5
+
6
+ require_relative 'verdict_assertion'
7
+
8
+ class MinitestLog
9
+
10
+ include VerdictAssertion
11
+
12
+ attr_accessor \
13
+ :assertions,
14
+ :counts,
15
+ :file,
16
+ :file_path,
17
+ :backtrace_filter,
18
+ :root_name,
19
+ :verdict_ids,
20
+ :xml_indentation,
21
+ :error_verdict,
22
+ :summary
23
+
24
+ include REXML
25
+ include Minitest::Assertions
26
+
27
+ class MinitestLogError < Exception; end
28
+ class NoBlockError < MinitestLogError; end
29
+ class DuplicateVerdictIdError < MinitestLogError; end
30
+ class IllegalElementNameError < MinitestLogError; end
31
+ class IllegalNewError < MinitestLogError; end
32
+
33
+ def initialize(file_path, options=Hash.new)
34
+ raise NoBlockError.new('No block given for MinitestLog#new.') unless (block_given?)
35
+ default_options = Hash[
36
+ :root_name => 'log',
37
+ :xml_indentation => 2,
38
+ :error_verdict => false,
39
+ :summary => false
40
+ ]
41
+ options = default_options.merge(options)
42
+ self.assertions = 0
43
+ self.file_path = file_path
44
+ self.root_name = options[:root_name]
45
+ self.xml_indentation = options[:xml_indentation]
46
+ self.summary = options[:summary]
47
+ self.error_verdict = options[:error_verdict] || false
48
+ self.backtrace_filter = options[:backtrace_filter] || /minitest/
49
+ self.file = File.open(self.file_path, 'w')
50
+ log_puts("REMARK\tThis text log is the precursor for an XML log.")
51
+ log_puts("REMARK\tIf the logged process completes, this text will be converted to XML.")
52
+ log_puts("BEGIN\t#{self.root_name}")
53
+ self.counts = Hash[
54
+ :verdict => 0,
55
+ :failure => 0,
56
+ :error => 0,
57
+ ]
58
+ begin
59
+ yield self
60
+ rescue => x
61
+ put_element('uncaught_exception', :timestamp, :class => x.class) do
62
+ put_element('message', x.message)
63
+ put_element('backtrace') do
64
+ backtrace = filter_backtrace(x.backtrace)
65
+ put_pre(backtrace.join("\n"))
66
+ end
67
+ end
68
+ end
69
+ dispose
70
+ nil
71
+ end
72
+
73
+ def section(name, *args)
74
+ put_element('section', {:name => name}, *args) do
75
+ yield if block_given?
76
+ end
77
+ nil
78
+ end
79
+
80
+ def comment(text)
81
+ if text.match("\n")
82
+ # Separate text from containing punctuation.
83
+ put_element('comment') do
84
+ cdata("\n#{text}\n")
85
+ end
86
+ else
87
+ put_element('comment', text)
88
+ end
89
+ nil
90
+ end
91
+
92
+ def put_element(element_name = 'element', *args)
93
+ if false ||
94
+ caller[0].match(/minitest_log.rb/) ||
95
+ caller[0].match(/verdict_assertion.rb/)
96
+ # Make the element name special.
97
+ element_name += '_'
98
+ elsif element_name.end_with?('_')
99
+ # Don't accept user's special.
100
+ message = "Element name should not end with underscore: #{element_name}"
101
+ raise IllegalElementNameError.new(message)
102
+ else
103
+ # Ok.
104
+ end
105
+ attributes = {}
106
+ pcdata = ''
107
+ start_time = nil
108
+ duration_to_be_included = false
109
+ block_to_be_rescued = false
110
+ args.each do |arg|
111
+ case
112
+ when arg.kind_of?(Hash)
113
+ attributes.merge!(arg)
114
+ when arg.kind_of?(String)
115
+ pcdata += arg
116
+ when arg == :timestamp
117
+ attributes[:timestamp] = MinitestLog.timestamp
118
+ when arg == :duration
119
+ duration_to_be_included = true
120
+ when arg == :rescue
121
+ block_to_be_rescued = true
122
+ else
123
+ pcdata = pcdata + arg.inspect
124
+ end
125
+ end
126
+ log_puts("BEGIN\t#{element_name}")
127
+ put_attributes(attributes)
128
+ unless pcdata.empty?
129
+ # Guard against using a terminator that's a substring of pcdata.
130
+ s = 'EOT'
131
+ terminator = s
132
+ while pcdata.match(terminator) do
133
+ terminator += s
134
+ end
135
+ log_puts("PCDATA\t<<#{terminator}")
136
+ log_puts(pcdata)
137
+ log_puts(terminator)
138
+ end
139
+ start_time = Time.new if duration_to_be_included
140
+ if block_given?
141
+ if block_to_be_rescued
142
+ begin
143
+ yield
144
+ rescue Exception => x
145
+ put_element('rescued_exception', {:class => x.class, :message => x.message}) do
146
+ put_element('backtrace') do
147
+ backtrace = filter_backtrace(x.backtrace)
148
+ put_pre(backtrace.join("\n"))
149
+ end
150
+ end
151
+ self.counts[:error] += 1
152
+ end
153
+ else
154
+ yield
155
+ end
156
+ end
157
+ if start_time
158
+ end_time = Time.now
159
+ duration_f = end_time.to_f - start_time.to_f
160
+ duration_s = format('%.3f', duration_f)
161
+ put_attributes({:duration_seconds => duration_s})
162
+ end
163
+ log_puts("END\t#{element_name}")
164
+ nil
165
+ end
166
+
167
+ def put_each_with_index(name, obj)
168
+ lines = ['']
169
+ obj.each_with_index do |item, i|
170
+ lines.push(format('%d: %s', i, item.to_s))
171
+ end
172
+ attrs = {
173
+ :name => name,
174
+ :class => obj.class,
175
+ :method => ':each_with_index',
176
+ }
177
+ add_attr_if(attrs, obj, :size)
178
+ put_element('data', attrs) do
179
+ put_pre(lines.join("\n"))
180
+ end
181
+ nil
182
+ end
183
+ alias put_array put_each_with_index
184
+ alias put_set put_each_with_index
185
+
186
+ def put_each(name, obj)
187
+ lines = ['']
188
+ obj.each do |item|
189
+ lines.push(item)
190
+ end
191
+ attrs = {
192
+ :name => name,
193
+ :class => obj.class,
194
+ :method => ':each',
195
+ }
196
+ add_attr_if(attrs, obj, :size)
197
+ put_element('data', attrs) do
198
+ put_pre(lines.join("\n"))
199
+ end
200
+ nil
201
+ end
202
+
203
+ def put_each_pair(name, obj)
204
+ lines = ['']
205
+ obj.each_pair do |key, value|
206
+ lines.push(format('%s => %s', key, value))
207
+ end
208
+ attrs = {
209
+ :name => name,
210
+ :class => obj.class,
211
+ :method => ':each_pair',
212
+ }
213
+ add_attr_if(attrs, obj, :size)
214
+ put_element('data', attrs) do
215
+ put_pre(lines.join("\n"))
216
+ end
217
+ nil
218
+ end
219
+ alias put_hash put_each_pair
220
+
221
+ def put_to_s(name, obj)
222
+ put_element('data', obj.to_s, :name => name, :class => obj.class, :method => ':to_s')
223
+ end
224
+
225
+ def put_string(name, obj)
226
+ put_element('data', obj.to_s, :name => name, :class => obj.class, :size => obj.size)
227
+ end
228
+
229
+ def put_inspect(name, obj)
230
+ put_element('data', obj.inspect, :name => name, :class => obj.class, :method => ':inspect')
231
+ end
232
+
233
+ def put_id(name, obj)
234
+ put_element('data', :name => name, :class => obj.class, :id => obj.__id__)
235
+ end
236
+
237
+ def put_data(name, obj)
238
+ case
239
+ when obj.kind_of?(String)
240
+ put_string(name, obj)
241
+ when obj.respond_to?(:each_pair)
242
+ put_each_pair(name, obj)
243
+ when obj.respond_to?(:each_with_index)
244
+ put_each_with_index(name, obj)
245
+ when obj.respond_to?(:each)
246
+ put_each(name, obj)
247
+ when obj.respond_to?(:to_s)
248
+ put_to_s(name, obj)
249
+ when obj.respond_to?(:inspect)
250
+ put_inspect(name, obj)
251
+ when obj.respond_to?(:__id__)
252
+ put_id(name, obj)
253
+ else
254
+ message = "Object does not respond to method :__id__: name=#{name}, obj=#{obj}"
255
+ raise ArgumentError.new(message)
256
+ end
257
+ end
258
+
259
+ def put_pre(text, verbatim = false)
260
+ if verbatim
261
+ put_cdata(text)
262
+ else
263
+ t = text.clone
264
+ until t.start_with?("\n")
265
+ t = "\n" + t
266
+ end
267
+ until t.end_with?("\n\n")
268
+ t = t + "\n"
269
+ end
270
+ put_cdata(t)
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ def dispose
277
+
278
+ # Add a verdict for the error count, if needed.
279
+ if self.error_verdict
280
+ verdict_assert_equal?('error_count', 0, self.counts[:error])
281
+ end
282
+
283
+ # Close the text log.
284
+ log_puts("END\t#{self.root_name}")
285
+ self.file.close
286
+
287
+ # Create the xml log.
288
+ document = REXML::Document.new
289
+ File.open(self.file_path, 'r') do |file|
290
+ element = document
291
+ stack = Array.new
292
+ data_a = Array.new
293
+ terminator = nil
294
+ file.each_line do |line|
295
+ line.chomp!
296
+ line_type, text = line.split("\t", 2)
297
+ case line_type
298
+ when 'REMARK'
299
+ next
300
+ when 'BEGIN'
301
+ element_name = text
302
+ element = element.add_element(element_name)
303
+ stack.push(element)
304
+ if stack.length == 1 && self.summary
305
+ summary_element = element.add_element('summary_')
306
+ summary_element.add_attribute('verdicts', self.counts[:verdict].to_s)
307
+ summary_element.add_attribute('failures', self.counts[:failure].to_s)
308
+ summary_element.add_attribute('errors', self.counts[:error].to_s)
309
+ end
310
+ when 'END'
311
+ stack.pop
312
+ element = stack.last
313
+ when 'ATTRIBUTE'
314
+ attr_name, attr_value = text.split("\t", 2)
315
+ element.add_attribute(attr_name, attr_value)
316
+ when 'CDATA'
317
+ stack.push(:cdata)
318
+ data_a = Array.new
319
+ terminator = text.split('<<', 2).last
320
+ when 'PCDATA'
321
+ stack.push(:pcdata)
322
+ data_a = Array.new
323
+ terminator = text.split('<<', 2).last
324
+ when terminator
325
+ data_s = data_a.join("\n")
326
+ data_a = Array.new
327
+ terminator = nil
328
+ data_type = stack.last
329
+ case data_type
330
+ when :cdata
331
+ cdata = CData.new(data_s)
332
+ element.add(cdata)
333
+ when :pcdata
334
+ element.add_text(data_s)
335
+ else
336
+ # Don't want to raise an exception and spoil the run
337
+ end
338
+ stack.pop
339
+ else
340
+ data_a.push(line) if (terminator)
341
+ end
342
+ end
343
+ document << XMLDecl.default
344
+ end
345
+
346
+ File.open(self.file_path, 'w') do |file|
347
+ document.write(file, self.xml_indentation)
348
+ end
349
+ # Trailing newline.
350
+ File.open(self.file_path, 'a') do |file|
351
+ file.write("\n")
352
+ end
353
+ nil
354
+ end
355
+
356
+ def _get_verdict?(verdict_method, verdict_id, message, args_hash)
357
+ assertion_method = assertion_method_for(verdict_method)
358
+ if block_given?
359
+ outcome, exception = get_assertion_outcome(verdict_id, assertion_method, *args_hash.values) do
360
+ yield
361
+ end
362
+ else
363
+ outcome, exception = get_assertion_outcome(verdict_id, assertion_method, *args_hash.values)
364
+ end
365
+ element_attributes = {
366
+ :method => verdict_method,
367
+ :outcome => outcome,
368
+ :id => verdict_id,
369
+ }
370
+ element_attributes.store(:message, message) unless message.nil?
371
+ put_element('verdict', element_attributes) do
372
+ args_hash.each_pair do |k, v|
373
+ put_element(k.to_s, {:class => v.class, :value => v.inspect})
374
+ end
375
+ if exception
376
+ self.counts[:failure] += 1
377
+ # If the encoding is not UTF-8, a string will have been added.
378
+ # Remove it, so that the message is the same on all platforms.
379
+ conditioned_message = exception.message.gsub("# encoding: UTF-8\n", '')
380
+ put_element('exception', {:class => exception.class, :message => conditioned_message}) do
381
+ put_element('backtrace') do
382
+ backtrace = filter_backtrace(exception.backtrace)
383
+ put_pre(backtrace.join("\n"))
384
+ end
385
+ end
386
+ end
387
+ end
388
+ outcome == :passed
389
+ end
390
+
391
+ def put_attributes(attributes)
392
+ attributes.each_pair do |name, value|
393
+ value = case
394
+ when value.is_a?(String)
395
+ value
396
+ when value.is_a?(Symbol)
397
+ value.to_s
398
+ else
399
+ value.inspect
400
+ end
401
+ log_puts("ATTRIBUTE\t#{name}\t#{value}")
402
+ end
403
+ nil
404
+ end
405
+
406
+ def log_puts(text)
407
+ self.file.puts(text)
408
+ self.file.flush
409
+ nil
410
+ end
411
+
412
+ def validate_verdict_id(verdict_id)
413
+ self.verdict_ids ||= Set.new
414
+ if self.verdict_ids.include?(verdict_id)
415
+ message = format('Duplicate verdict id %s; must be unique within its test method', verdict_id.inspect)
416
+ raise DuplicateVerdictIdError.new(message)
417
+ end
418
+ self.verdict_ids.add(verdict_id)
419
+ nil
420
+ end
421
+
422
+ def put_cdata(text)
423
+ # Guard against using a terminator that's a substring of the cdata.
424
+ s = 'EOT'
425
+ terminator = s
426
+ while text.match(terminator) do
427
+ terminator += s
428
+ end
429
+ log_puts("CDATA\t<<#{terminator}")
430
+ log_puts(text)
431
+ log_puts(terminator)
432
+ nil
433
+ end
434
+
435
+ def get_assertion_outcome(verdict_id, assertion_method, *assertion_args)
436
+ validate_verdict_id(verdict_id)
437
+ self.counts[:verdict] += 1
438
+ begin
439
+ if block_given?
440
+ send(assertion_method, *assertion_args) do
441
+ yield
442
+ end
443
+ else
444
+ send(assertion_method, *assertion_args)
445
+ end
446
+ return :passed, nil
447
+ rescue Minitest::Assertion => x
448
+ return :failed, x
449
+ end
450
+ end
451
+
452
+ # Filters lines that are from ruby or log, to make the backtrace more readable.
453
+ def filter_backtrace(lines)
454
+ filtered = []
455
+ lines.each do |line|
456
+ unless line.match(self.backtrace_filter)
457
+ filtered.push(line)
458
+ end
459
+ end
460
+ filtered
461
+ end
462
+
463
+ # Return a timestamp string.
464
+ # The important property of this string
465
+ # is that it can be incorporated into a legal directory path
466
+ # (i.e., has no colons, etc.).
467
+ def self.timestamp
468
+ now = Time.now
469
+ ts = now.strftime('%Y-%m-%d-%a-%H.%M.%S')
470
+ usec_s = (now.usec / 1000).to_s
471
+ while usec_s.length < 3 do
472
+ usec_s = '0' + usec_s
473
+ end
474
+ # noinspection RubyUnusedLocalVariable
475
+ ts += ".#{usec_s}"
476
+ end
477
+
478
+ def assertion_method_for(verdict_method)
479
+ # Our verdict method name is just an assertion method name
480
+ # with prefixed 'verdict_' and suffixed '?'.
481
+ # Just remove them to form the assertion method name.
482
+ verdict_method.to_s.sub('verdict_', '').sub('?', '').to_sym
483
+ end
484
+
485
+ def add_attr_if(attrs, obj, method)
486
+ return unless obj.respond_to?(method)
487
+ attrs[method] = obj.send(method)
488
+ end
489
+
490
+ def self.parse(file_path)
491
+ document = nil
492
+ File.open(file_path) do |file|
493
+ document = REXML::Document.new(file)
494
+ end
495
+ document
496
+ end
497
+
498
+ end