http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestRAMFileHandle.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestRAMFileHandle.cfh 
b/test/Lucy/Test/Store/TestRAMFileHandle.cfh
new file mode 100644
index 0000000..d5418cf
--- /dev/null
+++ b/test/Lucy/Test/Store/TestRAMFileHandle.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 TestLucy;
+
+class Lucy::Test::Store::TestRAMFileHandle nickname TestRAMFH
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestRAMFileHandle*
+    new();
+
+    void
+    Run(TestRAMFileHandle *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestRAMFolder.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestRAMFolder.c 
b/test/Lucy/Test/Store/TestRAMFolder.c
new file mode 100644
index 0000000..c148fe3
--- /dev/null
+++ b/test/Lucy/Test/Store/TestRAMFolder.c
@@ -0,0 +1,521 @@
+/* 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_RAMFOLDER
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestRAMFolder.h"
+#include "Lucy/Store/RAMFolder.h"
+#include "Lucy/Store/DirHandle.h"
+#include "Lucy/Store/RAMDirHandle.h"
+#include "Lucy/Store/RAMFileHandle.h"
+
+static String *foo           = NULL;
+static String *bar           = NULL;
+static String *baz           = NULL;
+static String *boffo         = NULL;
+static String *banana        = NULL;
+static String *foo_bar       = NULL;
+static String *foo_bar_baz   = NULL;
+static String *foo_bar_boffo = NULL;
+static String *foo_boffo     = NULL;
+static String *foo_foo       = NULL;
+static String *nope          = NULL;
+static String *nope_nyet     = NULL;
+
+TestRAMFolder*
+TestRAMFolder_new() {
+    return (TestRAMFolder*)Class_Make_Obj(TESTRAMFOLDER);
+}
+
+static void
+S_init_strings(void) {
+    foo           = Str_newf("foo");
+    bar           = Str_newf("bar");
+    baz           = Str_newf("baz");
+    boffo         = Str_newf("boffo");
+    banana        = Str_newf("banana");
+    foo_bar       = Str_newf("foo/bar");
+    foo_bar_baz   = Str_newf("foo/bar/baz");
+    foo_bar_boffo = Str_newf("foo/bar/boffo");
+    foo_boffo     = Str_newf("foo/boffo");
+    foo_foo       = Str_newf("foo/foo");
+    nope          = Str_newf("nope");
+    nope_nyet     = Str_newf("nope/nyet");
+}
+
+static void
+S_destroy_strings(void) {
+    DECREF(foo);
+    DECREF(bar);
+    DECREF(baz);
+    DECREF(boffo);
+    DECREF(banana);
+    DECREF(foo_bar);
+    DECREF(foo_bar_baz);
+    DECREF(foo_bar_boffo);
+    DECREF(foo_boffo);
+    DECREF(foo_foo);
+    DECREF(nope);
+    DECREF(nope_nyet);
+}
+
+static void
+test_Initialize_and_Check(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    RAMFolder_Initialize(folder);
+    PASS(runner, "Initialized concludes without incident");
+    TEST_TRUE(runner, RAMFolder_Check(folder), "Check succeeds");
+    DECREF(folder);
+}
+
+static void
+test_Local_Exists(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    FileHandle *fh = RAMFolder_Open_FileHandle(folder, boffo,
+                                               FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    RAMFolder_Local_MkDir(folder, foo);
+
+    TEST_TRUE(runner, RAMFolder_Local_Exists(folder, boffo),
+              "Local_Exists() returns true for file");
+    TEST_TRUE(runner, RAMFolder_Local_Exists(folder, foo),
+              "Local_Exists() returns true for dir");
+    TEST_FALSE(runner, RAMFolder_Local_Exists(folder, bar),
+               "Local_Exists() returns false for non-existent entry");
+
+    DECREF(folder);
+}
+
+static void
+test_Local_Is_Directory(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    FileHandle *fh = RAMFolder_Open_FileHandle(folder, boffo,
+                                               FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    RAMFolder_Local_MkDir(folder, foo);
+
+    TEST_FALSE(runner, RAMFolder_Local_Is_Directory(folder, boffo),
+               "Local_Is_Directory() returns false for file");
+    TEST_TRUE(runner, RAMFolder_Local_Is_Directory(folder, foo),
+              "Local_Is_Directory() returns true for dir");
+    TEST_FALSE(runner, RAMFolder_Local_Is_Directory(folder, bar),
+               "Local_Is_Directory() returns false for non-existent entry");
+
+    DECREF(folder);
+}
+
+static void
+test_Local_Find_Folder(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    RAMFolder *local;
+    FileHandle *fh;
+
+    RAMFolder_MkDir(folder, foo);
+    RAMFolder_MkDir(folder, foo_bar);
+    fh = RAMFolder_Open_FileHandle(folder, boffo,
+                                   FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    fh = RAMFolder_Open_FileHandle(folder, foo_boffo,
+                                   FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, nope);
+    TEST_TRUE(runner, local == NULL, "Non-existent entry yields NULL");
+
+    String *empty = SSTR_BLANK();
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, empty);
+    TEST_TRUE(runner, local == NULL, "Empty string yields NULL");
+
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, foo_bar);
+    TEST_TRUE(runner, local == NULL, "nested folder yields NULL");
+
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, foo_boffo);
+    TEST_TRUE(runner, local == NULL, "nested file yields NULL");
+
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, boffo);
+    TEST_TRUE(runner, local == NULL, "local file yields NULL");
+
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, bar);
+    TEST_TRUE(runner, local == NULL, "name of nested folder yields NULL");
+
+    local = (RAMFolder*)RAMFolder_Local_Find_Folder(folder, foo);
+    TEST_TRUE(runner,
+              local
+              && RAMFolder_is_a(local, RAMFOLDER)
+              && Str_Equals_Utf8(RAMFolder_Get_Path(local), "foo", 3),
+              "Find local directory");
+
+    DECREF(folder);
+}
+
+static void
+test_Local_MkDir(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    bool result;
+
+    result = RAMFolder_Local_MkDir(folder, foo);
+    TEST_TRUE(runner, result, "Local_MkDir succeeds and returns true");
+
+    Err_set_error(NULL);
+    result = RAMFolder_Local_MkDir(folder, foo);
+    TEST_FALSE(runner, result,
+               "Local_MkDir returns false when a dir already exists");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Local_MkDir sets global error when a dir already exists");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo),
+              "Existing dir untouched after failed Local_MkDir");
+
+    FileHandle *fh = RAMFolder_Open_FileHandle(folder, boffo,
+                                               FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    Err_set_error(NULL);
+    result = RAMFolder_Local_MkDir(folder, foo);
+    TEST_FALSE(runner, result,
+               "Local_MkDir returns false when a file already exists");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Local_MkDir sets global error when a file already exists");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, boffo) &&
+              !RAMFolder_Local_Is_Directory(folder, boffo),
+              "Existing file untouched after failed Local_MkDir");
+
+    DECREF(folder);
+}
+
+static void
+test_Local_Open_Dir(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    DirHandle *dh = RAMFolder_Local_Open_Dir(folder);
+    TEST_TRUE(runner, dh && DH_is_a(dh, RAMDIRHANDLE),
+              "Local_Open_Dir returns a RAMDirHandle");
+    DECREF(dh);
+    DECREF(folder);
+}
+
+static void
+test_Local_Open_FileHandle(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    FileHandle *fh;
+
+    fh = RAMFolder_Local_Open_FileHandle(folder, boffo,
+                                         FH_CREATE | FH_WRITE_ONLY);
+    TEST_TRUE(runner, fh && FH_is_a(fh, RAMFILEHANDLE),
+              "opened FileHandle");
+    DECREF(fh);
+
+    fh = RAMFolder_Local_Open_FileHandle(folder, boffo,
+                                         FH_CREATE | FH_WRITE_ONLY);
+    TEST_TRUE(runner, fh && FH_is_a(fh, RAMFILEHANDLE),
+              "opened FileHandle for append");
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    fh = RAMFolder_Local_Open_FileHandle(folder, boffo,
+                                         FH_CREATE | FH_WRITE_ONLY | 
FH_EXCLUSIVE);
+    TEST_TRUE(runner, fh == NULL, "FH_EXLUSIVE flag prevents open");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "failure due to FH_EXLUSIVE flag sets global error");
+
+    fh = RAMFolder_Local_Open_FileHandle(folder, boffo, FH_READ_ONLY);
+    TEST_TRUE(runner, fh && FH_is_a(fh, RAMFILEHANDLE),
+              "opened FileHandle for reading");
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    fh = RAMFolder_Local_Open_FileHandle(folder, nope, FH_READ_ONLY);
+    TEST_TRUE(runner, fh == NULL,
+              "Can't open non-existent file for reading");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Opening non-existent file for reading sets global error");
+
+    DECREF(folder);
+}
+
+static void
+test_Local_Delete(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    FileHandle *fh;
+
+    fh = RAMFolder_Open_FileHandle(folder, boffo, FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    TEST_TRUE(runner, RAMFolder_Local_Delete(folder, boffo),
+              "Local_Delete on file succeeds");
+
+    RAMFolder_Local_MkDir(folder, foo);
+    fh = RAMFolder_Open_FileHandle(folder, foo_boffo,
+                                   FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, RAMFolder_Local_Delete(folder, foo),
+               "Local_Delete on non-empty dir fails");
+
+    RAMFolder_Delete(folder, foo_boffo);
+    TEST_TRUE(runner, RAMFolder_Local_Delete(folder, foo),
+              "Local_Delete on empty dir succeeds");
+
+    DECREF(folder);
+}
+
+static void
+test_Rename(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    FileHandle *fh;
+    bool result;
+
+    RAMFolder_MkDir(folder, foo);
+    RAMFolder_MkDir(folder, foo_bar);
+    fh = RAMFolder_Open_FileHandle(folder, boffo, FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+
+    // Move files.
+
+    result = RAMFolder_Rename(folder, boffo, banana);
+    TEST_TRUE(runner, result, "Rename succeeds and returns true");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, banana),
+              "File exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, boffo),
+               "File no longer exists at old path");
+
+    result = RAMFolder_Rename(folder, banana, foo_bar_boffo);
+    TEST_TRUE(runner, result, "Rename to file in nested dir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar_boffo),
+              "File exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, banana),
+               "File no longer exists at old path");
+
+    result = RAMFolder_Rename(folder, foo_bar_boffo, boffo);
+    TEST_TRUE(runner, result, "Rename from file in nested dir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, boffo),
+              "File exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, foo_bar_boffo),
+               "File no longer exists at old path");
+
+    fh = RAMFolder_Open_FileHandle(folder, foo_boffo,
+                                   FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    result = RAMFolder_Rename(folder, boffo, foo_boffo);
+    TEST_TRUE(runner, result, "Clobber");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, boffo),
+               "File no longer exists at old path");
+
+    // Move Dirs.
+
+    RAMFolder_MkDir(folder, baz);
+    result = RAMFolder_Rename(folder, baz, boffo);
+    TEST_TRUE(runner, result, "Rename dir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, boffo),
+              "Folder exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, baz),
+               "Folder no longer exists at old path");
+
+    result = RAMFolder_Rename(folder, boffo, foo_foo);
+    TEST_TRUE(runner, result, "Rename dir into nested subdir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_foo),
+              "Folder exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, boffo),
+               "Folder no longer exists at old path");
+
+    result = RAMFolder_Rename(folder, foo_foo, foo_bar_baz);
+    TEST_TRUE(runner, result, "Rename dir from nested subdir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar_baz),
+              "Folder exists at new path");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, foo_foo),
+               "Folder no longer exists at old path");
+
+    // Test failed clobbers.
+
+    Err_set_error(NULL);
+    result = RAMFolder_Rename(folder, foo_boffo, foo_bar);
+    TEST_FALSE(runner, result, "Rename file clobbering dir fails");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Failed rename sets global error");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File still exists at old path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar),
+              "Dir still exists after failed clobber");
+
+    Err_set_error(NULL);
+    result = RAMFolder_Rename(folder, foo_bar, foo_boffo);
+    TEST_FALSE(runner, result, "Rename dir clobbering file fails");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Failed rename sets global error");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar),
+              "Dir still exists at old path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File still exists after failed clobber");
+
+    // Test that "renaming" succeeds where to and from are the same.
+
+    result = RAMFolder_Rename(folder, foo_boffo, foo_boffo);
+    TEST_TRUE(runner, result, "Renaming file to itself succeeds");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File still exists");
+
+    result = RAMFolder_Rename(folder, foo_bar, foo_bar);
+    TEST_TRUE(runner, result, "Renaming dir to itself succeeds");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar),
+              "Dir still exists");
+
+    // Invalid filepaths.
+
+    Err_set_error(NULL);
+    result = RAMFolder_Rename(folder, foo_boffo, nope_nyet);
+    TEST_FALSE(runner, result, "Rename into non-existent subdir fails");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Renaming into non-existent subdir sets global error");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "Entry still exists at old path");
+
+    Err_set_error(NULL);
+    result = RAMFolder_Rename(folder, nope_nyet, boffo);
+    TEST_FALSE(runner, result, "Rename non-existent file fails");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Renaming non-existent source file sets global error");
+
+    DECREF(folder);
+}
+
+static void
+test_Hard_Link(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    FileHandle *fh;
+    bool result;
+
+    RAMFolder_MkDir(folder, foo);
+    RAMFolder_MkDir(folder, foo_bar);
+    fh = RAMFolder_Open_FileHandle(folder, boffo, FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+
+    // Link files.
+
+    result = RAMFolder_Hard_Link(folder, boffo, banana);
+    TEST_TRUE(runner, result, "Hard_Link succeeds and returns true");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, banana),
+              "File exists at new path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, boffo),
+              "File still exists at old path");
+    RAMFolder_Delete(folder, boffo);
+
+    result = RAMFolder_Hard_Link(folder, banana, foo_bar_boffo);
+    TEST_TRUE(runner, result, "Hard_Link to target within nested dir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar_boffo),
+              "File exists at new path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, banana),
+              "File still exists at old path");
+    RAMFolder_Delete(folder, banana);
+
+    result = RAMFolder_Hard_Link(folder, foo_bar_boffo, foo_boffo);
+    TEST_TRUE(runner, result, "Hard_Link from file in nested dir");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File exists at new path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_bar_boffo),
+              "File still exists at old path");
+    RAMFolder_Delete(folder, foo_bar_boffo);
+
+    // Invalid clobbers.
+
+    fh = RAMFolder_Open_FileHandle(folder, boffo, FH_CREATE | FH_WRITE_ONLY);
+    DECREF(fh);
+    result = RAMFolder_Hard_Link(folder, foo_boffo, boffo);
+    TEST_FALSE(runner, result, "Clobber of file fails");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, boffo),
+              "File still exists at new path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File still exists at old path");
+    RAMFolder_Delete(folder, boffo);
+
+    RAMFolder_MkDir(folder, baz);
+    result = RAMFolder_Hard_Link(folder, foo_boffo, baz);
+    TEST_FALSE(runner, result, "Clobber of dir fails");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, baz),
+              "Dir still exists at new path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File still exists at old path");
+    RAMFolder_Delete(folder, baz);
+
+    // Invalid Hard_Link of dir.
+
+    RAMFolder_MkDir(folder, baz);
+    result = RAMFolder_Hard_Link(folder, baz, banana);
+    TEST_FALSE(runner, result, "Hard_Link dir fails");
+    TEST_FALSE(runner, RAMFolder_Exists(folder, banana),
+               "Nothing at new path");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, baz),
+              "Folder still exists at old path");
+    RAMFolder_Delete(folder, baz);
+
+    // Test that linking to yourself fails.
+
+    result = RAMFolder_Hard_Link(folder, foo_boffo, foo_boffo);
+    TEST_FALSE(runner, result, "Hard_Link file to itself fails");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "File still exists");
+
+    // Invalid filepaths.
+
+    Err_set_error(NULL);
+    result = RAMFolder_Rename(folder, foo_boffo, nope_nyet);
+    TEST_FALSE(runner, result, "Hard_Link into non-existent subdir fails");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Hard_Link into non-existent subdir sets global error");
+    TEST_TRUE(runner, RAMFolder_Exists(folder, foo_boffo),
+              "Entry still exists at old path");
+
+    Err_set_error(NULL);
+    result = RAMFolder_Rename(folder, nope_nyet, boffo);
+    TEST_FALSE(runner, result, "Hard_Link non-existent source file fails");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Hard_Link non-existent source file sets global error");
+
+    DECREF(folder);
+}
+
+static void
+test_Close(TestBatchRunner *runner) {
+    RAMFolder *folder = RAMFolder_new(NULL);
+    RAMFolder_Close(folder);
+    PASS(runner, "Close() concludes without incident");
+    RAMFolder_Close(folder);
+    RAMFolder_Close(folder);
+    PASS(runner, "Calling Close() multiple times is safe");
+    DECREF(folder);
+}
+
+void
+TestRAMFolder_Run_IMP(TestRAMFolder *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 98);
+    S_init_strings();
+    test_Initialize_and_Check(runner);
+    test_Local_Exists(runner);
+    test_Local_Is_Directory(runner);
+    test_Local_Find_Folder(runner);
+    test_Local_MkDir(runner);
+    test_Local_Open_Dir(runner);
+    test_Local_Open_FileHandle(runner);
+    test_Local_Delete(runner);
+    test_Rename(runner);
+    test_Hard_Link(runner);
+    test_Close(runner);
+    S_destroy_strings();
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestRAMFolder.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestRAMFolder.cfh 
b/test/Lucy/Test/Store/TestRAMFolder.cfh
new file mode 100644
index 0000000..6fc392c
--- /dev/null
+++ b/test/Lucy/Test/Store/TestRAMFolder.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 TestLucy;
+
+class Lucy::Test::Store::TestRAMFolder
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestRAMFolder*
+    new();
+
+    void
+    Run(TestRAMFolder *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/TestSchema.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/TestSchema.c b/test/Lucy/Test/TestSchema.c
new file mode 100644
index 0000000..1e1c3c1
--- /dev/null
+++ b/test/Lucy/Test/TestSchema.c
@@ -0,0 +1,112 @@
+/* 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_TESTLUCY_TESTSCHEMA
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Plan/TestArchitecture.h"
+#include "Lucy/Test/TestSchema.h"
+#include "Lucy/Analysis/StandardTokenizer.h"
+#include "Lucy/Plan/FullTextType.h"
+#include "Lucy/Plan/Architecture.h"
+#include "Lucy/Util/Freezer.h"
+
+TestSchema*
+TestSchema_new(bool use_alt_arch) {
+    TestSchema *self = (TestSchema*)Class_Make_Obj(TESTSCHEMA);
+    return TestSchema_init(self, use_alt_arch);
+}
+
+TestSchema*
+TestSchema_init(TestSchema *self, bool use_alt_arch) {
+    StandardTokenizer *tokenizer = StandardTokenizer_new();
+    FullTextType *type = FullTextType_new((Analyzer*)tokenizer);
+
+    TestSchema_IVARS(self)->use_alt_arch = use_alt_arch;
+
+    Schema_init((Schema*)self);
+    FullTextType_Set_Highlightable(type, true);
+    String *content = SSTR_WRAP_C("content");
+    TestSchema_Spec_Field(self, content, (FieldType*)type);
+    DECREF(type);
+    DECREF(tokenizer);
+
+    return self;
+}
+
+Architecture*
+TestSchema_Architecture_IMP(TestSchema *self) {
+    if (TestSchema_IVARS(self)->use_alt_arch) {
+        return Arch_new();
+    }
+    else {
+        return (Architecture*)TestArch_new();
+    }
+}
+
+TestBatchSchema*
+TestBatchSchema_new() {
+    return (TestBatchSchema*)Class_Make_Obj(TESTBATCHSCHEMA);
+}
+
+static void
+test_Equals(TestBatchRunner *runner) {
+    TestSchema *schema = TestSchema_new(false);
+    TestSchema *arch_differs = TestSchema_new(true);
+    TestSchema *spec_differs = TestSchema_new(false);
+    String     *content      = SSTR_WRAP_C("content");
+    FullTextType *type = (FullTextType*)TestSchema_Fetch_Type(spec_differs,
+                                                              content);
+
+    TEST_TRUE(runner, TestSchema_Equals(schema, (Obj*)schema), "Equals");
+
+    FullTextType_Set_Boost(type, 2.0f);
+    TEST_FALSE(runner, TestSchema_Equals(schema, (Obj*)spec_differs),
+               "Equals spoiled by differing FieldType");
+
+    TEST_FALSE(runner, TestSchema_Equals(schema, (Obj*)arch_differs),
+               "Equals spoiled by differing Architecture");
+
+    DECREF(schema);
+    DECREF(arch_differs);
+    DECREF(spec_differs);
+}
+
+static void
+test_Dump_and_Load(TestBatchRunner *runner) {
+    TestSchema *schema = TestSchema_new(false);
+    Obj        *dump   = (Obj*)TestSchema_Dump(schema);
+    TestSchema *loaded = (TestSchema*)Freezer_load(dump);
+
+    TEST_FALSE(runner, TestSchema_Equals(schema, (Obj*)loaded),
+               "Dump => Load round trip");
+
+    DECREF(schema);
+    DECREF(dump);
+    DECREF(loaded);
+}
+
+void
+TestBatchSchema_Run_IMP(TestBatchSchema *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 4);
+    test_Equals(runner);
+    test_Dump_and_Load(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/TestSchema.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/TestSchema.cfh b/test/Lucy/Test/TestSchema.cfh
new file mode 100644
index 0000000..5cd897a
--- /dev/null
+++ b/test/Lucy/Test/TestSchema.cfh
@@ -0,0 +1,48 @@
+/* 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 TestLucy;
+
+/** Schema for use by the test suite.
+ *
+ * Exposes problems faced by much larger indexes by using an TestArchitecture,
+ * which returns absurdly low values for [](cfish:.Index_Interval) and 
[](cfish:.Skip_Interval).
+ */
+
+class Lucy::Test::TestSchema inherits Lucy::Plan::Schema {
+    bool use_alt_arch;
+
+    inert incremented TestSchema*
+    new(bool use_alt_arch = false);
+
+    inert TestSchema*
+    init(TestSchema *self, bool use_alt_arch = false);
+
+    public incremented Architecture*
+    Architecture(TestSchema *self);
+}
+
+class Lucy::Test::TestBatchSchema
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestBatchSchema*
+    new();
+
+    void
+    Run(TestBatchSchema *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/TestSimple.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/TestSimple.c b/test/Lucy/Test/TestSimple.c
new file mode 100644
index 0000000..93fd747
--- /dev/null
+++ b/test/Lucy/Test/TestSimple.c
@@ -0,0 +1,112 @@
+/* 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_TESTLUCY_TESTSIMPLE
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Lucy/Test/TestSimple.h"
+#include "Lucy/Simple.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Document/Doc.h"
+#include "Lucy/Document/HitDoc.h"
+#include "Lucy/Store/RAMFolder.h"
+
+TestSimple*
+TestSimple_new() {
+    return (TestSimple*)Class_Make_Obj(TESTSIMPLE);
+}
+
+static void
+test_simple(TestBatchRunner *runner) {
+    RAMFolder *folder   = RAMFolder_new(NULL);
+    String    *language = SSTR_WRAP_C("en");
+    Simple    *lucy     = Simple_new((Obj*)folder, language);
+
+    String *food_field = SSTR_WRAP_C("food");
+
+    {
+        Doc *doc = Doc_new(NULL, 0);
+        String *value = SSTR_WRAP_C("creamed corn");
+        Doc_Store(doc, food_field, (Obj*)value);
+        Simple_Add_Doc(lucy, doc);
+        DECREF(doc);
+
+        String *query = SSTR_WRAP_C("creamed");
+        uint32_t num_results = Simple_Search(lucy, query, 0, 10);
+        TEST_INT_EQ(runner, num_results, 1, "Search works right after add");
+    }
+
+    {
+        Doc *doc = Doc_new(NULL, 0);
+        String *value = SSTR_WRAP_C("creamed spinach");
+        Doc_Store(doc, food_field, (Obj*)value);
+        Simple_Add_Doc(lucy, doc);
+        DECREF(doc);
+
+        String *query = SSTR_WRAP_C("creamed");
+        uint32_t num_results = Simple_Search(lucy, query, 0, 10);
+        TEST_INT_EQ(runner, num_results, 2, "Search returns total hits");
+    }
+
+    {
+        Doc *doc = Doc_new(NULL, 0);
+        String *value = SSTR_WRAP_C("creamed broccoli");
+        Doc_Store(doc, food_field, (Obj*)value);
+        Simple_Add_Doc(lucy, doc);
+        DECREF(doc);
+
+        DECREF(lucy);
+        lucy = Simple_new((Obj*)folder, language);
+
+        String *query = SSTR_WRAP_C("cream");
+        uint32_t num_results = Simple_Search(lucy, query, 0, 10);
+        TEST_INT_EQ(runner, num_results, 3, "commit upon destroy");
+
+        HitDoc *hit;
+        while ((hit = Simple_Next(lucy)) != NULL) {
+            String *food = (String*)HitDoc_Extract(hit, food_field);
+            TEST_TRUE(runner, Str_Starts_With_Utf8(food, "cream", 5), "Next");
+            DECREF(food);
+            DECREF(hit);
+        }
+    }
+
+    {
+        Doc *doc = Doc_new(NULL, 0);
+        String *band_field = SSTR_WRAP_C("band");
+        String *value = SSTR_WRAP_C("Cream");
+        Doc_Store(doc, band_field, (Obj*)value);
+        Simple_Add_Doc(lucy, doc);
+        DECREF(doc);
+
+        String *query = SSTR_WRAP_C("cream");
+        uint32_t num_results = Simple_Search(lucy, query, 0, 10);
+        TEST_INT_EQ(runner, num_results, 4,
+                    "Search uses correct EasyAnalyzer");
+    }
+
+    DECREF(lucy);
+    DECREF(folder);
+}
+
+void
+TestSimple_Run_IMP(TestSimple *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 7);
+    test_simple(runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/TestSimple.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/TestSimple.cfh b/test/Lucy/Test/TestSimple.cfh
new file mode 100644
index 0000000..045fe8c
--- /dev/null
+++ b/test/Lucy/Test/TestSimple.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 TestLucy;
+
+class Lucy::Test::TestSimple
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestSimple*
+    new();
+
+    void
+    Run(TestSimple *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/TestUtils.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/TestUtils.c b/test/Lucy/Test/TestUtils.c
new file mode 100644
index 0000000..bddfa7b
--- /dev/null
+++ b/test/Lucy/Test/TestUtils.c
@@ -0,0 +1,183 @@
+/* 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_TESTLUCY_TESTUTILS
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+#include <string.h>
+
+#include "Lucy/Test/TestUtils.h"
+#include "Lucy/Test.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Lucy/Analysis/Analyzer.h"
+#include "Lucy/Analysis/Inversion.h"
+#include "Lucy/Analysis/Token.h"
+#include "Lucy/Search/TermQuery.h"
+#include "Lucy/Search/PhraseQuery.h"
+#include "Lucy/Search/LeafQuery.h"
+#include "Lucy/Search/ANDQuery.h"
+#include "Lucy/Search/NOTQuery.h"
+#include "Lucy/Search/ORQuery.h"
+#include "Lucy/Search/RangeQuery.h"
+#include "Lucy/Store/FSFolder.h"
+#include "Lucy/Store/InStream.h"
+#include "Lucy/Store/OutStream.h"
+#include "Lucy/Store/RAMFile.h"
+#include "Lucy/Util/Freezer.h"
+
+Vector*
+TestUtils_doc_set() {
+    Vector *docs = Vec_new(10);
+
+    Vec_Push(docs, (Obj*)TestUtils_get_str("x"));
+    Vec_Push(docs, (Obj*)TestUtils_get_str("y"));
+    Vec_Push(docs, (Obj*)TestUtils_get_str("z"));
+    Vec_Push(docs, (Obj*)TestUtils_get_str("x a"));
+    Vec_Push(docs, (Obj*)TestUtils_get_str("x a b"));
+    Vec_Push(docs, (Obj*)TestUtils_get_str("x a b c"));
+    Vec_Push(docs, (Obj*)TestUtils_get_str("x foo a b c d"));
+
+    return docs;
+}
+
+PolyQuery*
+TestUtils_make_poly_query(uint32_t boolop, ...) {
+    va_list args;
+    Query *child;
+    PolyQuery *retval;
+    Vector *children = Vec_new(0);
+
+    va_start(args, boolop);
+    while (NULL != (child = va_arg(args, Query*))) {
+        Vec_Push(children, (Obj*)child);
+    }
+    va_end(args);
+
+    retval = boolop == BOOLOP_OR
+             ? (PolyQuery*)ORQuery_new(children)
+             : (PolyQuery*)ANDQuery_new(children);
+    DECREF(children);
+    return retval;
+}
+
+TermQuery*
+TestUtils_make_term_query(const char *field, const char *term) {
+    String *field_str = SSTR_WRAP_C(field);
+    String *term_str  = SSTR_WRAP_C(term);
+    return TermQuery_new((String*)field_str, (Obj*)term_str);
+}
+
+PhraseQuery*
+TestUtils_make_phrase_query(const char *field, ...) {
+    String *field_str = SSTR_WRAP_C(field);
+    va_list args;
+    Vector *terms = Vec_new(0);
+    PhraseQuery *query;
+    char *term_str;
+
+    va_start(args, field);
+    while (NULL != (term_str = va_arg(args, char*))) {
+        Vec_Push(terms, (Obj*)TestUtils_get_str(term_str));
+    }
+    va_end(args);
+
+    query = PhraseQuery_new(field_str, terms);
+    DECREF(terms);
+    return query;
+}
+
+LeafQuery*
+TestUtils_make_leaf_query(const char *field, const char *term) {
+    String *term_str  = SSTR_WRAP_C(term);
+    String *field_str = field ? SSTR_WRAP_C(field) : NULL;
+    return LeafQuery_new(field_str, term_str);
+}
+
+NOTQuery*
+TestUtils_make_not_query(Query* negated_query) {
+    NOTQuery *not_query = NOTQuery_new(negated_query);
+    DECREF(negated_query);
+    return not_query;
+}
+
+RangeQuery*
+TestUtils_make_range_query(const char *field, const char *lower_term,
+                           const char *upper_term, bool include_lower,
+                           bool include_upper) {
+    String *f     = SSTR_WRAP_C(field);
+    String *lterm = SSTR_WRAP_C(lower_term);
+    String *uterm = SSTR_WRAP_C(upper_term);
+    return RangeQuery_new(f, (Obj*)lterm, (Obj*)uterm, include_lower,
+                          include_upper);
+}
+
+void
+TestUtils_test_analyzer(TestBatchRunner *runner, Analyzer *analyzer,
+                        String *source, Vector *expected,
+                        const char *message) {
+    Token *seed = Token_new(Str_Get_Ptr8(source), Str_Get_Size(source),
+                            0, 0, 1.0f, 1);
+    Inversion *starter = Inversion_new(seed);
+    Inversion *transformed = Analyzer_Transform(analyzer, starter);
+    Vector *got = Vec_new(1);
+    Token *token;
+    while (NULL != (token = Inversion_Next(transformed))) {
+        String *token_text
+            = Str_new_from_utf8(Token_Get_Text(token), Token_Get_Len(token));
+        Vec_Push(got, (Obj*)token_text);
+    }
+    TEST_TRUE(runner, Vec_Equals(expected, (Obj*)got),
+              "Transform(): %s", message);
+    DECREF(transformed);
+
+    transformed = Analyzer_Transform_Text(analyzer, source);
+    Vec_Clear(got);
+    while (NULL != (token = Inversion_Next(transformed))) {
+        String *token_text
+            = Str_new_from_utf8(Token_Get_Text(token), Token_Get_Len(token));
+        Vec_Push(got, (Obj*)token_text);
+    }
+    TEST_TRUE(runner, Vec_Equals(expected, (Obj*)got),
+              "Transform_Text(): %s", message);
+    DECREF(transformed);
+
+    DECREF(got);
+    got = Analyzer_Split(analyzer, source);
+    TEST_TRUE(runner, Vec_Equals(expected, (Obj*)got), "Split(): %s", message);
+
+    DECREF(got);
+    DECREF(starter);
+    DECREF(seed);
+}
+
+FSFolder*
+TestUtils_modules_folder() {
+    static const char *const paths[] = { "modules", "../modules" };
+
+    for (size_t i = 0; i < sizeof(paths) / sizeof(char*); i++) {
+        String *path = Str_newf(paths[i]);
+        FSFolder *modules_folder = FSFolder_new(path);
+        DECREF(path);
+        if (FSFolder_Check(modules_folder)) {
+            return modules_folder;
+        }
+        DECREF(modules_folder);
+    }
+
+    return NULL;
+}
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/TestUtils.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/TestUtils.cfh b/test/Lucy/Test/TestUtils.cfh
new file mode 100644
index 0000000..0d3e15c
--- /dev/null
+++ b/test/Lucy/Test/TestUtils.cfh
@@ -0,0 +1,87 @@
+/* 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 TestLucy;
+
+inert class Lucy::Test::TestUtils  {
+
+    /** Return a Vector of Strings, each representing the content for a
+     * document in the shared collection.
+     */
+    inert incremented Vector*
+    doc_set();
+
+    /** Testing-only TermQuery factory.
+     */
+    inert incremented TermQuery*
+    make_term_query(const char *field, const char *term);
+
+    /** Testing-only PhraseQuery factory.
+     */
+    inert incremented PhraseQuery*
+    make_phrase_query(const char *field, ...);
+
+    /** Testing-only LeafQuery factory.
+     */
+    inert incremented LeafQuery*
+    make_leaf_query(const char *field, const char *term);
+
+    /** Return a new NOTQuery, decrementing the refcount for
+     * `negated_query`.
+     */
+    inert incremented NOTQuery*
+    make_not_query(Query *negated_query);
+
+    inert incremented RangeQuery*
+    make_range_query(const char *field, const char *lower_term = NULL,
+                     const char *upper_term = NULL,
+                     bool include_lower = true,
+                     bool include_upper = true);
+
+    /** Return either an ORQuery or an ANDQuery depending on the value of
+     * `boolop`.  Takes a NULL-terminated list of Query objects.
+     * Decrements the refcounts of all supplied children, under the assumption
+     * that they were created solely for inclusion within the aggregate query.
+     */
+    inert incremented PolyQuery*
+    make_poly_query(uint32_t boolop, ...);
+
+    /** Verify an Analyzer's transform, transform_text, and split methods.
+     */
+    inert void
+    test_analyzer(TestBatchRunner *runner, Analyzer *analyzer, String *source,
+                  Vector *expected, const char *message);
+
+    /** Return the "modules" folder.
+     *
+     * If the folder cannot be found, return NULL.
+     */
+    inert incremented nullable FSFolder*
+    modules_folder();
+}
+
+__C__
+
+#define LUCY_TESTUTILS_BOOLOP_OR  1
+#define LUCY_TESTUTILS_BOOLOP_AND 2
+#ifdef LUCY_USE_SHORT_NAMES
+  #define BOOLOP_OR        LUCY_TESTUTILS_BOOLOP_OR
+  #define BOOLOP_AND       LUCY_TESTUTILS_BOOLOP_AND
+#endif
+
+__END_C__
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestFreezer.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestFreezer.c 
b/test/Lucy/Test/Util/TestFreezer.c
new file mode 100644
index 0000000..2cfba78
--- /dev/null
+++ b/test/Lucy/Test/Util/TestFreezer.c
@@ -0,0 +1,178 @@
+/* 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 TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/Blob.h"
+#include "Clownfish/Boolean.h"
+#include "Clownfish/Num.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Lucy/Test/Util/TestFreezer.h"
+#include "Lucy/Store/InStream.h"
+#include "Lucy/Store/OutStream.h"
+#include "Lucy/Store/RAMFile.h"
+#include "Lucy/Util/Freezer.h"
+
+TestFreezer*
+TestFreezer_new() {
+    return (TestFreezer*)Class_Make_Obj(TESTFREEZER);
+}
+
+// Return the result of round-tripping the object through FREEZE and THAW.
+static Obj*
+S_freeze_thaw(Obj *object) {
+    if (object) {
+        RAMFile *ram_file = RAMFile_new(NULL, false);
+        OutStream *outstream = OutStream_open((Obj*)ram_file);
+        FREEZE(object, outstream);
+        OutStream_Close(outstream);
+        DECREF(outstream);
+
+        InStream *instream = InStream_open((Obj*)ram_file);
+        Obj *retval = THAW(instream);
+        DECREF(instream);
+        DECREF(ram_file);
+        return retval;
+    }
+    else {
+        return NULL;
+    }
+}
+
+// Return the result of round-tripping the object through dump() and load().
+static Obj*
+S_dump_load(Obj *object) {
+    if (object) {
+        Obj *dump = Freezer_dump(object);
+        Obj *loaded = Freezer_load(dump);
+        DECREF(dump);
+        return loaded;
+    }
+    else {
+        return NULL;
+    }
+}
+
+static void
+test_blob(TestBatchRunner *runner) {
+    Blob *wanted = Blob_new("foobar", 6);
+    Blob *got    = (Blob*)S_freeze_thaw((Obj*)wanted);
+    TEST_TRUE(runner, got && Blob_Equals(wanted, (Obj*)got),
+              "Serialization round trip");
+    DECREF(wanted);
+    DECREF(got);
+}
+
+static void
+test_string(TestBatchRunner *runner) {
+    String *wanted = TestUtils_get_str("foo");
+    String *got    = (String*)S_freeze_thaw((Obj*)wanted);
+    TEST_TRUE(runner, got && Str_Equals(wanted, (Obj*)got),
+              "Round trip through FREEZE/THAW");
+    DECREF(got);
+    DECREF(wanted);
+}
+
+static void
+test_hash(TestBatchRunner *runner) {
+    Hash  *wanted = Hash_new(0);
+
+    for (uint32_t i = 0; i < 10; i++) {
+        String *str = TestUtils_random_string(rand() % 1200);
+        Integer *num = Int_new(i);
+        Hash_Store(wanted, str, (Obj*)num);
+        DECREF(str);
+    }
+
+    {
+        Hash *got = (Hash*)S_freeze_thaw((Obj*)wanted);
+        TEST_TRUE(runner, got && Hash_Equals(wanted, (Obj*)got),
+                  "Round trip through serialization.");
+        DECREF(got);
+    }
+
+    {
+        Obj *got = S_dump_load((Obj*)wanted);
+        TEST_TRUE(runner, Hash_Equals(wanted, got),
+                  "Dump => Load round trip");
+        DECREF(got);
+    }
+
+    DECREF(wanted);
+}
+
+static void
+test_num(TestBatchRunner *runner) {
+    Float   *f64 = Float_new(1.33);
+    Integer *i64 = Int_new(-1);
+    Float   *f64_thaw = (Float*)S_freeze_thaw((Obj*)f64);
+    Integer *i64_thaw = (Integer*)S_freeze_thaw((Obj*)i64);
+
+    TEST_TRUE(runner, Float_Equals(f64, (Obj*)f64_thaw),
+              "Float freeze/thaw");
+    TEST_TRUE(runner, Int_Equals(i64, (Obj*)i64_thaw),
+              "Integer freeze/thaw");
+
+#ifdef LUCY_VALGRIND
+    SKIP(runner, 1, "known leaks");
+#else
+    Boolean *true_thaw = (Boolean*)S_freeze_thaw((Obj*)CFISH_TRUE);
+    TEST_TRUE(runner, Bool_Equals(CFISH_TRUE, (Obj*)true_thaw),
+              "Boolean freeze/thaw");
+#endif
+
+    DECREF(i64_thaw);
+    DECREF(f64_thaw);
+    DECREF(i64);
+    DECREF(f64);
+}
+
+static void
+test_varray(TestBatchRunner *runner) {
+    Vector *array = Vec_new(0);
+    Vec_Store(array, 1, (Obj*)Str_newf("foo"));
+    Vec_Store(array, 3, (Obj*)Str_newf("bar"));
+
+    {
+        Obj *got = S_freeze_thaw((Obj*)array);
+        TEST_TRUE(runner, got && Vec_Equals(array, got),
+                  "Round trip through FREEZE/THAW");
+        DECREF(got);
+    }
+
+    {
+        Obj *got = S_dump_load((Obj*)array);
+        TEST_TRUE(runner, got && Vec_Equals(array, got),
+                  "Dump => Load round trip");
+        DECREF(got);
+    }
+
+    DECREF(array);
+}
+
+void
+TestFreezer_Run_IMP(TestFreezer *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 9);
+    test_blob(runner);
+    test_string(runner);
+    test_hash(runner);
+    test_num(runner);
+    test_varray(runner);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestFreezer.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestFreezer.cfh 
b/test/Lucy/Test/Util/TestFreezer.cfh
new file mode 100644
index 0000000..4e7cd90
--- /dev/null
+++ b/test/Lucy/Test/Util/TestFreezer.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 TestLucy;
+
+class Lucy::Test::Util::TestFreezer
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestFreezer*
+    new();
+
+    void
+    Run(TestFreezer *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestIndexFileNames.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestIndexFileNames.c 
b/test/Lucy/Test/Util/TestIndexFileNames.c
new file mode 100644
index 0000000..a9d5708
--- /dev/null
+++ b/test/Lucy/Test/Util/TestIndexFileNames.c
@@ -0,0 +1,73 @@
+/* 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 TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Util/TestIndexFileNames.h"
+#include "Lucy/Util/IndexFileNames.h"
+
+TestIndexFileNames*
+TestIxFileNames_new() {
+    return (TestIndexFileNames*)Class_Make_Obj(TESTINDEXFILENAMES);
+}
+
+static void
+S_test_local_part(TestBatchRunner *runner, const char *source,
+                  const char *wanted, const char *test_name) {
+    String *source_str = SSTR_WRAP_C(source);
+    String *got = IxFileNames_local_part(source_str);
+    TEST_TRUE(runner, Str_Equals_Utf8(got, wanted, strlen(wanted)), test_name);
+    DECREF(got);
+}
+
+static void
+test_local_part(TestBatchRunner *runner) {
+    S_test_local_part(runner, "", "", "simple name");
+    S_test_local_part(runner, "foo.txt", "foo.txt", "name with extension");
+    S_test_local_part(runner, "/foo", "foo", "strip leading slash");
+    S_test_local_part(runner, "/foo/", "foo", "strip trailing slash");
+    S_test_local_part(runner, "foo/bar\\ ", "bar\\ ",
+                      "Include garbage like backslashes and spaces");
+    S_test_local_part(runner, "foo/bar/baz.txt", "baz.txt",
+                      "find last component");
+}
+
+static void
+S_test_extract_gen(TestBatchRunner *runner, const char *name, uint64_t gen,
+                   const char *test_name) {
+    String *source = SSTR_WRAP_C(name);
+    TEST_TRUE(runner, IxFileNames_extract_gen(source) == gen, test_name);
+}
+
+static void
+test_extract_gen(TestBatchRunner *runner) {
+    S_test_extract_gen(runner, "seg_9", 9, "extract_gen");
+    S_test_extract_gen(runner, "seg_9/", 9, "deal with trailing slash");
+    S_test_extract_gen(runner, "seg_9_8", 9, "Only go past first underscore");
+    S_test_extract_gen(runner, "snapshot_5.json", 5, "Deal with file suffix");
+}
+
+void
+TestIxFileNames_Run_IMP(TestIndexFileNames *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 10);
+    test_local_part(runner);
+    test_extract_gen(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestIndexFileNames.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestIndexFileNames.cfh 
b/test/Lucy/Test/Util/TestIndexFileNames.cfh
new file mode 100644
index 0000000..c5ec962
--- /dev/null
+++ b/test/Lucy/Test/Util/TestIndexFileNames.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 TestLucy;
+
+class Lucy::Test::Util::TestIndexFileNames nickname TestIxFileNames
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestIndexFileNames*
+    new();
+
+    void
+    Run(TestIndexFileNames *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestJson.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestJson.c b/test/Lucy/Test/Util/TestJson.c
new file mode 100644
index 0000000..c67e292
--- /dev/null
+++ b/test/Lucy/Test/Util/TestJson.c
@@ -0,0 +1,365 @@
+/* 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 TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/Num.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Util/TestJson.h"
+#include "Lucy/Util/Json.h"
+#include "Lucy/Store/FileHandle.h"
+#include "Lucy/Store/RAMFolder.h"
+
+TestJson*
+TestJson_new() {
+    return (TestJson*)Class_Make_Obj(TESTJSON);
+}
+
+// Create a test data structure including at least one each of Hash, Vector,
+// and String.
+static Obj*
+S_make_dump() {
+    Hash *dump = Hash_new(0);
+    Hash_Store_Utf8(dump, "foo", 3, (Obj*)Str_newf("foo"));
+    Hash_Store_Utf8(dump, "stuff", 5, (Obj*)Vec_new(0));
+    return (Obj*)dump;
+}
+
+static void
+test_tolerance(TestBatchRunner *runner) {
+    String *foo = Str_newf("foo");
+    String *not_json = Json_to_json((Obj*)foo);
+    TEST_TRUE(runner, not_json == NULL,
+              "to_json returns NULL when fed invalid data type");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "to_json sets global error when fed invalid data type");
+    DECREF(foo);
+}
+
+// Test escapes for control characters ASCII 0-31.
+static const char* control_escapes[] = {
+    "\\u0000",
+    "\\u0001",
+    "\\u0002",
+    "\\u0003",
+    "\\u0004",
+    "\\u0005",
+    "\\u0006",
+    "\\u0007",
+    "\\b",
+    "\\t",
+    "\\n",
+    "\\u000b",
+    "\\f",
+    "\\r",
+    "\\u000e",
+    "\\u000f",
+    "\\u0010",
+    "\\u0011",
+    "\\u0012",
+    "\\u0013",
+    "\\u0014",
+    "\\u0015",
+    "\\u0016",
+    "\\u0017",
+    "\\u0018",
+    "\\u0019",
+    "\\u001a",
+    "\\u001b",
+    "\\u001c",
+    "\\u001d",
+    "\\u001e",
+    "\\u001f",
+    NULL
+};
+
+// Test quote and backslash escape in isolation, then in context.
+static const char* quote_escapes_source[] = {
+    "\"",
+    "\\",
+    "abc\"",
+    "abc\\",
+    "\"xyz",
+    "\\xyz",
+    "\\\"",
+    "\"\\",
+    NULL
+};
+static const char* quote_escapes_json[] = {
+    "\\\"",
+    "\\\\",
+    "abc\\\"",
+    "abc\\\\",
+    "\\\"xyz",
+    "\\\\xyz",
+    "\\\\\\\"",
+    "\\\"\\\\",
+    NULL
+};
+
+static void
+test_escapes(TestBatchRunner *runner) {
+    for (int i = 0; control_escapes[i] != NULL; i++) {
+        String     *string  = Str_new_from_char(i);
+        const char *escaped = control_escapes[i];
+        String     *json    = Json_to_json((Obj*)string);
+        String     *trimmed = Str_Trim(json);
+        String     *decoded = (String*)Json_from_json(json);
+
+        String *json_wanted = Str_newf("\"%s\"", escaped);
+        TEST_TRUE(runner, Str_Equals(json_wanted, (Obj*)trimmed),
+                  "encode control escape: %s", escaped);
+
+        TEST_TRUE(runner, decoded != NULL && Str_Equals(string, (Obj*)decoded),
+                  "decode control escape: %s", escaped);
+
+        DECREF(string);
+        DECREF(json);
+        DECREF(trimmed);
+        DECREF(decoded);
+        DECREF(json_wanted);
+    }
+
+    for (int i = 0; quote_escapes_source[i] != NULL; i++) {
+        const char *source  = quote_escapes_source[i];
+        const char *escaped = quote_escapes_json[i];
+        String *string  = Str_new_from_utf8(source, strlen(source));
+        String *json    = Json_to_json((Obj*)string);
+        String *trimmed = Str_Trim(json);
+        String *decoded = (String*)Json_from_json(json);
+
+        String *json_wanted = Str_newf("\"%s\"", escaped);
+        TEST_TRUE(runner, Str_Equals(json_wanted, (Obj*)trimmed),
+                  "encode quote/backslash escapes: %s", source);
+
+        TEST_TRUE(runner, decoded != NULL && Str_Equals(string, (Obj*)decoded),
+                  "decode quote/backslash escapes: %s", source);
+
+        DECREF(string);
+        DECREF(json);
+        DECREF(trimmed);
+        DECREF(decoded);
+        DECREF(json_wanted);
+    }
+}
+
+static void
+test_numbers(TestBatchRunner *runner) {
+    Integer *i64     = Int_new(33);
+    String  *json    = Json_to_json((Obj*)i64);
+    String  *trimmed = Str_Trim(json);
+    TEST_TRUE(runner, Str_Equals_Utf8(trimmed, "33", 2), "Integer");
+    DECREF(json);
+    DECREF(trimmed);
+
+    Float *f64 = Float_new(33.33);
+    json = Json_to_json((Obj*)f64);
+    if (json) {
+        double value = Str_To_F64(json);
+        double diff = 33.33 - value;
+        if (diff < 0.0) { diff = 0.0 - diff; }
+        TEST_TRUE(runner, diff < 0.0001, "Float");
+        DECREF(json);
+    }
+    else {
+        FAIL(runner, "Float conversion to  json  failed.");
+    }
+
+    DECREF(i64);
+    DECREF(f64);
+}
+
+static void
+test_to_and_from(TestBatchRunner *runner) {
+    Obj *dump = S_make_dump();
+    String *json = Json_to_json(dump);
+    Obj *got = Json_from_json(json);
+    TEST_TRUE(runner, got != NULL && Obj_Equals(dump, got),
+              "Round trip through to_json and from_json");
+    DECREF(dump);
+    DECREF(json);
+    DECREF(got);
+}
+
+static void
+test_spew_and_slurp(TestBatchRunner *runner) {
+    Obj *dump = S_make_dump();
+    Folder *folder = (Folder*)RAMFolder_new(NULL);
+
+    String *foo = SSTR_WRAP_C("foo");
+    bool result = Json_spew_json(dump, folder, foo);
+    TEST_TRUE(runner, result, "spew_json returns true on success");
+    TEST_TRUE(runner, Folder_Exists(folder, foo),
+              "spew_json wrote file");
+
+    Obj *got = Json_slurp_json(folder, foo);
+    TEST_TRUE(runner, got && Obj_Equals(dump, got),
+              "Round trip through spew_json and slurp_json");
+    DECREF(got);
+
+    Err_set_error(NULL);
+    result = Json_spew_json(dump, folder, foo);
+    TEST_FALSE(runner, result, "Can't spew_json when file exists");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Failed spew_json sets global error");
+
+    Err_set_error(NULL);
+    String *bar = SSTR_WRAP_C("bar");
+    got = Json_slurp_json(folder, bar);
+    TEST_TRUE(runner, got == NULL,
+              "slurp_json returns NULL when file doesn't exist");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Failed slurp_json sets global error");
+
+    String *boffo = SSTR_WRAP_C("boffo");
+
+    FileHandle *fh
+        = Folder_Open_FileHandle(folder, boffo, FH_CREATE | FH_WRITE_ONLY);
+    FH_Write(fh, "garbage", 7);
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    got = Json_slurp_json(folder, boffo);
+    TEST_TRUE(runner, got == NULL,
+              "slurp_json returns NULL when file doesn't contain valid JSON");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Failed slurp_json sets global error");
+    DECREF(got);
+
+    DECREF(dump);
+    DECREF(folder);
+}
+
+static void
+S_verify_bad_syntax(TestBatchRunner *runner, const char *bad, const char 
*mess) {
+    String *has_errors = SSTR_WRAP_C(bad);
+    Err_set_error(NULL);
+    Obj *not_json = Json_from_json(has_errors);
+    TEST_TRUE(runner, not_json == NULL, "from_json returns NULL: %s", mess);
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "from_json sets global error: %s", mess);
+}
+
+static void
+test_syntax_errors(TestBatchRunner *runner) {
+    S_verify_bad_syntax(runner, "[", "unclosed left bracket");
+    S_verify_bad_syntax(runner, "]", "unopened right bracket");
+    S_verify_bad_syntax(runner, "{", "unclosed left curly");
+    S_verify_bad_syntax(runner, "}", "unopened right curly");
+    S_verify_bad_syntax(runner, "{}[]", "two top-level objects");
+    S_verify_bad_syntax(runner, "[1 \"foo\"]", "missing comma in array");
+    S_verify_bad_syntax(runner, "[1, \"foo\",]", "extra comma in array");
+    S_verify_bad_syntax(runner, "{\"1\":1 \"2\":2}", "missing comma in hash");
+    S_verify_bad_syntax(runner, "{\"1\":1,\"2\":2,}", "extra comma in hash");
+    S_verify_bad_syntax(runner, "\"1", "unterminated string");
+    // Tolerated by strtod().
+    // S_verify_bad_syntax(runner, "1. ", "float missing fraction");
+    // S_verify_bad_syntax(runner, "-.3 ", "Number missing integral part");
+    S_verify_bad_syntax(runner, "-. ", "Number missing any digits");
+    S_verify_bad_syntax(runner, "+1.0 ", "float with prepended plus");
+    S_verify_bad_syntax(runner, "\"\\g\"", "invalid char escape");
+    S_verify_bad_syntax(runner, "\"\\uAAAZ\"", "invalid \\u escape");
+}
+
+static void
+S_round_trip_integer(TestBatchRunner *runner, int64_t value) {
+    Integer *num = Int_new(value);
+    Vector *array = Vec_new(1);
+    Vec_Store(array, 0, (Obj*)num);
+    String *json = Json_to_json((Obj*)array);
+    Obj *dump = Json_from_json(json);
+    TEST_TRUE(runner, Vec_Equals(array, dump), "Round trip integer %ld",
+              (long)value);
+    DECREF(dump);
+    DECREF(json);
+    DECREF(array);
+}
+
+static void
+test_integers(TestBatchRunner *runner) {
+    S_round_trip_integer(runner, 0);
+    S_round_trip_integer(runner, -1);
+    S_round_trip_integer(runner, -1000000);
+    S_round_trip_integer(runner, 1000000);
+}
+
+static void
+S_round_trip_float(TestBatchRunner *runner, double value, double max_diff) {
+    Float *num = Float_new(value);
+    Vector *array = Vec_new(1);
+    Vec_Store(array, 0, (Obj*)num);
+    String *json = Json_to_json((Obj*)array);
+    Obj *dump = CERTIFY(Json_from_json(json), VECTOR);
+    Float *got = (Float*)CERTIFY(Vec_Fetch((Vector*)dump, 0), FLOAT);
+    double diff = Float_Get_Value(num) - Float_Get_Value(got);
+    if (diff < 0) { diff = 0 - diff; }
+    TEST_TRUE(runner, diff <= max_diff, "Round trip float %f", value);
+    DECREF(dump);
+    DECREF(json);
+    DECREF(array);
+}
+
+static void
+test_floats(TestBatchRunner *runner) {
+    S_round_trip_float(runner, 0.0, 0.0);
+    S_round_trip_float(runner, 0.1, 0.00001);
+    S_round_trip_float(runner, -0.1, 0.00001);
+    S_round_trip_float(runner, 1000000.5, 1.0);
+    S_round_trip_float(runner, -1000000.5, 1.0);
+}
+
+static void
+test_max_depth(TestBatchRunner *runner) {
+    Hash *circular = Hash_new(0);
+    Hash_Store_Utf8(circular, "circular", 8, INCREF(circular));
+    Err_set_error(NULL);
+    String *not_json = Json_to_json((Obj*)circular);
+    TEST_TRUE(runner, not_json == NULL,
+              "to_json returns NULL when fed recursing data");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "to_json sets global error when fed recursing data");
+    DECREF(Hash_Delete_Utf8(circular, "circular", 8));
+    DECREF(circular);
+}
+
+void
+TestJson_Run_IMP(TestJson *self, TestBatchRunner *runner) {
+    uint32_t num_tests = 105;
+#ifndef LUCY_VALGRIND
+    num_tests += 28; // FIXME: syntax errors leak memory.
+#endif
+    TestBatchRunner_Plan(runner, (TestBatch*)self, num_tests);
+
+    // Test tolerance, then liberalize for testing.
+    test_tolerance(runner);
+    Json_set_tolerant(true);
+
+    test_to_and_from(runner);
+    test_escapes(runner);
+    test_numbers(runner);
+    test_spew_and_slurp(runner);
+    test_integers(runner);
+    test_floats(runner);
+    test_max_depth(runner);
+
+#ifndef LUCY_VALGRIND
+    test_syntax_errors(runner);
+#endif
+}
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestJson.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestJson.cfh b/test/Lucy/Test/Util/TestJson.cfh
new file mode 100644
index 0000000..ddba309
--- /dev/null
+++ b/test/Lucy/Test/Util/TestJson.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 TestLucy;
+
+class Lucy::Test::Util::TestJson
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestJson*
+    new();
+
+    void
+    Run(TestJson *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestMemoryPool.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestMemoryPool.c 
b/test/Lucy/Test/Util/TestMemoryPool.c
new file mode 100644
index 0000000..7e960ab
--- /dev/null
+++ b/test/Lucy/Test/Util/TestMemoryPool.c
@@ -0,0 +1,61 @@
+/* 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_TESTLUCY_TESTMEMORYPOOL
+#define C_LUCY_MEMORYPOOL
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Util/TestMemoryPool.h"
+#include "Lucy/Util/MemoryPool.h"
+
+TestMemoryPool*
+TestMemPool_new() {
+    return (TestMemoryPool*)Class_Make_Obj(TESTMEMORYPOOL);
+}
+
+void
+TestMemPool_Run_IMP(TestMemoryPool *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 5);
+
+    MemoryPool *mem_pool = MemPool_new(0);
+    MemoryPoolIVARS *const ivars = MemPool_IVARS(mem_pool);
+    char *ptr_a, *ptr_b;
+
+    ptr_a = (char*)MemPool_Grab(mem_pool, 10);
+    size_t expected = sizeof(void*) == 8 ? 16 : 12;
+    TEST_UINT_EQ(runner, MemPool_Get_Consumed(mem_pool), expected,
+                 "Round up allocation to word size");
+    ptr_b = (char*)MemPool_Grab(mem_pool, 10);
+    TEST_UINT_EQ(runner, MemPool_Get_Consumed(mem_pool), expected * 2,
+                 "Accumulate consumed.");
+
+    ptr_a = ivars->buf;
+    MemPool_Resize(mem_pool, ptr_b, 6);
+    TEST_TRUE(runner, ivars->buf < ptr_a, "Resize adjusts next allocation");
+    TEST_TRUE(runner, MemPool_Get_Consumed(mem_pool) < expected * 2,
+                "Resize() adjusts `consumed`");
+
+    MemPool_Release_All(mem_pool);
+    TEST_UINT_EQ(runner, MemPool_Get_Consumed(mem_pool), 0,
+                 "Release_All() resets `consumed`");
+
+    DECREF(mem_pool);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Util/TestMemoryPool.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Util/TestMemoryPool.cfh 
b/test/Lucy/Test/Util/TestMemoryPool.cfh
new file mode 100644
index 0000000..f3d2433
--- /dev/null
+++ b/test/Lucy/Test/Util/TestMemoryPool.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 TestLucy;
+
+class Lucy::Test::Util::TestMemoryPool nickname TestMemPool
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestMemoryPool*
+    new();
+
+    void
+    Run(TestMemoryPool *self, TestBatchRunner *runner);
+}
+
+

Reply via email to