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.
- data/.gitignore +6 -0
- data/Gemfile +5 -0
- data/LICENSE +19 -0
- data/README.rdoc +109 -0
- data/Rakefile +2 -0
- data/bin/jcov +36 -0
- data/examples/jasmine/.gitignore +1 -0
- data/examples/jasmine/jasmine/ConsoleReporter.js +177 -0
- data/examples/jasmine/jasmine/MIT.LICENSE.txt +20 -0
- data/examples/jasmine/jasmine/jasmine.js +2476 -0
- data/examples/jasmine/jasmine/runner.js +28 -0
- data/examples/jasmine/javascripts/mean.js +11 -0
- data/examples/jasmine/jcov.yml +8 -0
- data/examples/jasmine/tests/mean_spec.js +15 -0
- data/examples/jspec/.gitignore +1 -0
- data/examples/jspec/javascripts/mean.js +11 -0
- data/examples/jspec/jcov.yml +8 -0
- data/examples/jspec/jspec/ext/slowness.js +43 -0
- data/examples/jspec/jspec/ext/trace.js +28 -0
- data/examples/jspec/jspec/lib/MIT.LICENSE.txt +7 -0
- data/examples/jspec/jspec/lib/jspec.js +1925 -0
- data/examples/jspec/jspec/runner.js +66 -0
- data/examples/jspec/tests/mean_spec.js +15 -0
- data/features/configuration.feature +134 -0
- data/features/coverage.feature +246 -0
- data/features/html_report.feature +130 -0
- data/features/javascript_interface.feature +72 -0
- data/features/reporting.feature +108 -0
- data/features/run.feature +217 -0
- data/features/step_definitions/report_steps.rb +54 -0
- data/features/support/env.rb +29 -0
- data/jcov.gemspec +29 -0
- data/lib/jcov.rb +11 -0
- data/lib/jcov/commands.rb +47 -0
- data/lib/jcov/configuration.rb +54 -0
- data/lib/jcov/coverage.rb +151 -0
- data/lib/jcov/reporter.rb +2 -0
- data/lib/jcov/reporter/console_reporter.rb +107 -0
- data/lib/jcov/reporter/file.html.erb +26 -0
- data/lib/jcov/reporter/html_reporter.rb +64 -0
- data/lib/jcov/reporter/report.css +53 -0
- data/lib/jcov/reporter/report.html.erb +45 -0
- data/lib/jcov/runner.rb +100 -0
- data/lib/jcov/version.rb +3 -0
- 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
|
data/jcov.gemspec
ADDED
@@ -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
|
data/lib/jcov.rb
ADDED
@@ -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,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
|