Skip to content

Commit 79aac2c

Browse files
authored
Merge pull request #1150 from james-garner-canonical/24.10/tests/validate-client-facades
#1150 #### Description `client_facades` in `juju/client/connection.py` is manually updated. It doesn't reflect the actual facade code present, which was recently generated from the current 3.1.X and 3.3.0 schemas currently checked in. Nor did it reflect the state of the generated code with the previous set of schemas (same + 3.2.X schemas), nor the state of the actually checked in code before the last regeneration of the code (which contained some orphan facades not present in the schemas). This PR updates `client_schemas` and adds a test to ensure that its facade versions always match the schemas present in the generated client code. The test can be run with `tox -e validate`, and is run in a separate github job as it didn't seem to fit in well as either a unit or integration test. In future we could add more tests to this group to validate other aspects of the codebase related to the code generation. #### QA Steps All non-quarantined tests should continue to work. ``` tox -e validate ``` Should pass currently. #### Notes & Discussion Does the current state of client_facades make sense? One thing that stood out to me was the addition of `Admin`. I'm wondering if any non-client facades are being added here, since facade code is generated from the full schemas still. If that's the case, then this should be deferred till after we switch to client-only schemas.
2 parents c040672 + e800b48 commit 79aac2c

4 files changed

Lines changed: 140 additions & 55 deletions

File tree

.github/workflows/test.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ jobs:
4343
run: |
4444
make build-test
4545
46+
validate:
47+
name: Validate
48+
runs-on: ubuntu-latest
49+
strategy:
50+
matrix:
51+
python:
52+
- "3.10"
53+
steps:
54+
- name: Check out code
55+
uses: actions/checkout@v4
56+
- name: Setup Python
57+
uses: actions/setup-python@v5
58+
with:
59+
python-version: ${{ matrix.python }}
60+
- name: Install dependencies
61+
run: pip install tox
62+
- name: Run validation tests
63+
run: tox -e validate
64+
4665
unit-tests:
4766
needs: lint
4867
name: Unit tests

juju/client/connection.py

Lines changed: 57 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import weakref
1010
from http.client import HTTPSConnection
1111
from dateutil.parser import parse
12+
from typing import Dict, List
1213

1314
import macaroonbakery.bakery as bakery
1415
import macaroonbakery.httpbakery as httpbakery
@@ -20,86 +21,89 @@
2021

2122
log = logging.getLogger('juju.client.connection')
2223

24+
# Manual list of facades present in schemas + codegen which python-libjuju does not yet support
25+
excluded_facades: Dict[str, List[int]] = {
26+
'Charms': [7],
27+
}
2328
# Please keep in alphabetical order
29+
# in future this will likely be generated automatically (perhaps at runtime)
2430
client_facades = {
25-
'Action': {'versions': [2, 6, 7]},
31+
'Action': {'versions': [7]},
2632
'ActionPruner': {'versions': [1]},
27-
'Agent': {'versions': [2, 3]},
33+
'Admin': {'versions': [3]},
34+
'Agent': {'versions': [3]},
2835
'AgentLifeFlag': {'versions': [1]},
2936
'AgentTools': {'versions': [1]},
30-
'AllModelWatcher': {'versions': [2, 3, 4]},
31-
'AllWatcher': {'versions': [1, 2, 3, 4]},
37+
'AllModelWatcher': {'versions': [4]},
38+
'AllWatcher': {'versions': [3]},
3239
'Annotations': {'versions': [2]},
33-
'Application': {'versions': [14, 15, 16, 17, 19]},
34-
'ApplicationOffers': {'versions': [1, 2, 4]},
40+
'Application': {'versions': [17, 18, 19]},
41+
'ApplicationOffers': {'versions': [4]},
3542
'ApplicationScaler': {'versions': [1]},
36-
'Backups': {'versions': [1, 2, 3]},
43+
'Backups': {'versions': [3]},
3744
'Block': {'versions': [2]},
38-
'Bundle': {'versions': [5, 6]},
39-
'CharmHub': {'versions': [1]},
40-
'CharmRevisionUpdater': {'versions': [2]},
41-
'CharmDownloader': {'versions': [1]},
42-
'Charms': {'versions': [5, 6]},
43-
'Cleaner': {'versions': [2]},
44-
'Client': {'versions': [5, 6]},
45-
'Cloud': {'versions': [1, 2, 3, 4, 5, 7]},
46-
'Controller': {'versions': [9, 11]},
47-
'CrossModelRelations': {'versions': [1, 2]},
48-
'CrossModelSecrets': {'versions': [1]},
49-
'CrossController': {'versions': [1]},
50-
'CredentialManager': {'versions': [1]},
51-
'CredentialValidator': {'versions': [1, 2]},
45+
'Bundle': {'versions': [6]},
5246
'CAASAdmission': {'versions': [1]},
53-
'CAASAgent': {'versions': [1, 2]},
47+
'CAASAgent': {'versions': [2]},
5448
'CAASApplication': {'versions': [1]},
5549
'CAASApplicationProvisioner': {'versions': [1]},
5650
'CAASFirewaller': {'versions': [1]},
57-
'CAASFirewallerEmbedded': {'versions': [1]},
5851
'CAASFirewallerSidecar': {'versions': [1]},
59-
'CAASModelOperator': {'versions': [1]},
6052
'CAASModelConfigManager': {'versions': [1]},
53+
'CAASModelOperator': {'versions': [1]},
6154
'CAASOperator': {'versions': [1]},
6255
'CAASOperatorProvisioner': {'versions': [1]},
6356
'CAASOperatorUpgrader': {'versions': [1]},
64-
'CAASUnitProvisioner': {'versions': [1, 2]},
57+
'CAASUnitProvisioner': {'versions': [2]},
58+
'CharmDownloader': {'versions': [1]},
59+
'CharmRevisionUpdater': {'versions': [2]},
60+
'Charms': {'versions': [6]},
61+
'Cleaner': {'versions': [2]},
62+
'Client': {'versions': [6, 7]},
63+
'Cloud': {'versions': [7]},
64+
'Controller': {'versions': [11]},
65+
'CredentialManager': {'versions': [1]},
66+
'CredentialValidator': {'versions': [2]},
67+
'CrossController': {'versions': [1]},
68+
'CrossModelRelations': {'versions': [2]},
69+
'CrossModelSecrets': {'versions': [1]},
6570
'Deployer': {'versions': [1]},
6671
'DiskManager': {'versions': [2]},
6772
'EntityWatcher': {'versions': [2]},
6873
'EnvironUpgrader': {'versions': [1]},
6974
'ExternalControllerUpdater': {'versions': [1]},
7075
'FanConfigurer': {'versions': [1]},
7176
'FilesystemAttachmentsWatcher': {'versions': [2]},
72-
'Firewaller': {'versions': [3, 4, 5, 7]},
7377
'FirewallRules': {'versions': [1]},
78+
'Firewaller': {'versions': [7]},
7479
'HighAvailability': {'versions': [2]},
7580
'HostKeyReporter': {'versions': [1]},
76-
'ImageManager': {'versions': [2]},
7781
'ImageMetadata': {'versions': [3]},
7882
'ImageMetadataManager': {'versions': [1]},
79-
'InstanceMutater': {'versions': [2, 3]},
80-
'InstancePoller': {'versions': [3, 4]},
83+
'InstanceMutater': {'versions': [3]},
84+
'InstancePoller': {'versions': [4]},
8185
'KeyManager': {'versions': [1]},
8286
'KeyUpdater': {'versions': [1]},
8387
'LeadershipService': {'versions': [2]},
8488
'LifeFlag': {'versions': [1]},
85-
'Logger': {'versions': [1]},
8689
'LogForwarding': {'versions': [1]},
87-
'Machiner': {'versions': [1, 2, 5]},
90+
'Logger': {'versions': [1]},
8891
'MachineActions': {'versions': [1]},
89-
'MachineManager': {'versions': [9, 10]},
92+
'MachineManager': {'versions': [10]},
9093
'MachineUndertaker': {'versions': [1]},
91-
'MeterStatus': {'versions': [1, 2]},
94+
'Machiner': {'versions': [5]},
95+
'MeterStatus': {'versions': [2]},
9296
'MetricsAdder': {'versions': [2]},
9397
'MetricsDebug': {'versions': [2]},
9498
'MetricsManager': {'versions': [1]},
9599
'MigrationFlag': {'versions': [1]},
96-
'MigrationMaster': {'versions': [1, 3]},
100+
'MigrationMaster': {'versions': [3]},
97101
'MigrationMinion': {'versions': [1]},
98102
'MigrationStatusWatcher': {'versions': [1]},
99-
'MigrationTarget': {'versions': [1]},
100-
'ModelConfig': {'versions': [1, 2, 3]},
101-
'ModelGeneration': {'versions': [1, 2, 4]},
102-
'ModelManager': {'versions': [2, 3, 4, 5, 9]},
103+
'MigrationTarget': {'versions': [1, 2]},
104+
'ModelConfig': {'versions': [3]},
105+
'ModelGeneration': {'versions': [4]},
106+
'ModelManager': {'versions': [9]},
103107
'ModelSummaryWatcher': {'versions': [1]},
104108
'ModelUpgrader': {'versions': [1]},
105109
'NotifyWatcher': {'versions': [1]},
@@ -108,45 +112,43 @@
108112
'PayloadsHookContext': {'versions': [1]},
109113
'Pinger': {'versions': [1]},
110114
'Provisioner': {'versions': [11]},
111-
'ProxyUpdater': {'versions': [1, 2]},
112-
'RaftLease': {'versions': [1, 2]},
115+
'ProxyUpdater': {'versions': [2]},
116+
'RaftLease': {'versions': [2]},
113117
'Reboot': {'versions': [2]},
114118
'RelationStatusWatcher': {'versions': [1]},
115119
'RelationUnitsWatcher': {'versions': [1]},
116-
'RemoteRelations': {'versions': [1, 2]},
117120
'RemoteRelationWatcher': {'versions': [1]},
118-
'Resources': {'versions': [1, 2, 3]},
121+
'RemoteRelations': {'versions': [2]},
122+
'Resources': {'versions': [3]},
119123
'ResourcesHookContext': {'versions': [1]},
120-
'Resumer': {'versions': [2]},
121124
'RetryStrategy': {'versions': [1]},
122-
'Secrets': {'versions': [1, 2]},
123-
'SecretsManager': {'versions': [1, 2]},
125+
'SSHClient': {'versions': [4]},
124126
'SecretBackends': {'versions': [1]},
125127
'SecretBackendsManager': {'versions': [1]},
126128
'SecretBackendsRotateWatcher': {'versions': [1]},
129+
'Secrets': {'versions': [1, 2]},
127130
'SecretsDrain': {'versions': [1]},
131+
'SecretsManager': {'versions': [1, 2]},
128132
'SecretsRevisionWatcher': {'versions': [1]},
129-
'SecretsRotationWatcher': {'versions': [1]},
130133
'SecretsTriggerWatcher': {'versions': [1]},
131134
'Singular': {'versions': [2]},
132135
'Spaces': {'versions': [6]},
133136
'StatusHistory': {'versions': [2]},
134-
'Storage': {'versions': [3, 4, 6]},
135-
'StorageProvisioner': {'versions': [3, 4]},
137+
'Storage': {'versions': [6]},
138+
'StorageProvisioner': {'versions': [4]},
136139
'StringsWatcher': {'versions': [1]},
137-
'Subnets': {'versions': [2, 4, 5]},
138-
'SSHClient': {'versions': [1, 2, 3, 4]},
140+
'Subnets': {'versions': [5]},
139141
'Undertaker': {'versions': [1]},
140142
'UnitAssigner': {'versions': [1]},
141-
'Uniter': {'versions': [18]},
143+
'Uniter': {'versions': [18, 19]},
144+
'UpgradeSeries': {'versions': [3]},
145+
'UpgradeSteps': {'versions': [2]},
142146
'Upgrader': {'versions': [1]},
143-
'UpgradeSeries': {'versions': [1, 3]},
144-
'UpgradeSteps': {'versions': [1, 2]},
145-
'UserManager': {'versions': [1, 2, 3]},
147+
'UserManager': {'versions': [3]},
146148
'UserSecretsDrain': {'versions': [1]},
147149
'UserSecretsManager': {'versions': [1]},
148-
'VolumeAttachmentsWatcher': {'versions': [2]},
149150
'VolumeAttachmentPlansWatcher': {'versions': [1]},
151+
'VolumeAttachmentsWatcher': {'versions': [2]},
150152
}
151153

152154

tests/validate/test_facades.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2023 Canonical Ltd.
2+
# Licensed under the Apache V2, see LICENCE file for details.
3+
4+
import importlib
5+
from collections import defaultdict
6+
from pathlib import Path
7+
from typing import Dict, List, TypedDict
8+
9+
import pytest
10+
11+
from juju.client.connection import client_facades, excluded_facades
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(pytestconfig: pytest.Config) -> Path:
23+
return pytestconfig.rootpath
24+
25+
26+
@pytest.fixture
27+
def generated_code_facades(project_root: Path) -> ClientFacades:
28+
"""Return a client_facades dictionary from generated code under project_root.
29+
30+
Iterates through all the generated files matching juju/client/_client*.py,
31+
extracting facade types (those that have .name and .version properties).
32+
Excludes facades in juju.client.connection.excluded_facades, as these are
33+
manually marked as incompatible with the current version of python-libjuju.
34+
"""
35+
facades: Dict[str, List[int]] = defaultdict(list)
36+
for file in project_root.glob('juju/client/_client*.py'):
37+
module = importlib.import_module(f'juju.client.{file.stem}')
38+
for cls_name in dir(module):
39+
cls = getattr(module, cls_name)
40+
try: # duck typing check for facade types
41+
cls.name
42+
cls.version
43+
except AttributeError:
44+
continue
45+
if cls.version in excluded_facades.get(cls.name, []):
46+
continue
47+
facades[cls.name].append(cls.version)
48+
return {name: {'versions': sorted(facades[name])} for name in sorted(facades)}
49+
50+
51+
def test_client_facades(project_root: Path, generated_code_facades: ClientFacades) -> None:
52+
"""Ensure that juju.client.connection.client_facades matches expected facades.
53+
54+
See generated_code_facades for how expected facades are computed.
55+
"""
56+
assert {
57+
k: v['versions'] for k, v in client_facades.items()
58+
} == {
59+
k: v['versions'] for k, v in generated_code_facades.items()
60+
}

tox.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ commands =
103103
pip install pylxd
104104
python -m pytest --tb native -ra -v -s {posargs:-m 'serial'}
105105

106+
[testenv:validate]
107+
envdir = {toxworkdir}/validate
108+
commands = python -m pytest --tb native -ra -vv tests/validate
109+
106110
[testenv:example]
107111
envdir = {toxworkdir}/py3
108112
commands = python {posargs}

0 commit comments

Comments
 (0)