Skip to content

Commit 04fa01f

Browse files
committed
Add more tests and refactor tui tests
1 parent 7457550 commit 04fa01f

File tree

4 files changed

+230
-122
lines changed

4 files changed

+230
-122
lines changed

tests/test_basic.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
"""Basic tests for datanomy."""
1+
"""Basic package-level tests for datanomy."""
22

33
import datanomy
44

55

66
def test_version() -> None:
7-
"""Test that version is defined."""
7+
"""Test that version is defined and valid."""
88
assert hasattr(datanomy, "__version__")
99
assert isinstance(datanomy.__version__, str)
10+
assert len(datanomy.__version__) > 0
11+
# Should be semver-ish (has dots)
12+
assert "." in datanomy.__version__

tests/test_cli.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Tests for the CLI module."""
2+
3+
from pathlib import Path
4+
from unittest.mock import Mock, patch
5+
6+
from click.testing import CliRunner
7+
8+
from datanomy.cli import main
9+
10+
11+
def test_cli_rejects_non_parquet_extension(tmp_path: Path) -> None:
12+
"""Test that CLI rejects files without .parquet extension."""
13+
bad_file = tmp_path / "test.txt"
14+
bad_file.write_text("not parquet")
15+
16+
runner = CliRunner()
17+
result = runner.invoke(main, [str(bad_file)])
18+
19+
assert result.exit_code == 1
20+
assert "does not appear to be a Parquet file" in result.output
21+
22+
23+
def test_cli_rejects_nonexistent_file() -> None:
24+
"""Test that CLI rejects files that don't exist."""
25+
runner = CliRunner()
26+
result = runner.invoke(main, ["/nonexistent/file.parquet"])
27+
28+
assert result.exit_code == 2
29+
assert "does not exist" in result.output.lower()
30+
31+
32+
@patch("datanomy.cli.DatanomyApp")
33+
def test_cli_launches_app_with_valid_file(mock_app: Mock, simple_parquet: Path) -> None:
34+
"""Test that CLI launches the app with a valid Parquet file."""
35+
# Mock the app to avoid actually running the TUI
36+
mock_app_instance = Mock()
37+
mock_app.return_value = mock_app_instance
38+
39+
runner = CliRunner()
40+
result = runner.invoke(main, [str(simple_parquet)])
41+
42+
# Should have created an app instance
43+
assert mock_app.called
44+
# Should have called run on the app
45+
assert mock_app_instance.run.called
46+
# Should exit successfully
47+
assert result.exit_code == 0
48+
49+
50+
@patch("datanomy.cli.DatanomyApp")
51+
@patch("datanomy.cli.ParquetReader")
52+
def test_cli_creates_reader(
53+
mock_reader: Mock, mock_app: Mock, simple_parquet: Path
54+
) -> None:
55+
"""Test that CLI creates a ParquetReader with the correct file path."""
56+
runner = CliRunner()
57+
runner.invoke(main, [str(simple_parquet)])
58+
59+
# Should have created a reader with the file path
60+
mock_reader.assert_called_once_with(simple_parquet)
61+
62+
63+
def test_cli_case_insensitive_extension(tmp_path: Path) -> None:
64+
"""Test that CLI accepts .PARQUET extension (case insensitive)."""
65+
# Create a valid parquet file with uppercase extension
66+
import pyarrow as pa
67+
import pyarrow.parquet as pq
68+
69+
file_path = tmp_path / "test.PARQUET"
70+
table = pa.table({"id": [1, 2, 3]})
71+
pq.write_table(table, file_path)
72+
73+
with patch("datanomy.cli.DatanomyApp"):
74+
runner = CliRunner()
75+
result = runner.invoke(main, [str(file_path)])
76+
77+
# Should accept uppercase extension
78+
assert result.exit_code == 0

tests/test_reader.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Tests for the ParquetReader module."""
2+
3+
from pathlib import Path
4+
5+
from datanomy.reader import ParquetReader
6+
7+
8+
def test_reader_simple_file(simple_parquet: Path) -> None:
9+
"""Test reader with a simple Parquet file."""
10+
reader = ParquetReader(simple_parquet)
11+
12+
assert reader.file_path == simple_parquet
13+
assert reader.file_path.exists()
14+
15+
assert reader.num_rows == 5
16+
assert reader.num_row_groups == 1
17+
assert len(reader.schema_arrow) == 4
18+
assert reader.file_size > 0
19+
20+
metadata = reader.metadata
21+
assert metadata is not None
22+
assert metadata.num_rows == 5
23+
assert metadata.num_row_groups == 1
24+
25+
26+
def test_reader_multi_row_groups(multi_row_group_parquet: Path) -> None:
27+
"""Test reader with multiple row groups."""
28+
reader = ParquetReader(multi_row_group_parquet)
29+
30+
assert reader.num_rows == 10000
31+
assert reader.num_row_groups == 5
32+
33+
# Check each row group
34+
for i in range(reader.num_row_groups):
35+
rg = reader.get_row_group_info(i)
36+
assert rg.num_rows == 2000
37+
assert rg.total_byte_size > 0
38+
39+
40+
def test_reader_empty_file(empty_parquet: Path) -> None:
41+
"""Test reader with empty Parquet file."""
42+
reader = ParquetReader(empty_parquet)
43+
44+
# file has schema but no rows
45+
assert reader.num_rows == 0
46+
assert reader.num_row_groups == 1
47+
assert len(reader.schema_arrow) == 2
48+
assert reader.file_size > 0
49+
50+
51+
def test_reader_large_schema(large_schema_parquet: Path) -> None:
52+
"""Test reader with many columns."""
53+
reader = ParquetReader(large_schema_parquet)
54+
55+
assert reader.num_rows == 3
56+
assert len(reader.schema_arrow) == 50
57+
58+
# Check all columns are named correctly
59+
field_names = [field.name for field in reader.schema_arrow]
60+
for i in range(50):
61+
assert f"col_{i}" in field_names

tests/test_tui.py

Lines changed: 86 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,100 @@
1-
"""Tests for the TUI module."""
1+
"""Tests for the TUI module.
2+
3+
These are smoke tests to ensure the UI doesn't crash.
4+
We don't test specific text/formatting as that's brittle and changes often.
5+
"""
26

37
from pathlib import Path
4-
from typing import Any, TypedDict
58

69
import pytest
7-
from rich.console import Console
8-
from textual.containers import Container, VerticalScroll
910

1011
from datanomy.reader import ParquetReader
1112
from datanomy.tui import DatanomyApp
1213

1314

14-
class FileDataFixture(TypedDict):
15-
"""Type definition for test file data."""
16-
17-
file_size: str
18-
num_rows: int
19-
num_row_groups: int
20-
schema: dict[str, str]
21-
22-
23-
@pytest.fixture
24-
def file(request: pytest.FixtureRequest) -> Any:
25-
"""Indirect fixture to get other fixtures by name."""
26-
return request.getfixturevalue(request.param)
27-
28-
29-
test_data_fixtures: dict[str, FileDataFixture] = {
30-
"simple.parquet": {
31-
"file_size": "0.00",
32-
"num_rows": 5,
33-
"num_row_groups": 1,
34-
"schema": {
35-
"id": "int64",
36-
"name": "string",
37-
"age": "int64",
38-
"score": "double",
39-
},
40-
},
41-
"multi_row_group.parquet": {
42-
"file_size": "0.11",
43-
"num_rows": 10000,
44-
"num_row_groups": 5,
45-
"schema": {
46-
"id": "int64",
47-
"category": "string",
48-
"value": "int64",
49-
},
50-
},
51-
"complex.parquet": {
52-
"file_size": "0.00",
53-
"num_rows": 3,
54-
"num_row_groups": 1,
55-
"schema": {
56-
"id": "int64",
57-
"data": "struct<x: int64, y: int64>",
58-
"tags": "list<element: string>",
59-
},
60-
},
61-
"empty.parquet": {
62-
"file_size": "0.00",
63-
"num_rows": 0,
64-
"num_row_groups": 1,
65-
"schema": {
66-
"id": "int64",
67-
"name": "string",
68-
},
69-
},
70-
"large_schema.parquet": {
71-
"file_size": "0.01",
72-
"num_rows": 3,
73-
"num_row_groups": 1,
74-
"schema": {f"col_{i}": "int64" for i in range(50)},
75-
},
76-
}
77-
78-
79-
async def check_app_for_file(filename: Path) -> None:
80-
reader = ParquetReader(filename)
15+
@pytest.mark.asyncio
16+
async def test_app_launches_without_crash(simple_parquet: Path) -> None:
17+
"""Test that app launches and runs without crashing."""
18+
reader = ParquetReader(simple_parquet)
19+
app = DatanomyApp(reader)
20+
21+
async with app.run_test():
22+
# If we get here, app launched successfully
23+
assert app is not None
24+
25+
26+
@pytest.mark.asyncio
27+
async def test_app_has_required_widgets(simple_parquet: Path) -> None:
28+
"""Test that all expected widgets are present."""
29+
reader = ParquetReader(simple_parquet)
30+
app = DatanomyApp(reader)
31+
32+
async with app.run_test():
33+
# Verify core widgets exist
34+
assert app.query_one("#file-info") is not None
35+
assert app.query_one("#schema") is not None
36+
assert app.query_one("#row-groups") is not None
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_widgets_render_without_error(simple_parquet: Path) -> None:
41+
"""Test that all widgets can render without throwing exceptions."""
42+
reader = ParquetReader(simple_parquet)
43+
app = DatanomyApp(reader)
44+
45+
async with app.run_test():
46+
# Call render on each widget - will raise if there's an error
47+
file_info = app.query_one("#file-info")
48+
file_info.render()
49+
50+
schema = app.query_one("#schema")
51+
schema.render()
52+
53+
row_groups = app.query_one("#row-groups")
54+
row_groups.render()
55+
56+
57+
@pytest.mark.asyncio
58+
async def test_app_with_empty_file(empty_parquet: Path) -> None:
59+
"""Test that app handles empty Parquet files."""
60+
reader = ParquetReader(empty_parquet)
61+
app = DatanomyApp(reader)
62+
63+
async with app.run_test():
64+
# Should not crash with empty file
65+
app.query_one("#file-info").render()
66+
app.query_one("#schema").render()
67+
app.query_one("#row-groups").render()
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_app_with_complex_schema(complex_schema_parquet: Path) -> None:
72+
"""Test that app handles complex nested schemas."""
73+
reader = ParquetReader(complex_schema_parquet)
8174
app = DatanomyApp(reader)
75+
8276
async with app.run_test():
83-
assert app.title == "DatanomyApp"
84-
console = Console()
85-
file_info_widget = (
86-
app.query_one(VerticalScroll).query_one(Container).query_one("#file-info")
87-
)
88-
with console.capture() as capture:
89-
console.print(file_info_widget.render())
90-
file_info = capture.get()
91-
file_data = test_data_fixtures[filename.name]
92-
assert (
93-
f"File: {filename.name}\nSize: {file_data['file_size']} MB\nRows: {file_data['num_rows']:,}\nRow Groups: {file_data['num_row_groups']}"
94-
in file_info
95-
)
96-
97-
schema_widget = (
98-
app.query_one(VerticalScroll).query_one(Container).query_one("#schema")
99-
)
100-
with console.capture() as capture:
101-
console.print(schema_widget.render())
102-
schema_info = capture.get()
103-
for field, dtype in file_data["schema"].items():
104-
assert f"{field}: {dtype}" in schema_info
105-
106-
row_groups_widget = (
107-
app.query_one(VerticalScroll).query_one(Container).query_one("#row-groups")
108-
)
109-
with console.capture() as capture:
110-
console.print(row_groups_widget.render())
111-
row_groups_info = capture.get()
112-
for i in range(file_data["num_row_groups"]):
113-
assert (
114-
f"Row Group {i}: {int(file_data['num_rows']) // int(file_data['num_row_groups']):,} rows"
115-
in row_groups_info
116-
)
77+
# Should handle nested types without crashing
78+
app.query_one("#schema").render()
11779

11880

11981
@pytest.mark.asyncio
120-
@pytest.mark.parametrize(
121-
"file",
122-
[
123-
"simple_parquet",
124-
"multi_row_group_parquet",
125-
"complex_schema_parquet",
126-
"empty_parquet",
127-
"large_schema_parquet",
128-
],
129-
indirect=True,
130-
)
131-
async def test_containers_with_files(
132-
file: Path,
133-
) -> None:
134-
await check_app_for_file(file)
82+
async def test_app_with_many_columns(large_schema_parquet: Path) -> None:
83+
"""Test that app handles files with many columns."""
84+
reader = ParquetReader(large_schema_parquet)
85+
app = DatanomyApp(reader)
86+
87+
async with app.run_test():
88+
# Should handle large schema without crashing
89+
app.query_one("#schema").render()
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_app_with_multiple_row_groups(multi_row_group_parquet: Path) -> None:
94+
"""Test that app handles multiple row groups."""
95+
reader = ParquetReader(multi_row_group_parquet)
96+
app = DatanomyApp(reader)
97+
98+
async with app.run_test():
99+
# Should handle multiple row groups without crashing
100+
app.query_one("#row-groups").render()

0 commit comments

Comments
 (0)