Skip to content

Commit 6e9621d

Browse files
authored
Adding reporting workflow to generate test_run manifest based on test workflow (opensearch-project#3636)
Signed-off-by: Zelin Hao <zelinhao@amazon.com>
1 parent 27f5ed0 commit 6e9621d

15 files changed

+1111
-0
lines changed

report.sh

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
3+
# Copyright OpenSearch Contributors
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# The OpenSearch Contributors require contributions made to
7+
# this file be licensed under the Apache-2.0 license or a
8+
# compatible open source license.
9+
10+
set -e
11+
12+
DIR="$(dirname "$0")"
13+
"$DIR/run.sh" "$DIR/src/run_test_report.py" $@

src/manifests/test_run_manifest.py

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Copyright OpenSearch Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# The OpenSearch Contributors require contributions made to
5+
# this file be licensed under the Apache-2.0 license or a
6+
# compatible open source license.
7+
8+
from typing import Optional
9+
10+
from manifests.component_manifest import Component, ComponentManifest, Components
11+
12+
13+
class TestRunManifest(ComponentManifest['TestRunManifest', 'TestComponents']):
14+
"""
15+
TestRunManifest contains the aggregated test results for the components.
16+
17+
The format for schema version 1.0 is:
18+
schema-version: '1.0'
19+
name: name of the product e.g. OpenSearch
20+
test-run:
21+
Command: command to trigger the integ test
22+
TestType: type of test this manifest reports. e.g. integ-test
23+
TestManifest: location of the test manifest used
24+
DistributionManifest: URL or local path of the bundle manifest.
25+
TestID: test id
26+
components:
27+
- name: sql
28+
command: command to trigger the integ test for only sql component
29+
configs:
30+
- name: with-security
31+
status: the status of the test run with this config. e.g. pass/fail
32+
yml: URL or local path to the component yml file
33+
"""
34+
35+
SCHEMA = {
36+
"schema-version": {"required": True, "type": "string", "allowed": ["1.0"]},
37+
"name": {"required": True, "type": "string", "allowed": ["OpenSearch", "OpenSearch Dashboards"]},
38+
"test-run": {
39+
"required": False,
40+
"type": "dict",
41+
"schema": {
42+
"Command": {"required": False, "type": "string"},
43+
"TestType": {"required": False, "type": "string"},
44+
"TestManifest": {"required": False, "type": "string"},
45+
"DistributionManifest": {"required": False, "type": "string"},
46+
"TestID": {"required": False, "type": "string"}
47+
},
48+
},
49+
"components": {
50+
"type": "list",
51+
"schema": {
52+
"type": "dict",
53+
"schema": {
54+
"name": {"required": True, "type": "string"},
55+
"command": {"type": "string"},
56+
"configs": {
57+
"type": "list",
58+
"schema": {
59+
"type": "dict",
60+
"schema": {
61+
"name": {"type": "string"},
62+
"status": {"type": "string"},
63+
"yml": {"type": "string"},
64+
}
65+
},
66+
},
67+
},
68+
},
69+
},
70+
}
71+
72+
def __init__(self, data: dict) -> None:
73+
super().__init__(data)
74+
self.name = str(data["name"])
75+
self.test_run = self.TestRun(data.get("test-run", None))
76+
self.components = TestComponents(data.get("components", [])) # type: ignore[assignment]
77+
78+
def __to_dict__(self) -> dict:
79+
return {
80+
"schema-version": "1.0",
81+
"name": self.name,
82+
"test-run": None if self.test_run is None else self.test_run.__to_dict__(),
83+
"components": self.components.__to_dict__()
84+
}
85+
86+
class TestRun:
87+
def __init__(self, data: dict) -> None:
88+
if data is None:
89+
self.test_run = None
90+
else:
91+
self.command = data["Command"]
92+
self.test_type = data["TestType"]
93+
self.test_manifest = data["TestManifest"]
94+
self.distribution_manifest = data["DistributionManifest"]
95+
self.test_id = data["TestID"]
96+
97+
def __to_dict__(self) -> Optional[dict]:
98+
if (self.command and self.test_type and self.test_manifest and self.distribution_manifest and self.test_id) is None:
99+
return None
100+
else:
101+
return {
102+
"Command": self.command,
103+
"TestType": self.test_type,
104+
"TestManifest": self.test_manifest,
105+
"DistributionManifest": self.distribution_manifest,
106+
"TestID": self.test_id
107+
}
108+
109+
110+
class TestComponents(Components['TestComponent']):
111+
@classmethod
112+
def __create__(self, data: dict) -> 'TestComponent':
113+
return TestComponent(data)
114+
115+
116+
class TestComponent(Component):
117+
def __init__(self, data: dict) -> None:
118+
super().__init__(data)
119+
self.command = data["command"]
120+
self.configs = self.TestComponentConfigs(data.get("configs", None))
121+
122+
def __to_dict__(self) -> dict:
123+
return {
124+
"name": self.name,
125+
"command": self.command,
126+
"configs": self.configs.__to_list__()
127+
}
128+
129+
class TestComponentConfigs:
130+
def __init__(self, data: list) -> None:
131+
self.configs = []
132+
for config in data:
133+
self.configs.append(self.TestComponentConfig(config).__to_dict__())
134+
135+
def __to_list__(self) -> list:
136+
return self.configs
137+
138+
class TestComponentConfig:
139+
def __init__(self, data: dict) -> None:
140+
self.name = data["name"]
141+
self.status = data["status"]
142+
self.yml = data["yml"]
143+
144+
def __to_dict__(self) -> dict:
145+
return {
146+
"name": self.name,
147+
"status": self.status,
148+
"yml": self.yml
149+
}
150+
151+
152+
TestRunManifest.VERSIONS = {"1.0": TestRunManifest}
153+
154+
TestComponent.__test__ = False # type: ignore[attr-defined]
155+
TestRunManifest.__test__ = False # type: ignore[attr-defined]

src/report_workflow/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright OpenSearch Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# The OpenSearch Contributors require contributions made to
5+
# this file be licensed under the Apache-2.0 license or a
6+
# compatible open source license.
7+
#
8+
# This page intentionally left blank.

src/report_workflow/report_args.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# The OpenSearch Contributors require contributions made to
4+
# this file be licensed under the Apache-2.0 license or a
5+
# compatible open source license.
6+
#
7+
# Modifications Copyright OpenSearch Contributors. See
8+
# GitHub history for details.
9+
10+
import argparse
11+
import logging
12+
13+
from test_workflow.test_kwargs import TestKwargs
14+
15+
16+
class ReportArgs:
17+
test_run_id: str
18+
keep: bool
19+
test_manifest_path: str
20+
artifact_paths: dict
21+
test_type: str
22+
logging_level: int
23+
24+
def __init__(self) -> None:
25+
parser = argparse.ArgumentParser(description="Generate test report for given test data")
26+
parser.add_argument("test_manifest_path", type=str, help="Specify a test manifest path.")
27+
parser.add_argument("-p", "--artifact-paths", nargs='*', action=TestKwargs, default={},
28+
help="Specify aritfacts paths for OpenSearch and OpenSearch Dashboards.")
29+
# e.g. --base-path https://ci.opensearch.org/ci/dbc/integ-test/2.7.0/7771/linux/x64/tar/test-results/1234<test-run-id>/integ-test use more to save arguments number
30+
parser.add_argument("--base-path", type=str, default="",
31+
help="Specify base paths for the integration test logs.")
32+
parser.add_argument("--test-type", type=str, default="integ-test", help="Specify test type of this.")
33+
parser.add_argument("--output-path", type=str, help="Specify the path location for the test-run manifest.")
34+
parser.add_argument("--test-run-id", type=int, help="The unique execution id for the test")
35+
parser.add_argument("--component", type=str, dest="components", nargs='*', help="Test a specific component or components instead of the entire distribution.")
36+
parser.add_argument(
37+
"-v", "--verbose", help="Show more verbose output.", action="store_const", default=logging.INFO, const=logging.DEBUG, dest="logging_level"
38+
)
39+
40+
args = parser.parse_args()
41+
self.test_run_id = args.test_run_id
42+
self.logging_level = args.logging_level
43+
self.test_manifest_path = args.test_manifest_path
44+
self.artifact_paths = args.artifact_paths
45+
self.base_path = args.base_path
46+
self.test_type = args.test_type
47+
self.components = args.components
48+
self.output_path = args.output_path
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright OpenSearch Contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# The OpenSearch Contributors require contributions made to
5+
# this file be licensed under the Apache-2.0 license or a
6+
# compatible open source license.
7+
8+
import logging
9+
import os
10+
import urllib.request
11+
from typing import Any
12+
from urllib.error import HTTPError
13+
14+
import validators
15+
import yaml
16+
17+
from manifests.test_manifest import TestManifest
18+
from manifests.test_run_manifest import TestRunManifest
19+
from report_workflow.report_args import ReportArgs
20+
21+
22+
class TestRunRunner:
23+
args: ReportArgs
24+
test_manifest: TestManifest
25+
tests_dir: str
26+
test_run_manifest: TestRunManifest
27+
test_run_data: dict
28+
29+
def __init__(self, args: ReportArgs, test_manifest: TestManifest) -> None:
30+
self.args = args
31+
self.base_path = args.base_path
32+
self.test_manifest = test_manifest
33+
self.test_run_data = self.test_run_manifest_data_template("manifest")
34+
self.product_name = test_manifest.__to_dict__().get("name")
35+
self.name = self.product_name.replace(" ", "-").lower()
36+
self.components = self.args.components
37+
self.test_run_id = args.test_run_id
38+
self.test_type = self.args.test_type
39+
self.test_manifest_path = self.args.test_manifest_path
40+
self.artifact_paths = ""
41+
for k, v in self.args.artifact_paths.items():
42+
self.artifact_paths = " ".join([self.artifact_paths, k + "=" + v]).strip(" ")
43+
44+
self.dist_manifest = "/".join([self.args.artifact_paths[self.name], "dist", self.name, "manifest.yml"]) if self.args.artifact_paths[self.name].startswith("https://") \
45+
else os.path.join(self.args.artifact_paths[self.name], "dist", self.name, "manifest.yml")
46+
self.test_components = self.test_manifest.components
47+
48+
def update_data(self) -> dict:
49+
self.test_run_data["name"] = self.product_name
50+
self.test_run_data["test-run"] = self.update_test_run_data()
51+
for component in self.test_components.select(focus=self.args.components):
52+
self.test_run_data["components"].append(self.component_entry(component.name))
53+
return self.test_run_data
54+
55+
def update_test_run_data(self) -> dict:
56+
test_run_data = {
57+
"Command": generate_test_command(self.test_type, self.test_manifest_path, self.artifact_paths),
58+
"TestType": self.test_type,
59+
"TestManifest": self.test_manifest_path,
60+
"DistributionManifest": self.dist_manifest,
61+
"TestID": str(self.test_run_id)
62+
}
63+
return test_run_data
64+
65+
def generate_report(self, data: dict, output_dir: str) -> Any:
66+
test_run_manifest = TestRunManifest(data)
67+
test_run_manifetest_run_manifest_file = os.path.join(output_dir, "test-run.yml")
68+
logging.info(f"Generating test-run.yml in {output_dir}")
69+
return test_run_manifest.to_file(test_run_manifetest_run_manifest_file)
70+
71+
def component_entry(self, component_name: str) -> Any:
72+
component = self.test_run_manifest_data_template("component")
73+
component["name"] = component_name
74+
component["command"] = generate_test_command(self.test_type, self.test_manifest_path, self.artifact_paths, component_name)
75+
76+
test_component = self.test_manifest.components[component_name]
77+
78+
config_names = [config for config in test_component.__to_dict__().get(self.test_type)["test-configs"]]
79+
logging.info(f"Configs for {component_name} on {self.test_type} are {config_names}")
80+
for config in config_names:
81+
config_dict = {
82+
"name": config,
83+
}
84+
85+
component_yml_ref = generate_component_yml_ref(self.base_path, str(self.test_run_id), self.test_type, component_name, config)
86+
logging.info(f"Loading {component_yml_ref}")
87+
try:
88+
if validators.url(component_yml_ref):
89+
with urllib.request.urlopen(component_yml_ref) as f:
90+
component_yml = yaml.safe_load(f.read().decode("utf-8"))
91+
test_result = component_yml["test_result"]
92+
else:
93+
with open(component_yml_ref, "r", encoding='utf8') as f:
94+
component_yml = yaml.safe_load(f)
95+
test_result = component_yml["test_result"]
96+
except (FileNotFoundError, HTTPError):
97+
logging.info(f"Component yml file for {component_name} for {config} is missing or the base path is incorrect.")
98+
test_result = "Not Available"
99+
component_yml_ref = "URL not available"
100+
config_dict["yml"] = component_yml_ref
101+
config_dict["status"] = test_result
102+
component["configs"].append(config_dict)
103+
return component
104+
105+
def test_run_manifest_data_template(self, template_type: str) -> Any:
106+
templates = {
107+
"manifest": {
108+
"schema-version": "1.0",
109+
"name": "",
110+
"test-run": {},
111+
"components": []
112+
},
113+
"component": {
114+
"name": "",
115+
"command": "",
116+
"configs": []
117+
}
118+
}
119+
return templates[template_type]
120+
121+
122+
def generate_component_yml_ref(base_path: str, test_number: str, test_type: str, component_name: str, config: str) -> str:
123+
if base_path.startswith("https://"):
124+
return "/".join([base_path.strip("/"), "test-results", test_number, test_type, component_name, config, f"{component_name}.yml"])
125+
else:
126+
return os.path.join(base_path, "test-results", test_number, test_type, component_name, config, f"{component_name}.yml")
127+
128+
129+
def generate_test_command(test_type: str, test_manifest_path: str, artifacts_path: str, component: str = "") -> str:
130+
command = " ".join(["./test.sh", test_type, test_manifest_path, "--paths", artifacts_path])
131+
if component:
132+
command = " ".join([command, "--component", component])
133+
logging.info(command)
134+
return command
135+
136+
137+
TestRunRunner.__test__ = False # type:ignore

0 commit comments

Comments
 (0)