Merge branch 'separate-tests'
diff --git a/c/.gitignore b/c/.gitignore
index 7873433..e28cfd4 100644
--- a/c/.gitignore
+++ b/c/.gitignore
@@ -1,16 +1,13 @@
+*.dll
+*.dll.a
+*.dylib
+*.exe
+*.exp
+*.lib
+*.so
+*.so.*
 /Makefile
 /autogen/
 /charmonizer
-/charmonizer.exe
 /charmony.h
-/cyglucy-*.dll
-/liblucy-*.dll
-/liblucy.*.dylib
-/liblucy.dylib
-/liblucy.so
-/liblucy.so.*
-/lucy-*.dll
-/lucy-*.exp
-/lucy-*.lib
 /t/test_lucy
-/t/test_lucy.exe
diff --git a/common/charmonizer.c b/common/charmonizer.c
index d99bbbf..b916fd3 100644
--- a/common/charmonizer.c
+++ b/common/charmonizer.c
@@ -650,8 +650,10 @@
 typedef struct chaz_MakeRule chaz_MakeRule;
 typedef struct chaz_MakeBinary chaz_MakeBinary;
 
-typedef void (*chaz_Make_list_files_callback_t)(const char *dir, char *file,
-                                                void *context);
+typedef void
+(*chaz_Make_file_callback_t)(const char *dir, char *file, void *context);
+typedef int
+(*chaz_Make_file_filter_t)(const char *dir, char *file, void *context);
 
 /** Initialize the environment.
  *
@@ -685,7 +687,7 @@
  */
 void
 chaz_Make_list_files(const char *dir, const char *ext,
-                     chaz_Make_list_files_callback_t callback, void *context);
+                     chaz_Make_file_callback_t callback, void *context);
 
 /** MakeFile constructor.
  */
@@ -810,15 +812,6 @@
 void
 chaz_MakeRule_add_command(chaz_MakeRule *self, const char *command);
 
-/** Add a command to be executed with a special runtime library path.
- *
- * @param command The additional command.
- * @param ... NULL-terminated list of library directories.
- */
-void
-chaz_MakeRule_add_command_with_libpath(chaz_MakeRule *self,
-                                       const char *command, ...);
-
 /** Add a command to remove one or more files.
  *
  * @param files The list of files.
@@ -858,6 +851,19 @@
 void
 chaz_MakeBinary_add_src_dir(chaz_MakeBinary *self, const char *path);
 
+/** Add .c files in a directory as sources for the binary if they match
+ * a filter.
+ *
+ * @param path The path to the directory.
+ * @param filter A callback that is invoked for every source file. The
+ * source file is only added if the callback returns true. May be NULL.
+ * @param context Context passed to filter.
+ */
+void
+chaz_MakeBinary_add_filtered_src_dir(chaz_MakeBinary *self, const char *path,
+                                     chaz_Make_file_filter_t filter,
+                                     void *context);
+
 /** Add a prerequisite to the make rule of the binary.
  *
  * @param prereq The prerequisite.
@@ -4551,6 +4557,12 @@
     size_t            num_binaries;
 };
 
+typedef struct {
+    chaz_MakeBinary         *binary;
+    chaz_Make_file_filter_t  filter;
+    void                    *filter_ctx;
+} chaz_MakeBinaryContext;
+
 /* Static vars. */
 static struct {
     char *make_command;
@@ -5373,58 +5385,6 @@
 }
 
 void
-chaz_MakeRule_add_command_with_libpath(chaz_MakeRule *self,
-                                       const char *command, ...) {
-    va_list args;
-    char *path        = NULL;
-    char *lib_command = NULL;
-    int binfmt = chaz_CC_binary_format();
-
-    if (binfmt == CHAZ_CC_BINFMT_ELF) {
-        va_start(args, command);
-        path = chaz_Util_vjoin(":", args);
-        va_end(args);
-
-        lib_command = chaz_Util_join("", "LD_LIBRARY_PATH=", path,
-                                     ":$$LD_LIBRARY_PATH ", command, NULL);
-
-        free(path);
-    }
-    else if (binfmt == CHAZ_CC_BINFMT_PE) {
-        if (chaz_Make.shell_type == CHAZ_OS_CMD_EXE) {
-            va_start(args, command);
-            path = chaz_Util_vjoin(";", args);
-            va_end(args);
-
-            /* It's important to not add a space before `&&`. Otherwise, the
-             * space is added to the search path.
-             */
-            lib_command = chaz_Util_join("", "path ", path, ";%path%&& ",
-                                         command, NULL);
-        }
-        else {
-            va_start(args, command);
-            path = chaz_Util_vjoin(":", args);
-            va_end(args);
-
-            lib_command = chaz_Util_join("", "PATH=", path, ":$$PATH ",
-                                         command, NULL);
-        }
-
-        free(path);
-    }
-    else {
-        /* Assume that library paths are compiled into the executable on
-         * Darwin.
-         */
-        lib_command = chaz_Util_strdup(command);
-    }
-
-    chaz_MakeRule_add_command(self, lib_command);
-    free(lib_command);
-}
-
-void
 chaz_MakeRule_add_rm_command(chaz_MakeRule *self, const char *files) {
     char *command;
 
@@ -5552,6 +5512,14 @@
 
 void
 chaz_MakeBinary_add_src_dir(chaz_MakeBinary *self, const char *path) {
+    chaz_MakeBinary_add_filtered_src_dir(self, path, NULL, NULL);
+}
+
+void
+chaz_MakeBinary_add_filtered_src_dir(chaz_MakeBinary *self, const char *path,
+                                     chaz_Make_file_filter_t filter,
+                                     void *filter_ctx) {
+    chaz_MakeBinaryContext context;
     size_t num_dirs = self->num_dirs;
     char **dirs = (char**)realloc(self->dirs, (num_dirs + 2) * sizeof(char*));
 
@@ -5560,18 +5528,27 @@
     self->dirs     = dirs;
     self->num_dirs = num_dirs + 1;
 
+    context.binary     = self;
+    context.filter     = filter;
+    context.filter_ctx = filter_ctx;
+
     chaz_Make_list_files(path, "c", S_chaz_MakeBinary_list_files_callback,
-                         self);
+                         &context);
 }
 
 static void
 S_chaz_MakeBinary_list_files_callback(const char *dir, char *file,
-                                      void *context) {
+                                      void *vcontext) {
+    chaz_MakeBinaryContext *context = (chaz_MakeBinaryContext*)vcontext;
     const char *dir_sep = chaz_OS_dir_sep();
-    char *path = chaz_Util_join(dir_sep, dir, file, NULL);
 
-    S_chaz_MakeBinary_do_add_src_file((chaz_MakeBinary*)context, path);
-    free(path);
+    if (context->filter == NULL
+        || context->filter(dir, file, context->filter_ctx) != 0
+       ) {
+        char *path = chaz_Util_join(dir_sep, dir, file, NULL);
+        S_chaz_MakeBinary_do_add_src_file(context->binary, path);
+        free(path);
+    }
 }
 
 static void
@@ -5661,7 +5638,7 @@
 
 void
 chaz_Make_list_files(const char *dir, const char *ext,
-                     chaz_Make_list_files_callback_t callback, void *context) {
+                     chaz_Make_file_callback_t callback, void *context) {
     int         shell_type = chaz_OS_shell_type();
     const char *pattern;
     char       *command;
@@ -8650,10 +8627,12 @@
     chaz_CLI        *cli;
     chaz_MakeFile   *makefile;
     chaz_MakeBinary *lib;
+    chaz_MakeBinary *test_lib;
 
     /* Directories. */
     const char *base_dir;
     char       *core_dir;
+    char       *test_dir;
     const char *host_src_dir;
     char       *autogen_src_dir;
     char       *autogen_inc_dir;
@@ -8666,9 +8645,8 @@
     char       *utf8proc_dir;
     char       *json_dir;
 
-    /* Files. */
-    char        *autogen_target;
-    const char **autogen_src_files;
+    /* Targets. */
+    char *autogen_target;
 
     /* Clownfish library. */
     char       *cfish_lib_dir;
@@ -8700,6 +8678,9 @@
 static void
 lucy_MakeFile_write_c_test_rules(lucy_MakeFile *self);
 
+static int
+S_core_dir_filter(const char *dir, char *file, void *context);
+
 static void
 S_cfh_file_callback(const char *dir, char *file, void *context);
 
@@ -8831,26 +8812,37 @@
 static lucy_MakeFile*
 lucy_MakeFile_new(chaz_CLI *cli) {
     const char *dir_sep      = chaz_OS_dir_sep();
+    const char *host         = chaz_CLI_strval(cli, "host");
     const char *cfish_prefix = chaz_CLI_strval(cli, "clownfish-prefix");
-
+    char *cfcore_filename = chaz_Util_join(dir_sep, "cfcore", "Lucy.cfp",
+                                           NULL);
     lucy_MakeFile *self = malloc(sizeof(lucy_MakeFile));
 
     self->cli      = cli;
     self->makefile = chaz_MakeFile_new();
     self->lib      = NULL;
+    self->test_lib = NULL;
 
     /* Initialize directories. */
-    self->base_dir = "..";
-    self->core_dir = chaz_Util_join(dir_sep, self->base_dir, "core", NULL);
-    if (chaz_CLI_defined(cli, "enable-perl")) {
-        self->host_src_dir = "xs";
+    if (chaz_Util_can_open_file(cfcore_filename)) {
+        self->base_dir = ".";
+        self->core_dir = chaz_Util_strdup("cfcore");
+        self->test_dir = chaz_Util_strdup("cftest");
     }
-	else if (chaz_CLI_defined(cli, "enable-go")) {
-        self->host_src_dir = "cfext";
-	}
     else {
+        self->base_dir = "..";
+        self->core_dir = chaz_Util_join(dir_sep, self->base_dir, "core", NULL);
+        self->test_dir = chaz_Util_join(dir_sep, self->base_dir, "test", NULL);
+    }
+    if (strcmp(host, "go") == 0) {
+        self->host_src_dir = "cfext";
+    }
+    else if (strcmp(host, "c") == 0) {
         self->host_src_dir = "src";
     }
+    else {
+        self->host_src_dir = NULL;
+    }
     self->autogen_src_dir = chaz_Util_join(dir_sep, "autogen", "source", NULL);
     self->autogen_inc_dir
         = chaz_Util_join(dir_sep, "autogen", "include", NULL);
@@ -8874,25 +8866,7 @@
         = chaz_Util_join(dir_sep, self->core_dir, "Lucy", "Util", "Json",
                          NULL);
 
-    /* Initialize file names. */
-    if (chaz_CLI_defined(cli, "enable-perl")) {
-        static const char *perl_autogen_src_files[] = {
-            "boot.c",
-            "callbacks.c",
-            "lucy_parcel.c",
-            "testlucy_parcel.c",
-            NULL
-        };
-        self->autogen_src_files = perl_autogen_src_files;
-    }
-    else {
-        static const char *c_autogen_src_files[] = {
-            "lucy_parcel.c",
-            "testlucy_parcel.c",
-            NULL
-        };
-        self->autogen_src_files = c_autogen_src_files;
-    }
+    /* Initialize targets. */
     self->autogen_target
         = chaz_Util_join(dir_sep, "autogen", "hierarchy.json", NULL);
 
@@ -8919,6 +8893,7 @@
     chaz_MakeFile_destroy(self->makefile);
 
     free(self->core_dir);
+    free(self->test_dir);
     free(self->autogen_inc_dir);
     free(self->autogen_src_dir);
     free(self->lemon_dir);
@@ -8943,6 +8918,9 @@
     const char *host     = chaz_CLI_strval(self->cli, "host");
     const char *math_lib = chaz_Floats_math_library();
 
+    const char *lib_objs      = NULL;
+    const char *test_lib_objs = NULL;
+
     chaz_MakeVar  *var;
     chaz_MakeRule *rule;
 
@@ -8952,7 +8930,6 @@
     chaz_CFlags *link_flags;
 
     char *scratch;
-    int   i;
 
     printf("Creating Makefile...\n");
 
@@ -8987,25 +8964,20 @@
 
     chaz_CFlags_destroy(makefile_cflags);
 
-    /* Binary. */
+    /* Core library. */
 
     if (strcmp(host, "c") == 0 || strcmp(host, "perl") == 0) {
+        /* Shared library for C and Perl. */
+
         chaz_MakeFile_add_rule(self->makefile, "all", "$(LUCY_SHARED_LIB)");
 
         self->lib
             = chaz_MakeFile_add_shared_lib(self->makefile, NULL, "lucy",
                                            lucy_version, lucy_major_version);
-        rule = chaz_MakeFile_add_rule(self->makefile,
-                                      "$(LUCY_SHARED_LIB_OBJS)",
-                                      self->autogen_target);
-        /*
-         * The dependency is actually on JsonParser.h, but make doesn't cope
-         * well with multiple output files.
-         */
-        scratch = chaz_Util_join(dir_sep, self->json_dir, "JsonParser.c",
-                                 NULL);
-        chaz_MakeRule_add_prereq(rule, scratch);
-        free(scratch);
+        lib_objs = "$(LUCY_SHARED_LIB_OBJS)";
+
+        compile_flags = chaz_MakeBinary_get_compile_flags(self->lib);
+        chaz_CFlags_add_define(compile_flags, "CFP_LUCY", NULL);
 
         link_flags = chaz_MakeBinary_get_link_flags(self->lib);
         chaz_CFlags_enable_debugging(link_flags);
@@ -9024,35 +8996,87 @@
         }
     }
     else {
-        chaz_MakeFile_add_rule(self->makefile, "static", "$(LUCY_STATIC_LIB)");
+        /* Static library for Go and Python. */
+
+        chaz_MakeFile_add_rule(self->makefile, "static",
+                               "$(LUCY_STATIC_LIB) $(TESTLUCY_STATIC_LIB)");
 
         self->lib
             = chaz_MakeFile_add_static_lib(self->makefile, NULL, "lucy");
-        rule = chaz_MakeFile_add_rule(self->makefile,
-                                      "$(LUCY_STATIC_LIB_OBJS)",
-                                      self->autogen_target);
-        scratch = chaz_Util_join(dir_sep, self->json_dir, "JsonParser.c",
-                                 NULL);
-        chaz_MakeRule_add_prereq(rule, scratch);
-        free(scratch);
+        lib_objs = "$(LUCY_STATIC_LIB_OBJS)";
+
+        if (strcmp(host, "python") == 0) {
+            /* For Python, the static library is linked into a shared
+             * library.
+             */
+            compile_flags = chaz_MakeBinary_get_compile_flags(self->lib);
+            chaz_CFlags_compile_shared_library(compile_flags);
+            chaz_CFlags_add_define(compile_flags, "CFP_CFISH", NULL);
+        }
     }
 
-    chaz_MakeBinary_add_src_dir(self->lib, self->host_src_dir);
-    chaz_MakeBinary_add_src_dir(self->lib, self->core_dir);
+    if (self->host_src_dir != NULL) {
+        chaz_MakeBinary_add_src_dir(self->lib, self->host_src_dir);
+    }
+    chaz_MakeBinary_add_filtered_src_dir(self->lib, self->core_dir,
+                                         S_core_dir_filter, NULL);
     chaz_MakeBinary_add_src_dir(self->lib, self->snowstem_dir);
     chaz_MakeBinary_add_src_dir(self->lib, self->snowstop_dir);
     chaz_MakeBinary_add_src_dir(self->lib, self->utf8proc_dir);
-
     chaz_MakeBinary_add_src_file(self->lib, self->json_dir, "JsonParser.c");
+    chaz_MakeBinary_add_src_file(self->lib, self->autogen_src_dir,
+                                 "lucy_parcel.c");
 
-    for (i = 0; self->autogen_src_files[i] != NULL; ++i) {
-        chaz_MakeBinary_add_src_file(self->lib, self->autogen_src_dir,
-                                     self->autogen_src_files[i]);
+    /* Test library. */
+
+    if (strcmp(host, "c") == 0 || strcmp(host, "perl") == 0) {
+        /* Shared library for C and Perl. */
+
+        self->test_lib
+            = chaz_MakeFile_add_shared_lib(self->makefile, NULL, "testlucy",
+                                           lucy_version, lucy_major_version);
+        test_lib_objs = "$(TESTLUCY_SHARED_LIB_OBJS)";
+
+        compile_flags = chaz_MakeBinary_get_compile_flags(self->test_lib);
+        chaz_CFlags_add_define(compile_flags, "CFP_TESTLUCY", NULL);
+
+        link_flags = chaz_MakeBinary_get_link_flags(self->test_lib);
+        chaz_CFlags_enable_debugging(link_flags);
+        if (self->cfish_lib_dir) {
+            chaz_CFlags_add_library_path(link_flags, self->cfish_lib_dir);
+        }
+        chaz_CFlags_add_shared_lib(link_flags, NULL, "lucy",
+                                   lucy_major_version);
+        chaz_CFlags_add_external_lib(link_flags, self->cfish_lib_name);
+        if (math_lib) {
+            chaz_CFlags_add_external_lib(link_flags, math_lib);
+        }
+        if (chaz_CLI_defined(self->cli, "enable-coverage")) {
+            chaz_CFlags_enable_code_coverage(link_flags);
+        }
+
+        chaz_MakeBinary_add_prereq(self->test_lib, "$(LUCY_SHARED_LIB)");
+    }
+    else {
+        /* Static library for Go and Python. */
+
+        self->test_lib
+            = chaz_MakeFile_add_static_lib(self->makefile, NULL, "testlucy");
+        test_lib_objs = "$(TESTLUCY_STATIC_LIB_OBJS)";
+
+        if (strcmp(host, "python") == 0) {
+            /* For Python, the static library is linked into a shared
+             * library.
+             */
+            compile_flags = chaz_MakeBinary_get_compile_flags(self->test_lib);
+            chaz_CFlags_compile_shared_library(compile_flags);
+            chaz_CFlags_add_define(compile_flags, "CFP_TESTLUCY", NULL);
+        }
     }
 
-    compile_flags = chaz_MakeBinary_get_compile_flags(self->lib);
-    chaz_CFlags_add_define(compile_flags, "CFP_LUCY", NULL);
-    chaz_CFlags_add_define(compile_flags, "CFP_TESTLUCY", NULL);
+    chaz_MakeBinary_add_src_dir(self->test_lib, self->test_dir);
+    chaz_MakeBinary_add_src_file(self->test_lib, self->autogen_src_dir,
+                                 "testlucy_parcel.c");
 
     /* Additional rules. */
 
@@ -9061,28 +9085,53 @@
     chaz_MakeFile_add_lemon_grammar(self->makefile, scratch);
     free(scratch);
 
+    /* Object files depend on autogenerated headers. */
+    rule = chaz_MakeFile_add_rule(self->makefile, lib_objs,
+                                  self->autogen_target);
+    /*
+     * The dependency is actually on JsonParser.h, but make doesn't cope
+     * well with multiple output files.
+     */
+    scratch = chaz_Util_join(dir_sep, self->json_dir, "JsonParser.c",
+                             NULL);
+    chaz_MakeRule_add_prereq(rule, scratch);
+    free(scratch);
+    chaz_MakeFile_add_rule(self->makefile, test_lib_objs,
+                           self->autogen_target);
+
     if (strcmp(host, "c") == 0) {
         lucy_MakeFile_write_c_cfc_rules(self);
         lucy_MakeFile_write_c_test_rules(self);
     }
 
-    /* Needed for parallel builds. */
-    for (i = 0; self->autogen_src_files[i] != NULL; ++i) {
-        char *path = chaz_Util_join("", self->autogen_src_dir, dir_sep,
-                                    self->autogen_src_files[i], NULL);
-        rule = chaz_MakeFile_add_rule(self->makefile, path,
-                                      self->autogen_target);
-        free(path);
-    }
+    /* Targets to compile object files for Perl. */
+    if (strcmp(host, "perl") == 0) {
+        char *objects;
 
-    rule = chaz_MakeFile_clean_rule(self->makefile);
-    chaz_MakeRule_add_recursive_rm_command(rule, "autogen");
+        chaz_MakeFile_add_rule(self->makefile, "core_objects",
+                               "$(LUCY_SHARED_LIB_OBJS)");
+        objects = chaz_MakeBinary_obj_string(self->lib);
+        chaz_ConfWriter_add_def("CORE_OBJECTS", objects);
+        free(objects);
+
+        chaz_MakeFile_add_rule(self->makefile, "test_objects",
+                               "$(TESTLUCY_SHARED_LIB_OBJS)");
+        objects = chaz_MakeBinary_obj_string(self->test_lib);
+        chaz_ConfWriter_add_def("TEST_OBJECTS", objects);
+        free(objects);
+    }
 
     chaz_MakeFile_write(self->makefile);
 }
 
 static void
 lucy_MakeFile_write_c_cfc_rules(lucy_MakeFile *self) {
+    static const char *const autogen_src_files[] = {
+        "lucy_parcel.c",
+        "testlucy_parcel.c",
+        NULL
+    };
+
     SourceFileContext sfc;
     chaz_MakeRule *rule;
 
@@ -9091,50 +9140,73 @@
 
     char *cfc_command;
 
+    int i;
+
     sfc.var = chaz_MakeFile_add_var(self->makefile, "CLOWNFISH_HEADERS", NULL);
     chaz_Make_list_files(self->core_dir, "cfh", S_cfh_file_callback, &sfc);
+    chaz_Make_list_files(self->test_dir, "cfh", S_cfh_file_callback, &sfc);
 
     rule = chaz_MakeFile_add_rule(self->makefile, self->autogen_target, NULL);
     chaz_MakeRule_add_prereq(rule, "$(CLOWNFISH_HEADERS)");
     if (cfish_prefix == NULL) {
         cfc_command
             = chaz_Util_join("", "cfc --source=", self->core_dir,
+                             " --source=", self->test_dir,
                              " --dest=autogen --header=cfc_header", NULL);
     }
     else {
         cfc_command
             = chaz_Util_join("", cfish_prefix, dir_sep, "bin", dir_sep,
-                             "cfc --source=", self->core_dir, " --include=",
-                             cfish_prefix, dir_sep, "share", dir_sep,
-                             "clownfish", dir_sep, "include",
-                             " --dest=autogen --header=cfc_header", NULL);
+                             "cfc --source=", self->core_dir, " --source=",
+                             self->test_dir, " --include=", cfish_prefix,
+                             dir_sep, "share", dir_sep, "clownfish", dir_sep,
+                             "include --dest=autogen --header=cfc_header",
+                             NULL);
     }
     chaz_MakeRule_add_command(rule, cfc_command);
 
+    /* Tell make how autogenerated source files are built. */
+    for (i = 0; autogen_src_files[i] != NULL; ++i) {
+        char *path = chaz_Util_join("", self->autogen_src_dir, dir_sep,
+                                    autogen_src_files[i], NULL);
+        rule = chaz_MakeFile_add_rule(self->makefile, path,
+                                      self->autogen_target);
+        free(path);
+    }
+
+    rule = chaz_MakeFile_clean_rule(self->makefile);
+    chaz_MakeRule_add_recursive_rm_command(rule, "autogen");
+
     free(cfc_command);
 }
 
 static void
 lucy_MakeFile_write_c_test_rules(lucy_MakeFile *self) {
-    chaz_MakeBinary *test_exe;
+    chaz_MakeBinary *exe;
     chaz_CFlags     *link_flags;
     chaz_MakeRule   *rule;
 
-    test_exe = chaz_MakeFile_add_exe(self->makefile, "t", "test_lucy");
-    chaz_MakeBinary_add_src_file(test_exe, "t", "test_lucy.c");
-    chaz_MakeFile_add_rule(self->makefile, "$(TEST_LUCY_EXE_OBJS)",
-                           self->autogen_target);
-    link_flags = chaz_MakeBinary_get_link_flags(test_exe);
-    chaz_CFlags_add_shared_lib(link_flags, NULL, "lucy", lucy_major_version);
-    chaz_MakeBinary_add_prereq(test_exe, "$(LUCY_SHARED_LIB)");
+    exe = chaz_MakeFile_add_exe(self->makefile, "t", "test_lucy");
+    chaz_MakeBinary_add_src_file(exe, "t", "test_lucy.c");
+
+    link_flags = chaz_MakeBinary_get_link_flags(exe);
     if (self->cfish_lib_dir) {
         chaz_CFlags_add_library_path(link_flags, self->cfish_lib_dir);
     }
-    chaz_CFlags_add_external_lib(link_flags, self->cfish_lib_name);
     chaz_CFlags_add_rpath(link_flags, "\"$$PWD\"");
     if (self->cfish_lib_dir) {
         chaz_CFlags_add_rpath(link_flags, self->cfish_lib_dir);
     }
+    chaz_CFlags_add_shared_lib(link_flags, NULL, "testlucy",
+                               lucy_major_version);
+    chaz_CFlags_add_shared_lib(link_flags, NULL, "lucy", lucy_major_version);
+    chaz_CFlags_add_external_lib(link_flags, self->cfish_lib_name);
+
+    chaz_MakeBinary_add_prereq(exe, "$(TESTLUCY_SHARED_LIB)");
+    chaz_MakeBinary_add_prereq(exe, "$(LUCY_SHARED_LIB)");
+
+    chaz_MakeFile_add_rule(self->makefile, "$(TEST_LUCY_EXE_OBJS)",
+                           self->autogen_target);
 
     rule = chaz_MakeFile_add_rule(self->makefile, "test", "$(TEST_LUCY_EXE)");
     chaz_MakeRule_add_command(rule, "$(TEST_LUCY_EXE)");
@@ -9166,6 +9238,11 @@
     }
 }
 
+static int
+S_core_dir_filter(const char *dir, char *file, void *context) {
+    return !S_ends_with(file, "JsonParser.c");
+}
+
 static void
 S_cfh_file_callback(const char *dir, char *file, void *context) {
     SourceFileContext *sfc = (SourceFileContext*)context;
diff --git a/common/charmonizer.main b/common/charmonizer.main
index 15a2061..dfd0c78 100644
--- a/common/charmonizer.main
+++ b/common/charmonizer.main
@@ -41,10 +41,12 @@
     chaz_CLI        *cli;
     chaz_MakeFile   *makefile;
     chaz_MakeBinary *lib;
+    chaz_MakeBinary *test_lib;
 
     /* Directories. */
     const char *base_dir;
     char       *core_dir;
+    char       *test_dir;
     const char *host_src_dir;
     char       *autogen_src_dir;
     char       *autogen_inc_dir;
@@ -57,9 +59,8 @@
     char       *utf8proc_dir;
     char       *json_dir;
 
-    /* Files. */
-    char        *autogen_target;
-    const char **autogen_src_files;
+    /* Targets. */
+    char *autogen_target;
 
     /* Clownfish library. */
     char       *cfish_lib_dir;
@@ -91,6 +92,9 @@
 static void
 lucy_MakeFile_write_c_test_rules(lucy_MakeFile *self);
 
+static int
+S_core_dir_filter(const char *dir, char *file, void *context);
+
 static void
 S_cfh_file_callback(const char *dir, char *file, void *context);
 
@@ -222,26 +226,37 @@
 static lucy_MakeFile*
 lucy_MakeFile_new(chaz_CLI *cli) {
     const char *dir_sep      = chaz_OS_dir_sep();
+    const char *host         = chaz_CLI_strval(cli, "host");
     const char *cfish_prefix = chaz_CLI_strval(cli, "clownfish-prefix");
-
+    char *cfcore_filename = chaz_Util_join(dir_sep, "cfcore", "Lucy.cfp",
+                                           NULL);
     lucy_MakeFile *self = malloc(sizeof(lucy_MakeFile));
 
     self->cli      = cli;
     self->makefile = chaz_MakeFile_new();
     self->lib      = NULL;
+    self->test_lib = NULL;
 
     /* Initialize directories. */
-    self->base_dir = "..";
-    self->core_dir = chaz_Util_join(dir_sep, self->base_dir, "core", NULL);
-    if (chaz_CLI_defined(cli, "enable-perl")) {
-        self->host_src_dir = "xs";
+    if (chaz_Util_can_open_file(cfcore_filename)) {
+        self->base_dir = ".";
+        self->core_dir = chaz_Util_strdup("cfcore");
+        self->test_dir = chaz_Util_strdup("cftest");
     }
-	else if (chaz_CLI_defined(cli, "enable-go")) {
-        self->host_src_dir = "cfext";
-	}
     else {
+        self->base_dir = "..";
+        self->core_dir = chaz_Util_join(dir_sep, self->base_dir, "core", NULL);
+        self->test_dir = chaz_Util_join(dir_sep, self->base_dir, "test", NULL);
+    }
+    if (strcmp(host, "go") == 0) {
+        self->host_src_dir = "cfext";
+    }
+    else if (strcmp(host, "c") == 0) {
         self->host_src_dir = "src";
     }
+    else {
+        self->host_src_dir = NULL;
+    }
     self->autogen_src_dir = chaz_Util_join(dir_sep, "autogen", "source", NULL);
     self->autogen_inc_dir
         = chaz_Util_join(dir_sep, "autogen", "include", NULL);
@@ -265,25 +280,7 @@
         = chaz_Util_join(dir_sep, self->core_dir, "Lucy", "Util", "Json",
                          NULL);
 
-    /* Initialize file names. */
-    if (chaz_CLI_defined(cli, "enable-perl")) {
-        static const char *perl_autogen_src_files[] = {
-            "boot.c",
-            "callbacks.c",
-            "lucy_parcel.c",
-            "testlucy_parcel.c",
-            NULL
-        };
-        self->autogen_src_files = perl_autogen_src_files;
-    }
-    else {
-        static const char *c_autogen_src_files[] = {
-            "lucy_parcel.c",
-            "testlucy_parcel.c",
-            NULL
-        };
-        self->autogen_src_files = c_autogen_src_files;
-    }
+    /* Initialize targets. */
     self->autogen_target
         = chaz_Util_join(dir_sep, "autogen", "hierarchy.json", NULL);
 
@@ -310,6 +307,7 @@
     chaz_MakeFile_destroy(self->makefile);
 
     free(self->core_dir);
+    free(self->test_dir);
     free(self->autogen_inc_dir);
     free(self->autogen_src_dir);
     free(self->lemon_dir);
@@ -334,6 +332,9 @@
     const char *host     = chaz_CLI_strval(self->cli, "host");
     const char *math_lib = chaz_Floats_math_library();
 
+    const char *lib_objs      = NULL;
+    const char *test_lib_objs = NULL;
+
     chaz_MakeVar  *var;
     chaz_MakeRule *rule;
 
@@ -343,7 +344,6 @@
     chaz_CFlags *link_flags;
 
     char *scratch;
-    int   i;
 
     printf("Creating Makefile...\n");
 
@@ -378,25 +378,20 @@
 
     chaz_CFlags_destroy(makefile_cflags);
 
-    /* Binary. */
+    /* Core library. */
 
     if (strcmp(host, "c") == 0 || strcmp(host, "perl") == 0) {
+        /* Shared library for C and Perl. */
+
         chaz_MakeFile_add_rule(self->makefile, "all", "$(LUCY_SHARED_LIB)");
 
         self->lib
             = chaz_MakeFile_add_shared_lib(self->makefile, NULL, "lucy",
                                            lucy_version, lucy_major_version);
-        rule = chaz_MakeFile_add_rule(self->makefile,
-                                      "$(LUCY_SHARED_LIB_OBJS)",
-                                      self->autogen_target);
-        /*
-         * The dependency is actually on JsonParser.h, but make doesn't cope
-         * well with multiple output files.
-         */
-        scratch = chaz_Util_join(dir_sep, self->json_dir, "JsonParser.c",
-                                 NULL);
-        chaz_MakeRule_add_prereq(rule, scratch);
-        free(scratch);
+        lib_objs = "$(LUCY_SHARED_LIB_OBJS)";
+
+        compile_flags = chaz_MakeBinary_get_compile_flags(self->lib);
+        chaz_CFlags_add_define(compile_flags, "CFP_LUCY", NULL);
 
         link_flags = chaz_MakeBinary_get_link_flags(self->lib);
         chaz_CFlags_enable_debugging(link_flags);
@@ -415,35 +410,87 @@
         }
     }
     else {
-        chaz_MakeFile_add_rule(self->makefile, "static", "$(LUCY_STATIC_LIB)");
+        /* Static library for Go and Python. */
+
+        chaz_MakeFile_add_rule(self->makefile, "static",
+                               "$(LUCY_STATIC_LIB) $(TESTLUCY_STATIC_LIB)");
 
         self->lib
             = chaz_MakeFile_add_static_lib(self->makefile, NULL, "lucy");
-        rule = chaz_MakeFile_add_rule(self->makefile,
-                                      "$(LUCY_STATIC_LIB_OBJS)",
-                                      self->autogen_target);
-        scratch = chaz_Util_join(dir_sep, self->json_dir, "JsonParser.c",
-                                 NULL);
-        chaz_MakeRule_add_prereq(rule, scratch);
-        free(scratch);
+        lib_objs = "$(LUCY_STATIC_LIB_OBJS)";
+
+        if (strcmp(host, "python") == 0) {
+            /* For Python, the static library is linked into a shared
+             * library.
+             */
+            compile_flags = chaz_MakeBinary_get_compile_flags(self->lib);
+            chaz_CFlags_compile_shared_library(compile_flags);
+            chaz_CFlags_add_define(compile_flags, "CFP_CFISH", NULL);
+        }
     }
 
-    chaz_MakeBinary_add_src_dir(self->lib, self->host_src_dir);
-    chaz_MakeBinary_add_src_dir(self->lib, self->core_dir);
+    if (self->host_src_dir != NULL) {
+        chaz_MakeBinary_add_src_dir(self->lib, self->host_src_dir);
+    }
+    chaz_MakeBinary_add_filtered_src_dir(self->lib, self->core_dir,
+                                         S_core_dir_filter, NULL);
     chaz_MakeBinary_add_src_dir(self->lib, self->snowstem_dir);
     chaz_MakeBinary_add_src_dir(self->lib, self->snowstop_dir);
     chaz_MakeBinary_add_src_dir(self->lib, self->utf8proc_dir);
-
     chaz_MakeBinary_add_src_file(self->lib, self->json_dir, "JsonParser.c");
+    chaz_MakeBinary_add_src_file(self->lib, self->autogen_src_dir,
+                                 "lucy_parcel.c");
 
-    for (i = 0; self->autogen_src_files[i] != NULL; ++i) {
-        chaz_MakeBinary_add_src_file(self->lib, self->autogen_src_dir,
-                                     self->autogen_src_files[i]);
+    /* Test library. */
+
+    if (strcmp(host, "c") == 0 || strcmp(host, "perl") == 0) {
+        /* Shared library for C and Perl. */
+
+        self->test_lib
+            = chaz_MakeFile_add_shared_lib(self->makefile, NULL, "testlucy",
+                                           lucy_version, lucy_major_version);
+        test_lib_objs = "$(TESTLUCY_SHARED_LIB_OBJS)";
+
+        compile_flags = chaz_MakeBinary_get_compile_flags(self->test_lib);
+        chaz_CFlags_add_define(compile_flags, "CFP_TESTLUCY", NULL);
+
+        link_flags = chaz_MakeBinary_get_link_flags(self->test_lib);
+        chaz_CFlags_enable_debugging(link_flags);
+        if (self->cfish_lib_dir) {
+            chaz_CFlags_add_library_path(link_flags, self->cfish_lib_dir);
+        }
+        chaz_CFlags_add_shared_lib(link_flags, NULL, "lucy",
+                                   lucy_major_version);
+        chaz_CFlags_add_external_lib(link_flags, self->cfish_lib_name);
+        if (math_lib) {
+            chaz_CFlags_add_external_lib(link_flags, math_lib);
+        }
+        if (chaz_CLI_defined(self->cli, "enable-coverage")) {
+            chaz_CFlags_enable_code_coverage(link_flags);
+        }
+
+        chaz_MakeBinary_add_prereq(self->test_lib, "$(LUCY_SHARED_LIB)");
+    }
+    else {
+        /* Static library for Go and Python. */
+
+        self->test_lib
+            = chaz_MakeFile_add_static_lib(self->makefile, NULL, "testlucy");
+        test_lib_objs = "$(TESTLUCY_STATIC_LIB_OBJS)";
+
+        if (strcmp(host, "python") == 0) {
+            /* For Python, the static library is linked into a shared
+             * library.
+             */
+            compile_flags = chaz_MakeBinary_get_compile_flags(self->test_lib);
+            chaz_CFlags_compile_shared_library(compile_flags);
+            chaz_CFlags_add_define(compile_flags, "CFP_TESTLUCY", NULL);
+        }
     }
 
-    compile_flags = chaz_MakeBinary_get_compile_flags(self->lib);
-    chaz_CFlags_add_define(compile_flags, "CFP_LUCY", NULL);
-    chaz_CFlags_add_define(compile_flags, "CFP_TESTLUCY", NULL);
+    chaz_MakeBinary_add_src_dir(self->test_lib, self->test_dir);
+    chaz_MakeBinary_add_src_file(self->test_lib, self->autogen_src_dir,
+                                 "testlucy_parcel.c");
 
     /* Additional rules. */
 
@@ -452,28 +499,53 @@
     chaz_MakeFile_add_lemon_grammar(self->makefile, scratch);
     free(scratch);
 
+    /* Object files depend on autogenerated headers. */
+    rule = chaz_MakeFile_add_rule(self->makefile, lib_objs,
+                                  self->autogen_target);
+    /*
+     * The dependency is actually on JsonParser.h, but make doesn't cope
+     * well with multiple output files.
+     */
+    scratch = chaz_Util_join(dir_sep, self->json_dir, "JsonParser.c",
+                             NULL);
+    chaz_MakeRule_add_prereq(rule, scratch);
+    free(scratch);
+    chaz_MakeFile_add_rule(self->makefile, test_lib_objs,
+                           self->autogen_target);
+
     if (strcmp(host, "c") == 0) {
         lucy_MakeFile_write_c_cfc_rules(self);
         lucy_MakeFile_write_c_test_rules(self);
     }
 
-    /* Needed for parallel builds. */
-    for (i = 0; self->autogen_src_files[i] != NULL; ++i) {
-        char *path = chaz_Util_join("", self->autogen_src_dir, dir_sep,
-                                    self->autogen_src_files[i], NULL);
-        rule = chaz_MakeFile_add_rule(self->makefile, path,
-                                      self->autogen_target);
-        free(path);
-    }
+    /* Targets to compile object files for Perl. */
+    if (strcmp(host, "perl") == 0) {
+        char *objects;
 
-    rule = chaz_MakeFile_clean_rule(self->makefile);
-    chaz_MakeRule_add_recursive_rm_command(rule, "autogen");
+        chaz_MakeFile_add_rule(self->makefile, "core_objects",
+                               "$(LUCY_SHARED_LIB_OBJS)");
+        objects = chaz_MakeBinary_obj_string(self->lib);
+        chaz_ConfWriter_add_def("CORE_OBJECTS", objects);
+        free(objects);
+
+        chaz_MakeFile_add_rule(self->makefile, "test_objects",
+                               "$(TESTLUCY_SHARED_LIB_OBJS)");
+        objects = chaz_MakeBinary_obj_string(self->test_lib);
+        chaz_ConfWriter_add_def("TEST_OBJECTS", objects);
+        free(objects);
+    }
 
     chaz_MakeFile_write(self->makefile);
 }
 
 static void
 lucy_MakeFile_write_c_cfc_rules(lucy_MakeFile *self) {
+    static const char *const autogen_src_files[] = {
+        "lucy_parcel.c",
+        "testlucy_parcel.c",
+        NULL
+    };
+
     SourceFileContext sfc;
     chaz_MakeRule *rule;
 
@@ -482,50 +554,73 @@
 
     char *cfc_command;
 
+    int i;
+
     sfc.var = chaz_MakeFile_add_var(self->makefile, "CLOWNFISH_HEADERS", NULL);
     chaz_Make_list_files(self->core_dir, "cfh", S_cfh_file_callback, &sfc);
+    chaz_Make_list_files(self->test_dir, "cfh", S_cfh_file_callback, &sfc);
 
     rule = chaz_MakeFile_add_rule(self->makefile, self->autogen_target, NULL);
     chaz_MakeRule_add_prereq(rule, "$(CLOWNFISH_HEADERS)");
     if (cfish_prefix == NULL) {
         cfc_command
             = chaz_Util_join("", "cfc --source=", self->core_dir,
+                             " --source=", self->test_dir,
                              " --dest=autogen --header=cfc_header", NULL);
     }
     else {
         cfc_command
             = chaz_Util_join("", cfish_prefix, dir_sep, "bin", dir_sep,
-                             "cfc --source=", self->core_dir, " --include=",
-                             cfish_prefix, dir_sep, "share", dir_sep,
-                             "clownfish", dir_sep, "include",
-                             " --dest=autogen --header=cfc_header", NULL);
+                             "cfc --source=", self->core_dir, " --source=",
+                             self->test_dir, " --include=", cfish_prefix,
+                             dir_sep, "share", dir_sep, "clownfish", dir_sep,
+                             "include --dest=autogen --header=cfc_header",
+                             NULL);
     }
     chaz_MakeRule_add_command(rule, cfc_command);
 
+    /* Tell make how autogenerated source files are built. */
+    for (i = 0; autogen_src_files[i] != NULL; ++i) {
+        char *path = chaz_Util_join("", self->autogen_src_dir, dir_sep,
+                                    autogen_src_files[i], NULL);
+        rule = chaz_MakeFile_add_rule(self->makefile, path,
+                                      self->autogen_target);
+        free(path);
+    }
+
+    rule = chaz_MakeFile_clean_rule(self->makefile);
+    chaz_MakeRule_add_recursive_rm_command(rule, "autogen");
+
     free(cfc_command);
 }
 
 static void
 lucy_MakeFile_write_c_test_rules(lucy_MakeFile *self) {
-    chaz_MakeBinary *test_exe;
+    chaz_MakeBinary *exe;
     chaz_CFlags     *link_flags;
     chaz_MakeRule   *rule;
 
-    test_exe = chaz_MakeFile_add_exe(self->makefile, "t", "test_lucy");
-    chaz_MakeBinary_add_src_file(test_exe, "t", "test_lucy.c");
-    chaz_MakeFile_add_rule(self->makefile, "$(TEST_LUCY_EXE_OBJS)",
-                           self->autogen_target);
-    link_flags = chaz_MakeBinary_get_link_flags(test_exe);
-    chaz_CFlags_add_shared_lib(link_flags, NULL, "lucy", lucy_major_version);
-    chaz_MakeBinary_add_prereq(test_exe, "$(LUCY_SHARED_LIB)");
+    exe = chaz_MakeFile_add_exe(self->makefile, "t", "test_lucy");
+    chaz_MakeBinary_add_src_file(exe, "t", "test_lucy.c");
+
+    link_flags = chaz_MakeBinary_get_link_flags(exe);
     if (self->cfish_lib_dir) {
         chaz_CFlags_add_library_path(link_flags, self->cfish_lib_dir);
     }
-    chaz_CFlags_add_external_lib(link_flags, self->cfish_lib_name);
     chaz_CFlags_add_rpath(link_flags, "\"$$PWD\"");
     if (self->cfish_lib_dir) {
         chaz_CFlags_add_rpath(link_flags, self->cfish_lib_dir);
     }
+    chaz_CFlags_add_shared_lib(link_flags, NULL, "testlucy",
+                               lucy_major_version);
+    chaz_CFlags_add_shared_lib(link_flags, NULL, "lucy", lucy_major_version);
+    chaz_CFlags_add_external_lib(link_flags, self->cfish_lib_name);
+
+    chaz_MakeBinary_add_prereq(exe, "$(TESTLUCY_SHARED_LIB)");
+    chaz_MakeBinary_add_prereq(exe, "$(LUCY_SHARED_LIB)");
+
+    chaz_MakeFile_add_rule(self->makefile, "$(TEST_LUCY_EXE_OBJS)",
+                           self->autogen_target);
 
     rule = chaz_MakeFile_add_rule(self->makefile, "test", "$(TEST_LUCY_EXE)");
     chaz_MakeRule_add_command(rule, "$(TEST_LUCY_EXE)");
@@ -557,6 +652,11 @@
     }
 }
 
+static int
+S_core_dir_filter(const char *dir, char *file, void *context) {
+    return !S_ends_with(file, "JsonParser.c");
+}
+
 static void
 S_cfh_file_callback(const char *dir, char *file, void *context) {
     SourceFileContext *sfc = (SourceFileContext*)context;
diff --git a/core/Lucy/Store/FSFileHandle.c b/core/Lucy/Store/FSFileHandle.c
index d2431fa..235d8f1 100644
--- a/core/Lucy/Store/FSFileHandle.c
+++ b/core/Lucy/Store/FSFileHandle.c
@@ -245,6 +245,14 @@
     }
 }
 
+int
+FSFH_Set_FD_IMP(FSFileHandle *self, int fd) {
+    FSFileHandleIVARS *const ivars = FSFH_IVARS(self);
+    int old_fd = ivars->fd;
+    ivars->fd = fd;
+    return old_fd;
+}
+
 /********************************* 64-bit *********************************/
 
 #if IS_64_BIT
diff --git a/core/Lucy/Store/FSFileHandle.cfh b/core/Lucy/Store/FSFileHandle.cfh
index c36e5b5..679745e 100644
--- a/core/Lucy/Store/FSFileHandle.cfh
+++ b/core/Lucy/Store/FSFileHandle.cfh
@@ -57,6 +57,11 @@
 
     bool
     Close(FSFileHandle *self);
+
+    /** Only for testing.
+     */
+    int
+    Set_FD(FSFileHandle *self, int fd);
 }
 
 
diff --git a/core/Lucy/Store/InStream.c b/core/Lucy/Store/InStream.c
index 273a79e..7d1010c 100644
--- a/core/Lucy/Store/InStream.c
+++ b/core/Lucy/Store/InStream.c
@@ -535,4 +535,19 @@
     return (int)(dest - (uint8_t*)buf);
 }
 
+FileWindow*
+InStream_Get_Window_IMP(InStream *self) {
+    return InStream_IVARS(self)->window;
+}
+
+FileHandle*
+InStream_Get_Handle_IMP(InStream *self) {
+    return InStream_IVARS(self)->file_handle;
+}
+
+int64_t
+InStream_Bytes_In_Buf_IMP(InStream *self) {
+    InStreamIVARS *const ivars = InStream_IVARS(self);
+    return (int64_t)(ivars->limit - ivars->buf);
+}
 
diff --git a/core/Lucy/Store/InStream.cfh b/core/Lucy/Store/InStream.cfh
index 3d96860..4a38b7d 100644
--- a/core/Lucy/Store/InStream.cfh
+++ b/core/Lucy/Store/InStream.cfh
@@ -204,6 +204,21 @@
      */
     String*
     Get_Filename(InStream *self);
+
+    /** Only for testing.
+     */
+    FileWindow*
+    Get_Window(InStream *self);
+
+    /** Only for testing.
+     */
+    FileHandle*
+    Get_Handle(InStream *self);
+
+    /** Only for testing.
+     */
+    int64_t
+    Bytes_In_Buf(InStream *self);
 }
 
 
diff --git a/core/Lucy/Util/MemoryPool.c b/core/Lucy/Util/MemoryPool.c
index ac89873..5755bf0 100644
--- a/core/Lucy/Util/MemoryPool.c
+++ b/core/Lucy/Util/MemoryPool.c
@@ -157,3 +157,8 @@
     ivars->consumed = 0;
 }
 
+char*
+MemPool_Get_Buf_IMP(MemoryPool *self) {
+    return MemPool_IVARS(self)->buf;
+}
+
diff --git a/core/Lucy/Util/MemoryPool.cfh b/core/Lucy/Util/MemoryPool.cfh
index 30216e4..5cc8ada 100644
--- a/core/Lucy/Util/MemoryPool.cfh
+++ b/core/Lucy/Util/MemoryPool.cfh
@@ -67,6 +67,11 @@
 
     public void
     Destroy(MemoryPool *self);
+
+    /** Only for testing.
+     */
+    char*
+    Get_Buf(MemoryPool *self);
 }
 
 
diff --git a/devel/bin/travis-test.sh b/devel/bin/travis-test.sh
index 6f245d8..4e07247 100755
--- a/devel/bin/travis-test.sh
+++ b/devel/bin/travis-test.sh
@@ -49,10 +49,12 @@
     export PERL5LIB="$install_dir/lib/perl5"
 
     # Install Clownfish.
-    cd lucy-clownfish/runtime/perl
+    cd lucy-clownfish/compiler/perl
+    cpanm --quiet --installdeps --notest .
     perl Build.PL
     ./Build install --install-base "$install_dir"
-    cd ../../compiler/perl
+    cd ../../runtime/perl
+    perl Build.PL
     ./Build install --install-base "$install_dir"
 
     cd ../../../perl
diff --git a/go/build.go b/go/build.go
index 3fea7a0..8dead66 100644
--- a/go/build.go
+++ b/go/build.go
@@ -118,6 +118,7 @@
 func runCFC() {
 	hierarchy := cfc.NewHierarchy("autogen")
 	hierarchy.AddSourceDir("../core")
+	hierarchy.AddSourceDir("../test")
 	hierarchy.Build()
 	autogenHeader := "Auto-generated by build.go.\n"
 	coreBinding := cfc.NewBindCore(hierarchy, autogenHeader, "")
@@ -539,6 +540,7 @@
 			"// #cgo LDFLAGS: -L%s\n"+
 			"// #cgo LDFLAGS: -L%s\n"+
 			"// #cgo LDFLAGS: -L%s\n"+
+			"// #cgo LDFLAGS: -ltestlucy\n"+
 			"// #cgo LDFLAGS: -llucy\n"+
 			"// #cgo LDFLAGS: -lclownfish\n"+
 			"// #cgo LDFLAGS: -lm\n"+
diff --git a/perl/.gitignore b/perl/.gitignore
index bcd7739..83fd141 100644
--- a/perl/.gitignore
+++ b/perl/.gitignore
@@ -4,6 +4,7 @@
 /Charmony.pm
 /MYMETA.json
 /MYMETA.yml
+/Makefile
 /_build/
 /autogen/
 /blib/
@@ -13,6 +14,8 @@
 /charmony.h
 /lib/Lucy.c
 /lib/Lucy.xs
+/lib/Lucy/Test.c
+/lib/Lucy/Test.xs
 /ppport.h
 /typemap
 
diff --git a/perl/Build.PL b/perl/Build.PL
index c37cf3f..d9d93a5 100644
--- a/perl/Build.PL
+++ b/perl/Build.PL
@@ -17,22 +17,20 @@
 use strict;
 use warnings;
 use lib 'buildlib';
-use File::Spec::Functions qw( catdir );
+use File::Spec::Functions qw( catdir updir );
 use Lucy::Build;
 
-my @BASE_PATH        = Lucy::Build->cf_base_path;
-my $MODULES_DIR      = catdir( @BASE_PATH, 'modules' );
-my $SNOWSTEM_SRC_DIR = catdir( $MODULES_DIR, qw( analysis snowstem source ) );
-my $SNOWSTEM_INC_DIR = catdir( $SNOWSTEM_SRC_DIR, 'include' );
-my $SNOWSTOP_SRC_DIR = catdir( $MODULES_DIR, qw( analysis snowstop source ) );
-my $UCD_INC_DIR      = catdir( $MODULES_DIR, qw( unicode ucd ) );
-my $UTF8PROC_SRC_DIR = catdir( $MODULES_DIR, qw( unicode utf8proc ) );
-my $CORE_SOURCE_DIR  = catdir( @BASE_PATH, 'core' );
-my $XS_SOURCE_DIR    = 'xs';
-
-my @cf_linker_flags = Clownfish::CFC::Perl::Build->cf_linker_flags(
-    'Clownfish',
-);
+my $IS_CPAN_DIST = -e 'cfcore';
+my $CORE_SOURCE_DIR;
+my $TEST_SOURCE_DIR;
+if ($IS_CPAN_DIST) {
+    $CORE_SOURCE_DIR = 'cfcore';
+    $TEST_SOURCE_DIR = 'cftest';
+}
+else {
+    $CORE_SOURCE_DIR = catdir( updir(), 'core' );
+    $TEST_SOURCE_DIR = catdir( updir(), 'test' );
+}
 
 my $builder = Lucy::Build->new(
     module_name => 'Lucy',
@@ -52,7 +50,7 @@
     build_requires     => {
         'Module::Build'      => 0.280801,
         'ExtUtils::CBuilder' => 0.21,
-        'ExtUtils::ParseXS'  => 2.18,
+        'ExtUtils::ParseXS'  => 3.00,
         'Devel::PPPort'      => 3.14,
         'Clownfish'          => 0.005000,
         'Clownfish::CFC'     => 0.005000,
@@ -65,32 +63,25 @@
             bugtracker => 'https://issues.apache.org/jira/browse/LUCY',
         },
     },
-    include_dirs => [
-        $CORE_SOURCE_DIR,
-        $XS_SOURCE_DIR,
-        $SNOWSTEM_INC_DIR,
-        $UCD_INC_DIR,
-        $UTF8PROC_SRC_DIR,
-    ],
+    include_dirs => [ $CORE_SOURCE_DIR ],
     clownfish_params => {
-        source => [
-            $CORE_SOURCE_DIR,
-        ],
+        source => [ $CORE_SOURCE_DIR, $TEST_SOURCE_DIR ],
         modules => [
             {
                 name          => 'Lucy',
-                parcels       => [ 'Lucy', 'TestLucy' ],
-                c_source_dirs => [
-                    $CORE_SOURCE_DIR,
-                    $XS_SOURCE_DIR,
-                    $SNOWSTEM_SRC_DIR,
-                    $SNOWSTOP_SRC_DIR,
-                    $UTF8PROC_SRC_DIR,
-                ],
+                parcels       => [ 'Lucy' ],
+                make_target   => 'core_objects',
+                c_source_dirs => [ 'xs' ],
+                xs_prereqs    => [ 'Clownfish' ],
+            },
+            {
+                name        => 'Lucy::Test',
+                parcels     => [ 'TestLucy' ],
+                make_target => 'test_objects',
+                xs_prereqs  => [ 'Lucy', 'Clownfish' ],
             },
         ],
     },
-    extra_linker_flags => [ @cf_linker_flags ],
     add_to_cleanup => [
         qw(
             Lucy-*
diff --git a/perl/buildlib/Lucy/Build.pm b/perl/buildlib/Lucy/Build.pm
index 7cb2392..8ddc912 100644
--- a/perl/buildlib/Lucy/Build.pm
+++ b/perl/buildlib/Lucy/Build.pm
@@ -25,27 +25,21 @@
 our $VERSION = '0.005000';
 $VERSION = eval $VERSION;
 
-use File::Spec::Functions qw( catdir catfile rel2abs );
+use File::Spec::Functions qw( catdir catfile rel2abs updir );
 use File::Path qw( rmtree );
 use File::Copy qw( move );
 use Config;
 use Carp;
 use Cwd qw( getcwd );
 
-my @BASE_PATH = __PACKAGE__->cf_base_path;
-
-my $COMMON_SOURCE_DIR = catdir( @BASE_PATH, 'common' );
-my $LEMON_DIR         = catdir( @BASE_PATH, 'lemon' );
-my $LEMON_EXE_PATH = catfile( $LEMON_DIR, "lemon$Config{_exe}" );
-my $CORE_SOURCE_DIR = catdir( @BASE_PATH, 'core' );
-my $LIB_DIR         = 'lib';
-my $IS_CPAN_DIST    = !@BASE_PATH;
+my $LIB_DIR      = 'lib';
+my $IS_CPAN_DIST = -e 'cfcore';
 my $CHARMONIZER_C;
 if ($IS_CPAN_DIST) {
     $CHARMONIZER_C = 'charmonizer.c';
 }
 else {
-    $CHARMONIZER_C = catfile( $COMMON_SOURCE_DIR, 'charmonizer.c' );
+    $CHARMONIZER_C = catfile( updir(), 'common', 'charmonizer.c' );
 }
 
 sub new {
@@ -68,6 +62,7 @@
         $self->config( optimize => $optimize );
     }
 
+    $self->charmonizer_params( create_makefile => 1 );
     $self->charmonizer_params( charmonizer_c => $CHARMONIZER_C );
 
     $self->clownfish_params( autogen_header => $self->autogen_header );
@@ -75,34 +70,6 @@
     return $self;
 }
 
-sub _run_make {
-    my ( $self, %params ) = @_;
-    my @command           = @{ $params{args} };
-    my $dir               = $params{dir};
-    my $current_directory = getcwd();
-    chdir $dir if $dir;
-    unshift @command, 'CC=' . $self->config('cc');
-    if ( $self->config('cc') =~ /^cl\b/ ) {
-        unshift @command, "-f", "Makefile.MSVC";
-    }
-    elsif ( $^O =~ /mswin/i ) {
-        unshift @command, "-f", "Makefile.MinGW";
-    }
-    unshift @command, "$Config{make}";
-    system(@command) and confess("$Config{make} failed");
-    chdir $current_directory if $dir;
-}
-
-# Build the Lemon parser generator.
-sub ACTION_lemon {
-    my $self = shift;
-    print "Building the Lemon parser generator...\n\n";
-    $self->_run_make(
-        dir  => $LEMON_DIR,
-        args => [],
-    );
-}
-
 sub ACTION_copy_clownfish_includes {
     my $self = shift;
 
@@ -191,22 +158,6 @@
     }
 }
 
-# Run all .y files through lemon.
-sub ACTION_parsers {
-    my $self = shift;
-    $self->depends_on('lemon');
-    my $y_files = $self->rscan_dir( $CORE_SOURCE_DIR, qr/\.y$/ );
-    for my $y_file (@$y_files) {
-        my $c_file = $y_file;
-        my $h_file = $y_file;
-        $c_file =~ s/\.y$/.c/ or die "no match";
-        $h_file =~ s/\.y$/.h/ or die "no match";
-        next if $self->up_to_date( $y_file, [ $c_file, $h_file ] );
-        $self->add_to_cleanup( $c_file, $h_file );
-        system( $LEMON_EXE_PATH, '-q', $y_file ) and die "lemon failed";
-    }
-}
-
 sub ACTION_clownfish {
     my $self = shift;
 
@@ -219,7 +170,7 @@
 sub ACTION_compile_custom_xs {
     my $self = shift;
 
-    $self->depends_on(qw( parsers charmony ));
+    $self->depends_on(qw( charmony ));
 
     # Add extra compiler flags from Charmonizer.
     my $charm_cflags = $self->charmony('EXTRA_CFLAGS');
@@ -291,7 +242,8 @@
     # bunch of stuff.  After the tarball is packaged up, we delete the copied
     # directories.
     my %to_copy = (
-        '../core'                          => 'core',
+        '../core'                          => 'cfcore',
+        '../test'                          => 'cftest',
         '../modules'                       => 'modules',
         '../devel'                         => 'devel',
         '../lemon'                         => 'lemon',
@@ -395,12 +347,6 @@
     }
 }
 
-sub ACTION_clean {
-    my $self = shift;
-    $self->_run_make( dir => $LEMON_DIR, args => ['clean'] );
-    $self->SUPER::ACTION_clean;
-}
-
 1;
 
 __END__
diff --git a/perl/buildlib/Lucy/Build/Binding/Misc.pm b/perl/buildlib/Lucy/Build/Binding/Misc.pm
index 11b14a6..b4bb516 100644
--- a/perl/buildlib/Lucy/Build/Binding/Misc.pm
+++ b/perl/buildlib/Lucy/Build/Binding/Misc.pm
@@ -232,7 +232,7 @@
 
 sub bind_test {
     my $xs_code = <<'END_XS_CODE';
-MODULE = Lucy   PACKAGE = Lucy::Test
+MODULE = Lucy::Test   PACKAGE = Lucy::Test
 
 #include "Clownfish/TestHarness/TestFormatter.h"
 #include "Clownfish/TestHarness/TestSuite.h"
diff --git a/perl/lib/Lucy/Test.pm b/perl/lib/Lucy/Test.pm
index ab06d11..e84161f 100644
--- a/perl/lib/Lucy/Test.pm
+++ b/perl/lib/Lucy/Test.pm
@@ -18,6 +18,14 @@
 our $VERSION = '0.005000';
 $VERSION = eval $VERSION;
 
+sub dl_load_flags { 1 }
+
+BEGIN {
+    require DynaLoader;
+    our @ISA = qw( DynaLoader );
+    bootstrap Lucy::Test '0.5.0';
+}
+
 # Set the default memory threshold for PostingListWriter to a low number so
 # that we simulate large indexes by performing a lot of PostingPool flushes.
 Lucy::Index::PostingListWriter::set_default_mem_thresh(0x1000);
diff --git a/perl/xs/Lucy/Analysis/RegexTokenizer.c b/perl/xs/Lucy/Analysis/RegexTokenizer.c
index 9a3fcc6..408acf1 100644
--- a/perl/xs/Lucy/Analysis/RegexTokenizer.c
+++ b/perl/xs/Lucy/Analysis/RegexTokenizer.c
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#define CFP_LUCY
 #define C_LUCY_REGEXTOKENIZER
 #define C_LUCY_TOKEN
 #include "XSBind.h"
diff --git a/perl/xs/Lucy/Document/Doc.c b/perl/xs/Lucy/Document/Doc.c
index a923e25..6bbc326 100644
--- a/perl/xs/Lucy/Document/Doc.c
+++ b/perl/xs/Lucy/Document/Doc.c
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#define CFP_LUCY
 #define C_LUCY_DOC
 #include "XSBind.h"
 #include "Lucy/Document/Doc.h"
diff --git a/perl/xs/Lucy/Index/DocReader.c b/perl/xs/Lucy/Index/DocReader.c
index 7daf909..15130ac 100644
--- a/perl/xs/Lucy/Index/DocReader.c
+++ b/perl/xs/Lucy/Index/DocReader.c
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#define CFP_LUCY
 #define C_LUCY_DOCREADER
 #define C_LUCY_DEFAULTDOCREADER
 #include "XSBind.h"
diff --git a/perl/xs/Lucy/Index/Inverter.c b/perl/xs/Lucy/Index/Inverter.c
index eed3a66..28c8e90 100644
--- a/perl/xs/Lucy/Index/Inverter.c
+++ b/perl/xs/Lucy/Index/Inverter.c
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#define CFP_LUCY
 #define C_LUCY_INVERTER
 #define C_LUCY_INVERTERENTRY
 #include "XSBind.h"
diff --git a/core/Lucy/Test.c b/test/Lucy/Test.c
similarity index 100%
rename from core/Lucy/Test.c
rename to test/Lucy/Test.c
diff --git a/core/Lucy/Test.cfh b/test/Lucy/Test.cfh
similarity index 100%
rename from core/Lucy/Test.cfh
rename to test/Lucy/Test.cfh
diff --git a/core/Lucy/Test/Analysis/TestAnalyzer.c b/test/Lucy/Test/Analysis/TestAnalyzer.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestAnalyzer.c
rename to test/Lucy/Test/Analysis/TestAnalyzer.c
diff --git a/core/Lucy/Test/Analysis/TestAnalyzer.cfh b/test/Lucy/Test/Analysis/TestAnalyzer.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestAnalyzer.cfh
rename to test/Lucy/Test/Analysis/TestAnalyzer.cfh
diff --git a/core/Lucy/Test/Analysis/TestCaseFolder.c b/test/Lucy/Test/Analysis/TestCaseFolder.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestCaseFolder.c
rename to test/Lucy/Test/Analysis/TestCaseFolder.c
diff --git a/core/Lucy/Test/Analysis/TestCaseFolder.cfh b/test/Lucy/Test/Analysis/TestCaseFolder.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestCaseFolder.cfh
rename to test/Lucy/Test/Analysis/TestCaseFolder.cfh
diff --git a/core/Lucy/Test/Analysis/TestNormalizer.c b/test/Lucy/Test/Analysis/TestNormalizer.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestNormalizer.c
rename to test/Lucy/Test/Analysis/TestNormalizer.c
diff --git a/core/Lucy/Test/Analysis/TestNormalizer.cfh b/test/Lucy/Test/Analysis/TestNormalizer.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestNormalizer.cfh
rename to test/Lucy/Test/Analysis/TestNormalizer.cfh
diff --git a/core/Lucy/Test/Analysis/TestPolyAnalyzer.c b/test/Lucy/Test/Analysis/TestPolyAnalyzer.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestPolyAnalyzer.c
rename to test/Lucy/Test/Analysis/TestPolyAnalyzer.c
diff --git a/core/Lucy/Test/Analysis/TestPolyAnalyzer.cfh b/test/Lucy/Test/Analysis/TestPolyAnalyzer.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestPolyAnalyzer.cfh
rename to test/Lucy/Test/Analysis/TestPolyAnalyzer.cfh
diff --git a/core/Lucy/Test/Analysis/TestRegexTokenizer.c b/test/Lucy/Test/Analysis/TestRegexTokenizer.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestRegexTokenizer.c
rename to test/Lucy/Test/Analysis/TestRegexTokenizer.c
diff --git a/core/Lucy/Test/Analysis/TestRegexTokenizer.cfh b/test/Lucy/Test/Analysis/TestRegexTokenizer.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestRegexTokenizer.cfh
rename to test/Lucy/Test/Analysis/TestRegexTokenizer.cfh
diff --git a/core/Lucy/Test/Analysis/TestSnowballStemmer.c b/test/Lucy/Test/Analysis/TestSnowballStemmer.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestSnowballStemmer.c
rename to test/Lucy/Test/Analysis/TestSnowballStemmer.c
diff --git a/core/Lucy/Test/Analysis/TestSnowballStemmer.cfh b/test/Lucy/Test/Analysis/TestSnowballStemmer.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestSnowballStemmer.cfh
rename to test/Lucy/Test/Analysis/TestSnowballStemmer.cfh
diff --git a/core/Lucy/Test/Analysis/TestSnowballStopFilter.c b/test/Lucy/Test/Analysis/TestSnowballStopFilter.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestSnowballStopFilter.c
rename to test/Lucy/Test/Analysis/TestSnowballStopFilter.c
diff --git a/core/Lucy/Test/Analysis/TestSnowballStopFilter.cfh b/test/Lucy/Test/Analysis/TestSnowballStopFilter.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestSnowballStopFilter.cfh
rename to test/Lucy/Test/Analysis/TestSnowballStopFilter.cfh
diff --git a/core/Lucy/Test/Analysis/TestStandardTokenizer.c b/test/Lucy/Test/Analysis/TestStandardTokenizer.c
similarity index 100%
rename from core/Lucy/Test/Analysis/TestStandardTokenizer.c
rename to test/Lucy/Test/Analysis/TestStandardTokenizer.c
diff --git a/core/Lucy/Test/Analysis/TestStandardTokenizer.cfh b/test/Lucy/Test/Analysis/TestStandardTokenizer.cfh
similarity index 100%
rename from core/Lucy/Test/Analysis/TestStandardTokenizer.cfh
rename to test/Lucy/Test/Analysis/TestStandardTokenizer.cfh
diff --git a/core/Lucy/Test/Highlight/TestHeatMap.c b/test/Lucy/Test/Highlight/TestHeatMap.c
similarity index 100%
rename from core/Lucy/Test/Highlight/TestHeatMap.c
rename to test/Lucy/Test/Highlight/TestHeatMap.c
diff --git a/core/Lucy/Test/Highlight/TestHeatMap.cfh b/test/Lucy/Test/Highlight/TestHeatMap.cfh
similarity index 100%
rename from core/Lucy/Test/Highlight/TestHeatMap.cfh
rename to test/Lucy/Test/Highlight/TestHeatMap.cfh
diff --git a/core/Lucy/Test/Highlight/TestHighlighter.c b/test/Lucy/Test/Highlight/TestHighlighter.c
similarity index 100%
rename from core/Lucy/Test/Highlight/TestHighlighter.c
rename to test/Lucy/Test/Highlight/TestHighlighter.c
diff --git a/core/Lucy/Test/Highlight/TestHighlighter.cfh b/test/Lucy/Test/Highlight/TestHighlighter.cfh
similarity index 100%
rename from core/Lucy/Test/Highlight/TestHighlighter.cfh
rename to test/Lucy/Test/Highlight/TestHighlighter.cfh
diff --git a/core/Lucy/Test/Index/TestDocWriter.c b/test/Lucy/Test/Index/TestDocWriter.c
similarity index 100%
rename from core/Lucy/Test/Index/TestDocWriter.c
rename to test/Lucy/Test/Index/TestDocWriter.c
diff --git a/core/Lucy/Test/Index/TestDocWriter.cfh b/test/Lucy/Test/Index/TestDocWriter.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestDocWriter.cfh
rename to test/Lucy/Test/Index/TestDocWriter.cfh
diff --git a/core/Lucy/Test/Index/TestHighlightWriter.c b/test/Lucy/Test/Index/TestHighlightWriter.c
similarity index 100%
rename from core/Lucy/Test/Index/TestHighlightWriter.c
rename to test/Lucy/Test/Index/TestHighlightWriter.c
diff --git a/core/Lucy/Test/Index/TestHighlightWriter.cfh b/test/Lucy/Test/Index/TestHighlightWriter.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestHighlightWriter.cfh
rename to test/Lucy/Test/Index/TestHighlightWriter.cfh
diff --git a/core/Lucy/Test/Index/TestIndexManager.c b/test/Lucy/Test/Index/TestIndexManager.c
similarity index 100%
rename from core/Lucy/Test/Index/TestIndexManager.c
rename to test/Lucy/Test/Index/TestIndexManager.c
diff --git a/core/Lucy/Test/Index/TestIndexManager.cfh b/test/Lucy/Test/Index/TestIndexManager.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestIndexManager.cfh
rename to test/Lucy/Test/Index/TestIndexManager.cfh
diff --git a/core/Lucy/Test/Index/TestPolyReader.c b/test/Lucy/Test/Index/TestPolyReader.c
similarity index 100%
rename from core/Lucy/Test/Index/TestPolyReader.c
rename to test/Lucy/Test/Index/TestPolyReader.c
diff --git a/core/Lucy/Test/Index/TestPolyReader.cfh b/test/Lucy/Test/Index/TestPolyReader.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestPolyReader.cfh
rename to test/Lucy/Test/Index/TestPolyReader.cfh
diff --git a/core/Lucy/Test/Index/TestPostingListWriter.c b/test/Lucy/Test/Index/TestPostingListWriter.c
similarity index 100%
rename from core/Lucy/Test/Index/TestPostingListWriter.c
rename to test/Lucy/Test/Index/TestPostingListWriter.c
diff --git a/core/Lucy/Test/Index/TestPostingListWriter.cfh b/test/Lucy/Test/Index/TestPostingListWriter.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestPostingListWriter.cfh
rename to test/Lucy/Test/Index/TestPostingListWriter.cfh
diff --git a/core/Lucy/Test/Index/TestSegWriter.c b/test/Lucy/Test/Index/TestSegWriter.c
similarity index 100%
rename from core/Lucy/Test/Index/TestSegWriter.c
rename to test/Lucy/Test/Index/TestSegWriter.c
diff --git a/core/Lucy/Test/Index/TestSegWriter.cfh b/test/Lucy/Test/Index/TestSegWriter.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestSegWriter.cfh
rename to test/Lucy/Test/Index/TestSegWriter.cfh
diff --git a/core/Lucy/Test/Index/TestSegment.c b/test/Lucy/Test/Index/TestSegment.c
similarity index 100%
rename from core/Lucy/Test/Index/TestSegment.c
rename to test/Lucy/Test/Index/TestSegment.c
diff --git a/core/Lucy/Test/Index/TestSegment.cfh b/test/Lucy/Test/Index/TestSegment.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestSegment.cfh
rename to test/Lucy/Test/Index/TestSegment.cfh
diff --git a/core/Lucy/Test/Index/TestSnapshot.c b/test/Lucy/Test/Index/TestSnapshot.c
similarity index 100%
rename from core/Lucy/Test/Index/TestSnapshot.c
rename to test/Lucy/Test/Index/TestSnapshot.c
diff --git a/core/Lucy/Test/Index/TestSnapshot.cfh b/test/Lucy/Test/Index/TestSnapshot.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestSnapshot.cfh
rename to test/Lucy/Test/Index/TestSnapshot.cfh
diff --git a/core/Lucy/Test/Index/TestSortWriter.c b/test/Lucy/Test/Index/TestSortWriter.c
similarity index 100%
rename from core/Lucy/Test/Index/TestSortWriter.c
rename to test/Lucy/Test/Index/TestSortWriter.c
diff --git a/core/Lucy/Test/Index/TestSortWriter.cfh b/test/Lucy/Test/Index/TestSortWriter.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestSortWriter.cfh
rename to test/Lucy/Test/Index/TestSortWriter.cfh
diff --git a/core/Lucy/Test/Index/TestTermInfo.c b/test/Lucy/Test/Index/TestTermInfo.c
similarity index 100%
rename from core/Lucy/Test/Index/TestTermInfo.c
rename to test/Lucy/Test/Index/TestTermInfo.c
diff --git a/core/Lucy/Test/Index/TestTermInfo.cfh b/test/Lucy/Test/Index/TestTermInfo.cfh
similarity index 100%
rename from core/Lucy/Test/Index/TestTermInfo.cfh
rename to test/Lucy/Test/Index/TestTermInfo.cfh
diff --git a/core/Lucy/Test/Object/TestBitVector.c b/test/Lucy/Test/Object/TestBitVector.c
similarity index 100%
rename from core/Lucy/Test/Object/TestBitVector.c
rename to test/Lucy/Test/Object/TestBitVector.c
diff --git a/core/Lucy/Test/Object/TestBitVector.cfh b/test/Lucy/Test/Object/TestBitVector.cfh
similarity index 100%
rename from core/Lucy/Test/Object/TestBitVector.cfh
rename to test/Lucy/Test/Object/TestBitVector.cfh
diff --git a/core/Lucy/Test/Object/TestI32Array.c b/test/Lucy/Test/Object/TestI32Array.c
similarity index 100%
rename from core/Lucy/Test/Object/TestI32Array.c
rename to test/Lucy/Test/Object/TestI32Array.c
diff --git a/core/Lucy/Test/Object/TestI32Array.cfh b/test/Lucy/Test/Object/TestI32Array.cfh
similarity index 100%
rename from core/Lucy/Test/Object/TestI32Array.cfh
rename to test/Lucy/Test/Object/TestI32Array.cfh
diff --git a/core/Lucy/Test/Plan/TestArchitecture.c b/test/Lucy/Test/Plan/TestArchitecture.c
similarity index 100%
rename from core/Lucy/Test/Plan/TestArchitecture.c
rename to test/Lucy/Test/Plan/TestArchitecture.c
diff --git a/core/Lucy/Test/Plan/TestArchitecture.cfh b/test/Lucy/Test/Plan/TestArchitecture.cfh
similarity index 100%
rename from core/Lucy/Test/Plan/TestArchitecture.cfh
rename to test/Lucy/Test/Plan/TestArchitecture.cfh
diff --git a/core/Lucy/Test/Plan/TestBlobType.c b/test/Lucy/Test/Plan/TestBlobType.c
similarity index 100%
rename from core/Lucy/Test/Plan/TestBlobType.c
rename to test/Lucy/Test/Plan/TestBlobType.c
diff --git a/core/Lucy/Test/Plan/TestBlobType.cfh b/test/Lucy/Test/Plan/TestBlobType.cfh
similarity index 100%
rename from core/Lucy/Test/Plan/TestBlobType.cfh
rename to test/Lucy/Test/Plan/TestBlobType.cfh
diff --git a/core/Lucy/Test/Plan/TestFieldMisc.c b/test/Lucy/Test/Plan/TestFieldMisc.c
similarity index 100%
rename from core/Lucy/Test/Plan/TestFieldMisc.c
rename to test/Lucy/Test/Plan/TestFieldMisc.c
diff --git a/core/Lucy/Test/Plan/TestFieldMisc.cfh b/test/Lucy/Test/Plan/TestFieldMisc.cfh
similarity index 100%
rename from core/Lucy/Test/Plan/TestFieldMisc.cfh
rename to test/Lucy/Test/Plan/TestFieldMisc.cfh
diff --git a/core/Lucy/Test/Plan/TestFieldType.c b/test/Lucy/Test/Plan/TestFieldType.c
similarity index 100%
rename from core/Lucy/Test/Plan/TestFieldType.c
rename to test/Lucy/Test/Plan/TestFieldType.c
diff --git a/core/Lucy/Test/Plan/TestFieldType.cfh b/test/Lucy/Test/Plan/TestFieldType.cfh
similarity index 100%
rename from core/Lucy/Test/Plan/TestFieldType.cfh
rename to test/Lucy/Test/Plan/TestFieldType.cfh
diff --git a/core/Lucy/Test/Plan/TestFullTextType.c b/test/Lucy/Test/Plan/TestFullTextType.c
similarity index 100%
rename from core/Lucy/Test/Plan/TestFullTextType.c
rename to test/Lucy/Test/Plan/TestFullTextType.c
diff --git a/core/Lucy/Test/Plan/TestFullTextType.cfh b/test/Lucy/Test/Plan/TestFullTextType.cfh
similarity index 100%
rename from core/Lucy/Test/Plan/TestFullTextType.cfh
rename to test/Lucy/Test/Plan/TestFullTextType.cfh
diff --git a/core/Lucy/Test/Plan/TestNumericType.c b/test/Lucy/Test/Plan/TestNumericType.c
similarity index 100%
rename from core/Lucy/Test/Plan/TestNumericType.c
rename to test/Lucy/Test/Plan/TestNumericType.c
diff --git a/core/Lucy/Test/Plan/TestNumericType.cfh b/test/Lucy/Test/Plan/TestNumericType.cfh
similarity index 100%
rename from core/Lucy/Test/Plan/TestNumericType.cfh
rename to test/Lucy/Test/Plan/TestNumericType.cfh
diff --git a/core/Lucy/Test/Search/TestLeafQuery.c b/test/Lucy/Test/Search/TestLeafQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestLeafQuery.c
rename to test/Lucy/Test/Search/TestLeafQuery.c
diff --git a/core/Lucy/Test/Search/TestLeafQuery.cfh b/test/Lucy/Test/Search/TestLeafQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestLeafQuery.cfh
rename to test/Lucy/Test/Search/TestLeafQuery.cfh
diff --git a/core/Lucy/Test/Search/TestMatchAllQuery.c b/test/Lucy/Test/Search/TestMatchAllQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestMatchAllQuery.c
rename to test/Lucy/Test/Search/TestMatchAllQuery.c
diff --git a/core/Lucy/Test/Search/TestMatchAllQuery.cfh b/test/Lucy/Test/Search/TestMatchAllQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestMatchAllQuery.cfh
rename to test/Lucy/Test/Search/TestMatchAllQuery.cfh
diff --git a/core/Lucy/Test/Search/TestNOTQuery.c b/test/Lucy/Test/Search/TestNOTQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestNOTQuery.c
rename to test/Lucy/Test/Search/TestNOTQuery.c
diff --git a/core/Lucy/Test/Search/TestNOTQuery.cfh b/test/Lucy/Test/Search/TestNOTQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestNOTQuery.cfh
rename to test/Lucy/Test/Search/TestNOTQuery.cfh
diff --git a/core/Lucy/Test/Search/TestNoMatchQuery.c b/test/Lucy/Test/Search/TestNoMatchQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestNoMatchQuery.c
rename to test/Lucy/Test/Search/TestNoMatchQuery.c
diff --git a/core/Lucy/Test/Search/TestNoMatchQuery.cfh b/test/Lucy/Test/Search/TestNoMatchQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestNoMatchQuery.cfh
rename to test/Lucy/Test/Search/TestNoMatchQuery.cfh
diff --git a/core/Lucy/Test/Search/TestPhraseQuery.c b/test/Lucy/Test/Search/TestPhraseQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestPhraseQuery.c
rename to test/Lucy/Test/Search/TestPhraseQuery.c
diff --git a/core/Lucy/Test/Search/TestPhraseQuery.cfh b/test/Lucy/Test/Search/TestPhraseQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestPhraseQuery.cfh
rename to test/Lucy/Test/Search/TestPhraseQuery.cfh
diff --git a/core/Lucy/Test/Search/TestPolyQuery.c b/test/Lucy/Test/Search/TestPolyQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestPolyQuery.c
rename to test/Lucy/Test/Search/TestPolyQuery.c
diff --git a/core/Lucy/Test/Search/TestPolyQuery.cfh b/test/Lucy/Test/Search/TestPolyQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestPolyQuery.cfh
rename to test/Lucy/Test/Search/TestPolyQuery.cfh
diff --git a/core/Lucy/Test/Search/TestQueryParser.c b/test/Lucy/Test/Search/TestQueryParser.c
similarity index 100%
rename from core/Lucy/Test/Search/TestQueryParser.c
rename to test/Lucy/Test/Search/TestQueryParser.c
diff --git a/core/Lucy/Test/Search/TestQueryParser.cfh b/test/Lucy/Test/Search/TestQueryParser.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestQueryParser.cfh
rename to test/Lucy/Test/Search/TestQueryParser.cfh
diff --git a/core/Lucy/Test/Search/TestQueryParserLogic.c b/test/Lucy/Test/Search/TestQueryParserLogic.c
similarity index 100%
rename from core/Lucy/Test/Search/TestQueryParserLogic.c
rename to test/Lucy/Test/Search/TestQueryParserLogic.c
diff --git a/core/Lucy/Test/Search/TestQueryParserLogic.cfh b/test/Lucy/Test/Search/TestQueryParserLogic.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestQueryParserLogic.cfh
rename to test/Lucy/Test/Search/TestQueryParserLogic.cfh
diff --git a/core/Lucy/Test/Search/TestQueryParserSyntax.c b/test/Lucy/Test/Search/TestQueryParserSyntax.c
similarity index 100%
rename from core/Lucy/Test/Search/TestQueryParserSyntax.c
rename to test/Lucy/Test/Search/TestQueryParserSyntax.c
diff --git a/core/Lucy/Test/Search/TestQueryParserSyntax.cfh b/test/Lucy/Test/Search/TestQueryParserSyntax.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestQueryParserSyntax.cfh
rename to test/Lucy/Test/Search/TestQueryParserSyntax.cfh
diff --git a/core/Lucy/Test/Search/TestRangeQuery.c b/test/Lucy/Test/Search/TestRangeQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestRangeQuery.c
rename to test/Lucy/Test/Search/TestRangeQuery.c
diff --git a/core/Lucy/Test/Search/TestRangeQuery.cfh b/test/Lucy/Test/Search/TestRangeQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestRangeQuery.cfh
rename to test/Lucy/Test/Search/TestRangeQuery.cfh
diff --git a/core/Lucy/Test/Search/TestReqOptQuery.c b/test/Lucy/Test/Search/TestReqOptQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestReqOptQuery.c
rename to test/Lucy/Test/Search/TestReqOptQuery.c
diff --git a/core/Lucy/Test/Search/TestReqOptQuery.cfh b/test/Lucy/Test/Search/TestReqOptQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestReqOptQuery.cfh
rename to test/Lucy/Test/Search/TestReqOptQuery.cfh
diff --git a/core/Lucy/Test/Search/TestSeriesMatcher.c b/test/Lucy/Test/Search/TestSeriesMatcher.c
similarity index 100%
rename from core/Lucy/Test/Search/TestSeriesMatcher.c
rename to test/Lucy/Test/Search/TestSeriesMatcher.c
diff --git a/core/Lucy/Test/Search/TestSeriesMatcher.cfh b/test/Lucy/Test/Search/TestSeriesMatcher.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestSeriesMatcher.cfh
rename to test/Lucy/Test/Search/TestSeriesMatcher.cfh
diff --git a/core/Lucy/Test/Search/TestSortSpec.c b/test/Lucy/Test/Search/TestSortSpec.c
similarity index 100%
rename from core/Lucy/Test/Search/TestSortSpec.c
rename to test/Lucy/Test/Search/TestSortSpec.c
diff --git a/core/Lucy/Test/Search/TestSortSpec.cfh b/test/Lucy/Test/Search/TestSortSpec.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestSortSpec.cfh
rename to test/Lucy/Test/Search/TestSortSpec.cfh
diff --git a/core/Lucy/Test/Search/TestSpan.c b/test/Lucy/Test/Search/TestSpan.c
similarity index 100%
rename from core/Lucy/Test/Search/TestSpan.c
rename to test/Lucy/Test/Search/TestSpan.c
diff --git a/core/Lucy/Test/Search/TestSpan.cfh b/test/Lucy/Test/Search/TestSpan.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestSpan.cfh
rename to test/Lucy/Test/Search/TestSpan.cfh
diff --git a/core/Lucy/Test/Search/TestTermQuery.c b/test/Lucy/Test/Search/TestTermQuery.c
similarity index 100%
rename from core/Lucy/Test/Search/TestTermQuery.c
rename to test/Lucy/Test/Search/TestTermQuery.c
diff --git a/core/Lucy/Test/Search/TestTermQuery.cfh b/test/Lucy/Test/Search/TestTermQuery.cfh
similarity index 100%
rename from core/Lucy/Test/Search/TestTermQuery.cfh
rename to test/Lucy/Test/Search/TestTermQuery.cfh
diff --git a/core/Lucy/Test/Store/MockFileHandle.c b/test/Lucy/Test/Store/MockFileHandle.c
similarity index 100%
rename from core/Lucy/Test/Store/MockFileHandle.c
rename to test/Lucy/Test/Store/MockFileHandle.c
diff --git a/core/Lucy/Test/Store/MockFileHandle.cfh b/test/Lucy/Test/Store/MockFileHandle.cfh
similarity index 100%
rename from core/Lucy/Test/Store/MockFileHandle.cfh
rename to test/Lucy/Test/Store/MockFileHandle.cfh
diff --git a/core/Lucy/Test/Store/TestCompoundFileReader.c b/test/Lucy/Test/Store/TestCompoundFileReader.c
similarity index 100%
rename from core/Lucy/Test/Store/TestCompoundFileReader.c
rename to test/Lucy/Test/Store/TestCompoundFileReader.c
diff --git a/core/Lucy/Test/Store/TestCompoundFileReader.cfh b/test/Lucy/Test/Store/TestCompoundFileReader.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestCompoundFileReader.cfh
rename to test/Lucy/Test/Store/TestCompoundFileReader.cfh
diff --git a/core/Lucy/Test/Store/TestCompoundFileWriter.c b/test/Lucy/Test/Store/TestCompoundFileWriter.c
similarity index 100%
rename from core/Lucy/Test/Store/TestCompoundFileWriter.c
rename to test/Lucy/Test/Store/TestCompoundFileWriter.c
diff --git a/core/Lucy/Test/Store/TestCompoundFileWriter.cfh b/test/Lucy/Test/Store/TestCompoundFileWriter.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestCompoundFileWriter.cfh
rename to test/Lucy/Test/Store/TestCompoundFileWriter.cfh
diff --git a/core/Lucy/Test/Store/TestFSDirHandle.c b/test/Lucy/Test/Store/TestFSDirHandle.c
similarity index 100%
rename from core/Lucy/Test/Store/TestFSDirHandle.c
rename to test/Lucy/Test/Store/TestFSDirHandle.c
diff --git a/core/Lucy/Test/Store/TestFSDirHandle.cfh b/test/Lucy/Test/Store/TestFSDirHandle.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestFSDirHandle.cfh
rename to test/Lucy/Test/Store/TestFSDirHandle.cfh
diff --git a/core/Lucy/Test/Store/TestFSFileHandle.c b/test/Lucy/Test/Store/TestFSFileHandle.c
similarity index 93%
rename from core/Lucy/Test/Store/TestFSFileHandle.c
rename to test/Lucy/Test/Store/TestFSFileHandle.c
index 7f9f2f1..518b71c 100644
--- a/core/Lucy/Test/Store/TestFSFileHandle.c
+++ b/test/Lucy/Test/Store/TestFSFileHandle.c
@@ -187,14 +187,13 @@
 #ifdef _MSC_VER
     SKIP(runner, 2, "LUCY-155");
 #else
-    int saved_fd = FSFH_IVARS(fh)->fd;
-    FSFH_IVARS(fh)->fd = -1;
+    int saved_fd = FSFH_Set_FD(fh, -1);
     Err_set_error(NULL);
     bool result = FSFH_Close(fh);
     TEST_FALSE(runner, result, "Failed Close() returns false");
     TEST_TRUE(runner, Err_get_error() != NULL,
               "Failed Close() sets global error");
-    FSFH_IVARS(fh)->fd = saved_fd;
+    FSFH_Set_FD(fh, saved_fd);
 #endif /* _MSC_VER */
     DECREF(fh);
 
@@ -210,7 +209,6 @@
     String *test_filename = SSTR_WRAP_C("_fstest");
     FSFileHandle *fh;
     FileWindow *window = FileWindow_new();
-    FileWindowIVARS *const window_ivars = FileWindow_IVARS(window);
     uint32_t i;
 
     S_remove(test_filename);
@@ -240,15 +238,20 @@
 
     TEST_TRUE(runner, FSFH_Window(fh, window, 1021, 2),
               "Window() returns true");
+    const char *buf = FileWindow_Get_Buf(window);
+    int64_t offset = FileWindow_Get_Offset(window);
     TEST_TRUE(runner,
-              strncmp(window_ivars->buf - window_ivars->offset + 1021, "oo", 2) == 0,
+              strncmp(buf - offset + 1021, "oo", 2) == 0,
               "Window()");
 
     TEST_TRUE(runner, FSFH_Release_Window(fh, window),
               "Release_Window() returns true");
-    TEST_TRUE(runner, window_ivars->buf == NULL, "Release_Window() resets buf");
-    TEST_TRUE(runner, window_ivars->offset == 0, "Release_Window() resets offset");
-    TEST_TRUE(runner, window_ivars->len == 0, "Release_Window() resets len");
+    TEST_TRUE(runner, FileWindow_Get_Buf(window) == NULL,
+              "Release_Window() resets buf");
+    TEST_INT_EQ(runner, FileWindow_Get_Offset(window), 0,
+                "Release_Window() resets offset");
+    TEST_INT_EQ(runner, FileWindow_Get_Len(window), 0,
+                "Release_Window() resets len");
 
     DECREF(window);
     DECREF(fh);
diff --git a/core/Lucy/Test/Store/TestFSFileHandle.cfh b/test/Lucy/Test/Store/TestFSFileHandle.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestFSFileHandle.cfh
rename to test/Lucy/Test/Store/TestFSFileHandle.cfh
diff --git a/core/Lucy/Test/Store/TestFSFolder.c b/test/Lucy/Test/Store/TestFSFolder.c
similarity index 100%
rename from core/Lucy/Test/Store/TestFSFolder.c
rename to test/Lucy/Test/Store/TestFSFolder.c
diff --git a/core/Lucy/Test/Store/TestFSFolder.cfh b/test/Lucy/Test/Store/TestFSFolder.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestFSFolder.cfh
rename to test/Lucy/Test/Store/TestFSFolder.cfh
diff --git a/core/Lucy/Test/Store/TestFileHandle.c b/test/Lucy/Test/Store/TestFileHandle.c
similarity index 100%
rename from core/Lucy/Test/Store/TestFileHandle.c
rename to test/Lucy/Test/Store/TestFileHandle.c
diff --git a/core/Lucy/Test/Store/TestFileHandle.cfh b/test/Lucy/Test/Store/TestFileHandle.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestFileHandle.cfh
rename to test/Lucy/Test/Store/TestFileHandle.cfh
diff --git a/core/Lucy/Test/Store/TestFolder.c b/test/Lucy/Test/Store/TestFolder.c
similarity index 100%
rename from core/Lucy/Test/Store/TestFolder.c
rename to test/Lucy/Test/Store/TestFolder.c
diff --git a/core/Lucy/Test/Store/TestFolder.cfh b/test/Lucy/Test/Store/TestFolder.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestFolder.cfh
rename to test/Lucy/Test/Store/TestFolder.cfh
diff --git a/core/Lucy/Test/Store/TestFolderCommon.c b/test/Lucy/Test/Store/TestFolderCommon.c
similarity index 100%
rename from core/Lucy/Test/Store/TestFolderCommon.c
rename to test/Lucy/Test/Store/TestFolderCommon.c
diff --git a/core/Lucy/Test/Store/TestFolderCommon.cfh b/test/Lucy/Test/Store/TestFolderCommon.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestFolderCommon.cfh
rename to test/Lucy/Test/Store/TestFolderCommon.cfh
diff --git a/core/Lucy/Test/Store/TestIOChunks.c b/test/Lucy/Test/Store/TestIOChunks.c
similarity index 88%
rename from core/Lucy/Test/Store/TestIOChunks.c
rename to test/Lucy/Test/Store/TestIOChunks.c
index bf35d0b..9a1f2eb 100644
--- a/core/Lucy/Test/Store/TestIOChunks.c
+++ b/test/Lucy/Test/Store/TestIOChunks.c
@@ -87,25 +87,24 @@
     OutStream_Close(outstream);
 
     instream = InStream_open((Obj*)file);
-    InStreamIVARS *const ivars = InStream_IVARS(instream);
     buf = InStream_Buf(instream, 5);
-    TEST_INT_EQ(runner, ivars->limit - buf, IO_STREAM_BUF_SIZE,
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), IO_STREAM_BUF_SIZE,
                 "Small request bumped up");
 
     buf += IO_STREAM_BUF_SIZE - 10; // 10 bytes left in buffer.
     InStream_Advance_Buf(instream, buf);
 
     buf = InStream_Buf(instream, 10);
-    TEST_INT_EQ(runner, ivars->limit - buf, 10,
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), 10,
                 "Exact request doesn't trigger refill");
 
     buf = InStream_Buf(instream, 11);
-    TEST_INT_EQ(runner, ivars->limit - buf, IO_STREAM_BUF_SIZE,
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), IO_STREAM_BUF_SIZE,
                 "Requesting over limit triggers refill");
 
-    int64_t     expected = InStream_Length(instream) - InStream_Tell(instream);
-    const char *buff     = InStream_Buf(instream, 100000);
-    int64_t     got      = CHY_PTR_TO_I64(ivars->limit) - CHY_PTR_TO_I64(buff);
+    int64_t expected = InStream_Length(instream) - InStream_Tell(instream);
+    InStream_Buf(instream, 100000);
+    int64_t got      = InStream_Bytes_In_Buf(instream);
     TEST_TRUE(runner, got == expected,
               "Requests greater than file size get pared down");
 
diff --git a/core/Lucy/Test/Store/TestIOChunks.cfh b/test/Lucy/Test/Store/TestIOChunks.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestIOChunks.cfh
rename to test/Lucy/Test/Store/TestIOChunks.cfh
diff --git a/core/Lucy/Test/Store/TestIOPrimitives.c b/test/Lucy/Test/Store/TestIOPrimitives.c
similarity index 100%
rename from core/Lucy/Test/Store/TestIOPrimitives.c
rename to test/Lucy/Test/Store/TestIOPrimitives.c
diff --git a/core/Lucy/Test/Store/TestIOPrimitives.cfh b/test/Lucy/Test/Store/TestIOPrimitives.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestIOPrimitives.cfh
rename to test/Lucy/Test/Store/TestIOPrimitives.cfh
diff --git a/core/Lucy/Test/Store/TestInStream.c b/test/Lucy/Test/Store/TestInStream.c
similarity index 72%
rename from core/Lucy/Test/Store/TestInStream.c
rename to test/Lucy/Test/Store/TestInStream.c
index c32f985..286dffd 100644
--- a/core/Lucy/Test/Store/TestInStream.c
+++ b/test/Lucy/Test/Store/TestInStream.c
@@ -41,8 +41,8 @@
     RAMFile    *file      = RAMFile_new(NULL, false);
     OutStream  *outstream = OutStream_open((Obj*)file);
     InStream   *instream;
+    FileWindow *window;
     char        scratch[5];
-    InStreamIVARS *ivars;
 
     for (int32_t i = 0; i < 1023; i++) {
         OutStream_Write_U8(outstream, 'x');
@@ -52,41 +52,38 @@
     OutStream_Close(outstream);
 
     instream = InStream_open((Obj*)file);
-    ivars = InStream_IVARS(instream);
     InStream_Refill(instream);
-    TEST_INT_EQ(runner, ivars->limit - ivars->buf, IO_STREAM_BUF_SIZE,
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), IO_STREAM_BUF_SIZE,
                 "Refill");
     TEST_INT_EQ(runner, (long)InStream_Tell(instream), 0,
                 "Correct file pos after standing-start Refill()");
     DECREF(instream);
 
     instream = InStream_open((Obj*)file);
-    ivars = InStream_IVARS(instream);
     InStream_Fill(instream, 30);
-    TEST_INT_EQ(runner, ivars->limit - ivars->buf, 30, "Fill()");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), 30, "Fill()");
     TEST_INT_EQ(runner, (long)InStream_Tell(instream), 0,
                 "Correct file pos after standing-start Fill()");
     DECREF(instream);
 
     instream = InStream_open((Obj*)file);
-    ivars = InStream_IVARS(instream);
     InStream_Read_Bytes(instream, scratch, 5);
-    TEST_INT_EQ(runner, ivars->limit - ivars->buf,
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream),
                 IO_STREAM_BUF_SIZE - 5, "small read triggers refill");
     DECREF(instream);
 
     instream = InStream_open((Obj*)file);
-    ivars = InStream_IVARS(instream);
     TEST_INT_EQ(runner, InStream_Read_U8(instream), 'x', "Read_U8");
     InStream_Seek(instream, 1023);
-    TEST_INT_EQ(runner, (long)FileWindow_IVARS(ivars->window)->offset, 0,
+    window = InStream_Get_Window(instream);
+    TEST_INT_EQ(runner, FileWindow_Get_Offset(window), 0,
                 "no unnecessary refill on Seek");
     TEST_INT_EQ(runner, (long)InStream_Tell(instream), 1023, "Seek/Tell");
     TEST_INT_EQ(runner, InStream_Read_U8(instream), 'y',
                 "correct data after in-buffer Seek()");
     TEST_INT_EQ(runner, InStream_Read_U8(instream), 'z', "automatic Refill");
-    TEST_TRUE(runner, (FileWindow_IVARS(ivars->window)->offset != 0),
-              "refilled");
+    window = InStream_Get_Window(instream);
+    TEST_TRUE(runner, FileWindow_Get_Offset(window) != 0, "refilled");
 
     DECREF(instream);
     DECREF(outstream);
@@ -149,7 +146,7 @@
     RAMFile  *file     = RAMFile_new(NULL, false);
     InStream *instream = InStream_open((Obj*)file);
     InStream_Close(instream);
-    TEST_TRUE(runner, InStream_IVARS(instream)->file_handle == NULL,
+    TEST_TRUE(runner, InStream_Get_Handle(instream) == NULL,
               "Close decrements FileHandle's refcount");
     DECREF(instream);
     DECREF(file);
@@ -163,51 +160,53 @@
     int64_t     gb12     = gb1 * 12;
     FileHandle *fh       = (FileHandle*)MockFileHandle_new(NULL, gb12);
     InStream   *instream = InStream_open((Obj*)fh);
-    InStreamIVARS *const ivars = InStream_IVARS(instream);
+    FileWindow *window;
 
     InStream_Buf(instream, 10000);
-    TEST_TRUE(runner, ivars->limit == ((char*)NULL) + 10000,
-              "InStream_Buf sets limit");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), 10000,
+                "InStream_Buf sets limit");
 
     InStream_Seek(instream, gb6);
-    TEST_TRUE(runner, InStream_Tell(instream) == gb6,
-              "Tell after seek forwards outside buffer");
-    TEST_TRUE(runner, ivars->buf == NULL,
+    TEST_INT_EQ(runner, InStream_Tell(instream), gb6,
+                "Tell after seek forwards outside buffer");
+    TEST_TRUE(runner, InStream_Buf(instream, 0) == NULL,
               "Seek forwards outside buffer sets buf to NULL");
-    TEST_TRUE(runner, ivars->limit == NULL,
-              "Seek forwards outside buffer sets limit to NULL");
-    TEST_TRUE(runner, FileWindow_IVARS(ivars->window)->offset == gb6,
-              "Seek forwards outside buffer tracks pos in window offset");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), 0,
+                "Seek forwards outside buffer sets limit to NULL");
+    window = InStream_Get_Window(instream);
+    TEST_INT_EQ(runner, FileWindow_Get_Offset(window), gb6,
+                "Seek forwards outside buffer tracks pos in window offset");
 
     InStream_Buf(instream, (size_t)gb1);
-    TEST_TRUE(runner, ivars->limit == ((char*)NULL) + gb1,
-              "InStream_Buf sets limit");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), gb1,
+                "InStream_Buf sets limit");
 
     InStream_Seek(instream, gb6 + 10);
-    TEST_TRUE(runner, InStream_Tell(instream) == gb6 + 10,
-              "Tell after seek forwards within buffer");
-    TEST_TRUE(runner, ivars->buf == ((char*)NULL) + 10,
+    TEST_INT_EQ(runner, InStream_Tell(instream), gb6 + 10,
+                "Tell after seek forwards within buffer");
+    TEST_TRUE(runner, InStream_Buf(instream, 0) == ((char*)NULL) + 10,
               "Seek within buffer sets buf");
-    TEST_TRUE(runner, ivars->limit == ((char*)NULL) + gb1,
-              "Seek within buffer leaves limit alone");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), gb1 - 10,
+                "Seek within buffer leaves limit alone");
 
     InStream_Seek(instream, gb6 + 1);
-    TEST_TRUE(runner, InStream_Tell(instream) == gb6 + 1,
-              "Tell after seek backwards within buffer");
-    TEST_TRUE(runner, ivars->buf == ((char*)NULL) + 1,
+    TEST_INT_EQ(runner, InStream_Tell(instream), gb6 + 1,
+                "Tell after seek backwards within buffer");
+    TEST_TRUE(runner, InStream_Buf(instream, 0) == ((char*)NULL) + 1,
               "Seek backwards within buffer sets buf");
-    TEST_TRUE(runner, ivars->limit == ((char*)NULL) + gb1,
-              "Seek backwards within buffer leaves limit alone");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), gb1 - 1,
+                "Seek backwards within buffer leaves limit alone");
 
     InStream_Seek(instream, gb3);
-    TEST_TRUE(runner, InStream_Tell(instream) == gb3,
-              "Tell after seek backwards outside buffer");
-    TEST_TRUE(runner, ivars->buf == NULL,
+    TEST_INT_EQ(runner, InStream_Tell(instream), gb3,
+                "Tell after seek backwards outside buffer");
+    TEST_TRUE(runner, InStream_Buf(instream, 0) == NULL,
               "Seek backwards outside buffer sets buf to NULL");
-    TEST_TRUE(runner, ivars->limit == NULL,
-              "Seek backwards outside buffer sets limit to NULL");
-    TEST_TRUE(runner, FileWindow_IVARS(ivars->window)->offset == gb3,
-              "Seek backwards outside buffer tracks pos in window offset");
+    TEST_INT_EQ(runner, InStream_Bytes_In_Buf(instream), 0,
+                "Seek backwards outside buffer sets limit to NULL");
+    window = InStream_Get_Window(instream);
+    TEST_INT_EQ(runner, FileWindow_Get_Offset(window), gb3,
+                "Seek backwards outside buffer tracks pos in window offset");
 
     DECREF(instream);
     DECREF(fh);
diff --git a/core/Lucy/Test/Store/TestInStream.cfh b/test/Lucy/Test/Store/TestInStream.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestInStream.cfh
rename to test/Lucy/Test/Store/TestInStream.cfh
diff --git a/core/Lucy/Test/Store/TestRAMDirHandle.c b/test/Lucy/Test/Store/TestRAMDirHandle.c
similarity index 100%
rename from core/Lucy/Test/Store/TestRAMDirHandle.c
rename to test/Lucy/Test/Store/TestRAMDirHandle.c
diff --git a/core/Lucy/Test/Store/TestRAMDirHandle.cfh b/test/Lucy/Test/Store/TestRAMDirHandle.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestRAMDirHandle.cfh
rename to test/Lucy/Test/Store/TestRAMDirHandle.cfh
diff --git a/core/Lucy/Test/Store/TestRAMFileHandle.c b/test/Lucy/Test/Store/TestRAMFileHandle.c
similarity index 93%
rename from core/Lucy/Test/Store/TestRAMFileHandle.c
rename to test/Lucy/Test/Store/TestRAMFileHandle.c
index e208784..f8d0f80 100644
--- a/core/Lucy/Test/Store/TestRAMFileHandle.c
+++ b/test/Lucy/Test/Store/TestRAMFileHandle.c
@@ -126,7 +126,6 @@
     RAMFile *file = RAMFile_new(NULL, false);
     RAMFileHandle *fh = RAMFH_open(NULL, FH_WRITE_ONLY, file);
     FileWindow *window = FileWindow_new();
-    FileWindowIVARS *const window_ivars = FileWindow_IVARS(window);
 
     for (uint32_t i = 0; i < 1024; i++) {
         RAMFH_Write(fh, "foo ", 4);
@@ -151,13 +150,17 @@
 
     TEST_TRUE(runner, RAMFH_Window(fh, window, 1021, 2),
               "Window() returns true");
-    TEST_TRUE(runner, strncmp(window_ivars->buf, "oo", 2) == 0, "Window()");
+    TEST_TRUE(runner, strncmp(FileWindow_Get_Buf(window), "oo", 2) == 0,
+              "Window()");
 
     TEST_TRUE(runner, RAMFH_Release_Window(fh, window),
               "Release_Window() returns true");
-    TEST_TRUE(runner, window_ivars->buf == NULL, "Release_Window() resets buf");
-    TEST_TRUE(runner, window_ivars->offset == 0, "Release_Window() resets offset");
-    TEST_TRUE(runner, window_ivars->len == 0, "Release_Window() resets len");
+    TEST_TRUE(runner, FileWindow_Get_Buf(window) == NULL,
+              "Release_Window() resets buf");
+    TEST_TRUE(runner, FileWindow_Get_Offset(window) == 0,
+              "Release_Window() resets offset");
+    TEST_TRUE(runner, FileWindow_Get_Len(window) == 0,
+              "Release_Window() resets len");
 
     DECREF(window);
     DECREF(fh);
diff --git a/core/Lucy/Test/Store/TestRAMFileHandle.cfh b/test/Lucy/Test/Store/TestRAMFileHandle.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestRAMFileHandle.cfh
rename to test/Lucy/Test/Store/TestRAMFileHandle.cfh
diff --git a/core/Lucy/Test/Store/TestRAMFolder.c b/test/Lucy/Test/Store/TestRAMFolder.c
similarity index 100%
rename from core/Lucy/Test/Store/TestRAMFolder.c
rename to test/Lucy/Test/Store/TestRAMFolder.c
diff --git a/core/Lucy/Test/Store/TestRAMFolder.cfh b/test/Lucy/Test/Store/TestRAMFolder.cfh
similarity index 100%
rename from core/Lucy/Test/Store/TestRAMFolder.cfh
rename to test/Lucy/Test/Store/TestRAMFolder.cfh
diff --git a/core/Lucy/Test/TestSchema.c b/test/Lucy/Test/TestSchema.c
similarity index 100%
rename from core/Lucy/Test/TestSchema.c
rename to test/Lucy/Test/TestSchema.c
diff --git a/core/Lucy/Test/TestSchema.cfh b/test/Lucy/Test/TestSchema.cfh
similarity index 100%
rename from core/Lucy/Test/TestSchema.cfh
rename to test/Lucy/Test/TestSchema.cfh
diff --git a/core/Lucy/Test/TestSimple.c b/test/Lucy/Test/TestSimple.c
similarity index 100%
rename from core/Lucy/Test/TestSimple.c
rename to test/Lucy/Test/TestSimple.c
diff --git a/core/Lucy/Test/TestSimple.cfh b/test/Lucy/Test/TestSimple.cfh
similarity index 100%
rename from core/Lucy/Test/TestSimple.cfh
rename to test/Lucy/Test/TestSimple.cfh
diff --git a/core/Lucy/Test/TestUtils.c b/test/Lucy/Test/TestUtils.c
similarity index 100%
rename from core/Lucy/Test/TestUtils.c
rename to test/Lucy/Test/TestUtils.c
diff --git a/core/Lucy/Test/TestUtils.cfh b/test/Lucy/Test/TestUtils.cfh
similarity index 100%
rename from core/Lucy/Test/TestUtils.cfh
rename to test/Lucy/Test/TestUtils.cfh
diff --git a/core/Lucy/Test/Util/TestFreezer.c b/test/Lucy/Test/Util/TestFreezer.c
similarity index 100%
rename from core/Lucy/Test/Util/TestFreezer.c
rename to test/Lucy/Test/Util/TestFreezer.c
diff --git a/core/Lucy/Test/Util/TestFreezer.cfh b/test/Lucy/Test/Util/TestFreezer.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestFreezer.cfh
rename to test/Lucy/Test/Util/TestFreezer.cfh
diff --git a/core/Lucy/Test/Util/TestIndexFileNames.c b/test/Lucy/Test/Util/TestIndexFileNames.c
similarity index 100%
rename from core/Lucy/Test/Util/TestIndexFileNames.c
rename to test/Lucy/Test/Util/TestIndexFileNames.c
diff --git a/core/Lucy/Test/Util/TestIndexFileNames.cfh b/test/Lucy/Test/Util/TestIndexFileNames.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestIndexFileNames.cfh
rename to test/Lucy/Test/Util/TestIndexFileNames.cfh
diff --git a/core/Lucy/Test/Util/TestJson.c b/test/Lucy/Test/Util/TestJson.c
similarity index 100%
rename from core/Lucy/Test/Util/TestJson.c
rename to test/Lucy/Test/Util/TestJson.c
diff --git a/core/Lucy/Test/Util/TestJson.cfh b/test/Lucy/Test/Util/TestJson.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestJson.cfh
rename to test/Lucy/Test/Util/TestJson.cfh
diff --git a/core/Lucy/Test/Util/TestMemoryPool.c b/test/Lucy/Test/Util/TestMemoryPool.c
similarity index 92%
rename from core/Lucy/Test/Util/TestMemoryPool.c
rename to test/Lucy/Test/Util/TestMemoryPool.c
index 7e960ab..f21cf44 100644
--- a/core/Lucy/Test/Util/TestMemoryPool.c
+++ b/test/Lucy/Test/Util/TestMemoryPool.c
@@ -34,7 +34,6 @@
     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);
@@ -45,9 +44,10 @@
     TEST_UINT_EQ(runner, MemPool_Get_Consumed(mem_pool), expected * 2,
                  "Accumulate consumed.");
 
-    ptr_a = ivars->buf;
+    ptr_a = MemPool_Get_Buf(mem_pool);
     MemPool_Resize(mem_pool, ptr_b, 6);
-    TEST_TRUE(runner, ivars->buf < ptr_a, "Resize adjusts next allocation");
+    TEST_TRUE(runner, MemPool_Get_Buf(mem_pool) < ptr_a,
+              "Resize adjusts next allocation");
     TEST_TRUE(runner, MemPool_Get_Consumed(mem_pool) < expected * 2,
                 "Resize() adjusts `consumed`");
 
diff --git a/core/Lucy/Test/Util/TestMemoryPool.cfh b/test/Lucy/Test/Util/TestMemoryPool.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestMemoryPool.cfh
rename to test/Lucy/Test/Util/TestMemoryPool.cfh
diff --git a/core/Lucy/Test/Util/TestNumberUtils.c b/test/Lucy/Test/Util/TestNumberUtils.c
similarity index 100%
rename from core/Lucy/Test/Util/TestNumberUtils.c
rename to test/Lucy/Test/Util/TestNumberUtils.c
diff --git a/core/Lucy/Test/Util/TestNumberUtils.cfh b/test/Lucy/Test/Util/TestNumberUtils.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestNumberUtils.cfh
rename to test/Lucy/Test/Util/TestNumberUtils.cfh
diff --git a/core/Lucy/Test/Util/TestPriorityQueue.c b/test/Lucy/Test/Util/TestPriorityQueue.c
similarity index 100%
rename from core/Lucy/Test/Util/TestPriorityQueue.c
rename to test/Lucy/Test/Util/TestPriorityQueue.c
diff --git a/core/Lucy/Test/Util/TestPriorityQueue.cfh b/test/Lucy/Test/Util/TestPriorityQueue.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestPriorityQueue.cfh
rename to test/Lucy/Test/Util/TestPriorityQueue.cfh
diff --git a/core/Lucy/Test/Util/TestSortExternal.c b/test/Lucy/Test/Util/TestSortExternal.c
similarity index 100%
rename from core/Lucy/Test/Util/TestSortExternal.c
rename to test/Lucy/Test/Util/TestSortExternal.c
diff --git a/core/Lucy/Test/Util/TestSortExternal.cfh b/test/Lucy/Test/Util/TestSortExternal.cfh
similarity index 100%
rename from core/Lucy/Test/Util/TestSortExternal.cfh
rename to test/Lucy/Test/Util/TestSortExternal.cfh
diff --git a/core/TestLucy.c b/test/TestLucy.c
similarity index 100%
rename from core/TestLucy.c
rename to test/TestLucy.c
diff --git a/core/TestLucy.cfp b/test/TestLucy.cfp
similarity index 100%
rename from core/TestLucy.cfp
rename to test/TestLucy.cfp