Author: lacton
Date: Tue Aug 26 15:31:47 2008
New Revision: 689269
URL: http://svn.apache.org/viewvc?rev=689269&view=rev
Log:
BUILDR-139 Incremental test run. Tests are run when and only when something
has changed in a project or its dependencies, just like compile.
Modified:
incubator/buildr/trunk/CHANGELOG
incubator/buildr/trunk/lib/buildr/core/test.rb
incubator/buildr/trunk/spec/test_spec.rb
Modified: incubator/buildr/trunk/CHANGELOG
URL:
http://svn.apache.org/viewvc/incubator/buildr/trunk/CHANGELOG?rev=689269&r1=689268&r2=689269&view=diff
==============================================================================
--- incubator/buildr/trunk/CHANGELOG (original)
+++ incubator/buildr/trunk/CHANGELOG Tue Aug 26 15:31:47 2008
@@ -10,6 +10,7 @@
* Change: Eclipse task updated to documented Scala plugin requirements
(http://www.scala-lang.org/node/94)
* Change: Buildr.application.buildfile returns a task instead of a String.
+* Change: BUILDR-139 Incremental test run.
* Fixed: BUILDR-106 download(artifact(...)=>url) broken in certain cases
(Lacton).
* Fixed: BUILDR-108 Trace to explain why a compile is done (Lacton).
Modified: incubator/buildr/trunk/lib/buildr/core/test.rb
URL:
http://svn.apache.org/viewvc/incubator/buildr/trunk/lib/buildr/core/test.rb?rev=689269&r1=689268&r2=689269&view=diff
==============================================================================
--- incubator/buildr/trunk/lib/buildr/core/test.rb (original)
+++ incubator/buildr/trunk/lib/buildr/core/test.rb Tue Aug 26 15:31:47 2008
@@ -188,13 +188,14 @@
@dependencies = FileList[]
@include = []
@exclude = []
+ @forced_need = false
parent_task = Project.parent_task(name)
if parent_task.respond_to?(:options)
@options = OpenObject.new { |hash, key| parent_task.options[key].clone
rescue parent_task.options[key] }
else
@options = OpenObject.new(default_options)
end
- enhance do
+ enhance [application.buildfile.name] do
run_tests if framework
end
end
@@ -217,6 +218,10 @@
end
def execute(args) #:nodoc:
+ if Buildr.options.test == false
+ info "Skipping tests for #{project.name}"
+ return
+ end
setup.invoke
begin
super
@@ -274,7 +279,7 @@
# :call-seq:
# with(*specs) => self
#
- # Specify artifacts (specs, tasks, files, etc) to include in the
depdenenciest list
+ # Specify artifacts (specs, tasks, files, etc) to include in the
dependencies list
# when compiling and running tests.
def with(*artifacts)
@dependencies |= Buildr.artifacts(artifacts.flatten).uniq
@@ -390,6 +395,22 @@
@report_to ||= file(@project.path_to(:reports, framework)=>self)
end
+ # The path to the file that stores the time stamp of the last successful
test run.
+ def last_successful_run_file #:nodoc:
+ File.join(report_to.to_s, 'last_successful_run')
+ end
+
+ # The time stamp of the last successful test run. Or Rake::EARLY if no
successful test run recorded.
+ def timestamp #:nodoc:
+ File.exist?(last_successful_run_file) ?
File.mtime(last_successful_run_file) : Rake::EARLY
+ end
+
+ # Call this method when a test run is successful to record the current
system time.
+ def record_successful_run #:nodoc:
+ mkdir_p report_to.to_s
+ touch last_successful_run_file
+ end
+
# The project this task belongs to.
attr_reader :project
@@ -434,12 +455,14 @@
fail 'Tests failed!'
end
end
+ record_successful_run unless @forced_need
end
# Limit running tests to specific list.
def only_run(tests)
@include = Array(tests)
@exclude.clear
+ @forced_need = true
end
def invoke_prerequisites(args, chain) #:nodoc:
@@ -447,6 +470,14 @@
super
end
+ def needed? #:nodoc:
+ latest_prerequisite = @prerequisites.map { |p| application[p, @scope]
}.sort_by(&:timestamp).last
+ needed = (timestamp == Rake::EARLY) || latest_prerequisite.timestamp >
timestamp
+ trace "Testing#{needed ? ' ' : ' not '}needed. " +
+ "Latest prerequisite change: #{latest_prerequisite.timestamp}
(#{latest_prerequisite.to_s}). " +
+ "Last successful test run: #{timestamp}."
+ return needed || @forced_need || Buildr.options.test == :all
+ end
end
@@ -508,7 +539,7 @@
# buildr test:MyTest
# will run the test com.example.MyTest, if such a test exists for this
project.
#
- # If you want to run multiple test, separate tham with a comma. You can
also use glob
+ # If you want to run multiple test, separate them with a comma. You can
also use glob
# (* and ?) patterns to match multiple tests, see the TestTask#include
method.
rule /^test:.*$/ do |task|
# The map works around a JRuby bug whereby the string looks fine, but
fails in fnmatch.
@@ -516,14 +547,6 @@
task('test').invoke
end
- task 'build' do |task|
- # Make sure this happens as the last action on the build, so all other
enhancements
- # are made to run before starting the tests.
- task.enhance do
- task('test').invoke unless Buildr.options.test == false
- end
- end
-
IntegrationTestsTask.define_task('integration')
# Similar to test:[pattern] but for integration tests.
@@ -565,6 +588,8 @@
test.with project.compile.dependencies
# Picking up the test frameworks adds further dependencies.
test.framework
+
+ project.build test unless test.options[:integration]
project.clean do
rm_rf test.compile.target.to_s, :verbose=>false if test.compile.target
Modified: incubator/buildr/trunk/spec/test_spec.rb
URL:
http://svn.apache.org/viewvc/incubator/buildr/trunk/spec/test_spec.rb?rev=689269&r1=689268&r2=689269&view=diff
==============================================================================
--- incubator/buildr/trunk/spec/test_spec.rb (original)
+++ incubator/buildr/trunk/spec/test_spec.rb Tue Aug 26 15:31:47 2008
@@ -17,6 +17,14 @@
require File.join(File.dirname(__FILE__), 'spec_helpers')
+module TestHelper
+ def set_last_successful_test_run test_task, timestamp
+ test_task.record_successful_run
+ File.utime(timestamp, timestamp, test_task.last_successful_run_file)
+ end
+end
+
+
describe Buildr::TestTask do
def test_task
@test_task ||= define('foo').test
@@ -235,6 +243,10 @@
depends = project('foo').test.dependencies
depends.index(project('foo').test.resources.target).should <
depends.index(project('foo').resources.target)
end
+
+ it 'should not have a last successful run timestamp before the tests are
run' do
+ test_task.timestamp.should == Rake::EARLY
+ end
it 'should clean after itself (test files)' do
define('foo') { test.compile.using(:javac) }
@@ -312,10 +324,17 @@
it 'should execute teardown task' do
lambda { test_task.invoke }.should run_task('foo:test:teardown')
end
+
+ it 'should update the last successful run timestamp' do
+ before = Time.now ; test_task.invoke ; after = Time.now
+ (before-1..after+1).should include(test_task.timestamp)
+ end
end
describe Buildr::TestTask, 'with failed test' do
+ include TestHelper
+
def test_task
@test_task ||= begin
define 'foo' do
@@ -365,6 +384,13 @@
it 'should execute teardown task' do
lambda { test_task.invoke rescue nil }.should run_task('foo:test:teardown')
end
+
+ it 'should not update the last successful run timestamp' do
+ a_second_ago = Time.now - 1
+ set_last_successful_test_run test_task, a_second_ago
+ test_task.invoke rescue nil
+ test_task.timestamp.should <= a_second_ago
+ end
end
@@ -450,6 +476,13 @@
test.options[:properties].should == {}
end
end
+
+ it "should run from project's build task" do
+ write 'src/main/java/Foo.java'
+ write 'src/test/java/FooTest.java'
+ define('foo')
+ lambda { task('foo:build').invoke }.should run_task('foo:test')
+ end
end
@@ -508,7 +541,7 @@
end
-describe Buildr::Project, 'test:resources' do
+describe Buildr::Project, '#test.resources' do
it 'should ignore resources unless they exist' do
define('foo').test.resources.sources.should be_empty
project('foo').test.resources.target.should be_nil
@@ -538,6 +571,99 @@
end
+describe Buildr::TestTask, '#invoke' do
+ include TestHelper
+
+ def test_task
+ @test_task ||= define('foo') {
+ test.using(:junit)
+ test.instance_eval do
+ @framework.stub!(:tests).and_return(['PassingTest'])
+ @framework.stub!(:run).and_return(['PassingTest'])
+ end
+ }.test
+ end
+
+ it 'should require dependencies to exist' do
+ lambda { test_task.with('no-such.jar').invoke }.should \
+ raise_error(RuntimeError, /Don't know how to build/)
+ end
+
+ it 'should run all dependencies as prerequisites' do
+ file(File.expand_path('no-such.jar')) { task('prereq').invoke }
+ lambda { test_task.with('no-such.jar').invoke }.should
run_tasks(['prereq', 'foo:test'])
+ end
+
+ it 'should run tests if they have never run' do
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should not run tests if test option is off' do
+ Buildr.options.test = false
+ lambda { test_task.invoke }.should_not run_task('foo:test')
+ end
+
+ describe 'when there was a successful test run already' do
+ before do
+ @a_second_ago = Time.now - 1
+ src = ['main/java/Foo.java', 'main/resources/config.xml',
'test/java/FooTest.java', 'test/resources/config-test.xml'].map { |f|
File.join('src', f) }
+ target = ['classes/Foo.class', 'resources/config.xml',
'test/classes/FooTest.class', 'test/resources/config-test.xml'].map { |f|
File.join('target', f) }
+ files = ['buildfile'] + src + target
+ files.each { |file| write file }
+ (files + files.map { |file| file.pathmap('%d') }).each { |file|
File.utime(@a_second_ago, @a_second_ago, file) }
+ set_last_successful_test_run test_task, @a_second_ago
+ end
+
+ it 'should not run tests if nothing changed' do
+ lambda { test_task.invoke }.should_not run_task('foo:test')
+ end
+
+ it 'should run tests if options.test is :all' do
+ Buildr.options.test = :all
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if main compile target changed' do
+ touch project('foo').compile.target.to_s
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if test compile target changed' do
+ touch test_task.compile.target.to_s
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if main resources changed' do
+ touch project('foo').resources.target.to_s
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if test resources changed' do
+ touch test_task.resources.target.to_s
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if compile-dependent project changed' do
+ write 'bar/src/main/java/Bar.java', 'public class Bar {}'
+ define('bar', :version=>'1.0', :base_dir=>'bar') { package :jar }
+ project('foo').compile.with project('bar')
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if test-dependent project changed' do
+ write 'bar/src/main/java/Bar.java', 'public class Bar {}'
+ define('bar', :version=>'1.0', :base_dir=>'bar') { package :jar }
+ test_task.with project('bar')
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+
+ it 'should run tests if buildfile changed' do
+ touch 'buildfile'
+ lambda { test_task.invoke }.should run_task('foo:test')
+ end
+ end
+end
+
describe Rake::Task, 'test' do
it 'should be recursive' do
define('foo') { define 'bar' }
@@ -604,6 +730,8 @@
describe 'test rule' do
+ include TestHelper
+
it 'should execute test task on local project' do
define('foo') { define 'bar' }
lambda { task('test:something').invoke }.should run_task('foo:test')
@@ -663,18 +791,26 @@
define 'foo'
task('test:Something').invoke
end
-end
-
-
-describe Rake::Task, 'build' do
- it 'should run test task if test option is on' do
- Buildr.options.test = true
- lambda { task('build').invoke }.should run_tasks('test')
+
+ it 'should execute the named tests even if the test task is not needed' do
+ define 'foo' do
+ test.using(:junit)
+ test.instance_eval { @framework.stub!(:tests).and_return(['something',
'nothing']) }
+ end
+ project('foo').test.record_successful_run
+ task('test:something').invoke
+ project('foo').test.tests.should include('something')
end
-
- it 'should not run test task if test option is off' do
- Buildr.options.test = false
- lambda { task('build').invoke }.should_not run_task('test')
+
+ it 'should not update the last successful test run timestamp' do
+ define 'foo' do
+ test.using(:junit)
+ test.instance_eval { @framework.stub!(:tests).and_return(['something',
'nothing']) }
+ end
+ a_second_ago = Time.now - 1
+ set_last_successful_test_run project('foo').test, a_second_ago
+ task('test:something').invoke
+ project('foo').test.timestamp.should <= a_second_ago
end
end
@@ -857,7 +993,7 @@
define('bar') { test.using :integration=>false }
end
lambda { task('package').invoke }.should run_tasks(['foo:package',
'foo:test'],
- ['foo:bar:build', 'foo:bar:test', 'foo:bar:package'])
+ ['foo:bar:test', 'foo:bar:package'])
end
it 'should not execute by local package task if test=no' do