Skip to content

Commit

Permalink
Merge pull request #155 from cpinitiative/new-online-judge
Browse files Browse the repository at this point in the history
Use Rust Online Judge
  • Loading branch information
thecodingwizard authored Jul 23, 2024
2 parents 8698726 + 14891cc commit 7580de2
Show file tree
Hide file tree
Showing 14 changed files with 206 additions and 143 deletions.
4 changes: 2 additions & 2 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const testRunCode = async (

export const switchLang = async (
page: Page,
lang: 'Java' | 'Python 3.8.1' | 'C++'
lang: 'Java' | 'Python 3.12.3' | 'C++'
) => {
await page.getByRole('button', { name: 'File' }).click();
await page.getByRole('menuitem', { name: 'Settings' }).click();
Expand All @@ -58,7 +58,7 @@ export const forEachLang = async (
await switchLang(page, 'Java');
await func();

await switchLang(page, 'Python 3.8.1');
await switchLang(page, 'Python 3.12.3');
await func();

await switchLang(page, 'C++');
Expand Down
2 changes: 1 addition & 1 deletion e2e/respects_permissions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ test.describe('Respects Permissions', () => {
expect(await page.$('text="// this is a comment"')).toBeTruthy();
await page2.waitForSelector('text="// this is a comment"');

await switchLang(page, 'Python 3.8.1');
await switchLang(page, 'Python 3.12.3');
await page.waitForSelector('button:has-text("Run Code")');
await page2.waitForSelector('button:has-text("Run Code")');
await page2.click(`${editorClass} div:nth-child(5)`);
Expand Down
46 changes: 17 additions & 29 deletions pages/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import useUserPermission from '../src/hooks/useUserPermission';
import { SettingsModal } from '../src/components/settings/SettingsModal';
import { getSampleIndex } from '../src/components/JudgeInterface/Samples';
import useJudgeResults from '../src/hooks/useJudgeResults';
import { cleanJudgeResult } from '../src/editorUtils';
import JudgeResult from '../src/types/judge';
import useUserFileConnection from '../src/hooks/useUserFileConnection';
import useUpdateUserDashboard from '../src/hooks/useUpdateUserDashboard';
Expand Down Expand Up @@ -79,15 +78,20 @@ function EditorPage() {
isCodeRunning: isRunning,
});
};
const fetchJudge = (code: string, input: string): Promise<Response> => {
const fetchJudge = (
code: string,
input: string,
expectedOutput?: string
): Promise<JudgeResult> => {
return submitToJudge(
fileData.settings.language,
code,
input,
fileData.settings.compilerOptions[fileData.settings.language],
problem?.input?.endsWith('.in')
? problem.input.substring(0, problem.input.length - 3)
: undefined
: undefined,
expectedOutput
);
};

Expand All @@ -112,21 +116,13 @@ function EditorPage() {
setResultAt(inputTabIndex, null);

const code = getMainEditorValue();
fetchJudge(code, input)
fetchJudge(code, input, expectedOutput)
.then(async resp => {
const data: JudgeResult = await resp.json();
if (!resp.ok) {
if (data.debugData?.errorType === 'Function.ResponseSizeTooLarge') {
alert(
'Error: Your program printed too much data to stdout/stderr.'
);
} else {
alert('Error: ' + (resp.status + ' - ' + JSON.stringify(data)));
}
} else {
cleanJudgeResult(data, expectedOutput, prefix);
setResultAt(inputTabIndex, data);
const data: JudgeResult = resp;
if (prefix && data.status !== 'compile_error') {
data.statusDescription = prefix + data.statusDescription;
}
setResultAt(inputTabIndex, data);
})
.catch(e => {
alert(
Expand Down Expand Up @@ -154,27 +150,19 @@ function EditorPage() {
const promises = [];
for (let index = 0; index < samples.length; ++index) {
const sample = samples[index];
promises.push(fetchJudge(code, sample.input));
promises.push(fetchJudge(code, sample.input, sample.output));
}

const newJudgeResults = judgeResults;
const results: JudgeResult[] = [];
for (let index = 0; index < samples.length; ++index) {
const sample = samples[index];
const resp = await promises[index];
const data: JudgeResult = await resp.json();
if (!resp.ok || data.status === 'internal_error') {
alert(
'Error: ' +
(data.message || resp.status + ' - ' + JSON.stringify(data))
);
console.error(data);
throw new Error('bad judge result');
}
const data = await promises[index];
let prefix = 'Sample';
if (samples.length > 1) prefix += ` ${index + 1}`;
prefix += ': ';
cleanJudgeResult(data, sample.output, prefix);
if (data.status !== 'compile_error') {
data.statusDescription = prefix + data.statusDescription;
}
results.push(data);
newJudgeResults[2 + index] = data;
}
Expand Down
2 changes: 1 addition & 1 deletion pages/api/createUSACOFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default async (
language: 'cpp',
problem,
compilerOptions: {
cpp: '-std=c++17 -O2 -Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op',
cpp: '-std=c++23 -O2 -Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op',
java: '',
py: '',
},
Expand Down
2 changes: 1 addition & 1 deletion pages/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function classNames(...classes: any[]) {
}

export const DEFAULT_COMPILER_OPTIONS = {
cpp: '-std=c++17 -O2 -Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op',
cpp: '-std=c++23 -O2 -Wall -Wextra -Wshadow -Wconversion -Wfloat-equal -Wduplicated-cond -Wlogical-op',
java: '',
py: '',
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/CodeInterface/CodeInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ export const CodeInterface = ({
defaultValue={defaultCode[lang]}
yjsDocumentId={`${fileData.id}.${lang}`}
useEditorWithVim={true}
lspEnabled={true} // at some point, maybe make this a user setting?
lspOptions={{
compilerOptions: fileData.settings.compilerOptions[lang],
}}
dataTestId="code-editor"
/>
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/components/NavBar/FileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { useAtomValue } from 'jotai';
import { useEditorContext } from '../../context/EditorContext';
import defaultCode from '../../scripts/defaultCode';
import download from '../../scripts/download';
import { extractJavaFilename } from '../../scripts/judge';
import useUserPermission from '../../hooks/useUserPermission';

export const FileMenu = (props: { onOpenSettings: Function }): JSX.Element => {
Expand Down Expand Up @@ -54,9 +53,15 @@ export const FileMenu = (props: { onOpenSettings: Function }): JSX.Element => {

const code = getMainEditorValue();

let javaFilename = 'Main.java';
const matches = Array.from(code.matchAll(/public\s+class\s+(\w+)/g));
if (matches.length > 0) {
javaFilename = matches[0][1] + '.java';
}

const fileNames = {
cpp: `${fileData.settings.workspaceName}.cpp`,
java: extractJavaFilename(code),
java: javaFilename,
py: `${fileData.settings.workspaceName}.py`,
};

Expand Down
11 changes: 3 additions & 8 deletions src/components/editor/MonacoEditor/MonacoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { initVimMode } from 'monaco-vim';
import { MonacoServices } from 'monaco-languageclient';
import { getOrCreateModel, usePrevious, useUpdate } from './utils';
import { EditorProps } from './monaco-editor-types';
import createLSPConnection from './lsp';
import useLSP from './lsp';
import { MonacoBinding } from 'y-monaco';

buildWorkerDefinition(
Expand Down Expand Up @@ -43,7 +43,7 @@ export default function MonacoEditor({
value = '',
onBeforeDispose,
vim = false,
lspEnabled = false,
lspOptions = null,
yjsInfo,
}: EditorProps) {
const ref = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -110,12 +110,7 @@ export default function MonacoEditor({
};
}, [editor, yjsInfo]);

useEffect(() => {
if (lspEnabled && (language === 'cpp' || language === 'python')) {
// yikes, ugly how there's both python and py
return createLSPConnection(language);
}
}, [lspEnabled, language]);
useLSP(language ?? '', lspOptions ?? null);

useEffect(() => {
if (vim) {
Expand Down
46 changes: 31 additions & 15 deletions src/components/editor/MonacoEditor/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import normalizeUrl from 'normalize-url';

import toast from 'react-hot-toast';
import { useEffect, useState } from 'react';

// note: all the toast notifications should probably be moved up to MonacoEditor.tsx
const notify = (message: string) => {
Expand All @@ -24,17 +25,21 @@ const notify = (message: string) => {
});
};

export default function createLSPConnection(language: 'cpp' | 'python') {
function createLSPConnection(
language: 'cpp' | 'python',
compiler_options: string | null
) {
if (language !== 'cpp' && language !== 'python') {
throw new Error('Unsupported LSP language: ' + language);
}

notify('Connecting to server...');
const url = createUrl(
'thecodingwizard--lsp-server-main.modal.run',
443,
language === 'cpp' ? '/clangd' : '/pyright'
);
let url = `wss://thecodingwizard--lsp-server-main.modal.run:443/${
language === 'cpp' ? 'clangd' : 'pyright'
}`;
if (compiler_options) {
url += `?compiler_options=${encodeURIComponent(compiler_options)}`;
}

let webSocket: WebSocket | null = new WebSocket(url);
let languageClient: MonacoLanguageClient | null;
Expand Down Expand Up @@ -66,7 +71,15 @@ export default function createLSPConnection(language: 'cpp' | 'python') {
}
});

let weClosedWebsocketConnection = false;
webSocket.addEventListener('close', event => {
if (languageClient) {
languageClient.stop();
languageClient = null;
}

if (weClosedWebsocketConnection) return;

if (event.wasClean) {
console.log('Connection closed cleanly');
} else {
Expand All @@ -80,11 +93,6 @@ export default function createLSPConnection(language: 'cpp' | 'python') {
} else {
notify('Connection closed unexpectedly');
}

if (languageClient) {
languageClient.stop();
languageClient = null;
}
});

function createLanguageClient(
Expand Down Expand Up @@ -120,10 +128,6 @@ export default function createLSPConnection(language: 'cpp' | 'python') {
});
}

function createUrl(hostname: string, port: number, path: string): string {
const protocol = location.protocol === 'https:' ? 'wss' : 'wss';
return normalizeUrl(`${protocol}://${hostname}:${port}${path}`);
}
function dispose() {
if (!languageClient) {
// possibly didn't connect to websocket before exiting
Expand All @@ -135,6 +139,18 @@ export default function createLSPConnection(language: 'cpp' | 'python') {
languageClient.stop();
languageClient = null;
}
weClosedWebsocketConnection = true;
}
return dispose;
}

export default function useLSP(
language: string,
lspOptions: { compilerOptions: string | null } | null
) {
useEffect(() => {
if (lspOptions && (language === 'cpp' || language === 'python')) {
return createLSPConnection(language, lspOptions.compilerOptions);
}
}, [language, lspOptions?.compilerOptions]);
}
7 changes: 6 additions & 1 deletion src/components/editor/MonacoEditor/monaco-editor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ export interface EditorProps {

vim?: boolean;

lspEnabled?: boolean;
/**
* Set to undefined / null to disable LSP.
*/
lspOptions?: {
compilerOptions: string | null;
} | null;

/**
* If provided, the code editor should create a yjs binding with the given information
Expand Down
2 changes: 1 addition & 1 deletion src/context/UserContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const LANGUAGES: { label: string; value: Language }[] = [
value: 'java',
},
{
label: 'Python 3.8.1',
label: 'Python 3.12.3',
value: 'py',
},
];
Expand Down
48 changes: 0 additions & 48 deletions src/editorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,3 @@ export function isFirebaseId(queryId: string): boolean {
queryId.length === 19
);
}

function cleanAndReplaceOutput(output: string): {
replaced: string;
cleaned: string;
} {
const replaced = output.replace(/ /g, '\u2423'); // spaces made visible
const lines = output.split('\n');
for (let i = 0; i < lines.length; ++i) lines[i] = lines[i].trim();
const cleaned = lines.join('\n').trim(); // remove leading / trailing whitespace on each line, trim
return { replaced, cleaned };
}

export function cleanJudgeResult(
data: JudgeResult,
expectedOutput?: string,
prefix?: string
): void {
const statusDescriptions: { [key in JudgeResultStatuses]: string } = {
success: 'Successful',
compile_error: 'Compilation Error',
runtime_error: 'Runtime Error',
internal_error: 'Internal Server Error',
time_limit_exceeded: 'Time Limit Exceeded',
wrong_answer: 'Wrong Answer',
};
data.statusDescription = statusDescriptions[data.status];
if (data.fileOutput) {
data.stdout = data.fileOutput;
}
if (expectedOutput && data.status === 'success') {
data.statusDescription = 'Successful';
let stdout = data.stdout ?? '';
if (!stdout.endsWith('\n')) stdout += '\n';
if (data.status === 'success' && stdout !== expectedOutput) {
data.status = 'wrong_answer';
const { cleaned, replaced } = cleanAndReplaceOutput(stdout);
if (cleaned === expectedOutput.trim()) {
data.statusDescription = 'Wrong Answer (Extra Whitespace)';
data.stdout = replaced; // show the extra whitespace
} else {
data.statusDescription = 'Wrong Answer';
}
}
}
if (prefix && data.status !== 'compile_error')
// only add prefix when no compilation error
data.statusDescription = prefix + data.statusDescription;
}
Loading

0 comments on commit 7580de2

Please sign in to comment.