log4lab 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- log4lab/__init__.py +1 -0
- log4lab/cli.py +64 -0
- log4lab/server.py +155 -0
- log4lab/tail.py +283 -0
- log4lab/templates/index.html +579 -0
- log4lab/templates/runs.html +182 -0
- log4lab-0.1.0.dist-info/METADATA +338 -0
- log4lab-0.1.0.dist-info/RECORD +14 -0
- log4lab-0.1.0.dist-info/WHEEL +5 -0
- log4lab-0.1.0.dist-info/entry_points.txt +2 -0
- log4lab-0.1.0.dist-info/licenses/LICENSE +21 -0
- log4lab-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_server.py +214 -0
tests/test_server.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Tests for Log4Lab server endpoints."""
|
|
2
|
+
import json
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from fastapi.testclient import TestClient
|
|
8
|
+
|
|
9
|
+
from log4lab import server
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def temp_log_file():
|
|
14
|
+
"""Create a temporary log file with test data."""
|
|
15
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
|
16
|
+
# Write test log entries
|
|
17
|
+
logs = [
|
|
18
|
+
{
|
|
19
|
+
"time": "2025-10-24T10:00:00Z",
|
|
20
|
+
"level": "INFO",
|
|
21
|
+
"section": "test",
|
|
22
|
+
"message": "First log entry",
|
|
23
|
+
"run_name": "test_run",
|
|
24
|
+
"run_id": "run_001",
|
|
25
|
+
"group": "test_group"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"time": "2025-10-24T10:05:00Z",
|
|
29
|
+
"level": "ERROR",
|
|
30
|
+
"section": "test",
|
|
31
|
+
"message": "Error log entry",
|
|
32
|
+
"run_name": "test_run",
|
|
33
|
+
"run_id": "run_001"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"time": "2025-10-24T11:00:00Z",
|
|
37
|
+
"level": "INFO",
|
|
38
|
+
"section": "another",
|
|
39
|
+
"message": "Different run",
|
|
40
|
+
"run_name": "test_run",
|
|
41
|
+
"run_id": "run_002"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"time": "2025-10-24T12:00:00Z",
|
|
45
|
+
"level": "DEBUG",
|
|
46
|
+
"section": "debug",
|
|
47
|
+
"message": "Debug message",
|
|
48
|
+
"run_name": "another_run",
|
|
49
|
+
"run_id": "run_003"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
for log in logs:
|
|
53
|
+
f.write(json.dumps(log) + '\n')
|
|
54
|
+
temp_path = Path(f.name)
|
|
55
|
+
|
|
56
|
+
yield temp_path
|
|
57
|
+
|
|
58
|
+
# Cleanup
|
|
59
|
+
temp_path.unlink()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.fixture
|
|
63
|
+
def client(temp_log_file):
|
|
64
|
+
"""Create a test client with a temporary log file."""
|
|
65
|
+
server.set_log_path(temp_log_file)
|
|
66
|
+
return TestClient(server.app)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_index_page(client):
|
|
70
|
+
"""Test that the index page loads successfully."""
|
|
71
|
+
response = client.get("/")
|
|
72
|
+
assert response.status_code == 200
|
|
73
|
+
assert "Log4Lab" in response.text
|
|
74
|
+
assert "Live" in response.text
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_runs_page(client):
|
|
78
|
+
"""Test that the runs index page loads successfully."""
|
|
79
|
+
response = client.get("/runs")
|
|
80
|
+
assert response.status_code == 200
|
|
81
|
+
assert "Log4Lab" in response.text
|
|
82
|
+
assert "Runs Index" in response.text
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_api_runs_endpoint(client, temp_log_file):
|
|
86
|
+
"""Test the /api/runs endpoint returns correct data structure."""
|
|
87
|
+
response = client.get("/api/runs")
|
|
88
|
+
assert response.status_code == 200
|
|
89
|
+
|
|
90
|
+
data = response.json()
|
|
91
|
+
assert "runs" in data
|
|
92
|
+
|
|
93
|
+
# Check that we have the expected run names
|
|
94
|
+
assert "test_run" in data["runs"]
|
|
95
|
+
assert "another_run" in data["runs"]
|
|
96
|
+
|
|
97
|
+
# Check test_run structure
|
|
98
|
+
test_run = data["runs"]["test_run"]
|
|
99
|
+
assert "total" in test_run
|
|
100
|
+
assert "run_ids" in test_run
|
|
101
|
+
assert test_run["total"] == 3 # Three entries for test_run
|
|
102
|
+
|
|
103
|
+
# Check run_ids structure
|
|
104
|
+
assert len(test_run["run_ids"]) == 2 # run_001 and run_002
|
|
105
|
+
|
|
106
|
+
run_id_001 = next(r for r in test_run["run_ids"] if r["run_id"] == "run_001")
|
|
107
|
+
assert run_id_001["count"] == 2
|
|
108
|
+
assert run_id_001["earliest"] == "2025-10-24T10:00:00Z"
|
|
109
|
+
assert run_id_001["latest"] == "2025-10-24T10:05:00Z"
|
|
110
|
+
|
|
111
|
+
run_id_002 = next(r for r in test_run["run_ids"] if r["run_id"] == "run_002")
|
|
112
|
+
assert run_id_002["count"] == 1
|
|
113
|
+
assert run_id_002["earliest"] == "2025-10-24T11:00:00Z"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_api_runs_empty_file():
|
|
117
|
+
"""Test /api/runs with an empty log file."""
|
|
118
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
|
119
|
+
temp_path = Path(f.name)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
server.set_log_path(temp_path)
|
|
123
|
+
client = TestClient(server.app)
|
|
124
|
+
|
|
125
|
+
response = client.get("/api/runs")
|
|
126
|
+
assert response.status_code == 200
|
|
127
|
+
|
|
128
|
+
data = response.json()
|
|
129
|
+
assert data["runs"] == {}
|
|
130
|
+
finally:
|
|
131
|
+
temp_path.unlink()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_api_runs_no_run_fields():
|
|
135
|
+
"""Test /api/runs with logs that don't have run_name/run_id fields."""
|
|
136
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
|
137
|
+
logs = [
|
|
138
|
+
{"time": "2025-10-24T10:00:00Z", "level": "INFO", "message": "No run info"},
|
|
139
|
+
{"time": "2025-10-24T10:05:00Z", "level": "ERROR", "message": "Another entry"}
|
|
140
|
+
]
|
|
141
|
+
for log in logs:
|
|
142
|
+
f.write(json.dumps(log) + '\n')
|
|
143
|
+
temp_path = Path(f.name)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
server.set_log_path(temp_path)
|
|
147
|
+
client = TestClient(server.app)
|
|
148
|
+
|
|
149
|
+
response = client.get("/api/runs")
|
|
150
|
+
assert response.status_code == 200
|
|
151
|
+
|
|
152
|
+
data = response.json()
|
|
153
|
+
assert data["runs"] == {}
|
|
154
|
+
finally:
|
|
155
|
+
temp_path.unlink()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_cache_file_serving(client, temp_log_file):
|
|
159
|
+
"""Test serving cache files from the log directory."""
|
|
160
|
+
# Create a test file in the same directory as the log file
|
|
161
|
+
log_dir = temp_log_file.parent
|
|
162
|
+
test_file = log_dir / "test_artifact.txt"
|
|
163
|
+
test_file.write_text("Test artifact content")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
response = client.get("/cache/test_artifact.txt")
|
|
167
|
+
assert response.status_code == 200
|
|
168
|
+
assert response.text == "Test artifact content"
|
|
169
|
+
finally:
|
|
170
|
+
test_file.unlink()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_cache_file_not_found(client):
|
|
174
|
+
"""Test that non-existent cache files return 404."""
|
|
175
|
+
response = client.get("/cache/nonexistent.txt")
|
|
176
|
+
assert response.status_code == 404
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_cache_file_path_traversal_protection(client):
|
|
180
|
+
"""Test that path traversal attacks are blocked."""
|
|
181
|
+
response = client.get("/cache/../../../etc/passwd")
|
|
182
|
+
# Should return an error code (403, 400, or 404 if path doesn't resolve)
|
|
183
|
+
assert response.status_code in [403, 400, 404]
|
|
184
|
+
# Most importantly, should NOT return 200
|
|
185
|
+
assert response.status_code != 200
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_get_set_log_path(temp_log_file):
|
|
189
|
+
"""Test the get_log_path and set_log_path functions."""
|
|
190
|
+
server.set_log_path(temp_log_file)
|
|
191
|
+
assert server.get_log_path() == temp_log_file
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_invalid_json_lines(client):
|
|
195
|
+
"""Test that invalid JSON lines are skipped gracefully."""
|
|
196
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.jsonl', delete=False) as f:
|
|
197
|
+
f.write('{"valid": "json"}\n')
|
|
198
|
+
f.write('invalid json line\n')
|
|
199
|
+
f.write('{"another": "valid", "run_name": "test", "run_id": "1"}\n')
|
|
200
|
+
temp_path = Path(f.name)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
server.set_log_path(temp_path)
|
|
204
|
+
client = TestClient(server.app)
|
|
205
|
+
|
|
206
|
+
response = client.get("/api/runs")
|
|
207
|
+
assert response.status_code == 200
|
|
208
|
+
|
|
209
|
+
data = response.json()
|
|
210
|
+
# Should only count the valid entries with run info
|
|
211
|
+
assert len(data["runs"]) == 1
|
|
212
|
+
assert "test" in data["runs"]
|
|
213
|
+
finally:
|
|
214
|
+
temp_path.unlink()
|