Skip to content

Commit a9601a7

Browse files
test: add a (failing) test to validate client_facades
It checks that juju/client/connection.py's client_facades matches the facade versions across the generated code (juju/client/_client*.py)
1 parent c040672 commit a9601a7

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

tests/validate/test_facades.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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

Comments
 (0)