""" 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()