REBOL [
    title: "Simple-Test"
    version: 0.4.1
    date: 12-Mar-2011
    author: Peter W A Wood
    file: %simple-test.r
    purpose: {A simple Rebol testing framework}
    library: [
      level: 'intermediate
      platform: 'all
      type: [package tool]
      domain: [test parse]
      license: 'mit
  ]
]

simple-test: make object! [
  
  ;; copy the built-in now function for use in case tests overwrite it
  test-now: :now
  
  ;; copy the built-in print function for use in case tests overwrite it
  test-print: :print
  
  ;; if the disarm function is not defined assume we are are running under R3
  ;;  define a disarm function
  if not word? 'disarm [
    disarm: func [value][:value]
  ]  
  
  ;; verbose flag to control amount of output
  verbose: false
  
  ;; overall counts
  final-tests: 0
	final-passed: 0
	final-failed: 0
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;; eval-case object  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  ;; Holds the parse rules for evaluate-case
  eval-case: make object! [
    
	  ;; local variables
	  assertion-no: 0
	  name: none
	  name-not-printed: true
	  result: none
	  result-type: none
	  run-time: none
	  timestamp: none
	  assertion-no: 0
	  actual: none
	  actual-result-type: none
	  expected: none
	  expected-result-type: none
	  tolerance: none
	  tolerance-result-type: none
	  any-failures: false
	  response: none
	  test-result: none
	  tr: none
	    
	  ;; "private" methods
	  assert-act-exp-action: func [
	    action [block!]
	    /local
	    rb                            ;; result block
	    res                           ;; test result
	    assertion                     ;; the asertion to be made
	  ][
	    inc-assertion-no
      get-actual-result
      get-expected-result
      assertion: copy [:actual :expected]
      insert assertion action
      either all [
        equal? :actual-result-type "normal"
        equal? :expected-result-type "normal"
        do assertion
      ][
        res: "passed"
      ][
        res: "failed"
      ]
      rb: reduce [
        'result :res
        'actual mold :actual
        'actual-restype :actual-result-type
        'expected mold :expected
        'expected-restype :expected-result-type
      ]   
      
      append test-result/assertions reduce [to-word join "a" assertion-no rb]
	  ]
	  
	  assert-result-type-action: func [
	    expected-result-type [string!]
	    /local
	    rb                            ;; result block
	    res                           ;; test result
	  ] [
	    inc-assertion-no
	    get-actual-result
	    	    
	    either equal? expected-result-type actual-result-type [
	      res: "passed"
	    ][
	      res: "failed"
	    ]
	    rb: reduce [
        'result :res
        'actual mold :actual
        'actual-restype :actual-result-type
      ]   
      
      append test-result/assertions reduce [to-word join "a" assertion-no rb]
	    
	  ]
	  
	  assert-equal-tolerance-action: func [
	    /local
	    rb                            ;; result block
	    res                           ;; test result
	  ][
	    inc-assertion-no
      get-actual-result
      get-expected-result
      get-tolerance-result
      
      either all [
        equal? :actual-result-type "normal"
        equal? :expected-result-type "normal"
        equal? :tolerance-result-type "normal"
        number? :actual
        number? :expected
        number? :tolerance
        tolerance >= abs (actual - expected)  
      ][
        res: "passed"
      ][
        res: "failed"
      ]
      rb: reduce [
        'result :res
        'actual mold :actual
        'actual-restype :actual-result-type
        'expected mold :expected
        'expected-restype :expected-result-type
        'tolerance mold :tolerance
        'tolerance-restype :tolerance-result-type
      ]   
      
      append test-result/assertions reduce [to-word join "a" assertion-no rb]
	  ]
	  
	  assert-result-type-action: func [
	    expected-result-type [string!]
	    /local
	    rb                            ;; result block
	    res                           ;; test result
	  ] [
	    inc-assertion-no
	    get-actual-result
	    	    
	    either equal? expected-result-type actual-result-type [
	      res: "passed"
	    ][
	      res: "failed"
	    ]
	    rb: reduce [
        'result :res
        'actual mold :actual
        'actual-restype :actual-result-type
      ]   
      
      append test-result/assertions reduce [to-word join "a" assertion-no rb]
	    
	  ]
	  
	  assert-not-error-action: func [
	    /local
	    rb                            ;; result block
	    res                           ;; test result
	  ] [
	    inc-assertion-no
	    get-actual-result
	    	    
	    either not equal? actual-result-type "error" [
	      res: "passed"
	    ][
	      res: "failed"
	    ]
	    rb: reduce [
        'result :res
        'actual mold :actual
        'actual-restype :actual-result-type
      ]   
      
      append test-result/assertions reduce [to-word join "a" assertion-no rb]
	    
	  ]
	  
	  assert-logic-action: func [
      /assert-false
	    /local
	    rb                            ;; result block
	    res                           ;; test result
	  ][
	    inc-assertion-no
	    get-actual-result
	    
	    either actual-result-type = "normal" [
	      either assert-false [
	        res: either actual =  false ["passed"] ["failed"]
	      ][
	        res: either actual = true  ["passed"] ["failed"]
	      ]
	    ][
	      res: "failed"
	    ]
	    
	    rb: reduce [
	      'result :res
	      'actual mold :actual
	      'actual-restype :actual-result-type
	    ]
	    
	    append test-result/assertions reduce [to-word join "a" assertion-no rb]
	  ]
	  
	  get-actual-result: does [
	    ;; get the actual result
      either all [
        unset! <> type? first actual-block 
        equal? 'do first actual-block
        equal? 1 length? actual-block
      ][
        actual: :tr
        actual-result-type: select test-result 'result-type
      ][
        response: evaluate :actual-block
        actual: select response 'result
        actual-result-type: :response/result-type
      ]
	  ]
	  
	  get-expected-result: does [
	    ;; evaluate the expected result
      response: evaluate :expected-block
      expected: select response 'result
      expected-result-type: :response/result-type
    ]
    
    get-tolerance-result: does [
	    ;; evaluate the tolerance result
      response: evaluate :tolerance-block
      tolerance: select response 'result
      tolerance-result-type: :response/result-type
    ]
    
    inc-assertion-no: does [
     assertion-no: add assertion-no 1 
    ]
    
	  init: does [
	    assertion-no: 0
	    name: none
	    actual: none
	    actual-result-type: none
	    expected: none
	    expected-result-type: none
	    tolerance: none
	    tolerance-result-type: none
	    test-result: copy [
	      status "normal"
	      case "not set"
	      timestamp "not set"
	      run-time "not set"
	      result "not set"
	      result-type "not set"
	      assertions "not set"
	    ]
	    test-result/assertions: copy []
	  ]
	  

    ;; object parse rules
    ;; name-rule - checks for properly formatted name
    name-rule: [
      'name string! 
    ]
    
    ;; setup-rule - evaluates any supplied setup code
    setup-rule: [
      'setup set setup [block!] (
        response: evaluate :setup
        if equal? :response/result-type "error" [
          test-result/status: "setup failure"
        ]
      )
    ]
    
    ;; teardown-rule - evaluates any supplied teardown code
    teardown-rule: [
      'teardown set teardown [block!] (
        response: evaluate :teardown
        if equal? :response/result-type "error" [
          either equal? test-result/status "setup failure" [
            test-result/status: "setup & teardown failure"
          ][
            test-result/status: "teardown failure"
          ]
        ]
      )
    ]
    
    ;; do-rule - evaluates the code being tested (the do block)
    do-rule: [
      'do set do-block [block!] (
        response: evaluate :do-block
        test-result/timestamp: mold :response/timestamp
        test-result/run-time: mold :response/run-time
        tr: select response 'result
        test-result/result: mold :tr
        test-result/result-type: :response/result-type
      )
    ]
    
    ;; assert-rule - evaluates an assertion supplied to check the test
    assert-rule: [
      assert-equal-rule
      |
      assert-equal-tolerance-rule
      |
      assert-error-rule
      |
      assert-false-rule
      |
      assert-not-equal-rule
      |
      assert-not-error-rule
      |
      assert-not-same-rule
      |
      assert-same-rule
      |
      assert-true-rule
      |
      assert-unset-rule
    ]
    
    ;; assert sub-rules
    assert-equal-rule: [
      'assert 'equal set actual-block block! set expected-block block! (
        assert-act-exp-action [equal?]
      )
    ]
    
    assert-equal-tolerance-rule: [
      'assert 'equal opt 'with 'tolerance 
      set actual-block block!
      set expected-block block!
      set tolerance-block block! (
        assert-equal-tolerance-action 
      )
    ]
    
    assert-error-rule: [
      'assert 'error set actual-block block! (
        assert-result-type-action "error"
      )
    ]
    
    assert-false-rule: [
      'assert 'false set actual-block block! (
        assert-logic-action/assert-false
      )
    ]
    
    assert-not-equal-rule: [
      'assert 'not 'equal set actual-block block! set expected-block block! (
        assert-act-exp-action [not equal?]
      )
    ]
    
    assert-not-error-rule: [
      'assert 'not 'error set actual-block block! (assert-not-error-action)
    ]
    
    assert-not-same-rule: [
      'assert 'not 'same set actual-block block! set expected-block block! (
        assert-act-exp-action [not same?]
      )
    ]
    
    assert-same-rule: [
      'assert 'same set actual-block block! set expected-block block! (
        assert-act-exp-action [same?]
      )
    ]
      
    assert-true-rule: [
      'assert 'true set actual-block block! (
        assert-logic-action
      )
    ]
    
    assert-unset-rule: [
      'assert 'unset set actual-block block! (
        assert-result-type-action "unset"
      )
    ]
    
    ; MAIN RULE
    rules: [
      name-rule 
      opt setup-rule
      do-rule
      some assert-rule
      opt teardown-rule
    ]
    
  ] ;; end eval-case object
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;; eval-set object  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  ;; Holds the parse rules for evaluate-set
  eval-set: make object! [
    
	  ;; local variables
	  name: none
	  setup-each: none
	  teardown-each: none
	  teardown-once: none
	  no-tests: 0
	  passes: 0
	  failures: 0
	  any-failures: false
	  
	  ;; "private" methods
	  init: does [
	    name: none
	    setup-each: none
	    teardown-each: none
	    teardown-once: none
	    no-tests: 0
	    passes: 0
	    failures: 0
	    simple-test/verbose: false
	    any-failures: false
	  ]
	  
	  perform-setup-each: does [
	    response: evaluate :setup-each
      if equal? :response/result-type "error" [
        test-print ["^-Setup each failed"]
      ]
	  ]
	  
	  perform-teardown-each: does [
	    response: evaluate :teardown-each
      if equal? :response/result-type "error" [
        test-print ["^-Teardown each failed"]
      ]
	  ]
	  
	  
	  print-type-value: func [act-exp [string!] type [string!] val [string!]][
	    switch  type [
	      "normal" [
	        test-print rejoin [
	          "^-" :act-exp " - type - " type? do val "^/^-" val
	        ]
	      ]
	      "error" [
	        test-print rejoin ["^-" :act-exp " - type - error!"]
	        test-print join "^-" val
	      ]
	      "unset" [
	        test-print  rejoin ["^-" :act-exp " - type - unset!"]
	      ]
	    ]
	  ]
	  
	  process-case-result: func [
	    cr [block!]
	  ][
	    if cr/status = "Invalid test case" [
	      test-print join "^/" [cr/status]
	      test-print rejoin ["^-" mold cr/case]
	      return none
	    ]
	    
	    ;; any failures ?
	    any-failures: false
	    foreach [a-no a-blk] cr/assertions [ 
	      if not equal? a-blk/result "passed" [any-failures: true]
	    ]
	    
	    either any-failures [
	      failures: add failures 1
	    ][
	      passes: add passes 1
	    ]
	    
	    ;; print test case name if required	    
	    if any [
	      any-failures
	      not equal? cr/status "normal"
	      simple-test/verbose
	    ][
	      test-print rejoin [
	        "^/Test - " cr/case/name 
	        either any-failures [" - *** failed ***"][" - passed"]
	      ]
	    ]
	    
	    if not equal? cr/status "normal" [test-print join "^-" cr/status]
	    
	    ;; print test case result if required
	    if any [
	      any-failures
	      simple-test/verbose
	    ][
	      test-print join "" [
	        "^-On " cr/timestamp "^/"
	        "^-Took " cr/run-time
	      ]
	    
	      foreach [a-no a-blk] cr/assertions [
	        test-print rejoin [
	          "^-Assertion " 
	          remove to-string a-no                  ;; strip off leading a
	          " " a-blk/result
	        ]
	        if not equal? a-blk/result "passed" [
	          print-type-value "actual" a-blk/actual-restype a-blk/actual
	          
	          if find a-blk 'expected [
	            print-type-value "expected" a-blk/expected-restype a-blk/expected
	          ]
	        ]	      
	      ]
	    ]
	  ]
	      
	  teardown-and-print: does [
	    if teardown-once [
	      response: evaluate teardown-once
        if equal? :response/result-type "error" [
          test-print ["^-Teardown once failed"]
        ]
      ]
	    test-print join "Totals^/" [
	      "^-Tests  = " no-tests #"^/"
	      "^-Passed = " passes #"^/"
	      "^-Failed = " failures
	    ]
	  ]
	  
    ;; object parse rules
    ;; name-rule - stores the test name 
    name-rule: [
      'set 'name set name string! (
        test-print join "Test Set " [name]
      )
    ]
    
    ;; setup-each-rule - stores the setup code
    setup-each-rule: [
      'setup 'each set setup-each block!
    ]
    
    ;; setup-once-rule - evaluates any supplied setup code
    setup-once-rule: [
      'setup 'once set setup block! (
        response: evaluate :setup
        if equal? :response/result-type "error" [
          test-print ["^-Setup once failed"]
        ]
      )
    ]
    
    ;; teardown-each-rule - stores the teardown code
    teardown-each-rule: [
      'teardown 'each set teardown-each block!
    ]
    
    ;; teardown-once-rule - stores any teardown code to run after test cases
    teardown-once-rule: [
      'teardown 'once set teardown-once block!
    ]
    
    ;; test-case rule - evaluates a test case
    test-case-rule: [
      'test 'case set test-case block! (
        no-tests: no-tests + 1
        if setup-each [
          perform-setup-each
        ]
        
        process-case-result evaluate-case :test-case
        
        if teardown-each [
          perform-teardown-each
        ]
      )
    ]
    
    ; MAIN RULE
    rules: [
      (init)
      opt ['verbose (simple-test/verbose: true)]
      name-rule 
      opt setup-once-rule
      opt setup-each-rule
      opt teardown-each-rule
      opt teardown-once-rule
      some test-case-rule
      end (teardown-and-print)
    ]
    
  ] ;; end eval-set object
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
    
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;; evaluate function  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  evaluate: func [
    {
      Evaluates the supplied code and returns a rebol block 
      about the evaluation:
        [
          code-block - block! - the code block evaluated
          timestamp - date! -  the time of evaluation
          run-time - time! - the execution time of the evaluation
          result - any! - the result of the evaluation
                        - this will be an error object if an error occurred
                        - none if the result is unset
          result-type - "normal" - evaluation produced a result
                      - "error" - an error occurred during evalutaion
                      - "unset" - the evaluation returned unset
        ]
    }
    code-block [block!]       ; Format [code]
    /local
    timestamp                 ; The time of evaluation
    start                     ; The start time of evaluation
    end                       ; The end time of evaluation
    run-time                  ; The time taken to perform the evaluation
    result                    ; The result of the evaluation
    result-type               ; "normal", "error" or "unset"
    error                     ; set if error occured
  ][
    ;; initialisations
    timestamp: none
    start: none
    end: none
    run-time: none
    result: none
    result-type: copy "normal"
    error: none
    ;; evaluate the code
    timestamp: test-now
    start: test-now/precise
    if error? set/any 'result try code-block [
      ;; catch errors in the evaluation of the code block
      result: disarm result
      result-type: copy "error"
    ]
    end: test-now/precise
    if all [
      :result-type <> "error"
      error? set/any 'result try [:result]
    ][
      result: none
      result-type: copy "unset"
    ]

    run-time: difference end start
    
    ;; create and return the output
    reduce [
      'code-block :code-block 'timestamp :timestamp
      'run-time :run-time 'result :result 'result-type :result-type
    ]
  
  ] ;; end of evaluate function
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;; evaluate-case ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  evaluate-case: func [
    { 
      Evaluates a single test case presented in the following dialect:
            name "test identifer"
  	        opt setup [setup code]
            do [the code being tested - this will be timed]
            some assert-XXXXX [assertions to check the result]
            opt teardown [teardown code]
    }
	  the-test [block!]
  ][
    eval-case/init
    eval-case/test-result/case: copy/deep :the-test
    either parse :the-test :eval-case/rules [
      get in eval-case 'test-result
    ][
      reduce [
        'status "Invalid test case"
        'case :the-test
      ]
    ]
	  
  ] ;; end of evaluate-case
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;; evaluate-set function  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  evaluate-set: func [
  	{ Evaluates a set of tests }
	  test-set [block!]             ; Format: [command [attributes]]
  ][
    either parse test-set eval-set/rules [
      final-tests: add final-tests eval-set/no-tests
      final-passed: add final-passed eval-set/passes
      final-failed: add final-failed eval-set/failures
      reduce [
        'name eval-set/name
        'tests eval-set/no-tests
        'passed eval-set/passes
        'failed eval-set/failures
      ]
    ][
      test-print "Test halted - syntax error"
      false
    ]
    
    
  ] ;; end of evaluate-set
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  ;;;;;;;;;;;;;;;;;; init-final-totals function  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  init-final-totals: does [
    final-tests: 0
	  final-passed: 0
	  final-failed: 0
  ] ;; end of init-final-totals
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

  ;;;;;;;;;;;;;;;;;; print-final-totals function  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  print-final-totals: does[
    test-print ""
    test-print join "Overall Tests " final-tests
	  test-print join "       Passed " final-passed
	  test-print join "       Failed " final-failed
  ] ;; end of print-final-totals
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;; run-tests function  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  run-tests: func [
  	{ Runs tests - either a set or suite of tests using recursion }
	  tests [file!]
  ][
    test-data: load tests
    either equal? 'suite first test-data [
      foreach suite-or-set second test-data [
        run-tests suite-or-set
      ]
    ][
      simple-test/evaluate-set test-data
    ] 
  ] ;; end of run-tests
  ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

] ;; end of test context!

run-test: func [
  { A wrapper for tests/run-tests in the global context }
  tests [file!]
][
  simple-test/init-final-totals
  simple-test/run-tests tests
  simple-test/print-final-totals
  exit
]