clucumber 0.1.0

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