|
4 | 4 | from collections import deque |
5 | 5 | from contextlib import contextmanager |
6 | 6 | 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 |
9 | 9 |
|
10 | 10 | import pytest |
11 | 11 |
|
@@ -632,6 +632,164 @@ def get_connection(self, shell: Shell) -> Connection: |
632 | 632 | raise ValueError(f"Unknown connection type: {conn_type}!") |
633 | 633 |
|
634 | 634 |
|
| 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 | + |
635 | 793 | HostType = TypeVar("HostType", bound=MultihostHost) |
636 | 794 |
|
637 | 795 |
|
|
0 commit comments