Skip to content

Commit 6594b8b

Browse files
authored
Merge pull request #90 from ut-code/runtime-test
テストを記述
2 parents 02b5982 + 2553497 commit 6594b8b

File tree

5 files changed

+759
-14
lines changed

5 files changed

+759
-14
lines changed

app/terminal/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ EditorComponent コンポーネントを提供します。
114114
115115
実行結果はEmbedContextに送信されます。
116116
117+
## page.tsx, tests.ts
118+
119+
ブラウザーで localhost:3000/terminal を開くと、各実行環境のテストを行います。
120+
121+
ランタイムを追加した場合、ここにテストケースを追加してください。
122+
117123
## 各言語の実装
118124
119125
### Pyodide (Python)

app/terminal/page.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
import { Heading } from "@/[docs_id]/markdown";
3+
import "mocha/mocha.js";
4+
import "mocha/mocha.css";
5+
import { useEffect, useRef, useState } from "react";
6+
import { usePyodide } from "./python/runtime";
7+
import { useWandbox } from "./wandbox/runtime";
8+
import { RuntimeContext, RuntimeLang } from "./runtime";
9+
import { useEmbedContext } from "./embedContext";
10+
import { defineTests } from "./tests";
11+
12+
export default function RuntimeTestPage() {
13+
const pyodide = usePyodide();
14+
const wandboxCpp = useWandbox("cpp");
15+
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
16+
runtimeRef.current = {
17+
python: pyodide,
18+
cpp: wandboxCpp,
19+
};
20+
const { files, writeFile } = useEmbedContext();
21+
const filesRef = useRef<Record<string, string>>({});
22+
filesRef.current = files;
23+
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
24+
"idle"
25+
);
26+
const [searchParams, setSearchParams] = useState<string>("");
27+
useEffect(() => {
28+
setSearchParams(window.location.search);
29+
}, []);
30+
31+
const runTest = () => {
32+
setMochaState("running");
33+
34+
mocha.setup("bdd");
35+
36+
for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) {
37+
defineTests(lang, runtimeRef, writeFile);
38+
}
39+
40+
const runner = mocha.run();
41+
runner.on("end", () => {
42+
setMochaState("finished");
43+
});
44+
};
45+
46+
return (
47+
<div className="p-4 mx-auto w-full max-w-200">
48+
<Heading level={1}>Runtime Test Page</Heading>
49+
<div className="border-1 border-transparent translate-x-0">
50+
{/* margin collapseさせない & fixedの対象をviewportではなくこのdivにする */}
51+
{mochaState === "idle" ? (
52+
<button className="btn btn-primary mt-4" onClick={runTest}>
53+
テストを実行
54+
</button>
55+
) : mochaState === "running" ? (
56+
<div className="alert mt-16 sm:mt-4 w-80">
57+
<svg
58+
xmlns="http://www.w3.org/2000/svg"
59+
className="animate-spin h-5 w-5 mr-3 border-2 border-solid border-current border-t-transparent rounded-full"
60+
fill="none"
61+
viewBox="0 0 24 24"
62+
></svg>
63+
テストを実行中です...
64+
</div>
65+
) : (
66+
<div className="alert mt-16 sm:mt-4 w-80">
67+
<svg
68+
xmlns="http://www.w3.org/2000/svg"
69+
fill="none"
70+
viewBox="0 0 24 24"
71+
className="stroke-info h-6 w-6 shrink-0"
72+
>
73+
<path
74+
strokeLinecap="round"
75+
strokeLinejoin="round"
76+
strokeWidth="2"
77+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
78+
></path>
79+
</svg>
80+
テストが完了しました
81+
</div>
82+
)}
83+
<p className="mt-8">
84+
{new URLSearchParams(searchParams).has("grep") && (
85+
<>
86+
一部のテストだけを実行します:
87+
<code className="ml-2 font-mono">
88+
{new URLSearchParams(searchParams).get("grep")}
89+
</code>
90+
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
91+
<a className="ml-4 link link-info" href="/terminal">
92+
{/* aタグでページをリロードしないと動作しない。 */}
93+
フィルタを解除
94+
</a>
95+
</>
96+
)}
97+
</p>
98+
<div className="m-0!" id="mocha" />
99+
</div>
100+
</div>
101+
);
102+
}

app/terminal/tests.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { expect } from "chai";
2+
import { RefObject } from "react";
3+
import { emptyMutex, RuntimeContext, RuntimeLang } from "./runtime";
4+
5+
export function defineTests(
6+
lang: RuntimeLang,
7+
runtimeRef: RefObject<Record<RuntimeLang, RuntimeContext>>,
8+
writeFile: (name: string, content: string) => void
9+
) {
10+
describe(`${lang} Runtime`, function () {
11+
this.timeout(
12+
(
13+
{
14+
python: 2000,
15+
cpp: 10000,
16+
} as Record<RuntimeLang, number>
17+
)[lang]
18+
);
19+
20+
beforeEach(async function () {
21+
this.timeout(60000);
22+
while (!runtimeRef.current[lang].ready) {
23+
await new Promise((resolve) => setTimeout(resolve, 100));
24+
}
25+
});
26+
27+
describe("REPL", function () {
28+
it("should capture stdout", async function () {
29+
const msg = "Hello, World!";
30+
const printCode = (
31+
{
32+
python: `print("${msg}")`,
33+
cpp: null,
34+
} satisfies Record<RuntimeLang, string | null>
35+
)[lang];
36+
if (!printCode) {
37+
this.skip();
38+
}
39+
const result = await (
40+
runtimeRef.current[lang].mutex || emptyMutex
41+
).runExclusive(() => runtimeRef.current[lang].runCommand!(printCode));
42+
console.log(`${lang} REPL stdout test: `, result);
43+
expect(result).to.be.deep.equal([
44+
{
45+
type: "stdout",
46+
message: msg,
47+
},
48+
]);
49+
});
50+
51+
it("should preserve variables across commands", async function () {
52+
const varName = "testVar";
53+
const value = 42;
54+
const [setIntVarCode, printIntVarCode] = (
55+
{
56+
python: [`${varName} = ${value}`, `print(${varName})`],
57+
cpp: [null, null],
58+
} satisfies Record<RuntimeLang, string[] | null[]>
59+
)[lang];
60+
if (!setIntVarCode || !printIntVarCode) {
61+
this.skip();
62+
}
63+
const result = await (
64+
runtimeRef.current[lang].mutex || emptyMutex
65+
).runExclusive(async () => {
66+
await runtimeRef.current[lang].runCommand!(setIntVarCode);
67+
return runtimeRef.current[lang].runCommand!(printIntVarCode);
68+
});
69+
console.log(`${lang} REPL variable preservation test: `, result);
70+
expect(result).to.be.deep.equal([
71+
{
72+
type: "stdout",
73+
message: value.toString(),
74+
},
75+
]);
76+
});
77+
78+
it("should capture errors", async function () {
79+
const errorMsg = "This is a test error.";
80+
const errorCode = (
81+
{
82+
python: `raise Exception("${errorMsg}")`,
83+
cpp: null,
84+
} satisfies Record<RuntimeLang, string | null>
85+
)[lang];
86+
if (!errorCode) {
87+
this.skip();
88+
}
89+
const result = await (
90+
runtimeRef.current[lang].mutex || emptyMutex
91+
).runExclusive(() => runtimeRef.current[lang].runCommand!(errorCode));
92+
console.log(`${lang} REPL error capture test: `, result);
93+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
94+
expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be
95+
.empty;
96+
});
97+
98+
it("should be able to be interrupted and recover state", async function () {
99+
const [setIntVarCode, infLoopCode, printIntVarCode] = (
100+
{
101+
python: [`testVar = 42`, `while True:\n pass`, `print(testVar)`],
102+
cpp: [null, null, null],
103+
} satisfies Record<RuntimeLang, (string | null)[]>
104+
)[lang];
105+
if (!setIntVarCode || !infLoopCode || !printIntVarCode) {
106+
this.skip();
107+
}
108+
const result = await (
109+
runtimeRef.current[lang].mutex || emptyMutex
110+
).runExclusive(async () => {
111+
await runtimeRef.current[lang].runCommand!(setIntVarCode);
112+
const runPromise = runtimeRef.current[lang].runCommand!(infLoopCode);
113+
// Wait a bit to ensure the infinite loop has started
114+
await new Promise((resolve) => setTimeout(resolve, 1000));
115+
runtimeRef.current[lang].interrupt!();
116+
await runPromise;
117+
return runtimeRef.current[lang].runCommand!(printIntVarCode);
118+
});
119+
console.log(`${lang} REPL interrupt recovery test: `, result);
120+
expect(result).to.be.deep.equal([
121+
{
122+
type: "stdout",
123+
message: "42",
124+
},
125+
]);
126+
});
127+
});
128+
129+
describe("File Execution", function () {
130+
it("should capture stdout", async function () {
131+
const msg = "Hello from file!";
132+
const [filename, code] = (
133+
{
134+
python: ["test.py", `print("${msg}")`],
135+
cpp: [
136+
"test.cpp",
137+
`#include <iostream>\nint main() {\n std::cout << "${msg}" << std::endl;\n return 0;\n}\n`,
138+
],
139+
} satisfies Record<RuntimeLang, [string, string]>
140+
)[lang];
141+
writeFile(filename, code);
142+
// use setTimeout to wait for writeFile to propagate.
143+
await new Promise((resolve) => setTimeout(resolve, 100));
144+
const result = await runtimeRef.current[lang].runFiles([filename]);
145+
console.log(`${lang} single file stdout test: `, result);
146+
expect(result).to.be.deep.equal([
147+
{
148+
type: "stdout",
149+
message: msg,
150+
},
151+
]);
152+
});
153+
154+
it("should capture errors", async function () {
155+
const errorMsg = "This is a test error";
156+
const [filename, code] = (
157+
{
158+
python: ["test_error.py", `raise Exception("${errorMsg}")\n`],
159+
cpp: [
160+
"test_error.cpp",
161+
`#include <stdexcept>\nint main() {\n throw std::runtime_error("${errorMsg}");\n return 0;\n}\n`,
162+
],
163+
} satisfies Record<RuntimeLang, [string, string]>
164+
)[lang];
165+
writeFile(filename, code);
166+
await new Promise((resolve) => setTimeout(resolve, 100));
167+
const result = await runtimeRef.current[lang].runFiles([filename]);
168+
console.log(`${lang} single file error capture test: `, result);
169+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
170+
expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be
171+
.empty;
172+
});
173+
174+
it("should capture stdout from multiple files", async function () {
175+
const msg = "Hello from multifile!";
176+
const [codes, execFiles] = (
177+
{
178+
python: [
179+
{
180+
"test_multi_main.py":
181+
"from test_multi_sub import print_message\nprint_message()\n",
182+
"test_multi_sub.py": `def print_message():\n print("${msg}")\n`,
183+
},
184+
["test_multi_main.py"],
185+
],
186+
cpp: [
187+
{
188+
"test_multi_main.cpp":
189+
'#include "test_multi_sub.h"\nint main() {\n print_message();\n return 0;\n}\n',
190+
"test_multi_sub.h": "void print_message();\n",
191+
"test_multi_sub.cpp": `#include <iostream>\nvoid print_message() {\n std::cout << "${msg}" << std::endl;\n}\n`,
192+
},
193+
["test_multi_main.cpp", "test_multi_sub.cpp"],
194+
],
195+
} satisfies Record<RuntimeLang, [Record<string, string>, string[]]>
196+
)[lang];
197+
for (const [filename, code] of Object.entries(codes)) {
198+
writeFile(filename, code);
199+
}
200+
await new Promise((resolve) => setTimeout(resolve, 100));
201+
const result = await runtimeRef.current[lang].runFiles(execFiles);
202+
console.log(`${lang} multifile stdout test: `, result);
203+
expect(result).to.be.deep.equal([
204+
{
205+
type: "stdout",
206+
message: msg,
207+
},
208+
]);
209+
});
210+
});
211+
});
212+
}

0 commit comments

Comments
 (0)