diff --git a/README.md b/README.md index 9db7fdc..a30a9ef 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Make sure bwoken is properly installed. Then, build
# will build and run all of your tests
 $ rake
 
+# will use xunit output formatter (default colorized)
+$ rake formatter=xunit
+
 # will run one file, relative to integration/coffeescript (note: no file extension)
 $ RUN=iphone/focused_test rake
 
diff --git a/lib/bwoken.rb b/lib/bwoken.rb index 83df83c..d10ae75 100644 --- a/lib/bwoken.rb +++ b/lib/bwoken.rb @@ -3,6 +3,7 @@ require 'bwoken/build' require 'bwoken/coffeescript' require 'bwoken/formatters/colorful_formatter' +require 'bwoken/formatters/xunit_formatter' require 'bwoken/script' require 'bwoken/simulator' require 'bwoken/device' @@ -23,7 +24,12 @@ def app_name end def formatter - @formatter ||= Bwoken::ColorfulFormatter.new + formatter_type = ENV['formatter'] || 'color' + if formatter_type == 'xunit' + @formatter = Bwoken::XunitFormatter::Formatter.new(File.join(results_path, "#{app_name}.xml")) + else + @formatter = Bwoken::ColorfulFormatter.new + end end def project_path diff --git a/lib/bwoken/formatter.rb b/lib/bwoken/formatter.rb index ff3a231..619f002 100644 --- a/lib/bwoken/formatter.rb +++ b/lib/bwoken/formatter.rb @@ -12,7 +12,7 @@ def format_build stdout def on name, &block define_method "_on_#{name}_callback" do |*line| - block.call(*line) + self.instance_exec(*line, &block) end end diff --git a/lib/bwoken/formatters/xunit_formatter.rb b/lib/bwoken/formatters/xunit_formatter.rb new file mode 100644 index 0000000..5890723 --- /dev/null +++ b/lib/bwoken/formatters/xunit_formatter.rb @@ -0,0 +1,150 @@ +require 'bwoken/formatter' +require 'date' + +module Bwoken::XunitFormatter + + ## + # based on the code by @sebastianludwig as a part of tuneup.js + # https://github.com/alexvollmer/tuneup_js/blob/master/test_runner/xunit_output.rb + ## + class TestSuite + attr_reader :name, :timestamp + attr_accessor :test_cases + + def initialize(name) + @name = name + @test_cases = [] + @timestamp = DateTime.now + end + + def failures + @test_cases.count { |test| test.failed? } + end + + def time + @test_cases.map { |test| test.time }.inject(:+) + end + end + + class TestCase + attr_reader :name + attr_accessor :messages + + def initialize(name) + @name = name + @messages = [] + @failed = true + @start = Time.now + @finish = nil + end + + def <<(message) + @messages << message + end + + def pass! + @failed = false; + @finish = Time.now + end + + def fail! + @finish = Time.now + end + + def failed? + @failed + end + + def time + return 0 if @finish.nil? + @finish - @start + end + end + + class XunitOutput + attr_reader :suite + + def initialize(filename) + @filename = filename + @suite = TestSuite.new(File.basename(filename, File.extname(filename))) + end + + def add(line) + return if @suite.test_cases.empty? + @suite.test_cases.last << line + end + + def add_status(status, msg) + case status + when :start + @suite.test_cases << TestCase.new(msg) + when :pass + @suite.test_cases.last.pass! + when :fail + @suite.test_cases.last.fail! + end + end + + def close + File.open(@filename, 'w') { |f| f.write(serialize(@suite)) } + end + + def xml_escape(input) + result = input.dup + + result.gsub!("&", "&") + result.gsub!("<", "<") + result.gsub!(">", ">") + result.gsub!("'", "'") + result.gsub!("\"", """) + + return result + end + + def serialize(suite) + output = "" << "\n" + output << "" << "\n" + + suite.test_cases.each do |test| + output << " " << "\n" + if test.failed? + output << " #{test.messages.map { |m| xml_escape(m) }.join("\n")}" << "\n" + end + output << " " << "\n" + end + + output << "" << "\n" + end + end + + class Formatter < Bwoken::Formatter + + def initialize(filename) + @output = XunitOutput.new(filename) + end + + on :complete do |line| + @output.close + end + + on :error do |line| + tokens = line.split(' ') + @output.add(tokens[4..-1].join(' ')) if line.include?('Exception') + end + + on :fail do |line| + tokens = line.split(' ') + @output.add_status(:fail, tokens[4..-1].join(' ')) + end + + on :start do |line| + tokens = line.split(' ') + @output.add_status(:start, tokens[4..-1].join(' ')) + end + + on :pass do |line| + tokens = line.split(' ') + @output.add_status(:pass, tokens[4..-1].join(' ')) + end + end +end diff --git a/spec/lib/bwoken/formatters/xunit_formatter_spec.rb b/spec/lib/bwoken/formatters/xunit_formatter_spec.rb new file mode 100644 index 0000000..0a798ef --- /dev/null +++ b/spec/lib/bwoken/formatters/xunit_formatter_spec.rb @@ -0,0 +1,177 @@ +require 'spec_helper' + +require 'bwoken/formatters/xunit_formatter' + +describe Bwoken::XunitFormatter::TestSuite do + subject { described_class.new('FakeProject') } + + describe '.failures' do + context 'when contain failed test case' do + before { subject.test_cases << Bwoken::XunitFormatter::TestCase.new('foo') } + it 'returns number of failed cases' do + subject.failures.should == 1 + end + end + end + + describe '.time' do + before do + foo = Bwoken::XunitFormatter::TestCase.new('foo') + bar = Bwoken::XunitFormatter::TestCase.new('bar') + foo.stub(:time => 10) + bar.stub(:time => 1) + subject.test_cases << foo + subject.test_cases << bar + end + + it 'returns number of seconds cosumed for all cases' do + subject.time.should == 11 + end + end +end + +describe Bwoken::XunitFormatter::TestCase do + subject { described_class.new('FakeTest') } + + describe '.pass!' do + before do + time = Time.now + Time.stub(:now => time) + subject << 'Triggering creation' + Time.stub(:now => time + 2) + subject.pass! + end + + it 'changes case status to passed' do + subject.failed?.should == false; + end + + it 'saves finished time' do + subject.time.should == 2 + end + end + + describe '.fail!' do + before do + time = Time.now + Time.stub(:now => time) + subject << 'Triggering creation' + Time.stub(:now => time + 2) + subject.fail! + end + + it 'saves finished time' do + subject.time.should == 2 + end + end + + describe '.failed?' do + context 'when test failed' do + it 'returns true' do + subject.failed?.should == true + end + end + + context 'when test passed' do + before { subject.pass! } + it 'returns false' do + subject.failed?.should == false + end + end + end + + describe '.time' do + context 'when test finished' do + before do + time = Time.now + Time.stub(:now => time) + subject << 'Triggering creation' + Time.stub(:now => time + 2) + subject.pass! + end + + it 'returns number of seconds consumed by test case' do + subject.time.should == 2 + end + end + + context 'when test is not finished' do + it 'returns 0' do + subject.time.should == 0 + end + end + end +end + +describe Bwoken::XunitFormatter::XunitOutput do + subject { described_class.new('fake_file_path') } + + describe '.add' do + before { subject.add_status(:start, '') } + it 'appends line to the last test case' do + subject.suite.test_cases.last.should_receive(:<<).with('foo') + subject.add('foo') + end + end + + describe '.addStatus' do + context 'with :start' do + it 'creates new test case' do + subject.suite.test_cases.should_receive(:<<) + subject.add_status(:start, '') + end + end + + context 'with :pass' do + before { subject.add_status(:start, '') } + it 'changes last test case status to passed' do + subject.suite.test_cases.last.should_receive(:pass!) + subject.add_status(:pass, '') + end + end + + context 'with :fail' do + before { subject.add_status(:start, '') } + it 'changes last test case status to failed' do + subject.suite.test_cases.last.should_receive(:fail!) + subject.add_status(:fail, '') + end + end + end + + describe '.close' do + it 'writes serialized test suite to the file' do + File.should_receive(:open).with('fake_file_path', 'w') + subject.close + end + end + + describe '.serialize' do + before do + @datetime = DateTime.now + DateTime.stub(:now => @datetime) + + @time = Time.now + Time.stub(:now => @time) + + subject.add_status(:start, 'failed case') + subject.add('foo') + subject.add_status(:fail, '') + subject.add_status(:start, 'passed case') + subject.add_status(:pass, '') + end + + it 'returns test suite as xml document' do + subject.serialize(subject.suite).should == <<-XML + + + + foo + + + + + XML + end + end +end diff --git a/spec/lib/bwoken_spec.rb b/spec/lib/bwoken_spec.rb index dc7e113..c796c4e 100644 --- a/spec/lib/bwoken_spec.rb +++ b/spec/lib/bwoken_spec.rb @@ -14,8 +14,21 @@ end describe '#formatter' do - it 'returns Bwoken::ColorfulFormatter' do - subject.formatter.should be_kind_of(Bwoken::ColorfulFormatter) + context 'without formatter env variable' do + it 'returns Bwoken::ColorfulFormatter' do + subject.formatter.should be_kind_of(Bwoken::ColorfulFormatter) + end + end + + context "with ENV['formatter'] = 'xunit'" do + before do + ENV['formatter'] = 'xunit' + subject.stub(:app_name => 'FakeProject') + end + + it 'returns Bwoken::XunitFormatter for ' do + subject.formatter.should be_kind_of(Bwoken::XunitFormatter::Formatter) + end end end