├── .gitignore ├── CHANGELOG ├── Gemfile ├── LICENSE ├── README.markdown ├── Rakefile ├── bin └── junit_merge ├── junit_merge.gemspec ├── lib ├── junit_merge.rb └── junit_merge │ ├── app.rb │ └── version.rb └── test ├── junit_merge └── test_app.rb ├── template.xml.erb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | == 0.1.2 2014-05-25 2 | 3 | * Invalid UTF-8 does not crash junit_merge. 4 | 5 | == 0.1.1 2014-04-18 6 | 7 | * Merging directories with --update-only shouldn't add files only in the source. 8 | 9 | == 0.1.0 2014-04-18 10 | 11 | * Allow passing any number of source files. 12 | * Add --update-only option, to not append new tests in the source. 13 | 14 | == 0.0.1 2014-04-15 15 | 16 | * Hi. 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'temporaries' 5 | 6 | group :dev do 7 | gem 'byebug' 8 | gem 'looksee' 9 | end 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## JUnit Merge 2 | 3 | Merges two or more JUnit XML reports, such that results from one run may 4 | override those 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 SOURCE1.xml SOURCE2.xml ... TARGET.xml 15 | 16 | Test results in SOURCE[1..n].xml will overwrite their counterparts in 17 | TARGET.xml. Summary statistics will be updated as necessary. The sources and 18 | target may be directories -- files at the same relative paths under each will be 19 | merged (recursively). 20 | 21 | ## Why? 22 | 23 | The intended use case is rerunning failures in CI. 24 | 25 | Of course, your test suite *should* pass 100% of the time, be free from 26 | nondeterminism, never modify global state, not rely on external services, and 27 | all those good things. 28 | 29 | But this is real life. 30 | 31 | Sometimes you don't have a spare week to diagnose intermittent failures plaguing 32 | your build. Or perhaps you're dealing with a legacy suite. Or you're relying on 33 | tools which offer no synchronization mechanisms, making you resort to sleeps 34 | which don't always suffice on a cheap, underpowered CI box. Or you're dealing 35 | with an integration suite that legitmately hits some external service over a 36 | flaky network connection. 37 | 38 | This one's for you poor buggers. :beer: 39 | 40 | ## Example 41 | 42 | Here's an example of how to set up an [RSpec][rspec] suite under 43 | [Jenkins][jenkins]. 44 | 45 | First, we need to output the results to a file in JUnit format. 46 | 47 | rspec --format progress --format RspecJunitFormatter --out reports/rspec.xml spec 48 | 49 | Next, we need to add options to dump the failed examples to a file. An easy way 50 | is using [respec][respec]: simply change `rspec` to `respec`. Another option 51 | is to use the failures logger in [parallel_tests][parallel-tests]. 52 | 53 | respec --format progress --format RspecJunitFormatter --out reports/rspec.xml spec 54 | 55 | Now, if the first build returns non-zero, we'll need to run just the 56 | failures. With respec, we can use the `f` specifier. We should also output the 57 | junit report to a different location: 58 | 59 | respec --format progress --format RspecJunitFormatter --out reports/rspec-rerun.xml f 60 | 61 | Finally, if the rerun was required, we can merge the rerun results into the 62 | original results: 63 | 64 | junit_merge reports/rspec-rerun.xml reports/rspec.xml 65 | 66 | Putting it all together: 67 | 68 | #!/bin/sh -x 69 | 70 | status=0 71 | if ! respec --format progress --format RspecJunitFormatter --out reports/rspec.xml spec; then 72 | respec --format progress --format RspecJunitFormatter --out reports/rspec-rerun.xml f 73 | status=$? 74 | junit_merge reports/rspec-rerun.xml reports/rspec.xml 75 | fi 76 | exit $status 77 | 78 | Note that if you don't specify the shebang, Jenkins will run your shell with 79 | `-ex`, which will stop execution after the first build failure. 80 | 81 | [rspec]: https://github.com/rspec/rspec 82 | [jenkins]: http://jenkins-ci.org/ 83 | [respec]: https://github.com/oggy/respec 84 | [parallel-tests]: https://github.com/grosser/parallel_tests 85 | 86 | ## Contributing 87 | 88 | * [Bug reports](https://github.com/oggy/junit_merge/issues) 89 | * [Source](https://github.com/oggy/junit_merge) 90 | * Patches: Fork on Github, send pull request. 91 | * Include tests where practical. 92 | * Leave the version alone, or bump it in a separate commit. 93 | 94 | ## Copyright 95 | 96 | Copyright (c) George Ogata. See LICENSE for details. 97 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'ritual' 2 | -------------------------------------------------------------------------------- /bin/junit_merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'junit_merge/app' 4 | 5 | exit JunitMerge::App.new.run(*ARGV) 6 | -------------------------------------------------------------------------------- /junit_merge.gemspec: -------------------------------------------------------------------------------- 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 = 'https://github.com/oggy/junit_merge' 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 20 | -------------------------------------------------------------------------------- /lib/junit_merge.rb: -------------------------------------------------------------------------------- 1 | module JunitMerge 2 | autoload :App, 'junit_merge/app' 3 | autoload :VERSION, 'junit_merge/version' 4 | end 5 | -------------------------------------------------------------------------------- /lib/junit_merge/app.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'find' 3 | require 'fileutils' 4 | require 'nokogiri' 5 | 6 | module JunitMerge 7 | class App 8 | Error = Class.new(RuntimeError) 9 | 10 | def initialize(options={}) 11 | @stdin = options[:stdin ] || STDIN 12 | @stdout = options[:stdout] || STDOUT 13 | @stderr = options[:stderr] || STDERR 14 | @update_only = false 15 | end 16 | 17 | attr_reader :stdin, :stdout, :stderr 18 | 19 | def run(*args) 20 | *source_paths, target_path = parse_args(args) 21 | all_paths = [*source_paths, target_path] 22 | 23 | not_found = all_paths.select { |path| !File.exist?(path) } 24 | not_found.empty? or 25 | raise Error, "no such file(s): #{not_found.join(', ')}" 26 | 27 | if source_paths.empty? 28 | stderr.puts "warning: no source files given" 29 | else 30 | source_paths.each do |source_path| 31 | if File.directory?(source_path) 32 | Find.find(source_path) do |source_file_path| 33 | next if !File.file?(source_file_path) 34 | target_file_path = source_file_path.sub(source_path, target_path) 35 | if File.exist?(target_file_path) 36 | merge_file(source_file_path, target_file_path) 37 | elsif !@update_only 38 | FileUtils.mkdir_p(File.dirname(target_file_path)) 39 | FileUtils.cp(source_file_path, target_file_path) 40 | end 41 | end 42 | else File.exist?(source_path) 43 | merge_file(source_path, target_path) 44 | end 45 | end 46 | end 47 | 0 48 | rescue Error, OptionParser::ParseError => error 49 | stderr.puts error.message 50 | 1 51 | end 52 | 53 | private 54 | 55 | def merge_file(source_path, target_path) 56 | source_text = File.read(source_path).encode!('UTF-8', invalid: :replace) 57 | target_text = File.read(target_path).encode!('UTF-8', invalid: :replace) 58 | 59 | if target_text =~ /\A\s*\z/m 60 | return 61 | end 62 | 63 | if source_text =~ /\A\s*\z/m 64 | FileUtils.cp source_path, target_path 65 | return 66 | end 67 | 68 | source = Nokogiri::XML::Document.parse(source_text) 69 | target = Nokogiri::XML::Document.parse(target_text) 70 | 71 | source.xpath("//testsuite/testcase").each do |node| 72 | summary_diff = SummaryDiff.new 73 | 74 | predicates = [ 75 | attribute_predicate('classname', node['classname']), 76 | attribute_predicate('name', node['name']), 77 | ].join(' and ') 78 | original = target.xpath("testsuite/testcase[#{predicates}]").first 79 | 80 | if original 81 | summary_diff.add(node, 1) 82 | summary_diff.add(original, -1) 83 | original.replace(node) 84 | elsif !@update_only 85 | summary_diff.add(node, 1) 86 | testsuite = target.xpath("testsuite").first 87 | testsuite.add_child(node) 88 | end 89 | 90 | node.ancestors.select { |a| a.name =~ /\Atestsuite?\z/ }.each do |suite| 91 | summary_diff.apply_to(suite) 92 | end 93 | end 94 | 95 | open(target_path, 'w') { |f| f.write(target.to_s) } 96 | end 97 | 98 | def attribute_predicate(name, value) 99 | # XPath doesn't let you escape the delimiting quotes. Need concat() here 100 | # to support the general case. 101 | escaped = value.to_s.gsub('"', '", \'"\', "') 102 | "@#{name}=concat('', \"#{escaped}\")" 103 | end 104 | 105 | def apply_summary_diff(diff, node) 106 | summary_diff.each do |key, delta| 107 | end 108 | end 109 | 110 | SummaryDiff = Struct.new(:tests, :failures, :errors, :skipped) do 111 | def initialize 112 | self.tests = self.failures = self.errors = self.skipped = 0 113 | end 114 | 115 | def add(test_node, delta) 116 | self.tests += delta 117 | self.failures += delta if !test_node.xpath('failure').empty? 118 | self.errors += delta if !test_node.xpath('error').empty? 119 | self.skipped += delta if !test_node.xpath('skipped').empty? 120 | end 121 | 122 | def apply_to(node) 123 | %w[tests failures errors skipped].each do |attribute| 124 | if (value = node[attribute]) 125 | node[attribute] = value.to_i + send(attribute) 126 | end 127 | end 128 | end 129 | end 130 | 131 | def parse_args(args) 132 | parser = OptionParser.new do |parser| 133 | parser.banner = "USAGE: #$0 [options] SOURCES ... TARGET" 134 | parser.on '-u', '--update-only', "Only update nodes, don't append new nodes in the source." do 135 | @update_only = true 136 | end 137 | end 138 | 139 | parser.parse!(args) 140 | 141 | args.size >= 1 or 142 | raise Error, parser.banner 143 | 144 | args 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/junit_merge/version.rb: -------------------------------------------------------------------------------- 1 | module JunitMerge 2 | VERSION = [0, 1, 2] 3 | 4 | class << VERSION 5 | include Comparable 6 | 7 | def to_s 8 | join('.') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/junit_merge/test_app.rb: -------------------------------------------------------------------------------- 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 by default" 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 "skips nodes only in the source if --update-only is given" do 110 | create_file("#{tmp}/source.xml", 'a.a' => :fail, 'a.b' => :error) 111 | create_file("#{tmp}/target.xml", 'a.a' => :pass) 112 | app.run('--update-only', "#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0 113 | document = parse_file("#{tmp}/target.xml") 114 | results(document).must_equal([['a.a', :fail]]) 115 | summaries(document).must_equal([{tests: 1, failures: 1, errors: 0, skipped: 0}]) 116 | stdout.string.must_equal('') 117 | stderr.string.must_equal('') 118 | end 119 | 120 | it "correctly merges tests with metacharacters in the name" do 121 | create_file("#{tmp}/source.xml", 'a\'"a.b"\'b' => :pass) 122 | create_file("#{tmp}/target.xml", 'a\'"a.b"\'b' => :fail) 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.b"\'b', :pass]]) 126 | summaries(document).must_equal([{tests: 1, failures: 0, errors: 0, skipped: 0}]) 127 | stdout.string.must_equal('') 128 | stderr.string.must_equal('') 129 | end 130 | 131 | it "correctly merges tests with the same name in different classes" do 132 | create_file("#{tmp}/source.xml", 'a.a' => :pass, 'b.a' => :fail) 133 | create_file("#{tmp}/target.xml", 'a.a' => :fail, 'b.a' => :pass) 134 | app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0 135 | document = parse_file("#{tmp}/target.xml") 136 | results(document).must_equal([['a.a', :pass], ['b.a', :fail]]) 137 | stdout.string.must_equal('') 138 | stderr.string.must_equal('') 139 | end 140 | end 141 | 142 | describe "when merging directories" do 143 | it "updates target files from each file in the source directory" do 144 | create_file("#{tmp}/source/a.xml", 'a.a' => :pass, 'a.b' => :fail) 145 | create_file("#{tmp}/target/a.xml", 'a.a' => :fail, 'a.b' => :pass) 146 | app.run("#{tmp}/source", "#{tmp}/target").must_equal 0 147 | document = parse_file("#{tmp}/target/a.xml") 148 | results(document).must_equal([['a.a', :pass], ['a.b', :fail]]) 149 | stdout.string.must_equal('') 150 | stderr.string.must_equal('') 151 | end 152 | 153 | it "does not modify files only in the target" do 154 | FileUtils.mkdir "#{tmp}/source" 155 | create_file("#{tmp}/target/a.xml", 'a.a' => :fail, 'a.b' => :pass) 156 | app.run("#{tmp}/source", "#{tmp}/target").must_equal 0 157 | document = parse_file("#{tmp}/target/a.xml") 158 | results(document).must_equal([['a.a', :fail], ['a.b', :pass]]) 159 | stdout.string.must_equal('') 160 | stderr.string.must_equal('') 161 | end 162 | 163 | it "adds files only in the source by default" do 164 | create_file("#{tmp}/source/a.xml", 'a.a' => :fail, 'a.b' => :pass) 165 | create_directory("#{tmp}/target") 166 | app.run("#{tmp}/source", "#{tmp}/target").must_equal 0 167 | document = parse_file("#{tmp}/target/a.xml") 168 | results(document).must_equal([['a.a', :fail], ['a.b', :pass]]) 169 | stdout.string.must_equal('') 170 | stderr.string.must_equal('') 171 | end 172 | 173 | it "skips files only in the source if --update-only is given" do 174 | create_file("#{tmp}/source/a.xml", 'a.a' => :fail, 'a.b' => :pass) 175 | create_directory("#{tmp}/target") 176 | app.run('--update-only', "#{tmp}/source", "#{tmp}/target").must_equal 0 177 | File.exist?("#{tmp}/target/a.xml").must_equal false 178 | stdout.string.must_equal('') 179 | stderr.string.must_equal('') 180 | end 181 | end 182 | 183 | it "does not complain about empty files" do 184 | FileUtils.touch "#{tmp}/source.xml" 185 | FileUtils.touch "#{tmp}/target.xml" 186 | app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0 187 | File.read("#{tmp}/target.xml").must_equal('') 188 | stdout.string.must_equal('') 189 | stderr.string.must_equal('') 190 | end 191 | 192 | it "works around invalid UTF-8" do 193 | create_file("#{tmp}/source.xml", "a.a\xFFb" => :pass) 194 | create_file("#{tmp}/target.xml", "a.a\xFFb" => :fail) 195 | app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 0 196 | 197 | document = parse_file("#{tmp}/target.xml") 198 | results(document).must_equal([["a.a\uFFFDb", :pass]]) 199 | 200 | stdout.string.must_equal('') 201 | stderr.string.must_equal('') 202 | end 203 | 204 | it "whines if the source does not exist" do 205 | FileUtils.touch "#{tmp}/target.xml" 206 | app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 1 207 | File.read("#{tmp}/target.xml").must_equal('') 208 | stdout.string.must_equal('') 209 | stderr.string.must_match /no such file/ 210 | end 211 | 212 | it "whines if the target does not exist" do 213 | FileUtils.touch "#{tmp}/source.xml" 214 | app.run("#{tmp}/source.xml", "#{tmp}/target.xml").must_equal 1 215 | stdout.string.must_equal('') 216 | stderr.string.must_match /no such file/ 217 | end 218 | 219 | it "exits with a warning if no source files are given" do 220 | create_file("#{tmp}/target.xml", 'a.a' => :pass, 'a.b' => :fail) 221 | app.run("#{tmp}/target.xml").must_equal 0 222 | document = parse_file("#{tmp}/target.xml") 223 | results(document).must_equal([['a.a', :pass], ['a.b', :fail]]) 224 | stdout.string.must_equal('') 225 | stderr.string.must_equal("warning: no source files given\n") 226 | end 227 | 228 | it "can merge multiple source files into the target in order" do 229 | create_file("#{tmp}/source1.xml", 'a.a' => :fail, 'a.b' => :fail) 230 | create_file("#{tmp}/source2.xml", 'a.a' => :pass) 231 | create_file("#{tmp}/target.xml", 'a.a' => :error, 'a.b' => :error, 'a.c' => :error) 232 | app.run("#{tmp}/source1.xml", "#{tmp}/source2.xml", "#{tmp}/target.xml").must_equal 0 233 | document = parse_file("#{tmp}/target.xml") 234 | results(document).must_equal([['a.a', :pass], ['a.b', :fail], ['a.c', :error]]) 235 | stdout.string.must_equal('') 236 | stderr.string.must_equal('') 237 | end 238 | 239 | it "errors with a usage message if no args aren't given" do 240 | FileUtils.touch "#{tmp}/source.xml" 241 | app.run.must_equal 1 242 | File.read("#{tmp}/source.xml").must_equal('') 243 | stdout.string.must_equal('') 244 | stderr.string.must_match /USAGE/ 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /test/template.xml.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% tests.each do |name, result| %> 5 | <% name = name.dup.force_encoding('ASCII-8BIT') %> 6 | <% class_name, test_name = name.gsub('"', '"').split('.') %> 7 | 8 | <% if result == :fail %> 9 | 10 | 11 | 12 | <% elsif result == :error %> 13 | 14 | 15 | 16 | <% elsif result == :skipped %> 17 | 18 | <% end %> 19 | 20 | <% end %> 21 | 22 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 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' 9 | --------------------------------------------------------------------------------