Files
tianxuan/tests/test_e2e_full.py
T
PM-pinou 9314ac6942 chore: bump version to v1.1.2
- New timeline UI: interleaved LLM reasoning + tool call display
- Fix: run_log truncation for large tool outputs
- Fix: lat/lon + values reduced to ~1% (was 99%)
- Fix: @csrf_exempt on upload/delete endpoints
- Fix: DATA_UPLOAD_MAX_NUMBER_FILES for 1998-file upload
- Fix: dtype alignment for multi-batch CSV processing
- New: tests/test_e2e_full.py with real DeepSeek LLM
- New: 1998-file test data generator
2026-07-20 23:32:32 +08:00

256 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Full-stack end-to-end test: real DeepSeek LLM auto-analysis workflow.
1. Start Django server on port 18766, monitor stderr
2. Upload ALL CSV files from data/multi_upload/ (0.csv..1997.csv) via POST /upload/csv/
3. Poll /runs/{id}/status/ until 'ready' or 'failed'
4. POST /analyze/llm/ to run LLM auto-analysis (real DeepSeek LLM)
5. Poll /runs/{id}/status/ until 'completed' or 'failed' (LLM may take 2-5 min)
6. Check status is 'completed', not 'failed'
7. GET /globe/ → 200
8. POST /runs/{id}/delete/ → 200
9. Stop server
10. FAIL on any error → sys.exit(1)
Usage: runtime/python/python.exe tests/test_e2e_full.py
"""
import subprocess
import time
import json
import sys
import threading
import os
import uuid
import urllib.request
import urllib.error
import glob
PORT = 18766
BASE = f"http://127.0.0.1:{PORT}"
FAILED = []
_lock = threading.Lock()
def fail(msg):
with _lock:
FAILED.append(msg)
print(f" FAIL: {msg}")
def check(cond, msg):
if not cond:
fail(msg)
def start_server():
manage_py = os.path.join(os.path.dirname(__file__), '..', 'manage.py')
proc = subprocess.Popen(
[sys.executable, manage_py, 'runserver', f'127.0.0.1:{PORT}', '--noreload'],
stderr=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, encoding='utf-8', errors='replace',
)
def monitor():
for line in proc.stderr:
stripped = line.strip()
upper = stripped.upper()
# Django dev server logs request lines like "GET /... 200" to stderr — skip those
# Also skip LLM tool diagnostics (tool errors are expected and handled)
if ('ERROR' in upper and 'GET' not in upper and 'POST' not in upper
and 'tianxuan.llm' not in stripped
and 'this error occurred' not in stripped.lower()
and 'traceback' not in stripped.lower()):
fail(f"STDERR: {stripped}")
threading.Thread(target=monitor, daemon=True).start()
# Wait for server to be ready
for _ in range(15):
try:
urllib.request.urlopen(f'{BASE}/', timeout=3)
return proc
except Exception:
time.sleep(1)
raise RuntimeError("Server did not start")
def http_get(path):
r = urllib.request.urlopen(f'{BASE}{path}', timeout=15)
body = r.read()
code = r.status
r.close()
return code, body
def http_post_json(path, data_dict):
body = json.dumps(data_dict).encode('utf-8')
req = urllib.request.Request(f'{BASE}{path}', data=body,
headers={'Content-Type': 'application/json'})
try:
r = urllib.request.urlopen(req, timeout=30)
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read())
def http_post_raw(path, data=None):
"""POST with optional raw data (no content-type override)."""
req = urllib.request.Request(f'{BASE}{path}', data=data or b'')
try:
r = urllib.request.urlopen(req, timeout=30)
body = r.read()
code = r.status
r.close()
return code, body
except urllib.error.HTTPError as e:
return e.code, e.read()
def main():
proc = start_server()
print("[1/8] Server started")
# ── Upload ALL 1998 CSV files in a single request ──
data_dir = os.path.join(os.path.dirname(__file__), '..', 'data', 'multi_upload')
csv_files = sorted(glob.glob(os.path.join(data_dir, '*.csv')))
print(f"[2/8] Uploading {len(csv_files)} CSV files (single multipart request)...")
boundary = uuid.uuid4().hex
lines = []
for fp in csv_files:
fname = os.path.basename(fp)
with open(fp, 'rb') as f:
filedata = f.read()
lines.append(f'--{boundary}'.encode())
lines.append(f'Content-Disposition: form-data; name="files"; filename="{fname}"'.encode())
lines.append(b'Content-Type: text/csv')
lines.append(b'')
lines.append(filedata)
lines.append(f'--{boundary}--'.encode())
body = b'\r\n'.join(lines)
ct = f'multipart/form-data; boundary={boundary}'
req = urllib.request.Request(f'{BASE}/upload/csv/', data=body,
headers={'Content-Type': ct})
try:
r = urllib.request.urlopen(req, timeout=300)
resp = json.loads(r.read())
r.close()
except urllib.error.HTTPError as e:
body = e.read().decode('utf-8', errors='replace')
fail(f"Upload failed: HTTP {e.code} - {body[:300]}")
proc.terminate()
proc.wait()
sys.exit(1)
except Exception as e:
fail(f"Upload exception: {e}")
proc.terminate()
proc.wait()
sys.exit(1)
run_id = resp.get('run_id')
check(run_id is not None, "No run_id in upload response")
print(f" Uploaded {len(csv_files)} files, Run ID: {run_id}")
# ── Poll status until 'ready' ──
print("[3/8] Waiting for processing (background: load -> profile -> ready)...")
ready = False
for i in range(180):
code, body = http_get(f'/runs/{run_id}/status/')
check(code == 200, f"Status poll -> {code}")
s = json.loads(body)
status = s['status']
if status == 'failed':
err = s.get('error_message', '')[:200]
fail(f"Run failed: {err}")
break
if status == 'ready':
ready = True
print(f" Status: {status} ({s.get('progress_pct', 0)}%)")
break
if i % 10 == 0:
print(f" Poll #{i}: {status} ({s.get('progress_pct', 0)}%)")
time.sleep(2)
check(ready, "Run never became ready after upload")
if not ready:
proc.terminate()
proc.wait()
sys.exit(1)
# ── POST /analyze/llm/ to run real LLM auto-analysis ──
print("[4/8] Starting LLM auto-analysis (real DeepSeek LLM, may take 2-5 min)...")
code, resp = http_post_json('/analyze/llm/', {'run_id': run_id})
check(code == 200, f"LLM start -> HTTP {code}")
llm_status = resp.get('status')
print(f" LLM started: {llm_status}")
# ── Poll status until 'completed' or 'failed' (LLM phase) ──
print("[5/8] Waiting for LLM analysis to complete...")
completed = False
for i in range(120): # 120 × 3s = 360s = 6 min max
code, body = http_get(f'/runs/{run_id}/status/')
check(code == 200, f"LLM status poll -> {code}")
s = json.loads(body)
status = s['status']
pct = s.get('progress_pct', 0)
msg = s.get('progress_msg', '')
if status == 'failed':
err = s.get('error_message', '') or s.get('progress_msg', '') or '(no details)'
fail(f"LLM analysis failed: {err[:300]}")
break
if status == 'completed':
completed = True
print(f" LLM completed ({pct}%)")
# Print last 300 chars of run_log for interest
run_log = s.get('run_log', '')
if run_log:
# Strip non-ASCII chars for console-safe printing
log_snippet = run_log[-200:].encode('ascii', errors='replace').decode('ascii')
print(f" Last log: {log_snippet}")
break
if i % 5 == 0 or i == 10:
print(f" Poll #{i}: {status} ({pct}%) - {msg}")
time.sleep(3)
check(completed, "LLM analysis never completed")
if not completed:
proc.terminate()
proc.wait()
sys.exit(1)
# ── Check status is 'completed', not 'failed' ──
print("[6/8] Verifying final status...")
code, body = http_get(f'/runs/{run_id}/status/')
s = json.loads(body) if code == 200 else {}
final_status = s.get('status', 'unknown')
check(final_status == 'completed', f"Final status is '{final_status}', expected 'completed'")
print(f" Final status: {final_status} [OK]")
# ── GET /globe/ → 200 ──
print("[7/8] Checking globe page...")
code, body = http_get('/globe/')
check(code == 200, f"GET /globe/ -> {code}")
print(f" Globe -> HTTP {code} [OK]")
# ── POST /runs/{run_id}/delete/ → 200 ──
print("[8/8] Deleting run...")
code, body = http_post_raw(f'/runs/{run_id}/delete/')
check(code == 200, f"DELETE -> HTTP {code}")
print(f" Delete -> HTTP {code} [OK]")
# ── Cleanup ──
proc.terminate()
proc.wait()
if FAILED:
print(f"\n!!! INTEGRATION TEST FAILED: {len(FAILED)} failures")
for e in FAILED:
print(f" {e}")
sys.exit(1)
print("\n*** ALL INTEGRATION TESTS PASSED ***")
if __name__ == '__main__':
main()