jcov 1.0.0

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 (45) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +5 -0
  3. data/LICENSE +19 -0
  4. data/README.rdoc +109 -0
  5. data/Rakefile +2 -0
  6. data/bin/jcov +36 -0
  7. data/examples/jasmine/.gitignore +1 -0
  8. data/examples/jasmine/jasmine/ConsoleReporter.js +177 -0
  9. data/examples/jasmine/jasmine/MIT.LICENSE.txt +20 -0
  10. data/examples/jasmine/jasmine/jasmine.js +2476 -0
  11. data/examples/jasmine/jasmine/runner.js +28 -0
  12. data/examples/jasmine/javascripts/mean.js +11 -0
  13. data/examples/jasmine/jcov.yml +8 -0
  14. data/examples/jasmine/tests/mean_spec.js +15 -0
  15. data/examples/jspec/.gitignore +1 -0
  16. data/examples/jspec/javascripts/mean.js +11 -0
  17. data/examples/jspec/jcov.yml +8 -0
  18. data/examples/jspec/jspec/ext/slowness.js +43 -0
  19. data/examples/jspec/jspec/ext/trace.js +28 -0
  20. data/examples/jspec/jspec/lib/MIT.LICENSE.txt +7 -0
  21. data/examples/jspec/jspec/lib/jspec.js +1925 -0
  22. data/examples/jspec/jspec/runner.js +66 -0
  23. data/examples/jspec/tests/mean_spec.js +15 -0
  24. data/features/configuration.feature +134 -0
  25. data/features/coverage.feature +246 -0
  26. data/features/html_report.feature +130 -0
  27. data/features/javascript_interface.feature +72 -0
  28. data/features/reporting.feature +108 -0
  29. data/features/run.feature +217 -0
  30. data/features/step_definitions/report_steps.rb +54 -0
  31. data/features/support/env.rb +29 -0
  32. data/jcov.gemspec +29 -0
  33. data/lib/jcov.rb +11 -0
  34. data/lib/jcov/commands.rb +47 -0
  35. data/lib/jcov/configuration.rb +54 -0
  36. data/lib/jcov/coverage.rb +151 -0
  37. data/lib/jcov/reporter.rb +2 -0
  38. data/lib/jcov/reporter/console_reporter.rb +107 -0
  39. data/lib/jcov/reporter/file.html.erb +26 -0
  40. data/lib/jcov/reporter/html_reporter.rb +64 -0
  41. data/lib/jcov/reporter/report.css +53 -0
  42. data/lib/jcov/reporter/report.html.erb +45 -0
  43. data/lib/jcov/runner.rb +100 -0
  44. data/lib/jcov/version.rb +3 -0
  45. metadata +195 -0
@@ -0,0 +1,54 @@
1
+ RSpec::Matchers.define :have_line do |this_line|
2
+ match do |lines|
3
+ regex = Regexp.new(Regexp.escape(this_line))
4
+ lines.any? {|line| line =~ regex}.should be_true
5
+ end
6
+ failure_message_for_should do |list|
7
+ # generate and return the appropriate string.
8
+ "did not find \"#{this_line}\""
9
+ end
10
+ failure_message_for_should_not do |list|
11
+ "found \"#{this_line}\""
12
+ end
13
+ end
14
+
15
+ Given /^I open the report$/ do
16
+ visit "/report.html"
17
+ end
18
+
19
+ Then /^I should see "([^"]*)"$/ do |text|
20
+ page.should have_content text
21
+ end
22
+
23
+ Then /^I should see "([^"]*)" in the HTML$/ do |text|
24
+ page.html.should match Regexp.new(Regexp.escape(text))
25
+ end
26
+
27
+ Then /^I should see these lines (covered|not covered|uncoverable):$/ do |type, table|
28
+
29
+ # find all the lines of the given type
30
+ lines = case type
31
+ when 'covered'
32
+ all('.line[data-coverage!="0"][data-coverage!="uncoverable"] .code')
33
+ when 'not covered'
34
+ all('.line[data-coverage="0"] .code')
35
+ when 'uncoverable'
36
+ all('.line[data-coverage="uncoverable"] .code')
37
+ end
38
+
39
+ lines.map! &:text
40
+
41
+ # interate over the list and see if they match
42
+ table.hashes.each do |row|
43
+ lines.should have_line(row[:line])
44
+ end
45
+ end
46
+
47
+ When /^I click "([^\"]*)"$/ do |link|
48
+ click_link(link)
49
+ end
50
+
51
+ Then /^(?:|I )should be on (.+)$/ do |path|
52
+ current_path = URI.parse(current_url).path
53
+ File.expand_path(current_path).should == File.expand_path(path)
54
+ end
@@ -0,0 +1,29 @@
1
+ lib_dir = File.expand_path('../../../lib', __FILE__)
2
+
3
+ $:.unshift lib_dir
4
+
5
+ require 'aruba/cucumber'
6
+ require 'jcov'
7
+ require 'rack'
8
+ require 'capybara'
9
+ require 'capybara/dsl'
10
+ require 'capybara/cucumber'
11
+ require 'capybara/session'
12
+
13
+ Capybara.default_selector = :css
14
+
15
+ include Capybara::DSL
16
+
17
+ Capybara.app = Rack::Builder.new do
18
+ map "/" do
19
+ use Rack::Static, :urls => ["/"], :root => 'tmp/aruba/jcov'
20
+ run lambda {|env| [404, {}, '']}
21
+ end
22
+ end.to_app
23
+
24
+ Before do
25
+ @aruba_timeout_seconds = 10
26
+ end
27
+
28
+ # add the lib dir to RUBYLIB so bin/jcov can find what it needs
29
+ ENV['RUBYLIB'] = lib_dir
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "jcov/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "jcov"
7
+ s.version = JCov::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Doug McInnes"]
10
+ s.email = ["dmcinnes@attinteractive.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Javascript Coverage Tool}
13
+ s.description = %q{Javascript Coverage Tool}
14
+
15
+ s.add_dependency "commander", "~> 4.0"
16
+ s.add_dependency "therubyracer", "= 0.9.0"
17
+ s.add_dependency "rkelly", "~> 1.0.4"
18
+
19
+ s.add_development_dependency "cucumber", "~> 1.0"
20
+ s.add_development_dependency "aruba", "~> 0.4.6"
21
+ s.add_development_dependency "capybara", "~> 1.1.1"
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+
28
+ s.executables = ['jcov']
29
+ end
@@ -0,0 +1,11 @@
1
+ require 'v8'
2
+ require 'rkelly'
3
+
4
+ require 'jcov/version'
5
+ require 'jcov/configuration'
6
+ require 'jcov/commands'
7
+ require 'jcov/runner'
8
+ require 'jcov/coverage'
9
+ require 'jcov/reporter'
10
+ require 'jcov/reporter/console_reporter'
11
+ require 'jcov/reporter/html_reporter'
@@ -0,0 +1,47 @@
1
+ require 'yaml'
2
+
3
+ module JCov::Commands
4
+
5
+ # the check command
6
+ class Check
7
+ def initialize args, options
8
+ config = JCov::Configuration.new options.config
9
+
10
+ if config.filename
11
+ puts "Using configuration file: #{config.filename}"
12
+ else
13
+ puts "No configuration file! Using defaults."
14
+ end
15
+
16
+ puts config
17
+ end
18
+ end
19
+
20
+ # the run command
21
+ class Run
22
+ def initialize args, options
23
+ # default to no color unless we're on a tty
24
+ options.default :color => $stdout.tty?
25
+ options.default :coverage => true
26
+
27
+ options.args = args
28
+
29
+ config = JCov::Configuration.new(options.config)
30
+
31
+ runner = JCov::Coverage::CoverageRunner.new(config, options)
32
+
33
+ runner.run
34
+
35
+ abort "Test Failures! :(" if runner.failure_count > 0
36
+
37
+ if options.report
38
+ JCov::Reporter::HTMLReporter.new(runner).report
39
+ end
40
+
41
+ reporter = JCov::Reporter::ConsoleReporter.new(runner)
42
+
43
+ abort unless reporter.report
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,54 @@
1
+ require 'yaml'
2
+
3
+ module JCov
4
+ class Configuration
5
+
6
+ LOCATIONS = %w{config/jcov.yml ./jcov.yml}
7
+
8
+ attr_reader :config
9
+ attr_reader :filename
10
+
11
+ DEFAULTS = {
12
+ "test_directory" => "test/javascripts",
13
+ "source_directory" => "public/javascripts",
14
+ "test_runner" => "test/javascripts/runner.js",
15
+ "error_field" => "error_count",
16
+ "report_output_directory" => "jcov",
17
+ }
18
+
19
+ def initialize file
20
+ @filename = find_file(file)
21
+ @config = DEFAULTS.merge(@filename && YAML.load_file(@filename) || {})
22
+ create_readers
23
+ end
24
+
25
+ def to_s
26
+ @config.to_yaml
27
+ end
28
+
29
+ # ignore unset configuration values
30
+ def method_missing method
31
+ nil
32
+ end
33
+
34
+ private
35
+
36
+ def find_file file
37
+ raise "Cannot find file \"#{file}\"" if file && !File.exists?(file)
38
+
39
+ file || LOCATIONS.find do |file|
40
+ File.exists?(file)
41
+ end
42
+ end
43
+
44
+ # create methods to access the configuration
45
+ def create_readers
46
+ @config.each_key do |key|
47
+ self.class.send(:define_method, key) do
48
+ @config[key]
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,151 @@
1
+ module JCov
2
+ module Coverage
3
+
4
+ # extend RKelly's ECMAVisitor to include our coverage_tick
5
+ class CoverageVisitor < RKelly::Visitors::ECMAVisitor
6
+ def initialize(coverage)
7
+ @coverage = coverage
8
+ @indent = 0
9
+ end
10
+
11
+ # whenever we hit a line, add the instrumentation
12
+ def visit_SourceElementsNode(o)
13
+ o.value.map { |x|
14
+ if (x.filename && x.line)
15
+ coverage = "_coverage_tick('#{x.filename}', #{x.line});"
16
+ @coverage[x.filename][x.line] = 0
17
+ end
18
+ "#{coverage || ""}#{indent}#{x.accept(self)}"
19
+ }.join("\n")
20
+ end
21
+ end
22
+
23
+
24
+ class CoverageRunner
25
+ attr_reader :config
26
+ attr_reader :options
27
+ attr_reader :runner
28
+ attr_reader :instrumented_files
29
+
30
+ def initialize config, options
31
+ @config = config
32
+ @options = options
33
+
34
+ @runner = JCov::Runner.new(config, options)
35
+
36
+ override_runners_load_method
37
+ add_coverage_method_to_context
38
+
39
+ @visitor = CoverageVisitor.new(coverage_data)
40
+ @parser = RKelly::Parser.new
41
+
42
+ @instrumented_files = {}
43
+ end
44
+
45
+ def coverable_files
46
+ if @coverable_files.nil?
47
+ # all the files we're testing on
48
+ @coverable_files = Dir.glob(File.join(config.source_directory, "**", "*.js"))
49
+ # only run coverage on files that we haven't specifically ignored
50
+ ignore = config.ignore || []
51
+ @coverable_files.delete_if {|file| ignore.any? {|i| file.match(i) }}
52
+ # remove the runner if it's in there
53
+ @coverable_files.delete(config.test_runner)
54
+ end
55
+ @coverable_files
56
+ end
57
+
58
+ def coverage_data
59
+ if @coverage_data.nil?
60
+ # set up coverage data structure
61
+ @coverage_data = {}
62
+ coverable_files.each {|file| @coverage_data[file] = {} }
63
+ end
64
+ @coverage_data
65
+ end
66
+
67
+ # our new load method
68
+ def load file
69
+ if instrumented_files[file]
70
+ # reuse previously loaded file
71
+ content = instrumented_files[file]
72
+ else
73
+ content = File.read(file)
74
+
75
+ # is this a file we need to instrument?
76
+ if coverable_files.include? file
77
+ # run it through the js parser and custom renderer
78
+ tree = @parser.parse(content, file)
79
+ content = @visitor.accept(tree)
80
+
81
+ # cache the file if it's reloaded
82
+ instrumented_files[file] = content
83
+ end
84
+ end
85
+
86
+ runner.context.eval(content, file)
87
+ end
88
+
89
+ def _coverage_tick file, line
90
+ coverage_data[file][line] += 1
91
+ end
92
+
93
+ def run
94
+ runner.run
95
+ end
96
+
97
+ # proxy to runner
98
+ def failure_count
99
+ runner.failure_count
100
+ end
101
+
102
+ # reduce the coverage data to file, total line count, and covered line count
103
+ def reduced_coverage_data
104
+ if @reduced_coverage_data.nil?
105
+ @reduced_coverage_data = coverage_data.map do |file, lines|
106
+ # if we don't have any data for this file it was never loaded
107
+ if lines.empty?
108
+ # load it now
109
+ content = File.read(file)
110
+
111
+ # run it through the js parser and custom renderer
112
+ # the visitor will fill out the coverage data for this line
113
+ tree = @parser.parse(content, file)
114
+ content = @visitor.accept(tree)
115
+
116
+ # re-get the lines
117
+ lines = coverage_data[file]
118
+
119
+ # this file was never run
120
+ cover = 0
121
+ else
122
+ # munge the count data together to get coverage
123
+ cover = lines.values.inject(0) { |memo, count| memo + ((count > 0) ? 1 : 0) }
124
+ end
125
+
126
+ total = lines.count
127
+
128
+ [file, total, cover]
129
+ end
130
+ end
131
+ @reduced_coverage_data
132
+ end
133
+
134
+ def get_binding
135
+ binding
136
+ end
137
+
138
+ private
139
+
140
+ def add_coverage_method_to_context
141
+ runner.context['_coverage_tick'] = self.method('_coverage_tick')
142
+ end
143
+
144
+ def override_runners_load_method
145
+ runner.context['load'] = self.method('load')
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,2 @@
1
+ module JCov::Reporter
2
+ end
@@ -0,0 +1,107 @@
1
+ module JCov::Reporter
2
+
3
+ class ConsoleReporter
4
+
5
+ attr_accessor :total_count, :covered_count, :percent
6
+
7
+ def initialize coverage_runner
8
+ @coverage_runner = coverage_runner
9
+
10
+ calculate_total_coverage if report_coverage?
11
+ end
12
+
13
+ def report
14
+ if total_count && total_count == 0
15
+ # report a warning message if we're not checking any files for coverage
16
+ puts "No files were checked for coverage. Maybe your ignore list in #{config.filename} is too inclusive?"
17
+ elsif report_coverage?
18
+ report_file_coverage if options.report
19
+ report_total_coverage
20
+ report_threshold_errors if config.threshold
21
+ passed?
22
+ else
23
+ true
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def config
30
+ @coverage_runner.config
31
+ end
32
+
33
+ def options
34
+ @coverage_runner.options
35
+ end
36
+
37
+ # no_coverage.nil? because no_coverage gets set to false when set because it's prefixed as 'no'
38
+ def report_coverage?
39
+ options.report || options.no_coverage.nil? && options.coverage && options.test.nil? && options.args.empty?
40
+ end
41
+
42
+ # passes if any are true:
43
+ # 1. we're not checking a threshold
44
+ # 2. thresholds must match and they do
45
+ # 3. coverage percent is greater than or equal to threshold
46
+ def passed?
47
+ config.threshold.nil? ||
48
+ (config.threshold_must_match && percent == config.threshold) ||
49
+ percent >= config.threshold
50
+ end
51
+
52
+ # report an under threshold error
53
+ # report an over threshold error if threshold_must_match is set to true
54
+ def report_threshold_errors
55
+ if percent < config.threshold
56
+ puts "FAIL! Coverage is lower than threshold! #{percent}% < #{config.threshold}% :("
57
+ elsif config.threshold_must_match && percent != config.threshold
58
+ puts "Coverage does not match threshold! #{percent}% != #{config.threshold}%"
59
+ puts "Please raise the threshold in #{config.filename}"
60
+ end
61
+ end
62
+
63
+ def report_total_coverage
64
+ printf "\nTotal Coverage: (%d/%d) %3.1f%\n\n", covered_count, total_count, percent
65
+ end
66
+
67
+ def report_file_coverage
68
+ puts "\n"
69
+
70
+ puts "Coverage Report"
71
+ puts "===================="
72
+
73
+ filename_length = @coverage_runner.coverage_data.map(&:first).map(&:length).max
74
+
75
+ @coverage_runner.reduced_coverage_data.each do |file, total, cover|
76
+ if (total > 0)
77
+ percent = 100 * cover / total
78
+ coverage_string = "(#{cover}/#{total})"
79
+ else
80
+ percent = 100
81
+ coverage_string = "(EMPTY)"
82
+ end
83
+ if options.test.nil? || percent > 0 # only show ran files if we're doing a focused test
84
+ printf "%-#{filename_length}s %-10s %3s%\n", file, coverage_string, percent
85
+ end
86
+ end
87
+ puts "===================="
88
+ end
89
+
90
+ def calculate_total_coverage
91
+ @total_count = 0
92
+ @covered_count = 0
93
+
94
+ @coverage_runner.reduced_coverage_data.each do |file, tot, cover|
95
+ if !config.test || cover > 0 # only show ran files if we're doing a focused test
96
+ @total_count += tot
97
+ @covered_count += cover
98
+ end
99
+ end
100
+
101
+ # only keep one significant digit
102
+ @percent = (100.0 * @covered_count / @total_count).round(1)
103
+ end
104
+
105
+ end
106
+
107
+ end