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


Reply via email to