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