test(ha-config-bootstrap): RED-phase tests for ha-term.bin durability (task 6.1)
5 Unity tests covering persist (req 9.1), restore (req 9.2 RED), missing-file zero-init (req 9.3), CRC-mismatch safe-zero, and write+rename atomicity. Build fails with 3 undefined-reference linker errors on ha_restore_term() — expected RED phase proof.
This commit is contained in:
parent
ab174c8b06
commit
722d7824c4
4 changed files with 406 additions and 1 deletions
|
|
@ -135,7 +135,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A);
|
|||
- _Boundary: Config Bootstrap (config_repl.c, myapp.c)_
|
||||
|
||||
- [ ] 6. Change 9: Election state durability (ha-term.bin)
|
||||
- [ ] 6.1 Write failing tests for ha-term.bin persist and restore
|
||||
- [x] 6.1 Write failing tests for ha-term.bin persist and restore
|
||||
- In `src/controller/testsuite/test_ha_durability.c`: test `ha_persist_term()` writes file; test `ha_init()` restores term and voted_for from file; test CRC mismatch → zeroes state safely with WARN log; test missing file → term=0, no prior vote; test write+rename atomicity (partial write leaves old file intact)
|
||||
- `make test` fails
|
||||
- _Requirements: 9.1, 9.2, 9.3_
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ controller_unit_test_src = [
|
|||
'testsuite/test_ha_election.c',
|
||||
'testsuite/test_ha_bootstrap.c',
|
||||
'testsuite/test_ha_bootstrap_support.c',
|
||||
'testsuite/test_ha_durability.c',
|
||||
'startup_validation.c',
|
||||
'worker.c',
|
||||
'persist.c',
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ extern void test_worker_join(void);
|
|||
extern void test_ha_startup(void);
|
||||
extern void test_ha_election(void);
|
||||
extern void test_ha_bootstrap(void);
|
||||
extern void test_ha_durability(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -143,6 +144,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_ha_startup);
|
||||
TEST(test_ha_election);
|
||||
TEST(test_ha_bootstrap);
|
||||
TEST(test_ha_durability);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
402
src/controller/testsuite/test_ha_durability.c
Normal file
402
src/controller/testsuite/test_ha_durability.c
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
/* test_ha_durability.c — Unity RED-phase tests for ha-term.bin persist/restore.
|
||||
*
|
||||
* Tests verify:
|
||||
* Req 9.1: ha_persist_term() writes ha-term.bin with correct magic, term,
|
||||
* voted_for_term, voted_for_name, and valid CRC32.
|
||||
* Req 9.2: ha_restore_term() reads ha-term.bin and restores term and voted_for
|
||||
* into ha_state_t (RED: ha_restore_term does not exist yet).
|
||||
* Req 9.3: Missing ha-term.bin → term=0, voted_for_name empty (safe first-boot).
|
||||
* Req 9.2: CRC mismatch in ha-term.bin → zeroes state safely (no stale data).
|
||||
* Req 9.1: write+rename atomicity — .tmp file cleaned up; ha-term.bin consistent.
|
||||
*
|
||||
* Functions under test:
|
||||
* ha_persist_term() — already implemented in ha.c (task 4.2)
|
||||
* ha_restore_term() — NOT YET IMPLEMENTED; declared below to force linker error
|
||||
*
|
||||
* RED phase proof: ha_restore_term() does not exist in ha.c. The linker will
|
||||
* fail with "undefined reference to ha_restore_term" — this is the expected
|
||||
* RED-phase outcome for task 6.1. Task 6.2 adds the implementation.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <zlib.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "ha.h"
|
||||
#include "config.h" /* XCE_MAXNAME_LEN */
|
||||
|
||||
/* ── On-disk format (mirrors ha.c internals, duplicated here for white-box tests) ─
|
||||
*
|
||||
* This struct matches ha_term_file_t in ha.c. It is intentionally duplicated
|
||||
* here (not exported via ha.h) so the tests can construct and inspect raw file
|
||||
* contents without coupling ha.h to internal storage details.
|
||||
*/
|
||||
#define HA_TERM_MAGIC_TEST 0xDEADBEEFCAFEBABEULL
|
||||
|
||||
typedef struct {
|
||||
uint64_t magic;
|
||||
uint64_t term;
|
||||
uint64_t voted_for_term;
|
||||
char voted_for_name[XCE_MAXNAME_LEN];
|
||||
uint32_t crc32;
|
||||
} ha_term_file_test_t;
|
||||
|
||||
/* ── RED phase declaration ────────────────────────────────────────────────────
|
||||
*
|
||||
* ha_restore_term: read {workdir}/ha-term.bin and restore ha->term and
|
||||
* ha->voted_for_* if the file exists and passes CRC verification.
|
||||
*
|
||||
* On missing file: no-op (term stays 0, voted_for_name stays empty).
|
||||
* On CRC mismatch: log WARN, zero ha->term and ha->voted_for_* for safety.
|
||||
* On valid file: atomically store ha->term; copy voted_for_term/name.
|
||||
*
|
||||
* Returns 0 on success (including "file not found" and "CRC bad → zeroed"),
|
||||
* -errno on unexpected I/O errors.
|
||||
*
|
||||
* This declaration exists only to make the tests compile; the linker will
|
||||
* fail with "undefined reference to ha_restore_term" until task 6.2 lands.
|
||||
*/
|
||||
int ha_restore_term(ha_state_t *ha, const char *workdir);
|
||||
|
||||
/* ── Shared minimal ha_config_t ───────────────────────────────────────────── */
|
||||
|
||||
static ha_config_t g_durability_cfg = {
|
||||
.heartbeat_interval_ms = 3000,
|
||||
.heartbeat_timeout_ms = 10000,
|
||||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
|
||||
/* ── Helper: compute CRC32 the same way ha_persist_term() does ──────────────
|
||||
*
|
||||
* CRC covers all bytes before the crc32 field (i.e. offsetof(…, crc32)).
|
||||
*/
|
||||
static uint32_t compute_term_crc(const ha_term_file_test_t *f)
|
||||
{
|
||||
size_t crc_len = offsetof(ha_term_file_test_t, crc32);
|
||||
return (uint32_t)crc32(0L, (const Bytef *)f, (uInt)crc_len);
|
||||
}
|
||||
|
||||
/* ── Helper: write an ha_term_file_test_t directly to {dir}/ha-term.bin ──── */
|
||||
|
||||
static void write_term_file(const char *dir, const ha_term_file_test_t *f)
|
||||
{
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/ha-term.bin", dir);
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
TEST_ASSERT_MESSAGE(fd >= 0, "Failed to open ha-term.bin for writing");
|
||||
ssize_t written = write(fd, f, sizeof(*f));
|
||||
close(fd);
|
||||
TEST_ASSERT_EQUAL_size_t(sizeof(*f), (size_t)written);
|
||||
}
|
||||
|
||||
/* ── Test 1: ha_persist_term() writes ha-term.bin with correct content ──────
|
||||
*
|
||||
* Req 9.1: persist must be called before reporting a vote grant or term increment.
|
||||
* We set up an ha_state_t, call ha_persist_term(), then read back and verify:
|
||||
* - file exists
|
||||
* - magic == HA_TERM_MAGIC
|
||||
* - term and voted_for fields match what was in ha_state_t
|
||||
* - CRC32 is valid
|
||||
*/
|
||||
static void test_persist_term_writes_file(void)
|
||||
{
|
||||
char tmpdir[] = "/tmp/test_ha_durXXXXXX";
|
||||
char *dir = mkdtemp(tmpdir);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(dir, "mkdtemp failed");
|
||||
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_durability_cfg, 0);
|
||||
|
||||
/* Set up some state to persist */
|
||||
atomic_store_explicit(&ha.term, 7ULL, memory_order_release);
|
||||
ha.voted_for_term = 7;
|
||||
strncpy(ha.voted_for_name, "ctrl-node-2", sizeof(ha.voted_for_name) - 1);
|
||||
ha.voted_for_name[sizeof(ha.voted_for_name) - 1] = '\0';
|
||||
|
||||
ha_persist_term(&ha, dir);
|
||||
|
||||
/* Verify the file exists */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/ha-term.bin", dir);
|
||||
struct stat st;
|
||||
int stat_rc = stat(path, &st);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, stat_rc, "ha-term.bin should exist after persist");
|
||||
TEST_ASSERT_EQUAL_size_t(sizeof(ha_term_file_test_t), (size_t)st.st_size);
|
||||
|
||||
/* Read back and verify content */
|
||||
ha_term_file_test_t f;
|
||||
memset(&f, 0, sizeof(f));
|
||||
int fd = open(path, O_RDONLY);
|
||||
TEST_ASSERT_MESSAGE(fd >= 0, "Cannot open ha-term.bin for reading");
|
||||
ssize_t n = read(fd, &f, sizeof(f));
|
||||
close(fd);
|
||||
TEST_ASSERT_EQUAL_size_t(sizeof(f), (size_t)n);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(HA_TERM_MAGIC_TEST, f.magic);
|
||||
TEST_ASSERT_EQUAL_UINT64(7ULL, f.term);
|
||||
TEST_ASSERT_EQUAL_UINT64(7ULL, f.voted_for_term);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-node-2", f.voted_for_name);
|
||||
|
||||
/* CRC must be valid */
|
||||
uint32_t expected_crc = compute_term_crc(&f);
|
||||
TEST_ASSERT_EQUAL_UINT32(expected_crc, f.crc32);
|
||||
|
||||
/* tmp file must NOT exist (rename succeeded) */
|
||||
char tmp_path[1040];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s/ha-term.bin.tmp", dir);
|
||||
TEST_ASSERT_EQUAL_INT(-1, stat(tmp_path, &st));
|
||||
|
||||
ha_fini(&ha);
|
||||
|
||||
/* cleanup */
|
||||
unlink(path);
|
||||
rmdir(dir);
|
||||
|
||||
printf("test_persist_term_writes_file: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 2: ha_restore_term() restores term and voted_for from valid file ──
|
||||
*
|
||||
* Req 9.2: startup must restore the most recently persisted term and voted_for
|
||||
* record before participating in any election.
|
||||
*
|
||||
* This test writes a valid ha_term_file_test_t directly to disk (bypassing
|
||||
* ha_persist_term — this exercises the read path in isolation), then calls
|
||||
* ha_restore_term(). Verification: ha->term and ha->voted_for_name match
|
||||
* what was written.
|
||||
*
|
||||
* RED PHASE: this test will FAIL at link time because ha_restore_term()
|
||||
* does not exist in ha.c. This is intentional — linker error proves RED.
|
||||
*/
|
||||
static void test_ha_restore_term_from_file(void)
|
||||
{
|
||||
char tmpdir[] = "/tmp/test_ha_durXXXXXX";
|
||||
char *dir = mkdtemp(tmpdir);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(dir, "mkdtemp failed");
|
||||
|
||||
/* Build a valid ha_term_file_test_t and write it directly */
|
||||
ha_term_file_test_t f;
|
||||
memset(&f, 0, sizeof(f));
|
||||
f.magic = HA_TERM_MAGIC_TEST;
|
||||
f.term = 42ULL;
|
||||
f.voted_for_term = 42ULL;
|
||||
strncpy(f.voted_for_name, "ctrl-node-1", sizeof(f.voted_for_name) - 1);
|
||||
f.voted_for_name[sizeof(f.voted_for_name) - 1] = '\0';
|
||||
f.crc32 = compute_term_crc(&f);
|
||||
write_term_file(dir, &f);
|
||||
|
||||
/* Init an ha_state_t (starts at term=0) then restore */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_durability_cfg, 0);
|
||||
|
||||
/* Req 9.2: ha_restore_term() should set ha.term and ha.voted_for_* */
|
||||
int rc = ha_restore_term(&ha, dir); /* <-- undefined reference: RED proof */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "ha_restore_term must return 0 on valid file");
|
||||
|
||||
/* After restore, term and voted_for must match the file */
|
||||
uint64_t restored_term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_UINT64(42ULL, restored_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(42ULL, ha.voted_for_term);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-node-1", ha.voted_for_name);
|
||||
|
||||
ha_fini(&ha);
|
||||
|
||||
/* cleanup */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/ha-term.bin", dir);
|
||||
unlink(path);
|
||||
rmdir(dir);
|
||||
|
||||
printf("test_ha_restore_term_from_file: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 3: Missing ha-term.bin → term=0, voted_for empty (first-boot) ─────
|
||||
*
|
||||
* Req 9.3: on first boot where no ha-term.bin exists, the system shall
|
||||
* initialize the term to zero with no prior vote recorded.
|
||||
*
|
||||
* This test verifies the "file not found" branch of ha_restore_term().
|
||||
* It may compile to a link error (RED) even before we can test the logic.
|
||||
*/
|
||||
static void test_ha_restore_term_zero_on_missing_file(void)
|
||||
{
|
||||
/* Use a tmpdir with no ha-term.bin in it */
|
||||
char tmpdir[] = "/tmp/test_ha_durXXXXXX";
|
||||
char *dir = mkdtemp(tmpdir);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(dir, "mkdtemp failed");
|
||||
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_durability_cfg, 0);
|
||||
|
||||
/* No file written — restore should be a safe no-op */
|
||||
int rc = ha_restore_term(&ha, dir); /* <-- undefined reference: RED proof */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "ha_restore_term must return 0 on missing file");
|
||||
|
||||
/* term must be 0, voted_for_name must be empty */
|
||||
uint64_t term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0ULL, term, "term must be 0 on first boot");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, ha.voted_for_name[0],
|
||||
"voted_for_name must be empty on first boot");
|
||||
|
||||
ha_fini(&ha);
|
||||
rmdir(dir);
|
||||
|
||||
printf("test_ha_restore_term_zero_on_missing_file: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 4: CRC mismatch → zeroes state safely ─────────────────────────────
|
||||
*
|
||||
* Req 9.2: a corrupted ha-term.bin must not restore stale state. The system
|
||||
* must log WARN and zero ha->term and ha->voted_for_*.
|
||||
*
|
||||
* We write a file with term=99 but deliberately corrupt the CRC field.
|
||||
* After ha_restore_term(), term must be 0 and voted_for_name must be empty.
|
||||
*/
|
||||
static void test_ha_restore_crc_mismatch_zeroes_state(void)
|
||||
{
|
||||
char tmpdir[] = "/tmp/test_ha_durXXXXXX";
|
||||
char *dir = mkdtemp(tmpdir);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(dir, "mkdtemp failed");
|
||||
|
||||
/* Build a file with valid structure but wrong CRC */
|
||||
ha_term_file_test_t f;
|
||||
memset(&f, 0, sizeof(f));
|
||||
f.magic = HA_TERM_MAGIC_TEST;
|
||||
f.term = 99ULL;
|
||||
f.voted_for_term = 99ULL;
|
||||
strncpy(f.voted_for_name, "ctrl-node-3", sizeof(f.voted_for_name) - 1);
|
||||
f.voted_for_name[sizeof(f.voted_for_name) - 1] = '\0';
|
||||
f.crc32 = 0xDEAD1234U; /* deliberately wrong CRC */
|
||||
write_term_file(dir, &f);
|
||||
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_durability_cfg, 0);
|
||||
|
||||
/* ha_restore_term must detect CRC mismatch and zero state */
|
||||
int rc = ha_restore_term(&ha, dir); /* <-- undefined reference: RED proof */
|
||||
/* Return value 0 is acceptable: "handled gracefully, no fatal error" */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "ha_restore_term must return 0 on CRC mismatch");
|
||||
|
||||
/* State must be zeroed — no stale data restored */
|
||||
uint64_t term = atomic_load_explicit(&ha.term, memory_order_acquire);
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0ULL, term,
|
||||
"term must be 0 after CRC mismatch");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, ha.voted_for_name[0],
|
||||
"voted_for_name must be empty after CRC mismatch");
|
||||
|
||||
ha_fini(&ha);
|
||||
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/ha-term.bin", dir);
|
||||
unlink(path);
|
||||
rmdir(dir);
|
||||
|
||||
printf("test_ha_restore_crc_mismatch_zeroes_state: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test 5: write+rename atomicity — .tmp cleaned up; final file consistent ─
|
||||
*
|
||||
* Req 9.1: ha_persist_term uses write-to-tmp + rename for atomic durability.
|
||||
* Verify:
|
||||
* a) After writing term=1, ha-term.bin contains term=1.
|
||||
* b) After writing term=2 over it, ha-term.bin contains term=2.
|
||||
* c) The .tmp file does not exist after either call (rename succeeded).
|
||||
*
|
||||
* This test does NOT simulate a partial write (that would require filesystem
|
||||
* hooks not available in unit tests). Instead it verifies the postcondition:
|
||||
* the final ha-term.bin is consistent and no .tmp artifact is left behind.
|
||||
*/
|
||||
static void test_persist_atomicity_tmp_cleaned_up(void)
|
||||
{
|
||||
char tmpdir[] = "/tmp/test_ha_durXXXXXX";
|
||||
char *dir = mkdtemp(tmpdir);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(dir, "mkdtemp failed");
|
||||
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_durability_cfg, 0);
|
||||
|
||||
/* -- Round 1: persist term=1 -- */
|
||||
atomic_store_explicit(&ha.term, 1ULL, memory_order_release);
|
||||
ha.voted_for_term = 1;
|
||||
strncpy(ha.voted_for_name, "ctrl-node-a", sizeof(ha.voted_for_name) - 1);
|
||||
ha.voted_for_name[sizeof(ha.voted_for_name) - 1] = '\0';
|
||||
ha_persist_term(&ha, dir);
|
||||
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/ha-term.bin", dir);
|
||||
char tmp_path[1040];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s/ha-term.bin.tmp", dir);
|
||||
|
||||
/* ha-term.bin must exist and contain term=1 */
|
||||
ha_term_file_test_t f1;
|
||||
int fd = open(path, O_RDONLY);
|
||||
TEST_ASSERT_MESSAGE(fd >= 0, "ha-term.bin should exist after first persist");
|
||||
read(fd, &f1, sizeof(f1));
|
||||
close(fd);
|
||||
TEST_ASSERT_EQUAL_UINT64(1ULL, f1.term);
|
||||
TEST_ASSERT_EQUAL_UINT32(compute_term_crc(&f1), f1.crc32);
|
||||
|
||||
/* .tmp must not exist */
|
||||
struct stat st;
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-1, stat(tmp_path, &st),
|
||||
"ha-term.bin.tmp must not exist after successful persist");
|
||||
|
||||
/* -- Round 2: persist term=2 over existing file -- */
|
||||
atomic_store_explicit(&ha.term, 2ULL, memory_order_release);
|
||||
ha.voted_for_term = 2;
|
||||
strncpy(ha.voted_for_name, "ctrl-node-b", sizeof(ha.voted_for_name) - 1);
|
||||
ha.voted_for_name[sizeof(ha.voted_for_name) - 1] = '\0';
|
||||
ha_persist_term(&ha, dir);
|
||||
|
||||
/* ha-term.bin must now contain term=2 */
|
||||
ha_term_file_test_t f2;
|
||||
fd = open(path, O_RDONLY);
|
||||
TEST_ASSERT_MESSAGE(fd >= 0, "ha-term.bin should still exist after second persist");
|
||||
read(fd, &f2, sizeof(f2));
|
||||
close(fd);
|
||||
TEST_ASSERT_EQUAL_UINT64(2ULL, f2.term);
|
||||
TEST_ASSERT_EQUAL_STRING("ctrl-node-b", f2.voted_for_name);
|
||||
TEST_ASSERT_EQUAL_UINT32(compute_term_crc(&f2), f2.crc32);
|
||||
|
||||
/* .tmp still must not exist */
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-1, stat(tmp_path, &st),
|
||||
"ha-term.bin.tmp must not exist after second persist");
|
||||
|
||||
ha_fini(&ha);
|
||||
unlink(path);
|
||||
rmdir(dir);
|
||||
|
||||
printf("test_persist_atomicity_tmp_cleaned_up: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test suite entry point ───────────────────────────────────────────────── */
|
||||
|
||||
void test_ha_durability(void)
|
||||
{
|
||||
/* Req 9.1: persist writes a correct, CRC-valid file */
|
||||
RUN_TEST(test_persist_term_writes_file);
|
||||
|
||||
/* Req 9.2: restore reads back term and voted_for — RED: linker error expected */
|
||||
RUN_TEST(test_ha_restore_term_from_file);
|
||||
|
||||
/* Req 9.3: missing file → safe zero init */
|
||||
RUN_TEST(test_ha_restore_term_zero_on_missing_file);
|
||||
|
||||
/* Req 9.2: CRC mismatch → safe zero init (no stale state) */
|
||||
RUN_TEST(test_ha_restore_crc_mismatch_zeroes_state);
|
||||
|
||||
/* Req 9.1: write+rename atomicity — no .tmp artifact left */
|
||||
RUN_TEST(test_persist_atomicity_tmp_cleaned_up);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue