Skip to content

Commit ebf2a97

Browse files
committed
feat: test runner
1 parent 4154aaa commit ebf2a97

9 files changed

Lines changed: 256 additions & 6 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import TestCaseEditor from '@/lib/components/core/TestCaseEditor';
99
import { Button } from '@/lib/components/ui/Button';
1010
import TaskEditorSidebar from '@/lib/components/core/TaskEditorSidebar';
1111
import Breadcrumbs from '@/lib/components/core/Breadcrumbs';
12+
import useTestRunner from '@/lib/hooks/useTestRunner';
1213

1314
export default function TaskTemplateEditPage({ params }: { params: Promise<{ id: string }> }) {
1415
const { id } = use(params);
@@ -44,6 +45,8 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
4445
handleLanguageSelectionChange,
4546
generateStubsForLanguages,
4647
} = useTaskTemplateEditPage(id);
48+
const { runAssessmentTests, runEditPageTests, setCode, setLanguage, error, loading, output } =
49+
useTestRunner(id);
4750

4851
if (isLoading) {
4952
return (
@@ -73,6 +76,7 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
7376
>
7477
{isSaving ? 'Saving...' : 'Save Changes'}
7578
</Button>
79+
<Button onClick={runEditPageTests}> Run Code </Button>
7680
</div>
7781

7882
<div className="flex min-h-0 flex-1 overflow-hidden">
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import judge0Connector, {
2+
type JudgeSubmissionRequestBody,
3+
} from '@/lib/connectors/judge0.connector';
4+
import { SubmissionSchema } from '@/lib/schemas/submission.schema';
5+
import TaskTemplateService from '@/lib/services/task-template.service';
6+
import { getSession } from '@/lib/utils/auth.utils';
7+
import { handleError } from '@/lib/utils/errors.utils';
8+
import { mapLanguageToJudge } from '@/lib/utils/language.utils';
9+
import { type NextRequest } from 'next/server';
10+
11+
export async function POST(
12+
request: NextRequest,
13+
{ params }: { params: Promise<{ taskTemplateId: string }> }
14+
) {
15+
try {
16+
const body = await request.json();
17+
const session = await getSession();
18+
const { taskTemplateId } = await params;
19+
const parsed = SubmissionSchema.parse(body);
20+
const languageId = mapLanguageToJudge(parsed.language);
21+
const taskTemplate = await TaskTemplateService.getTaskTemplate(
22+
taskTemplateId,
23+
session.activeOrganizationId
24+
);
25+
const tests = [...taskTemplate.privateTestCases, ...taskTemplate.publicTestCases];
26+
const formatted: JudgeSubmissionRequestBody[] = tests.map((test) => ({
27+
source_code: parsed.code,
28+
language_id: languageId,
29+
stdin: test.input,
30+
expected_output: test.output,
31+
}));
32+
33+
const result = await judge0Connector.executeSubmissions(formatted);
34+
return Response.json({
35+
data: result,
36+
status: 200,
37+
});
38+
} catch (err) {
39+
return handleError(err);
40+
}
41+
}

src/app/api/runner/route.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { handleError } from '@/lib/utils/errors.utils';
2+
import { assertRecruiterOrAbove } from '@/lib/utils/permissions.utils';
3+
import { type NextRequest } from 'next/server';
4+
import judge0Connector, {
5+
type JudgeSubmissionRequestBody,
6+
} from '@/lib/connectors/judge0.connector';
7+
import { TestSubmissionSchema } from '@/lib/schemas/submission.schema';
8+
import { mapLanguageToJudge } from '@/lib/utils/language.utils';
9+
10+
export async function POST(request: NextRequest) {
11+
try {
12+
await assertRecruiterOrAbove(request.headers);
13+
const body = await request.json();
14+
const parsed = TestSubmissionSchema.parse(body);
15+
const languageId = mapLanguageToJudge(parsed.language);
16+
const formatted: JudgeSubmissionRequestBody[] = parsed.tests.map((test) => ({
17+
// it doesnt make sense to send the code multiple times so formatting happens on server
18+
source_code: parsed.code,
19+
language_id: languageId,
20+
stdin: test.input,
21+
expected_output: test.output,
22+
}));
23+
24+
const result = await judge0Connector.executeSubmissions(formatted);
25+
26+
return Response.json(
27+
{
28+
data: result,
29+
},
30+
{
31+
status: 200,
32+
}
33+
);
34+
} catch (err) {
35+
return handleError(err);
36+
}
37+
}

src/lib/api/runner.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {
2+
type SubmissionSchemaDTO,
3+
type TestSubmissionSchemaDTO,
4+
} from '@/lib/schemas/submission.schema';
5+
import { type JudgeResultRequestBody } from '@/lib/connectors/judge0.connector';
6+
7+
export async function runEditorSubmission(
8+
submission: TestSubmissionSchemaDTO
9+
): Promise<JudgeResultRequestBody> {
10+
const result = await fetch(`/api/runner`, {
11+
method: 'POST',
12+
headers: {
13+
'Content-Type': 'application/json',
14+
},
15+
body: JSON.stringify(submission),
16+
});
17+
18+
const json = await result.json();
19+
20+
if (!result.ok) {
21+
throw new Error(json.message);
22+
}
23+
24+
return json.data;
25+
}
26+
27+
export async function runAssessmentSubmission(
28+
taskTemplateId: string,
29+
submission: SubmissionSchemaDTO
30+
): Promise<JudgeResultRequestBody> {
31+
const result = await fetch(`/api/${taskTemplateId}/runner`, {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
},
36+
body: JSON.stringify(submission),
37+
});
38+
39+
const json = await result.json();
40+
41+
if (!result.ok) {
42+
throw new Error(json.message);
43+
}
44+
45+
return json.data;
46+
}

src/lib/connectors/judge0.connector.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ export interface JudgeSubmissionRequestBody {
1010

1111
export interface JudgeResultRequestBody {
1212
stdout: string;
13-
status_id: number;
1413
language_id: number;
1514
stderr: string;
16-
status: object;
15+
status: {
16+
id: number;
17+
description: string;
18+
};
1719
token: string;
1820
}
1921

@@ -35,7 +37,6 @@ class Judge0Connector {
3537
if (!body || body.length === 0) {
3638
throw new BadRequestException('At least one submission is required');
3739
}
38-
3940
const url = `${process.env.JUDGE_URL}/submissions/batch?fields=${this.fields.join(',')}`;
4041
const requestBody = { submissions: [...body] };
4142
const response = await fetch(url, {
@@ -71,7 +72,10 @@ class Judge0Connector {
7172
throw new InternalServerException(`Judge0 API error: ${jsonResponse.error}`);
7273
}
7374

74-
return jsonResponse;
75+
if (!('submissions' in jsonResponse)) {
76+
throw new InternalServerException(`Submissions Not Returned`); // REVIEWER LOOK AT THIS IN CASE I FORGOR
77+
}
78+
return jsonResponse['submissions'];
7579
}
7680

7781
// create the provided submissions w/judge0 for running test cases
@@ -114,7 +118,7 @@ class Judge0Connector {
114118

115119
results.forEach((submissionResult, index) => {
116120
const isComplete =
117-
submissionResult.status_id !== 1 && submissionResult.status_id !== 2;
121+
submissionResult.status.id !== 1 && submissionResult.status.id !== 2;
118122
if (isComplete) {
119123
allResults.push(submissionResult);
120124
} else {
@@ -146,7 +150,7 @@ class Judge0Connector {
146150
};
147151

148152
allResults.forEach((submissionResult) => {
149-
switch (submissionResult.status_id) {
153+
switch (submissionResult.status.id) {
150154
case 3: // Accepted
151155
categorizedResults.accepted.push(submissionResult);
152156
break;

src/lib/hooks/useTestRunner.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useState } from 'react';
2+
import { runEditorSubmission, runAssessmentSubmission } from '@/lib/api/runner';
3+
import { type JudgeResultRequestBody } from '@/lib/connectors/judge0.connector';
4+
import { type ProgrammingLanguage } from '@/generated/prisma';
5+
6+
export default function useTestRunner(taskTemplateId: string) {
7+
const [error, setError] = useState<Error>();
8+
const [loading, setLoading] = useState<boolean>(false);
9+
const [code, setCode] = useState<string>('');
10+
const [language, setLanguage] = useState<ProgrammingLanguage>('python');
11+
const [output, setOutput] = useState<JudgeResultRequestBody>();
12+
13+
async function runEditPageTests() {
14+
try {
15+
setLoading(true);
16+
const result = await runEditorSubmission({
17+
code: 'x=input()\nprint(x)',
18+
language,
19+
tests: [{ input: '2', output: '2' }],
20+
});
21+
setOutput(result);
22+
} catch (err) {
23+
setError(err as Error);
24+
} finally {
25+
setLoading(false);
26+
}
27+
}
28+
29+
async function runAssessmentTests() {
30+
try {
31+
setLoading(true);
32+
const result = await runAssessmentSubmission(taskTemplateId, {
33+
code,
34+
language,
35+
});
36+
setOutput(result);
37+
} catch (err) {
38+
setError(err as Error);
39+
} finally {
40+
setLoading(false);
41+
}
42+
}
43+
44+
return {
45+
runAssessmentTests,
46+
runEditPageTests,
47+
setCode,
48+
setLanguage,
49+
error,
50+
loading,
51+
output,
52+
};
53+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ProgrammingLanguage } from '@/generated/prisma';
2+
import { testCaseSchema } from '@/lib/schemas/task-template.schema';
3+
import { z } from 'zod';
4+
5+
export const SubmissionSchema = z.object({
6+
code: z.string(),
7+
language: z.enum(ProgrammingLanguage),
8+
additonalTests: z.array(testCaseSchema).optional(), // May be useful for letting users create their own test cases in the future
9+
});
10+
11+
export const TestSubmissionSchema = SubmissionSchema.omit({
12+
additonalTests: true,
13+
}).extend({
14+
tests: z.array(testCaseSchema),
15+
});
16+
17+
export type SubmissionSchemaDTO = z.infer<typeof SubmissionSchema>;
18+
export type TestSubmissionSchemaDTO = z.infer<typeof TestSubmissionSchema>;

src/lib/utils/language.utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,19 @@ export function generateCodeStub(
205205
return `// Code stub for ${language} is not available.`;
206206
}
207207
}
208+
209+
export function mapLanguageToJudge(language: string): number {
210+
try {
211+
switch (language) {
212+
case 'python':
213+
return 100;
214+
case 'javascript':
215+
return 102;
216+
default:
217+
return -1;
218+
}
219+
} catch {
220+
// figure this out later
221+
throw new Error('language not foudn');
222+
}
223+
}

test_runner_reqs.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Objects:
3+
- TaskTemplate: Stores test cases
4+
- TaskTemplateLanguage: stores solution stub
5+
- Task: Stores candidate code
6+
7+
Requirements for CRM
8+
Object mutation
9+
- Can mutate all parts of Task and Task Template object
10+
Code running
11+
- Uses solution from TaskTemplateLanguage as code text to run
12+
13+
Requirements for OA:
14+
Object mutation
15+
- Update Task object to change code
16+
- Can't update object to change assessmentId, taskTemplateId
17+
Code running:
18+
- uses candidateCode from task template language as code text to run
19+
20+
Need to store current language in both
21+
22+
Would it make sense to have two separate endpoints?
23+
24+
One requires a POST to send user code
25+
26+
One requires a GET because there is no user code, unless we are changing solution
27+
28+
Both could be POST if we always send solution as well
29+
30+
refactor use task hook
31+

0 commit comments

Comments
 (0)