Skip to content

Commit 6c305a9

Browse files
author
Trong Nhan Mai
authored
test: add analysis report JSON schema validation (#824)
Signed-off-by: Trong Nhan Mai <[email protected]>
1 parent 849a47a commit 6c305a9

File tree

7 files changed

+332
-1
lines changed

7 files changed

+332
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ test = [
100100
]
101101

102102
test-docker = [
103+
"jsonschema >= 4.22.0,<5.0.0",
103104
"cfgv >=3.4.0,<4.0.0",
104105
"ruamel.yaml >=0.18.6,<1.0.0",
105106
]

tests/integration/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,12 @@ In case you want to debug the utility script itself, there is the verbose mode f
186186
### Step Schema
187187
188188
* `name` (`string`, required): The name of the step.
189-
* `kind` (`"analyze" | "verify" | "compare" | "shell"`, required): The kind of the step. There are 4 kinds of steps:
189+
* `kind` (`"analyze" | "verify" | "compare" | "shell" | "validate_schema`, required): The kind of the step. There are 5 kinds of steps:
190190
* `"analyze"`: runs the `macaron analyze` command.
191191
* `"verify"`: runs the `macaron verify-policy` command.
192192
* `"compare"`: compares an output file with an expected output file.
193193
* `"shell"`: runs an arbitrary shell command.
194+
* `"validate_schema"`: validates an output file with a schema.
194195
* `options`: Configuration options for the step. These options are specific to the step kind. See their schema below.
195196
* `env` (`dict[string, string | null]`, optional): Key value pairs of environment variables being modified during the step after inheriting the environment in which the utility is executed within. Each value can be a string if you want to set a value to the environment variable, or null if you want to "unset" the variable.
196197
* `expect_fail` (`bool`, optional, default is `false`): If `true`, assert that the step must exit with non-zero code. This should be used for cases where we expect a command to fail.
@@ -218,6 +219,13 @@ In case you want to debug the utility script itself, there is the verbose mode f
218219
* `result` (`string`, required): The output file (a relative path from test case directory).
219220
* `expected` (`string`, required): The expected output file (a relative path from test case directory).
220221
222+
### Validate Schema step options Schema
223+
224+
* `kind` (`"json_schema"`, required): The kind of schema validation to perform. For now, only json-schema is supported.
225+
* `result` (`string`, required): The output file (a relative path from test case directory).
226+
* `schema` (`output_json_report`, required): The name of the schema for the validation. These are the default schemas available for the integration test.
227+
* `custom_schema_path` (`string`, optional): The path to the custom schema (a relative path from test case directory). If it is provided, the validation will use this schema and ignore the schema corresponding with `schema`.
228+
221229
### Shell step options Schema
222230
223231
* `cmd` (`string`, required): The shell command to run.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ steps:
1515
- -c
1616
- micronaut_test_config.yaml
1717
- --skip-deps
18+
- name: Validate JSON report schema
19+
kind: validate_schema
20+
options:
21+
kind: json_schema
22+
schema: output_json_report
23+
result: output/reports/github_com/micronaut-projects/micronaut-test/micronaut-test.json
1824
- name: Compare dependency report
1925
kind: compare
2026
options:

tests/integration/run.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ def configure_logging(verbose: bool) -> None:
8181
"vsa": ["tests", "vsa", "compare_vsa.py"],
8282
}
8383

84+
VALIDATE_SCHEMA_SCRIPTS: dict[str, Sequence[str]] = {
85+
"json_schema": ["tests", "schema_validation", "json_schema_validate.py"],
86+
}
87+
88+
DEFAULT_SCHEMAS: dict[str, Sequence[str]] = {
89+
"output_json_report": ["tests", "schema_validation", "report_schema.json"],
90+
}
91+
8492

8593
def check_required_file(cwd: str) -> Callable[[str], None]:
8694
"""Check for a required file of a test case."""
@@ -208,6 +216,72 @@ def cmd(self, macaron_cmd: str) -> list[str]:
208216
return self.options["cmd"].strip().split()
209217

210218

219+
class ValidateSchemaStepOptions(TypedDict):
220+
"""The configuration options of a schema validation step."""
221+
222+
kind: str
223+
result: str
224+
schema: str
225+
custom_schema_path: str | None
226+
227+
228+
@dataclass
229+
class ValidateSchemaStep(Step[ValidateSchemaStepOptions]):
230+
"""A schema validation step in a test case, which allows for validating a file against a schema."""
231+
232+
@staticmethod
233+
def options_schema(cwd: str, check_expected_result_files: bool) -> cfgv.Map:
234+
"""Generate the schema of a schema validation step."""
235+
if check_expected_result_files:
236+
check_file = check_required_file(cwd)
237+
else:
238+
check_file = cfgv.check_string
239+
240+
return cfgv.Map(
241+
"schema options",
242+
None,
243+
*[
244+
cfgv.Required(
245+
key="kind",
246+
check_fn=cfgv.check_one_of(tuple(VALIDATE_SCHEMA_SCRIPTS.keys())),
247+
),
248+
cfgv.Required(
249+
key="result",
250+
check_fn=cfgv.check_string,
251+
),
252+
cfgv.Required(
253+
key="schema",
254+
check_fn=cfgv.check_one_of(tuple(DEFAULT_SCHEMAS.keys())),
255+
),
256+
cfgv.Optional(
257+
key="custom_schema_path",
258+
default=None,
259+
check_fn=check_file,
260+
),
261+
],
262+
)
263+
264+
def cmd(self, macaron_cmd: str) -> list[str]:
265+
kind = self.options["kind"]
266+
result_file = self.options["result"]
267+
schema = self.options["schema"]
268+
custom_schema_path = self.options["custom_schema_path"]
269+
270+
if custom_schema_path is None:
271+
return [
272+
"python",
273+
os.path.abspath(os.path.join(*VALIDATE_SCHEMA_SCRIPTS[kind])),
274+
*[result_file, os.path.abspath(os.path.join(*DEFAULT_SCHEMAS[schema]))],
275+
]
276+
277+
logger.info("A custom schema path at %s is given, using that instead.", custom_schema_path)
278+
return [
279+
"python",
280+
os.path.abspath(os.path.join(*VALIDATE_SCHEMA_SCRIPTS[kind])),
281+
*[result_file, custom_schema_path],
282+
]
283+
284+
211285
class CompareStepOptions(TypedDict):
212286
"""Configuration of a compare step."""
213287

@@ -473,6 +547,7 @@ def gen_step_schema(cwd: str, check_expected_result_files: bool) -> cfgv.Map:
473547
"compare",
474548
"analyze",
475549
"verify",
550+
"validate_schema",
476551
),
477552
),
478553
),
@@ -482,6 +557,15 @@ def gen_step_schema(cwd: str, check_expected_result_files: bool) -> cfgv.Map:
482557
key="options",
483558
schema=ShellStep.options_schema(),
484559
),
560+
cfgv.ConditionalRecurse(
561+
condition_key="kind",
562+
condition_value="validate_schema",
563+
key="options",
564+
schema=ValidateSchemaStep.options_schema(
565+
cwd=cwd,
566+
check_expected_result_files=check_expected_result_files,
567+
),
568+
),
485569
cfgv.ConditionalRecurse(
486570
condition_key="kind",
487571
condition_value="compare",
@@ -699,6 +783,7 @@ def parse_step_config(step_id: int, step_config: Mapping) -> Step:
699783
"verify": VerifyStep,
700784
"shell": ShellStep,
701785
"compare": CompareStep,
786+
"validate_schema": ValidateSchemaStep,
702787
}[kind]
703788
return step_cls( # type: ignore # https://github.com/python/mypy/issues/3115
704789
step_id=step_id,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) 2024 - 2024, 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/.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (c) 2024 - 2024, 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 module validates the result JSON files against a JSON schema."""
5+
6+
import json
7+
import os
8+
import sys
9+
from collections.abc import Sequence
10+
11+
import jsonschema
12+
13+
14+
def main(argv: Sequence[str] | None = None) -> int:
15+
"""Run main logic."""
16+
if not argv or not len(argv) == 3:
17+
print("Usage: python3 schema_validate.py <json_path> <schema_path>")
18+
return os.EX_USAGE
19+
20+
data_path = sys.argv[1]
21+
schema_path = sys.argv[2]
22+
23+
schema = None
24+
with open(schema_path, encoding="utf-8") as file:
25+
try:
26+
schema = json.load(file)
27+
except json.JSONDecodeError as error:
28+
print(f"Failed to load schema at {schema_path}, err:\n{error}\n")
29+
return os.EX_DATAERR
30+
31+
data = None
32+
with open(data_path, encoding="utf-8") as file:
33+
try:
34+
data = json.load(file)
35+
except json.JSONDecodeError as error:
36+
print(f"Failed to load JSON data at {data_path}, err:\n{error}\n")
37+
return os.EX_DATAERR
38+
39+
try:
40+
jsonschema.validate(
41+
schema=schema,
42+
instance=data,
43+
)
44+
print(f"JSON data at {data_path} PASSED schema {schema_path}.")
45+
return os.EX_OK
46+
except jsonschema.ValidationError as error:
47+
print(f"JSON data at {data_path} FAILED schema {schema_path}, err:\n{error}\n")
48+
return os.EX_DATAERR
49+
except jsonschema.SchemaError as error:
50+
print(f"The schema at {schema_path} is not valid, err:\n{error}\n")
51+
return os.EX_DATAERR
52+
53+
54+
if __name__ == "__main__":
55+
raise SystemExit(main(sys.argv))

0 commit comments

Comments
 (0)