From e4c15bcd51ac6887396860b94b62ecba660a2240 Mon Sep 17 00:00:00 2001 From: "A. Speller" Date: Fri, 14 Sep 2012 20:38:59 +0100 Subject: [PATCH] Proof of concept coverage implementation --- Gemfile | 1 + lib/guard/jasmine.rb | 2 + lib/guard/jasmine/jscoverage.rb | 28 ++++ lib/guard/jasmine/phantomjs/lib/reporter.js | 29 +++- .../jasmine/phantomjs/src/reporter.coffee | 18 +++ lib/guard/jasmine/runner.rb | 42 +++++- spec/guard/jasmine/runner_spec.rb | 129 ++++++++++++++++++ 7 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 lib/guard/jasmine/jscoverage.rb diff --git a/Gemfile b/Gemfile index ca5e307..254a1d7 100644 --- a/Gemfile +++ b/Gemfile @@ -18,3 +18,4 @@ gem 'yard' gem 'redcarpet' gem 'pry' gem 'yajl-ruby' +gem 'tilt' diff --git a/lib/guard/jasmine.rb b/lib/guard/jasmine.rb index f6b4152..f4cfc0e 100644 --- a/lib/guard/jasmine.rb +++ b/lib/guard/jasmine.rb @@ -4,6 +4,8 @@ require 'guard/guard' require 'guard/watcher' +require 'guard/jasmine/jscoverage' + module Guard # The Jasmine guard that gets notifications about the following diff --git a/lib/guard/jasmine/jscoverage.rb b/lib/guard/jasmine/jscoverage.rb new file mode 100644 index 0000000..72f6aa8 --- /dev/null +++ b/lib/guard/jasmine/jscoverage.rb @@ -0,0 +1,28 @@ +# coding: utf-8 +require 'tilt' +class JasmineCoverage < Tilt::Template + def prepare + end + + def evaluate(context, locals) + return data unless file.include?(Rails.root.to_s) + return data unless file.include?(Rails.root.join('app', 'assets').to_s) + filename = File.basename(file, '.coffee') + puts "Generating jscoverage instrumented file for #{file.gsub(Rails.root.to_s, '')}" + Dir.mktmpdir do |path| + Dir.mkdir File.join(path, 'in') + File.write File.join(path, 'in', filename), data + `jscoverage --encoding=UTF-8 #{path}/in #{path}/out` + raise "Could not genrate jscoverage instrumented file for #{file}" unless $?.success? + File.read File.join(path, 'out', filename) + end + end +end + +if ENV['JSCOVERAGE'] == 'true' and defined?(Rails) + class GuardJasmineCoverageEngine < Rails::Engine + config.after_initialize do |app| + app.assets.register_postprocessor 'application/javascript', JasmineCoverage + end + end +end diff --git a/lib/guard/jasmine/phantomjs/lib/reporter.js b/lib/guard/jasmine/phantomjs/lib/reporter.js index fb6549f..1bd49cc 100644 --- a/lib/guard/jasmine/phantomjs/lib/reporter.js +++ b/lib/guard/jasmine/phantomjs/lib/reporter.js @@ -1,5 +1,6 @@ (function() { - var ConsoleReporter; + var ConsoleReporter, + __hasProp = {}.hasOwnProperty; ConsoleReporter = (function() { @@ -30,7 +31,7 @@ if (!spec.results().skipped) { specResult = { id: spec.id, - description: spec.description, + description: '' + spec.description, passed: spec.results().failedCount === 0 }; if (spec.results().failedCount !== 0) { @@ -56,7 +57,7 @@ suiteResult = { id: suite.id, parent: (_ref = suite.parentSuite) != null ? _ref.id : void 0, - description: suite.description, + description: '' + suite.description, passed: suite.results().failedCount === 0, specs: this.currentSpecs[suite.id] || [], suites: [] @@ -76,7 +77,7 @@ }; ConsoleReporter.prototype.reportRunnerResults = function(runner) { - var end, runtime; + var end, executedLoc, file, fileLoc, lines, runtime, totalExecutedLoc, totalLoc, _ref; runtime = (new Date().getTime() - this.startTime) / 1000; this.runnerResult['passed'] = runner.results().failedCount === 0; this.runnerResult['stats'] = { @@ -84,6 +85,26 @@ failures: runner.results().failedCount, time: runtime }; + if (window._$jscoverage != null) { + this.runnerResult['coverage'] = {}; + totalLoc = 0; + totalExecutedLoc = 0; + _ref = window._$jscoverage; + for (file in _ref) { + if (!__hasProp.call(_ref, file)) continue; + lines = window._$jscoverage[file]; + fileLoc = lines.filter(function(line) { + return line != null; + }).length; + totalLoc += fileLoc; + executedLoc = lines.filter(function(line) { + return (line != null) && line > 0; + }).length; + totalExecutedLoc += executedLoc; + this.runnerResult['coverage'][file] = (executedLoc / fileLoc) * 100; + } + this.runnerResult['coverage']['total'] = (totalExecutedLoc / totalLoc) * 100; + } end = function() { return console.log("RUNNER_END"); }; diff --git a/lib/guard/jasmine/phantomjs/src/reporter.coffee b/lib/guard/jasmine/phantomjs/src/reporter.coffee index 2c62af5..9f51a3b 100644 --- a/lib/guard/jasmine/phantomjs/src/reporter.coffee +++ b/lib/guard/jasmine/phantomjs/src/reporter.coffee @@ -85,6 +85,24 @@ class ConsoleReporter failures: runner.results().failedCount time: runtime } + + # Report jscoverage results if jscoverage data present + if window._$jscoverage? + @runnerResult['coverage'] = {} + totalLoc = 0 + totalExecutedLoc = 0 + + for own file of window._$jscoverage + lines = window._$jscoverage[file] + fileLoc = lines.filter((line) -> line?).length + totalLoc += fileLoc + executedLoc = lines.filter((line) -> line? and line > 0).length + totalExecutedLoc += executedLoc + # Report one line for each file + @runnerResult['coverage'][file] = (executedLoc / fileLoc) * 100 + + # Report total coverage + @runnerResult['coverage']['total'] = (totalExecutedLoc / totalLoc) * 100 # Delay the end runner message, so that logs and errors can be retreived in between end = -> console.log "RUNNER_END" diff --git a/lib/guard/jasmine/runner.rb b/lib/guard/jasmine/runner.rb index 28971b6..9a1645d 100644 --- a/lib/guard/jasmine/runner.rb +++ b/lib/guard/jasmine/runner.rb @@ -167,7 +167,15 @@ def evaluate_response(output, file, options) result['file'] = file notify_spec_result(result, options) end - + + if result['coverage'] + notify_coverage_result(result['coverage'], options) + + if result['coverage']['total'] < 100 + result['error'] = "Coverage below 100%" + end + end + result rescue => e @@ -230,6 +238,38 @@ def notify_spec_result(result, options) Formatter.info("Done.\n") end + + + # Notification about the coverage of a spec run, success or failure, + # and some stats. + # + # @param [Hash] coverage the coverage hash from the JSON + # @param [Hash] options the options for the execution + # @option options [Boolean] :notification show notifications + # @option options [Boolean] :hide_success hide success message notification + # + def notify_coverage_result(coverage, options) + percentage = '%.0f%' % coverage['total'] + if coverage['total'] < 100.0 + coverage.each_pair do |file, value| + next if file == 'total' + next unless value + coverage_for_file = "#{file}: #{'%.0f' % value}%" + if value < 100 + Formatter.error(coverage_for_file) + else + Formatter.success(coverage_for_file) + end + end + Formatter.error("Code Coverage: #{percentage}") + Formatter.notify("#{percentage} covered", :title => "Code coverage below 100%", :image => :failed, :priority => 2) if options[:notification] + else + Formatter.success('Code Coverage: 100%') + Formatter.notify("#{percentage} covered", :title => 'Code Coverage') if options[:notification] && !options[:hide_success] + end + rescue Exception => e + puts e.backtrace + end # Specdoc like formatting of the result. # diff --git a/spec/guard/jasmine/runner_spec.rb b/spec/guard/jasmine/runner_spec.rb index 502c091..059a953 100644 --- a/spec/guard/jasmine/runner_spec.rb +++ b/spec/guard/jasmine/runner_spec.rb @@ -129,6 +129,64 @@ JSON end + let(:phantomjs_partial_coverage_response) do + <<-JSON + { + "passed": true, + "stats": { + "specs": 1, + "failures": 0, + "time": 0.009 + }, + "coverage": { + "application.js": 50.12, + "todo.js": 100.0, + "total": 84.78260869565217 + }, + "suites": [ + { + "description": "Success suite", + "specs": [ + { + "description": "Success test tests something", + "passed": true + } + ] + } + ] + } + JSON + end + + let(:phantomjs_full_coverage_response) do + <<-JSON + { + "passed": true, + "stats": { + "specs": 1, + "failures": 0, + "time": 0.009 + }, + "coverage": { + "application.js": 100.0, + "todo.js": 100.0, + "total": 100.0 + }, + "suites": [ + { + "description": "Success suite", + "specs": [ + { + "description": "Success test tests something", + "passed": true + } + ] + } + ] + } + JSON + end + let(:phantomjs_command) do "/usr/local/bin/phantomjs #{ @project_path }/lib/guard/jasmine/phantomjs/guard-jasmine.coffee" end @@ -195,6 +253,11 @@ response.first.should be_false response.last.should =~ [] end + + it "does not show coverage" do + runner.should_not_receive(:notify_coverage_result) + runner.run(['spec/javascripts/a.js.coffee'], defaults) + end context 'with notifications' do it 'shows an error notification' do @@ -234,6 +297,12 @@ response.last.should =~ ['spec/javascripts/x/b.js.coffee'] end + + it "does not show coverage" do + runner.should_not_receive(:notify_coverage_result) + runner.run(['spec/javascripts/a.js.coffee'], defaults) + end + context 'with the specdoc set to :never' do context 'and console and errors set to :never' do it 'shows the summary in the console' do @@ -572,7 +641,67 @@ response.first.should be_true response.last.should =~ [] end + + context "with coverage" do + + context 'when coverage is present' do + before do + IO.stub(:popen).and_return StringIO.new(phantomjs_full_coverage_response) + end + + + it 'notifies coverage when present' do + runner.should_receive(:notify_coverage_result) + runner.run(['spec/javascripts/t.js.coffee'], defaults) + end + + it 'shows a success notification' do + formatter.should_receive(:notify).with("1 spec, 0 failures\nin 0.009 seconds", :title=>"Jasmine suite passed") + formatter.should_receive(:notify).with('100% covered', :title => "Code Coverage") + runner.run(['spec/javascripts/t.js.coffee'], defaults) + end + + it 'logs the coverage to the console' do + formatter.should_receive(:success).with('Code Coverage: 100%') + runner.run(['spec/javascripts/t.js.coffee'], defaults) + end + + + context 'when coverage is below 100%' do + before do + IO.stub(:popen).and_return StringIO.new(phantomjs_partial_coverage_response) + end + + it 'shows a failure notification' do + formatter.should_receive(:notify).with( + "85% covered", + :title => 'Code coverage below 100%', + :image => :failed, + :priority => 2 + ) + runner.run(['spec/javascripts/t.js.coffee'], defaults) + end + + it 'logs the coverage to the console for all the files' do + formatter.should_receive(:error).with('Code Coverage: 85%') + formatter.should_receive(:error).with('application.js: 50%') + formatter.should_receive(:success).with('todo.js: 100%') + + runner.run(['spec/javascripts/t.js.coffee'], defaults) + end + + it 'fails the build' do + response = runner.run(['spec/javascripts/x/b.js.coffee'], defaults) + response.first.should be_false + response.last.should =~ [] + end + + end + end + + end + context 'with the specdoc set to :always' do it 'shows the specdoc in the console' do formatter.should_receive(:info).with(