forge-srpm-macros/tests/test_vectors.py
2024-08-13 16:56:46 -05:00

134 lines
4.2 KiB
Python

# Copyright (C) 2023 Maxwell G <maxwell@gtmx.me>
# SPDX-License-Identifier: GPL-1.0-or-later OR MIT
from __future__ import annotations
import dataclasses
from collections.abc import Iterable
from pathlib import Path
from typing import Any
import pytest
import yaml
HERE = Path(__file__).resolve().parent
VECTORS = HERE / "vectors.yaml"
SENTINEL = "*-*" * 20
@dataclasses.dataclass()
class _TestVector:
# Test id
id: str
# Longer description
description: str | None = None
# Macros to %define
defines: dict[str, str] = dataclasses.field(default_factory=dict)
# Macros to %undefine
undefines: list[str] = dataclasses.field(default_factory=list)
# String to --eval
evals: str | list[str] = dataclasses.field(default_factory=lambda: ["%forgemeta"])
# Whether the tests should fail
should_fail: bool = False
stdout_contains: str | None = None
stderr_contains: str | None = None
# Macros that should be set in the end
expected: dict[str, str] = dataclasses.field(default_factory=dict)
# Macros that should be unset in the end
expected_undefined: list[str] = dataclasses.field(default_factory=list)
# These macros should be the same with and without a zero at the end
zero_indexed: list[str] = dataclasses.field(default_factory=list)
def _param(item: dict[str, Any]):
case = _TestVector(**item)
return pytest.param(case, id=case.id)
def get_vectors() -> Iterable:
with VECTORS.open() as fp:
data = yaml.safe_load(fp)
for item in data["cases"]:
result = item.get("defines")
if not result or isinstance(result, dict):
yield _param(item)
continue
for index, defines in enumerate(result):
newitem = item | {"id": item["id"] + str(index), "defines": defines}
yield _param(newitem)
def join_expected(
lines: list[str], expected: dict[str, str]
) -> tuple[dict[str, str], dict[str, str]]:
final: dict[str, str] = {}
expected_expanded: dict[str, str] = {}
macros = iter(expected)
out: list[str] = []
macro = next(macros, ...)
if macro is ...:
return final, expected_expanded
for line in lines:
if line != SENTINEL:
out.append(line)
continue
if macro in final:
expected_expanded[macro] = "\n".join(out)
macro = next(macros, ...)
if macro is ...:
break
else:
final[macro] = "\n".join(out)
out.clear()
assert len(expected) == len(expected_expanded) == len(final)
return final, expected_expanded
@pytest.mark.parametrize("case", list(get_vectors()))
def test_vectors(case: _TestVector, evaluater):
expected_macros = case.expected.copy()
# Handle zero indexed
for macro in case.zero_indexed:
name = macro + "0"
expected_macros[name] = f"%{macro}"
# Handle expected_undefined
for macro in case.expected_undefined:
expected_macros["{!?" + macro + ":UNDEFINED}"] = "UNDEFINED"
if macro in expected_macros:
del expected_macros[macro]
# Set up evals
evals = [case.evals] if isinstance(case.evals, str) else case.evals.copy()
evals.append(SENTINEL)
for macro, expected in expected_macros.items():
evals.append(f"%{macro}")
evals.append(SENTINEL)
evals.append(expected)
evals.append(SENTINEL)
# Run commands
stdout: str
stderr: str
stdout, stderr = evaluater(evals, case.defines, case.undefines, case.should_fail)
if case.stderr_contains is not None:
assert case.stderr_contains.strip() in stderr
else:
# Help debugging
print(stderr)
if case.should_fail:
assert not expected_macros, "Cannot check expected macros in should_fail mode"
return
lines = stdout.splitlines()
idx = lines.index(SENTINEL)
user_stdout = "\n".join(lines[:idx])
macro_stdout_lines = lines[idx + 1 :]
# Check case.expected
final, expected_expanded = join_expected(macro_stdout_lines, expected_macros)
assert final == expected_expanded
if case.stdout_contains is not None:
assert case.stdout_contains.strip() in user_stdout