Skip to content

Commit 65a932a

Browse files
authored
Feature: Add support for app manifest v3 (#30)
1 parent e956bb5 commit 65a932a

File tree

6 files changed

+211
-39
lines changed

6 files changed

+211
-39
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,15 @@ jobs:
105105

106106
- name: Prepare .velocitas.json for integration test
107107
run: |
108-
NEW_CONFIG="$(jq --arg GITHUB_SHA "$GITHUB_SHA" '.packages[0].version |= $GITHUB_SHA' test/${{ matrix.language }}/.velocitas.json)"
108+
COMMIT_REF=$GITHUB_SHA
109+
110+
if [ "$GITHUB_EVENT_NAME" = "pull_request" ];
111+
then
112+
echo "Running in context of a PR!"
113+
COMMIT_REF=$GITHUB_HEAD_REF
114+
fi
115+
116+
NEW_CONFIG="$(jq --arg COMMIT_REF "$COMMIT_REF" '.packages[0].version |= $COMMIT_REF' test/${{ matrix.language }}/.velocitas.json)"
109117
echo "${NEW_CONFIG}" > test/${{ matrix.language }}/repo/.velocitas.json
110118
111119
- name: Init velocitas project

setup/src/python/common/devcontainer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
"ms-azuretools.vscode-docker",
8080
"ms-python.python",
8181
"cschleiden.vscode-github-actions",
82-
"pspester.pester-test",
8382
"rpdswtk.vsmqtt",
8483
"dotjoshjohnson.xml",
8584
"ms-kubernetes-tools.vscode-kubernetes-tools",

vehicle-model-lifecycle/src/download_vspec.py

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
import json
1818
import os
1919
import re
20+
from typing import Any, Dict, List
2021

2122
import requests
2223

24+
FUNCTIONAL_INTERFACE_TYPE_KEY = "vehicle-signal-interface"
25+
2326

2427
def require_env(name: str) -> str:
2528
"""Require and return an environment variable.
@@ -84,21 +87,104 @@ def download_file(uri: str, local_file_path: str):
8487
outfile.write(chunk)
8588

8689

87-
def get_vehicle_model_src():
88-
manifest_data_str = require_env("VELOCITAS_APP_MANIFEST")
89-
manifest_data = json.loads(manifest_data_str)
90+
def is_legacy_app_manifest(app_manifest_dict: Dict[str, Any]) -> bool:
91+
"""Check if the used app manifest is a legacy file.
92+
93+
Args:
94+
app_manifest_dict (Dict[str, Any]): The app manifest.
95+
96+
Returns:
97+
bool: True if the app manifest is a legacy file, False if not.
98+
"""
99+
return "manifestVersion" not in app_manifest_dict
100+
101+
102+
def get_legacy_model_src(app_manifest_dict: Dict[str, Any]) -> str:
103+
"""Get the source from the legacy vehicle model (app manifest < v3)
104+
105+
Args:
106+
app_manifest_dict (Dict[str, Any]): The app manifest dict.
90107
108+
Returns:
109+
str: The source URI of the vehicle model
110+
"""
91111
possible_keys = ["vehicleModel", "VehicleModel"]
92112

93113
for key in possible_keys:
94-
if key in manifest_data:
95-
return manifest_data[key]["src"]
114+
if key in app_manifest_dict:
115+
return app_manifest_dict[key]["src"]
96116

97117
raise KeyError("App manifest does not contain a valid vehicle model!")
98118

99119

100-
def main():
101-
vspec_src = get_vehicle_model_src()
120+
def is_proper_interface_type(interface: Dict[str, Any]) -> bool:
121+
"""Return if the interface is of the correct type.
122+
123+
Args:
124+
interface (Dict[str, Any]): The interface to check.
125+
126+
Returns:
127+
bool: True if the type matches, False otherwise.
128+
"""
129+
return "type" in interface and interface["type"] == FUNCTIONAL_INTERFACE_TYPE_KEY
130+
131+
132+
def get_vehicle_signal_interfaces(
133+
app_manifest_dict: Dict[str, Any]
134+
) -> List[Dict[str, Any]]:
135+
"""Return all vehicle signal interfaces in the app manifest.
136+
137+
Args:
138+
app_manifest_dict (Dict[str, Any]): The app manifest.
139+
140+
Returns:
141+
List[Dict[str, Any]]: List containing all functional interfaces
142+
of type {FUNCTIONAL_INTERFACE_TYPE_KEY}
143+
"""
144+
interfaces = list()
145+
146+
for interface in app_manifest_dict["interfaces"]:
147+
if is_proper_interface_type(interface):
148+
interfaces.append(interface)
149+
150+
return interfaces
151+
152+
153+
def get_vehicle_signal_interface_src(interface: Dict[str, Any]) -> str:
154+
"""Return the URI of the source for the Vehicle Signal Interface.
155+
156+
Args:
157+
interface (Dict[str, Any]): The interface.
158+
159+
Returns:
160+
str: The URI of the source for the Vehicle Signal Interface.
161+
"""
162+
return interface["config"]["src"]
163+
164+
165+
def main(app_manifest_dict: Dict[str, Any]):
166+
"""Entry point for downloading the vspec file for a
167+
vehicle-signal-interface.
168+
169+
Args:
170+
app_manifest_dict (Dict[str, Any]): The app manifest.
171+
172+
Raises:
173+
KeyError: If there are multiple vehicle signal interfaces defined
174+
in the app manifest.
175+
"""
176+
if is_legacy_app_manifest(app_manifest_dict):
177+
vspec_src = get_legacy_model_src(app_manifest_dict)
178+
else:
179+
interfaces = get_vehicle_signal_interfaces(app_manifest_dict)
180+
if len(interfaces) > 1:
181+
raise KeyError(
182+
f"Only up to one {FUNCTIONAL_INTERFACE_TYPE_KEY!r} supported!"
183+
)
184+
elif len(interfaces) == 0:
185+
return
186+
vspec_src = get_vehicle_signal_interface_src(interfaces[0])
187+
102188
local_vspec_path = os.path.join(
103189
get_velocitas_workspace_dir(), os.path.normpath(vspec_src)
104190
)
@@ -113,4 +199,7 @@ def main():
113199

114200

115201
if __name__ == "__main__":
116-
main()
202+
manifest_data_str = require_env("VELOCITAS_APP_MANIFEST")
203+
app_manifest_dict = json.loads(manifest_data_str)
204+
205+
main(app_manifest_dict)

vehicle-model-lifecycle/src/generate_model.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,22 @@ def install_model_if_required(language: str, model_path: str) -> None:
107107
subprocess.check_call([sys.executable, "-m", "pip", "install", model_path])
108108

109109

110-
if __name__ == "__main__":
111-
model_src_file = json.loads(require_env("VELOCITAS_CACHE_DATA"))["vspec_file_path"]
110+
def main():
111+
"""Main entry point for generation of vehicle models."""
112+
cache_data = json.loads(require_env("VELOCITAS_CACHE_DATA"))
113+
114+
if "vspec_file_path" not in cache_data:
115+
return
116+
117+
model_src_file = cache_data["vspec_file_path"]
112118
model_language = require_env("language")
113119
model_output_dir = get_model_output_dir()
114120
os.makedirs(model_output_dir, exist_ok=True)
115121

116122
remove_old_model(model_output_dir)
117123
invoke_generator(model_src_file, model_language, model_output_dir)
118124
install_model_if_required(model_language, model_output_dir)
125+
126+
127+
if __name__ == "__main__":
128+
main()

vehicle-model-lifecycle/test/test_download_vspec.py

Lines changed: 92 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,24 @@
1919
from test_lib import capture_stdout, mock_env
2020

2121
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))
22-
from download_vspec import download_file, get_vehicle_model_src, is_uri, main # noqa
22+
from download_vspec import ( # noqa
23+
download_file,
24+
get_legacy_model_src,
25+
is_proper_interface_type,
26+
is_uri,
27+
main,
28+
require_env,
29+
)
2330

2431
vspec_300_uri = "https://github.com/COVESA/vehicle_signal_specification/releases/download/v3.0/vss_rel_3.0.json" # noqa
25-
app_manifest = {"VehicleModel": {"src": ""}}
32+
33+
34+
def get_vspec_file_path_value(capturedOutput: str) -> str:
35+
return (
36+
capturedOutput.split("vspec_file_path=")[1]
37+
.split(" >> VELOCITAS_CACHE")[0]
38+
.replace("'", "")
39+
)
2640

2741

2842
def test_is_uri():
@@ -38,48 +52,100 @@ def test_download_file():
3852
assert os.path.isfile(local_file_path)
3953

4054

41-
def test_camel_case_vehicle_model_key():
55+
def test_get_legacy_model_src__camel_case_vehicle_model_key():
4256
app_manifest_dict = {"vehicleModel": {"src": "foo"}}
43-
with mock_env(app_manifest_dict):
44-
assert get_vehicle_model_src() == "foo"
57+
assert get_legacy_model_src(app_manifest_dict) == "foo"
4558

4659

47-
def test_pascal_case_vehicle_model_key():
60+
def test_get_legacy_model_src__pascal_case_vehicle_model_key():
4861
app_manifest_dict = {"VehicleModel": {"src": "bar"}}
49-
with mock_env(app_manifest_dict):
50-
assert get_vehicle_model_src() == "bar"
62+
assert get_legacy_model_src(app_manifest_dict) == "bar"
5163

5264

53-
def test_invalid_vehicle_model_key():
65+
def test_get_legacy_model_src__invalid_vehicle_model_key():
5466
app_manifest_dict = {"Vehicle.Model": {"src": "baz"}}
55-
with mock_env(app_manifest_dict):
56-
with pytest.raises(KeyError):
57-
get_vehicle_model_src()
67+
with pytest.raises(KeyError):
68+
get_legacy_model_src(app_manifest_dict)
69+
70+
71+
def test_proper_interface_type__wrong_type():
72+
assert not is_proper_interface_type({"type": "foo"})
73+
5874

75+
def test_proper_interface_type__no_type():
76+
assert not is_proper_interface_type({"notype": "foo"})
5977

60-
def test_int_relative_src_converted_to_absolute():
61-
app_manifest["VehicleModel"]["src"] = "./app/vspec.json"
62-
with capture_stdout() as capture, mock_env(app_manifest):
63-
main()
78+
79+
def test_proper_interface_type__correct_type():
80+
assert is_proper_interface_type({"type": "vehicle-signal-interface"})
81+
82+
83+
def test_require_env__var_not_present__raises_error():
84+
with pytest.raises(ValueError):
85+
require_env("foo")
86+
87+
88+
@pytest.mark.parametrize(
89+
"app_manifest",
90+
[
91+
{
92+
"manifestVersion": "v3",
93+
"interfaces": [
94+
{
95+
"type": "vehicle-signal-interface",
96+
"config": {"src": "./app/vspec.json"},
97+
}
98+
],
99+
},
100+
{"VehicleModel": {"src": "./app/vspec.json"}},
101+
],
102+
)
103+
def test_main__relative_src__converted_to_absolute(app_manifest):
104+
with capture_stdout() as capture, mock_env():
105+
main(app_manifest)
64106

65107
vspec_file_path = get_vspec_file_path_value(capture.getvalue())
66108
assert os.path.isabs(vspec_file_path)
67109
assert vspec_file_path == "/workspaces/my_vehicle_app/app/vspec.json"
68110

69111

70-
def test_int_uri_src_downloaded_and_stored_in_cache():
71-
app_manifest["VehicleModel"]["src"] = vspec_300_uri
72-
with capture_stdout() as capture, mock_env(app_manifest):
73-
main()
112+
@pytest.mark.parametrize(
113+
"app_manifest",
114+
[
115+
{
116+
"manifestVersion": "v3",
117+
"interfaces": [
118+
{"type": "vehicle-signal-interface", "config": {"src": vspec_300_uri}}
119+
],
120+
},
121+
{"VehicleModel": {"src": vspec_300_uri}},
122+
],
123+
)
124+
def test_main__valid_app_manifest__uri_src_downloaded_and_stored_in_cache(app_manifest):
125+
with capture_stdout() as capture, mock_env():
126+
main(app_manifest)
74127

75128
vspec_file_path = get_vspec_file_path_value(capture.getvalue())
76129
assert os.path.isabs(vspec_file_path)
77130
assert vspec_file_path == "/tmp/velocitas/vspec.json"
78131

79132

80-
def get_vspec_file_path_value(capturedOutput: str) -> str:
81-
return (
82-
capturedOutput.split("vspec_file_path=")[1]
83-
.split(" >> VELOCITAS_CACHE")[0]
84-
.replace("'", "")
85-
)
133+
def test_main__duplicate_vehicle_signal_interface__raises_error():
134+
app_manifest = {
135+
"manifestVersion": "v3",
136+
"interfaces": [
137+
{"type": "vehicle-signal-interface", "config": {"src": vspec_300_uri}},
138+
{"type": "vehicle-signal-interface", "config": {"src": vspec_300_uri}},
139+
],
140+
}
141+
with pytest.raises(KeyError):
142+
main(app_manifest)
143+
144+
145+
def test_main__no_vehicle_signal_interface__silently_exists():
146+
app_manifest = {
147+
"manifestVersion": "v3",
148+
"interfaces": [{"type": "pubsub", "config": {}}],
149+
}
150+
with capture_stdout(), mock_env():
151+
main(app_manifest)

vehicle-model-lifecycle/test/test_lib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ def __exit__(self, _type, _value, _traceback):
5555
del os.environ["VELOCITAS_WORKSPACE_DIR"]
5656

5757

58-
def mock_env(app_manifest) -> MockEnv:
58+
def mock_env(app_manifest=None) -> MockEnv:
5959
return MockEnv(app_manifest)

0 commit comments

Comments
 (0)