1256 lines
43 KiB
C
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();
|
|
}
|