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.
@@ -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 = ["dmcinnes@yp.com"]
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"
@@ -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'
@@ -4,8 +4,8 @@ module JCov::Commands
4
4
 
5
5
  # the check command
6
6
  class Check
7
- def initialize args, options
8
- config = JCov::Configuration.new options.config
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 args, options
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.config)
29
+ config = JCov::Configuration.new(options)
30
30
 
31
- runner = JCov::Coverage::CoverageRunner.new(config, options)
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
@@ -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 file
20
- @filename = find_file(file)
21
- @config = DEFAULTS.merge(@filename && YAML.load_file(@filename) || {})
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
 
@@ -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
@@ -1,151 +1,76 @@
1
1
  module JCov
2
- module Coverage
3
2
 
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)
3
+ class Coverage
35
4
 
36
- override_runners_load_method
37
- add_coverage_method_to_context
5
+ attr_accessor :loader
6
+ attr_accessor :config
38
7
 
39
- @visitor = CoverageVisitor.new(coverage_data)
40
- @parser = RKelly::Parser.new
8
+ def initialize(config)
9
+ @config = config
41
10
 
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
11
+ # for loading and instrumenting fiels
12
+ @loader = JCov::Loader.new(coverable_files, :dump => config.dump)
13
+ end
57
14
 
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
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
- # 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
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
- 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
45
+ # ignore nil values
46
+ total = lines.values.compact.count
101
47
 
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
48
+ [file, total, cover]
130
49
  end
131
- @reduced_coverage_data
132
- end
50
+ end
133
51
 
134
- def get_binding
135
- binding
136
- end
52
+ def coverage_data
53
+ @loader.coverage_data
54
+ end
137
55
 
138
- private
56
+ # for coverage reporting
57
+ def get_binding
58
+ binding
59
+ end
139
60
 
140
- def add_coverage_method_to_context
141
- runner.context['_coverage_tick'] = self.method('_coverage_tick')
142
- end
61
+ private
143
62
 
144
- def override_runners_load_method
145
- runner.context['load'] = self.method('load')
146
- end
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
@@ -0,0 +1,6 @@
1
+ module JCov
2
+
3
+ class ParseError < StandardError
4
+ end
5
+
6
+ end
@@ -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
+ })();