guard-jasmine 2.0.0 → 2.0.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.
@@ -6,7 +6,6 @@ require 'guard/jasmine/util'
6
6
 
7
7
  module Guard
8
8
  class Jasmine
9
-
10
9
  # The Jasmine runner handles the execution of the spec through the PhantomJS binary,
11
10
  # evaluates the JSON response from the PhantomJS Script `guard_jasmine.coffee`,
12
11
  # writes the result to the console and triggers optional system notifications.
@@ -44,7 +43,7 @@ module Guard
44
43
  # Only specs with failures will be returned. Therefore an empty? return hash indicates success.
45
44
  def run(paths, per_run_options = {})
46
45
  previous_options = @options
47
- @options.merge!( per_run_options )
46
+ @options.merge!(per_run_options)
48
47
 
49
48
  return {} if paths.empty?
50
49
 
@@ -58,616 +57,602 @@ module Guard
58
57
  # return the errors
59
58
  return run_results.each_with_object({}) do | spec_run, hash |
60
59
  file, r = spec_run
61
- errors = collect_spec_errors(r['suites']||[])
62
- errors.push( r['error'] ) if r.has_key? 'error'
60
+ errors = collect_spec_errors(r['suites'] || [])
61
+ errors.push(r['error']) if r.key? 'error'
63
62
  hash[file] = errors unless errors.empty?
64
63
  end
65
64
  ensure
66
- @options=previous_options
65
+ @options = previous_options
67
66
  end
68
67
 
69
- private
68
+ private
70
69
 
71
70
  # Shows a notification in the console that the runner starts.
72
71
  #
73
- # @param [Array<String>] paths the spec files or directories
74
- #
75
- def notify_start_message(paths)
76
- message = if paths == [options[:spec_dir]]
77
- 'Run all Jasmine suites'
78
- else
79
- "Run Jasmine suite#{ paths.size == 1 ? '' : 's' } #{ paths.join(' ') }"
80
- end
72
+ # @param [Array<String>] paths the spec files or directories
73
+ #
74
+ def notify_start_message(paths)
75
+ message = if paths == [options[:spec_dir]]
76
+ 'Run all Jasmine suites'
77
+ else
78
+ "Run Jasmine suite#{ paths.size == 1 ? '' : 's' } #{ paths.join(' ') }"
79
+ end
80
+
81
+ Formatter.info(message, reset: true)
82
+ end
81
83
 
82
- Formatter.info(message, reset: true)
83
- end
84
+ # Run the Jasmine spec by executing the PhantomJS script.
85
+ #
86
+ # @param [String] file the path of the spec
87
+ #
88
+ def run_jasmine_spec(file)
89
+ suite = jasmine_suite(file)
90
+
91
+ arguments = [
92
+ options[:timeout] * 1000,
93
+ options[:specdoc],
94
+ options[:focus],
95
+ options[:console],
96
+ options[:errors],
97
+ options[:junit],
98
+ options[:junit_consolidate],
99
+ "'#{ options[:junit_save_path] }'"
100
+ ]
101
+ cmd = "#{ phantomjs_command } \"#{ suite }\" #{ arguments.collect(&:to_s).join(' ')}"
102
+ puts cmd if options[:debug]
103
+ IO.popen(cmd, 'r:UTF-8')
104
+ end
84
105
 
85
- # Run the Jasmine spec by executing the PhantomJS script.
86
- #
87
- # @param [String] file the path of the spec
88
- #
89
- def run_jasmine_spec(file)
90
- suite = jasmine_suite(file)
91
-
92
- arguments = [
93
- options[:timeout] * 1000,
94
- options[:specdoc],
95
- options[:focus],
96
- options[:console],
97
- options[:errors],
98
- options[:junit],
99
- options[:junit_consolidate],
100
- "'#{ options[:junit_save_path] }'"
101
- ]
102
- cmd = "#{ phantomjs_command } \"#{ suite }\" #{ arguments.collect { |i| i.to_s }.join(' ')}"
103
- puts cmd if options[:debug]
104
- IO.popen(cmd, 'r:UTF-8')
105
- end
106
+ # Get the PhantomJS binary and script to execute.
107
+ #
108
+ # @return [String] the command
109
+ #
110
+ def phantomjs_command
111
+ options[:phantomjs_bin] + ' ' + phantomjs_script
112
+ # options[:phantomjs_bin] + ' --remote-debugger-port=9000 ' + phantomjs_script
113
+ end
106
114
 
107
- # Get the PhantomJS binary and script to execute.
108
- #
109
- # @return [String] the command
110
- #
111
- def phantomjs_command
112
- options[:phantomjs_bin] + ' ' + phantomjs_script
113
- #options[:phantomjs_bin] + ' --remote-debugger-port=9000 ' + phantomjs_script
114
- end
115
+ # Get the Jasmine test runner URL with the appended suite name
116
+ # that acts as the spec filter.
117
+ #
118
+ # @param [String] file the spec file
119
+ # @return [String] the Jasmine url
120
+ #
121
+ def jasmine_suite(file)
122
+ options[:jasmine_url] + query_string_for_suite(file)
123
+ end
115
124
 
116
- # Get the Jasmine test runner URL with the appended suite name
117
- # that acts as the spec filter.
118
- #
119
- # @param [String] file the spec file
120
- # @return [String] the Jasmine url
121
- #
122
- def jasmine_suite(file)
123
- options[:jasmine_url] + query_string_for_suite(file)
124
- end
125
+ # Get the PhantomJS script that executes the spec and extracts
126
+ # the result from the headless DOM.
127
+ #
128
+ # @return [String] the path to the PhantomJS script
129
+ #
130
+ def phantomjs_script
131
+ File.expand_path(File.join(File.dirname(__FILE__), 'phantomjs', 'guard-jasmine.js'))
132
+ end
125
133
 
126
- # Get the PhantomJS script that executes the spec and extracts
127
- # the result from the headless DOM.
128
- #
129
- # @return [String] the path to the PhantomJS script
130
- #
131
- def phantomjs_script
132
- File.expand_path(File.join(File.dirname(__FILE__), 'phantomjs', 'guard-jasmine.js'))
133
- end
134
+ # The suite name must be extracted from the spec that
135
+ # will be run.
136
+ #
137
+ # @param [String] file the spec file
138
+ # @return [String] the suite name
139
+ #
140
+ def query_string_for_suite(file)
141
+ params = {}
142
+ params.merge!(options[:query_params]) if options[:query_params]
134
143
 
135
- # The suite name must be extracted from the spec that
136
- # will be run.
137
- #
138
- # @param [String] file the spec file
139
- # @return [String] the suite name
140
- #
141
- def query_string_for_suite(file)
142
- params = {}
143
- if options[:query_params]
144
- params.merge!(options[:query_params])
145
- end
146
- if file != options[:spec_dir]
147
- params[:spec] = suite_from_line_number(file) || suite_from_first_describe(file)
148
- end
149
- params.empty? ? "" : "?"+URI.encode_www_form(params).gsub('+','%20')
150
- end
144
+ params[:spec] = suite_from_line_number(file) || suite_from_first_describe(file) if file != options[:spec_dir]
145
+ params.empty? ? '' : '?' + URI.encode_www_form(params).gsub('+', '%20')
146
+ end
151
147
 
152
- # When providing a line number by either the option or by
153
- # a number directly after the file name the suite is extracted
154
- # fromt the corresponding line number in the file.
155
- #
156
- # @param [String] file the spec file
157
- # @return [String] the suite name
158
- #
159
- def suite_from_line_number(file)
160
- file_name, line_number = file_and_line_number_parts(file)
161
- line_number ||= options[:line_number]
162
-
163
- if line_number
164
- lines = it_and_describe_lines(file_name, 0, line_number)
165
- last = lines.pop
166
-
167
- last_indentation = last[/^\s*/].length
168
- # keep only lines with lower indentation
169
- lines.delete_if { |x| x[/^\s*/].length >= last_indentation }
170
- # remove all 'it'
171
- lines.delete_if { |x| x =~ /^\s*it/ }
172
-
173
- lines << last
174
- lines.map { |x| spec_title(x) }.join(' ')
175
- end
176
- end
148
+ # When providing a line number by either the option or by
149
+ # a number directly after the file name the suite is extracted
150
+ # fromt the corresponding line number in the file.
151
+ #
152
+ # @param [String] file the spec file
153
+ # @return [String] the suite name
154
+ #
155
+ def suite_from_line_number(file)
156
+ file_name, line_number = file_and_line_number_parts(file)
157
+ line_number ||= options[:line_number]
177
158
 
178
- # The suite name must be extracted from the spec that
179
- # will be run. This is done by parsing from the head of
180
- # the spec file until the first `describe` function is
181
- # found.
182
- #
183
- # @param [String] file the spec file
184
- # @return [String] the suite name
185
- #
186
- def suite_from_first_describe(file)
187
- File.foreach(file) do |line|
188
- if line =~ /describe\s*[("']+(.*?)["')]+/ #'
189
- return $1
190
- end
191
- end
192
- end
159
+ if line_number
160
+ lines = it_and_describe_lines(file_name, 0, line_number)
161
+ last = lines.pop
193
162
 
194
- # Splits the file name into the physical file name
195
- # and the line number if present. E.g.:
196
- # 'some_spec.js.coffee:10' -> ['some_spec.js.coffee', 10].
197
- #
198
- # If the line number is missing the second part of the
199
- # returned array is `nil`.
200
- #
201
- # @param [String] file the spec file
202
- # @return [Array] `[file_name, line_number]`
203
- #
204
- def file_and_line_number_parts(file)
205
- match = file.match(/^(.+?)(?::(\d+))?$/)
206
- [match[1], match[2].nil? ? nil : match[2].to_i]
207
- end
163
+ last_indentation = last[/^\s*/].length
164
+ # keep only lines with lower indentation
165
+ lines.delete_if { |x| x[/^\s*/].length >= last_indentation }
166
+ # remove all 'it'
167
+ lines.delete_if { |x| x =~ /^\s*it/ }
208
168
 
209
- # Returns all lines of the file that are either a
210
- # 'describe' or a 'it' declaration.
211
- #
212
- # @param [String] file the spec file
213
- # @param [Numeric] from the first line in the range
214
- # @param [Numeric] to the last line in the range
215
- # @Return [Array] the line contents
216
- #
217
- def it_and_describe_lines(file, from, to)
218
- File.readlines(file)[from, to].
219
- select { |x| x =~ /^\s*(it|describe)/ }
169
+ lines << last
170
+ lines.map { |x| spec_title(x) }.join(' ')
220
171
  end
172
+ end
221
173
 
222
- # Extracts the title of a 'description' or a 'it' declaration.
223
- #
224
- # @param [String] the line content
225
- # @return [String] the extracted title
226
- #
227
- def spec_title(line)
228
- line[/['"](.+?)["']/, 1]
174
+ # The suite name must be extracted from the spec that
175
+ # will be run. This is done by parsing from the head of
176
+ # the spec file until the first `describe` function is
177
+ # found.
178
+ #
179
+ # @param [String] file the spec file
180
+ # @return [String] the suite name
181
+ #
182
+ def suite_from_first_describe(file)
183
+ File.foreach(file) do |line|
184
+ return Regexp.last_match[1] if line =~ /describe\s*[("']+(.*?)["')]+/ # '
229
185
  end
186
+ end
230
187
 
231
- # Evaluates the JSON response that the PhantomJS script
232
- # writes to stdout. The results triggers further notification
233
- # actions.
234
- #
235
- # @param [String] output the JSON output the spec run
236
- # @param [String] file the file name of the spec
237
- # @return [Hash] results of the suite's specs
238
- #
239
- def evaluate_response(output, file)
240
- json = output.read
241
- json = json.encode('UTF-8') if json.respond_to?(:encode)
242
- begin
243
- result = MultiJson.decode(json, { max_nesting: false })
244
- raise 'No response from Jasmine runner' if !result && options[:is_cli]
245
- pp result if options[:debug]
246
- if result['error']
247
- if options[:is_cli]
248
- raise 'An error occurred in the Jasmine runner'
249
- else
250
- notify_runtime_error(result)
251
- end
252
- elsif result
253
- result['file'] = file
254
- notify_spec_result(result)
255
- end
188
+ # Splits the file name into the physical file name
189
+ # and the line number if present. E.g.:
190
+ # 'some_spec.js.coffee:10' -> ['some_spec.js.coffee', 10].
191
+ #
192
+ # If the line number is missing the second part of the
193
+ # returned array is `nil`.
194
+ #
195
+ # @param [String] file the spec file
196
+ # @return [Array] `[file_name, line_number]`
197
+ #
198
+ def file_and_line_number_parts(file)
199
+ match = file.match(/^(.+?)(?::(\d+))?$/)
200
+ [match[1], match[2].nil? ? nil : match[2].to_i]
201
+ end
256
202
 
257
- if result && result['coverage'] && options[:coverage]
258
- notify_coverage_result(result['coverage'], file)
259
- end
203
+ # Returns all lines of the file that are either a
204
+ # 'describe' or a 'it' declaration.
205
+ #
206
+ # @param [String] file the spec file
207
+ # @param [Numeric] from the first line in the range
208
+ # @param [Numeric] to the last line in the range
209
+ # @Return [Array] the line contents
210
+ #
211
+ def it_and_describe_lines(file, from, to)
212
+ File.readlines(file)[from, to]
213
+ .select { |x| x =~ /^\s*(it|describe)/ }
214
+ end
260
215
 
261
- return result
216
+ # Extracts the title of a 'description' or a 'it' declaration.
217
+ #
218
+ # @param [String] the line content
219
+ # @return [String] the extracted title
220
+ #
221
+ def spec_title(line)
222
+ line[/['"](.+?)["']/, 1]
223
+ end
262
224
 
263
- rescue MultiJson::DecodeError => e
264
- if e.data == ''
265
- if options[:is_cli]
266
- raise 'No response from Jasmine runner'
267
- else
268
- Formatter.error('No response from the Jasmine runner!')
269
- end
225
+ # Evaluates the JSON response that the PhantomJS script
226
+ # writes to stdout. The results triggers further notification
227
+ # actions.
228
+ #
229
+ # @param [String] output the JSON output the spec run
230
+ # @param [String] file the file name of the spec
231
+ # @return [Hash] results of the suite's specs
232
+ #
233
+ def evaluate_response(output, file)
234
+ json = output.read
235
+ json = json.encode('UTF-8') if json.respond_to?(:encode)
236
+ begin
237
+ result = MultiJson.decode(json, max_nesting: false)
238
+ fail 'No response from Jasmine runner' if !result && options[:is_cli]
239
+ pp result if options[:debug]
240
+ if result['error']
241
+ if options[:is_cli]
242
+ fail 'An error occurred in the Jasmine runner'
270
243
  else
271
- if options[:is_cli]
272
- raise "Cannot decode JSON from PhantomJS runner, message received was:\n#{json}"
273
- else
274
- Formatter.error("Cannot decode JSON from PhantomJS runner: #{ e.message }")
275
- Formatter.error("JSON response: #{ e.data }")
276
- Formatter.error("message received was:\n#{json}")
277
- end
244
+ notify_runtime_error(result)
278
245
  end
279
- ensure
280
- output.close
246
+ elsif result
247
+ result['file'] = file
248
+ notify_spec_result(result)
281
249
  end
282
- end
283
-
284
- # Notification when a system error happens that
285
- # prohibits the execution of the Jasmine spec.
286
- #
287
- # @param [Hash] result the suite result
288
- #
289
- def notify_runtime_error(result)
290
- message = "An error occurred: #{ result['error'] }"
291
- Formatter.error(message )
292
- Formatter.error( result['trace'] ) if result['trace']
293
- Formatter.notify(message, title: 'Jasmine error', image: :failed, priority: 2) if options[:notification]
294
- end
295
-
296
- # Notification about a spec run, success or failure,
297
- # and some stats.
298
- #
299
- # @param [Hash] result the suite result
300
- #
301
- def notify_spec_result(result)
302
- specs = result['stats']['specs'] - result['stats']['disabled']
303
- failed = result['stats']['failed']
304
- time = sprintf( '%0.2f', result['stats']['time'] )
305
- specs_plural = specs == 1 ? '' : 's'
306
- failed_plural = failed == 1 ? '' : 's'
307
- Formatter.info("Finished in #{ time } seconds")
308
- pending = result['stats']['pending'].to_i > 0 ? " #{result['stats']['pending']} pending," : ""
309
- message = "#{ specs } spec#{ specs_plural },#{pending} #{ failed } failure#{ failed_plural }"
310
- full_message = "#{ message }\nin #{ time } seconds"
311
- passed = failed == 0
312
-
313
- report_specdoc(result, passed) if specdoc_shown?(passed)
314
-
315
- if passed
316
- Formatter.success(message)
317
- Formatter.notify(full_message, title: 'Jasmine suite passed') if options[:notification] && !options[:hide_success]
318
- else
319
- errors = collect_spec_errors(result['suites'])
320
- error_message = errors[0..options[:max_error_notify]].join("\n")
321
250
 
322
- Formatter.error(message)
323
- if options[:notification]
324
- Formatter.notify( "#{error_message}\n#{full_message}",
325
- title: 'Jasmine suite failed', image: :failed, priority: 2)
326
- end
251
+ if result && result['coverage'] && options[:coverage]
252
+ notify_coverage_result(result['coverage'], file)
327
253
  end
328
254
 
329
- end
330
-
331
- # Notification about the coverage of a spec run, success or failed,
332
- # and some stats.
333
- #
334
- # @param [Hash] coverage the coverage hash from the JSON
335
- # @param [String] file the file name of the spec
336
- #
337
- def notify_coverage_result(coverage, file)
338
- if coverage_bin
339
- FileUtils.mkdir_p(coverage_root) unless File.exist?(coverage_root)
255
+ return result
340
256
 
341
- update_coverage(coverage, file)
342
-
343
- if options[:coverage_summary]
344
- generate_summary_report
257
+ rescue MultiJson::DecodeError => e
258
+ if e.data == ''
259
+ if options[:is_cli]
260
+ raise 'No response from Jasmine runner'
345
261
  else
346
- generate_text_report(file)
347
- end
348
-
349
- check_coverage
350
-
351
- if options[:coverage_html]
352
- generate_html_report
262
+ Formatter.error('No response from the Jasmine runner!')
353
263
  end
354
264
  else
355
- Formatter.error('Skipping coverage report: unable to locate istanbul in your PATH')
265
+ if options[:is_cli]
266
+ raise "Cannot decode JSON from PhantomJS runner, message received was:\n#{json}"
267
+ else
268
+ Formatter.error("Cannot decode JSON from PhantomJS runner: #{ e.message }")
269
+ Formatter.error("JSON response: #{ e.data }")
270
+ Formatter.error("message received was:\n#{json}")
271
+ end
356
272
  end
273
+ ensure
274
+ output.close
357
275
  end
276
+ end
358
277
 
359
- # Uses the Istanbul text reported to output the result of the
360
- # last coverage run.
361
- #
362
- # @param [String] file the file name of the spec
363
- #
364
- def generate_text_report(file)
365
- Formatter.info 'Spec coverage details:'
278
+ # Notification when a system error happens that
279
+ # prohibits the execution of the Jasmine spec.
280
+ #
281
+ # @param [Hash] result the suite result
282
+ #
283
+ def notify_runtime_error(result)
284
+ message = "An error occurred: #{ result['error'] }"
285
+ Formatter.error(message)
286
+ Formatter.error(result['trace']) if result['trace']
287
+ Formatter.notify(message, title: 'Jasmine error', image: :failed, priority: 2) if options[:notification]
288
+ end
366
289
 
367
- if file == options[:spec_dir]
368
- matcher = /[|+]$/
369
- else
370
- impl = file.sub('_spec', '').sub(options[:spec_dir], '')
371
- matcher = /(-+|All files|% Lines|#{ Regexp.escape(File.basename(impl)) }|#{ File.dirname(impl).sub(/^\//, '') }\/[^\/])/
290
+ # Notification about a spec run, success or failure,
291
+ # and some stats.
292
+ #
293
+ # @param [Hash] result the suite result
294
+ #
295
+ def notify_spec_result(result)
296
+ specs = result['stats']['specs'] - result['stats']['disabled']
297
+ failed = result['stats']['failed']
298
+ time = sprintf('%0.2f', result['stats']['time'])
299
+ specs_plural = specs == 1 ? '' : 's'
300
+ failed_plural = failed == 1 ? '' : 's'
301
+ Formatter.info("Finished in #{ time } seconds")
302
+ pending = result['stats']['pending'].to_i > 0 ? " #{result['stats']['pending']} pending," : ''
303
+ message = "#{ specs } spec#{ specs_plural },#{pending} #{ failed } failure#{ failed_plural }"
304
+ full_message = "#{ message }\nin #{ time } seconds"
305
+ passed = failed == 0
306
+
307
+ report_specdoc(result, passed) if specdoc_shown?(passed)
308
+
309
+ if passed
310
+ Formatter.success(message)
311
+ Formatter.notify(full_message, title: 'Jasmine suite passed') if options[:notification] && !options[:hide_success]
312
+ else
313
+ errors = collect_spec_errors(result['suites'])
314
+ error_message = errors[0..options[:max_error_notify]].join("\n")
315
+
316
+ Formatter.error(message)
317
+ if options[:notification]
318
+ Formatter.notify("#{error_message}\n#{full_message}",
319
+ title: 'Jasmine suite failed', image: :failed, priority: 2)
372
320
  end
321
+ end
322
+ end
373
323
 
374
- puts ''
375
-
376
- `#{coverage_bin} report --root #{ coverage_root } text #{ coverage_file }`.each_line do |line|
377
- puts line.sub(/\n$/, '') if line =~ matcher
378
- end
324
+ # Notification about the coverage of a spec run, success or failed,
325
+ # and some stats.
326
+ #
327
+ # @param [Hash] coverage the coverage hash from the JSON
328
+ # @param [String] file the file name of the spec
329
+ #
330
+ def notify_coverage_result(coverage, file)
331
+ if coverage_bin
332
+ FileUtils.mkdir_p(coverage_root) unless File.exist?(coverage_root)
379
333
 
380
- puts ''
381
- end
334
+ update_coverage(coverage, file)
382
335
 
383
- # Uses the Istanbul text reported to output the result of the
384
- # last coverage run.
385
- #
386
- def check_coverage
387
- if any_coverage_threshold?
388
- coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options } #{ coverage_file } 2>&1`
389
- coverage = coverage.split("\n").grep(/ERROR/).join.sub('ERROR:', '')
390
- failed = $? && $?.exitstatus != 0
391
-
392
- if failed
393
- Formatter.error coverage
394
- Formatter.notify(coverage, title: 'Code coverage failed', image: :failed, priority: 2) if options[:notification]
395
- else
396
- Formatter.success 'Code coverage succeed'
397
- Formatter.notify('All code is adequately covered with specs', title: 'Code coverage succeed') if options[:notification] && !options[:hide_success]
398
- end
336
+ if options[:coverage_summary]
337
+ generate_summary_report
338
+ else
339
+ generate_text_report(file)
399
340
  end
400
- end
401
341
 
402
- # Uses the Istanbul text reported to output the result of the
403
- # last coverage run.
404
- #
405
- def generate_html_report
406
- report_directory = coverage_report_directory
407
- `#{coverage_bin} report --dir #{ report_directory } --root #{ coverage_root } html #{ coverage_file }`
408
- Formatter.info "Updated HTML report available at: #{ report_directory }/index.html"
342
+ check_coverage
343
+
344
+ generate_html_report if options[:coverage_html]
345
+ else
346
+ Formatter.error('Skipping coverage report: unable to locate istanbul in your PATH')
409
347
  end
348
+ end
410
349
 
411
- # Uses the Istanbul text-summary reporter to output the
412
- # summary of all the coverage runs combined.
413
- #
414
- def generate_summary_report
415
- Formatter.info 'Spec coverage summary:'
350
+ # Uses the Istanbul text reported to output the result of the
351
+ # last coverage run.
352
+ #
353
+ # @param [String] file the file name of the spec
354
+ #
355
+ def generate_text_report(file)
356
+ Formatter.info 'Spec coverage details:'
416
357
 
417
- puts ''
358
+ if file == options[:spec_dir]
359
+ matcher = /[|+]$/
360
+ else
361
+ impl = file.sub('_spec', '').sub(options[:spec_dir], '')
362
+ matcher = /(-+|All files|% Lines|#{ Regexp.escape(File.basename(impl)) }|#{ File.dirname(impl).sub(/^\//, '') }\/[^\/])/
363
+ end
418
364
 
419
- `#{coverage_bin} report --root #{ coverage_root } text-summary #{ coverage_file }`.each_line do |line|
420
- puts line.sub(/\n$/, '') if line =~ /\)$/
421
- end
365
+ puts ''
422
366
 
423
- puts ''
367
+ `#{coverage_bin} report --root #{ coverage_root } text #{ coverage_file }`.each_line do |line|
368
+ puts line.sub(/\n$/, '') if line =~ matcher
424
369
  end
425
370
 
426
- # Specdoc like formatting of the result.
427
- #
428
- # @param [Hash] result the suite result
429
- # @param [Boolean] passed status
430
- #
431
- def report_specdoc(result, passed)
432
- result['suites'].each do |suite|
433
- report_specdoc_suite(suite, passed)
434
- end
435
- end
371
+ puts ''
372
+ end
436
373
 
437
- # Show the suite result.
438
- #
439
- # @param [Hash] suite the suite
440
- # @param [Boolean] passed status
441
- # @param [Number] level the indention level
442
- #
443
- def report_specdoc_suite(suite, run_passed, level = 0)
444
-
445
- # Print the suite description when the specdoc is shown or there are logs to display
446
- Formatter.suite_name((' ' * level) + suite['description'])
447
-
448
- suite['specs'].each do |spec|
449
- # Specs are shown if they failed, or if they passed and the "focus" option is falsey
450
- # If specs are going to be shown, then pending are also shown
451
- # If the focus option is set, then only failing tests are shown
452
- next unless :always==options[:specdoc] || spec['status'] == 'failed' || ( !run_passed && !options[:focus] )
453
- if spec['status'] == 'passed'
454
- Formatter.success(indent(" ✔ #{ spec['description'] }", level))
455
- elsif spec['status'] == 'failed'
456
- Formatter.spec_failed(indent(" ✘ #{ spec['description'] }", level))
457
- else
458
- Formatter.spec_pending(indent(" ○ #{ spec['description'] }", level))
459
- end
460
- report_specdoc_errors(spec, level)
461
- report_specdoc_logs(spec, level)
374
+ # Uses the Istanbul text reported to output the result of the
375
+ # last coverage run.
376
+ #
377
+ def check_coverage
378
+ if any_coverage_threshold?
379
+ coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options } #{ coverage_file } 2>&1`
380
+ coverage = coverage.split("\n").grep(/ERROR/).join.sub('ERROR:', '')
381
+ failed = $CHILD_STATUS && $CHILD_STATUS.exitstatus != 0
382
+
383
+ if failed
384
+ Formatter.error coverage
385
+ Formatter.notify(coverage, title: 'Code coverage failed', image: :failed, priority: 2) if options[:notification]
386
+ else
387
+ Formatter.success 'Code coverage succeed'
388
+ Formatter.notify('All code is adequately covered with specs', title: 'Code coverage succeed') if options[:notification] && !options[:hide_success]
462
389
  end
463
-
464
- suite['suites'].each { |s| report_specdoc_suite(s, run_passed, level + 2) } if suite['suites']
465
- end
466
-
467
- # Is the specdoc shown for this suite?
468
- #
469
- # @param [Boolean] passed the spec status
470
- #
471
- def specdoc_shown?(passed)
472
- options[:specdoc] == :always || (options[:specdoc] == :failure && !passed)
473
390
  end
391
+ end
474
392
 
393
+ # Uses the Istanbul text reported to output the result of the
394
+ # last coverage run.
395
+ #
396
+ def generate_html_report
397
+ report_directory = coverage_report_directory
398
+ `#{coverage_bin} report --dir #{ report_directory } --root #{ coverage_root } html #{ coverage_file }`
399
+ Formatter.info "Updated HTML report available at: #{ report_directory }/index.html"
400
+ end
475
401
 
476
- # Are console logs shown for this spec?
477
- #
478
- # @param [Hash] spec the spec
479
- #
480
- def console_for_spec?(spec)
481
- spec['logs'] && (( spec['status'] == 'passed' && options[:console] == :always) ||
482
- (spec['status'] == 'failed' && options[:console] != :never) )
483
- end
402
+ # Uses the Istanbul text-summary reporter to output the
403
+ # summary of all the coverage runs combined.
404
+ #
405
+ def generate_summary_report
406
+ Formatter.info 'Spec coverage summary:'
484
407
 
408
+ puts ''
485
409
 
486
- # Are errors shown for this spec?
487
- #
488
- # @param [Hash] spec the spec
489
- def errors_for_spec?(spec)
490
- spec['errors'] && ((spec['status']=='passed' && options[:errors] == :always) ||
491
- (spec['status']=='failed' && options[:errors] != :never))
410
+ `#{coverage_bin} report --root #{ coverage_root } text-summary #{ coverage_file }`.each_line do |line|
411
+ puts line.sub(/\n$/, '') if line =~ /\)$/
492
412
  end
493
413
 
494
- # Is the description shown for this spec?
495
- #
496
- # @param [Boolean] passed the spec status
497
- # @param [Hash] spec the spec
498
- #
499
- def description_shown?(passed, spec)
500
- specdoc_shown?(passed) || console_for_spec?(spec) || errors_for_spec?(spec)
501
- end
414
+ puts ''
415
+ end
502
416
 
503
- # Shows the logs for a given spec.
504
- #
505
- # @param [Hash] spec the spec result
506
- # @param [Number] level the indention level
507
- #
508
- def report_specdoc_logs(spec, level)
509
- if console_for_spec?(spec)
510
- spec['logs'].each do |log_level, message|
511
- log_level = log_level == 'log' ? '' : "#{log_level.upcase}: "
512
- Formatter.info(indent(" • #{log_level}#{ message }", level))
513
- end
514
- end
417
+ # Specdoc like formatting of the result.
418
+ #
419
+ # @param [Hash] result the suite result
420
+ # @param [Boolean] passed status
421
+ #
422
+ def report_specdoc(result, passed)
423
+ result['suites'].each do |suite|
424
+ report_specdoc_suite(suite, passed)
515
425
  end
426
+ end
516
427
 
517
- # Shows the errors for a given spec.
518
- #
519
- # @param [Hash] spec the spec result
520
- # @param [Number] level the indention level
521
- #
522
- def report_specdoc_errors(spec, level)
523
- if spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && spec['status']=='failed'))
524
- spec['errors'].each do |error|
525
- Formatter.spec_failed(indent(" ➤ #{ format_error(error,true) }", level))
526
- if error['trace']
527
- error['trace'].each do |trace|
528
- Formatter.spec_failed(indent(" ➜ #{ trace['file'] } on line #{ trace['line'] }", level+2))
529
- end
530
- end
531
- end
428
+ # Show the suite result.
429
+ #
430
+ # @param [Hash] suite the suite
431
+ # @param [Boolean] passed status
432
+ # @param [Number] level the indention level
433
+ #
434
+ def report_specdoc_suite(suite, run_passed, level = 0)
435
+ # Print the suite description when the specdoc is shown or there are logs to display
436
+ Formatter.suite_name((' ' * level) + suite['description'])
437
+
438
+ suite['specs'].each do |spec|
439
+ # Specs are shown if they failed, or if they passed and the "focus" option is falsey
440
+ # If specs are going to be shown, then pending are also shown
441
+ # If the focus option is set, then only failing tests are shown
442
+ next unless :always == options[:specdoc] || spec['status'] == 'failed' || (!run_passed && !options[:focus])
443
+ if spec['status'] == 'passed'
444
+ Formatter.success(indent(" ✔ #{ spec['description'] }", level))
445
+ elsif spec['status'] == 'failed'
446
+ Formatter.spec_failed(indent(" ✘ #{ spec['description'] }", level))
447
+ else
448
+ Formatter.spec_pending(indent(" ○ #{ spec['description'] }", level))
532
449
  end
450
+ report_specdoc_errors(spec, level)
451
+ report_specdoc_logs(spec, level)
533
452
  end
534
453
 
535
- # Indent a message.
536
- #
537
- # @param [String] message the message
538
- # @param [Number] level the indention level
539
- #
540
- def indent(message, level)
541
- (' ' * level) + message
542
- end
454
+ suite['suites'].each { |s| report_specdoc_suite(s, run_passed, level + 2) } if suite['suites']
455
+ end
543
456
 
457
+ # Is the specdoc shown for this suite?
458
+ #
459
+ # @param [Boolean] passed the spec status
460
+ #
461
+ def specdoc_shown?(passed)
462
+ options[:specdoc] == :always || (options[:specdoc] == :failure && !passed)
463
+ end
544
464
 
545
- # Tests if the given suite has a failing spec underneath.
546
- #
547
- # @param [Hash] suite the suite result
548
- # @return [Boolean] the search result
549
- #
550
- def contains_failed_spec?(suite)
551
- collect_specs([suite]).any? { |spec| spec['status'] == 'failed' }
552
- end
465
+ # Are console logs shown for this spec?
466
+ #
467
+ # @param [Hash] spec the spec
468
+ #
469
+ def console_for_spec?(spec)
470
+ spec['logs'] && ((spec['status'] == 'passed' && options[:console] == :always) ||
471
+ (spec['status'] == 'failed' && options[:console] != :never))
472
+ end
553
473
 
474
+ # Are errors shown for this spec?
475
+ #
476
+ # @param [Hash] spec the spec
477
+ def errors_for_spec?(spec)
478
+ spec['errors'] && ((spec['status'] == 'passed' && options[:errors] == :always) ||
479
+ (spec['status'] == 'failed' && options[:errors] != :never))
480
+ end
554
481
 
555
- # Get all failed specs from the suites and its nested suites.
556
- #
557
- # @param suites [Array<Hash>] the suites results
558
- # @return [Array<Hash>] all failed
559
- #
560
- def collect_spec_errors(suites)
561
- collect_specs(suites).map { |spec|
562
- (spec['errors']||[]).map { |error| format_error(error,false) }
563
- }.flatten
564
- end
482
+ # Is the description shown for this spec?
483
+ #
484
+ # @param [Boolean] passed the spec status
485
+ # @param [Hash] spec the spec
486
+ #
487
+ def description_shown?(passed, spec)
488
+ specdoc_shown?(passed) || console_for_spec?(spec) || errors_for_spec?(spec)
489
+ end
565
490
 
566
- # Get all specs from the suites and its nested suites.
567
- #
568
- # @param suites [Array<Hash>] the suites results
569
- # @return [Array<Hash>] all specs
570
- #
571
- def collect_specs(suites)
572
- suites.each_with_object([]) do |suite, specs|
573
- specs.concat( suite['specs'] )
574
- specs.concat( collect_specs(suite['suites']) ) if suite['suites']
491
+ # Shows the logs for a given spec.
492
+ #
493
+ # @param [Hash] spec the spec result
494
+ # @param [Number] level the indention level
495
+ #
496
+ def report_specdoc_logs(spec, level)
497
+ if console_for_spec?(spec)
498
+ spec['logs'].each do |log_level, message|
499
+ log_level = log_level == 'log' ? '' : "#{log_level.upcase}: "
500
+ Formatter.info(indent(" • #{log_level}#{ message }", level))
575
501
  end
576
502
  end
503
+ end
577
504
 
578
- # Formats a message.
579
- #
580
- # @param [String] message the error message
581
- # @param [Boolean] short show a short version of the message
582
- # @return [String] the cleaned error message
583
- #
584
- def format_error(error, short)
585
- message = error['message'].gsub(%r{ in http.*\(line \d+\)$},'')
586
- if !short && error['trace'] && error['trace'].length > 0
587
- location = error['trace'][0]
588
- "#{message} in #{location['file']}:#{location['line']}"
589
- else
590
- message
505
+ # Shows the errors for a given spec.
506
+ #
507
+ # @param [Hash] spec the spec result
508
+ # @param [Number] level the indention level
509
+ #
510
+ def report_specdoc_errors(spec, level)
511
+ return unless spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && spec['status'] == 'failed'))
512
+
513
+ spec['errors'].each do |error|
514
+ Formatter.spec_failed(indent(" ➤ #{ format_error(error, true) }", level))
515
+ next unless error['trace']
516
+
517
+ error['trace'].each do |trace|
518
+ Formatter.spec_failed(indent(" ➜ #{ trace['file'] } on line #{ trace['line'] }", level + 2))
591
519
  end
592
520
  end
521
+ end
593
522
 
594
- # Updates the coverage data with new data for the implementation file.
595
- # It replaces the coverage data if the file is the spec dir.
596
- #
597
- # @param [Hash] coverage the last run coverage data
598
- # @param [String] file the file name of the spec
599
- #
600
- def update_coverage(coverage, file)
601
- if file == options[:spec_dir]
602
- File.write(coverage_file, MultiJson.encode(coverage, { max_nesting: false }))
603
- else
604
- if File.exist?(coverage_file)
605
- impl = file.sub('_spec', '').sub(options[:spec_dir], '')
606
- coverage = MultiJson.decode(File.read(coverage_file), { max_nesting: false })
523
+ # Indent a message.
524
+ #
525
+ # @param [String] message the message
526
+ # @param [Number] level the indention level
527
+ #
528
+ def indent(message, level)
529
+ (' ' * level) + message
530
+ end
607
531
 
608
- coverage.each do |coverage_file, data|
609
- coverage[coverage_file] = data if coverage_file == impl
610
- end
532
+ # Tests if the given suite has a failing spec underneath.
533
+ #
534
+ # @param [Hash] suite the suite result
535
+ # @return [Boolean] the search result
536
+ #
537
+ def contains_failed_spec?(suite)
538
+ collect_specs([suite]).any? { |spec| spec['status'] == 'failed' }
539
+ end
611
540
 
612
- File.write(coverage_file, MultiJson.encode(coverage, { max_nesting: false }))
613
- else
614
- File.write(coverage_file, MultiJson.encode({ }))
615
- end
616
- end
617
- end
541
+ # Get all failed specs from the suites and its nested suites.
542
+ #
543
+ # @param suites [Array<Hash>] the suites results
544
+ # @return [Array<Hash>] all failed
545
+ #
546
+ def collect_spec_errors(suites)
547
+ collect_specs(suites).map do |spec|
548
+ (spec['errors'] || []).map { |error| format_error(error, false) }
549
+ end.flatten
550
+ end
618
551
 
619
- # Do we should check the coverage?
620
- #
621
- # @return [Boolean] true if any coverage threshold is set
622
- #
623
- def any_coverage_threshold?
624
- THRESHOLDS.any? { |threshold| options[threshold] != 0 }
552
+ # Get all specs from the suites and its nested suites.
553
+ #
554
+ # @param suites [Array<Hash>] the suites results
555
+ # @return [Array<Hash>] all specs
556
+ #
557
+ def collect_specs(suites)
558
+ suites.each_with_object([]) do |suite, specs|
559
+ specs.concat(suite['specs'])
560
+ specs.concat(collect_specs(suite['suites'])) if suite['suites']
625
561
  end
562
+ end
626
563
 
627
- # Converts the options to Istanbul recognized options
628
- #
629
- # @return [String] the command line options
630
- #
631
- def istanbul_coverage_options
632
- THRESHOLDS.inject([]) do |coverage, name|
633
- threshold = options[name]
634
- coverage << (threshold != 0 ? "--#{ name.to_s.sub('_threshold', '') } #{ threshold }" : '')
635
- end.reject(&:empty?).join(' ')
564
+ # Formats a message.
565
+ #
566
+ # @param [String] message the error message
567
+ # @param [Boolean] short show a short version of the message
568
+ # @return [String] the cleaned error message
569
+ #
570
+ def format_error(error, short)
571
+ message = error['message'].gsub(%r{ in http.*\(line \d+\)$}, '')
572
+ if !short && error['trace'] && error['trace'].length > 0
573
+ location = error['trace'][0]
574
+ "#{message} in #{location['file']}:#{location['line']}"
575
+ else
576
+ message
636
577
  end
578
+ end
637
579
 
638
- # Returns the coverage executable path.
639
- #
640
- # @return [String] the path
641
- #
642
- def coverage_bin
643
- @coverage_bin ||= which 'istanbul'
644
- end
580
+ # Updates the coverage data with new data for the implementation file.
581
+ # It replaces the coverage data if the file is the spec dir.
582
+ #
583
+ # @param [Hash] coverage the last run coverage data
584
+ # @param [String] file the file name of the spec
585
+ #
586
+ def update_coverage(coverage, file)
587
+ if file == options[:spec_dir]
588
+ File.write(coverage_file, MultiJson.encode(coverage, max_nesting: false))
589
+ else
590
+ if File.exist?(coverage_file)
591
+ impl = file.sub('_spec', '').sub(options[:spec_dir], '')
592
+ coverage = MultiJson.decode(File.read(coverage_file), max_nesting: false)
593
+
594
+ coverage.each do |coverage_file, data|
595
+ coverage[coverage_file] = data if coverage_file == impl
596
+ end
645
597
 
646
- # Get the coverage file to save all coverage data.
647
- # Creates `tmp/coverage` if not exists.
648
- #
649
- # @return [String] the filename to use
650
- #
651
- def coverage_file
652
- File.join(coverage_root, 'coverage.json')
598
+ File.write(coverage_file, MultiJson.encode(coverage, max_nesting: false))
599
+ else
600
+ File.write(coverage_file, MultiJson.encode({}))
601
+ end
653
602
  end
603
+ end
654
604
 
655
- # Create and returns the coverage root directory.
656
- #
657
- # @return [String] the coverage root
658
- #
659
- def coverage_root
660
- File.expand_path(File.join('tmp', 'coverage'))
661
- end
605
+ # Do we should check the coverage?
606
+ #
607
+ # @return [Boolean] true if any coverage threshold is set
608
+ #
609
+ def any_coverage_threshold?
610
+ THRESHOLDS.any? { |threshold| options[threshold] != 0 }
611
+ end
662
612
 
663
- # Creates and returns the coverage report directory.
664
- #
665
- # @return [String] the coverage report directory
666
- #
667
- def coverage_report_directory
668
- File.expand_path(options[:coverage_html_dir])
669
- end
613
+ # Converts the options to Istanbul recognized options
614
+ #
615
+ # @return [String] the command line options
616
+ #
617
+ def istanbul_coverage_options
618
+ THRESHOLDS.inject([]) do |coverage, name|
619
+ threshold = options[name]
620
+ coverage << (threshold != 0 ? "--#{ name.to_s.sub('_threshold', '') } #{ threshold }" : '')
621
+ end.reject(&:empty?).join(' ')
622
+ end
623
+
624
+ # Returns the coverage executable path.
625
+ #
626
+ # @return [String] the path
627
+ #
628
+ def coverage_bin
629
+ @coverage_bin ||= which 'istanbul'
630
+ end
631
+
632
+ # Get the coverage file to save all coverage data.
633
+ # Creates `tmp/coverage` if not exists.
634
+ #
635
+ # @return [String] the filename to use
636
+ #
637
+ def coverage_file
638
+ File.join(coverage_root, 'coverage.json')
639
+ end
640
+
641
+ # Create and returns the coverage root directory.
642
+ #
643
+ # @return [String] the coverage root
644
+ #
645
+ def coverage_root
646
+ File.expand_path(File.join('tmp', 'coverage'))
647
+ end
648
+
649
+ # Creates and returns the coverage report directory.
650
+ #
651
+ # @return [String] the coverage report directory
652
+ #
653
+ def coverage_report_directory
654
+ File.expand_path(options[:coverage_html_dir])
655
+ end
670
656
  end
671
657
  end
672
658
  end
673
-