Skip to content

Commit 21713d5

Browse files
committed
multihost: add MultihostBackupHost
This is a base host that provides automatic host backup and restore functionality.
1 parent 6de116d commit 21713d5

File tree

2 files changed

+162
-2
lines changed

2 files changed

+162
-2
lines changed

pytest_mh/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from ._private.marks import KnownTopologyBase, KnownTopologyGroupBase, TopologyMark
1919
from ._private.multihost import (
20+
MultihostBackupHost,
2021
MultihostConfig,
2122
MultihostDomain,
2223
MultihostHost,
@@ -47,6 +48,7 @@
4748
"MultihostDomain",
4849
"MultihostFixture",
4950
"MultihostHost",
51+
"MultihostBackupHost",
5052
"MultihostHostArtifacts",
5153
"MultihostItemData",
5254
"MultihostOSFamily",

pytest_mh/_private/multihost.py

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from collections import deque
55
from contextlib import contextmanager
66
from functools import wraps
7-
from pathlib import Path
8-
from typing import TYPE_CHECKING, Any, Generator, Generic, Self, Type, TypeVar
7+
from pathlib import Path, PurePath
8+
from typing import TYPE_CHECKING, Any, Generator, Generic, Self, Sequence, Type, TypeVar
99

1010
import pytest
1111

@@ -632,6 +632,164 @@ def get_connection(self, shell: Shell) -> Connection:
632632
raise ValueError(f"Unknown connection type: {conn_type}!")
633633

634634

635+
class MultihostBackupHost(MultihostHost[DomainType], ABC):
636+
"""
637+
Abstract class implementing automatic backup and restore for a host.
638+
639+
A backup of the host is created once when pytest starts and the host is
640+
restored automatically (unless disabled) when a test run is finished.
641+
642+
If the backup data is stored as :class:`~pathlib.PurePath` or a sequence of
643+
:class:`~pathlib.PurePath`, the file is automatically removed from the host
644+
when all tests are finished. Otherwise no action is done -- it is possible
645+
to overwrite :meth:`remove_backup` to clean up your data if needed.
646+
647+
It is required to implement :meth:`start`, :meth:`stop`, :meth:`backup` and
648+
:meth:`restore`. The :meth:`start` method is called in :meth:`pytest_setup`
649+
unless ``auto_start`` is set to False and the implementation of this method
650+
may raise ``NotImplementedError`` which will be ignored.
651+
652+
By default, the host is reverted when each test run is finished. This may
653+
not always be desirable and can be disabled via ``auto_restore`` parameter
654+
of the constructor.
655+
"""
656+
657+
def __init__(self, *args, auto_start: bool = True, auto_restore: bool = True, **kwargs) -> None:
658+
"""
659+
:param auto_start: Automatically start service before taking the first
660+
backup.
661+
:type auto_restore: bool, optional
662+
:param auto_restore: If True, the host is automatically restored to the
663+
backup state when a test is finished in :meth:`teardown`, defaults
664+
to True
665+
:type auto_restore: bool, optional
666+
"""
667+
super().__init__(*args, **kwargs)
668+
669+
self.backup_data: PurePath | Sequence[PurePath] | Any | None = None
670+
"""Backup data of vanilla state of this host."""
671+
672+
self._backup_auto_start: bool = auto_start
673+
"""
674+
If True, the host is automatically started prior taking the first
675+
backup.
676+
"""
677+
678+
self._backup_auto_restore: bool = auto_restore
679+
"""
680+
If True, the host is automatically restored to the backup state when a
681+
test is finished in :meth:`teardown`.
682+
"""
683+
684+
def pytest_setup(self) -> None:
685+
"""
686+
Start the services via :meth:`start` and take a backup by calling
687+
:meth:`backup`.
688+
"""
689+
# Make sure required services are running
690+
if self._backup_auto_start:
691+
try:
692+
self.start()
693+
except NotImplementedError:
694+
pass
695+
696+
# Create backup of initial state
697+
self.backup_data = self.backup()
698+
699+
def pytest_teardown(self) -> None:
700+
"""
701+
Remove backup files from the host (calls :meth:`remove_backup`).
702+
"""
703+
self.remove_backup(self.backup_data)
704+
705+
def teardown(self) -> None:
706+
"""
707+
Restore the host from the backup by calling :meth:`restore`.
708+
"""
709+
if self._backup_auto_restore:
710+
self.restore(self.backup_data)
711+
712+
super().teardown()
713+
714+
def remove_backup(self, backup_data: PurePath | Sequence[PurePath] | Any | None) -> None:
715+
"""
716+
Remove backup data from the host.
717+
718+
If backup_data is not :class:`~pathlib.PurePath` or a sequence of
719+
:class:`~pathlib.PurePath`, this will not have any effect. Otherwise,
720+
the paths are removed from the host.
721+
722+
:param backup_data: Backup data.
723+
:type backup_data: PurePath | Sequence[PurePath] | Any | None
724+
"""
725+
if backup_data is None:
726+
return
727+
728+
if isinstance(backup_data, PurePath):
729+
backup_data = [backup_data]
730+
731+
if isinstance(backup_data, Sequence):
732+
only_paths = True
733+
for item in backup_data:
734+
if not isinstance(item, PurePath):
735+
only_paths = False
736+
break
737+
738+
if only_paths:
739+
if isinstance(self.conn.shell, Powershell):
740+
for item in backup_data:
741+
path = str(item)
742+
self.conn.exec(["Remove-Item", "-Force", "-Recurse", path])
743+
else:
744+
for item in backup_data:
745+
path = str(item)
746+
self.conn.exec(["rm", "-fr", path])
747+
748+
@abstractmethod
749+
def start(self) -> None:
750+
"""
751+
Start required services.
752+
753+
:raises NotImplementedError: If start operation is not supported.
754+
"""
755+
pass
756+
757+
@abstractmethod
758+
def stop(self) -> None:
759+
"""
760+
Stop required services.
761+
762+
:raises NotImplementedError: If stop operation is not supported.
763+
"""
764+
pass
765+
766+
@abstractmethod
767+
def backup(self) -> PurePath | Sequence[PurePath] | Any | None:
768+
"""
769+
Backup backend data.
770+
771+
Returns directory or file path where the backup is stored (as
772+
:class:`~pathlib.PurePath` or sequence of :class:`~pathlib.PurePath`) or
773+
any Python data relevant for the backup. This data is passed to
774+
:meth:`restore` which will use this information to restore the host to
775+
its original state.
776+
777+
:return: Backup data.
778+
:rtype: PurePath | Sequence[PurePath] | Any | None
779+
"""
780+
pass
781+
782+
@abstractmethod
783+
def restore(self, backup_data: Any | None) -> None:
784+
"""
785+
Restore data from the backup.
786+
787+
:param backup_data: Backup data.
788+
:type backup_data: PurePath | Sequence[PurePath] | Any | None
789+
"""
790+
pass
791+
792+
635793
HostType = TypeVar("HostType", bound=MultihostHost)
636794

637795

0 commit comments

Comments
 (0)