Files
codetutor/frontend/src/components/editor/test-runner.tsx
T

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>
);
}