Coverage for tests/unit_tests/test_profile.py: 100%
67 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
1"""
2Tests of apio_profile.py
3"""
5import json
6from datetime import datetime, timedelta
7from pytest import LogCaptureFixture, raises
8from tests.conftest import ApioRunner
9from apio.profile import (
10 Profile,
11 get_datetime_stamp,
12 days_between_datetime_stamps,
13)
14from apio.utils import util
15from apio.apio_context import (
16 ApioContext,
17 PackagesPolicy,
18 ProjectPolicy,
19 RemoteConfigPolicy,
20)
23def get_test_data(
24 apio_ctx: ApioContext,
25 loaded_by_apio_version: str,
26 loaded_at_days: int,
27):
28 """Returns a fake profile.json content. 'loaded_at_days' is the value of
29 the remote config "loaded-at" relative to today in days."""
30 loaded_at_datetime = datetime.now() + timedelta(days=loaded_at_days)
31 loaded_at_stamp = get_datetime_stamp(loaded_at_datetime)
32 assert isinstance(loaded_at_stamp, str)
34 return {
35 "preferences": {"theme": "light"},
36 "remote-config": {
37 "metadata": {
38 "loaded-at": loaded_at_stamp,
39 "loaded-by": loaded_by_apio_version,
40 "loaded-from": apio_ctx.profile.remote_config_url,
41 },
42 "packages": {
43 "drivers": {
44 "release": {
45 "package-file": (
46 "apio-drivers-${PLATFORM}-${YYYYMMDD}.tgz"
47 ),
48 "release-tag": "${YYYY-MM-DD}",
49 "version": "2025.06.13",
50 },
51 "repository": {
52 "name": "tools-drivers",
53 "organization": "fpgawars",
54 },
55 },
56 "examples": {
57 "release": {
58 "package-file": "apio-examples-${YYYYMMDD}.tgz",
59 "release-tag": "${YYYY-MM-DD}",
60 "version": "2025.06.21",
61 },
62 "repository": {
63 "name": "apio-examples",
64 "organization": "fpgawars",
65 },
66 },
67 },
68 },
69 }
72def test_profile_loading_config_ok(apio_runner: ApioRunner):
73 """Tests the loading and validation of a profile file with no need
74 to fetch a remote config."""
76 with apio_runner.in_sandbox() as sb:
78 apio_ctx = ApioContext(
79 project_policy=ProjectPolicy.NO_PROJECT,
80 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
81 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
82 )
84 # -- Write a test profile.json file.
85 path = sb.home_dir / "profile.json"
86 test_data = get_test_data(apio_ctx, util.get_apio_version_str(), 0)
87 sb.write_file(
88 path,
89 json.dumps(
90 test_data,
91 indent=2,
92 ),
93 exists_ok=True,
94 )
96 # -- Read back the content.
97 profile = Profile(
98 sb.home_dir,
99 sb.packages_dir,
100 apio_ctx.profile.remote_config_url,
101 5, # TTL in days
102 60, # Remote config retry mins.
103 RemoteConfigPolicy.CACHED_OK,
104 )
106 # -- Verify
107 assert profile.preferences == test_data["preferences"]
108 assert profile.remote_config == test_data["remote-config"]
111def test_profile_loading_config_stale_version(apio_runner: ApioRunner):
112 """Tests the loading and validation of a profile file that include
113 an old config that needs to be refreshed."""
115 with apio_runner.in_sandbox() as sb:
117 apio_ctx = ApioContext(
118 project_policy=ProjectPolicy.NO_PROJECT,
119 remote_config_policy=RemoteConfigPolicy.CACHED_OK,
120 packages_policy=PackagesPolicy.ENSURE_PACKAGES,
121 )
123 # -- Write a test profile.json file. We set an old apio version
124 # -- to cause the cached remote config to be classified as stale.
125 path = sb.home_dir / "profile.json"
126 original_loaded_by = "0.9.6"
127 assert original_loaded_by != util.get_apio_version_str()
128 test_data = get_test_data(apio_ctx, original_loaded_by, 0)
130 sb.write_file(
131 path,
132 json.dumps(
133 test_data,
134 indent=2,
135 ),
136 exists_ok=True,
137 )
139 # -- Read back the content.
140 profile = Profile(
141 sb.home_dir,
142 sb.packages_dir,
143 apio_ctx.profile.remote_config_url,
144 5, # TTL in days
145 60, # Remote config retry mins.
146 RemoteConfigPolicy.CACHED_OK,
147 )
149 # -- Verify. Remote config should be a fresh one, loaded by this
150 # -- apio version.
151 assert profile.preferences == test_data["preferences"]
152 # assert profile.installed_packages == test_data["installed-packages"]
153 assert (
154 profile.remote_config["metadata"]["loaded-by"]
155 == util.get_apio_version_str()
156 )
159def test_profile_with_corrupt_profile_file(
160 apio_runner: ApioRunner, capsys: LogCaptureFixture
161):
162 """Tests that a corrupt profile.json results in a clean error message
163 instead of an unhandled JSONDecodeError on every command."""
165 bad_contents = [
166 "{ corrupt json", # -- Not a valid json.
167 "[1, 2, 3]", # -- Not a json dict.
168 '{"remote-config": "corrupt"}', # -- Field is not a dict.
169 '{"preferences": "corrupt"}', # -- Field is not a dict.
170 '{"installed-packages": {"examples": 5}}', # -- Package not a dict.
171 ]
173 with apio_runner.in_sandbox() as sb:
175 # -- A packages dir that is private to this test. We don't use
176 # -- sb.packages_dir since it may point to the shared packages cache.
177 packages_dir = sb.sandbox_dir / "packages"
179 for bad_content in bad_contents:
181 # -- Write a corrupt profile.json file.
182 sb.write_file(
183 sb.home_dir / "profile.json", bad_content, exists_ok=True
184 )
186 # -- The theme reader should quietly fall back to the default.
187 assert Profile.read_preferences_theme(default="dark") == "dark"
189 # -- Loading the profile should exit with a clean error message.
190 with raises(SystemExit) as e:
191 Profile(
192 sb.home_dir,
193 packages_dir,
194 "http://localhost/apio-{major}.{minor}.jsonc",
195 5, # TTL in days
196 60, # Remote config retry mins.
197 RemoteConfigPolicy.CACHED_OK,
198 )
199 assert e.value.code == 1, bad_content
200 assert "Invalid profile file" in capsys.readouterr().out
203def test_profile_with_unknown_theme(apio_runner: ApioRunner):
204 """Tests that an unknown theme name in profile.json falls back to the
205 default theme instead of crashing every command with an
206 AssertionError."""
208 with apio_runner.in_sandbox() as sb:
209 for bogus_theme in ["no-such-theme", ["dark"], 5, None]:
211 # -- Write a profile.json file with a bogus theme value.
212 sb.write_file(
213 sb.home_dir / "profile.json",
214 json.dumps({"preferences": {"theme": bogus_theme}}),
215 exists_ok=True,
216 )
218 # -- The theme reader should quietly fall back to the default.
219 assert Profile.read_preferences_theme(default="light") == "light"
222def test_profile_with_corrupt_packages_index(
223 apio_runner: ApioRunner, capsys: LogCaptureFixture
224):
225 """Tests that a corrupt installed_packages.json results in a clean
226 error message instead of an unhandled exception on every command."""
228 bad_contents = [
229 "not json", # -- Not a valid json.
230 "[1, 2, 3]", # -- Not a json dict.
231 '{"examples": "boom"}', # -- Package info is not a dict.
232 ]
234 with apio_runner.in_sandbox() as sb:
236 # -- A packages dir that is private to this test. We don't use
237 # -- sb.packages_dir since it may point to the shared packages cache.
238 packages_dir = sb.sandbox_dir / "packages"
240 for bad_content in bad_contents:
242 # -- Write a corrupt installed_packages.json file.
243 sb.write_file(
244 packages_dir / "installed_packages.json",
245 bad_content,
246 exists_ok=True,
247 )
249 # -- Loading the profile should exit with a clean error message.
250 with raises(SystemExit) as e:
251 Profile(
252 sb.home_dir,
253 packages_dir,
254 "http://localhost/apio-{major}.{minor}.jsonc",
255 5, # TTL in days
256 60, # Remote config retry mins.
257 RemoteConfigPolicy.CACHED_OK,
258 )
259 assert e.value.code == 1, bad_content
260 assert "Invalid packages index file" in capsys.readouterr().out
263def test_datetime_stamp_diff_days():
264 """Test the datetime timestamp diff."""
266 assert (
267 get_datetime_stamp(
268 datetime(year=2025, month=6, day=30, hour=14, minute=45)
269 )
270 == "2025-06-30-14-45"
271 )
273 ts_now = get_datetime_stamp()
274 assert (
275 days_between_datetime_stamps(
276 ts_now,
277 ts_now,
278 default=9999,
279 )
280 == 0
281 )
283 assert (
284 days_between_datetime_stamps(
285 "2025-06-15-07-30",
286 "2025-06-16-00-01",
287 default=9999,
288 )
289 == 1
290 )
292 assert (
293 days_between_datetime_stamps(
294 "2025-06-16-00-01",
295 "2025-06-15-07-30",
296 default=9999,
297 )
298 == -1
299 )
301 assert (
302 days_between_datetime_stamps(
303 "2025-06-15-00-00",
304 "2025-06-15-23-59",
305 default=9999,
306 )
307 == 0
308 )
310 assert (
311 days_between_datetime_stamps(
312 "2025-06-15-00-0x",
313 "2025-06-15-23-59",
314 default=9999,
315 )
316 == 9999
317 )
319 assert (
320 days_between_datetime_stamps(
321 "2025-06-15-20-15",
322 "2025-06-20-00-01",
323 default=9999,
324 )
325 == 5
326 )