Skip to content

Commit bc55bee

Browse files
yarikopticclaude
andcommitted
Extend testing to Python 3.8-3.13 and improve test coverage
- Update tox.ini to test across Python 3.8-3.13 with py3 as default env - Update GitHub Actions workflow following zarr_checksum pattern: * Use toxenv: [py3] as default for all Python versions * Add lint job specifically for Python 3.10 via include - Add 11 new tests for comprehensive coverage (22 tests total): * Color output verification with ANSI codes (--color=on/off/auto) * Format comparison tests (inline vs full-lines differences) * Structure parsing for Python/XML/JSON nested hierarchies * Edge cases: missing files, empty files, out-of-range lines * Case sensitivity verification - Fix color tests to use FORCE_COLOR=1 for non-TTY environments - Update CLAUDE.md with detailed test coverage documentation All 22 tests pass across Python 3.8-3.13. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a838c2b commit bc55bee

4 files changed

Lines changed: 296 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- "3.10"
2222
- "3.11"
2323
- "3.12"
24+
- "3.13"
2425
toxenv: [py3]
2526
include:
2627
- python-version: "3.10"

CLAUDE.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,41 @@ Located in `bin/run-and-xclip`. A bash helper that runs a command and copies its
2929
## Development Commands
3030

3131
### Testing
32-
The project uses pytest with tox for testing:
32+
The project uses pytest with tox for testing across multiple Python versions (3.8-3.13):
3333

3434
```bash
35-
# Run all tests
36-
tox -e py3
35+
# Run tests on all Python versions
36+
tox
37+
38+
# Run tests on specific Python version
39+
tox -e py313
3740

3841
# Run tests with verbose output
39-
tox -e py3 -- -v
42+
tox -e py313 -- -v
4043

4144
# Run a specific test
42-
tox -e py3 -- tests/test_show_paths.py::test_show_paths_python_file_regex
45+
tox -e py313 -- tests/test_show_paths.py::test_show_paths_color_on
46+
47+
# Run tests on multiple versions
48+
tox -e py38,py310,py312
4349

4450
# Run tests directly with pytest (without tox)
4551
pytest -v
4652
```
4753

54+
**Test Coverage** (22 tests):
55+
- Basic functionality: regex search, line numbers, stdin input
56+
- Format testing: inline vs full-lines format differences
57+
- Color output: verification of ANSI codes with `--color=on`, `--color=off`, `--color=auto`
58+
- Structure parsing: Python nested functions, XML hierarchy, JSON nesting
59+
- Edge cases: missing files, empty files, out-of-range line numbers, case sensitivity
60+
- Help output formatting
61+
4862
**Test Conventions**:
4963
- All tests should be marked with `@pytest.mark.ai_generated` when generated by AI assistants
5064
- Test fixtures are located in `tests/data/` (sample Python, XML, JSON files)
5165
- The `ai_generated` marker is registered in `pyproject.toml` for filtering/identification
66+
- Color tests use `FORCE_COLOR=1` environment variable to test ANSI codes in non-TTY environments
5267

5368
### Code Quality
5469
The project uses pre-commit hooks with the following checks:

tests/test_show_paths.py

Lines changed: 274 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
"""Tests for show-paths utility."""
33

4+
import os
45
import subprocess
56
import sys
67
from pathlib import Path
@@ -12,15 +13,23 @@
1213
SHOW_PATHS = BIN_DIR / "show-paths"
1314
DATA_DIR = Path(__file__).parent / "data"
1415

16+
# ANSI color codes for verification
17+
ANSI_RED = "\x1b[31m"
18+
ANSI_DARK = "\x1b[2m"
19+
ANSI_RESET = "\x1b[0m"
1520

16-
def run_show_paths(*args, input_data=None):
21+
22+
def run_show_paths(*args, input_data=None, env=None):
1723
"""Helper to run show-paths command."""
1824
cmd = [sys.executable, str(SHOW_PATHS)] + list(args)
25+
if env is None:
26+
env = os.environ.copy()
1927
result = subprocess.run(
2028
cmd,
2129
input=input_data,
2230
capture_output=True,
2331
text=True,
32+
env=env,
2433
)
2534
return result
2635

@@ -219,3 +228,267 @@ def test_show_paths_no_matches():
219228
assert result.returncode == 0
220229
# Should return successfully but with no output
221230
assert result.stdout.strip() == ""
231+
232+
233+
@pytest.mark.ai_generated
234+
def test_show_paths_color_on():
235+
"""Test that --color=on adds ANSI color codes to output."""
236+
# Force termcolor to produce colors even without TTY
237+
env = os.environ.copy()
238+
env['FORCE_COLOR'] = '1'
239+
240+
result = run_show_paths(
241+
str(DATA_DIR / "sample.py"),
242+
"-e",
243+
"find_me",
244+
"--color",
245+
"on",
246+
env=env,
247+
)
248+
assert result.returncode == 0
249+
output = result.stdout
250+
251+
# Should contain ANSI color codes
252+
# The matched line should be in red
253+
assert ANSI_RED in output or "\x1b[" in output, "Color codes should be present"
254+
# Should contain the matched content
255+
assert "find_me" in output
256+
257+
258+
@pytest.mark.ai_generated
259+
def test_show_paths_color_on_full_lines():
260+
"""Test that --color=on works with full-lines format."""
261+
# Force termcolor to produce colors even without TTY
262+
env = os.environ.copy()
263+
env['FORCE_COLOR'] = '1'
264+
265+
result = run_show_paths(
266+
str(DATA_DIR / "sample.json"),
267+
"-e",
268+
"target_field",
269+
"-f",
270+
"full-lines",
271+
"--color",
272+
"on",
273+
env=env,
274+
)
275+
assert result.returncode == 0
276+
output = result.stdout
277+
278+
# Should contain color codes for both path lines (dark) and matched line (red)
279+
assert "\x1b[" in output, "Color codes should be present in full-lines mode"
280+
assert "target_field" in output
281+
282+
283+
@pytest.mark.ai_generated
284+
def test_show_paths_color_auto_no_tty():
285+
"""Test that --color=auto produces no colors when not a TTY."""
286+
# When not connected to a TTY, auto should not produce colors
287+
result = run_show_paths(
288+
str(DATA_DIR / "sample.py"),
289+
"-e",
290+
"find_me",
291+
"--color",
292+
"auto",
293+
)
294+
assert result.returncode == 0
295+
output = result.stdout
296+
297+
# Since we're running in subprocess (no TTY), should not have color codes
298+
assert ANSI_RED not in output, "Auto mode should not add colors without TTY"
299+
assert "find_me" in output
300+
301+
302+
@pytest.mark.ai_generated
303+
def test_show_paths_inline_vs_full_lines_difference():
304+
"""Test that inline and full-lines formats produce different output."""
305+
inline_result = run_show_paths(
306+
str(DATA_DIR / "sample.json"),
307+
"-e",
308+
"target_field",
309+
"-f",
310+
"inline",
311+
"--color",
312+
"off",
313+
)
314+
full_result = run_show_paths(
315+
str(DATA_DIR / "sample.json"),
316+
"-e",
317+
"target_field",
318+
"-f",
319+
"full-lines",
320+
"--color",
321+
"off",
322+
)
323+
324+
assert inline_result.returncode == 0
325+
assert full_result.returncode == 0
326+
327+
inline_output = inline_result.stdout
328+
full_output = full_result.stdout
329+
330+
# Both should contain the matched content
331+
assert "target_field" in inline_output
332+
assert "target_field" in full_output
333+
334+
# Full-lines should have more lines (showing the full path)
335+
inline_lines = len(inline_output.strip().split("\n"))
336+
full_lines = len(full_output.strip().split("\n"))
337+
assert full_lines > inline_lines, "Full-lines format should show more lines"
338+
339+
# Inline should show path in dot notation
340+
assert ":" in inline_output # line number:path content
341+
# Full lines should show actual file lines
342+
assert "data" in full_output or "{" in full_output
343+
344+
345+
@pytest.mark.ai_generated
346+
def test_show_paths_path_structure():
347+
"""Test that path structure is correctly identified in nested data."""
348+
result = run_show_paths(
349+
str(DATA_DIR / "sample.json"),
350+
"-e",
351+
"target_field",
352+
"-f",
353+
"inline",
354+
"--color",
355+
"off",
356+
)
357+
assert result.returncode == 0
358+
output = result.stdout
359+
360+
# Should show a structured path leading to target_field
361+
# The path should reflect nesting: data -> items -> properties
362+
lines = output.strip().split("\n")
363+
assert len(lines) > 0
364+
# Check that line number is present
365+
assert any(":" in line for line in lines)
366+
367+
368+
@pytest.mark.ai_generated
369+
def test_show_paths_xml_nested_structure():
370+
"""Test that XML nested structure is correctly parsed."""
371+
result = run_show_paths(
372+
str(DATA_DIR / "sample.xml"),
373+
"-e",
374+
"target_element",
375+
"-f",
376+
"full-lines",
377+
"--color",
378+
"off",
379+
)
380+
assert result.returncode == 0
381+
output = result.stdout
382+
383+
lines = output.strip().split("\n")
384+
# Should show multiple levels of nesting
385+
assert len(lines) > 2, "Should show parent elements in the path"
386+
387+
# Should show the path from root to target
388+
assert "target_element" in output
389+
# Check that XML content has indentation (after line number prefix)
390+
# Lines with increasing indentation indicate nesting
391+
assert "<content>" in output or "<section" in output
392+
assert "<subsection>" in output
393+
394+
395+
@pytest.mark.ai_generated
396+
def test_show_paths_python_nested_functions():
397+
"""Test that Python nested function structure is correctly identified."""
398+
result = run_show_paths(
399+
str(DATA_DIR / "sample.py"),
400+
"-e",
401+
"deeply_nested",
402+
"-f",
403+
"full-lines",
404+
"--color",
405+
"off",
406+
)
407+
assert result.returncode == 0
408+
output = result.stdout
409+
410+
lines = output.strip().split("\n")
411+
# Should show the nesting hierarchy of functions
412+
assert len(lines) > 1, "Should show parent function definitions"
413+
assert "deeply_nested" in output
414+
415+
416+
@pytest.mark.ai_generated
417+
def test_show_paths_missing_file():
418+
"""Test behavior when file doesn't exist."""
419+
result = run_show_paths(
420+
"/nonexistent/file/path.txt",
421+
"-e",
422+
"pattern",
423+
"--color",
424+
"off",
425+
)
426+
# Should fail with non-zero exit code
427+
assert result.returncode != 0
428+
# Should have error message
429+
assert result.stderr or "No such file" in result.stdout
430+
431+
432+
@pytest.mark.ai_generated
433+
def test_show_paths_empty_file():
434+
"""Test behavior with empty file."""
435+
empty_file = DATA_DIR / "empty.txt"
436+
empty_file.write_text("")
437+
438+
try:
439+
result = run_show_paths(
440+
str(empty_file),
441+
"-e",
442+
"pattern",
443+
"--color",
444+
"off",
445+
)
446+
assert result.returncode == 0
447+
# Empty file with no matches should produce no output
448+
assert result.stdout.strip() == ""
449+
finally:
450+
empty_file.unlink()
451+
452+
453+
@pytest.mark.ai_generated
454+
def test_show_paths_line_number_out_of_range():
455+
"""Test behavior when line number is out of range."""
456+
result = run_show_paths(
457+
str(DATA_DIR / "sample.py"),
458+
"-n",
459+
"999999",
460+
"--color",
461+
"off",
462+
)
463+
assert result.returncode == 0
464+
# Should not crash, just return no results
465+
assert result.stdout.strip() == ""
466+
467+
468+
@pytest.mark.ai_generated
469+
def test_show_paths_regex_case_sensitive():
470+
"""Test that regex search is case-sensitive by default."""
471+
# Search for lowercase
472+
result_lower = run_show_paths(
473+
str(DATA_DIR / "sample.py"),
474+
"-e",
475+
"myclass",
476+
"--color",
477+
"off",
478+
)
479+
# Search for correct case
480+
result_correct = run_show_paths(
481+
str(DATA_DIR / "sample.py"),
482+
"-e",
483+
"MyClass",
484+
"--color",
485+
"off",
486+
)
487+
488+
assert result_lower.returncode == 0
489+
assert result_correct.returncode == 0
490+
491+
# Lowercase should find nothing
492+
assert result_lower.stdout.strip() == ""
493+
# Correct case should find the class
494+
assert "MyClass" in result_correct.stdout

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py3
2+
envlist = py3,py{38,39,310,311,312,313}
33
skipsdist = True
44

55
[testenv]

0 commit comments

Comments
 (0)