diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index 04a10dae9c..0155cb403d 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -9,7 +9,6 @@ import { getEngineProcessManager } from "./manager/engineProcessManager"; import { getRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { getVvppManager } from "./manager/vvppManager"; import { getWindowManager } from "./manager/windowManager"; -import { ProgressCallback } from "./type"; import { EngineId, EngineInfo, @@ -23,6 +22,7 @@ import { } from "@/domain/defaultEngine/latetDefaultEngine"; import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; import { UnreachableError } from "@/type/utility"; +import { ProgressCallback } from "@/helpers/progressHelper"; import { createLogger } from "@/helpers/log"; const log = createLogger("EngineAndVvppController"); diff --git a/src/backend/electron/fileHelper.ts b/src/backend/electron/fileHelper.ts index 062389c845..1fdba47b77 100644 --- a/src/backend/electron/fileHelper.ts +++ b/src/backend/electron/fileHelper.ts @@ -1,5 +1,4 @@ import fs from "fs"; -import { moveFileSync } from "move-file"; import { uuid4 } from "@/helpers/random"; import { createLogger } from "@/helpers/log"; @@ -14,15 +13,13 @@ export function writeFileSafely( data: string | NodeJS.ArrayBufferView, ) { const tmpPath = `${path}-${uuid4()}.tmp`; - fs.writeFileSync(tmpPath, data, { flag: "wx" }); try { - moveFileSync(tmpPath, path, { - overwrite: true, - }); + fs.writeFileSync(tmpPath, data, { flag: "wx" }); + fs.renameSync(tmpPath, path); } catch (error) { if (fs.existsSync(tmpPath)) { - fs.promises.unlink(tmpPath).catch((reason) => { + void fs.promises.unlink(tmpPath).catch((reason) => { log.warn("Failed to remove %s\n %o", tmpPath, reason); }); } diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index be61cf1b3f..0f8d3fa008 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -1,32 +1,22 @@ -import fs from "fs"; -import path from "path"; -import { spawn } from "child_process"; +import fs from "node:fs"; +import path from "node:path"; import { moveFile } from "move-file"; import { app, dialog } from "electron"; -import MultiStream from "multistream"; -import { glob } from "glob"; import AsyncLock from "async-lock"; -import { ProgressCallback } from "../type"; import { EngineId, EngineInfo, - minimumEngineManifestSchema, MinimumEngineManifestType, } from "@/type/preload"; import { errorToMessage } from "@/helpers/errorHelper"; +import { extractVvpp } from "@/backend/electron/vvppFile"; +import { ProgressCallback } from "@/helpers/progressHelper"; import { createLogger } from "@/helpers/log"; const log = createLogger("VvppManager"); const isNotWin = process.platform !== "win32"; -// https://www.garykessler.net/library/file_sigs.html#:~:text=7-zip%20compressed%20file -const SEVEN_ZIP_MAGIC_NUMBER = Buffer.from([ - 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c, -]); - -const ZIP_MAGIC_NUMBER = Buffer.from([0x50, 0x4b, 0x03, 0x04]); - export const isVvppFile = (filePath: string) => { return ( path.extname(filePath) === ".vvpp" || path.extname(filePath) === ".vvppp" @@ -35,136 +25,6 @@ export const isVvppFile = (filePath: string) => { const lockKey = "lock-key-for-vvpp-manager"; -/** VVPPファイルが分割されている場合、それらのファイルを取得する */ -async function getArchiveFileParts( - vvppLikeFilePath: string, -): Promise { - let archiveFileParts: string[]; - // 名前.数値.vvpppの場合は分割されているとみなして連結する - if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { - log.info("vvpp is split, finding other parts..."); - const vvpppPathGlob = vvppLikeFilePath - .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") - .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する - const filePaths: string[] = []; - for (const p of await glob(vvpppPathGlob)) { - if (!p.match(/\.[0-9]+\.vvppp$/)) { - continue; - } - log.info(`found ${p}`); - filePaths.push(p); - } - filePaths.sort((a, b) => { - const aMatch = a.match(/\.([0-9]+)\.vvppp$/); - const bMatch = b.match(/\.([0-9]+)\.vvppp$/); - if (aMatch == null || bMatch == null) { - throw new Error(`match is null: a=${a}, b=${b}`); - } - return parseInt(aMatch[1]) - parseInt(bMatch[1]); - }); - archiveFileParts = filePaths; - } else { - log.info("Not a split file"); - archiveFileParts = [vvppLikeFilePath]; - } - return archiveFileParts; -} - -/** 分割されているVVPPファイルを連結して返す */ -async function concatenateVvppFiles( - format: "zip" | "7z", - archiveFileParts: string[], -) { - // -siオプションでの7z解凍はサポートされていないため、 - // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 - log.info(`Concatenating ${archiveFileParts.length} files...`); - const tmpConcatenatedFile = path.join( - app.getPath("temp"), - `vvpp-${new Date().getTime()}.${format}`, - ); - log.info("Temporary file:", tmpConcatenatedFile); - await new Promise((resolve, reject) => { - if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); - const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f)); - const outputStream = fs.createWriteStream(tmpConcatenatedFile); - new MultiStream(inputStreams) - .pipe(outputStream) - .on("close", () => { - outputStream.close(); - resolve(); - }) - .on("error", reject); - }); - log.info("Concatenated"); - return tmpConcatenatedFile; -} - -/** 7zでファイルを解凍する */ -async function unarchive( - payload: { - archiveFile: string; - outputDir: string; - format: "zip" | "7z"; - }, - callbacks?: { onProgress?: ProgressCallback }, -) { - const { archiveFile, outputDir, format } = payload; - - const args = [ - "x", - "-o" + outputDir, - archiveFile, - "-t" + format, - "-bsp1", // 進捗出力 - ]; - - let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; - if (!sevenZipPath) { - throw new Error("7z path is not defined"); - } - if (import.meta.env.PROD) { - sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); - } - log.info("Spawning 7z:", sevenZipPath, args.join(" ")); - await new Promise((resolve, reject) => { - const child = spawn(sevenZipPath, args, { - stdio: ["pipe", "pipe", "pipe"], - }); - - child.stdout?.on("data", (data: Buffer) => { - const output = data.toString("utf-8"); - log.info(`7z STDOUT: ${output}`); - - // 進捗を取得 - // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る - // TODO: 出力が変わるかもしれないのでテストが必要 - const progressMatch = output.match( - / *(?\d+)% ?(?\d+)? ?(?.*)/, - ); - if (progressMatch?.groups?.percent) { - callbacks?.onProgress?.({ - progress: parseInt(progressMatch.groups.percent), - }); - } - }); - - child.stderr?.on("data", (data: Buffer) => { - log.error(`7z STDERR: ${data.toString("utf-8")}`); - }); - - child.on("exit", (code) => { - if (code === 0) { - callbacks?.onProgress?.({ progress: 100 }); - resolve(); - } else { - reject(new Error(`7z exited with code ${code}`)); - } - }); - // FIXME: rejectが2回呼ばれることがある - child.on("error", reject); - }); -} - // # 軽い概要 // // フォルダ名:"エンジン名+UUID" @@ -240,67 +100,6 @@ export class VvppManager { return true; } - private async extractVvpp( - vvppLikeFilePath: string, - callbacks?: { onProgress?: ProgressCallback }, - ): Promise<{ outputDir: string; manifest: MinimumEngineManifestType }> { - callbacks?.onProgress?.({ progress: 0 }); - - const nonce = new Date().getTime().toString(); - const outputDir = path.join(this.vvppEngineDir, ".tmp", nonce); - - const archiveFileParts = await getArchiveFileParts(vvppLikeFilePath); - - const format = await this.detectFileFormat(archiveFileParts[0]); - if (!format) { - throw new Error(`Unknown file format: ${archiveFileParts[0]}`); - } - log.info("Format:", format); - log.info("Extracting vvpp to", outputDir); - try { - let tmpConcatenatedFile: string | undefined; - let archiveFile: string; - try { - if (archiveFileParts.length > 1) { - tmpConcatenatedFile = await concatenateVvppFiles( - format, - archiveFileParts, - ); - archiveFile = tmpConcatenatedFile; - } else { - archiveFile = archiveFileParts[0]; - log.info("Single file, not concatenating"); - } - - await unarchive({ archiveFile, outputDir, format }, callbacks); - } finally { - if (tmpConcatenatedFile) { - log.info("Removing temporary file", tmpConcatenatedFile); - await fs.promises.rm(tmpConcatenatedFile); - } - } - const manifest: MinimumEngineManifestType = - minimumEngineManifestSchema.parse( - JSON.parse( - await fs.promises.readFile( - path.join(outputDir, "engine_manifest.json"), - "utf-8", - ), - ), - ); - return { - outputDir, - manifest, - }; - } catch (e) { - if (fs.existsSync(outputDir)) { - log.info("Failed to extract vvpp, removing", outputDir); - await fs.promises.rm(outputDir, { recursive: true }); - } - throw e; - } - } - /** * 追加 */ @@ -314,7 +113,14 @@ export class VvppManager { vvppPath: string, callbacks?: { onProgress?: ProgressCallback }, ) { - const { outputDir, manifest } = await this.extractVvpp(vvppPath, callbacks); + const { outputDir, manifest } = await extractVvpp( + { + vvppLikeFilePath: vvppPath, + vvppEngineDir: this.vvppEngineDir, + tmpDir: app.getPath("temp"), + }, + callbacks, + ); const dirName = this.toValidDirName(manifest); const engineDirectory = path.join(this.vvppEngineDir, dirName); @@ -408,23 +214,6 @@ export class VvppManager { this.willReplaceEngineDirs.length > 0 || this.willDeleteEngineIds.size > 0 ); } - - private async detectFileFormat( - filePath: string, - ): Promise<"zip" | "7z" | undefined> { - const file = await fs.promises.open(filePath, "r"); - - const buffer = Buffer.alloc(8); - await file.read(buffer, 0, 8, 0); - await file.close(); - - if (buffer.compare(SEVEN_ZIP_MAGIC_NUMBER, 0, 6, 0, 6) === 0) { - return "7z"; - } else if (buffer.compare(ZIP_MAGIC_NUMBER, 0, 4, 0, 4) === 0) { - return "zip"; - } - return undefined; - } } export default VvppManager; diff --git a/src/backend/electron/vvppFile.ts b/src/backend/electron/vvppFile.ts new file mode 100644 index 0000000000..94a1898488 --- /dev/null +++ b/src/backend/electron/vvppFile.ts @@ -0,0 +1,237 @@ +/** + * VVPPファイルを扱うモジュール。 + */ + +import fs from "node:fs"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import MultiStream from "multistream"; +import { glob } from "glob"; +import { app } from "electron"; +import { + minimumEngineManifestSchema, + MinimumEngineManifestType, +} from "@/type/preload"; +import { ProgressCallback } from "@/helpers/progressHelper"; +import { createLogger } from "@/helpers/log"; + +const log = createLogger("vvppFile"); + +/** VVPPファイルが分割されている場合、それらのファイルを取得する */ +async function getArchiveFileParts( + vvppLikeFilePath: string, +): Promise { + let archiveFileParts: string[]; + // 名前.数値.vvpppの場合は分割されているとみなして連結する + if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { + log.info("vvpp is split, finding other parts..."); + const vvpppPathGlob = vvppLikeFilePath + .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") + .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する + const filePaths: string[] = []; + for (const p of await glob(vvpppPathGlob)) { + if (!p.match(/\.[0-9]+\.vvppp$/)) { + continue; + } + log.info(`found ${p}`); + filePaths.push(p); + } + filePaths.sort((a, b) => { + const aMatch = a.match(/\.([0-9]+)\.vvppp$/); + const bMatch = b.match(/\.([0-9]+)\.vvppp$/); + if (aMatch == null || bMatch == null) { + throw new Error(`match is null: a=${a}, b=${b}`); + } + return parseInt(aMatch[1]) - parseInt(bMatch[1]); + }); + archiveFileParts = filePaths; + } else { + log.info("Not a split file"); + archiveFileParts = [vvppLikeFilePath]; + } + return archiveFileParts; +} + +/** 分割されているVVPPファイルを連結して返す */ +async function concatenateVvppFiles( + archiveFileParts: string[], + outputFilePath: string, +) { + log.info(`Concatenating ${archiveFileParts.length} files...`); + + await new Promise((resolve, reject) => { + const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f)); + const outputStream = fs.createWriteStream(outputFilePath); + new MultiStream(inputStreams) + .pipe(outputStream) + .on("close", () => { + outputStream.close(); + resolve(); + }) + .on("error", reject); + }); + log.info("Concatenated"); +} + +/** 7zでファイルを解凍する */ +async function unarchive( + payload: { + archiveFile: string; + outputDir: string; + format: "zip" | "7z"; + }, + callbacks?: { onProgress?: ProgressCallback }, +) { + const { archiveFile, outputDir, format } = payload; + + const args = [ + "x", + "-o" + outputDir, + archiveFile, + "-t" + format, + "-bsp1", // 進捗出力 + ]; + + const sevenZipPath = getSevenZipPath(); + log.info("Spawning 7z:", sevenZipPath, args.join(" ")); + await new Promise((resolve, reject) => { + const child = spawn(sevenZipPath, args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + child.stdout?.on("data", (data: Buffer) => { + const output = data.toString("utf-8"); + log.info(`7z STDOUT: ${output}`); + + // 進捗を取得 + // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る + // TODO: 出力が変わるかもしれないのでテストが必要 + const progressMatch = output.match( + / *(?\d+)% ?(?\d+)? ?(?.*)/, + ); + if (progressMatch?.groups?.percent) { + callbacks?.onProgress?.({ + progress: parseInt(progressMatch.groups.percent), + }); + } + }); + + child.stderr?.on("data", (data: Buffer) => { + log.error(`7z STDERR: ${data.toString("utf-8")}`); + }); + + child.on("exit", (code) => { + if (code === 0) { + callbacks?.onProgress?.({ progress: 100 }); + resolve(); + } else { + reject(new Error(`7z exited with code ${code}`)); + } + }); + // FIXME: rejectが2回呼ばれることがある + child.on("error", reject); + }); + + function getSevenZipPath() { + let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; + if (!sevenZipPath) { + throw new Error("7z path is not defined"); + } + if (import.meta.env.PROD) { + sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); + } + return sevenZipPath; + } +} + +export async function extractVvpp( + payload: { vvppLikeFilePath: string; vvppEngineDir: string; tmpDir: string }, + callbacks?: { onProgress?: ProgressCallback }, +): Promise<{ outputDir: string; manifest: MinimumEngineManifestType }> { + const { vvppLikeFilePath, vvppEngineDir, tmpDir } = payload; + callbacks?.onProgress?.({ progress: 0 }); + + const nonce = new Date().getTime().toString(); + const outputDir = path.join(vvppEngineDir, ".tmp", nonce); + + const archiveFileParts = await getArchiveFileParts(vvppLikeFilePath); + + const format = await detectFileFormat(archiveFileParts[0]); + if (!format) { + throw new Error(`Unknown file format: ${archiveFileParts[0]}`); + } + log.info("Format:", format); + log.info("Extracting vvpp to", outputDir); + try { + let tmpConcatenatedFile: string | undefined; + let archiveFile: string; + try { + if (archiveFileParts.length > 1) { + // -siオプションでの7z解凍はサポートされていないため、 + // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 + tmpConcatenatedFile = createTmpConcatenatedFilePath(); + log.info("Temporary file:", tmpConcatenatedFile); + + await concatenateVvppFiles(archiveFileParts, tmpConcatenatedFile); + archiveFile = tmpConcatenatedFile; + } else { + archiveFile = archiveFileParts[0]; + log.info("Single file, not concatenating"); + } + + await unarchive({ archiveFile, outputDir, format }, callbacks); + } finally { + if (tmpConcatenatedFile) { + log.info("Removing temporary file", tmpConcatenatedFile); + await fs.promises.rm(tmpConcatenatedFile); + } + } + const manifest: MinimumEngineManifestType = + minimumEngineManifestSchema.parse( + JSON.parse( + await fs.promises.readFile( + path.join(outputDir, "engine_manifest.json"), + "utf-8", + ), + ), + ); + return { + outputDir, + manifest, + }; + } catch (e) { + if (fs.existsSync(outputDir)) { + log.info("Failed to extract vvpp, removing", outputDir); + await fs.promises.rm(outputDir, { recursive: true }); + } + throw e; + } + + function createTmpConcatenatedFilePath(): string { + return path.join(tmpDir, `vvpp-${new Date().getTime()}.${format}`); + } +} + +async function detectFileFormat( + filePath: string, +): Promise<"zip" | "7z" | undefined> { + const file = await fs.promises.open(filePath, "r"); + + const buffer = Buffer.alloc(8); + await file.read(buffer, 0, 8, 0); + await file.close(); + + // https://www.garykessler.net/library/file_sigs.html#:~:text=7-zip%20compressed%20file + const SEVEN_ZIP_MAGIC_NUMBER = Buffer.from([ + 0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c, + ]); + + const ZIP_MAGIC_NUMBER = Buffer.from([0x50, 0x4b, 0x03, 0x04]); + + if (buffer.compare(SEVEN_ZIP_MAGIC_NUMBER, 0, 6, 0, 6) === 0) { + return "7z"; + } else if (buffer.compare(ZIP_MAGIC_NUMBER, 0, 4, 0, 4) === 0) { + return "zip"; + } + return undefined; +} diff --git a/src/components/Dialog/DictionaryEditWordDialog.vue b/src/components/Dialog/DictionaryEditWordDialog.vue index 7b916a9bdc..9cefe07a89 100644 --- a/src/components/Dialog/DictionaryEditWordDialog.vue +++ b/src/components/Dialog/DictionaryEditWordDialog.vue @@ -163,10 +163,7 @@ + + + diff --git a/src/components/Sing/SequencerParameterPanel.vue b/src/components/Sing/SequencerParameterPanel.vue new file mode 100644 index 0000000000..bd4834e0f4 --- /dev/null +++ b/src/components/Sing/SequencerParameterPanel.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/Sing/SequencerRuler/index.stories.ts b/src/components/Sing/SequencerRuler/index.stories.ts index 8e7e625df0..9eaef47d25 100644 --- a/src/components/Sing/SequencerRuler/index.stories.ts +++ b/src/components/Sing/SequencerRuler/index.stories.ts @@ -27,6 +27,7 @@ const meta: Meta = { offset: 0, numMeasures: 32, sequencerSnapType: 16, + uiLocked: false, "onUpdate:playheadTicks": fn<(value: number) => void>(), onDeselectAllNotes: fn(), }, diff --git a/src/backend/electron/type.ts b/src/helpers/progressHelper.ts similarity index 100% rename from src/backend/electron/type.ts rename to src/helpers/progressHelper.ts diff --git a/src/store/setting.ts b/src/store/setting.ts index e000302461..1cd2aa1dd9 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -48,6 +48,7 @@ export const settingStoreState: SettingStoreState = { enableMorphing: false, enableMultiSelect: false, shouldKeepTuningOnTextChange: false, + showParameterPanel: false, }, splitTextWhenPaste: "PERIOD_AND_NEW_LINE", splitterPosition: { diff --git a/src/type/preload.ts b/src/type/preload.ts index 16f825d51f..f5e565f70b 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -357,6 +357,7 @@ export const experimentalSettingSchema = z.object({ enableMorphing: z.boolean().default(false), enableMultiSelect: z.boolean().default(false), shouldKeepTuningOnTextChange: z.boolean().default(false), + showParameterPanel: z.boolean().default(false), }); export type ExperimentalSettingType = z.infer; diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" index c5e5a5971d..175e5c9728 100644 Binary files "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" and "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" index 6044bd2f18..e60f2719ad 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-0-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" index fb17f84399..84326c3de8 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-1-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" index 65dc00209a..47355f0bbe 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-2-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" index 1eb623fc37..46d6396909 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-3-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" index 455f46dcdf..af07079da6 100644 Binary files "a/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" and "b/tests/e2e/browser/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260/\350\250\255\345\256\232\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts-snapshots/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210-4-browser-win32.png" differ diff --git a/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap b/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap index 4da839ee6a..23598f3943 100644 --- a/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap +++ b/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap @@ -29,6 +29,7 @@ exports[`0.13.0からマイグレーションできる 1`] = ` "enableMorphing": false, "enableMultiSelect": false, "shouldKeepTuningOnTextChange": false, + "showParameterPanel": false, }, "hotkeySettings": [ { diff --git a/tests/unit/backend/electron/fileHelper.node.spec.ts b/tests/unit/backend/electron/fileHelper.node.spec.ts new file mode 100644 index 0000000000..2530da26c0 --- /dev/null +++ b/tests/unit/backend/electron/fileHelper.node.spec.ts @@ -0,0 +1,45 @@ +import fs from "fs"; +import path from "path"; +import os from "os"; +import { test, expect, beforeAll, afterAll } from "vitest"; +import { writeFileSafely } from "@/backend/electron/fileHelper"; +import { uuid4 } from "@/helpers/random"; + +let tmpDir: string; + +beforeAll(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), uuid4())); +}); + +afterAll(() => { + fs.rmdirSync(tmpDir, { recursive: true }); +}); + +describe("writeFileSafely", () => { + test("ファイルを書き込める", async () => { + const filePath = path.join(tmpDir, uuid4()); + const content = "Hello World"; + writeFileSafely(filePath, content); + expect(fs.readFileSync(filePath, "utf-8")).toBe(content); + }); + + test("ファイルを上書きできる", async () => { + const filePath = path.join(tmpDir, uuid4()); + fs.writeFileSync(filePath, "old content"); + const newContent = "new content"; + writeFileSafely(filePath, newContent); + expect(fs.readFileSync(filePath, "utf-8")).toBe(newContent); + }); + + test("存在しないディレクトリに書き込もうとするとエラー", async () => { + const nonExistentDir = path.join(tmpDir, uuid4(), "not-exist"); + const filePath = path.join(nonExistentDir, "test.txt"); + expect(() => writeFileSafely(filePath, "data")).toThrow(); + }); + + test("指定したパスにディレクトリが存在するとエラー", async () => { + const filePath = path.join(tmpDir, uuid4()); + fs.mkdirSync(filePath); + expect(() => writeFileSafely(filePath, "data")).toThrow(); + }); +}); diff --git a/tests/unit/backend/electron/vvppFile.node.spec.ts b/tests/unit/backend/electron/vvppFile.node.spec.ts new file mode 100644 index 0000000000..3dd04933f2 --- /dev/null +++ b/tests/unit/backend/electron/vvppFile.node.spec.ts @@ -0,0 +1,104 @@ +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 { extractVvpp } from "@/backend/electron/vvppFile"; +import { uuid4 } from "@/helpers/random"; + +let tmpDir: string; +beforeAll(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), uuid4())); +}); +afterAll(() => { + fs.rmdirSync(tmpDir, { recursive: true }); +}); + +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 vvppEngineDir = createVvppEngineDir(); + await extractVvpp({ + vvppLikeFilePath: outputFilePath, + vvppEngineDir, + tmpDir, + }); + expectManifestExists(vvppEngineDir); +}); + +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 outputFilePath1 = outputFilePath + ".1.vvppp"; + const outputFilePath2 = outputFilePath + ".2.vvppp"; + splitFile(outputFilePath, outputFilePath1, outputFilePath2); + + const vvppEngineDir = createVvppEngineDir(); + await extractVvpp({ + vvppLikeFilePath: outputFilePath1, + vvppEngineDir, + tmpDir, + }); + expectManifestExists(vvppEngineDir); +}); + +test.each([ + ["invalid_manifest.vvpp", /SyntaxError|is not valid JSON/], + ["no_engine_id.vvpp", undefined], // TODO: エンジンIDが見つからない専用のエラーを用意する + ["no_manifest.vvpp", /ENOENT|engine_manifest.json/], +])( + "不正なVVPPファイルからエンジンを切り出せない: %s", + async (targetName, expectedError) => { + const sourceDir = path.join(__dirname, "vvpps", targetName); + const outputFilePath = path.join(tmpDir, uuid4() + targetName); + await createZipFile(sourceDir, outputFilePath); + await expect( + extractVvpp({ + vvppLikeFilePath: outputFilePath, + vvppEngineDir: tmpDir, + tmpDir, + }), + ).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() { + const dir = path.join(tmpDir, uuid4()); + fs.mkdirSync(dir); + return dir; +} + +function expectManifestExists(vvppEngineDir: string) { + const files = fs.readdirSync(vvppEngineDir, { recursive: true }); + const manifestExists = files.some( + (file) => + typeof file === "string" && path.basename(file) == "engine_manifest.json", + ); + expect(manifestExists).toBe(true); +} + +/** ファイルを2つに分割する */ +function splitFile( + inputFilePath: string, + outputFilePath1: string, + outputFilePath2: string, +) { + const data = fs.readFileSync(inputFilePath); + const midPoint = Math.floor(data.length / 2); + fs.writeFileSync(outputFilePath1, data.subarray(0, midPoint)); + fs.writeFileSync(outputFilePath2, data.subarray(midPoint)); +} diff --git a/tests/unit/backend/electron/vvpps/README.md b/tests/unit/backend/electron/vvpps/README.md new file mode 100644 index 0000000000..72730c93d0 --- /dev/null +++ b/tests/unit/backend/electron/vvpps/README.md @@ -0,0 +1,9 @@ +# vvpps + +テスト用のVVPPファイルを作るためのディレクトリ。 +各ディレクトリをzip化して拡張子を.vvppに変えればテスト用ファイルになる。 + +- perfect.vvpp:完璧なVVPP +- invalid_manifest.vvpp:不正なエンジンマニフェストがあるVVPP +- no_manifest.vvpp:エンジンマニフェストがないVVPP +- no_engine_id.vvpp:エンジンマニフェストはあるが、エンジンidがないVVPP diff --git a/tests/unit/backend/electron/vvpps/invalid_manifest.vvpp/engine_manifest.json b/tests/unit/backend/electron/vvpps/invalid_manifest.vvpp/engine_manifest.json new file mode 100644 index 0000000000..ea368b7a02 --- /dev/null +++ b/tests/unit/backend/electron/vvpps/invalid_manifest.vvpp/engine_manifest.json @@ -0,0 +1 @@ +invalid manifest \ No newline at end of file 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 new file mode 100644 index 0000000000..e2575552e2 --- /dev/null +++ b/tests/unit/backend/electron/vvpps/no_engine_id.vvpp/engine_manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Dummy Engine", + "command": "dummy.exe", + "port": 404, + "supported_features": {} +} diff --git a/tests/unit/backend/electron/vvpps/no_manifest.vvpp/invalid_file_name.json b/tests/unit/backend/electron/vvpps/no_manifest.vvpp/invalid_file_name.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json b/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json new file mode 100644 index 0000000000..7f32baf842 --- /dev/null +++ b/tests/unit/backend/electron/vvpps/perfect.vvpp/engine_manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Dummy Engine", + "uuid": "00000000-0000-0000-0000-000000000001", + "command": "dummy.exe", + "port": 404, + "supported_features": {} +}