junit_merge 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/CHANGELOG +3 -0
- data/Gemfile +9 -0
- data/LICENSE +20 -0
- data/README.markdown +94 -0
- data/Rakefile +1 -0
- data/bin/junit_merge +5 -0
- data/junit_merge.gemspec +19 -0
- data/lib/junit_merge.rb +4 -0
- data/lib/junit_merge/app.rb +133 -0
- data/lib/junit_merge/version.rb +11 -0
- data/test/junit_merge/test_app.rb +194 -0
- data/test/template.xml.erb +20 -0
- data/test/test_helper.rb +8 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 169f38c3ea35ae927f10d041fbad8900d4f4a6f2
|
4
|
+
data.tar.gz: 4a030a4641249fb436e1743f9d94af9c531e3a30
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba9ad5426d4470db02143255d8417e3b78d870be9b31b3334fbb89e77e0a19967c8fdfe7c03f853d1bee02cd083f4a4a86a07cc9cbc967c6f074998bfcd2235d
|
7
|
+
data.tar.gz: 8ed014fb7239a6964fa45804ac453062613d7af00c7ddbd1679b6ca1dad872164c2a9fca2d5fcc6f9fc4809c642ce984185d594b319739b8c3e793b2b59effa8
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) George Ogata
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
## JUnit Merge
|
2
|
+
|
3
|
+
Merges two JUnit XML reports, such that results from one run may override those
|
4
|
+
in the other. Reports may be single files or directory trees.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
Install:
|
9
|
+
|
10
|
+
gem install junit_merge
|
11
|
+
|
12
|
+
Run:
|
13
|
+
|
14
|
+
junit_merge SOURCE.xml TARGET.xml
|
15
|
+
|
16
|
+
Test results in SOURCE.xml will overwrite their counterparts in
|
17
|
+
TARGET.xml. Summary statistics will be updated as necessary.
|
18
|
+
|
19
|
+
## Why?
|
20
|
+
|
21
|
+
The intended use case is rerunning failures in CI.
|
22
|
+
|
23
|
+
Of course, your test suite *should* pass 100% of the time, be free from
|
24
|
+
nondeterminism, never modify global state, not rely on external services, and
|
25
|
+
all those good things.
|
26
|
+
|
27
|
+
But this is real life.
|
28
|
+
|
29
|
+
Sometimes you don't have a spare week to diagnose intermittent failures plaguing
|
30
|
+
your build. Or perhaps you're dealing with a legacy suite. Or you're relying on
|
31
|
+
tools which offer no synchronization mechanisms, making you resort to sleeps
|
32
|
+
which don't always suffice on a cheap, underpowered CI box. Or you're dealing
|
33
|
+
with an integration suite that legitmately hits some external service over a
|
34
|
+
flaky network connection.
|
35
|
+
|
36
|
+
This one's for you poor buggers. :beer:
|
37
|
+
|
38
|
+
## Example
|
39
|
+
|
40
|
+
Here's an example of how to set up an [RSpec][rspec] suite under
|
41
|
+
[Jenkins][jenkins].
|
42
|
+
|
43
|
+
First, we need to output the results to a file in JUnit format.
|
44
|
+
|
45
|
+
rspec --format progress --format RspecJunitFormatter --out reports/rspec.xml spec
|
46
|
+
|
47
|
+
Next, we need to add options to dump the failed examples to a file. An easy way
|
48
|
+
is using [respec][respec]: simply change `rspec` to `respec`. Another option
|
49
|
+
is to use the failures logger in [parallel_tests][parallel-tests].
|
50
|
+
|
51
|
+
respec --format progress --format RspecJunitFormatter --out reports/rspec.xml spec
|
52
|
+
|
53
|
+
Now, if the first build returns non-zero, we'll need to run just the
|
54
|
+
failures. With respec, we can use the `f` specifier. We should also output the
|
55
|
+
junit report to a different location:
|
56
|
+
|
57
|
+
respec --format progress --format RspecJunitFormatter --out reports/rspec-rerun.xml f
|
58
|
+
|
59
|
+
Finally, if the rerun was required, we can merge the rerun results into the
|
60
|
+
original results:
|
61
|
+
|
62
|
+
junit_merge reports/rspec-rerun.xml reports/rspec.xml
|
63
|
+
|
64
|
+
Putting it all together:
|
65
|
+
|
66
|
+
#!/bin/sh -x
|
67
|
+
|
68
|
+
status=0
|
69
|
+
if ! respec --format progress --format RspecJunitFormatter --out reports/rspec.xml spec; then
|
70
|
+
respec --format progress --format RspecJunitFormatter --out reports/rspec-rerun.xml f
|
71
|
+
status=$?
|
72
|
+
junit_merge reports/rspec-rerun.xml reports/rspec.xml
|
73
|
+
fi
|
74
|
+
exit $status
|
75
|
+
|
76
|
+
Note that if you don't specify the shebang, Jenkins will run your shell with
|
77
|
+
`-ex`, which will stop execution after the first build failure.
|
78
|
+
|
79
|
+
[rspec]: https://github.com/rspec/rspec
|
80
|
+
[jenkins]: http://jenkins-ci.org/
|
81
|
+
[respec]: https://github.com/oggy/respec
|
82
|
+
[parallel-tests]: https://github.com/grosser/parallel_tests
|
83
|
+
|
84
|
+
## Contributing
|
85
|
+
|
86
|
+
* [Bug reports](https://github.com/oggy/junit_merge/issues)
|
87
|
+
* [Source](https://github.com/oggy/junit_merge)
|
88
|
+
* Patches: Fork on Github, send pull request.
|
89
|
+
* Include tests where practical.
|
90
|
+
* Leave the version alone, or bump it in a separate commit.
|
91
|
+
|
92
|
+
## Copyright
|
93
|
+
|
94
|
+
Copyright (c) George Ogata. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ritual'
|
data/bin/junit_merge
ADDED
data/junit_merge.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.unshift File.expand_path('lib', File.dirname(__FILE__))
|
2
|
+
require 'junit_merge/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'junit_merge'
|
6
|
+
gem.version = JunitMerge::VERSION
|
7
|
+
gem.authors = ['George Ogata']
|
8
|
+
gem.email = ['george.ogata@gmail.com']
|
9
|
+
gem.description = "Tool to merge JUnit XML reports."
|
10
|
+
gem.summary = ""
|
11
|
+
gem.homepage = ''
|
12
|
+
|
13
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
|
17
|
+
gem.add_runtime_dependency 'nokogiri', '>= 1.5', '< 2.0'
|
18
|
+
gem.add_development_dependency 'ritual', '~> 0.4'
|
19
|
+
end
|
data/lib/junit_merge.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'find'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module JunitMerge
|
6
|
+
class App
|
7
|
+
Error = Class.new(RuntimeError)
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
@stdin = options[:stdin ] || STDIN
|
11
|
+
@stdout = options[:stdout] || STDOUT
|
12
|
+
@stderr = options[:stderr] || STDERR
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :stdin, :stdout, :stderr
|
16
|
+
|
17
|
+
def run(*args)
|
18
|
+
source_path, target_path = parse_args(args)
|
19
|
+
|
20
|
+
not_found = [source_path, target_path].select { |path| !File.exist?(path) }
|
21
|
+
not_found.empty? or
|
22
|
+
raise Error, "no such file: #{not_found.join(', ')}"
|
23
|
+
|
24
|
+
if File.directory?(source_path)
|
25
|
+
Find.find(source_path) do |source_file_path|
|
26
|
+
next if !File.file?(source_file_path)
|
27
|
+
target_file_path = source_file_path.sub(source_path, target_path)
|
28
|
+
if File.exist?(target_file_path)
|
29
|
+
merge_file(source_file_path, target_file_path)
|
30
|
+
else
|
31
|
+
FileUtils.mkdir_p(File.dirname(target_file_path))
|
32
|
+
FileUtils.cp(source_file_path, target_file_path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
elsif File.exist?(source_path)
|
36
|
+
merge_file(source_path, target_path)
|
37
|
+
else
|
38
|
+
raise Error, "no such file: #{source_path}"
|
39
|
+
end
|
40
|
+
0
|
41
|
+
rescue Error => error
|
42
|
+
stderr.puts error.message
|
43
|
+
1
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def merge_file(source_path, target_path)
|
49
|
+
source_text = File.read(source_path)
|
50
|
+
target_text = File.read(target_path)
|
51
|
+
|
52
|
+
if target_text =~ /\A\s*\z/m
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
if source_text =~ /\A\s*\z/m
|
57
|
+
FileUtils.cp source_path, target_path
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
source = Nokogiri::XML::Document.parse(source_text)
|
62
|
+
target = Nokogiri::XML::Document.parse(target_text)
|
63
|
+
|
64
|
+
source.xpath("//testsuite/testcase").each do |node|
|
65
|
+
summary_diff = SummaryDiff.new
|
66
|
+
summary_diff.add(node, 1)
|
67
|
+
|
68
|
+
predicates = [
|
69
|
+
attribute_predicate('classname', node['classname']),
|
70
|
+
attribute_predicate('name', node['name']),
|
71
|
+
].join(' and ')
|
72
|
+
original = target.xpath("testsuite/testcase[#{predicates}]").first
|
73
|
+
|
74
|
+
if original
|
75
|
+
summary_diff.add(original, -1)
|
76
|
+
original.replace(node)
|
77
|
+
else
|
78
|
+
testsuite = target.xpath("testsuite").first
|
79
|
+
testsuite.add_child(node)
|
80
|
+
end
|
81
|
+
|
82
|
+
node.ancestors.select { |a| a.name =~ /\Atestsuite?\z/ }.each do |suite|
|
83
|
+
summary_diff.apply_to(suite)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
open(target_path, 'w') { |f| f.write(target.to_s) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def attribute_predicate(name, value)
|
91
|
+
# XPath doesn't let you escape the delimiting quotes. Need concat() here
|
92
|
+
# to support the general case.
|
93
|
+
escaped = value.to_s.gsub('"', '", \'"\', "')
|
94
|
+
"@#{name}=concat('', \"#{escaped}\")"
|
95
|
+
end
|
96
|
+
|
97
|
+
def apply_summary_diff(diff, node)
|
98
|
+
summary_diff.each do |key, delta|
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
SummaryDiff = Struct.new(:tests, :failures, :errors, :skipped) do
|
103
|
+
def initialize
|
104
|
+
self.tests = self.failures = self.errors = self.skipped = 0
|
105
|
+
end
|
106
|
+
|
107
|
+
def add(test_node, delta)
|
108
|
+
self.tests += delta
|
109
|
+
self.failures += delta if !test_node.xpath('failure').empty?
|
110
|
+
self.errors += delta if !test_node.xpath('error').empty?
|
111
|
+
self.skipped += delta if !test_node.xpath('skipped').empty?
|
112
|
+
end
|
113
|
+
|
114
|
+
def apply_to(node)
|
115
|
+
%w[tests failures errors skipped].each do |attribute|
|
116
|
+
if (value = node[attribute])
|
117
|
+
node[attribute] = value.to_i + send(attribute)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_args(args)
|
124
|
+
args.size == 2 or
|
125
|
+
raise Error, usage
|
126
|
+
args
|
127
|
+
end
|
128
|
+
|
129
|
+
def usage
|
130
|
+
"USAGE: #$0 SOURCE TARGET"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
describe JunitMerge::App do
|
5
|
+
TEMPLATE = File.read("#{ROOT}/test/template.xml.erb")
|
6
|
+
|
7
|
+
use_temporary_directory "#{ROOT}/test/tmp"
|
8
|
+
|
9
|
+
def create_file(path, tests)
|
10
|
+
num_tests = tests.size
|
11
|
+
num_failures = tests.values.count(:fail)
|
12
|
+
num_errors = tests.values.count(:error)
|
13
|
+
num_skipped = tests.values.count(:skipped)
|
14
|
+
|
15
|
+
FileUtils.mkdir_p File.dirname(path)
|
16
|
+
open(path, 'w') do |file|
|
17
|
+
file.puts ERB.new(TEMPLATE).result(binding)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_directory(path)
|
22
|
+
FileUtils.mkdir_p path
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_file(path)
|
26
|
+
Nokogiri::XML::Document.parse(File.read(path))
|
27
|
+
end
|
28
|
+
|
29
|
+
def results(node)
|
30
|
+
results = []
|
31
|
+
node.xpath('//testcase').each do |testcase_node|
|
32
|
+
if !testcase_node.xpath('failure').empty?
|
33
|
+
result = :fail
|
34
|
+
elsif !testcase_node.xpath('error').empty?
|
35
|
+
result = :error
|
36
|
+
elsif !testcase_node.xpath('skipped').empty?
|
37
|
+
result = :skipped
|
38
|
+
else
|
39
|
+
result = :pass
|
40
|
+
end
|
41
|
+
class_name = testcase_node['classname']
|
42
|
+
test_name = testcase_node['name']
|
43
|
+
results << ["#{class_name}.#{test_name}", result]
|
44
|
+
end
|
45
|
+
results
|
46
|
+
end
|
47
|
+
|
48
|
+
def summaries(node)
|
49
|
+
summaries = []
|
50
|
+
node.xpath('//testsuite | //testsuites').each do |node|
|
51
|
+
summary = {}
|
52
|
+
%w[tests failures errors skipped].each do |attribute|
|
53
|
+
if (value = node[attribute])
|
54
|
+
summary[attribute.to_sym] = Integer(value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
summaries << summary
|
58
|
+
end
|
59
|
+
summaries
|
60
|
+
end
|
61
|
+
|
62
|
+
let(:stdin ) { StringIO.new }
|
63
|
+
let(:stdout) { StringIO.new }
|
64
|
+
let(:stderr) { StringIO.new }
|
65
|
+
let(:app) { JunitMerge::App.new(stdin: stdin, stdout: stdout, stderr: stderr) }
|
66
|
+
|
67
|
+
describe "when merging files" do
|
68
|
+
it "merges results" do
|
69
|
+
create_file("#{tmp}/source.xml", 'a.a' => :pass, 'a.b' => :fail, 'a.c' => :error, 'a.d' => :skipped)
|
70
|
+
create_file("#{tmp}/target.xml", 'a.a' => :fail, 'a.b' => :error, 'a.c' => :skipped, 'a.d' => :pass)
|
71
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
72
|
+
document = parse_file("#{tmp}/target.xml")
|
73
|
+
results(document).must_equal([['a.a', :pass], ['a.b', :fail], ['a.c', :error], ['a.d', :skipped]])
|
74
|
+
stdout.string.must_equal('')
|
75
|
+
stderr.string.must_equal('')
|
76
|
+
end
|
77
|
+
|
78
|
+
it "updates summaries" do
|
79
|
+
create_file("#{tmp}/source.xml", 'a.a' => :pass, 'a.b' => :skipped, 'a.c' => :fail)
|
80
|
+
create_file("#{tmp}/target.xml", 'a.a' => :fail, 'a.b' => :error, 'a.c' => :fail)
|
81
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
82
|
+
document = parse_file("#{tmp}/target.xml")
|
83
|
+
summaries(document).must_equal([{tests: 3, failures: 1, errors: 0, skipped: 1}])
|
84
|
+
stdout.string.must_equal('')
|
85
|
+
stderr.string.must_equal('')
|
86
|
+
end
|
87
|
+
|
88
|
+
it "does not modify nodes only in the target" do
|
89
|
+
create_file("#{tmp}/source.xml", 'a.b' => :pass)
|
90
|
+
create_file("#{tmp}/target.xml", 'a.a' => :pass, 'a.b' => :fail)
|
91
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
92
|
+
document = parse_file("#{tmp}/target.xml")
|
93
|
+
results(document).must_equal([['a.a', :pass], ['a.b', :pass]])
|
94
|
+
stdout.string.must_equal('')
|
95
|
+
stderr.string.must_equal('')
|
96
|
+
end
|
97
|
+
|
98
|
+
it "appends nodes only in the source" do
|
99
|
+
create_file("#{tmp}/source.xml", 'a.a' => :fail, 'a.b' => :error)
|
100
|
+
create_file("#{tmp}/target.xml", 'a.a' => :pass)
|
101
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
102
|
+
document = parse_file("#{tmp}/target.xml")
|
103
|
+
results(document).must_equal([['a.a', :fail], ['a.b', :error]])
|
104
|
+
summaries(document).must_equal([{tests: 2, failures: 1, errors: 1, skipped: 0}])
|
105
|
+
stdout.string.must_equal('')
|
106
|
+
stderr.string.must_equal('')
|
107
|
+
end
|
108
|
+
|
109
|
+
it "correctly merges tests with metacharacters in the name" do
|
110
|
+
create_file("#{tmp}/source.xml", 'a\'"a.b"\'b' => :pass)
|
111
|
+
create_file("#{tmp}/target.xml", 'a\'"a.b"\'b' => :fail)
|
112
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
113
|
+
document = parse_file("#{tmp}/target.xml")
|
114
|
+
results(document).must_equal([['a\'"a.b"\'b', :pass]])
|
115
|
+
summaries(document).must_equal([{tests: 1, failures: 0, errors: 0, skipped: 0}])
|
116
|
+
stdout.string.must_equal('')
|
117
|
+
stderr.string.must_equal('')
|
118
|
+
end
|
119
|
+
|
120
|
+
it "correctly merges tests with the same name in different classes" do
|
121
|
+
create_file("#{tmp}/source.xml", 'a.a' => :pass, 'b.a' => :fail)
|
122
|
+
create_file("#{tmp}/target.xml", 'a.a' => :fail, 'b.a' => :pass)
|
123
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
124
|
+
document = parse_file("#{tmp}/target.xml")
|
125
|
+
results(document).must_equal([['a.a', :pass], ['b.a', :fail]])
|
126
|
+
stdout.string.must_equal('')
|
127
|
+
stderr.string.must_equal('')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "when merging directories" do
|
132
|
+
it "updates target files from each file in the source directory" do
|
133
|
+
create_file("#{tmp}/source/a.xml", 'a.a' => :pass, 'a.b' => :fail)
|
134
|
+
create_file("#{tmp}/target/a.xml", 'a.a' => :fail, 'a.b' => :pass)
|
135
|
+
app.run("#{tmp}/source", "#{tmp}/target").must_equal 0
|
136
|
+
document = parse_file("#{tmp}/target/a.xml")
|
137
|
+
results(document).must_equal([['a.a', :pass], ['a.b', :fail]])
|
138
|
+
stdout.string.must_equal('')
|
139
|
+
stderr.string.must_equal('')
|
140
|
+
end
|
141
|
+
|
142
|
+
it "does not modify files only in the target" do
|
143
|
+
FileUtils.mkdir "#{tmp}/source"
|
144
|
+
create_file("#{tmp}/target/a.xml", 'a.a' => :fail, 'a.b' => :pass)
|
145
|
+
app.run("#{tmp}/source", "#{tmp}/target").must_equal 0
|
146
|
+
document = parse_file("#{tmp}/target/a.xml")
|
147
|
+
results(document).must_equal([['a.a', :fail], ['a.b', :pass]])
|
148
|
+
stdout.string.must_equal('')
|
149
|
+
stderr.string.must_equal('')
|
150
|
+
end
|
151
|
+
|
152
|
+
it "adds files only in the source" do
|
153
|
+
create_file("#{tmp}/source/a.xml", 'a.a' => :fail, 'a.b' => :pass)
|
154
|
+
create_directory("#{tmp}/target")
|
155
|
+
app.run("#{tmp}/source", "#{tmp}/target").must_equal 0
|
156
|
+
document = parse_file("#{tmp}/target/a.xml")
|
157
|
+
results(document).must_equal([['a.a', :fail], ['a.b', :pass]])
|
158
|
+
stdout.string.must_equal('')
|
159
|
+
stderr.string.must_equal('')
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
it "does not complain about empty files" do
|
164
|
+
FileUtils.touch "#{tmp}/source.xml"
|
165
|
+
FileUtils.touch "#{tmp}/target.xml"
|
166
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0
|
167
|
+
File.read("#{tmp}/target.xml").must_equal('')
|
168
|
+
stdout.string.must_equal('')
|
169
|
+
stderr.string.must_equal('')
|
170
|
+
end
|
171
|
+
|
172
|
+
it "whines if the source does not exist" do
|
173
|
+
FileUtils.touch "#{tmp}/target.xml"
|
174
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 1
|
175
|
+
File.read("#{tmp}/target.xml").must_equal('')
|
176
|
+
stdout.string.must_equal('')
|
177
|
+
stderr.string.must_match /no such file/
|
178
|
+
end
|
179
|
+
|
180
|
+
it "whines if the target does not exist" do
|
181
|
+
FileUtils.touch "#{tmp}/source.xml"
|
182
|
+
app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 1
|
183
|
+
stdout.string.must_equal('')
|
184
|
+
stderr.string.must_match /no such file/
|
185
|
+
end
|
186
|
+
|
187
|
+
it "errors with a usage message if 2 args aren't given" do
|
188
|
+
FileUtils.touch "#{tmp}/source.xml"
|
189
|
+
app.run("#{tmp}/source.xml").must_equal 1
|
190
|
+
File.read("#{tmp}/source.xml").must_equal('')
|
191
|
+
stdout.string.must_equal('')
|
192
|
+
stderr.string.must_match /USAGE/
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<testsuite tests="<%= num_tests %>" failures="<%= num_failures %>" errors="<%= num_errors %>" skipped="<%= num_skipped %>" time="0.000001" timestamp="2014-04-10T14:29:06-04:00">
|
3
|
+
<properties/>
|
4
|
+
<% tests.each do |name, result| %>
|
5
|
+
<% class_name, test_name = name.gsub('"', '"').split('.') %>
|
6
|
+
<testcase classname="<%= class_name %>" name="<%= test_name %>" time="0.000001">
|
7
|
+
<% if result == :fail %>
|
8
|
+
<failure message="MESSAGE" type="TYPE">
|
9
|
+
<![CDATA[BODY]]>
|
10
|
+
</failure>
|
11
|
+
<% elsif result == :error %>
|
12
|
+
<error message="MESSAGE" type="TYPE">
|
13
|
+
<![CDATA[BODY]]>
|
14
|
+
</error>
|
15
|
+
<% elsif result == :skipped %>
|
16
|
+
<skipped/>
|
17
|
+
<% end %>
|
18
|
+
</testcase>
|
19
|
+
<% end %>
|
20
|
+
</testsuite>
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: junit_merge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- George Ogata
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-04-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.5'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: ritual
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.4'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.4'
|
47
|
+
description: Tool to merge JUnit XML reports.
|
48
|
+
email:
|
49
|
+
- george.ogata@gmail.com
|
50
|
+
executables:
|
51
|
+
- junit_merge
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- ".gitignore"
|
56
|
+
- CHANGELOG
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE
|
59
|
+
- README.markdown
|
60
|
+
- Rakefile
|
61
|
+
- bin/junit_merge
|
62
|
+
- junit_merge.gemspec
|
63
|
+
- lib/junit_merge.rb
|
64
|
+
- lib/junit_merge/app.rb
|
65
|
+
- lib/junit_merge/version.rb
|
66
|
+
- test/junit_merge/test_app.rb
|
67
|
+
- test/template.xml.erb
|
68
|
+
- test/test_helper.rb
|
69
|
+
homepage: ''
|
70
|
+
licenses: []
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.2.2
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: ''
|
92
|
+
test_files:
|
93
|
+
- test/junit_merge/test_app.rb
|
94
|
+
- test/template.xml.erb
|
95
|
+
- test/test_helper.rb
|