chickumber / chickumber-server.scm

;; 
;; %%HEADER%%
;;

(module chickumber-server
(+default-port+
 start-wire-server
 load-step-files!
 find-wire-procedure
 add-wire-procedure!
 wire-procedures
 define-wire-procedure
 find-and-apply-wire-procedure
 handle-wire-request
 wrap-wire-procedure
 current-exit-continuation
 fail succeed pending suggest-step-snippet-for
 current-step-evaluator
 boolean-step-evaluator
 test-step-evaluator
 missbehave-step-evaluator
 current-step-evaluator-dependencies
 )

(import chicken scheme)
(require-library tcp json files srfi-13 chickumber test posix defstruct missbehave)



(import
  chickumber
  test
  defstruct
  (except missbehave pending $)
  (only posix file-modification-time glob current-directory directory? set-signal-handler! signal/int signal/hup signal/term signal/quit)
  (only extras read-line sprintf read-file printf)
  (only data-structures alist-ref conc)
  (only ports with-input-from-string with-output-to-string)
  (only srfi-1 fold filter any)
  (only srfi-13 string=? string-join)
  (only tcp tcp-listen tcp-accept tcp-close)
  (only regex regexp regexp-escape string-substitute string-match-positions string-search)
  (only irregex string-match)
  (only files pathname-strip-directory normalize-pathname absolute-pathname?)
  (only json json-write json-read))

(define wire-procedures (list))
(define +default-port+ 61616)
(define *current-step-files* (list))

(define (handle-wire-request input-port output-port #!key (debug #f))
  (let loop ((line (read-line input-port)))
    (unless (eof-object? line)
     
      (reload-step-files-if-needed)
      
      (let* ((request   (with-input-from-string line json-read))
             (message   (car request))
             (arguments (if (> (length request) 1) (cdr request) '()))
             (response  (find-and-apply-wire-procedure message arguments)))
        (when debug
          (printf "Read: ~S ~%" line)
          (printf "Write: ~S =>  ~S~%" response (with-output-to-string (lambda () (json-write (or response (make-message-not-understood-response message arguments)))))))
        (display ".")
        (flush-output (current-output-port))
        (json-write (or response (make-message-not-understood-response message arguments)) output-port)
        (newline output-port)
        (flush-output output-port)
        (loop (read-line input-port))))))

(define (start-wire-server additional-files #!key (framework 'boolean) (port +default-port+) (debug #f) (on-shutdown values) (on-sig-hup values))
  (with-test-framework
   framework
   (lambda ()
     (load-step-files! (discover-step-files additional-files))
     (set-signal-handler! signal/hup (lambda (sig)
                                       (on-sig-hup)
                                       (load-step-files! (discover-step-files additional-files) reload: #t)))
     
     (let* ((listener (tcp-listen port))
            (shutdown (lambda (sig)
                        (tcp-close listener)
                        (on-shutdown))))
       
       (for-each (cut set-signal-handler! <> shutdown)
                 (list signal/int signal/term signal/quit))
       
       (let loop ()
         (receive (input-port output-port) (tcp-accept listener)           
           (handle-wire-request input-port output-port debug: debug)
           (close-input-port input-port)
           (close-output-port output-port))
         (loop))))))

(define (find-wire-procedure message)
  (alist-ref message wire-procedures string=?))

(define (add-wire-procedure! message procedure)
  (set! wire-procedures (cons (cons message procedure) wire-procedures)))

(define-syntax define-wire-procedure
  (syntax-rules ()
    ((_ message (argument ...) code more-code ...)
     (add-wire-procedure! message (wrap-wire-procedure (list (symbol->string (quote argument)) ...)
                                                       (lambda (argument ...) code more-code ...))))))

(define ((wrap-wire-procedure wanted-argument-names procedure) #!optional (arguments #f))
  (if arguments
      (let ((wanted-arguments (extract-wanted-arguments wanted-argument-names arguments)))
        (apply procedure wanted-arguments))
      (procedure)))

(define (extract-wanted-arguments argument-names arguments)
  (let ((arguments (vector->list arguments)))
    (map (lambda (argument-name)
           (alist-ref argument-name arguments string=?))
         argument-names)))

(define (find-and-apply-wire-procedure message arguments)
  (let ((procedure (find-wire-procedure message)))
    (if (procedure? procedure)
        (apply procedure arguments)
        #f)))

(define (make-message-not-understood-response message args)
  (fail (sprintf "Unknown wire-message: ~A with arguments ~A" message args)))

(define (succeed #!optional (arguments #f))
  (if arguments
      (list "success" arguments)
      (list "success")))

(define (fail message #!optional (backtrace #f) (exn #f))
  (cond
   ((and backtrace exn)
    `("fail" #(("message" . ,message) ("backtrace" . ,backtrace) ("exception" . ,exn))))
   (backtrace
    `("fail" #(("message" . ,message) ("backtrace" . ,backtrace))))
   (exn
    `("fail" #(("message" . ,message) ("exception" . ,exn))))
   (else
    `("fail" #(("message" . ,message))))))

(define-wire-procedure "step_matches" (name_to_match)
  (define (convert-submatches submatch-positions)
    (map (lambda (pair)
           `#(("val" . ,(substring name_to_match (car pair) (cadr pair)))
              ("pos" . ,(car pair))))
         submatch-positions))
  
  (define (step-selector step result)
    (let* ((step-rx (step-regex step))
           (id      (step-id step))
           (matches (string-match-positions step-rx name_to_match)))
      (if matches
          (let ((submatches (convert-submatches (cdr matches))))
            (if (null? submatches)
                (cons `#(("id" . ,(number->string id)) ("args") ("regexp" . ,(step-regex-string step)) ("source" . ,(step-source step))) result)
                (cons `#(("id" . ,(number->string id)) ("args" ,@submatches) ("regexp" . ,(step-regex-string step)) ("source" . ,(step-source step))) result)))
          result)))
  (succeed
   (fold step-selector '() *step-definitions*)))

(define-wire-procedure "snippet_text" (step_keyword step_name multiline_arg_class)
  (succeed (suggest-step-snippet-for step_keyword step_name)))

(define (suggest-step-snippet-for keyword step-name)
  (receive (new-rx capture-arguments) (translate-step-name step-name)
    (with-output-to-string
      (lambda ()
        (print "(" keyword " #/^" new-rx "$/ (" (string-join capture-arguments) ")")
        (print "  ;write the code you wish you had")
        (display "  (pending))")))))

(define (translate-step-name step-name)
  (let* ((rx (regexp "\"([^\"]*)\""))
         (escaped-step-name (regexp-escape step-name))
         (captures (string-search rx escaped-step-name)))
    (values
     (string-substitute rx "\"([^\"]+)\"" escaped-step-name)
     (if captures
         (let ((index 0))
           (map (lambda (_)
                  (set! index (+ index 1))
                  (sprintf "arg~A" index))
                (cdr captures)))
         '()))))

(define-wire-procedure "begin_scenario" ()
  (apply-hooks 'before)
  (succeed))

(define-wire-procedure "end_scenario" ()
  (apply-hooks 'after)
  (succeed))

(define-wire-procedure "invoke" (id args)
  (let ((step (find-step (string->number id))))
    (if step
        (run-step step args)
        (fail (sprintf "There is no step with id ~A" id)))))



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Stepevaluators
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; This is the simple default evaluator
;; it signals success if the code evaluates to #t and fails otherwise
(define (boolean-step-evaluator step args)
  (handle-exceptions exn
      (begin
        (fail ((condition-property-accessor 'exn 'message) exn)
              backtrace: ((condition-property-accessor 'exn 'location) exn)))
    (if (apply (step-code step) args)
        (succeed)
        (fail "Step failed"))))

(define (assq-ref ls key . o)
  (cond ((assq key ls) => cdr)
        ((pair? o) (car o))
        (else #f)))

(define (collect-test-error status expect expr info)
  (with-output-to-string
    (lambda ()
      (cond
       ((eq? status 'ERROR)
        (cond ((assq 'exception info)
               => (lambda (e)
                    (print-error-message (cdr e) (current-output-port))))))
       ((and (eq? status 'FAIL) (assq-ref info 'assertion))
        (display "assertion failed\n"))
       ((and (eq? status 'FAIL) (assq-ref info 'expect-error))
        (display "expected an error but got ")
        (write (assq-ref info 'result)) (newline))
       ((eq? status 'FAIL)
        (display "expected ") (write (assq-ref info 'expected))
        (display " but got ") (write (assq-ref info 'result)) (newline))))))

(define (test-step-evaluator step args)
  (let ((errors '()))
    (define (test-handler status expect expr info)
      (when (or (eq? status 'ERROR) (eq? status 'FAIL))
        (set! errors (cons (collect-test-error status expect expr info) errors))))

    (let ((original-test-applier (current-test-applier)))
      (parameterize ((current-test-handler test-handler)
                     (current-test-verbosity #f)
                     (current-test-group-reporter (lambda (_) #t))
                     (current-test-applier (lambda args
                                             (parameterize ((current-output-port (open-output-string)))
                                               (apply original-test-applier args)))))
        (handle-exceptions exn
            (begin
              (fail ((condition-property-accessor 'exn 'message) exn)
                    backtrace: ((condition-property-accessor 'exn 'location) exn)))
        
          (apply (step-code step) args)
          (if (null? errors)
              (succeed)
              (fail (string-join errors "\n"))))))))

(define (missbehave-step-evaluator step args)
  (let ((errors '()))
    (define (reporter data #!key (mode 'adhoc))
      (when (and (eq? mode 'adhoc)
                 (example-result? data)
                 (example-failed? data))
        (set! errors (cons (example-result-messages data) errors))))
    (handle-exceptions exn
        (begin
          (fail ((condition-property-accessor 'exn 'message) exn)
                backtrace: ((condition-property-accessor 'exn 'location) exn)))
      (run-specification
       (call-with-specification
        (make-empty-specification)
        (lambda ()
          (describe "Step"
             (it "runs"
                (apply (step-code step) args)))))
       reporter: reporter))
    (if (null? errors)
        (succeed)
        (fail (string-join errors "\n")))))

;; this parameter is the adapter to plug in
;; custom step-evaluators that are aware of the test-mechanism used.
(define current-step-evaluator    (make-parameter boolean-step-evaluator))
(define current-step-evaluator-dependencies (make-parameter '()))
(define current-step-evaluator-prolog (make-parameter '()))

(define (with-test-framework framework thunk)
  (case framework
    ((boolean)
     (parameterize ((current-step-evaluator boolean-step-evaluator))
       (thunk)))
    ((missbehave)
     (parameterize ((current-step-evaluator missbehave-step-evaluator)
                    (current-step-evaluator-prolog `((require-extension (except missbehave pending $)))))
       (thunk)))
    ((test)
     (parameterize ((current-step-evaluator test-step-evaluator)
                    (current-step-evaluator-dependencies '(test)))
       (thunk)))
    (else (error "Invalid test-framework given"))))

(define (run-step step args)
  (call-with-current-continuation
   (lambda (exit)
     (parameterize ((current-exit-continuation (lambda (message)
                                                 (if message
                                                     (exit (list 'pending message))
                                                     (exit (list 'pending))))))
       ((current-step-evaluator) step args)))))

;; load steps
(defstruct stepfile path last-modified)

(define (create-step-file path)
  (if (stepfile? path)
      path
      (make-stepfile path: path last-modified: (file-modification-time path))))

(define (default-step-files)
  (let loop ((defaults (map absolutize-path (list "features/support" "features/step_definitions")))
             (files '()))
    (cond
     ((null? defaults) (reverse files))
     ((directory? (car defaults))
       (loop (cdr defaults) (add-files-from-directory (car defaults) files)))
     (else (loop (cdr defaults) files)))))

(define (discover-step-files arguments)
  (let loop ((arguments arguments) (step-files (default-step-files)))
    (if (null? arguments) (reverse step-files)
        (let ((full-path (absolutize-path (car arguments))))
          (if (directory? full-path)
              (loop (cdr arguments) (add-files-from-directory full-path step-files))
              (loop (cdr arguments) (cons (create-step-file full-path) step-files)))))))

(define (add-files-from-directory directory step-files)
  (map create-step-file (fold cons step-files (glob (conc directory "/*.scm")))))

(define (absolutize-path path)
  (if (absolute-pathname? path)
      (normalize-pathname path)
      (normalize-pathname (conc (current-directory) "/" path))))

(define (load-step-files! files #!key (reload #f))
  (when reload
    (set! *step-definitions* '())
    (reset-step-id-generator))
  (let ((step-files (filter (lambda (file) (file-exists? (stepfile-path file))) files)))
    (set! *current-step-files*
      (map (lambda (file)
             (update-stepfile file last-modified: (file-modification-time (stepfile-path file))))
           step-files))
    (for-each eval-step-file step-files)))

(define (reload-step-files-if-needed)
  (when (any reload-needed? *current-step-files*)
    (load-step-files! *current-step-files* reload: #t)))

(define (reload-needed? file)
  (let ((path (stepfile-path file))
        (mtime (stepfile-last-modified file)))
    (and (file-exists? path)
         (> (file-modification-time path) mtime))))

(define (eval-step-file file)
  (let ((content (read-file (stepfile-path file))))
    (unless (null? content)
      (eval (decorate-content content (pathname-strip-directory (stepfile-path file)))))))

(define (decorate-content content file)
  (cond-expand
    (development
     `(begin
        (use regex ,@(current-step-evaluator-dependencies))
        ,@(current-step-evaluator-prolog)
        (load "chickumber.scm")
        (import chickumber)
        (parameterize ((current-steps-file ,file))
          ,@content)))
    (else
     `(begin
        (use regex ,@(current-step-evaluator-dependencies))
        ,@(current-step-evaluator-prolog)
        (require-extension chickumber)
        (parameterize ((current-steps-file ,file))
          ,@content))))))
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.