DVX_GUI/src/test/fileops/fileOpsTest.c
2026-10-13 13:49:12 -05:00

1256 lines
43 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// fileOpsTest.c -- host tests of the file operations library
//
// Everything runs against real files in a fresh temporary directory:
// listing, sorting, formatting, paths, attributes, and copy/move/delete
// jobs with conflicts, read-only files, errors, Retry/Skip, Cancel and the
// BASIC entry points.
#include "dvxtest.h"
#include "dvxPlat.h"
#include "fileops/fileOps.h"
#include "stb_ds_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <utime.h>
#define PATH_MAX_T DVX_MAX_PATH
#define BIG_FILE_BYTES (FILEOPS_COPY_CHUNK * 5 + 123)
#define MID_FILE_BYTES 20000
#define ANSWERS_MAX 16
#define STEP_LIMIT 100000
#define OLD_TIME 946684800 // 2000-01-01, a stamp copies must keep
#define NEWER_TIME (OLD_TIME + 86400)
#define MODE_NONE 0000
#define MODE_DIR 0755
#define MODE_READONLY 0444
#define MODE_FILE 0644
#define LONG_NAME_LEN (DVX_MAX_PATH - 8)
#define BAS_TRUE (-1)
#define BAS_FALSE 0
#define NO_DRIVE (-1) // FileOpsDriveType of a drive that is not there
#define JOB_SLICE_MS 1000
#define STAMP_MINUTES "2001-02-03 04:05"
#define STAMP_SECONDS "2001-02-03 04:05:06"
// Scripted answers for runScripted
typedef struct {
FileOpsAnswerE answers[ANSWERS_MAX];
int32_t count;
int32_t next;
int32_t asks; // FileOpsStepAskE seen
int32_t errors; // FileOpsStepErrorE seen
char lastMessage[FILEOPS_MSG_MAX];
} ScriptT;
// ============================================================
// Prototypes
// ============================================================
static void caseAttributes(void);
static void caseBasic(void);
static void caseBasicPlatform(void);
static void caseCancel(void);
static void caseClearAttr(void);
static void caseConflicts(void);
static void caseCopyErrors(void);
static void caseCopyIntoItself(void);
static void caseCopyTree(void);
static void caseDelete(void);
static void caseFormat(void);
static void caseListSort(void);
static void caseMakeRename(void);
static void caseMove(void);
static void casePaths(void);
static void caseRunCallbacks(void);
static void caseSymlinks(void);
static void caseTreeSize(void);
static bool exists(const char *path);
static int64_t fileSize(const char *path);
static void makeDir(const char *path);
static void makeFile(const char *path, int32_t bytes, char fill);
static char readFirst(const char *path);
static bool runCancelProgress(FileOpsJobT *job, void *ctx);
static FileOpsStepE runScripted(FileOpsJobT *job, ScriptT *s);
static const char *tp(const char *rel);
static void treeMake(const char *base);
static void caseAttributes(void) {
tCase("attributes and dates");
char f[PATH_MAX_T];
char h[PATH_MAX_T];
uint8_t attr;
struct stat st;
snprintf(f, sizeof(f), "%s", tp("attr.txt"));
snprintf(h, sizeof(h), "%s", tp(".hidden"));
makeFile(f, 10, 'a');
makeFile(h, 1, 'h');
TEST_TRUE(fileOpsGetAttr(f, &attr));
TEST_EQ(attr & FILEOPS_ATTR_READONLY, 0);
TEST_TRUE(fileOpsSetAttr(f, FILEOPS_ATTR_READONLY | FILEOPS_ATTR_ARCHIVE));
TEST_TRUE(fileOpsGetAttr(f, &attr));
TEST_EQ(attr & FILEOPS_ATTR_READONLY, FILEOPS_ATTR_READONLY);
TEST_TRUE(fileOpsSetAttr(f, 0));
TEST_TRUE(fileOpsGetAttr(f, &attr));
TEST_EQ(attr & FILEOPS_ATTR_READONLY, 0);
TEST_TRUE(fileOpsGetAttr(h, &attr));
TEST_EQ(attr & FILEOPS_ATTR_HIDDEN, FILEOPS_ATTR_HIDDEN);
TEST_TRUE(!fileOpsGetAttr(tp("missing"), &attr));
TEST_TRUE(!fileOpsSetAttr(tp("missing"), 0));
TEST_TRUE(fileOpsSetTime(f, OLD_TIME));
TEST_EQ(stat(f, &st), 0);
TEST_EQ(st.st_mtime, OLD_TIME);
TEST_TRUE(!fileOpsSetTime(tp("missing"), OLD_TIME));
}
static void caseBasic(void) {
tCase("BASIC entry points");
makeDir(tp("bas"));
makeDir(tp("bas/src"));
makeFile(tp("bas/src/one.txt"), 5, '1');
makeDir(tp("bas/dst"));
TEST_EQ(FileOpsCopyTree(tp("bas/src"), tp("bas/dst"), 0), -1);
TEST_TRUE(exists(tp("bas/dst/src/one.txt")));
// Existing file kept without overwrite: still True (nothing failed).
makeFile(tp("bas/src/one.txt"), 7, '2');
TEST_EQ(FileOpsCopyTree(tp("bas/src"), tp("bas/dst"), 0), -1);
TEST_EQ(readFirst(tp("bas/dst/src/one.txt")), '1');
TEST_EQ(FileOpsCopyTree(tp("bas/src"), tp("bas/dst"), 1), -1);
TEST_EQ(readFirst(tp("bas/dst/src/one.txt")), '2');
makeDir(tp("bas/moved"));
TEST_EQ(FileOpsMoveTree(tp("bas/src"), tp("bas/moved"), 0), -1);
TEST_TRUE(!exists(tp("bas/src")));
TEST_TRUE(exists(tp("bas/moved/src/one.txt")));
// A read-only file inside is deleted too.
TEST_EQ(chmod(tp("bas/moved/src/one.txt"), MODE_READONLY), 0);
TEST_EQ(FileOpsDeleteTree(tp("bas/moved")), -1);
TEST_TRUE(!exists(tp("bas/moved")));
TEST_EQ(FileOpsCopyTree(tp("bas/nothing"), tp("bas/dst"), 0), 0);
TEST_EQ(FileOpsDeleteTree(tp("bas/nothing")), 0);
TEST_STR_EQ(FileOpsDriveList(), "/");
TEST_TRUE(FileOpsFreeKb(tTempDir()) >= 0);
// A folder not made yet reports the disk it would be made on.
TEST_TRUE(FileOpsFreeKb(tp("bas/nothing/at/all")) >= 0);
}
// The fileops.bas entry points added on the platform layer (drives,
// disks, paths, filters, the temp folder), a job's byte progress and its
// attribute clearing, and setting a file's time from FileOpsModified$'s
// text.
static void caseBasicPlatform(void) {
tCase("BASIC entry points: drives, paths, filters, job bytes, times");
makeDir(tp("bp"));
makeDir(tp("bp/src"));
makeDir(tp("bp/dst"));
makeFile(tp("bp/src/a.txt"), MID_FILE_BYTES, 'a');
TEST_EQ(chmod(tp("bp/src/a.txt"), MODE_READONLY), 0);
// Drives and disks: the host has one drive, "/", and no boot drive
TEST_EQ(FileOpsDriveType("/"), PlatformDriveFixedE);
TEST_EQ(FileOpsDriveType("Q:"), NO_DRIVE);
TEST_EQ(FileOpsDriveType(""), NO_DRIVE);
TEST_EQ(FileOpsDriveType(NULL), NO_DRIVE);
TEST_STR_EQ(FileOpsBootDrive(), "");
TEST_TRUE(FileOpsTotalBytes(tTempDir()) > 0);
TEST_TRUE(FileOpsTotalBytes(tTempDir()) >= FileOpsFreeBytes(tTempDir()));
TEST_EQ(FileOpsIsWritable(tp("bp")), BAS_TRUE);
TEST_EQ(FileOpsIsWritable(tp("bp/none")), BAS_FALSE);
TEST_EQ(FileOpsIsWritable(""), BAS_FALSE);
TEST_EQ(FileOpsIsWritable(NULL), BAS_FALSE);
platformSetReadOnly(true);
TEST_EQ(FileOpsReadOnlySystem(), BAS_TRUE);
platformSetReadOnly(false);
TEST_EQ(FileOpsReadOnlySystem(), BAS_FALSE);
// The temp folder follows TEMP, then TMP; "" with neither
char savedTemp[DVX_MAX_PATH];
char savedTmp[DVX_MAX_PATH];
const char *envTemp = getenv("TEMP");
const char *envTmp = getenv("TMP");
snprintf(savedTemp, sizeof(savedTemp), "%s", envTemp ? envTemp : "");
snprintf(savedTmp, sizeof(savedTmp), "%s", envTmp ? envTmp : "");
unsetenv("TEMP");
unsetenv("TMP");
TEST_STR_EQ(FileOpsTempDir(), "");
setenv("TMP", tp("bp"), 1);
TEST_STR_EQ(FileOpsTempDir(), tp("bp"));
setenv("TEMP", tp("bp/src"), 1);
TEST_STR_EQ(FileOpsTempDir(), tp("bp/src"));
unsetenv("TEMP");
unsetenv("TMP");
if (savedTemp[0]) {
setenv("TEMP", savedTemp, 1);
}
if (savedTmp[0]) {
setenv("TMP", savedTmp, 1);
}
// Paths and filters
TEST_EQ(FileOpsPathIsAbsolute("/x/y"), BAS_TRUE);
TEST_EQ(FileOpsPathIsAbsolute("C:\\DOCS"), BAS_TRUE);
TEST_EQ(FileOpsPathIsAbsolute("docs/a.txt"), BAS_FALSE);
TEST_EQ(FileOpsPathIsAbsolute(NULL), BAS_FALSE);
TEST_EQ(FileOpsPathIsInside(tp("bp/src/a.txt"), tp("bp")), BAS_TRUE);
TEST_EQ(FileOpsPathIsInside(tp("bp"), tp("bp")), BAS_TRUE);
TEST_EQ(FileOpsPathIsInside(tp("bp"), tp("bp/src")), BAS_FALSE);
TEST_EQ(FileOpsPathIsInside(NULL, tp("bp")), BAS_FALSE);
TEST_STR_EQ(FileOpsPathSep(), DVX_PATH_SEP);
TEST_STR_EQ(FileOpsPathNative("/a/b.txt"), "/a/b.txt");
TEST_STR_EQ(FileOpsPathNative(NULL), "");
TEST_EQ(FileOpsFilterMatch("A.TXT", "*.txt"), BAS_TRUE);
TEST_EQ(FileOpsFilterMatch("a.bmp", "*.txt;*.bmp"), BAS_TRUE);
TEST_EQ(FileOpsFilterMatch("a.png", "*.txt;*.bmp"), BAS_FALSE);
TEST_EQ(FileOpsFilterMatch("a.png", ""), BAS_TRUE);
TEST_EQ(FileOpsFilterMatch("a.png", PLATFORM_FILTER_ALL), BAS_TRUE);
TEST_EQ(FileOpsFilterMatch(NULL, "*.txt"), BAS_FALSE);
// A copy job: byte progress, and the read-only bit cleared on the copy
int32_t job = FileOpsJobCreate(FileOpsCopyE, tp("bp/dst"));
TEST_TRUE(job != 0);
TEST_EQ(FileOpsJobAddSource(job, tp("bp/src")), BAS_TRUE);
FileOpsJobClearAttr(job, FILEOPS_ATTR_READONLY);
TEST_TRUE(FileOpsJobBytesDone(job) == 0);
int32_t step = FileOpsStepBusyE;
for (int32_t i = 0; i < STEP_LIMIT && step == FileOpsStepBusyE; i++) {
step = FileOpsJobRunFor(job, JOB_SLICE_MS);
}
TEST_EQ(step, FileOpsStepDoneE);
TEST_TRUE(FileOpsJobBytesTotal(job) == MID_FILE_BYTES);
TEST_TRUE(FileOpsJobBytesDone(job) == MID_FILE_BYTES);
TEST_EQ(FileOpsGetAttr(tp("bp/dst/src/a.txt")) & FILEOPS_ATTR_READONLY, 0);
TEST_EQ(FileOpsGetAttr(tp("bp/src/a.txt")) & FILEOPS_ATTR_READONLY, FILEOPS_ATTR_READONLY);
FileOpsJobFree(job);
TEST_TRUE(FileOpsJobBytesTotal(job) == 0);
TEST_TRUE(FileOpsJobBytesDone(0) == 0);
FileOpsJobClearAttr(0, FILEOPS_ATTR_READONLY);
// Modification times in FileOpsModified$'s form
const char *f = tp("bp/dst/src/a.txt");
time_t t = 0;
char text[FILEOPS_DATE_TEXT_MAX];
TEST_EQ(FileOpsSetModified(f, STAMP_MINUTES), BAS_TRUE);
TEST_STR_EQ(FileOpsModified(f), STAMP_MINUTES);
TEST_EQ(FileOpsSetModified(f, STAMP_SECONDS), BAS_TRUE);
TEST_STR_EQ(FileOpsModified(f), STAMP_MINUTES);
TEST_EQ(FileOpsSetModified(f, "2001-02-30 04:05"), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, "2001-13-01 04:05"), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, "2001-02-03 24:05"), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, "2001-02-03"), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, "2001-02-03 04:05 pm"), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, "2001-02-03 04:05:"), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, ""), BAS_FALSE);
TEST_EQ(FileOpsSetModified(f, NULL), BAS_FALSE);
TEST_EQ(FileOpsSetModified(tp("bp/missing.txt"), STAMP_MINUTES), BAS_FALSE);
TEST_EQ(FileOpsSetModified(NULL, STAMP_MINUTES), BAS_FALSE);
// fileOpsParseDate reads back what fileOpsFormatDate writes
fileOpsFormatDate(OLD_TIME, text, sizeof(text));
TEST_TRUE(fileOpsParseDate(text, &t));
TEST_EQ(t, OLD_TIME);
TEST_TRUE(!fileOpsParseDate(text, NULL));
}
static void caseCancel(void) {
tCase("cancel mid-file removes the partial copy");
makeDir(tp("can"));
makeDir(tp("can/dst"));
makeFile(tp("can/big.bin"), BIG_FILE_BYTES, 'b');
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("can/dst"));
FileOpsProgressT p;
FileOpsStepE step = FileOpsStepBusyE;
TEST_TRUE(fileOpsJobAddSource(job, tp("can/big.bin")));
for (int32_t i = 0; i < STEP_LIMIT; i++) {
step = fileOpsJobStep(job);
fileOpsJobProgress(job, &p);
if (p.bytesDone > FILEOPS_COPY_CHUNK || step != FileOpsStepBusyE) {
break;
}
}
TEST_EQ(step, FileOpsStepBusyE);
TEST_TRUE(exists(tp("can/dst/big.bin")));
TEST_TRUE(p.percent > 0 && p.percent < FILEOPS_PERCENT_FULL);
TEST_EQ(fileOpsJobOp(job), FileOpsCopyE);
fileOpsJobCancel(job);
TEST_EQ(fileOpsJobStep(job), FileOpsStepCancelledE);
TEST_EQ(fileOpsJobStep(job), FileOpsStepCancelledE);
TEST_TRUE(!exists(tp("can/dst/big.bin")));
fileOpsJobProgress(job, &p);
TEST_EQ(p.bytesDone, 0);
fileOpsJobFree(job);
// Freeing a job in the middle of a file also removes the partial copy.
job = fileOpsJobCreate(FileOpsCopyE, tp("can/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("can/big.bin")));
for (int32_t i = 0; i < STEP_LIMIT && !exists(tp("can/dst/big.bin")); i++) {
fileOpsJobStep(job);
}
fileOpsJobStep(job);
fileOpsJobFree(job);
TEST_TRUE(!exists(tp("can/dst/big.bin")));
// Cancel answered to a question stops the job.
makeFile(tp("can/dst/big.bin"), 1, 'x');
job = fileOpsJobCreate(FileOpsCopyE, tp("can/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("can/big.bin")));
ScriptT s;
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerCancelE;
TEST_EQ(runScripted(job, &s), FileOpsStepCancelledE);
TEST_EQ(s.asks, 1);
TEST_EQ(fileSize(tp("can/dst/big.bin")), 1);
fileOpsJobFree(job);
fileOpsJobFree(NULL);
}
// fileOpsJobClearAttr: the copies lose the bits (a read-only CD-ROM
// install), the sources keep them.
static void caseClearAttr(void) {
tCase("copy clears the requested attribute bits");
uint8_t attr = 0;
makeDir(tp("ro"));
makeDir(tp("ro/src"));
makeDir(tp("ro/dst"));
makeFile(tp("ro/src/a.txt"), 3, 'a');
TEST_EQ(chmod(tp("ro/src/a.txt"), MODE_READONLY), 0);
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("ro/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("ro/src")));
fileOpsJobClearAttr(job, FILEOPS_ATTR_READONLY);
TEST_EQ(fileOpsJobRun(job, NULL), FileOpsStepDoneE);
fileOpsJobFree(job);
TEST_TRUE(fileOpsGetAttr(tp("ro/dst/src/a.txt"), &attr));
TEST_EQ(attr & FILEOPS_ATTR_READONLY, 0);
TEST_TRUE(fileOpsGetAttr(tp("ro/src/a.txt"), &attr));
TEST_EQ(attr & FILEOPS_ATTR_READONLY, FILEOPS_ATTR_READONLY);
// Without the option the copy stays read-only.
job = fileOpsJobCreate(FileOpsCopyE, tp("ro"));
TEST_TRUE(fileOpsJobAddSource(job, tp("ro/src/a.txt")));
TEST_EQ(fileOpsJobRun(job, NULL), FileOpsStepDoneE);
fileOpsJobFree(job);
TEST_TRUE(fileOpsGetAttr(tp("ro/a.txt"), &attr));
TEST_EQ(attr & FILEOPS_ATTR_READONLY, FILEOPS_ATTR_READONLY);
}
static void caseConflicts(void) {
tCase("file exists: Yes / No / All / read-only destination");
const char *base = tp("con");
ScriptT s;
makeDir(base);
makeDir(tp("con/src"));
makeDir(tp("con/dst"));
makeDir(tp("con/dst/src"));
makeFile(tp("con/src/a.txt"), 3, 'n');
makeFile(tp("con/src/b.txt"), 3, 'n');
makeFile(tp("con/src/c.txt"), 3, 'n');
makeFile(tp("con/src/d.txt"), 3, 'n');
makeFile(tp("con/dst/src/a.txt"), 3, 'o');
makeFile(tp("con/dst/src/b.txt"), 3, 'o');
makeFile(tp("con/dst/src/c.txt"), 3, 'o');
makeFile(tp("con/dst/src/d.txt"), 3, 'o');
TEST_EQ(chmod(tp("con/dst/src/b.txt"), MODE_READONLY), 0);
// Directory order is not sorted: count answers, then check by content.
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("con/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("con/src")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerNoE;
s.answers[s.count++] = FileOpsAnswerYesE;
s.answers[s.count++] = FileOpsAnswerAllE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.asks, 3);
TEST_TRUE(strstr(s.lastMessage, "already exists") != NULL);
fileOpsJobFree(job);
int32_t replaced = 0;
int32_t kept = 0;
const char *names[] = { "con/dst/src/a.txt", "con/dst/src/b.txt", "con/dst/src/c.txt", "con/dst/src/d.txt" };
for (int32_t i = 0; i < DVX_ARRAY_LEN(names); i++) {
char c = readFirst(tp(names[i]));
replaced += c == 'n';
kept += c == 'o';
}
TEST_EQ(replaced, 3);
TEST_EQ(kept, 1);
// A folder where a file would go is an error, not a question.
makeDir(tp("con/dst2"));
makeDir(tp("con/dst2/a.txt"));
job = fileOpsJobCreate(FileOpsCopyE, tp("con/dst2"));
TEST_TRUE(fileOpsJobAddSource(job, tp("con/src/a.txt")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerSkipE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.errors, 1);
TEST_TRUE(strstr(s.lastMessage, "folder with that name") != NULL);
fileOpsJobFree(job);
// A file where a folder would go: its contents are skipped with it.
makeDir(tp("con/dst3"));
makeFile(tp("con/dst3/src"), 1, 'f');
job = fileOpsJobCreate(FileOpsCopyE, tp("con/dst3"));
TEST_TRUE(fileOpsJobAddSource(job, tp("con/src")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerSkipE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.errors, 1);
TEST_EQ(s.asks, 0);
TEST_EQ(fileSize(tp("con/dst3/src")), 1);
fileOpsJobFree(job);
}
static void caseCopyErrors(void) {
tCase("unreadable folder and file: Retry, Skip");
if (geteuid() == 0) {
TEST_TRUE(true);
return;
}
ScriptT s;
makeDir(tp("err"));
makeDir(tp("err/src"));
makeDir(tp("err/src/locked"));
makeFile(tp("err/src/locked/x.txt"), 2, 'x');
makeFile(tp("err/src/ok.txt"), 2, 'k');
makeFile(tp("err/src/noread.txt"), 2, 'r');
makeDir(tp("err/dst"));
TEST_EQ(chmod(tp("err/src/locked"), MODE_NONE), 0);
TEST_EQ(chmod(tp("err/src/noread.txt"), MODE_NONE), 0);
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("err/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("err/src")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerRetryE;
s.answers[s.count++] = FileOpsAnswerSkipE;
s.answers[s.count++] = FileOpsAnswerSkipE;
s.answers[s.count++] = FileOpsAnswerSkipE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
// Folder read: error, Retry (error again), Skip; file read: error, Skip.
TEST_EQ(s.errors, 3);
TEST_TRUE(exists(tp("err/dst/src/ok.txt")));
TEST_TRUE(!exists(tp("err/dst/src/noread.txt")));
TEST_TRUE(!exists(tp("err/dst/src/locked/x.txt")));
fileOpsJobFree(job);
// Deleting: the unreadable folder cannot be emptied, so it and its
// parent stay; everything else goes.
job = fileOpsJobCreate(FileOpsDeleteE, NULL);
TEST_TRUE(fileOpsJobAddSource(job, tp("err/src")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerSkipE;
s.answers[s.count++] = FileOpsAnswerSkipE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_TRUE(s.errors >= 1);
TEST_TRUE(exists(tp("err/src/locked")));
TEST_TRUE(!exists(tp("err/src/ok.txt")));
fileOpsJobFree(job);
TEST_EQ(chmod(tp("err/src/locked"), MODE_DIR), 0);
}
static void caseCopyIntoItself(void) {
tCase("copy onto itself, into itself; move onto itself");
ScriptT s;
makeDir(tp("self"));
makeDir(tp("self/fold"));
makeDir(tp("self/fold/inner"));
makeFile(tp("self/file.txt"), 4, 's');
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("self"));
TEST_TRUE(fileOpsJobAddSource(job, tp("self/file.txt")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerSkipE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.errors, 1);
TEST_TRUE(strstr(s.lastMessage, "onto itself") != NULL);
TEST_EQ(fileSize(tp("self/file.txt")), 4);
fileOpsJobFree(job);
job = fileOpsJobCreate(FileOpsCopyE, tp("self/fold/inner"));
TEST_TRUE(fileOpsJobAddSource(job, tp("self/fold")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerSkipE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_TRUE(strstr(s.lastMessage, "copied into itself") != NULL);
TEST_TRUE(!exists(tp("self/fold/inner/fold")));
fileOpsJobFree(job);
job = fileOpsJobCreate(FileOpsMoveE, tp("self/fold"));
TEST_TRUE(fileOpsJobAddSource(job, tp("self/fold")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerCancelE;
TEST_EQ(runScripted(job, &s), FileOpsStepCancelledE);
TEST_TRUE(strstr(s.lastMessage, "moved into itself") != NULL);
fileOpsJobFree(job);
// Moving to where it already is does nothing, quietly.
job = fileOpsJobCreate(FileOpsMoveE, tp("self"));
TEST_TRUE(fileOpsJobAddSource(job, tp("self/file.txt")));
memset(&s, 0, sizeof(s));
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.errors + s.asks, 0);
TEST_TRUE(exists(tp("self/file.txt")));
fileOpsJobFree(job);
// Sources that cannot be added.
char longName[LONG_NAME_LEN + 1];
memset(longName, 'n', LONG_NAME_LEN);
longName[LONG_NAME_LEN] = '\0';
job = fileOpsJobCreate(FileOpsCopyE, tp("self"));
TEST_TRUE(!fileOpsJobAddSource(job, tp("self/missing.txt")));
fileOpsJobFree(job);
char deepDest[DVX_MAX_PATH];
snprintf(deepDest, sizeof(deepDest), "/%s", longName);
job = fileOpsJobCreate(FileOpsCopyE, deepDest);
TEST_TRUE(!fileOpsJobAddSource(job, tp("self/file.txt")));
fileOpsJobFree(job);
}
static void caseCopyTree(void) {
tCase("copy a folder tree");
makeDir(tp("cp"));
treeMake(tp("cp/src"));
makeDir(tp("cp/dst"));
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("cp/dst"));
FileOpsProgressT p;
ScriptT s;
struct stat st;
TEST_TRUE(job != NULL);
fileOpsJobProgress(job, &p);
TEST_STR_EQ(p.current, "");
TEST_TRUE(fileOpsJobAddSource(job, tp("cp/src")));
TEST_TRUE(fileOpsJobAddSource(job, tp("cp/top.txt")) == false);
memset(&s, 0, sizeof(s));
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.asks + s.errors, 0);
fileOpsJobProgress(job, &p);
TEST_EQ(p.filesTotal, 3);
TEST_EQ(p.filesDone, 3);
TEST_EQ(p.bytesTotal, 10 + 20 + MID_FILE_BYTES);
TEST_EQ(p.bytesDone, p.bytesTotal);
TEST_EQ(p.percent, FILEOPS_PERCENT_FULL);
TEST_EQ(fileOpsJobStep(job), FileOpsStepDoneE);
fileOpsJobFree(job);
TEST_EQ(fileSize(tp("cp/dst/src/a.txt")), 10);
TEST_EQ(fileSize(tp("cp/dst/src/sub/b.txt")), 20);
TEST_EQ(fileSize(tp("cp/dst/src/sub/deep/c.bin")), MID_FILE_BYTES);
TEST_EQ(readFirst(tp("cp/dst/src/sub/deep/c.bin")), 'c');
TEST_TRUE(exists(tp("cp/dst/src/empty")));
TEST_TRUE(exists(tp("cp/src/a.txt")));
// Dates and the read-only attribute travel with the copy.
TEST_EQ(stat(tp("cp/dst/src/a.txt"), &st), 0);
TEST_EQ(st.st_mtime, OLD_TIME);
TEST_EQ(stat(tp("cp/dst/src/sub/b.txt"), &st), 0);
TEST_EQ(st.st_mode & S_IWUSR, 0);
}
static void caseDelete(void) {
tCase("delete a tree with a read-only file");
ScriptT s;
treeMake(tp("del"));
makeFile(tp("del2.txt"), 1, 'd');
// No to the read-only file: it and its folders stay.
FileOpsJobT *job = fileOpsJobCreate(FileOpsDeleteE, NULL);
TEST_TRUE(fileOpsJobAddSource(job, tp("del")));
TEST_TRUE(fileOpsJobAddSource(job, tp("del2.txt")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerNoE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.asks, 1);
TEST_TRUE(strstr(s.lastMessage, "read-only") != NULL);
TEST_TRUE(exists(tp("del/sub/b.txt")));
TEST_TRUE(!exists(tp("del/sub/deep")));
TEST_TRUE(!exists(tp("del/a.txt")));
TEST_TRUE(!exists(tp("del/empty")));
TEST_TRUE(!exists(tp("del2.txt")));
fileOpsJobFree(job);
// Yes: everything goes.
job = fileOpsJobCreate(FileOpsDeleteE, NULL);
TEST_TRUE(fileOpsJobAddSource(job, tp("del")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerYesE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_TRUE(!exists(tp("del")));
fileOpsJobFree(job);
// All, and Cancel.
treeMake(tp("del3"));
makeFile(tp("del3/ro2.txt"), 1, 'r');
TEST_EQ(chmod(tp("del3/ro2.txt"), MODE_READONLY), 0);
job = fileOpsJobCreate(FileOpsDeleteE, NULL);
TEST_TRUE(fileOpsJobAddSource(job, tp("del3")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerAllE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.asks, 1);
TEST_TRUE(!exists(tp("del3")));
fileOpsJobFree(job);
treeMake(tp("del4"));
job = fileOpsJobCreate(FileOpsDeleteE, NULL);
TEST_TRUE(fileOpsJobAddSource(job, tp("del4")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerCancelE;
TEST_EQ(runScripted(job, &s), FileOpsStepCancelledE);
TEST_TRUE(exists(tp("del4/sub/b.txt")));
fileOpsJobFree(job);
}
static void caseFormat(void) {
tCase("size, date and type text");
char buf[FILEOPS_SIZE_TEXT_MAX];
FileOpsEntryT e;
fileOpsFormatSize(0, buf, sizeof(buf));
TEST_STR_EQ(buf, "0");
fileOpsFormatSize(999, buf, sizeof(buf));
TEST_STR_EQ(buf, "999");
fileOpsFormatSize(1000, buf, sizeof(buf));
TEST_STR_EQ(buf, "1,000");
fileOpsFormatSize(1234567, buf, sizeof(buf));
TEST_STR_EQ(buf, "1,234,567");
fileOpsFormatSize(UINT64_MAX, buf, sizeof(buf));
TEST_STR_EQ(buf, "18,446,744,073,709,551,615");
fileOpsFormatSize(1234567, buf, 4);
TEST_STR_EQ(buf, "1,2");
fileOpsFormatDate(OLD_TIME, buf, sizeof(buf));
TEST_EQ((int64_t)strlen(buf), 16);
TEST_TRUE(strncmp(buf, "1999", 4) == 0 || strncmp(buf, "2000", 4) == 0);
TEST_STR_EQ(fileOpsExtension("a.txt"), "txt");
TEST_STR_EQ(fileOpsExtension("dir.d/readme"), "");
TEST_STR_EQ(fileOpsExtension(".profile"), "");
TEST_STR_EQ(fileOpsExtension("a.tar.gz"), "gz");
memset(&e, 0, sizeof(e));
e.name = "notes.txt";
fileOpsTypeName(&e, buf, sizeof(buf));
TEST_STR_EQ(buf, "TXT File");
e.name = "run.app";
fileOpsTypeName(&e, buf, sizeof(buf));
TEST_STR_EQ(buf, "Program");
e.name = "README";
fileOpsTypeName(&e, buf, sizeof(buf));
TEST_STR_EQ(buf, "File");
e.isDir = true;
fileOpsTypeName(&e, buf, sizeof(buf));
TEST_STR_EQ(buf, "Folder");
}
static void caseListSort(void) {
tCase("list a folder, filter and sort");
makeDir(tp("ls"));
makeDir(tp("ls/zdir"));
makeDir(tp("ls/adir"));
makeFile(tp("ls/b.txt"), 300, 'b');
makeFile(tp("ls/a.bat"), 100, 'a');
makeFile(tp("ls/c.txt"), 200, 'c');
struct utimbuf t;
t.actime = NEWER_TIME;
t.modtime = NEWER_TIME;
TEST_EQ(utime(tp("ls/a.bat"), &t), 0);
t.actime = OLD_TIME;
t.modtime = OLD_TIME;
TEST_EQ(utime(tp("ls/b.txt"), &t), 0);
TEST_EQ(utime(tp("ls/c.txt"), &t), 0);
FileOpsEntryT *e = NULL;
int32_t count = 0;
TEST_TRUE(fileOpsListDir(tp("ls"), "*.txt", &e, &count));
TEST_EQ(count, 4);
fileOpsListSort(e, count, FileOpsSortNameE, false);
TEST_STR_EQ(e[0].name, "adir");
TEST_TRUE(e[0].isDir);
TEST_STR_EQ(e[1].name, "zdir");
TEST_STR_EQ(e[2].name, "b.txt");
TEST_STR_EQ(e[3].name, "c.txt");
fileOpsListFree(e, count);
TEST_TRUE(fileOpsListDir(tp("ls"), PLATFORM_FILTER_ALL, &e, &count));
TEST_EQ(count, 5);
fileOpsListSort(e, count, FileOpsSortNameE, true);
TEST_STR_EQ(e[0].name, "zdir");
TEST_STR_EQ(e[2].name, "c.txt");
TEST_STR_EQ(e[4].name, "a.bat");
fileOpsListSort(e, count, FileOpsSortSizeE, false);
TEST_STR_EQ(e[2].name, "a.bat");
TEST_STR_EQ(e[4].name, "b.txt");
TEST_EQ(e[4].size, 300);
fileOpsListSort(e, count, FileOpsSortSizeE, true);
TEST_TRUE(e[0].isDir && e[1].isDir);
TEST_STR_EQ(e[2].name, "b.txt");
// Equal dates fall back to the name, always A to Z.
fileOpsListSort(e, count, FileOpsSortDateE, true);
TEST_STR_EQ(e[2].name, "a.bat");
TEST_STR_EQ(e[3].name, "b.txt");
TEST_STR_EQ(e[4].name, "c.txt");
fileOpsListSort(e, count, FileOpsSortTypeE, false);
TEST_STR_EQ(e[2].name, "a.bat");
TEST_STR_EQ(e[3].name, "b.txt");
TEST_STR_EQ(e[4].name, "c.txt");
fileOpsListSort(e, 1, FileOpsSortDateE, false);
fileOpsListFree(e, count);
TEST_TRUE(!fileOpsListDir(tp("ls/none"), NULL, &e, &count));
}
static void caseMakeRename(void) {
tCase("new folder and rename");
char err[FILEOPS_MSG_MAX];
makeDir(tp("mk"));
TEST_TRUE(fileOpsMakeDir(tp("mk"), "newdir", err, sizeof(err)));
TEST_TRUE(exists(tp("mk/newdir")));
TEST_TRUE(!fileOpsMakeDir(tp("mk"), "newdir", err, sizeof(err)));
TEST_TRUE(strstr(err, "already exists") != NULL);
TEST_TRUE(!fileOpsMakeDir(tp("mk"), "a/b", err, sizeof(err)));
TEST_TRUE(!fileOpsMakeDir(tp("mk"), "", err, sizeof(err)));
TEST_TRUE(!fileOpsValidateName(NULL, "..", err, sizeof(err)));
TEST_TRUE(fileOpsValidateName(NULL, "ok.txt", err, sizeof(err)));
char longName[LONG_NAME_LEN + 1];
memset(longName, 'q', LONG_NAME_LEN);
longName[LONG_NAME_LEN] = '\0';
TEST_TRUE(!fileOpsMakeDir(tp("mk"), longName, err, sizeof(err)));
TEST_TRUE(strstr(err, "too long") != NULL);
makeFile(tp("mk/f.txt"), 1, 'f');
TEST_TRUE(fileOpsRename(tp("mk"), "f.txt", "g.txt", err, sizeof(err)));
TEST_TRUE(exists(tp("mk/g.txt")));
TEST_TRUE(!exists(tp("mk/f.txt")));
TEST_TRUE(!fileOpsRename(tp("mk"), "g.txt", "newdir", err, sizeof(err)));
TEST_TRUE(strstr(err, "already exists") != NULL);
TEST_TRUE(!fileOpsRename(tp("mk"), "nothere", "x", err, sizeof(err)));
TEST_TRUE(!fileOpsRename(tp("mk"), "g.txt", "", err, sizeof(err)));
TEST_TRUE(!fileOpsRename(tp("mk"), "g.txt", longName, err, sizeof(err)));
TEST_TRUE(fileOpsRename(tp("mk"), "newdir", "renamed", err, sizeof(err)));
TEST_TRUE(exists(tp("mk/renamed")));
// BASIC: a whole chain of new folders, one that is there already, a
// file in the way, nothing
TEST_EQ(FileOpsMakePath(tp("mk/deep/er/still")), -1);
TEST_TRUE(exists(tp("mk/deep/er/still")));
TEST_EQ(FileOpsMakePath(tp("mk/deep")), -1);
TEST_EQ(FileOpsMakePath(tp("mk/g.txt/below")), 0);
TEST_EQ(FileOpsMakePath(""), 0);
TEST_EQ(FileOpsMakePath(NULL), 0);
// BASIC: file names, and an atomic save through the scratch sibling
TEST_EQ(FileOpsValidName("", "ok.txt"), -1);
TEST_STR_EQ(FileOpsError(), "");
TEST_EQ(FileOpsValidName("", ".."), 0);
TEST_TRUE(FileOpsError()[0] != '\0');
// BASIC: the long-name and 8.3 rules by themselves; the host has no
// short names, so a path stays as it is
TEST_EQ(FileOpsValidLongName("My Report v2.final.txt"), -1);
TEST_STR_EQ(FileOpsError(), "");
TEST_EQ(FileOpsValidShortName("My Report v2.final.txt"), 0);
TEST_TRUE(FileOpsError()[0] != '\0');
TEST_EQ(FileOpsValidShortName("REPORT.TXT"), -1);
TEST_STR_EQ(FileOpsError(), "");
TEST_EQ(FileOpsValidLongName("con.txt"), 0);
TEST_EQ(FileOpsValidLongName("ends with dot."), 0);
TEST_STR_EQ(FileOpsShortPath(tp("mk")), tp("mk"));
TEST_STR_EQ(FileOpsShortPath(NULL), "");
char target[DVX_MAX_PATH];
char scratch[DVX_MAX_PATH];
snprintf(target, sizeof(target), "%s", tp("mk/save.bas"));
makeFile(target, 1, 'o');
snprintf(scratch, sizeof(scratch), "%s", FileOpsTempPath(target));
TEST_TRUE(scratch[0] != '\0' && strcmp(scratch, target) != 0);
makeFile(scratch, 1, 'n');
TEST_EQ(FileOpsReplaceFile(scratch, target), -1);
TEST_EQ(readFirst(target), 'n');
TEST_TRUE(!exists(scratch));
TEST_EQ(FileOpsReplaceFile(tp("mk/none.$ba"), target), 0);
TEST_TRUE(strstr(FileOpsError(), "could not be replaced") != NULL);
TEST_EQ(readFirst(target), 'n');
}
static void caseMove(void) {
tCase("move: whole-item rename and merge into an existing folder");
ScriptT s;
makeDir(tp("mv"));
treeMake(tp("mv/src"));
makeDir(tp("mv/dst"));
FileOpsJobT *job = fileOpsJobCreate(FileOpsMoveE, tp("mv/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("mv/src")));
memset(&s, 0, sizeof(s));
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_TRUE(!exists(tp("mv/src")));
TEST_TRUE(exists(tp("mv/dst/src/sub/deep/c.bin")));
fileOpsJobFree(job);
// Merge: the destination folder exists, so files are moved one by one
// (existing ones replaced after All) and emptied source folders removed.
treeMake(tp("mv/src"));
makeFile(tp("mv/src/extra.txt"), 6, 'e');
makeFile(tp("mv/dst/src/a.txt"), 1, 'o');
TEST_EQ(chmod(tp("mv/dst/src/sub/b.txt"), MODE_FILE), 0);
job = fileOpsJobCreate(FileOpsMoveE, tp("mv/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("mv/src")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerAllE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.asks, 1);
TEST_TRUE(!exists(tp("mv/src")));
TEST_EQ(fileSize(tp("mv/dst/src/a.txt")), 10);
TEST_TRUE(exists(tp("mv/dst/src/extra.txt")));
TEST_TRUE(exists(tp("mv/dst/src/empty")));
fileOpsJobFree(job);
// No keeps the source file, and so its folder.
treeMake(tp("mv/src"));
job = fileOpsJobCreate(FileOpsMoveE, tp("mv/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("mv/src")));
memset(&s, 0, sizeof(s));
s.answers[s.count++] = FileOpsAnswerNoE;
s.answers[s.count++] = FileOpsAnswerNoE;
s.answers[s.count++] = FileOpsAnswerNoE;
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_EQ(s.asks, 3);
TEST_TRUE(exists(tp("mv/src/a.txt")));
TEST_TRUE(exists(tp("mv/src/sub/deep/c.bin")));
TEST_TRUE(!exists(tp("mv/src/empty")));
fileOpsJobFree(job);
}
static void casePaths(void) {
tCase("paths");
char out[DVX_MAX_PATH];
char small[4];
TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "/", "x"));
TEST_STR_EQ(out, "/x");
TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "/a", "b"));
TEST_STR_EQ(out, "/a/b");
TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "C:\\", "B"));
TEST_STR_EQ(out, "C:" DVX_PATH_SEP "B");
TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "C:/A\\", "B/C"));
TEST_STR_EQ(out, "C:" DVX_PATH_SEP "A" DVX_PATH_SEP "B" DVX_PATH_SEP "C");
TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "/a", "C:\\X"));
TEST_STR_EQ(out, "C:" DVX_PATH_SEP "X");
TEST_TRUE(fileOpsPathJoin(out, sizeof(out), "", "x"));
TEST_STR_EQ(out, "x");
TEST_TRUE(!fileOpsPathJoin(small, sizeof(small), "/abc", "def"));
TEST_TRUE(fileOpsPathParent("/a/b", out, sizeof(out)));
TEST_STR_EQ(out, "/a");
TEST_TRUE(fileOpsPathParent("/a/b/", out, sizeof(out)));
TEST_STR_EQ(out, "/a");
TEST_TRUE(fileOpsPathParent("/a", out, sizeof(out)));
TEST_STR_EQ(out, "/");
TEST_TRUE(fileOpsPathParent("C:\\DVX", out, sizeof(out)));
TEST_STR_EQ(out, "C:" DVX_PATH_SEP);
TEST_TRUE(fileOpsPathParent("C:\\DVX\\APPS", out, sizeof(out)));
TEST_STR_EQ(out, "C:" DVX_PATH_SEP "DVX");
TEST_TRUE(fileOpsPathParent("C:/DVX/APPS/", out, sizeof(out)));
TEST_STR_EQ(out, "C:" DVX_PATH_SEP "DVX");
TEST_TRUE(!fileOpsPathParent("/", out, sizeof(out)));
TEST_TRUE(!fileOpsPathParent("C:\\", out, sizeof(out)));
TEST_TRUE(!fileOpsPathParent("name", out, sizeof(out)));
TEST_TRUE(!fileOpsPathParent("/abcdef/x", small, sizeof(small)));
TEST_TRUE(fileOpsPathIsRoot("/"));
TEST_TRUE(fileOpsPathIsRoot("\\"));
TEST_TRUE(fileOpsPathIsRoot("C:\\"));
TEST_TRUE(fileOpsPathIsRoot("C:/"));
TEST_TRUE(fileOpsPathIsRoot("C:"));
TEST_TRUE(!fileOpsPathIsRoot("/a"));
TEST_TRUE(!fileOpsPathIsRoot("C:\\A"));
TEST_TRUE(!fileOpsPathIsRoot(""));
TEST_TRUE(fileOpsPathIsInside("/a/b", "/a"));
TEST_TRUE(fileOpsPathIsInside("/a", "/a/"));
TEST_TRUE(fileOpsPathIsInside("/a/b", "/"));
TEST_TRUE(fileOpsPathIsInside("C:\\A\\B", "C:/A"));
TEST_TRUE(!fileOpsPathIsInside("/ab", "/a"));
TEST_TRUE(!fileOpsPathIsInside("/a", "/a/b"));
TEST_TRUE(fileOpsPathSame("/a/", "/a"));
TEST_TRUE(fileOpsPathSame("C:\\A", "C:/A"));
TEST_TRUE(!fileOpsPathSame("/a/b", "/a"));
TEST_TRUE(!fileOpsPathSame("/a", "/b"));
char dotted[DVX_MAX_PATH];
makeDir(tp("np"));
makeDir(tp("np/sub"));
makeFile(tp("np/file"), 1, 'f');
snprintf(dotted, sizeof(dotted), "%s/np/./sub/..", tTempDir());
TEST_TRUE(fileOpsPathNormalize(dotted, out, sizeof(out)));
TEST_TRUE(fileOpsPathSame(out, tp("np")) || strstr(out, "/np") != NULL);
TEST_TRUE(strstr(out, "..") == NULL);
TEST_TRUE(!fileOpsPathNormalize(tp("np/file"), out, sizeof(out)));
TEST_TRUE(!fileOpsPathNormalize(tp("np/none"), out, sizeof(out)));
TEST_TRUE(!fileOpsPathNormalize(tp("np"), small, sizeof(small)));
}
static void caseRunCallbacks(void) {
tCase("fileOpsJobRun: callbacks and defaults");
makeDir(tp("run"));
makeDir(tp("run/dst"));
makeFile(tp("run/big.bin"), BIG_FILE_BYTES, 'g');
makeFile(tp("run/dst/big.bin"), 1, 'o');
// No callbacks: questions answered No (the existing file is kept).
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("run/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("run/big.bin")));
TEST_EQ(fileOpsJobRun(job, NULL), FileOpsStepDoneE);
TEST_EQ(fileSize(tp("run/dst/big.bin")), 1);
fileOpsJobFree(job);
// Progress returning false cancels.
TEST_EQ(remove(tp("run/dst/big.bin")), 0);
int32_t calls = 0;
FileOpsCallbacksT cb = { runCancelProgress, NULL, &calls };
job = fileOpsJobCreate(FileOpsCopyE, tp("run/dst"));
TEST_TRUE(fileOpsJobAddSource(job, tp("run/big.bin")));
TEST_EQ(fileOpsJobRun(job, &cb), FileOpsStepCancelledE);
TEST_TRUE(calls > 0);
TEST_TRUE(!exists(tp("run/dst/big.bin")));
fileOpsJobFree(job);
}
// Links are items of their own: a link to a parent folder does not make
// the walk recurse forever, and deleting a link leaves its target alone.
static void caseSymlinks(void) {
tCase("symbolic links are not followed");
ScriptT s;
makeDir(tp("sl"));
makeDir(tp("sl/target"));
makeFile(tp("sl/target/keep.txt"), 4, 'k');
makeDir(tp("sl/tree"));
makeDir(tp("sl/copy"));
makeFile(tp("sl/tree/file.txt"), 2, 'f');
TEST_EQ(symlink("..", tp("sl/tree/up")), 0);
TEST_EQ(symlink("../target", tp("sl/tree/out")), 0);
uint64_t bytes;
int32_t files;
int32_t folders;
TEST_TRUE(fileOpsTreeSize(tp("sl/tree"), &bytes, &files, &folders));
TEST_EQ(folders, 0);
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, tp("sl/copy"));
TEST_TRUE(fileOpsJobAddSource(job, tp("sl/tree")));
memset(&s, 0, sizeof(s));
for (int32_t i = 0; i < ANSWERS_MAX; i++) {
s.answers[s.count++] = FileOpsAnswerSkipE;
}
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_TRUE(exists(tp("sl/copy/tree/file.txt")));
fileOpsJobFree(job);
job = fileOpsJobCreate(FileOpsDeleteE, NULL);
TEST_TRUE(fileOpsJobAddSource(job, tp("sl/tree")));
memset(&s, 0, sizeof(s));
TEST_EQ(runScripted(job, &s), FileOpsStepDoneE);
TEST_TRUE(!exists(tp("sl/tree")));
TEST_TRUE(exists(tp("sl/target/keep.txt")));
fileOpsJobFree(job);
}
static void caseTreeSize(void) {
tCase("tree size");
uint64_t bytes;
int32_t files;
int32_t folders;
treeMake(tp("ts"));
TEST_TRUE(fileOpsTreeSize(tp("ts"), &bytes, &files, &folders));
TEST_EQ(bytes, 10 + 20 + MID_FILE_BYTES);
TEST_EQ(files, 3);
TEST_EQ(folders, 3);
TEST_TRUE(fileOpsTreeSize(tp("ts/a.txt"), &bytes, &files, &folders));
TEST_EQ(bytes, 10);
TEST_EQ(files, 1);
TEST_EQ(folders, 0);
TEST_TRUE(!fileOpsTreeSize(tp("ts/none"), &bytes, &files, &folders));
}
static bool exists(const char *path) {
struct stat st;
return stat(path, &st) == 0;
}
static int64_t fileSize(const char *path) {
struct stat st;
return stat(path, &st) == 0 ? (int64_t)st.st_size : -1;
}
static void makeDir(const char *path) {
TEST_EQ(mkdir(path, MODE_DIR), 0);
}
static void makeFile(const char *path, int32_t bytes, char fill) {
char *buf = (char *)malloc((size_t)bytes + 1);
memset(buf, fill, (size_t)bytes);
chmod(path, MODE_FILE);
TEST_TRUE(tWriteFile(path, buf, bytes));
free(buf);
}
static char readFirst(const char *path) {
FILE *f = fopen(path, "rb");
int32_t c = f ? fgetc(f) : EOF;
if (f) {
fclose(f);
}
return (char)c;
}
// Cancel once the copy has started.
static bool runCancelProgress(FileOpsJobT *job, void *ctx) {
FileOpsProgressT p;
int32_t *calls = (int32_t *)ctx;
(*calls)++;
fileOpsJobProgress(job, &p);
return p.bytesDone == 0;
}
// Step the job to the end, answering questions from the script (a missing
// answer is Cancel).
static FileOpsStepE runScripted(FileOpsJobT *job, ScriptT *s) {
for (int32_t i = 0; i < STEP_LIMIT; i++) {
FileOpsStepE step = fileOpsJobStep(job);
if (step == FileOpsStepDoneE || step == FileOpsStepCancelledE) {
return step;
}
if (step == FileOpsStepAskE || step == FileOpsStepErrorE) {
FileOpsAnswerE answer = s->next < s->count ? s->answers[s->next++] : FileOpsAnswerCancelE;
s->asks += step == FileOpsStepAskE;
s->errors += step == FileOpsStepErrorE;
snprintf(s->lastMessage, sizeof(s->lastMessage), "%s", fileOpsJobMessage(job));
// Unanswered, the job keeps waiting.
TEST_EQ(fileOpsJobStep(job), step);
fileOpsJobAnswer(job, answer);
}
}
return FileOpsStepBusyE;
}
// Path inside the temporary directory (one of a few rotating buffers).
static const char *tp(const char *rel) {
static char bufs[4][PATH_MAX_T];
static int32_t next = 0;
char *b = bufs[next];
next = (next + 1) % 4;
snprintf(b, PATH_MAX_T, "%s/%s", tTempDir(), rel);
return b;
}
// base/{a.txt(10, old date), empty/, sub/{b.txt(20, read-only), deep/c.bin}}
static void treeMake(const char *base) {
char p[PATH_MAX_T];
struct utimbuf t;
makeDir(base);
snprintf(p, sizeof(p), "%s/a.txt", base);
makeFile(p, 10, 'a');
t.actime = OLD_TIME;
t.modtime = OLD_TIME;
TEST_EQ(utime(p, &t), 0);
snprintf(p, sizeof(p), "%s/empty", base);
makeDir(p);
snprintf(p, sizeof(p), "%s/sub", base);
makeDir(p);
snprintf(p, sizeof(p), "%s/sub/b.txt", base);
makeFile(p, 20, 'b');
TEST_EQ(chmod(p, MODE_READONLY), 0);
snprintf(p, sizeof(p), "%s/sub/deep", base);
makeDir(p);
snprintf(p, sizeof(p), "%s/sub/deep/c.bin", base);
makeFile(p, MID_FILE_BYTES, 'c');
}
int main(void) {
platformSetLogPath("fileOpsTest.log");
tSuiteBegin("fileops");
caseFormat();
casePaths();
caseAttributes();
caseClearAttr();
caseListSort();
caseMakeRename();
caseTreeSize();
caseCopyTree();
caseConflicts();
caseCopyIntoItself();
caseMove();
caseDelete();
caseCancel();
caseCopyErrors();
caseRunCallbacks();
caseSymlinks();
caseBasic();
caseBasicPlatform();
return tSuiteEnd();
}