Remove package code
All checks were successful
builds.sr.ht/minimal_arch_test Job completed
builds.sr.ht/minimal_debian_test Job completed
builds.sr.ht/minimal_fedora_test Job completed
builds.sr.ht/full_debian_test Job completed
builds.sr.ht/website_deploy Job completed

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:
Matthew Fennell 2024-05-19 19:31:51 +01:00
parent 984685ada7
commit 2fa24cd847
Signed by: matthew
GPG key ID: AB49A7177B0ED3FE
30 changed files with 2 additions and 1650 deletions

View file

@ -21,7 +21,6 @@ packages:
- precious
- python3-distlib
- python3-pyfakefs
- python3-xdg
- reuse
- shellcheck
- yamllint

View file

@ -11,7 +11,6 @@ packages:
- openscenegraph
- python-distlib
- python-pyfakefs
- python-pyxdg
sources:
- https://source.motoristic.org/motoristic/motoristic
tasks:

View file

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

View file

@ -13,7 +13,6 @@ packages:
- libglvnd-devel
- python3-distlib
- python3-pyfakefs
- python3-pyxdg
sources:
- https://source.motoristic.org/motoristic/motoristic
tasks:

View file

@ -9,9 +9,6 @@ build-backend = "setuptools.build_meta"
[project]
name = "motoristic"
version = "0.0.1"
dependencies = [
"xdg.BaseDirectory",
]
[project.optional-dependencies]
test = [

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +0,0 @@
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
#
# SPDX-License-Identifier: AGPL-3.0-only

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,5 +2,4 @@
#
# SPDX-License-Identifier: AGPL-3.0-only
add_subdirectory(launcher)
add_subdirectory(model_viewer)

View file

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

View file

@ -1,3 +0,0 @@
# SPDX-FileCopyrightText: 2023 Matthew Fennell <matthew@fennell.dev>
#
# SPDX-License-Identifier: AGPL-3.0-only

View file

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

View file

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

View file

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

View file

@ -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"
),
),
},
},
)

View file

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

View file

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

View file

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

View file

@ -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",
],
)

View file

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

View file

@ -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([])