Skip to content

Commit 81dd2cd

Browse files
committed
テストを記述
1 parent 02b5982 commit 81dd2cd

File tree

5 files changed

+755
-14
lines changed

5 files changed

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

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: 5000,
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)