|
| 1 | +import importlib |
| 2 | +import re |
| 3 | +import sys |
| 4 | +import warnings |
| 5 | +from pathlib import Path |
| 6 | +from types import ModuleType |
| 7 | +from typing import Dict, List, Optional, Set, Tuple, TypedDict, cast |
| 8 | + |
| 9 | +import pytest |
| 10 | + |
| 11 | +from juju.client import connection |
| 12 | + |
| 13 | + |
| 14 | +class Versions(TypedDict, total=True): |
| 15 | + versions: List[int] |
| 16 | + |
| 17 | + |
| 18 | +ClientFacades = Dict[str, Versions] |
| 19 | + |
| 20 | + |
| 21 | +@pytest.fixture |
| 22 | +def project_root(request: pytest.FixtureRequest): |
| 23 | + return request.config.rootpath |
| 24 | + |
| 25 | + |
| 26 | +class TestFacades: |
| 27 | + def test_client_facades(self, project_root: Path) -> None: |
| 28 | + good_facades = self.generate_client_facades(project_root) |
| 29 | + client_facades = cast(ClientFacades, connection.client_facades) |
| 30 | + |
| 31 | + errors: List[Tuple[str, Optional[List[int]], Optional[List[int]]]] = [] |
| 32 | + all_names = sorted(set(connection.client_facades).union(good_facades))#, key=str.swapcase) |
| 33 | + for name in all_names: |
| 34 | + expected = self._versions_from_facades(good_facades, name) |
| 35 | + actual = self._versions_from_facades(client_facades, name) |
| 36 | + if expected != actual: |
| 37 | + errors.append((name, expected, actual)) |
| 38 | + |
| 39 | + if errors: |
| 40 | + print('The following errors were found in connection.client_facades:') |
| 41 | + for name, expected, actual in errors: |
| 42 | + expected_msg = ( |
| 43 | + f'should be {expected},' |
| 44 | + if expected is not None |
| 45 | + else 'should not be present,' |
| 46 | + ) |
| 47 | + actual_msg = ( |
| 48 | + f'not {actual}' |
| 49 | + if actual is not None |
| 50 | + else 'but is not present' |
| 51 | + ) |
| 52 | + print(f' {name!r} {expected_msg} {actual_msg}') |
| 53 | + |
| 54 | + assert not errors |
| 55 | + |
| 56 | + @classmethod |
| 57 | + def generate_client_facades(cls, project_root: Path) -> ClientFacades: |
| 58 | + """Return a client_facades dictionary from generated code under project_root. |
| 59 | + """ |
| 60 | + files_by_version: List[Tuple[int, Path]] = [] |
| 61 | + # [(facade_version, Path), ...] |
| 62 | + for file in (project_root / 'juju' / 'client').glob('_client[0-9]*.py'): |
| 63 | + files_by_version.append((cls._version_from_filename(file), file)) |
| 64 | + files_by_version.sort() |
| 65 | + |
| 66 | + # _clientN.py files import * from _definitions |
| 67 | + # so we will ignore any names from there |
| 68 | + ignore = dir(importlib.import_module('juju.client._definitions')) |
| 69 | + |
| 70 | + facades_by_version: Dict[int, Set[str]] = {} |
| 71 | + # {facade_version: {facade_name, ...}, ...} |
| 72 | + for version, file in files_by_version: |
| 73 | + module = cls._try_import(f'juju.client.{file.stem}') |
| 74 | + facades = { |
| 75 | + name.removesuffix("Facade") |
| 76 | + for name in dir(module) |
| 77 | + if not (name.startswith('_') or name in ignore) |
| 78 | + } |
| 79 | + facades_by_version[version] = facades |
| 80 | + |
| 81 | + # client_facades in connection.py is sorted |
| 82 | + # so we sort facade names before constructing it |
| 83 | + first, *rest = facades_by_version.values() |
| 84 | + sorted_facade_names: list[str] = sorted(first.union(*rest)) #, key=str.swapcase) |
| 85 | + |
| 86 | + client_facades: ClientFacades = {} |
| 87 | + # {facade_name: {'versions': [1, 2, 3, ...]}, ...} |
| 88 | + for name in sorted_facade_names: |
| 89 | + versions: List[int] = [] |
| 90 | + for version, facades in facades_by_version.items(): |
| 91 | + if name in facades: |
| 92 | + versions.append(version) |
| 93 | + client_facades[name] = {'versions': versions} |
| 94 | + return client_facades |
| 95 | + |
| 96 | + @staticmethod |
| 97 | + def _try_import(module_name: str) -> ModuleType | None: |
| 98 | + try: |
| 99 | + return importlib.import_module(module_name) |
| 100 | + except NameError as e: |
| 101 | + warnings.warn(f'error on importing {module_name}:\n{type(e).__name__}: {e}') |
| 102 | + return None |
| 103 | + |
| 104 | + @staticmethod |
| 105 | + def _version_from_filename(path: Path) -> int: |
| 106 | + match = re.search('_client([0-9]+).py', path.name) |
| 107 | + assert match |
| 108 | + return int(match.group(1)) |
| 109 | + |
| 110 | + @staticmethod |
| 111 | + def _versions_from_facades(facades: ClientFacades, name: str) -> Optional[List[int]]: |
| 112 | + if name not in facades: |
| 113 | + return None |
| 114 | + return facades[name]['versions'] |
0 commit comments