CI / lint (pull_request) Successful in 1m53s
CI / parity-full (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / parity-fast (pull_request) Successful in 17s
CI / build-and-test (pull_request) Successful in 54s
359 lines
12 KiB
C
359 lines
12 KiB
C
#include "test_delay_updates.h"
|
|
#include "config.h"
|
|
#include "delay_updates.h"
|
|
#include "file.h"
|
|
#include "file_receive.h"
|
|
#include "test_utils.h"
|
|
#include "utils.h"
|
|
#include <dirent.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
/* Recursively remove a test tree (never follows symlinks). */
|
|
static void remove_tree(const char* path) {
|
|
struct stat st;
|
|
if (lstat(path, &st) != 0)
|
|
return;
|
|
if (S_ISDIR(st.st_mode)) {
|
|
DIR* dir = opendir(path);
|
|
if (!dir)
|
|
return;
|
|
const struct dirent* entry;
|
|
while ((entry = readdir(dir)) != NULL) {
|
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
|
continue;
|
|
char* child = path_cat(path, entry->d_name);
|
|
if (child) {
|
|
remove_tree(child);
|
|
free(child);
|
|
}
|
|
}
|
|
closedir(dir);
|
|
rmdir(path);
|
|
} else {
|
|
unlink(path);
|
|
}
|
|
}
|
|
|
|
/* Build a File that carries `content`. */
|
|
static File* make_file(const char* path, const char* content) {
|
|
File* f = file_create(path);
|
|
if (!f)
|
|
return NULL;
|
|
f->data->data = malloc(strlen(content));
|
|
if (!f->data->data) {
|
|
file_destroy(f);
|
|
return NULL;
|
|
}
|
|
memcpy(f->data->data, content, strlen(content));
|
|
f->data->size = strlen(content);
|
|
return f;
|
|
}
|
|
|
|
static char* read_all(const char* path) {
|
|
FILE* fp = fopen(path, "rb");
|
|
if (!fp)
|
|
return NULL;
|
|
char buf[256] = {0};
|
|
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
|
fclose(fp);
|
|
char* out = malloc(n + 1);
|
|
if (!out)
|
|
return NULL;
|
|
memcpy(out, buf, n);
|
|
out[n] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static void test_delay_updates_no_final_before_publish() {
|
|
const char* root = "test_delay_tmp";
|
|
remove_tree(root);
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
cfg->delay_updates = true;
|
|
|
|
File* f = make_file("sub/file.txt", "staged payload");
|
|
EXPECT_NOT_NULL(f);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!cfg || !f)
|
|
goto out;
|
|
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN);
|
|
EXPECT_NOT_NULL(cfg->delay_context);
|
|
|
|
const char* final_path = "test_delay_tmp/sub/file.txt";
|
|
/* Before publication the final destination must not contain the file. */
|
|
EXPECT_FALSE(file_path_exists_secure(final_path));
|
|
/* The complete staged copy must live inside the per-run staging tree. */
|
|
EXPECT_NOT_NULL(cfg->delay_context->staging_name);
|
|
EXPECT_EQ_INT(strncmp(cfg->delay_context->staging_name, ".fastsync-stage.", 16), 0);
|
|
char* staged = path_cat(cfg->delay_context->staging_root, "/sub/file.txt");
|
|
EXPECT_NOT_NULL(staged);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (staged) {
|
|
char* content = read_all(staged);
|
|
EXPECT_NOT_NULL(content);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (content) {
|
|
EXPECT_EQ_STR(content, "staged payload");
|
|
free(content);
|
|
}
|
|
free(staged);
|
|
}
|
|
|
|
out:
|
|
file_destroy(f);
|
|
config_delete(cfg);
|
|
remove_tree(root);
|
|
}
|
|
|
|
static void test_delay_updates_publish_installs_files() {
|
|
const char* root = "test_delay_pub_tmp";
|
|
remove_tree(root);
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
cfg->delay_updates = true;
|
|
|
|
File* f = make_file("sub/file.txt", "published payload");
|
|
EXPECT_NOT_NULL(f);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!cfg || !f)
|
|
goto out;
|
|
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN);
|
|
const char* final_path = "test_delay_pub_tmp/sub/file.txt";
|
|
EXPECT_FALSE(file_path_exists_secure(final_path));
|
|
|
|
char* staging_root = str_dup(cfg->delay_context->staging_root);
|
|
EXPECT_NOT_NULL(staging_root);
|
|
EXPECT_TRUE(delay_updates_publish(cfg->delay_context, cfg));
|
|
/* After a successful publish the file is installed and staging is gone. */
|
|
char* content = read_all(final_path);
|
|
EXPECT_NOT_NULL(content);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (content) {
|
|
EXPECT_EQ_STR(content, "published payload");
|
|
free(content);
|
|
}
|
|
EXPECT_FALSE(file_path_exists_secure(staging_root));
|
|
free(staging_root);
|
|
|
|
out:
|
|
file_destroy(f);
|
|
config_delete(cfg);
|
|
remove_tree(root);
|
|
}
|
|
|
|
/* The staged tree is cleaned on the error/abort path and final files that were
|
|
never published do not appear at the destination. */
|
|
static void test_delay_updates_cleanup_removes_staged() {
|
|
const char* root = "test_delay_clean_tmp";
|
|
remove_tree(root);
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
cfg->delay_updates = true;
|
|
|
|
File* f = make_file("sub/file.txt", "never installed");
|
|
EXPECT_NOT_NULL(f);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!cfg || !f)
|
|
goto out;
|
|
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN);
|
|
char* staged_file = path_cat(cfg->delay_context->staging_root, "/sub/file.txt");
|
|
char* staging_root = str_dup(cfg->delay_context->staging_root);
|
|
EXPECT_NOT_NULL(staged_file);
|
|
EXPECT_NOT_NULL(staging_root);
|
|
EXPECT_TRUE(file_path_exists_secure(staged_file));
|
|
|
|
delay_updates_cleanup(cfg->delay_context);
|
|
EXPECT_FALSE(file_path_exists_secure(staging_root));
|
|
EXPECT_FALSE(file_path_exists_secure("test_delay_clean_tmp/sub/file.txt"));
|
|
free(staged_file);
|
|
free(staging_root);
|
|
|
|
out:
|
|
file_destroy(f);
|
|
config_delete(cfg);
|
|
remove_tree(root);
|
|
}
|
|
|
|
/* With --backup the previous version is only moved aside at publication. */
|
|
static void test_delay_updates_backup_deferred_to_publish() {
|
|
const char* root = "test_delay_bak_tmp";
|
|
remove_tree(root);
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
cfg->delay_updates = true;
|
|
cfg->backup = true;
|
|
|
|
EXPECT_TRUE(file_write_to_disk("test_delay_bak_tmp/file.txt", "AAAA", 4, false, false));
|
|
|
|
File* f = make_file("file.txt", "BBBB");
|
|
EXPECT_NOT_NULL(f);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!cfg || !f)
|
|
goto out;
|
|
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, f, cfg), FILE_SAVE_WRITTEN);
|
|
/* Stage time must not touch the final file or create the backup yet. */
|
|
char* before = read_all("test_delay_bak_tmp/file.txt");
|
|
EXPECT_NOT_NULL(before);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (before) {
|
|
EXPECT_EQ_STR(before, "AAAA");
|
|
free(before);
|
|
}
|
|
EXPECT_FALSE(file_path_exists_secure("test_delay_bak_tmp/file.txt~"));
|
|
|
|
EXPECT_TRUE(delay_updates_publish(cfg->delay_context, cfg));
|
|
char* after = read_all("test_delay_bak_tmp/file.txt");
|
|
char* backup = read_all("test_delay_bak_tmp/file.txt~");
|
|
EXPECT_NOT_NULL(after);
|
|
EXPECT_NOT_NULL(backup);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (after) {
|
|
EXPECT_EQ_STR(after, "BBBB");
|
|
free(after);
|
|
}
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (backup) {
|
|
EXPECT_EQ_STR(backup, "AAAA");
|
|
free(backup);
|
|
}
|
|
|
|
out:
|
|
file_destroy(f);
|
|
config_delete(cfg);
|
|
remove_tree(root);
|
|
}
|
|
|
|
/* Skip/update policy checks run against the final path at stage time, matching
|
|
what an immediate run would decide. */
|
|
static void test_delay_updates_skip_semantics() {
|
|
const char* root = "test_delay_skip_tmp";
|
|
remove_tree(root);
|
|
Config* cfg = config_create();
|
|
EXPECT_NOT_NULL(cfg);
|
|
cfg->delay_updates = true;
|
|
|
|
/* --existing: final destination missing -> skipped, nothing staged. */
|
|
File* missing = make_file("missing.txt", "new");
|
|
EXPECT_NOT_NULL(missing);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!cfg || !missing)
|
|
goto out;
|
|
cfg->existing = true;
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, missing, cfg), FILE_SAVE_SKIPPED);
|
|
cfg->existing = false;
|
|
|
|
/* --ignore-existing: final destination present -> skipped. */
|
|
EXPECT_TRUE(file_write_to_disk("test_delay_skip_tmp/existing.txt", "old", 3, false, false));
|
|
File* present = make_file("existing.txt", "new");
|
|
EXPECT_NOT_NULL(present);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!present)
|
|
goto out;
|
|
cfg->ignore_existing = true;
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, present, cfg), FILE_SAVE_SKIPPED);
|
|
cfg->ignore_existing = false;
|
|
|
|
/* Without a skip flag the file is staged and later published. */
|
|
File* fresh = make_file("fresh.txt", "content");
|
|
EXPECT_NOT_NULL(fresh);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!fresh)
|
|
goto out;
|
|
EXPECT_EQ_INT(file_save_to_disk_full(root, fresh, cfg), FILE_SAVE_WRITTEN);
|
|
EXPECT_TRUE(delay_updates_publish(cfg->delay_context, cfg));
|
|
char* content = read_all("test_delay_skip_tmp/fresh.txt");
|
|
EXPECT_NOT_NULL(content);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (content) {
|
|
EXPECT_EQ_STR(content, "content");
|
|
free(content);
|
|
}
|
|
|
|
out:
|
|
file_destroy(missing);
|
|
file_destroy(present);
|
|
file_destroy(fresh);
|
|
config_delete(cfg);
|
|
remove_tree(root);
|
|
}
|
|
|
|
/* Every context picks its own staging directory name, so two delayed
|
|
transfers to the same root can never share (and corrupt) a staging tree. */
|
|
static void test_delay_updates_unique_staging_name() {
|
|
DelayUpdatesContext* first = delay_updates_context_create("test_delay_uniq_tmp");
|
|
DelayUpdatesContext* second = delay_updates_context_create("test_delay_uniq_tmp");
|
|
EXPECT_NOT_NULL(first);
|
|
EXPECT_NOT_NULL(second);
|
|
/* cppcheck-suppress knownConditionTrueFalse -- the EXPECT_NOT_NULL checks above return on NULL */
|
|
if (first && second) {
|
|
EXPECT_EQ_INT(strncmp(first->staging_name, ".fastsync-stage.", 16), 0);
|
|
EXPECT_EQ_INT(strncmp(second->staging_name, ".fastsync-stage.", 16), 0);
|
|
EXPECT_TRUE(strcmp(first->staging_name, second->staging_name) != 0);
|
|
EXPECT_TRUE(strcmp(first->staging_root, second->staging_root) != 0);
|
|
}
|
|
delay_updates_context_destroy(first);
|
|
delay_updates_context_destroy(second);
|
|
}
|
|
|
|
/* A pre-existing destination entry at the exact (random) staging path is not
|
|
ours: prepare() must refuse rather than wipe it. */
|
|
static void test_delay_updates_prepare_refuses_non_owned_collision() {
|
|
const char* root = "test_delay_collide_tmp";
|
|
remove_tree(root);
|
|
DelayUpdatesContext* context = delay_updates_context_create(root);
|
|
EXPECT_NOT_NULL(context);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (!context)
|
|
return;
|
|
/* Plant a genuine directory with user data at the exact staging path. */
|
|
EXPECT_TRUE(file_ensure_directory_secure(context->staging_root));
|
|
char* inner = path_cat(context->staging_root, "keepme.txt");
|
|
EXPECT_NOT_NULL(inner);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (inner) {
|
|
EXPECT_TRUE(file_write_to_disk(inner, "genuine", 7, false, false));
|
|
EXPECT_FALSE(delay_updates_prepare(context));
|
|
char* content = read_all(inner);
|
|
EXPECT_NOT_NULL(content);
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
if (content) {
|
|
EXPECT_EQ_STR(content, "genuine");
|
|
free(content);
|
|
}
|
|
free(inner);
|
|
}
|
|
delay_updates_context_destroy(context);
|
|
remove_tree(root);
|
|
}
|
|
|
|
/* The reserved staging name must be recognizable for validation, including
|
|
with a trailing slash. */
|
|
static void test_delay_updates_reserved_name_helper() {
|
|
EXPECT_TRUE(delay_updates_staging_name_conflict(".fastsync-stage"));
|
|
EXPECT_TRUE(delay_updates_staging_name_conflict(".fastsync-stage/"));
|
|
EXPECT_TRUE(delay_updates_staging_name_conflict(".fastsync-stage///"));
|
|
EXPECT_FALSE(delay_updates_staging_name_conflict(NULL));
|
|
EXPECT_FALSE(delay_updates_staging_name_conflict(""));
|
|
EXPECT_FALSE(delay_updates_staging_name_conflict("backups"));
|
|
EXPECT_FALSE(delay_updates_staging_name_conflict(".fastsync-stage.bak"));
|
|
}
|
|
|
|
void test_delay_updates() {
|
|
test_delay_updates_reserved_name_helper();
|
|
test_delay_updates_unique_staging_name();
|
|
test_delay_updates_prepare_refuses_non_owned_collision();
|
|
test_delay_updates_no_final_before_publish();
|
|
test_delay_updates_publish_installs_files();
|
|
test_delay_updates_cleanup_removes_staged();
|
|
test_delay_updates_backup_deferred_to_publish();
|
|
test_delay_updates_skip_semantics();
|
|
}
|