# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with this # work for additional information regarding copyright ownership. The ASF # licenses this file to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helpers')) module TestCoverageHelper def write_test options write File.join(options[:in], "#{options[:for]}Test.java"), "public class #{options[:for]}Test extends junit.framework.TestCase { public void test#{options[:for]}() { new #{options[:for]}(); } }" end # Rspec matcher using file glob patterns. class FileNamePatternMatcher def initialize(pattern) @expected_pattern = pattern @pattern_matcher = lambda { |filename| File.fnmatch? pattern, filename } end def matches?(directory) @actual_filenames = Dir[File.join(directory,'*')] @actual_filenames.any? &@pattern_matcher end def failure_message "Expected to find at least one element matching '#{@expected_pattern}' among #{@actual_filenames.inspect}, but found none" end def negative_failure_message "Expected to find no element matching '#{@expected_pattern}' among #{@actual_filenames.inspect}, but found matching element(s) #{@actual_filenames.select(&@pattern_matcher).inspect}" end end # Test if a directory contains at least one file matching a given glob pattern. # # For example, to check that a directory contains at least one HTML file: # '/path/to/some/directory'.should have_files_matching('*.html') def have_files_matching pattern FileNamePatternMatcher.new pattern end end shared_examples_for 'test coverage tool' do include TestCoverageHelper def toolname @tool_module.name.split('::').last.downcase end def test_coverage_config project('foo').send(toolname) end describe 'project-specific' do before do write 'src/main/java/Foo.java', 'public class Foo {}' write_test :for=>'Foo', :in=>'src/test/java' end describe 'clean' do before { define('foo') } it 'should remove the instrumented directory' do mkdir_p test_coverage_config.instrumented_dir.to_s task('foo:clean').invoke file(test_coverage_config.instrumented_dir).should_not exist end it 'should remove the reporting directory' do mkdir_p test_coverage_config.report_dir task('foo:clean').invoke file(test_coverage_config.report_dir).should_not exist end end describe 'instrumented directory' do it 'should have a default value' do define('foo') test_coverage_config.instrumented_dir.should point_to_path('target/instrumented/classes') end it 'should be overridable' do toolname = toolname() define('foo') { send(toolname).instrumented_dir = path_to('target/coverage/classes') } test_coverage_config.instrumented_dir.should point_to_path('target/coverage/classes') end it 'should be created during instrumentation' do define('foo') task("foo:#{toolname}:instrument").invoke file(test_coverage_config.instrumented_dir).should exist end end describe 'instrumentation' do def instrumented_dir file(test_coverage_config.instrumented_dir) end it 'should happen after compile' do define('foo') lambda { task("foo:#{toolname}:instrument").invoke }.should run_task('foo:compile') end it 'should put classes from compile.target in the instrumented directory' do define('foo') task("foo:#{toolname}:instrument").invoke Dir.entries(instrumented_dir.to_s).should == Dir.entries(project('foo').compile.target.to_s) end it 'should touch instrumented directory if anything instrumented' do a_long_time_ago = Time.now - 10 define('foo') mkpath instrumented_dir.to_s File.utime(a_long_time_ago, a_long_time_ago, instrumented_dir.to_s) task("foo:#{toolname}:instrument").invoke instrumented_dir.timestamp.should be_within(2).of(Time.now) end it 'should not touch instrumented directory if nothing instrumented' do a_long_time_ago = Time.now - 10 define('foo').compile.invoke mkpath instrumented_dir.to_s [project('foo').compile.target, instrumented_dir].map(&:to_s).each { |dir| File.utime(a_long_time_ago, a_long_time_ago, dir) } task("foo:#{toolname}:instrument").invoke instrumented_dir.timestamp.should be_within(2).of(a_long_time_ago) end end describe 'testing classpath' do it 'should give priority to instrumented classes over non-instrumented ones' do define('foo') depends = project('foo').test.dependencies depends.index(test_coverage_config.instrumented_dir).should < depends.index(project('foo').compile.target) end it 'should have the test coverage tools artifacts' do define('foo') artifacts(@tool_module.dependencies).each { |artifact| project('foo').test.dependencies.should include(artifact) } end end describe 'html report' do it 'should have html files' do define('foo') task("foo:#{toolname}:html").invoke test_coverage_config.report_to(:html).should have_files_matching('*.html') end it 'should contain full source code, including comments' do write 'src/main/java/Foo.java', 'public class Foo { /* This comment is a TOKEN to check that test coverage reports include the source code */ }' define('foo') task("foo:#{toolname}:html").invoke html_report_contents = Dir[File.join(test_coverage_config.report_dir, '**/*.html')].map{|path|File.open(path).read}.join html_report_contents.force_encoding('ascii-8bit') if RUBY_VERSION >= '1.9' html_report_contents.should =~ /TOKEN/ end end end describe 'cross-project' do describe 'reporting' do before do write 'src/main/java/Foo.java', 'public class Foo {}' write 'bar/src/main/java/Bar.java', 'public class Bar {}' write_test :for=>'Bar', :in=>'bar/src/test/java' define('foo') { define('bar') } end it 'should have a default target' do @tool_module.report_to.should point_to_path(File.join('reports', toolname)) end describe 'in html' do it 'should be a defined task' do Rake::Task.task_defined?("#{toolname}:html").should be(true) end it 'should happen after project instrumentation and testing' do lambda { task("#{toolname}:html").invoke }.should run_tasks(["foo:#{toolname}:instrument", 'foo:bar:test']) end it 'should have html files' do task("#{toolname}:html").invoke @tool_module.report_to(:html).should have_files_matching('*.html') end it 'should contain full source code, including comments' do write 'bar/src/main/java/Bar.java', 'public class Bar { /* This comment is a TOKEN to check that test coverage reports include the source code */ }' task("#{toolname}:html").invoke html_report_contents = Dir[File.join(@tool_module.report_to(:html), '**/*.html')].map{|path|File.read(path)}.join html_report_contents.force_encoding('ascii-8bit') if RUBY_VERSION >= '1.9' html_report_contents.should =~ /TOKEN/ end it 'should handle gracefully a project with no source' do define 'baz', :base_dir=>'baz' task("#{toolname}:html").invoke lambda { task("#{toolname}:html").invoke }.should_not raise_error end end end describe 'clean' do it 'should remove the report directory' do define('foo') mkdir_p @tool_module.report_to task("#{toolname}:clean").invoke file(@tool_module.report_to).should_not exist end it 'should be called when calling global clean' do define('foo') lambda { task('clean').invoke }.should run_task("#{toolname}:clean") end end end describe 'project with no source' do it 'should not define an html report task' do define 'foo' Rake::Task.task_defined?("foo:#{toolname}:html").should be(false) end it 'should not raise an error when instrumenting' do define('foo') lambda { task("foo:#{toolname}:instrument").invoke }.should_not raise_error end it 'should not add the instrumented directory to the testing classpath' do define 'foo' depends = project('foo').test.dependencies depends.should_not include(test_coverage_config.instrumented_dir) end it 'should not add the test coverage tools artifacts to the testing classpath' do define('foo') @tool_module.dependencies.each { |artifact| project('foo').test.dependencies.should_not include(artifact) } end end end