jcov 1.0.1 → 1.1
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/bin/jcov +1 -0
- data/examples/jasmine/javascripts/mean.js +15 -0
- data/features/coverage.feature +8 -13
- data/features/html_report.feature +3 -3
- data/features/instrumentation.feature +310 -0
- data/features/javascript_interface.feature +36 -0
- data/features/run.feature +16 -4
- data/jcov.gemspec +1 -2
- data/lib/jcov.rb +5 -1
- data/lib/jcov/commands.rb +7 -7
- data/lib/jcov/configuration.rb +9 -3
- data/lib/jcov/context.rb +17 -0
- data/lib/jcov/context/instrumentation_context.rb +31 -0
- data/lib/jcov/context/run_context.rb +44 -0
- data/lib/jcov/coverage.rb +55 -130
- data/lib/jcov/errors.rb +6 -0
- data/lib/jcov/js/parser.js +44 -0
- data/lib/jcov/loader.rb +92 -0
- data/lib/jcov/reporter/console_reporter.rb +3 -7
- data/lib/jcov/reporter/html_reporter.rb +1 -1
- data/lib/jcov/runner.rb +22 -62
- data/lib/jcov/version.rb +1 -1
- data/vendor/acorn.js +1711 -0
- data/vendor/walk.js +305 -0
- metadata +13 -19
data/jcov.gemspec
CHANGED
@@ -7,14 +7,13 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = JCov::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Doug McInnes"]
|
10
|
-
s.email = ["
|
10
|
+
s.email = ["doug@dougmcinnes.com"]
|
11
11
|
s.homepage = ""
|
12
12
|
s.summary = %q{Javascript Coverage Tool}
|
13
13
|
s.description = %q{Javascript Coverage Tool}
|
14
14
|
|
15
15
|
s.add_dependency "commander", "~> 4.0"
|
16
16
|
s.add_dependency "therubyracer", "~> 0.11.1"
|
17
|
-
s.add_dependency "rkelly", "~> 1.0.4"
|
18
17
|
|
19
18
|
s.add_development_dependency "cucumber", "~> 1.0"
|
20
19
|
s.add_development_dependency "aruba", "~> 0.4.6"
|
data/lib/jcov.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
require 'v8'
|
2
|
-
require 'rkelly'
|
3
2
|
|
4
3
|
require 'jcov/version'
|
4
|
+
require 'jcov/errors'
|
5
5
|
require 'jcov/configuration'
|
6
6
|
require 'jcov/commands'
|
7
|
+
require 'jcov/loader'
|
8
|
+
require 'jcov/context'
|
9
|
+
require 'jcov/context/run_context'
|
10
|
+
require 'jcov/context/instrumentation_context'
|
7
11
|
require 'jcov/runner'
|
8
12
|
require 'jcov/coverage'
|
9
13
|
require 'jcov/reporter'
|
data/lib/jcov/commands.rb
CHANGED
@@ -4,8 +4,8 @@ module JCov::Commands
|
|
4
4
|
|
5
5
|
# the check command
|
6
6
|
class Check
|
7
|
-
def initialize
|
8
|
-
config = JCov::Configuration.new
|
7
|
+
def initialize(args, options)
|
8
|
+
config = JCov::Configuration.new(options)
|
9
9
|
|
10
10
|
if config.filename
|
11
11
|
puts "Using configuration file: #{config.filename}"
|
@@ -19,26 +19,26 @@ module JCov::Commands
|
|
19
19
|
|
20
20
|
# the run command
|
21
21
|
class Run
|
22
|
-
def initialize
|
22
|
+
def initialize(args, options)
|
23
23
|
# default to no color unless we're on a tty
|
24
24
|
options.default :color => $stdout.tty?
|
25
25
|
options.default :coverage => true
|
26
26
|
|
27
27
|
options.args = args
|
28
28
|
|
29
|
-
config = JCov::Configuration.new(options
|
29
|
+
config = JCov::Configuration.new(options)
|
30
30
|
|
31
|
-
runner = JCov::
|
31
|
+
runner = JCov::Runner.new(config)
|
32
32
|
|
33
33
|
runner.run
|
34
34
|
|
35
35
|
abort "Test Failures! :(" if runner.failure_count > 0
|
36
36
|
|
37
37
|
if options.report
|
38
|
-
JCov::Reporter::HTMLReporter.new(runner).report
|
38
|
+
JCov::Reporter::HTMLReporter.new(runner.coverage).report
|
39
39
|
end
|
40
40
|
|
41
|
-
reporter = JCov::Reporter::ConsoleReporter.new(runner)
|
41
|
+
reporter = JCov::Reporter::ConsoleReporter.new(runner.coverage)
|
42
42
|
|
43
43
|
abort unless reporter.report
|
44
44
|
end
|
data/lib/jcov/configuration.rb
CHANGED
@@ -14,11 +14,17 @@ module JCov
|
|
14
14
|
"test_runner" => "test/javascripts/runner.js",
|
15
15
|
"error_field" => "error_count",
|
16
16
|
"report_output_directory" => "jcov",
|
17
|
+
"verbose" => false,
|
18
|
+
"color" => true,
|
19
|
+
"coverage" => true,
|
20
|
+
"report" => false,
|
21
|
+
"dump" => false
|
17
22
|
}
|
18
23
|
|
19
|
-
def initialize
|
20
|
-
@filename = find_file(
|
21
|
-
@config = DEFAULTS.merge(
|
24
|
+
def initialize(options)
|
25
|
+
@filename = find_file(options.config)
|
26
|
+
@config = DEFAULTS.merge(options.__hash__)
|
27
|
+
@config.merge!(YAML.load_file(@filename)) if @filename
|
22
28
|
create_readers
|
23
29
|
end
|
24
30
|
|
data/lib/jcov/context.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module JCov::Context
|
2
|
+
|
3
|
+
# create a V8 context object with our context methods mapped onto it
|
4
|
+
def create
|
5
|
+
@context = V8::Context.new
|
6
|
+
|
7
|
+
# map our context objects methods onto the v8 object
|
8
|
+
# doing this instead of using :with because of this:
|
9
|
+
# https://github.com/cowboyd/therubyracer/issues/251
|
10
|
+
(self.methods - Object.new.methods - [:create]).each do |method|
|
11
|
+
@context[method] = self.method(method)
|
12
|
+
end
|
13
|
+
|
14
|
+
@context
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module JCov::Context
|
2
|
+
|
3
|
+
class InstrumentationContext
|
4
|
+
include JCov::Context
|
5
|
+
|
6
|
+
def initialize(coverage_data)
|
7
|
+
@coverage_data = coverage_data
|
8
|
+
end
|
9
|
+
|
10
|
+
# called when a line can be covered by tests
|
11
|
+
def lineCovered(file, line, column)
|
12
|
+
# only record if the line hasn't been recorded yet
|
13
|
+
# or if it has, the line is not flagged with nil
|
14
|
+
if !@coverage_data[file].has_key?(line) || !@coverage_data[file].nil?
|
15
|
+
@coverage_data[file][line] = column
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# called when a line should be ignored in the coverage report
|
20
|
+
def ignoreLine(file, line)
|
21
|
+
@coverage_data[file][line] = nil # set to nil so it's ignored
|
22
|
+
end
|
23
|
+
|
24
|
+
# for testing
|
25
|
+
def print(str)
|
26
|
+
puts str
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module JCov::Context
|
2
|
+
|
3
|
+
class RunContext
|
4
|
+
include JCov::Context
|
5
|
+
|
6
|
+
def initialize(loader)
|
7
|
+
@loader = loader
|
8
|
+
@coverage_data = loader.coverage_data
|
9
|
+
end
|
10
|
+
|
11
|
+
def print(s)
|
12
|
+
Kernel.print s
|
13
|
+
end
|
14
|
+
|
15
|
+
def println(*s)
|
16
|
+
Kernel.puts s
|
17
|
+
end
|
18
|
+
|
19
|
+
# so we can do our dotted line output
|
20
|
+
def putc(char)
|
21
|
+
$stdout.putc(char);
|
22
|
+
$stdout.flush();
|
23
|
+
end
|
24
|
+
|
25
|
+
# for JSpec
|
26
|
+
def readFile(file)
|
27
|
+
File.read(file)
|
28
|
+
end
|
29
|
+
|
30
|
+
def load(file)
|
31
|
+
# use the configured loader
|
32
|
+
content = @loader.load file
|
33
|
+
|
34
|
+
# evaluate the javascript
|
35
|
+
@context.eval(content, file)
|
36
|
+
end
|
37
|
+
|
38
|
+
# increment
|
39
|
+
def _coverage_tick(file, line)
|
40
|
+
@coverage_data[file][line] += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/lib/jcov/coverage.rb
CHANGED
@@ -1,151 +1,76 @@
|
|
1
1
|
module JCov
|
2
|
-
module Coverage
|
3
2
|
|
4
|
-
|
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)
|
3
|
+
class Coverage
|
35
4
|
|
36
|
-
|
37
|
-
|
5
|
+
attr_accessor :loader
|
6
|
+
attr_accessor :config
|
38
7
|
|
39
|
-
|
40
|
-
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
41
10
|
|
42
|
-
|
43
|
-
|
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
|
11
|
+
# for loading and instrumenting fiels
|
12
|
+
@loader = JCov::Loader.new(coverable_files, :dump => config.dump)
|
13
|
+
end
|
57
14
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
@
|
15
|
+
def coverable_files
|
16
|
+
if @coverable_files.nil?
|
17
|
+
# all the files we're testing on
|
18
|
+
@coverable_files = Dir.glob(File.join(@config.source_directory, "**", "*.js"))
|
19
|
+
# only run coverage on files that we haven't specifically ignored
|
20
|
+
ignore = @config.ignore || []
|
21
|
+
@coverable_files.delete_if {|file| ignore.any? {|i| file.match(i) }}
|
22
|
+
# remove the runner if it's in there
|
23
|
+
@coverable_files.delete(@config.test_runner)
|
65
24
|
end
|
25
|
+
@coverable_files
|
26
|
+
end
|
66
27
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
instrumented_files[file] = content
|
28
|
+
# reduce the coverage data to file, total line count, and covered line count
|
29
|
+
def reduced_coverage_data
|
30
|
+
# cache the result
|
31
|
+
@reduced_coverage_data ||=
|
32
|
+
coverage_data.map do |file, lines|
|
33
|
+
# if we don't have any data for this file it was never loaded
|
34
|
+
if lines.empty?
|
35
|
+
# load it now
|
36
|
+
lines = examine_uncovered_file file
|
37
|
+
|
38
|
+
# this file was never run so it has zero coverage
|
39
|
+
cover = 0
|
40
|
+
else
|
41
|
+
# munge the count data together to get coverage
|
42
|
+
cover = lines.values.compact.inject(0) { |memo, count| memo + ((count > 0) ? 1 : 0) }
|
83
43
|
end
|
84
|
-
end
|
85
44
|
|
86
|
-
|
87
|
-
|
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
|
45
|
+
# ignore nil values
|
46
|
+
total = lines.values.compact.count
|
101
47
|
|
102
|
-
|
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
|
48
|
+
[file, total, cover]
|
130
49
|
end
|
131
|
-
|
132
|
-
end
|
50
|
+
end
|
133
51
|
|
134
|
-
|
135
|
-
|
136
|
-
|
52
|
+
def coverage_data
|
53
|
+
@loader.coverage_data
|
54
|
+
end
|
137
55
|
|
138
|
-
|
56
|
+
# for coverage reporting
|
57
|
+
def get_binding
|
58
|
+
binding
|
59
|
+
end
|
139
60
|
|
140
|
-
|
141
|
-
runner.context['_coverage_tick'] = self.method('_coverage_tick')
|
142
|
-
end
|
61
|
+
private
|
143
62
|
|
144
|
-
|
145
|
-
|
146
|
-
|
63
|
+
# when a file is in the list to be examined but never loaded
|
64
|
+
# we have to load it ourselves to calculate the full coverage counts
|
65
|
+
def examine_uncovered_file(filename)
|
66
|
+
# run it through the Loader
|
67
|
+
# it will fill out the coverage data for this file
|
68
|
+
@loader.load filename
|
147
69
|
|
70
|
+
# re-get the lines
|
71
|
+
coverage_data[filename]
|
148
72
|
end
|
149
73
|
|
150
74
|
end
|
75
|
+
|
151
76
|
end
|
data/lib/jcov/errors.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
// for acorn
|
2
|
+
var self = this;
|
3
|
+
|
4
|
+
JCov = (function () {
|
5
|
+
|
6
|
+
var calculateCoverageData = function (code, file) {
|
7
|
+
var tree = acorn.parse(code, {locations: true});
|
8
|
+
|
9
|
+
acorn.walk.simple(tree, {
|
10
|
+
Statement: function (node) {
|
11
|
+
// cover all statements
|
12
|
+
if (node.type !== "BlockStatement" &&
|
13
|
+
node.type !== "EmptyStatement") {
|
14
|
+
var pos = node.loc.start;
|
15
|
+
lineCovered(file, pos.line, pos.column);
|
16
|
+
}
|
17
|
+
},
|
18
|
+
IfStatement: function (node) {
|
19
|
+
// ignore else statements
|
20
|
+
if (node.alternate) {
|
21
|
+
ignoreLine(file, node.alternate.loc.start.line);
|
22
|
+
}
|
23
|
+
|
24
|
+
if (node.consequent.type !== "BlockStatement") {
|
25
|
+
var ifStart = node.loc.start.line;
|
26
|
+
var thenStart = node.consequent.loc.start.line;
|
27
|
+
if (ifStart < thenStart) {
|
28
|
+
// ignore non-block single if statements if they're on
|
29
|
+
// their own line
|
30
|
+
ignoreLine(file, node.consequent.loc.start.line);
|
31
|
+
} else if (ifStart === thenStart) {
|
32
|
+
// single line if statement, mark the whole line
|
33
|
+
lineCovered(file, ifStart, 0);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
});
|
38
|
+
};
|
39
|
+
|
40
|
+
return {
|
41
|
+
calculateCoverageData: calculateCoverageData
|
42
|
+
};
|
43
|
+
|
44
|
+
})();
|