junit_merge 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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