Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/(web)/crm/task-templates/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TestCaseEditor from '@/lib/components/core/TestCaseEditor';
import { Button } from '@/lib/components/ui/Button';
import TaskEditorSidebar from '@/lib/components/core/TaskEditorSidebar';
import Breadcrumbs from '@/lib/components/core/Breadcrumbs';
import useTestRunner from '@/lib/hooks/useTestRunner';

export default function TaskTemplateEditPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
Expand Down Expand Up @@ -43,7 +44,12 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
clearAllLanguages,
handleLanguageSelectionChange,
generateStubsForLanguages,
getEditorContent,
} = useTaskTemplateEditPage(id);
const { runEditPageTests } = useTestRunner(
getEditorContent(),
languages ? languages[selectedLanguage].language : 'python' // UHHHH
);

if (isLoading) {
return (
Expand Down Expand Up @@ -133,6 +139,7 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
setPublicTestCases={setPublicTestCases}
privateTestCases={privateTestCases}
setPrivateTestCases={setPrivateTestCases}
runTests={runEditPageTests}
isSaving={isSaving}
/>
</div>
Expand Down
40 changes: 40 additions & 0 deletions src/app/api/runner/[taskTemplateId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import judge0Connector, {
type JudgeSubmissionRequestBody,
} from '@/lib/connectors/judge0.connector';
import { SubmissionSchema } from '@/lib/schemas/submission.schema';
import TaskTemplateService from '@/lib/services/task-template.service';
// import { getSession } from '@/lib/utils/auth.utils';
import { handleError } from '@/lib/utils/errors.utils';
import { mapLanguageToJudge } from '@/lib/utils/language.utils';
import { type NextRequest } from 'next/server';

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ taskTemplateId: string }> }
) {
try {
const body = await request.json();
const { taskTemplateId } = await params;
const parsed = SubmissionSchema.parse(body);
const languageId = mapLanguageToJudge(parsed.language);
const taskTemplate = await TaskTemplateService.getTaskTemplate(
taskTemplateId,
'TODO: REPLACE THIS LATER LAITH WITH COOKIE'
);
const tests = [...taskTemplate.privateTestCases, ...taskTemplate.publicTestCases];
const formatted: JudgeSubmissionRequestBody[] = tests.map((test) => ({
source_code: parsed.code,
language_id: languageId,
stdin: test.input,
expected_output: test.output,
}));

const result = await judge0Connector.executeSubmissions(formatted);
return Response.json({
data: result,
status: 200,
});
} catch (err) {
return handleError(err);
}
}
32 changes: 32 additions & 0 deletions src/app/api/runner/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { handleError } from '@/lib/utils/errors.utils';
import { assertRecruiterOrAbove } from '@/lib/utils/permissions.utils';
import { type NextRequest } from 'next/server';
import judge0Connector, {
type JudgeSubmissionRequestBody,
} from '@/lib/connectors/judge0.connector';
import { TestSubmissionSchema } from '@/lib/schemas/submission.schema';
import { mapLanguageToJudge } from '@/lib/utils/language.utils';

export async function POST(request: NextRequest) {
try {
await assertRecruiterOrAbove(request.headers);
const body = await request.json();
const parsed = TestSubmissionSchema.parse(body);
const languageId = mapLanguageToJudge(parsed.language);
const formatted: JudgeSubmissionRequestBody[] = parsed.tests.map((test) => ({
source_code: parsed.code,
language_id: languageId,
stdin: test.input,
expected_output: test.output,
}));

const result = await judge0Connector.executeSubmissions(formatted);

return Response.json({
data: result,
status: 200,
});
} catch (err) {
return handleError(err);
}
}
46 changes: 46 additions & 0 deletions src/lib/api/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
type SubmissionSchemaDTO,
type TestSubmissionSchemaDTO,
} from '@/lib/schemas/submission.schema';
import { type JudgeResultRequestBody } from '@/lib/connectors/judge0.connector';

export async function runEditorSubmission(
submission: TestSubmissionSchemaDTO
): Promise<JudgeResultRequestBody> {
const result = await fetch(`/api/runner`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});

const json = await result.json();

if (!result.ok) {
throw new Error(json.message);
}

return json.data;
}

export async function runAssessmentSubmission(
taskTemplateId: string,
submission: SubmissionSchemaDTO
): Promise<JudgeResultRequestBody> {
const result = await fetch(`/api/runner/${taskTemplateId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});

const json = await result.json();

if (!result.ok) {
throw new Error(json.message);
}

return json.data;
}
12 changes: 10 additions & 2 deletions src/lib/components/core/TestCaseEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ interface TestCaseEditorProps {
privateTestCases: TestCaseDTO[];
setPrivateTestCases: React.Dispatch<React.SetStateAction<TestCaseDTO[]>>;
isSaving: boolean;
runTests: (tests: TestCaseDTO[]) => void;
}

export default function TestCaseEditor(props: TestCaseEditorProps) {
const { publicTestCases, setPublicTestCases, privateTestCases, setPrivateTestCases, isSaving } =
props;
const {
publicTestCases,
setPublicTestCases,
privateTestCases,
setPrivateTestCases,
isSaving,
runTests,
} = props;
const {
addTestCase,
removeTestCase,
Expand All @@ -36,6 +43,7 @@ export default function TestCaseEditor(props: TestCaseEditorProps) {
onRemoveTestCase={removeTestCase}
onTestCaseUpdate={updateTestCase}
onToggleTestCaseVisibility={toggleTestCaseVisibility}
runTests={runTests}
/>
);
}
32 changes: 23 additions & 9 deletions src/lib/components/core/TestCasePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type EditablePanelProps = TestCasePanelBaseProps & {
value: string
) => void;
onToggleTestCaseVisibility: (index: number, tab: TestTab) => void;
runTests: (tests: TestCaseDTO[]) => void;
};

type ReadOnlyPanelProps = TestCasePanelBaseProps & {
Expand Down Expand Up @@ -151,15 +152,28 @@ export default function TestCasePanel(props: TestCasePanelProps) {
{activeLabel} ({activeTestCases.length})
</span>
{!props.readOnly && (
<Button
className="items-center gap-1 rounded-md px-3 py-1 text-sm"
variant="secondary"
onClick={() => props.onAddTestCase(activeTab)}
disabled={props.isSaving}
>
<PlusIcon className="stroke-sarge-primary-500" height={18} width={18} />
Add test
</Button>
<div className="flex items-center justify-between gap-2">
<Button
className="items-center gap-1 rounded-md px-3 py-1 text-sm"
variant="secondary"
onClick={() => props.onAddTestCase(activeTab)}
disabled={props.isSaving}
>
<PlusIcon
className="stroke-sarge-primary-500"
height={18}
width={18}
/>
Add test
</Button>
<Button
className="items-center rounded-md px-3 py-1 text-sm"
variant="primary"
onClick={() => props.runTests(activeTestCases)}
>
Run
</Button>
</div>
)}
</div>

Expand Down
16 changes: 10 additions & 6 deletions src/lib/connectors/judge0.connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export interface JudgeSubmissionRequestBody {

export interface JudgeResultRequestBody {
stdout: string;
status_id: number;
language_id: number;
stderr: string;
status: object;
status: {
id: number;
description: string;
};
token: string;
}

Expand All @@ -35,7 +37,6 @@ class Judge0Connector {
if (!body || body.length === 0) {
throw new BadRequestException('At least one submission is required');
}

const url = `${process.env.JUDGE_URL}/submissions/batch?fields=${this.fields.join(',')}`;
const requestBody = { submissions: [...body] };
const response = await fetch(url, {
Expand Down Expand Up @@ -71,7 +72,10 @@ class Judge0Connector {
throw new InternalServerException(`Judge0 API error: ${jsonResponse.error}`);
}

return jsonResponse;
if (!('submissions' in jsonResponse)) {
throw new InternalServerException(`Submissions Not Returned`);
}
return jsonResponse['submissions'];
}

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

results.forEach((submissionResult, index) => {
const isComplete =
submissionResult.status_id !== 1 && submissionResult.status_id !== 2;
submissionResult.status.id !== 1 && submissionResult.status.id !== 2;
if (isComplete) {
allResults.push(submissionResult);
} else {
Expand Down Expand Up @@ -146,7 +150,7 @@ class Judge0Connector {
};

allResults.forEach((submissionResult) => {
switch (submissionResult.status_id) {
switch (submissionResult.status.id) {
case 3: // Accepted
categorizedResults.accepted.push(submissionResult);
break;
Expand Down
51 changes: 51 additions & 0 deletions src/lib/hooks/useTestRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState } from 'react';
import { runEditorSubmission, runAssessmentSubmission } from '@/lib/api/runner';
import { type JudgeResultRequestBody } from '@/lib/connectors/judge0.connector';
import { type ProgrammingLanguage } from '@/generated/prisma';
import { type TestCaseDTO } from '@/lib/schemas/task-template.schema';

export default function useTestRunner(code: string, language: ProgrammingLanguage) {
const [error, setError] = useState<Error>();
const [loading, setLoading] = useState<boolean>(false);
const [output, setOutput] = useState<JudgeResultRequestBody>();

async function runEditPageTests(tests: TestCaseDTO[]) {
try {
setLoading(true);
const result = await runEditorSubmission({
code,
language,
tests,
});
setOutput(result);
console.warn(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}

async function runAssessmentTests(taskTemplateId: string) {
try {
setLoading(true);
const result = await runAssessmentSubmission(taskTemplateId, {
code,
language,
});
setOutput(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}

return {
runAssessmentTests,
runEditPageTests,
error,
loading,
output,
};
}
18 changes: 18 additions & 0 deletions src/lib/schemas/submission.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ProgrammingLanguage } from '@/generated/prisma';
import { testCaseSchema } from '@/lib/schemas/task-template.schema';
import { z } from 'zod';

export const SubmissionSchema = z.object({
code: z.string(),
language: z.enum(ProgrammingLanguage),
additionalTests: z.array(testCaseSchema).optional(), // May be useful for letting users create their own test cases in the future
});

export const TestSubmissionSchema = SubmissionSchema.omit({
additionalTests: true,
}).extend({
tests: z.array(testCaseSchema),
});

export type SubmissionSchemaDTO = z.infer<typeof SubmissionSchema>;
export type TestSubmissionSchemaDTO = z.infer<typeof TestSubmissionSchema>;
4 changes: 2 additions & 2 deletions src/lib/schemas/task-template.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { TagSchema } from './tag.schema';
import { TaskTemplateLanguageSchema } from './task-template-language.schema';

export const testCaseSchema = z.object({
input: z.string().min(1, 'Expected input required'),
output: z.string().min(1, 'Expected output required'),
input: z.string(),
output: z.string(),
});

export const getTaskTemplateSchema = z.object({
Expand Down
24 changes: 24 additions & 0 deletions src/lib/utils/language.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,27 @@ export function generateCodeStub(
return `// Code stub for ${language} is not available.`;
}
}

export function mapLanguageToJudge(language: string): number {
try {
switch (language) {
case 'python':
return 100;
case 'javascript':
return 102;
case 'ruby':
return 72;
case 'typescript':
return 101;
case 'c':
return 103;
case 'cpp':
return 105;
default:
return -1;
}
} catch {
// figure this out later
throw new Error('language not foudn');
}
}
Loading