You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucy.apache.org by nw...@apache.org on 2016/07/15 11:11:57 UTC

[05/22] lucy git commit: Move tests to separate directory

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Search/TestSortSpec.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Search/TestSortSpec.c b/test/Lucy/Test/Search/TestSortSpec.c
new file mode 100644
index 0000000..be6d23f
--- /dev/null
+++ b/test/Lucy/Test/Search/TestSortSpec.c
@@ -0,0 +1,642 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#define C_TESTLUCY_TESTREVERSETYPE
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "charmony.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Clownfish/TestHarness/TestUtils.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Search/TestSortSpec.h"
+#include "Lucy/Test/TestUtils.h"
+#include "Lucy/Search/SortSpec.h"
+
+#include "Clownfish/CharBuf.h"
+#include "Clownfish/Num.h"
+#include "Lucy/Analysis/StandardTokenizer.h"
+#include "Lucy/Document/Doc.h"
+#include "Lucy/Document/HitDoc.h"
+#include "Lucy/Index/Indexer.h"
+#include "Lucy/Plan/FullTextType.h"
+#include "Lucy/Plan/NumericType.h"
+#include "Lucy/Plan/Schema.h"
+#include "Lucy/Plan/StringType.h"
+#include "Lucy/Search/Hits.h"
+#include "Lucy/Search/IndexSearcher.h"
+#include "Lucy/Search/SortRule.h"
+#include "Lucy/Store/RAMFolder.h"
+
+static String *air_str;
+static String *airplane_str;
+static String *bike_str;
+static String *car_str;
+static String *carrot_str;
+static String *cat_str;
+static String *float32_str;
+static String *float64_str;
+static String *food_str;
+static String *home_str;
+static String *int32_str;
+static String *int64_str;
+static String *land_str;
+static String *name_str;
+static String *nope_str;
+static String *num_str;
+static String *random_str;
+static String *sloth_str;
+static String *speed_str;
+static String *unknown_str;
+static String *unused_str;
+static String *vehicle_str;
+static String *weight_str;
+
+static String *random_float32s_str;
+static String *random_float64s_str;
+static String *random_int32s_str;
+static String *random_int64s_str;
+
+TestSortSpec*
+TestSortSpec_new() {
+    return (TestSortSpec*)Class_Make_Obj(TESTSORTSPEC);
+}
+
+static void
+S_init_strings() {
+    air_str      = Str_newf("air");
+    airplane_str = Str_newf("airplane");
+    bike_str     = Str_newf("bike");
+    car_str      = Str_newf("car");
+    carrot_str   = Str_newf("carrot");
+    cat_str      = Str_newf("cat");
+    float32_str  = Str_newf("float32");
+    float64_str  = Str_newf("float64");
+    food_str     = Str_newf("food");
+    home_str     = Str_newf("home");
+    int32_str    = Str_newf("int32");
+    int64_str    = Str_newf("int64");
+    land_str     = Str_newf("land");
+    name_str     = Str_newf("name");
+    nope_str     = Str_newf("nope");
+    num_str      = Str_newf("num");
+    random_str   = Str_newf("random");
+    sloth_str    = Str_newf("sloth");
+    speed_str    = Str_newf("speed");
+    unknown_str  = Str_newf("unknown");
+    unused_str   = Str_newf("unused");
+    vehicle_str  = Str_newf("vehicle");
+    weight_str   = Str_newf("weight");
+
+    random_float32s_str = Str_newf("random_float32s");
+    random_float64s_str = Str_newf("random_float64s");
+    random_int32s_str   = Str_newf("random_int32s");
+    random_int64s_str   = Str_newf("random_int64s");
+}
+
+static void
+S_destroy_strings() {
+    DECREF(air_str);
+    DECREF(airplane_str);
+    DECREF(bike_str);
+    DECREF(car_str);
+    DECREF(carrot_str);
+    DECREF(cat_str);
+    DECREF(float32_str);
+    DECREF(float64_str);
+    DECREF(food_str);
+    DECREF(home_str);
+    DECREF(int32_str);
+    DECREF(int64_str);
+    DECREF(land_str);
+    DECREF(name_str);
+    DECREF(nope_str);
+    DECREF(num_str);
+    DECREF(random_str);
+    DECREF(sloth_str);
+    DECREF(speed_str);
+    DECREF(unknown_str);
+    DECREF(unused_str);
+    DECREF(vehicle_str);
+    DECREF(weight_str);
+
+    DECREF(random_float32s_str);
+    DECREF(random_float64s_str);
+    DECREF(random_int32s_str);
+    DECREF(random_int64s_str);
+}
+
+TestReverseType*
+TestReverseType_new() {
+    TestReverseType *self = (TestReverseType*)Class_Make_Obj(TESTREVERSETYPE);
+    return TestReverseType_init(self);
+}
+
+TestReverseType*
+TestReverseType_init(TestReverseType *self) {
+    return TestReverseType_init2(self, 1.0, false, true, true);
+}
+
+TestReverseType*
+TestReverseType_init2(TestReverseType *self, float boost, bool indexed,
+                      bool stored, bool sortable) {
+    Int32Type_init2((Int32Type*)self, boost, indexed, stored, sortable);
+    return self;
+}
+
+int32_t
+TestReverseType_Compare_Values_IMP(TestReverseType *self, Obj *a, Obj *b) {
+    UNUSED_VAR(self);
+    return Obj_Compare_To(b, a);
+}
+
+static Schema*
+S_create_schema() {
+    Schema *schema = Schema_new();
+
+    StandardTokenizer *tokenizer = StandardTokenizer_new();
+    FullTextType *unsortable = FullTextType_new((Analyzer*)tokenizer);
+    DECREF(tokenizer);
+
+    StringType *string_type = StringType_new();
+    StringType_Set_Sortable(string_type, true);
+
+    Int32Type *int32_type = Int32Type_new();
+    Int32Type_Set_Indexed(int32_type, false);
+    Int32Type_Set_Sortable(int32_type, true);
+
+    Int64Type *int64_type = Int64Type_new();
+    Int64Type_Set_Indexed(int64_type, false);
+    Int64Type_Set_Sortable(int64_type, true);
+
+    Float32Type *float32_type = Float32Type_new();
+    Float32Type_Set_Indexed(float32_type, false);
+    Float32Type_Set_Sortable(float32_type, true);
+
+    Float64Type *float64_type = Float64Type_new();
+    Float64Type_Set_Indexed(float64_type, false);
+    Float64Type_Set_Sortable(float64_type, true);
+
+    TestReverseType *reverse_type = TestReverseType_new();
+
+    Schema_Spec_Field(schema, name_str,    (FieldType*)string_type);
+    Schema_Spec_Field(schema, speed_str,   (FieldType*)int32_type);
+    Schema_Spec_Field(schema, sloth_str,   (FieldType*)reverse_type);
+    Schema_Spec_Field(schema, weight_str,  (FieldType*)int32_type);
+    Schema_Spec_Field(schema, int32_str,   (FieldType*)int32_type);
+    Schema_Spec_Field(schema, int64_str,   (FieldType*)int64_type);
+    Schema_Spec_Field(schema, float32_str, (FieldType*)float32_type);
+    Schema_Spec_Field(schema, float64_str, (FieldType*)float64_type);
+    Schema_Spec_Field(schema, home_str,    (FieldType*)string_type);
+    Schema_Spec_Field(schema, cat_str,     (FieldType*)string_type);
+    Schema_Spec_Field(schema, unused_str,  (FieldType*)string_type);
+    Schema_Spec_Field(schema, nope_str,    (FieldType*)unsortable);
+
+    DECREF(reverse_type);
+    DECREF(float64_type);
+    DECREF(float32_type);
+    DECREF(int64_type);
+    DECREF(int32_type);
+    DECREF(string_type);
+    DECREF(unsortable);
+
+    return schema;
+}
+
+static void
+S_refresh_indexer(Indexer **indexer, Schema *schema, RAMFolder *folder) {
+    if (*indexer) {
+        Indexer_Commit(*indexer);
+        DECREF(*indexer);
+    }
+    *indexer = Indexer_new(schema, (Obj*)folder, NULL, 0);
+}
+
+static void
+S_add_vehicle(Indexer *indexer, String *name, int32_t speed, int32_t sloth,
+              int32_t weight, String *home, String *cat) {
+    Doc       *doc   = Doc_new(NULL, 0);
+
+    Doc_Store(doc, name_str, (Obj*)name);
+    Doc_Store(doc, home_str, (Obj*)home);
+    Doc_Store(doc, cat_str,  (Obj*)cat);
+
+    Integer *speed_obj = Int_new(speed);
+    Doc_Store(doc, speed_str, (Obj*)speed_obj);
+    DECREF(speed_obj);
+    Integer *sloth_obj = Int_new(sloth);
+    Doc_Store(doc, sloth_str, (Obj*)sloth_obj);
+    DECREF(sloth_obj);
+    Integer *weight_obj = Int_new(weight);
+    Doc_Store(doc, weight_str, (Obj*)weight_obj);
+    DECREF(weight_obj);
+
+    Indexer_Add_Doc(indexer, doc, 1.0f);
+
+    DECREF(doc);
+}
+
+static void
+S_add_doc(Indexer *indexer, Obj *value, String *cat, String *field_name) {
+    Doc *doc = Doc_new(NULL, 0);
+    String *name = Obj_To_String(value);
+    Doc_Store(doc, name_str, (Obj*)name);
+    Doc_Store(doc, cat_str,  (Obj*)cat);
+    if (field_name) {
+        Doc_Store(doc, field_name, value);
+    }
+    Indexer_Add_Doc(indexer, doc, 1.0f);
+    DECREF(name);
+    DECREF(doc);
+}
+
+typedef Obj* (*random_generator_t)();
+
+static Obj*
+S_random_string() {
+    size_t length = 1 + rand() % 10;
+    CharBuf *buf = CB_new(length);
+    while (length--) {
+        int32_t code_point = 'a' + rand() % ('z' - 'a' + 1);
+        CB_Cat_Char(buf, code_point);
+    }
+    String *string = CB_Yield_String(buf);
+    DECREF(buf);
+    return (Obj*)string;
+}
+
+static Obj*
+S_random_int32() {
+    uint64_t num = TestUtils_random_u64();
+    return (Obj*)Int_new(num & 0x7FFFFFFF);
+}
+
+static Obj*
+S_random_int64() {
+    uint64_t num = TestUtils_random_u64();
+    return (Obj*)Int_new(num & INT64_C(0x7FFFFFFFFFFFFFFF));
+}
+
+static Obj*
+S_random_float32() {
+    uint64_t num = TestUtils_random_u64();
+    double d = CHY_U64_TO_DOUBLE(num) * (10.0 / UINT64_MAX);
+    return (Obj*)Float_new((float)d);
+}
+
+static Obj*
+S_random_float64() {
+    uint64_t num = TestUtils_random_u64();
+    return (Obj*)Float_new(CHY_U64_TO_DOUBLE(num) * (10.0 / UINT64_MAX));
+}
+
+static Vector*
+S_add_random_objects(Indexer **indexer, Schema *schema, RAMFolder *folder,
+                     random_generator_t rng, String *field_name,
+                     String *cat) {
+    Vector *objects = Vec_new(100);
+
+    for (int i = 0; i < 100; ++i) {
+        Obj *object = rng();
+        S_add_doc(*indexer, object, cat, field_name);
+        Vec_Push(objects, object);
+        if (i % 10 == 0) {
+            S_refresh_indexer(indexer, schema, folder);
+        }
+    }
+
+    Vec_Sort(objects);
+
+    for (size_t i = 0; i < 100; ++i) {
+        Obj *obj = Vec_Fetch(objects, i);
+        String *string = Obj_To_String(obj);
+        Vec_Store(objects, i, (Obj*)string);
+    }
+
+    return objects;
+}
+
+static Vector*
+S_test_sorted_search(IndexSearcher *searcher, String *query,
+                     uint32_t num_wanted, ...) {
+    Vector  *rules = Vec_new(2);
+    String *field;
+    va_list  args;
+
+    va_start(args, num_wanted);
+    while (NULL != (field = va_arg(args, String*))) {
+        int       reverse = va_arg(args, int);
+        SortRule *rule    = SortRule_new(SortRule_FIELD, field, !!reverse);
+        Vec_Push(rules, (Obj*)rule);
+    }
+    va_end(args);
+    SortRule *rule = SortRule_new(SortRule_DOC_ID, NULL, 0);
+    Vec_Push(rules, (Obj*)rule);
+    SortSpec *spec = SortSpec_new(rules);
+
+    Hits *hits = IxSearcher_Hits(searcher, (Obj*)query, 0, num_wanted, spec);
+
+    Vector *results = Vec_new(10);
+    HitDoc *hit_doc;
+    while (NULL != (hit_doc = Hits_Next(hits))) {
+        String *name = (String*)HitDoc_Extract(hit_doc, name_str);
+        Vec_Push(results, (Obj*)Str_Clone((String*)name));
+        DECREF(name);
+        DECREF(hit_doc);
+    }
+
+    DECREF(hits);
+    DECREF(spec);
+    DECREF(rules);
+
+    return results;
+}
+
+typedef struct SortContext {
+    IndexSearcher *searcher;
+    String        *sort_field;
+} SortContext;
+
+static void
+S_attempt_sorted_search(void *context) {
+    SortContext *sort_ctx = (SortContext*)context;
+    Vector *results = S_test_sorted_search(sort_ctx->searcher, vehicle_str, 100,
+                                           sort_ctx->sort_field, false, NULL);
+    DECREF(results);
+}
+
+static void
+test_sort_spec(TestBatchRunner *runner) {
+    RAMFolder *folder  = RAMFolder_new(NULL);
+    Schema    *schema  = S_create_schema();
+    Indexer   *indexer = NULL;
+    Vector    *wanted  = Vec_new(10);
+    Vector    *results;
+    Vector    *results2;
+
+    // First, add vehicles.
+    S_refresh_indexer(&indexer, schema, folder);
+    S_add_vehicle(indexer, airplane_str, 200, 200, 8000, air_str,  vehicle_str);
+    S_add_vehicle(indexer, bike_str,      15,  15,   25, land_str, vehicle_str);
+    S_add_vehicle(indexer, car_str,       70,  70, 3000, land_str, vehicle_str);
+
+    // Add random objects.
+    Vector *random_strings =
+        S_add_random_objects(&indexer, schema, folder, S_random_string,
+                             NULL, random_str);
+    Vector *random_int32s =
+        S_add_random_objects(&indexer, schema, folder, S_random_int32,
+                             int32_str, random_int32s_str);
+    Vector *random_int64s =
+        S_add_random_objects(&indexer, schema, folder, S_random_int64,
+                             int64_str, random_int64s_str);
+    Vector *random_float32s =
+        S_add_random_objects(&indexer, schema, folder, S_random_float32,
+                             float32_str, random_float32s_str);
+    Vector *random_float64s =
+        S_add_random_objects(&indexer, schema, folder, S_random_float64,
+                             float64_str, random_float64s_str);
+
+    // Add numbers to verify consistent ordering.
+    int32_t *nums = (int32_t*)MALLOCATE(100 * sizeof(int32_t));
+    for (int i = 0; i < 100; ++i) {
+        nums[i] = i;
+    }
+    // Shuffle
+    for (int i = 99; i > 0; --i) {
+        int r = rand() % (i + 1);
+        if (r != i) {
+            // Swap
+            int32_t tmp = nums[i];
+            nums[i] = nums[r];
+            nums[r] = tmp;
+        }
+    }
+    for (int i = 0; i < 100; ++i) {
+        char name_buf[3];
+        sprintf(name_buf, "%02d", nums[i]);
+        String *name = SSTR_WRAP_UTF8(name_buf, 2);
+        S_add_doc(indexer, (Obj*)name, num_str, NULL);
+        if (i % 10 == 0) {
+            S_refresh_indexer(&indexer, schema, folder);
+        }
+    }
+    FREEMEM(nums);
+
+    Indexer_Commit(indexer);
+    DECREF(indexer);
+
+    // Start tests
+
+    IndexSearcher *searcher = IxSearcher_new((Obj*)folder);
+
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   name_str, false, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(airplane_str));
+    Vec_Push(wanted, INCREF(bike_str));
+    Vec_Push(wanted, INCREF(car_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted), "sort by one criteria");
+    DECREF(results);
+
+#ifdef LUCY_VALGRIND
+    SKIP(runner, 2, "known leaks");
+#else
+    Err *error;
+    SortContext sort_ctx;
+    sort_ctx.searcher = searcher;
+
+    sort_ctx.sort_field = nope_str;
+    error = Err_trap(S_attempt_sorted_search, &sort_ctx);
+    TEST_TRUE(runner, error != NULL
+              && Err_is_a(error, ERR)
+              && Str_Contains_Utf8(Err_Get_Mess(error), "sortable", 8),
+              "sorting on a non-sortable field throws an error");
+    DECREF(error);
+
+    sort_ctx.sort_field = unknown_str;
+    error = Err_trap(S_attempt_sorted_search, &sort_ctx);
+    TEST_TRUE(runner, error != NULL
+              && Err_is_a(error, ERR)
+              && Str_Contains_Utf8(Err_Get_Mess(error), "sortable", 8),
+              "sorting on an unknown field throws an error");
+    DECREF(error);
+#endif
+
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   weight_str, false, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(bike_str));
+    Vec_Push(wanted, INCREF(car_str));
+    Vec_Push(wanted, INCREF(airplane_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted), "sort by one criteria");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   name_str, true, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(car_str));
+    Vec_Push(wanted, INCREF(bike_str));
+    Vec_Push(wanted, INCREF(airplane_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted), "reverse sort");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   home_str, false, name_str, false, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(airplane_str));
+    Vec_Push(wanted, INCREF(bike_str));
+    Vec_Push(wanted, INCREF(car_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted), "multiple criteria");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   home_str, false, name_str, true, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(airplane_str));
+    Vec_Push(wanted, INCREF(car_str));
+    Vec_Push(wanted, INCREF(bike_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted),
+              "multiple criteria with reverse");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   speed_str, true, NULL);
+    results2 = S_test_sorted_search(searcher, vehicle_str, 100,
+                                    sloth_str, false, NULL);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)results2),
+              "FieldType_Compare_Values");
+    DECREF(results2);
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, random_str, 100,
+                                   name_str, false, NULL);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)random_strings),
+              "random strings");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, random_int32s_str, 100,
+                                   int32_str, false, NULL);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)random_int32s),
+              "int32");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, random_int64s_str, 100,
+                                   int64_str, false, NULL);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)random_int64s),
+              "int64");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, random_float32s_str, 100,
+                                   float32_str, false, NULL);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)random_float32s),
+              "float32");
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, random_float64s_str, 100,
+                                   float64_str, false, NULL);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)random_float64s),
+              "float64");
+    DECREF(results);
+
+    String *bbbcca_str = Str_newf("bike bike bike car car airplane");
+    results = S_test_sorted_search(searcher, bbbcca_str, 100,
+                                   unused_str, false, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(airplane_str));
+    Vec_Push(wanted, INCREF(bike_str));
+    Vec_Push(wanted, INCREF(car_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted),
+              "sorting on field with no values sorts by doc id");
+    DECREF(results);
+    DECREF(bbbcca_str);
+
+    String *nn_str        = Str_newf("99");
+    String *nn_or_car_str = Str_newf("99 OR car");
+    results = S_test_sorted_search(searcher, nn_or_car_str, 10,
+                                   speed_str, false, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(car_str));
+    Vec_Push(wanted, INCREF(nn_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted),
+              "doc with NULL value sorts last");
+    DECREF(results);
+    DECREF(nn_str);
+    DECREF(nn_or_car_str);
+
+    results = S_test_sorted_search(searcher, num_str, 10,
+                                   name_str, false, NULL);
+    results2 = S_test_sorted_search(searcher, num_str, 30,
+                                    name_str, false, NULL);
+    Vec_Resize(results2, 10);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)results2),
+              "same order regardless of queue size");
+    DECREF(results2);
+    DECREF(results);
+
+    results = S_test_sorted_search(searcher, num_str, 10,
+                                   name_str, true, NULL);
+    results2 = S_test_sorted_search(searcher, num_str, 30,
+                                    name_str, true, NULL);
+    Vec_Resize(results2, 10);
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)results2),
+              "same order regardless of queue size (reverse sort)");
+    DECREF(results2);
+    DECREF(results);
+
+    DECREF(searcher);
+
+    // Add another seg to index.
+    indexer = Indexer_new(schema, (Obj*)folder, NULL, 0);
+    S_add_vehicle(indexer, carrot_str, 0, 0, 1, land_str, food_str);
+    Indexer_Commit(indexer);
+    DECREF(indexer);
+
+    searcher = IxSearcher_new((Obj*)folder);
+    results = S_test_sorted_search(searcher, vehicle_str, 100,
+                                   name_str, false, NULL);
+    Vec_Clear(wanted);
+    Vec_Push(wanted, INCREF(airplane_str));
+    Vec_Push(wanted, INCREF(bike_str));
+    Vec_Push(wanted, INCREF(car_str));
+    TEST_TRUE(runner, Vec_Equals(results, (Obj*)wanted), "Multi-segment sort");
+    DECREF(results);
+    DECREF(searcher);
+
+    DECREF(random_strings);
+    DECREF(random_int32s);
+    DECREF(random_int64s);
+    DECREF(random_float32s);
+    DECREF(random_float64s);
+
+    DECREF(wanted);
+    DECREF(schema);
+    DECREF(folder);
+}
+
+void
+TestSortSpec_Run_IMP(TestSortSpec *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 18);
+    S_init_strings();
+    test_sort_spec(runner);
+    S_destroy_strings();
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Search/TestSortSpec.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Search/TestSortSpec.cfh b/test/Lucy/Test/Search/TestSortSpec.cfh
new file mode 100644
index 0000000..1d559da
--- /dev/null
+++ b/test/Lucy/Test/Search/TestSortSpec.cfh
@@ -0,0 +1,43 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Search::TestSortSpec
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestSortSpec*
+    new();
+
+    void
+    Run(TestSortSpec *self, TestBatchRunner *runner);
+}
+
+class Lucy::Test::Search::TestReverseType inherits Lucy::Plan::Int32Type {
+    public inert TestReverseType*
+    new();
+
+    public inert TestReverseType*
+    init(TestReverseType *self);
+
+    inert TestReverseType*
+    init2(TestReverseType *self, float boost = 1.0, bool indexed = true,
+          bool stored = true, bool sortable = false);
+
+    int32_t
+    Compare_Values(TestReverseType *self, Obj *a, Obj *b);
+}
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Search/TestSpan.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Search/TestSpan.c b/test/Lucy/Test/Search/TestSpan.c
new file mode 100644
index 0000000..6743f22
--- /dev/null
+++ b/test/Lucy/Test/Search/TestSpan.c
@@ -0,0 +1,53 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define C_TESTLUCY_TESTTERMINFO
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Search/TestSpan.h"
+#include "Lucy/Search/Span.h"
+
+TestSpan*
+TestSpan_new() {
+    return (TestSpan*)Class_Make_Obj(TESTSPAN);
+}
+
+void 
+test_span_init_values(TestBatchRunner *runner) {
+    Span* span = Span_new(2,3,7.0);
+    TEST_INT_EQ(runner, Span_Get_Offset(span), 2, "get_offset" );
+    TEST_INT_EQ(runner, Span_Get_Length(span), 3, "get_length" );
+    TEST_FLOAT_EQ(runner, Span_Get_Weight(span), 7.0, "get_weight" );
+
+    Span_Set_Offset(span, 10);
+    Span_Set_Length(span, 1);
+    Span_Set_Weight(span, 4.0);
+
+    TEST_INT_EQ(runner, Span_Get_Offset(span), 10, "set_offset" );
+    TEST_INT_EQ(runner, Span_Get_Length(span), 1, "set_length" );
+    TEST_FLOAT_EQ(runner, Span_Get_Weight(span), 4.0, "set_weight" );
+
+    DECREF(span);
+}
+
+void
+TestSpan_Run_IMP(TestSpan *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 6);
+    test_span_init_values(runner);
+}

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Search/TestSpan.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Search/TestSpan.cfh b/test/Lucy/Test/Search/TestSpan.cfh
new file mode 100644
index 0000000..d84c45f
--- /dev/null
+++ b/test/Lucy/Test/Search/TestSpan.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Search::TestSpan
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestSpan*
+    new();
+
+    void
+    Run(TestSpan *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Search/TestTermQuery.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Search/TestTermQuery.c b/test/Lucy/Test/Search/TestTermQuery.c
new file mode 100644
index 0000000..a330913
--- /dev/null
+++ b/test/Lucy/Test/Search/TestTermQuery.c
@@ -0,0 +1,66 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define C_TESTLUCY_TESTTERMQUERY
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+#include <math.h>
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Search/TestTermQuery.h"
+#include "Lucy/Test/TestUtils.h"
+#include "Lucy/Search/TermQuery.h"
+
+TestTermQuery*
+TestTermQuery_new() {
+    return (TestTermQuery*)Class_Make_Obj(TESTTERMQUERY);
+}
+
+static void
+test_Dump_Load_and_Equals(TestBatchRunner *runner) {
+    TermQuery *query         = TestUtils_make_term_query("content", "foo");
+    TermQuery *field_differs = TestUtils_make_term_query("stuff", "foo");
+    TermQuery *term_differs  = TestUtils_make_term_query("content", "bar");
+    TermQuery *boost_differs = TestUtils_make_term_query("content", "foo");
+    Obj       *dump          = (Obj*)TermQuery_Dump(query);
+    TermQuery *clone         = (TermQuery*)TermQuery_Load(term_differs, dump);
+
+    TEST_FALSE(runner, TermQuery_Equals(query, (Obj*)field_differs),
+               "Equals() false with different field");
+    TEST_FALSE(runner, TermQuery_Equals(query, (Obj*)term_differs),
+               "Equals() false with different term");
+    TermQuery_Set_Boost(boost_differs, 0.5);
+    TEST_FALSE(runner, TermQuery_Equals(query, (Obj*)boost_differs),
+               "Equals() false with different boost");
+    TEST_TRUE(runner, TermQuery_Equals(query, (Obj*)clone),
+              "Dump => Load round trip");
+
+    DECREF(query);
+    DECREF(term_differs);
+    DECREF(field_differs);
+    DECREF(boost_differs);
+    DECREF(dump);
+    DECREF(clone);
+}
+
+void
+TestTermQuery_Run_IMP(TestTermQuery *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 4);
+    test_Dump_Load_and_Equals(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Search/TestTermQuery.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Search/TestTermQuery.cfh b/test/Lucy/Test/Search/TestTermQuery.cfh
new file mode 100644
index 0000000..ac997aa
--- /dev/null
+++ b/test/Lucy/Test/Search/TestTermQuery.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Search::TestTermQuery
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestTermQuery*
+    new();
+
+    void
+    Run(TestTermQuery *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/MockFileHandle.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/MockFileHandle.c b/test/Lucy/Test/Store/MockFileHandle.c
new file mode 100644
index 0000000..029eb5f
--- /dev/null
+++ b/test/Lucy/Test/Store/MockFileHandle.c
@@ -0,0 +1,66 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define C_TESTLUCY_MOCKFILEHANDLE
+#define C_LUCY_FILEWINDOW
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Lucy/Test/Store/MockFileHandle.h"
+#include "Lucy/Store/FileWindow.h"
+
+MockFileHandle*
+MockFileHandle_new(String *path, int64_t length) {
+    MockFileHandle *self = (MockFileHandle*)Class_Make_Obj(MOCKFILEHANDLE);
+    return MockFileHandle_init(self, path, length);
+}
+
+MockFileHandle*
+MockFileHandle_init(MockFileHandle *self, String *path,
+                    int64_t length) {
+    FH_do_open((FileHandle*)self, path, 0);
+    MockFileHandleIVARS *const ivars = MockFileHandle_IVARS(self);
+    ivars->len = length;
+    return self;
+}
+
+bool
+MockFileHandle_Window_IMP(MockFileHandle *self, FileWindow *window,
+                          int64_t offset, int64_t len) {
+    UNUSED_VAR(self);
+    FileWindow_Set_Window(window, NULL, offset, len);
+    return true;
+}
+
+bool
+MockFileHandle_Release_Window_IMP(MockFileHandle *self, FileWindow *window) {
+    UNUSED_VAR(self);
+    FileWindow_Set_Window(window, NULL, 0, 0);
+    return true;
+}
+
+int64_t
+MockFileHandle_Length_IMP(MockFileHandle *self) {
+    return MockFileHandle_IVARS(self)->len;
+}
+
+bool
+MockFileHandle_Close_IMP(MockFileHandle *self) {
+    UNUSED_VAR(self);
+    return true;
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/MockFileHandle.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/MockFileHandle.cfh b/test/Lucy/Test/Store/MockFileHandle.cfh
new file mode 100644
index 0000000..fe979f4
--- /dev/null
+++ b/test/Lucy/Test/Store/MockFileHandle.cfh
@@ -0,0 +1,44 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+/** Mock-object FileHandle for testing InStream/OutStream.
+ */
+class Lucy::Store::MockFileHandle inherits Lucy::Store::FileHandle {
+
+    int64_t len;
+
+    inert incremented MockFileHandle*
+    new(String *path = NULL, int64_t length);
+
+    inert MockFileHandle*
+    init(MockFileHandle *self, String *path = NULL, int64_t length);
+
+    bool
+    Window(MockFileHandle *self, FileWindow *window, int64_t offset, int64_t len);
+
+    bool
+    Release_Window(MockFileHandle *self, FileWindow *window);
+
+    int64_t
+    Length(MockFileHandle *self);
+
+    bool
+    Close(MockFileHandle *self);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestCompoundFileReader.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestCompoundFileReader.c b/test/Lucy/Test/Store/TestCompoundFileReader.c
new file mode 100644
index 0000000..f3cb6f3
--- /dev/null
+++ b/test/Lucy/Test/Store/TestCompoundFileReader.c
@@ -0,0 +1,375 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define C_LUCY_RAMFOLDER
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestCompoundFileReader.h"
+#include "Lucy/Store/CompoundFileReader.h"
+#include "Lucy/Store/CompoundFileWriter.h"
+#include "Lucy/Store/FileHandle.h"
+#include "Lucy/Store/InStream.h"
+#include "Lucy/Store/OutStream.h"
+#include "Lucy/Store/RAMFolder.h"
+#include "Lucy/Util/Json.h"
+
+static String *cfmeta_file = NULL;
+static String *cfmeta_temp = NULL;
+static String *cf_file     = NULL;
+static String *foo         = NULL;
+static String *bar         = NULL;
+static String *baz         = NULL;
+static String *seg_1       = NULL;
+static String *stuff       = NULL;
+
+TestCompoundFileReader*
+TestCFReader_new() {
+    return (TestCompoundFileReader*)Class_Make_Obj(TESTCOMPOUNDFILEREADER);
+}
+
+static void
+S_init_strings(void) {
+    cfmeta_file = Str_newf("cfmeta.json");
+    cfmeta_temp = Str_newf("cfmeta.json.temp");
+    cf_file     = Str_newf("cf.dat");
+    foo         = Str_newf("foo");
+    bar         = Str_newf("bar");
+    baz         = Str_newf("baz");
+    seg_1       = Str_newf("seg_1");
+    stuff       = Str_newf("stuff");
+}
+
+static void
+S_destroy_strings(void) {
+    DECREF(cfmeta_file);
+    DECREF(cfmeta_temp);
+    DECREF(cf_file);
+    DECREF(foo);
+    DECREF(bar);
+    DECREF(baz);
+    DECREF(seg_1);
+    DECREF(stuff);
+}
+
+static Folder*
+S_folder_with_contents() {
+    RAMFolder *folder  = RAMFolder_new(seg_1);
+    OutStream *foo_out = RAMFolder_Open_Out(folder, foo);
+    OutStream *bar_out = RAMFolder_Open_Out(folder, bar);
+    OutStream_Write_Bytes(foo_out, "foo", 3);
+    OutStream_Write_Bytes(bar_out, "bar", 3);
+    OutStream_Close(foo_out);
+    OutStream_Close(bar_out);
+    DECREF(foo_out);
+    DECREF(bar_out);
+    String *empty = SSTR_BLANK();
+    RAMFolder_Consolidate(folder, empty);
+    return (Folder*)folder;
+}
+
+static void
+test_open(TestBatchRunner *runner) {
+    Folder *real_folder;
+    CompoundFileReader *cf_reader;
+    Hash *metadata;
+
+    Err_set_error(NULL);
+    real_folder = S_folder_with_contents();
+    Folder_Delete(real_folder, cfmeta_file);
+    cf_reader = CFReader_open(real_folder);
+    TEST_TRUE(runner, cf_reader == NULL,
+              "Return NULL when cfmeta file missing");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Set global error when cfmeta file missing");
+    DECREF(real_folder);
+
+    Err_set_error(NULL);
+    real_folder = S_folder_with_contents();
+    Folder_Delete(real_folder, cf_file);
+    cf_reader = CFReader_open(real_folder);
+    TEST_TRUE(runner, cf_reader == NULL,
+              "Return NULL when cf.dat file missing");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Set global error when cf.dat file missing");
+    DECREF(real_folder);
+
+    Err_set_error(NULL);
+    real_folder = S_folder_with_contents();
+    metadata = (Hash*)Json_slurp_json(real_folder, cfmeta_file);
+    Hash_Store_Utf8(metadata, "format", 6, (Obj*)Str_newf("%i32", -1));
+    Folder_Delete(real_folder, cfmeta_file);
+    Json_spew_json((Obj*)metadata, real_folder, cfmeta_file);
+    cf_reader = CFReader_open(real_folder);
+    TEST_TRUE(runner, cf_reader == NULL,
+              "Return NULL when format is invalid");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Set global error when format is invalid");
+
+    Err_set_error(NULL);
+    Hash_Store_Utf8(metadata, "format", 6, (Obj*)Str_newf("%i32", 1000));
+    Folder_Delete(real_folder, cfmeta_file);
+    Json_spew_json((Obj*)metadata, real_folder, cfmeta_file);
+    cf_reader = CFReader_open(real_folder);
+    TEST_TRUE(runner, cf_reader == NULL,
+              "Return NULL when format is too recent");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Set global error when format too recent");
+
+    Err_set_error(NULL);
+    DECREF(Hash_Delete_Utf8(metadata, "format", 6));
+    Folder_Delete(real_folder, cfmeta_file);
+    Json_spew_json((Obj*)metadata, real_folder, cfmeta_file);
+    cf_reader = CFReader_open(real_folder);
+    TEST_TRUE(runner, cf_reader == NULL,
+              "Return NULL when format key is missing");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Set global error when format key is missing");
+
+    Hash_Store_Utf8(metadata, "format", 6,
+                    (Obj*)Str_newf("%i32", CFWriter_current_file_format));
+    DECREF(Hash_Delete_Utf8(metadata, "files", 5));
+    Folder_Delete(real_folder, cfmeta_file);
+    Json_spew_json((Obj*)metadata, real_folder, cfmeta_file);
+    cf_reader = CFReader_open(real_folder);
+    TEST_TRUE(runner, cf_reader == NULL,
+              "Return NULL when files key is missing");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Set global error when files key is missing");
+
+    DECREF(metadata);
+    DECREF(real_folder);
+}
+
+static void
+test_Local_MkDir_and_Find_Folder(TestBatchRunner *runner) {
+    Folder *real_folder = S_folder_with_contents();
+    CompoundFileReader *cf_reader = CFReader_open(real_folder);
+
+    TEST_FALSE(runner,
+               CFReader_Local_Is_Directory(cf_reader, stuff),
+               "Local_Is_Directory returns false for non-existent entry");
+
+    TEST_TRUE(runner, CFReader_MkDir(cf_reader, stuff),
+              "MkDir returns true");
+    TEST_TRUE(runner,
+              Folder_Find_Folder(real_folder, stuff) != NULL,
+              "Local_MkDir pass-through");
+    TEST_TRUE(runner,
+              Folder_Find_Folder(real_folder, stuff)
+              == CFReader_Find_Folder(cf_reader, stuff),
+              "Local_Find_Folder pass-through");
+    TEST_TRUE(runner,
+              CFReader_Local_Is_Directory(cf_reader, stuff),
+              "Local_Is_Directory pass through");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, CFReader_MkDir(cf_reader, stuff),
+               "MkDir returns false when dir already exists");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "MkDir sets global error when dir already exists");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, CFReader_MkDir(cf_reader, foo),
+               "MkDir returns false when virtual file exists");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "MkDir sets global error when virtual file exists");
+
+    TEST_TRUE(runner,
+              CFReader_Find_Folder(cf_reader, foo) == NULL,
+              "Virtual file not reported as directory");
+    TEST_FALSE(runner, CFReader_Local_Is_Directory(cf_reader, foo),
+               "Local_Is_Directory returns false for virtual file");
+
+    DECREF(real_folder);
+    DECREF(cf_reader);
+}
+
+static void
+test_Local_Delete_and_Exists(TestBatchRunner *runner) {
+    Folder *real_folder = S_folder_with_contents();
+    CompoundFileReader *cf_reader = CFReader_open(real_folder);
+
+    CFReader_MkDir(cf_reader, stuff);
+    TEST_TRUE(runner, CFReader_Local_Exists(cf_reader, stuff),
+              "pass through for Local_Exists");
+    TEST_TRUE(runner, CFReader_Local_Exists(cf_reader, foo),
+              "Local_Exists returns true for virtual file");
+
+    TEST_TRUE(runner,
+              CFReader_Local_Exists(cf_reader, cfmeta_file),
+              "cfmeta file exists");
+
+    TEST_TRUE(runner, CFReader_Local_Delete(cf_reader, stuff),
+              "Local_Delete returns true when zapping real entity");
+    TEST_FALSE(runner, CFReader_Local_Exists(cf_reader, stuff),
+               "Local_Exists returns false after real entity zapped");
+
+    TEST_TRUE(runner, CFReader_Local_Delete(cf_reader, foo),
+              "Local_Delete returns true when zapping virtual file");
+    TEST_FALSE(runner, CFReader_Local_Exists(cf_reader, foo),
+               "Local_Exists returns false after virtual file zapped");
+
+    TEST_TRUE(runner, CFReader_Local_Delete(cf_reader, bar),
+              "Local_Delete returns true when zapping last virtual file");
+    TEST_FALSE(runner,
+               CFReader_Local_Exists(cf_reader, cfmeta_file),
+               "cfmeta file deleted when last virtual file deleted");
+    TEST_FALSE(runner,
+               CFReader_Local_Exists(cf_reader, cf_file),
+               "compound data file deleted when last virtual file deleted");
+
+    DECREF(cf_reader);
+    DECREF(real_folder);
+}
+
+static void
+test_Local_Open_Dir(TestBatchRunner *runner) {
+
+    Folder *real_folder = S_folder_with_contents();
+    CompoundFileReader *cf_reader = CFReader_open(real_folder);
+    bool saw_foo       = false;
+    bool saw_stuff     = false;
+    bool stuff_was_dir = false;
+
+    CFReader_MkDir(cf_reader, stuff);
+
+    DirHandle *dh = CFReader_Local_Open_Dir(cf_reader);
+    while (DH_Next(dh)) {
+        String *entry = DH_Get_Entry(dh);
+        if (Str_Equals(entry, (Obj*)foo)) {
+            saw_foo = true;
+        }
+        else if (Str_Equals(entry, (Obj*)stuff)) {
+            saw_stuff = true;
+            stuff_was_dir = DH_Entry_Is_Dir(dh);
+        }
+        DECREF(entry);
+    }
+
+    TEST_TRUE(runner, saw_foo, "DirHandle iterated over virtual file");
+    TEST_TRUE(runner, saw_stuff, "DirHandle iterated over real directory");
+    TEST_TRUE(runner, stuff_was_dir,
+              "DirHandle knew that real entry was dir");
+
+    DECREF(dh);
+    DECREF(cf_reader);
+    DECREF(real_folder);
+}
+
+static void
+test_Local_Open_FileHandle(TestBatchRunner *runner) {
+    Folder *real_folder = S_folder_with_contents();
+    CompoundFileReader *cf_reader = CFReader_open(real_folder);
+    FileHandle *fh;
+
+    OutStream *outstream = CFReader_Open_Out(cf_reader, baz);
+    OutStream_Write_Bytes(outstream, "baz", 3);
+    OutStream_Close(outstream);
+    DECREF(outstream);
+
+    fh = CFReader_Local_Open_FileHandle(cf_reader, baz,
+                                        FH_READ_ONLY);
+    TEST_TRUE(runner, fh != NULL,
+              "Local_Open_FileHandle pass-through for real file");
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    fh = CFReader_Local_Open_FileHandle(cf_reader, stuff,
+                                        FH_READ_ONLY);
+    TEST_TRUE(runner, fh == NULL,
+              "Local_Open_FileHandle for non-existent file returns NULL");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Local_Open_FileHandle for non-existent file sets global error");
+
+    Err_set_error(NULL);
+    fh = CFReader_Local_Open_FileHandle(cf_reader, foo,
+                                        FH_READ_ONLY);
+    TEST_TRUE(runner, fh == NULL,
+              "Local_Open_FileHandle for virtual file returns NULL");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Local_Open_FileHandle for virtual file sets global error");
+
+    DECREF(cf_reader);
+    DECREF(real_folder);
+}
+
+static void
+test_Local_Open_In(TestBatchRunner *runner) {
+    Folder *real_folder = S_folder_with_contents();
+    CompoundFileReader *cf_reader = CFReader_open(real_folder);
+    InStream *instream;
+
+    instream = CFReader_Local_Open_In(cf_reader, foo);
+    TEST_TRUE(runner, instream != NULL,
+              "Local_Open_In for virtual file");
+    TEST_TRUE(runner,
+              Str_Starts_With(InStream_Get_Filename(instream), CFReader_Get_Path(cf_reader)),
+              "InStream's path includes directory");
+    DECREF(instream);
+
+    OutStream *outstream = CFReader_Open_Out(cf_reader, baz);
+    OutStream_Write_Bytes(outstream, "baz", 3);
+    OutStream_Close(outstream);
+    DECREF(outstream);
+    instream = CFReader_Local_Open_In(cf_reader, baz);
+    TEST_TRUE(runner, instream != NULL,
+              "Local_Open_In pass-through for real file");
+    DECREF(instream);
+
+    Err_set_error(NULL);
+    instream = CFReader_Local_Open_In(cf_reader, stuff);
+    TEST_TRUE(runner, instream == NULL,
+              "Local_Open_In for non-existent file returns NULL");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Local_Open_In for non-existent file sets global error");
+
+    DECREF(cf_reader);
+    DECREF(real_folder);
+}
+
+static void
+test_Close(TestBatchRunner *runner) {
+    Folder *real_folder = S_folder_with_contents();
+    CompoundFileReader *cf_reader = CFReader_open(real_folder);
+
+    CFReader_Close(cf_reader);
+    PASS(runner, "Close completes without incident");
+
+    CFReader_Close(cf_reader);
+    PASS(runner, "Calling Close() multiple times is ok");
+
+    DECREF(cf_reader);
+    DECREF(real_folder);
+}
+
+void
+TestCFReader_Run_IMP(TestCompoundFileReader *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 48);
+    S_init_strings();
+    test_open(runner);
+    test_Local_MkDir_and_Find_Folder(runner);
+    test_Local_Delete_and_Exists(runner);
+    test_Local_Open_Dir(runner);
+    test_Local_Open_FileHandle(runner);
+    test_Local_Open_In(runner);
+    test_Close(runner);
+    S_destroy_strings();
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestCompoundFileReader.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestCompoundFileReader.cfh b/test/Lucy/Test/Store/TestCompoundFileReader.cfh
new file mode 100644
index 0000000..69af74d
--- /dev/null
+++ b/test/Lucy/Test/Store/TestCompoundFileReader.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Store::TestCompoundFileReader nickname TestCFReader
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestCompoundFileReader*
+    new();
+
+    void
+    Run(TestCompoundFileReader *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestCompoundFileWriter.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestCompoundFileWriter.c b/test/Lucy/Test/Store/TestCompoundFileWriter.c
new file mode 100644
index 0000000..22a27cb
--- /dev/null
+++ b/test/Lucy/Test/Store/TestCompoundFileWriter.c
@@ -0,0 +1,162 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdlib.h>
+
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "charmony.h"
+
+#include "Clownfish/HashIterator.h"
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestCompoundFileWriter.h"
+#include "Lucy/Store/CompoundFileWriter.h"
+#include "Lucy/Store/FileHandle.h"
+#include "Lucy/Store/OutStream.h"
+#include "Lucy/Store/RAMFolder.h"
+#include "Lucy/Util/Json.h"
+
+static String *cfmeta_file = NULL;
+static String *cfmeta_temp = NULL;
+static String *cf_file     = NULL;
+static String *foo         = NULL;
+static String *bar         = NULL;
+static String *seg_1       = NULL;
+
+TestCompoundFileWriter*
+TestCFWriter_new() {
+    return (TestCompoundFileWriter*)Class_Make_Obj(TESTCOMPOUNDFILEWRITER);
+}
+
+static void
+S_init_strings(void) {
+    cfmeta_file = Str_newf("cfmeta.json");
+    cfmeta_temp = Str_newf("cfmeta.json.temp");
+    cf_file     = Str_newf("cf.dat");
+    foo         = Str_newf("foo");
+    bar         = Str_newf("bar");
+    seg_1       = Str_newf("seg_1");
+}
+
+static void
+S_destroy_strings(void) {
+    DECREF(cfmeta_file);
+    DECREF(cfmeta_temp);
+    DECREF(cf_file);
+    DECREF(foo);
+    DECREF(bar);
+    DECREF(seg_1);
+}
+
+static Folder*
+S_folder_with_contents() {
+    RAMFolder *folder  = RAMFolder_new(seg_1);
+    OutStream *foo_out = RAMFolder_Open_Out(folder, foo);
+    OutStream *bar_out = RAMFolder_Open_Out(folder, bar);
+    OutStream_Write_Bytes(foo_out, "foo", 3);
+    OutStream_Write_Bytes(bar_out, "bar", 3);
+    OutStream_Close(foo_out);
+    OutStream_Close(bar_out);
+    DECREF(foo_out);
+    DECREF(bar_out);
+    return (Folder*)folder;
+}
+
+static void
+test_Consolidate(TestBatchRunner *runner) {
+    Folder *folder = S_folder_with_contents();
+    FileHandle *fh;
+
+    // Fake up detritus from failed consolidation.
+    fh = Folder_Open_FileHandle(folder, cf_file,
+                                FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+    DECREF(fh);
+    fh = Folder_Open_FileHandle(folder, cfmeta_temp,
+                                FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+    DECREF(fh);
+
+    CompoundFileWriter *cf_writer = CFWriter_new(folder);
+    CFWriter_Consolidate(cf_writer);
+    PASS(runner, "Consolidate completes despite leftover files");
+    DECREF(cf_writer);
+
+    TEST_TRUE(runner, Folder_Exists(folder, cf_file),
+              "cf.dat file written");
+    TEST_TRUE(runner, Folder_Exists(folder, cfmeta_file),
+              "cfmeta.json file written");
+    TEST_FALSE(runner, Folder_Exists(folder, foo),
+               "original file zapped");
+    TEST_FALSE(runner, Folder_Exists(folder, cfmeta_temp),
+               "detritus from failed consolidation zapped");
+
+    DECREF(folder);
+}
+
+static void
+test_offsets(TestBatchRunner *runner) {
+    Folder *folder = S_folder_with_contents();
+    CompoundFileWriter *cf_writer = CFWriter_new(folder);
+    Hash    *cf_metadata;
+    Hash    *files;
+
+    CFWriter_Consolidate(cf_writer);
+
+    cf_metadata = (Hash*)CERTIFY(
+                      Json_slurp_json(folder, cfmeta_file), HASH);
+    files = (Hash*)CERTIFY(
+                Hash_Fetch_Utf8(cf_metadata, "files", 5), HASH);
+
+    bool     offsets_ok = true;
+
+    TEST_TRUE(runner, Hash_Get_Size(files) > 0, "Multiple files");
+
+    HashIterator *iter = HashIter_new(files);
+    while (HashIter_Next(iter)) {
+        String *file   = HashIter_Get_Key(iter);
+        Hash   *stats  = (Hash*)CERTIFY(HashIter_Get_Value(iter), HASH);
+        Obj    *offset = CERTIFY(Hash_Fetch_Utf8(stats, "offset", 6), OBJ);
+        int64_t offs   = Json_obj_to_i64(offset);
+        if (offs % 8 != 0) {
+            offsets_ok = false;
+            char *str = Str_To_Utf8(file);
+            FAIL(runner, "Offset %" PRId64 " for %s not a multiple of 8",
+                 offset, str);
+            free(str);
+            break;
+        }
+    }
+    DECREF(iter);
+    if (offsets_ok) {
+        PASS(runner, "All offsets are multiples of 8");
+    }
+
+    DECREF(cf_metadata);
+    DECREF(cf_writer);
+    DECREF(folder);
+}
+
+void
+TestCFWriter_Run_IMP(TestCompoundFileWriter *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 7);
+    S_init_strings();
+    test_Consolidate(runner);
+    test_offsets(runner);
+    S_destroy_strings();
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestCompoundFileWriter.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestCompoundFileWriter.cfh b/test/Lucy/Test/Store/TestCompoundFileWriter.cfh
new file mode 100644
index 0000000..051ad65
--- /dev/null
+++ b/test/Lucy/Test/Store/TestCompoundFileWriter.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Store::TestCompoundFileWriter nickname TestCFWriter
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestCompoundFileWriter*
+    new();
+
+    void
+    Run(TestCompoundFileWriter *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFSDirHandle.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFSDirHandle.c b/test/Lucy/Test/Store/TestFSDirHandle.c
new file mode 100644
index 0000000..4be2b84
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFSDirHandle.c
@@ -0,0 +1,106 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "charmony.h"
+
+// rmdir
+#ifdef CHY_HAS_DIRECT_H
+  #include <direct.h>
+#endif
+
+// rmdir
+#ifdef CHY_HAS_UNISTD_H
+  #include <unistd.h>
+#endif
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestFSDirHandle.h"
+#include "Lucy/Store/FSDirHandle.h"
+#include "Lucy/Store/FSFolder.h"
+#include "Lucy/Store/OutStream.h"
+
+TestFSDirHandle*
+TestFSDH_new() {
+    return (TestFSDirHandle*)Class_Make_Obj(TESTFSDIRHANDLE);
+}
+
+static void
+test_all(TestBatchRunner *runner) {
+    String   *foo           = SSTR_WRAP_C("foo");
+    String   *boffo         = SSTR_WRAP_C("boffo");
+    String   *foo_boffo     = SSTR_WRAP_C("foo/boffo");
+    String   *test_dir      = SSTR_WRAP_C("_fsdir_test");
+    FSFolder *folder        = FSFolder_new(test_dir);
+    bool      saw_foo       = false;
+    bool      saw_boffo     = false;
+    bool      foo_was_dir   = false;
+    bool      boffo_was_dir = false;
+    int       count         = 0;
+
+    // Clean up after previous failed runs.
+    FSFolder_Delete(folder, foo_boffo);
+    FSFolder_Delete(folder, foo);
+    FSFolder_Delete(folder, boffo);
+    rmdir("_fsdir_test");
+
+    FSFolder_Initialize(folder);
+    FSFolder_MkDir(folder, foo);
+    OutStream *outstream = FSFolder_Open_Out(folder, boffo);
+    DECREF(outstream);
+    outstream = FSFolder_Open_Out(folder, foo_boffo);
+    DECREF(outstream);
+
+    FSDirHandle *dh = FSDH_open(test_dir);
+    while (FSDH_Next(dh)) {
+        count++;
+        String *entry = FSDH_Get_Entry(dh);
+        if (Str_Equals(entry, (Obj*)foo)) {
+            saw_foo = true;
+            foo_was_dir = FSDH_Entry_Is_Dir(dh);
+        }
+        else if (Str_Equals(entry, (Obj*)boffo)) {
+            saw_boffo = true;
+            boffo_was_dir = FSDH_Entry_Is_Dir(dh);
+        }
+        DECREF(entry);
+    }
+    TEST_INT_EQ(runner, 2, count, "correct number of entries");
+    TEST_TRUE(runner, saw_foo, "Directory was iterated over");
+    TEST_TRUE(runner, foo_was_dir,
+              "Dir correctly identified by Entry_Is_Dir");
+    TEST_TRUE(runner, saw_boffo, "File was iterated over");
+    TEST_FALSE(runner, boffo_was_dir,
+               "File correctly identified by Entry_Is_Dir");
+
+    DECREF(dh);
+    FSFolder_Delete(folder, foo_boffo);
+    FSFolder_Delete(folder, foo);
+    FSFolder_Delete(folder, boffo);
+    DECREF(folder);
+    rmdir("_fsdir_test");
+}
+
+void
+TestFSDH_Run_IMP(TestFSDirHandle *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 5);
+    test_all(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFSDirHandle.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFSDirHandle.cfh b/test/Lucy/Test/Store/TestFSDirHandle.cfh
new file mode 100644
index 0000000..9b10d79
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFSDirHandle.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Store::TestFSDirHandle nickname TestFSDH
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestFSDirHandle*
+    new();
+
+    void
+    Run(TestFSDirHandle *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFSFileHandle.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFSFileHandle.c b/test/Lucy/Test/Store/TestFSFileHandle.c
new file mode 100644
index 0000000..7f9f2f1
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFSFileHandle.c
@@ -0,0 +1,267 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h> // for remove()
+#include <stdlib.h>
+
+#define C_LUCY_FSFILEHANDLE
+#define C_LUCY_FILEWINDOW
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#ifdef CHY_HAS_UNISTD_H
+  #include <unistd.h> // close
+#elif defined(CHY_HAS_IO_H)
+  #include <io.h> // close
+#endif
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestFSFileHandle.h"
+#include "Lucy/Store/FSFileHandle.h"
+#include "Lucy/Store/FileWindow.h"
+
+static void
+S_remove(String *path) {
+    char *str = Str_To_Utf8(path);
+    remove(str);
+    free(str);
+}
+
+TestFSFileHandle*
+TestFSFH_new() {
+    return (TestFSFileHandle*)Class_Make_Obj(TESTFSFILEHANDLE);
+}
+
+static void
+test_open(TestBatchRunner *runner) {
+
+    FSFileHandle *fh;
+    String *test_filename = SSTR_WRAP_C("_fstest");
+
+    S_remove(test_filename);
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_READ_ONLY);
+    TEST_TRUE(runner, fh == NULL,
+              "open() with FH_READ_ONLY on non-existent file returns NULL");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "open() with FH_READ_ONLY on non-existent file sets error");
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_WRITE_ONLY);
+    TEST_TRUE(runner, fh == NULL,
+              "open() without FH_CREATE returns NULL");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "open() without FH_CREATE sets error");
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_CREATE);
+    TEST_TRUE(runner, fh == NULL,
+              "open() without FH_WRITE_ONLY returns NULL");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "open() without FH_WRITE_ONLY sets error");
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+    TEST_TRUE(runner, fh && FSFH_is_a(fh, FSFILEHANDLE), "open() succeeds");
+    TEST_TRUE(runner, Err_get_error() == NULL, "open() no errors");
+    FSFH_Write(fh, "foo", 3);
+    if (!FSFH_Close(fh)) { RETHROW(INCREF(Err_get_error())); }
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+    TEST_TRUE(runner, fh == NULL, "FH_EXCLUSIVE blocks open()");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "FH_EXCLUSIVE blocks open(), sets error");
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_CREATE | FH_WRITE_ONLY);
+    TEST_TRUE(runner, fh && FSFH_is_a(fh, FSFILEHANDLE),
+              "open() for append");
+    TEST_TRUE(runner, Err_get_error() == NULL,
+              "open() for append -- no errors");
+    FSFH_Write(fh, "bar", 3);
+    if (!FSFH_Close(fh)) { RETHROW(INCREF(Err_get_error())); }
+    DECREF(fh);
+
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_READ_ONLY);
+    TEST_TRUE(runner, fh && FSFH_is_a(fh, FSFILEHANDLE), "open() read only");
+    TEST_TRUE(runner, Err_get_error() == NULL,
+              "open() read only -- no errors");
+    DECREF(fh);
+
+    S_remove(test_filename);
+}
+
+static void
+test_Read_Write(TestBatchRunner *runner) {
+    FSFileHandle *fh;
+    const char *foo = "foo";
+    const char *bar = "bar";
+    char buffer[12];
+    char *buf = buffer;
+    String *test_filename = SSTR_WRAP_C("_fstest");
+
+    S_remove(test_filename);
+    fh = FSFH_open(test_filename,
+                   FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+
+    TEST_TRUE(runner, FSFH_Length(fh) == INT64_C(0), "Length initially 0");
+    TEST_TRUE(runner, FSFH_Write(fh, foo, 3), "Write returns success");
+    TEST_TRUE(runner, FSFH_Length(fh) == INT64_C(3), "Length after Write");
+    TEST_TRUE(runner, FSFH_Write(fh, bar, 3), "Write returns success");
+    TEST_TRUE(runner, FSFH_Length(fh) == INT64_C(6), "Length after 2 Writes");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, FSFH_Read(fh, buf, 0, 2),
+               "Reading from a write-only handle returns false");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Reading from a write-only handle sets error");
+    if (!FSFH_Close(fh)) { RETHROW(INCREF(Err_get_error())); }
+    DECREF(fh);
+
+    // Reopen for reading.
+    Err_set_error(NULL);
+    fh = FSFH_open(test_filename, FH_READ_ONLY);
+
+    TEST_TRUE(runner, FSFH_Length(fh) == INT64_C(6), "Length on Read");
+    TEST_TRUE(runner, FSFH_Read(fh, buf, 0, 6), "Read returns success");
+    TEST_TRUE(runner, strncmp(buf, "foobar", 6) == 0, "Read/Write");
+    TEST_TRUE(runner, FSFH_Read(fh, buf, 2, 3), "Read returns success");
+    TEST_TRUE(runner, strncmp(buf, "oba", 3) == 0, "Read with offset");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, FSFH_Read(fh, buf, -1, 4),
+               "Read() with a negative offset returns false");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Read() with a negative offset sets error");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, FSFH_Read(fh, buf, 6, 1),
+               "Read() past EOF returns false");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Read() past EOF sets error");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, FSFH_Write(fh, foo, 3),
+               "Writing to a read-only handle returns false");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Writing to a read-only handle sets error");
+
+    DECREF(fh);
+    S_remove(test_filename);
+}
+
+static void
+test_Close(TestBatchRunner *runner) {
+    String *test_filename = SSTR_WRAP_C("_fstest");
+    FSFileHandle *fh;
+
+    S_remove(test_filename);
+    fh = FSFH_open(test_filename,
+                   FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+    TEST_TRUE(runner, FSFH_Close(fh), "Close returns true for write-only");
+    DECREF(fh);
+
+    // Simulate an OS error when closing the file descriptor.  This
+    // approximates what would happen if, say, we run out of disk space.
+    S_remove(test_filename);
+    fh = FSFH_open(test_filename,
+                   FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+#ifdef _MSC_VER
+    SKIP(runner, 2, "LUCY-155");
+#else
+    int saved_fd = FSFH_IVARS(fh)->fd;
+    FSFH_IVARS(fh)->fd = -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;
+#endif /* _MSC_VER */
+    DECREF(fh);
+
+    fh = FSFH_open(test_filename, FH_READ_ONLY);
+    TEST_TRUE(runner, FSFH_Close(fh), "Close returns true for read-only");
+
+    DECREF(fh);
+    S_remove(test_filename);
+}
+
+static void
+test_Window(TestBatchRunner *runner) {
+    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);
+    fh = FSFH_open(test_filename,
+                   FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
+    for (i = 0; i < 1024; i++) {
+        FSFH_Write(fh, "foo ", 4);
+    }
+    if (!FSFH_Close(fh)) { RETHROW(INCREF(Err_get_error())); }
+
+    // Reopen for reading.
+    DECREF(fh);
+    fh = FSFH_open(test_filename, FH_READ_ONLY);
+    if (!fh) { RETHROW(INCREF(Err_get_error())); }
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, FSFH_Window(fh, window, -1, 4),
+               "Window() with a negative offset returns false");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Window() with a negative offset sets error");
+
+    Err_set_error(NULL);
+    TEST_FALSE(runner, FSFH_Window(fh, window, 4000, 1000),
+               "Window() past EOF returns false");
+    TEST_TRUE(runner, Err_get_error() != NULL,
+              "Window() past EOF sets error");
+
+    TEST_TRUE(runner, FSFH_Window(fh, window, 1021, 2),
+              "Window() returns true");
+    TEST_TRUE(runner,
+              strncmp(window_ivars->buf - window_ivars->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");
+
+    DECREF(window);
+    DECREF(fh);
+    S_remove(test_filename);
+}
+
+void
+TestFSFH_Run_IMP(TestFSFileHandle *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 46);
+    test_open(runner);
+    test_Read_Write(runner);
+    test_Close(runner);
+    test_Window(runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFSFileHandle.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFSFileHandle.cfh b/test/Lucy/Test/Store/TestFSFileHandle.cfh
new file mode 100644
index 0000000..b9825b8
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFSFileHandle.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Store::TestFSFileHandle nickname TestFSFH
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestFSFileHandle*
+    new();
+
+    void
+    Run(TestFSFileHandle *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFSFolder.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFSFolder.c b/test/Lucy/Test/Store/TestFSFolder.c
new file mode 100644
index 0000000..03eef57
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFSFolder.c
@@ -0,0 +1,218 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "charmony.h"
+
+// mkdir, rmdir
+#ifdef CHY_HAS_DIRECT_H
+  #include <direct.h>
+#endif
+
+// rmdir
+#ifdef CHY_HAS_UNISTD_H
+  #include <unistd.h>
+#endif
+
+// mkdir, stat
+#ifdef CHY_HAS_SYS_STAT_H
+  #include <sys/stat.h>
+#endif
+
+#ifdef CHY_HAS_ERRNO_H
+  #include "errno.h"
+#endif
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestFSFolder.h"
+#include "Lucy/Test/Store/TestFolderCommon.h"
+#include "Lucy/Store/FSFolder.h"
+#include "Lucy/Store/OutStream.h"
+
+/* The tests involving symlinks have to be run with administrator privileges
+ * under Windows, so disable by default.
+ */
+#ifndef CHY_HAS_WINDOWS_H
+#define ENABLE_SYMLINK_TESTS
+// Create the symlinks needed by test_protect_symlinks().
+static bool
+S_create_test_symlinks(void);
+#endif /* CHY_HAS_WINDOWS_H */
+
+TestFSFolder*
+TestFSFolder_new() {
+    return (TestFSFolder*)Class_Make_Obj(TESTFSFOLDER);
+}
+
+static Folder*
+S_set_up() {
+    rmdir("_fstest");
+    String   *test_dir = SSTR_WRAP_C("_fstest");
+    FSFolder *folder   = FSFolder_new(test_dir);
+    FSFolder_Initialize(folder);
+    if (!FSFolder_Check(folder)) {
+        RETHROW(INCREF(Err_get_error()));
+    }
+    return (Folder*)folder;
+}
+
+static void
+S_tear_down() {
+    struct stat stat_buf;
+    int result = rmdir("_fstest");
+    if (result < 0) {
+        /* FIXME: This can fail on Windows with ENOTEMPTY. */
+        THROW(ERR, "Can't clean up directory _fstest: %s", strerror(errno));
+    }
+    /* FIXME: This can also fail on Windows even if rmdir was successful. */
+    if (stat("_fstest", &stat_buf) != -1) {
+        THROW(ERR, "Can't clean up directory _fstest");
+    }
+}
+
+static void
+test_Initialize_and_Check(TestBatchRunner *runner) {
+    rmdir("_fstest");
+    String   *test_dir = SSTR_WRAP_C("_fstest");
+    FSFolder *folder   = FSFolder_new(test_dir);
+    TEST_FALSE(runner, FSFolder_Check(folder),
+               "Check() returns false when folder dir doesn't exist");
+    FSFolder_Initialize(folder);
+    PASS(runner, "Initialize() concludes without incident");
+    TEST_TRUE(runner, FSFolder_Check(folder),
+              "Initialize() created dir, and now Check() succeeds");
+    DECREF(folder);
+    S_tear_down();
+}
+
+static void
+test_protect_symlinks(TestBatchRunner *runner) {
+#ifdef ENABLE_SYMLINK_TESTS
+    FSFolder *folder    = (FSFolder*)S_set_up();
+    String   *foo       = SSTR_WRAP_C("foo");
+    String   *bar       = SSTR_WRAP_C("bar");
+    String   *foo_boffo = SSTR_WRAP_C("foo/boffo");
+
+    FSFolder_MkDir(folder, foo);
+    FSFolder_MkDir(folder, bar);
+    OutStream *outstream = FSFolder_Open_Out(folder, foo_boffo);
+    DECREF(outstream);
+
+    if (!S_create_test_symlinks()) {
+        FAIL(runner, "symlink creation failed");
+        FAIL(runner, "symlink creation failed");
+        FAIL(runner, "symlink creation failed");
+        FAIL(runner, "symlink creation failed");
+        FAIL(runner, "symlink creation failed");
+        // Try to clean up anyway.
+        FSFolder_Delete_Tree(folder, foo);
+        FSFolder_Delete_Tree(folder, bar);
+    }
+    else {
+        Vector *list = FSFolder_List_R(folder, NULL);
+        bool saw_bazooka_boffo = false;
+        for (size_t i = 0, max = Vec_Get_Size(list); i < max; i++) {
+            String *entry = (String*)Vec_Fetch(list, i);
+            if (Str_Ends_With_Utf8(entry, "bazooka/boffo", 13)) {
+                saw_bazooka_boffo = true;
+            }
+        }
+        TEST_FALSE(runner, saw_bazooka_boffo,
+                   "List_R() shouldn't follow symlinks");
+        DECREF(list);
+
+        TEST_TRUE(runner, FSFolder_Delete_Tree(folder, bar),
+                  "Delete_Tree() returns true");
+        TEST_FALSE(runner, FSFolder_Exists(folder, bar),
+                   "Tree is really gone");
+        TEST_TRUE(runner, FSFolder_Exists(folder, foo),
+                  "Original folder sill there");
+        TEST_TRUE(runner, FSFolder_Exists(folder, foo_boffo),
+                  "Delete_Tree() did not follow directory symlink");
+        FSFolder_Delete_Tree(folder, foo);
+    }
+    DECREF(folder);
+    S_tear_down();
+#else
+    SKIP(runner, 5, "Tests requiring symlink() disabled");
+#endif // ENABLE_SYMLINK_TESTS
+}
+
+void
+test_disallow_updir(TestBatchRunner *runner) {
+    FSFolder *outer_folder = (FSFolder*)S_set_up();
+
+    String *foo = SSTR_WRAP_C("foo");
+    String *bar = SSTR_WRAP_C("bar");
+    FSFolder_MkDir(outer_folder, foo);
+    FSFolder_MkDir(outer_folder, bar);
+
+    String *inner_path = SSTR_WRAP_C("_fstest/foo");
+    FSFolder *foo_folder = FSFolder_new(inner_path);
+    String *up_bar = SSTR_WRAP_C("../bar");
+    TEST_FALSE(runner, FSFolder_Exists(foo_folder, up_bar),
+               "up-dirs are inaccessible.");
+
+    DECREF(foo_folder);
+    FSFolder_Delete(outer_folder, foo);
+    FSFolder_Delete(outer_folder, bar);
+    DECREF(outer_folder);
+    S_tear_down();
+}
+
+void
+TestFSFolder_Run_IMP(TestFSFolder *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self,
+                          TestFolderCommon_num_tests() + 9);
+    test_Initialize_and_Check(runner);
+    TestFolderCommon_run_tests(runner, S_set_up, S_tear_down);
+    test_protect_symlinks(runner);
+    test_disallow_updir(runner);
+}
+
+#ifdef ENABLE_SYMLINK_TESTS
+
+#ifdef CHY_HAS_WINDOWS_H
+#include "windows.h"
+#elif defined(CHY_HAS_UNISTD_H)
+#include <unistd.h>
+#else
+#error "Don't have either windows.h or unistd.h"
+#endif
+
+static bool
+S_create_test_symlinks(void) {
+#ifdef CHY_HAS_WINDOWS_H
+    if (!CreateSymbolicLink("_fstest\\bar\\banana", "_fstest\\foo\\boffo", 0)
+        || !CreateSymbolicLink("_fstest\\bar\\bazooka", "_fstest\\foo", 1)
+       ) {
+        return false;
+    }
+#else
+    if (symlink("_fstest/foo/boffo", "_fstest/bar/banana")
+        || symlink("_fstest/foo", "_fstest/bar/bazooka")
+       ) {
+        return false;
+    }
+#endif
+    return true;
+}
+
+#endif /* ENABLE_SYMLINK_TESTS */
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFSFolder.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFSFolder.cfh b/test/Lucy/Test/Store/TestFSFolder.cfh
new file mode 100644
index 0000000..bbdae58
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFSFolder.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Store::TestFSFolder
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestFSFolder*
+    new();
+
+    void
+    Run(TestFSFolder *self, TestBatchRunner *runner);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFileHandle.c
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFileHandle.c b/test/Lucy/Test/Store/TestFileHandle.c
new file mode 100644
index 0000000..2e9850f
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFileHandle.c
@@ -0,0 +1,66 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define C_TESTLUCY_TESTINSTREAM
+#define C_LUCY_INSTREAM
+#define C_LUCY_FILEWINDOW
+#define TESTLUCY_USE_SHORT_NAMES
+#include "Lucy/Util/ToolSet.h"
+
+#include "Clownfish/TestHarness/TestBatchRunner.h"
+#include "Lucy/Test.h"
+#include "Lucy/Test/Store/TestFileHandle.h"
+#include "Lucy/Store/FileHandle.h"
+#include "Lucy/Store/FileWindow.h"
+
+TestFileHandle*
+TestFH_new() {
+    return (TestFileHandle*)Class_Make_Obj(TESTFILEHANDLE);
+}
+
+static void
+S_no_op_method(const void *vself) {
+    UNUSED_VAR(vself);
+}
+
+static FileHandle*
+S_new_filehandle() {
+    String *class_name = SSTR_WRAP_C("TestFileHandle");
+    FileHandle *fh;
+    Class *klass = Class_fetch_class(class_name);
+    if (!klass) {
+        klass = Class_singleton(class_name, FILEHANDLE);
+    }
+    Class_Override(klass, S_no_op_method, LUCY_FH_Close_OFFSET);
+    fh = (FileHandle*)Class_Make_Obj(klass);
+    return FH_do_open(fh, NULL, 0);
+}
+
+void
+TestFH_Run_IMP(TestFileHandle *self, TestBatchRunner *runner) {
+    TestBatchRunner_Plan(runner, (TestBatch*)self, 2);
+
+    FileHandle *fh  = S_new_filehandle();
+    String     *foo = SSTR_WRAP_C("foo");
+
+    TEST_TRUE(runner, Str_Equals_Utf8(FH_Get_Path(fh), "", 0), "Get_Path");
+    FH_Set_Path(fh, foo);
+    TEST_TRUE(runner, Str_Equals(FH_Get_Path(fh), (Obj*)foo), "Set_Path");
+
+    DECREF(fh);
+}
+
+

http://git-wip-us.apache.org/repos/asf/lucy/blob/572d3564/test/Lucy/Test/Store/TestFileHandle.cfh
----------------------------------------------------------------------
diff --git a/test/Lucy/Test/Store/TestFileHandle.cfh b/test/Lucy/Test/Store/TestFileHandle.cfh
new file mode 100644
index 0000000..1c97c5f
--- /dev/null
+++ b/test/Lucy/Test/Store/TestFileHandle.cfh
@@ -0,0 +1,29 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+parcel TestLucy;
+
+class Lucy::Test::Store::TestFileHandle nickname TestFH
+    inherits Clownfish::TestHarness::TestBatch {
+
+    inert incremented TestFileHandle*
+    new();
+
+    void
+    Run(TestFileHandle *self, TestBatchRunner *runner);
+}
+
+