Skip to content

Commit

Permalink
Merge pull request #4 from ajatkj/update_tests_and_coverage
Browse files Browse the repository at this point in the history
update_tests_and_add_coverage
  • Loading branch information
ajatkj authored Feb 8, 2024
2 parents d091430 + 192ae39 commit 3d0bdf4
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 23 deletions.
42 changes: 39 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ on:
branches: ["main"]

jobs:
build:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
Expand All @@ -26,11 +27,46 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- name: Lint
run: |
poe lint
- run: mkdir coverage
- name: Test
run: |
poe test
poe coverage
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}

- name: store coverage files
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}-${{ matrix.python-version }}
path: coverage

coverage-combine:
needs: [test]
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.8"
- name: get coverage files
uses: actions/download-artifact@v4
with:
merge-multiple: true
pattern: coverage-*
path: coverage
- run: pip install coverage[toml]
- run: ls -la coverage
- run: coverage combine coverage
- run: coverage report
- run: coverage html --show-contexts --title "typed-configparser coverage for ${{ github.sha }}"
- name: Store coverage html
uses: actions/upload-artifact@v4
with:
name: coverage-html
path: htmlcov
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,34 @@ help = "Clear all cache."

[tool.poe.tasks.test]
cmd = "python3 -m unittest -v tests/tests.py"
help = "Run tests"

[tool.poe.tasks._coverage]
shell = "coverage run -m unittest tests/tests.py"
env.COVERAGE_FILE.default = ".coverage_default/coverage_local"
env.CONTEXT.default = "default_context"

[tool.poe.tasks._coverage_pre]
shell = "mkdir -p $(dirname $COVERAGE_FILE)"
env.COVERAGE_FILE.default = ".coverage_default/coverage_local"

[tool.poe.tasks.coverage]
sequence = ["_coverage_pre", "_coverage"]
help = "Run coverage"

[tool.poe.tasks.coverage_report]
shell = "coverage report && coverage html --show-contexts --title 'typed_configparser coverage'"
help = ""
env.COVERAGE_FILE.default = ".coverage_default/coverage_local"
env.CONTEXT.default = "default_context"

[tool.coverage.report]
exclude_also = [
"def _CUSTOM_REPR_METHOD",
"def _CUSTOM_STR_METHOD",
"from _typeshed",
]

[tool.coverage.run]
omit = ["tests/*"]
context = '${CONTEXT}'
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mypy
ruff
poethepoet
pre-commit
pre-commit
coverage
159 changes: 157 additions & 2 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ class TestDataclass:
option2: str
option3: float
option4: bool
option5: None

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "42")
self.config_parser.set(_SECTION_, "option2", "value")
self.config_parser.set(_SECTION_, "option3", "10.5")
self.config_parser.set(_SECTION_, "option4", "True")
self.config_parser.set(_SECTION_, "option5", "None")

result = self.config_parser.parse_section(TestDataclass, _SECTION_)

Expand All @@ -38,14 +40,16 @@ class TestDataclass:
self.assertIsInstance(result.option3, float)
self.assertEqual(result.option4, True)
self.assertIsInstance(result.option4, bool)
self.assertEqual(result.option5, None)
self.assertIsInstance(result.option5, type(None))

def test_parse_section_invalid_dataclass(self) -> None:
class NotADataclass:
pass

self.config_parser.add_section(_SECTION_)

with self.assertRaises(ParseError):
with self.assertRaisesRegex(ParseError, f"ParseError in section '{_SECTION_}'"):
self.config_parser.parse_section(NotADataclass, _SECTION_) # type: ignore

def test_parse_section_extra_fields_allow(self) -> None:
Expand Down Expand Up @@ -200,9 +204,160 @@ class TestDataclass:

self.config_parser.add_section(_SECTION_)

with self.assertRaises(ParseError):
with self.assertRaisesRegex(ParseError, f"ParseError in section '{_SECTION_}' for option 'option1'"):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_boolean(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: bool

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'boolean'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_int(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: int

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'int'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_float(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: float

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'float'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_str(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: str

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", '["foo", "bar"]')

with self.assertRaisesRegex(
ParseError,
f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value '"
+ '\\["foo", "bar"\\]'
+ "' to 'str'",
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_none(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: None

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'None'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_union(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: typing.Union[int, float]

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError,
f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'union type'",
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_list(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: typing.List[int]

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "12")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value '12' to 'list'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_tuple(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: typing.Tuple[str, int]

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "12")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value '12' to 'tuple'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_dict(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: typing.Dict[str, int]

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError, f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'dict'"
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_invalid_any(self) -> None:
class CustomType:
def __init__(self) -> None:
pass

@dataclasses.dataclass
class TestDataclass:
option1: CustomType

self.config_parser.add_section(_SECTION_)
self.config_parser.set(_SECTION_, "option1", "foo")

with self.assertRaisesRegex(
ParseError,
f"ParseError in section '{_SECTION_}' for option 'option1': Cannot cast value 'foo' to 'CustomType'",
):
self.config_parser.parse_section(TestDataclass, _SECTION_)

def test_parse_section_default_factory(self) -> None:
@dataclasses.dataclass
class TestDataclass:
option1: typing.List[str] = dataclasses.field(default_factory=lambda: ["foo", "bar", "baz"])

self.config_parser.add_section(_SECTION_)
result = self.config_parser.parse_section(TestDataclass, _SECTION_)

self.assertIsInstance(result, TestDataclass)
self.assertEqual(result.option1, ["foo", "bar", "baz"])
self.assertIsInstance(result.option1, typing.List)


def start_test() -> None:
unittest.main()
Expand Down
Loading

0 comments on commit 3d0bdf4

Please sign in to comment.