#lang scheme/base (require mzlib/etc scheme/class srfi/26/cut (planet untyped/unlib:3/pipeline) "base.ss" "snooze-syntax.ss" "test-base.ss" "test-data.ss" "test-util.ss" "era/era.ss" "sql/sql.ss") ; Tests ---------------------------------------- ; snooze% -> test-suite (define (make-snooze-transaction-tests snooze) (define-snooze-interface snooze) (define-alias per person) (define-alias cou course) ; (integer -> course) (define (find-course-by-value val) (find-one (sql:select #:from cou #:where (sql:= cou-value val)))) ; time-tai (define time-tai1 (string->time-tai "2001-01-01 01:01:01")) ; course (define course (make-course 'code "Name" 12345 1234.5 #t time-tai1)) ; (U integer #f) ; initialised below (define course-revision #f) ; test-suite (test-suite "snooze-transaction-tests" ; ***** NOTE ***** ; Each test below depends on the tests before it. Add/edit tests at your peril! ; **************** ; create test data for transaction tests #:before (lambda () (create-table entity:course) (create-table entity:person) (save! course) (set! course-revision (struct-revision course))) ; delete test data from transaction tests #:after (lambda () (drop-table entity:person) (drop-table entity:course)) (test-case "call-with-transaction: transaction committed" (check-not-false (find-course-by-value 12345) "Precondition failed.") (call-with-transaction (lambda () (set-course-value! course 54321) (save! course))) ; Revision number should have increased by 1: (check-equal? (struct-revision course) (add1 course-revision)) (check-not-false (find-course-by-value 54321) "Postcondition failed.") ; Need to reset the course-revision variable for subsequent tests: (set! course-revision (struct-revision course))) (test-case "call-with-transaction: transaction aborted" (check-not-false (find-course-by-value 54321) "Precondition failed.") (with-handlers ([exn:snooze? void]) (call-with-transaction (lambda () (set-course-value! course 12345) (save! course) (raise-exn exn:snooze "Aborting transaction.")))) (check-equal? (struct-revision course) course-revision "check 1") (check-not-false (find-course-by-value 54321) "Postcondition failed.")) (test-case "call-with-transaction: nested transactions aborted" (check-not-false (find-course-by-value 54321) "check 1 - precondition 1") (check-equal? (course-value course) 54321 "check 2 - precondition 2") (with-handlers ([exn:snooze? void]) (call-with-transaction (lambda () (set-course-value! course 12345) (save! course) (call-with-transaction (lambda () (set-course-value! course 13579) (save! course) (raise-exn exn:snooze "Aborting transaction.")))))) (check-equal? (struct-revision course) course-revision "check 3") (check-equal? (course-value course) 54321 "check 4") (check-not-false (find-course-by-value 54321) "check 5 - postcondition")) (test-case "call-with-transaction: inner nested transaction aborted (SQLite will fail this test)" (check-not-false (find-course-by-value 54321) "Precondition failed.") (call-with-transaction (lambda () (set-course-value! course 12345) (save! course) (with-handlers ([exn:snooze? void]) (call-with-transaction (lambda () (set-course-value! course 13579) (save! course) (raise-exn exn:snooze "Aborting transaction.")))))) (check-not-false (find-course-by-value 12345) (format "Postcondition failed (~a)." (if (find-course-by-value 13579) "both nested transactions were aborted: this is the expected behaviour for SQLite" (format "final course value was: ~a" (course-value (find-by-id entity:course (course-id course)))))))) ; Persistent struct roll back ----------- (test-case "call-with-transaction: attributes rolled back" (let ([course (make-course 'code "Name" 10000 1234.5 #t time-tai1)]) (set-course-value! course 12345) (save! course) (set-course-value! course 23456) (check-equal? (struct-revision course) 0 "check 1") (check-equal? (course-value course) 23456 "check 2") (with-handlers ([exn:snooze? void]) ; Should roll back to here, where value is 23456 (call-with-transaction (lambda () (set-course-value! course 54321) (save! course) (check-equal? (struct-revision course) 1 "check 3") (check-equal? (course-value course) 54321 "check 4") (raise-exn exn:snooze "Aborting transaction.")))) (check-equal? (struct-revision course) 0 "check 5") (check-equal? (course-value course) 23456 "check 6") (check-not-exn (cut save! course) "check 7") (check-not-exn (cut delete! course) "check 8"))) (test-case "call-with-transaction: repeated assignments rolled back" (let ([course (make-course 'code "Name" 10000 1234.5 #t time-tai1)]) (save! course) (set-course-value! course 12345) (set-course-value! course 23456) (with-handlers ([exn:snooze? void]) (call-with-transaction (lambda () (set-course-value! course 54321) (set-course-value! course 65432) (save! course) (check-equal? (course-value course) 65432 "check 1") (raise-exn exn:snooze "Aborting transaction.")))) (check-equal? (course-value course) 23456 "check 2") (check-not-exn (cut save! course) "check 3") (check-not-exn (cut delete! course) "check 4"))) (test-case "call-with-transaction: set enable-transaction-backups? to #f" (parameterize ([enable-transaction-backups? #f]) (let ([course (make-course 'code "Name" 10000 1234.5 #t time-tai1)]) (save! course) (set-course-value! course 12345) (set-course-value! course 23456) (with-handlers ([exn:snooze? void]) (call-with-transaction (lambda () (set-course-value! course 54321) (set-course-value! course 65432) (save! course) (check-equal? (course-value course) 65432 "check 1") ; Changes shouldn't be undone: (raise-exn exn:snooze "Aborting transaction.")))) (check-equal? (course-value course) 65432) ; Can't delete or save because revision numbers are out of sync: (check-exn exn:fail:snooze:revision? (cut save! course) "check 2") (check-exn exn:fail:snooze:revision? (cut delete! course) "check 3") ; Have to delete the test record by loading/deleting it: (check-not-exn (cut delete! (find-by-id entity:course (struct-id course))) "check 4")))) (test-case "cannot make full continuation jumps into or out of transactions" (begin-with-definitions ; General continuation jump out: (let/cc escape (call-with-transaction (lambda () (check-exn exn:fail:contract:continuation? (lambda () (escape #f)) "check 1")))) ; Escape continuation jump out: (define resume (check-not-exn (lambda () (let/ec escape (call-with-transaction (lambda () (let/cc resume (escape resume)))))) "check 2")) ; General continuation jump in: (check-exn exn:fail:contract:continuation? (lambda () (resume #f)) "check 3"))) (test-case "transaction-pipeline called" (begin-with-definitions ; (U string #f) (define num-transactions 0) ; stage (define-stage (log-stage continue conn . args) (set! num-transactions (add1 num-transactions)) (apply continue conn args)) (send snooze set-transaction-pipeline! (list log-stage)) (delete! (save! (make-person "Dave"))) (check-equal? num-transactions 2))) (test-case "transaction-pipeline aborts transaction before body" (begin-with-definitions ; stage (define-stage (log-stage continue conn . args) (raise-exn exn:snooze "Escaping") (apply continue conn args)) (send snooze set-transaction-pipeline! (list log-stage)) (with-handlers ([exn:snooze? void]) (save! (make-person "Dave"))) (check-equal? (length (find-all (sql:select #:from per))) 0 "check 1"))) (test-case "transaction-pipeline aborts transaction after body" (begin-with-definitions ; stage (define-stage (log-stage continue conn . args) (begin0 (apply continue conn args) (raise-exn exn:snooze "Escaping"))) (send snooze set-transaction-pipeline! (list log-stage)) (with-handlers ([exn:snooze? void]) (save! (make-person "Dave"))) (check-equal? (length (find-all (sql:select #:from per))) 0 "check 1"))) (test-case "structs rolled back when transaction aborted" (begin-with-definitions (define person (make-person "Dave")) (send snooze set-transaction-pipeline! null) (check-not-exn (lambda () (let/ec escape (call-with-transaction (lambda () (set-person-name! person "Noel") (save! person) (escape #f))))) "check 0") (check-equal? (person-name person) "Dave" "check 1") (check-not-exn (lambda () (let/ec escape (set-person-name! person "Matt") (save! person) (escape #f))) "check 1.5") (check-equal? (person-name person) "Matt" "check 2") (delete! person))))) ; Provide statements ----------------------------- (provide make-snooze-transaction-tests)