Rendering Engine 0.2.17
Modular Graphics Rendering Engine | v0.2.17
Loading...
Searching...
No Matches
project_workflow_tests.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# This file is part of the Rendering Engine project.
3# Author: Alexander Obzherin
4# Distributed under the terms of the zlib License. See LICENSE.md for details.
5"""Validate project creation and registration workflows on Linux and FreeBSD.
6
7Creates temporary internal and external applications, builds and launches
8them against the source engine or packaged SDK, and cleans up test artifacts.
9Requires an installed engine, a graphical environment, and an SDK archive
10for SDK workflow tests.
11"""
12
13import argparse
14from contextlib import contextmanager
15import os
16from pathlib import Path
17import shutil
18import subprocess
19import sys
20import tarfile
21import tempfile
22
23from application_smoke_tests import ROOT, run_application, run_build
24
25SCRIPTS = ROOT / "RenderingEngine" / "Scripts"
26INTERNAL_ROOT = ROOT / "UserApplications"
27INTERNAL_CMAKE = INTERNAL_ROOT / "CMakeLists.txt"
28SOURCE_REGISTRY = ROOT / "Local" / "registered_projects.cmake"
29
30
31@contextmanager
32def preserve_file(path):
33 """Restore an existing file byte-for-byte, or remove a newly created file."""
34 existed = path.is_file()
35 original = path.read_bytes() if existed else None
36 try:
37 yield
38 finally:
39 if existed:
40 path.parent.mkdir(parents=True, exist_ok=True)
41 path.write_bytes(original)
42 elif path.exists():
43 path.unlink()
44
45
46def verify_project(project):
47 if not project.is_dir():
48 raise RuntimeError(f"Project was not created: {project}")
49 for name in ("CMakeLists.txt", "build_project.sh"):
50 if not (project / name).is_file():
51 raise RuntimeError(f"Missing generated file: {project / name}")
52
53
54def run_checked(command, cwd, required_output=()):
55 """Execute a command, show its output, and verify expected CMake messages."""
56 print(f"\n$ {' '.join(map(str, command))}", flush=True)
57 result = subprocess.run(
58 command, cwd=cwd, stdout=subprocess.PIPE,
59 stderr=subprocess.STDOUT, text=True, errors="replace", check=False,
60 )
61 print(result.stdout, end="" if result.stdout.endswith("\n") else "\n", flush=True)
62 if result.returncode != 0:
63 raise RuntimeError(f"Command failed (exit code {result.returncode}): {command}")
64 for expected in required_output:
65 if expected not in result.stdout:
66 raise RuntimeError(f"Expected build output not found: {expected}")
67
68
69def verify_registration(project, engine_root):
70 config = project / "Local" / "rendering_engine.cmake"
71 if not config.is_file():
72 raise RuntimeError(f"Missing project registration: {config}")
73 expected = str(engine_root.resolve()).replace("\\", "/")
74 if f'"{expected}"' not in config.read_text():
75 raise RuntimeError(f"Project registration does not point to {expected}: {config}")
76
77
78def build_and_run(project, build_type, build_flag, seconds, shutdown_timeout,
79 external, expected_engine=None, expected_dev_mode=None):
80 command = ["sh", str(project / "build_project.sh"), build_flag]
81 if expected_engine is None:
82 run_build(command, project)
83 else:
84 run_checked(command, project, (
85 f"RenderingEngine: using registered engine at {expected_engine}",
86 f"RenderingEngine: RE_DEV_MODE={expected_dev_mode}",
87 ))
88
89 build_root = (project / "Build" if external else
90 ROOT / "Build" / "UserApplications" / project.name)
91 executable = build_root / build_type / project.name
92 if not executable.is_file() or not os.access(executable, os.X_OK):
93 raise RuntimeError(f"Executable missing or not executable: {executable}")
94 status, reason = run_application(executable, seconds, shutdown_timeout)
95 if status != "PASS":
96 raise RuntimeError(reason)
97 return reason
98
99
100def test_internal(build_type, build_flag, seconds, shutdown_timeout):
101 name = "HelloApp1"
102 project = INTERNAL_ROOT / name
103 build_dir = ROOT / "Build" / "UserApplications" / name
104 if project.exists() or project.is_symlink():
105 raise RuntimeError(f"Refusing to overwrite existing project: {project}")
106 if build_dir.exists() or build_dir.is_symlink():
107 raise RuntimeError(f"Refusing to overwrite existing build: {build_dir}")
108 if not INTERNAL_CMAKE.is_file():
109 raise RuntimeError(f"Missing internal CMake file: {INTERNAL_CMAKE}")
110
111 with preserve_file(INTERNAL_CMAKE):
112 try:
113 run_build([sys.executable, str(SCRIPTS / "create_project.py"), name], ROOT)
114 verify_project(project)
115 return build_and_run(project, build_type, build_flag,
116 seconds, shutdown_timeout, False)
117 finally:
118 if project.is_dir() and not project.is_symlink():
119 shutil.rmtree(project)
120 if build_dir.is_dir() and not build_dir.is_symlink():
121 shutil.rmtree(build_dir)
122
123
124def test_external(build_type, build_flag, seconds, shutdown_timeout):
125 name = "HelloApp1"
126 with tempfile.TemporaryDirectory(prefix="rendering-engine-workflow-") as temp:
127 destination = Path(temp)
128 project = destination / name
129 with preserve_file(SOURCE_REGISTRY):
130 run_build([sys.executable, str(SCRIPTS / "create_project.py"),
131 name, str(destination)], ROOT)
132 verify_project(project)
133 verify_registration(project, ROOT)
134 return build_and_run(project, build_type, build_flag,
135 seconds, shutdown_timeout, True, ROOT, "ON")
136
137
138def discover_archive(archive_arg):
139 if archive_arg:
140 archive = Path(archive_arg).expanduser().resolve()
141 if not archive.is_file():
142 raise RuntimeError(f"SDK archive not found: {archive}")
143 return archive
144 platform = "FreeBSD" if sys.platform.startswith("freebsd") else "Linux"
145 candidates = sorted((ROOT / "Build" / "Packages").glob(
146 f"RenderingEngine-*-SDK-{platform}.tar.gz"))
147 if len(candidates) != 1:
148 raise RuntimeError(
149 f"Expected exactly one {platform} SDK archive under Build/Packages; "
150 f"found {len(candidates)}. Specify --sdk-archive explicitly."
151 )
152 return candidates[0]
153
154
155def extract_sdk(archive, destination):
156 """Extract a trusted locally produced SDK archive, rejecting unsafe paths."""
157 with tarfile.open(archive, "r:gz") as tar:
158 members = tar.getmembers()
159 roots = {Path(member.name).parts[0] for member in members if Path(member.name).parts}
160 if len(roots) != 1:
161 raise RuntimeError(f"Expected one top-level SDK directory: {roots}")
162 root_name = roots.pop()
163 target_root = (destination / root_name).resolve()
164 for member in members:
165 target = (destination / member.name).resolve()
166 if not target.is_relative_to(target_root):
167 raise RuntimeError(f"Unsafe archive path: {member.name}")
168 if member.issym() or member.islnk() or member.isdev():
169 raise RuntimeError(f"Unsupported archive entry: {member.name}")
170 # The SDK is produced locally; use the validated member set.
171 tar.extractall(destination, members=members, filter="data")
172 sdk = destination / root_name
173 for relative in (
174 "RenderingEngine/Scripts/create_project.py",
175 "RenderingEngine/Scripts/register_project.py",
176 "RenderingEngine/RenderingLibrary/Library/libRenderingEngine.so",
177 ):
178 if not (sdk / relative).is_file():
179 raise RuntimeError(f"SDK is missing {relative}")
180 return sdk
181
182
183def test_sdk_created(sdk, workspace, build_type, build_flag, seconds, shutdown_timeout):
184 name = "HelloApp2"
185 destination = workspace / "sdk-created"
186 destination.mkdir()
187 project = destination / name
188 sdk_scripts = sdk / "RenderingEngine" / "Scripts"
189 sdk_registry = sdk / "Local" / "registered_projects.cmake"
190 with preserve_file(sdk_registry):
191 run_build([sys.executable, str(sdk_scripts / "create_project.py"),
192 name, str(destination)], sdk)
193 verify_project(project)
194 verify_registration(project, sdk)
195 return build_and_run(project, build_type, build_flag,
196 seconds, shutdown_timeout, True, sdk, "OFF")
197
198
199def test_sdk_registered(sdk, workspace, build_type, build_flag, seconds, shutdown_timeout):
200 name = "HelloApp3"
201 destination = workspace / "source-created"
202 destination.mkdir()
203 project = destination / name
204 sdk_scripts = sdk / "RenderingEngine" / "Scripts"
205 sdk_registry = sdk / "Local" / "registered_projects.cmake"
206 with preserve_file(SOURCE_REGISTRY), preserve_file(sdk_registry):
207 run_build([sys.executable, str(SCRIPTS / "create_project.py"),
208 name, str(destination)], ROOT)
209 verify_project(project)
210 verify_registration(project, ROOT)
211 run_build([sys.executable, str(sdk_scripts / "register_project.py"),
212 str(project)], sdk)
213 verify_registration(project, sdk)
214 return build_and_run(project, build_type, build_flag,
215 seconds, shutdown_timeout, True, sdk, "OFF")
216
217
218def main():
219 parser = argparse.ArgumentParser(description=__doc__)
220 parser.add_argument("--release", action="store_true", help="Build Release instead of Debug")
221 parser.add_argument("--seconds", type=float, default=3.0, help="GUI observation time")
222 parser.add_argument("--shutdown-timeout", type=float, default=3.0)
223 group = parser.add_mutually_exclusive_group()
224 group.add_argument("--internal-only", action="store_true")
225 group.add_argument("--external-only", action="store_true")
226 group.add_argument("--sdk-only", action="store_true")
227 parser.add_argument("--sdk-archive", help="Explicit path to packaged SDK .tar.gz")
228 parser.add_argument("--skip-sdk", action="store_true", help="Run developer-mode tests only")
229 args = parser.parse_args()
230 if os.name != "posix" or not (sys.platform == "linux" or sys.platform.startswith("freebsd")):
231 parser.error("This test supports Linux and FreeBSD only")
232 if args.seconds <= 0 or args.shutdown_timeout <= 0:
233 parser.error("Timeouts must be positive")
234 if args.sdk_only and args.skip_sdk:
235 parser.error("--sdk-only and --skip-sdk cannot be combined")
236
237 build_type = "Release" if args.release else "Debug"
238 build_flag = "--release" if args.release else "--debug"
239 results = []
240
241 def execute(name, test):
242 print(f"\n=== {name} ===", flush=True)
243 try:
244 reason = test()
245 status = "PASS"
246 except (OSError, RuntimeError, tarfile.TarError) as exc:
247 status, reason = "FAIL", str(exc)
248 results.append((name, status, reason))
249 print(f"{status}: {name}: {reason}", flush=True)
250
251 print(f"Rendering Engine root: {ROOT}")
252 print(f"Build configuration: {build_type}")
253 print("Prerequisite: the engine and SDK archive are already built.", flush=True)
254
255 if not args.sdk_only and not args.external_only:
256 execute("Internal project creation", lambda: test_internal(
257 build_type, build_flag, args.seconds, args.shutdown_timeout))
258 if not args.sdk_only and not args.internal_only:
259 execute("External project creation", lambda: test_external(
260 build_type, build_flag, args.seconds, args.shutdown_timeout))
261
262 if not (args.skip_sdk or args.internal_only or args.external_only):
263 try:
264 archive = discover_archive(args.sdk_archive)
265 print(f"SDK archive: {archive}", flush=True)
266 # All test artifacts stay under Build; the archive is not modified.
267 with tempfile.TemporaryDirectory(prefix="sdk-workflow-", dir=ROOT / "Build") as temp:
268 workspace = Path(temp)
269 sdk = extract_sdk(archive, workspace)
270 print(f"Extracted SDK: {sdk}", flush=True)
271 execute("SDK project creation", lambda: test_sdk_created(
272 sdk, workspace, build_type, build_flag, args.seconds, args.shutdown_timeout))
273 execute("SDK registration", lambda: test_sdk_registered(
274 sdk, workspace, build_type, build_flag, args.seconds, args.shutdown_timeout))
275 except (OSError, RuntimeError, tarfile.TarError, ValueError) as exc:
276 execute("SDK preparation", lambda: (_ for _ in ()).throw(RuntimeError(str(exc))))
277
278 print("\n=== Summary ===")
279 for name, status, reason in results:
280 print(f"{status:4} {name}: {reason}")
281 passed = sum(status == "PASS" for _, status, _ in results)
282 print(f"Passed {passed}/{len(results)}")
283 return 0 if passed == len(results) else 1
284
285
286if __name__ == "__main__":
287 sys.exit(main())
test_external(build_type, build_flag, seconds, shutdown_timeout)
extract_sdk(archive, destination)
test_internal(build_type, build_flag, seconds, shutdown_timeout)
test_sdk_created(sdk, workspace, build_type, build_flag, seconds, shutdown_timeout)
run_checked(command, cwd, required_output=())
verify_registration(project, engine_root)
build_and_run(project, build_type, build_flag, seconds, shutdown_timeout, external, expected_engine=None, expected_dev_mode=None)
test_sdk_registered(sdk, workspace, build_type, build_flag, seconds, shutdown_timeout)