diff --git a/borg/archiver.py b/borg/archiver.py index afe9162f4..5729f51f5 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -34,6 +34,7 @@ from .constants import * # NOQA from .key import key_creator, RepoKey, PassphraseKey from .archive import Archive, ArchiveChecker, ArchiveRecreater from .remote import RepositoryServer, RemoteRepository, cache_if_remote +from .selftest import selftest from .hashindex import ChunkIndexEntry has_lchflags = hasattr(os, 'lchflags') @@ -1924,13 +1925,17 @@ class Archiver: update_excludes(args) return args + def prerun_checks(self, logger): + check_extension_modules() + selftest(logger) + def run(self, args): os.umask(args.umask) # early, before opening files self.lock_wait = args.lock_wait setup_logging(level=args.log_level, is_serve=args.func == self.do_serve) # do not use loggers before this! if args.show_version: logger.info('borgbackup version %s' % __version__) - check_extension_modules() + self.prerun_checks(logger) if is_slow_msgpack(): logger.warning("Using a pure-python msgpack! This will result in lower performance.") return args.func(args) diff --git a/borg/crypto.pyx b/borg/crypto.pyx index 8bee39fe4..16f1cda82 100644 --- a/borg/crypto.pyx +++ b/borg/crypto.pyx @@ -5,7 +5,7 @@ This could be replaced by PyCrypto maybe? from libc.stdlib cimport malloc, free from cpython.buffer cimport PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release -API_VERSION = 2 +API_VERSION = 3 cdef extern from "openssl/rand.h": diff --git a/borg/helpers.py b/borg/helpers.py index b134ba86b..395e078e0 100644 --- a/borg/helpers.py +++ b/borg/helpers.py @@ -80,7 +80,7 @@ def check_extension_modules(): raise ExtensionModuleError if chunker.API_VERSION != 2: raise ExtensionModuleError - if crypto.API_VERSION != 2: + if crypto.API_VERSION != 3: raise ExtensionModuleError if platform.API_VERSION != 2: raise ExtensionModuleError diff --git a/borg/selftest.py b/borg/selftest.py new file mode 100644 index 000000000..2093b89a9 --- /dev/null +++ b/borg/selftest.py @@ -0,0 +1,79 @@ +""" +Self testing module +=================== + +The selftest() function runs a small test suite of relatively fast tests that are meant to discover issues +with the way Borg was compiled or packaged and also bugs in Borg itself. + +Theses tests are a subset of the borg/testsuite and are run with Pythons built-in unittest, hence none of +the tests used for this can or should be ported to py.test currently. + +To assert that self test discovery works correctly the number of tests is kept in the SELFTEST_COUNT +variable. SELFTEST_COUNT must be updated if new tests are added or removed to or from any of the tests +used here. +""" + + +import sys +import time +from unittest import TestResult, TestSuite, defaultTestLoader + +from .testsuite.hashindex import HashIndexDataTestCase, HashIndexRefcountingTestCase, HashIndexTestCase +from .testsuite.crypto import CryptoTestCase +from .testsuite.chunker import ChunkerTestCase + +SELFTEST_CASES = [ + HashIndexDataTestCase, + HashIndexRefcountingTestCase, + HashIndexTestCase, + CryptoTestCase, + ChunkerTestCase, +] + +SELFTEST_COUNT = 27 + + +class SelfTestResult(TestResult): + def __init__(self): + super().__init__() + self.successes = [] + + def addSuccess(self, test): + super().addSuccess(test) + self.successes.append(test) + + def test_name(self, test): + return test.shortDescription() or str(test) + + def log_results(self, logger): + for test, failure in self.errors + self.failures + self.unexpectedSuccesses: + logger.error('self test %s FAILED:\n%s', self.test_name(test), failure) + for test, reason in self.skipped: + logger.warning('self test %s skipped: %s', self.test_name(test), reason) + + def successful_test_count(self): + return len(self.successes) + + +def selftest(logger): + selftest_started = time.perf_counter() + result = SelfTestResult() + test_suite = TestSuite() + for test_case in SELFTEST_CASES: + test_suite.addTest(defaultTestLoader.loadTestsFromTestCase(test_case)) + test_suite.run(result) + result.log_results(logger) + successful_tests = result.successful_test_count() + count_mismatch = successful_tests != SELFTEST_COUNT + if result.wasSuccessful() and count_mismatch: + # only print this if all tests succeeded + logger.error("self test count (%d != %d) mismatch, either test discovery is broken or a test was added " + "without updating borg.selftest", + successful_tests, SELFTEST_COUNT) + if not result.wasSuccessful() or count_mismatch: + logger.error("self test failed\n" + "This is a bug either in Borg or in the package / distribution you use.") + sys.exit(2) + assert False, "sanity assertion failed: ran beyond sys.exit()" + selftest_elapsed = time.perf_counter() - selftest_started + logger.debug("%d self tests completed in %.2f seconds", successful_tests, selftest_elapsed) diff --git a/borg/testsuite/__init__.py b/borg/testsuite/__init__.py index 5c1a0a6fd..cccf97a82 100644 --- a/borg/testsuite/__init__.py +++ b/borg/testsuite/__init__.py @@ -8,7 +8,8 @@ import sysconfig import time import unittest from ..xattr import get_all -from ..logger import setup_logging + +# Note: this is used by borg.selftest, do not use or import py.test functionality here. try: import llfuse @@ -17,6 +18,11 @@ try: except ImportError: have_fuse_mtime_ns = False +try: + from pytest import raises +except ImportError: + raises = None + has_lchflags = hasattr(os, 'lchflags') @@ -31,9 +37,6 @@ else: if sys.platform.startswith('netbsd'): st_mtime_ns_round = -4 # only >1 microsecond resolution here? -# Ensure that the loggers exist for all tests -setup_logging() - class BaseTestCase(unittest.TestCase): """ @@ -42,9 +45,13 @@ class BaseTestCase(unittest.TestCase): assert_not_in = unittest.TestCase.assertNotIn assert_equal = unittest.TestCase.assertEqual assert_not_equal = unittest.TestCase.assertNotEqual - assert_raises = unittest.TestCase.assertRaises assert_true = unittest.TestCase.assertTrue + if raises: + assert_raises = staticmethod(raises) + else: + assert_raises = unittest.TestCase.assertRaises + @contextmanager def assert_creates_file(self, path): self.assert_true(not os.path.exists(path), '{} should not exist'.format(path)) diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index 77fb44277..bbefea3f2 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -61,6 +61,7 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw): sys.stdout = sys.stderr = output = StringIO() if archiver is None: archiver = Archiver() + archiver.prerun_checks = lambda *args: None archiver.exit_code = EXIT_SUCCESS args = archiver.parse_args(list(args)) ret = archiver.run(args) diff --git a/borg/testsuite/chunker.py b/borg/testsuite/chunker.py index 0db7203d5..2a14bd604 100644 --- a/borg/testsuite/chunker.py +++ b/borg/testsuite/chunker.py @@ -4,6 +4,9 @@ from ..chunker import Chunker, buzhash, buzhash_update from ..constants import * # NOQA from . import BaseTestCase +# Note: these tests are part of the self test, do not use or import py.test functionality here. +# See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT + class ChunkerTestCase(BaseTestCase): diff --git a/borg/testsuite/conftest.py b/borg/testsuite/conftest.py new file mode 100644 index 000000000..0c350fb7f --- /dev/null +++ b/borg/testsuite/conftest.py @@ -0,0 +1,4 @@ +from ..logger import setup_logging + +# Ensure that the loggers exist for all tests +setup_logging() diff --git a/borg/testsuite/crypto.py b/borg/testsuite/crypto.py index 9609e259a..e3eff8bec 100644 --- a/borg/testsuite/crypto.py +++ b/borg/testsuite/crypto.py @@ -3,6 +3,9 @@ from binascii import hexlify, unhexlify from ..crypto import AES, bytes_to_long, bytes_to_int, long_to_bytes, hmac_sha256 from . import BaseTestCase +# Note: these tests are part of the self test, do not use or import py.test functionality here. +# See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT + class CryptoTestCase(BaseTestCase): diff --git a/borg/testsuite/hashindex.py b/borg/testsuite/hashindex.py index 3aac0c7db..000dfe4c3 100644 --- a/borg/testsuite/hashindex.py +++ b/borg/testsuite/hashindex.py @@ -1,15 +1,16 @@ import base64 import hashlib import os -import struct import tempfile import zlib -import pytest from ..hashindex import NSIndex, ChunkIndex from .. import hashindex from . import BaseTestCase +# Note: these tests are part of the self test, do not use or import py.test functionality here. +# See borg.selftest for details. If you add/remove test methods, update SELFTEST_COUNT + def H(x): # make some 32byte long thing that depends on x @@ -194,7 +195,7 @@ class HashIndexRefcountingTestCase(BaseTestCase): def test_decref_zero(self): idx1 = ChunkIndex() idx1[H(1)] = 0, 0, 0 - with pytest.raises(AssertionError): + with self.assert_raises(AssertionError): idx1.decref(H(1)) def test_incref_decref(self): @@ -208,18 +209,18 @@ class HashIndexRefcountingTestCase(BaseTestCase): def test_setitem_raises(self): idx1 = ChunkIndex() - with pytest.raises(AssertionError): + with self.assert_raises(AssertionError): idx1[H(1)] = hashindex.MAX_VALUE + 1, 0, 0 def test_keyerror(self): idx = ChunkIndex() - with pytest.raises(KeyError): + with self.assert_raises(KeyError): idx.incref(H(1)) - with pytest.raises(KeyError): + with self.assert_raises(KeyError): idx.decref(H(1)) - with pytest.raises(KeyError): + with self.assert_raises(KeyError): idx[H(1)] - with pytest.raises(OverflowError): + with self.assert_raises(OverflowError): idx.add(H(1), -1, 0, 0) @@ -269,10 +270,11 @@ class HashIndexDataTestCase(BaseTestCase): assert idx1[H(3)] == (hashindex.MAX_VALUE, 6, 7) -def test_nsindex_segment_limit(): - idx = NSIndex() - with pytest.raises(AssertionError): - idx[H(1)] = hashindex.MAX_VALUE + 1, 0 - assert H(1) not in idx - idx[H(2)] = hashindex.MAX_VALUE, 0 - assert H(2) in idx +class NSIndexTestCase(BaseTestCase): + def test_nsindex_segment_limit(self): + idx = NSIndex() + with self.assert_raises(AssertionError): + idx[H(1)] = hashindex.MAX_VALUE + 1, 0 + assert H(1) not in idx + idx[H(2)] = hashindex.MAX_VALUE, 0 + assert H(2) in idx