The codebase conforms strictly to C99 and POSIX.1-2001. All code and tests must be able to compile and run without depending on GNU-specific extensions or non-POSIX compliant command line tools. Exempt from this rule are tools used only during development. Like GNU Make, clang-format, or compiler-specific pragmas within ifdef statements for testing. The goal is to allow users to compile the codebase during an emergency on a bare POSIX system without having to wrangle with dependencies.
The ./scripts/
directory contains helper scripts for replicating what the
CI jobs do locally. To generate a compilation database for tools like
clangd, run this script:
./scripts/generate-compile-commands-json.sh
Backups are atomic by design. A crash, like a segfault, will leave the backup as it was before. This simplifies a lot of things, like error handling.
Every non-recoverable error, or any other situation which prevents a backup
from completing reliably, is handled by calling die()
or dieErrno()
.
Those calls will cleanup all currently held resources and free all
associated memory.
#include "error-handling.h"
if(some_function() == -1)
{
die("failed to call some function");
}
if(posix_function() == -1)
{
/* This will print an explanation of the current errno value. */
dieErrno("failed to foobar");
}
Memory is managed via regions, which get freed automatically when the program exits or terminates with an error.
#include "CRegion/region.h"
CR_Region *r = CR_RegionNew();
char *data = CR_RegionAlloc(r, 1024); /* Will call die() on failure. */
CR_RegionRelease(r); /* If omitted, it will be released trough atexit() */
Callbacks can be attached to regions and will be called when the region gets released:
void cleanup(void *data) { ... }
Foo *foo = fooNew();
CR_RegionAttach(r, cleanup, foo);
Generic wrappers around regions can be found in ./src/allocator.h.
Strings are immutable slices which don't own the memory they point to:
#include "str.h"
StringView path = str("/etc/conf.d/boot.conf");
StringView dirname = strSplitPath(path).head;
/* String views may contain content which is not null-terminated. Use the
function strGetContent() to get a terminated C string. It will copy and
terminate the given StringView if required. */
Allocator *a = ...;
raw_c_function(strGetContent(dirname, a));
The source code comes with its own testing framework. Here an example test program:
#include "test.h"
int main(void)
{
testGroupStart("some asserts");
assert_true(5 + 5 == 10);
/* Calls to die() can be tested like this: */
assert_error(sMalloc(0), "unable to allocate 0 bytes");
testGroupEnd();
}
The example test above must be stored in the test directory, e.g.
"test/foo.c"
. Now "foo" must be added to "test/run-tests.sh"
. This has
to be done manually to ensure that tests run in the correct order.
Full program tests are located in "test/full program tests/CATEGORY/NAME/"
. The directory "NAME/"
can contain the scripts
"init.sh"
, "run.sh"
and "clean.sh"
. These scripts are run inside
"NAME/"
by being passed to "/bin/sh -e"
. Thus every failing command
causes the entire test to fail. If one of these scripts doesn't exist, a
fallback alternative will be used. These are located in "test/full program tests/fallback targets/"
.
From inside these scripts the following variables are accessible:
Variable | Description |
---|---|
NB | The path to the nb executable. |
PROJECT_PATH | The path to the projects root directory which contains the Makefile, etc. |
PHASE_PATH | The path to the current phase relative to "NAME/" . Defaults to "." . |
It is usually not required to implement "init.sh"
, "run.sh"
or
"clean.sh"
. The fallback scripts should be flexible enough and can be
complemented with scripts like "pre-run.sh"
or "post-init.sh"
, etc. The
fallback scripts will create the directory "generated/"
, where all the
data generated during the test should be stored. Additionally the directory
"generated/repo/"
will be created and contains the repository to test.
The fallback scripts must be configured by creating one of the following
files inside "NAME/"
:
File name | Description |
---|---|
expected-output | The expected program output with paths relative to "NAME/" . |
expected-output-test-data | The expected program output with paths relative to "$PROJECT_PATH/test/data" . |
Additionally the following optional files can be created inside "NAME/"
:
File name | Description |
---|---|
input | This files content gets piped into nb. Defaults to "yes". |
arguments | This files content gets passed to nb as argument. Defaults to "generated/repo". |
exit-status | The expected exit status of nb. Defaults to 0. |
config | The config file to use with paths relative to "NAME/" . |
config-test-data | The config file to use with paths relative to "$PROJECT_PATH/test/data" . |
expected-repo-files | A list of files expected to be inside the repository after the test. Paths are relative to "generated/repo/" . |
To test multiple backups using the same repository, the test must be broken
down into phases. This can be done by moving all the files described above
into a directory named "1/"
. The 1 stands for the first phase. Additional
directories named "2/"
, "3/"
, etc. can be created. Each phase will
reuse the following files and directories from preceding phases, if they
are not overridden:
- generated/
- generated/repo/
- config
- config-test-data
- expected-output
- expected-output-test-data
- expected-repo-files
The following files are specific to each phase and will not be reused from preceding phases. If they are not provided, their defaults will be used:
- input
- arguments
- exit-status
Note: The scripts "pre-clean.sh"
, "clean.sh"
and "post-clean.sh"
will be run only before the first phase and after the last phase.