Skip to content

Commit cc39c38

Browse files
authored
[ISV-5281] [ISV-5282] Integration tests (#838)
* [ISV-5281] [ISV-5282] Integration tests - Implemented integration test main loop - Partially implemented a basic test case --------- Signed-off-by: Maurizio Porrato <mporrato@redhat.com>
1 parent 2d48088 commit cc39c38

12 files changed

Lines changed: 443 additions & 21 deletions

File tree

.coveragerc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[run]
22
source = operator-pipeline-images/operatorcert
3-
omit = operator-pipeline-images/operatorcert/webhook/*
4-
3+
omit =
4+
operator-pipeline-images/operatorcert/webhook/*
5+
operator-pipeline-images/operatorcert/integration/testcases/*

integration-tests-config-sample.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
---
22

3+
fixtures_repository:
4+
# The GitHub repo containing fixtures used by test cases
5+
url: https://github.com/foo/operators-integration-tests-fixtures
6+
token: secretABC
37
operator_repository:
48
# The GitHub repository hosting the operators for integration tests
59
url: https://github.com/foo/operators-integration-tests

operator-pipeline-images/operatorcert/entrypoints/integration_tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from operatorcert.integration.runner import run_integration_tests
1111

12+
1213
LOGGER = logging.getLogger("operator-cert")
1314

1415

operator-pipeline-images/operatorcert/integration/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,18 @@ class Config(BaseModel):
4646
Root configuration object
4747
"""
4848

49+
# GitHub repo containing fixtures used by test cases
50+
fixtures_repository: GitHubRepoConfig
51+
# Main GitHub repo to be used by test cases. PRs submitted to this repo will trigger
52+
# the hosted pipeline
4953
operator_repository: GitHubRepoConfig
54+
# GitHub repo to submit PRs from
5055
contributor_repository: GitHubRepoConfig
56+
# Container registry where to store bundle and index images created by test cases
5157
bundle_registry: ContainerRegistryConfig
58+
# Container registry where to push the operator-pipeline image
5259
test_registry: ContainerRegistryConfig
60+
# The IIB instance to be used by integration tests
5361
iib: IIBConfig
5462

5563
@classmethod

operator-pipeline-images/operatorcert/integration/runner.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from operatorcert.integration.config import Config
1414
from operatorcert.integration.external_tools import Ansible, Podman, Secret
15+
from operatorcert.integration.testcase import run_tests
1516

1617
LOGGER = logging.getLogger("operator-cert")
1718

@@ -98,14 +99,14 @@ def run_integration_tests(
9899
return 1
99100
LOGGER.debug("Connected to %s", oc_context.strip())
100101
cfg = Config.from_yaml(config_path)
101-
test_id = datetime.now().strftime("%Y%m%d%H%M%S")
102-
namespace = f"inttest-{test_id}"
102+
run_id = datetime.now().strftime("%Y%m%d%H%M%S")
103+
namespace = f"inttest-{run_id}"
103104
try:
104105
if image:
105106
tagged_image_name = image
106107
else:
107108
image_name = f"{cfg.test_registry.base_ref}/operator-pipeline-images"
108-
tagged_image_name = f"{image_name}:{test_id}"
109+
tagged_image_name = f"{image_name}:{run_id}"
109110
auth = (
110111
{image_name: (cfg.test_registry.username, cfg.test_registry.password)}
111112
if cfg.test_registry.username and cfg.test_registry.password
@@ -117,6 +118,7 @@ def run_integration_tests(
117118
auth=auth,
118119
)
119120
_deploy_pipelines(project_dir, namespace, tagged_image_name)
121+
run_tests(cfg, run_id)
120122
except subprocess.CalledProcessError as e:
121123
LOGGER.error("Command %s failed:", e.cmd)
122124
for line in e.stdout.decode("utf-8").splitlines():

operator-pipeline-images/operatorcert/integration/testcase.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import logging
66
from typing import TypeVar
77

8-
from operatorcert.integration.config import Config
98
from colorama import Fore, Style, init as colorama_init
109

10+
from operatorcert.integration.config import Config
11+
from operatorcert.integration.testcases import import_testcases
1112

1213
LOGGER = logging.getLogger("operator-cert")
1314

@@ -20,6 +21,7 @@ class BaseTestCase:
2021
def __init__(self, config: Config, logger: logging.Logger) -> None:
2122
self.config = config
2223
self.logger = logger
24+
self.run_id = "(unset)"
2325

2426
def setup(self) -> None:
2527
"""
@@ -48,11 +50,12 @@ def cleanup(self) -> None:
4850
any resources created during the execution of the test
4951
"""
5052

51-
def run(self) -> None:
53+
def run(self, run_id: str) -> None:
5254
"""
5355
Execute the test case; `setup()`, `watch()`, `validate()` and
5456
`cleanup()` are called in order
5557
"""
58+
self.run_id = run_id
5659
try:
5760
self.setup()
5861
self.watch()
@@ -77,22 +80,23 @@ def integration_test_case(test_class: _T) -> _T:
7780
return test_class
7881

7982

80-
def run_tests(config: Config) -> int:
83+
def run_tests(config: Config, run_id: str) -> int:
8184
"""
8285
Executes all the test cases that have been registered using the
8386
`integration_test_case` decorator
8487
8588
Return:
8689
number of test cases that failed
8790
"""
91+
import_testcases()
8892
colorama_init()
8993
failed = 0
9094
for test_class in _test_cases:
9195
test_name = test_class.__name__
9296
print(f"Running {test_name} ", end="")
9397
try:
9498
test_instance = test_class(config, LOGGER)
95-
test_instance.run()
99+
test_instance.run(run_id)
96100
print(f"{Fore.GREEN}PASS{Style.RESET_ALL}")
97101
except Exception as e: # pylint: disable=broad-except
98102
print(f"{Fore.RED}FAIL{Style.RESET_ALL}")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Import all submodules, so that all testcases are available.
3+
"""
4+
5+
import importlib
6+
import pkgutil
7+
from types import ModuleType
8+
9+
10+
def import_submodules(
11+
package: str | ModuleType, recursive: bool = True
12+
) -> dict[str, ModuleType]:
13+
"""Import all submodules of a module, recursively, including subpackages.
14+
15+
This function dynamically imports all submodules found within a given package,
16+
optionally including subpackages if recursive mode is enabled. It's useful for
17+
automatically loading all modules in a package without explicitly importing each one.
18+
19+
Args:
20+
package (str | ModuleType): The package to import submodules from. Can be either:
21+
- A string representing the module name (e.g., 'mypackage.subpackage')
22+
- A ModuleType object representing an already imported module
23+
recursive (bool, optional): Whether to recursively import submodules of
24+
subpackages. Defaults to True. If False, only direct submodules of the
25+
given package are imported.
26+
27+
Returns:
28+
dict[str, ModuleType]: A dictionary mapping module names to their corresponding
29+
imported module objects. Keys are fully qualified module names (strings)
30+
and values are the imported ModuleType objects. If a module fails to import
31+
due to ModuleNotFoundError, it is silently skipped and not included in the
32+
returned dictionary.
33+
"""
34+
if isinstance(package, str):
35+
package = importlib.import_module(package)
36+
results = {}
37+
for _, name, is_pkg in pkgutil.walk_packages(package.__path__):
38+
full_name = package.__name__ + "." + name
39+
try:
40+
results[full_name] = importlib.import_module(full_name)
41+
except ModuleNotFoundError:
42+
continue
43+
if recursive and is_pkg:
44+
results.update(import_submodules(full_name))
45+
return results
46+
47+
48+
def import_testcases() -> None:
49+
"""Import all testcases in this module"""
50+
import_submodules(__name__)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Basic test cases
3+
"""
4+
5+
import logging
6+
from pathlib import Path
7+
from shutil import rmtree
8+
from tempfile import mkdtemp
9+
from typing import Optional
10+
11+
from git import Repo
12+
13+
from operatorcert.integration.config import Config
14+
from operatorcert.integration.testcase import BaseTestCase, integration_test_case
15+
16+
17+
@integration_test_case
18+
class AddBundle(BaseTestCase): # pragma: no cover
19+
"""
20+
Test the addition of a non-FBC bundle
21+
"""
22+
23+
def __init__(self, config: Config, logger: logging.Logger) -> None:
24+
super().__init__(config, logger)
25+
self.tempdir: Optional[Path] = None
26+
27+
def setup(self) -> None:
28+
self.tempdir = Path(mkdtemp(prefix=self.run_id + "-"))
29+
test_branch = "add-bundle"
30+
remote_branch = f"test-{self.run_id}"
31+
git_fixture_dir = self.tempdir / "git-fixture"
32+
# clone fixture repo locally
33+
local_repo = Repo.clone_from(
34+
self.config.fixtures_repository.url, git_fixture_dir, branch=test_branch
35+
)
36+
# configure remotes
37+
local_repo.create_remote("operators", url=self.config.operator_repository.url)
38+
local_repo.create_remote(
39+
"contributor", url=self.config.contributor_repository.url
40+
)
41+
# The fixture branch contains the operator structure needed for the test
42+
# and the latest commit is the change being tested. This means we need
43+
# to exclude the latest commit when pushing to the operator repo.
44+
local_repo.create_head(
45+
# create local contributor branch
46+
"contributor",
47+
commit="HEAD",
48+
)
49+
local_repo.remotes.contributor.push(
50+
# force push local contributor branch to test branch in contributor repo
51+
f"+contributor:{remote_branch}"
52+
)
53+
local_repo.create_head(
54+
# create local operators branch discarding latest commit
55+
"operators",
56+
commit="HEAD~1",
57+
)
58+
local_repo.remotes.operators.push(
59+
# force push local operators branch to test branch in operators repo
60+
f"+operators:{remote_branch}"
61+
)
62+
# TODO: create github webhook
63+
# TODO: create github PR
64+
65+
def watch(self) -> None:
66+
# TODO: wait until pipelinerun finishes
67+
pass
68+
69+
def validate(self) -> None:
70+
# TODO: check pipelinerun status
71+
# TODO: check generated bundle image
72+
# TODO: check bundle is in index
73+
pass
74+
75+
def cleanup(self) -> None:
76+
# TODO: remove bundle image
77+
# TODO: remove index image
78+
# TODO: remove github webhook
79+
# TODO: delete test git branches
80+
# remove temp dir
81+
if self.tempdir:
82+
rmtree(self.tempdir, ignore_errors=True)

operator-pipeline-images/tests/integration/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def operator_pipelines_path(tmp_path: Path) -> Path:
3636
def integration_tests_config_file(tmp_path: Path) -> Path:
3737
sample_config = """
3838
---
39+
fixtures_repository:
40+
url: https://github.com/foo/foo
41+
token: secretABC
3942
operator_repository:
4043
url: https://github.com/foo/bar
4144
token: asdfg

0 commit comments

Comments
 (0)