guard-jasmine 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
-