# 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

RSpec.shared_examples '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
