jcov 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +5 -0
  3. data/LICENSE +19 -0
  4. data/README.rdoc +109 -0
  5. data/Rakefile +2 -0
  6. data/bin/jcov +36 -0
  7. data/examples/jasmine/.gitignore +1 -0
  8. data/examples/jasmine/jasmine/ConsoleReporter.js +177 -0
  9. data/examples/jasmine/jasmine/MIT.LICENSE.txt +20 -0
  10. data/examples/jasmine/jasmine/jasmine.js +2476 -0
  11. data/examples/jasmine/jasmine/runner.js +28 -0
  12. data/examples/jasmine/javascripts/mean.js +11 -0
  13. data/examples/jasmine/jcov.yml +8 -0
  14. data/examples/jasmine/tests/mean_spec.js +15 -0
  15. data/examples/jspec/.gitignore +1 -0
  16. data/examples/jspec/javascripts/mean.js +11 -0
  17. data/examples/jspec/jcov.yml +8 -0
  18. data/examples/jspec/jspec/ext/slowness.js +43 -0
  19. data/examples/jspec/jspec/ext/trace.js +28 -0
  20. data/examples/jspec/jspec/lib/MIT.LICENSE.txt +7 -0
  21. data/examples/jspec/jspec/lib/jspec.js +1925 -0
  22. data/examples/jspec/jspec/runner.js +66 -0
  23. data/examples/jspec/tests/mean_spec.js +15 -0
  24. data/features/configuration.feature +134 -0
  25. data/features/coverage.feature +246 -0
  26. data/features/html_report.feature +130 -0
  27. data/features/javascript_interface.feature +72 -0
  28. data/features/reporting.feature +108 -0
  29. data/features/run.feature +217 -0
  30. data/features/step_definitions/report_steps.rb +54 -0
  31. data/features/support/env.rb +29 -0
  32. data/jcov.gemspec +29 -0
  33. data/lib/jcov.rb +11 -0
  34. data/lib/jcov/commands.rb +47 -0
  35. data/lib/jcov/configuration.rb +54 -0
  36. data/lib/jcov/coverage.rb +151 -0
  37. data/lib/jcov/reporter.rb +2 -0
  38. data/lib/jcov/reporter/console_reporter.rb +107 -0
  39. data/lib/jcov/reporter/file.html.erb +26 -0
  40. data/lib/jcov/reporter/html_reporter.rb +64 -0
  41. data/lib/jcov/reporter/report.css +53 -0
  42. data/lib/jcov/reporter/report.html.erb +45 -0
  43. data/lib/jcov/runner.rb +100 -0
  44. data/lib/jcov/version.rb +3 -0
  45. 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
@@ -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
@@ -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,2 @@
1
+ module JCov::Reporter
2
+ 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