Compare commits

..

2 Commits

7 changed files with 332 additions and 14 deletions

View File

@ -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]:

View File

@ -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

View File

@ -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

View File

@ -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 <jakub@redhat.com>
)"""))
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"),
]

View File

@ -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,
return Counters(
total=10,
unknown=1,
pending=2,
building=3,
failed=4,
success=0)
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:
"""

View File

@ -1,5 +1,4 @@
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock

View File

@ -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)