]> git.sesse.net Git - bcachefs-tools-debian/commitdiff
Initial version of bcachefs tests.
authorJustin Husted <sigstop@gmail.com>
Sun, 3 Nov 2019 07:35:03 +0000 (00:35 -0700)
committerKent Overstreet <kent.overstreet@gmail.com>
Mon, 4 Nov 2019 04:17:43 +0000 (23:17 -0500)
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 <sigstop@gmail.com>
Makefile
tests/test_basic.py [new file with mode: 0644]
tests/test_fixture.py [new file with mode: 0644]
tests/test_helper.c [new file with mode: 0644]
tests/test_helper_trick.c [new file with mode: 0644]
tests/util.py [new file with mode: 0644]

index 2ec19e7fce651cecd19864c466ca5d00f9c2c26c..3c3db9bc13c915d89092971db22de3954ce0f2df 100644 (file)
--- 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 (file)
index 0000000..9cd7b2f
--- /dev/null
@@ -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 (file)
index 0000000..8dfb392
--- /dev/null
@@ -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 (file)
index 0000000..c7604f0
--- /dev/null
@@ -0,0 +1,103 @@
+#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;
+}
diff --git a/tests/test_helper_trick.c b/tests/test_helper_trick.c
new file mode 100644 (file)
index 0000000..8bc11fd
--- /dev/null
@@ -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 (file)
index 0000000..6eea103
--- /dev/null
@@ -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)