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