guard-jasmine 1.19.2 → 2.0.0beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -234
  3. data/lib/generators/guard_jasmine/install_generator.rb +17 -0
  4. data/lib/generators/guard_jasmine/templates/Guardfile +9 -0
  5. data/lib/guard/jasmine.rb +18 -15
  6. data/lib/guard/jasmine/cli.rb +5 -5
  7. data/lib/guard/jasmine/formatter.rb +10 -0
  8. data/lib/guard/jasmine/inspector.rb +1 -2
  9. data/lib/guard/jasmine/phantomjs/guard-jasmine.js +54 -180
  10. data/lib/guard/jasmine/phantomjs/guard-reporter.js +187 -0
  11. data/lib/guard/jasmine/phantomjs/src/guard-jasmine.coffee +101 -0
  12. data/lib/guard/jasmine/phantomjs/src/guard-reporter.coffee +109 -0
  13. data/lib/guard/jasmine/phantomjs/test/guard-reporter_spec.coffee +41 -0
  14. data/lib/guard/jasmine/runner.rb +178 -268
  15. data/lib/guard/jasmine/server.rb +17 -3
  16. data/lib/guard/jasmine/util.rb +1 -7
  17. data/lib/guard/jasmine/version.rb +1 -1
  18. metadata +135 -26
  19. data/lib/guard/jasmine/phantomjs/guard-jasmine.coffee +0 -193
  20. data/lib/guard/jasmine/phantomjs/lib/console.js +0 -188
  21. data/lib/guard/jasmine/phantomjs/lib/junit_reporter.js +0 -224
  22. data/lib/guard/jasmine/phantomjs/lib/reporter.js +0 -144
  23. data/lib/guard/jasmine/phantomjs/lib/result.js +0 -155
  24. data/lib/guard/jasmine/phantomjs/src/console.coffee +0 -149
  25. data/lib/guard/jasmine/phantomjs/src/reporter.coffee +0 -139
  26. data/lib/guard/jasmine/phantomjs/src/result.coffee +0 -95
  27. data/lib/guard/jasmine/phantomjs/test/console_spec.coffee +0 -125
  28. data/lib/guard/jasmine/phantomjs/test/reporter_spec.coffee +0 -0
  29. data/lib/guard/jasmine/phantomjs/test/result_spec.coffee +0 -311
@@ -0,0 +1,101 @@
1
+
2
+ # Set default values
3
+ options =
4
+ url: phantom.args[0] || 'http://127.0.0.1:3000/jasmine'
5
+ timeout: parseInt(phantom.args[1] || 10000)
6
+
7
+ # Create the web page.
8
+ page = require('webpage').create()
9
+
10
+ # Catch JavaScript errors
11
+ # abort the request and return the error
12
+ page.onError = (message, trace) ->
13
+ reportError "Javascript error encountered on Jasmine test page: #{ message }", trace
14
+
15
+ # Once the page is initialized, setup the script for
16
+ # the GuardReporter class
17
+ page.onInitialized = ->
18
+ page.injectJs 'guard-reporter.js'
19
+ page.evaluate ->
20
+ window.onload = ->
21
+ window.reporter = new GuardReporter()
22
+ window.jasmine.getEnv().addReporter(window.reporter) if window.jasmine
23
+
24
+ # Once the page is finished loading
25
+ page.onLoadFinished = (status)->
26
+ if status isnt 'success'
27
+ reportError "Unable to access Jasmine specs at #{ options.url }, page returned status: #{status}"
28
+ else
29
+ waitFor reporterReady, jasmineAvailable, options.timeout, reporterMissing
30
+
31
+ # Open web page, which will kick off the Jasmine test runner
32
+ page.open options.url
33
+
34
+ # Test if Jasmine and guard has been loaded
35
+ reporterReady = ->
36
+ page.evaluate ->
37
+ window.jasmine && window.reporter
38
+
39
+ # Start specs after they are have been loaded
40
+ jasmineAvailable = ->
41
+ waitFor specsDone, exitSuccessfully, options.timeout, specsTimedout
42
+
43
+ # Error message for when jasmine never loaded asynchronously
44
+ reporterMissing = ->
45
+ text = page.evaluate -> document.getElementsByTagName('body')[0]?.innerText
46
+ reportError """
47
+ The reporter is not available!
48
+ Perhaps the url ( #{ options.url } ) is incorrect?
49
+
50
+ #{ text }
51
+ """
52
+
53
+ # tests if the resultComplete flag is set on the reporter
54
+ specsDone = ->
55
+ result = page.evaluate ->
56
+ window.reporter.resultComplete
57
+
58
+ # We should end up here. Logs the results as JSON and exits
59
+ exitSuccessfully = ->
60
+ results = page.evaluate -> window.reporter.results()
61
+ console.log JSON.stringify( results )
62
+ phantom.exit()
63
+
64
+
65
+ # Error message for when specs time out
66
+ specsTimedout = ->
67
+ text = page.evaluate -> document.getElementsByTagName('body')[0]?.innerText
68
+ reportError """
69
+ Timeout waiting for the Jasmine test results!
70
+
71
+ #{ text }
72
+ """
73
+
74
+ # Wait until the test condition is true or a timeout occurs.
75
+ #
76
+ # @param [Function] test the test that returns true if condition is met
77
+ # @param [Function] ready the action when the condition is fulfilled
78
+ # @param [Number] timeout the max amount of time to wait in milliseconds
79
+ #
80
+ waitFor = (test, ready, timeout = 10000, timeoutFunction)->
81
+ condition = false
82
+ interval = undefined
83
+ start = Date.now(0)
84
+ wait = ->
85
+ if !condition && (Date.now() - start < timeout)
86
+ condition = test()
87
+ else
88
+ clearInterval interval
89
+ if condition
90
+ ready()
91
+ else
92
+ timeoutFunction()
93
+ interval = setInterval( wait, 250 )
94
+
95
+ # Logs the error to the console as JSON and exits with status '1'
96
+ reportError = (msg, trace=[])->
97
+ if 0 == trace.length
98
+ err = new Error();
99
+ trace = err.stack
100
+ console.log JSON.stringify({ error: msg, trace: trace })
101
+ phantom.exit(1)
@@ -0,0 +1,109 @@
1
+ # Capture statements that were logged to the console
2
+ # during spec execution.
3
+ #
4
+ # To do so it substitues it's own functions for the console.<levels>
5
+ #
6
+ extendObject = (a, b)->
7
+ for key,value of b
8
+ a[key] = value if b.hasOwnProperty(key)
9
+ return a
10
+
11
+
12
+ class ConsoleCapture
13
+ # Instead of attempting to de-activate the console dot reporter in hacky ways,
14
+ # just ignore it's output
15
+ @DOT_REPORTER_MATCH = /\[\d+m[F.]..0m/
16
+ @levels: ['log','info','warn','error','debug' ]
17
+ @original = console
18
+
19
+ @original_levels = {}
20
+ @original_levels[level] = console[level] for level in ConsoleCapture.levels
21
+
22
+ constructor:->
23
+ @original = {}
24
+ @captured = []
25
+ this._reassign_level( level ) for level in ConsoleCapture.levels
26
+
27
+ revert: ->
28
+ for level in ConsoleCapture.levels
29
+ ConsoleCapture.original[level] = ConsoleCapture.original_levels[level]
30
+
31
+ _reassign_level: ( level )->
32
+ my = this
33
+ console[level] = ->
34
+ args = Array.prototype.slice.call(arguments, 0)
35
+ return if args[0] && args[0].toString && args[0].toString().match( ConsoleCapture.DOT_REPORTER_MATCH )
36
+ my.captured.push( [ level ].concat( args ) )
37
+ ConsoleCapture.original_levels[ level ].apply( ConsoleCapture.original, arguments )
38
+
39
+
40
+ # Implements a Jasmine reporter
41
+ class GuardReporter
42
+ @STACK_MATCHER=new RegExp("__spec__\/(.*):([0-9]+)","g")
43
+
44
+ jasmineStarted: ->
45
+ @console = new ConsoleCapture();
46
+ @startedAt = Date.now()
47
+ @currentSuite = { suites: [] }
48
+ @stack = [ @currentSuite ]
49
+
50
+ suiteStarted: (suite)->
51
+ suite = extendObject({ specs: [], suites: [] }, suite )
52
+ @currentSuite.suites.push( suite )
53
+ @currentSuite = suite
54
+ @stack.push(suite)
55
+
56
+ suiteDone: (Suite)->
57
+ @stack.pop()
58
+ @currentSuite = @stack[@stack.length-1]
59
+
60
+ jasmineDone: ->
61
+ @resultComplete = true
62
+
63
+ specDone: (spec)->
64
+ @resultReceived = true
65
+ spec = extendObject({ logs: @console.captured, errors: [] }, spec )
66
+ for failure in spec.failedExpectations
67
+ error = extendObject({trace:[]}, failure )
68
+ while match = GuardReporter.STACK_MATCHER.exec( failure.stack )
69
+ error.trace.push({ file: match[1], line: parseInt(match[2]) })
70
+ delete error.stack
71
+ spec.errors.push( error )
72
+ delete spec.failedExpectations
73
+ @currentSuite.specs.push( spec )
74
+
75
+ this.resetConsoleLog()
76
+ spec
77
+
78
+ resetConsoleLog: ->
79
+ @console.revert()
80
+ @console = new ConsoleCapture
81
+
82
+ eachSuite: (suite)->
83
+ suites = [].concat( suite.suites )
84
+ for suite in suite.suites
85
+ suites = suites.concat( this.eachSuite(suite) )
86
+ suites
87
+
88
+ results: ->
89
+ stats = {
90
+ time : ( Date.now() - @startedAt ) / 1000
91
+ specs : 0
92
+ failed : 0
93
+ pending : 0
94
+ disabled : 0
95
+ }
96
+ for suite in this.eachSuite(@stack[0])
97
+ stats.specs += suite.specs.length
98
+ for spec in suite.specs
99
+ stats[spec.status] += 1 unless undefined == stats[spec.status]
100
+ {
101
+ jasmine_version: jasmine?.version
102
+ stats: stats
103
+ suites: @stack[0].suites
104
+ }
105
+
106
+ if typeof module isnt 'undefined' and module.exports
107
+ module.exports = GuardReporter
108
+ else
109
+ window.GuardReporter = GuardReporter
@@ -0,0 +1,41 @@
1
+ sinon = require 'sinon'
2
+ {expect} = require 'chai'
3
+
4
+ GuardReporter = require '../src/guard-reporter'
5
+
6
+ describe 'Reporter', ->
7
+ beforeEach ->
8
+ @reporter = new GuardReporter
9
+ @reporter.jasmineStarted()
10
+
11
+ it 'captures the console', ->
12
+ @reporter.suiteStarted({name:'Blank'})
13
+ console.log("A %s Logging", "Test")
14
+ console.warn("This is your last warning")
15
+ @reporter.specDone( { failedExpectations: [] } )
16
+ expect( @reporter.results() )
17
+ .to.have.deep.property('.suites[0].specs[0].logs')
18
+ .and.equal([
19
+ [ 'log', 'A %s Logging', "Test" ],
20
+ [ 'warn', 'This is your last warning' ]
21
+ ])
22
+
23
+ it "reports counts", ->
24
+ @reporter.suiteStarted({name:'Blank'})
25
+ console.log("A %s Logging", "Test")
26
+ console.warn("This is your last warning")
27
+ @reporter.specDone( { status: 'passed', failedExpectations: [] } )
28
+ @reporter.specDone( { status: 'failed', failedExpectations: [{
29
+ matcherName:"toEqual",
30
+ message: "Expected 2 to equal 5"
31
+ }] })
32
+ @reporter.specDone( { status: 'passed', failedExpectations: [] } )
33
+ @reporter.specDone( { status: 'pending', failedExpectations: [] } )
34
+ results = @reporter.results()
35
+ expect( results ).to.have.property('stats')
36
+ expect( results.stats )
37
+ .to.have.property('specs').and.equal(4)
38
+ expect( results.stats )
39
+ .to.have.property('failed').and.equal(1)
40
+ expect( results.stats )
41
+ .to.have.property('pending').and.equal(1)
@@ -11,53 +11,68 @@ module Guard
11
11
  # evaluates the JSON response from the PhantomJS Script `guard_jasmine.coffee`,
12
12
  # writes the result to the console and triggers optional system notifications.
13
13
  #
14
- module Runner
15
- extend ::Guard::Jasmine::Util
16
-
17
- class << self
18
-
19
- # Name of the coverage threshold options
20
- THRESHOLDS = [:statements_threshold, :functions_threshold, :branches_threshold, :lines_threshold]
21
-
22
- # Run the supplied specs.
23
- #
24
- # @param [Array<String>] paths the spec files or directories
25
- # @param [Hash] options the options for the execution
26
- # @option options [String] :jasmine_url the url of the Jasmine test runner
27
- # @option options [String] :phantomjs_bin the location of the PhantomJS binary
28
- # @option options [Integer] :timeout the maximum time in seconds to wait for the spec runner to finish
29
- # @option options [String] :rackup_config custom rackup config to use
30
- # @option options [Boolean] :notification show notifications
31
- # @option options [Boolean] :hide_success hide success message notification
32
- # @option options [Integer] :max_error_notify maximum error notifications to show
33
- # @option options [Symbol] :specdoc options for the specdoc output, either :always, :never
34
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
35
- # @option options [String] :spec_dir the directory with the Jasmine specs
36
- # @return [Boolean, Array<String>] the status of the run and the failed files
37
- #
38
- def run(paths, options = { })
39
- return [false, []] if paths.empty?
14
+ class Runner
15
+ include ::Guard::Jasmine::Util
16
+
17
+ attr_reader :options
18
+
19
+ # Name of the coverage threshold options
20
+ THRESHOLDS = [:statements_threshold, :functions_threshold, :branches_threshold, :lines_threshold]
21
+
22
+ # Run the supplied specs.
23
+ #
24
+ # @param [Hash] options the options for the execution
25
+ # @option options [String] :jasmine_url the url of the Jasmine test runner
26
+ # @option options [String] :phantomjs_bin the location of the PhantomJS binary
27
+ # @option options [Integer] :timeout the maximum time in seconds to wait for the spec runner to finish
28
+ # @option options [String] :rackup_config custom rackup config to use
29
+ # @option options [Boolean] :notification show notifications
30
+ # @option options [Boolean] :hide_success hide success message notification
31
+ # @option options [Integer] :max_error_notify maximum error notifications to show
32
+ # @option options [Symbol] :specdoc options for the specdoc output, either :always, :never
33
+ # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
34
+ # @option options [String] :spec_dir the directory with the Jasmine specs
35
+ # @option options [Hash] :query_params Parameters to pass along with the request
36
+ # @option options [Boolean] :debug display raw JSON output from the runner
37
+ #
38
+ def initialize(options)
39
+ @options = options
40
+ end
40
41
 
41
- notify_start_message(paths, options)
42
+ # @param [Array<String>] paths the spec files or directories
43
+ # @return [Hash<String,Array>] keys for the spec_file, value is array of failure messages.
44
+ # Only specs with failures will be returned. Therefore an empty? return hash indicates success.
45
+ def run(paths, per_run_options = {})
46
+ previous_options = @options
47
+ @options.merge!( per_run_options )
42
48
 
43
- results = paths.inject([]) do |results, file|
44
- results << evaluate_response(run_jasmine_spec(file, options), file, options) if File.exist?(file_and_line_number_parts(file)[0])
49
+ return {} if paths.empty?
45
50
 
46
- results
47
- end.compact
51
+ notify_start_message(paths)
48
52
 
49
- [response_status_for(results), failed_paths_from(results)]
53
+ run_results = paths.each_with_object({}) do |file, results|
54
+ if File.exist?(file_and_line_number_parts(file)[0])
55
+ results[file] = evaluate_response(run_jasmine_spec(file), file)
56
+ end
57
+ end
58
+ # return the errors
59
+ return run_results.each_with_object({}) do | spec_run, hash |
60
+ file, r = spec_run
61
+ errors = collect_spec_errors(r['suites']||[])
62
+ errors.push( r['error'] ) if r.has_key? 'error'
63
+ hash[file] = errors unless errors.empty?
50
64
  end
65
+ ensure
66
+ @options=previous_options
67
+ end
51
68
 
52
- private
69
+ private
53
70
 
54
- # Shows a notification in the console that the runner starts.
55
- #
71
+ # Shows a notification in the console that the runner starts.
72
+ #
56
73
  # @param [Array<String>] paths the spec files or directories
57
- # @param [Hash] options the options for the execution
58
- # @option options [String] :spec_dir the directory with the Jasmine specs
59
74
  #
60
- def notify_start_message(paths, options)
75
+ def notify_start_message(paths)
61
76
  message = if paths == [options[:spec_dir]]
62
77
  'Run all Jasmine suites'
63
78
  else
@@ -67,33 +82,12 @@ module Guard
67
82
  Formatter.info(message, reset: true)
68
83
  end
69
84
 
70
- # Returns the failed spec file names.
71
- #
72
- # @param [Array<Object>] results the spec runner results
73
- # @return [Array<String>] the list of failed spec files
74
- #
75
- def failed_paths_from(results)
76
- results.map { |r| !r['passed'] ? r['file'] : nil }.compact
77
- end
78
-
79
- # Returns the response status for the given result set.
80
- #
81
- # @param [Array<Object>] results the spec runner results
82
- # @return [Boolean] whether it has passed or not
83
- #
84
- def response_status_for(results)
85
- results.none? { |r| r.has_key?('error') || !r['passed'] }
86
- end
87
-
88
85
  # Run the Jasmine spec by executing the PhantomJS script.
89
86
  #
90
87
  # @param [String] file the path of the spec
91
- # @param [Hash] options the options for the execution
92
- # @option options [Integer] :timeout the maximum time in seconds to wait for the spec runner to finish
93
88
  #
94
- def run_jasmine_spec(file, options)
95
- suite = jasmine_suite(file, options)
96
- Formatter.info("Run Jasmine suite at #{ suite }")
89
+ def run_jasmine_spec(file)
90
+ suite = jasmine_suite(file)
97
91
 
98
92
  arguments = [
99
93
  options[:timeout] * 1000,
@@ -105,30 +99,27 @@ module Guard
105
99
  options[:junit_consolidate],
106
100
  "'#{ options[:junit_save_path] }'"
107
101
  ]
108
-
109
- IO.popen("#{ phantomjs_command(options) } \"#{ suite }\" #{ arguments.collect { |i| i.to_s }.join(' ')}", 'r:UTF-8')
102
+ cmd = "#{ phantomjs_command } \"#{ suite }\" #{ arguments.collect { |i| i.to_s }.join(' ')}"
103
+ IO.popen(cmd, 'r:UTF-8')
110
104
  end
111
105
 
112
106
  # Get the PhantomJS binary and script to execute.
113
107
  #
114
- # @param [Hash] options the options for the execution
115
- # @option options [String] :phantomjs_bin the location of the PhantomJS binary
116
108
  # @return [String] the command
117
109
  #
118
- def phantomjs_command(options)
110
+ def phantomjs_command
119
111
  options[:phantomjs_bin] + ' ' + phantomjs_script
112
+ #options[:phantomjs_bin] + ' --remote-debugger-port=9000 ' + phantomjs_script
120
113
  end
121
114
 
122
115
  # Get the Jasmine test runner URL with the appended suite name
123
116
  # that acts as the spec filter.
124
117
  #
125
118
  # @param [String] file the spec file
126
- # @param [Hash] options the options for the execution
127
- # @option options [String] :jasmine_url the url of the Jasmine test runner
128
119
  # @return [String] the Jasmine url
129
120
  #
130
- def jasmine_suite(file, options)
131
- options[:jasmine_url] + query_string_for_suite(file, options)
121
+ def jasmine_suite(file)
122
+ options[:jasmine_url] + query_string_for_suite(file)
132
123
  end
133
124
 
134
125
  # Get the PhantomJS script that executes the spec and extracts
@@ -144,22 +135,17 @@ module Guard
144
135
  # will be run.
145
136
  #
146
137
  # @param [String] file the spec file
147
- # @param [Hash] options the options for the execution
148
- # @option options [String] :spec_dir the directory with the Jasmine specs
149
138
  # @return [String] the suite name
150
139
  #
151
- def query_string_for_suite(file, options)
152
- return '' if file == options[:spec_dir]
153
-
154
- query_string = query_string_for_suite_from_line_number(file, options)
155
-
156
- unless query_string
157
- query_string = query_string_for_suite_from_first_describe(file, options)
140
+ def query_string_for_suite(file)
141
+ params = {}
142
+ if options[:query_params]
143
+ params.merge!(options[:query_params])
158
144
  end
159
-
160
- query_string = query_string ? "?spec=#{ query_string }" : ''
161
-
162
- URI.encode(query_string)
145
+ if file != options[:spec_dir]
146
+ params[:spec] = suite_from_line_number(file) || suite_from_first_describe(file)
147
+ end
148
+ params.empty? ? "" : "?"+URI.encode_www_form(params)
163
149
  end
164
150
 
165
151
  # When providing a line number by either the option or by
@@ -167,11 +153,9 @@ module Guard
167
153
  # fromt the corresponding line number in the file.
168
154
  #
169
155
  # @param [String] file the spec file
170
- # @param [Hash] options the options for the execution
171
- # @option options [Fixnum] :line_number the line number to run
172
156
  # @return [String] the suite name
173
157
  #
174
- def query_string_for_suite_from_line_number(file, options)
158
+ def suite_from_line_number(file)
175
159
  file_name, line_number = file_and_line_number_parts(file)
176
160
  line_number ||= options[:line_number]
177
161
 
@@ -196,12 +180,11 @@ module Guard
196
180
  # found.
197
181
  #
198
182
  # @param [String] file the spec file
199
- # @param [Hash] options the options for the execution
200
183
  # @return [String] the suite name
201
184
  #
202
- def query_string_for_suite_from_first_describe(file, options)
185
+ def suite_from_first_describe(file)
203
186
  File.foreach(file) do |line|
204
- if line =~ /describe\s*[("']+(.*?)["')]+/
187
+ if line =~ /describe\s*[("']+(.*?)["')]+/ #'
205
188
  return $1
206
189
  end
207
190
  end
@@ -241,7 +224,7 @@ module Guard
241
224
  # @return [String] the extracted title
242
225
  #
243
226
  def spec_title(line)
244
- line[/['"](.+?)['"]/, 1]
227
+ line[/['"](.+?)["']/, 1]
245
228
  end
246
229
 
247
230
  # Evaluates the JSON response that the PhantomJS script
@@ -250,33 +233,31 @@ module Guard
250
233
  #
251
234
  # @param [String] output the JSON output the spec run
252
235
  # @param [String] file the file name of the spec
253
- # @param [Hash] options the options for the execution
254
- # @return [Hash] the suite result
236
+ # @return [Hash] results of the suite's specs
255
237
  #
256
- def evaluate_response(output, file, options)
238
+ def evaluate_response(output, file)
257
239
  json = output.read
258
240
  json = json.encode('UTF-8') if json.respond_to?(:encode)
259
-
260
241
  begin
261
242
  result = MultiJson.decode(json, { max_nesting: false })
262
243
  raise 'No response from Jasmine runner' if !result && options[:is_cli]
263
-
244
+ pp result if options[:debug]
264
245
  if result['error']
265
246
  if options[:is_cli]
266
247
  raise 'An error occurred in the Jasmine runner'
267
248
  else
268
- notify_runtime_error(result, options)
249
+ notify_runtime_error(result)
269
250
  end
270
251
  elsif result
271
252
  result['file'] = file
272
- notify_spec_result(result, options)
253
+ notify_spec_result(result)
273
254
  end
274
255
 
275
256
  if result && result['coverage'] && options[:coverage]
276
- notify_coverage_result(result['coverage'], file, options)
257
+ notify_coverage_result(result['coverage'], file)
277
258
  end
278
259
 
279
- result
260
+ return result
280
261
 
281
262
  rescue MultiJson::DecodeError => e
282
263
  if e.data == ''
@@ -287,10 +268,11 @@ module Guard
287
268
  end
288
269
  else
289
270
  if options[:is_cli]
290
- raise 'Cannot decode JSON from PhantomJS runner'
271
+ raise "Cannot decode JSON from PhantomJS runner, message received was:\n#{json}"
291
272
  else
292
273
  Formatter.error("Cannot decode JSON from PhantomJS runner: #{ e.message }")
293
274
  Formatter.error("JSON response: #{ e.data }")
275
+ Formatter.error("message received was:\n#{json}")
294
276
  end
295
277
  end
296
278
  ensure
@@ -302,12 +284,11 @@ module Guard
302
284
  # prohibits the execution of the Jasmine spec.
303
285
  #
304
286
  # @param [Hash] result the suite result
305
- # @param [Hash] options the options for the execution
306
- # @option options [Boolean] :notification show notifications
307
287
  #
308
- def notify_runtime_error(result, options)
288
+ def notify_runtime_error(result)
309
289
  message = "An error occurred: #{ result['error'] }"
310
- Formatter.error(message)
290
+ Formatter.error(message )
291
+ Formatter.error( result['trace'] ) if result['trace']
311
292
  Formatter.notify(message, title: 'Jasmine error', image: :failed, priority: 2) if options[:notification]
312
293
  end
313
294
 
@@ -315,62 +296,59 @@ module Guard
315
296
  # and some stats.
316
297
  #
317
298
  # @param [Hash] result the suite result
318
- # @param [Hash] options the options for the execution
319
- # @option options [Boolean] :notification show notifications
320
- # @option options [Boolean] :hide_success hide success message notification
321
299
  #
322
- def notify_spec_result(result, options)
323
- specs = result['stats']['specs']
324
- failures = result['stats']['failures']
325
- time = result['stats']['time']
326
- specs_plural = specs == 1 ? '' : 's'
327
- failures_plural = failures == 1 ? '' : 's'
328
-
329
- Formatter.info("\nFinished in #{ time } seconds")
330
-
331
- message = "#{ specs } spec#{ specs_plural }, #{ failures } failure#{ failures_plural }"
300
+ def notify_spec_result(result)
301
+ specs = result['stats']['specs'] - result['stats']['disabled']
302
+ failed = result['stats']['failed']
303
+ time = sprintf( '%0.2f', result['stats']['time'] )
304
+ specs_plural = specs == 1 ? '' : 's'
305
+ failed_plural = failed == 1 ? '' : 's'
306
+ Formatter.info("Finished in #{ time } seconds")
307
+ pending = result['stats']['pending'].to_i > 0 ? " #{result['stats']['pending']} pending," : ""
308
+ message = "#{ specs } spec#{ specs_plural },#{pending} #{ failed } failure#{ failed_plural }"
332
309
  full_message = "#{ message }\nin #{ time } seconds"
333
- passed = failures == 0
310
+ passed = failed == 0
311
+
312
+ report_specdoc(result, passed) if specdoc_shown?(passed)
334
313
 
335
314
  if passed
336
- report_specdoc(result, passed, options)
337
315
  Formatter.success(message)
338
316
  Formatter.notify(full_message, title: 'Jasmine suite passed') if options[:notification] && !options[:hide_success]
339
317
  else
340
- report_specdoc(result, passed, options)
318
+ errors = collect_spec_errors(result['suites'])
319
+ error_message = errors[0..options[:max_error_notify]].join("\n")
320
+
341
321
  Formatter.error(message)
342
- notify_errors(result, options)
343
- Formatter.notify(full_message, title: 'Jasmine suite failed', image: :failed, priority: 2) if options[:notification]
322
+ if options[:notification]
323
+ Formatter.notify( "#{error_message}\n#{full_message}",
324
+ title: 'Jasmine suite failed', image: :failed, priority: 2)
325
+ end
344
326
  end
345
327
 
346
- Formatter.info("Done.\n")
347
328
  end
348
329
 
349
- # Notification about the coverage of a spec run, success or failure,
330
+ # Notification about the coverage of a spec run, success or failed,
350
331
  # and some stats.
351
332
  #
352
333
  # @param [Hash] coverage the coverage hash from the JSON
353
334
  # @param [String] file the file name of the spec
354
- # @param [Hash] options the options for the execution
355
- # @option options [Boolean] :notification show notifications
356
- # @option options [Boolean] :hide_success hide success message notification
357
335
  #
358
- def notify_coverage_result(coverage, file, options)
336
+ def notify_coverage_result(coverage, file)
359
337
  if coverage_bin
360
338
  FileUtils.mkdir_p(coverage_root) unless File.exist?(coverage_root)
361
339
 
362
- update_coverage(coverage, file, options)
340
+ update_coverage(coverage, file)
363
341
 
364
342
  if options[:coverage_summary]
365
343
  generate_summary_report
366
344
  else
367
- generate_text_report(file, options)
345
+ generate_text_report(file)
368
346
  end
369
347
 
370
- check_coverage(options)
348
+ check_coverage
371
349
 
372
350
  if options[:coverage_html]
373
- generate_html_report(options)
351
+ generate_html_report
374
352
  end
375
353
  else
376
354
  Formatter.error('Skipping coverage report: unable to locate istanbul in your PATH')
@@ -381,9 +359,8 @@ module Guard
381
359
  # last coverage run.
382
360
  #
383
361
  # @param [String] file the file name of the spec
384
- # @param [Hash] options the options for the execution
385
362
  #
386
- def generate_text_report(file, options)
363
+ def generate_text_report(file)
387
364
  Formatter.info 'Spec coverage details:'
388
365
 
389
366
  if file == options[:spec_dir]
@@ -405,11 +382,9 @@ module Guard
405
382
  # Uses the Istanbul text reported to output the result of the
406
383
  # last coverage run.
407
384
  #
408
- # @param [Hash] options the options for the coverage
409
- #
410
- def check_coverage(options)
411
- if any_coverage_threshold?(options)
412
- coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options(options) } #{ coverage_file } 2>&1`
385
+ def check_coverage
386
+ if any_coverage_threshold?
387
+ coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options } #{ coverage_file } 2>&1`
413
388
  coverage = coverage.split("\n").grep(/ERROR/).join.sub('ERROR:', '')
414
389
  failed = $? && $?.exitstatus != 0
415
390
 
@@ -426,10 +401,8 @@ module Guard
426
401
  # Uses the Istanbul text reported to output the result of the
427
402
  # last coverage run.
428
403
  #
429
- # @param [Hash] options for the HTML report
430
- #
431
- def generate_html_report(options)
432
- report_directory = coverage_report_directory(options)
404
+ def generate_html_report
405
+ report_directory = coverage_report_directory
433
406
  `#{coverage_bin} report --dir #{ report_directory } --root #{ coverage_root } html #{ coverage_file }`
434
407
  Formatter.info "Updated HTML report available at: #{ report_directory }/index.html"
435
408
  end
@@ -453,12 +426,10 @@ module Guard
453
426
  #
454
427
  # @param [Hash] result the suite result
455
428
  # @param [Boolean] passed status
456
- # @param [Hash] options the options
457
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
458
429
  #
459
- def report_specdoc(result, passed, options)
430
+ def report_specdoc(result, passed)
460
431
  result['suites'].each do |suite|
461
- report_specdoc_suite(suite, passed, options)
432
+ report_specdoc_suite(suite, passed)
462
433
  end
463
434
  end
464
435
 
@@ -466,128 +437,78 @@ module Guard
466
437
  #
467
438
  # @param [Hash] suite the suite
468
439
  # @param [Boolean] passed status
469
- # @param [Hash] options the options
470
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
471
- # @option options [Symbol] :focus options for focus on failures in the specdoc
472
440
  # @param [Number] level the indention level
473
441
  #
474
- def report_specdoc_suite(suite, passed, options, level = 0)
475
- # Print the suite description when the specdoc is shown or there are logs to display
476
- if specdoc_shown?(passed, options) || console_logs_shown?(suite, passed, options) || error_logs_shown?(suite, passed, options)
477
- Formatter.suite_name((' ' * level) + suite['description']) if passed || options[:focus] && contains_failed_spec?(suite)
478
- end
442
+ def report_specdoc_suite(suite, run_passed, level = 0)
443
+
444
+ # Print the suite description when the specdoc is shown or there are logs to display
445
+ Formatter.suite_name((' ' * level) + suite['description'])
479
446
 
480
447
  suite['specs'].each do |spec|
481
- if spec['passed']
482
- if passed || !options[:focus] || console_for_spec?(spec, options) || errors_for_spec?(spec, options)
483
- Formatter.success(indent(" ✔ #{ spec['description'] }", level)) if description_shown?(passed, spec, options)
484
- report_specdoc_errors(spec, options, level)
485
- report_specdoc_logs(spec, options, level)
486
- end
448
+ # Specs are shown if they failed, or if they passed and the "focus" option is falsey
449
+ # If specs are going to be shown, then pending are also shown
450
+ # If the focus option is set, then only failing tests are shown
451
+ next unless :always==options[:specdoc] || spec['status'] == 'failed' || ( !run_passed && !options[:focus] )
452
+ if spec['status'] == 'passed'
453
+ Formatter.success(indent(" ✔ #{ spec['description'] }", level))
454
+ elsif spec['status'] == 'failed'
455
+ Formatter.spec_failed(indent(" ✘ #{ spec['description'] }", level))
487
456
  else
488
- Formatter.spec_failed(indent(" #{ spec['description'] }", level)) if description_shown?(passed, spec, options)
489
- spec['messages'].each do |message|
490
- Formatter.spec_failed(indent(" ➤ #{ format_message(message, false) }", level)) if specdoc_shown?(passed, options)
491
- end
492
- report_specdoc_errors(spec, options, level)
493
- report_specdoc_logs(spec, options, level)
457
+ Formatter.spec_pending(indent(" #{ spec['description'] }", level))
494
458
  end
459
+ report_specdoc_errors(spec, level)
460
+ report_specdoc_logs(spec, level)
495
461
  end
496
462
 
497
- suite['suites'].each { |s| report_specdoc_suite(s, passed, options, level + 2) } if suite['suites']
463
+ suite['suites'].each { |s| report_specdoc_suite(s, run_passed, level + 2) } if suite['suites']
498
464
  end
499
465
 
500
466
  # Is the specdoc shown for this suite?
501
467
  #
502
468
  # @param [Boolean] passed the spec status
503
- # @param [Hash] options the options
504
469
  #
505
- def specdoc_shown?(passed, options = { })
470
+ def specdoc_shown?(passed)
506
471
  options[:specdoc] == :always || (options[:specdoc] == :failure && !passed)
507
472
  end
508
473
 
509
- # Are console logs shown for this suite?
510
- #
511
- # @param [Hash] suite the suite
512
- # @param [Boolean] passed the spec status
513
- # @param [Hash] options the options
514
- #
515
- def console_logs_shown?(suite, passed, options = { })
516
- # Are console messages displayed?
517
- console_enabled = options[:console] == :always || (options[:console] == :failure && !passed)
518
-
519
- # Are there any logs to display at all for this suite?
520
- logs_for_current_options = suite['specs'].select do |spec|
521
- spec['logs'] && (options[:console] == :always || (options[:console] == :failure && !spec['passed']))
522
- end
523
-
524
- any_logs_present = !logs_for_current_options.empty?
525
-
526
- console_enabled && any_logs_present
527
- end
528
474
 
529
475
  # Are console logs shown for this spec?
530
476
  #
531
477
  # @param [Hash] spec the spec
532
- # @param [Hash] options the options
533
478
  #
534
- def console_for_spec?(spec, options = { })
535
- spec['logs'] && ((spec['passed'] && options[:console] == :always) ||
536
- (!spec['passed'] && options[:console] != :never))
479
+ def console_for_spec?(spec)
480
+ spec['logs'] && (( spec['status'] == 'passed' && options[:console] == :always) ||
481
+ (spec['status'] == 'failed' && options[:console] != :never) )
537
482
  end
538
483
 
539
- # Are error logs shown for this suite?
540
- #
541
- # @param [Hash] suite the suite
542
- # @param [Boolean] passed the spec status
543
- # @param [Hash] options the options
544
- #
545
- def error_logs_shown?(suite, passed, options = { })
546
- # Are error messages displayed?
547
- errors_enabled = options[:errors] == :always || (options[:errors] == :failure && !passed)
548
-
549
- # Are there any errors to display at all for this suite?
550
- errors_for_current_options = suite['specs'].select do |spec|
551
- spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && !spec['passed']))
552
- end
553
-
554
- any_errors_present= !errors_for_current_options.empty?
555
-
556
- errors_enabled && any_errors_present
557
- end
558
484
 
559
485
  # Are errors shown for this spec?
560
486
  #
561
487
  # @param [Hash] spec the spec
562
- # @param [Hash] options the options
563
- def errors_for_spec?(spec, options = { })
564
- spec['errors'] && ((spec['passed'] && options[:errors] == :always) ||
565
- (!spec['passed'] && options[:errors] != :never))
488
+ def errors_for_spec?(spec)
489
+ spec['errors'] && ((spec['status']=='passed' && options[:errors] == :always) ||
490
+ (spec['status']=='failed' && options[:errors] != :never))
566
491
  end
567
492
 
568
493
  # Is the description shown for this spec?
569
494
  #
570
495
  # @param [Boolean] passed the spec status
571
496
  # @param [Hash] spec the spec
572
- # @param [Hash] options the options
573
497
  #
574
- def description_shown?(passed, spec, options = { })
575
- specdoc_shown?(passed, options) || console_for_spec?(spec, options) || errors_for_spec?(spec, options)
498
+ def description_shown?(passed, spec)
499
+ specdoc_shown?(passed) || console_for_spec?(spec) || errors_for_spec?(spec)
576
500
  end
577
501
 
578
502
  # Shows the logs for a given spec.
579
503
  #
580
504
  # @param [Hash] spec the spec result
581
- # @param [Hash] options the options
582
- # @option options [Symbol] :console options for the console.log output, either :always, :never or :failure
583
505
  # @param [Number] level the indention level
584
506
  #
585
- def report_specdoc_logs(spec, options, level)
586
- if spec['logs'] && (options[:console] == :always || (options[:console] == :failure && !spec['passed']))
587
- spec['logs'].each do |log|
588
- log.split("\n").each_with_index do |message, index|
589
- Formatter.info(indent(" #{ index == 0 ? '•' : ' ' } #{ message }", level))
590
- end
507
+ def report_specdoc_logs(spec, level)
508
+ if console_for_spec?(spec)
509
+ spec['logs'].each do |log_level, message|
510
+ log_level = log_level == 'log' ? '' : "#{log_level.upcase}: "
511
+ Formatter.info(indent(" #{log_level}#{ message }", level))
591
512
  end
592
513
  end
593
514
  end
@@ -595,19 +516,16 @@ module Guard
595
516
  # Shows the errors for a given spec.
596
517
  #
597
518
  # @param [Hash] spec the spec result
598
- # @param [Hash] options the options
599
- # @option options [Symbol] :errors options for the errors output, either :always, :never or :failure
600
519
  # @param [Number] level the indention level
601
520
  #
602
- def report_specdoc_errors(spec, options, level)
603
- if spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && !spec['passed']))
521
+ def report_specdoc_errors(spec, level)
522
+ if spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && spec['status']=='failed'))
604
523
  spec['errors'].each do |error|
524
+ Formatter.spec_failed(indent(" ➤ #{ format_error(error,true) }", level))
605
525
  if error['trace']
606
526
  error['trace'].each do |trace|
607
- Formatter.spec_failed(indent(" ➜ Exception: #{ error['msg'] } in #{ trace['file'] } on line #{ trace['line'] }", level))
527
+ Formatter.spec_failed(indent(" ➜ #{ trace['file'] } on line #{ trace['line'] }", level+2))
608
528
  end
609
- else
610
- Formatter.spec_failed(indent(" ➜ Exception: #{ error['msg'] }", level))
611
529
  end
612
530
  end
613
531
  end
@@ -622,24 +540,6 @@ module Guard
622
540
  (' ' * level) + message
623
541
  end
624
542
 
625
- # Show system notifications about the occurred errors.
626
- #
627
- # @param [Hash] result the suite result
628
- # @param [Hash] options the options
629
- # @option options [Integer] :max_error_notify maximum error notifications to show
630
- # @option options [Boolean] :notification show notifications
631
- #
632
- def notify_errors(result, options)
633
- collect_specs(result['suites']).each_with_index do |spec, index|
634
- if !spec['passed'] && options[:max_error_notify] > index
635
- msg = spec['messages'].map { |message| format_message(message, true) }.join(', ')
636
- Formatter.notify("#{ spec['description'] }: #{ msg }",
637
- title: 'Jasmine spec failed',
638
- image: :failed,
639
- priority: 2) if options[:notification]
640
- end
641
- end
642
- end
643
543
 
644
544
  # Tests if the given suite has a failing spec underneath.
645
545
  #
@@ -647,7 +547,19 @@ module Guard
647
547
  # @return [Boolean] the search result
648
548
  #
649
549
  def contains_failed_spec?(suite)
650
- collect_specs([suite]).any? { |spec| !spec['passed'] }
550
+ collect_specs([suite]).any? { |spec| spec['status'] == 'failed' }
551
+ end
552
+
553
+
554
+ # Get all failed specs from the suites and its nested suites.
555
+ #
556
+ # @param suites [Array<Hash>] the suites results
557
+ # @return [Array<Hash>] all failed
558
+ #
559
+ def collect_spec_errors(suites)
560
+ collect_specs(suites).map { |spec|
561
+ (spec['errors']||[]).map { |error| format_error(error,false) }
562
+ }.flatten
651
563
  end
652
564
 
653
565
  # Get all specs from the suites and its nested suites.
@@ -656,10 +568,9 @@ module Guard
656
568
  # @return [Array<Hash>] all specs
657
569
  #
658
570
  def collect_specs(suites)
659
- suites.inject([]) do |specs, suite|
660
- specs = (specs | suite['specs']) if suite['specs']
661
- specs = (specs | collect_specs(suite['suites'])) if suite['suites']
662
- specs
571
+ suites.each_with_object([]) do |suite, specs|
572
+ specs.concat( suite['specs'] )
573
+ specs.concat( collect_specs(suite['suites']) ) if suite['suites']
663
574
  end
664
575
  end
665
576
 
@@ -669,9 +580,11 @@ module Guard
669
580
  # @param [Boolean] short show a short version of the message
670
581
  # @return [String] the cleaned error message
671
582
  #
672
- def format_message(message, short)
673
- if message =~ /(.*?) in http.+?assets\/(.*)\?body=\d+\s\((line\s\d+)/
674
- short ? $1 : "#{ $1 } in #{ $2 } on #{ $3 }"
583
+ def format_error(error, short)
584
+ message = error['message'].gsub(%r{ in http.*\(line \d+\)$},'')
585
+ if !short && error['trace'] && error['trace'].length > 0
586
+ location = error['trace'][0]
587
+ "#{message} in #{location['file']}:#{location['line']}"
675
588
  else
676
589
  message
677
590
  end
@@ -682,9 +595,8 @@ module Guard
682
595
  #
683
596
  # @param [Hash] coverage the last run coverage data
684
597
  # @param [String] file the file name of the spec
685
- # @param [Hash] options the options for the execution
686
598
  #
687
- def update_coverage(coverage, file, options)
599
+ def update_coverage(coverage, file)
688
600
  if file == options[:spec_dir]
689
601
  File.write(coverage_file, MultiJson.encode(coverage, { max_nesting: false }))
690
602
  else
@@ -707,16 +619,15 @@ module Guard
707
619
  #
708
620
  # @return [Boolean] true if any coverage threshold is set
709
621
  #
710
- def any_coverage_threshold?(options)
622
+ def any_coverage_threshold?
711
623
  THRESHOLDS.any? { |threshold| options[threshold] != 0 }
712
624
  end
713
625
 
714
626
  # Converts the options to Istanbul recognized options
715
627
  #
716
- # @param [Hash] options the options for the coverage
717
628
  # @return [String] the command line options
718
629
  #
719
- def istanbul_coverage_options(options)
630
+ def istanbul_coverage_options
720
631
  THRESHOLDS.inject([]) do |coverage, name|
721
632
  threshold = options[name]
722
633
  coverage << (threshold != 0 ? "--#{ name.to_s.sub('_threshold', '') } #{ threshold }" : '')
@@ -750,13 +661,12 @@ module Guard
750
661
 
751
662
  # Creates and returns the coverage report directory.
752
663
  #
753
- # @param [Hash] options for the coverage report directory
754
664
  # @return [String] the coverage report directory
755
665
  #
756
- def coverage_report_directory(options)
666
+ def coverage_report_directory
757
667
  File.expand_path(options[:coverage_html_dir])
758
668
  end
759
- end
760
669
  end
761
670
  end
762
671
  end
672
+