From 06385319b5700fff85f25cdb64f5ca4e614a8e36 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 13 Oct 2026 00:13:57 +0200 Subject: [PATCH] docs: add rules for using assert, test the -O guard, refs #8649 Add a "Using assert" section to the development docs: assert is only for conditions that can be false only due to a borg bug; everything coming from outside (repo objects, local files, legacy data, remote responses, user input, OS results) needs a real check that raises a borg exception, done at the boundary where the data enters. Add a regression test that borg refuses to run with assertions disabled (python -O and PYTHONOPTIMIZE=1). Co-Authored-By: Claude Opus 5.5 --- docs/development.rst | 34 +++++++++++++++++++ .../testsuite/archiver/version_cmd_test.py | 25 ++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 29b313810b..6f712ad2dc 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -241,6 +241,40 @@ follow their `guide about avoiding ruining git blame`_: .. _black: https://black.readthedocs.io/ .. _guide about avoiding ruining git blame: https://black.readthedocs.io/en/stable/guides/introducing_black_to_your_project.html#avoiding-ruining-git-blame +Using assert +------------ + +Use ``assert`` only for conditions that can be false only if borg itself has a bug: +internal invariants, pre- and postconditions between borg's own functions, types of +arguments passed by borg's own callers and unreachable branches. A traceback is the +wanted outcome there. + +Anything that can be false without a bug in borg must be checked with +``if ...: raise ...``, e.g.: + +- repository/store objects (authenticated does not mean well-formed), +- local cache, config and key files, +- legacy borg 1.x data, +- responses from a remote server, +- user input (command line arguments, environment variables, patterns, paths), +- results from the OS and the filesystem. + +Prefer borg's own exception classes there (``IntegrityError``, ``Error`` subclasses with an +exit code), so that users get an error message and a meaningful return code instead of a +traceback. Raise ``ValueError`` or ``TypeError`` only where the callers catch them. + +Validate such data at the boundary where it enters borg (where it is parsed). Deeper helpers +that only get the validated data may then keep using ``assert`` for their invariants. + +Never use ``assert`` for security decisions (e.g. path sanitizing, authentication, permission +checks) and never put side effects into an ``assert`` expression. + +Borg refuses to start if assertions are disabled (``python -O`` or ``PYTHONOPTIMIZE``). The +guard for that is at the top of ``src/borg/archiver/__init__.py``, so it only covers code +paths that import ``borg.archiver``. + +The test suite uses plain pytest-style ``assert`` statements. + Continuous Integration ---------------------- diff --git a/src/borg/testsuite/archiver/version_cmd_test.py b/src/borg/testsuite/archiver/version_cmd_test.py index 8400997790..9342c32b1c 100644 --- a/src/borg/testsuite/archiver/version_cmd_test.py +++ b/src/borg/testsuite/archiver/version_cmd_test.py @@ -1,5 +1,12 @@ import json +import os +import subprocess +import sys + +import pytest + from borg import __version__ +from borg.helpers import EXIT_ERROR from borg.version import format_version, parse_version from borg.testsuite.archiver import Archiver, cmd @@ -31,3 +38,21 @@ def test_from_borg1_default_off(): args = archiver.parse_args(["-r", "/mnt/backup", "version"]) assert args.func == archiver.do_version assert args.v1_legacy is False + + +@pytest.mark.parametrize( + "python_opts, pythonoptimize", [(["-O"], None), ([], "1")], ids=["option-O", "env-PYTHONOPTIMIZE"] +) +def test_refuses_to_run_without_assertions(python_opts, pythonoptimize): + """borg must not run with assertions disabled (see the guard at the top of borg.archiver).""" + env = os.environ.copy() + env.pop("PYTHONOPTIMIZE", None) + if pythonoptimize is not None: + env["PYTHONOPTIMIZE"] = pythonoptimize + python = sys.executable or "python3" + result = subprocess.run( + [python, *python_opts, "-m", "borg", "--version"], env=env, capture_output=True, text=True, timeout=60 + ) + assert result.returncode == EXIT_ERROR + assert "Borg requires working assertions" in result.stderr + assert result.stdout == ""