Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Rust Online Judge #155

Merged
merged 6 commits into from
Jul 23, 2024
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
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