cucover 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Licence.txt +22 -0
- data/README.markdown +69 -0
- data/Rakefile +86 -0
- data/VERSION +1 -0
- data/bin/cucover +3 -0
- data/cucover.gemspec +182 -0
- data/cucumber.yml +3 -0
- data/examples/self_test/rails/.gitignore +4 -0
- data/examples/self_test/rails/Rakefile +10 -0
- data/examples/self_test/rails/app/controllers/application_controller.rb +2 -0
- data/examples/self_test/rails/app/controllers/widgets_controller.rb +2 -0
- data/examples/self_test/rails/app/helpers/application_helper.rb +3 -0
- data/examples/self_test/rails/app/views/widgets/index.html.erb +1 -0
- data/examples/self_test/rails/config/boot.rb +110 -0
- data/examples/self_test/rails/config/cucumber.yml +7 -0
- data/examples/self_test/rails/config/database.yml +25 -0
- data/examples/self_test/rails/config/environment.rb +41 -0
- data/examples/self_test/rails/config/environments/cucumber.rb +27 -0
- data/examples/self_test/rails/config/environments/development.rb +17 -0
- data/examples/self_test/rails/config/environments/production.rb +28 -0
- data/examples/self_test/rails/config/environments/test.rb +28 -0
- data/examples/self_test/rails/config/initializers/backtrace_silencers.rb +7 -0
- data/examples/self_test/rails/config/initializers/inflections.rb +10 -0
- data/examples/self_test/rails/config/initializers/mime_types.rb +5 -0
- data/examples/self_test/rails/config/initializers/new_rails_defaults.rb +21 -0
- data/examples/self_test/rails/config/initializers/session_store.rb +15 -0
- data/examples/self_test/rails/config/locales/en.yml +5 -0
- data/examples/self_test/rails/config/routes.rb +43 -0
- data/examples/self_test/rails/db/seeds.rb +7 -0
- data/examples/self_test/rails/features/see_widgets.feature +7 -0
- data/examples/self_test/rails/features/step_definitions/web_steps.rb +273 -0
- data/examples/self_test/rails/features/support/env.rb +57 -0
- data/examples/self_test/rails/features/support/paths.rb +29 -0
- data/examples/self_test/rails/lib/tasks/cucumber.rake +47 -0
- data/examples/self_test/rails/public/404.html +30 -0
- data/examples/self_test/rails/public/422.html +30 -0
- data/examples/self_test/rails/public/500.html +30 -0
- data/examples/self_test/rails/public/favicon.ico +0 -0
- data/examples/self_test/rails/public/images/rails.png +0 -0
- data/examples/self_test/rails/public/index.html +275 -0
- data/examples/self_test/rails/public/javascripts/application.js +2 -0
- data/examples/self_test/rails/public/javascripts/controls.js +963 -0
- data/examples/self_test/rails/public/javascripts/dragdrop.js +973 -0
- data/examples/self_test/rails/public/javascripts/effects.js +1128 -0
- data/examples/self_test/rails/public/javascripts/prototype.js +4320 -0
- data/examples/self_test/rails/public/robots.txt +5 -0
- data/examples/self_test/rails/script/about +4 -0
- data/examples/self_test/rails/script/console +3 -0
- data/examples/self_test/rails/script/cucumber +10 -0
- data/examples/self_test/rails/script/dbconsole +3 -0
- data/examples/self_test/rails/script/destroy +3 -0
- data/examples/self_test/rails/script/generate +3 -0
- data/examples/self_test/rails/script/performance/benchmarker +3 -0
- data/examples/self_test/rails/script/performance/profiler +3 -0
- data/examples/self_test/rails/script/plugin +3 -0
- data/examples/self_test/rails/script/runner +3 -0
- data/examples/self_test/rails/script/server +3 -0
- data/examples/self_test/simple/features/call_foo.feature +4 -0
- data/examples/self_test/simple/features/call_foo_and_bar_together.feature +5 -0
- data/examples/self_test/simple/features/call_foo_from_background_then_bar.feature +7 -0
- data/examples/self_test/simple/features/call_foo_from_background_then_bar_then_baz.feature +10 -0
- data/examples/self_test/simple/features/call_foo_then_bar.feature +7 -0
- data/examples/self_test/simple/features/call_foo_then_bar_from_scenario_outline_examples.feature +9 -0
- data/examples/self_test/simple/features/fail.feature +10 -0
- data/examples/self_test/simple/features/step_definitions/main_steps.rb +15 -0
- data/examples/self_test/simple/lib/bar.rb +5 -0
- data/examples/self_test/simple/lib/baz.rb +5 -0
- data/examples/self_test/simple/lib/foo.rb +9 -0
- data/features/call_foo.feature +0 -0
- data/features/coverage_of.feature +81 -0
- data/features/fail.feature +60 -0
- data/features/help.feature +15 -0
- data/features/lazy_run.feature +76 -0
- data/features/lazy_run_per_scenario.feature +108 -0
- data/features/lazy_run_per_scenario_outline_example.feature +29 -0
- data/features/lazy_run_triggered_by_rails_view_change.feature +43 -0
- data/features/run.feature +25 -0
- data/features/show_recordings.feature +28 -0
- data/features/step_definitions/main_steps.rb +41 -0
- data/features/support/env.rb +52 -0
- data/lib/at_exit_hook.rb +3 -0
- data/lib/cucover.rb +65 -0
- data/lib/cucover/cli.rb +50 -0
- data/lib/cucover/cli_commands/coverage_of.rb +44 -0
- data/lib/cucover/cli_commands/cucumber.rb +46 -0
- data/lib/cucover/cli_commands/show_recordings.rb +29 -0
- data/lib/cucover/cli_commands/version.rb +13 -0
- data/lib/cucover/controller.rb +28 -0
- data/lib/cucover/cucumber_hooks.rb +45 -0
- data/lib/cucover/line_numbers.rb +10 -0
- data/lib/cucover/logging_config.rb +16 -0
- data/lib/cucover/monkey.rb +14 -0
- data/lib/cucover/rails.rb +23 -0
- data/lib/cucover/recorder.rb +37 -0
- data/lib/cucover/recording.rb +66 -0
- data/lib/cucover/recording/covered_file.rb +53 -0
- data/lib/cucover/store.rb +70 -0
- data/lib/dependencies.rb +10 -0
- data/spec/cucover/cli_spec.rb +31 -0
- data/spec/cucover/controller_spec.rb +16 -0
- data/spec/cucover/recording/covered_file_spec.rb +20 -0
- data/spec/cucover/store_spec.rb +28 -0
- data/spec/spec_helper.rb +7 -0
- data/tmp/.gitignore +0 -0
- metadata +236 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Cucover
|
2
|
+
class Controller
|
3
|
+
def initialize(file_colon_line, store)
|
4
|
+
@file_colon_line = file_colon_line
|
5
|
+
@store = store
|
6
|
+
end
|
7
|
+
|
8
|
+
def should_execute?
|
9
|
+
dirty? or failed_on_last_run?
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def failed_on_last_run?
|
15
|
+
return false unless recording
|
16
|
+
recording.failed?
|
17
|
+
end
|
18
|
+
|
19
|
+
def dirty?
|
20
|
+
Cucover.logger.debug("Assuming dirty as no recording found") and return true unless recording
|
21
|
+
recording.covered_files.any?{ |f| f.dirty? }
|
22
|
+
end
|
23
|
+
|
24
|
+
def recording
|
25
|
+
@recording ||= @store.latest_recording(@file_colon_line)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cucover
|
2
|
+
module FeatureElementExtensions
|
3
|
+
def reset_skipped_steps!
|
4
|
+
return unless @steps
|
5
|
+
@steps.each do |step|
|
6
|
+
step.instance_variable_set("@skip_invoke", nil)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Cucover
|
13
|
+
module ExampleRowExtensions
|
14
|
+
include FeatureElementExtensions
|
15
|
+
|
16
|
+
def file_colon_line
|
17
|
+
"#{file}:#{line}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def file
|
21
|
+
@scenario_outline.file_colon_line.split(':').first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Cucover::Monkey.extend_every Cucumber::Ast::Scenario => Cucover::FeatureElementExtensions
|
27
|
+
Cucover::Monkey.extend_every Cucumber::Ast::OutlineTable::ExampleRow => Cucover::ExampleRowExtensions
|
28
|
+
|
29
|
+
Before do |scenario_or_table_row|
|
30
|
+
scenario_or_table_row.reset_skipped_steps!
|
31
|
+
|
32
|
+
Cucover.logger.info("Starting #{scenario_or_table_row.class} #{scenario_or_table_row.file_colon_line}")
|
33
|
+
Cucover::Rails.patch_if_necessary
|
34
|
+
|
35
|
+
if Cucover.should_execute?(scenario_or_table_row)
|
36
|
+
Cucover.start_recording!(scenario_or_table_row)
|
37
|
+
else
|
38
|
+
announce "[ Cucover - Skipping clean scenario ]"
|
39
|
+
scenario_or_table_row.skip_invoke!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
After do
|
44
|
+
Cucover.stop_recording!
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Logging.configure do
|
2
|
+
logger('Cucover') do
|
3
|
+
level :debug
|
4
|
+
appenders 'logfile'
|
5
|
+
end
|
6
|
+
|
7
|
+
appender('logfile') do
|
8
|
+
type 'File'
|
9
|
+
level :debug
|
10
|
+
filename File.dirname(__FILE__) + '/../../tmp/cucover.log'
|
11
|
+
layout do
|
12
|
+
type 'Pattern'
|
13
|
+
pattern '[%d] %l %c : %m\n'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Cucover
|
2
|
+
module Monkey
|
3
|
+
def self.extend_every(args)
|
4
|
+
class_to_extend = args.keys.first
|
5
|
+
module_to_extend_with = args.values.first
|
6
|
+
|
7
|
+
class_to_extend.instance_eval <<-PATCH
|
8
|
+
def new(*args)
|
9
|
+
super(*args).extend(#{module_to_extend_with})
|
10
|
+
end
|
11
|
+
PATCH
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Cucover
|
2
|
+
module Rails
|
3
|
+
class << self
|
4
|
+
def patch_if_necessary
|
5
|
+
return if @patched
|
6
|
+
return unless defined?(ActionView)
|
7
|
+
|
8
|
+
Monkey.extend_every ActionView::Base => Cucover::Rails::RecordsRenders
|
9
|
+
# Monkey.extend_every ActionView::Template => Cucover::Rails::RecordsRenders # TODO: patch nicer template
|
10
|
+
|
11
|
+
@patched = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module RecordsRenders
|
16
|
+
def render(*args)
|
17
|
+
filename = args[0][:file].filename
|
18
|
+
Cucover.record_file(filename)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Cucover
|
2
|
+
class Recorder
|
3
|
+
def initialize(scenario_or_table_row)
|
4
|
+
@scenario_or_table_row = scenario_or_table_row
|
5
|
+
@analyzer = Rcov::CodeCoverageAnalyzer.new
|
6
|
+
@additional_covered_files = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def record_file!(source_file)
|
10
|
+
unless @additional_covered_files.include?(source_file)
|
11
|
+
@additional_covered_files << source_file
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def start!
|
16
|
+
@start_time = Time.now
|
17
|
+
@analyzer.install_hook
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop!
|
21
|
+
@end_time = Time.now
|
22
|
+
@analyzer.remove_hook
|
23
|
+
Cucover.logger.info("Finished recording #{@scenario_or_table_row.file_colon_line}.")
|
24
|
+
Cucover.logger.debug("Covered files: #{@analyzer.analyzed_files.join(',')}")
|
25
|
+
Cucover.logger.debug("Additional Covered files: #{@additional_covered_files.join(',')}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def recording
|
29
|
+
Recording.new(
|
30
|
+
@scenario_or_table_row.file_colon_line,
|
31
|
+
@scenario_or_table_row.exception,
|
32
|
+
@additional_covered_files,
|
33
|
+
@analyzer,
|
34
|
+
@start_time, @end_time)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Cucover
|
2
|
+
class Recording < Struct.new(
|
3
|
+
:file_colon_line,
|
4
|
+
:exception,
|
5
|
+
:additional_covered_files,
|
6
|
+
:analyzer,
|
7
|
+
:start_time, :end_time)
|
8
|
+
|
9
|
+
IGNORE_PATTERNS = [
|
10
|
+
/gem/,
|
11
|
+
/vendor/,
|
12
|
+
/lib\/ruby/,
|
13
|
+
/cucover\/lib/
|
14
|
+
]
|
15
|
+
|
16
|
+
def feature_filename
|
17
|
+
file_colon_line.split(':').first
|
18
|
+
end
|
19
|
+
|
20
|
+
def covers_file?(source_file)
|
21
|
+
covered_files.include?(source_file)
|
22
|
+
end
|
23
|
+
|
24
|
+
def covers_line?(source_file, line_number)
|
25
|
+
covered_files.detect{ |f| f.file == source_file }.covers_line?(line_number)
|
26
|
+
end
|
27
|
+
|
28
|
+
def covered_files
|
29
|
+
@covered_files ||= analyzed_covered_files + additional_covered_files
|
30
|
+
end
|
31
|
+
|
32
|
+
def failed?
|
33
|
+
!!exception
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def additional_covered_files
|
39
|
+
super.map do |filename|
|
40
|
+
CoveredFile.new(filename, nil, self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def analyzed_covered_files
|
45
|
+
filtered_analyzed_files.map do |filename|
|
46
|
+
lines, marked_info, count_info = analyzer.data(filename)
|
47
|
+
CoveredFile.new(filename, marked_info, self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def boring?(file)
|
52
|
+
IGNORE_PATTERNS.any? do |expression|
|
53
|
+
file.match expression
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def filtered_analyzed_files
|
58
|
+
analyzer.analyzed_files.reject{ |f| boring?(f) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def normalized_files
|
62
|
+
cleaned_analyzed_files + additional_covered_files
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
require 'cucover/recording/covered_file'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Cucover
|
2
|
+
class Recording::CoveredFile
|
3
|
+
attr_reader :file
|
4
|
+
|
5
|
+
def initialize(full_filename, rcov_marked_info, recording)
|
6
|
+
@full_filename = full_filename
|
7
|
+
@rcov_marked_info = rcov_marked_info
|
8
|
+
@recording = recording
|
9
|
+
@file = File.expand_path(full_filename).gsub(/^#{Dir.pwd}\//, '')
|
10
|
+
|
11
|
+
extend HasLineNumberDetail if @rcov_marked_info
|
12
|
+
end
|
13
|
+
|
14
|
+
def dirty?
|
15
|
+
Cucover.logger.debug("#{file} last modified at #{File.mtime(@full_filename)}")
|
16
|
+
Cucover.logger.debug("#{file} recording started at #{@recording.start_time}")
|
17
|
+
result = File.mtime(@full_filename).to_i >= @recording.start_time.to_i
|
18
|
+
Cucover.logger.debug("verdict: #{(result ? "dirty" : "not dirty")}")
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
def covers_line?(line_number)
|
23
|
+
covered_lines.include?(line_number)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
other == file
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"#{file}:#{covered_lines.join(':')}"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def covered_lines
|
37
|
+
['<unknown lines>']
|
38
|
+
end
|
39
|
+
|
40
|
+
module HasLineNumberDetail
|
41
|
+
def covered_lines
|
42
|
+
return @covered_lines if @covered_lines
|
43
|
+
|
44
|
+
@covered_lines = []
|
45
|
+
@rcov_marked_info.each_with_index do |covered, index|
|
46
|
+
line_number = index + Cucover::LineNumbers.offset
|
47
|
+
@covered_lines << line_number if covered
|
48
|
+
end
|
49
|
+
@covered_lines
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Cucover
|
2
|
+
class Store
|
3
|
+
def initialize(cache = DiskCache.new)
|
4
|
+
@cache = cache
|
5
|
+
end
|
6
|
+
|
7
|
+
def latest_recordings
|
8
|
+
recordings.keys.map{ |file_colon_line| latest_recording(file_colon_line) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def latest_recording(file_colon_line)
|
12
|
+
recordings[file_colon_line].sort{ |x, y| x.end_time <=> y.end_time }.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def keep!(recording_data)
|
16
|
+
Cucover.logger.debug("Storing recording of #{recording_data.file_colon_line}")
|
17
|
+
recordings[recording_data.file_colon_line] << recording_data
|
18
|
+
@cache.save(@recordings)
|
19
|
+
end
|
20
|
+
|
21
|
+
def recordings_covering(source_file)
|
22
|
+
latest_recordings.select { |r| r.covers_file?(source_file) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def recordings
|
26
|
+
ensure_recordings_loaded!
|
27
|
+
@recordings
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def ensure_recordings_loaded!
|
33
|
+
return if @recordings
|
34
|
+
|
35
|
+
if @recordings = @cache.load
|
36
|
+
Cucover.logger.debug("Loaded #{@recordings.length} recording(s)")
|
37
|
+
else
|
38
|
+
Cucover.logger.debug("Starting with no existing coverage data.")
|
39
|
+
@recordings = Recordings.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Recordings < Hash
|
44
|
+
def [](key)
|
45
|
+
self[key] = [] if super.nil?
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class DiskCache
|
51
|
+
def save(recordings)
|
52
|
+
Cucover.logger.debug("Saving #{recordings.length} recording(s) to #{data_file}")
|
53
|
+
File.open(data_file, 'w') { |f| f.puts Marshal.dump(recordings) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def load
|
57
|
+
return unless File.exists?(data_file)
|
58
|
+
|
59
|
+
Cucover.logger.debug("Reading existing coverage data from #{data_file}")
|
60
|
+
File.open(data_file) { |f| Marshal.load(f) }
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def data_file
|
66
|
+
Dir.pwd + '/cucover.data'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/dependencies.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Cucover::Cli do
|
4
|
+
describe "given a --coverage-of command" do
|
5
|
+
before(:each) do
|
6
|
+
@args = ['--coverage-of', 'lib/foo.rb']
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should create a CoverageOf command object and execute it" do
|
10
|
+
Cucover::CliCommands::CoverageOf.should_receive(:new).with(['--coverage-of', 'lib/foo.rb']).and_return(command = mock('command'))
|
11
|
+
command.should_receive(:execute)
|
12
|
+
cli = Cucover::Cli.new(@args)
|
13
|
+
cli.start
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "given arguments for Cucumber" do
|
18
|
+
before(:each) do
|
19
|
+
@args = ['--', 'c', 'd']
|
20
|
+
end
|
21
|
+
it "should pass the arguments after the -- to cucumber" do
|
22
|
+
cli = Cucover::Cli.new(@args)
|
23
|
+
|
24
|
+
Kernel.stub!(:load).with(Cucumber::BINARY) do
|
25
|
+
ARGV.should == ['c', 'd']
|
26
|
+
end
|
27
|
+
|
28
|
+
cli.start
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
module Cucover
|
4
|
+
describe Controller do
|
5
|
+
describe "#should_execute?" do
|
6
|
+
before(:each) do
|
7
|
+
@store = mock(Store)
|
8
|
+
@example_id = 'foo.feature:123'
|
9
|
+
end
|
10
|
+
it "when no previous recording exists, it should always return true" do
|
11
|
+
@store.should_receive(:latest_recording).with(@example_id).and_return(nil)
|
12
|
+
Controller.new(@example_id, @store).should_execute?.should be_true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|