diff --git a/Makefile b/Makefile index 39fe47ca4..e0821653a 100644 --- a/Makefile +++ b/Makefile @@ -480,7 +480,7 @@ endif $(BUILD_DIR)/venv/bin/pip install $(PIP_ARGS) $(PACKAGE) pytest $(BUILD_DIR)/venv/bin/rflx --version mkdir -p $(BUILD_DIR)/tests - cp -r tests/{__init__.py,const.py,end_to_end,data} $(BUILD_DIR)/tests/ + cp -r tests/{__init__.py,const.py,utils.py,end_to_end,data} $(BUILD_DIR)/tests/ cd $(BUILD_DIR) && source venv/bin/activate && rflx --version && venv/bin/pytest -vv tests/end_to_end $(RM) -r $(BUILD_DIR)/{venv,tests} diff --git a/rflx/fatal_error.py b/rflx/fatal_error.py index 328ac52c2..0f8a99c9b 100644 --- a/rflx/fatal_error.py +++ b/rflx/fatal_error.py @@ -3,9 +3,23 @@ import sys import traceback import types -from typing import Callable, Optional +from typing import Callable, Final, Optional -from rflx.version import version +from rflx.version import is_gnat_tracker_release, version + +BUG_BOX_MSG_GITHUB: Final = """ +A bug was detected. Please report this issue on GitHub: + +https://github.com/AdaCore/RecordFlux/issues/new?labels=bug +""" + +BUG_BOX_MSG_GNAT_TRACKER: Final = """ +A bug was detected. Please submit a bug report using GNATtracker at +https://support.adacore.com/csm by logging in and clicking the +'Create A New Case' button. Alternatively, submit a bug report by email to +support@adacore.com, including your account number #xxxx in the subject line. +Use a subject line meaningful to you and us to track the bug. +""" class FatalErrorHandler: @@ -47,10 +61,8 @@ def fatal_error_message(unsafe: bool) -> str: {traceback.format_exc()} ---------------------------------------------------------------------------- -A bug was detected. Please report this issue on GitHub: - -https://github.com/AdaCore/RecordFlux/issues/new?labels=bug +{BUG_BOX_MSG_GNAT_TRACKER if is_gnat_tracker_release() else BUG_BOX_MSG_GITHUB} Include the complete content of the bug box shown above and all input files -in the report.""" +in the report. Use plain ASCII or MIME attachment(s).""" return result diff --git a/rflx/version.py b/rflx/version.py index 3b2b9dd2b..62ce1c8be 100644 --- a/rflx/version.py +++ b/rflx/version.py @@ -4,6 +4,7 @@ from importlib import metadata from typing import Optional +import rflx from rflx import __version__ from rflx.error import fatal_fail @@ -25,6 +26,29 @@ def dependencies() -> list[str]: ] +def is_gnat_tracker_release() -> bool: + """ + Return True iff the current instance of the tool is part of the GNATtracker release track. + + RecordFlux has two release tracks: PyPI and GNATtracker. This function + determines the active track indirectly from the version number. The PyPI + track uses semantic versioning while the GNATtracker track does not. + """ + # Note: The regex below is from https://semver.org/ with one slight + # difference: the prerelease section is allowed to start also with a '.' + # instead of '-'. This allows for the intermediate version numbers, such as + # "0.22.1.dev13+1ef4d7163" that are generated by Poetry Dynamic Versioning + # also to be categorized as being on the PyPI release track. + semver_regex = ( + r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)" + r"(?:[-\.](?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + r"(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" + ) + match = re.match(semver_regex, rflx.__version__) + return not bool(match) + + class Requirement: def __init__(self, string: str) -> None: self.name: str diff --git a/tests/const.py b/tests/const.py index 486dc66d5..10cd73b8d 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Final TEST_DIR = Path("tests") DATA_DIR = TEST_DIR / "data" @@ -12,3 +13,8 @@ EX_SPEC_DIR = Path("examples/specs") MAIN = "main.adb" + +GITHUB_TRACKER_REF_PATTERN: Final = ( + r".*GitHub.*https://github.com/AdaCore/RecordFlux/issues/new\?labels=bug.*" +) +GNAT_TRACKER_REF_PATTERN: Final = r".*GNATtracker.*https://support.adacore.com/csm.*" diff --git a/tests/end_to_end/cli_test.py b/tests/end_to_end/cli_test.py index 008bac9b2..eeae8a1a3 100644 --- a/tests/end_to_end/cli_test.py +++ b/tests/end_to_end/cli_test.py @@ -1,8 +1,22 @@ +import re import subprocess import textwrap from pathlib import Path -from tests.const import DATA_DIR, SPEC_DIR, VALIDATOR_DIR +import pytest + +from rflx import cli +from tests.const import ( + DATA_DIR, + GITHUB_TRACKER_REF_PATTERN, + GNAT_TRACKER_REF_PATTERN, + SPEC_DIR, + VALIDATOR_DIR, +) +from tests.utils import is_gnat_tracker_release_testing, raise_fatal_error + +MESSAGE_SPEC_FILE = str(SPEC_DIR / "tlv.rflx") +SESSION_SPEC_FILE = str(SPEC_DIR / "session.rflx") def test_check() -> None: @@ -312,3 +326,25 @@ def test_run_ls() -> None: Closing the event loop. """, # noqa: E501 ) + + +def test_unexpected_exception( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capfd: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setattr(cli, "generate", lambda _: raise_fatal_error()) + with pytest.raises(SystemExit, match="^2$"): + cli.main( + ["rflx", "generate", "-d", str(tmp_path), MESSAGE_SPEC_FILE, SESSION_SPEC_FILE], + ) + tracker_ref_pattern = ( + GNAT_TRACKER_REF_PATTERN + if is_gnat_tracker_release_testing() + else GITHUB_TRACKER_REF_PATTERN + ) + assert re.fullmatch( + rf"\n-* RecordFlux Bug -*.*Traceback.*-*.*{tracker_ref_pattern}.*", + capfd.readouterr().err, + re.DOTALL, + ) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 268c662a2..5384803ab 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -13,14 +13,14 @@ import pytest import rflx.specification -from rflx import cli, generator, validator +from rflx import cli, fatal_error, generator, validator from rflx.converter import iana -from rflx.error import fail, fatal_fail +from rflx.error import fail from rflx.ls.server import server from rflx.pyrflx import PyRFLXError from rflx.rapidflux import ErrorEntry, Location, RecordFluxError, Severity, logging -from tests.const import DATA_DIR, SPEC_DIR -from tests.utils import assert_stderr_regex +from tests.const import DATA_DIR, GITHUB_TRACKER_REF_PATTERN, GNAT_TRACKER_REF_PATTERN, SPEC_DIR +from tests.utils import assert_stderr_regex, raise_fatal_error MESSAGE_SPEC_FILE = str(SPEC_DIR / "tlv.rflx") SESSION_SPEC_FILE = str(SPEC_DIR / "session.rflx") @@ -53,10 +53,6 @@ def raise_unexpected_exception() -> None: raise NotImplementedError("Not implemented") -def raise_fatal_error() -> None: - fatal_fail("TEST") - - def test_run(monkeypatch: pytest.MonkeyPatch) -> None: expected = ["rflx", "foo", "bar"] monkeypatch.setattr(sys, "argv", expected) @@ -651,18 +647,28 @@ def test_main_validate_fatal_error( assert "RecordFlux Bug" in capfd.readouterr().err +@pytest.mark.parametrize( + ("gnat_tracker_release", "tracker_ref_pattern"), + [ + (False, GITHUB_TRACKER_REF_PATTERN), + (True, GNAT_TRACKER_REF_PATTERN), + ], +) def test_main_unexpected_exception( + gnat_tracker_release: bool, + tracker_ref_pattern: str, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capfd: pytest.CaptureFixture[str], ) -> None: monkeypatch.setattr(cli, "generate", lambda _: raise_fatal_error()) + monkeypatch.setattr(fatal_error, "is_gnat_tracker_release", lambda: gnat_tracker_release) with pytest.raises(SystemExit, match="^2$"): cli.main( ["rflx", "generate", "-d", str(tmp_path), MESSAGE_SPEC_FILE, SESSION_SPEC_FILE], ) assert re.fullmatch( - r"\n-* RecordFlux Bug -*.*Traceback.*-*.*RecordFlux/issues.*", + rf"\n-* RecordFlux Bug -*.*Traceback.*-*.*{tracker_ref_pattern}.*", capfd.readouterr().err, re.DOTALL, ) diff --git a/tests/unit/fatal_error_test.py b/tests/unit/fatal_error_test.py index ebfd985e3..d2a13cdf3 100644 --- a/tests/unit/fatal_error_test.py +++ b/tests/unit/fatal_error_test.py @@ -4,7 +4,12 @@ import pytest -from rflx.fatal_error import FatalErrorHandler +from rflx import fatal_error +from rflx.fatal_error import ( + FatalErrorHandler, + fatal_error_message, +) +from tests.const import GITHUB_TRACKER_REF_PATTERN, GNAT_TRACKER_REF_PATTERN def test_fatal_error_handler() -> None: @@ -41,3 +46,19 @@ def store(msg: str) -> None: result[0], re.DOTALL, ) + + +@pytest.mark.parametrize( + ("gnat_tracker_release", "expected"), + [ + (False, GITHUB_TRACKER_REF_PATTERN), + (True, GNAT_TRACKER_REF_PATTERN), + ], +) +def test_fatal_error_message( + gnat_tracker_release: bool, + expected: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(fatal_error, "is_gnat_tracker_release", lambda: gnat_tracker_release) + assert re.match(expected, fatal_error_message(unsafe=False), re.DOTALL) diff --git a/tests/unit/version_test.py b/tests/unit/version_test.py index c5a3976d3..d37af2c67 100644 --- a/tests/unit/version_test.py +++ b/tests/unit/version_test.py @@ -4,14 +4,38 @@ import pytest +import rflx from rflx import __version__, version from rflx.error import FatalError +from rflx.version import is_gnat_tracker_release def test_version() -> None: assert version.version().startswith(f"RecordFlux {__version__}") +@pytest.mark.parametrize( + ("rflx_version", "expected"), + [ + ("0.22.1", False), + ("0.22.1.dev13+1ef4d7163", False), + ("0.22.1-dev13+1ef4d7163", False), + ("25.1.0", False), + ("25.0", True), + ("25.dev20240804", True), + ("27.1", True), + ("27.2", True), + ], +) +def test_is_gnat_tracker_release( + rflx_version: str, + expected: bool, + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(rflx, "__version__", rflx_version) + assert is_gnat_tracker_release() == expected + + @pytest.mark.parametrize( ("requirement", "name", "extra"), [ diff --git a/tests/utils.py b/tests/utils.py index 29c3751af..e93913c5b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,6 +13,7 @@ from rflx import ada, lang from rflx.common import STDIN +from rflx.error import fatal_fail from rflx.expr import Expr from rflx.generator import Debug, Generator, const from rflx.identifier import ID, StrID @@ -1246,6 +1247,10 @@ def parse_expression(data: str, rule: str = lang.GrammarRule.extended_expression return expression +def raise_fatal_error() -> None: + fatal_fail("TEST") + + def get_test_model(name: str) -> Model: parser = Parser() parser.parse(SPEC_DIR / f"{name}.rflx") @@ -1268,3 +1273,7 @@ def assert_stderr_regex( check_regex(expected_regex) capture = capfd.readouterr() assert re.match(expected_regex, capture.err) is not None + + +def is_gnat_tracker_release_testing() -> bool: + return "RFLX_GNAT_TRACKER_RELEASE_TEST" in os.environ