Skip to content
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
6 changes: 6 additions & 0 deletions app/terminal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ EditorComponent コンポーネントを提供します。

実行結果はEmbedContextに送信されます。

## page.tsx, tests.ts

ブラウザーで localhost:3000/terminal を開くと、各実行環境のテストを行います。

ランタイムを追加した場合、ここにテストケースを追加してください。

## 各言語の実装

### Pyodide (Python)
Expand Down
102 changes: 102 additions & 0 deletions app/terminal/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";
import { Heading } from "@/[docs_id]/markdown";
import "mocha/mocha.js";
import "mocha/mocha.css";
import { useEffect, useRef, useState } from "react";
import { usePyodide } from "./python/runtime";
import { useWandbox } from "./wandbox/runtime";
import { RuntimeContext, RuntimeLang } from "./runtime";
import { useEmbedContext } from "./embedContext";
import { defineTests } from "./tests";

export default function RuntimeTestPage() {
const pyodide = usePyodide();
const wandboxCpp = useWandbox("cpp");
const runtimeRef = useRef<Record<RuntimeLang, RuntimeContext>>(null!);
runtimeRef.current = {
python: pyodide,
cpp: wandboxCpp,
};
const { files, writeFile } = useEmbedContext();
const filesRef = useRef<Record<string, string>>({});
filesRef.current = files;
const [mochaState, setMochaState] = useState<"idle" | "running" | "finished">(
"idle"
);
const [searchParams, setSearchParams] = useState<string>("");
useEffect(() => {
setSearchParams(window.location.search);
}, []);

const runTest = () => {
setMochaState("running");

mocha.setup("bdd");

for (const lang of Object.keys(runtimeRef.current) as RuntimeLang[]) {
defineTests(lang, runtimeRef, writeFile);
}

const runner = mocha.run();
runner.on("end", () => {
setMochaState("finished");
});
};

return (
<div className="p-4 mx-auto w-full max-w-200">
<Heading level={1}>Runtime Test Page</Heading>
<div className="border-1 border-transparent translate-x-0">
{/* margin collapseさせない & fixedの対象をviewportではなくこのdivにする */}
{mochaState === "idle" ? (
<button className="btn btn-primary mt-4" onClick={runTest}>
テストを実行
</button>
) : mochaState === "running" ? (
<div className="alert mt-16 sm:mt-4 w-80">
<svg
xmlns="http://www.w3.org/2000/svg"
className="animate-spin h-5 w-5 mr-3 border-2 border-solid border-current border-t-transparent rounded-full"
fill="none"
viewBox="0 0 24 24"
></svg>
テストを実行中です...
</div>
) : (
<div className="alert mt-16 sm:mt-4 w-80">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="stroke-info h-6 w-6 shrink-0"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
テストが完了しました
</div>
)}
<p className="mt-8">
{new URLSearchParams(searchParams).has("grep") && (
<>
一部のテストだけを実行します:
<code className="ml-2 font-mono">
{new URLSearchParams(searchParams).get("grep")}
</code>
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
<a className="ml-4 link link-info" href="/terminal">
{/* aタグでページをリロードしないと動作しない。 */}
フィルタを解除
</a>
</>
)}
</p>
<div className="m-0!" id="mocha" />
</div>
</div>
);
}
212 changes: 212 additions & 0 deletions app/terminal/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { expect } from "chai";
import { RefObject } from "react";
import { emptyMutex, RuntimeContext, RuntimeLang } from "./runtime";

export function defineTests(
lang: RuntimeLang,
runtimeRef: RefObject<Record<RuntimeLang, RuntimeContext>>,
writeFile: (name: string, content: string) => void
) {
describe(`${lang} Runtime`, function () {
this.timeout(
(
{
python: 2000,
cpp: 10000,
} as Record<RuntimeLang, number>
)[lang]
);

beforeEach(async function () {
this.timeout(60000);
while (!runtimeRef.current[lang].ready) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
});

describe("REPL", function () {
it("should capture stdout", async function () {
const msg = "Hello, World!";
const printCode = (
{
python: `print("${msg}")`,
cpp: null,
} satisfies Record<RuntimeLang, string | null>
)[lang];
if (!printCode) {
this.skip();
}
const result = await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(() => runtimeRef.current[lang].runCommand!(printCode));
console.log(`${lang} REPL stdout test: `, result);
expect(result).to.be.deep.equal([
{
type: "stdout",
message: msg,
},
]);
});

it("should preserve variables across commands", async function () {
const varName = "testVar";
const value = 42;
const [setIntVarCode, printIntVarCode] = (
{
python: [`${varName} = ${value}`, `print(${varName})`],
cpp: [null, null],
} satisfies Record<RuntimeLang, string[] | null[]>
)[lang];
if (!setIntVarCode || !printIntVarCode) {
this.skip();
}
const result = await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(async () => {
await runtimeRef.current[lang].runCommand!(setIntVarCode);
return runtimeRef.current[lang].runCommand!(printIntVarCode);
});
console.log(`${lang} REPL variable preservation test: `, result);
expect(result).to.be.deep.equal([
{
type: "stdout",
message: value.toString(),
},
]);
});

it("should capture errors", async function () {
const errorMsg = "This is a test error.";
const errorCode = (
{
python: `raise Exception("${errorMsg}")`,
cpp: null,
} satisfies Record<RuntimeLang, string | null>
)[lang];
if (!errorCode) {
this.skip();
}
const result = await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(() => runtimeRef.current[lang].runCommand!(errorCode));
console.log(`${lang} REPL error capture test: `, result);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be
.empty;
});

it("should be able to be interrupted and recover state", async function () {
const [setIntVarCode, infLoopCode, printIntVarCode] = (
{
python: [`testVar = 42`, `while True:\n pass`, `print(testVar)`],
cpp: [null, null, null],
} satisfies Record<RuntimeLang, (string | null)[]>
)[lang];
if (!setIntVarCode || !infLoopCode || !printIntVarCode) {
this.skip();
}
const result = await (
runtimeRef.current[lang].mutex || emptyMutex
).runExclusive(async () => {
await runtimeRef.current[lang].runCommand!(setIntVarCode);
const runPromise = runtimeRef.current[lang].runCommand!(infLoopCode);
// Wait a bit to ensure the infinite loop has started
await new Promise((resolve) => setTimeout(resolve, 1000));
runtimeRef.current[lang].interrupt!();
await runPromise;
return runtimeRef.current[lang].runCommand!(printIntVarCode);
});
console.log(`${lang} REPL interrupt recovery test: `, result);
expect(result).to.be.deep.equal([
{
type: "stdout",
message: "42",
},
]);
});
});

describe("File Execution", function () {
it("should capture stdout", async function () {
const msg = "Hello from file!";
const [filename, code] = (
{
python: ["test.py", `print("${msg}")`],
cpp: [
"test.cpp",
`#include <iostream>\nint main() {\n std::cout << "${msg}" << std::endl;\n return 0;\n}\n`,
],
} satisfies Record<RuntimeLang, [string, string]>
)[lang];
writeFile(filename, code);
// use setTimeout to wait for writeFile to propagate.
await new Promise((resolve) => setTimeout(resolve, 100));
const result = await runtimeRef.current[lang].runFiles([filename]);
console.log(`${lang} single file stdout test: `, result);
expect(result).to.be.deep.equal([
{
type: "stdout",
message: msg,
},
]);
});

it("should capture errors", async function () {
const errorMsg = "This is a test error";
const [filename, code] = (
{
python: ["test_error.py", `raise Exception("${errorMsg}")\n`],
cpp: [
"test_error.cpp",
`#include <stdexcept>\nint main() {\n throw std::runtime_error("${errorMsg}");\n return 0;\n}\n`,
],
} satisfies Record<RuntimeLang, [string, string]>
)[lang];
writeFile(filename, code);
await new Promise((resolve) => setTimeout(resolve, 100));
const result = await runtimeRef.current[lang].runFiles([filename]);
console.log(`${lang} single file error capture test: `, result);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(result.filter((r) => r.message.includes(errorMsg))).to.not.be
.empty;
});

it("should capture stdout from multiple files", async function () {
const msg = "Hello from multifile!";
const [codes, execFiles] = (
{
python: [
{
"test_multi_main.py":
"from test_multi_sub import print_message\nprint_message()\n",
"test_multi_sub.py": `def print_message():\n print("${msg}")\n`,
},
["test_multi_main.py"],
],
cpp: [
{
"test_multi_main.cpp":
'#include "test_multi_sub.h"\nint main() {\n print_message();\n return 0;\n}\n',
"test_multi_sub.h": "void print_message();\n",
"test_multi_sub.cpp": `#include <iostream>\nvoid print_message() {\n std::cout << "${msg}" << std::endl;\n}\n`,
},
["test_multi_main.cpp", "test_multi_sub.cpp"],
],
} satisfies Record<RuntimeLang, [Record<string, string>, string[]]>
)[lang];
for (const [filename, code] of Object.entries(codes)) {
writeFile(filename, code);
}
await new Promise((resolve) => setTimeout(resolve, 100));
const result = await runtimeRef.current[lang].runFiles(execFiles);
console.log(`${lang} multifile stdout test: `, result);
expect(result).to.be.deep.equal([
{
type: "stdout",
message: msg,
},
]);
});
});
});
}
Loading