Skip to content

Commit e65cd0d

Browse files
committed
impl: added more testing coverage
1 parent 1d0a510 commit e65cd0d

7 files changed

Lines changed: 130 additions & 19 deletions

File tree

_data/transformations/algorithm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TEST_ENV_KEY=987

ocean_runner/config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from enum import StrEnum, auto
1+
from enum import Enum
22
from logging import Logger
33
from pathlib import Path
44
from typing import Generic, Sequence, Type, TypeVar
@@ -11,11 +11,11 @@
1111
DEFAULT = "DEFAULT"
1212

1313

14-
class Keys(StrEnum):
15-
SECRET = auto()
16-
BASE_DIR = auto()
17-
TRANSFORMATION_DID = auto()
18-
DIDS = auto()
14+
class Keys(str, Enum):
15+
SECRET = "secret"
16+
BASE_DIR = "base_dir"
17+
TRANSFORMATION_DID = "transformation_did"
18+
DIDS = "dids"
1919

2020

2121
class Environment(BaseSettings):

ocean_runner/entrypoint.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ class CLIRunnerConfig(BaseModel):
2424
description="The python module path to import (e.g. 'src.algorithm')",
2525
)
2626

27-
base_dir: Path = Field(default=Path("../_data"), description="Base data path")
27+
base_dir: Path = Field(
28+
default=Path("../_data"),
29+
description="Base data path",
30+
)
2831

2932
def model_post_init(self, context, /) -> None:
3033
if not self.base_dir.exists():

ocean_runner/runner.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
TypeVar,
1616
cast,
1717
overload,
18-
override,
1918
)
2019

2120
from oceanprotocol_job_details import (
@@ -26,7 +25,8 @@
2625
)
2726
from oceanprotocol_job_details.ocean import _BaseJobDetails
2827
from pydantic import BaseModel, JsonValue
29-
from returns.result import Success, Failure
28+
from returns.result import Failure, Success
29+
from typing_extensions import override
3030

3131
from ocean_runner.config import Config
3232

@@ -110,7 +110,7 @@ def create(cls, config: Config[None]) -> EmptyAlgorithm[ResultT]:
110110

111111
@overload
112112
@classmethod
113-
def create(cls, config: None) -> EmptyAlgorithm[ResultT]:
113+
def create(cls, config: None = None) -> EmptyAlgorithm[ResultT]:
114114
pass # pragma: no cover
115115

116116
@classmethod
@@ -191,7 +191,9 @@ def on_error(self, fn: ErrorFuncT) -> ErrorFuncT:
191191
# Execution Pipeline
192192
# ---------------------------
193193

194-
async def execute(self) -> ResultT | None:
194+
def load(self) -> None:
195+
"""Load the JobDetails instance"""
196+
195197
env = self.configuration.environment
196198
config: Dict[str, JsonValue] = {
197199
"base_dir": str(env.base_dir),
@@ -206,8 +208,10 @@ async def execute(self) -> ResultT | None:
206208
custom_input = self.configuration.custom_input
207209

208210
self._job_details = load_job_details(custom_input, config)
211+
self._job_details.paths.outputs.mkdir(exist_ok=True)
209212
self.logger.info("Loaded JobDetails")
210213

214+
async def execute(self) -> ResultT | None:
211215
try:
212216
await run_in_executor(
213217
self._functions.validate,
@@ -219,8 +223,6 @@ async def execute(self) -> ResultT | None:
219223
self,
220224
)
221225

222-
self._job_details.paths.outputs.mkdir(exist_ok=True)
223-
224226
await run_in_executor(
225227
self._functions.save,
226228
self,
@@ -239,6 +241,7 @@ def job_details(self) -> _BaseJobDetails[InputT]: ...
239241

240242
def __call__(self) -> ResultT | None:
241243
"""Executes the algorithm pipeline: validate → run → save_results."""
244+
self.load()
242245
return asyncio.run(self.execute())
243246

244247

@@ -253,8 +256,8 @@ def job_details(self) -> ParametrizedJobDetails[InputT]:
253256
return parametrized_job_details
254257
case Failure(error):
255258
raise error
256-
257-
return self._job_details.read().unwrap()
259+
case _: # pragma: no cover
260+
raise RuntimeError("Unreachable code")
258261

259262

260263
class EmptyAlgorithm(Algorithm[None, ResultT]):

tests/test_entrypoint.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import sys
12
from importlib.metadata import PackageNotFoundError
3+
from pathlib import Path
24
from unittest.mock import MagicMock, patch
35

46
import pytest
@@ -9,6 +11,8 @@
911
get_config,
1012
get_version,
1113
main,
14+
main_test,
15+
run_tests,
1216
)
1317
from ocean_runner.runner import Algorithm
1418

@@ -29,6 +33,12 @@ def test_config_validation_custom_args():
2933
assert config.base_dir.exists()
3034

3135

36+
@patch("pathlib.Path.exists", return_value=False)
37+
def test_config_validation_raises_if_bad_path(mock_exists):
38+
with pytest.raises(FileNotFoundError):
39+
get_config(["custom.path", "--base-dir", "./_data"])
40+
41+
3242
@patch("importlib.import_module")
3343
def test_get_algorithm_success(mock_import):
3444
"""Test that get_algorithm correctly finds an Algorithm instance."""
@@ -57,7 +67,7 @@ def test_get_algorithm_no_instance(mock_import):
5767
@patch("ocean_runner.entrypoint.get_algorithm")
5868
@patch(
5969
"ocean_runner.entrypoint.get_config",
60-
return_value=CLIRunnerConfig(module="test.mod", base_dir="./_data"),
70+
return_value=CLIRunnerConfig(module="test.mod", base_dir=Path("./_data")),
6171
)
6272
def test_main_execution_flow(mock_get_config, mock_get_algo):
6373
"""Test the full main loop triggers the algorithm call."""
@@ -74,7 +84,7 @@ def test_main_execution_flow(mock_get_config, mock_get_algo):
7484
@patch("ocean_runner.entrypoint.get_algorithm", return_value=None)
7585
@patch(
7686
"ocean_runner.entrypoint.get_config",
77-
return_value=CLIRunnerConfig(module="bad.mod", base_dir="./_data"),
87+
return_value=CLIRunnerConfig(module="bad.mod", base_dir=Path("./_data")),
7888
)
7989
def test_main_failure_exit(mock_get_config, mock_get_algo):
8090
"""Test that the runner exits with code 1 if no algorithm is found."""
@@ -91,3 +101,78 @@ def test_version(mock_version):
91101
@patch("ocean_runner.entrypoint.version", side_effect=PackageNotFoundError())
92102
def test_version_failing(mock_version):
93103
assert "unknown" in get_version()
104+
105+
106+
@patch("importlib.import_module", side_effect=ImportError())
107+
def test_import_algorithm_error(mock_import, capsys):
108+
result = get_algorithm("test")
109+
110+
captured = capsys.readouterr()
111+
112+
assert result is None
113+
assert "Error loading" in captured.err
114+
115+
116+
def test_import_not_in_path():
117+
fake_cwd = "/fake/path"
118+
119+
with patch("os.getcwd", return_value=fake_cwd):
120+
with patch("sys.path", []):
121+
get_algorithm("some.module")
122+
123+
assert fake_cwd in sys.path
124+
125+
126+
@patch("sys.exit")
127+
@patch("pytest.main", return_value=0)
128+
@patch("ocean_runner.entrypoint.setup_environment")
129+
def test_run_tests(mock_setup_env, mock_pytest_main, mock_exit, capsys):
130+
config = MagicMock()
131+
config.base_dir = Path("/tmp/project")
132+
133+
args = ["-k", "test_something"]
134+
135+
run_tests(config, args)
136+
137+
# Check environment setup
138+
mock_setup_env.assert_called_once_with(config.base_dir)
139+
140+
# Check pytest execution
141+
mock_pytest_main.assert_called_once_with(args)
142+
143+
# Check exit called with pytest result
144+
mock_exit.assert_called_once_with(0)
145+
146+
# Check printed message
147+
captured = capsys.readouterr()
148+
assert "Preparing Test Environment at:" in captured.out
149+
150+
151+
@patch("ocean_runner.entrypoint.run_tests")
152+
@patch("ocean_runner.entrypoint.setup")
153+
def test_main_test_with_separator(mock_setup, mock_run_tests):
154+
mock_config = MagicMock()
155+
mock_setup.return_value = mock_config
156+
157+
test_argv = ["prog", "--config", "dev", "--", "-k", "test_api"]
158+
159+
with patch("sys.argv", test_argv):
160+
main_test()
161+
162+
mock_setup.assert_called_once_with(["--config", "dev"])
163+
mock_run_tests.assert_called_once_with(mock_config, ["-k", "test_api"])
164+
165+
166+
@patch("ocean_runner.entrypoint.run_tests")
167+
@patch("ocean_runner.entrypoint.setup")
168+
def test_main_test_without_separator(mock_setup, mock_run_tests):
169+
mock_config = MagicMock()
170+
mock_setup.return_value = mock_config
171+
172+
test_argv = ["prog", "--config", "dev"]
173+
174+
with patch("sys.argv", test_argv):
175+
main_test()
176+
177+
mock_setup.assert_called_once_with(["--config", "dev"])
178+
mock_run_tests.assert_called_once_with(mock_config, ["tests"])

tests/test_runner.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from returns.result import Failure
2+
from unittest.mock import patch
13
import logging
24
from functools import partial
35
from pathlib import Path
@@ -41,7 +43,7 @@ def _(_) -> int:
4143
yield algorithm
4244

4345

44-
def test_algorithm_without_config_raises():
46+
def test_algorithm_without_config_does_not_raise():
4547
Algorithm.create()
4648

4749

@@ -73,6 +75,23 @@ def test_parametrized_job_details(config: Config[CustomInput]):
7375
assert algorithm.job_details.input_parameters.isTrue
7476

7577

78+
def test_parametrized_job_details_failure_reading(config: Config[CustomInput]):
79+
class MockError(BaseException): ...
80+
81+
config = config.model_copy()
82+
config.custom_input = CustomInput
83+
84+
algorithm = Algorithm[CustomInput, int].create(config)
85+
algorithm.load()
86+
87+
with patch(
88+
"oceanprotocol_job_details.JobDetails.read",
89+
return_value=Failure(MockError()),
90+
):
91+
with raises(MockError):
92+
algorithm.job_details
93+
94+
7695
def test_empty_job_details_raises(config):
7796
algorithm = Algorithm.create(config=config)
7897

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)