Skip to content

Commit 59880f0

Browse files
committed
test: add an integration test for build spec generation and add a script to compare 2 build specs in the tests
1 parent f169bac commit 59880f0

File tree

4 files changed

+258
-1
lines changed

4 files changed

+258
-1
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3+
4+
"""This script compares 2 Reproducible Central Buildspec files."""
5+
6+
import logging
7+
import os
8+
import sys
9+
from collections.abc import Callable
10+
11+
CompareFn = Callable[[object, object], bool]
12+
13+
logger = logging.getLogger(__name__)
14+
logger.setLevel(logging.DEBUG)
15+
logging.basicConfig(format="[%(filename)s:%(lineno)s %(tag)s] %(message)s")
16+
17+
18+
def log_with_tag(tag: str) -> Callable[[str], None]:
19+
"""Generate a log function that prints the name of the file and a tag at the beginning of each line."""
20+
21+
def log_fn(msg: str) -> None:
22+
logger.info(msg, extra={"tag": tag})
23+
24+
return log_fn
25+
26+
27+
log_info = log_with_tag("INFO")
28+
log_err = log_with_tag("ERROR")
29+
log_failed = log_with_tag("FAILED")
30+
log_passed = log_with_tag("PASSED")
31+
32+
33+
def log_diff_str(name: str, result: str, expected: str) -> None:
34+
"""Pretty-print the diff of two Python strings."""
35+
output = [
36+
f"'{name}'",
37+
*("---- Result ---", f"{result}"),
38+
*("---- Expected ---", f"{expected}"),
39+
"-----------------",
40+
]
41+
log_info("\n".join(output))
42+
43+
44+
def skip_compare(_result: object, _expected: object) -> bool:
45+
"""Return ``True`` always.
46+
47+
This compare function is used when we want to skip comparing a field.
48+
"""
49+
return True
50+
51+
52+
def compare_rc_build_spec(
53+
result: dict[str, str],
54+
expected: dict[str, str],
55+
compare_fn_map: dict[str, CompareFn],
56+
) -> bool:
57+
"""Compare two dictionaries obatained from 2 Reproducible Central build spec.
58+
59+
Parameters
60+
----------
61+
result : dict[str, str]
62+
The result object.
63+
expected : dict[str, str]
64+
The expected object.
65+
compare_fn_map : str
66+
A map from field name to corresponding compare function.
67+
68+
Returns
69+
-------
70+
bool
71+
``True`` if the comparison is successful, ``False`` otherwise.
72+
"""
73+
result_keys_only = result.keys() - expected.keys()
74+
expected_keys_only = expected.keys() - result.keys()
75+
76+
equal = True
77+
78+
if len(result_keys_only) > 0:
79+
log_err(f"Result has the following extraneous fields: {result_keys_only}")
80+
equal = False
81+
82+
if len(expected_keys_only) > 0:
83+
log_err(f"Result does not contain these expected fields: {expected_keys_only}")
84+
equal = False
85+
86+
common_keys = set(result.keys()).intersection(set(expected.keys()))
87+
88+
for key in common_keys:
89+
if key in compare_fn_map:
90+
equal &= compare_fn_map[key](result, expected)
91+
continue
92+
93+
if result[key] != expected[key]:
94+
log_err(f"Mismatch found in '{key}'")
95+
log_diff_str(key, result[key], expected[key])
96+
equal = False
97+
98+
return equal
99+
100+
101+
def extract_data_from_build_spec(build_spec_path: str) -> dict[str, str] | None:
102+
"""Extract data from build spec."""
103+
original_build_spec_content = None
104+
try:
105+
with open(build_spec_path, encoding="utf-8") as build_spec_file:
106+
original_build_spec_content = build_spec_file.read()
107+
except OSError as error:
108+
log_err(f"Failed to read the Reproducible Central Buildspec file at {build_spec_path}. Error {error}.")
109+
return None
110+
111+
build_spec_values: dict[str, str] = {}
112+
113+
# A Reproducible Central buildspec is a valid bash script.
114+
# We use the following assumption to parse all key value mapping in a Reproducible Central buildspec.
115+
# 1. Each variable-value mapping has the form of
116+
# <variable>=<value>
117+
# For example ``tool=mvn``
118+
# 2. If the first letter of a line is "#" we treat that line as a comment and ignore
119+
# it.
120+
for line in original_build_spec_content.splitlines():
121+
if not line or line.startswith("#"):
122+
continue
123+
124+
variable, _, value = line.partition("=")
125+
# We allow defining a variable multiple times, where subsequent definition
126+
# override the previous one.
127+
build_spec_values[variable] = value
128+
129+
return build_spec_values
130+
131+
132+
def main() -> int:
133+
"""Compare a Reproducible Central Buildspec file with an expected output."""
134+
result_path = sys.argv[1]
135+
expect_path = sys.argv[2]
136+
137+
result_build_spec = extract_data_from_build_spec(result_path)
138+
expect_build_spec = extract_data_from_build_spec(expect_path)
139+
140+
if not expect_build_spec:
141+
log_err(f"Failed to extract bash variables from expected Buildspec at {expect_path}.")
142+
return os.EX_USAGE
143+
144+
if not result_build_spec:
145+
log_err(f"Failed to extract bash variables from result Buildspec at {result_build_spec}.")
146+
return os.EX_USAGE
147+
148+
equal = compare_rc_build_spec(
149+
result=result_build_spec,
150+
expected=expect_build_spec,
151+
compare_fn_map={
152+
"buildinfo": skip_compare,
153+
},
154+
)
155+
156+
if not equal:
157+
log_failed("The result RC Buildspec does not match the RC Buildspec.")
158+
return os.EX_DATAERR
159+
160+
log_passed("The result RC Buildspec matches the RC Buildspec.")
161+
return os.EX_OK
162+
163+
164+
if __name__ == "__main__":
165+
raise SystemExit(main())
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) 2025, Oracle and/or its affiliates.
2+
# Generated by Macaron version 0.15.0
3+
4+
# Input PURL - pkg:maven/io.micronaut/micronaut-core@4.2.3
5+
# Initial default JDK version 8 and default build command [['./gradlew', '-x', 'test', '-Pskip.signing', '-PskipSigning', '-Pgnupg.skip', 'clean', 'assemble']].
6+
# The lookup build command: ['./gradlew', 'publishToSonatype', 'closeAndReleaseSonatypeStagingRepository']
7+
# Jdk version from lookup build command 17.
8+
9+
groupId=io.micronaut
10+
artifactId=micronaut-core
11+
version=4.2.3
12+
13+
gitRepo=https://github.com/micronaut-projects/micronaut-core
14+
15+
gitTag=36dcaf0539536dce5fc753677341609ff7f273ca
16+
17+
tool=gradle
18+
jdk=17
19+
20+
newline=lf
21+
22+
command="./gradlew -x test -Pskip.signing -PskipSigning -Pgnupg.skip clean assemble"
23+
24+
buildinfo=target/micronaut-core-4.2.3.buildinfo

tests/integration/cases/micronaut-projects_micronaut-core/test.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved.
22
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
33

44
description: |
55
Analyzing the PURL when automatic dependency resolution is skipped.
66
Run policy CLI with micronaut-core results to test deploy command information.
7+
Also generate a build spec for this PURL and validate the build spec content.
78
89
tags:
910
- macaron-python-package
@@ -30,3 +31,15 @@ steps:
3031
kind: policy_report
3132
result: output/policy_report.json
3233
expected: policy_report.json
34+
- name: Run build spec generation
35+
kind: gen-build-spec
36+
options:
37+
command_args:
38+
- -purl
39+
- pkg:maven/io.micronaut/micronaut-core@4.2.3
40+
- name: Compare Buildspec.
41+
kind: compare
42+
options:
43+
kind: rc_build_spec
44+
result: ./output/macaron.buildspec
45+
expected: expected_macaron.buildspec

tests/integration/run.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def configure_logging(verbose: bool) -> None:
8080
"deps_report": ["tests", "dependency_analyzer", "compare_dependencies.py"],
8181
"vsa": ["tests", "vsa", "compare_vsa.py"],
8282
"find_source": ["tests", "find_source", "compare_source_reports.py"],
83+
"rc_build_spec": ["tests", "build_spec_generator", "reproducible_central", "compare_rc_build_spec.py"],
8384
}
8485

8586
VALIDATE_SCHEMA_SCRIPTS: dict[str, Sequence[str]] = {
@@ -465,6 +466,52 @@ def cmd(self, macaron_cmd: str) -> list[str]:
465466
return args
466467

467468

469+
class GenBuildSpecStepOptions(TypedDict):
470+
"""The configuration options of an gen-build-spec step."""
471+
472+
main_args: Sequence[str]
473+
command_args: Sequence[str]
474+
database: str
475+
476+
477+
class GenBuildSpecStep(Step[GenBuildSpecStepOptions]):
478+
"""A step running the ``macaron gen-build-spec`` command."""
479+
480+
@staticmethod
481+
def options_schema(cwd: str) -> cfgv.Map: # pylint: disable=unused-argument
482+
"""Generate the schema of a gen-build-spec step."""
483+
return cfgv.Map(
484+
"gen-build-spec options",
485+
None,
486+
*[
487+
cfgv.Optional(
488+
key="main_args",
489+
check_fn=cfgv.check_array(cfgv.check_string),
490+
default=[],
491+
),
492+
cfgv.Optional(
493+
key="command_args",
494+
check_fn=cfgv.check_array(cfgv.check_string),
495+
default=[],
496+
),
497+
cfgv.Optional(
498+
key="database",
499+
check_fn=cfgv.check_string,
500+
default="./output/macaron.db",
501+
),
502+
],
503+
)
504+
505+
def cmd(self, macaron_cmd: str) -> list[str]:
506+
"""Generate the command of the step."""
507+
args = [macaron_cmd]
508+
args.extend(self.options["main_args"])
509+
args.append("gen-build-spec")
510+
args.extend(["--database", self.options["database"]])
511+
args.extend(self.options["command_args"])
512+
return args
513+
514+
468515
class VerifyStepOptions(TypedDict):
469516
"""The configuration options of a verify step."""
470517

@@ -599,6 +646,7 @@ def gen_step_schema(cwd: str, check_expected_result_files: bool) -> cfgv.Map:
599646
"verify",
600647
"validate_schema",
601648
"find-source",
649+
"gen-build-spec",
602650
),
603651
),
604652
),
@@ -638,6 +686,12 @@ def gen_step_schema(cwd: str, check_expected_result_files: bool) -> cfgv.Map:
638686
key="options",
639687
schema=VerifyStep.options_schema(cwd=cwd),
640688
),
689+
cfgv.ConditionalRecurse(
690+
condition_key="kind",
691+
condition_value="gen-build-spec",
692+
key="options",
693+
schema=GenBuildSpecStep.options_schema(cwd=cwd),
694+
),
641695
cfgv.ConditionalRecurse(
642696
condition_key="kind",
643697
condition_value="find-source",
@@ -842,6 +896,7 @@ def parse_step_config(step_id: int, step_config: Mapping) -> Step:
842896
"compare": CompareStep,
843897
"validate_schema": ValidateSchemaStep,
844898
"find-source": FindSourceStep,
899+
"gen-build-spec": GenBuildSpecStep,
845900
}[kind]
846901
return step_cls( # type: ignore # https://github.com/python/mypy/issues/3115
847902
step_id=step_id,

0 commit comments

Comments
 (0)