summaryrefslogtreecommitdiffstats
path: root/src/debputy/plugin/api/test_api/test_spec.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/plugin/api/test_api/test_spec.py')
-rw-r--r--src/debputy/plugin/api/test_api/test_spec.py364
1 files changed, 364 insertions, 0 deletions
diff --git a/src/debputy/plugin/api/test_api/test_spec.py b/src/debputy/plugin/api/test_api/test_spec.py
new file mode 100644
index 0000000..b05f7ed
--- /dev/null
+++ b/src/debputy/plugin/api/test_api/test_spec.py
@@ -0,0 +1,364 @@
+import dataclasses
+import os
+from abc import ABCMeta
+from typing import (
+ Iterable,
+ Mapping,
+ Callable,
+ Optional,
+ Union,
+ List,
+ Tuple,
+ Set,
+ Sequence,
+ Generic,
+ Type,
+ Self,
+ FrozenSet,
+)
+
+from debian.substvars import Substvars
+
+from debputy import filesystem_scan
+from debputy.plugin.api import (
+ VirtualPath,
+ PackageProcessingContext,
+ DpkgTriggerType,
+ Maintscript,
+)
+from debputy.plugin.api.impl_types import PluginProvidedTrigger
+from debputy.plugin.api.spec import DSD, ServiceUpgradeRule, PathDef
+from debputy.substitution import VariableContext
+
+DEBPUTY_TEST_AGAINST_INSTALLED_PLUGINS = (
+ os.environ.get("DEBPUTY_TEST_PLUGIN_LOCATION", "uninstalled") == "installed"
+)
+
+
+@dataclasses.dataclass(slots=True, frozen=True)
+class ADRExampleIssue:
+ name: str
+ example_index: int
+ inconsistent_paths: Sequence[str]
+
+
+def build_virtual_file_system(
+ paths: Iterable[Union[str, PathDef]],
+ read_write_fs: bool = True,
+) -> VirtualPath:
+ """Create a pure-virtual file system for use with metadata detectors
+
+ This method will generate a virtual file system a list of path names or virtual path definitions. It will
+ also insert any implicit path required to make the file system connected. As an example:
+
+ >>> fs_root = build_virtual_file_system(['./usr/share/doc/package/copyright'])
+ >>> # The file we explicitly requested is obviously there
+ >>> fs_root.lookup('./usr/share/doc/package/copyright') is not None
+ True
+ >>> # but so is every directory up to that point
+ >>> all(fs_root.lookup(d).is_dir
+ ... for d in ['./usr', './usr/share', './usr/share/doc', './usr/share/doc/package']
+ ... )
+ True
+
+ Any string provided will be pased to `virtual_path` using all defaults for other parameters, making `str`
+ arguments a nice easy shorthand if you just want a path to exist, but do not really care about it otherwise
+ (or `virtual_path_def` defaults happens to work for you).
+
+ Here is a very small example of how to create some basic file system objects to get you started:
+
+ >>> from debputy.plugin.api import virtual_path_def
+ >>> path_defs = [
+ ... './usr/share/doc/', # Create a directory
+ ... virtual_path_def("./bin/zcat", link_target="/bin/gzip"), # Create a symlink
+ ... virtual_path_def("./bin/gzip", mode=0o755), # Create a file (with a custom mode)
+ ... ]
+ >>> fs_root = build_virtual_file_system(path_defs)
+ >>> fs_root.lookup('./usr/share/doc').is_dir
+ True
+ >>> fs_root.lookup('./bin/zcat').is_symlink
+ True
+ >>> fs_root.lookup('./bin/zcat').readlink() == '/bin/gzip'
+ True
+ >>> fs_root.lookup('./bin/gzip').is_file
+ True
+ >>> fs_root.lookup('./bin/gzip').mode == 0o755
+ True
+
+ :param paths: An iterable any mix of path names (str) and virtual_path_def definitions
+ (results from `virtual_path_def`).
+ :param read_write_fs: Whether the file system is read-write (True) or read-only (False).
+ Note that this is the default permission; the plugin test API may temporarily turn a
+ read-write to read-only temporarily (when running a metadata detector, etc.).
+ :return: The root of the generated file system
+ """
+ return filesystem_scan.build_virtual_fs(paths, read_write_fs=read_write_fs)
+
+
+@dataclasses.dataclass(slots=True, frozen=True)
+class RegisteredTrigger:
+ dpkg_trigger_type: DpkgTriggerType
+ dpkg_trigger_target: str
+
+ def serialized_format(self) -> str:
+ """The semantic contents of the DEBIAN/triggers file"""
+ return f"{self.dpkg_trigger_type} {self.dpkg_trigger_target}"
+
+ @classmethod
+ def from_plugin_provided_trigger(
+ cls,
+ plugin_provided_trigger: PluginProvidedTrigger,
+ ) -> "Self":
+ return cls(
+ plugin_provided_trigger.dpkg_trigger_type,
+ plugin_provided_trigger.dpkg_trigger_target,
+ )
+
+
+@dataclasses.dataclass(slots=True, frozen=True)
+class RegisteredMaintscript:
+ """Details about a maintscript registered by a plugin"""
+
+ """Which maintscript is applies to (e.g., "postinst")"""
+ maintscript: Maintscript
+ """Which method was used to trigger the script (e.g., "on_configure")"""
+ registration_method: str
+ """The snippet provided by the plugin as it was provided
+
+ That is, no indentation/conditions/substitutions have been applied to this text
+ """
+ plugin_provided_script: str
+ """Whether substitutions would have been applied in a production run"""
+ requested_substitution: bool
+
+
+@dataclasses.dataclass(slots=True, frozen=True)
+class DetectedService(Generic[DSD]):
+ path: VirtualPath
+ names: Sequence[str]
+ type_of_service: str
+ service_scope: str
+ enable_by_default: bool
+ start_by_default: bool
+ default_upgrade_rule: ServiceUpgradeRule
+ service_context: Optional[DSD]
+
+
+class RegisteredPackagerProvidedFile(metaclass=ABCMeta):
+ """Record of a registered packager provided file - No instantiation
+
+ New "mandatory" attributes may be added in minor versions, which means instantiation will break tests.
+ Plugin providers should therefore not create instances of this dataclass. It is visible only to aid
+ test writing by providing type-safety / auto-completion.
+ """
+
+ """The name stem used for generating the file"""
+ stem: str
+ """The recorded directory these file should be installed into"""
+ installed_path: str
+ """The mode that debputy will give these files when installed (unless overridden)"""
+ default_mode: int
+ """The default priority assigned to files unless overriden (if priories are assigned at all)"""
+ default_priority: Optional[int]
+ """The filename format to be used"""
+ filename_format: Optional[str]
+ """The formatting correcting callback"""
+ post_formatting_rewrite: Optional[Callable[[str], str]]
+
+ def compute_dest(
+ self,
+ assigned_name: str,
+ *,
+ assigned_priority: Optional[int] = None,
+ owning_package: Optional[str] = None,
+ path: Optional[VirtualPath] = None,
+ ) -> Tuple[str, str]:
+ """Determine the basename of this packager provided file
+
+ This method is useful for verifying that the `installed_path` and `post_formatting_rewrite` works
+ as intended. As example, some programs do not support "." in their configuration files, so you might
+ have a post_formatting_rewrite à la `lambda x: x.replace(".", "_")`. Then you can test it by
+ calling `assert rppf.compute_dest("python3.11")[1] == "python3_11"` to verify that if a package like
+ `python3.11` were to use this packager provided file, it would still generate a supported file name.
+
+ For the `assigned_name` parameter, then this is normally derived from the filename. Examples for
+ how to derive it:
+
+ * `debian/my-pkg.stem` => `my-pkg`
+ * `debian/my-pkg.my-custom-name.stem` => `my-custom-name`
+
+ Note that all parts (`my-pkg`, `my-custom-name` and `stem`) can contain periods (".") despite
+ also being a delimiter. Additionally, `my-custom-name` is not restricted to being a valid package
+ name, so it can have any file-system valid character in it.
+
+ For the 0.01% case, where the plugin is using *both* `{name}` *and* `{owning_package}` in the
+ installed_path, then you can separately *also* set the `owning_package` attribute. However, by
+ default the `assigned_named` is used for both when `owning_package` is not provided.
+
+ :param assigned_name: The name assigned. Usually this is the name of the package containing the file.
+ :param assigned_priority: Optionally a priority override for the file (if priority is supported). Must be
+ omitted/None if priorities are not supported.
+ :param owning_package: Optionally the name of the owning package. It is only needed for those exceedingly
+ rare cases where the `installed_path` contains both `{owning_package}` (usually in addition to `{name}`).
+ :param path: Special-case param, only needed for when testing a special `debputy` PPF..
+ :return: A tuple of the directory name and the basename (in that order) that combined makes up that path
+ that debputy would use.
+ """
+ raise NotImplementedError
+
+
+class RegisteredMetadata:
+ __slots__ = ()
+
+ @property
+ def substvars(self) -> Substvars:
+ """Returns the Substvars
+
+ :return: The substvars in their current state.
+ """
+ raise NotImplementedError
+
+ @property
+ def triggers(self) -> List[RegisteredTrigger]:
+ raise NotImplementedError
+
+ def maintscripts(
+ self,
+ *,
+ maintscript: Optional[Maintscript] = None,
+ ) -> List[RegisteredMaintscript]:
+ """Extract the maintscript provided by the given metadata detector
+
+ :param maintscript: If provided, only snippet registered for the given maintscript is returned. Can be
+ used to say "Give me all the 'postinst' snippets by this metadata detector", which can simplify
+ verification in some cases.
+ :return: A list of all matching maintscript registered by the metadata detector. If the detector has
+ not been run, then the list will be empty. If the metadata detector has been run multiple times,
+ then this is the aggregation of all the runs.
+ """
+ raise NotImplementedError
+
+
+class InitializedPluginUnderTest:
+ def packager_provided_files(self) -> Iterable[RegisteredPackagerProvidedFile]:
+ """An iterable of all packager provided files registered by the plugin under test
+
+ If you want a particular order, please sort the result.
+ """
+ return self.packager_provided_files_by_stem().values()
+
+ def packager_provided_files_by_stem(
+ self,
+ ) -> Mapping[str, RegisteredPackagerProvidedFile]:
+ """All packager provided files registered by the plugin under test grouped by name stem"""
+ raise NotImplementedError
+
+ def run_metadata_detector(
+ self,
+ metadata_detector_id: str,
+ fs_root: VirtualPath,
+ context: Optional[PackageProcessingContext] = None,
+ ) -> RegisteredMetadata:
+ """Run a metadata detector (by its ID) against a given file system
+
+ :param metadata_detector_id: The ID of the metadata detector to run
+ :param fs_root: The file system the metadata detector should see (must be the root of the file system)
+ :param context: The context the metadata detector should see. If not provided, one will be mock will be
+ provided to the extent possible.
+ :return: The metadata registered by the metadata detector
+ """
+ raise NotImplementedError
+
+ def run_package_processor(
+ self,
+ package_processor_id: str,
+ fs_root: VirtualPath,
+ context: Optional[PackageProcessingContext] = None,
+ ) -> None:
+ """Run a package processor (by its ID) against a given file system
+
+ Note: Dependency processors are *not* run first.
+
+ :param package_processor_id: The ID of the package processor to run
+ :param fs_root: The file system the package processor should see (must be the root of the file system)
+ :param context: The context the package processor should see. If not provided, one will be mock will be
+ provided to the extent possible.
+ """
+ raise NotImplementedError
+
+ @property
+ def declared_manifest_variables(self) -> Union[Set[str], FrozenSet[str]]:
+ """Extract the manifest variables declared by the plugin
+
+ :return: All manifest variables declared by the plugin
+ """
+ raise NotImplementedError
+
+ def automatic_discard_rules_examples_with_issues(self) -> Sequence[ADRExampleIssue]:
+ """Validate examples of the automatic discard rules
+
+ For any failed example, use `debputy plugin show automatic-discard-rules <name>` to see
+ the failed example in full.
+
+ :return: If any examples have issues, this will return a non-empty sequence with an
+ entry with each issue.
+ """
+ raise NotImplementedError
+
+ def run_service_detection_and_integrations(
+ self,
+ service_manager: str,
+ fs_root: VirtualPath,
+ context: Optional[PackageProcessingContext] = None,
+ *,
+ service_context_type_hint: Optional[Type[DSD]] = None,
+ ) -> Tuple[List[DetectedService[DSD]], RegisteredMetadata]:
+ """Run the service manager's detection logic and return the results
+
+ This method can be used to validate the service detection and integration logic of a plugin
+ for a given service manager.
+
+ First the service detector is run and if it finds any services, the integrator code is then
+ run on those services with their default values.
+
+ :param service_manager: The name of the service manager as provided during the initialization
+ :param fs_root: The file system the system detector should see (must be the root of
+ the file system)
+ :param context: The context the service detector should see. If not provided, one will be mock
+ will be provided to the extent possible.
+ :param service_context_type_hint: Unused; but can be used as a type hint for `mypy` (etc.)
+ to align the return type.
+ :return: A tuple of the list of all detected services in the provided file system and the
+ metadata generated by the integrator (if any services were detected).
+ """
+ raise NotImplementedError
+
+ def manifest_variables(
+ self,
+ *,
+ resolution_context: Optional[VariableContext] = None,
+ mocked_variables: Optional[Mapping[str, str]] = None,
+ ) -> Mapping[str, str]:
+ """Provide a table of the manifest variables registered by the plugin
+
+ Each key is a manifest variable and the value of said key is the value of the manifest
+ variable. Lazy loaded variables are resolved when accessed for the first time and may
+ raise exceptions if the preconditions are not correct.
+
+ Note this method can be called multiple times with different parameters to provide
+ different contexts. Lazy loaded variables are resolved at most once per context.
+
+ :param resolution_context: An optional context for lazy loaded manifest variables.
+ Create an instance of it via `manifest_variable_resolution_context`.
+ :param mocked_variables: An optional mapping that provides values for certain manifest
+ variables. This can be used if you want a certain variable to have a certain value
+ for the test to be stable (or because the manifest variable you are mocking is from
+ another plugin, and you do not want to deal with the implementation details of how
+ it is set). Any variable that depends on the mocked variable will use the mocked
+ variable in the given context.
+ :return: A table of the manifest variables provided by the plugin. Note this table
+ only contains manifest variables registered by the plugin. Attempting to resolve
+ other variables (directly), such as mocked variables or from other plugins, will
+ trigger a `KeyError`.
+ """
+ raise NotImplementedError