#lang racket (require rackunit "spec-ast.rkt" "parser.rkt") (require/expose "parser.rkt" (parse-name parse-command parse-posn-arg-spec parse-help-text parse-once-specs parse-multi-specs parse-flag-specs parse-commands assert-switch-valid! assert-switches-unique! assert-keywords-unique!)) (provide parser-tests) (define parse-name-tests (test-suite "parse-name" (test-case "string input" (check-equal? (parse-name "foo" "bar") "bar")) (test-case "symbol input" (check-equal? (parse-name "foo" 'bar) "bar")) (test-case "bogus input" (check-exn exn:functional-command:parse? (lambda () (parse-name "foo" 3)))))) (define assert-switch-valid-tests (test-suite "assert-switch-valid!" (test-case "valid single-char switch" (check-not-false (assert-switch-valid! "-x"))) (test-case "valid long switch" (check-not-false (assert-switch-valid! "--long-form"))) (test-case "bogus long switch" (check-exn exn:functional-command:parse? (lambda () (assert-switch-valid! "--xyz!abc")))) (test-case "bogus short switch" (check-exn exn:functional-command:parse? (lambda () (assert-switch-valid! "-!")))) (test-case "short switch too long" (check-exn exn:functional-command:parse? (lambda () (assert-switch-valid! "-long-form")))) (test-case "reserved switch name" (check-exn exn:functional-command:parse? (lambda () (assert-switch-valid! "--help")))))) (define parse-posn-arg-spec-tests (test-suite "parse-posn-arg-spec" (test-case "empty arg list" (check-equal? (values->list (parse-posn-arg-spec '())) '(() #f))) (test-case "singleton argument" (check-equal? (values->list (parse-posn-arg-spec '(x))) '((x) #f))) (test-case "multiple arguments" (check-equal? (values->list (parse-posn-arg-spec '(x y))) '((x y) #f))) (test-case "rest arg" (check-equal? (values->list (parse-posn-arg-spec '(x y . rest))) '((x y) rest))) (test-case "rest arg w/o required args" (check-equal? (values->list (parse-posn-arg-spec 'rest)) '(() rest))) (test-case "bogus input: bad list structure" (check-exn exn:functional-command:parse? (lambda () (parse-posn-arg-spec '(x (y) z . a))))) (test-case "bogus input: non symbol" (check-exn exn:functional-command:parse? (lambda () (parse-posn-arg-spec '(x 3 y))))))) (define parse-help-tests (test-suite "parse-help-text" (test-case "single line" (check-equal? (parse-help-text "foo") '("foo"))) (test-case "multiple lines" (check-equal? (parse-help-text '("foo" "bar")) '("foo" "bar"))) (test-case "bogus list element" (check-exn exn:functional-command:parse? (lambda () (parse-help-text '("foo" 3))))) (test-case "bogus value" (check-exn exn:functional-command:parse? (lambda () (parse-help-text 3)))))) (define parse-once-specs-tests (test-suite "parse-once-specs" (test-case "single spec, no args" (check-equal? (parse-once-specs '((("-l" "--long-switch") #:long "simple help"))) (list (optional-flag (set "-l" "--long-switch") '#:long '("simple help") null)))) (test-case "single spec, arguments" (check-equal? (parse-once-specs '((("-l" "--long-switch") #:long ("help text" "with multiple lines") foo bar))) (list (optional-flag (set "-l" "--long-switch") '#:long '("help text" "with multiple lines") '(foo bar))))) (test-case "multiple specs" (check-equal? (parse-once-specs '((("-a" "--switch-a") #:a "foo" foo bar) (("-b" "--switch-b") #:b "bar" foo bar))) (list (optional-flag (set "-a" "--switch-a") '#:a '("foo") '(foo bar)) (optional-flag (set "-b" "--switch-b") '#:b '("bar") '(foo bar))))))) (define parse-multi-specs-tests (test-suite "parse-multi-specs" (test-case "single spec, no args" (check-exn exn:functional-command:parse? (lambda () (parse-multi-specs '((("-l" "--long-switch") #:long "simple help")))))) (test-case "single spec, arguments" (check-equal? (parse-multi-specs '((("-l" "--long-switch") #:long ("help text" "with multiple lines") foo bar))) (list (multi-flag (set "-l" "--long-switch") '#:long '("help text" "with multiple lines") '(foo bar))))) (test-case "multiple specs" (check-equal? (parse-multi-specs '((("-a" "--switch-a") #:a "foo" foo bar) (("-b" "--switch-b") #:b "bar" foo bar))) (list (multi-flag (set "-a" "--switch-a") '#:a '("foo") '(foo bar)) (multi-flag (set "-b" "--switch-b") '#:b '("bar") '(foo bar))))))) (define parse-flag-specs-tests (test-suite "parse-flag-specs" (test-case "empty" (check-equal? (parse-flag-specs null) null)) (test-case "two optional flags" (check-equal? (parse-flag-specs '(#:once-each (("-a") #:a "foo" a) (("-b") #:b "bar" b))) (list (optional-flag (set "-a") '#:a '("foo") '(a)) (optional-flag (set "-b") '#:b '("bar") '(b))))) (test-case "two optional flags in different sections" (check-equal? (parse-flag-specs '(#:once-each (("-a") #:a "foo" a) #:once-each (("-b") #:b "bar" b))) (list (optional-flag (set "-a") '#:a '("foo") '(a)) (optional-flag (set "-b") '#:b '("bar") '(b))))) (test-case "optional & multi" (check-equal? (parse-flag-specs '(#:multi (("-a") #:a "foo" a) #:once-each (("-b") #:b "bar" b))) (list (multi-flag (set "-a") '#:a '("foo") '(a)) (optional-flag (set "-b") '#:b '("bar") '(b))))) (test-case "optional, multi, and group" (check-equal? (parse-flag-specs '(#:multi (("-a") #:a "foo" a) #:once-any (("-c") #:c "baz" c) (("-d") #:d "quux" d) #:once-each (("-b") #:b "bar" b))) (list (multi-flag (set "-a") '#:a '("foo") '(a)) (group (list (optional-flag (set "-c") '#:c '("baz") '(c)) (optional-flag (set "-d") '#:d '("quux") '(d)))) (optional-flag (set "-b") '#:b '("bar") '(b))))) (test-case "two groups" (check-equal? (parse-flag-specs '(#:once-any (("-c") #:c "baz" c) (("-d") #:d "quux" d1 d2) #:once-any (("-a") #:a "foo" a) (("-b") #:b ("bar" "bar2") b))) (list (group (list (optional-flag (set "-c") '#:c '("baz") '(c)) (optional-flag (set "-d") '#:d '("quux") '(d1 d2)))) (group (list (optional-flag (set "-a") '#:a '("foo") '(a)) (optional-flag (set "-b") '#:b '("bar" "bar2") '(b))))))))) (define assert-switches-unique-tests (test-suite "assert-switches-unique!" (test-case "no dups" (check-not-false (assert-switches-unique! "foo" (list (multi-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-1") '#:a '("a") null) (group (list (optional-flag (set "-c" "--long-3") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null))))))) (test-case "dups across groups" (check-exn exn:functional-command:parse? (lambda () (assert-switches-unique! "foo" (list (group (list (optional-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-1") '#:a '("a") null))) (group (list (optional-flag (set "-c" "--long-2") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null)))))))) (test-case "dup in group & optional" (check-exn exn:functional-command:parse? (lambda () (assert-switches-unique! "foo" (list (multi-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-4") '#:a '("a") null) (group (list (optional-flag (set "-c" "--long-3") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null)))))))) (test-case "dup outside group" (check-exn exn:functional-command:parse? (lambda () (assert-switches-unique! "foo" (list (multi-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-2") '#:a '("a") null) (group (list (optional-flag (set "-c" "--long-3") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null)))))))))) (define assert-keywords-unique-tests (test-suite "assert-keywords-unique!" (test-case "no dups" (check-not-false (assert-keywords-unique! "foo" (list (multi-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-1") '#:a '("a") null) (group (list (optional-flag (set "-c" "--long-3") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null))))))) (test-case "dups across groups" (check-exn exn:functional-command:parse? (lambda () (assert-keywords-unique! "foo" (list (group (list (optional-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-1") '#:a '("a") null))) (group (list (optional-flag (set "-c" "--long-3") '#:a '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null)))))))) (test-case "dups in group & optional" (check-exn exn:functional-command:parse? (lambda () (assert-keywords-unique! "foo" (list (multi-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-4") '#:c '("a") null) (group (list (optional-flag (set "-c" "--long-3") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null)))))))) (test-case "dup outside group" (check-exn exn:functional-command:parse? (lambda () (assert-keywords-unique! "foo" (list (multi-flag (set "-b" "--long-2") '#:b '("b") '(b)) (optional-flag (set "-a" "--long-4") '#:b '("a") null) (group (list (optional-flag (set "-c" "--long-3") '#:c '("c") null) (optional-flag (set "-d" "--long-4") '#:d '("d") null)))))))))) (define parse-command-tests (test-suite "parse-command" (test-case "basic" (let ([cmd-name "show"] [short-help "Shows detailed info about a task"] [long-help "Displays full details about the task with the identifier <id>."] [f (lambda args args)]) (check-equal? (parse-command `(,cmd-name ,short-help ,long-help (id) ,f)) (command cmd-name short-help long-help '(id) #f null f)))) (test-case "rest args" (let ([f (lambda args args)]) (check-equal? (parse-command `("add" "foo" "bar" summary #:once-each (("-s" "--start-date") #:start-date "x" date) (("-d" "--due-date") #:due-date "y" date) #:once-any (("-n" "--note") #:note "n" filename) (("-e" "--edit-note") #:edit-note "e") #:once-each (("-u" "--url") #:url "u" url) ,f)) (command "add" "foo" "bar" null 'summary (list (optional-flag (set "-s" "--start-date") '#:start-date '("x") '(date)) (optional-flag (set "-d" "--due-date") '#:due-date '("y") '(date)) (group (list (optional-flag (set "-n" "--note") '#:note '("n") '(filename)) (optional-flag (set "-e" "--edit-note") '#:edit-note '("e") null))) (optional-flag (set "-u" "--url") '#:url '("u") '(url))) f)))) (test-case "duplicate switch" (check-exn exn:functional-command:parse? (lambda () (parse-command `("add" "foo" "bar" summary #:once-each (("-s" "--start-date") #:start-date "x" date) (("-d" "--note") #:due-date "y" date) #:once-any (("-n" "--note") #:note "n" filename) (("-e" "--edit-note") #:edit-note "e") #:once-each (("-u" "--url") #:url "u" url) ,(lambda (x) x)))))) (test-case "duplicate keyword" (check-exn exn:functional-command:parse? (lambda () (parse-command `("add" "foo" "bar" summary #:once-each (("-s" "--start-date") #:start-date "x" date) (("-d" "--due-date") #:due-date "y" date) #:once-any (("-n" "--note") #:note "n" filename) (("-e" "--edit-note") #:edit-note "e") #:once-each (("-u" "--url") #:note "u" url) ,(lambda (x) x)))))) (test-case "help not string" (check-exn exn:functional-command:parse? (lambda () (parse-command `(add foo "bar" ,(lambda (x) x)))))) (test-case "missing function" (check-exn exn:functional-command:parse? (lambda () (parse-command '(add "foo" "bar"))))) (test-case "crud after function" (check-exn exn:functional-command:parse? (lambda () (parse-command `(add "foo" "bar" ,(lambda (x) x) positional-arg))))) (test-case "reserved command name" (check-exn exn:functional-command:parse? (lambda () (parse-command `(help "foo" "bar" ,(lambda (x) x)))))) (test-case "reserved switch name" (check-exn exn:functional-command:parse? (lambda () (parse-command `(foo "bar" "baz" #:once-each (("-h" "--help" #:help "x")) ,(lambda (x) x)))))))) (define parse-commands-tests (test-suite "Tests for parse-commands" (test-case "empty" (check-exn exn:functional-command:parse? (lambda () (parse-commands null)))) (test-case "no dups" (let ([f (lambda args args)]) (check-equal? (parse-commands `((show "show" "show long" (id) ,f) (complete "complete" "complete long" (id) ,f))) (list (command "show" "show" "show long" '(id) #f null f) (command "complete" "complete" "complete long" '(id) #f null f))))) (test-case "dup cmd name" (check-exn exn:functional-command:parse? (lambda () (parse-commands `((show "show" "show long" (id) ,(lambda (x) x)) (show "complete" "complete long" (id) ,(lambda (x) x))))))))) (define parse-spec-tests (let ([f (lambda args args)]) (test-suite "parse-spec" (test-equal? "simple" (parse-spec `("test" "spec for testing" (show "show" "show long" (id) ,f) (complete "complete" "complete long" (id) ,f))) (spec "test" "spec for testing" (list (command "show" "show" "show long" '(id) #f null f) (command "complete" "complete" "complete long" '(id) #f null f)))) (test-exn "bad help text" exn:functional-command:parse? (lambda () (parse-spec `("test" invalid-help (show "show" "show long" (id) ,f))))) (test-exn "bad syntax" exn:functional-command:parse? (lambda () (parse-spec '(x y))))))) (define spec-parser-tests (test-suite "Tests for the command line specification parser" parse-name-tests assert-switch-valid-tests parse-posn-arg-spec-tests parse-help-tests parse-once-specs-tests parse-multi-specs-tests parse-flag-specs-tests assert-switches-unique-tests assert-keywords-unique-tests parse-command-tests parse-commands-tests parse-spec-tests)) (define parser-tests (test-suite "Tests for parser.rkt" spec-parser-tests)) (define-syntax values->list (lambda (stx) (syntax-case stx () [(values->list e) #'(call-with-values (lambda () e) list)])))