Permalink
Browse files

Basic support for scenario outlines

The primary shortcoming of this implementation is that
it generates scenarios using the outlines at parse-time,
which results in the ecukes output being the scenarios
rather than the outlines that cucumber proper would
print out.

But if you really want scenario outlines, like I did,
this is better than nothing!
  • Loading branch information...
1 parent c915913 commit 48e691bc3c64635e9fc17e719400fd7fe4b8ec9a @pd pd committed Jan 11, 2013
View
@@ -3,7 +3,7 @@
(defstruct ecukes-feature
"A feature is the top level structure for a feature file."
- intro background scenarios)
+ intro background outlines scenarios)
(defstruct ecukes-intro
"A feature introduction is a description of a feature. It is
@@ -14,6 +14,10 @@ optional, but is conventionally included."
"A feature background is a few steps that are run before each scenario."
steps)
+(defstruct ecukes-outline
+ "A scenario outline contains examples that are used to generate concrete scenarios."
+ name steps tags table)
+
(defstruct ecukes-scenario
"A feature scenario is a scenario that is built up by steps."
name steps tags)
View
@@ -13,6 +13,14 @@
"^[\t ]*Scenario:[\t ]*\\(.+?\\)[\t ]*$"
"Regexp matching scenario header.")
+(defconst ecukes-parse-outline-re
+ "^[\t ]*Scenario Outline:[\t ]*\\(.+?\\)[\t ]*$"
+ "Regexp matching scenario outline header.")
+
+(defconst ecukes-parse-examples-re
+ "^[\t ]*Examples:"
+ "Regexp matching scenario outline examples header.")
+
(defconst ecukes-parse-step-re
"^\\s-*\\(Given\\|When\\|Then\\|And\\|But\\)\\s-+\\(.+[^ ]\\)\\s-*$"
"Regexp matching step.")
@@ -35,10 +43,11 @@
(with-temp-buffer
(insert-file-contents-literally feature)
(goto-char (point-min))
- (let ((intro (ecukes-parse-intro))
- (background (ecukes-parse-background))
- (scenarios (ecukes-parse-scenarios)))
- (make-ecukes-feature :intro intro :background background :scenarios scenarios))))
+ (let* ((intro (ecukes-parse-intro))
+ (background (ecukes-parse-background))
+ (outlines (ecukes-parse-outlines))
+ (scenarios (append (ecukes-parse-scenarios) (-mapcat 'ecukes-generate-outlined-scenarios outlines))))
+ (make-ecukes-feature :intro intro :background background :outlines outlines :scenarios scenarios))))
(defun ecukes-parse-intro ()
"Parse intro."
@@ -55,8 +64,81 @@
(let ((steps (ecukes-parse-block-steps)))
(make-ecukes-background :steps steps))))
+(defun ecukes-parse-outlines ()
+ "Parse all scenario outlines."
+ (goto-char (point-min))
+ (let ((outlines))
+ (while (re-search-forward ecukes-parse-outline-re nil t)
+ (add-to-list 'outlines (ecukes-parse-outline) t))
+ outlines))
+
+(defun ecukes-parse-outline ()
+ "Parse a single scenario outline."
+ (let ((name (ecukes-parse-outline-name))
+ (tags (ecukes-parse-scenario-tags))
+ (steps (ecukes-parse-block-steps))
+ (table (ecukes-parse-outline-table)))
+ (make-ecukes-outline :name name :tags tags :steps steps :table table)))
+
+(defun ecukes-parse-outline-name ()
+ "Parse scenario outline name."
+ (save-excursion
+ (let ((line (ecukes-parse-line)))
+ (nth 1 (s-match ecukes-parse-outline-re line)))))
+
+(defun ecukes-parse-outline-table ()
+ "Parse examples table for a scenario outline."
+ (save-excursion
+ (catch 'table
+ (let ((line (ecukes-parse-line)))
+ (while (and (not (s-matches? ecukes-parse-examples-re (or line "")))
+ (not (ecukes-parse-new-section-p)))
+ (forward-line 1)
+ (setq line (ecukes-parse-line)))
+ (when (s-matches? ecukes-parse-examples-re (or line ""))
+ (throw 'table (ecukes-parse-table-step)))))))
+
+(defun ecukes-substitute-in-steps (steps subs)
+ (-map (lambda (step)
+ (let ((gen (copy-ecukes-step step))
+ (type (ecukes-step-type step)))
+ (setf (ecukes-step-name gen) (ecukes-substitute-in-string (ecukes-step-name gen) subs)
+ (ecukes-step-body gen) (ecukes-substitute-in-string (ecukes-step-body gen) subs))
+ (cond
+ ((eq type 'py-string)
+ (setf (ecukes-step-arg gen) (ecukes-substitute-in-string (ecukes-step-arg gen) subs)))
+ ((eq type 'table)
+ (setf (ecukes-step-arg gen) (ecukes-substitute-in-table (ecukes-step-arg gen) subs))))
+ gen))
+ steps))
+
+(defun ecukes-substitute-in-string (string subs)
+ (let ((new-s (copy-sequence string))
+ (reps (copy-sequence subs)))
+ (while (not (zerop (length reps)))
+ (setq new-s (s-replace (format "<%s>" (car reps)) (cadr reps) new-s))
+ (setq reps (cddr reps)))
+ new-s))
+
+(defun ecukes-substitute-in-table (table subs)
+ (-map (lambda (row)
+ (-map (lambda (cell) (ecukes-substitute-in-string cell subs)) row))
+ table))
+
+(defun ecukes-generate-outlined-scenarios (outline)
+ "Generate scenarios from an outline."
+ (let* ((name (ecukes-outline-name outline))
+ (steps (ecukes-outline-steps outline))
+ (tags (ecukes-outline-tags outline))
+ (table (ecukes-outline-table outline))
+ (header (car table)))
+ (-map (lambda (row)
+ (make-ecukes-scenario :name name :tags tags :steps (ecukes-substitute-in-steps steps (-interleave header row))))
+ (cdr table))))
+
(defun ecukes-parse-scenarios ()
"Parse scenarios."
+ (goto-char (point-min))
(let ((scenarios))
(while (re-search-forward ecukes-parse-scenario-re nil t)
(add-to-list 'scenarios (ecukes-parse-scenario) t))
@@ -179,6 +261,8 @@
(or
(eobp)
(s-matches? ecukes-parse-background-re line)
+ (s-matches? ecukes-parse-outline-re line)
+ (s-matches? ecukes-parse-examples-re line)
(s-matches? ecukes-parse-scenario-re line)
(s-matches? ecukes-parse-tags-re line))))
@@ -0,0 +1,80 @@
+(require 'ecukes-parse)
+
+(defun with-parse-outline (name fn)
+ (let* ((feature-file (fixture-file-path "outlines" name))
+ (feature (ecukes-parse-feature feature-file))
+ (outlines (ecukes-feature-outlines feature))
+ (scenarios (ecukes-feature-scenarios feature)))
+ (funcall fn outlines scenarios)))
+
+(defun should-parse-outline (name expected-examples &optional tests)
+ (with-parse-outline
+ name
+ (lambda (outlines scenarios)
+ (should (= 1 (length outlines)))
+ (let* ((outline (car outlines))
+ (tags (ecukes-outline-tags outline))
+ (table (ecukes-outline-table outline)))
+ (should (equal expected-examples (1- (length table))))
+ (should (equal expected-examples (length scenarios)))
+ (should (-all? (lambda (scenario) (equal tags (ecukes-scenario-tags scenario))) scenarios))
+ (when (functionp tests) (funcall tests (car outlines) scenarios))))))
+
+(ert-deftest parse-outline-no-examples ()
+ "Should parse scenario outlines with no examples."
+ (with-parse-outline
+ "no-examples"
+ (lambda (outlines scenarios)
+ (should (= 1 (length outlines)))
+ (should-not (ecukes-outline-table (car outlines)))
+ (should (= 0 (length scenarios))))))
+
+(ert-deftest parse-outline-one-example ()
+ "Should parse scenario outline with one example."
+ (should-parse-outline
+ "one-example" 1
+ (lambda (outline scenarios)
+ (should (ecukes-outline-table outline))
+ (should (= 1 (length scenarios))))))
+
+(ert-deftest parse-outline-multiple-examples ()
+ "Should parse scenario outlines with multiple examples."
+ (should-parse-outline "multiple-examples" 3))
+
+(ert-deftest parse-outline-tags ()
+ "Should parse scenario outlines with tags."
+ (should-parse-outline "tags" 2))
+
+(ert-deftest parse-outline-background ()
+ "Should parse scenario outlines with backgrounds."
+ (should-parse-outline "background" 1))
+
+(ert-deftest parse-outline-bad-indents ()
+ "Should parse scenario outlines with bad indentation."
+ (should-parse-outline "bad-indents" 2))
+
+(ert-deftest parse-outline-substitution ()
+ "Should substitute the values from examples into the generated scenarios."
+ (should-parse-outline
+ "substitution" 3
+ (lambda (outline scenarios)
+ (let* ((scenario (car scenarios))
+ (simple-step (car (ecukes-scenario-steps scenario)))
+ (py-step (nth 1 (ecukes-scenario-steps scenario)))
+ (table-step (nth 2 (ecukes-scenario-steps scenario))))
+ (should (string= "Given I want to marry" (ecukes-step-name simple-step)))
+ (should (string= "I want to marry" (ecukes-step-body simple-step)))
+ (should-not (ecukes-step-arg simple-step))
+
+ (should (string= "You are great! I want to marry you." (ecukes-step-arg py-step)))
+
+ (should (equal '(("response" "desired") ("positive" "true"))
+ (ecukes-step-arg table-step)))))))
+
+(ert-deftest parse-outline-wrong-column-names ()
+ "Generates scenarios without any substitutions if your column names are wrong."
+ (should-parse-outline
+ "wrong-column-names" 1
+ (lambda (outline scenarios)
+ (let ((step (car (ecukes-scenario-steps (car scenarios)))))
+ (should (string= "Given <foo> is <bar>" (ecukes-step-name step)))))))
@@ -0,0 +1,11 @@
+Feature: scenario outlines
+
+ Background:
+ Given I do something
+
+ Scenario Outline: with background
+ Given <foo> is <bar>
+
+ Examples:
+ | foo | bar |
+ | 1 | 2 |
@@ -0,0 +1,9 @@
+Feature: scenario outlines
+
+ Scenario Outline: weird indents
+ Given <foo> is <bar>
+
+ Examples:
+ | foo | bar |
+ | 1 | 2 |
+ | 3 | 4|
@@ -0,0 +1,10 @@
+Feature: scenario outline
+
+ Scenario Outline: multiple examples
+ Given <foo> is <bar>
+
+ Examples:
+ | foo | bar |
+ | 1 | 2 |
+ | 3 | 4 |
+ | 5 | 6 |
@@ -0,0 +1,4 @@
+Feature: scenario outlines
+
+ Scenario Outline: no examples
+ Given <foo> is <bar>
@@ -0,0 +1,8 @@
+Feature: scenario outlines
+
+ Scenario Outline: single example given
+ Given <foo> is <bar>
+
+ Examples:
+ | foo | bar |
+ | 1 | 2 |
@@ -0,0 +1,17 @@
+Feature: scenario outlines
+
+ Scenario Outline: py-string and table substitution
+ Given I want to <activity>
+ When I say:
+ """
+ You are <compliment>! I want to <activity> you.
+ """
+ Then the results are:
+ | response | desired |
+ | <response> | <desired> |
+
+ Examples:
+ | activity | compliment | response | desired |
+ | marry | great | positive | true |
+ | marry | not bad | negative | false |
+ | divorce | an idiot | negative | true |
@@ -0,0 +1,10 @@
+Feature: scenario outlines
+
+ @regression @ui
+ Scenario Outline: with tags
+ Given <foo> is <bar>
+
+ Examples:
+ | foo | bar |
+ | 1 | 2 |
+ | 3 | 4 |
@@ -0,0 +1,8 @@
+Feature: scenario outlines
+
+ Scenario Outline: wrong column names
+ Given <foo> is <bar>
+
+ Examples:
+ | baz | quux |
+ | 1 | 2 |

0 comments on commit 48e691b

Please sign in to comment.