From 275ea8341aa1f8a58d6af31caad4844bef4f440c Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Sat, 14 Sep 2024 14:58:02 +0300 Subject: [PATCH] tests update --- src/ahriman/core/alpm/pkgbuild_parser.py | 20 ++- src/ahriman/core/exceptions.py | 19 +++ src/ahriman/models/pkgbuild.py | 2 +- .../ahriman/core/alpm/test_pkgbuild_parser.py | 139 ++++++++++++++++++ tests/ahriman/models/conftest.py | 31 +++- tests/ahriman/models/test_package.py | 1 - tests/ahriman/models/test_pkgbuild.py | 134 +++++++++++++++++ 7 files changed, 332 insertions(+), 14 deletions(-) diff --git a/src/ahriman/core/alpm/pkgbuild_parser.py b/src/ahriman/core/alpm/pkgbuild_parser.py index 8e357f70..c9e3b7e5 100644 --- a/src/ahriman/core/alpm/pkgbuild_parser.py +++ b/src/ahriman/core/alpm/pkgbuild_parser.py @@ -25,6 +25,7 @@ from collections.abc import Generator from enum import StrEnum from typing import IO +from ahriman.core.exceptions import PkgbuildParserError from ahriman.models.pkgbuild_patch import PkgbuildPatch @@ -92,7 +93,7 @@ class PkgbuildParser(shlex.shlex): list[str]: either source array or expanded array if possible Raises: - ValueError: if there are errors in parser + PkgbuildParserError: if there are errors in parser """ # we are using comma as marker for expansion (if any) if PkgbuildToken.Comma not in array: @@ -136,7 +137,7 @@ class PkgbuildParser(shlex.shlex): # small sanity check if prefix is not None: - raise ValueError(f"Could not expand `{array}` as array") + raise PkgbuildParserError("error in array expansion", array) return result @@ -149,7 +150,7 @@ class PkgbuildParser(shlex.shlex): list[str]: extracted arrays elements Raises: - ValueError: if array is not closed + PkgbuildParserError: if array is not closed """ def extract() -> Generator[str, None, None]: while token := self.get_token(): @@ -161,7 +162,7 @@ class PkgbuildParser(shlex.shlex): yield token if token != PkgbuildToken.ArrayEnds: - raise ValueError("No closing array bracket found") + raise PkgbuildParserError("no closing array bracket found") return self._expand_array(list(extract())) @@ -175,7 +176,7 @@ class PkgbuildParser(shlex.shlex): str: function body Raises: - ValueError: if function body wasn't found or parser input stream doesn't support position reading + PkgbuildParserError: if function body wasn't found or parser input stream doesn't support position reading """ # find start and end positions start_position, end_position = -1, -1 @@ -188,12 +189,19 @@ class PkgbuildParser(shlex.shlex): break if not 0 < start_position < end_position: - raise ValueError("Function body wasn't found") + raise PkgbuildParserError("function body wasn't found") # read the specified interval from source stream self._io.seek(start_position - 1) # start from the previous symbol content = self._io.read(end_position - start_position) + # special case of the end of file + if self.state == self.eof: # type: ignore[attr-defined] + content += self._io.read() + + # reset position (because the last position was before the next token starts) + self._io.seek(end_position) + return content def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]: diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py index 9fbe845b..1eb0cab9 100644 --- a/src/ahriman/core/exceptions.py +++ b/src/ahriman/core/exceptions.py @@ -234,6 +234,25 @@ class PacmanError(RuntimeError): RuntimeError.__init__(self, f"Could not perform operation with pacman: `{details}`") +class PkgbuildParserError(ValueError): + """ + exception raises in case of PKGBUILD parser errors + """ + + def __init__(self, reason: str, source: Any = None) -> None: + """ + default constructor + + Args: + reason(str): parser error reason + source(Any, optional): source line if available (Default value = None) + """ + message = f"Could not parse PKGBUILD: {reason}" + if source is not None: + message += f", source: `{source}`" + ValueError.__init__(self, message) + + class PathError(ValueError): """ exception which will be raised on path which is not belong to root directory diff --git a/src/ahriman/models/pkgbuild.py b/src/ahriman/models/pkgbuild.py index 72cd6e32..173b8107 100644 --- a/src/ahriman/models/pkgbuild.py +++ b/src/ahriman/models/pkgbuild.py @@ -73,7 +73,7 @@ class Pkgbuild(Mapping[str, Any]): parse PKGBUILD from input stream Args: - stream: IO[str]: input stream containing PKGBUILD content + stream(IO[str]): input stream containing PKGBUILD content Returns: Self: constructed instance of self diff --git a/tests/ahriman/core/alpm/test_pkgbuild_parser.py b/tests/ahriman/core/alpm/test_pkgbuild_parser.py index e69de29b..853d6b3e 100644 --- a/tests/ahriman/core/alpm/test_pkgbuild_parser.py +++ b/tests/ahriman/core/alpm/test_pkgbuild_parser.py @@ -0,0 +1,139 @@ +import pytest + +from io import StringIO + +from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser +from ahriman.core.exceptions import PkgbuildParserError +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +def test_expand_array() -> None: + """ + must correctly expand array + """ + assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs", ",", "-fortran}"]) == [ + "${pkgbase}", "${pkgbase}-libs", "${pkgbase}-fortran" + ] + assert PkgbuildParser._expand_array(["first", "prefix{1", ",", "2", ",", "3}suffix", "last"]) == [ + "first", "prefix1suffix", "prefix2suffix", "prefix3suffix", "last" + ] + + +def test_expand_array_no_comma() -> None: + """ + must skip array extraction if there is no comma + """ + assert PkgbuildParser._expand_array(["${pkgbase}{", "-libs", "-fortran}"]) == ["${pkgbase}{", "-libs", "-fortran}"] + + +def test_expand_array_short() -> None: + """ + must skip array extraction if it is short + """ + assert PkgbuildParser._expand_array(["${pkgbase}{", ","]) == ["${pkgbase}{", ","] + + +def test_expand_array_exception() -> None: + """ + must raise exception if there is unclosed element + """ + with pytest.raises(PkgbuildParserError): + assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"]) + + +def test_parse_array() -> None: + """ + must parse array + """ + parser = PkgbuildParser(StringIO("var=(first second)")) + assert list(parser.parse()) == [PkgbuildPatch("var", ["first", "second"])] + + +def test_parse_array_comment() -> None: + """ + must parse array with comments inside + """ + parser = PkgbuildParser(StringIO("""validpgpkeys=( + 'F3691687D867B81B51CE07D9BBE43771487328A9' # bpiotrowski@archlinux.org + '86CFFCA918CF3AF47147588051E8B148A9999C34' # evangelos@foutrelis.com + '13975A70E63C361C73AE69EF6EEB81F8981C74C7' # richard.guenther@gmail.com + 'D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62' # Jakub Jelinek +)""")) + assert list(parser.parse()) == [PkgbuildPatch("validpgpkeys", [ + "F3691687D867B81B51CE07D9BBE43771487328A9", + "86CFFCA918CF3AF47147588051E8B148A9999C34", + "13975A70E63C361C73AE69EF6EEB81F8981C74C7", + "D3A93CAD751C2AF4F8C7AD516C35B99309B5FA62", + ])] + + +def test_parse_array_exception() -> None: + """ + must raise exception if there is no closing bracket + """ + parser = PkgbuildParser(StringIO("var=(first second")) + with pytest.raises(PkgbuildParserError): + assert list(parser.parse()) + + +def test_parse_function() -> None: + """ + must parse function + """ + parser = PkgbuildParser(StringIO("var() { echo hello world } ")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ echo hello world }")] + + +def test_parse_function_eof() -> None: + """ + must parse function with "}" at the end of the file + """ + parser = PkgbuildParser(StringIO("var() { echo hello world }")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ echo hello world }")] + + +def test_parse_function_spaces() -> None: + """ + must parse function with spaces in declaration + """ + parser = PkgbuildParser(StringIO("var ( ) { echo hello world } ")) + assert list(parser.parse()) == [PkgbuildPatch("var()", "{ echo hello world }")] + + +def test_parse_function_exception() -> None: + """ + must raise exception if no bracket found + """ + parser = PkgbuildParser(StringIO("var() echo hello world } ")) + with pytest.raises(PkgbuildParserError): + assert list(parser.parse()) + + parser = PkgbuildParser(StringIO("var() { echo hello world")) + with pytest.raises(PkgbuildParserError): + assert list(parser.parse()) + + +def test_parse_token_assignment() -> None: + """ + must parse simple assignment + """ + parser = PkgbuildParser(StringIO()) + assert next(parser._parse_token("var=value")) == PkgbuildPatch("var", "value") + assert next(parser._parse_token("var=$value")) == PkgbuildPatch("var", "$value") + assert next(parser._parse_token("var=${value}")) == PkgbuildPatch("var", "${value}") + assert next(parser._parse_token("var=${value/-/_}")) == PkgbuildPatch("var", "${value/-/_}") + + +def test_parse_token_comment() -> None: + """ + must correctly parse comment + """ + parser = PkgbuildParser(StringIO("""first=1 # comment + # comment line + second=2 + #third=3 + """)) + assert list(parser.parse()) == [ + PkgbuildPatch("first", "1"), + PkgbuildPatch("second", "2"), + ] diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index b60fec97..6dae3366 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -1,5 +1,6 @@ import pytest +from pathlib import Path from unittest.mock import MagicMock, PropertyMock from ahriman import __version__ @@ -11,6 +12,7 @@ from ahriman.models.internal_status import InternalStatus from ahriman.models.package import Package from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource +from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.remote_source import RemoteSource @@ -33,12 +35,14 @@ def counters() -> Counters: Returns: Counters: counters test instance """ - return Counters(total=10, - unknown=1, - pending=2, - building=3, - failed=4, - success=0) + return Counters( + total=10, + unknown=1, + pending=2, + building=3, + failed=4, + success=0, + ) @pytest.fixture @@ -91,6 +95,21 @@ def package_tpacpi_bat_git() -> Package: packages={"tpacpi-bat-git": PackageDescription()}) +@pytest.fixture +def pkgbuild_ahriman(resource_path_root: Path) -> Pkgbuild: + """ + pkgbuild fixture + + Args: + resource_path_root(Path): resource path root directory + + Returns: + Pkgbuild: pkgbuild test instance + """ + pkgbuild = resource_path_root / "models" / "package_ahriman_pkgbuild" + return Pkgbuild.from_file(pkgbuild) + + @pytest.fixture def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock: """ diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index f4fca4af..01a50e41 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -1,5 +1,4 @@ from pathlib import Path - from pytest_mock import MockerFixture from unittest.mock import MagicMock diff --git a/tests/ahriman/models/test_pkgbuild.py b/tests/ahriman/models/test_pkgbuild.py index e69de29b..5cdb0bc5 100644 --- a/tests/ahriman/models/test_pkgbuild.py +++ b/tests/ahriman/models/test_pkgbuild.py @@ -0,0 +1,134 @@ +import pytest + +from io import StringIO +from pathlib import Path +from pytest_mock import MockerFixture + +from ahriman.models.pkgbuild import Pkgbuild +from ahriman.models.pkgbuild_patch import PkgbuildPatch + + +def test_variables(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must correctly generate list of variables + """ + assert pkgbuild_ahriman.variables + assert "pkgver" in pkgbuild_ahriman.variables + assert "build" not in pkgbuild_ahriman.variables + assert "source" not in pkgbuild_ahriman.variables + + +def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must correctly load from file + """ + open_mock = mocker.patch("pathlib.Path.open") + load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman) + + assert Pkgbuild.from_file(Path("local")) + open_mock.assert_called_once_with() + load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) + + +def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must correctly load from io + """ + load_mock = mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", + return_value=pkgbuild_ahriman.fields.values()) + assert Pkgbuild.from_io(StringIO("mock")) == pkgbuild_ahriman + load_mock.assert_called_once_with() + + +def test_from_io_pkgbase(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must assign missing pkgbase if pkgname is presented + """ + mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", side_effect=[ + [value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase",)], + [value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase", "pkgname",)], + [value for key, value in pkgbuild_ahriman.fields.items()] + [PkgbuildPatch("pkgbase", "pkgbase")], + ]) + + assert Pkgbuild.from_io(StringIO("mock"))["pkgbase"] == pkgbuild_ahriman["pkgname"] + assert "pkgbase" not in Pkgbuild.from_io(StringIO("mock")) + assert Pkgbuild.from_io(StringIO("mock"))["pkgbase"] == "pkgbase" + + +def test_from_io_empty(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: + """ + must skip empty patches + """ + mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", + return_value=list(pkgbuild_ahriman.fields.values()) + [PkgbuildPatch("", "")]) + assert Pkgbuild.from_io(StringIO("mock")) == pkgbuild_ahriman + + +def test_packages(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must correctly generate load package function + """ + assert pkgbuild_ahriman.packages() == {pkgbuild_ahriman["pkgbase"]: Pkgbuild({})} + + +def test_packages_multi(resource_path_root: Path) -> None: + """ + must correctly generate load list of package functions + """ + pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_gcc10_pkgbuild") + packages = pkgbuild.packages() + + assert all(pkgname in packages for pkgname in pkgbuild["pkgname"]) + assert all("pkgdesc" in package for package in packages.values()) + assert all("depends" in package for package in packages.values()) + + +def test_getitem(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return element by key + """ + assert pkgbuild_ahriman["pkgname"] == pkgbuild_ahriman.fields["pkgname"].value + assert pkgbuild_ahriman["build()"] == pkgbuild_ahriman.fields["build()"].substitute(pkgbuild_ahriman.variables) + + +def test_getitem_substitute(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return element by key and substitute variables + """ + pkgbuild_ahriman.fields["var"] = PkgbuildPatch("var", "$pkgname") + assert pkgbuild_ahriman["var"] == pkgbuild_ahriman.fields["pkgname"].value + + +def test_getitem_function(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return element by key with fallback to function + """ + assert pkgbuild_ahriman["build"] == pkgbuild_ahriman.fields["build()"].substitute(pkgbuild_ahriman.variables) + + pkgbuild_ahriman.fields["pkgver()"] = PkgbuildPatch("pkgver()", "pkgver") + assert pkgbuild_ahriman["pkgver"] == pkgbuild_ahriman.fields["pkgver"].value + assert pkgbuild_ahriman["pkgver()"] == pkgbuild_ahriman.fields["pkgver()"].value + + +def test_getitem_exception(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must raise KeyError for unknown key + """ + with pytest.raises(KeyError): + assert pkgbuild_ahriman["field"] + + +def test_iter(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return keys iterator + """ + for key in list(pkgbuild_ahriman): + del pkgbuild_ahriman.fields[key] + assert not pkgbuild_ahriman.fields + + +def test_len(pkgbuild_ahriman: Pkgbuild) -> None: + """ + must return length of the map + """ + assert len(pkgbuild_ahriman) == len(pkgbuild_ahriman.fields)