/ocuality

Integrated Logging, Assertion, and Unit Testing library for building quality Ocaml programs.

Primary LanguageOCamlMIT LicenseMIT

#ocuality#

Integrated Logging, Assertion, and Unit Testing library for building quality Ocaml programs.

##Log

Global logging that writes to standard out. Multiple levels of granularity (Spew, Debug, ..., Crash) available, with the ability to set the level to accept and print (plus all levels of higher precedence).

E.g.

let logExampleMain() = begin
    (** all log messages Info or higher will end up in the logs *)
    Log.setRunLevel Log.Info;

    Log.info "This info message will get logged";
    Log.warn ("This warning will show up in the logs, too, it's higher " ^
        "precedence than Log.Info");
    Log.debug ("This debug statement won't show up. It would only show up " ^
        " if Log.setRunLevel was Log.Debug or Log.Spew")
    end

will produce the output:

[2012/10/17-22:51::11] [INFO] This info message will get logged
[2012/10/17-22:51::11] [WARN] This warning will show up in the logs, too, it's higher precedence than Log.Info

##Comparer

Encapsulation of compare() and toString() methods. Implementation for primitives included.

(** the type we want to be able to compare *)
type tSkyObject =
    |Sun
    |Moon

(** compare function *)   
let compareSkyObjects skyObj1 skyObj2 = begin
    match skyObj1, skyObj2 with
    |Sun, Sun -> 0
    |Sun, Moon -> 1
    |Moon, Sun -> (-1)
    |Moon, Moon -> 0
    end

(** toString function *)   
let skyObjectToString skyObj = begin
    match skyObj with
    |Sun -> "Sun"
    |Moon -> "Moon"
    end

let comparerExampleMain () = begin

    let skyComp = Comparer.create
        ~toStringFun:skyObjectToString
        ~compareFun:compareSkyObjects
    in

    let s1 = Sun in
    let s2 = Moon in

    if (Comparer.greaterThan skyComp s1 s2) then begin
        Log.info ("s1 was greater than s2 for values: [s1 = " ^
            (Comparer.toString skyComp s1) ^
            ", s2 = " ^
            (Comparer.toString skyComp s2) ^ 
            "]");
        end;
    end

will produce the output:

[2012/10/17-22:51::11] [INFO] s1 was greater than s2 for values: [s1 = Sun, s2 = Moon]

##Verify

Assertions that use Comparer's when applicable. Produces helpful log messages when the verification fails or (optionally) on Success.

Continuing with the Sky/Sun/Moon example:

let verifyExampleMain () = begin

    let skyComp = Comparer.create
        ~toStringFun:skyObjectToString
        ~compareFun:compareSkyObjects
    in

    (* set the global type of response to create when a verification
    succeeds or fails *) 
    CheckHandler.setSuccessResponse Log.Info;
    CheckHandler.setFailResponse CheckHandler.Continue Log.Crash;

    Verify.notEqual
        ~label: "I have two sky objects, making sure they aren't the same"
        ~cmp: skyComp
        ~x1: Sun
        ~x2: Moon;

    (* Most importantly, we can compose higher order Comparers that
       still work with the Verify.* methods. see Comparer.make*() methods
       for more.  *)
       
    (*  Here, we make a comparer for a pair of type (tSkyObject * int) *)
    let pairComparer = Comparer.makePair skyComp Comparer.ints in

    (** this verification is going to fail at runtime *)
    Verify.areEqual
        ~label:"Are my pairs equal?"
        ~cmp:pairComparer
        ~x1:(Sun, 1) 
        ~x2:(Moon, 2);

    end

will produce the following log message, then exit after the Failure occurs.

[2012/10/18-04:14::03] [INFO] [Success] I have two sky objects, making sure they aren't the same :: (x1 != x2) for values: (x1 = Sun), (x2 = Moon)
[2012/10/18-04:14::03] [CRASH] [FAILURE] Are my pairs equal? :: (x1 == x2) for values: (x1 = (Sun, 1)), (x2 = (Moon, 2))

##TestCase Finally, we can put it all together with a unit testing framework. The TestCase module makes test out of simple (fun () -> ()) methods, and inside those methods we can call Verify.* methods to ensure things are returning correct values.

Note that the Verify.* methods can be called in the application code or test code. Calling Verify.* methods in application code can enforce preconditions that must hold whenever that production code runs. This way you only need to learn one Assertion library, instead of of using the built in 'assert' method in production code then a separate api from a unit testing library in test code.

let testCaseExampleMain () = begin

    (** application code that creates a list of the numbers (1..x) *)
    let countToX x = begin

        (** this could fail in either testing or while running 
            the application *)
        Verify.gt
            ~label: "can only count to positive numbers, is x > 0?"
            ~cmp:Comparer.ints
            ~x1: x
            ~x2: 0;

        let rec makeListLoop  accList countdown = begin
            match countdown == 0 with
            |true -> accList
            |false -> makeListLoop (countdown::accList) (countdown - 1)
            end
        in    

        let countList = makeListLoop [] x in
        countList
        end
    in    

    (** a test case that will exercise our countToX method and make sure it
        outputs some correct values. *)
    let testCase1 = begin
        let testFun () = begin
            Log.test "Starting TestCase1";

            let observedCountTo3 = countToX 3 in
            let expectedCountTo3 = [1; 2; 3] in

            let intListComparer = ListUtil.makeComparer Comparer.ints in
            
            Verify.areEqual
                ~label:"(exp, obs) countTo3"
                ~cmp: intListComparer
                ~x1: expectedCountTo3
                ~x2: observedCountTo3;

            (** Now to get fancy... This will verify that the block of 
                code generates a failure. Note that the failure will
                occur inside the countToX method. *)

            Verify.doesFail    
                ~label:"Passing in a negative number should fail"
                ~block: (fun () -> ignore(countToX (-22)));
                
            Log.test "Finished TestCase1";
            ()
            end
        in
        let atomicTestCase = TestCase.createAtomic
            ~label:"TestCase1"
            ~testFun:testFun
        in
        atomicTestCase
        end    
    in

    CheckHandler.setFailResponse CheckHandler.Exit Log.Crash;
    (** now run the test case. Results will be printed to the log (stdout). *)
    ignore (TestCase.run testCase1);
    end


will produce the following log message. Note that when the test case is run, log messages about the number of verifications that passed/failed are summarized on the very last line. When there are deeply nested test cases, it will even tell you where the broken tests are.

[2012/10/18-04:14::03] [TEST] 

    Atom: 'testcase1' Begin.

[2012/10/18-04:14::03] [TEST] Starting TestCase1
[2012/10/18-04:14::03] [INFO] [Success] can only count to positive numbers, is x > 0? :: (x1 > x2) for values: (x1 = 3), (x2 = 0)
[2012/10/18-04:14::03] [INFO] [Success] (exp, obs) countTo3 :: (x1 == x2) for values: (x1 = [1; 2; 3]), (x2 = [1; 2; 3])
[2012/10/18-04:14::03] [CRASH] [IgnoredFailure] can only count to positive numbers, is x > 0? :: (x1 > x2) for values: (x1 = -22), (x2 = 0)
[2012/10/18-04:14::03] [INFO] Verify.doesFail block exited with exception
[2012/10/18-04:14::03] [INFO] [Success] Verify.doesFail: failed as expected. Passing in a negative number should fail
[2012/10/18-04:14::03] [TEST] Finished TestCase1
[2012/10/18-04:14::03] [INFO] [Success] Atom: 'testcase1' Finished Properly
[2012/10/18-04:14::03] [TEST] Atom: 'testcase1' End.
[2012/10/18-04:14::03] [TEST] Atom: 'testcase1' Passed. 0/5 Verifications Failed
[2012/10/18-04:14::03] [TEST] testcase1: All Tests Passed.