From 4048d7e4549a880f15b9df0a6421d698c6d0d3ac Mon Sep 17 00:00:00 2001 From: Hiroshiba Kazuyuki Date: Wed, 19 Feb 2025 19:41:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?test:=20VvppManager=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/electron/main.ts | 2 +- src/backend/electron/manager/vvppManager.ts | 48 ++++--- src/backend/electron/vvppFile.ts | 20 +-- .../backend/electron/VvppManager.node.spec.ts | 122 +++++++++++++++--- tests/unit/backend/electron/helper.ts | 19 +++ .../backend/electron/vvppFile.node.spec.ts | 56 ++++---- .../no_engine_id.vvpp/engine_manifest.json | 2 +- .../backend/electron/vvpps/perfect.vvpp/dummy | 1 + .../vvpps/perfect.vvpp/engine_manifest.json | 2 +- 9 files changed, 184 insertions(+), 88 deletions(-) create mode 100644 tests/unit/backend/electron/helper.ts create mode 100644 tests/unit/backend/electron/vvpps/perfect.vvpp/dummy diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index d158bad9ce..eec8c40814 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -210,7 +210,7 @@ initializeEngineInfoManager({ vvppEngineDir, }); initializeEngineProcessManager({ onEngineProcessError }); -initializeVvppManager({ vvppEngineDir }); +initializeVvppManager({ vvppEngineDir, tmpDir: app.getPath("temp") }); const configManager = getConfigManager(); const windowManager = getWindowManager(); diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index 550fb97c63..10dacf9acd 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -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, @@ -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" @@ -50,15 +49,17 @@ const lockKey = "lock-key-for-vvpp-manager"; // // エンジンを停止してからではないとディレクトリを削除できないため、このような実装になっている。 export class VvppManager { - vvppEngineDir: string; + private vvppEngineDir: string; + private tmpDir: string; - willDeleteEngineIds: Set; - willReplaceEngineDirs: { from: string; to: string }[]; + private willDeleteEngineIds: Set; + 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 = []; } @@ -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}`); } @@ -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(); @@ -128,11 +136,11 @@ 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", @@ -140,6 +148,11 @@ export class VvppManager { } } + 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()); } @@ -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() { diff --git a/src/backend/electron/vvppFile.ts b/src/backend/electron/vvppFile.ts index d3ee41981e..d62dba1e85 100644 --- a/src/backend/electron/vvppFile.ts +++ b/src/backend/electron/vvppFile.ts @@ -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 { 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ファイルが分割されている場合、それらのファイルを取得する */ diff --git a/tests/unit/backend/electron/VvppManager.node.spec.ts b/tests/unit/backend/electron/VvppManager.node.spec.ts index 780dd43657..201cd3682a 100644 --- a/tests/unit/backend/electron/VvppManager.node.spec.ts +++ b/tests/unit/backend/electron/VvppManager.node.spec.ts @@ -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(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(async (context) => { + context.vvppEngineDir = path.join(tmpDir, uuid4()); + context.manager = new VvppManager({ + vvppEngineDir: context.vvppEngineDir, + tmpDir, + }); }); -test("追加エンジンのディレクトリ名は想定通りか", ({ - manager, -}) => { +test("追加エンジンのディレクトリ名は想定通りか", ({ 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("エンジンをインストールできる", async ({ + vvppEngineDir, + manager, +}) => { + const targetName = "perfect.vvpp"; + const vvppFilePath = await createVvppFile(targetName, tmpDir); + + await manager.install(vvppFilePath); + expect(getEngineDirInfos(vvppEngineDir).length).toBe(1); +}); + +test("エンジンを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("エンジンをアンインストール予約すると、後で削除される", 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; +} diff --git a/tests/unit/backend/electron/helper.ts b/tests/unit/backend/electron/helper.ts new file mode 100644 index 0000000000..1a16fa05ca --- /dev/null +++ b/tests/unit/backend/electron/helper.ts @@ -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); +} diff --git a/tests/unit/backend/electron/vvppFile.node.spec.ts b/tests/unit/backend/electron/vvppFile.node.spec.ts index d821a7ca28..80b438f7b2 100644 --- a/tests/unit/backend/electron/vvppFile.node.spec.ts +++ b/tests/unit/backend/electron/vvppFile.node.spec.ts @@ -1,9 +1,8 @@ import os from "os"; import path from "path"; -import { exec } from "child_process"; import fs from "fs"; -import { promisify } from "util"; import { test, afterAll, beforeAll } from "vitest"; +import { createVvppFile } from "./helper"; import { VvppFileExtractor } from "@/backend/electron/vvppFile"; import { uuid4 } from "@/helpers/random"; @@ -17,36 +16,32 @@ afterAll(() => { test("正しいVVPPファイルからエンジンを切り出せる", async () => { const targetName = "perfect.vvpp"; - const sourceDir = path.join(__dirname, "vvpps", targetName); - const outputFilePath = path.join(tmpDir, uuid4() + targetName); - await createZipFile(sourceDir, outputFilePath); + const vvppFilePath = await createVvppFile(targetName, tmpDir); - const vvppEngineDir = createVvppEngineDir(); + const outputDir = createOutputDir(); await new VvppFileExtractor({ - vvppLikeFilePath: outputFilePath, - vvppEngineDir, + vvppLikeFilePath: vvppFilePath, + outputDir, tmpDir, }).extract(); - expectManifestExists(vvppEngineDir); + assertIsEngineDir(outputDir); }); test("分割されたVVPPファイルからエンジンを切り出せる", async () => { const targetName = "perfect.vvpp"; - const sourceDir = path.join(__dirname, "vvpps", targetName); - const outputFilePath = path.join(tmpDir, uuid4() + targetName); - await createZipFile(sourceDir, outputFilePath); + const vvppFilePath = await createVvppFile(targetName, tmpDir); - const outputFilePath1 = outputFilePath + ".1.vvppp"; - const outputFilePath2 = outputFilePath + ".2.vvppp"; - splitFile(outputFilePath, outputFilePath1, outputFilePath2); + const vvpppFilePath1 = vvppFilePath + ".1.vvppp"; + const vvpppFilePath2 = vvppFilePath + ".2.vvppp"; + splitFile(vvppFilePath, vvpppFilePath1, vvpppFilePath2); - const vvppEngineDir = createVvppEngineDir(); + const outputDir = createOutputDir(); await new VvppFileExtractor({ - vvppLikeFilePath: outputFilePath1, - vvppEngineDir, + vvppLikeFilePath: vvpppFilePath1, + outputDir, tmpDir, }).extract(); - expectManifestExists(vvppEngineDir); + assertIsEngineDir(outputDir); }); test.each([ @@ -56,37 +51,32 @@ test.each([ ])( "不正なVVPPファイルからエンジンを切り出せない: %s", async (targetName, expectedError) => { - const sourceDir = path.join(__dirname, "vvpps", targetName); - const outputFilePath = path.join(tmpDir, uuid4() + targetName); - await createZipFile(sourceDir, outputFilePath); + const outputFilePath = await createVvppFile(targetName, tmpDir); await expect( new VvppFileExtractor({ vvppLikeFilePath: outputFilePath, - vvppEngineDir: tmpDir, + outputDir: createOutputDir(), tmpDir, }).extract(), ).rejects.toThrow(expectedError); }, ); -/** 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); -} - -function createVvppEngineDir() { +function createOutputDir() { const dir = path.join(tmpDir, uuid4()); fs.mkdirSync(dir); return dir; } -function expectManifestExists(vvppEngineDir: string) { +/** + * エンジンディレクトリであることを確認する。 + */ +function assertIsEngineDir(vvppEngineDir: string) { const files = fs.readdirSync(vvppEngineDir, { recursive: true }); const manifestExists = files.some( (file) => - typeof file === "string" && path.basename(file) == "engine_manifest.json", + typeof file === "string" && + path.basename(file) === "engine_manifest.json", ); expect(manifestExists).toBe(true); } diff --git a/tests/unit/backend/electron/vvpps/no_engine_id.vvpp/engine_manifest.json b/tests/unit/backend/electron/vvpps/no_engine_id.vvpp/engine_manifest.json index e2575552e2..97fb3c2664 100644 --- a/tests/unit/backend/electron/vvpps/no_engine_id.vvpp/engine_manifest.json +++ b/tests/unit/backend/electron/vvpps/no_engine_id.vvpp/engine_manifest.json @@ -1,6 +1,6 @@ { "name": "Dummy Engine", - "command": "dummy.exe", + "command": "dummy", "port": 404, "supported_features": {} } diff --git a/tests/unit/backend/electron/vvpps/perfect.vvpp/dummy b/tests/unit/backend/electron/vvpps/perfect.vvpp/dummy new file mode 100644 index 0000000000..2995a4d0e7 --- /dev/null +++ b/tests/unit/backend/electron/vvpps/perfect.vvpp/dummy @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json b/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json index 7f32baf842..bd8ee36ddd 100644 --- a/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json +++ b/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json @@ -1,7 +1,7 @@ { "name": "Dummy Engine", "uuid": "00000000-0000-0000-0000-000000000001", - "command": "dummy.exe", + "command": "dummy", "port": 404, "supported_features": {} } From e6cf1b6b84bc477a98bfbdb7da60e2f98d70d72d Mon Sep 17 00:00:00 2001 From: Hiroshiba Kazuyuki Date: Wed, 19 Feb 2025 19:49:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20createOutputDir=E3=82=92buildOu?= =?UTF-8?q?tputDir=E3=81=AB=E3=83=AA=E3=83=8D=E3=83=BC=E3=83=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/backend/electron/vvppFile.node.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/backend/electron/vvppFile.node.spec.ts b/tests/unit/backend/electron/vvppFile.node.spec.ts index 80b438f7b2..649ed9045b 100644 --- a/tests/unit/backend/electron/vvppFile.node.spec.ts +++ b/tests/unit/backend/electron/vvppFile.node.spec.ts @@ -18,7 +18,7 @@ test("正しいVVPPファイルからエンジンを切り出せる", async () = const targetName = "perfect.vvpp"; const vvppFilePath = await createVvppFile(targetName, tmpDir); - const outputDir = createOutputDir(); + const outputDir = buildOutputDir(); await new VvppFileExtractor({ vvppLikeFilePath: vvppFilePath, outputDir, @@ -35,7 +35,7 @@ test("分割されたVVPPファイルからエンジンを切り出せる", asyn const vvpppFilePath2 = vvppFilePath + ".2.vvppp"; splitFile(vvppFilePath, vvpppFilePath1, vvpppFilePath2); - const outputDir = createOutputDir(); + const outputDir = buildOutputDir(); await new VvppFileExtractor({ vvppLikeFilePath: vvpppFilePath1, outputDir, @@ -55,14 +55,14 @@ test.each([ await expect( new VvppFileExtractor({ vvppLikeFilePath: outputFilePath, - outputDir: createOutputDir(), + outputDir: buildOutputDir(), tmpDir, }).extract(), ).rejects.toThrow(expectedError); }, ); -function createOutputDir() { +function buildOutputDir() { const dir = path.join(tmpDir, uuid4()); fs.mkdirSync(dir); return dir;