Skip to content
Open
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
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ An implementation of `autotest` for Python inspired by [autotest](https://github
* test files
* will run that test file
* upon success, will run the entire suite
* pytest configuration
* supports configuration of different args to pass to pytest on a per unit or per suite basis

# Install

Expand All @@ -20,17 +22,37 @@ An implementation of `autotest` for Python inspired by [autotest](https://github
pip install autopytest

# poetry
poetry add autopytest
poetry add --group dev autopytest
```

# Configuration

In your `pyproject.toml` add the following.
* `source_directories`: the directories of your source code
* `test_directory`: the directory of your test code
* `include_source_dir_in_test_path`: whether or not to include the source directory when inferring the test path
* `pytest_unit_args`: the args to pass to pytest when running a `unit` test
* `pytest_suite_args`: the args to pass to pytest when running the entire test `suite`

In your `pyproject.toml` add the following minimal configuration:

```toml
[tool.autopytest]
source_directories = ["app"]
test_directory = "tests"
```

## `pytest` args

We (currently) automatically add the option `--no-header` to the `pytest` args to ensure that the output is consistent and easy to read/parse.

* NOTE: when using with `pytest-cov` we recommend not adding `--cov` options to your default `pytest` args in `pyproject.toml` to ensure debugger support

```toml
[tool.autopytest]
source_directories = ["app"]
test_directory = "tests"
pytest_unit_args = ["--failed-first", "--newest-first"]
pytest_suite_args = ["--cov", "--no-cov-on-fail", "--failed-first", "--newest-first"]
```

# Usage
Expand Down
8 changes: 7 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
* ~adds logging for when a file is modified~
* ~adds logging for when a test file cannot be found~
* ~if there is no matching unit test file, run the entire suite~
* ~allow configuration of matchers in case of using a different directory structure~
* ~allow configuration of pytest args~
* Future
* allow configuration of matchers in case of using a different directory structure
* remember if the last suite run failed or not and add `--last-failed` if so
* remember if the last unit run failed or not and add `--last-failed` if so (more difficult as we need to keep track of the unit
test and the result)
* ensure debugger support
* automatic disabling of code coverage (only if it's enabled) when attempting to run in debug mode?
* parse pytest results
* send notifications via 'terminal-notifier'
* support notifications from within a docker container
Expand Down
4 changes: 2 additions & 2 deletions autopytest/autotest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def match_strategy(self, path: Path) -> None:

if re.search(self.config.test_pattern, matcher):
test_path = path.relative_to(self.config.path.absolute())
strategy = TestFileStrategy(test_path)
strategy = TestFileStrategy(test_path, config=self.config)
strategy.execute()
return

Expand All @@ -80,7 +80,7 @@ def match_strategy(self, path: Path) -> None:
test_directory=self.config.test_directory,
)

strategy = SourceFileStrategy(source_file)
strategy = SourceFileStrategy(source_file, config=self.config)
strategy.execute()
return

Expand Down
3 changes: 3 additions & 0 deletions autopytest/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import platform
import re
from dataclasses import dataclass
Expand Down Expand Up @@ -35,6 +36,8 @@ class Config:
source_directories: list[str]
test_directory: str
include_source_dir_in_test_path: bool = True
pytest_unit_args: list[str] = dataclasses.field(default_factory=list)
pytest_suite_args: list[str] = dataclasses.field(default_factory=list)

@classmethod
def parse(cls, path: str) -> Self:
Expand Down
11 changes: 6 additions & 5 deletions autopytest/runners/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
class Pytest:
ARGS: Final[list] = [
"pytest",
"--no-cov",
"--no-header",
]

@classmethod
def run(cls, path: str) -> int:
return subprocess.call(cls.args(path)) # noqa: S603
def run(cls, path: str, args: list[str] | None = None) -> int:
args = args or []
return subprocess.call(cls.args(path, args)) # noqa: S603

@classmethod
def args(cls, path: str) -> list[str]:
return [*cls.ARGS, path]
def args(cls, path: str, args: list[str] | None = None) -> list[str]:
args = args or []
return [*cls.ARGS + args, path]
17 changes: 13 additions & 4 deletions autopytest/strategy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging as log
from pathlib import Path

from .config import Config
from .file import File
from .runners.pytest import Pytest

Expand All @@ -13,8 +14,10 @@ def __init__(self) -> None:
class Strategy:
_path: Path | None = None
_file: File | None = None
config: Config

def __init__(self, path_or_file: str | Path | File) -> None:
def __init__(self, path_or_file: str | Path | File, config: Config) -> None:
self.config = config
if isinstance(path_or_file, str):
self._path = Path(path_or_file)
elif isinstance(path_or_file, File):
Expand All @@ -23,7 +26,7 @@ def __init__(self, path_or_file: str | Path | File) -> None:
self._path = path_or_file

def execute(self) -> bool:
return Pytest.run(".") == 0
return Pytest.run(".", self.config.pytest_suite_args) == 0

@property
def test_path(self) -> str:
Expand Down Expand Up @@ -58,9 +61,15 @@ def execute(self) -> bool:
f"{self.file_path} - no matching test found at: {self.test_path}",
)

return Pytest.run(self.test_path) == 0 and super().execute()
return (
Pytest.run(self.test_path, self.config.pytest_unit_args) == 0
and super().execute()
)


class TestFileStrategy(Strategy):
def execute(self) -> bool:
return Pytest.run(self.test_path) == 0 and super().execute()
return (
Pytest.run(self.test_path, self.config.pytest_unit_args) == 0
and super().execute()
)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ignore = [
"D103",
"D104",
"D107",
"W503", # https://www.flake8rules.com/rules/W503.html
]
max-line-length = 88

Expand Down
5 changes: 2 additions & 3 deletions tests/runners/test_pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
def test_pytest() -> None:
assert Pytest.args("tests/test_calculator.py") == [
"pytest",
"--no-cov",
"--no-header",
"tests/test_calculator.py",
]
Expand All @@ -15,11 +14,11 @@ def test_pytest() -> None:
@patch("autopytest.runners.pytest.subprocess")
def test_pytest_run(mock_subprocess: MagicMock) -> None:
Pytest.run(".")
mock_subprocess.call.assert_called_with(["pytest", "--no-cov", "--no-header", "."])
mock_subprocess.call.assert_called_with(["pytest", "--no-header", "."])


@patch("autopytest.runners.pytest.subprocess")
def test_pytest_run_with_path(mock_subprocess: MagicMock) -> None:
path = "tests/test_calculator.py"
Pytest.run(path)
mock_subprocess.call.assert_called_with(["pytest", "--no-cov", "--no-header", path])
mock_subprocess.call.assert_called_with(["pytest", "--no-header", path])
12 changes: 6 additions & 6 deletions tests/test_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ def test_execute_source_file_strategy(mock_run: MagicMock) -> None:
test_directory=autotest.config.test_directory,
)

strategy = SourceFileStrategy(source_file)
strategy = SourceFileStrategy(source_file, config=autotest.config)
result = strategy.execute()

assert result
mock_run.assert_has_calls(
[
call("tests/app/test_module.py"),
call("."),
call("tests/app/test_module.py", []),
call(".", []),
],
)

Expand All @@ -36,13 +36,13 @@ def test_execute_test_file_strategy(mock_run: MagicMock) -> None:
path = Path("fixtures/application/tests/test_module.py").absolute()
path = path.relative_to(autotest.config.path.absolute())

strategy = TestFileStrategy(path)
strategy = TestFileStrategy(path, config=autotest.config)
result = strategy.execute()

assert result
mock_run.assert_has_calls(
[
call("tests/test_module.py"),
call("."),
call("tests/test_module.py", []),
call(".", []),
],
)