PREFIX?=/usr/local
PKG_CONFIG?=pkg-config
INSTALL=install
+PYTEST=pytest-3
CFLAGS+=-std=gnu89 -O2 -g -MMD -Wall \
-Wno-pointer-sign \
-fno-strict-aliasing \
.PHONY: all
all: bcachefs
+.PHONY: check
+check: tests/test_helper bcachefs
+ cd tests; $(PYTEST)
+
SRCS=$(shell find . -type f -iname '*.c')
DEPS=$(SRCS:.c=.d)
-include $(DEPS)
OBJS=$(SRCS:.c=.o)
-bcachefs: $(OBJS)
+bcachefs: $(filter-out ./tests/%.o, $(OBJS))
+
+tests/test_helper: $(filter ./tests/%.o, $(OBJS))
# If the version string differs from the last build, update the last version
ifneq ($(VERSION),$(shell cat .version 2>/dev/null))
.PHONY: clean
clean:
- $(RM) bcachefs .version $(OBJS) $(DEPS)
+ $(RM) bcachefs tests/test_helper .version $(OBJS) $(DEPS)
.PHONY: deb
deb: all
--- /dev/null
+#!/usr/bin/python3
+#
+# Basic bcachefs functionality tests.
+
+import re
+import util
+
+def test_help():
+ ret = util.run_bch(valgrind=True)
+
+ assert ret.returncode == 1
+ assert "missing command" in ret.stdout
+ assert len(ret.stderr) == 0
+
+def test_format(tmpdir):
+ dev = util.device_1g(tmpdir)
+ ret = util.run_bch('format', dev, valgrind=True)
+
+ assert ret.returncode == 0
+ assert len(ret.stdout) > 0
+ assert len(ret.stderr) == 0
+
+def test_fsck(tmpdir):
+ dev = util.device_1g(tmpdir)
+ util.run_bch('format', dev, valgrind=False, check=True)
+
+ ret = util.run_bch('fsck', dev, valgrind=True)
+
+ assert ret.returncode == 0
+ assert len(ret.stdout) > 0
+ assert len(ret.stderr) == 0
+
+def test_list(tmpdir):
+ dev = util.device_1g(tmpdir)
+ util.run_bch('format', dev, valgrind=False, check=True)
+
+ ret = util.run_bch('list', dev, valgrind=True)
+
+ assert ret.returncode == 0
+ assert len(ret.stderr) == 0
+ assert "recovering from clean shutdown" in ret.stdout
+ assert len(ret.stdout.splitlines()) == 2
+
+def test_list_inodes(tmpdir):
+ dev = util.device_1g(tmpdir)
+ util.run_bch('format', dev, valgrind=False, check=True)
+
+ ret = util.run_bch('list', '-b', 'inodes', dev, valgrind=True)
+
+ assert ret.returncode == 0
+ assert len(ret.stderr) == 0
+ assert len(ret.stdout.splitlines()) == (2 + 2) # 2 inodes on clean format
+
+def test_list_dirent(tmpdir):
+ dev = util.device_1g(tmpdir)
+ util.run_bch('format', dev, valgrind=False, check=True)
+
+ ret = util.run_bch('list', '-b', 'dirents', dev, valgrind=True)
+
+ assert ret.returncode == 0
+ assert len(ret.stderr) == 0
+ assert len(ret.stdout.splitlines()) == (2 + 1) # 1 dirent
+
+ # Example:
+ # u64s 8 type dirent 4096:2449855786607753081
+ # snap 0 len 0 ver 0: lost+found -> 4097
+ last = ret.stdout.splitlines()[-1]
+ assert re.match(r'^.*type dirent.*: lost\+found ->.*$', last)
--- /dev/null
+#!/usr/bin/python3
+#
+# Tests of the functions in util.py
+
+import pytest
+import signal
+import subprocess
+
+import util
+from pathlib import Path
+
+#helper = Path('.') / 'test_helper'
+helper = './test_helper'
+
+def test_sparse_file(tmpdir):
+ dev = util.sparse_file(tmpdir / '1k', 1024)
+ assert dev.stat().st_size == 1024
+
+def test_device_1g(tmpdir):
+ dev = util.device_1g(tmpdir)
+ assert dev.stat().st_size == 1024**3
+
+def test_abort():
+ ret = util.run(helper, 'abort')
+ assert ret.returncode == -signal.SIGABRT
+
+def test_segfault():
+ ret = util.run(helper, 'segfault')
+ assert ret.returncode == -signal.SIGSEGV
+
+def test_check():
+ with pytest.raises(subprocess.CalledProcessError):
+ ret = util.run(helper, 'abort', check=True)
+
+def test_leak():
+ with pytest.raises(util.ValgrindFailedError):
+ ret = util.run(helper, 'leak', valgrind=True)
+
+def test_undefined():
+ with pytest.raises(util.ValgrindFailedError):
+ ret = util.run(helper, 'undefined', valgrind=True)
+
+def test_undefined_branch():
+ with pytest.raises(util.ValgrindFailedError):
+ ret = util.run(helper, 'undefined_branch', valgrind=True)
+
+def test_read_after_free():
+ with pytest.raises(util.ValgrindFailedError):
+ ret = util.run(helper, 'read_after_free', valgrind=True)
+
+def test_write_after_free():
+ with pytest.raises(util.ValgrindFailedError):
+ ret = util.run(helper, 'write_after_free', valgrind=True)
--- /dev/null
+#include <assert.h>
+#include <malloc.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void trick_compiler(int *x);
+
+static void test_abort(void)
+{
+ abort();
+}
+
+static void test_segfault(void)
+{
+ raise(SIGSEGV);
+}
+
+static void test_leak(void)
+{
+ int *p = malloc(sizeof *p);
+ trick_compiler(p);
+}
+
+static void test_undefined(void)
+{
+ int *p = malloc(1);
+ printf("%d\n", *p);
+}
+
+static void test_undefined_branch(void)
+{
+ int x;
+ trick_compiler(&x);
+
+ if (x)
+ printf("1\n");
+ else
+ printf("0\n");
+}
+
+static void test_read_after_free(void)
+{
+ int *p = malloc(sizeof *p);
+ free(p);
+
+ printf("%d\n", *p);
+}
+
+static void test_write_after_free(void)
+{
+ int *p = malloc(sizeof *p);
+ free(p);
+
+ printf("%d\n", *p);
+}
+
+typedef void (*test_fun)(void);
+
+struct test {
+ const char *name;
+ test_fun fun;
+};
+
+#define TEST(f) { .name = #f, .fun = test_##f, }
+static struct test tests[] = {
+ TEST(abort),
+ TEST(segfault),
+ TEST(leak),
+ TEST(undefined),
+ TEST(undefined_branch),
+ TEST(read_after_free),
+ TEST(write_after_free),
+};
+#define ntests (sizeof tests / sizeof *tests)
+
+int main(int argc, char *argv[])
+{
+ int i;
+
+ if (argc != 2) {
+ fprintf(stderr, "Usage: test_helper <test>\n");
+ exit(1);
+ }
+
+ bool found = false;
+ for (i = 0; i < ntests; ++i)
+ if (!strcmp(argv[1], tests[i].name)) {
+ found = true;
+ printf("Running test: %s\n", tests[i].name);
+ tests[i].fun();
+ break;
+ }
+
+ if (!found) {
+ fprintf(stderr, "Unable to find test: %s\n", argv[1]);
+ exit(1);
+ }
+
+ return 0;
+}
--- /dev/null
+/*
+ * Prevent compiler from optimizing away a variable by referencing it from
+ * another compilation unit.
+ */
+void
+trick_compiler(int *x)
+{
+}
--- /dev/null
+#!/usr/bin/python3
+
+import os
+import re
+import subprocess
+import tempfile
+from pathlib import Path
+
+DIR = Path('..')
+BCH_PATH = DIR / 'bcachefs'
+
+VPAT = re.compile(r'ERROR SUMMARY: (\d+) errors from (\d+) contexts')
+
+class ValgrindFailedError(Exception):
+ def __init__(self, log):
+ self.log = log
+
+def check_valgrind(logfile):
+ log = logfile.read().decode('utf-8')
+ m = VPAT.search(log)
+ assert m is not None, 'Internal error: valgrind log did not match.'
+
+ errors = int(m.group(1))
+ if errors > 0:
+ raise ValgrindFailedError(log)
+
+def run(cmd, *args, valgrind=False, check=False):
+ """Run an external program via subprocess, optionally with valgrind.
+
+ This subprocess wrapper will capture the stdout and stderr. If valgrind is
+ requested, it will be checked for errors and raise a
+ ValgrindFailedError if there's a problem.
+ """
+ cmds = [cmd] + list(args)
+
+ if valgrind:
+ vout = tempfile.NamedTemporaryFile()
+ vcmd = ['valgrind',
+ '--leak-check=full',
+ '--log-file={}'.format(vout.name)]
+ cmds = vcmd + cmds
+
+ print("Running '{}'".format(cmds))
+ res = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ encoding='utf-8', check=check)
+
+ if valgrind:
+ check_valgrind(vout)
+
+ return res
+
+def run_bch(*args, **kwargs):
+ """Wrapper to run the bcachefs binary specifically."""
+ cmds = [BCH_PATH] + list(args)
+ return run(*cmds, **kwargs)
+
+def sparse_file(lpath, size):
+ """Construct a sparse file of the specified size.
+
+ This is typically used to create device files for bcachefs.
+ """
+ path = Path(lpath)
+ f = path.touch(mode = 0o600, exist_ok = False)
+ os.truncate(path, size)
+
+ return path
+
+def device_1g(tmpdir):
+ """Default 1g sparse file for use with bcachefs."""
+ path = tmpdir / 'dev-1g'
+ return sparse_file(path, 1024**3)