Skip to content

Commit 3dd0118

Browse files
committed
feat(270): Connect frontend to test runners
1 parent 23c76a0 commit 3dd0118

8 files changed

Lines changed: 167 additions & 75 deletions

File tree

src/app/(web)/crm/task-templates/[id]/edit/page.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Button } from '@/lib/components/ui/Button';
1010
import TaskEditorSidebar from '@/lib/components/core/TaskEditorSidebar';
1111
import Breadcrumbs from '@/lib/components/core/Breadcrumbs';
1212
import useTestRunner from '@/lib/hooks/useTestRunner';
13+
import { type TestCaseDTO } from '@/lib/schemas/task-template.schema';
14+
import { type ProgrammingLanguage } from '@/generated/prisma';
1315

1416
export default function TaskTemplateEditPage({ params }: { params: Promise<{ id: string }> }) {
1517
const { id } = use(params);
@@ -46,10 +48,14 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
4648
generateStubsForLanguages,
4749
getEditorContent,
4850
} = useTaskTemplateEditPage(id);
49-
const { runEditPageTests } = useTestRunner(
50-
getEditorContent(),
51-
languages ? languages[selectedLanguage].language : 'python' // UHHHH
52-
);
51+
const { runEditPageTests } = useTestRunner();
52+
53+
const handleRunTests = (tests: TestCaseDTO[]) => {
54+
const code = getEditorContent();
55+
const language = languages?.[selectedLanguage]?.language;
56+
if (!language) return;
57+
runEditPageTests(tests, code, language as ProgrammingLanguage);
58+
};
5359

5460
if (isLoading) {
5561
return (
@@ -139,7 +145,7 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
139145
setPublicTestCases={setPublicTestCases}
140146
privateTestCases={privateTestCases}
141147
setPrivateTestCases={setPrivateTestCases}
142-
runTests={runEditPageTests}
148+
runTests={handleRunTests}
143149
isSaving={isSaving}
144150
/>
145151
</div>

src/app/api/runner/[taskTemplateId]/route.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
import { type NextRequest } from 'next/server';
12
import judge0Connector, {
3+
formatJudgeResult,
24
type JudgeSubmissionRequestBody,
35
} from '@/lib/connectors/judge0.connector';
46
import { SubmissionSchema } from '@/lib/schemas/submission.schema';
57
import TaskTemplateService from '@/lib/services/task-template.service';
6-
// import { getSession } from '@/lib/utils/auth.utils';
78
import { handleError } from '@/lib/utils/errors.utils';
89
import { mapLanguageToJudge } from '@/lib/utils/language.utils';
9-
import { type NextRequest } from 'next/server';
1010

1111
export async function POST(
1212
request: NextRequest,
@@ -19,9 +19,9 @@ export async function POST(
1919
const languageId = mapLanguageToJudge(parsed.language);
2020
const taskTemplate = await TaskTemplateService.getTaskTemplate(
2121
taskTemplateId,
22-
'TODO: REPLACE THIS LATER LAITH WITH COOKIE'
22+
'org_nextlab_001' // lmk if we want this to stay as the TODO
2323
);
24-
const tests = [...taskTemplate.privateTestCases, ...taskTemplate.publicTestCases];
24+
const tests = [...taskTemplate.publicTestCases, ...taskTemplate.privateTestCases];
2525
const formatted: JudgeSubmissionRequestBody[] = tests.map((test) => ({
2626
source_code: parsed.code,
2727
language_id: languageId,
@@ -30,9 +30,11 @@ export async function POST(
3030
}));
3131

3232
const result = await judge0Connector.executeSubmissions(formatted);
33+
const formattedResults = result.map(formatJudgeResult);
34+
3335
return Response.json({
34-
data: result,
35-
status: 200,
36+
data: formattedResults,
37+
status: 201,
3638
});
3739
} catch (err) {
3840
return handleError(err);

src/app/api/runner/route.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { handleError } from '@/lib/utils/errors.utils';
22
import { assertRecruiterOrAbove } from '@/lib/utils/permissions.utils';
33
import { type NextRequest } from 'next/server';
44
import judge0Connector, {
5+
formatJudgeResult,
56
type JudgeSubmissionRequestBody,
67
} from '@/lib/connectors/judge0.connector';
78
import { TestSubmissionSchema } from '@/lib/schemas/submission.schema';
@@ -21,10 +22,11 @@ export async function POST(request: NextRequest) {
2122
}));
2223

2324
const result = await judge0Connector.executeSubmissions(formatted);
25+
const formattedResults = result.map(formatJudgeResult);
2426

2527
return Response.json({
26-
data: result,
27-
status: 200,
28+
data: formattedResults,
29+
status: 201,
2830
});
2931
} catch (err) {
3032
return handleError(err);

src/lib/api/runner.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import {
22
type SubmissionSchemaDTO,
33
type TestSubmissionSchemaDTO,
44
} from '@/lib/schemas/submission.schema';
5-
import { type JudgeResultRequestBody } from '@/lib/connectors/judge0.connector';
5+
import type { CandidateTestResult } from '@/lib/types/candidate-assessment.types';
66

77
export async function runEditorSubmission(
88
submission: TestSubmissionSchemaDTO
9-
): Promise<JudgeResultRequestBody> {
9+
): Promise<CandidateTestResult[]> {
1010
const result = await fetch(`/api/runner`, {
1111
method: 'POST',
1212
headers: {
@@ -27,7 +27,7 @@ export async function runEditorSubmission(
2727
export async function runAssessmentSubmission(
2828
taskTemplateId: string,
2929
submission: SubmissionSchemaDTO
30-
): Promise<JudgeResultRequestBody> {
30+
): Promise<CandidateTestResult[]> {
3131
const result = await fetch(`/api/runner/${taskTemplateId}`, {
3232
method: 'POST',
3333
headers: {

src/lib/connectors/judge0.connector.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BadRequestException, InternalServerException } from '@/lib/utils/errors.utils';
22
import { sleep } from '@/lib/utils/utils';
3+
import type { CandidateTestResult } from '@/lib/types/candidate-assessment.types';
34

45
export interface JudgeSubmissionRequestBody {
56
source_code: string;
@@ -19,6 +20,15 @@ export interface JudgeResultRequestBody {
1920
token: string;
2021
}
2122

23+
export function formatJudgeResult(result: JudgeResultRequestBody): CandidateTestResult {
24+
return {
25+
stdout: result.stdout,
26+
stderr: result.stderr,
27+
description: result.status?.description,
28+
statusId: result.status?.id,
29+
};
30+
}
31+
2232
class Judge0Connector {
2333
private headers: HeadersInit;
2434
private fields: string[];
@@ -193,6 +203,13 @@ class Judge0Connector {
193203
): Promise<JudgeResultRequestBody[]> {
194204
const tokens = await this.registerSubmissions(submissions);
195205
const { allResults } = await this.waitForSubmissions(tokens, totalTimeout);
206+
const tokenIndexMap = new Map(tokens.map((token, index) => [token, index]));
207+
208+
// Sort results by their original submission order (this assumes judge0 returns the tokens in the right order)\
209+
// in my practice this is true but I am unsure
210+
allResults.sort(
211+
(a, b) => (tokenIndexMap.get(a.token) ?? 0) - (tokenIndexMap.get(b.token) ?? 0)
212+
);
196213
return allResults;
197214
}
198215
}

src/lib/hooks/useAssessment.ts

Lines changed: 84 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import { type Monaco } from '@monaco-editor/react';
66
import { toast } from 'sonner';
77
import { getCandidateAssessment, submitCandidateAssessment } from '@/lib/api/candidate-assessment';
88
import { useAssessmentTimer } from '@/lib/hooks/useAssessmentTimer';
9+
import useTestRunner from '@/lib/hooks/useTestRunner';
910
import type {
1011
AssessmentPhase,
1112
AssessmentQuestion,
1213
CandidateAssessment,
1314
OutroReason,
1415
SectionState,
16+
TestCaseResultStatus,
1517
} from '@/lib/types/candidate-assessment.types';
1618
import { createToken } from '@/lib/api/token';
19+
import { type ProgrammingLanguage } from '@/generated/prisma';
1720

1821
function buildInitialSections(questions: AssessmentQuestion[]): SectionState[] {
1922
return questions.map((q, i) => {
@@ -52,6 +55,71 @@ export default function useAssessment(assessmentId: string) {
5255
currentSectionIndexRef.current = currentSectionIndex;
5356
}, [currentSectionIndex]);
5457

58+
const {
59+
error: testError,
60+
loading: testLoading,
61+
output: testOutput,
62+
runAssessmentTests,
63+
reset: resetTests,
64+
} = useTestRunner();
65+
66+
useEffect(() => {
67+
if (testLoading) {
68+
setSections((prev) =>
69+
prev.map((section) => ({
70+
...section,
71+
testCaseResults: section.testCaseResults.map((test) => ({
72+
...test,
73+
status: 'loading',
74+
})),
75+
}))
76+
);
77+
} else if (testError) {
78+
setSections((prev) =>
79+
prev.map((section) => ({
80+
...section,
81+
testCaseResults: section.testCaseResults.map((test) => ({
82+
...test,
83+
status: 'runtime_error',
84+
actualOutput: 'An error occurred while running tests.',
85+
})),
86+
}))
87+
);
88+
} else if (testOutput) {
89+
setSections((prev) =>
90+
prev.map((section) => {
91+
return {
92+
...section,
93+
testCaseResults: section.testCaseResults.map((test, i) => ({
94+
...test,
95+
status: resolveStatusId(testOutput[i]?.statusId ?? 0),
96+
actualOutput: testOutput[i]?.stdout ?? testOutput[i]?.stderr ?? '',
97+
})),
98+
};
99+
})
100+
);
101+
}
102+
}, [testLoading, testError, testOutput, currentSectionIndex]);
103+
104+
function resolveStatusId(statusId: number): TestCaseResultStatus {
105+
switch (statusId) {
106+
case 3:
107+
return 'passed';
108+
case 5:
109+
case 7:
110+
case 8:
111+
case 9:
112+
case 10:
113+
case 11:
114+
case 12:
115+
return 'runtime_error';
116+
case 4:
117+
return 'failed';
118+
default:
119+
return 'failed';
120+
}
121+
}
122+
55123
// the timer is in seconds however our model is in minutes
56124
const totalEstimatedMinutes = sections.reduce(
57125
(sum, s) => sum + (s.taskTemplate.estimatedTime ?? 0),
@@ -122,6 +190,7 @@ export default function useAssessment(assessmentId: string) {
122190
handleSubmitAssessment('submitted');
123191
} else {
124192
setIsTransitioning(true);
193+
resetTests();
125194
setTimeout(() => {
126195
setSections((prev) =>
127196
prev.map((s, i) => {
@@ -137,10 +206,19 @@ export default function useAssessment(assessmentId: string) {
137206
}
138207
}
139208

140-
function updateCode(code: string) {
141-
setSections((prev) =>
142-
prev.map((s, i) => (i === currentSectionIndexRef.current ? { ...s, code } : s))
143-
);
209+
function updateCode() {
210+
const code = editorRef.current?.getValue() ?? '';
211+
setSections((prev) => prev.map((s, i) => (i === currentSectionIndex ? { ...s, code } : s)));
212+
213+
return code;
214+
}
215+
216+
function runTests() {
217+
const code = updateCode() || '';
218+
const section = sections[currentSectionIndex];
219+
if (!section) return;
220+
221+
runAssessmentTests(section.taskTemplateId, code, section.language as ProgrammingLanguage);
144222
}
145223

146224
function changeLanguage(language: string) {
@@ -162,58 +240,12 @@ export default function useAssessment(assessmentId: string) {
162240
}
163241
}
164242

165-
// TODO: Replace with Judge0 execution
166-
function runTests() {
167-
const section = sections[currentSectionIndex];
168-
if (!section) return;
169-
if (section.testCaseResults.some((r) => r.status === 'loading')) return;
170-
171-
const MOCK_STATUSES = ['passed', 'failed', 'runtime_error'] as const;
172-
173-
setSections((prev) =>
174-
prev.map((s, i) =>
175-
i === currentSectionIndex
176-
? {
177-
...s,
178-
testCaseResults: s.testCaseResults.map(() => ({
179-
status: 'loading' as const,
180-
})),
181-
}
182-
: s
183-
)
184-
);
185-
186-
setTimeout(() => {
187-
setSections((prev) =>
188-
prev.map((s, i) => {
189-
if (i !== currentSectionIndex) return s;
190-
return {
191-
...s,
192-
testCaseResults: s.testCaseResults.map((_, idx) => {
193-
const status =
194-
MOCK_STATUSES[Math.floor(Math.random() * MOCK_STATUSES.length)];
195-
const tc = section.taskTemplate.publicTestCases[idx];
196-
if (status === 'passed')
197-
return { status, actualOutput: tc?.output ?? '' };
198-
if (status === 'runtime_error')
199-
return {
200-
status,
201-
actualOutput: 'Timeout Error: Execution Time Exceeded',
202-
};
203-
return { status, actualOutput: 'wrong_answer' };
204-
}),
205-
};
206-
})
207-
);
208-
}, 1500);
209-
}
210-
211243
function handleEditorMount(editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) {
212244
editorRef.current = editorInstance;
213245
monacoRef.current = monaco;
214246

215247
editorInstance.onDidChangeModelContent(() => {
216-
updateCode(editorInstance.getValue());
248+
updateCode();
217249
});
218250
}
219251

@@ -234,6 +266,7 @@ export default function useAssessment(assessmentId: string) {
234266
isSubmitting,
235267
isTransitioning,
236268
error,
269+
testError,
237270
startAssessment,
238271
submitAndContinue,
239272
updateCode,

0 commit comments

Comments
 (0)