From: Justin Husted Date: Sun, 3 Nov 2019 07:35:03 +0000 (-0700) Subject: Initial version of bcachefs tests. X-Git-Url: https://git.sesse.net/?a=commitdiff_plain;h=61bc316a4da4831d8812eb5051732cca27652d8d;p=bcachefs-tools-debian Initial version of bcachefs tests. So far, these tests just test basic format, fsck, and list functions under valgrind, as well as a few self-validation tests. Signed-off-by: Justin Husted --- diff --git a/Makefile b/Makefile index 2ec19e7..3c3db9b 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ 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 \ @@ -65,12 +66,18 @@ endif .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)) @@ -97,7 +104,7 @@ install: bcachefs .PHONY: clean clean: - $(RM) bcachefs .version $(OBJS) $(DEPS) + $(RM) bcachefs tests/test_helper .version $(OBJS) $(DEPS) .PHONY: deb deb: all diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..9cd7b2f --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,68 @@ +#!/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) diff --git a/tests/test_fixture.py b/tests/test_fixture.py new file mode 100644 index 0000000..8dfb392 --- /dev/null +++ b/tests/test_fixture.py @@ -0,0 +1,53 @@ +#!/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) diff --git a/tests/test_helper.c b/tests/test_helper.c new file mode 100644 index 0000000..c7604f0 --- /dev/null +++ b/tests/test_helper.c @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include +#include + +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 \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; +} diff --git a/tests/test_helper_trick.c b/tests/test_helper_trick.c new file mode 100644 index 0000000..8bc11fd --- /dev/null +++ b/tests/test_helper_trick.c @@ -0,0 +1,8 @@ +/* + * Prevent compiler from optimizing away a variable by referencing it from + * another compilation unit. + */ +void +trick_compiler(int *x) +{ +} diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..6eea103 --- /dev/null +++ b/tests/util.py @@ -0,0 +1,71 @@ +#!/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)