jcov 1.0.1 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
})();
|