Skip to content

Commit

Permalink
test: VvppManagerのテスト追加 (#2547)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hiroshiba authored Feb 20, 2025
1 parent 914d815 commit 43142b2
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 88 deletions.
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 @@ -22,37 +22,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

0 comments on commit 43142b2

Please sign in to comment.