You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucy.apache.org by nw...@apache.org on 2013/02/17 01:46:58 UTC

[lucy-commits] [2/2] git commit: refs/heads/clownfish-test-v2 - Rework test infrastructure

Updated Branches:
  refs/heads/clownfish-test-v2 [created] e15e8697d


Rework test infrastructure

* Add 'Run' method to TestBatch
* Pluggable output formats for tests
* New class TestRunner to run multiple tets batches and print a summary


Project: http://git-wip-us.apache.org/repos/asf/lucy/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucy/commit/ce6c0f5e
Tree: http://git-wip-us.apache.org/repos/asf/lucy/tree/ce6c0f5e
Diff: http://git-wip-us.apache.org/repos/asf/lucy/diff/ce6c0f5e

Branch: refs/heads/clownfish-test-v2
Commit: ce6c0f5ec9c4cc017f7f8f2d0b0cd36c8608ce6b
Parents: b751260
Author: Nick Wellnhofer <we...@aevum.de>
Authored: Fri Feb 15 21:41:08 2013 +0100
Committer: Nick Wellnhofer <we...@aevum.de>
Committed: Sun Feb 17 01:41:28 2013 +0100

----------------------------------------------------------------------
 core/Clownfish/Test/Formatter/TestFormatterTAP.c   |   79 +++++
 core/Clownfish/Test/Formatter/TestFormatterTAP.cfh |   45 +++
 core/Clownfish/Test/TestFormatter.c                |   55 ++++
 core/Clownfish/Test/TestFormatter.cfh              |   50 +++
 core/Clownfish/Test/TestRunner.c                   |  115 +++++++
 core/Clownfish/Test/TestRunner.cfh                 |   54 ++++
 core/Lucy/Test.c                                   |  246 ++++++++-------
 core/Lucy/Test.cfh                                 |   35 ++-
 perl/buildlib/Lucy/Build/Binding/Misc.pm           |    7 +-
 9 files changed, 563 insertions(+), 123 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Clownfish/Test/Formatter/TestFormatterTAP.c
----------------------------------------------------------------------
diff --git a/core/Clownfish/Test/Formatter/TestFormatterTAP.c b/core/Clownfish/Test/Formatter/TestFormatterTAP.c
new file mode 100644
index 0000000..d67edb2
--- /dev/null
+++ b/core/Clownfish/Test/Formatter/TestFormatterTAP.c
@@ -0,0 +1,79 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+
+#define C_LUCY_TESTFORMATTERTAP
+#define LUCY_USE_SHORT_NAMES
+#define CHY_USE_SHORT_NAMES
+
+#include "Clownfish/Test/Formatter/TestFormatterTAP.h"
+#include "Lucy/Test.h"
+#include "Clownfish/Test/TestRunner.h"
+#include "Clownfish/VTable.h"
+
+TestFormatterTAP*
+TestFormatterTAP_new() {
+    TestFormatterTAP *self
+        = (TestFormatterTAP*)VTable_Make_Obj(TESTFORMATTERTAP);
+    return TestFormatterTAP_init(self);
+}
+
+TestFormatterTAP*
+TestFormatterTAP_init(TestFormatterTAP *self) {
+    return (TestFormatterTAP*)TestFormatter_init((TestFormatter*)self);
+}
+
+void
+TestFormatterTAP_batch_prologue(TestFormatterTAP *self, TestBatch *batch) {
+    UNUSED_VAR(self);
+    printf("1..%" PRId64 "\n", TestBatch_Get_Num_Planned(batch));
+}
+
+void
+TestFormatterTAP_vtest_result(TestFormatterTAP *self, bool pass,
+                              uint32_t test_num, const char *fmt,
+                              va_list args) {
+    UNUSED_VAR(self);
+    const char *result = pass ? "ok" : "not ok";
+    printf("%s %d - ", result, test_num);
+    vprintf(fmt, args);
+    printf("\n");
+}
+
+void
+TestFormatterTAP_vtest_comment(TestFormatterTAP *self, const char *fmt,
+                               va_list args) {
+    UNUSED_VAR(self);
+    printf("#   ");
+    vprintf(fmt, args);
+}
+
+void
+TestFormatterTAP_vbatch_comment(TestFormatterTAP *self, const char *fmt,
+                                va_list args) {
+    UNUSED_VAR(self);
+    printf("# ");
+    vprintf(fmt, args);
+}
+
+void
+TestFormatterTAP_summary(TestFormatterTAP *self, TestRunner *runner) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Clownfish/Test/Formatter/TestFormatterTAP.cfh
----------------------------------------------------------------------
diff --git a/core/Clownfish/Test/Formatter/TestFormatterTAP.cfh b/core/Clownfish/Test/Formatter/TestFormatterTAP.cfh
new file mode 100644
index 0000000..f8eeba1
--- /dev/null
+++ b/core/Clownfish/Test/Formatter/TestFormatterTAP.cfh
@@ -0,0 +1,45 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel Lucy;
+
+class Clownfish::Test::Formatter::TestFormatterTAP
+    inherits Clownfish::Test::TestFormatter {
+
+    inert incremented TestFormatterTAP*
+    new();
+
+    inert TestFormatterTAP*
+    init(TestFormatterTAP *self);
+
+    void
+    Batch_Prologue(TestFormatterTAP *self, TestBatch *batch);
+
+    void
+    VTest_Result(TestFormatterTAP *self, bool pass, uint32_t test_num,
+                 const char *fmt, va_list args);
+
+    void
+    VTest_Comment(TestFormatterTAP *self, const char *fmt, va_list args);
+
+    void
+    VBatch_Comment(TestFormatterTAP *self, const char *fmt, va_list args);
+
+    void
+    Summary(TestFormatterTAP *self, TestRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Clownfish/Test/TestFormatter.c
----------------------------------------------------------------------
diff --git a/core/Clownfish/Test/TestFormatter.c b/core/Clownfish/Test/TestFormatter.c
new file mode 100644
index 0000000..207e675
--- /dev/null
+++ b/core/Clownfish/Test/TestFormatter.c
@@ -0,0 +1,55 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define C_LUCY_TESTFORMATTER
+#define LUCY_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestFormatter.h"
+#include "Clownfish/Err.h"
+
+TestFormatter*
+TestFormatter_init(TestFormatter *self) {
+    ABSTRACT_CLASS_CHECK(self, TESTFORMATTER);
+    return self;
+}
+
+void
+TestFormatter_test_result(void *vself, bool pass, uint32_t test_num,
+                          const char *fmt, ...) {
+    va_list args;
+    va_start(args, fmt);
+    TestFormatter_VTest_Result((TestFormatter*)vself, pass, test_num, fmt,
+                               args);
+    va_end(args);
+}
+
+void
+TestFormatter_test_comment(void *vself, const char *fmt, ...) {
+    va_list args;
+    va_start(args, fmt);
+    TestFormatter_VTest_Comment((TestFormatter*)vself, fmt, args);
+    va_end(args);
+}
+
+void
+TestFormatter_batch_comment(void *vself, const char *fmt, ...) {
+    va_list args;
+    va_start(args, fmt);
+    TestFormatter_VBatch_Comment((TestFormatter*)vself, fmt, args);
+    va_end(args);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Clownfish/Test/TestFormatter.cfh
----------------------------------------------------------------------
diff --git a/core/Clownfish/Test/TestFormatter.cfh b/core/Clownfish/Test/TestFormatter.cfh
new file mode 100644
index 0000000..cae9afe
--- /dev/null
+++ b/core/Clownfish/Test/TestFormatter.cfh
@@ -0,0 +1,50 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel Lucy;
+
+abstract class Clownfish::Test::TestFormatter inherits Clownfish::Obj {
+    inert TestFormatter*
+    init(TestFormatter *self);
+
+    inert void
+    test_result(void *vself, bool pass, uint32_t test_num, const char *fmt,
+                ...);
+
+    inert void
+    test_comment(void *vself, const char *fmt, ...);
+
+    inert void
+    batch_comment(void *vself, const char *fmt, ...);
+
+    abstract void
+    Batch_Prologue(TestFormatter *self, TestBatch *batch);
+
+    abstract void
+    VTest_Result(TestFormatter *self, bool pass, uint32_t test_num,
+                 const char *fmt, va_list args);
+
+    abstract void
+    VTest_Comment(TestFormatter *self, const char *fmt, va_list args);
+
+    abstract void
+    VBatch_Comment(TestFormatter *self, const char *fmt, va_list args);
+
+    abstract void
+    Summary(TestFormatter *self, TestRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Clownfish/Test/TestRunner.c
----------------------------------------------------------------------
diff --git a/core/Clownfish/Test/TestRunner.c b/core/Clownfish/Test/TestRunner.c
new file mode 100644
index 0000000..e76ef01
--- /dev/null
+++ b/core/Clownfish/Test/TestRunner.c
@@ -0,0 +1,115 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <string.h>
+
+#define C_LUCY_TESTRUNNER
+#define LUCY_USE_SHORT_NAMES
+#define CHY_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestRunner.h"
+#include "Clownfish/CharBuf.h"
+#include "Clownfish/Err.h"
+#include "Clownfish/Test/Formatter/TestFormatterTAP.h"
+#include "Lucy/Test.h"
+#include "Clownfish/VArray.h"
+#include "Clownfish/VTable.h"
+
+TestRunner*
+TestRunner_new(TestFormatter *formatter) {
+    TestRunner *self = (TestRunner*)VTable_Make_Obj(TESTRUNNER);
+    return TestRunner_init(self, formatter);
+}
+
+TestRunner*
+TestRunner_init(TestRunner *self, TestFormatter *formatter) {
+    self->formatter          = (TestFormatter*)INCREF(formatter);
+    self->num_tests          = 0;
+    self->num_tests_failed   = 0;
+    self->num_batches        = 0;
+    self->num_batches_failed = 0;
+
+    return self;
+}
+
+void
+TestRunner_destroy(TestRunner *self) {
+    DECREF(self->formatter);
+    SUPER_DESTROY(self, TESTRUNNER);
+}
+
+bool
+TestRunner_run_batch(TestRunner *self, TestBatch *batch) {
+    TestBatch_Plan(batch);
+    TestBatch_Run(batch);
+
+    int64_t num_planned = TestBatch_Get_Num_Planned(batch);
+    int64_t num_tests   = TestBatch_Get_Num_Tests(batch);
+    int64_t num_failed  = TestBatch_Get_Num_Failed(batch);
+    bool    failed      = false;
+
+    if (num_failed > 0) {
+        failed = true;
+        TestFormatter_batch_comment(self->formatter, "%d/%d tests failed.\n",
+                                    num_failed, num_tests);
+    }
+    if (num_tests != num_planned) {
+        failed = true;
+        TestFormatter_batch_comment(self->formatter,
+                                    "Bad plan: You planned %d tests but ran"
+                                    " %d.\n",
+                                    num_planned, num_tests);
+    }
+
+    self->num_tests         += num_tests;
+    self->num_tests_failed  += num_failed;
+    self->num_batches       += 1;
+
+    if (failed) {
+        self->num_batches_failed += 1;
+    }
+
+    return !failed;
+}
+
+bool
+TestRunner_finish(TestRunner *self) {
+    TestFormatter_Summary(self->formatter, self);
+
+    return self->num_batches != 0 && self->num_batches_failed == 0;
+}
+
+uint32_t
+TestRunner_get_num_tests(TestRunner *self) {
+    return self->num_tests;
+}
+
+uint32_t
+TestRunner_get_num_tests_failed(TestRunner *self) {
+    return self->num_tests_failed;
+}
+
+uint32_t
+TestRunner_get_num_batches(TestRunner *self) {
+    return self->num_batches;
+}
+
+uint32_t
+TestRunner_get_num_batches_failed(TestRunner *self) {
+    return self->num_batches_failed;
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Clownfish/Test/TestRunner.cfh
----------------------------------------------------------------------
diff --git a/core/Clownfish/Test/TestRunner.cfh b/core/Clownfish/Test/TestRunner.cfh
new file mode 100644
index 0000000..a5e8721
--- /dev/null
+++ b/core/Clownfish/Test/TestRunner.cfh
@@ -0,0 +1,54 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel Lucy;
+
+class Clownfish::Test::TestRunner inherits Clownfish::Obj {
+    TestFormatter *formatter;
+    uint32_t       num_tests;
+    uint32_t       num_tests_failed;
+    uint32_t       num_batches;
+    uint32_t       num_batches_failed;
+
+    inert incremented TestRunner*
+    new(TestFormatter *formatter);
+
+    inert TestRunner*
+    init(TestRunner *self, TestFormatter *formatter);
+
+    public void
+    Destroy(TestRunner *self);
+
+    bool
+    Run_Batch(TestRunner *self, TestBatch *batch);
+
+    bool
+    Finish(TestRunner *self);
+
+    uint32_t
+    Get_Num_Tests(TestRunner *self);
+
+    uint32_t
+    Get_Num_Tests_Failed(TestRunner *self);
+
+    uint32_t
+    Get_Num_Batches(TestRunner *self);
+
+    uint32_t
+    Get_Num_Batches_Failed(TestRunner *self);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Lucy/Test.c
----------------------------------------------------------------------
diff --git a/core/Lucy/Test.c b/core/Lucy/Test.c
index 75ca9d2..3640989 100644
--- a/core/Lucy/Test.c
+++ b/core/Lucy/Test.c
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#include <math.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -22,6 +23,60 @@
 #include "Lucy/Util/ToolSet.h"
 
 #include "Lucy/Test.h"
+#include "Clownfish/Test/Formatter/TestFormatterTAP.h"
+#include "Clownfish/Test/TestFormatter.h"
+#include "Clownfish/Test/TestRunner.h"
+
+static bool
+S_vtest_true(TestBatch *self, bool condition, const char *pattern,
+             va_list args);
+
+static VArray*
+S_all_test_batches() {
+    VArray *batches = VA_new(0);
+
+    return batches;
+}
+
+bool
+Test_run_batch(CharBuf *class_name, TestFormatter *formatter) {
+    VArray   *batches = S_all_test_batches();
+    uint32_t  size    = VA_Get_Size(batches);
+
+    for (uint32_t i = 0; i < size; ++i) {
+        TestBatch *batch = (TestBatch*)VA_Fetch(batches, i);
+
+        if (CB_Equals(TestBatch_Get_Class_Name(batch), (Obj*)class_name)) {
+            TestRunner *runner  = TestRunner_new(formatter);
+            bool result = TestRunner_Run_Batch(runner, batch);
+            DECREF(runner);
+            DECREF(batches);
+            return result;
+        }
+    }
+
+    DECREF(batches);
+    THROW(ERR, "Couldn't find test class '%o'", class_name);
+    UNREACHABLE_RETURN(bool);
+}
+
+bool
+Test_run_all_batches(TestFormatter *formatter) {
+    TestRunner *runner  = TestRunner_new(formatter);
+    VArray     *batches = S_all_test_batches();
+    uint32_t    size    = VA_Get_Size(batches);
+
+    for (uint32_t i = 0; i < size; ++i) {
+        TestBatch *batch = (TestBatch*)VA_Fetch(batches, i);
+        TestRunner_Run_Batch(runner, batch);
+    }
+
+    bool result = TestRunner_Finish(runner);
+
+    DECREF(runner);
+    DECREF(batches);
+    return result;
+}
 
 TestBatch*
 TestBatch_new(int64_t num_tests) {
@@ -35,6 +90,7 @@ TestBatch_init(TestBatch *self, int64_t num_tests) {
     self->num_tests       = num_tests;
 
     // Initialize.
+    self->formatter       = (TestFormatter*)TestFormatterTAP_new();
     self->test_num        = 0;
     self->num_passed      = 0;
     self->num_failed      = 0;
@@ -50,8 +106,33 @@ TestBatch_init(TestBatch *self, int64_t num_tests) {
 }
 
 void
+TestBatch_destroy(TestBatch *self) {
+    DECREF(self->formatter);
+    SUPER_DESTROY(self, TESTBATCH);
+}
+
+void
 TestBatch_plan(TestBatch *self) {
-    printf("1..%" PRId64 "\n", self->num_tests);
+    TestFormatter_Batch_Prologue(self->formatter, self);
+}
+
+void
+TestBatch_run(TestBatch *self) {
+}
+
+int64_t
+TestBatch_get_num_planned(TestBatch *self) {
+    return self->num_tests;
+}
+
+int64_t
+TestBatch_get_num_tests(TestBatch *self) {
+    return self->test_num;
+}
+
+int64_t
+TestBatch_get_num_failed(TestBatch *self) {
+    return self->num_failed;
 }
 
 bool
@@ -136,159 +217,92 @@ TestBatch_skip(void *vself, const char *pattern, ...) {
 bool
 TestBatch_vtest_true(TestBatch *self, bool condition, const char *pattern,
                      va_list args) {
-    // Increment test number.
-    self->test_num++;
-
-    // Test condition and pass or fail.
-    if (condition) {
-        self->num_passed++;
-        printf("ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return true;
-    }
-    else {
-        self->num_failed++;
-        printf("not ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return false;
-    }
+    return S_vtest_true(self, condition, pattern, args);
 }
 
 bool
 TestBatch_vtest_false(TestBatch *self, bool condition,
                       const char *pattern, va_list args) {
-    // Increment test number.
-    self->test_num++;
-
-    // Test condition and pass or fail.
-    if (!condition) {
-        self->num_passed++;
-        printf("ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return true;
-    }
-    else {
-        self->num_failed++;
-        printf("not ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return false;
-    }
+    return S_vtest_true(self, !condition, pattern, args);
 }
 
 bool
 TestBatch_vtest_int_equals(TestBatch *self, long got, long expected,
                            const char *pattern, va_list args) {
-    // Increment test number.
-    self->test_num++;
-
-    // Test condition and pass or fail.
-    if (expected == got) {
-        self->num_passed++;
-        printf("ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return true;
-    }
-    else {
-        self->num_failed++;
-        printf("not ok %" PRId64 " - Expected '%ld', got '%ld'\n    ",
-               self->test_num, expected, got);
-        vprintf(pattern, args);
-        printf("\n");
-        return false;
+    bool pass = (got == expected);
+    S_vtest_true(self, pass, pattern, args);
+    if (!pass) {
+        TestFormatter_test_comment(self->formatter,
+                                   "Expected '%ld', got '%ld'.\n",
+                                   expected, got);
     }
+    return pass;
 }
 
 bool
 TestBatch_vtest_float_equals(TestBatch *self, double got, double expected,
                              const char *pattern, va_list args) {
-    double diff = expected / got;
-
-    // Increment test number.
-    self->test_num++;
-
-    // Evaluate condition and pass or fail.
-    if (diff > 0.00001) {
-        self->num_passed++;
-        printf("ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return true;
-    }
-    else {
-        self->num_failed++;
-        printf("not ok %" PRId64 " - Expected '%f', got '%f'\n    ",
-               self->test_num, expected, got);
-        vprintf(pattern, args);
-        printf("\n");
-        return false;
+    double relative_error = got / expected - 1.0;
+    bool   pass           = (fabs(relative_error) < 1e-6);
+    S_vtest_true(self, pass, pattern, args);
+    if (!pass) {
+        TestFormatter_test_comment(self->formatter,
+                                   "Expected '%e', got '%e'.\n",
+                                   expected, got);
     }
+    return pass;
 }
 
 bool
 TestBatch_vtest_string_equals(TestBatch *self, const char *got,
                               const char *expected, const char *pattern,
                               va_list args) {
-    // Increment test number.
-    self->test_num++;
-
-    // Test condition and pass or fail.
-    if (strcmp(expected, got) == 0) {
-        self->num_passed++;
-        printf("ok %" PRId64 " - ", self->test_num);
-        vprintf(pattern, args);
-        printf("\n");
-        return true;
-    }
-    else {
-        self->num_failed++;
-        printf("not ok %" PRId64 " - Expected '%s', got '%s'\n    ",
-               self->test_num, expected, got);
-        vprintf(pattern, args);
-        printf("\n");
-        return false;
+    bool pass = (strcmp(got, expected) == 0);
+    S_vtest_true(self, pass, pattern, args);
+    if (!pass) {
+        TestFormatter_test_comment(self->formatter,
+                                   "Expected '%s', got '%s'.\n",
+                                   expected, got);
     }
+    return pass;
 }
 
 bool
 TestBatch_vpass(TestBatch *self, const char *pattern, va_list args) {
-    // Increment test number.
-    self->test_num++;
-
-    // Update counter, indicate pass.
-    self->num_passed++;
-    printf("ok %" PRId64 " - ", self->test_num);
-    vprintf(pattern, args);
-    printf("\n");
-
-    return true;
+    return S_vtest_true(self, true, pattern, args);
 }
 
 bool
 TestBatch_vfail(TestBatch *self, const char *pattern, va_list args) {
-    // Increment test number.
-    self->test_num++;
-
-    // Update counter, indicate failure.
-    self->num_failed++;
-    printf("not ok %" PRId64 " - ", self->test_num);
-    vprintf(pattern, args);
-    printf("\n");
-
-    return false;
+    return S_vtest_true(self, false, pattern, args);
 }
 
 void
 TestBatch_vskip(TestBatch *self, const char *pattern, va_list args) {
     self->test_num++;
-    printf("ok %" PRId64 " # SKIP ", self->test_num);
-    vprintf(pattern, args);
-    printf("\n");
+    // TODO: Add a VTest_Skip method to TestFormatter
+    TestFormatter_VTest_Result(self->formatter, true, self->num_tests,
+                               pattern, args);
     self->num_skipped++;
 }
 
+static bool
+S_vtest_true(TestBatch* self, bool condition, const char *pattern,
+             va_list args) {
+    // Increment test number.
+    self->test_num++;
+
+    if (condition) {
+        self->num_passed++;
+    }
+    else {
+        self->num_failed++;
+    }
+
+    TestFormatter_VTest_Result(self->formatter, condition, self->test_num,
+                               pattern, args);
+
+    return condition;
+}
+
 

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/core/Lucy/Test.cfh
----------------------------------------------------------------------
diff --git a/core/Lucy/Test.cfh b/core/Lucy/Test.cfh
index 8a84888..609d8a0 100644
--- a/core/Lucy/Test.cfh
+++ b/core/Lucy/Test.cfh
@@ -18,14 +18,21 @@ parcel Lucy;
 
 /** Testing framework.
  */
-inert class Lucy::Test { }
+inert class Lucy::Test {
+    inert bool
+    run_batch(CharBuf *class_name, TestFormatter *formatter);
+
+    inert bool
+    run_all_batches(TestFormatter *formatter);
+}
 
 class Lucy::Test::TestBatch inherits Clownfish::Obj {
-    int64_t    test_num;
-    int64_t    num_tests;
-    int64_t    num_passed;
-    int64_t    num_failed;
-    int64_t    num_skipped;
+    TestFormatter *formatter;
+    int64_t        test_num;
+    int64_t        num_tests;
+    int64_t        num_passed;
+    int64_t        num_failed;
+    int64_t        num_skipped;
 
     inert incremented TestBatch*
     new(int64_t num_tests);
@@ -33,9 +40,25 @@ class Lucy::Test::TestBatch inherits Clownfish::Obj {
     inert TestBatch*
     init(TestBatch *self, int64_t num_tests);
 
+    public void
+    Destroy(TestBatch *self);
+
     void
     Plan(TestBatch *self);
 
+    /* Will made be abstract later. */
+    void
+    Run(TestBatch *self);
+
+    int64_t
+    Get_Num_Planned(TestBatch *self);
+
+    int64_t
+    Get_Num_Tests(TestBatch *self);
+
+    int64_t
+    Get_Num_Failed(TestBatch *self);
+
     inert bool
     test_true(void *vself, bool condition, const char *pattern, ...);
 

http://git-wip-us.apache.org/repos/asf/lucy/blob/ce6c0f5e/perl/buildlib/Lucy/Build/Binding/Misc.pm
----------------------------------------------------------------------
diff --git a/perl/buildlib/Lucy/Build/Binding/Misc.pm b/perl/buildlib/Lucy/Build/Binding/Misc.pm
index 6c9d8de..f49768a 100644
--- a/perl/buildlib/Lucy/Build/Binding/Misc.pm
+++ b/perl/buildlib/Lucy/Build/Binding/Misc.pm
@@ -307,7 +307,12 @@ PPCODE:
         lucy_TestHighlighter_run_tests();
     }
     else {
-        THROW(LUCY_ERR, "Unknown test id: %s", package);
+        lucy_CharBuf *class_name = lucy_CB_newf("%s", package);
+        lucy_TestFormatter *formatter
+            = (lucy_TestFormatter*)lucy_TestFormatterTAP_new();
+        lucy_Test_run_batch(class_name, formatter);
+        CFISH_DECREF(class_name);
+        CFISH_DECREF(formatter);
     }
 }
 END_XS_CODE