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