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.
- checksums.yaml +4 -4
- data/lib/generators/guard_jasmine/install_generator.rb +2 -3
- data/lib/generators/guard_jasmine/templates/Guardfile +4 -4
- data/lib/guard/jasmine.rb +14 -21
- data/lib/guard/jasmine/cli.rb +5 -10
- data/lib/guard/jasmine/coverage.rb +7 -9
- data/lib/guard/jasmine/formatter.rb +8 -11
- data/lib/guard/jasmine/inspector.rb +1 -4
- data/lib/guard/jasmine/phantomjs/guard-jasmine.js +12 -4
- data/lib/guard/jasmine/phantomjs/src/guard-jasmine.coffee +14 -4
- data/lib/guard/jasmine/runner.rb +508 -523
- data/lib/guard/jasmine/server.rb +42 -43
- data/lib/guard/jasmine/task.rb +2 -5
- data/lib/guard/jasmine/util.rb +1 -5
- data/lib/guard/jasmine/version.rb +1 -1
- metadata +16 -58
data/lib/guard/jasmine/runner.rb
CHANGED
@@ -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!(
|
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(
|
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
|
-
|
68
|
+
private
|
70
69
|
|
71
70
|
# Shows a notification in the console that the runner starts.
|
72
71
|
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
136
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
210
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
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
|
-
|
280
|
-
|
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
|
-
|
323
|
-
|
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
|
-
|
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
|
-
|
342
|
-
|
343
|
-
if options[:
|
344
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
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
|
-
|
381
|
-
end
|
334
|
+
update_coverage(coverage, file)
|
382
335
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
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
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
-
|
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
|
-
|
420
|
-
puts line.sub(/\n$/, '') if line =~ /\)$/
|
421
|
-
end
|
365
|
+
puts ''
|
422
366
|
|
423
|
-
|
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
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
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
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
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
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
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
|
-
|
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
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
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
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
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
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
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
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
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
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
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
|
-
|
609
|
-
|
610
|
-
|
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
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
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
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
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
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
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
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
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
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
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
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
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
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
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
|
-
|