The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/* 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 "Lucy/Test.h"
#include "Lucy/Test/Store/TestFolderCommon.h"
#include "Clownfish/TestHarness/TestBatchRunner.h"
#include "Lucy/Store/Folder.h"
#include "Lucy/Store/DirHandle.h"
#include "Lucy/Store/FileHandle.h"
#include "Lucy/Store/InStream.h"
#include "Lucy/Store/OutStream.h"

#define set_up_t    LUCY_TestFolderCommon_Set_Up_t
#define tear_down_t LUCY_TestFolderCommon_Tear_Down_t

static String *foo           = NULL;
static String *bar           = NULL;
static String *baz           = NULL;
static String *boffo         = NULL;
static String *banana        = NULL;
static String *foo_bar       = NULL;
static String *foo_bar_baz   = NULL;
static String *foo_bar_boffo = NULL;
static String *foo_boffo     = NULL;
static String *foo_foo       = NULL;
static String *nope          = NULL;
static String *nope_nyet     = NULL;

static void
S_init_strings(void) {
    foo           = Str_newf("foo");
    bar           = Str_newf("bar");
    baz           = Str_newf("baz");
    boffo         = Str_newf("boffo");
    banana        = Str_newf("banana");
    foo_bar       = Str_newf("foo/bar");
    foo_bar_baz   = Str_newf("foo/bar/baz");
    foo_bar_boffo = Str_newf("foo/bar/boffo");
    foo_boffo     = Str_newf("foo/boffo");
    foo_foo       = Str_newf("foo/foo");
    nope          = Str_newf("nope");
    nope_nyet     = Str_newf("nope/nyet");
}

static void
S_destroy_strings(void) {
    DECREF(foo);
    DECREF(bar);
    DECREF(baz);
    DECREF(boffo);
    DECREF(banana);
    DECREF(foo_bar);
    DECREF(foo_bar_baz);
    DECREF(foo_bar_boffo);
    DECREF(foo_boffo);
    DECREF(foo_foo);
    DECREF(nope);
    DECREF(nope_nyet);
}

static void
test_Local_Exists(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    OutStream *outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);
    Folder_Local_MkDir(folder, foo);
    outstream = Folder_Open_Out(folder, foo_boffo);
    DECREF(outstream);

    TEST_TRUE(runner, Folder_Local_Exists(folder, boffo),
              "Local_Exists() returns true for file");
    TEST_TRUE(runner, Folder_Local_Exists(folder, foo),
              "Local_Exists() returns true for dir");
    TEST_FALSE(runner, Folder_Local_Exists(folder, foo_boffo),
               "Local_Exists() returns false for nested entry");
    TEST_FALSE(runner, Folder_Local_Exists(folder, bar),
               "Local_Exists() returns false for non-existent entry");

    Folder_Delete(folder, foo_boffo);
    Folder_Delete(folder, foo);
    Folder_Delete(folder, boffo);
    DECREF(folder);
    tear_down();
}

static void
test_Local_Is_Directory(TestBatchRunner *runner, set_up_t set_up,
                        tear_down_t tear_down) {
    Folder *folder = set_up();
    OutStream *outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);
    Folder_Local_MkDir(folder, foo);

    TEST_FALSE(runner, Folder_Local_Is_Directory(folder, boffo),
               "Local_Is_Directory() returns false for file");
    TEST_TRUE(runner, Folder_Local_Is_Directory(folder, foo),
              "Local_Is_Directory() returns true for dir");
    TEST_FALSE(runner, Folder_Local_Is_Directory(folder, bar),
               "Local_Is_Directory() returns false for non-existent entry");

    Folder_Delete(folder, boffo);
    Folder_Delete(folder, foo);
    DECREF(folder);
    tear_down();
}

static void
test_Local_Find_Folder(TestBatchRunner *runner, set_up_t set_up,
                       tear_down_t tear_down) {
    Folder    *folder = set_up();
    Folder    *local;
    OutStream *outstream;

    Folder_MkDir(folder, foo);
    Folder_MkDir(folder, foo_bar);
    outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);
    outstream = Folder_Open_Out(folder, foo_boffo);
    DECREF(outstream);

    local = Folder_Local_Find_Folder(folder, nope);
    TEST_TRUE(runner, local == NULL, "Non-existent entry yields NULL");

    String *empty = SSTR_BLANK();
    local = Folder_Local_Find_Folder(folder, empty);
    TEST_TRUE(runner, local == NULL, "Empty string yields NULL");

    local = Folder_Local_Find_Folder(folder, foo_bar);
    TEST_TRUE(runner, local == NULL, "nested folder yields NULL");

    local = Folder_Local_Find_Folder(folder, foo_boffo);
    TEST_TRUE(runner, local == NULL, "nested file yields NULL");

    local = Folder_Local_Find_Folder(folder, boffo);
    TEST_TRUE(runner, local == NULL, "local file yields NULL");

    local = Folder_Local_Find_Folder(folder, bar);
    TEST_TRUE(runner, local == NULL, "name of nested folder yields NULL");

    local = Folder_Local_Find_Folder(folder, foo);
    TEST_TRUE(runner,
              local
              && Folder_is_a(local, FOLDER)
              && Str_Ends_With(Folder_Get_Path(local), foo),
              "Find local directory");

    Folder_Delete(folder, foo_bar);
    Folder_Delete(folder, foo_boffo);
    Folder_Delete(folder, foo);
    Folder_Delete(folder, boffo);
    DECREF(folder);
    tear_down();
}

static void
test_Local_MkDir(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    bool result;

    result = Folder_Local_MkDir(folder, foo);
    TEST_TRUE(runner, result, "Local_MkDir succeeds and returns true");

    Err_set_error(NULL);
    result = Folder_Local_MkDir(folder, foo);
    TEST_FALSE(runner, result,
               "Local_MkDir returns false when a dir already exists");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Local_MkDir sets global error when a dir already exists");
    TEST_TRUE(runner, Folder_Exists(folder, foo),
              "Existing dir untouched after failed Local_MkDir");

    OutStream *outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);
    Err_set_error(NULL);
    result = Folder_Local_MkDir(folder, foo);
    TEST_FALSE(runner, result,
               "Local_MkDir returns false when a file already exists");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Local_MkDir sets global error when a file already exists");
    TEST_TRUE(runner, Folder_Exists(folder, boffo) &&
              !Folder_Local_Is_Directory(folder, boffo),
              "Existing file untouched after failed Local_MkDir");

    Folder_Delete(folder, foo);
    Folder_Delete(folder, boffo);
    DECREF(folder);
    tear_down();
}

static void
test_Local_Open_Dir(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    DirHandle *dh = Folder_Local_Open_Dir(folder);
    TEST_TRUE(runner, dh && DH_is_a(dh, DIRHANDLE),
              "Local_Open_Dir returns an DirHandle");
    DECREF(dh);
    DECREF(folder);
    tear_down();
}

static void
test_Local_Open_FileHandle(TestBatchRunner *runner, set_up_t set_up,
                           tear_down_t tear_down) {
    Folder *folder = set_up();
    FileHandle *fh;

    fh = Folder_Local_Open_FileHandle(folder, boffo,
                                      FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
    TEST_TRUE(runner, fh && FH_is_a(fh, FILEHANDLE),
              "opened FileHandle");
    DECREF(fh);

    fh = Folder_Local_Open_FileHandle(folder, boffo,
                                      FH_CREATE | FH_WRITE_ONLY);
    TEST_TRUE(runner, fh && FH_is_a(fh, FILEHANDLE),
              "opened FileHandle for append");
    DECREF(fh);

    Err_set_error(NULL);
    fh = Folder_Local_Open_FileHandle(folder, boffo,
                                      FH_CREATE | FH_WRITE_ONLY | FH_EXCLUSIVE);
    TEST_TRUE(runner, fh == NULL, "FH_EXLUSIVE flag prevents clobber");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "failure due to FH_EXLUSIVE flag sets global error");

    fh = Folder_Local_Open_FileHandle(folder, boffo, FH_READ_ONLY);
    TEST_TRUE(runner, fh && FH_is_a(fh, FILEHANDLE),
              "opened FileHandle for reading");
    DECREF(fh);

    Err_set_error(NULL);
    fh = Folder_Local_Open_FileHandle(folder, nope, FH_READ_ONLY);
    TEST_TRUE(runner, fh == NULL,
              "Can't open non-existent file for reading");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Opening non-existent file for reading sets global error");

    Folder_Delete(folder, boffo);
    DECREF(folder);
    tear_down();
}

static void
test_Local_Delete(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    OutStream *outstream;

    outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);
    TEST_TRUE(runner, Folder_Local_Delete(folder, boffo),
              "Local_Delete on file succeeds");
    TEST_FALSE(runner, Folder_Exists(folder, boffo),
               "File is really gone");

    Folder_Local_MkDir(folder, foo);
    outstream = Folder_Open_Out(folder, foo_boffo);
    DECREF(outstream);

    Err_set_error(NULL);
    TEST_FALSE(runner, Folder_Local_Delete(folder, foo),
               "Local_Delete on non-empty dir fails");

    Folder_Delete(folder, foo_boffo);
    TEST_TRUE(runner, Folder_Local_Delete(folder, foo),
              "Local_Delete on empty dir succeeds");
    // FIXME: This test sometimes fails on Windows.
    TEST_FALSE(runner, Folder_Exists(folder, foo),
               "Dir is really gone");

    DECREF(folder);
    tear_down();
}

static void
test_Rename(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    OutStream *outstream;
    bool result;

    Folder_MkDir(folder, foo);
    Folder_MkDir(folder, foo_bar);
    outstream = Folder_Open_Out(folder, boffo);
    OutStream_Close(outstream);
    DECREF(outstream);

    // Move files.

    result = Folder_Rename(folder, boffo, banana);
    TEST_TRUE(runner, result, "Rename succeeds and returns true");
    TEST_TRUE(runner, Folder_Exists(folder, banana),
              "File exists at new path");
    TEST_FALSE(runner, Folder_Exists(folder, boffo),
               "File no longer exists at old path");

    result = Folder_Rename(folder, banana, foo_bar_boffo);
    TEST_TRUE(runner, result, "Rename to file in nested dir");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar_boffo),
              "File exists at new path");
    TEST_FALSE(runner, Folder_Exists(folder, banana),
               "File no longer exists at old path");

    result = Folder_Rename(folder, foo_bar_boffo, boffo);
    TEST_TRUE(runner, result, "Rename from file in nested dir");
    TEST_TRUE(runner, Folder_Exists(folder, boffo),
              "File exists at new path");
    TEST_FALSE(runner, Folder_Exists(folder, foo_bar_boffo),
               "File no longer exists at old path");

    outstream = Folder_Open_Out(folder, foo_boffo);
    OutStream_Close(outstream);
    DECREF(outstream);
    result = Folder_Rename(folder, boffo, foo_boffo);
    if (result) {
        PASS(runner, "Rename clobbers on this system");
        TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
                  "File exists at new path");
        TEST_FALSE(runner, Folder_Exists(folder, boffo),
                   "File no longer exists at old path");
    }
    else {
        PASS(runner, "Rename does not clobber on this system");
        TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
                  "File exists at new path");
        TEST_TRUE(runner, Folder_Exists(folder, boffo),
                  "File still exists at old path");
        Folder_Delete(folder, boffo);
    }

    // Move Dirs.

    Folder_MkDir(folder, baz);
    result = Folder_Rename(folder, baz, boffo);
    TEST_TRUE(runner, result, "Rename dir");
    TEST_TRUE(runner, Folder_Exists(folder, boffo),
              "Folder exists at new path");
    TEST_FALSE(runner, Folder_Exists(folder, baz),
               "Folder no longer exists at old path");

    result = Folder_Rename(folder, boffo, foo_foo);
    TEST_TRUE(runner, result, "Rename dir into nested subdir");
    TEST_TRUE(runner, Folder_Exists(folder, foo_foo),
              "Folder exists at new path");
    TEST_FALSE(runner, Folder_Exists(folder, boffo),
               "Folder no longer exists at old path");

    result = Folder_Rename(folder, foo_foo, foo_bar_baz);
    TEST_TRUE(runner, result, "Rename dir from nested subdir");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar_baz),
              "Folder exists at new path");
    TEST_FALSE(runner, Folder_Exists(folder, foo_foo),
               "Folder no longer exists at old path");

    // Test failed clobbers.

    Err_set_error(NULL);
    result = Folder_Rename(folder, foo_boffo, foo_bar);
    TEST_FALSE(runner, result, "Rename file clobbering dir fails");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Failed rename sets global error");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File still exists at old path");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar),
              "Dir still exists after failed clobber");

    Err_set_error(NULL);
    result = Folder_Rename(folder, foo_bar, foo_boffo);
    TEST_FALSE(runner, result, "Rename dir clobbering file fails");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Failed rename sets global error");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar),
              "Dir still exists at old path");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File still exists after failed clobber");

    // Test that "renaming" succeeds where to and from are the same.

    result = Folder_Rename(folder, foo_boffo, foo_boffo);
    TEST_TRUE(runner, result, "Renaming file to itself succeeds");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File still exists");

    result = Folder_Rename(folder, foo_bar, foo_bar);
    // FIXME: This test sometimes fails on Windows with "Permission denied".
    TEST_TRUE(runner, result, "Renaming dir to itself succeeds");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar),
              "Dir still exists");

    // Invalid filepaths.

    Err_set_error(NULL);
    result = Folder_Rename(folder, foo_boffo, nope_nyet);
    TEST_FALSE(runner, result, "Rename into non-existent subdir fails");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Renaming into non-existent subdir sets global error");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "Entry still exists at old path");

    Err_set_error(NULL);
    result = Folder_Rename(folder, nope_nyet, boffo);
    TEST_FALSE(runner, result, "Rename non-existent file fails");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Renaming non-existent source file sets global error");

    Folder_Delete(folder, foo_bar_baz);
    Folder_Delete(folder, foo_bar);
    Folder_Delete(folder, foo_boffo);
    Folder_Delete(folder, foo);
    DECREF(folder);
    tear_down();
}

static void
test_Hard_Link(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    OutStream *outstream;
    bool result;

    Folder_MkDir(folder, foo);
    Folder_MkDir(folder, foo_bar);
    outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);

    // Link files.

    result = Folder_Hard_Link(folder, boffo, banana);
    TEST_TRUE(runner, result, "Hard_Link succeeds and returns true");
    TEST_TRUE(runner, Folder_Exists(folder, banana),
              "File exists at new path");
    TEST_TRUE(runner, Folder_Exists(folder, boffo),
              "File still exists at old path");
    Folder_Delete(folder, boffo);

    result = Folder_Hard_Link(folder, banana, foo_bar_boffo);
    TEST_TRUE(runner, result, "Hard_Link to target within nested dir");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar_boffo),
              "File exists at new path");
    TEST_TRUE(runner, Folder_Exists(folder, banana),
              "File still exists at old path");
    Folder_Delete(folder, banana);

    result = Folder_Hard_Link(folder, foo_bar_boffo, foo_boffo);
    TEST_TRUE(runner, result, "Hard_Link from file in nested dir");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File exists at new path");
    TEST_TRUE(runner, Folder_Exists(folder, foo_bar_boffo),
              "File still exists at old path");
    Folder_Delete(folder, foo_bar_boffo);

    // Invalid clobbers.

    outstream = Folder_Open_Out(folder, boffo);
    DECREF(outstream);
    result = Folder_Hard_Link(folder, foo_boffo, boffo);
    TEST_FALSE(runner, result, "Clobber of file fails");
    TEST_TRUE(runner, Folder_Exists(folder, boffo),
              "File still exists at new path");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File still exists at old path");
    Folder_Delete(folder, boffo);

    Folder_MkDir(folder, baz);
    result = Folder_Hard_Link(folder, foo_boffo, baz);
    TEST_FALSE(runner, result, "Clobber of dir fails");
    TEST_TRUE(runner, Folder_Exists(folder, baz),
              "Dir still exists at new path");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File still exists at old path");
    Folder_Delete(folder, baz);

    // Invalid Hard_Link of dir.

    Folder_MkDir(folder, baz);
    result = Folder_Hard_Link(folder, baz, banana);
    TEST_FALSE(runner, result, "Hard_Link dir fails");
    TEST_FALSE(runner, Folder_Exists(folder, banana),
               "Nothing at new path");
    // FIXME: This test sometimes fails on Windows.
    TEST_TRUE(runner, Folder_Exists(folder, baz),
              "Folder still exists at old path");
    Folder_Delete(folder, baz);

    // Test that linking to yourself fails.

    result = Folder_Hard_Link(folder, foo_boffo, foo_boffo);
    TEST_FALSE(runner, result, "Hard_Link file to itself fails");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "File still exists");

    // Invalid filepaths.

    Err_set_error(NULL);
    result = Folder_Rename(folder, foo_boffo, nope_nyet);
    TEST_FALSE(runner, result, "Hard_Link into non-existent subdir fails");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Hard_Link into non-existent subdir sets global error");
    TEST_TRUE(runner, Folder_Exists(folder, foo_boffo),
              "Entry still exists at old path");

    Err_set_error(NULL);
    result = Folder_Rename(folder, nope_nyet, boffo);
    TEST_FALSE(runner, result, "Hard_Link non-existent source file fails");
    TEST_TRUE(runner, Err_get_error() != NULL,
              "Hard_Link non-existent source file sets global error");

    Folder_Delete(folder, foo_bar);
    Folder_Delete(folder, foo_boffo);
    Folder_Delete(folder, foo);
    DECREF(folder);
    tear_down();
}

static void
test_Close(TestBatchRunner *runner, set_up_t set_up, tear_down_t tear_down) {
    Folder *folder = set_up();
    Folder_Close(folder);
    PASS(runner, "Close() concludes without incident");
    Folder_Close(folder);
    Folder_Close(folder);
    PASS(runner, "Calling Close() multiple times is safe");
    DECREF(folder);
    tear_down();
}

uint32_t
TestFolderCommon_num_tests() {
    return 99;
}

void
TestFolderCommon_run_tests(TestBatchRunner *runner, set_up_t set_up,
                           tear_down_t tear_down) {
    S_init_strings();
    test_Local_Exists(runner, set_up, tear_down);
    test_Local_Is_Directory(runner, set_up, tear_down);
    test_Local_Find_Folder(runner, set_up, tear_down);
    test_Local_MkDir(runner, set_up, tear_down);
    test_Local_Open_Dir(runner, set_up, tear_down);
    test_Local_Open_FileHandle(runner, set_up, tear_down);
    test_Local_Delete(runner, set_up, tear_down);
    test_Rename(runner, set_up, tear_down);
    test_Hard_Link(runner, set_up, tear_down);
    test_Close(runner, set_up, tear_down);
    S_destroy_strings();
}