Skip to content

Commit 7a30fe8

Browse files
author
Patrick J. McNerthney
committed
More pytest cases and add CI report
1 parent 58e7bcb commit 7a30fe8

File tree

12 files changed

+351
-42
lines changed

12 files changed

+351
-42
lines changed

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
omit = tests/*

.github/workflows/ci.yaml

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ name: CI
33
on:
44
push:
55
branches:
6-
- main
7-
- release-*
6+
- main
7+
- release-*
88
pull_request: {}
99
workflow_dispatch:
1010
inputs:
@@ -53,22 +53,38 @@ jobs:
5353
# - name: Lint
5454
# run: hatch run lint:check
5555

56-
unit-test:
56+
test:
5757
runs-on: ubuntu-24.04
5858
steps:
59-
- name: Checkout
60-
uses: actions/checkout@v4
61-
62-
- name: Setup Python
63-
uses: actions/setup-python@v5
64-
with:
65-
python-version: ${{ env.PYTHON_VERSION }}
66-
67-
- name: Setup Hatch
68-
run: pipx install hatch==1.14.1
69-
70-
- name: Run Unit Tests
71-
run: hatch run test:unit
59+
- name: Checkout
60+
uses: actions/checkout@v4
61+
62+
- name: Setup Python
63+
uses: actions/setup-python@v5
64+
with:
65+
python-version: ${{ env.PYTHON_VERSION }}
66+
67+
- name: Setup Hatch
68+
run: pipx install hatch==1.14.1
69+
70+
- name: Run Unit Tests
71+
run: hatch run test:ci
72+
73+
- name: Pytest coverage comment
74+
uses: MishaKav/pytest-coverage-comment@v1.1.54
75+
with:
76+
badge-title: Coverage
77+
title: Coverage Report
78+
pytest-xml-coverage-path: reports/pytest-coverage.xml
79+
junitxml-title: Unit Tests
80+
junitxml-path: reports/pytest-junit.xml
81+
#hide-badge: false
82+
#hide-report: false
83+
#create-new-comment: false
84+
#hide-comment: false
85+
#report-only-changed-files: false
86+
#remove-link-from-badge: false
87+
#unique-id-for-comment: python3.8
7288

7389
# We want to build most packages for the amd64 and arm64 architectures. To
7490
# speed this up we build single-platform packages in parallel. We then upload

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ coverage.xml
5353
.hypothesis/
5454
.pytest_cache/
5555
cover/
56+
reports/
5657

5758
# Translations
5859
*.mo

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ region = request.observed.composite.resource.spec.region
8585
region = request['observed']['composite']['resource']['spec']['region']
8686
```
8787
Getting values from free form map and list values will not throw
88-
errors for keys that do not exist, but will return an empty placeholder
88+
errors for keys that do not exist, but will return an unknown placeholder
8989
which evaluates as False. For example, the following will evaluate as False
9090
with a just created RunFunctionResponse message:
9191
```python
@@ -97,7 +97,7 @@ Note that maps or lists that do exist but do not have any members will evaluate
9797
as True, contrary to Python dicts and lists. Use the `len` function to test
9898
if the map or list exists and has members.
9999

100-
When setting fields, all empty intermediary placeholders will automatically
100+
When setting fields, all intermediary unknown placeholders will automatically
101101
be created. For example, this will create all items needed to set the
102102
region on the desired resource:
103103
```python
@@ -114,6 +114,7 @@ The following functions are provided to create Protobuf structures:
114114
| ----- | ----------- |
115115
| Map | Create a new Protobuf map |
116116
| List | Create a new Protobuf list |
117+
| Unknown | Create a new Protobuf unknown placeholder |
117118
| Yaml | Create a new Protobuf structure from a yaml string |
118119
| Json | Create a new Protobuf structure from a json string |
119120
| Base64Encode | Encode a string into base 64 |

examples/helm-copy-secret/composition.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ spec:
4141
#vcluster_secrets = self.requireds.Secret('v1', 'Secret', namespace, secret_name)
4242
vcluster_secrets = self.requireds.Secret('v1', 'Secret', labels={'vcluster-name':name})
4343
for secret in vcluster_secrets:
44-
if secret.metadata.name != secret_name:
44+
if secret.metadata.namespace != namespace or secret.metadata.name != secret_name:
4545
continue
4646
argocd_secret = self.resources.secret('v1', 'Secret', 'argocd', secret_name)
4747
argocd_secret.metadata.labels['argocd.argoproj.io/secret-type'] = 'cluster'

examples/helm-copy-secret/xrd.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: CompositeResourceDefinition
3+
metadata:
4+
name: xclusters.example.joebowbeer.com
5+
spec:
6+
group: example.joebowbeer.com
7+
names:
8+
kind: XCluster
9+
plural: xclusters
10+
scope: Cluster
11+
versions:
12+
- name: v1alpha1
13+
served: true
14+
referenceable: true
15+
schema:
16+
openAPIV3Schema:
17+
type: object
18+
properties:
19+
spec:
20+
type: object
21+
properties: {}

function/fn.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def __init__(self):
106106
self.BaseComposite = function.composite.BaseComposite
107107
self.Map = function.protobuf.Map
108108
self.List = function.protobuf.List
109+
self.Unknown = function.protobuf.Unknown
109110
self.Yaml = function.protobuf.Yaml
110111
self.Json = function.protobuf.Json
111112
self.B64Encode = lambda s: base64.b64encode(s.encode('utf-8')).decode('utf-8')

function/protobuf.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,13 @@
2222

2323

2424
def Map(**kwargs):
25-
values = Values(None, None, google.protobuf.struct_pb2.Struct(), Values.Type.MAP)
26-
for key, value in kwargs.items():
27-
values[key] = value
28-
return values
25+
return Values(None, None, google.protobuf.struct_pb2.Struct(), Values.Type.MAP)(**kwargs)
2926

3027
def List(*args):
31-
values = Values(None, None, google.protobuf.struct_pb2.ListValue(), Values.Type.LIST)
32-
for ix, value in enumerate(args):
33-
values[ix] = value
34-
return values
28+
return Values(None, None, google.protobuf.struct_pb2.ListValue(), Values.Type.LIST)(*args)
29+
30+
def Unknown():
31+
return Values(None, None, None, Values.Type.UNKNOWN)
3532

3633
def Yaml(string, readOnly=None):
3734
return _Object(yaml.safe_load(string), readOnly)
@@ -618,15 +615,21 @@ def _create_child(self, key, type):
618615
raise ValueError('Invalid key, must be a str for maps')
619616
self.__dict__['_type'] = self.Type.MAP
620617
if self._values is None:
621-
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
618+
if self._parent is None:
619+
self.__dict__['_values'] = google.protobuf.struct_pb2.Struct()
620+
else:
621+
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
622622
struct_value = self._values.fields[key]
623623
elif isinstance(key, int):
624624
if not self._isList:
625625
if not self._isUnknown:
626626
raise ValueError('Invalid key, must be an int for lists')
627627
self.__dict__['_type'] = self.Type.LIST
628628
if self._values is None:
629-
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
629+
if self._parent is None:
630+
self.__dict__['_values'] = google.protobuf.struct_pb2.ListValue()
631+
else:
632+
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
630633
while key >= len(self._values.values):
631634
self._values.values.add()
632635
struct_value = self._values.values[key]
@@ -671,13 +674,6 @@ def __call__(self, *args, **kwargs):
671674
self._values.Clear()
672675
for key in range(len(args)):
673676
self[key] = args[key]
674-
else:
675-
if not self._isMap:
676-
if not self._isUnknown:
677-
self.__dict__['_type'] = self.Type.MAP # Assume a map is wanted
678-
if self._values is None:
679-
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
680-
self._values.Clear()
681677
return self
682678

683679
def __setattr__(self, key, value):
@@ -692,15 +688,21 @@ def __setitem__(self, key, value):
692688
raise ValueError('Invalid key, must be a str for maps')
693689
self.__dict__['_type'] = self.Type.MAP
694690
if self._values is None:
695-
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
691+
if self._parent is None:
692+
self.__dict__['_values'] = google.protobuf.struct_pb2.Struct()
693+
else:
694+
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
696695
values = self._values.fields
697696
elif isinstance(key, int):
698697
if not self._isList:
699698
if not self._isUnknown:
700699
raise ValueError('Invalid key, must be an int for lists')
701700
self.__dict__['_type'] = self.Type.LIST
702701
if self._values is None:
703-
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
702+
if self._parent is None:
703+
self.__dict__['_values'] = google.protobuf.struct_pb2.ListValue()
704+
else:
705+
self.__dict__['_values'] = self._parent._create_child(self._key, self._type)
704706
values = self._values.values
705707
while key >= len(values):
706708
values.add()

pyproject.toml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,29 @@ validate-bump = false # Allow going from 0.0.0.dev0+x to 0.0.0.dev0+y
4343
type = "virtual"
4444
path = ".venv-default"
4545
dependencies = ["ipython==9.1.0"]
46-
scripts = { development = "python function/main.py --insecure --debug" }
46+
[tool.hatch.envs.default.scripts]
47+
development = "python function/main.py --insecure --debug"
4748

4849
[tool.hatch.envs.lint]
4950
type = "virtual"
5051
detached = true
5152
path = ".venv-lint"
5253
dependencies = ["ruff==0.11.5"]
53-
scripts = { check = "ruff check function tests && ruff format --diff function tests" }
54+
[tool.hatch.envs.lint.scripts]
55+
check = "ruff check function tests && ruff format --diff function tests"
5456

5557
[tool.hatch.envs.test]
5658
type = "virtual"
5759
path = ".venv-test"
58-
dependencies = ["pytest==8.4.1", "pytest-asyncio==1.1.0"]
59-
scripts = { unit = "python -m pytest tests" }
60+
dependencies = [
61+
"pytest==8.4.1",
62+
"pytest-asyncio==1.1.0",
63+
"pytest-cov==6.2.1",
64+
]
65+
[tool.hatch.envs.test.scripts]
66+
all = "pytest tests/ --verbose --verbose --cov --cov-report=term --cov-report=html:reports"
67+
protobuf = "pytest tests/test_protobuf*.py --verbose --verbose --cov --cov-report=term --cov-report=html:reports"
68+
ci = "pytest tests --junitxml=reports/pytest-junit.xml --cov --cov-report=xml:reports/pytest-coverage.xml"
6069

6170
[tool.ruff]
6271
target-version = "py311"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
request:
2+
observed:
3+
composite:
4+
resource:
5+
metadata:
6+
name: my-app
7+
spec:
8+
image: nginx
9+
resources:
10+
deployment:
11+
resource:
12+
status:
13+
availableReplicas: 2
14+
conditions:
15+
- type: Available
16+
status: 'True'
17+
reason: MinimumReplicasAvailable
18+
message: Deployment has minimum availability
19+
service:
20+
resource:
21+
spec:
22+
clusterIP: 10.96.196.65
23+
input:
24+
composite: |
25+
class Composite(BaseComposite):
26+
def compose(self):
27+
labels = {'example.crossplane.io/app': self.metadata.name}
28+
29+
d = self.resources.deployment('apps/v1', 'Deployment')
30+
d.metadata.labels = labels
31+
d.spec.replicas = 2
32+
d.spec.selector.matchLabels = labels
33+
d.spec.template.metadata.labels = labels
34+
d.spec.template.spec.containers[0].name = 'app'
35+
d.spec.template.spec.containers[0].image = self.spec.image
36+
d.spec.template.spec.containers[0].ports[0].containerPort = 80
37+
d.ready = d.conditions.Available.status
38+
39+
s = self.resources.service('v1', 'Service')
40+
s.metadata.labels = labels
41+
s.spec.selector = labels
42+
s.spec.ports[0].protocol = 'TCP'
43+
s.spec.ports[0].port = 8080
44+
s.spec.ports[0].targetPort = 80
45+
s.ready = s.observed.spec.clusterIP
46+
47+
self.status.replicas = d.status.availableReplicas
48+
self.status.address = s.observed.spec.clusterIP
49+
50+
response:
51+
desired:
52+
composite:
53+
resource:
54+
status:
55+
replicas: 2
56+
address: 10.96.196.65
57+
resources:
58+
deployment:
59+
resource:
60+
apiVersion: apps/v1
61+
kind: Deployment
62+
metadata:
63+
labels:
64+
example.crossplane.io/app: my-app
65+
spec:
66+
replicas: 2
67+
selector:
68+
matchLabels:
69+
example.crossplane.io/app: my-app
70+
template:
71+
metadata:
72+
labels:
73+
example.crossplane.io/app: my-app
74+
spec:
75+
containers:
76+
- image: nginx
77+
name: app
78+
ports:
79+
- containerPort: 80
80+
ready: 1
81+
service:
82+
resource:
83+
apiVersion: v1
84+
kind: Service
85+
metadata:
86+
labels:
87+
example.crossplane.io/app: my-app
88+
spec:
89+
selector:
90+
example.crossplane.io/app: my-app
91+
ports:
92+
- protocol: TCP
93+
port: 8080
94+
targetPort: 80
95+
ready: 1

0 commit comments

Comments
 (0)