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 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
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2014-04-15
2
+
3
+ * Hi.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'temporaries'
5
+
6
+ group :dev do
7
+ gem 'byebug'
8
+ gem 'looksee'
9
+ end
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'junit_merge/app'
4
+
5
+ exit JunitMerge::App.new.run(*ARGV)
@@ -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
@@ -0,0 +1,4 @@
1
+ module JunitMerge
2
+ autoload :App, 'junit_merge/app'
3
+ autoload :VERSION, 'junit_merge/version'
4
+ end
@@ -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,11 @@
1
+ module JunitMerge
2
+ VERSION = [0, 0, 1]
3
+
4
+ class << VERSION
5
+ include Comparable
6
+
7
+ def to_s
8
+ join('.')
9
+ end
10
+ end
11
+ 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('"', '&quot;').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>
@@ -0,0 +1,8 @@
1
+ ROOT = File.expand_path('..', File.dirname(__FILE__))
2
+ $:.unshift "#{ROOT}/lib"
3
+
4
+ require 'junit_merge'
5
+ require 'minitest/spec'
6
+ require 'temporaries'
7
+ require 'byebug'
8
+ require 'looksee'
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