Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update_tests_and_add_coverage #4

Merged
merged 1 commit into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading