Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit 73a60db

Browse files
Enable custom runners (#156)
The trickiest part of ATS is the runner system. Every customer will have their own test process, and we can't hope to accomodate all of them. While we will try to provider the most used runners for them, giving them the option to decide what runner they'll use is a good idea. These changes enable the dynamic import of runners. It works by defining the runner in the yaml config.
1 parent f9ee6eb commit 73a60db

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

codecov_cli/runners/__init__.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import logging
2+
import typing
3+
from importlib import import_module
4+
5+
import click
26

37
from codecov_cli.runners.dan_runner import DoAnythingNowRunner
48
from codecov_cli.runners.python_standard_runner import PythonStandardRunner
@@ -11,8 +15,30 @@ class UnableToFindRunner(Exception):
1115
pass
1216

1317

14-
def _load_runner_from_yaml() -> LabelAnalysisRunnerInterface:
15-
raise NotImplementedError()
18+
def _load_runner_from_yaml(plugin_dict: typing.Dict) -> LabelAnalysisRunnerInterface:
19+
try:
20+
module_obj = import_module(plugin_dict["module"])
21+
class_obj = getattr(module_obj, plugin_dict["class"])
22+
except ModuleNotFoundError:
23+
click.secho(
24+
f"Unable to dynamically load module {plugin_dict['module']}",
25+
err=True,
26+
)
27+
raise
28+
except AttributeError:
29+
click.secho(
30+
f"Unable to dynamically load class {plugin_dict['class']} from module {plugin_dict['module']}",
31+
err=True,
32+
)
33+
raise
34+
try:
35+
return class_obj(**plugin_dict["params"])
36+
except TypeError:
37+
click.secho(
38+
f"Unable to instantiate {class_obj} with parameters {plugin_dict['params']}",
39+
err=True,
40+
)
41+
raise
1642

1743

1844
def get_runner(cli_config, runner_name) -> LabelAnalysisRunnerInterface:

tests/runners/test_runners.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from unittest.mock import patch
22

3-
from codecov_cli.runners import get_runner
3+
import pytest
4+
5+
from codecov_cli.runners import _load_runner_from_yaml, get_runner
46
from codecov_cli.runners.dan_runner import DoAnythingNowRunner
57
from codecov_cli.runners.python_standard_runner import PythonStandardRunner
8+
from tests.factory import FakeRunner
69

710

811
class TestRunners(object):
@@ -42,3 +45,57 @@ def test_get_runner_from_yaml(self, mock_load_runner):
4245
mock_load_runner.return_value = "MyRunner()"
4346
assert get_runner(config, "my_runner") == "MyRunner()"
4447
mock_load_runner.assert_called_with({"path": "path_to_my_runner"})
48+
49+
def test_load_runner_from_yaml(self, mocker):
50+
fake_module = mocker.MagicMock(FakeRunner=FakeRunner)
51+
mocker.patch("codecov_cli.runners.import_module", return_value=fake_module)
52+
res = _load_runner_from_yaml(
53+
{
54+
"module": "mymodule.runner",
55+
"class": "FakeRunner",
56+
"params": {"collect_tests_response": ["list", "of", "labels"]},
57+
}
58+
)
59+
assert isinstance(res, FakeRunner)
60+
assert res.collect_tests() == ["list", "of", "labels"]
61+
assert res.process_labelanalysis_result({}) == "I ran tests :D"
62+
63+
def test_load_runner_from_yaml_module_not_found(self, mocker):
64+
def side_effect(*args, **kwargs):
65+
raise ModuleNotFoundError()
66+
67+
mocker.patch("codecov_cli.runners.import_module", side_effect=side_effect)
68+
with pytest.raises(ModuleNotFoundError):
69+
_load_runner_from_yaml(
70+
{
71+
"module": "mymodule.runner",
72+
"class": "FakeRunner",
73+
"params": {"collect_tests_response": ["list", "of", "labels"]},
74+
}
75+
)
76+
77+
def test_load_runner_from_yaml_class_not_found(self, mocker):
78+
import tests.factory as fake_module
79+
80+
mocker.patch("codecov_cli.runners.import_module", return_value=fake_module)
81+
82+
with pytest.raises(AttributeError):
83+
_load_runner_from_yaml(
84+
{
85+
"module": "mymodule.runner",
86+
"class": "WrongClassName",
87+
"params": {"collect_tests_response": ["list", "of", "labels"]},
88+
}
89+
)
90+
91+
def test_load_runner_from_yaml_fail_instantiate_class(self, mocker):
92+
fake_module = mocker.MagicMock(FakeRunner=FakeRunner)
93+
mocker.patch("codecov_cli.runners.import_module", return_value=fake_module)
94+
with pytest.raises(TypeError):
95+
_load_runner_from_yaml(
96+
{
97+
"module": "mymodule.runner",
98+
"class": "FakeRunner",
99+
"params": {"wrong_params": ["list", "of", "labels"]},
100+
}
101+
)

0 commit comments

Comments
 (0)