Skip to content

Commit 11b9780

Browse files
authored
ci(coverage): adding mandatory 80% level of coverage for diff (#239)
1 parent 0c0ced5 commit 11b9780

11 files changed

Lines changed: 250 additions & 11 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ jobs:
2727
uv venv && uv pip install -e '.[dev]' coverage
2828
2929
- name: Run tests with coverage
30-
run: uv run coverage run -m pytest
30+
run: uv run coverage run -m pytest --junitxml="junit.xml"
3131

3232
- name: Print coverage summary
33-
run: uv run coverage report -m --fail-under=70
33+
run: uv run coverage report -m --fail-under=80
3434

3535
- name: Generate coverage XML
3636
run: uv run coverage xml -o coverage.xml
@@ -40,6 +40,12 @@ jobs:
4040
uses: codecov/codecov-action@v7
4141
with:
4242
token: ${{ secrets.CODECOV_TOKEN }}
43-
flags: connectors
4443
fail_ci_if_error: false
4544
verbose: true
45+
46+
- name: Upload test results to Codecov
47+
if: ${{ !cancelled() && hashFiles('junit.xml') != '' }}
48+
uses: codecov/codecov-action@v7
49+
with:
50+
token: ${{ secrets.CODECOV_TOKEN }}
51+
report_type: test_results

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ coverage.xml
7070
.hypothesis/
7171
.pytest_cache/
7272
cover/
73+
junit.xml
7374

7475
# Translations
7576
*.mo
@@ -136,4 +137,4 @@ dmypy.json
136137
cython_debug/
137138

138139
# testing
139-
test.py
140+
test.py

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ developing features or fixing bugs yourself.
1919
* If you are interested in contributing code, fork the repository, create a
2020
branch, and open a pull request.
2121

22+
## Code quality and testing
23+
24+
In order to maintain a certain standard regarding code quality, this repository CI will run various tools against expectations:
25+
- `isort` will be used to check for proper imports sorting, and the CI will fail if the checks fail
26+
- `black` will be used to check for proper code formatting, and the CI will fail if the checks fail
27+
- `flake8` will be used to check for proper code styling, and the CI will fail if the checks fail
28+
- `pytest` will be used to run various levels of testing, and the CI will fail if the tests fail
29+
- `coverage` with `codecov` will be used to check for proper code coverage, and the CI will fail if the level is below 80% for the current diff (a warning will be emitted if the level falls below 80% for the overall project)
30+
2231
<!-- filigran-conventions:start -->
2332
## Commit, pull request & issue conventions
2433

codecov.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
informational: true
7+
patch:
8+
default:
9+
target: 80%
10+
informational: false
11+
12+
comment:
13+
layout: "diff, files, condensed_footer"
14+
hide_project_coverage: true # set to true
15+
require_changes: "coverage_drop AND uncovered_patch"
16+
17+
ignore:
18+
# Ignore any folder whose name starts with "test" anywhere in the tree
19+
- "**/test*/**"

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,6 @@ warn_unused_ignores = true
106106
omit = [
107107
"test/*",
108108
]
109+
110+
[tool.coverage.report]
111+
fail_under = 80

test/backends/test_backend.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,38 @@
11
import unittest
2+
from unittest.mock import MagicMock
23

34
from pyoaev.backends import backend as module
45

56

7+
class TestTokenAuth(unittest.TestCase):
8+
def test_auto_bearer_token(self):
9+
request = MagicMock()
10+
request.headers = dict()
11+
token = "my-secret-token"
12+
13+
token_auth = module.TokenAuth(token=token)
14+
15+
_request = token_auth(request)
16+
17+
self.assertEqual(
18+
_request.headers["Authorization"],
19+
f"Bearer {token}",
20+
)
21+
22+
23+
class TestRequestsReponse(unittest.TestCase):
24+
def test_init_and_properties(self):
25+
response = MagicMock()
26+
27+
rr = module.RequestsResponse(response)
28+
29+
self.assertEqual(response, rr.response)
30+
self.assertEqual(response.status_code, rr.status_code)
31+
self.assertEqual(response.headers, rr.headers)
32+
self.assertEqual(response.content, rr.content)
33+
self.assertEqual(response.reason, rr.reason)
34+
35+
636
class TestRequestsBackend(unittest.TestCase):
737
def test_no_cookie_allowed(self):
838
backend = module.RequestsBackend()

test/configuration/test_configuration.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import unittest
33
from unittest.mock import patch
44

5+
from pydantic_settings import BaseSettings
6+
57
from pyoaev.configuration import Configuration
8+
from pyoaev.configuration.connector_config_schema_generator import (
9+
ConnectorConfigSchemaGenerator,
10+
)
611

712
TEST_CONFIG_HINTS = {
813
"string_config_direct": {"data": "this is string_config_direct"},
@@ -252,6 +257,23 @@ def test_when_bool_config_has_default_when_key_is_not_found_return_default(self)
252257

253258
self.assertEqual(value, True)
254259

260+
def test_configuration_schema_generation(self):
261+
config_obj = Configuration(
262+
config_hints=TEST_CONFIG_HINTS,
263+
config_base_model=BaseSettings(),
264+
)
265+
266+
_schema = config_obj.schema()
267+
268+
self.assertEqual(
269+
_schema,
270+
BaseSettings.model_json_schema(
271+
by_alias=False,
272+
schema_generator=ConnectorConfigSchemaGenerator,
273+
mode="validation",
274+
),
275+
)
276+
255277

256278
if __name__ == "__main__":
257279
unittest.main()

test/daemons/test_base_daemon.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,21 @@ def bound_method(self):
5151
)
5252

5353

54+
@unittest.mock.patch("argparse.ArgumentParser")
5455
class TestBaseDaemon(unittest.TestCase):
55-
def test_when_no_callback_when_complete_config_ctor_ok(self):
56+
def test_when_no_callback_when_complete_config_ctor_ok(self, _):
5657
daemon = DaemonForTest(configuration=TEST_DAEMON_CONFIGURATION)
5758

5859
self.assertIsInstance(daemon, BaseDaemon)
5960

60-
def test_when_no_callback_when_lacking_config_key_ctor_throws(self):
61+
def test_when_no_callback_when_lacking_config_key_ctor_throws(self, _):
6162
with self.assertRaises(Exception):
6263
DaemonForTest(configuration=Configuration(config_hints={}))
6364

64-
def test_when_no_callback_daemon_cant_start(self):
65+
def test_when_no_callback_daemon_cant_start(self, m_argparser):
66+
m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock(
67+
dump_config_schema=False
68+
)
6569
daemon, mock_setup, mock_start_loop, _ = create_mock_daemon()
6670

6771
with self.assertRaises(OpenAEVError):
@@ -70,23 +74,26 @@ def test_when_no_callback_daemon_cant_start(self):
7074
mock_setup.assert_not_called()
7175
mock_start_loop.assert_not_called()
7276

73-
def test_when_callback_daemon_can_start(self):
77+
def test_when_callback_daemon_can_start(self, m_argparser):
78+
m_argparser.return_value.parse_args.return_value = unittest.mock.MagicMock(
79+
dump_config_schema=False
80+
)
7481
daemon, mock_setup, mock_start_loop, _ = create_mock_daemon(lambda: None)
7582

7683
daemon.start()
7784

7885
mock_setup.assert_called_once()
7986
mock_start_loop.assert_called_once()
8087

81-
def test_when_callback_is_bound_method_daemon_can_call(self):
88+
def test_when_callback_is_bound_method_daemon_can_call(self, _):
8289
daemon, mock_setup, mock_start_loop, inner_mock_func = create_mock_daemon()
8390
daemon.set_callback(daemon.bound_method)
8491

8592
daemon._try_callback()
8693

8794
inner_mock_func.assert_called_once()
8895

89-
def test_when_callback_is_func_with_collector_parameter_daemon_can_call(self):
96+
def test_when_callback_is_func_with_collector_parameter_daemon_can_call(self, _):
9097
daemon, mock_setup, mock_start_loop, _ = create_mock_daemon()
9198
inner_mock_func = unittest.mock.MagicMock()
9299
daemon.set_callback(lambda collector: inner_mock_func())
@@ -95,7 +102,7 @@ def test_when_callback_is_func_with_collector_parameter_daemon_can_call(self):
95102

96103
inner_mock_func.assert_called_once()
97104

98-
def test_when_callback_is_func_with_other_parameter_daemon_cant_call(self):
105+
def test_when_callback_is_func_with_other_parameter_daemon_cant_call(self, _):
99106
daemon, mock_setup, mock_start_loop, _ = create_mock_daemon()
100107
inner_mock_func = unittest.mock.MagicMock()
101108
daemon.set_callback(lambda other_parameter: inner_mock_func())

test/test_base.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import unittest
2+
from unittest.mock import MagicMock, sentinel
3+
4+
import pyoaev.base as module
5+
6+
7+
class TestRESTObject(unittest.TestCase):
8+
def test_restobject_minimal_init(self):
9+
manager = MagicMock()
10+
manager.parent_attrs = {"parentkey1": "parentvalue1"}
11+
attrs = {"key1": "value1"}
12+
created_from_list = sentinel._created_from_list
13+
14+
rest_object = module.RESTObject(
15+
manager, attrs, created_from_list=created_from_list
16+
)
17+
18+
self.assertEqual(rest_object.manager, manager)
19+
self.assertEqual(rest_object._attrs, attrs)
20+
self.assertEqual(rest_object._created_from_list, created_from_list)
21+
self.assertEqual(rest_object._updated_attrs, {})
22+
self.assertEqual(rest_object._parent_attrs, manager.parent_attrs)
23+
self.assertEqual(
24+
str(rest_object), "<class 'pyoaev.base.RESTObject'> => {'key1': 'value1'}"
25+
)
26+
27+
# properties
28+
self.assertEqual(
29+
rest_object.attributes, {"key1": "value1", "parentkey1": "parentvalue1"}
30+
)
31+
self.assertIsNone(rest_object.encoded_id)
32+
33+
def test_restobject_failed_init(self):
34+
manager = MagicMock()
35+
attrs = MagicMock()
36+
created_from_list = sentinel._created_from_list
37+
38+
with self.assertRaises(module.OpenAEVParsingError):
39+
module.RESTObject(manager, attrs, created_from_list=created_from_list)
40+
41+
def test_restobject_to_json(self):
42+
manager = MagicMock()
43+
manager.parent_attrs = {"parentkey1": "parentvalue1"}
44+
attrs = {"key1": "value1"}
45+
created_from_list = sentinel._created_from_list
46+
47+
rest_object = module.RESTObject(
48+
manager, attrs, created_from_list=created_from_list
49+
)
50+
51+
jsondata = rest_object.to_json()
52+
53+
self.assertEqual(jsondata, '{"key1": "value1"}')
54+
self.assertNotIn("parentkey1", jsondata)
55+
56+
def test_restobject_get_id(self):
57+
manager = MagicMock()
58+
manager.parent_attrs = {"parentkey1": "parentvalue1"}
59+
attrs = {"key1": "value1"}
60+
created_from_list = sentinel._created_from_list
61+
62+
rest_object = module.RESTObject(
63+
manager, attrs, created_from_list=created_from_list
64+
)
65+
66+
self.assertIsNone(rest_object.get_id())
67+
68+
rest_object._update_attrs({"_id_attr": "my_id"})
69+
70+
self.assertIsNone(rest_object.get_id())

test/test_exceptions.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import unittest
2+
from unittest.mock import MagicMock
3+
4+
import pyoaev.exceptions as module
5+
6+
7+
class TestExceptions(unittest.TestCase):
8+
def test_openaeverror_init(self):
9+
error_message = MagicMock()
10+
response_code = MagicMock()
11+
response_body = MagicMock()
12+
13+
exception = module.OpenAEVError(error_message, response_code, response_body)
14+
15+
self.assertEqual(exception.response_code, response_code)
16+
self.assertEqual(exception.response_body, response_body)
17+
self.assertEqual(exception.error_message, error_message.decode.return_value)
18+
19+
def test_openaeverror_init_auto_decode(self):
20+
response_code = MagicMock()
21+
response_body = MagicMock()
22+
23+
exception = module.OpenAEVError(b"test", response_code, response_body)
24+
25+
self.assertEqual(exception.error_message, "test")
26+
27+
exception = module.OpenAEVError("test", response_code, response_body)
28+
29+
self.assertEqual(exception.error_message, "test")
30+
31+
def test_openaeverror_strcast_no_message(self):
32+
error_message = None
33+
response_code = 418
34+
response_body = MagicMock()
35+
36+
exception = module.OpenAEVError(error_message, response_code, response_body)
37+
38+
strdata = str(exception)
39+
40+
self.assertEqual(strdata, "418: Unknown error")
41+
42+
def test_openaeverror_strcast_no_message_no_code(self):
43+
error_message = None
44+
response_code = None
45+
response_body = MagicMock()
46+
47+
exception = module.OpenAEVError(error_message, response_code, response_body)
48+
49+
strdata = str(exception)
50+
51+
self.assertEqual(strdata, "Unknown error")
52+
53+
def test_openaeverror_strcast_with_body(self):
54+
error_message = None
55+
response_code = 418
56+
response_body = b'{"message": "I am a teapot"}'
57+
58+
exception = module.OpenAEVError(error_message, response_code, response_body)
59+
60+
strdata = str(exception)
61+
62+
self.assertEqual(strdata, "418: I am a teapot")

0 commit comments

Comments
 (0)