Skip to content

Commit 03e524c

Browse files
committed
feat(cli): add comprehensive test command group to Tux CLI
Introduce a new 'test' command group in the Tux CLI, providing various testing-related commands. This includes running tests with coverage, quick tests, plain output tests, parallel execution, HTML report generation, benchmark tests, and coverage reports with multiple formats. The commands are now organized in a dedicated `test.py` module, improving maintainability and separation of concerns. The changes aim to enhance the testing capabilities of the Tux CLI, making it easier for developers to run tests and generate reports with different configurations. This modular approach also allows for easier future extensions and modifications to the testing commands.
1 parent 4103948 commit 03e524c

File tree

4 files changed

+298
-210
lines changed

4 files changed

+298
-210
lines changed

tux/cli/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ tux # Main entry point (defined in cli/core.py)
3434
│ ├── format # Format code
3535
│ ├── type-check # Check types
3636
│ └── pre-commit # Run pre-commit checks
37+
├── test # Testing commands (defined in cli/test.py)
38+
│ ├── run # Run tests with coverage (enhanced output via pytest-sugar)
39+
│ ├── quick # Run tests without coverage (faster)
40+
│ ├── plain # Run tests with plain output (no pytest-sugar)
41+
│ ├── parallel # Run tests in parallel using multiple workers
42+
│ ├── html # Run tests and generate HTML report
43+
│ ├── benchmark # Run benchmark tests to measure performance
44+
│ ├── coverage # Generate coverage reports with options
45+
│ ├── coverage-clean # Clean coverage files
46+
│ └── coverage-open # Open HTML coverage report
3747
├── docker # Docker commands (defined in cli/docker.py)
3848
│ ├── build # Build Docker image
3949
│ ├── up # Start Docker services
@@ -79,6 +89,33 @@ poetry run tux db push --prod
7989

8090
# Run docker compose up using development settings (flag after command)
8191
poetry run tux docker up --build --dev
92+
93+
# Run tests with enhanced output (pytest-sugar enabled by default)
94+
poetry run tux test run
95+
96+
# Run quick tests without coverage (faster)
97+
poetry run tux test quick
98+
99+
# Run tests with plain output (no pytest-sugar)
100+
poetry run tux test plain
101+
102+
# Run tests in parallel (utilizes all CPU cores)
103+
poetry run tux test parallel
104+
105+
# Generate beautiful HTML test reports
106+
poetry run tux test html
107+
108+
# Run performance benchmarks
109+
poetry run tux test benchmark
110+
111+
# Generate HTML coverage report and open it
112+
poetry run tux test coverage --format=html --open
113+
114+
# Generate coverage for specific component with threshold
115+
poetry run tux test coverage --specific=tux/database --fail-under=90
116+
117+
# Clean coverage files and generate fresh report
118+
poetry run tux test coverage --clean --format=html
82119
```
83120

84121
## Environment Handling

tux/cli/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def group_func() -> None:
197197
def register_commands() -> None:
198198
"""Load and register all CLI commands."""
199199

200-
modules = ["database", "dev", "docs", "docker"]
200+
modules = ["database", "dev", "docs", "docker", "test"]
201201

202202
for module_name in modules:
203203
try:

tux/cli/dev.py

Lines changed: 0 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
"""Development tools and utilities for Tux."""
22

3-
import shutil
4-
import webbrowser
5-
from pathlib import Path
6-
7-
import click
8-
from loguru import logger
9-
103
from tux.cli.core import (
114
command_registration_decorator,
125
create_group,
@@ -45,205 +38,3 @@ def type_check() -> int:
4538
def check() -> int:
4639
"""Run pre-commit checks."""
4740
return run_command(["pre-commit", "run", "--all-files"])
48-
49-
50-
@command_registration_decorator(dev_group, name="test")
51-
def test() -> int:
52-
"""Run tests with coverage."""
53-
return run_command(["pytest", "--cov=tux", "--cov-report=term-missing"])
54-
55-
56-
@command_registration_decorator(dev_group, name="test-quick")
57-
def test_quick() -> int:
58-
"""Run tests without coverage (faster)."""
59-
return run_command(["pytest", "--no-cov"])
60-
61-
62-
def _build_coverage_command(specific: str | None, quick: bool, report_format: str, fail_under: int | None) -> list[str]:
63-
"""Build the pytest coverage command with options."""
64-
cmd = ["pytest"]
65-
66-
# Set coverage path (specific or default)
67-
if specific:
68-
logger.info(f"🔍 Running coverage for specific path: {specific}")
69-
cmd.append(f"--cov={specific}")
70-
else:
71-
cmd.append("--cov=tux")
72-
73-
# Handle quick mode (no reports)
74-
if quick:
75-
logger.info("⚡ Quick coverage check (no reports)...")
76-
cmd.append("--cov-report=")
77-
return cmd
78-
79-
# Add report format
80-
_add_report_format(cmd, report_format)
81-
82-
# Add fail-under if specified
83-
if fail_under is not None:
84-
logger.info(f"🎯 Running with {fail_under}% coverage threshold...")
85-
cmd.extend(["--cov-fail-under", str(fail_under)])
86-
87-
return cmd
88-
89-
90-
def _add_report_format(cmd: list[str], report_format: str) -> None:
91-
"""Add report format option to command."""
92-
match report_format:
93-
case "term":
94-
logger.info("🏃 Running tests with terminal coverage report...")
95-
cmd.append("--cov-report=term-missing")
96-
case "html":
97-
logger.info("📊 Generating HTML coverage report...")
98-
cmd.append("--cov-report=html")
99-
case "xml":
100-
logger.info("📄 Generating XML coverage report...")
101-
cmd.append("--cov-report=xml")
102-
case "json":
103-
logger.info("📋 Generating JSON coverage report...")
104-
cmd.append("--cov-report=json")
105-
case _:
106-
# Default case - should not happen due to click choices
107-
cmd.append("--cov-report=term-missing")
108-
109-
110-
def _handle_post_coverage_actions(result: int, report_format: str, open_browser: bool) -> None:
111-
"""Handle post-command actions after coverage run."""
112-
if result != 0:
113-
return
114-
115-
match report_format:
116-
case "html":
117-
logger.success("✅ HTML report generated at: htmlcov/index.html")
118-
if open_browser:
119-
logger.info("🌐 Opening HTML coverage report...")
120-
try:
121-
webbrowser.open("htmlcov/index.html")
122-
except Exception:
123-
logger.warning("Could not open browser. HTML report is available at htmlcov/index.html")
124-
case "xml":
125-
logger.success("✅ XML report generated at: coverage.xml")
126-
case "json":
127-
logger.success("✅ JSON report generated at: coverage.json")
128-
case _:
129-
# For terminal or other formats, no specific post-action needed
130-
pass
131-
132-
133-
@command_registration_decorator(dev_group, name="coverage")
134-
@click.option(
135-
"--format",
136-
"report_format",
137-
type=click.Choice(["term", "html", "xml", "json"], case_sensitive=False),
138-
default="term",
139-
help="Coverage report format",
140-
)
141-
@click.option(
142-
"--fail-under",
143-
type=click.IntRange(0, 100),
144-
help="Fail if coverage is below this percentage",
145-
)
146-
@click.option(
147-
"--open",
148-
is_flag=True,
149-
help="Open HTML report in browser (only with --format=html)",
150-
)
151-
@click.option(
152-
"--quick",
153-
is_flag=True,
154-
help="Quick coverage check without generating reports",
155-
)
156-
@click.option(
157-
"--clean",
158-
is_flag=True,
159-
help="Clean coverage files before running",
160-
)
161-
@click.option(
162-
"--specific",
163-
type=str,
164-
help="Run coverage for specific path (e.g., tux/utils)",
165-
)
166-
def coverage(
167-
report_format: str,
168-
fail_under: int | None,
169-
open: bool, # noqa: A002
170-
quick: bool,
171-
clean: bool,
172-
specific: str | None,
173-
) -> int:
174-
"""Generate coverage reports with various options."""
175-
# Clean first if requested
176-
if clean:
177-
logger.info("🧹 Cleaning coverage files...")
178-
coverage_clean()
179-
180-
# Build and run command
181-
cmd = _build_coverage_command(specific, quick, report_format, fail_under)
182-
result = run_command(cmd)
183-
184-
# Handle post-command actions
185-
_handle_post_coverage_actions(result, report_format, open)
186-
187-
return result
188-
189-
190-
@command_registration_decorator(dev_group, name="coverage-clean")
191-
def coverage_clean() -> int:
192-
"""Clean coverage files and reports."""
193-
logger.info("🧹 Cleaning coverage files...")
194-
195-
files_to_remove = [".coverage", "coverage.xml", "coverage.json"]
196-
dirs_to_remove = ["htmlcov"]
197-
198-
# Remove individual files
199-
for file_name in files_to_remove:
200-
file_path = Path(file_name)
201-
try:
202-
if file_path.exists():
203-
file_path.unlink()
204-
logger.info(f"Removed {file_name}")
205-
except OSError as e:
206-
logger.error(f"Error removing {file_name}: {e}")
207-
208-
# Remove directories
209-
for dir_name in dirs_to_remove:
210-
dir_path = Path(dir_name)
211-
try:
212-
if dir_path.exists():
213-
shutil.rmtree(dir_path)
214-
logger.info(f"Removed {dir_name}")
215-
except OSError as e:
216-
logger.error(f"Error removing {dir_name}: {e}")
217-
218-
# Remove .coverage.* pattern files using Path.glob
219-
cwd = Path()
220-
for coverage_file in cwd.glob(".coverage.*"):
221-
try:
222-
coverage_file.unlink()
223-
logger.info(f"Removed {coverage_file.name}")
224-
except OSError as e:
225-
logger.error(f"Error removing {coverage_file.name}: {e}")
226-
227-
logger.success("✅ Coverage files cleaned")
228-
return 0
229-
230-
231-
@command_registration_decorator(dev_group, name="coverage-open")
232-
def coverage_open() -> int:
233-
"""Open HTML coverage report in browser."""
234-
html_report = Path("htmlcov/index.html")
235-
236-
if not html_report.exists():
237-
logger.error("❌ HTML report not found. Run 'poetry run tux dev coverage --format=html' first")
238-
return 1
239-
240-
logger.info("🌐 Opening HTML coverage report...")
241-
try:
242-
webbrowser.open(str(html_report))
243-
except Exception as e:
244-
logger.error(f"Could not open browser: {e}")
245-
logger.info(f"HTML report is available at: {html_report}")
246-
return 1
247-
else:
248-
logger.success("✅ Coverage report opened in browser")
249-
return 0

0 commit comments

Comments
 (0)