Skip to content

Commit 42ba8fe

Browse files
CopilotCodyCBakerPhDpre-commit-ci[bot]
authored
Add CLI tests with subprocess coverage capture (#9)
* Initial plan * Add CLI tests and configure subprocess coverage Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Update workflows for coverage collection and fix pip installation Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Refactor CLI tests: add helper function and fix code review issues Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix CI: Move test dependencies to optional-dependencies for pip compatibility Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Revert optional-dependencies changes and restore --group all flag - Remove test/coverage/dev from [project.optional-dependencies] - Keep them in [dependency-groups] as originally intended - Restore --group all flag to both workflow files for pip dependency-groups support Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Remove s3_log_extraction from coverage tracking Only track coverage for dandi_s3_log_extraction package since that's what this repository develops. s3_log_extraction is an external dependency. Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> * Fix CLI tests: Remove coverage run wrapper from subprocess calls The tests were failing because `coverage run` suppresses CLI output. Coverage is already captured by pytest's --cov flag, so we don't need to wrap subprocess calls with coverage run. Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: CodyCBakerPhD <51133164+CodyCBakerPhD@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 871d803 commit 42ba8fe

File tree

5 files changed

+170
-4
lines changed

5 files changed

+170
-4
lines changed

.github/workflows/remote_testing.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ jobs:
5757
- name: Run pytest with coverage and printout coverage for debugging
5858
run: |
5959
pytest -m "not remote" -vv -rsx --cov=dandi_s3_log_extraction --cov-report xml:./coverage.xml
60+
coverage combine || true
6061
cat ./coverage.xml
6162
6263
- name: Upload coverage to Codecov

.github/workflows/testing.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ jobs:
5050

5151
- name: Run pytest with coverage and printout coverage for debugging
5252
run: |
53-
pytest tests -m "not remote" -vv -rsx --cov=s3_log_extraction --cov-report xml:./coverage.xml
53+
pytest tests -m "not remote" -vv -rsx --cov=dandi_s3_log_extraction --cov-report xml:./coverage.xml
54+
coverage combine || true
5455
cat ./coverage.xml
5556
5657
- name: Upload coverage to Codecov

pyproject.toml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ dandis3logextraction = "dandi_s3_log_extraction._command_line_interface._cli:_da
8484

8585
[dependency-groups]
8686
test = ["pytest"]
87-
coverage = ["pytest-cov", "pytest-env"]
87+
coverage = ["pytest-cov", "pytest-env", "coverage[toml]"]
8888
dev = ["ipython", "pre-commit"]
8989
all = [
9090
{include-group = "test"},
@@ -157,6 +157,27 @@ markers = [
157157

158158

159159
[tool.coverage.run]
160+
concurrency = ["thread", "multiprocessing"]
161+
parallel = true
162+
source = ["src/dandi_s3_log_extraction"]
163+
sigterm = true
164+
165+
[tool.coverage.report]
166+
exclude_lines = [
167+
"pragma: no cover",
168+
"def __repr__",
169+
"raise AssertionError",
170+
"raise NotImplementedError",
171+
"if __name__ == .__main__.:",
172+
"if TYPE_CHECKING:",
173+
"if typing.TYPE_CHECKING:",
174+
]
175+
176+
[tool.coverage.paths]
177+
source = [
178+
"src/dandi_s3_log_extraction",
179+
"*/dandi_s3_log_extraction",
180+
]
160181
omit = [
161182
"src/dandi_s3_log_extraction/_parallel/_utils.py",
162183
]

tests/README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,34 @@ The names of the following test files are patterned off of the S3 log filename c
88

99

1010

11-
# 2020-01-01-05-06-35-0123456789ABCDEF (Easy lines)
11+
# Test Suite Overview
12+
13+
## Extraction Tests
14+
15+
### 2020-01-01-05-06-35-0123456789ABCDEF (Easy lines)
1216

1317
The 'easy' collection contains the most typical lines which follow a nice, simple, and reliable structure.
1418

1519

1620

17-
# 2022-01-01-05-06-35-0123456789ABCDEF (Hard lines)
21+
### 2022-01-01-05-06-35-0123456789ABCDEF (Hard lines)
1822

1923
The 'hard' collection contains many of the most difficult lines to extract as they were found from error reports.
24+
25+
26+
27+
## CLI Tests (test_cli.py)
28+
29+
Tests for the command-line interface using subprocess execution. These tests:
30+
- Verify all CLI commands and subcommands work correctly
31+
- Test help text and error handling
32+
- Run CLI commands directly via subprocess
33+
34+
Coverage for CLI code is captured by pytest's `--cov` flag at the test execution level.
35+
36+
To run CLI tests with coverage:
37+
```bash
38+
pytest tests/test_cli.py -vv --cov=dandi_s3_log_extraction
39+
```
40+
41+
The coverage configuration in `pyproject.toml` enables parallel and subprocess coverage collection.

tests/test_cli.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Tests for the command line interface."""
2+
3+
import os
4+
import pathlib
5+
import subprocess
6+
import sys
7+
8+
import pytest
9+
10+
11+
def _run_cli_command(*args):
12+
"""Helper function to run CLI commands."""
13+
return subprocess.run(
14+
[sys.executable, "-m", "dandi_s3_log_extraction._command_line_interface._cli", *args],
15+
capture_output=True,
16+
text=True,
17+
)
18+
19+
20+
def test_cli_help():
21+
"""Test that the main CLI help command works."""
22+
result = _run_cli_command()
23+
# Should fail with exit code 2 because no command is provided, but still show usage
24+
assert result.returncode != 0
25+
# Check output contains expected help information
26+
assert "Usage:" in result.stdout or "Usage:" in result.stderr
27+
28+
29+
def test_cli_main_help_flag():
30+
"""Test the main CLI --help flag."""
31+
result = _run_cli_command("--help")
32+
assert result.returncode == 0
33+
assert "Usage:" in result.stdout
34+
35+
36+
def test_extract_help():
37+
"""Test the extract command help."""
38+
result = _run_cli_command("extract", "--help")
39+
assert result.returncode == 0
40+
assert "extract" in result.stdout.lower()
41+
assert "directory" in result.stdout.lower()
42+
43+
44+
def test_stop_help():
45+
"""Test the stop command help."""
46+
result = _run_cli_command("stop", "--help")
47+
assert result.returncode == 0
48+
assert "stop" in result.stdout.lower()
49+
assert "timeout" in result.stdout.lower()
50+
51+
52+
def test_update_help():
53+
"""Test the update command help."""
54+
result = _run_cli_command("update", "--help")
55+
assert result.returncode == 0
56+
assert "update" in result.stdout.lower()
57+
58+
59+
def test_update_database_help():
60+
"""Test the update database command help."""
61+
result = _run_cli_command("update", "database", "--help")
62+
assert result.returncode == 0
63+
assert "database" in result.stdout.lower()
64+
65+
66+
def test_update_summaries_help():
67+
"""Test the update summaries command help."""
68+
result = _run_cli_command("update", "summaries", "--help")
69+
assert result.returncode == 0
70+
assert "summaries" in result.stdout.lower()
71+
72+
73+
def test_update_totals_help():
74+
"""Test the update totals command help."""
75+
result = _run_cli_command("update", "totals", "--help")
76+
assert result.returncode == 0
77+
assert "totals" in result.stdout.lower()
78+
79+
80+
def test_extract_with_example_logs(tmpdir):
81+
"""Test the extract command with example logs."""
82+
base_directory = pathlib.Path(__file__).parent
83+
test_logs_directory = base_directory / "example_logs"
84+
85+
# Only run if example logs exist
86+
if not test_logs_directory.exists():
87+
pytest.skip("Example logs directory not found")
88+
89+
tmpdir = pathlib.Path(tmpdir)
90+
output_directory = tmpdir / "test_cli_extraction"
91+
92+
result = subprocess.run(
93+
[
94+
sys.executable,
95+
"-m",
96+
"dandi_s3_log_extraction._command_line_interface._cli",
97+
"extract",
98+
str(test_logs_directory),
99+
"--workers",
100+
"1",
101+
"--limit",
102+
"1",
103+
],
104+
capture_output=True,
105+
text=True,
106+
env={
107+
**os.environ,
108+
"S3_LOG_EXTRACTION_CACHE_DIRECTORY": str(output_directory),
109+
},
110+
)
111+
# Command should succeed or fail gracefully (not crash)
112+
# Exit code depends on whether environment is properly set up
113+
# We just want to ensure it doesn't crash with an unexpected error
114+
assert result.returncode in [0, 1]
115+
116+
117+
def test_extract_invalid_directory():
118+
"""Test the extract command with an invalid directory."""
119+
result = _run_cli_command("extract", "/nonexistent/directory/that/does/not/exist")
120+
# Should fail because directory doesn't exist
121+
assert result.returncode != 0

0 commit comments

Comments
 (0)