|
Server : Apache System : Linux server.mata-lashes.com 3.10.0-1160.90.1.el7.x86_64 #1 SMP Thu May 4 15:21:22 UTC 2023 x86_64 User : matalashes ( 1004) PHP Version : 8.1.29 Disable Function : NONE Directory : /usr/src/cloud-init/tests/unittests/ |
Upload File : |
# Copyright (C) 2020 Canonical Ltd.
#
# Author: Daniel Watkins <oddbloke@ubuntu.com>
#
# This file is part of cloud-init. See LICENSE file for license information.
"""
Tests for cloudinit.persistence.
Per https://docs.python.org/3/library/pickle.html, only "classes that are
defined at the top level of a module" can be pickled. This means that all of
our ``CloudInitPickleMixin`` subclasses for testing must be defined at
module-level (rather than being defined inline or dynamically in the body of
test methods, as we would do without this constraint).
``TestPickleMixin.test_subclasses`` iterates over a list of all of these
classes, and tests that they round-trip through a pickle dump/load. As the
interface we're testing is that ``_unpickle`` is called appropriately on
subclasses, our subclasses define their assertions in their ``_unpickle``
implementation. (This means that the assertions will not be executed if
``_unpickle`` is not called at all; we have
``TestPickleMixin.test_unpickle_called`` to ensure it is called.)
To avoid manually maintaining a list of classes for parametrization we use a
simple metaclass, ``_Collector``, to gather them up.
"""
import pickle
from typing import List, Type
from unittest import mock
import pytest
from cloudinit.persistence import CloudInitPickleMixin
class _Collector(type):
"""Any class using this as a metaclass will be stored in test_classes."""
test_classes: List[Type] = []
def __new__(cls, *args):
new_cls = super().__new__(cls, *args)
_Collector.test_classes.append(new_cls)
return new_cls
class InstanceVersionNotUsed(CloudInitPickleMixin, metaclass=_Collector):
"""Test that the class version is used over one set in instance state."""
_ci_pkl_version = 1
def __init__(self):
self._ci_pkl_version = 2
def _unpickle(self, ci_pkl_version: int) -> None:
assert 1 == ci_pkl_version
class MissingVersionHandled(CloudInitPickleMixin, metaclass=_Collector):
"""Test that pickles without ``_ci_pkl_version`` are handled gracefully.
This is tested by overriding ``__getstate__`` so the dumped pickle of this
class will not have ``_ci_pkl_version`` included.
"""
def __getstate__(self):
return self.__dict__
def _unpickle(self, ci_pkl_version: int) -> None:
assert 0 == ci_pkl_version
class OverridenVersionHonored(CloudInitPickleMixin, metaclass=_Collector):
"""Test that the subclass's version is used."""
_ci_pkl_version = 1
def _unpickle(self, ci_pkl_version: int) -> None:
assert 1 == ci_pkl_version
class StateIsRestored(CloudInitPickleMixin, metaclass=_Collector):
"""Instance state should be restored before ``_unpickle`` is called."""
def __init__(self):
self.some_state = "some state"
def _unpickle(self, ci_pkl_version: int) -> None:
assert "some state" == self.some_state
class UnpickleCanBeUnoverriden(CloudInitPickleMixin, metaclass=_Collector):
"""Subclasses should not need to override ``_unpickle``."""
class VersionDefaultsToZero(CloudInitPickleMixin, metaclass=_Collector):
"""Test that the default version is 0."""
def _unpickle(self, ci_pkl_version: int) -> None:
assert 0 == ci_pkl_version
class VersionIsPoppedFromState(CloudInitPickleMixin, metaclass=_Collector):
"""Test _ci_pkl_version is popped from state before being restored."""
def _unpickle(self, ci_pkl_version: int) -> None:
# `self._ci_pkl_version` returns the type's _ci_pkl_version if it isn't
# in instance state, so we need to explicitly check self.__dict__.
assert "_ci_pkl_version" not in self.__dict__
class TestPickleMixin:
def test_unpickle_called(self):
"""Test that self._unpickle is called on unpickle."""
with mock.patch.object(
CloudInitPickleMixin, "_unpickle"
) as m_unpickle:
pickle.loads(pickle.dumps(CloudInitPickleMixin()))
assert 1 == m_unpickle.call_count
@pytest.mark.parametrize("cls", _Collector.test_classes)
def test_subclasses(self, cls):
"""For each collected class, round-trip through pickle dump/load.
Assertions are implemented in ``cls._unpickle``, and so are evoked as
part of the pickle load.
"""
pickle.loads(pickle.dumps(cls()))