cucover 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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