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

test: VvppManagerのテスト追加 #2547

Merged
merged 2 commits into from
Feb 20, 2025
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
2 changes: 1 addition & 1 deletion src/backend/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ initializeEngineInfoManager({
vvppEngineDir,
});
initializeEngineProcessManager({ onEngineProcessError });
initializeVvppManager({ vvppEngineDir });
initializeVvppManager({ vvppEngineDir, tmpDir: app.getPath("temp") });

const configManager = getConfigManager();
const windowManager = getWindowManager();
Expand Down
48 changes: 32 additions & 16 deletions src/backend/electron/manager/vvppManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { moveFile } from "move-file";
import { app, dialog } from "electron";
import { dialog } from "electron";
import AsyncLock from "async-lock";
import {
EngineId,
Expand All @@ -12,11 +12,10 @@ import { errorToMessage } from "@/helpers/errorHelper";
import { VvppFileExtractor } from "@/backend/electron/vvppFile";
import { ProgressCallback } from "@/helpers/progressHelper";
import { createLogger } from "@/helpers/log";
import { isWindows } from "@/helpers/platform";

const log = createLogger("VvppManager");

const isNotWin = process.platform !== "win32";

export const isVvppFile = (filePath: string) => {
return (
path.extname(filePath) === ".vvpp" || path.extname(filePath) === ".vvppp"
Expand Down Expand Up @@ -50,15 +49,17 @@ const lockKey = "lock-key-for-vvpp-manager";
//
// エンジンを停止してからではないとディレクトリを削除できないため、このような実装になっている。
export class VvppManager {
vvppEngineDir: string;
private vvppEngineDir: string;
private tmpDir: string;

willDeleteEngineIds: Set<EngineId>;
willReplaceEngineDirs: { from: string; to: string }[];
private willDeleteEngineIds: Set<EngineId>;
private willReplaceEngineDirs: { from: string; to: string }[];

private lock = new AsyncLock();

constructor({ vvppEngineDir }: { vvppEngineDir: string }) {
this.vvppEngineDir = vvppEngineDir;
constructor(params: { vvppEngineDir: string; tmpDir: string }) {
this.vvppEngineDir = params.vvppEngineDir;
this.tmpDir = params.tmpDir;
this.willDeleteEngineIds = new Set();
this.willReplaceEngineDirs = [];
}
Expand All @@ -79,6 +80,10 @@ export class VvppManager {
return `${manifest.name.replace(/[\s<>:"/\\|?*]+/g, "_")}+${manifest.uuid}`;
}

buildEngineDirPath(manifest: MinimumEngineManifestType) {
return path.join(this.vvppEngineDir, this.toValidDirName(manifest));
}

isEngineDirName(dir: string, manifest: MinimumEngineManifestType) {
return dir.endsWith(`+${manifest.uuid}`);
}
Expand Down Expand Up @@ -113,10 +118,13 @@ export class VvppManager {
vvppPath: string,
callbacks?: { onProgress?: ProgressCallback },
) {
const { outputDir, manifest } = await new VvppFileExtractor({
const tmpEngineDir = this.buildTemporaryEngineDir(this.vvppEngineDir);
log.info("Extracting vvpp to", tmpEngineDir);

const manifest = await new VvppFileExtractor({
vvppLikeFilePath: vvppPath,
vvppEngineDir: this.vvppEngineDir,
tmpDir: app.getPath("temp"),
outputDir: tmpEngineDir,
tmpDir: this.tmpDir,
callbacks,
}).extract();

Expand All @@ -128,18 +136,23 @@ export class VvppManager {
return this.isEngineDirName(dir, manifest);
});
if (oldEngineDirName) {
this.markWillMove(outputDir, dirName);
this.markWillMove(tmpEngineDir, dirName);
} else {
await moveFile(outputDir, engineDirectory);
await moveFile(tmpEngineDir, engineDirectory);
}
if (isNotWin) {
if (!isWindows) {
await fs.promises.chmod(
path.join(engineDirectory, manifest.command),
"755",
);
}
}

private buildTemporaryEngineDir(vvppEngineDir: string): string {
const nonce = new Date().getTime().toString();
return path.join(vvppEngineDir, ".tmp", nonce);
}

async handleMarkedEngineDirs() {
await this.lock.acquire(lockKey, () => this._handleMarkedEngineDirs());
}
Expand Down Expand Up @@ -218,8 +231,11 @@ export default VvppManager;

let manager: VvppManager | undefined;

export function initializeVvppManager(payload: { vvppEngineDir: string }) {
manager = new VvppManager(payload);
export function initializeVvppManager(params: {
vvppEngineDir: string;
tmpDir: string;
}) {
manager = new VvppManager(params);
}

export function getVvppManager() {
Expand Down
20 changes: 5 additions & 15 deletions src/backend/electron/vvppFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,27 @@ type Format = "zip" | "7z";
/** VVPPファイルを vvppEngineDir で指定したディレクトリ以下の .tmp ディレクトリに展開する */
export class VvppFileExtractor {
private readonly vvppLikeFilePath: string;
private readonly outputDir: string;
private readonly tmpDir: string;
private readonly callbacks?: { onProgress?: ProgressCallback };

private readonly outputDir: string;

constructor(params: {
vvppLikeFilePath: string;
vvppEngineDir: string;
outputDir: string;
tmpDir: string;
callbacks?: { onProgress?: ProgressCallback };
}) {
this.vvppLikeFilePath = params.vvppLikeFilePath;
this.outputDir = params.outputDir;
this.tmpDir = params.tmpDir;
this.callbacks = params.callbacks;

this.outputDir = this.buildOutputDirPath(params.vvppEngineDir);
}

private buildOutputDirPath(vvppEngineDir: string): string {
const nonce = new Date().getTime().toString();
return path.join(vvppEngineDir, ".tmp", nonce);
}

async extract(): Promise<{
outputDir: string;
manifest: MinimumEngineManifestType;
}> {
async extract(): Promise<MinimumEngineManifestType> {
log.info("Extracting vvpp to", this.outputDir);
const archiveFileParts = await this.getArchiveFileParts();
const manifest = await this.extractOrCleanup(archiveFileParts);
return { outputDir: this.outputDir, manifest };
return manifest;
}

/** VVPPファイルが分割されている場合、それらのファイルを取得する */
Expand Down
122 changes: 101 additions & 21 deletions tests/unit/backend/electron/VvppManager.node.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,117 @@
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import fs from "fs";
import os from "os";
import path from "path";
import { beforeEach, expect, test } from "vitest";
import { createVvppFile } from "./helper";
import { EngineId, MinimumEngineManifestType } from "@/type/preload";
import VvppManager from "@/backend/electron/manager/vvppManager";
import { uuid4 } from "@/helpers/random";

const dummyMinimumManifest: MinimumEngineManifestType = {
name: "Test Engine",
uuid: EngineId("295c656b-b800-449f-aee6-b03e493816d7"),
command: "",
port: 5021,
supported_features: {},
};

interface VvppManagerTestContext {
interface Context {
vvppEngineDir: string;
manager: VvppManager;
}

beforeEach<VvppManagerTestContext>(async (context) => {
const vvppEngineDir = await mkdtemp(join(tmpdir(), "vvppTest"));
context.manager = new VvppManager({ vvppEngineDir });
let tmpDir: string;
beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), uuid4()));
});
afterAll(() => {
fs.rmdirSync(tmpDir, { recursive: true });
});

return async () => {
await rm(vvppEngineDir, { recursive: true });
};
beforeEach<Context>(async (context) => {
context.vvppEngineDir = path.join(tmpDir, uuid4());
context.manager = new VvppManager({
vvppEngineDir: context.vvppEngineDir,
tmpDir,
});
});

test<VvppManagerTestContext>("追加エンジンのディレクトリ名は想定通りか", ({
manager,
}) => {
test<Context>("追加エンジンのディレクトリ名は想定通りか", ({ manager }) => {
const dummyMinimumManifest: MinimumEngineManifestType = {
name: "Test Engine",
uuid: EngineId("295c656b-b800-449f-aee6-b03e493816d7"),
command: "",
port: 5021,
supported_features: {},
};

const dirName = manager.toValidDirName(dummyMinimumManifest);
// NOTE: パターンを変更する場合アンインストーラーのコードを変更する必要がある
const pattern = /^.+\+.{8}-.{4}-.{4}-.{4}-.{12}$/;

expect(dirName).toMatch(pattern);
});

test<Context>("エンジンをインストールできる", async ({
vvppEngineDir,
manager,
}) => {
const targetName = "perfect.vvpp";
const vvppFilePath = await createVvppFile(targetName, tmpDir);

await manager.install(vvppFilePath);
expect(getEngineDirInfos(vvppEngineDir).length).toBe(1);
});

test<Context>("エンジンを2回インストールすると処理が予約され、後で上書きされる", async ({
vvppEngineDir,
manager,
}) => {
const targetName = "perfect.vvpp";
const vvppFilePath = await createVvppFile(targetName, tmpDir);

await manager.install(vvppFilePath);
const infos1 = getEngineDirInfos(vvppEngineDir);
expect(infos1.length).toBe(1);

await manager.install(vvppFilePath);
const infos2 = getEngineDirInfos(vvppEngineDir);
expect(infos2.length).toBe(1);
expect(infos1[0].createdTime).toBe(infos2[0].createdTime); // 同じファイル

await manager.handleMarkedEngineDirs();
const infos3 = getEngineDirInfos(vvppEngineDir);
expect(infos3.length).toBe(1);
expect(infos1[0].createdTime).not.toBe(infos3[0].createdTime); // 別のファイル
});

test<Context>("エンジンをアンインストール予約すると、後で削除される", async ({
vvppEngineDir,
manager,
}) => {
const targetName = "perfect.vvpp";
const targetUuid = EngineId("00000000-0000-0000-0000-000000000001");
const vvppFilePath = await createVvppFile(targetName, tmpDir);

await manager.install(vvppFilePath);
const infos1 = getEngineDirInfos(vvppEngineDir);
expect(infos1.length).toBe(1);

manager.markWillDelete(targetUuid);
const infos2 = getEngineDirInfos(vvppEngineDir);
expect(infos2.length).toBe(1);

await manager.handleMarkedEngineDirs();
const infos3 = getEngineDirInfos(vvppEngineDir);
expect(infos3.length).toBe(0);
});

/**
* インストールされているエンジンディレクトリの情報を取得する
*/
export function getEngineDirInfos(vvppEngineDir: string) {
const files = fs.readdirSync(vvppEngineDir, {
recursive: true,
withFileTypes: true,
});
const notTmpFiles = files.filter((file) => !file.parentPath.includes(".tmp"));
const manifestFiles = notTmpFiles.filter(
(file) => path.basename(file.name) === "engine_manifest.json",
);
const infos = manifestFiles.map((file) => ({
createdTime: fs.statSync(path.join(file.parentPath, file.name)).ctimeMs,
}));
return infos;
}
19 changes: 19 additions & 0 deletions tests/unit/backend/electron/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { exec } from "child_process";
import { promisify } from "util";
import path from "@/helpers/path";
import { uuid4 } from "@/helpers/random";

/** テスト用のVVPPファイルを作成する */
export async function createVvppFile(targetName: string, tmpDir: string) {
const sourceDir = path.join(__dirname, "vvpps", targetName);
const outputFilePath = path.join(tmpDir, uuid4() + targetName);
await createZipFile(sourceDir, outputFilePath);
return outputFilePath;
}

/** 7zを使って指定したフォルダからzipファイルを作成する */
async function createZipFile(sourceDir: string, outputFilePath: string) {
const sevenZipBin = import.meta.env.VITE_7Z_BIN_NAME;
const command = `"${sevenZipBin}" a -tzip "${outputFilePath}" "${path.join(sourceDir, "*")}"`;
await promisify(exec)(command);
}
Loading
Loading