clucumber 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Andreas Fuchs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,54 @@
1
+ Clucumber, the CL Cucumber adapter
2
+ ==================================
3
+
4
+ This is a (somewhat complete) implementation of the [cucumber wire
5
+ protocol](http://wiki.github.com/aslakhellesoy/cucumber/wire-protocol) in (as portable as possible) Common Lisp. This means you can
6
+ write [cucumber](http://cukes.info/) features, and write lisp code to execute
7
+ your steps.
8
+
9
+ Getting started
10
+ ---------------
11
+
12
+ First, you write your cucumber features like you would any other.
13
+
14
+ Then you define cucumber steps in CL: Just place them in
15
+ features/step_definitions/*.lisp.
16
+
17
+ If your application needs any support code, place that in
18
+ support/*.lisp.
19
+
20
+ Files in support and step_definitions/ are loaded (not file-compiled)
21
+ in alphabetical order, with support/ files being loaded before step
22
+ definitions.
23
+
24
+ Running tests
25
+ -------------
26
+
27
+ In your `features/support/env.rb`, you load the clucumber.rb included in this distribution. Then, you run something like this:
28
+
29
+ begin
30
+ @main_clucumber = ClucumberSubprocess.new(File.expand_path("../", File.dirname(__FILE__)),
31
+ :port => 42428)
32
+ at_exit do
33
+ @main_clucumber.kill
34
+ end
35
+
36
+ @main_clucumber.start <<-LISP
37
+ ;; Put code here that loads your application.
38
+ LISP
39
+ rescue PTY::ChildExited
40
+ puts(@main_clucumber && @main_clucumber.output)
41
+ end
42
+
43
+ This will launch a lisp with clucumber loaded (pass :lisp parameter to `ClucumberSubprocess.new` to specify which lisp, it defaults to sbcl), and start listening on port 42428.
44
+
45
+ Then, on the command line, you run cucumber:
46
+
47
+ $ cucumber
48
+
49
+ And you watch the lines zip by.
50
+
51
+ That should be all (-:
52
+
53
+ Over the next few days, I hope to fill out the test suite with more
54
+ interesting examples that you can use as a reference.
@@ -0,0 +1,5 @@
1
+ (asdf:defsystem clucumber
2
+ :depends-on (:cl-interpol :cl-ppcre :trivial-backtrace :usocket :st-json)
3
+ :serial t
4
+ :components ((:file "packages")
5
+ (:file "server")))
@@ -0,0 +1,17 @@
1
+ (cl:defpackage #:clucumber-external
2
+ (:export #:start))
3
+
4
+ (cl:defpackage #:clucumber-steps
5
+ (:export #:define-test-package #:*test-package*
6
+ #:Given* #:When* #:Then*
7
+ #:Before #:After
8
+ #:pending #:fail
9
+ #:var)
10
+ (:use #:cl #:cl-interpol))
11
+
12
+ (cl:defpackage #:clucumber-user
13
+ (:export)
14
+ (:use #:cl #:clucumber-steps))
15
+
16
+ (cl:defpackage #:clucumber
17
+ (:use #:cl #:clucumber-steps #:clucumber-external #:usocket #:st-json))
@@ -0,0 +1,242 @@
1
+ (cl:in-package #:clucumber)
2
+
3
+ (defvar clucumber-steps:*test-package* (find-package :clucumber-user))
4
+
5
+ (defvar *print-backtraces* t)
6
+
7
+ (defvar *base-pathname*)
8
+
9
+ (defparameter *default-step-regex-delimiter* #\{)
10
+
11
+ (defparameter *default-step-regex-close-delimiter* #\})
12
+
13
+ (defun load-definitions (base-pathname)
14
+ (let ((support-files (directory (merge-pathnames (make-pathname :directory '(:relative "support"
15
+ :wild-inferiors)
16
+ :name :wild
17
+ :type "lisp")
18
+ base-pathname)))
19
+ (step-files (directory (merge-pathnames (make-pathname :directory '(:relative "step_definitions"
20
+ :wild-inferiors)
21
+ :name :wild
22
+ :type "lisp")
23
+ base-pathname))))
24
+ (dolist (files (list support-files step-files))
25
+ (let ((*readtable* (copy-readtable))
26
+ (*package* *test-package*)
27
+ (cl-interpol::*regex-delimiters* (cons *default-step-regex-delimiter*
28
+ cl-interpol::*regex-delimiters*)))
29
+ (cl-interpol:enable-interpol-syntax)
30
+ (mapc #'load (sort files #'string<
31
+ :key (lambda (path)
32
+ (enough-namestring path base-pathname))))))))
33
+
34
+ (defun serve-cucumber-requests (socket &aux (stream (socket-stream socket)))
35
+ (handler-case
36
+ (loop
37
+ (let* ((line (read-line stream))
38
+ (message (read-json line nil))
39
+ (reply (call-wire-protocol-method message)))
40
+ (st-json:write-json reply stream)
41
+ (terpri stream)
42
+ (finish-output stream)))
43
+ (end-of-file nil nil)))
44
+
45
+
46
+ ;;; Step definitions
47
+
48
+
49
+ (defparameter *steps* (make-array 0 :adjustable t :fill-pointer t))
50
+
51
+ (defclass step-definition ()
52
+ ((regex :initarg :regex :accessor regex)
53
+ (cont :initarg :continuation :accessor continuation)
54
+ (scanner :accessor scanner)
55
+ (definition-file :initform *load-truename* :accessor definition-file)))
56
+
57
+ (defmethod initialize-instance :after ((o step-definition) &key regex &allow-other-keys)
58
+ (setf (scanner o) (cl-ppcre:create-scanner regex)))
59
+
60
+ (defun add-step (regex function)
61
+ (let ((existing-step (find regex *steps* :key #'regex :test #'string=)))
62
+ (if existing-step
63
+ (setf (continuation existing-step) function
64
+ (definition-file existing-step) *load-truename*)
65
+ (vector-push-extend
66
+ (make-instance 'step-definition :regex regex :continuation function)
67
+ *steps*)))
68
+ *steps*)
69
+
70
+ (defmacro clucumber-steps:Given* (regex args &body body)
71
+ `(eval-when (:compile-toplevel :load-toplevel :execute)
72
+ (add-step ,regex (lambda (,@args) ,@body))))
73
+
74
+ (defmacro clucumber-steps:Then* (regex args &body body)
75
+ `(eval-when (:compile-toplevel :load-toplevel :execute)
76
+ (add-step ,regex (lambda (,@args) ,@body))))
77
+
78
+ (defmacro clucumber-steps:When* (regex args &body body)
79
+ `(eval-when (:compile-toplevel :load-toplevel :execute)
80
+ (add-step ,regex (lambda (,@args) ,@body))))
81
+
82
+ ;;; Packages
83
+
84
+ (defun clucumber-external:start (*base-pathname* host port &key quit)
85
+ (setf (fill-pointer *steps*) 0)
86
+ (load-definitions *base-pathname*)
87
+ (let ((server (usocket:socket-listen host port :reuse-address t)))
88
+ (unwind-protect
89
+ (loop
90
+ (let ((socket (usocket:socket-accept server :element-type 'character)))
91
+ (serve-cucumber-requests socket))
92
+ (when quit (return)))
93
+ (usocket:socket-close server))))
94
+
95
+ (defmacro clucumber-steps:define-test-package (name &rest defpackage-arguments)
96
+ `(eval-when (:compile-toplevel :load-toplevel :execute)
97
+ (defpackage ,name
98
+ (:use #:clucumber)
99
+ ,@defpackage-arguments)
100
+ (setf *test-package* (find-package ',name))))
101
+
102
+ ;;; Before / after hooks:
103
+
104
+ ;; I'm not sure if these can handle tags. It would certainly be nice if they could.
105
+
106
+ (defvar *before-hooks* (make-array 0 :adjustable t :fill-pointer t))
107
+ (defvar *after-hooks* (make-array 0 :adjustable t :fill-pointer t))
108
+
109
+ (defmacro clucumber-steps:Before (&body body)
110
+ `(vector-push-extend (lambda () ,@body) *before-hooks*))
111
+
112
+ (defmacro clucumber-steps:After (&body body)
113
+ `(vector-push-extend (lambda () ,@body) *before-hooks*))
114
+
115
+ ;;; Wire protocol
116
+
117
+ (defvar *wire-protocol-methods* (make-hash-table :test #'equal))
118
+
119
+ (defmacro define-wire-protocol-method (name args &body body)
120
+ (let ((params (gensym)))
121
+ `(setf (gethash ,name *wire-protocol-methods*)
122
+ (lambda (,params)
123
+ (declare (ignorable ,params))
124
+ (catch 'wire-protocol-method
125
+ (let (,@(mapcar (lambda (arg-spec)
126
+ (destructuring-bind (arg-name &optional
127
+ (jso-name (string-downcase arg-name)))
128
+ (if (listp arg-spec) arg-spec (list arg-spec))
129
+ `(,arg-name (getjso ,jso-name ,params))))
130
+ args))
131
+ ,@body))))))
132
+
133
+ (defun call-wire-protocol-method (wire-protocol-message)
134
+ (let ((method (gethash (first wire-protocol-message) *wire-protocol-methods*)))
135
+ (if method
136
+ (funcall method
137
+ (second wire-protocol-message))
138
+ (list "fail"))))
139
+
140
+ (defun clucumber-steps:fail (message &key format-args exception backtrace)
141
+ (throw 'wire-protocol-method
142
+ (list "fail"
143
+ (apply #'jso
144
+ `("message" ,(apply #'format nil message format-args)
145
+ ,@(when exception
146
+ `("exception" ,exception))
147
+ ,@(when backtrace
148
+ `("backtrace" ,backtrace)))))))
149
+
150
+ (defun clucumber-steps:pending (&optional message)
151
+ (throw 'wire-protocol-method
152
+ `("pending" ,@(when message
153
+ (list message)))))
154
+
155
+ (defun backtrace-for (condition)
156
+ (if *print-backtraces*
157
+ (trivial-backtrace:print-backtrace condition :output nil)
158
+ ""))
159
+
160
+ (defmacro with-error-handling (&body body)
161
+ `(let ((*debugger-hook* (lambda (condition prev-hook)
162
+ (declare (ignore prev-hook))
163
+ (fail "Non-error condition invoked the debugger"
164
+ :exception (princ-to-string condition)
165
+ :backtrace (backtrace-for condition)))))
166
+ (handler-case
167
+ (progn ,@body)
168
+ (error (condition)
169
+ (fail "Caught an error"
170
+ :exception (princ-to-string condition)
171
+ :backtrace (backtrace-for condition))))))
172
+
173
+ (define-wire-protocol-method "begin_scenario" ()
174
+ (with-error-handling
175
+ (map nil 'funcall *before-hooks*)
176
+ (list "success")))
177
+
178
+ (define-wire-protocol-method "end_scenario" ()
179
+ (with-error-handling
180
+ (map nil 'funcall *after-hooks*)
181
+ (reset-state)
182
+ (list "success")))
183
+
184
+ (define-wire-protocol-method "step_matches" ((name-to-match "name_to_match"))
185
+ (list "success"
186
+ (loop for posn from 0
187
+ for step across *steps*
188
+ for scanner = (scanner step)
189
+ for (matchp end starts ends) = (multiple-value-list (cl-ppcre:scan scanner name-to-match))
190
+ for arguments = (map 'list (lambda (start end)
191
+ (jso "val" (subseq name-to-match start end)
192
+ "pos" start))
193
+ starts ends)
194
+ if matchp collect (jso "id" posn "args" arguments
195
+ "regexp" (regex step)
196
+ "source" (enough-namestring (definition-file step)
197
+ *base-pathname*)))))
198
+
199
+ (define-wire-protocol-method "invoke" (id args)
200
+ (let ((step (elt *steps* id)))
201
+ (if step
202
+ (with-error-handling
203
+ (apply (continuation step) args)
204
+ (list "success"))
205
+ (fail "Step ~S is undefined" :format-args `(,id)))))
206
+
207
+
208
+ (defun make-dwim-step-regex (step)
209
+ (let* ((escaped (cl-ppcre:regex-replace-all "[][}{)(]" step "\\\\\\&"))
210
+ (count 0))
211
+ (values (or (cl-ppcre:regex-replace-all "\"[^\"]+\"" escaped
212
+ (lambda (&rest _)
213
+ (declare (ignore _))
214
+ (incf count)
215
+ "\"([^\"]*)\""))
216
+ escaped)
217
+ count)))
218
+
219
+ (define-wire-protocol-method "snippet_text" ((keyword "step_keyword") (step-name "step_name"))
220
+ ;; TODO: figure out multiline_arg_class
221
+ (list "success"
222
+ (multiple-value-bind (step-re group-count) (make-dwim-step-regex step-name)
223
+ (let ((group-vars (loop for i from 0 below group-count
224
+ collect (format nil "group-~D" i))))
225
+ (format nil "(~A* #?~C^~A$~C (~{~A~^ ~})~% ~
226
+ ;; express the regexp above with the code you wish you had~% ~
227
+ (pending))" (string-trim '(#\Space) keyword) *default-step-regex-delimiter*
228
+ step-re *default-step-regex-close-delimiter*
229
+ group-vars)))))
230
+
231
+ ;;; Sharing state between steps:
232
+
233
+ (defvar *variables* (make-hash-table :test #'eql))
234
+
235
+ (defun clucumber-steps:var (name &optional default)
236
+ (gethash name *variables* default))
237
+
238
+ (defun (setf clucumber-steps:var) (new-val name &optional default)
239
+ (setf (gethash name *variables* default) new-val))
240
+
241
+ (defun reset-state ()
242
+ (clrhash *variables*))
data/lib/clucumber.rb ADDED
@@ -0,0 +1,71 @@
1
+ require 'pty'
2
+ require 'fileutils'
3
+
4
+ class ClucumberSubprocess
5
+ class LaunchFailed < RuntimeError; end
6
+
7
+ attr_reader :output
8
+
9
+ def initialize(dir, options={})
10
+ @dir = dir
11
+ lisp = options[:lisp] || ENV['LISP'] || 'sbcl --disable-debugger'
12
+ @port = options[:port] || raise("Need a port to run clucumber on.")
13
+ @output = ""
14
+
15
+ Dir.chdir(@dir) do
16
+ @out, @in, @pid = PTY.spawn(lisp)
17
+ end
18
+ @reader = Thread.start {
19
+ record_output
20
+ }
21
+ @in.puts(<<-LISP)
22
+ (require :asdf)
23
+ (load #p"#{File.expand_path("clucumber/clucumber.asd", File.dirname(__FILE__))}")
24
+ LISP
25
+ end
26
+
27
+ def start(additional_forms="")
28
+ @in.puts <<-LISP
29
+ #{additional_forms}
30
+ (asdf:oos 'asdf:load-op :clucumber)
31
+ (clucumber-external:start #p"./" "localhost" #{@port})
32
+ LISP
33
+ until socket = TCPSocket.new("localhost", @port) rescue nil
34
+ raise LaunchFailed, "Couldn't start clucumber:\n#{@output}" unless alive?
35
+ sleep 0.01
36
+ end
37
+ File.open(File.join(@dir, "step_definitions", "clucumber.wire"), "w") do |out|
38
+ YAML.dump({'host' => "localhost", 'port' => @port}, out)
39
+ end
40
+ socket.close
41
+ end
42
+
43
+ def record_output
44
+ begin
45
+ while line = @out.readline
46
+ @output << line
47
+ end
48
+ rescue PTY::ChildExited
49
+ STDOUT.puts "child exited, stopping."
50
+ nil
51
+ end
52
+ end
53
+
54
+ def kill
55
+ if @pid
56
+ FileUtils.rm_f File.join(@dir, "step_definitions", "clucumber.wire")
57
+ @reader.terminate!
58
+ Process.kill("TERM", @pid)
59
+ Process.waitpid(@pid)
60
+ @pid = nil
61
+ end
62
+ rescue PTY::ChildExited
63
+ @pid = nil
64
+ end
65
+
66
+ def alive?
67
+ if !@pid.nil?
68
+ (Process.kill("CONT", @pid) && true) rescue false
69
+ end
70
+ end
71
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'clucumber'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestClucumber < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clucumber
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Andreas Fuchs
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-03 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: aruba
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: cucumber
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :development
43
+ version_requirements: *id002
44
+ description: |-
45
+ A cucumber extension that lets you write your step definitions in Common Lisp.
46
+ Set internal state in your Hunchentoot web app or your library, and use the full power of Cucumber and its other extensions.
47
+ email: asf@boinkor.net
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - LICENSE
54
+ - README.rdoc
55
+ files:
56
+ - lib/clucumber.rb
57
+ - lib/clucumber/clucumber.asd
58
+ - lib/clucumber/packages.lisp
59
+ - lib/clucumber/server.lisp
60
+ - LICENSE
61
+ - README.rdoc
62
+ has_rdoc: true
63
+ homepage: http://github.com/antifuchs/clucumber
64
+ licenses: []
65
+
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --charset=UTF-8
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ requirements: []
86
+
87
+ rubyforge_project:
88
+ rubygems_version: 1.3.6
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Test drive your Common Lisp application from Cucumber
92
+ test_files:
93
+ - test/helper.rb
94
+ - test/test_clucumber.rb