diff --git a/devs.md b/devs.md index d9d9ef1..816c2be 100644 --- a/devs.md +++ b/devs.md @@ -17,7 +17,7 @@ The plugin uses a dynamic module system to reduce coupling and improve maintaina - `coreObsidian/` - Obsidian-specific core (e.g., `ModuleFileAccessObsidian`) - `essential/` - Required modules (e.g., `ModuleMigration`, `ModuleKeyValueDB`) - `features/` - Optional features (e.g., `ModuleLog`, `ModuleObsidianSettings`) - - `extras/` - Development/testing tools (e.g., `ModuleDev`, `ModuleIntegratedTest`) + - `extras/` - Development/testing tools (e.g., `ModuleDev`, ~~`ModuleIntegratedTest`~~) - **Services**: Core services (e.g., `database`, `replicator`, `storageAccess`) are registered in `ServiceHub` and accessed by modules. They provide an extension point for add new behaviour without modifying existing code. - For example, checks before the replication can be added to the `replication.onBeforeReplicate` handler, and the handlers can be return `false` to prevent replication-starting. `vault.isTargetFile` also can be used to prevent processing specific files. - **ServiceModule**: A new type of module that directly depends on services. diff --git a/src/main.ts b/src/main.ts index 6c5c3f3..3de86ce 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,8 +12,6 @@ import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidian import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts"; import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts"; import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts"; -import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts"; -import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts"; import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts"; @@ -156,8 +154,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin { new ModuleInteractiveConflictResolver(this, core), new ModuleObsidianGlobalHistory(this, core), new ModuleDev(this, core), - new ModuleReplicateTest(this, core), - new ModuleIntegratedTest(this, core), new SetupManager(core), // this should be moved to core? new ModuleMigration(core), ]; diff --git a/src/modules/extras/ModuleIntegratedTest.ts b/src/modules/extras/ModuleIntegratedTest.ts deleted file mode 100644 index df5cc3a..0000000 --- a/src/modules/extras/ModuleIntegratedTest.ts +++ /dev/null @@ -1,446 +0,0 @@ -import { delay } from "octagonal-wheels/promises"; -import { LOG_LEVEL_NOTICE, REMOTE_MINIO, type FilePathWithPrefix } from "src/lib/src/common/types"; -import { shareRunningResult } from "octagonal-wheels/concurrency/lock"; -import { AbstractObsidianModule } from "../AbstractObsidianModule"; - -export class ModuleIntegratedTest extends AbstractObsidianModule { - async waitFor(proc: () => Promise, timeout = 10000): Promise { - await delay(100); - const start = Date.now(); - while (!(await proc())) { - if (timeout > 0) { - if (Date.now() - start > timeout) { - this._log(`Timeout`); - return false; - } - } - await delay(500); - } - return true; - } - waitWithReplicating(proc: () => Promise, timeout = 10000): Promise { - return this.waitFor(async () => { - await this.tryReplicate(); - return await proc(); - }, timeout); - } - async storageContentIsEqual(file: string, content: string): Promise { - try { - const fileContent = await this.readStorageContent(file as FilePathWithPrefix); - if (fileContent === content) { - return true; - } else { - // this._log(`Content is not same \n Expected:${content}\n Actual:${fileContent}`, LOG_LEVEL_VERBOSE); - return false; - } - } catch (e) { - this._log(`Error: ${e}`); - return false; - } - } - async assert(proc: () => Promise): Promise { - if (!(await proc())) { - this._log(`Assertion failed`); - return false; - } - return true; - } - async __orDie(key: string, proc: () => Promise): Promise | never { - if (!(await this._test(key, proc))) { - throw new Error(`${key}`); - } - return true; - } - tryReplicate() { - if (!this.settings.liveSync) { - return shareRunningResult("replicate-test", async () => { - await this.services.replication.replicate(); - }); - } - } - async readStorageContent(file: FilePathWithPrefix): Promise { - if (!(await this.core.storageAccess.isExistsIncludeHidden(file))) { - return undefined; - } - return await this.core.storageAccess.readHiddenFileText(file); - } - async __proceed(no: number, title: string): Promise { - const stepFile = "_STEP.md" as FilePathWithPrefix; - const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix; - const stepContent = `Step ${no}`; - await this.services.conflict.resolveByNewest(stepFile); - await this.core.storageAccess.writeFileAuto(stepFile, stepContent); - await this.__orDie(`Wait for acknowledge ${no}`, async () => { - if ( - !(await this.waitWithReplicating(async () => { - return await this.storageContentIsEqual(stepAckFile, stepContent); - }, 20000)) - ) - return false; - return true; - }); - return true; - } - async __join(no: number, title: string): Promise { - const stepFile = "_STEP.md" as FilePathWithPrefix; - const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix; - // const otherStepFile = `_STEP_${isLeader ? "R" : "L"}.md` as FilePathWithPrefix; - const stepContent = `Step ${no}`; - - await this.__orDie(`Wait for step ${no} (${title})`, async () => { - if ( - !(await this.waitWithReplicating(async () => { - return await this.storageContentIsEqual(stepFile, stepContent); - }, 20000)) - ) - return false; - return true; - }); - await this.services.conflict.resolveByNewest(stepAckFile); - await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent); - await this.tryReplicate(); - return true; - } - - async performStep({ - step, - title, - isGameChanger, - proc, - check, - }: { - step: number; - title: string; - isGameChanger: boolean; - proc: () => Promise; - check: () => Promise; - }): Promise { - if (isGameChanger) { - await this.__proceed(step, title); - try { - await proc(); - } catch (e) { - this._log(`Error: ${e}`); - return false; - } - return await this.__orDie(`Step ${step} - ${title}`, async () => await this.waitWithReplicating(check)); - } else { - return await this.__join(step, title); - } - } - // // see scenario.md - // async testLeader(testMain: (testFileName: FilePathWithPrefix) => Promise): Promise { - - // } - // async testReceiver(testMain: (testFileName: FilePathWithPrefix) => Promise): Promise { - - // } - async nonLiveTestRunner( - isLeader: boolean, - testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise - ): Promise { - const storage = this.core.storageAccess; - // const database = this.core.databaseFileAccess; - // const _orDie = this._orDie.bind(this); - const testCommandFile = "IT.md" as FilePathWithPrefix; - const textCommandResponseFile = "ITx.md" as FilePathWithPrefix; - let testFileName: FilePathWithPrefix; - this.addTestResult( - "-------Starting ... ", - true, - `Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}` - ); - if (isLeader) { - await this.__proceed(0, "start"); - } - await this.tryReplicate(); - - await this.performStep({ - step: 0, - title: "Make sure that command File Not Exists", - isGameChanger: isLeader, - proc: async () => await storage.removeHidden(testCommandFile), - check: async () => !(await storage.isExistsIncludeHidden(testCommandFile)), - }); - await this.performStep({ - step: 1, - title: "Make sure that command File Not Exists On Receiver", - isGameChanger: !isLeader, - proc: async () => await storage.removeHidden(textCommandResponseFile), - check: async () => !(await storage.isExistsIncludeHidden(textCommandResponseFile)), - }); - - await this.performStep({ - step: 2, - title: "Decide the test file name", - isGameChanger: isLeader, - proc: async () => { - testFileName = (Date.now() + "-" + Math.ceil(Math.random() * 1000) + ".md") as FilePathWithPrefix; - const testCommandFile = "IT.md" as FilePathWithPrefix; - await storage.writeFileAuto(testCommandFile, testFileName); - }, - check: () => Promise.resolve(true), - }); - await this.performStep({ - step: 3, - title: "Wait for the command file to be arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await storage.isExistsIncludeHidden(testCommandFile), - }); - - await this.performStep({ - step: 4, - title: "Send the response file", - isGameChanger: !isLeader, - proc: async () => { - await storage.writeHiddenFileAuto(textCommandResponseFile, "!"); - }, - check: () => Promise.resolve(true), - }); - await this.performStep({ - step: 5, - title: "Wait for the response file to be arrived", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => await storage.isExistsIncludeHidden(textCommandResponseFile), - }); - - await this.performStep({ - step: 6, - title: "Proceed to begin the test", - isGameChanger: isLeader, - proc: async () => {}, - check: () => Promise.resolve(true), - }); - await this.performStep({ - step: 6, - title: "Begin the test", - isGameChanger: !false, - proc: async () => {}, - check: () => { - return Promise.resolve(true); - }, - }); - // await this.step(0, isLeader, true); - try { - this.addTestResult("** Main------", true, ``); - if (isLeader) { - return await testMain(testFileName!, true); - } else { - const testFileName = await this.readStorageContent(testCommandFile); - this.addTestResult("testFileName", true, `Request client to use :${testFileName!}`); - return await testMain(testFileName! as FilePathWithPrefix, false); - } - } finally { - this.addTestResult("Teardown", true, `Deleting ${testFileName!}`); - await storage.removeHidden(testFileName!); - } - - return true; - // Make sure the - } - - async testBasic(filename: FilePathWithPrefix, isLeader: boolean): Promise { - const storage = this.core.storageAccess; - const database = this.core.databaseFileAccess; - - await this.addTestResult( - `---**Starting Basic Test**---`, - true, - `Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}` - ); - // if (isLeader) { - // await this._proceed(0); - // } - // await this.tryReplicate(); - - await this.performStep({ - step: 0, - title: "Make sure that file is not exist", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => !(await storage.isExists(filename)), - }); - - await this.performStep({ - step: 1, - title: "Write a file", - isGameChanger: isLeader, - proc: async () => await storage.writeFileAuto(filename, "Hello World"), - check: async () => await storage.isExists(filename), - }); - await this.performStep({ - step: 2, - title: "Make sure the file is arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await storage.isExists(filename), - }); - await this.performStep({ - step: 3, - title: "Update to Hello World 2", - isGameChanger: isLeader, - proc: async () => await storage.writeFileAuto(filename, "Hello World 2"), - check: async () => await this.storageContentIsEqual(filename, "Hello World 2"), - }); - await this.performStep({ - step: 4, - title: "Make sure the modified file is arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, "Hello World 2"), - }); - await this.performStep({ - step: 5, - title: "Update to Hello World 3", - isGameChanger: !isLeader, - proc: async () => await storage.writeFileAuto(filename, "Hello World 3"), - check: async () => await this.storageContentIsEqual(filename, "Hello World 3"), - }); - await this.performStep({ - step: 6, - title: "Make sure the modified file is arrived", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, "Hello World 3"), - }); - - const multiLineContent = `Line1:A -Line2:B -Line3:C -Line4:D`; - - await this.performStep({ - step: 7, - title: "Update to Multiline", - isGameChanger: isLeader, - proc: async () => await storage.writeFileAuto(filename, multiLineContent), - check: async () => await this.storageContentIsEqual(filename, multiLineContent), - }); - - await this.performStep({ - step: 8, - title: "Make sure the modified file is arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, multiLineContent), - }); - - // While LiveSync, possibly cannot cause the conflict. - if (!this.settings.liveSync) { - // Step 9 Make Conflict But Resolvable - const multiLineContentL = `Line1:A -Line2:B -Line3:C! -Line4:D`; - const multiLineContentC = `Line1:A -Line2:bbbbb -Line3:C -Line4:D`; - - await this.performStep({ - step: 9, - title: "Progress to be conflicted", - isGameChanger: isLeader, - proc: async () => {}, - check: () => Promise.resolve(true), - }); - - await storage.writeFileAuto(filename, isLeader ? multiLineContentL : multiLineContentC); - - await this.performStep({ - step: 10, - title: "Update As Conflicted", - isGameChanger: !isLeader, - proc: async () => {}, - check: () => Promise.resolve(true), - }); - - await this.performStep({ - step: 10, - title: "Make sure Automatically resolved", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => (await database.getConflictedRevs(filename)).length === 0, - }); - await this.performStep({ - step: 11, - title: "Make sure Automatically resolved", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => (await database.getConflictedRevs(filename)).length === 0, - }); - - const sensiblyMergedContent = `Line1:A -Line2:bbbbb -Line3:C! -Line4:D`; - - await this.performStep({ - step: 12, - title: "Make sure Sensibly Merged on Leader", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent), - }); - await this.performStep({ - step: 13, - title: "Make sure Sensibly Merged on Receiver", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent), - }); - } - await this.performStep({ - step: 14, - title: "Delete File", - isGameChanger: isLeader, - proc: async () => { - await storage.removeHidden(filename); - }, - check: async () => !(await storage.isExists(filename)), - }); - - await this.performStep({ - step: 15, - title: "Make sure File is deleted", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => !(await storage.isExists(filename)), - }); - this._log(`The Basic Test has been completed`, LOG_LEVEL_NOTICE); - return true; - } - - async testBasicEvent(isLeader: boolean) { - this.settings.liveSync = false; - await this.saveSettings(); - await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l))); - } - async testBasicLive(isLeader: boolean) { - this.settings.liveSync = true; - await this.saveSettings(); - await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l))); - } - - async _everyModuleTestMultiDevice(): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - const isLeader = this.core.services.vault.vaultName().indexOf("recv") === -1; - this.addTestResult("-------", true, `Test as ${isLeader ? "Leader" : "Receiver"}`); - try { - this._log(`Starting Test`); - await this.testBasicEvent(isLeader); - if (this.settings.remoteType == REMOTE_MINIO) await this.testBasicLive(isLeader); - } catch (e) { - this._log(e); - this._log(`Error: ${e}`); - return Promise.resolve(false); - } - - return Promise.resolve(true); - } - override onBindFunction(core: typeof this.core, services: typeof core.services): void { - services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this)); - } -} diff --git a/src/modules/extras/ModuleReplicateTest.ts b/src/modules/extras/ModuleReplicateTest.ts deleted file mode 100644 index 394042a..0000000 --- a/src/modules/extras/ModuleReplicateTest.ts +++ /dev/null @@ -1,590 +0,0 @@ -// I intend to discontinue maintenance of this class. It seems preferable to test it externally. -import { delay } from "octagonal-wheels/promises"; -import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import { eventHub } from "../../common/events"; -import { getWebCrypto } from "../../lib/src/mods.ts"; -import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex"; -import { parseYaml, requestUrl, stringifyYaml } from "@/deps.ts"; -import type { FilePath } from "../../lib/src/common/types.ts"; -import { scheduleTask } from "octagonal-wheels/concurrency/task"; -import { getFileRegExp } from "../../lib/src/common/utils.ts"; -import type { LiveSyncCore } from "../../main.ts"; - -declare global { - interface LSEvents { - "debug-sync-status": string[]; - } -} - -export class ModuleReplicateTest extends AbstractObsidianModule { - testRootPath = "_test/"; - testInfoPath = "_testinfo/"; - - get isLeader() { - return ( - this.services.vault.getVaultName().indexOf("dev") >= 0 && - this.services.vault.vaultName().indexOf("recv") < 0 - ); - } - - get nameByKind() { - if (!this.isLeader) { - return "RECV"; - } else if (this.isLeader) { - return "LEADER"; - } - } - get pairName() { - if (this.isLeader) { - return "RECV"; - } else if (!this.isLeader) { - return "LEADER"; - } - } - - watchIsSynchronised = false; - - statusBarSyncStatus?: HTMLElement; - async readFileContent(file: string) { - try { - return await this.core.storageAccess.readHiddenFileText(file); - } catch { - return ""; - } - } - - async dumpList() { - if (this.settings.syncInternalFiles) { - this._log("Write file list (Include Hidden)"); - await this.__dumpFileListIncludeHidden("files.md"); - } else { - this._log("Write file list"); - await this.__dumpFileList("files.md"); - } - } - async _everyBeforeReplicate(showMessage: boolean): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - await this.dumpList(); - return true; - } - private _everyOnloadAfterLoadSettings(): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - this.addCommand({ - id: "dump-file-structure-normal", - name: `Dump Structure (Normal)`, - callback: () => { - void this.__dumpFileList("files.md").finally(() => { - void this.refreshSyncStatus(); - }); - }, - }); - this.addCommand({ - id: "dump-file-structure-ih", - name: "Dump Structure (Include Hidden)", - callback: () => { - const d = "files.md"; - void this.__dumpFileListIncludeHidden(d); - }, - }); - this.addCommand({ - id: "dump-file-structure-auto", - name: "Dump Structure", - callback: () => { - void this.dumpList(); - }, - }); - this.addCommand({ - id: "dump-file-test", - name: `Perform Test (Dev) ${this.isLeader ? "(Leader)" : "(Recv)"}`, - callback: () => { - void this.performTestManually(); - }, - }); - this.addCommand({ - id: "watch-sync-result", - name: `Watch sync result is matched between devices`, - callback: () => { - this.watchIsSynchronised = !this.watchIsSynchronised; - void this.refreshSyncStatus(); - }, - }); - this.app.vault.on("modify", async (file) => { - if (file.path.startsWith(this.testInfoPath)) { - await this.refreshSyncStatus(); - } else { - scheduleTask("dumpStatus", 125, async () => { - await this.dumpList(); - return true; - }); - } - }); - this.statusBarSyncStatus = this.plugin.addStatusBarItem(); - return Promise.resolve(true); - } - async getSyncStatusAsText() { - const fileMine = this.testInfoPath + this.nameByKind + "/" + "files.md"; - const filePair = this.testInfoPath + this.pairName + "/" + "files.md"; - const mine = parseYaml(await this.readFileContent(fileMine)); - const pair = parseYaml(await this.readFileContent(filePair)); - const result = [] as string[]; - if (mine.length != pair.length) { - result.push(`File count is different: ${mine.length} vs ${pair.length}`); - } - const filesAll = new Set([...mine.map((e: any) => e.path), ...pair.map((e: any) => e.path)]); - for (const file of filesAll) { - const mineFile = mine.find((e: any) => e.path == file); - const pairFile = pair.find((e: any) => e.path == file); - if (!mineFile || !pairFile) { - result.push(`File not found: ${file}`); - } else { - if (mineFile.size != pairFile.size) { - result.push(`Size is different: ${file} ${mineFile.size} vs ${pairFile.size}`); - } - if (mineFile.hash != pairFile.hash) { - result.push(`Hash is different: ${file} ${mineFile.hash} vs ${pairFile.hash}`); - } - } - } - eventHub.emitEvent("debug-sync-status", result); - return result.join("\n"); - } - - async refreshSyncStatus() { - if (this.watchIsSynchronised) { - // Normal Files - const syncStatus = await this.getSyncStatusAsText(); - if (syncStatus) { - this.statusBarSyncStatus!.setText(`Sync Status: Having Error`); - this._log(`Sync Status: Having Error\n${syncStatus}`, LOG_LEVEL_INFO); - } else { - this.statusBarSyncStatus!.setText(`Sync Status: Synchronised`); - } - } else { - this.statusBarSyncStatus!.setText(""); - } - } - - async __dumpFileList(outFile?: string) { - if (!this.core || !this.core.storageAccess) { - this._log("No storage access", LOG_LEVEL_INFO); - return; - } - const files = await this.core.storageAccess.getFiles(); - const out = [] as any[]; - const webcrypto = await getWebCrypto(); - for (const file of files) { - if (!(await this.services.vault.isTargetFile(file.path))) { - continue; - } - if (file.path.startsWith(this.testInfoPath)) continue; - const stat = await this.core.storageAccess.stat(file.path); - if (stat) { - const hashSrc = await this.core.storageAccess.readHiddenFileBinary(file.path); - const hash = await webcrypto.subtle.digest("SHA-1", hashSrc); - const hashStr = uint8ArrayToHexString(new Uint8Array(hash)); - const item = { - path: file.path, - name: file.name, - size: stat.size, - mtime: stat.mtime, - hash: hashStr, - }; - // const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`; - out.push(item); - } - } - out.sort((a, b) => a.path.localeCompare(b.path)); - if (outFile) { - outFile = this.testInfoPath + this.nameByKind + "/" + outFile; - await this.core.storageAccess.ensureDir(outFile); - await this.core.storageAccess.writeHiddenFileAuto(outFile, stringifyYaml(out)); - } else { - // console.dir(out); - } - this._log(`Dumped ${out.length} files`, LOG_LEVEL_INFO); - } - - async __dumpFileListIncludeHidden(outFile?: string) { - const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns"); - const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns"); - const out = [] as any[]; - const files = await this.core.storageAccess.getFilesIncludeHidden("", targetPatterns, ignorePatterns); - // console.dir(files); - const webcrypto = await getWebCrypto(); - for (const file of files) { - // if (!await this.core.$$isTargetFile(file)) { - // continue; - // } - if (file.startsWith(this.testInfoPath)) continue; - const stat = await this.core.storageAccess.statHidden(file); - if (stat) { - const hashSrc = await this.core.storageAccess.readHiddenFileBinary(file); - const hash = await webcrypto.subtle.digest("SHA-1", hashSrc); - const hashStr = uint8ArrayToHexString(new Uint8Array(hash)); - const item = { - path: file, - name: file.split("/").pop(), - size: stat.size, - mtime: stat.mtime, - hash: hashStr, - }; - // const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`; - out.push(item); - } - } - out.sort((a, b) => a.path.localeCompare(b.path)); - if (outFile) { - outFile = this.testInfoPath + this.nameByKind + "/" + outFile; - await this.core.storageAccess.ensureDir(outFile); - await this.core.storageAccess.writeHiddenFileAuto(outFile, stringifyYaml(out)); - } else { - // console.dir(out); - } - this._log(`Dumped ${out.length} files`, LOG_LEVEL_NOTICE); - } - - async collectTestFiles() { - const remoteTopDir = "https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/refs/heads/main/"; - const files = [ - "README.md", - "docs/adding_translations.md", - "docs/design_docs_of_journalsync.md", - "docs/design_docs_of_keep_newborn_chunks.md", - "docs/design_docs_of_prefixed_hidden_file_sync.md", - "docs/design_docs_of_sharing_tweak_value.md", - "docs/quick_setup_cn.md", - "docs/quick_setup_ja.md", - "docs/quick_setup.md", - "docs/settings_ja.md", - "docs/settings.md", - "docs/setup_cloudant_ja.md", - "docs/setup_cloudant.md", - "docs/setup_flyio.md", - "docs/setup_own_server_cn.md", - "docs/setup_own_server_ja.md", - "docs/setup_own_server.md", - "docs/tech_info_ja.md", - "docs/tech_info.md", - "docs/terms.md", - "docs/troubleshooting.md", - "images/1.png", - "images/2.png", - "images/corrupted_data.png", - "images/hatch.png", - "images/lock_pattern1.png", - "images/lock_pattern2.png", - "images/quick_setup_1.png", - "images/quick_setup_2.png", - "images/quick_setup_3.png", - "images/quick_setup_3b.png", - "images/quick_setup_4.png", - "images/quick_setup_5.png", - "images/quick_setup_6.png", - "images/quick_setup_7.png", - "images/quick_setup_8.png", - "images/quick_setup_9_1.png", - "images/quick_setup_9_2.png", - "images/quick_setup_10.png", - "images/remote_db_setting.png", - "images/write_logs_into_the_file.png", - ]; - for (const file of files) { - const remote = remoteTopDir + file; - const local = this.testRootPath + file; - try { - const f = (await requestUrl(remote)).arrayBuffer; - await this.core.storageAccess.ensureDir(local); - await this.core.storageAccess.writeHiddenFileAuto(local, f); - } catch (ex) { - this._log(`Could not fetch ${remote}`, LOG_LEVEL_VERBOSE); - this._log(ex, LOG_LEVEL_VERBOSE); - } - } - - await this.dumpList(); - } - - async waitFor(proc: () => Promise, timeout = 10000): Promise { - await delay(100); - const start = Date.now(); - while (!(await proc())) { - if (timeout > 0) { - if (Date.now() - start > timeout) { - this._log(`Timeout`); - return false; - } - } - await delay(500); - } - return true; - } - - async testConflictedManually1() { - await this.services.replication.replicate(); - - const commonFile = `Resolve! -*****, the amazing chocolatier!!`; - - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", commonFile); - } - - await this.services.replication.replicate(); - await this.services.replication.replicate(); - if ( - (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - - const fileA = `Resolve to KEEP THIS -Willy Wonka, Willy Wonka, the amazing chocolatier!!`; - - const fileB = `Resolve to DISCARD THIS -Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`; - - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileA); - } else { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileB); - } - - if ( - (await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - await this.services.replication.replicate(); - await this.services.replication.replicate(); - - if ( - !(await this.waitFor(async () => { - await this.services.replication.replicate(); - return ( - (await this.__assertStorageContent( - (this.testRootPath + "wonka.md") as FilePath, - fileA, - false, - true - )) == true - ); - }, 30000)) - ) { - return await this.__assertStorageContent((this.testRootPath + "wonka.md") as FilePath, fileA, false, true); - } - return true; - // We have to check the result - } - - async testConflictedManually2() { - await this.services.replication.replicate(); - - const commonFile = `Resolve To concatenate -ABCDEFG`; - - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", commonFile); - } - - await this.services.replication.replicate(); - await this.services.replication.replicate(); - if ( - (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - - const fileA = `Resolve to Concatenate -ABCDEFGHIJKLMNOPQRSTYZ`; - - const fileB = `Resolve to Concatenate -AJKLMNOPQRSTUVWXYZ`; - - const concatenated = `Resolve to Concatenate -ABCDEFGHIJKLMNOPQRSTUVWXYZ`; - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileA); - } else { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileB); - } - if ( - (await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - await this.services.replication.replicate(); - await this.services.replication.replicate(); - - if ( - !(await this.waitFor(async () => { - await this.services.replication.replicate(); - return ( - (await this.__assertStorageContent( - (this.testRootPath + "concat.md") as FilePath, - concatenated, - false, - true - )) == true - ); - }, 30000)) - ) { - return await this.__assertStorageContent( - (this.testRootPath + "concat.md") as FilePath, - concatenated, - false, - true - ); - } - return true; - } - - async testConflictAutomatic() { - if (this.isLeader) { - const baseDoc = `Tasks! -- [ ] Task 1 -- [ ] Task 2 -- [ ] Task 3 -- [ ] Task 4 -`; - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc); - } - await delay(100); - await this.services.replication.replicate(); - await this.services.replication.replicate(); - - if ( - (await this.core.confirm.askYesNoDialog("Ready to test conflict?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - const mod1Doc = `Tasks! -- [ ] Task 1 -- [v] Task 2 -- [ ] Task 3 -- [ ] Task 4 -`; - - const mod2Doc = `Tasks! -- [ ] Task 1 -- [ ] Task 2 -- [v] Task 3 -- [ ] Task 4 -`; - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod1Doc); - } else { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod2Doc); - } - - await this.services.replication.replicate(); - await this.services.replication.replicate(); - await delay(1000); - if ( - (await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) == - "no" - ) { - return; - } - await this.services.replication.replicate(); - await this.services.replication.replicate(); - const mergedDoc = `Tasks! -- [ ] Task 1 -- [v] Task 2 -- [v] Task 3 -- [ ] Task 4 -`; - return this.__assertStorageContent((this.testRootPath + "task.md") as FilePath, mergedDoc, false, true); - } - - // No longer tested - async checkConflictResolution() { - this._log("Before testing conflicted files, resolve all once", LOG_LEVEL_NOTICE); - await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); - await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); - await this.services.replication.replicate(); - await delay(1000); - if (!(await this.testConflictAutomatic())) { - this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE); - return false; - } - if (!(await this.testConflictedManually1())) { - this._log("Conflict resolution (Manual1) failed", LOG_LEVEL_NOTICE); - return false; - } - if (!(await this.testConflictedManually2())) { - this._log("Conflict resolution (Manual2) failed", LOG_LEVEL_NOTICE); - return false; - } - return true; - } - - async __assertStorageContent( - fileName: FilePath, - content: string, - inverted = false, - showResult = false - ): Promise { - try { - const fileContent = await this.core.storageAccess.readHiddenFileText(fileName); - let result = fileContent === content; - if (inverted) { - result = !result; - } - if (result) { - return true; - } else { - if (showResult) { - this._log(`Content is not same \n Expected:${content}\n Actual:${fileContent}`, LOG_LEVEL_VERBOSE); - } - return `Content is not same \n Expected:${content}\n Actual:${fileContent}`; - } - } catch (e) { - this._log(`Cannot assert storage content: ${e}`); - return false; - } - } - async performTestManually() { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - await this.checkConflictResolution(); - // await this.collectTestFiles(); - } - - // testResults = writable<[boolean, string, string][]>([]); - // testResults: string[] = []; - - // $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void { - // const logLine = `${name}: ${key} ${summary ?? ""}`; - // this.testResults.update((results) => { - // results.push([result, logLine, message ?? ""]); - // return results; - // }); - // } - private async _everyModuleTestMultiDevice(): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - // this.core.$$addTestResult("DevModule", "Test", true); - // return Promise.resolve(true); - await this._test("Conflict resolution", async () => await this.checkConflictResolution()); - return this.testDone(); - } - override onBindFunction(core: LiveSyncCore, services: typeof core.services): void { - services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this)); - services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this)); - services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this)); - } -}