spl-core 7.14.0rc4.dev2__py3-none-any.whl → 7.15.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spl_core/__init__.py +1 -1
- spl_core/common.cmake +23 -1
- spl_core/test_utils/artifacts_archiver.py +208 -19
- spl_core/test_utils/junit_merger.py +122 -0
- {spl_core-7.14.0rc4.dev2.dist-info → spl_core-7.15.0rc1.dist-info}/METADATA +2 -1
- {spl_core-7.14.0rc4.dev2.dist-info → spl_core-7.15.0rc1.dist-info}/RECORD +9 -8
- {spl_core-7.14.0rc4.dev2.dist-info → spl_core-7.15.0rc1.dist-info}/WHEEL +1 -1
- spl_core-7.15.0rc1.dist-info/entry_points.txt +4 -0
- spl_core-7.14.0rc4.dev2.dist-info/entry_points.txt +0 -3
- {spl_core-7.14.0rc4.dev2.dist-info → spl_core-7.15.0rc1.dist-info}/licenses/LICENSE +0 -0
spl_core/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "7.
|
|
1
|
+
__version__ = "7.15.0-rc.1"
|
spl_core/common.cmake
CHANGED
|
@@ -606,15 +606,36 @@ set(COV_OUT_JSON coverage.json)
|
|
|
606
606
|
function(_spl_coverage_create_overall_report)
|
|
607
607
|
if(_SPL_COVERAGE_CREATE_OVERALL_REPORT_IS_NECESSARY)
|
|
608
608
|
set(COV_OUT_VARIANT_HTML reports/coverage/index.html)
|
|
609
|
+
set(COV_OUT_VARIANT_JSON variant-coverage.json)
|
|
610
|
+
set(JUNIT_OUT_VARIANT_XML variant-junit.xml)
|
|
611
|
+
|
|
612
|
+
# Generate variant-level merged coverage JSON
|
|
613
|
+
add_custom_command(
|
|
614
|
+
OUTPUT ${COV_OUT_VARIANT_JSON}
|
|
615
|
+
COMMAND gcovr --root ${CMAKE_SOURCE_DIR} --add-tracefile \"${CMAKE_CURRENT_BINARY_DIR}/**/${COV_OUT_JSON}\" --json --output ${COV_OUT_VARIANT_JSON}
|
|
616
|
+
DEPENDS ${GLOBAL_COMPONENTS_COVERAGE_JSON_LIST}
|
|
617
|
+
COMMENT "Generating variant-level merged coverage JSON ${COV_OUT_VARIANT_JSON} ..."
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Generate variant-level merged JUnit XML
|
|
621
|
+
add_custom_command(
|
|
622
|
+
OUTPUT ${JUNIT_OUT_VARIANT_XML}
|
|
623
|
+
COMMAND junit_merger --output ${JUNIT_OUT_VARIANT_XML} --inputs ${GLOBAL_COMPONENTS_JUNIT_XML_LIST}
|
|
624
|
+
DEPENDS ${GLOBAL_COMPONENTS_JUNIT_XML_LIST}
|
|
625
|
+
COMMENT "Generating variant-level merged JUnit XML ${JUNIT_OUT_VARIANT_XML} ..."
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Generate variant-level HTML coverage report
|
|
609
629
|
add_custom_command(
|
|
610
630
|
OUTPUT ${COV_OUT_VARIANT_HTML}
|
|
611
631
|
COMMAND gcovr --root ${CMAKE_SOURCE_DIR} --add-tracefile \"${CMAKE_CURRENT_BINARY_DIR}/**/${COV_OUT_JSON}\" --html --html-details --output ${COV_OUT_VARIANT_HTML}
|
|
612
632
|
DEPENDS ${GLOBAL_COMPONENTS_COVERAGE_JSON_LIST}
|
|
613
633
|
COMMENT "Generating overall code coverage report ${COV_OUT_VARIANT_HTML} ..."
|
|
614
634
|
)
|
|
635
|
+
|
|
615
636
|
add_custom_target(
|
|
616
637
|
unittests
|
|
617
|
-
DEPENDS coverage ${COV_OUT_VARIANT_HTML}
|
|
638
|
+
DEPENDS coverage ${COV_OUT_VARIANT_HTML} ${COV_OUT_VARIANT_JSON} ${JUNIT_OUT_VARIANT_XML}
|
|
618
639
|
)
|
|
619
640
|
add_custom_target(
|
|
620
641
|
coverage_overall_report
|
|
@@ -718,6 +739,7 @@ macro(_spl_add_test_suite COMPONENT_NAME PROD_SRC TEST_SOURCES)
|
|
|
718
739
|
DEPENDS ${exe_name}
|
|
719
740
|
)
|
|
720
741
|
|
|
742
|
+
set(GLOBAL_COMPONENTS_JUNIT_XML_LIST "${GLOBAL_COMPONENTS_JUNIT_XML_LIST};${CMAKE_CURRENT_BINARY_DIR}/${TEST_OUT_JUNIT}" CACHE INTERNAL "List of all ${TEST_OUT_JUNIT} files")
|
|
721
743
|
set(GLOBAL_COMPONENTS_COVERAGE_JSON_LIST "${GLOBAL_COMPONENTS_COVERAGE_JSON_LIST};${CMAKE_CURRENT_BINARY_DIR}/${COV_OUT_JSON}" CACHE INTERNAL "List of all ${COV_OUT_JSON} files")
|
|
722
744
|
|
|
723
745
|
# Create coverage results (coverage.json)
|
|
@@ -3,9 +3,45 @@ import os
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, List, Optional
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
import py7zr
|
|
9
|
+
from py_app_dev.core.logging import logger
|
|
10
|
+
from py_app_dev.core.subprocess import SubprocessExecutor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BuildMetadata:
|
|
15
|
+
"""
|
|
16
|
+
Contains build metadata extracted from environment variables.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
branch_name: The branch name, PR identifier (e.g., "PR-123"), or tag name
|
|
20
|
+
build_number: The build number or "local_build"
|
|
21
|
+
is_tag: Whether this is a tag build
|
|
22
|
+
pr_number: The PR number (without "PR-" prefix) for pull request builds, None otherwise
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
branch_name: str
|
|
26
|
+
build_number: str
|
|
27
|
+
is_tag: bool
|
|
28
|
+
pr_number: Optional[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class GitMetadata:
|
|
33
|
+
"""
|
|
34
|
+
Contains git metadata extracted from environment variables or git commands.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
commit_id: The git commit SHA (full hash)
|
|
38
|
+
commit_message: The git commit message subject line (first line only)
|
|
39
|
+
repository_url: The git repository URL (from remote.origin.url)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
commit_id: Optional[str]
|
|
43
|
+
commit_message: Optional[str]
|
|
44
|
+
repository_url: Optional[str]
|
|
9
45
|
|
|
10
46
|
|
|
11
47
|
class ArtifactsArchive:
|
|
@@ -84,7 +120,7 @@ class ArtifactsArchive:
|
|
|
84
120
|
archive_path.unlink()
|
|
85
121
|
|
|
86
122
|
if not self.archive_artifacts:
|
|
87
|
-
|
|
123
|
+
logger.warning("No artifacts registered for archiving")
|
|
88
124
|
# Create empty 7z file
|
|
89
125
|
with py7zr.SevenZipFile(archive_path, "w") as archive:
|
|
90
126
|
pass
|
|
@@ -94,7 +130,7 @@ class ArtifactsArchive:
|
|
|
94
130
|
with py7zr.SevenZipFile(archive_path, "w") as archive:
|
|
95
131
|
for artifact in self.archive_artifacts:
|
|
96
132
|
if not artifact.absolute_path.exists():
|
|
97
|
-
|
|
133
|
+
logger.warning(f"Artifact {artifact.absolute_path} does not exist, skipping")
|
|
98
134
|
continue
|
|
99
135
|
|
|
100
136
|
try:
|
|
@@ -104,13 +140,13 @@ class ArtifactsArchive:
|
|
|
104
140
|
# py7zr can handle directories directly
|
|
105
141
|
archive.writeall(artifact.absolute_path, arcname=str(artifact.archive_path))
|
|
106
142
|
except Exception as file_error:
|
|
107
|
-
|
|
143
|
+
logger.warning(f"Failed to add {artifact.absolute_path} to archive: {file_error}")
|
|
108
144
|
continue
|
|
109
145
|
|
|
110
|
-
|
|
146
|
+
logger.info(f"7z file created at: {archive_path}")
|
|
111
147
|
return archive_path
|
|
112
148
|
except Exception as e:
|
|
113
|
-
|
|
149
|
+
logger.error(f"Error creating artifacts 7z file: {e}")
|
|
114
150
|
raise e
|
|
115
151
|
|
|
116
152
|
|
|
@@ -120,10 +156,10 @@ class ArtifactsArchiver:
|
|
|
120
156
|
It provides a unified interface for registering artifacts to different archives.
|
|
121
157
|
"""
|
|
122
158
|
|
|
123
|
-
def __init__(self) -> None:
|
|
159
|
+
def __init__(self, artifactory_base_url: Optional[str] = None) -> None:
|
|
124
160
|
self.archives: Dict[str, ArtifactsArchive] = {}
|
|
125
161
|
self._target_repos: Dict[str, str] = {}
|
|
126
|
-
|
|
162
|
+
self.artifactory_base_url = artifactory_base_url
|
|
127
163
|
|
|
128
164
|
def add_archive(self, out_dir: Path, archive_filename: str, target_repo: Optional[str] = None, archive_name: str = "default") -> ArtifactsArchive:
|
|
129
165
|
"""
|
|
@@ -179,6 +215,37 @@ class ArtifactsArchiver:
|
|
|
179
215
|
|
|
180
216
|
return self.archives[archive_name]
|
|
181
217
|
|
|
218
|
+
def get_archive_url(self, archive_name: str = "default") -> Optional[str]:
|
|
219
|
+
"""
|
|
220
|
+
Get the Artifactory URL for a specific archive.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
archive_name: Name of the archive (defaults to "default")
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The full Artifactory URL for the archive, or None if no target repo configured
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
"https://artifactory.example.com/artifactory/my-repo/results/develop/123/result.7z"
|
|
230
|
+
"""
|
|
231
|
+
if archive_name not in self.archives:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
if archive_name not in self._target_repos:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
if self.artifactory_base_url is None:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
archive = self.archives[archive_name]
|
|
241
|
+
target_repo = self._target_repos[archive_name]
|
|
242
|
+
metadata = self._get_build_metadata()
|
|
243
|
+
|
|
244
|
+
# Construct the URL following the same pattern as create_rt_upload_json
|
|
245
|
+
archive_url = f"{self.artifactory_base_url}/{target_repo}/{metadata.branch_name}/{metadata.build_number}/{archive.archive_name}"
|
|
246
|
+
|
|
247
|
+
return archive_url
|
|
248
|
+
|
|
182
249
|
def create_all_archives(self) -> Dict[str, Path]:
|
|
183
250
|
"""
|
|
184
251
|
Create all registered archives.
|
|
@@ -217,7 +284,7 @@ class ArtifactsArchiver:
|
|
|
217
284
|
return 28 # 4 weeks for PRs, feature branches, and other branches
|
|
218
285
|
|
|
219
286
|
@staticmethod
|
|
220
|
-
def _get_build_metadata() ->
|
|
287
|
+
def _get_build_metadata() -> BuildMetadata:
|
|
221
288
|
"""
|
|
222
289
|
Get build metadata from environment variables or defaults.
|
|
223
290
|
|
|
@@ -225,14 +292,16 @@ class ArtifactsArchiver:
|
|
|
225
292
|
to local development defaults.
|
|
226
293
|
|
|
227
294
|
Returns:
|
|
228
|
-
|
|
229
|
-
- branch_name: The branch
|
|
295
|
+
BuildMetadata instance containing:
|
|
296
|
+
- branch_name: The branch name, PR identifier (e.g., "PR-123"), or tag name
|
|
230
297
|
- build_number: The build number or "local_build"
|
|
231
298
|
- is_tag: Whether this is a tag build
|
|
299
|
+
- pr_number: The PR number (without "PR-" prefix) for pull requests, None otherwise
|
|
232
300
|
"""
|
|
233
301
|
branch_name = "local_branch"
|
|
234
302
|
build_number = "local_build"
|
|
235
303
|
is_tag = False
|
|
304
|
+
pr_number = None
|
|
236
305
|
|
|
237
306
|
if os.environ.get("JENKINS_URL"):
|
|
238
307
|
change_id = os.environ.get("CHANGE_ID")
|
|
@@ -243,6 +312,7 @@ class ArtifactsArchiver:
|
|
|
243
312
|
if change_id:
|
|
244
313
|
# Pull request case
|
|
245
314
|
branch_name = f"PR-{change_id}"
|
|
315
|
+
pr_number = change_id
|
|
246
316
|
elif tag_name:
|
|
247
317
|
# Tag build case
|
|
248
318
|
branch_name = tag_name
|
|
@@ -254,7 +324,79 @@ class ArtifactsArchiver:
|
|
|
254
324
|
if jenkins_build_number:
|
|
255
325
|
build_number = jenkins_build_number
|
|
256
326
|
|
|
257
|
-
return
|
|
327
|
+
return BuildMetadata(
|
|
328
|
+
branch_name=branch_name,
|
|
329
|
+
build_number=build_number,
|
|
330
|
+
is_tag=is_tag,
|
|
331
|
+
pr_number=pr_number,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _get_git_metadata() -> GitMetadata:
|
|
336
|
+
"""
|
|
337
|
+
Get git metadata from environment variables or git commands.
|
|
338
|
+
|
|
339
|
+
Attempts to retrieve git information in the following order:
|
|
340
|
+
1. Environment variables (GIT_COMMIT, GIT_URL) - typically set by Jenkins Git plugin
|
|
341
|
+
2. Git commands as fallback - executed locally using git CLI
|
|
342
|
+
|
|
343
|
+
The commit message captured is only the subject line (first line), not the full message.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
GitMetadata instance containing:
|
|
347
|
+
- commit_id: The git commit SHA, or None if unavailable
|
|
348
|
+
- commit_message: The commit subject line (first line only), or None if unavailable
|
|
349
|
+
- repository_url: The git repository URL, or None if unavailable
|
|
350
|
+
"""
|
|
351
|
+
commit_id = None
|
|
352
|
+
commit_message = None
|
|
353
|
+
repository_url = None
|
|
354
|
+
|
|
355
|
+
# Try environment variables first (Jenkins Git plugin)
|
|
356
|
+
env_commit = os.environ.get("GIT_COMMIT")
|
|
357
|
+
env_url = os.environ.get("GIT_URL")
|
|
358
|
+
|
|
359
|
+
if env_commit:
|
|
360
|
+
commit_id = env_commit if env_commit.strip() else None
|
|
361
|
+
if env_url:
|
|
362
|
+
repository_url = env_url if env_url.strip() else None
|
|
363
|
+
|
|
364
|
+
# Fallback to git commands if environment variables not available
|
|
365
|
+
# Get commit ID
|
|
366
|
+
if not commit_id:
|
|
367
|
+
try:
|
|
368
|
+
result = SubprocessExecutor(["git", "rev-parse", "HEAD"]).execute(handle_errors=False)
|
|
369
|
+
if result and result.returncode == 0:
|
|
370
|
+
value = result.stdout.strip()
|
|
371
|
+
commit_id = value if value else None
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.warning(f"Failed to get commit ID from git: {e}")
|
|
374
|
+
|
|
375
|
+
# Get commit message (subject line only)
|
|
376
|
+
if not commit_message:
|
|
377
|
+
try:
|
|
378
|
+
result = SubprocessExecutor(["git", "log", "-1", "--format=%s"]).execute(handle_errors=False)
|
|
379
|
+
if result and result.returncode == 0:
|
|
380
|
+
value = result.stdout.strip()
|
|
381
|
+
commit_message = value if value else None
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.warning(f"Failed to get commit message from git: {e}")
|
|
384
|
+
|
|
385
|
+
# Get repository URL
|
|
386
|
+
if not repository_url:
|
|
387
|
+
try:
|
|
388
|
+
result = SubprocessExecutor(["git", "config", "--get", "remote.origin.url"]).execute(handle_errors=False)
|
|
389
|
+
if result and result.returncode == 0:
|
|
390
|
+
value = result.stdout.strip()
|
|
391
|
+
repository_url = value if value else None
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.warning(f"Failed to get repository URL from git: {e}")
|
|
394
|
+
|
|
395
|
+
return GitMetadata(
|
|
396
|
+
commit_id=commit_id,
|
|
397
|
+
commit_message=commit_message,
|
|
398
|
+
repository_url=repository_url,
|
|
399
|
+
)
|
|
258
400
|
|
|
259
401
|
def create_rt_upload_json(self, out_dir: Path) -> Path:
|
|
260
402
|
"""
|
|
@@ -271,10 +413,10 @@ class ArtifactsArchiver:
|
|
|
271
413
|
Path to the created rt-upload.json file
|
|
272
414
|
"""
|
|
273
415
|
# Get build metadata from environment or defaults
|
|
274
|
-
|
|
416
|
+
metadata = self._get_build_metadata()
|
|
275
417
|
|
|
276
418
|
# Calculate retention period based on branch/tag
|
|
277
|
-
retention_period = self.calculate_retention_period(branch_name, is_tag)
|
|
419
|
+
retention_period = self.calculate_retention_period(metadata.branch_name, metadata.is_tag)
|
|
278
420
|
|
|
279
421
|
# Create the files array for Artifactory upload format
|
|
280
422
|
files_array = []
|
|
@@ -284,7 +426,7 @@ class ArtifactsArchiver:
|
|
|
284
426
|
target_repo = self._target_repos[archive_name]
|
|
285
427
|
|
|
286
428
|
# Construct the RT target path
|
|
287
|
-
rt_target = f"{target_repo}/{branch_name}/{build_number}/"
|
|
429
|
+
rt_target = f"{target_repo}/{metadata.branch_name}/{metadata.build_number}/"
|
|
288
430
|
|
|
289
431
|
# Add this archive to the files array with retention_period property
|
|
290
432
|
files_array.append(
|
|
@@ -316,6 +458,17 @@ class ArtifactsArchiver:
|
|
|
316
458
|
but no artifacts. Use update_artifacts_json() to add artifact categories.
|
|
317
459
|
It uses Jenkins environment variables when available, otherwise falls back to default values.
|
|
318
460
|
|
|
461
|
+
The JSON file includes conditional keys based on the build type:
|
|
462
|
+
- For pull requests: includes "pull_request" key with the PR number (e.g., "117")
|
|
463
|
+
- For tag builds: includes "tag" key with the tag name (e.g., "v1.2.3")
|
|
464
|
+
- For regular branch builds: includes "branch" key with the branch name (e.g., "develop")
|
|
465
|
+
|
|
466
|
+
Optional fields (included only if available):
|
|
467
|
+
- build_url: Jenkins build URL from BUILD_URL environment variable
|
|
468
|
+
- commit_id: Git commit SHA from GIT_COMMIT env var or git rev-parse HEAD
|
|
469
|
+
- commit_message: Git commit subject line from git log (first line only)
|
|
470
|
+
- repository_url: Git repository URL from GIT_URL env var or git config
|
|
471
|
+
|
|
319
472
|
Args:
|
|
320
473
|
variant: The variant name (e.g., "Disco")
|
|
321
474
|
out_dir: Directory where the artifacts.json file will be created
|
|
@@ -330,11 +483,43 @@ class ArtifactsArchiver:
|
|
|
330
483
|
if not variant or not variant.strip():
|
|
331
484
|
raise ValueError("Variant name cannot be empty or None")
|
|
332
485
|
|
|
333
|
-
# Get
|
|
334
|
-
|
|
486
|
+
# Get metadata from environment or defaults
|
|
487
|
+
build_metadata = self._get_build_metadata()
|
|
488
|
+
git_metadata = self._get_git_metadata()
|
|
489
|
+
|
|
490
|
+
# Create the initial artifacts.json structure with base metadata
|
|
491
|
+
artifacts_data: Dict[str, Any] = {
|
|
492
|
+
"variant": variant,
|
|
493
|
+
"build_timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z",
|
|
494
|
+
"build_number": build_metadata.build_number,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
# Add build_url if available
|
|
498
|
+
build_url = os.environ.get("BUILD_URL")
|
|
499
|
+
if build_url:
|
|
500
|
+
artifacts_data["build_url"] = build_url
|
|
501
|
+
|
|
502
|
+
# Add conditional keys based on build type
|
|
503
|
+
if build_metadata.pr_number:
|
|
504
|
+
# Pull request build
|
|
505
|
+
artifacts_data["pull_request"] = build_metadata.pr_number
|
|
506
|
+
elif build_metadata.is_tag:
|
|
507
|
+
# Tag build
|
|
508
|
+
artifacts_data["tag"] = build_metadata.branch_name
|
|
509
|
+
else:
|
|
510
|
+
# Regular branch build (or local build)
|
|
511
|
+
artifacts_data["branch"] = build_metadata.branch_name
|
|
512
|
+
|
|
513
|
+
# Add git metadata if available
|
|
514
|
+
if git_metadata.commit_id:
|
|
515
|
+
artifacts_data["commit_id"] = git_metadata.commit_id
|
|
516
|
+
if git_metadata.commit_message:
|
|
517
|
+
artifacts_data["commit_message"] = git_metadata.commit_message
|
|
518
|
+
if git_metadata.repository_url:
|
|
519
|
+
artifacts_data["repository_url"] = git_metadata.repository_url
|
|
335
520
|
|
|
336
|
-
#
|
|
337
|
-
artifacts_data
|
|
521
|
+
# Add empty artifacts dictionary
|
|
522
|
+
artifacts_data["artifacts"] = {}
|
|
338
523
|
|
|
339
524
|
# Create the artifacts.json file
|
|
340
525
|
json_path = out_dir / "artifacts.json"
|
|
@@ -459,6 +644,10 @@ class ArtifactsArchiver:
|
|
|
459
644
|
# out_dir = Path("./build/output")
|
|
460
645
|
#
|
|
461
646
|
# # Create initial artifacts.json file first, then add categories
|
|
647
|
+
# # The resulting JSON will contain conditional keys based on build type:
|
|
648
|
+
# # - For PRs: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "pull_request": "117", "artifacts": {}}
|
|
649
|
+
# # - For tags: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "tag": "v1.2.3", "artifacts": {}}
|
|
650
|
+
# # - For branches: {"variant": "Disco", "build_timestamp": "...", "build_number": "123", "branch": "develop", "artifacts": {}}
|
|
462
651
|
# artifacts_json_path = archiver.create_artifacts_json(variant, out_dir)
|
|
463
652
|
# archiver.update_artifacts_json("test_reports", test_reports, artifacts_json_path)
|
|
464
653
|
# archiver.update_artifacts_json("sca_reports", sca_reports, artifacts_json_path)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JUnit XML Merger
|
|
3
|
+
|
|
4
|
+
Merges multiple JUnit XML files into a single variant-level JUnit XML file.
|
|
5
|
+
This enables CI/CD tooling to consume aggregated test results from all components.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from junitparser import JUnitXml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JUnitMergerError(Exception):
|
|
17
|
+
"""Exception raised when JUnit XML merging fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JUnitMerger:
|
|
23
|
+
"""
|
|
24
|
+
Merges multiple JUnit XML files into a single variant-level JUnit XML file.
|
|
25
|
+
|
|
26
|
+
This class provides functionality to aggregate test results from multiple
|
|
27
|
+
components into a unified JUnit XML file for CI/CD tooling.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, input_files: List[str], output_file: str):
|
|
31
|
+
"""
|
|
32
|
+
Initialize the JUnit merger.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
input_files: List of paths to input JUnit XML files
|
|
36
|
+
output_file: Path to output merged JUnit XML file
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If input_files list is empty
|
|
40
|
+
"""
|
|
41
|
+
if not input_files:
|
|
42
|
+
raise ValueError("No input files provided for merging")
|
|
43
|
+
|
|
44
|
+
self.input_files = input_files
|
|
45
|
+
self.output_file = output_file
|
|
46
|
+
|
|
47
|
+
def merge(self) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Merge all input JUnit XML files into the output file.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
FileNotFoundError: If any input file doesn't exist
|
|
53
|
+
Exception: If any input file contains malformed XML or validation fails
|
|
54
|
+
"""
|
|
55
|
+
# Create merged JUnit XML container
|
|
56
|
+
merged_xml = JUnitXml()
|
|
57
|
+
|
|
58
|
+
# Process each input file
|
|
59
|
+
for input_path in self.input_files:
|
|
60
|
+
input_file = Path(input_path)
|
|
61
|
+
|
|
62
|
+
if not input_file.exists():
|
|
63
|
+
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Parse the input JUnit XML file
|
|
67
|
+
xml = JUnitXml.fromfile(str(input_file))
|
|
68
|
+
|
|
69
|
+
# Add all test suites from this file to the merged result
|
|
70
|
+
for suite in xml:
|
|
71
|
+
merged_xml.add_testsuite(suite)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise JUnitMergerError(f"Failed to parse JUnit XML file '{input_path}': {e}") from e
|
|
75
|
+
|
|
76
|
+
# Write merged result to output file
|
|
77
|
+
output_path = Path(self.output_file)
|
|
78
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
merged_xml.write(str(output_path))
|
|
80
|
+
|
|
81
|
+
# Validate the output
|
|
82
|
+
self._validate_output()
|
|
83
|
+
|
|
84
|
+
def _validate_output(self) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Validate that the merged output file is valid JUnit XML.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
Exception: If the output file cannot be parsed as valid JUnit XML
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
JUnitXml.fromfile(str(self.output_file))
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise JUnitMergerError(f"Validation failed: merged output is not valid JUnit XML: {e}") from e
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main() -> int:
|
|
98
|
+
"""
|
|
99
|
+
Main entry point for command-line interface.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Exit code (0 for success, non-zero for failure)
|
|
103
|
+
"""
|
|
104
|
+
parser = argparse.ArgumentParser(description="Merge multiple JUnit XML files into a single variant-level file")
|
|
105
|
+
parser.add_argument("--output", required=True, help="Path to output merged JUnit XML file")
|
|
106
|
+
parser.add_argument("--inputs", nargs="+", required=True, help="Paths to input JUnit XML files to merge")
|
|
107
|
+
|
|
108
|
+
args = parser.parse_args()
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
merger = JUnitMerger(args.inputs, args.output)
|
|
112
|
+
merger.merge()
|
|
113
|
+
print(f"Successfully merged {len(args.inputs)} JUnit XML file(s) into {args.output}")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"ERROR: Failed to merge JUnit XML files: {e}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spl-core
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.15.0rc1
|
|
4
4
|
Summary: Software Product Line Support for CMake
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -20,6 +20,7 @@ Requires-Dist: cookiecutter (==2.6.0)
|
|
|
20
20
|
Requires-Dist: doxysphinx (>=3.3,<4.0)
|
|
21
21
|
Requires-Dist: gcovr (>=8.3,<9.0)
|
|
22
22
|
Requires-Dist: hammocking (>=0.8,<0.10)
|
|
23
|
+
Requires-Dist: junitparser
|
|
23
24
|
Requires-Dist: kconfiglib (>=14.1,<15.0)
|
|
24
25
|
Requires-Dist: mlx-traceability (>=10,<12)
|
|
25
26
|
Requires-Dist: myst-parser (>=0.16)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
spl_core/__init__.py,sha256=
|
|
1
|
+
spl_core/__init__.py,sha256=rd-3AFgV7lu3heK1l9r3gZgWwebihPwp_6qLxbR_UMk,28
|
|
2
2
|
spl_core/__run.py,sha256=DphnN7_Bjiw_mOOztsHxTDHS8snz1g2MMWAaJpZxPKM,361
|
|
3
3
|
spl_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
spl_core/common/path.py,sha256=sDujd3n4XP1XGjHc7ImXEdjihO6A8BOIDbKCf7HgQ0Y,462
|
|
5
|
-
spl_core/common.cmake,sha256=
|
|
5
|
+
spl_core/common.cmake,sha256=0OPENrmJ30z8oZ_jjHg6yYTJPKEwP3xWKMY2kqGxB9Q,40465
|
|
6
6
|
spl_core/conan.cmake,sha256=i1AuyN-e8cczX7TI1nl6e3Y8N-EP-QXPVY7LG6NUyJY,41958
|
|
7
7
|
spl_core/config/KConfig,sha256=atlUwl0kPIdoGjbOI2PoaCQ2wgao7-mblZKn3dXUCxI,1755
|
|
8
8
|
spl_core/gcov_maid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -61,11 +61,12 @@ spl_core/main.py,sha256=_hL4j155WZMXog_755bgAH1PeUwvTdJZvVdVw9EWhvo,1225
|
|
|
61
61
|
spl_core/spl.cmake,sha256=YQMhpSJ9yZaJ34m_W1UqrlTh_r7AKMDuH6-hzK4w98A,4585
|
|
62
62
|
spl_core/steps/collect_pr_changes.py,sha256=su3yCtSJM9XDlMNOD0L72ooQdKzruc0lUat858Na0Kg,5539
|
|
63
63
|
spl_core/test_utils/archive_artifacts_collection.py,sha256=x7LH5dGIvssyhXsTFzB6rjgb5D2efKvHVpnjId3MNDk,5126
|
|
64
|
-
spl_core/test_utils/artifacts_archiver.py,sha256=
|
|
64
|
+
spl_core/test_utils/artifacts_archiver.py,sha256=xrsYbjlqpg4M0Xy1zSjQNcxDXJctom1ZtzR74ZJp-rs,25996
|
|
65
65
|
spl_core/test_utils/base_variant_test_runner.py,sha256=Oq27lkJlpB_y-p2_8S23F5zjn1438HW148q-hQNz3EY,3795
|
|
66
|
+
spl_core/test_utils/junit_merger.py,sha256=Ez3QbcQvaZJgIWdQiqoLsvvcZ4i5BbH3HLX2xFnjIOo,3886
|
|
66
67
|
spl_core/test_utils/spl_build.py,sha256=bSM6hwhTH9aRryvUvtSPDfk_zoZuKEO5g3QXK4SIrco,8442
|
|
67
|
-
spl_core-7.
|
|
68
|
-
spl_core-7.
|
|
69
|
-
spl_core-7.
|
|
70
|
-
spl_core-7.
|
|
71
|
-
spl_core-7.
|
|
68
|
+
spl_core-7.15.0rc1.dist-info/METADATA,sha256=mykQkPDS9ze6t4uL15KOeCYRvejqOsqc1W_CqiWAzyg,5344
|
|
69
|
+
spl_core-7.15.0rc1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
70
|
+
spl_core-7.15.0rc1.dist-info/entry_points.txt,sha256=Gwxb1NomJ8O2_c9q0f2F4RuG2luGO8mzj4aA7tf95Uc,96
|
|
71
|
+
spl_core-7.15.0rc1.dist-info/licenses/LICENSE,sha256=UjjA0o8f5tT3wVm7qodTLAhPWLl6kgVyn9FPAd1VeYY,1099
|
|
72
|
+
spl_core-7.15.0rc1.dist-info/RECORD,,
|
|
File without changes
|