;; The first three lines of this file were inserted by DrRacket. They record metadata ;; about the language level of this file in a form that our tools can easily process. #reader(lib "htdp-advanced-reader.ss" "lang")((modname H0-tests) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #t #t none #f () #f))) ; Note: this file is written in *advanced*-student ; (because the test-functions do sequences of I/O, rather than return values). ; *Your* program should be written in "intermediate student with lambda". (require "H0.rkt") (require "scanner.rkt") (require rackunit) (require "student-extras.rkt") ; solely for `for-each` ; Some expressions to test in a non-automated way: (check-equal? (parse! (create-scanner "34")) 34) (check-equal? (parse! (create-scanner "-34")) -34) ; test string->expr (check-equal? (string->expr "34") 34) (check-equal? (string->expr "shake 34 bake") (make-paren 34)) (check-equal? (string->expr " shake -34 bake ") (make-paren -34)) (check-equal? (string->expr "add 3 to 4") (make-binop "add" 3 4)) (check-equal? (string->expr "skim 3 off 4") (make-binop "skim" 3 4)) (check-equal? (string->expr "scale 3 to serve 4") (make-binop "scale" 3 4)) (check-equal? (string->expr "add shake 34 bake to scale 3 to serve 4") (make-binop "add" (make-paren 34) (make-binop "scale" 3 4))) (check-equal? (string->expr "sample 3; add 4 to taste or use 5 instead") (make-if-zero 3 4 5)) (check-equal? (string->expr "sample -2; add 4 to taste or use 5 instead") (make-if-zero -2 4 5)) (check-equal? (string->expr "sample skim 4 off 2; add add 1 to 2 to taste or use add 3 to 4 instead") (make-if-zero (make-binop "skim" 4 2) (make-binop "add" 1 2) (make-binop "add" 3 4))) (check-equal? (string->expr "sample 7; add add 3 to 4 to taste or use 5 instead") (make-if-zero 7 (make-binop "add" 3 4) 5)) ;;; test `expr->string` alongside `string->expr`: (check-equal? (expr->string (string->expr "34")) "34") (check-equal? (expr->string (string->expr "shake 34 bake")) "shake 34 bake") (check-equal? (expr->string (string->expr "shake 34 bake")) "shake 34 bake") (check-equal? (expr->string (string->expr "add 3 to 4")) "add 3 to 4") (check-equal? (expr->string (string->expr "skim 3 off 4")) "skim 3 off 4") (check-equal? (expr->string (string->expr "scale 3 to serve 4")) "scale 3 to serve 4") (check-equal? (expr->string (string->expr "add shake 34 bake to scale 3 to serve 4")) "add shake 34 bake to scale 3 to serve 4") (check-equal? (expr->string (string->expr "sample 3; add 4 to taste or use 5 instead")) "sample 3; add 4 to taste or use 5 instead") (check-equal? (expr->string (string->expr "sample -2; add 4 to taste or use 5 instead")) "sample -2; add 4 to taste or use 5 instead") (check-equal? (expr->string (string->expr "sample skim 4 off 2; add add 1 to 2 to taste or use add 3 to 4 instead")) "sample skim 4 off 2; add add 1 to 2 to taste or use add 3 to 4 instead") (check-equal? (expr->string (string->expr "sample 7; add add 3 to 4 to taste or use 5 instead")) "sample 7; add add 3 to 4 to taste or use 5 instead") ;;; test `eval` (check-equal? (eval 34) 34) (check-equal? (eval (string->expr "34")) 34) (check-equal? (eval (string->expr "shake 34 bake")) 34) (check-equal? (eval (string->expr "add 3 to 4")) 7) (check-equal? (eval (string->expr "skim 3 off 4")) 1) (check-equal? (eval (string->expr "skim 4 off 3")) -1) (check-equal? (eval (string->expr "scale 3 to serve 4")) 12) (check-equal? (eval (string->expr "add shake 34 bake to scale 3 to serve 4")) 46) (check-equal? (eval (string->expr "sample 3; add 4 to taste or use 5 instead")) 5) (check-equal? (eval (string->expr "sample -2; add 4 to taste or use 5 instead")) 5) (check-equal? (eval (string->expr "sample skim 4 off 2; add add 1 to 2 to taste or use add 3 to 4 instead")) 7) (check-equal? (eval (string->expr "sample 7; add add 3 to 4 to taste or use 5 instead")) 5) ;; You may add more specific tests here, ;; if you want things more specific that provided via adding to `all-tests` below. ;; Defining e0..e4, towards automating: (define e0 "43") (define e1 "shake 43 bake") (define e2 "add 4 to 3") (define e3 "shake shake add 4 to shake 3 bake bake bake") (define e4 "add shake 43 bake to scale 42 to serve 3") ;;; we can automate checking that string->expr is the (right)inverses of expr->string: (for-each (λ(e) (check-equal? (expr->string (string->expr e)) e)) (list e0 e1 e2 e3 e4)) ; `for-each` is like map except that it discards the result from each function-call; ; it is suitable for functions which are called solely for a side-effect. ; (`test-all` is such a function.) ;;; Though: we *also* want to check that e0..e4 eval to 43,43,7,7,169 respectively. (for-each (λ(e v) (check-equal? (eval (string->expr e)) v)) ; is the source-Expression; v for Value (list e0 e1 e2 e3 e4) (list 43 43 7 7 169)) ;;; The above is a promising start, to automating tests. ;;; Okay, we'll generalize the above to a more complete test-harness. ;;; One thing, is that we don't want to have two parallel-lists; ;;; instead keep a list of pairs. ;;; Three sorts of tests we want to make, for many different exprs:(check-equal? (string->expr "add 4 to 3") (make-binop "add" 4 3)) (check-equal? (eval (string->expr "add 4 to 3")) 7) (check-equal? (expr->string (string->expr "add 4 to 3")) "add 4 to 3") ;;;;;;;;;;;; begin test-harness ;;;;;;;;;;; ; Data Def'n: a `H-example` is a list of length two or length three: ; '[str val] (where val is the expected result `(eval (string->expr str))`, or ; '[str val expr] (as above, but `expr` is the internal (struct) representation of `(string->expr str)`). ; A list of H-examples; ; The last line of this file runs two-to-three tests on each H-example. ; ; BE AWARE of the comma preceding the constructors; it's necessary to actually call it. ; See explanation at http://www.radford.edu/~cs380/2025spring-ibarland/Lectures/backquote.html ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; run the tests ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (printf "Running H-test-harness~n") (define all-tests `{ ;>>> H0-tests ("7" 7) ("shake 3 bake" 3) ("add 3 to 4" 7) ("scale 3 to serve 4" 12) ("add add 3 to 4 to scale 3 to serve 4" 19) ("scale shake 3 bake to serve shake add 2 to 3 bake" 15) ("sample 0; add 1 to taste or use 2 instead" 1) ("sample 1; add 1 to taste or use 2 instead" 2) ("sample add 3 to -3; add 1 to taste or use 2 instead" 1) ("sample add sample sample 0; add 1 to taste or use 2 instead; add 3 to taste or use 4 instead to -3; add 1 to taste or use 2 instead" 2) #| ;>>> H1-tests ("chop 3 into 4" 3) ("chop add 5 to 6 into 3" 2) ("chop 8.1 into 3" 21/10) ("chop 8 into 3.1" 9/5) ("chop -8.1 into 3" 9/10) ("chop -8 into 3.1" 13/10) ("chop 8.1 into -3" -9/10) ("chop 8 into -3.1" -13/10) ("chop -8.1 into -3" -21/10) ("chop -8 into -3.1" -9/5) ("chop 8 into 2" 0) ("chop -8 into 2" 0) ("chop 8 into -2" 0) ("chop -8 into -2" 0) ("chop 8 into 3" 2) ("chop -8 into 3" 1) ("chop 8 into -3" -1) ("chop -8 into -3" -2) ;;;YOU should make H1-tests for `ifLT+` exprs |# ;>>> H2-tests ;;; YOU should make H2-tests for s. ;;; You'll also FIRST want to test parsing & expr->string of s. ;;; Alas, since `eval` of an ("unbound") should throw an error, ;;; you can't do that via this automated test harness and `all-tests`. ;;; So: hand-craft old-fashioned check-expects ;;; for parsing & expr->string of an . ;;; Test those BEFORE making tests of s. }) ; ; For info on backquote, see documentation and/or: ; http://www.radford.edu/itec380/2024fall-ibarland/Lectures/backquote.html ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;; DO NOT CHANGE the below ;;;;;;;;;;;;; (unless you really want to) ;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; helper functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; The following functions should really be in a separate file, and exported from there. ;; However, putting this in to a file 'H_i-test-harness.rkt' would become mildly problematic: ;; since it calls 'eval', 'string->expr' as provided in H0, it needs to require H0.rkt. ;; As we update our implementation H0.rkt to H2.rkt,H4.rkt etc, ;; we'd then need to update *this* file each time, changing *nothing* but the 'require'. Yuck. ;; An actual solution would be using "units", where when we call `require` we "pass in" the dependencies: ;; http://docs.racket-lang.org/reference/creatingunits.html?q=unit#%28form._%28%28lib._racket%2Funit..rkt%29._unit%29%29 ;; ;; But rather than add that level of indirection for a student-assignment, we'll just repeat ;; this code inside each H_i-test.rkt. (define/contract (my-check-equal? actual expected err-msg) (-> any? any? string? boolean?) ; If the two values aren't equal, print an error message. ; (Nothing prints on success; that is better done at a higher level, in `test-all`.) ; >>>Uses the global-variable `prog#`. (let {[result (equal? actual expected)]} (begin (when (not result) (printf "~ntest-program #~a failed:\n~a\n" prog# err-msg)) result))) ; test-internal-representation : H_i-example -> boolean ; Test that t parses to the correct internal tree-representation (if provided) ; (define (test-internal-representation t) (or (< (length t) 3) ; if no internal-rep provided, we pass the test! (with-handlers {[exn:fail? (print-exception t 'parse)]} (my-check-equal? (string->expr (first t)) (third t) (format "Parsing ~v\nresulted in ~v\ninstead of ~v\nas expected." (first t) (string->expr (first t)) (third t)))))) ; normalize-expected-value : H_i-example -> (or/c number? struct? string? procedure?) ; Just return the expected-value from the H_i-example -- the second field. ; BUT we also do some extra work to support "error results": if the expected-result was… ; - …a function expecting 1 arg, return it (presumably it's an exception handler). ; - …a function expecting 0 args, return it as an exception-handler-that-ignores-its-input; cf. `check-error. ; - …a symbol or string starting with the letters "error", return an exception-handler that just returns #t. ; (define (normalize-expected-eval t) (let* {[e (second t)]} (cond [(number? e) e] ; the expected result [(struct? e) e] ; the expected result (presumably some sort of func-expr) ; Now, look for "error handlers", when the (eval (first t)) is *desired* to fail -- ; E.g. an unbound identifier or divide-by-0. [(and (or (symbol? e) (string? e)) (regexp-match? #px"^(?i:error).*" (if (symbol? e) (symbol->string e) e))) (λ(_) #t)] ; an exception-handler that just swallows the exception and returns #t. [(and (procedure? e) (procedure-arity-includes? e 1)) e] ; an exception-handler [(and (procedure? e) (procedure-arity-includes? e 0)) (λ(_) (e))] ; convert a thunk to an exception-handler ; Finally, we go to great lengths in case ; the H_i-example was quoted-inside-quasiquote: `(... ("someProg" 'error) ...) [(and (cons? e) (equal? (first e) 'quote) (cons? (rest e)) (symbol? (second e)) (regexp-match? #px"^(?i:error).*" (symbol->string (second e)))) (λ(_) #t)] [else e] ; pass-through anything else; if `test-eval` is being called, ; presumably it *won't* match the expected-result, failing `test-eval`. ))) (define struct:2*3 (make-binop "scale" 2 3)) (check-equal? (normalize-expected-eval `("someProg" 17)) 17) (check-equal? (normalize-expected-eval `("someProg" 17 ,struct:2*3)) 17) (check-equal? (normalize-expected-eval `("someProg" ,struct:2*3)) struct:2*3) (check-equal? (normalize-expected-eval `("someProg" ,struct:2*3 ,struct:2*3)) struct:2*3) ; if the second-item is the string/symbol "error" (or just starts with that), ; then normalize-expected-eval returns an error-handler. Call it: (check-equal? ((normalize-expected-eval `("someProg" "error")) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" "error" ,struct:2*3)) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" "error-ho!")) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" "erRoR, eh?" ,struct:2*3)) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" error)) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" error ,struct:2*3)) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" error-ho)) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" erRoR-eh? ,struct:2*3)) "a-mock-exception") #t) ; (The following are *double* quoting the symbol, but our code tries to handle it ; (not a good design choice, but meant to keep a student from banging their head ; for 30min+ if they don't realize they have an extra quote):) (check-equal? ((normalize-expected-eval `("someProg" 'error)) "a-mock-exception") #t) (check-equal? ((normalize-expected-eval `("someProg" 'error ,struct:2*3)) "a-mock-exception") #t) ; The second-item can also be a function-value. ; (Here, the unquoting is necessary (since we don't heroically/misguidedly try to fix it here).) (check-equal? ((normalize-expected-eval `("someProg" ,(λ(v) 17))) "a-mock-exception") 17) (check-equal? ((normalize-expected-eval `("someProg" ,(λ(v) 17) ,struct:2*3)) "a-mock-exception") 17) (check-equal? ((normalize-expected-eval `("someProg" ,(λ() 17))) "a-mock-exception") 17) (check-equal? ((normalize-expected-eval `("someProg" ,(λ() 17) ,struct:2*3)) "a-mock-exception") 17) ; test-eval : H_i-example -> boolean ; Test that the H_i-example `eval`s to what it should. ; (define (test-eval t) (with-handlers {[exn:fail? (if (procedure? (normalize-expected-eval t)) (normalize-expected-eval t) (print-exception t 'eval))]} (my-check-equal? (eval (string->expr (first t))) (second t) (format "Program ~v\neval'd to ~v\ninstead of ~v\nas expected." (first t) (eval (string->expr (first t))) (second t))))) ; test-parse-inverse-of-to-string : H_i-example -> void? ; Test that `parse` and `expr->string` are inverses of each other: ; `parse` is a right-inverse: for a string `s`, (expr->string (parse s)) = s, and ; `parse` is a left- inverse: for a tree `expr`, (parse (expr->string expr)) = expr. ; Note that spaces between tokens in a string is ignored, so they're not *quite* exact inverses. ; ; Also, other tests are redundant with checking the left-inverse, ; but we still check it to be independent of other code. ; (define (test-parse-inverse-of-to-string t) (and (with-handlers {[exn:fail? (print-exception t 'parse-then-tostring)]} (my-check-equal? (string->tokens (expr->string (string->expr (first t)))) (string->tokens (first t)) (format "Parsing ~v then converting back to string gave ~v." (first t) (expr->string (string->expr (first t)))))) (or (< (length t) 3) ; don't call it failure, if we don't have info to do further test! (with-handlers {[exn:fail? (print-exception t 'tostring-then-parse)]} (my-check-equal? (string->expr (expr->string (third t))) (third t) (format "Converting ~v to string and re-parsing it gave ~v." (third t) (expr->string (third t)))))))) ; test-one-prog-2-4-ways : H_i-example -> void? ; Make sure that t meets the following properties: ; i. Parsing the string results in the expected internal representation (*) ; ii. Check that parsing the string and then to-string'ing the result ; gives back the initial string ; iii. Check that to-string'ing the internal representation and then parsing ; that resulting string gives back the initial internal representation (*) ; iv. check that eval'ing the (parsed) string gives the expected value. ; ; (*) steps i,iii can only be performed if the H_i-example contained all three values. ; If it only contained a string and a value, then only *two* tests get performed. ; This affects the test-number reported, should a later test fail. ; (define prog# 0) (define (test-one-prog-2-4-ways t) (begin (set! prog# (add1 prog#)) (printf ".") (begin0 (and (test-internal-representation t) (test-parse-inverse-of-to-string t) ; N.B. counts as two tests (test-eval t)) (when (zero? (remainder prog# 5)) (printf " "))))) (define exceptions-seen 0) (define (print-exception t where) (λ(exn) (begin (set! exceptions-seen (add1 exceptions-seen)) (printf "~nEXCEPTION: ~a~nOccurred while trying to ~a test-program #~a: ~v~n" (exn-message exn) where prog# (first t)) #f))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; run the tests ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (printf "Running OG-test-harness~n") (define test-results (map test-one-prog-2-4-ways all-tests)) ; This line actually invokes the checker (define n (length test-results)) (define fails (length (filter false? test-results))) (define passes (length (filter (λ(x) (equal? x #true)) test-results))) (if (zero? n) (printf "Please add H0 test-cases to `all-tests`.") (printf "~nH-test-harness done: ~v/~v (~v%) H programs passed 2-to-4 checks each; ~a.~n" passes n (floor (* 100 (/ passes n))) (if (zero? fails) "nice" (format "~v H programs failed" fails)))) ;; a line which just "re"prints out the tests, ;; except with the *actual* (not expected) results of eval, string->expr. ;; #;(pretty-print (map (λ(tst) (let* {[prog (string->expr (first tst))]} (list (expr->string prog) (eval prog)))) all-tests)) #| @author ibarland@radford.edu @version 2021-Nov-08 @original-at http://www.radford.edu/itec380/2021fall-ibarland/Homeworks/Project/OG0-tests.rkt @license CC-BY -- share/adapt this file freely, but include attribution, thx. https://creativecommons.org/licenses/by/4.0/ https://creativecommons.org/licenses/by/4.0/legalcode Including a link to the *original* file satisifies "appropriate attribution". |#