Remove package code
I would like to spend more time working on the core datatypes of the track, physics, AI etc. I feel we will not need this package-related code for some time. I feel it is holding me back and making things more complicated at the moment.
This commit is contained in:
parent
984685ada7
commit
2fa24cd847
|
@ -21,7 +21,6 @@ packages:
|
|||
- precious
|
||||
- python3-distlib
|
||||
- python3-pyfakefs
|
||||
- python3-xdg
|
||||
- reuse
|
||||
- shellcheck
|
||||
- yamllint
|
||||
|
|
|
@ -11,7 +11,6 @@ packages:
|
|||
- openscenegraph
|
||||
- python-distlib
|
||||
- python-pyfakefs
|
||||
- python-pyxdg
|
||||
sources:
|
||||
- https://source.motoristic.org/motoristic/motoristic
|
||||
tasks:
|
||||
|
|
|
@ -11,7 +11,6 @@ packages:
|
|||
- libopenscenegraph-dev
|
||||
- python3-distlib
|
||||
- python3-pyfakefs
|
||||
- python3-xdg
|
||||
sources:
|
||||
- https://source.motoristic.org/motoristic/motoristic
|
||||
# TODO #97 derive PATH automatically, without manual override
|
||||
|
|
|
@ -13,7 +13,6 @@ packages:
|
|||
- libglvnd-devel
|
||||
- python3-distlib
|
||||
- python3-pyfakefs
|
||||
- python3-pyxdg
|
||||
sources:
|
||||
- https://source.motoristic.org/motoristic/motoristic
|
||||
tasks:
|
||||
|
|
|
@ -9,9 +9,6 @@ build-backend = "setuptools.build_meta"
|
|||
[project]
|
||||
name = "motoristic"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"xdg.BaseDirectory",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
|
|
@ -5,21 +5,8 @@
|
|||
from sys import argv
|
||||
|
||||
from motoristic.launcher.options.parser import OptionsParser
|
||||
from motoristic.launcher.package.finder import PackageFinder
|
||||
from motoristic.launcher.package.hasher import Hasher
|
||||
from motoristic.launcher.package.lister import PackageLister
|
||||
from motoristic.launcher.package.search_paths import package_search_paths
|
||||
from motoristic.launcher.package.searcher import PackageSearcher
|
||||
from motoristic.launcher.package.selector import highest_precedence_package_selector
|
||||
|
||||
|
||||
def cli() -> int:
|
||||
packages = PackageLister(
|
||||
Hasher(), package_search_paths("motoristic")
|
||||
).list_packages()
|
||||
finder = PackageFinder(
|
||||
PackageSearcher(packages), highest_precedence_package_selector
|
||||
)
|
||||
parser = OptionsParser(finder)
|
||||
options = parser.parsed(argv[1:])
|
||||
OptionsParser().parse(argv[1:])
|
||||
return 0
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from motoristic.launcher.package.package import Package
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Options:
|
||||
car: Package
|
||||
track: Package
|
|
@ -1,54 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from argparse import Action, ArgumentError, ArgumentParser, Namespace
|
||||
from typing import Callable, Optional, Sequence, Union
|
||||
|
||||
from motoristic.launcher.package.finder import PackageFinder
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
|
||||
class PackageAction(Action):
|
||||
def __init__(
|
||||
self,
|
||||
option_strings: list[str],
|
||||
dest: str,
|
||||
finder: PackageFinder,
|
||||
const: Optional[bool] = None,
|
||||
default: Optional[Package] = None,
|
||||
type: Optional[Callable[[str], Package]] = None,
|
||||
choices: Optional[set[Package]] = None,
|
||||
required: bool = False,
|
||||
help: Optional[str] = None,
|
||||
metavar: Optional[str] = None,
|
||||
) -> None:
|
||||
self.finder = finder
|
||||
super().__init__(
|
||||
option_strings,
|
||||
dest,
|
||||
nargs=None,
|
||||
const=const,
|
||||
default=default,
|
||||
type=type,
|
||||
choices=choices,
|
||||
required=required,
|
||||
help=help,
|
||||
metavar=metavar,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
parser: ArgumentParser,
|
||||
namespace: Namespace,
|
||||
package_value: Optional[Union[str, Sequence[str]]],
|
||||
option_string: Optional[str] = None,
|
||||
) -> None:
|
||||
assert isinstance(package_value, str)
|
||||
assert option_string
|
||||
package_content = PackageContent.from_flag(option_string)
|
||||
assert package_content
|
||||
package = self.finder.find(package_value, package_content)
|
||||
if package is None:
|
||||
raise ArgumentError(self, f"Could not find package(s) {package_value}")
|
||||
setattr(namespace, self.dest, package)
|
|
@ -3,12 +3,6 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from pathlib import Path
|
||||
|
||||
from motoristic.launcher.options.options import Options
|
||||
from motoristic.launcher.options.package_action import PackageAction
|
||||
from motoristic.launcher.package.finder import PackageFinder
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
version = """%(prog)s 0.0.1
|
||||
Copyright (C) Motoristic contributors.
|
||||
|
@ -18,10 +12,7 @@ There is NO WARRANTY, to the extent permitted by law."""
|
|||
|
||||
|
||||
class OptionsParser:
|
||||
def __init__(self, finder: PackageFinder) -> None:
|
||||
self.finder = finder
|
||||
|
||||
def parsed(self, args: list[str]) -> Options:
|
||||
def parse(self, args: list[str]) -> None:
|
||||
parser = ArgumentParser(
|
||||
prog="motoristic",
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
|
@ -29,14 +20,4 @@ class OptionsParser:
|
|||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-v", "--version", action="version", version=version)
|
||||
for package_content in PackageContent:
|
||||
parser.add_argument(
|
||||
f"-{package_content.short_flag}",
|
||||
f"--{package_content.long_flag}",
|
||||
action=PackageAction,
|
||||
finder=self.finder,
|
||||
required=True,
|
||||
help=f"The name of, or path to, the {package_content.name} to use",
|
||||
)
|
||||
parsed_args = parser.parse_args(args)
|
||||
return Options(car=parsed_args.car, track=parsed_args.track)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
@ -1,28 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
from motoristic.launcher.package.searcher import PackageSearcher
|
||||
|
||||
|
||||
class PackageFinder:
|
||||
def __init__(
|
||||
self, searcher: PackageSearcher, selector: Callable[[list[Package]], Package]
|
||||
) -> None:
|
||||
self.searcher = searcher
|
||||
self.selector = selector
|
||||
|
||||
def find(self, search_term: str, content: PackageContent) -> Optional[Package]:
|
||||
packages_with_path = self.searcher.packages_with_path(Path(search_term))
|
||||
if packages_with_path:
|
||||
return self.selector(packages_with_path)
|
||||
packages_with_name_and_content = self.searcher.packages_with_name_and_content(
|
||||
search_term, content
|
||||
)
|
||||
if packages_with_name_and_content:
|
||||
return self.selector(packages_with_name_and_content)
|
||||
return None
|
|
@ -1,33 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from hashlib import sha512
|
||||
from pathlib import Path
|
||||
|
||||
from _hashlib import HASH
|
||||
|
||||
|
||||
class Hasher:
|
||||
chunk_bytes = 4096
|
||||
|
||||
def update_hash_with_file(self, file: Path, current_hash: HASH) -> HASH:
|
||||
with open(str(file), "rb") as current_file:
|
||||
for chunk in iter(lambda: current_file.read(self.chunk_bytes), b""):
|
||||
current_hash.update(chunk)
|
||||
return current_hash
|
||||
|
||||
def update_hash_with_dir(self, path: Path, current_hash: HASH) -> HASH:
|
||||
for subpath in sorted(
|
||||
Path(path).iterdir(), key=lambda current_path: str(current_path)
|
||||
):
|
||||
current_hash.update(subpath.name.encode())
|
||||
if subpath.is_file():
|
||||
current_hash = self.update_hash_with_file(subpath, current_hash)
|
||||
elif subpath.is_dir():
|
||||
current_hash = self.update_hash_with_dir(subpath, current_hash)
|
||||
return current_hash
|
||||
|
||||
def get_hash(self, path: Path) -> str:
|
||||
current_hash = self.update_hash_with_dir(path, sha512())
|
||||
return current_hash.hexdigest()
|
|
@ -1,57 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from itertools import product
|
||||
from os.path import isdir
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
from motoristic.launcher.package.hasher import Hasher
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
|
||||
class PackageLister:
|
||||
def __init__(self, hasher: Hasher, libraries: list[Path]) -> None:
|
||||
assert len(libraries) == len(set(libraries))
|
||||
self.hasher = hasher
|
||||
self.libraries = libraries
|
||||
|
||||
def _package_from_directory(self, package_root: Path) -> Package:
|
||||
return Package(
|
||||
name=package_root.name,
|
||||
package_hash=self.hasher.get_hash(package_root),
|
||||
path=package_root,
|
||||
)
|
||||
|
||||
def _package_group_path_from_library_root_and_package_type(
|
||||
self, package_content: PackageContent, library_root: Path
|
||||
) -> Path:
|
||||
return library_root.joinpath(package_content.directory_name)
|
||||
|
||||
def _packages_of_type_in_library(
|
||||
self, package_content: PackageContent, library_root: Path
|
||||
) -> set[Package]:
|
||||
package_group_root = (
|
||||
self._package_group_path_from_library_root_and_package_type(
|
||||
package_content, library_root
|
||||
)
|
||||
)
|
||||
if not package_group_root.is_dir():
|
||||
return set()
|
||||
candidates = filter(isdir, package_group_root.iterdir())
|
||||
return set(
|
||||
map(lambda candidate: self._package_from_directory(candidate), candidates)
|
||||
)
|
||||
|
||||
def list_packages(self) -> dict[tuple[PackageContent, Path], set[Package]]:
|
||||
library_package_content_permutations: product[
|
||||
tuple[PackageContent, Path]
|
||||
] = product(PackageContent, self.libraries)
|
||||
package_list: dict[tuple[PackageContent, Path], set[Package]] = {
|
||||
permutation: self._packages_of_type_in_library(
|
||||
package_content=permutation[0], library_root=permutation[1]
|
||||
)
|
||||
for permutation in library_package_content_permutations
|
||||
}
|
||||
return package_list
|
|
@ -1,59 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum, auto
|
||||
from pathlib import Path
|
||||
from typing import Optional, Self
|
||||
|
||||
|
||||
class PackageContent(StrEnum):
|
||||
CAR = (auto(), "c", "car")
|
||||
TRACK = (auto(), "t", "track")
|
||||
|
||||
# We would like to treat the name as the main value of the enum, with the
|
||||
# short and long flags being passed only being treated as additional
|
||||
# components.
|
||||
def __new__(cls, name: str, short_flag: str, long_flag: str) -> Self:
|
||||
package_content = str.__new__(cls, [name])
|
||||
package_content._value_ = name
|
||||
return package_content
|
||||
|
||||
def __init__(self, name: str, short_flag: str, long_flag: str) -> None:
|
||||
self._short_flag = short_flag
|
||||
self._long_flag = long_flag
|
||||
|
||||
# The flag-based functions are used by the options parser to discover the
|
||||
# PackageContent value the user is specifying given a particular flag.
|
||||
def from_flag(flag: str) -> Optional["PackageContent"]:
|
||||
flag_str: str = flag.strip("-").lower()
|
||||
for member in PackageContent:
|
||||
if flag_str == member.short_flag or flag_str == member.long_flag:
|
||||
return member
|
||||
return None
|
||||
|
||||
@property
|
||||
def short_flag(self) -> str:
|
||||
return self._short_flag
|
||||
|
||||
@property
|
||||
def long_flag(self) -> str:
|
||||
return self._long_flag
|
||||
|
||||
# This function is used by the package lister to determine the name of the
|
||||
# package group that packages of this content are stored in.
|
||||
@property
|
||||
def directory_name(self) -> str:
|
||||
return self._value_
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._value_
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Package:
|
||||
name: str
|
||||
package_hash: str
|
||||
path: Path
|
|
@ -1,14 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from xdg.BaseDirectory import load_data_paths
|
||||
|
||||
|
||||
# Postconditions:
|
||||
# - all the paths will exist and end in a directory named game_name
|
||||
# - there will be no duplicates in the returned list
|
||||
def package_search_paths(game_name: str) -> list[Path]:
|
||||
return list(dict.fromkeys(load_data_paths(game_name)))
|
|
@ -1,65 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
|
||||
class PackageSearcher:
|
||||
# The package groups given as keys in the packages dict should be given in
|
||||
# order of increasing precedence. For instance, /usr/share/... should come
|
||||
# before /usr/local/share/... This is because the ordering of package
|
||||
# groups is used to determine which packages should take higher precedence
|
||||
# in case the user searches for multiple packages with the same attributes.
|
||||
def __init__(
|
||||
self, packages: dict[tuple[PackageContent, Path], set[Package]]
|
||||
) -> None:
|
||||
self.packages = packages
|
||||
|
||||
# We reverse the order of the list here, as the packages in self.packages
|
||||
# are given from lowest precedence to highest precedence. However, we want
|
||||
# the highest precedence packages to be at the front of the list, as they
|
||||
# should take priority when deciding which of the matching packages should
|
||||
# be selected. Reversing the list here instead of in the search function
|
||||
# itself allows for all methods that search by content to share this
|
||||
# behaviour.
|
||||
def _packages_with_content(self, content: PackageContent) -> list[Package]:
|
||||
return list(
|
||||
reversed(
|
||||
[
|
||||
package
|
||||
for package_group, packages in self.packages.items()
|
||||
for package in packages
|
||||
if package_group[0] == content
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Even though we should only return (at most) one package from this
|
||||
# function, we return a list instead of an optional here for consistency
|
||||
# with the other search functions.
|
||||
def packages_with_path(self, path: Path) -> list[Package]:
|
||||
if not self.packages.values():
|
||||
return []
|
||||
all_packages: set[Package] = set.union(*self.packages.values())
|
||||
return list(filter(lambda package: package.path == path, all_packages))
|
||||
|
||||
def packages_with_name_and_content(
|
||||
self, name: str, content: PackageContent
|
||||
) -> list[Package]:
|
||||
return [
|
||||
package
|
||||
for package in self._packages_with_content(content)
|
||||
if package.name == name
|
||||
]
|
||||
|
||||
def packages_with_hash_and_content(
|
||||
self, package_hash: str, content: PackageContent
|
||||
) -> list[Package]:
|
||||
return [
|
||||
package
|
||||
for package in self._packages_with_content(content)
|
||||
if package.package_hash == package_hash
|
||||
]
|
|
@ -1,10 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from motoristic.launcher.package.package import Package
|
||||
|
||||
|
||||
def highest_precedence_package_selector(packages: list[Package]) -> Package:
|
||||
assert packages
|
||||
return packages[0]
|
|
@ -2,5 +2,4 @@
|
|||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
add_subdirectory(launcher)
|
||||
add_subdirectory(model_viewer)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
add_test(
|
||||
NAME launcher_test
|
||||
COMMAND "${Python_EXECUTABLE}" -m unittest discover
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
WORKING_DIRECTORY "$<TARGET_PROPERTY:shell,SOURCE_DIR>/src")
|
|
@ -1,3 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
@ -1,119 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from argparse import Action, ArgumentParser
|
||||
from typing import Callable, Optional, Union
|
||||
from unittest import TestCase
|
||||
from unittest.mock import ANY, MagicMock, Mock, patch
|
||||
|
||||
from motoristic.launcher.options.package_action import PackageAction
|
||||
|
||||
|
||||
class ExampleAction(Action):
|
||||
def __init__(
|
||||
self,
|
||||
option_strings: list[str],
|
||||
dest: str,
|
||||
extra_parameter: str,
|
||||
nargs: Union[str, int] = None,
|
||||
const: Optional[bool] = None,
|
||||
default: Optional[str] = None,
|
||||
type: Optional[Callable[[str], str]] = None,
|
||||
choices: Optional[set[str]] = None,
|
||||
required: bool = False,
|
||||
help: Optional[str] = None,
|
||||
metavar: Optional[str] = None,
|
||||
) -> None:
|
||||
self.extra_parameter = extra_parameter
|
||||
super().__init__(
|
||||
option_strings,
|
||||
dest,
|
||||
nargs=nargs,
|
||||
const=const,
|
||||
default=default,
|
||||
type=type,
|
||||
choices=choices,
|
||||
required=required,
|
||||
help=help,
|
||||
metavar=metavar,
|
||||
)
|
||||
|
||||
|
||||
# The package action tests make some assumpmtions about how argparse passes
|
||||
# arguments to subclasses of Action. These assumptions are baked into the tests
|
||||
# in TestPackageAction, as the test driver takes the role that argparse would
|
||||
# take in production code, namely calling the __init__ and __call__ methods of
|
||||
# the action.
|
||||
# Instead of writing full integration tests that run argparse directly and
|
||||
# check that packages are stored correctly, I have written PackageAction unit
|
||||
# tests in TestPackageAction and ArgParse tests here. These tests ensure that
|
||||
# the contract betwen the external argparse code and the PackageAction class is
|
||||
# not broken, and if it is, there are some tests that will fail.
|
||||
class TestArgParse(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.extra_parameter = "extra_parameter"
|
||||
self.parser = ArgumentParser(prog="example_program")
|
||||
self.parser.add_argument(
|
||||
"-e",
|
||||
"--example_flag",
|
||||
action=ExampleAction,
|
||||
extra_parameter=self.extra_parameter,
|
||||
)
|
||||
self.parser.add_argument("-o", "--other_flag")
|
||||
|
||||
# The call to add_argument raises an AttributeError here, as argparse
|
||||
# expects you to call super.__init__() to instantiate the Action's fields
|
||||
# (or at least for you to instantiate them manually).
|
||||
# Ideally, I would wrap my mock around the existing implementation given in
|
||||
# ExampleAction above. However, Python does not (from what I can tell)
|
||||
# offer an easy way to wrap an instance method. Therefore, there is no easy
|
||||
# way for me to initialise these values, and argparse raises an
|
||||
# AttributeError.
|
||||
# Thankfully, we only need to verify that our custom action's __init__() is
|
||||
# called with the expected values. Since this happens before the error is
|
||||
# raised, we can simply ignore the error by catching it in the except
|
||||
# block, before verifying that the expected values were passed.
|
||||
@patch.object(ExampleAction, "__init__", return_value=None)
|
||||
def test_extra_parameter_should_be_passed_to_parameter(
|
||||
self, action_init_method: MagicMock
|
||||
) -> None:
|
||||
try:
|
||||
parser = ArgumentParser(prog="example_program")
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--example_flag",
|
||||
action=ExampleAction,
|
||||
extra_parameter=self.extra_parameter,
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
finally:
|
||||
action_init_method.assert_called_with(
|
||||
option_strings=["-e", "--example_flag"],
|
||||
dest="example_flag",
|
||||
extra_parameter=self.extra_parameter,
|
||||
)
|
||||
|
||||
@patch.object(ExampleAction, "__call__", return_value=None)
|
||||
def test_should_not_call_action_if_action_not_given(
|
||||
self, action_call_method: MagicMock
|
||||
) -> None:
|
||||
self.parser.parse_args(["--other_flag", "unimportant_value"])
|
||||
action_call_method.assert_not_called()
|
||||
|
||||
@patch.object(ExampleAction, "__call__", return_value=None)
|
||||
def test_should_call_action_method_with_long_flag_if_given(
|
||||
self, action_call_method: MagicMock
|
||||
) -> None:
|
||||
self.parser.parse_args(["--example_flag", "example_value"])
|
||||
action_call_method.assert_called_with(
|
||||
ANY, ANY, "example_value", "--example_flag"
|
||||
)
|
||||
|
||||
@patch.object(ExampleAction, "__call__", return_value=None)
|
||||
def test_should_call_action_method_with_short_flag_if_given(
|
||||
self, action_call_method: MagicMock
|
||||
) -> None:
|
||||
self.parser.parse_args(["-e", "example_value"])
|
||||
action_call_method.assert_called_with(ANY, ANY, "example_value", "-e")
|
|
@ -1,120 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
from motoristic.launcher.package.finder import PackageFinder
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
from motoristic.launcher.package.searcher import PackageSearcher
|
||||
|
||||
|
||||
class TestPackageFinder(TestCase):
|
||||
def test_should_return_package_at_path_if_found(self) -> None:
|
||||
package_path = "/tmp/kart"
|
||||
package_to_find = Package(
|
||||
name="kart", package_hash="ae4fbe", path=Path(package_path)
|
||||
)
|
||||
searcher = Mock(spec=PackageSearcher)
|
||||
searcher.packages_with_path = MagicMock(return_value=[package_to_find])
|
||||
searcher.packages_with_name_and_content = MagicMock(return_value=[])
|
||||
selector = MagicMock(return_value=package_to_find)
|
||||
finder = PackageFinder(searcher, selector)
|
||||
|
||||
found_package = finder.find(package_path, PackageContent.CAR)
|
||||
|
||||
self.assertEqual(package_to_find, found_package)
|
||||
searcher.packages_with_path.assert_called_with(Path(package_path))
|
||||
selector.assert_called_with([package_to_find])
|
||||
|
||||
def test_should_return_package_with_name_if_found(self) -> None:
|
||||
package_path = "/usr/share/motoristic/car/kart"
|
||||
package_to_find = Package(
|
||||
name="kart", package_hash="ae4fbe", path=Path(package_path)
|
||||
)
|
||||
searcher = Mock(spec=PackageSearcher)
|
||||
searcher.packages_with_path = MagicMock(return_value=[])
|
||||
searcher.packages_with_name_and_content = MagicMock(
|
||||
return_value=[package_to_find]
|
||||
)
|
||||
selector = MagicMock(return_value=package_to_find)
|
||||
finder = PackageFinder(searcher, selector)
|
||||
|
||||
found_package = finder.find("kart", PackageContent.CAR)
|
||||
|
||||
self.assertEqual(package_to_find, found_package)
|
||||
searcher.packages_with_name_and_content.assert_called_with(
|
||||
"kart", PackageContent.CAR
|
||||
)
|
||||
selector.assert_called_with([package_to_find])
|
||||
|
||||
def test_package_by_path_should_take_precedence(self) -> None:
|
||||
package_by_path = Package(
|
||||
name="mortlake_park",
|
||||
package_hash="5e47f7",
|
||||
path=Path("/home/user1/mortlake_park"),
|
||||
)
|
||||
package_by_name_usr_share = Package(
|
||||
name="mortlake_park",
|
||||
package_hash="a1ae73",
|
||||
path=Path("/usr/share/motoristic/track/mortlake_park"),
|
||||
)
|
||||
package_by_name_usr_local_share = Package(
|
||||
name="mortlake_park",
|
||||
package_hash="89312a",
|
||||
path=Path("/usr/local/share/motoristic/track/mortlake_park"),
|
||||
)
|
||||
searcher = Mock(spec=PackageSearcher)
|
||||
searcher.packages_with_path = MagicMock(return_value=[package_by_path])
|
||||
searcher.packages_with_name_and_content = MagicMock(
|
||||
return_value=[package_by_name_usr_local_share, package_by_name_usr_share]
|
||||
)
|
||||
selector = MagicMock(return_value=package_by_path)
|
||||
finder = PackageFinder(searcher, selector)
|
||||
|
||||
found_package = finder.find("mortlake_park", PackageContent.TRACK)
|
||||
|
||||
self.assertEqual(package_by_path, found_package)
|
||||
searcher.packages_with_path.assert_called_with(Path("mortlake_park"))
|
||||
selector.assert_called_with([package_by_path])
|
||||
|
||||
def test_should_handle_multiple_packages_using_selector(self) -> None:
|
||||
package_in_usr_share = Package(
|
||||
name="mortlake_park",
|
||||
package_hash="a1ae73",
|
||||
path=Path("/usr/share/motoristic/track/mortlake_park"),
|
||||
)
|
||||
package_in_usr_local_share = Package(
|
||||
name="mortlake_park",
|
||||
package_hash="89312a",
|
||||
path=Path("/usr/local/share/motoristic/track/mortlake_park"),
|
||||
)
|
||||
searcher = Mock(spec=PackageSearcher)
|
||||
searcher.packages_with_path = MagicMock(return_value=[])
|
||||
searcher.packages_with_name_and_content = MagicMock(
|
||||
return_value=[package_in_usr_local_share, package_in_usr_share]
|
||||
)
|
||||
selector = MagicMock(return_value=package_in_usr_share)
|
||||
finder = PackageFinder(searcher, selector)
|
||||
|
||||
found_package = finder.find("mortlake_park", PackageContent.TRACK)
|
||||
|
||||
self.assertEqual(package_in_usr_share, found_package)
|
||||
searcher.packages_with_name_and_content.assert_called_with(
|
||||
"mortlake_park", PackageContent.TRACK
|
||||
)
|
||||
selector.assert_called_with([package_in_usr_local_share, package_in_usr_share])
|
||||
|
||||
def test_should_return_none_if_no_package_found_with_search_term(self) -> None:
|
||||
searcher = Mock(spec=PackageSearcher)
|
||||
searcher.packages_with_path = MagicMock(return_value=[])
|
||||
searcher.packages_with_name_and_content = MagicMock(return_value=[])
|
||||
selector = MagicMock(return_value=[])
|
||||
finder = PackageFinder(searcher, selector)
|
||||
|
||||
found_package = finder.find("non_existent_car", PackageContent.CAR)
|
||||
|
||||
self.assertEqual(None, found_package)
|
||||
selector.assert_not_called()
|
|
@ -1,124 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
from motoristic.launcher.package.hasher import Hasher
|
||||
from pyfakefs.fake_filesystem_unittest import TestCase
|
||||
|
||||
|
||||
# TODO #5 remove this ignore once pyfakefs supports mypy
|
||||
class TestHasher(TestCase): # type: ignore
|
||||
def _create_package_with_subdirs(
|
||||
self, path: str, name: str, subdirectories: list[str]
|
||||
) -> None:
|
||||
package_dir_path = os.path.join(path, name)
|
||||
if not os.path.exists(package_dir_path):
|
||||
self.fs.create_dir(package_dir_path)
|
||||
for subdirectory in subdirectories:
|
||||
self.fs.create_dir(os.path.join(package_dir_path, subdirectory))
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.setUpPyfakefs()
|
||||
|
||||
self._create_package_with_subdirs("/tmp/dir1", "package1", ["models"])
|
||||
self.fs.create_file(
|
||||
"/tmp/dir1/package1/models/model_1.example",
|
||||
contents="I am a model!",
|
||||
)
|
||||
|
||||
self._create_package_with_subdirs("/tmp/dir2", "package1", ["models"])
|
||||
self.fs.create_file(
|
||||
"/tmp/dir2/package1/models/model_1.example",
|
||||
contents="I am a model!",
|
||||
)
|
||||
|
||||
self._create_package_with_subdirs("/tmp/dir1", "package2", ["models"])
|
||||
self.fs.create_file(
|
||||
"/tmp/dir1/package2/models/model_1.example",
|
||||
contents="I am a model!",
|
||||
)
|
||||
|
||||
self._create_package_with_subdirs("/tmp/dir1", "package3", ["models"])
|
||||
self.fs.create_file(
|
||||
"/tmp/dir1/package3/models/model_1.example",
|
||||
contents="I am a model!",
|
||||
)
|
||||
self.fs.create_file(
|
||||
"/tmp/dir1/package3/models/model_2.example",
|
||||
contents="I am a new model",
|
||||
)
|
||||
|
||||
self._create_package_with_subdirs("/tmp/dir1", "package4", ["models"])
|
||||
self.fs.create_file(
|
||||
"/tmp/dir1/package4/models/model_1.example",
|
||||
contents="Different model",
|
||||
)
|
||||
|
||||
self._create_package_with_subdirs(
|
||||
"/tmp/dir1", "package5", ["models", "positions"]
|
||||
)
|
||||
self.fs.create_file(
|
||||
"/tmp/package5/models/model_1.example", contents="I am a model!"
|
||||
)
|
||||
self.fs.create_file(
|
||||
"/tmp/package5/positions/grid_spots",
|
||||
contents="Position1\nPosition2",
|
||||
)
|
||||
|
||||
self._create_package_with_subdirs(
|
||||
"/tmp/dir1", "package6", ["models", "empty_dir"]
|
||||
)
|
||||
self.fs.create_file(
|
||||
"/tmp/dir1/package6/models/model_1.example",
|
||||
contents="I am a model!",
|
||||
)
|
||||
|
||||
def test_hash_of_same_directory_should_be_equal(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
self.assertEqual(hash1, hash2)
|
||||
|
||||
def test_hash_of_same_package_in_different_directory_should_be_equal(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir2/package1"))
|
||||
self.assertEqual(hash1, hash2)
|
||||
|
||||
def test_hash_with_same_contents_but_different_name_should_be_equal(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir1/package2"))
|
||||
self.assertEqual(hash1, hash2)
|
||||
|
||||
def test_hash_with_extra_model_should_not_be_equal(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir1/package3"))
|
||||
self.assertNotEqual(hash1, hash2)
|
||||
|
||||
def test_hash_with_different_model_should_not_be_equal(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir1/package4"))
|
||||
self.assertNotEqual(hash1, hash2)
|
||||
|
||||
def test_hash_with_extra_position_files_should_not_be_equal(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir1/package5"))
|
||||
self.assertNotEqual(hash1, hash2)
|
||||
|
||||
# File metadata, such as last written time, should not affect the hash, since
|
||||
# the same package built at two different times should be compatible.
|
||||
def test_hash_should_be_the_same_after_touching_files(self) -> None:
|
||||
hash_before_touching_files = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
pathlib.Path("/tmp/dir1/package1/models/model_1.example").touch()
|
||||
hash_after_touching_files = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
self.assertEqual(hash_before_touching_files, hash_after_touching_files)
|
||||
|
||||
# We would like empty directories to be included in the hash, as in theory
|
||||
# an empty folder could change how the game handles the package.
|
||||
def test_hash_should_be_different_if_empty_directory_is_added(self) -> None:
|
||||
hash1 = Hasher().get_hash(Path("/tmp/dir1/package1"))
|
||||
hash2 = Hasher().get_hash(Path("/tmp/dir1/package6"))
|
||||
self.assertNotEqual(hash1, hash2)
|
|
@ -1,157 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
import pyfakefs.fake_filesystem_unittest
|
||||
from motoristic.launcher.package.hasher import Hasher
|
||||
from motoristic.launcher.package.lister import PackageLister
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
|
||||
# TODO #5 remove this ignore once pyfakefs supports mypy
|
||||
class TestPackageLister(pyfakefs.fake_filesystem_unittest.TestCase): # type: ignore
|
||||
def setUp(self) -> None:
|
||||
self.setUpPyfakefs()
|
||||
|
||||
self.fs.create_dir("/usr/share/motoristic")
|
||||
self.fs.create_dir("/usr/local/share/motoristic")
|
||||
self.fs.create_dir("/home/user1/local/.share/motoristic")
|
||||
|
||||
self.hasher = Hasher()
|
||||
self.lister = PackageLister(
|
||||
self.hasher,
|
||||
[
|
||||
Path("/usr/share/motoristic"),
|
||||
Path("/usr/local/share/motoristic"),
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_should_return_no_packages_if_no_packages_present(self) -> None:
|
||||
self.assertEqual(
|
||||
self.lister.list_packages(),
|
||||
{
|
||||
(PackageContent.CAR, Path("/usr/share/motoristic")): set(),
|
||||
(PackageContent.CAR, Path("/usr/local/share/motoristic")): set(),
|
||||
(
|
||||
PackageContent.CAR,
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
): set(),
|
||||
(PackageContent.TRACK, Path("/usr/share/motoristic")): set(),
|
||||
(PackageContent.TRACK, Path("/usr/local/share/motoristic")): set(),
|
||||
(
|
||||
PackageContent.TRACK,
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
): set(),
|
||||
},
|
||||
)
|
||||
|
||||
def test_should_raise_error_if_duplicate_search_path_provided(self) -> None:
|
||||
with self.assertRaises(AssertionError):
|
||||
PackageLister(
|
||||
Hasher(),
|
||||
[
|
||||
Path("/usr/share/motoristic"),
|
||||
Path("/usr/share/motoristic"),
|
||||
Path("/usr/local/share/motoristic"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_should_list_single_package(self) -> None:
|
||||
self.hasher.get_hash = MagicMock(
|
||||
return_value="20eea93593c180546c948b6c2342916935621b6ed133a7d2c7c67fb9045c36a0291c9932611d30472490416f96c0858dccb4b3c1055532eb733eec8b3fcce867"
|
||||
)
|
||||
lister = PackageLister(
|
||||
self.hasher,
|
||||
[
|
||||
Path("/usr/share/motoristic"),
|
||||
Path("/usr/local/share/motoristic"),
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
],
|
||||
)
|
||||
self.fs.create_dir("/usr/share/motoristic/car/kart")
|
||||
|
||||
self.assertEqual(
|
||||
self.lister.list_packages(),
|
||||
{
|
||||
(PackageContent.CAR, Path("/usr/share/motoristic")): {
|
||||
Package(
|
||||
name="kart",
|
||||
package_hash="20eea93593c180546c948b6c2342916935621b6ed133a7d2c7c67fb9045c36a0291c9932611d30472490416f96c0858dccb4b3c1055532eb733eec8b3fcce867",
|
||||
path=Path("/usr/share/motoristic/car/kart"),
|
||||
)
|
||||
},
|
||||
(PackageContent.CAR, Path("/usr/local/share/motoristic")): set(),
|
||||
(
|
||||
PackageContent.CAR,
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
): set(),
|
||||
(PackageContent.TRACK, Path("/usr/share/motoristic")): set(),
|
||||
(PackageContent.TRACK, Path("/usr/local/share/motoristic")): set(),
|
||||
(
|
||||
PackageContent.TRACK,
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
): set(),
|
||||
},
|
||||
)
|
||||
|
||||
def test_should_list_multiple_packages(self) -> None:
|
||||
hash_1 = "20eea93593c180546c948b6c2342916935621b6ed133a7d2c7c67fb9045c36a0291c9932611d30472490416f96c0858dccb4b3c1055532eb733eec8b3fcce867"
|
||||
hash_2 = "c310f18e1b3a4e526e1baf0f498cb7cb69bf3777ab775afb8bc3ddb3e64c122d49058814b601717777609847715d91e298568a0e5444b60452200a96d408b978"
|
||||
hash_3 = "15baffd3bf783077f8c6375f37246782fa31a740e7a3b73fc3afcf8d6815b8dbc3d90ecb588b6c1d5cd8e972b698dc97c711da51629db78a08924e35f7ee3444"
|
||||
self.hasher.get_hash = MagicMock(side_effect=[hash_1, hash_2, hash_1, hash_3])
|
||||
lister = PackageLister(
|
||||
self.hasher,
|
||||
[
|
||||
Path("/usr/share/motoristic"),
|
||||
Path("/usr/local/share/motoristic"),
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
],
|
||||
)
|
||||
self.fs.create_dir("/usr/share/motoristic/car/package_1")
|
||||
self.fs.create_dir("/usr/share/motoristic/car/package_2")
|
||||
self.fs.create_dir("/home/user1/.local/share/motoristic/car/package_1")
|
||||
self.fs.create_dir("/home/user1/.local/share/motoristic/track/package_3")
|
||||
|
||||
self.assertEqual(
|
||||
self.lister.list_packages(),
|
||||
{
|
||||
(PackageContent.CAR, Path("/usr/share/motoristic")): {
|
||||
Package(
|
||||
name="package_1",
|
||||
package_hash=hash_1,
|
||||
path=Path("/usr/share/motoristic/car/package_1"),
|
||||
),
|
||||
Package(
|
||||
name="package_2",
|
||||
package_hash=hash_2,
|
||||
path=Path("/usr/share/motoristic/car/package_2"),
|
||||
),
|
||||
},
|
||||
(PackageContent.CAR, Path("/usr/local/share/motoristic")): set(),
|
||||
(PackageContent.CAR, Path("/home/user1/.local/share/motoristic")): {
|
||||
Package(
|
||||
name="package_1",
|
||||
package_hash=hash_1,
|
||||
path=Path("/home/user1/.local/share/motoristic/car/package_1"),
|
||||
),
|
||||
},
|
||||
(PackageContent.TRACK, Path("/usr/share/motoristic")): set(),
|
||||
(PackageContent.TRACK, Path("/usr/local/share/motoristic")): set(),
|
||||
(
|
||||
PackageContent.TRACK,
|
||||
Path("/home/user1/.local/share/motoristic"),
|
||||
): {
|
||||
Package(
|
||||
name="package_3",
|
||||
package_hash=hash_3,
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/track/package_3"
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
|
@ -1,47 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from motoristic.launcher.package.package import PackageContent
|
||||
|
||||
|
||||
class TestPackage(TestCase):
|
||||
def test_should_return_car_from_car_strings(self) -> None:
|
||||
strings_that_should_convert_to_car = [
|
||||
"car",
|
||||
"CAR",
|
||||
"--car",
|
||||
"-c",
|
||||
]
|
||||
for car_string in strings_that_should_convert_to_car:
|
||||
with self.subTest(car_string):
|
||||
self.assertEqual(
|
||||
PackageContent.CAR, PackageContent.from_flag(car_string)
|
||||
)
|
||||
|
||||
def test_should_return_track_from_track_strings(self) -> None:
|
||||
strings_that_should_convert_to_track = [
|
||||
"track",
|
||||
"TRACK",
|
||||
"--track",
|
||||
"-t",
|
||||
]
|
||||
for track_string in strings_that_should_convert_to_track:
|
||||
with self.subTest(track_string):
|
||||
self.assertEqual(
|
||||
PackageContent.TRACK, PackageContent.from_flag(track_string)
|
||||
)
|
||||
|
||||
def test_should_return_short_flag(self) -> None:
|
||||
self.assertEqual("c", PackageContent.CAR.short_flag)
|
||||
self.assertEqual("t", PackageContent.TRACK.short_flag)
|
||||
|
||||
def test_should_return_long_flag(self) -> None:
|
||||
self.assertEqual("car", PackageContent.CAR.long_flag)
|
||||
self.assertEqual("track", PackageContent.TRACK.long_flag)
|
||||
|
||||
def test_should_return_directory_name(self) -> None:
|
||||
self.assertEqual("car", PackageContent.CAR.directory_name)
|
||||
self.assertEqual("track", PackageContent.TRACK.directory_name)
|
|
@ -1,71 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from argparse import ArgumentError, ArgumentParser, Namespace
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
from motoristic.launcher.options.package_action import PackageAction
|
||||
from motoristic.launcher.package.finder import PackageFinder
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
|
||||
class TestPackageAction(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.finder = Mock(spec=PackageFinder)
|
||||
self.namespace = Mock(spec=Namespace)
|
||||
self.parser = Mock(spec=ArgumentParser)
|
||||
|
||||
def test_should_save_package_with_path_if_found(self) -> None:
|
||||
for valid_car_flag in ["--car", "-c"]:
|
||||
with self.subTest(valid_car_flag):
|
||||
expected_package = Package(
|
||||
name="car", package_hash="cf2e54", path=Path("/tmp/car")
|
||||
)
|
||||
self.finder.find = MagicMock(return_value=expected_package)
|
||||
package_action = PackageAction(["-c", "--car"], "car", self.finder)
|
||||
|
||||
package_action(self.parser, self.namespace, "/tmp/car", valid_car_flag)
|
||||
|
||||
self.assertEqual(self.namespace.car, expected_package)
|
||||
self.finder.find.assert_called_with("/tmp/car", PackageContent.CAR)
|
||||
|
||||
def test_should_find_track_if_track_specified(self) -> None:
|
||||
for valid_track_flag in ["--track", "-t"]:
|
||||
with self.subTest(valid_track_flag):
|
||||
expected_package = Package(
|
||||
name="track", package_hash="771601", path=Path("/tmp/track")
|
||||
)
|
||||
self.finder.find = MagicMock(return_value=expected_package)
|
||||
package_action = PackageAction(["-t", "--track"], "track", self.finder)
|
||||
|
||||
package_action(
|
||||
self.parser, self.namespace, "/tmp/track", valid_track_flag
|
||||
)
|
||||
|
||||
self.assertEqual(self.namespace.track, expected_package)
|
||||
self.finder.find.assert_called_with("/tmp/track", PackageContent.TRACK)
|
||||
|
||||
# We should raise a ValueError to be consistent with the other actions
|
||||
# provided by argparse, and to allow the library to catch the error and
|
||||
# display a useful help message.
|
||||
def test_should_raise_value_error_if_finder_returns_no_packages(self) -> None:
|
||||
self.finder.find = MagicMock(return_value=None)
|
||||
package_action = PackageAction(["-t", "--track"], "track", self.finder)
|
||||
|
||||
with self.assertRaises(ArgumentError):
|
||||
package_action(self.parser, self.namespace, "/tmp/track", "--track")
|
||||
|
||||
def test_should_raise_assertion_error_if_option_string_not_found(self) -> None:
|
||||
package_action = PackageAction(["-t", "--track"], "track", self.finder)
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
package_action(self.parser, self.namespace, "/tmp/track", "--tra")
|
||||
|
||||
def test_should_raise_assertion_error_if_option_string_not_given(self) -> None:
|
||||
package_action = PackageAction(["-t", "--track"], "track", self.finder)
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
package_action(self.parser, self.namespace, "/tmp/track", None)
|
|
@ -1,69 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from argparse import ArgumentError
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, Mock, call
|
||||
|
||||
from motoristic.launcher.options.parser import OptionsParser
|
||||
from motoristic.launcher.package.finder import PackageFinder
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
|
||||
|
||||
class TestOptionsParser(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.finder = Mock(spec=PackageFinder)
|
||||
self.parser = OptionsParser(self.finder)
|
||||
|
||||
def test_should_ask_finder_for_package_with_given_search_terms(self) -> None:
|
||||
expected_car = Package(
|
||||
name="kart", package_hash="5685df", path=Path("/tmp/kart")
|
||||
)
|
||||
expected_track = Package(
|
||||
name="mortlake_park",
|
||||
package_hash="43c200",
|
||||
path=Path("/usr/share/motoristic/track/mortlake_park"),
|
||||
)
|
||||
|
||||
def finder_side_effect(
|
||||
search_term: str, package_content: PackageContent
|
||||
) -> Optional[Package]:
|
||||
if search_term == "/tmp/kart" and package_content == PackageContent.CAR:
|
||||
return expected_car
|
||||
if (
|
||||
search_term == "mortlake_park"
|
||||
and package_content == PackageContent.TRACK
|
||||
):
|
||||
return expected_track
|
||||
return None
|
||||
|
||||
self.finder.find.side_effect = finder_side_effect
|
||||
|
||||
options = self.parser.parsed(["--car", "/tmp/kart", "-t", "mortlake_park"])
|
||||
|
||||
self.assertEqual(options.car, expected_car)
|
||||
self.assertEqual(options.track, expected_track)
|
||||
|
||||
def test_should_exit_on_syntactically_invalid_args(
|
||||
self,
|
||||
) -> None:
|
||||
for invalid_args in [
|
||||
["--car"],
|
||||
["-t"],
|
||||
["-c", "car", "-t"],
|
||||
["--c", "kart", "--t", "mortlake_park"],
|
||||
["--ca", "kart", "--tr", "mortlake_park"],
|
||||
["-c", "-t"],
|
||||
]:
|
||||
with self.subTest(invalid_args):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.parser.parsed(invalid_args)
|
||||
|
||||
def test_should_exit_if_finder_cannot_find_packages(self) -> None:
|
||||
self.finder.find = MagicMock(return_value=None)
|
||||
|
||||
with self.assertRaises(SystemExit):
|
||||
options = self.parser.parsed(["--car", "/tmp/kart", "-t", "mortlake_park"])
|
|
@ -1,162 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
import motoristic.launcher.package.search_paths
|
||||
import xdg.BaseDirectory
|
||||
|
||||
# In this block, we import xdg.BaseDirectory. The xdg.BaseDirectory module
|
||||
# statically creates a value for xdg_data_home when it is first imported. This
|
||||
# is then used by load_data_paths, which is called by package_search_paths in
|
||||
# order to find the packages to search inside.
|
||||
#
|
||||
# In this test, we patch the $HOME environment variable in order to test
|
||||
# behaviour of package_search_paths with different values of xdg_data_home.
|
||||
#
|
||||
# However, since xdg.BaseDirectory is imported before we can patch $HOME,
|
||||
# xdg_data_home will default to the real $HOME on the system on which we are
|
||||
# currently running, causing unpredictable results.
|
||||
#
|
||||
# As a result, we need to reload xdg.BaseDirectory during the setUp of the
|
||||
# test, as this is after $HOME has been patched. However, in order to reload
|
||||
# the package, it needs to have been imported first. Imports of the style 'from
|
||||
# xdg.BaseDirectory import load_data_paths' are not sufficient - it has to be
|
||||
# of the style 'import xdg.BaseDirectory' to allow the reload to occur.
|
||||
#
|
||||
# This is not ideal, because the test is not strictly supposed to know that
|
||||
# package_search_paths uses xdg.BaseDirectory under the hood. But,
|
||||
# unfortunately, the test already needs to work around the library by reloading
|
||||
# it anyway, so it is what it is.
|
||||
from pyfakefs.fake_filesystem_unittest import TestCase
|
||||
|
||||
|
||||
# TODO #5 remove this ignore once pyfakefs supports mypy
|
||||
class TestSearchPaths(TestCase): # type: ignore
|
||||
def setUp(self) -> None:
|
||||
self.setUpPyfakefs()
|
||||
self.fs.create_dir("/home/user/.local/share/motoristic")
|
||||
self.fs.create_dir("/usr/local/share/motoristic")
|
||||
self.fs.create_dir("/usr/share/motoristic")
|
||||
self.fs.create_dir("/tmp/manual_data_home/motoristic")
|
||||
self.fs.create_dir("/tmp/manual_data_dirs/1/motoristic")
|
||||
self.fs.create_dir("/tmp/manual_data_dirs/2/motoristic")
|
||||
|
||||
def reload_modules_with_new_environment(self) -> None:
|
||||
import motoristic.launcher.package.search_paths
|
||||
import xdg.BaseDirectory
|
||||
|
||||
xdg.BaseDirectory = importlib.reload(xdg.BaseDirectory)
|
||||
motoristic.launcher.package.search_paths = importlib.reload(
|
||||
motoristic.launcher.package.search_paths
|
||||
)
|
||||
|
||||
@mock.patch.dict(os.environ, {"HOME": "/home/user"}, clear=True)
|
||||
def test_should_find_data_home_and_data_dirs(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
[
|
||||
"/home/user/.local/share/motoristic",
|
||||
"/usr/local/share/motoristic",
|
||||
"/usr/share/motoristic",
|
||||
],
|
||||
)
|
||||
|
||||
@mock.patch.dict(os.environ, {"HOME": "/home/nonexistentuser"}, clear=True)
|
||||
def test_should_not_find_data_home_of_non_existent_user(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
["/usr/local/share/motoristic", "/usr/share/motoristic"],
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"HOME": "/home/user", "XDG_DATA_HOME": "/tmp/manual_data_home"},
|
||||
clear=True,
|
||||
)
|
||||
def test_should_prefer_explicitly_given_data_home(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
[
|
||||
"/tmp/manual_data_home/motoristic",
|
||||
"/usr/local/share/motoristic",
|
||||
"/usr/share/motoristic",
|
||||
],
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HOME": "/home/user",
|
||||
"XDG_DATA_DIRS": "/tmp/manual_data_dirs/1:/tmp/manual_data_dirs/2",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_should_prefer_explicitly_given_data_dirs(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
[
|
||||
"/home/user/.local/share/motoristic",
|
||||
"/tmp/manual_data_dirs/1/motoristic",
|
||||
"/tmp/manual_data_dirs/2/motoristic",
|
||||
],
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{"HOME": "/home/user", "XDG_DATA_DIRS": "/tmp/manual_data_dirs/1"},
|
||||
clear=True,
|
||||
)
|
||||
def test_should_merge_duplicate_data_dirs(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
[
|
||||
"/home/user/.local/share/motoristic",
|
||||
"/tmp/manual_data_dirs/1/motoristic",
|
||||
],
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HOME": "/home/user",
|
||||
"XDG_DATA_HOME": "/tmp/manual_data_dirs/1",
|
||||
"XDG_DATA_DIRS": "/tmp/manual_data_dirs/1",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_should_merge_duplicate_data_home_and_data_dirs(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
["/tmp/manual_data_dirs/1/motoristic"],
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"HOME": "/home/user",
|
||||
"XDG_DATA_HOME": "/tmp/manual_data_home",
|
||||
"XDG_DATA_DIRS": "/tmp/manual_data_dirs/1:/tmp/manual_data_dirs/2",
|
||||
},
|
||||
clear=True,
|
||||
)
|
||||
def test_should_use_both_user_and_data_dirs(self) -> None:
|
||||
self.reload_modules_with_new_environment()
|
||||
self.assertEqual(
|
||||
motoristic.launcher.package.search_paths.package_search_paths("motoristic"),
|
||||
[
|
||||
"/tmp/manual_data_home/motoristic",
|
||||
"/tmp/manual_data_dirs/1/motoristic",
|
||||
"/tmp/manual_data_dirs/2/motoristic",
|
||||
],
|
||||
)
|
|
@ -1,346 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
import pyfakefs.fake_filesystem_unittest
|
||||
from motoristic.launcher.package.package import Package, PackageContent
|
||||
from motoristic.launcher.package.searcher import PackageSearcher
|
||||
|
||||
|
||||
class TestPackageSearcher(TestCase):
|
||||
def setUp(self) -> None:
|
||||
# This searcher is used in the majority of the tests. It is used to
|
||||
# check that both the filtering and ordering of packages returned by
|
||||
# the various search methods is handled correctly.
|
||||
self.many_package_searcher = PackageSearcher(
|
||||
{
|
||||
(PackageContent.CAR, Path("/usr/share/motoristic")): set(),
|
||||
(PackageContent.TRACK, Path("/usr/share/motoristic")): {
|
||||
Package(
|
||||
name="track1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/track/track1"),
|
||||
)
|
||||
},
|
||||
(PackageContent.CAR, Path("/usr/local/share/motoristic")): {
|
||||
Package(
|
||||
name="car1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/local/share/motoristic/car/car1"),
|
||||
)
|
||||
},
|
||||
(PackageContent.TRACK, Path("/usr/local/share/motoristic")): {
|
||||
Package(
|
||||
name="track1",
|
||||
package_hash="93c57f",
|
||||
path=Path("/usr/local/share/motoristic/track/track1"),
|
||||
)
|
||||
},
|
||||
(PackageContent.CAR, Path("/home/user1/.local/share/motoristic")): {
|
||||
Package(
|
||||
name="random_package_from_internet",
|
||||
package_hash="ca5be5",
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/car/random_package_from_internet"
|
||||
),
|
||||
)
|
||||
},
|
||||
(PackageContent.TRACK, Path("/home/user1/.local/share/motoristic")): {
|
||||
Package(
|
||||
name="random_package_from_internet",
|
||||
package_hash="b37a32",
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/track/random_package_from_internet"
|
||||
),
|
||||
),
|
||||
Package(
|
||||
name="random_package_from_internet-reskinned",
|
||||
package_hash="b37a32",
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/track/random_package_from_internet-reskinned"
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# These searchers are designed to ensure that the searcher can handle
|
||||
# either empty, or very sparsely populated packages being installed
|
||||
# (for instance, only one package being installed). While such
|
||||
# situations are unlikely in reality, the search methods should be able
|
||||
# to handle this without blowing up.
|
||||
self.no_library_searcher = PackageSearcher({})
|
||||
self.empty_library_searcher = PackageSearcher(
|
||||
{
|
||||
(PackageContent.CAR, Path("/usr/share/motoristic")): set(),
|
||||
(PackageContent.TRACK, Path("/usr/share/motoristic")): set(),
|
||||
}
|
||||
)
|
||||
self.single_package_searcher = PackageSearcher(
|
||||
{
|
||||
(PackageContent.CAR, Path("/usr/share/motoristic")): {
|
||||
Package(
|
||||
name="car1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/car/car1"),
|
||||
),
|
||||
},
|
||||
(PackageContent.TRACK, Path("/usr/share/motoristic")): set(),
|
||||
}
|
||||
)
|
||||
|
||||
def test_should_find_package_with_existing_path(self) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="track1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/track/track1"),
|
||||
)
|
||||
],
|
||||
self.many_package_searcher.packages_with_path(
|
||||
Path("/usr/share/motoristic/track/track1")
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_return_empty_list_if_no_such_package_exists(self) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.many_package_searcher.packages_with_path(
|
||||
Path("/tmp/non-existent-package")
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_return_no_package_if_no_libraries_available(self) -> None:
|
||||
self.assertEqual(
|
||||
[], self.no_library_searcher.packages_with_path(Path("/tmp/non-existent"))
|
||||
)
|
||||
|
||||
def test_should_return_single_package_if_matching_search(self) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="car1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/car/car1"),
|
||||
),
|
||||
],
|
||||
self.single_package_searcher.packages_with_path(
|
||||
Path("/usr/share/motoristic/car/car1")
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_return_no_package_if_only_empty_libraries(self) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.empty_library_searcher.packages_with_path(
|
||||
Path("/usr/share/motoristic/car/non-existent")
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_no_package_by_name_and_content_if_no_such_package_exists(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.many_package_searcher.packages_with_name_and_content(
|
||||
"non-existent-package", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_single_matching_package_by_name_and_content(self) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="car1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/local/share/motoristic/car/car1"),
|
||||
)
|
||||
],
|
||||
self.many_package_searcher.packages_with_name_and_content(
|
||||
"car1", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
# The packages should be returned with the package in /usr/local/share
|
||||
# before the package in /usr/share. This is because, when the searcher
|
||||
# is initialised, the lower precedence libraries are placed first.
|
||||
def test_should_find_packages_matching_name_and_content_in_library_order(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="track1",
|
||||
package_hash="93c57f",
|
||||
path=Path("/usr/local/share/motoristic/track/track1"),
|
||||
),
|
||||
Package(
|
||||
name="track1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/track/track1"),
|
||||
),
|
||||
],
|
||||
self.many_package_searcher.packages_with_name_and_content(
|
||||
"track1", PackageContent.TRACK
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_not_find_any_packages_by_name_and_content_when_no_libraries(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.no_library_searcher.packages_with_name_and_content(
|
||||
"non-existent", PackageContent.TRACK
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_not_find_any_packages_by_name_and_content_on_empty_libraries(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.empty_library_searcher.packages_with_name_and_content(
|
||||
"non-existent", PackageContent.TRACK
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_single_package_by_name_and_content(self) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="car1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/car/car1"),
|
||||
),
|
||||
],
|
||||
self.single_package_searcher.packages_with_name_and_content(
|
||||
"car1", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_no_package_by_hash_and_content_if_no_package_with_given_hash_exists(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.many_package_searcher.packages_with_hash_and_content(
|
||||
"5f8670", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_no_package_by_hash_and_content_if_no_package_with_given_content_exists(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.many_package_searcher.packages_with_hash_and_content(
|
||||
"93c57f", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_matching_package_if_package_with_given_hash_and_content_exists(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="random_package_from_internet",
|
||||
package_hash="ca5be5",
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/car/random_package_from_internet"
|
||||
),
|
||||
)
|
||||
],
|
||||
self.many_package_searcher.packages_with_hash_and_content(
|
||||
"ca5be5", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
# In this example, we have many packages that share the hash, including a
|
||||
# "track" that is actually a car (we can tell this, since it has the same
|
||||
# hash as many of the other cars) that the user has accidentally installed
|
||||
# into a track package group.
|
||||
# Note that packages with the same hash should be seen as functionally
|
||||
# equivalent by the game engine. In theory, there could be car or track
|
||||
# reskins could produce packages that look different, but behave the same,
|
||||
# and thus have the same hash. Therefore, such a scenario as below, where
|
||||
# there are multiple packages with the same hash, are not completely out of
|
||||
# the question.
|
||||
def test_should_find_packages_matching_hash_and_content_in_library_order(
|
||||
self,
|
||||
) -> None:
|
||||
found_packages = self.many_package_searcher.packages_with_hash_and_content(
|
||||
"b37a32", PackageContent.TRACK
|
||||
)
|
||||
expected_usr_share_package = Package(
|
||||
name="track1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/track/track1"),
|
||||
)
|
||||
expected_local_share_packages = {
|
||||
Package(
|
||||
name="random_package_from_internet",
|
||||
package_hash="b37a32",
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/track/random_package_from_internet"
|
||||
),
|
||||
),
|
||||
Package(
|
||||
name="random_package_from_internet-reskinned",
|
||||
package_hash="b37a32",
|
||||
path=Path(
|
||||
"/home/user1/.local/share/motoristic/track/random_package_from_internet-reskinned"
|
||||
),
|
||||
),
|
||||
}
|
||||
# The order of packages within a library is an implementation detail.
|
||||
# However, packages in higher precedence libraries should all appear
|
||||
# before any packages in lower precedence libraries. Since there is only
|
||||
# one package in /usr/share in this example, and /usr/share is the
|
||||
# lowest precedence library in which packages could be found, the
|
||||
# expected_usr_share_package must be the final package that is returned.
|
||||
# The order of the other packages does not matter, so long as they are
|
||||
# all present.
|
||||
self.assertCountEqual(
|
||||
{expected_usr_share_package} | expected_local_share_packages, found_packages
|
||||
)
|
||||
self.assertEqual(expected_usr_share_package, found_packages[-1])
|
||||
|
||||
def test_should_not_find_any_packages_by_hash_and_content_when_no_libraries(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.no_library_searcher.packages_with_hash_and_content(
|
||||
"b37a32", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_not_find_any_packages_by_hash_and_content_on_empty_libraries(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.empty_library_searcher.packages_with_hash_and_content(
|
||||
"b37a32", PackageContent.CAR
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_find_single_package_by_hash_and_content(self) -> None:
|
||||
self.assertEqual(
|
||||
[
|
||||
Package(
|
||||
name="car1",
|
||||
package_hash="b37a32",
|
||||
path=Path("/usr/share/motoristic/car/car1"),
|
||||
),
|
||||
],
|
||||
self.single_package_searcher.packages_with_hash_and_content(
|
||||
"b37a32", PackageContent.CAR
|
||||
),
|
||||
)
|
|
@ -1,45 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2024 Matthew Fennell <matthew@fennell.dev>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
from motoristic.launcher.package.package import Package
|
||||
from motoristic.launcher.package.selector import highest_precedence_package_selector
|
||||
|
||||
|
||||
class TestPackageSelector(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.high_precedence_package = Package(
|
||||
name="/usr/local/share/motoristic/car/kart",
|
||||
package_hash="fbeef8",
|
||||
path=Path("/usr/local/share/motoristic/car/kart"),
|
||||
)
|
||||
self.low_precedence_package = Package(
|
||||
name="/usr/share/motoristic/car/kart",
|
||||
package_hash="fbeef8",
|
||||
path=Path("/usr/share/motoristic/car/kart"),
|
||||
)
|
||||
|
||||
def test_should_return_single_package_if_single_package_given(self) -> None:
|
||||
self.assertEqual(
|
||||
self.high_precedence_package,
|
||||
highest_precedence_package_selector([self.high_precedence_package]),
|
||||
)
|
||||
|
||||
def test_should_return_highest_precedence_package_if_multiple_packages_given(
|
||||
self,
|
||||
) -> None:
|
||||
self.assertEqual(
|
||||
self.high_precedence_package,
|
||||
highest_precedence_package_selector(
|
||||
[self.high_precedence_package, self.low_precedence_package]
|
||||
),
|
||||
)
|
||||
|
||||
def test_should_raise_error_if_no_packages_given_to_select(
|
||||
self,
|
||||
) -> None:
|
||||
with self.assertRaises(AssertionError):
|
||||
highest_precedence_package_selector([])
|
Loading…
Reference in a new issue