cucover 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. data/.gitignore +6 -0
  2. data/Licence.txt +22 -0
  3. data/README.markdown +69 -0
  4. data/Rakefile +86 -0
  5. data/VERSION +1 -0
  6. data/bin/cucover +3 -0
  7. data/cucover.gemspec +182 -0
  8. data/cucumber.yml +3 -0
  9. data/examples/self_test/rails/.gitignore +4 -0
  10. data/examples/self_test/rails/Rakefile +10 -0
  11. data/examples/self_test/rails/app/controllers/application_controller.rb +2 -0
  12. data/examples/self_test/rails/app/controllers/widgets_controller.rb +2 -0
  13. data/examples/self_test/rails/app/helpers/application_helper.rb +3 -0
  14. data/examples/self_test/rails/app/views/widgets/index.html.erb +1 -0
  15. data/examples/self_test/rails/config/boot.rb +110 -0
  16. data/examples/self_test/rails/config/cucumber.yml +7 -0
  17. data/examples/self_test/rails/config/database.yml +25 -0
  18. data/examples/self_test/rails/config/environment.rb +41 -0
  19. data/examples/self_test/rails/config/environments/cucumber.rb +27 -0
  20. data/examples/self_test/rails/config/environments/development.rb +17 -0
  21. data/examples/self_test/rails/config/environments/production.rb +28 -0
  22. data/examples/self_test/rails/config/environments/test.rb +28 -0
  23. data/examples/self_test/rails/config/initializers/backtrace_silencers.rb +7 -0
  24. data/examples/self_test/rails/config/initializers/inflections.rb +10 -0
  25. data/examples/self_test/rails/config/initializers/mime_types.rb +5 -0
  26. data/examples/self_test/rails/config/initializers/new_rails_defaults.rb +21 -0
  27. data/examples/self_test/rails/config/initializers/session_store.rb +15 -0
  28. data/examples/self_test/rails/config/locales/en.yml +5 -0
  29. data/examples/self_test/rails/config/routes.rb +43 -0
  30. data/examples/self_test/rails/db/seeds.rb +7 -0
  31. data/examples/self_test/rails/features/see_widgets.feature +7 -0
  32. data/examples/self_test/rails/features/step_definitions/web_steps.rb +273 -0
  33. data/examples/self_test/rails/features/support/env.rb +57 -0
  34. data/examples/self_test/rails/features/support/paths.rb +29 -0
  35. data/examples/self_test/rails/lib/tasks/cucumber.rake +47 -0
  36. data/examples/self_test/rails/public/404.html +30 -0
  37. data/examples/self_test/rails/public/422.html +30 -0
  38. data/examples/self_test/rails/public/500.html +30 -0
  39. data/examples/self_test/rails/public/favicon.ico +0 -0
  40. data/examples/self_test/rails/public/images/rails.png +0 -0
  41. data/examples/self_test/rails/public/index.html +275 -0
  42. data/examples/self_test/rails/public/javascripts/application.js +2 -0
  43. data/examples/self_test/rails/public/javascripts/controls.js +963 -0
  44. data/examples/self_test/rails/public/javascripts/dragdrop.js +973 -0
  45. data/examples/self_test/rails/public/javascripts/effects.js +1128 -0
  46. data/examples/self_test/rails/public/javascripts/prototype.js +4320 -0
  47. data/examples/self_test/rails/public/robots.txt +5 -0
  48. data/examples/self_test/rails/script/about +4 -0
  49. data/examples/self_test/rails/script/console +3 -0
  50. data/examples/self_test/rails/script/cucumber +10 -0
  51. data/examples/self_test/rails/script/dbconsole +3 -0
  52. data/examples/self_test/rails/script/destroy +3 -0
  53. data/examples/self_test/rails/script/generate +3 -0
  54. data/examples/self_test/rails/script/performance/benchmarker +3 -0
  55. data/examples/self_test/rails/script/performance/profiler +3 -0
  56. data/examples/self_test/rails/script/plugin +3 -0
  57. data/examples/self_test/rails/script/runner +3 -0
  58. data/examples/self_test/rails/script/server +3 -0
  59. data/examples/self_test/simple/features/call_foo.feature +4 -0
  60. data/examples/self_test/simple/features/call_foo_and_bar_together.feature +5 -0
  61. data/examples/self_test/simple/features/call_foo_from_background_then_bar.feature +7 -0
  62. data/examples/self_test/simple/features/call_foo_from_background_then_bar_then_baz.feature +10 -0
  63. data/examples/self_test/simple/features/call_foo_then_bar.feature +7 -0
  64. data/examples/self_test/simple/features/call_foo_then_bar_from_scenario_outline_examples.feature +9 -0
  65. data/examples/self_test/simple/features/fail.feature +10 -0
  66. data/examples/self_test/simple/features/step_definitions/main_steps.rb +15 -0
  67. data/examples/self_test/simple/lib/bar.rb +5 -0
  68. data/examples/self_test/simple/lib/baz.rb +5 -0
  69. data/examples/self_test/simple/lib/foo.rb +9 -0
  70. data/features/call_foo.feature +0 -0
  71. data/features/coverage_of.feature +81 -0
  72. data/features/fail.feature +60 -0
  73. data/features/help.feature +15 -0
  74. data/features/lazy_run.feature +76 -0
  75. data/features/lazy_run_per_scenario.feature +108 -0
  76. data/features/lazy_run_per_scenario_outline_example.feature +29 -0
  77. data/features/lazy_run_triggered_by_rails_view_change.feature +43 -0
  78. data/features/run.feature +25 -0
  79. data/features/show_recordings.feature +28 -0
  80. data/features/step_definitions/main_steps.rb +41 -0
  81. data/features/support/env.rb +52 -0
  82. data/lib/at_exit_hook.rb +3 -0
  83. data/lib/cucover.rb +65 -0
  84. data/lib/cucover/cli.rb +50 -0
  85. data/lib/cucover/cli_commands/coverage_of.rb +44 -0
  86. data/lib/cucover/cli_commands/cucumber.rb +46 -0
  87. data/lib/cucover/cli_commands/show_recordings.rb +29 -0
  88. data/lib/cucover/cli_commands/version.rb +13 -0
  89. data/lib/cucover/controller.rb +28 -0
  90. data/lib/cucover/cucumber_hooks.rb +45 -0
  91. data/lib/cucover/line_numbers.rb +10 -0
  92. data/lib/cucover/logging_config.rb +16 -0
  93. data/lib/cucover/monkey.rb +14 -0
  94. data/lib/cucover/rails.rb +23 -0
  95. data/lib/cucover/recorder.rb +37 -0
  96. data/lib/cucover/recording.rb +66 -0
  97. data/lib/cucover/recording/covered_file.rb +53 -0
  98. data/lib/cucover/store.rb +70 -0
  99. data/lib/dependencies.rb +10 -0
  100. data/spec/cucover/cli_spec.rb +31 -0
  101. data/spec/cucover/controller_spec.rb +16 -0
  102. data/spec/cucover/recording/covered_file_spec.rb +20 -0
  103. data/spec/cucover/store_spec.rb +28 -0
  104. data/spec/spec_helper.rb +7 -0
  105. data/tmp/.gitignore +0 -0
  106. metadata +236 -0
@@ -0,0 +1,13 @@
1
+ module Cucover
2
+ module CliCommands
3
+ class Version
4
+
5
+ def initialize(cli_args)
6
+ end
7
+
8
+ def execute
9
+ puts File.read(File.dirname(__FILE__) + '/../../../VERSION')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -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,10 @@
1
+ require 'rbconfig'
2
+
3
+ module Cucover
4
+ module LineNumbers
5
+ # Different Ruby versions have slightly different line numbers with rcov
6
+ def self.offset
7
+ Config::CONFIG['MINOR'] == '9' ? 0 : 1
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+
3
+ gem 'logging', '>= 1.4.1'
4
+ require 'logging'
5
+
6
+ gem 'cucumber', '>= 0.3.1'
7
+ require 'cucumber'
8
+
9
+ gem 'rcov', '>= 0.9.8'
10
+ require 'rcov'
@@ -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