246 lines
6.7 KiB
TypeScript
246 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback } from "react";
|
|
import { Play, Send, Loader2 } from "lucide-react";
|
|
import { usePyodide } from "@/hooks/use-pyodide";
|
|
import { submitSolution } from "@/lib/api";
|
|
import { TestResults } from "./test-results";
|
|
import type {
|
|
VisibleTestCase,
|
|
HiddenTestInput,
|
|
TestResult,
|
|
HiddenTestOutput,
|
|
} from "@/types";
|
|
|
|
interface TestRunnerProps {
|
|
code: string;
|
|
functionName: string;
|
|
slug: string;
|
|
visibleTestCases: VisibleTestCase[];
|
|
hiddenTestInputs: HiddenTestInput[];
|
|
}
|
|
|
|
interface ExecutionResult {
|
|
test_id: number;
|
|
output: unknown;
|
|
error?: string;
|
|
}
|
|
|
|
export function TestRunner({
|
|
code,
|
|
functionName,
|
|
slug,
|
|
visibleTestCases,
|
|
hiddenTestInputs,
|
|
}: TestRunnerProps) {
|
|
const { pyodide, loading: pyodideLoading, runPython } = usePyodide();
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [visibleResults, setVisibleResults] = useState<TestResult[]>([]);
|
|
const [hiddenPassedCount, setHiddenPassedCount] = useState(0);
|
|
const [hiddenTotalCount, setHiddenTotalCount] = useState(0);
|
|
|
|
const generateTestHarness = useCallback(
|
|
(tests: Array<{ id: number; input: Record<string, unknown> }>) => {
|
|
const testsJson = JSON.stringify(tests);
|
|
return `
|
|
import json
|
|
|
|
${code}
|
|
|
|
__tests__ = json.loads('${testsJson.replace(/'/g, "\\'")}')
|
|
__results__ = []
|
|
|
|
for test in __tests__:
|
|
try:
|
|
result = ${functionName}(**test["input"])
|
|
__results__.append({"test_id": test["id"], "output": result})
|
|
except Exception as e:
|
|
__results__.append({"test_id": test["id"], "output": None, "error": str(e)})
|
|
|
|
json.dumps(__results__)
|
|
`;
|
|
},
|
|
[code, functionName]
|
|
);
|
|
|
|
const runTests = useCallback(
|
|
async (tests: Array<{ id: number; input: Record<string, unknown> }>) => {
|
|
const harness = generateTestHarness(tests);
|
|
const { output, error } = await runPython(harness);
|
|
|
|
if (error) {
|
|
return tests.map((t) => ({
|
|
test_id: t.id,
|
|
output: null,
|
|
error: error,
|
|
}));
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(output as string) as ExecutionResult[];
|
|
} catch {
|
|
return tests.map((t) => ({
|
|
test_id: t.id,
|
|
output: null,
|
|
error: "Failed to parse output",
|
|
}));
|
|
}
|
|
},
|
|
[generateTestHarness, runPython]
|
|
);
|
|
|
|
const handleRunTests = useCallback(async () => {
|
|
if (!pyodide || isRunning) return;
|
|
|
|
setIsRunning(true);
|
|
setHiddenPassedCount(0);
|
|
setHiddenTotalCount(0);
|
|
|
|
try {
|
|
const tests = visibleTestCases.map((tc) => ({
|
|
id: tc.id,
|
|
input: tc.input,
|
|
}));
|
|
const results = await runTests(tests);
|
|
|
|
const testResults: TestResult[] = results.map((r, idx) => {
|
|
const testCase = visibleTestCases[idx];
|
|
const passed =
|
|
!r.error &&
|
|
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
|
|
|
|
return {
|
|
test_id: r.test_id,
|
|
passed,
|
|
input: testCase.input,
|
|
expected: testCase.expected,
|
|
actual: r.output,
|
|
error: r.error,
|
|
};
|
|
});
|
|
|
|
setVisibleResults(testResults);
|
|
} finally {
|
|
setIsRunning(false);
|
|
}
|
|
}, [pyodide, isRunning, visibleTestCases, runTests]);
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!pyodide || isSubmitting) return;
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
// Run visible tests first
|
|
const visibleTests = visibleTestCases.map((tc) => ({
|
|
id: tc.id,
|
|
input: tc.input,
|
|
}));
|
|
const visibleExecutionResults = await runTests(visibleTests);
|
|
|
|
const visibleTestResults: TestResult[] = visibleExecutionResults.map(
|
|
(r, idx) => {
|
|
const testCase = visibleTestCases[idx];
|
|
const passed =
|
|
!r.error &&
|
|
JSON.stringify(r.output) === JSON.stringify(testCase.expected);
|
|
|
|
return {
|
|
test_id: r.test_id,
|
|
passed,
|
|
input: testCase.input,
|
|
expected: testCase.expected,
|
|
actual: r.output,
|
|
error: r.error,
|
|
};
|
|
}
|
|
);
|
|
|
|
setVisibleResults(visibleTestResults);
|
|
|
|
// Run hidden tests
|
|
const hiddenTests = hiddenTestInputs.map((tc) => ({
|
|
id: tc.id,
|
|
input: tc.input,
|
|
}));
|
|
const hiddenExecutionResults = await runTests(hiddenTests);
|
|
|
|
// Submit hidden outputs to server for validation
|
|
const hiddenOutputs: HiddenTestOutput[] = hiddenExecutionResults.map(
|
|
(r) => ({
|
|
test_id: r.test_id,
|
|
output: r.output,
|
|
})
|
|
);
|
|
|
|
const response = await submitSolution(slug, {
|
|
code,
|
|
hidden_outputs: hiddenOutputs,
|
|
});
|
|
|
|
setHiddenPassedCount(response.total_passed);
|
|
setHiddenTotalCount(response.total_tests);
|
|
} catch (error) {
|
|
console.error("Submission error:", error);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [
|
|
pyodide,
|
|
isSubmitting,
|
|
visibleTestCases,
|
|
hiddenTestInputs,
|
|
runTests,
|
|
slug,
|
|
code,
|
|
]);
|
|
|
|
const isLoading = isRunning || isSubmitting;
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center gap-2 px-3 py-2 bg-[var(--secondary)] border-b border-[var(--border)]">
|
|
<button
|
|
onClick={handleRunTests}
|
|
disabled={pyodideLoading || isLoading}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded bg-[var(--muted)] hover:bg-[var(--muted)]/80 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{isRunning ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Play className="h-4 w-4" />
|
|
)}
|
|
Run Tests
|
|
</button>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={pyodideLoading || isLoading}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded bg-green-600 hover:bg-green-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{isSubmitting ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Send className="h-4 w-4" />
|
|
)}
|
|
Submit
|
|
</button>
|
|
{pyodideLoading && (
|
|
<span className="text-sm text-[var(--muted-foreground)] ml-2">
|
|
Loading Python runtime...
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 overflow-auto">
|
|
<TestResults
|
|
visibleResults={visibleResults}
|
|
hiddenPassedCount={hiddenPassedCount}
|
|
hiddenTotalCount={hiddenTotalCount}
|
|
isRunning={isLoading}
|
|
visibleTestCases={visibleTestCases}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|