You've already forked RecordFlux
mirror of
https://github.com/AdaCore/RecordFlux.git
synced 2026-02-12 13:01:56 -08:00
1116 lines
32 KiB
Python
1116 lines
32 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.resources
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from argparse import ArgumentParser
|
|
from collections.abc import Callable
|
|
from io import TextIOWrapper
|
|
from pathlib import Path
|
|
from typing import ClassVar, NoReturn
|
|
|
|
import pytest
|
|
|
|
import rflx.specification
|
|
from rflx import cli, fatal_error, generator, validator
|
|
from rflx.converter import iana
|
|
from rflx.error import fail
|
|
from rflx.ls.server import server
|
|
from rflx.pyrflx import PyRFLXError
|
|
from rflx.rapidflux import (
|
|
NO_LOCATION,
|
|
ErrorEntry,
|
|
Location,
|
|
RecordFluxError,
|
|
Severity,
|
|
logging,
|
|
)
|
|
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")
|
|
STATE_MACHINE_SPEC_FILE = str(SPEC_DIR / "state_machine.rflx")
|
|
IANA_XML_FILE = str(DATA_DIR / "bootp-dhcp-parameters.xml")
|
|
|
|
|
|
def validator_mock(
|
|
self: object, # noqa: ARG001
|
|
files: object, # noqa: ARG001
|
|
checksum_module: str | None = None, # noqa: ARG001
|
|
cache: object | None = None, # noqa: ARG001
|
|
split_disjunctions: bool = False, # noqa: ARG001
|
|
) -> None:
|
|
return None
|
|
|
|
|
|
def raise_error() -> None:
|
|
fail("TEST", Severity.ERROR, Location((8, 22)))
|
|
|
|
|
|
def raise_pyrflx_error() -> None:
|
|
raise PyRFLXError([ErrorEntry("TEST", Severity.ERROR, NO_LOCATION)])
|
|
|
|
|
|
def raise_validation_error() -> None:
|
|
raise validator.ValidationError("TEST")
|
|
|
|
|
|
def raise_unexpected_exception() -> None:
|
|
raise NotImplementedError("Not implemented")
|
|
|
|
|
|
def test_run(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
expected = ["rflx", "foo", "bar"]
|
|
monkeypatch.setattr(sys, "argv", expected)
|
|
|
|
provided = []
|
|
monkeypatch.setattr(cli, "main", lambda x: provided.extend(x))
|
|
|
|
cli.run()
|
|
|
|
assert provided == expected
|
|
|
|
|
|
def test_main_noarg() -> None:
|
|
assert cli.main(["rflx"]) == 2
|
|
|
|
|
|
def test_main_help() -> None:
|
|
with pytest.raises(SystemExit):
|
|
assert cli.main(["rflx", "-h"]) == 0
|
|
|
|
|
|
def test_main_version() -> None:
|
|
assert cli.main(["rflx", "--version"]) == 0
|
|
|
|
|
|
def test_main_check() -> None:
|
|
assert cli.main(["rflx", "check", MESSAGE_SPEC_FILE, STATE_MACHINE_SPEC_FILE]) == 0
|
|
|
|
|
|
def test_main_check_quiet() -> None:
|
|
assert cli.main(["rflx", "-q", "check", MESSAGE_SPEC_FILE, STATE_MACHINE_SPEC_FILE]) == 0
|
|
assert cli.main(["rflx", "--quiet", "check", MESSAGE_SPEC_FILE, STATE_MACHINE_SPEC_FILE]) == 0
|
|
logging.set_quiet(False)
|
|
|
|
|
|
def test_main_check_parser_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setattr(cli, "check", lambda _: raise_error())
|
|
assert cli.main(["rflx", "check", MESSAGE_SPEC_FILE]) == 1
|
|
assert_stderr_regex("^<stdin>:8:22: error: TEST\n$", capfd)
|
|
|
|
|
|
def test_main_check_model_error_parse(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setattr(cli, "check", lambda _: raise_error())
|
|
assert cli.main(["rflx", "check", MESSAGE_SPEC_FILE]) == 1
|
|
assert "<stdin>:8:22: error: TEST" in capfd.readouterr().err
|
|
|
|
|
|
def test_main_check_model_error_create_model(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setattr(rflx.specification.Parser, "parse", lambda _x, _y: raise_error())
|
|
monkeypatch.setattr(rflx.specification.Parser, "create_model", lambda _: raise_error())
|
|
assert cli.main(["rflx", "check", MESSAGE_SPEC_FILE]) == 1
|
|
assert_stderr_regex("^<stdin>:8:22: error: TEST\n<stdin>:8:22: error: TEST$", capfd)
|
|
|
|
|
|
def test_main_check_non_existent_file(
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
assert cli.main(["rflx", "check", "non-existent file"]) == 1
|
|
assert_stderr_regex(
|
|
"^info: Verifying __BUILTINS__::Boolean\n"
|
|
"info: Verifying __INTERNAL__::Opaque\n"
|
|
'error: file not found: "non-existent file"\n$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_main_generate(tmp_path: Path) -> None:
|
|
assert (
|
|
cli.main(
|
|
["rflx", "generate", "-d", str(tmp_path), MESSAGE_SPEC_FILE, STATE_MACHINE_SPEC_FILE],
|
|
)
|
|
== 0
|
|
)
|
|
top_level_package = Path(tmp_path) / (cli.DEFAULT_PREFIX.lower() + ".ads")
|
|
assert top_level_package.exists()
|
|
|
|
|
|
def test_main_generate_no_library_files(tmp_path: Path) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"generate",
|
|
"-d",
|
|
str(tmp_path),
|
|
"-p",
|
|
"",
|
|
"-n",
|
|
str(SPEC_DIR / "empty_package.rflx"),
|
|
],
|
|
)
|
|
== 0
|
|
)
|
|
assert len(list(tmp_path.glob("*"))) == 0
|
|
|
|
|
|
def test_main_generate_prefix(tmp_path: Path) -> None:
|
|
for index, prefix in enumerate(["", " ", "A", "A.B", "A.B.C"]):
|
|
path = tmp_path / str(index)
|
|
path.mkdir()
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"generate",
|
|
"-d",
|
|
str(path),
|
|
"-p",
|
|
prefix,
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 0
|
|
)
|
|
top_level_package = Path(tmp_path) / (prefix.replace(".", "-").lower() + ".ads")
|
|
assert not top_level_package.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("prefix", [".", "A.B.", ".A.B", "A..B"])
|
|
def test_main_generate_invalid_prefix(
|
|
capfd: pytest.CaptureFixture[str],
|
|
tmp_path: Path,
|
|
prefix: str,
|
|
) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"generate",
|
|
"-d",
|
|
str(tmp_path),
|
|
"-p",
|
|
prefix,
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
assert_stderr_regex(rf'^error: invalid prefix: "{prefix}"$', capfd)
|
|
|
|
|
|
def test_main_generate_no_output_files(tmp_path: Path) -> None:
|
|
assert (
|
|
cli.main(["rflx", "generate", "-d", str(tmp_path), str(SPEC_DIR / "empty_package.rflx")])
|
|
== 0
|
|
)
|
|
|
|
|
|
def test_main_generate_non_existent_directory(capfd: pytest.CaptureFixture[str]) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"generate",
|
|
"-d",
|
|
"non-existent directory",
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
assert_stderr_regex('^error: directory not found: "non-existent directory"\n$', capfd)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("args", "expected"),
|
|
[
|
|
([], generator.Debug.NONE),
|
|
(["--debug", "built-in"], generator.Debug.BUILTIN),
|
|
(["--debug", "external"], generator.Debug.EXTERNAL),
|
|
],
|
|
)
|
|
def test_main_generate_debug(
|
|
args: list[str],
|
|
expected: generator.Debug,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
result = []
|
|
|
|
def generator_mock(
|
|
self: object, # noqa: ARG001
|
|
prefix: str, # noqa: ARG001
|
|
workers: int, # noqa: ARG001
|
|
reproducible: bool, # noqa: ARG001
|
|
debug: generator.Debug,
|
|
ignore_unsupported_checksum: bool, # noqa: ARG001
|
|
) -> None:
|
|
result.append(debug)
|
|
|
|
monkeypatch.setattr(generator.Generator, "__init__", generator_mock)
|
|
monkeypatch.setattr(
|
|
generator.Generator,
|
|
"generate",
|
|
lambda self, model, integration, directory, library_files, top_level_package: None, # noqa: ARG005
|
|
)
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"generate",
|
|
"-d",
|
|
str(tmp_path),
|
|
*args,
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 0
|
|
)
|
|
assert result == [expected]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("args", "expected"),
|
|
[
|
|
([], False),
|
|
(["--reproducible"], True),
|
|
],
|
|
)
|
|
def test_main_generate_reproducible(
|
|
args: list[str],
|
|
expected: bool,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
result = []
|
|
|
|
def generator_mock(
|
|
self: object, # noqa: ARG001
|
|
prefix: str, # noqa: ARG001
|
|
workers: int, # noqa: ARG001
|
|
reproducible: bool,
|
|
debug: generator.Debug, # noqa: ARG001
|
|
ignore_unsupported_checksum: bool, # noqa: ARG001
|
|
) -> None:
|
|
result.append(reproducible)
|
|
|
|
monkeypatch.setattr(generator.Generator, "__init__", generator_mock)
|
|
monkeypatch.setattr(
|
|
generator.Generator,
|
|
"generate",
|
|
lambda self, model, integration, directory, library_files, top_level_package: None, # noqa: ARG005
|
|
)
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"generate",
|
|
"-d",
|
|
str(tmp_path),
|
|
*args,
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 0
|
|
)
|
|
assert result == [expected]
|
|
|
|
|
|
def test_main_optimize(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
call = []
|
|
|
|
def optimize_mock(project_file: Path) -> None:
|
|
call.append(project_file)
|
|
|
|
monkeypatch.setattr(generator.optimizer, "optimize", optimize_mock)
|
|
|
|
project_file = tmp_path / "test.gpr"
|
|
project_file.touch()
|
|
|
|
assert (
|
|
cli.main(
|
|
["rflx", "optimize", str(project_file)],
|
|
)
|
|
== 0
|
|
)
|
|
assert call == [project_file]
|
|
|
|
|
|
def test_main_optimize_non_existent_project_file(capfd: pytest.CaptureFixture[str]) -> None:
|
|
assert cli.main(["rflx", "optimize", "non-existent file"]) == 1
|
|
assert_stderr_regex('^error: project file not found: "non-existent file"$', capfd)
|
|
|
|
|
|
def test_main_graph(tmp_path: Path) -> None:
|
|
assert (
|
|
cli.main(["rflx", "graph", "-d", str(tmp_path), MESSAGE_SPEC_FILE, STATE_MACHINE_SPEC_FILE])
|
|
== 0
|
|
)
|
|
|
|
|
|
def test_main_graph_non_existent_file(
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
assert cli.main(["rflx", "graph", "-d", str(tmp_path), "non-existent file"]) == 1
|
|
assert_stderr_regex(
|
|
"^info: Verifying __BUILTINS__::Boolean\n"
|
|
"info: Verifying __INTERNAL__::Opaque\n"
|
|
'error: file not found: "non-existent file"$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_main_graph_non_existent_files(
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"graph",
|
|
"-d",
|
|
str(tmp_path),
|
|
"non-existent file 1",
|
|
"non-existent file 2",
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
|
|
assert_stderr_regex(
|
|
"^info: Verifying __BUILTINS__::Boolean\n"
|
|
"info: Verifying __INTERNAL__::Opaque\n"
|
|
'error: file not found: "non-existent file 1"\n'
|
|
'error: file not found: "non-existent file 2"$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_main_graph_non_existent_directory(
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"graph",
|
|
"-d",
|
|
"non-existent directory",
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
assert_stderr_regex('^error: directory not found: "non-existent directory"$', capfd)
|
|
|
|
|
|
def test_main_graph_no_output_files(tmp_path: Path) -> None:
|
|
assert (
|
|
cli.main(["rflx", "graph", "-d", str(tmp_path), str(SPEC_DIR / "empty_package.rflx")]) == 0
|
|
)
|
|
|
|
|
|
def test_main_validate_required_arg_not_provided(tmp_path: Path) -> None:
|
|
with pytest.raises(SystemExit, match="^2$"):
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
"Test::Message",
|
|
"-v",
|
|
str(tmp_path),
|
|
"-i",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
|
|
with pytest.raises(SystemExit, match="^2$"):
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
str(tmp_path / "test.rflx"),
|
|
"-v",
|
|
str(tmp_path),
|
|
"-i",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
|
|
|
|
def test_main_validate(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setattr(validator.Validator, "__init__", validator_mock)
|
|
monkeypatch.setattr(
|
|
validator.Validator,
|
|
"validate",
|
|
lambda _a, _b, _c, _d, _e, _f, _g, _h: None,
|
|
)
|
|
assert (
|
|
cli.main(
|
|
# The positional and optional arguments are intentionally mixed
|
|
# below so that a positional argument appears after the "-i" and its
|
|
# value. '-i' and '-v' occur intentionally multiple times.
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
"-v",
|
|
str(tmp_path),
|
|
"-i",
|
|
str(tmp_path),
|
|
"-v",
|
|
str(tmp_path),
|
|
"-i",
|
|
str(tmp_path),
|
|
str(tmp_path / "test.rflx"),
|
|
"Test::Message",
|
|
"--split-disjunctions",
|
|
"--abort-on-error",
|
|
"--coverage",
|
|
"-c",
|
|
"CHECKSUM_MODULE",
|
|
"-o",
|
|
str(tmp_path),
|
|
"--target-coverage",
|
|
"99",
|
|
],
|
|
)
|
|
== 0
|
|
)
|
|
assert (
|
|
cli.main(
|
|
# The positional and optional arguments are intentionally mixed
|
|
# below so that a positional argument appears after the "-v" and its
|
|
# value.
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
"-v",
|
|
str(tmp_path),
|
|
str(tmp_path / "test.rflx"),
|
|
"Test::Message",
|
|
"--split-disjunctions",
|
|
"-i",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
== 0
|
|
)
|
|
|
|
|
|
def test_main_validate_invalid_identifier(
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
str(tmp_path / "test.rflx"),
|
|
"Ethernet Frame",
|
|
"-v",
|
|
str(tmp_path),
|
|
"-i",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
|
|
assert_stderr_regex(
|
|
'^error: invalid identifier "Ethernet Frame"$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_main_validate_non_fatal_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setattr(validator.Validator, "__init__", validator_mock)
|
|
monkeypatch.setattr(
|
|
validator.Validator,
|
|
"validate",
|
|
lambda _a, _b, _c, _d, _e, _f, _g, _h: raise_error(),
|
|
)
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
str(tmp_path / "test.rflx"),
|
|
"Test::Message",
|
|
"-v",
|
|
str(tmp_path),
|
|
"-i",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
assert_stderr_regex("^<stdin>:8:22: error: TEST\n$", capfd)
|
|
|
|
|
|
def test_main_validate_validation_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setattr(validator.Validator, "__init__", validator_mock)
|
|
monkeypatch.setattr(
|
|
validator.Validator,
|
|
"validate",
|
|
lambda _a, _b, _c, _d, _e, _f, _g, _h: raise_validation_error(),
|
|
)
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
str(tmp_path / "test.rflx"),
|
|
"Test::Message",
|
|
"-v",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
assert_stderr_regex("^error: TEST$", capfd)
|
|
|
|
|
|
@pytest.mark.parametrize("raise_error", [raise_unexpected_exception, raise_pyrflx_error])
|
|
def test_main_validate_fatal_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
raise_error: Callable[[], None],
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.setattr(validator.Validator, "__init__", validator_mock)
|
|
monkeypatch.setattr(
|
|
validator.Validator,
|
|
"validate",
|
|
lambda _a, _b, _c, _d, _e, _f, _g, _h: raise_error(),
|
|
)
|
|
with pytest.raises(SystemExit, match="^2$"):
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"validate",
|
|
str(tmp_path / "test.rflx"),
|
|
"Test::Message",
|
|
"-v",
|
|
str(tmp_path),
|
|
],
|
|
)
|
|
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, STATE_MACHINE_SPEC_FILE],
|
|
)
|
|
assert re.fullmatch(
|
|
rf"\n-* RecordFlux Bug -*.*Traceback.*-*.*{tracker_ref_pattern}.*",
|
|
capfd.readouterr().err,
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def test_fail_fast(capfd: pytest.CaptureFixture[str]) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"--max-errors",
|
|
"5",
|
|
"check",
|
|
str(SPEC_DIR / "invalid/multiple_errors.rflx"),
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
|
|
capture = capfd.readouterr().err
|
|
assert len([l for l in capture.split("\n") if "error: " in l]) == 5, capture
|
|
|
|
|
|
def test_install_gnatstudio_plugin(tmp_path: Path) -> None:
|
|
gnat_studio_dir = tmp_path / ".gnatstudio"
|
|
gnat_studio_dir.mkdir(parents=True, exist_ok=True)
|
|
args = ["rflx", "install", "gnatstudio", "--gnat-studio-dir", str(gnat_studio_dir)]
|
|
|
|
# Install plugin into empty dir
|
|
assert cli.main(args) == 0
|
|
plugin = gnat_studio_dir / "plug-ins/recordflux.py"
|
|
assert plugin.exists()
|
|
assert plugin.is_file()
|
|
|
|
# Install with plugin dir already present
|
|
plugin.unlink()
|
|
assert cli.main(args) == 0
|
|
assert plugin.exists()
|
|
assert plugin.is_file()
|
|
|
|
|
|
def test_install_vscode_extension(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
run_called = []
|
|
vscode_extension = tmp_path / "recordflux.vsix"
|
|
|
|
monkeypatch.setattr(
|
|
subprocess,
|
|
"run",
|
|
lambda cmd, check: run_called.append(cmd), # noqa: ARG005
|
|
)
|
|
monkeypatch.setattr(cli, "vscode_extension", lambda: vscode_extension)
|
|
|
|
with pytest.raises(SystemExit, match=r"^2$"):
|
|
cli.main(["rflx", "install", "vscode"])
|
|
|
|
vscode_extension.touch()
|
|
|
|
assert cli.main(["rflx", "install", "vscode"]) == 0
|
|
|
|
assert run_called == [["code", "--install-extension", vscode_extension, "--force"]]
|
|
|
|
def run_mock(cmd: object, check: object) -> subprocess.CompletedProcess[object]: # noqa: ARG001
|
|
raise FileNotFoundError("file not found")
|
|
|
|
monkeypatch.setattr(subprocess, "run", run_mock)
|
|
|
|
assert cli.main(["rflx", "install", "vscode"]) == 1
|
|
assert (
|
|
"error: installation of VS Code extension failed: file not found\n"
|
|
in capfd.readouterr().err
|
|
)
|
|
|
|
|
|
def test_install_nvim_xdg_location(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
config_path = tmp_path / "nvim"
|
|
config_path.mkdir(parents=True, exist_ok=True)
|
|
script_path = tmp_path / "recordflux.vim"
|
|
script_path.write_text("nvim syntax file content")
|
|
expected_script_location = config_path / "syntax" / "recordflux.vim"
|
|
expected_ftdetect_location = config_path / "ftdetect" / "recordflux.vim"
|
|
|
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
|
monkeypatch.setattr(cli, "vim_syntax_file", lambda **_: script_path)
|
|
|
|
assert cli.main(["rflx", "install", "nvim"]) == 0
|
|
assert expected_script_location.read_text() == "nvim syntax file content"
|
|
assert (
|
|
expected_ftdetect_location.read_text()
|
|
== "autocmd BufRead,BufNewFile *.rflx set filetype=recordflux\n"
|
|
)
|
|
|
|
|
|
def test_install_nvim_home_location(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
config_path = tmp_path / ".config" / "nvim"
|
|
config_path.mkdir(parents=True, exist_ok=True)
|
|
script_path = tmp_path / "recordflux.vim"
|
|
script_path.write_text("nvim syntax file content")
|
|
expected_script_location = config_path / "syntax" / "recordflux.vim"
|
|
expected_ftdetect_location = config_path / "ftdetect" / "recordflux.vim"
|
|
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setattr(cli, "vim_syntax_file", lambda **_: script_path)
|
|
|
|
assert cli.main(["rflx", "install", "nvim"]) == 0
|
|
assert expected_script_location.read_text() == "nvim syntax file content"
|
|
assert (
|
|
expected_ftdetect_location.read_text()
|
|
== "autocmd BufRead,BufNewFile *.rflx set filetype=recordflux\n"
|
|
)
|
|
|
|
|
|
def test_install_nvim_no_env_variable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
for variable in ("HOME", "XDG_CONFIG_HOME"):
|
|
if os.environ.get(variable) is not None:
|
|
monkeypatch.delenv(variable)
|
|
|
|
assert cli.main(["rflx", "install", "nvim"]) == 1
|
|
assert_stderr_regex(
|
|
r"^error: could not find config directory\n"
|
|
r"help: make sure \$HOME or \$XDG_CONFIG_HOME variable is set\n$",
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_install_vim(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
script_path = tmp_path / "recordflux.vim"
|
|
script_path.write_text("vim syntax file content")
|
|
config_dir = tmp_path / ".vim"
|
|
expected_syntax_location = config_dir / "syntax" / "recordflux.vim"
|
|
expected_ftdetect_location = config_dir / "ftdetect" / "recordflux.vim"
|
|
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setattr(cli, "vim_syntax_file", lambda *_: script_path)
|
|
|
|
assert cli.main(["rflx", "install", "vim"]) == 0
|
|
assert expected_syntax_location.read_text() == "vim syntax file content"
|
|
assert (
|
|
expected_ftdetect_location.read_text()
|
|
== "autocmd BufRead,BufNewFile *.rflx set filetype=recordflux\n"
|
|
)
|
|
assert_stderr_regex(
|
|
rf'^info: Installed vim ftdetect file in "{expected_ftdetect_location}"\n'
|
|
rf'info: Installed vim syntax file in "{expected_syntax_location}"\n$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_install_vim_no_home(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capfd: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
monkeypatch.delenv("HOME")
|
|
assert cli.main(["rflx", "install", "vim"]) == 1
|
|
assert_stderr_regex(
|
|
r"^error: could not locate home directory\nhelp: make sure \$HOME variable is set\n$",
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_vim_syntax_file_ressource(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
expected_location = Path("rflx") / "ide" / "vim" / "recordflux.vim"
|
|
monkeypatch.setattr(importlib.resources, "files", lambda _: Path("rflx"))
|
|
assert cli.vim_syntax_file() == expected_location
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"editor",
|
|
["vim", "nvim"],
|
|
)
|
|
def test_install_permission_denied(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
capfd: pytest.CaptureFixture[str],
|
|
editor: str,
|
|
) -> None:
|
|
tmp_dir = tmp_path / "vim"
|
|
tmp_dir.mkdir()
|
|
perm_denied = tmp_dir / "recordflux.vim"
|
|
perm_denied.touch()
|
|
tmp_dir.chmod(0o444)
|
|
monkeypatch.setenv("HOME", str(tmp_dir))
|
|
|
|
assert cli.main(["rflx", "install", editor]) == 1
|
|
assert_stderr_regex(
|
|
rf"^error: failed to install {editor} files: "
|
|
rf'"\[Errno 13\] Permission denied: \'[^\']+\'"\n$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_install_invalid() -> None:
|
|
args = ["rflx", "install", "invalid"]
|
|
|
|
with pytest.raises(SystemExit, match=r"^2$"):
|
|
cli.main(args)
|
|
|
|
|
|
def test_missing_unsafe_option(capfd: pytest.CaptureFixture[str]) -> None:
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"rflx",
|
|
"--no-verification",
|
|
"check",
|
|
MESSAGE_SPEC_FILE,
|
|
],
|
|
)
|
|
== 1
|
|
)
|
|
|
|
assert_stderr_regex(
|
|
'^error: unsafe option "--no-verification" given without "--unsafe"\n$',
|
|
capfd,
|
|
)
|
|
|
|
|
|
def test_exception_in_unsafe_mode(
|
|
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",
|
|
"--unsafe",
|
|
"generate",
|
|
"-d",
|
|
str(tmp_path),
|
|
MESSAGE_SPEC_FILE,
|
|
STATE_MACHINE_SPEC_FILE,
|
|
],
|
|
)
|
|
assert re.fullmatch(
|
|
r"\n-*\nEXCEPTION IN UNSAFE MODE, PLEASE RERUN WITHOUT UNSAFE OPTIONS\n-*\n"
|
|
r"Traceback.*\n-*$",
|
|
capfd.readouterr().err,
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def test_main_convert_iana(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
result = []
|
|
|
|
def convert_mock(
|
|
data: str,
|
|
source: TextIOWrapper,
|
|
always_valid: bool,
|
|
output_dir: Path,
|
|
reproducible: bool = False,
|
|
) -> None:
|
|
result.append((data, source.name, always_valid, output_dir, reproducible))
|
|
|
|
monkeypatch.setattr(iana, "convert", convert_mock)
|
|
assert (
|
|
cli.main(
|
|
["rflx", "convert", "--reproducible", "iana", "-d", str(tmp_path / "1"), IANA_XML_FILE],
|
|
)
|
|
== 0
|
|
)
|
|
assert (
|
|
cli.main(
|
|
["rflx", "convert", "iana", "-d", str(tmp_path / "2"), "--always-valid", IANA_XML_FILE],
|
|
)
|
|
== 0
|
|
)
|
|
|
|
data = Path(IANA_XML_FILE).read_text(encoding="utf-8")
|
|
assert result == [
|
|
(data, IANA_XML_FILE, False, tmp_path / "1", True),
|
|
(data, IANA_XML_FILE, True, tmp_path / "2", False),
|
|
]
|
|
|
|
|
|
def test_main_run_ls(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
called = []
|
|
monkeypatch.setattr(server, "start_io", lambda: called.append(True))
|
|
assert cli.main(["rflx", "run_ls"]) == 0
|
|
assert any(called)
|
|
|
|
|
|
def test_main_doc(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
received_args: list[list[str]] = []
|
|
|
|
def mock_run(args: list[str], check: bool) -> None: # noqa: ARG001
|
|
nonlocal received_args
|
|
received_args.append(args)
|
|
|
|
with monkeypatch.context() as p:
|
|
p.setattr(cli, "DOC_DIR", "/mock/doc/dir")
|
|
p.setattr(subprocess, "run", mock_run)
|
|
assert cli.main(["rflx", "doc", "ug"]) == 0
|
|
assert cli.main(["rflx", "doc", "lrm"]) == 0
|
|
assert received_args == [
|
|
["xdg-open", "file:///mock/doc/dir/user_guide/index.html"],
|
|
["xdg-open", "file:///mock/doc/dir/language_reference/index.html"],
|
|
]
|
|
|
|
|
|
class TestDuplicateArgs:
|
|
"""
|
|
Test the rejection of duplicate options in the CLI.
|
|
|
|
Concretely, such options that have action 'store' (the default action) are tested here. The test
|
|
data consists of a full list of call arguments (optionally split to prefix and suffix) and the
|
|
duplicate options. The actual values for the arguments do not matter in these tests, but must
|
|
have the expected data type.
|
|
"""
|
|
|
|
call_main_prefix: ClassVar[list[str]] = [
|
|
"rflx",
|
|
"--max-errors",
|
|
"0",
|
|
"--workers",
|
|
"3",
|
|
]
|
|
# Note: 'check' is used below as a concrete rflx subcommand. However, the focus in this set of
|
|
# test data is on the rflx common arguments.
|
|
call_main_suffix: ClassVar[list[str]] = [
|
|
"check",
|
|
"SPECIFICATION_FILE",
|
|
]
|
|
call_generate: ClassVar[list[str]] = [
|
|
"rflx",
|
|
"generate",
|
|
"SPECIFICATION_FILE",
|
|
"-p",
|
|
"PREFIX",
|
|
"-d",
|
|
"OUTPUT_DIRECTORY",
|
|
"--debug",
|
|
"built-in",
|
|
"--integration-files-dir",
|
|
"INTEGRATION_FILES_DIR",
|
|
]
|
|
call_graph: ClassVar[list[str]] = [
|
|
"rflx",
|
|
"graph",
|
|
"SPECIFICATION_FILE",
|
|
"-f",
|
|
"jpg",
|
|
"-d",
|
|
"OUTPUT_DIRECTORY",
|
|
]
|
|
call_validate: ClassVar[list[str]] = [
|
|
"rflx",
|
|
"validate",
|
|
"SPECIFICATION_FILE",
|
|
"MESSAGE_IDENTIFIER",
|
|
"-v",
|
|
"VALID_SAMPLES_DIRECTORIES",
|
|
"-i",
|
|
"INVALID_SAMPLES_DIRECTORIES",
|
|
"-c",
|
|
"CHECKSUM_MODULE",
|
|
"-o",
|
|
"OUTPUT_FILE",
|
|
"--target-coverage",
|
|
"50",
|
|
]
|
|
call_install: ClassVar[list[str]] = [
|
|
"rflx",
|
|
"install",
|
|
"gnatstudio",
|
|
"--gnat-studio-dir",
|
|
"GNAT_STUDIO_DIR",
|
|
]
|
|
call_convert_iana: ClassVar[list[str]] = [
|
|
"rflx",
|
|
"convert",
|
|
"iana",
|
|
"-d",
|
|
"SOME_PATH",
|
|
]
|
|
|
|
class ArgParseError(Exception):
|
|
"""Used to mock the command line parsing error in argparse."""
|
|
|
|
def raise_argparse_error(self, message: str) -> None:
|
|
raise self.ArgParseError(message)
|
|
|
|
@pytest.mark.parametrize(
|
|
("call_prefix", "duplicate_option", "duplicate_option_value", "call_suffix"),
|
|
[
|
|
(call_main_prefix, "--max-errors", "5", call_main_suffix),
|
|
(call_main_prefix, "--workers", "10", call_main_suffix),
|
|
(call_generate, "-p", "OTHER_STR", []),
|
|
(call_generate, "-d", "OTHER_STR", []),
|
|
(call_generate, "--debug", "external", []),
|
|
(call_generate, "--integration-files-dir", "OTHER_STR", []),
|
|
(call_graph, "-f", "png", []),
|
|
(call_graph, "-d", "OTHER_STR", []),
|
|
(call_validate, "-c", "OTHER_STR", []),
|
|
(call_validate, "-o", "OTHER_STR", []),
|
|
(call_validate, "--target-coverage", "75", []),
|
|
(call_install, "--gnat-studio-dir", "OTHER_STR", []),
|
|
(call_convert_iana, "-d", "OTHER_STR", []),
|
|
],
|
|
)
|
|
def test_duplicate_args(
|
|
self,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
call_prefix: list[str],
|
|
duplicate_option: str,
|
|
duplicate_option_value: str,
|
|
call_suffix: list[str],
|
|
) -> None:
|
|
monkeypatch.setattr(ArgumentParser, "error", lambda _s, m: self.raise_argparse_error(m))
|
|
args = [*call_prefix, duplicate_option, duplicate_option_value, *call_suffix]
|
|
with pytest.raises(self.ArgParseError, match=f"^{duplicate_option} appears several times$"):
|
|
cli.main(args)
|
|
|
|
def test_uniquestore_nargs_is_0(self) -> None:
|
|
parser = argparse.ArgumentParser()
|
|
message = (
|
|
"nargs for store actions must be != 0; if you "
|
|
"have nothing to store, actions such as store "
|
|
"true or store const may be more appropriate"
|
|
)
|
|
with pytest.raises(ValueError, match=f"^{message}$"):
|
|
parser.add_argument("-x", action=cli.UniqueStore, nargs=0)
|
|
|
|
def test_uniquestore_bad_const(self) -> None:
|
|
parser = argparse.ArgumentParser()
|
|
message = "nargs must be '\\?' to supply const"
|
|
with pytest.raises(ValueError, match=f"^{message}$"):
|
|
parser.add_argument("-x", action=cli.UniqueStore, const=0)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"subcommand",
|
|
["check", "generate"],
|
|
)
|
|
def test_legacy_error_message(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
capfd: pytest.CaptureFixture[str],
|
|
subcommand: str,
|
|
) -> None:
|
|
def patched(*_: object) -> NoReturn:
|
|
raise RecordFluxError(
|
|
[ErrorEntry("oops", Severity.ERROR, Location((1, 1), Path("foo.rflx")))],
|
|
)
|
|
|
|
monkeypatch.setattr(cli, subcommand, patched)
|
|
monkeypatch.setattr(sys, "argv", ["rflx", "--legacy-errors", subcommand, "foo.rflx"])
|
|
|
|
assert cli.run() == 1
|
|
assert_stderr_regex(r"^foo.rflx:1:1: error: oops\n$", capfd)
|