From 4bfe8db12a345e642f4567b4a672448cba85d754 Mon Sep 17 00:00:00 2001 From: Zilong Yao <59038614+ZLY201@users.noreply.github.com> Date: Mon, 23 Oct 2023 01:47:44 +0800 Subject: [PATCH] feat: support run cases (#6) * feat: support run test cases * fix: local cache lost option --- .github/workflows/compressed-size.yml | 2 +- config/i18n.json | 2 +- src/modules/Editor/index.tsx | 2 +- src/modules/Editor/monaco-editor.tsx | 6 +- src/modules/Question/Description.tsx | 4 +- src/modules/Results/index.module.less | 30 ++- src/modules/Results/index.tsx | 211 ++++++++++++++---- .../difficulties/transpose-matrix/index.ts | 3 +- .../difficulties/transpose-matrix/test | 3 + src/types/problems.d.ts | 2 +- src/utils/local-cache.ts | 14 +- src/utils/problems.ts | 29 ++- .../validate-monaco-model.ts} | 2 +- 13 files changed, 236 insertions(+), 74 deletions(-) create mode 100644 src/problems/difficulties/transpose-matrix/test rename src/{modules/Editor/revalidate-model.ts => utils/validate-monaco-model.ts} (96%) diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml index fbef873..2b02b6c 100644 --- a/.github/workflows/compressed-size.yml +++ b/.github/workflows/compressed-size.yml @@ -28,6 +28,6 @@ jobs: - name: Compressed size uses: preactjs/compressed-size-action@v2 with: - pattern: dist + pattern: dist/**/* compression: none build-script: build diff --git a/config/i18n.json b/config/i18n.json index c0bcfcb..3920742 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -105,6 +105,6 @@ }, "record_tip": { "en": "In order to save memory, we only save the latest 8 records. You can choose to manually delete some useless data to break this limit.", - "zh-cn": "为了节省内存,我们仅保存最新的 8 条记录,您可以选择手动删除一些数据来突破这个限制" + "zh-cn": "为了节省内存,我们仅保存最新的 8 条记录,您可以在达到限制后手动删除一些数据" } } diff --git a/src/modules/Editor/index.tsx b/src/modules/Editor/index.tsx index 8e69356..8a65f0b 100644 --- a/src/modules/Editor/index.tsx +++ b/src/modules/Editor/index.tsx @@ -20,7 +20,7 @@ function Editor() { const [{ setting, currentProblem }] = useContext(Context); function onChange(filename: ProblemFiles, content: string) { - if (!raw) return; + if (!raw || filename !== ProblemFiles.template) return; localCache.setProblemCache(currentProblem.key, { lastUpdated: content, }); diff --git a/src/modules/Editor/monaco-editor.tsx b/src/modules/Editor/monaco-editor.tsx index c79418a..35fc29e 100644 --- a/src/modules/Editor/monaco-editor.tsx +++ b/src/modules/Editor/monaco-editor.tsx @@ -10,7 +10,7 @@ import { } from '@src/utils/problems'; import { Setting } from '@src/utils/setting'; import emitter from '@src/utils/emit'; -import { revalidateModel } from './revalidate-model'; +import { validateMonacoModel } from '@src/utils/validate-monaco-model'; self.MonacoEnvironment = { getWorker: function () { @@ -49,7 +49,7 @@ export interface MonacoEditorProps { height?: number | string; namespace?: string; raw: ProblemRaw; - selectedFilename: ProblemFiles; + selectedFilename: Exclude; onChange?: (filename: ProblemFiles, content: string) => void; setting: Setting; } @@ -149,7 +149,7 @@ const MonacoEditor = decorateWithAutoResize( for (const filename of Object.keys(this.props.raw)) { this.models[filename].updateOptions(this.props.setting); if (!filename.includes('node_modules')) { - revalidateModel(this.models[filename]); + validateMonacoModel(this.models[filename]); } } } diff --git a/src/modules/Question/Description.tsx b/src/modules/Question/Description.tsx index 88b036f..feb0e52 100644 --- a/src/modules/Question/Description.tsx +++ b/src/modules/Question/Description.tsx @@ -42,7 +42,7 @@ const Description = function () { [currentProblem, state], ); - const updateDescription = useCallback( + const updateData = useCallback( debounce(async function (problem: Problem) { const desc = await getProblemDocs(problem, ProblemDocs.description); setDesc(desc); @@ -54,7 +54,7 @@ const Description = function () { useEffect( function () { setLoading(true); - updateDescription(currentProblem); + updateData(currentProblem); }, [currentProblem], ); diff --git a/src/modules/Results/index.module.less b/src/modules/Results/index.module.less index 9c9bd56..5a50b97 100644 --- a/src/modules/Results/index.module.less +++ b/src/modules/Results/index.module.less @@ -99,19 +99,29 @@ color: rgb(var(--red-6)); } .result-error-info { - width: 100%; height: auto; - padding: 8px 16px; - box-sizing: border-box; - border-radius: 8px; - font-size: 16px; - color: rgb(var(--red-6)); - background-color: rgba(var(--red-6), 0.12); - .result-error-item { - padding: 4px 0; - } } } + .result-error-info { + width: 100%; + height: auto; + padding: 8px 16px; + box-sizing: border-box; + border-radius: 8px; + font-size: 16px; + color: rgb(var(--red-6)); + background-color: rgba(var(--red-6), 0.12); + .result-error-item { + height: auto; + padding: 4px 0; + } + } + .result-pass { + width: 100%; + height: auto; + font-size: 24px; + color: rgb(var(--green-6)); + } } .footer { width: 100%; diff --git a/src/modules/Results/index.tsx b/src/modules/Results/index.tsx index e97094d..daaa3dc 100644 --- a/src/modules/Results/index.tsx +++ b/src/modules/Results/index.tsx @@ -7,8 +7,15 @@ import { CustomTabs } from '@src/components/CustomTabs'; import localCache, { PROBLEM_STATUS } from '@src/utils/local-cache'; import emitter from '@src/utils/emit'; import Context from '@src/utils/context'; -import { NULL_CASE, Problem } from '@src/utils/problems'; +import { + getProblemTestRaw, + NULL_CASE, + Problem, + ProblemFiles, + ProblemTestReplaceVal, +} from '@src/utils/problems'; import i18nJson from '@config/i18n.json'; +import { validateMonacoModel } from '@src/utils/validate-monaco-model'; import styles from './index.module.less'; const enum MainTab { @@ -16,6 +23,14 @@ const enum MainTab { result = 'result', } +function formatErrorFromMarkers(markers: editor.IMarker[]) { + return markers.map(function (maker) { + return `${maker.resource.path}:${maker.startLineNumber}:${ + maker.startColumn + }: error: ${maker.code ? `TS${maker.code}: ` : ''}${maker.message}`; + }); +} + const Results = function () { const [{ currentProblem, setting }] = useContext(Context); const [loading, setLoading] = useState(true); @@ -24,14 +39,95 @@ const Results = function () { const { key, cases: originCases = [] } = currentProblem; const noCases = useMemo(() => originCases.length === 0, [originCases]); const [cases, setCases] = useState(noCases ? [NULL_CASE] : originCases); + const [testRaw, setTestRaw] = useState(undefined); + const [model, setModel] = useState(undefined); + const [casesErrors, setCasesErrors] = useState([]); + + const updateData = useCallback( + debounce(async function (problem: Problem) { + const raw = await getProblemTestRaw(problem); + setTestRaw(raw); + setStatus([]); + setCasesErrors([]); + setCases(problem.cases || [NULL_CASE]); + setActiveMainTab(MainTab.cases); + setLoading(false); + }, 500), + [], + ); + + useEffect( + function () { + setLoading(true); + updateData(currentProblem); + }, + [currentProblem], + ); - function onSubmit() { - const markers = editor.getModelMarkers({}); - const errors = markers.map(function (maker) { - return `${maker.resource.path}:${maker.startLineNumber}:${ - maker.startColumn - }: error: ${maker.code ? `TS${maker.code}: ` : ''}${maker.message}`; + useEffect( + function () { + if (testRaw === undefined) { + setModel(undefined); + } else { + const uri = Uri.file(`${currentProblem.key}/${ProblemFiles.test}`); + const m = + editor.getModel(uri) || editor.createModel(testRaw, undefined, uri); + setModel(m); + } + return function () { + model?.dispose(); + }; + }, + [testRaw], + ); + + async function run() { + setLoading(true); + setActiveMainTab(MainTab.result); + setStatus([]); + await new Promise(resolve => setTimeout(resolve, 500)); + const templateUri = Uri.file( + `${currentProblem.key}/${ProblemFiles.template}`, + ); + const markers = editor.getModelMarkers({ + resource: templateUri, }); + const errors = formatErrorFromMarkers(markers); + if (errors.length > 0) { + setStatus(errors); + } else { + const e = []; + if (model && testRaw) { + for (const { source, target } of cases) { + const content = testRaw + .replace(ProblemTestReplaceVal.source, source) + .replace(ProblemTestReplaceVal.target, target); + model.setValue(content); + await validateMonacoModel(model); + const markers = editor.getModelMarkers({ + resource: Uri.file(`${currentProblem.key}/${ProblemFiles.test}`), + }); + const errors = formatErrorFromMarkers(markers); + e.push(errors); + } + setCasesErrors(e); + } + } + setLoading(false); + } + + async function onSubmit() { + setLoading(true); + await new Promise(resolve => setTimeout(resolve, 500)); + const markers = [ + ...editor.getModelMarkers({ + resource: Uri.file(`${currentProblem.key}/${ProblemFiles.template}`), + }), + ...editor.getModelMarkers({ + resource: Uri.file(`${currentProblem.key}/${ProblemFiles.check}`), + }), + ]; + const errors = formatErrorFromMarkers(markers); setStatus(errors.length > 0 ? errors : 'Accept!'); const status = errors.length > 0 ? PROBLEM_STATUS.unAccepted : PROBLEM_STATUS.accepted; @@ -48,23 +144,71 @@ const Results = function () { }); emitter.emit('submit-code'); setActiveMainTab(MainTab.result); + setLoading(false); } - const updateCases = useCallback( - debounce(function (cases: Problem['cases']) { - setCases(cases || [NULL_CASE]); - setLoading(false); - }, 500), - [], - ); - - useEffect( + const resultContent = useMemo( function () { - setLoading(true); - setStatus([]); - updateCases(currentProblem.cases); + if (typeof status === 'string') { + return
Accepted!
; + } else if (Array.isArray(status) && status.length > 0) { + return ( +
+
+ Compilation Error +
+
+ {status.map(function (error) { + return ( +
+ {error} +
+ ); + })} +
+
+ ); + } else if (casesErrors.length > 0) { + return ( + + {cases.map(function (_, index) { + const result = casesErrors[index]; + return ( + + {result.length > 0 && ( +
+ {result.map(function (error) { + return ( +
+ {error} +
+ ); + })} +
+ )} + {result.length === 0 && ( +
Pass!
+ )} +
+ ); + })} +
+ ); + } else { + return ( +
+ {i18nJson['please_run_or_submit_first'][setting.language]} +
+ ); + } }, - [currentProblem], + [cases, casesErrors, status], ); return ( @@ -114,34 +258,11 @@ const Results = function () { key={MainTab.result} title={i18nJson[MainTab.result][setting.language]} > - {Array.isArray(status) && status.length === 0 && ( -
- {i18nJson['please_run_or_submit_first'][setting.language]} -
- )} - {typeof status === 'string' && ( -
{'Accepted!'}
- )} - {Array.isArray(status) && status.length > 0 && ( -
-
- Compilation Error -
-
- {status.map(function (error) { - return ( -
- {error} -
- ); - })} -
-
- )} + {resultContent}
-