// 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 #include #include #include #include #include #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(); }