forked from ecukes/ecukes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ecukes-reporter.el
431 lines (346 loc) · 13.9 KB
/
ecukes-reporter.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
;;; ecukes-reporter.el --- generic reporter interface and helper functions
(require 'f)
(require 's)
(require 'dash)
(require 'ansi)
(require 'ecukes-core)
(require 'ecukes-def)
(require 'ecukes-steps)
(require 'ecukes-template)
;;;; Variables and constants
(eval-when-compile
(defvar ecukes-path)
(defvar ecukes-failing-scenarios-file))
(defconst ecukes-reporters
'((gangsta . "gangsta talk")
(landing . "landing plane")
(magnars . "@magnars stripped spec")
(progress . "progress bar")
(spec . "full blown spec")
(dot . "one colored dot per scenario (default)"))
"List of available reporters, with description.")
(defconst ecukes-reporters-path
(f-expand "reporters" ecukes-path)
"Path to reporters directory.")
;;;; Hooks
(defvar ecukes-reporter-start-hook nil
"Called before anything runs.")
(defvar ecukes-reporter-end-hook nil
"Called when everything has run, with stats as argument.
The stats alist contains these slots:
- `scenarios' total number of scenarios
- `scenarios-passed' number of passed scenarios
- `scenarios-failed' number of failed scenarios
- `steps' total number of steps
- `steps-passed' number of passed steps
- `steps-failed' number of failed steps
- `steps-skipped' number of skipped steps")
(defvar ecukes-reporter-before-first-feature-hook nil
"Called before first feature runs, with feature as argument.")
(defvar ecukes-reporter-before-last-feature-hook nil
"Called before last feature runs, with feature as argument.")
(defvar ecukes-reporter-before-feature-hook nil
"Called before feature runs with, feature as argument.")
(defvar ecukes-reporter-after-first-feature-hook nil
"Called after first feature runs, with feature as argument.")
(defvar ecukes-reporter-after-last-feature-hook nil
"Called after last feature runs, with feature as argument..")
(defvar ecukes-reporter-after-feature-hook nil
"Called after feature runs, with feature as argument.")
(defvar ecukes-reporter-before-first-scenario-hook nil
"Called before first scenario runs, with scenario as argument.")
(defvar ecukes-reporter-before-last-scenario-hook nil
"Called before last scenario runs, with scenario as argument.")
(defvar ecukes-reporter-before-scenario-hook nil
"Called before scenario runs, with scenario as argument.")
(defvar ecukes-reporter-after-first-scenario-hook nil
"Called before after first scenario runs, with scenario as argument.")
(defvar ecukes-reporter-after-last-scenario-hook nil
"Called before after last scenario runs, with scenario as argument.")
(defvar ecukes-reporter-after-scenario-hook nil
"Called before after scenario runs, with scenario as argument.")
(defvar ecukes-reporter-scenario-passed-hook nil
"Called when scenario passed, with scenario as argument.")
(defvar ecukes-reporter-scenario-failed-hook nil
"Called when scenario failed, with scenario as argument.")
(defvar ecukes-reporter-pending-scenario-hook nil
"Called for pending scenarios.
That is scenarios that will not run becuase of for example some
pattern, anti-pattern or tags.")
(defvar ecukes-reporter-before-first-step-hook nil
"Called before first step runs, with step and status as arguments.")
(defvar ecukes-reporter-before-last-step-hook nil
"Called before last step runs, with step and status as arguments.")
(defvar ecukes-reporter-before-step-hook nil
"Called before step runs, with step and status as arguments.")
(defvar ecukes-reporter-after-first-step-hook nil
"Called before after first step runs, with step and status as arguments.")
(defvar ecukes-reporter-after-last-step-hook nil
"Called before after last step runs, with step and status as arguments.")
(defvar ecukes-reporter-after-step-hook nil
"Called before after step runs, with step and status as arguments.")
(defvar ecukes-reporter-after-step-success-hook nil
"Called after step success, with step as argument.")
(defvar ecukes-reporter-after-step-failed-hook nil
"Called after step failed, with step as argument.")
(defvar ecukes-reporter-after-step-skipped-hook nil
"Called after step skipped, with step as argument.")
(defvar ecukes-reporter-after-step-hook nil
"Called after step, with step as argument.")
(defvar ecukes-reporter-before-background-hook nil
"Called before backgrund runs.")
(defvar ecukes-reporter-after-background-hook nil
"Called after backgrund runs.")
(defvar ecukes-reporter-steps-without-definition-hook nil
"...")
;;;; Internal helpers
(defun ecukes-reporter-valid? (reporter)
"Return if REPORTER is valid or not."
(let ((reporters (--map (symbol-name (car it)) ecukes-reporters)))
(-contains? reporters reporter)))
(defvar ecukes-reporter-failed-scenarios nil
"List of failing scenarios.")
(add-hook 'ecukes-reporter-scenario-failed-hook
(lambda (scenario)
(add-to-list 'ecukes-reporter-failed-scenarios scenario t)))
;;;; Core functions
(defun ecukes-reporter-use (reporter)
"Use REPORTER."
(unless (ecukes-reporter-valid? reporter)
(ecukes-fail "Invalid reporter: %s" reporter))
(let ((full-reporter (format "ecukes-reporter-%s" reporter)))
(require (intern full-reporter)
(f-expand full-reporter ecukes-reporters-path))))
(defun ecukes-reporter-print (&rest args)
"Print message.
If first message is an integer, indent with that amount of
whitespace before the actual text content is printed.
The rest of the arguments will be applied to `format'."
(let (indent format-string objects)
(cond ((stringp (car args))
(setq indent 0)
(setq format-string (car args))
(setq objects (cdr args)))
(:else
(setq indent (car args))
(setq format-string (nth 1 args))
(setq objects (-drop 2 args))))
(let ((ecukes-message t))
(princ (s-concat (s-repeat indent " ") (apply 'format (cons format-string objects)))))))
(defun ecukes-reporter-println (&rest args)
"Like `ecukes-reporter-print' but also prints a newline."
(apply 'ecukes-reporter-print args)
(ecukes-reporter-print-newline))
(defun ecukes-reporter-print-newline ()
"Print newline."
(ecukes-reporter-print "\n"))
;;;; Summary
(defun ecukes-reporter-print-scenarios-summary (stats)
"Print scenario summary."
(let ((scenarios (cdr (assoc 'scenarios stats))))
(ecukes-reporter-print
(if (zerop scenarios)
"0 scenarios"
(format
"%d scenarios (%s, %s)"
scenarios
(ansi-red "%d failed" (cdr (assoc 'scenarios-failed stats)))
(ansi-green "%d passed" (cdr (assoc 'scenarios-passed stats))))))))
(defun ecukes-reporter-print-steps-summary (stats)
"Print step summary."
(let ((steps (cdr (assoc 'steps stats))))
(ecukes-reporter-print
(if (zerop steps)
"0 steps"
(format
"%d steps (%s, %s, %s)"
steps
(ansi-red "%d failed" (cdr (assoc 'steps-failed stats)))
(ansi-cyan "%d skipped" (cdr (assoc 'steps-skipped stats)))
(ansi-green "%d passed" (cdr (assoc 'steps-passed stats))))))))
(defun ecukes-reporter-print-summary (stats)
"Print summary of STATS."
(ecukes-reporter-print-scenarios-summary stats)
(ecukes-reporter-print-newline)
(ecukes-reporter-print-steps-summary stats)
(ecukes-reporter-print-newline))
;;;; Spec style print functions
(defun ecukes-reporter-print-feature-header (feature)
"Print FEATURE and description if any."
(-when-let (intro (ecukes-feature-intro feature))
(let* ((header (ecukes-intro-header intro))
(description (ecukes-intro-description intro))
(scenarios (ecukes-feature-scenarios feature)))
(ecukes-reporter-println "Feature: %s" header)
(--each description (ecukes-reporter-println 2 it))
(when description
(ecukes-reporter-print-newline)))))
(defun ecukes-reporter-print-background-header ()
"Print background header."
(ecukes-reporter-println 2 "Background:"))
(defun ecukes-reporter-print-scenario-header (scenario &optional color)
"Print SCENARIO header."
(let* ((name (ecukes-scenario-name scenario))
(tags (ecukes-scenario-tags scenario))
(header (format "Scenario: %s" name))
(header (if color (ansi-apply color header) header)))
(when tags
(let ((tags-string (ansi-cyan (s-join " " (--map (s-concat "@" it) tags)))))
(ecukes-reporter-println 2 tags-string)))
(ecukes-reporter-println 2 header)))
(defun ecukes-reporter-print-table (step)
"Print STEP table."
(let* (widths
(table (ecukes-step-arg step))
(rows (length table))
(cols (length (car table))))
(-dotimes
cols
(lambda (col)
(let ((width 0))
(-dotimes
rows
(lambda (row)
(setq width (max width (length (nth col (nth row table)))))))
(!cons width widths))))
(setq widths (reverse widths))
(-dotimes
rows
(lambda (row)
(let (col-strings)
(-dotimes
cols
(lambda (col)
(let* ((orig (nth col (nth row table)))
(pad (- (nth col widths) (length orig)))
(col-string (s-concat orig (s-repeat pad " "))))
(push col-string col-strings))))
(ecukes-reporter-println 6 "| %s |" (s-join " | " (nreverse col-strings))))))))
(defun ecukes-reporter-print-py-string (step)
"Print STEP py-string."
(ecukes-reporter-println 6 "\"\"\"")
(let ((lines (s-lines (ecukes-step-arg step))))
(--each lines (ecukes-reporter-println 6 it)))
(ecukes-reporter-println 6 "\"\"\""))
(defun ecukes-reporter-print-step (step)
"Print STEP."
(let* ((name (ecukes-step-name step))
(type (ecukes-step-type step))
(status (ecukes-step-status step))
(color
(cond ((eq status 'success) 'ansi-green)
((eq status 'failure) 'ansi-red)
((eq status 'skipped) 'ansi-cyan))))
(ecukes-reporter-println 4 (funcall color name))
(when (eq type 'table)
(ecukes-reporter-print-table step))
(when (eq type 'py-string)
(ecukes-reporter-print-py-string step))
(let ((err (ecukes-step-err step)))
(when err
(--each (s-lines err)
(ecukes-reporter-println 6 (ansi-red it)))))))
(defun ecukes-reporter-print-failing-scenarios-summary ()
"Print a summary of failing scenarios."
(when ecukes-reporter-failed-scenarios
(-each
ecukes-reporter-failed-scenarios
(lambda (scenario)
(let ((steps (ecukes-scenario-steps scenario)))
(ecukes-reporter-print-newline)
(ecukes-reporter-print-scenario-header scenario)
(-each steps 'ecukes-reporter-print-step))))))
;;;; Missing steps
(defun ecukes-reporter-print-missing-steps (steps)
"Print missing steps"
(ecukes-reporter-println
(ansi-yellow "Please implement the following step definitions"))
(ecukes-reporter-print-newline)
(let (step-bodies)
(-each
steps
(lambda (step)
(let ((step-body (ecukes-reporter--step-body step))
(step-string (ecukes-reporter--step-string step)))
(unless
(--any? (equal step-body it) step-bodies)
(push step-body step-bodies)
(ecukes-reporter-println step-string)))))))
(defun ecukes-reporter--step-string (step)
"Return missing step string."
(let ((head (ecukes-step-head step))
(body (ecukes-reporter--step-body step))
(args (ecukes-reporter--step-args step)))
(ansi-yellow
(ecukes-template-get
'missing-step
`(("head" . ,head)
("body" . ,body)
("args" . ,args))))))
(defun ecukes-reporter--step-args (step)
"Return args from STEP."
(let* (result
(arg (ecukes-step-arg step))
(args (ecukes-steps-args step))
(type (ecukes-step-type step))
(args-count
(+
(length args)
(if (or
(equal type 'table)
(equal type 'py-string)) 1 0))))
(if (= args-count 1)
"arg"
(progn
(-dotimes
args-count
(lambda (n)
(push (format "arg-%d" (1+ n)) result)))
(s-join " " (nreverse result))))))
(defun ecukes-reporter--step-body (step)
"Return body from STEP."
(let* ((body (ecukes-step-body step))
(args (ecukes-steps-args step))
(result body))
(when args
(-each
args
(lambda (arg)
(setq result (s-replace (s-concat "\"" arg "\"") "\\\"\\\\([^\\\"]+\\\\)\\\"" result)))))
result))
(add-hook 'ecukes-reporter-steps-without-definition-hook
(lambda (steps)
(ecukes-reporter-print-missing-steps steps)))
;;;; Printing step definitions
(defun ecukes-reporter-print-steps (&optional with-doc with-file)
"Print all available steps defined for this project.
Include docstring when WITH-DOC is non-nil."
(-each
ecukes-steps-definitions
(lambda (step-def)
(let (row)
(when with-file
(let ((file (ecukes-step-file-name step-def t)))
(setq row (s-concat row file ": "))))
(let ((regex (ecukes-step-def-regex step-def)))
(setq row (s-concat row (ansi-green regex))))
(when with-doc
(let ((doc (ecukes-step-def-doc step-def)))
(when doc
(setq row (s-concat row "\n" (ansi-cyan doc) "\n")))))
(ecukes-reporter-println row)))))
;;;; Save list of failed scenarios to file
(add-hook 'ecukes-reporter-end-hook
(lambda (stats)
(if ecukes-reporter-failed-scenarios
(let* ((scenario-names
(-map
(lambda (scenario)
(s-downcase (ecukes-scenario-name scenario)))
ecukes-reporter-failed-scenarios))
(lines (s-join "\n" scenario-names)))
(f-write-text lines 'utf-8 ecukes-failing-scenarios-file))
(when (f-file? ecukes-failing-scenarios-file)
(f-delete ecukes-failing-scenarios-file)))))
(provide 'ecukes-reporter)
;;; ecukes-reporter.el ends here