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 2016/07/11 11:25:39 UTC

[06/14] lucy-clownfish git commit: Move tests to separate directory

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestClass.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestClass.c b/runtime/test/Clownfish/Test/TestClass.c
new file mode 100644
index 0000000..a6646f8
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestClass.c
@@ -0,0 +1,158 @@
+/* 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_CFISH_BOOLEAN
+#define C_CFISH_CLASS
+#define CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "charmony.h"
+
+#include <string.h>
+
+#include "Clownfish/Test/TestClass.h"
+
+#include "Clownfish/Boolean.h"
+#include "Clownfish/Class.h"
+#include "Clownfish/Method.h"
+#include "Clownfish/String.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/Util/Memory.h"
+#include "Clownfish/Vector.h"
+
+TestClass*
+TestClass_new() {
+    return (TestClass*)Class_Make_Obj(TESTCLASS);
+}
+
+#if DEBUG_CLASS_CONTENTS
+
+#include <stdio.h>
+
+static void
+S_memdump(void *vptr, size_t size) {
+    unsigned char *ptr = (unsigned char*)vptr;
+    for (size_t i = 0; i < size; i++) {
+        printf("%02X ", ptr[i]);
+    }
+    printf("\n");
+}
+
+#endif /* DEBUG_CLASS_CONTENTS */
+
+static void
+test_bootstrap_idempotence(TestBatchRunner *runner) {
+    Class    *bool_class        = BOOLEAN;
+    uint32_t  bool_class_size   = BOOLEAN->class_alloc_size;
+    uint32_t  bool_ivars_offset = cfish_Bool_IVARS_OFFSET;
+    Boolean  *true_singleton    = Bool_true_singleton;
+
+    char *bool_class_contents = (char*)MALLOCATE(bool_class_size);
+    memcpy(bool_class_contents, BOOLEAN, bool_class_size);
+
+    // Force another bootstrap run.
+    cfish_bootstrap_internal(1);
+
+#if DEBUG_CLASS_CONTENTS
+    printf("Before\n");
+    S_memdump(bool_class_contents, bool_class_size);
+    printf("After\n");
+    S_memdump(BOOLEAN, bool_class_size);
+#endif
+
+    TEST_TRUE(runner, bool_class == BOOLEAN,
+              "Boolean class pointer unchanged");
+    TEST_TRUE(runner,
+              memcmp(bool_class_contents, BOOLEAN, bool_class_size) == 0,
+              "Boolean class unchanged");
+    TEST_TRUE(runner, bool_ivars_offset == cfish_Bool_IVARS_OFFSET,
+              "Boolean ivars offset unchanged");
+    TEST_TRUE(runner, true_singleton == Bool_true_singleton,
+              "Boolean singleton unchanged");
+
+    FREEMEM(bool_class_contents);
+}
+
+static String*
+MyObj_To_String_IMP(Obj *self) {
+    UNUSED_VAR(self);
+    return Str_newf("delta");
+}
+
+static void
+test_simple_subclass(TestBatchRunner *runner) {
+    String *class_name = SSTR_WRAP_C("Clownfish::Test::MyObj");
+    Class *subclass = Class_singleton(class_name, OBJ);
+
+    TEST_TRUE(runner, Str_Equals(Class_Get_Name(subclass), (Obj*)class_name),
+              "Get_Name");
+    TEST_TRUE(runner, Class_Get_Parent(subclass) == OBJ, "Get_Parent");
+
+    Obj *obj = Class_Make_Obj(subclass);
+    TEST_TRUE(runner, Obj_is_a(obj, subclass), "Make_Obj");
+
+    Class_Override(subclass, (cfish_method_t)MyObj_To_String_IMP,
+                   CFISH_Obj_To_String_OFFSET);
+    String *str = Obj_To_String(obj);
+    TEST_TRUE(runner, Str_Equals_Utf8(str, "delta", 5), "Override");
+    DECREF(str);
+
+    DECREF(obj);
+}
+
+static void
+test_add_alias_to_registry(TestBatchRunner *runner) {
+    static const char alias[] = "Clownfish::Test::ObjAlias";
+    bool added;
+
+    added = Class_add_alias_to_registry(OBJ, alias, sizeof(alias) - 1);
+    TEST_TRUE(runner, added, "add_alias_to_registry returns true");
+    Class *klass = Class_fetch_class(SSTR_WRAP_C(alias));
+    TEST_TRUE(runner, klass == OBJ, "add_alias_to_registry works");
+
+    added = Class_add_alias_to_registry(CLASS, alias, sizeof(alias) - 1);
+    TEST_FALSE(runner, added, "add_alias_to_registry returns false");
+}
+
+static void
+test_Get_Methods(TestBatchRunner *runner) {
+    Vector *methods = Class_Get_Methods(OBJ);
+    Method *destroy = NULL;
+
+    for (size_t i = 0, size = Vec_Get_Size(methods); i < size; i++) {
+        Method *method = (Method*)Vec_Fetch(methods, i);
+
+        if (Str_Equals_Utf8(Method_Get_Name(method), "Destroy", 7)) {
+            destroy = method;
+        }
+    }
+
+    TEST_TRUE(runner, destroy != NULL, "Destroy method found");
+
+    DECREF(methods);
+}
+
+void
+TestClass_Run_IMP(TestClass *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 12);
+    test_bootstrap_idempotence(runner);
+    test_simple_subclass(runner);
+    test_add_alias_to_registry(runner);
+    test_Get_Methods(runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestClass.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestClass.cfh b/runtime/test/Clownfish/Test/TestClass.cfh
new file mode 100644
index 0000000..615c34e
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestClass.cfh
@@ -0,0 +1,28 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestClass
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestClass*
+    new();
+
+    void
+    Run(TestClass *self, TestBatchRunner *runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestErr.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestErr.c b/runtime/test/Clownfish/Test/TestErr.c
new file mode 100644
index 0000000..46ecb1e
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestErr.c
@@ -0,0 +1,193 @@
+/* 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 CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestErr.h"
+
+#include "Clownfish/String.h"
+#include "Clownfish/Err.h"
+#include "Clownfish/Test.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Clownfish/Class.h"
+
+TestErr*
+TestErr_new() {
+    return (TestErr*)Class_Make_Obj(TESTERR);
+}
+
+static void
+test_To_String(TestBatchRunner *runner) {
+    String *message = Str_newf("oops");
+    Err *error = Err_new(message);
+    String *string = Err_To_String(error);
+    TEST_TRUE(runner, Str_Equals(message, (Obj*)string),
+              "Stringifies as message");
+    DECREF(string);
+    DECREF(error);
+}
+
+static void
+test_Cat_Mess(TestBatchRunner *runner) {
+    Err *error = Err_new(Str_newf("alpha"));
+    Err_Cat_Mess(error, SSTR_WRAP_C("\nbeta"));
+    String *mess = Err_Get_Mess(error);
+    TEST_TRUE(runner, Str_Equals_Utf8(mess, "alpha\nbeta", 10), "Cat_Mess");
+    DECREF(error);
+}
+
+static void
+test_Add_Frame(TestBatchRunner *runner) {
+    {
+        Err *error = Err_new(Str_newf("alpha"));
+        Err_Add_Frame(error, "source.c", 128, "function");
+        String *mess = Err_Get_Mess(error);
+        const char *expected = "alpha\n\tfunction at source.c line 128\n";
+        TEST_TRUE(runner, Str_Equals_Utf8(mess, expected, strlen(expected)),
+                  "Add_Frame");
+        DECREF(error);
+    }
+
+    {
+        Err *error = Err_new(Str_newf("alpha\n"));
+        Err_Add_Frame(error, "source.c", 128, "function");
+        String *mess = Err_Get_Mess(error);
+        const char *expected = "alpha\n\tfunction at source.c line 128\n";
+        TEST_TRUE(runner, Str_Equals_Utf8(mess, expected, strlen(expected)),
+                  "Add_Frame with trailing newline");
+        DECREF(error);
+    }
+
+    {
+        Err *error = Err_new(Str_newf("alpha"));
+        Err_Add_Frame(error, "source.c", 128, NULL);
+        String *mess = Err_Get_Mess(error);
+        const char *expected = "alpha\n\tat source.c line 128\n";
+        TEST_TRUE(runner, Str_Equals_Utf8(mess, expected, strlen(expected)),
+                  "Add_Frame without func");
+        DECREF(error);
+    }
+}
+
+static void
+S_rethrow(void *context) {
+    Err *error = (Err*)context;
+    Err_rethrow(error, "src.c", 12, "fn");
+}
+
+static void
+test_rethrow(TestBatchRunner *runner) {
+    Err *error = Err_new(Str_newf("error"));
+    Err *rethrown = Err_trap(S_rethrow, error);
+    String *mess = Err_Get_Mess(rethrown);
+    const char *expected = "error\n\tfn at src.c line 12\n";
+    TEST_TRUE(runner, Str_Starts_With_Utf8(mess, expected, strlen(expected)),
+              "rethrow");
+    DECREF(error);
+}
+
+static void
+S_invalid_downcast(void *context) {
+    Obj *obj = (Obj*)context;
+    DOWNCAST(obj, ERR);
+}
+
+static void
+test_downcast(TestBatchRunner *runner) {
+    Obj *obj = (Obj*)Str_newf("gamma");
+
+    TEST_TRUE(runner, DOWNCAST(obj, STRING) != NULL, "downcast");
+
+    TEST_TRUE(runner, DOWNCAST(NULL, STRING) == NULL, "downcast NULL");
+
+    Err *error = Err_trap(S_invalid_downcast, obj);
+    TEST_TRUE(runner, error != NULL, "downcast throws");
+    DECREF(error);
+
+    DECREF(obj);
+}
+
+static void
+S_invalid_certify(void *context) {
+    Obj *obj = (Obj*)context;
+    CERTIFY(obj, ERR);
+}
+
+static void
+test_certify(TestBatchRunner *runner) {
+    Obj *obj = (Obj*)Str_newf("epsilon");
+    Err *error;
+
+    TEST_TRUE(runner, CERTIFY(obj, STRING) != NULL, "certify");
+
+    error = Err_trap(S_invalid_certify, NULL);
+    TEST_TRUE(runner, error != NULL, "certify NULL");
+    DECREF(error);
+
+    error = Err_trap(S_invalid_certify, obj);
+    TEST_TRUE(runner, error != NULL, "certify throws");
+    DECREF(error);
+
+    DECREF(obj);
+}
+
+static void
+S_err_thread(void *arg) {
+    TestBatchRunner *runner = (TestBatchRunner*)arg;
+
+    TEST_TRUE(runner, Err_get_error() == NULL,
+              "global error in thread initialized to null");
+
+    Err_set_error(Err_new(Str_newf("thread")));
+    String *mess = Err_Get_Mess(Err_get_error());
+    TEST_TRUE(runner, Str_Equals_Utf8(mess, "thread", 6),
+              "set_error in thread works");
+}
+
+static void
+test_threads(TestBatchRunner *runner) {
+    if (!TestUtils_has_threads) {
+        SKIP(runner, 3, "no thread support");
+        return;
+    }
+
+    Err_set_error(Err_new(Str_newf("main")));
+
+    void *runtime = TestUtils_clone_host_runtime();
+    Thread *thread = TestUtils_thread_create(S_err_thread, runner, runtime);
+    TestUtils_thread_join(thread);
+    TestUtils_destroy_host_runtime(runtime);
+
+    String *mess = Err_Get_Mess(Err_get_error());
+    TEST_TRUE(runner, Str_Equals_Utf8(mess, "main", 4),
+              "thread doesn't clobber global error");
+}
+
+void
+TestErr_Run_IMP(TestErr *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 15);
+    test_To_String(runner);
+    test_Cat_Mess(runner);
+    test_Add_Frame(runner);
+    test_rethrow(runner);
+    test_downcast(runner);
+    test_certify(runner);
+    test_threads(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestErr.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestErr.cfh b/runtime/test/Clownfish/Test/TestErr.cfh
new file mode 100644
index 0000000..178017c
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestErr.cfh
@@ -0,0 +1,28 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestErr
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestErr*
+    new();
+
+    void
+    Run(TestErr *self, TestBatchRunner *runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestHash.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestHash.c b/runtime/test/Clownfish/Test/TestHash.c
new file mode 100644
index 0000000..b00d891
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestHash.c
@@ -0,0 +1,325 @@
+/* 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 <stdlib.h>
+#include <time.h>
+
+#define CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+#define C_CFISH_HASH
+
+#include "Clownfish/Test/TestHash.h"
+
+#include "Clownfish/String.h"
+#include "Clownfish/Boolean.h"
+#include "Clownfish/Hash.h"
+#include "Clownfish/Test.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Clownfish/Vector.h"
+#include "Clownfish/Class.h"
+
+TestHash*
+TestHash_new() {
+    return (TestHash*)Class_Make_Obj(TESTHASH);
+}
+
+static void
+test_Equals(TestBatchRunner *runner) {
+    Hash *hash  = Hash_new(0);
+    Hash *other = Hash_new(0);
+    String *stuff = SSTR_WRAP_C("stuff");
+
+    TEST_TRUE(runner, Hash_Equals(hash, (Obj*)other),
+              "Empty hashes are equal");
+
+    Hash_Store_Utf8(hash, "foo", 3, (Obj*)CFISH_TRUE);
+    TEST_FALSE(runner, Hash_Equals(hash, (Obj*)other),
+               "Add one pair and Equals returns false");
+
+    Hash_Store_Utf8(other, "foo", 3, (Obj*)CFISH_TRUE);
+    TEST_TRUE(runner, Hash_Equals(hash, (Obj*)other),
+              "Add a matching pair and Equals returns true");
+
+    Hash_Store_Utf8(other, "foo", 3, INCREF(stuff));
+    TEST_FALSE(runner, Hash_Equals(hash, (Obj*)other),
+               "Non-matching value spoils Equals");
+
+    DECREF(hash);
+    DECREF(other);
+}
+
+static void
+test_Store_and_Fetch(TestBatchRunner *runner) {
+    Hash          *hash         = Hash_new(100);
+    Hash          *dupe         = Hash_new(100);
+    const size_t   starting_cap = Hash_Get_Capacity(hash);
+    Vector        *expected     = Vec_new(100);
+    Vector        *got          = Vec_new(100);
+    String        *twenty       = SSTR_WRAP_C("20");
+    String        *forty        = SSTR_WRAP_C("40");
+    String        *foo          = SSTR_WRAP_C("foo");
+
+    for (int32_t i = 0; i < 100; i++) {
+        String *str = Str_newf("%i32", i);
+        Hash_Store(hash, str, (Obj*)str);
+        Hash_Store(dupe, str, INCREF(str));
+        Vec_Push(expected, INCREF(str));
+    }
+    TEST_TRUE(runner, Hash_Equals(hash, (Obj*)dupe), "Equals");
+
+    TEST_UINT_EQ(runner, Hash_Get_Capacity(hash), starting_cap,
+                 "Initial capacity sufficient (no rebuilds)");
+
+    for (size_t i = 0; i < 100; i++) {
+        String *key  = (String*)Vec_Fetch(expected, i);
+        Obj    *elem = Hash_Fetch(hash, key);
+        Vec_Push(got, (Obj*)INCREF(elem));
+    }
+
+    TEST_TRUE(runner, Vec_Equals(got, (Obj*)expected),
+              "basic Store and Fetch");
+    TEST_UINT_EQ(runner, Hash_Get_Size(hash), 100,
+                 "size incremented properly by Hash_Store");
+
+    TEST_TRUE(runner, Hash_Fetch(hash, foo) == NULL,
+              "Fetch against non-existent key returns NULL");
+
+    String *twelve = (String*)Hash_Fetch_Utf8(hash, "12", 2);
+    TEST_TRUE(runner, Str_Equals_Utf8(twelve, "12", 2), "Fetch_Utf8");
+
+    Obj *stored_foo = INCREF(foo);
+    Hash_Store(hash, forty, stored_foo);
+    TEST_TRUE(runner, Str_Equals(foo, Hash_Fetch(hash, forty)),
+              "Hash_Store replaces existing value");
+    TEST_FALSE(runner, Hash_Equals(hash, (Obj*)dupe),
+               "replacement value spoils equals");
+    TEST_UINT_EQ(runner, Hash_Get_Size(hash), 100,
+                 "size unaffected after value replaced");
+
+    TEST_TRUE(runner, Hash_Delete(hash, forty) == stored_foo,
+              "Delete returns value");
+    DECREF(stored_foo);
+    TEST_UINT_EQ(runner, Hash_Get_Size(hash), 99,
+                 "size decremented by successful Delete");
+    TEST_TRUE(runner, Hash_Delete(hash, forty) == NULL,
+              "Delete returns NULL when key not found");
+    TEST_UINT_EQ(runner, Hash_Get_Size(hash), 99,
+                 "size not decremented by unsuccessful Delete");
+    DECREF(Hash_Delete(dupe, forty));
+    TEST_TRUE(runner, Vec_Equals(got, (Obj*)expected), "Equals after Delete");
+
+    Obj *forty_one = Hash_Delete_Utf8(hash, "41", 2);
+    TEST_TRUE(runner, forty_one != NULL, "Delete_Utf8");
+    TEST_UINT_EQ(runner, Hash_Get_Size(hash), 98,
+                 "Delete_Utf8 decrements size");
+    DECREF(forty_one);
+
+    Hash_Clear(hash);
+    TEST_TRUE(runner, Hash_Fetch(hash, twenty) == NULL, "Clear");
+    TEST_TRUE(runner, Hash_Get_Size(hash) == 0, "size is 0 after Clear");
+
+    Hash_Clear(hash);
+    Hash_Store(hash, forty, NULL);
+    TEST_TRUE(runner, Hash_Fetch(hash, forty) == NULL, "Store NULL");
+    TEST_TRUE(runner, Hash_Get_Size(hash) == 1, "Size after Store NULL");
+    TEST_TRUE(runner, Hash_Delete(hash, forty) == NULL, "Delete NULL value");
+    TEST_TRUE(runner, Hash_Get_Size(hash) == 0,
+              "Size after Deleting NULL val");
+
+    DECREF(hash);
+    DECREF(dupe);
+    DECREF(got);
+    DECREF(expected);
+}
+
+static void
+test_Keys_Values(TestBatchRunner *runner) {
+    Hash     *hash     = Hash_new(0); // trigger multiple rebuilds.
+    Vector   *expected = Vec_new(100);
+    Vector   *keys;
+    Vector   *values;
+
+    for (uint32_t i = 0; i < 500; i++) {
+        String *str = Str_newf("%u32", i);
+        Hash_Store(hash, str, (Obj*)str);
+        Vec_Push(expected, INCREF(str));
+    }
+
+    Vec_Sort(expected);
+
+    keys   = Hash_Keys(hash);
+    values = Hash_Values(hash);
+    Vec_Sort(keys);
+    Vec_Sort(values);
+    TEST_TRUE(runner, Vec_Equals(keys, (Obj*)expected), "Keys");
+    TEST_TRUE(runner, Vec_Equals(values, (Obj*)expected), "Values");
+    Vec_Clear(keys);
+    Vec_Clear(values);
+
+    {
+        String *forty = SSTR_WRAP_C("40");
+        String *nope  = SSTR_WRAP_C("nope");
+        TEST_TRUE(runner, Hash_Has_Key(hash, forty), "Has_Key");
+        TEST_FALSE(runner, Hash_Has_Key(hash, nope),
+                   "Has_Key returns false for non-existent key");
+    }
+
+    DECREF(hash);
+    DECREF(expected);
+    DECREF(keys);
+    DECREF(values);
+}
+
+static void
+test_stress(TestBatchRunner *runner) {
+    Hash     *hash     = Hash_new(0); // trigger multiple rebuilds.
+    Vector   *expected = Vec_new(1000);
+    Vector   *keys;
+    Vector   *values;
+
+    for (uint32_t i = 0; i < 1000; i++) {
+        String *str = TestUtils_random_string((size_t)(rand() % 1200));
+        while (Hash_Fetch(hash, str)) {
+            DECREF(str);
+            str = TestUtils_random_string((size_t)(rand() % 1200));
+        }
+        Hash_Store(hash, str, (Obj*)str);
+        Vec_Push(expected, INCREF(str));
+    }
+
+    Vec_Sort(expected);
+
+    // Overwrite for good measure.
+    for (uint32_t i = 0; i < 1000; i++) {
+        String *str = (String*)Vec_Fetch(expected, i);
+        Hash_Store(hash, str, INCREF(str));
+    }
+
+    keys   = Hash_Keys(hash);
+    values = Hash_Values(hash);
+    Vec_Sort(keys);
+    Vec_Sort(values);
+    TEST_TRUE(runner, Vec_Equals(keys, (Obj*)expected), "stress Keys");
+    TEST_TRUE(runner, Vec_Equals(values, (Obj*)expected), "stress Values");
+
+    DECREF(keys);
+    DECREF(values);
+    DECREF(expected);
+    DECREF(hash);
+}
+
+static void
+test_collision(TestBatchRunner *runner) {
+    Hash   *hash = Hash_new(0);
+    String *one  = Str_newf("A");
+    String *two  = Str_newf("P{2}|=~-ULE/d");
+
+    TEST_TRUE(runner, Str_Hash_Sum(one) == Str_Hash_Sum(two),
+              "Keys have the same hash sum");
+
+    Hash_Store(hash, one, INCREF(one));
+    Hash_Store(hash, two, INCREF(two));
+    String *elem = (String*)Hash_Fetch(hash, two);
+    TEST_TRUE(runner, elem == two, "Fetch works with collisions");
+
+    DECREF(one);
+    DECREF(two);
+    DECREF(hash);
+}
+
+static void
+test_store_skips_tombstone(TestBatchRunner *runner) {
+    Hash *hash = Hash_new(0);
+    size_t mask = Hash_Get_Capacity(hash) - 1;
+
+    String *one = Str_newf("one");
+    size_t slot = Str_Hash_Sum(one) & mask;
+
+    // Find a colliding key.
+    String *two = NULL;
+    for (int i = 0; i < 100000; i++) {
+        two = Str_newf("%i32", i);
+        if (slot == (Str_Hash_Sum(two) & mask)) {
+            break;
+        }
+        DECREF(two);
+        two = NULL;
+    }
+
+    Hash_Store(hash, one, (Obj*)CFISH_TRUE);
+    Hash_Store(hash, two, (Obj*)CFISH_TRUE);
+    Hash_Delete(hash, one);
+    Hash_Store(hash, two, (Obj*)CFISH_TRUE);
+
+    TEST_UINT_EQ(runner, Hash_Get_Size(hash), 1, "Store skips tombstone");
+
+    DECREF(one);
+    DECREF(two);
+    DECREF(hash);
+}
+
+static void
+test_threshold_accounting(TestBatchRunner *runner) {
+    Hash   *hash = Hash_new(20);
+    String *key  = Str_newf("key");
+
+    size_t threshold = hash->threshold;
+    Hash_Store(hash, key, (Obj*)CFISH_TRUE);
+    Hash_Delete(hash, key);
+    TEST_UINT_EQ(runner, hash->threshold, threshold - 1,
+                 "Tombstone creation decreases threshold");
+
+    Hash_Store(hash, key, (Obj*)CFISH_TRUE);
+    TEST_UINT_EQ(runner, hash->threshold, threshold,
+                 "Tombstone destruction increases threshold");
+
+    DECREF(key);
+    DECREF(hash);
+}
+
+static void
+test_tombstone_identification(TestBatchRunner *runner) {
+    Hash   *hash = Hash_new(20);
+    String *key  = Str_newf("P{2}|=~-U@!y>");
+
+    // Tombstones have a zero hash_sum.
+    TEST_UINT_EQ(runner, Str_Hash_Sum(key), 0, "Key has zero hash sum");
+
+    Hash_Store(hash, key, (Obj*)CFISH_TRUE);
+    Hash_Delete(hash, key);
+    TEST_TRUE(runner, Hash_Fetch(hash, key) == NULL,
+              "Key with zero hash sum isn't mistaken for tombstone");
+
+    DECREF(key);
+    DECREF(hash);
+}
+
+void
+TestHash_Run_IMP(TestHash *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 39);
+    srand((unsigned int)time((time_t*)NULL));
+    test_Equals(runner);
+    test_Store_and_Fetch(runner);
+    test_Keys_Values(runner);
+    test_stress(runner);
+    test_collision(runner);
+    test_store_skips_tombstone(runner);
+    test_threshold_accounting(runner);
+    test_tombstone_identification(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestHash.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestHash.cfh b/runtime/test/Clownfish/Test/TestHash.cfh
new file mode 100644
index 0000000..a730105
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestHash.cfh
@@ -0,0 +1,29 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestHash
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestHash*
+    new();
+
+    void
+    Run(TestHash *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestHashIterator.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestHashIterator.c b/runtime/test/Clownfish/Test/TestHashIterator.c
new file mode 100644
index 0000000..fac7d69
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestHashIterator.c
@@ -0,0 +1,253 @@
+/* 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 <stdlib.h>
+#include <time.h>
+
+#define CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestHashIterator.h"
+
+#include "Clownfish/Err.h"
+#include "Clownfish/String.h"
+#include "Clownfish/Hash.h"
+#include "Clownfish/HashIterator.h"
+#include "Clownfish/Test.h"
+#include "Clownfish/Vector.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Clownfish/Class.h"
+
+TestHashIterator*
+TestHashIterator_new() {
+    return (TestHashIterator*)Class_Make_Obj(TESTHASHITERATOR);
+}
+
+static void
+test_Next(TestBatchRunner *runner) {
+    Hash     *hash     = Hash_new(0); // trigger multiple rebuilds.
+    Vector   *expected = Vec_new(100);
+    Vector   *keys     = Vec_new(500);
+    Vector   *values   = Vec_new(500);
+
+    for (uint32_t i = 0; i < 500; i++) {
+        String *str = Str_newf("%u32", i);
+        Hash_Store(hash, str, (Obj*)str);
+        Vec_Push(expected, INCREF(str));
+    }
+
+    Vec_Sort(expected);
+
+    {
+        HashIterator *iter = HashIter_new(hash);
+        while (HashIter_Next(iter)) {
+            String *key = HashIter_Get_Key(iter);
+            Obj *value = HashIter_Get_Value(iter);
+            Vec_Push(keys, INCREF(key));
+            Vec_Push(values, INCREF(value));
+        }
+        TEST_TRUE(runner, !HashIter_Next(iter),
+                  "Next continues to return false after iteration finishes.");
+
+        DECREF(iter);
+    }
+
+    Vec_Sort(keys);
+    Vec_Sort(values);
+    TEST_TRUE(runner, Vec_Equals(keys, (Obj*)expected), "Keys from Iter");
+    TEST_TRUE(runner, Vec_Equals(values, (Obj*)expected), "Values from Iter");
+
+    DECREF(hash);
+    DECREF(expected);
+    DECREF(keys);
+    DECREF(values);
+}
+
+static void
+S_invoke_Next(void *context) {
+    HashIterator *iter = (HashIterator*)context;
+    HashIter_Next(iter);
+}
+
+static void
+S_invoke_Get_Key(void *context) {
+    HashIterator *iter = (HashIterator*)context;
+    HashIter_Get_Key(iter);
+}
+
+static void
+S_invoke_Get_Value(void *context) {
+    HashIterator *iter = (HashIterator*)context;
+    HashIter_Get_Value(iter);
+}
+
+static void
+test_empty(TestBatchRunner *runner) {
+    Hash         *hash = Hash_new(0);
+    HashIterator *iter = HashIter_new(hash);
+
+    TEST_TRUE(runner, !HashIter_Next(iter),
+              "First call to next false on empty hash iteration");
+
+    Err *get_key_error = Err_trap(S_invoke_Get_Key, iter);
+    TEST_TRUE(runner, get_key_error != NULL,
+              "Get_Key throws exception on empty hash.");
+    DECREF(get_key_error);
+
+    Err *get_value_error = Err_trap(S_invoke_Get_Value, iter);
+    TEST_TRUE(runner, get_value_error != NULL,
+              "Get_Value throws exception on empty hash.");
+    DECREF(get_value_error);
+
+    DECREF(hash);
+    DECREF(iter);
+}
+
+static void
+test_Get_Key_and_Get_Value(TestBatchRunner *runner) {
+    Hash   *hash = Hash_new(0);
+    String *str  = Str_newf("foo");
+    Hash_Store(hash, str, (Obj*)str);
+    bool ok;
+
+    HashIterator *iter = HashIter_new(hash);
+    DECREF(hash);
+
+    Err *get_key_error = Err_trap(S_invoke_Get_Key, iter);
+    TEST_TRUE(runner, get_key_error != NULL,
+              "Get_Key throws exception before first call to Next.");
+    ok = Str_Contains_Utf8(Err_Get_Mess(get_key_error), "before", 6);
+    TEST_TRUE(runner, ok, "Get_Key before Next throws correct message");
+    DECREF(get_key_error);
+
+    Err *get_value_error = Err_trap(S_invoke_Get_Value, iter);
+    TEST_TRUE(runner, get_value_error != NULL,
+              "Get_Value throws exception before first call to Next.");
+    ok = Str_Contains_Utf8(Err_Get_Mess(get_value_error), "before", 6);
+    TEST_TRUE(runner, ok, "Get_Value before Next throws correct message");
+    DECREF(get_value_error);
+
+    HashIter_Next(iter);
+    TEST_TRUE(runner, HashIter_Get_Key(iter) != NULL,
+              "Get_Key during iteration.");
+    TEST_TRUE(runner, HashIter_Get_Value(iter) != NULL,
+              "Get_Value during iteration.");
+
+    HashIter_Next(iter);
+    get_key_error = Err_trap(S_invoke_Get_Key, iter);
+    TEST_TRUE(runner, get_key_error != NULL,
+              "Get_Key throws exception after end of iteration.");
+    ok = Str_Contains_Utf8(Err_Get_Mess(get_key_error), "after", 5);
+    TEST_TRUE(runner, ok, "Get_Key after end throws correct message");
+    DECREF(get_key_error);
+
+    get_value_error = Err_trap(S_invoke_Get_Value, iter);
+    TEST_TRUE(runner, get_value_error != NULL,
+              "Get_Value throws exception after end of iteration.");
+    ok = Str_Contains_Utf8(Err_Get_Mess(get_value_error), "after", 5);
+    TEST_TRUE(runner, ok, "Get_Value after end throws correct message");
+    DECREF(get_value_error);
+
+
+    DECREF(iter);
+}
+
+static void
+test_illegal_modification(TestBatchRunner *runner) {
+    Hash *hash = Hash_new(0);
+
+    for (uint32_t i = 0; i < 3; i++) {
+        String *str = Str_newf("%u32", i);
+        Hash_Store(hash, str, (Obj*)str);
+    }
+
+    HashIterator *iter = HashIter_new(hash);
+    HashIter_Next(iter);
+
+    for (uint32_t i = 0; i < 100; i++) {
+        String *str = Str_newf("foo %u32", i);
+        Hash_Store(hash, str, (Obj*)str);
+    }
+
+    Err *next_error = Err_trap(S_invoke_Next, iter);
+    TEST_TRUE(runner, next_error != NULL,
+              "Next on resized hash throws exception.");
+    DECREF(next_error);
+
+    Err *get_key_error = Err_trap(S_invoke_Get_Key, iter);
+    TEST_TRUE(runner, get_key_error != NULL,
+              "Get_Key on resized hash throws exception.");
+    DECREF(get_key_error);
+
+    Err *get_value_error = Err_trap(S_invoke_Get_Value, iter);
+    TEST_TRUE(runner, get_value_error != NULL,
+              "Get_Value on resized hash throws exception.");
+    DECREF(get_value_error);
+
+    DECREF(hash);
+    DECREF(iter);
+}
+
+static void
+test_tombstone(TestBatchRunner *runner) {
+    {
+        Hash   *hash = Hash_new(0);
+        String *str  = Str_newf("foo");
+        Hash_Store(hash, str, INCREF(str));
+        DECREF(Hash_Delete(hash, str));
+        DECREF(str);
+
+        HashIterator *iter = HashIter_new(hash);
+        TEST_TRUE(runner, !HashIter_Next(iter), "Next advances past tombstones.");
+
+        DECREF(iter);
+        DECREF(hash);
+    }
+
+    {
+        Hash   *hash = Hash_new(0);
+        String *str  = Str_newf("foo");
+        Hash_Store(hash, str, INCREF(str));
+
+        HashIterator *iter = HashIter_new(hash);
+        HashIter_Next(iter);
+        DECREF(Hash_Delete(hash, str));
+
+
+        Err *get_key_error = Err_trap(S_invoke_Get_Key, iter);
+        TEST_TRUE(runner, get_key_error != NULL,
+                  "Get_Key doesn't return tombstone and throws error.");
+        DECREF(get_key_error);
+
+        DECREF(str);
+        DECREF(iter);
+        DECREF(hash);
+    }
+}
+
+void
+TestHashIterator_Run_IMP(TestHashIterator *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 21);
+    srand((unsigned int)time((time_t*)NULL));
+    test_Next(runner);
+    test_empty(runner);
+    test_Get_Key_and_Get_Value(runner);
+    test_illegal_modification(runner);
+    test_tombstone(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestHashIterator.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestHashIterator.cfh b/runtime/test/Clownfish/Test/TestHashIterator.cfh
new file mode 100644
index 0000000..4765f76
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestHashIterator.cfh
@@ -0,0 +1,29 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestHashIterator
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestHashIterator*
+    new();
+
+    void
+    Run(TestHashIterator *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestHost.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestHost.c b/runtime/test/Clownfish/Test/TestHost.c
new file mode 100644
index 0000000..6da0efa
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestHost.c
@@ -0,0 +1,125 @@
+/* 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 CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestHost.h"
+#include "Clownfish/Class.h"
+#include "Clownfish/String.h"
+
+TestHost*
+TestHost_new() {
+    return (TestHost*)Class_Make_Obj(TESTHOST);
+}
+
+Obj*
+TestHost_Test_Obj_Pos_Arg_IMP(TestHost *self, Obj *arg) {
+    UNUSED_VAR(self);
+    return arg;
+}
+
+Obj*
+TestHost_Test_Obj_Pos_Arg_Def_IMP(TestHost *self, Obj *arg) {
+    UNUSED_VAR(self);
+    return arg;
+}
+
+Obj*
+TestHost_Test_Obj_Label_Arg_IMP(TestHost *self, Obj *arg, bool unused) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(unused);
+    return arg;
+}
+
+Obj*
+TestHost_Test_Obj_Label_Arg_Def_IMP(TestHost *self, Obj *arg, bool unused) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(unused);
+    return arg;
+}
+
+int32_t
+TestHost_Test_Int32_Pos_Arg_IMP(TestHost *self, int32_t arg) {
+    UNUSED_VAR(self);
+    return arg;
+}
+
+int32_t
+TestHost_Test_Int32_Pos_Arg_Def_IMP(TestHost *self, int32_t arg) {
+    UNUSED_VAR(self);
+    return arg;
+}
+
+int32_t
+TestHost_Test_Int32_Label_Arg_IMP(TestHost *self, int32_t arg, bool unused) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(unused);
+    return arg;
+}
+
+int32_t
+TestHost_Test_Int32_Label_Arg_Def_IMP(TestHost *self, int32_t arg,
+                                      bool unused) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(unused);
+    return arg;
+}
+
+bool
+TestHost_Test_Bool_Pos_Arg_IMP(TestHost *self, bool arg) {
+    UNUSED_VAR(self);
+    return arg;
+}
+
+bool
+TestHost_Test_Bool_Pos_Arg_Def_IMP(TestHost *self, bool arg) {
+    UNUSED_VAR(self);
+    return arg;
+}
+
+bool
+TestHost_Test_Bool_Label_Arg_IMP(TestHost *self, bool arg, bool unused) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(unused);
+    return arg;
+}
+
+bool
+TestHost_Test_Bool_Label_Arg_Def_IMP(TestHost *self, bool arg, bool unused) {
+    UNUSED_VAR(self);
+    UNUSED_VAR(unused);
+    return arg;
+}
+
+void
+TestHost_Invoke_Invalid_Callback_From_C_IMP(TestHost *self) {
+    TestHost_Invalid_Callback(self);
+}
+
+String*
+TestHost_Aliased_IMP(TestHost *self) {
+    UNUSED_VAR(self);
+    return Str_newf("C");
+}
+
+String*
+TestHost_Invoke_Aliased_From_C_IMP(TestHost *self) {
+    UNUSED_VAR(self);
+    return TestHost_Aliased(self);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestHost.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestHost.cfh b/runtime/test/Clownfish/Test/TestHost.cfh
new file mode 100644
index 0000000..92bc272
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestHost.cfh
@@ -0,0 +1,81 @@
+/* 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 TestClownfish;
+
+/** Clownfish test suite.
+ */
+class Clownfish::Test::TestHost {
+    inert incremented TestHost*
+    new();
+
+    Obj*
+    Test_Obj_Pos_Arg(TestHost *self, Obj *arg);
+
+    Obj*
+    Test_Obj_Pos_Arg_Def(TestHost *self, nullable Obj *arg = NULL);
+
+    Obj*
+    Test_Obj_Label_Arg(TestHost *self, Obj *arg, bool unused = false);
+
+    Obj*
+    Test_Obj_Label_Arg_Def(TestHost *self, nullable Obj *arg = NULL,
+                           bool unused = false);
+
+    int32_t
+    Test_Int32_Pos_Arg(TestHost *self, int32_t arg);
+
+    int32_t
+    Test_Int32_Pos_Arg_Def(TestHost *self, int32_t arg = 101);
+
+    int32_t
+    Test_Int32_Label_Arg(TestHost *self, int32_t arg, bool unused = false);
+
+    int32_t
+    Test_Int32_Label_Arg_Def(TestHost *self, int32_t arg = 101,
+                             bool unused = false);
+
+    bool
+    Test_Bool_Pos_Arg(TestHost *self, bool arg);
+
+    bool
+    Test_Bool_Pos_Arg_Def(TestHost *self, bool arg = true);
+
+    bool
+    Test_Bool_Label_Arg(TestHost *self, bool arg, bool unused = false);
+
+    bool
+    Test_Bool_Label_Arg_Def(TestHost *self, bool arg = true,
+                            bool unused = false);
+
+    /** A method that can't be overridden from the host language.
+     */
+    abstract void*
+    Invalid_Callback(TestHost *self);
+
+    void
+    Invoke_Invalid_Callback_From_C(TestHost *self);
+
+    /** A method with a custom host language alias.
+     */
+    incremented String*
+    Aliased(TestHost *self);
+
+    incremented String*
+    Invoke_Aliased_From_C(TestHost* self);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestLockFreeRegistry.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestLockFreeRegistry.c b/runtime/test/Clownfish/Test/TestLockFreeRegistry.c
new file mode 100644
index 0000000..b70ff1f
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestLockFreeRegistry.c
@@ -0,0 +1,168 @@
+/* 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 <stdlib.h>
+#include <string.h>
+
+#define CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestLockFreeRegistry.h"
+
+#include "Clownfish/Class.h"
+#include "Clownfish/LockFreeRegistry.h"
+#include "Clownfish/String.h"
+#include "Clownfish/Test.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Clownfish/Util/Memory.h"
+
+#define NUM_THREADS 5
+
+typedef struct ThreadArgs {
+    LockFreeRegistry *registry;
+    uint32_t         *nums;
+    uint32_t          num_objs;
+    uint64_t          target_time;
+    uint32_t          succeeded;
+} ThreadArgs;
+
+TestLockFreeRegistry*
+TestLFReg_new() {
+    return (TestLockFreeRegistry*)Class_Make_Obj(TESTLOCKFREEREGISTRY);
+}
+
+static void
+test_all(TestBatchRunner *runner) {
+    LockFreeRegistry *registry = LFReg_new(1);
+    String *foo = Str_newf("foo");
+    String *bar = Str_newf("bar");
+    String *baz = Str_newf("baz");
+    String *foo_dupe = Str_newf("foo");
+
+    TEST_TRUE(runner, LFReg_register(registry, foo, (Obj*)foo),
+              "Register() returns true on success");
+    TEST_FALSE(runner,
+               LFReg_register(registry, foo_dupe, (Obj*)foo_dupe),
+               "Can't Register() keys that test equal");
+
+    TEST_TRUE(runner, LFReg_register(registry, bar, (Obj*)bar),
+              "Register() key with the same Hash_Sum but that isn't Equal");
+
+    TEST_TRUE(runner, LFReg_fetch(registry, foo_dupe) == (Obj*)foo,
+              "Fetch()");
+    TEST_TRUE(runner, LFReg_fetch(registry, bar) == (Obj*)bar,
+              "Fetch() again");
+    TEST_TRUE(runner, LFReg_fetch(registry, baz) == NULL,
+              "Fetch() non-existent key returns NULL");
+
+    DECREF(foo_dupe);
+    DECREF(baz);
+    DECREF(bar);
+    DECREF(foo);
+    LFReg_destroy(registry);
+}
+
+static void
+S_register_many(void *varg) {
+    ThreadArgs *args = (ThreadArgs*)varg;
+
+    // Encourage contention, so that all threads try to register at the same
+    // time.
+
+    // Sleep until target_time.
+    uint64_t time = TestUtils_time();
+    if (args->target_time > time) {
+        TestUtils_usleep(args->target_time - time);
+    }
+
+    TestUtils_thread_yield();
+
+    uint32_t succeeded = 0;
+    for (uint32_t i = 0; i < args->num_objs; i++) {
+        String *obj = Str_newf("%u32", args->nums[i]);
+        if (LFReg_register(args->registry, obj, (Obj*)obj)) {
+            succeeded++;
+        }
+        DECREF(obj);
+    }
+
+    args->succeeded = succeeded;
+}
+
+static void
+test_threads(TestBatchRunner *runner) {
+    if (!TestUtils_has_threads) {
+        SKIP(runner, 1, "No thread support");
+        return;
+    }
+
+    LockFreeRegistry *registry = LFReg_new(32);
+    ThreadArgs thread_args[NUM_THREADS];
+    uint32_t num_objs = 10000;
+
+    for (uint32_t i = 0; i < NUM_THREADS; i++) {
+        uint32_t *nums = (uint32_t*)MALLOCATE(num_objs * sizeof(uint32_t));
+
+        for (uint32_t j = 0; j < num_objs; j++) {
+            nums[j] = j;
+        }
+
+        // Fisher-Yates shuffle.
+        for (uint32_t j = num_objs - 1; j > 0; j--) {
+            uint32_t r = (uint32_t)TestUtils_random_u64() % (j + 1);
+            uint32_t tmp = nums[j];
+            nums[j] = nums[r];
+            nums[r] = tmp;
+        }
+
+        thread_args[i].registry = registry;
+        thread_args[i].nums     = nums;
+        thread_args[i].num_objs = num_objs;
+    }
+
+    Thread *threads[NUM_THREADS];
+    uint64_t target_time = TestUtils_time() + 200 * 1000;
+
+    for (uint32_t i = 0; i < NUM_THREADS; i++) {
+        thread_args[i].target_time = target_time;
+        threads[i]
+            = TestUtils_thread_create(S_register_many, &thread_args[i], NULL);
+    }
+
+    uint32_t total_succeeded = 0;
+
+    for (uint32_t i = 0; i < NUM_THREADS; i++) {
+        TestUtils_thread_join(threads[i]);
+        total_succeeded += thread_args[i].succeeded;
+        FREEMEM(thread_args[i].nums);
+    }
+
+    TEST_INT_EQ(runner, total_succeeded, num_objs,
+                "registered exactly the right number of entries across all"
+                " threads");
+
+    LFReg_destroy(registry);
+}
+
+void
+TestLFReg_Run_IMP(TestLockFreeRegistry *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 7);
+    test_all(runner);
+    test_threads(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestLockFreeRegistry.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestLockFreeRegistry.cfh b/runtime/test/Clownfish/Test/TestLockFreeRegistry.cfh
new file mode 100644
index 0000000..784f745
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestLockFreeRegistry.cfh
@@ -0,0 +1,29 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestLockFreeRegistry nickname TestLFReg
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestLockFreeRegistry*
+    new();
+
+    void
+    Run(TestLockFreeRegistry *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestMethod.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestMethod.c b/runtime/test/Clownfish/Test/TestMethod.c
new file mode 100644
index 0000000..7c39759
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestMethod.c
@@ -0,0 +1,91 @@
+/* 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 CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "Clownfish/Test/TestMethod.h"
+
+#include "Clownfish/Err.h"
+#include "Clownfish/Method.h"
+#include "Clownfish/String.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+
+TestMethod*
+TestMethod_new() {
+    return (TestMethod*)Class_Make_Obj(TESTMETHOD);
+}
+
+static void
+S_set_host_alias(void *context) {
+    Method *method = (Method*)context;
+    Method_Set_Host_Alias(method, SSTR_WRAP_C("foo"));
+}
+
+static void
+test_accessors(TestBatchRunner *runner) {
+    String *name = SSTR_WRAP_C("Frobnicate_Widget");
+    Method *method = Method_new(name, NULL, 0);
+
+    TEST_TRUE(runner, Str_Equals(Method_Get_Name(method), (Obj*)name),
+              "Get_Name");
+
+    String *alias = SSTR_WRAP_C("host_frob");
+    Method_Set_Host_Alias(method, alias);
+    TEST_TRUE(runner, Str_Equals(Method_Get_Host_Alias(method), (Obj*)alias),
+              "Set_Host_Alias");
+    Err *error = Err_trap(S_set_host_alias, method);
+    TEST_TRUE(runner, error != NULL,
+              "Set_Host_Alias can't be called more than once");
+    DECREF(error);
+
+    TEST_FALSE(runner, Method_Is_Excluded_From_Host(method),
+               "Is_Excluded_From_Host");
+
+    Method_Destroy(method);
+}
+
+static void
+test_lower_snake_alias(TestBatchRunner *runner) {
+    String *name = SSTR_WRAP_C("Frobnicate_Widget");
+    Method *method = Method_new(name, NULL, 0);
+
+    {
+        String *alias = Method_lower_snake_alias(method);
+        TEST_TRUE(runner, Str_Equals_Utf8(alias, "frobnicate_widget", 17),
+                  "lower_snake_alias without explicit alias");
+        DECREF(alias);
+    }
+
+    {
+        String *new_alias = SSTR_WRAP_C("host_frob");
+        Method_Set_Host_Alias(method, new_alias);
+        String *alias = Method_lower_snake_alias(method);
+        TEST_TRUE(runner, Str_Equals(alias, (Obj*)new_alias),
+                  "lower_snake_alias with explicit alias");
+        DECREF(alias);
+    }
+
+    Method_Destroy(method);
+}
+
+void
+TestMethod_Run_IMP(TestMethod *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 6);
+    test_accessors(runner);
+    test_lower_snake_alias(runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestMethod.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestMethod.cfh b/runtime/test/Clownfish/Test/TestMethod.cfh
new file mode 100644
index 0000000..76b9558
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestMethod.cfh
@@ -0,0 +1,28 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestMethod
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestMethod*
+    new();
+
+    void
+    Run(TestMethod *self, TestBatchRunner *runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestNum.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestNum.c b/runtime/test/Clownfish/Test/TestNum.c
new file mode 100644
index 0000000..dfa6769
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestNum.c
@@ -0,0 +1,278 @@
+/* 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 CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include <math.h>
+
+#include "charmony.h"
+
+#include "Clownfish/Test/TestNum.h"
+
+#include "Clownfish/Err.h"
+#include "Clownfish/String.h"
+#include "Clownfish/Num.h"
+#include "Clownfish/Test.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Clownfish/Class.h"
+
+TestNum*
+TestNum_new() {
+    return (TestNum*)Class_Make_Obj(TESTNUM);
+}
+
+static void
+test_To_String(TestBatchRunner *runner) {
+    Float   *f64 = Float_new(1.33);
+    Integer *i64 = Int_new(INT64_MAX);
+    String *f64_string = Float_To_String(f64);
+    String *i64_string = Int_To_String(i64);
+
+    TEST_TRUE(runner, Str_Starts_With_Utf8(f64_string, "1.3", 3),
+              "Float_To_String");
+    TEST_TRUE(runner, Str_Equals_Utf8(i64_string, "9223372036854775807", 19),
+              "Int_To_String");
+
+    DECREF(i64_string);
+    DECREF(f64_string);
+    DECREF(i64);
+    DECREF(f64);
+}
+
+static void
+S_float_to_i64(void *context) {
+    Float *f = (Float*)context;
+    Float_To_I64(f);
+}
+
+static void
+test_accessors(TestBatchRunner *runner) {
+    Float   *f64 = Float_new(1.33);
+    Integer *i64 = Int_new(INT64_MIN);
+    double wanted64 = 1.33;
+    double got64;
+
+    got64 = Float_Get_Value(f64);
+    TEST_TRUE(runner, *(int64_t*)&got64 == *(int64_t*)&wanted64,
+              "F64 Get_Value");
+    TEST_TRUE(runner, Float_To_I64(f64) == 1, "Float_To_I64");
+
+    {
+        Float *huge = Float_new(1e40);
+        Err *error = Err_trap(S_float_to_i64, huge);
+        TEST_TRUE(runner, error != NULL,
+                  "Float_To_I64 throws when out of range (+)");
+        DECREF(error);
+        DECREF(huge);
+    }
+
+    {
+        Float *huge = Float_new(-1e40);
+        Err *error = Err_trap(S_float_to_i64, huge);
+        TEST_TRUE(runner, error != NULL,
+                  "Float_To_I64 throws when out of range (-)");
+        DECREF(error);
+        DECREF(huge);
+    }
+
+    TEST_TRUE(runner, Int_Get_Value(i64) == INT64_MIN, "I64 Get_Value");
+    TEST_TRUE(runner, Int_To_F64(i64) == -9223372036854775808.0, "Int_To_F64");
+
+    DECREF(i64);
+    DECREF(f64);
+}
+
+static void
+S_test_compare_float_int(TestBatchRunner *runner, double f64_val,
+                         int64_t i64_val, int32_t result) {
+    Float *f64;
+    Integer *i64;
+
+    f64 = Float_new(f64_val);
+    i64 = Int_new(i64_val);
+    TEST_INT_EQ(runner, Float_Compare_To(f64, (Obj*)i64), result,
+                "Float_Compare_To %f %" PRId64, f64_val, i64_val);
+    TEST_INT_EQ(runner, Int_Compare_To(i64, (Obj*)f64), -result,
+                "Int_Compare_To %" PRId64" %f", i64_val, f64_val);
+    TEST_INT_EQ(runner, Float_Equals(f64, (Obj*)i64), result == 0,
+                "Float_Equals %f %" PRId64, f64_val, i64_val);
+    TEST_INT_EQ(runner, Int_Equals(i64, (Obj*)f64), result == 0,
+                "Int_Equals %" PRId64 " %f", i64_val, f64_val);
+    DECREF(f64);
+    DECREF(i64);
+
+    if (i64_val == INT64_MIN) { return; }
+
+    f64 = Float_new(-f64_val);
+    i64 = Int_new(-i64_val);
+    TEST_INT_EQ(runner, Float_Compare_To(f64, (Obj*)i64), -result,
+                "Float_Compare_To %f %" PRId64, -f64_val, -i64_val);
+    TEST_INT_EQ(runner, Int_Compare_To(i64, (Obj*)f64), result,
+                "Int_Compare_To %" PRId64" %f", -i64_val, -f64_val);
+    TEST_INT_EQ(runner, Float_Equals(f64, (Obj*)i64), result == 0,
+                "Float_Equals %f %" PRId64, -f64_val, -i64_val);
+    TEST_INT_EQ(runner, Int_Equals(i64, (Obj*)f64), result == 0,
+                "Int_Equals %" PRId64 " %f", -i64_val, -f64_val);
+    DECREF(f64);
+    DECREF(i64);
+}
+
+static void
+S_float_compare_to(void *context) {
+    Float *f = (Float*)context;
+    Float_Compare_To(f, (Obj*)OBJ);
+}
+
+static void
+S_int_compare_to(void *context) {
+    Integer *i = (Integer*)context;
+    Int_Compare_To(i, (Obj*)OBJ);
+}
+
+static void
+test_Equals_and_Compare_To(TestBatchRunner *runner) {
+    {
+        Float *f1 = Float_new(1.0);
+        Float *f2 = Float_new(1.0);
+        TEST_TRUE(runner, Float_Compare_To(f1, (Obj*)f2) == 0,
+                  "Float_Compare_To equal");
+        TEST_TRUE(runner, Float_Equals(f1, (Obj*)f2),
+                  "Float_Equals equal");
+        DECREF(f1);
+        DECREF(f2);
+    }
+
+    {
+        Float *f1 = Float_new(1.0);
+        Float *f2 = Float_new(2.0);
+        TEST_TRUE(runner, Float_Compare_To(f1, (Obj*)f2) < 0,
+                  "Float_Compare_To less than");
+        TEST_FALSE(runner, Float_Equals(f1, (Obj*)f2),
+                   "Float_Equals less than");
+        DECREF(f1);
+        DECREF(f2);
+    }
+
+    {
+        Float *f1 = Float_new(1.0);
+        Float *f2 = Float_new(0.0);
+        TEST_TRUE(runner, Float_Compare_To(f1, (Obj*)f2) > 0,
+                  "Float_Compare_To greater than");
+        TEST_FALSE(runner, Float_Equals(f1, (Obj*)f2),
+                   "Float_Equals greater than");
+        DECREF(f1);
+        DECREF(f2);
+    }
+
+    {
+        Float *f = Float_new(1.0);
+        Err *error = Err_trap(S_float_compare_to, f);
+        TEST_TRUE(runner, error != NULL,
+                  "Float_Compare_To with invalid type throws");
+        TEST_FALSE(runner, Float_Equals(f, (Obj*)OBJ),
+                   "Float_Equals with different type");
+        DECREF(error);
+        DECREF(f);
+    }
+
+    {
+        Integer *i1 = Int_new(INT64_C(0x6666666666666666));
+        Integer *i2 = Int_new(INT64_C(0x6666666666666666));
+        TEST_TRUE(runner, Int_Compare_To(i1, (Obj*)i2) == 0,
+                  "Int_Compare_To equal");
+        TEST_TRUE(runner, Int_Equals(i1, (Obj*)i2),
+                  "Int_Equals equal");
+        DECREF(i1);
+        DECREF(i2);
+    }
+
+    {
+        Integer *i1 = Int_new(INT64_C(0x6666666666666666));
+        Integer *i2 = Int_new(INT64_C(0x6666666666666667));
+        TEST_TRUE(runner, Int_Compare_To(i1, (Obj*)i2) < 0,
+                  "Int_Compare_To less than");
+        TEST_FALSE(runner, Int_Equals(i1, (Obj*)i2),
+                   "Int_Equals less than");
+        DECREF(i1);
+        DECREF(i2);
+    }
+
+    {
+        Integer *i1 = Int_new(INT64_C(0x6666666666666666));
+        Integer *i2 = Int_new(INT64_C(0x6666666666666665));
+        TEST_TRUE(runner, Int_Compare_To(i1, (Obj*)i2) > 0,
+                  "Int_Compare_To greater than");
+        TEST_FALSE(runner, Int_Equals(i1, (Obj*)i2),
+                   "Int_Equals greater than");
+        DECREF(i1);
+        DECREF(i2);
+    }
+
+    {
+        Integer *i = Int_new(0);
+        Err *error = Err_trap(S_int_compare_to, i);
+        TEST_TRUE(runner, error != NULL,
+                  "Int_Compare_To with invalid type throws");
+        TEST_FALSE(runner, Int_Equals(i, (Obj*)OBJ),
+                   "Int_Equals with different type");
+        DECREF(error);
+        DECREF(i);
+    }
+
+    // NOTICE: When running these tests on x86/x64, it's best to compile
+    // with -ffloat-store to avoid excess FPU precision which can hide
+    // implementation bugs.
+    S_test_compare_float_int(runner, (double)INT64_MAX * 2.0, INT64_MAX, 1);
+    S_test_compare_float_int(runner, pow(2.0, 60.0), INT64_C(1) << 60, 0);
+    S_test_compare_float_int(runner, pow(2.0, 60.0), (INT64_C(1) << 60) - 1,
+                             1);
+    S_test_compare_float_int(runner, pow(2.0, 60.0), (INT64_C(1) << 60) + 1,
+                             -1);
+    S_test_compare_float_int(runner, pow(2.0, 63.0), INT64_MAX, 1);
+    S_test_compare_float_int(runner, -pow(2.0, 63.0), INT64_MIN, 0);
+    // -9223372036854777856.0 == nextafter(-pow(2, 63), -INFINITY)
+    S_test_compare_float_int(runner, -9223372036854777856.0, INT64_MIN, -1);
+    S_test_compare_float_int(runner, 1.0, 2, -1);
+}
+
+static void
+test_Clone(TestBatchRunner *runner) {
+    Float   *f64 = Float_new(1.33);
+    Integer *i64 = Int_new(INT64_MAX);
+    Float   *f64_dupe = Float_Clone(f64);
+    Integer *i64_dupe = Int_Clone(i64);
+    TEST_TRUE(runner, Float_Equals(f64, (Obj*)f64_dupe),
+              "Float Clone");
+    TEST_TRUE(runner, Int_Equals(i64, (Obj*)i64_dupe),
+              "Integer Clone");
+    DECREF(i64_dupe);
+    DECREF(f64_dupe);
+    DECREF(i64);
+    DECREF(f64);
+}
+
+void
+TestNum_Run_IMP(TestNum *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 82);
+    test_To_String(runner);
+    test_accessors(runner);
+    test_Equals_and_Compare_To(runner);
+    test_Clone(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestNum.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestNum.cfh b/runtime/test/Clownfish/Test/TestNum.cfh
new file mode 100644
index 0000000..6d1f663
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestNum.cfh
@@ -0,0 +1,29 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestNum
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestNum*
+    new();
+
+    void
+    Run(TestNum *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestObj.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestObj.c b/runtime/test/Clownfish/Test/TestObj.c
new file mode 100644
index 0000000..c4cdafe
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestObj.c
@@ -0,0 +1,152 @@
+/* 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 CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include "charmony.h"
+
+#include "Clownfish/Test/TestObj.h"
+
+#include "Clownfish/String.h"
+#include "Clownfish/Err.h"
+#include "Clownfish/Test.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/Class.h"
+
+TestObj*
+TestObj_new() {
+    return (TestObj*)Class_Make_Obj(TESTOBJ);
+}
+
+static Obj*
+S_new_testobj() {
+    String *class_name = SSTR_WRAP_C("TestObj");
+    Obj *obj;
+    Class *klass = Class_fetch_class(class_name);
+    if (!klass) {
+        klass = Class_singleton(class_name, OBJ);
+    }
+    obj = Class_Make_Obj(klass);
+    return Obj_init(obj);
+}
+
+static void
+test_refcounts(TestBatchRunner *runner) {
+    Obj *obj = S_new_testobj();
+
+    TEST_INT_EQ(runner, CFISH_REFCOUNT_NN(obj), 1,
+                "Correct starting refcount");
+
+    obj = CFISH_INCREF_NN(obj);
+    TEST_INT_EQ(runner, CFISH_REFCOUNT_NN(obj), 2, "INCREF_NN");
+
+    CFISH_DECREF_NN(obj);
+    TEST_INT_EQ(runner, CFISH_REFCOUNT_NN(obj), 1, "DECREF_NN");
+
+    DECREF(obj);
+}
+
+static void
+test_To_String(TestBatchRunner *runner) {
+    Obj *testobj = S_new_testobj();
+    String *string = Obj_To_String(testobj);
+    TEST_TRUE(runner, Str_Contains_Utf8(string, "TestObj", 7), "To_String");
+    DECREF(string);
+    DECREF(testobj);
+}
+
+static void
+test_Equals(TestBatchRunner *runner) {
+    Obj *testobj = S_new_testobj();
+    Obj *other   = S_new_testobj();
+
+    TEST_TRUE(runner, Obj_Equals(testobj, testobj),
+              "Equals is true for the same object");
+    TEST_FALSE(runner, Obj_Equals(testobj, other),
+               "Distinct objects are not equal");
+
+    DECREF(testobj);
+    DECREF(other);
+}
+
+static void
+test_is_a(TestBatchRunner *runner) {
+    String *string     = Str_new_from_trusted_utf8("", 0);
+    Class  *str_class  = Str_get_class(string);
+    String *class_name = Str_get_class_name(string);
+
+    TEST_TRUE(runner, Str_is_a(string, STRING), "String is_a String.");
+    TEST_TRUE(runner, Str_is_a(string, OBJ), "String is_a Obj.");
+    TEST_TRUE(runner, str_class == STRING, "get_class");
+    TEST_TRUE(runner, Str_Equals(Class_Get_Name(STRING), (Obj*)class_name),
+              "get_class_name");
+    TEST_FALSE(runner, Obj_is_a(NULL, OBJ), "NULL is not an Obj");
+
+    DECREF(string);
+}
+
+static void
+S_attempt_init(void *context) {
+    Obj_init((Obj*)context);
+}
+
+static void
+S_attempt_Clone(void *context) {
+    Obj_Clone((Obj*)context);
+}
+
+static void
+S_attempt_Compare_To(void *context) {
+    Obj_Compare_To((Obj*)context, (Obj*)context);
+}
+
+static void
+S_verify_abstract_error(TestBatchRunner *runner, Err_Attempt_t routine,
+                        void *context, const char *name) {
+    char message[100];
+    sprintf(message, "%s() is abstract", name);
+    Err *error = Err_trap(routine, context);
+    TEST_TRUE(runner, error != NULL
+              && Err_is_a(error, ERR)
+              && Str_Contains_Utf8(Err_Get_Mess(error), "bstract", 7),
+              message);
+    DECREF(error);
+}
+
+static void
+test_abstract_routines(TestBatchRunner *runner) {
+    Obj *blank = Class_Make_Obj(OBJ);
+    S_verify_abstract_error(runner, S_attempt_init, blank, "init");
+
+    Obj *obj = S_new_testobj();
+    S_verify_abstract_error(runner, S_attempt_Clone,      obj, "Clone");
+    S_verify_abstract_error(runner, S_attempt_Compare_To, obj, "Compare_To");
+    DECREF(obj);
+}
+
+void
+TestObj_Run_IMP(TestObj *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 14);
+    test_refcounts(runner);
+    test_To_String(runner);
+    test_Equals(runner);
+    test_is_a(runner);
+    test_abstract_routines(runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestObj.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestObj.cfh b/runtime/test/Clownfish/Test/TestObj.cfh
new file mode 100644
index 0000000..c5cc401
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestObj.cfh
@@ -0,0 +1,28 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestObj
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestObj*
+    new();
+
+    void
+    Run(TestObj *self, TestBatchRunner *runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestPtrHash.c
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestPtrHash.c b/runtime/test/Clownfish/Test/TestPtrHash.c
new file mode 100644
index 0000000..3bf7003
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestPtrHash.c
@@ -0,0 +1,108 @@
+/* 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 CFISH_USE_SHORT_NAMES
+#define TESTCFISH_USE_SHORT_NAMES
+
+#include <stdlib.h>
+#include <time.h>
+
+#include "Clownfish/Test/TestPtrHash.h"
+#include "Clownfish/Class.h"
+#include "Clownfish/PtrHash.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Clownfish/Util/Memory.h"
+
+TestPtrHash*
+TestPtrHash_new() {
+    return (TestPtrHash*)Class_Make_Obj(TESTPTRHASH);
+}
+
+static void
+test_Store_and_Fetch(TestBatchRunner *runner) {
+    PtrHash *hash = PtrHash_new(100);
+    char dummy[100];
+
+    for (int i = 0; i < 100; i++) {
+        void *key = &dummy[i];
+        PtrHash_Store(hash, key, key);
+    }
+
+    bool all_equal = true;
+    for (int i = 0; i < 100; i++) {
+        void *key = &dummy[i];
+        void *value = PtrHash_Fetch(hash, key);
+        if (value != key) {
+            all_equal = false;
+            break;
+        }
+    }
+    TEST_TRUE(runner, all_equal, "basic Store and Fetch");
+
+    TEST_TRUE(runner, PtrHash_Fetch(hash, &dummy[100]) == NULL,
+              "Fetch against non-existent key returns NULL");
+
+    PtrHash_Store(hash, &dummy[50], dummy);
+    TEST_TRUE(runner, PtrHash_Fetch(hash, &dummy[50]) == dummy,
+              "Store replaces existing value");
+
+    PtrHash_Destroy(hash);
+}
+
+static void
+test_stress(TestBatchRunner *runner) {
+    PtrHash *hash = PtrHash_new(0); // trigger multiple rebuilds.
+    size_t num_elems = 200000;
+    void **keys = (void**)MALLOCATE(num_elems * sizeof(void*));
+
+    for (size_t i = 0; i < num_elems; i++) {
+        size_t index = (size_t)(TestUtils_random_u64() % num_elems);
+        void *key = &keys[index];
+        PtrHash_Store(hash, key, key);
+        keys[i] = key;
+    }
+
+    // Overwrite for good measure.
+    for (size_t i = 0; i < num_elems; i++) {
+        void *key = keys[i];
+        PtrHash_Store(hash, key, key);
+    }
+
+    bool all_equal = true;
+    for (size_t i = 0; i < num_elems; i++) {
+        void *key = keys[i];
+        void *got = PtrHash_Fetch(hash, key);
+        if (got != key) {
+            all_equal = false;
+            break;
+        }
+    }
+    TEST_TRUE(runner, all_equal, "stress test");
+
+    FREEMEM(keys);
+    PtrHash_Destroy(hash);
+}
+
+void
+TestPtrHash_Run_IMP(TestPtrHash *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 4);
+    srand((unsigned int)time(NULL));
+    test_Store_and_Fetch(runner);
+    test_stress(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy-clownfish/blob/8ba4e619/runtime/test/Clownfish/Test/TestPtrHash.cfh
----------------------------------------------------------------------
diff --git a/runtime/test/Clownfish/Test/TestPtrHash.cfh b/runtime/test/Clownfish/Test/TestPtrHash.cfh
new file mode 100644
index 0000000..83589cb
--- /dev/null
+++ b/runtime/test/Clownfish/Test/TestPtrHash.cfh
@@ -0,0 +1,29 @@
+/* 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 TestClownfish;
+
+class Clownfish::Test::TestPtrHash
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestPtrHash*
+    new();
+
+    void
+    Run(TestPtrHash *self, TestBatchRunner *runner);
+}
+
+