From 56743c4e8cc1ee4e5b3aa092338ed95e5880852f Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 5 Aug 2024 12:51:38 +0400 Subject: [PATCH 01/13] [cli] pocket-ic v4 support --- cli/commands/bench-replica.ts | 17 +++++++++++------ cli/package-lock.json | 16 ++++++++-------- cli/package.json | 2 +- mops.toml | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/cli/commands/bench-replica.ts b/cli/commands/bench-replica.ts index f89fccad..19699440 100644 --- a/cli/commands/bench-replica.ts +++ b/cli/commands/bench-replica.ts @@ -3,7 +3,7 @@ import {execSync} from 'node:child_process'; import path from 'node:path'; import fs from 'node:fs'; import {execaCommand} from 'execa'; -import {PocketIc} from 'pic-ic'; +import {PocketIc, PocketIcServer} from 'pic-ic'; import {getRootDir, readConfig} from '../mops.js'; import {createActor, idlFactory} from '../declarations/bench/index.js'; import {toolchain} from './toolchain/index.js'; @@ -12,6 +12,7 @@ export class BenchReplica { type : 'dfx' | 'pocket-ic'; verbose = false; canisters : Record = {}; + pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; constructor(type : 'dfx' | 'pocket-ic', verbose = false) { @@ -31,11 +32,14 @@ export class BenchReplica { else { let pocketIcBin = await toolchain.bin('pocket-ic'); let config = readConfig(); - if (config.toolchain?.['pocket-ic'] !== '1.0.0') { - console.error('Currently only pocket-ic 1.0.0 is supported'); + if (config.toolchain?.['pocket-ic'] !== '4.0.0') { + console.error('Current Mops CLI only supports pocket-ic 4.0.0'); process.exit(1); } - this.pocketIc = await PocketIc.create(pocketIcBin); + this.pocketIcServer = await PocketIcServer.start({ + binPath: pocketIcBin, + }); + this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl()); } } @@ -44,8 +48,9 @@ export class BenchReplica { let dir = path.join(getRootDir(), '.mops/.bench'); execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); } - else if (this.pocketIc) { + else if (this.pocketIc && this.pocketIcServer) { await this.pocketIc.tearDown(); + await this.pocketIcServer.stop(); } } @@ -61,7 +66,7 @@ export class BenchReplica { this.canisters[name] = {cwd, canisterId, actor}; } else if (this.pocketIc) { - let {canisterId, actor} = await this.pocketIc.setupCanister(idlFactory, wasm); + let {canisterId, actor} = await this.pocketIc.setupCanister({idlFactory, wasm}); this.canisters[name] = { cwd, canisterId: canisterId.toText(), diff --git a/cli/package-lock.json b/cli/package-lock.json index 172be48c..d503f1f8 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -43,7 +43,7 @@ "node-fetch": "3.3.2", "octokit": "3.1.2", "pem-file": "1.0.1", - "pic-ic": "0.3.3", + "pic-ic": "0.4.0", "prompts": "2.4.2", "semver": "7.6.3", "stream-to-promise": "3.0.0", @@ -5839,18 +5839,18 @@ "license": "MIT" }, "node_modules/pic-ic": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/pic-ic/-/pic-ic-0.3.3.tgz", - "integrity": "sha512-DxlNzYD2+4gjwuNbQmKj/7rJIb+s220M9kSXMwr7S8nbumxPdvAw02eBlB2BW+OlBFcYNN5bL1HGaYkV6OLTNA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/pic-ic/-/pic-ic-0.4.0.tgz", + "integrity": "sha512-JgEPBSSL2L81udOO7IQ1tr69sXg0tP/EwYtm3c6BXN5kYjrApFoUUPX/cn2dm+wmoLOlpL3p5yY2dwqytz3Nuw==", "license": "Apache-2.0", "dependencies": { "bip39": "^3.1.0" }, "peerDependencies": { - "@dfinity/agent": "*", - "@dfinity/candid": "*", - "@dfinity/identity": "*", - "@dfinity/principal": "*" + "@dfinity/agent": "^2.0.0", + "@dfinity/candid": "^2.0.0", + "@dfinity/identity": "^2.0.0", + "@dfinity/principal": "^2.0.0" } }, "node_modules/picomatch": { diff --git a/cli/package.json b/cli/package.json index a63c8a63..659a69c0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -73,7 +73,7 @@ "node-fetch": "3.3.2", "octokit": "3.1.2", "pem-file": "1.0.1", - "pic-ic": "0.3.3", + "pic-ic": "0.4.0", "prompts": "2.4.2", "semver": "7.6.3", "stream-to-promise": "3.0.0", diff --git a/mops.toml b/mops.toml index 0987cb14..0760cb2c 100644 --- a/mops.toml +++ b/mops.toml @@ -17,4 +17,4 @@ bench = "1.0.0" [toolchain] moc = "0.12.0" wasmtime = "23.0.1" -pocket-ic = "1.0.0" \ No newline at end of file +pocket-ic = "4.0.0" \ No newline at end of file From eb857660db9d2c58f02d2fedc5eba1fe2664292a Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 5 Aug 2024 17:10:14 +0400 Subject: [PATCH 02/13] [cli] actor test WIP --- cli/commands/bench.ts | 4 +- cli/commands/replica.ts | 112 ++++++++++++++++++++ cli/commands/test/test.ts | 25 +++-- test/{actor.test.mo => actor-class.test.mo} | 0 test/storage-actor.test.mo | 92 ++++++++++++++++ 5 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 cli/commands/replica.ts rename test/{actor.test.mo => actor-class.test.mo} (100%) create mode 100644 test/storage-actor.test.mo diff --git a/cli/commands/bench.ts b/cli/commands/bench.ts index a7e65588..09643e3d 100644 --- a/cli/commands/bench.ts +++ b/cli/commands/bench.ts @@ -96,7 +96,7 @@ export async function bench(filter = '', optionsArg : Partial = {} files.sort(); let benchDir = `${getRootDir()}/.mops/.bench/`; - fs.rmSync(benchDir, {recursive: true, force: true}); + // fs.rmSync(benchDir, {recursive: true, force: true}); fs.mkdirSync(benchDir, {recursive: true}); if (!options.silent) { @@ -145,7 +145,7 @@ export async function bench(filter = '', optionsArg : Partial = {} options.silent || console.log('Stopping replica...'); await replica.stop(); - fs.rmSync(benchDir, {recursive: true, force: true}); + // fs.rmSync(benchDir, {recursive: true, force: true}); return benchResults; } diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts new file mode 100644 index 00000000..19699440 --- /dev/null +++ b/cli/commands/replica.ts @@ -0,0 +1,112 @@ +import process from 'node:process'; +import {execSync} from 'node:child_process'; +import path from 'node:path'; +import fs from 'node:fs'; +import {execaCommand} from 'execa'; +import {PocketIc, PocketIcServer} from 'pic-ic'; +import {getRootDir, readConfig} from '../mops.js'; +import {createActor, idlFactory} from '../declarations/bench/index.js'; +import {toolchain} from './toolchain/index.js'; + +export class BenchReplica { + type : 'dfx' | 'pocket-ic'; + verbose = false; + canisters : Record = {}; + pocketIcServer ?: PocketIcServer; + pocketIc ?: PocketIc; + + constructor(type : 'dfx' | 'pocket-ic', verbose = false) { + this.type = type; + this.verbose = verbose; + } + + async start({silent = false} = {}) { + silent || console.log(`Starting ${this.type} replica...`); + + if (this.type == 'dfx') { + await this.stop(); + let dir = path.join(getRootDir(), '.mops/.bench'); + fs.writeFileSync(path.join(dir, 'dfx.json'), JSON.stringify(this.dfxJson(''), null, 2)); + execSync('dfx start --background --clean --artificial-delay 0' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['inherit', this.verbose ? 'inherit' : 'ignore', 'inherit']}); + } + else { + let pocketIcBin = await toolchain.bin('pocket-ic'); + let config = readConfig(); + if (config.toolchain?.['pocket-ic'] !== '4.0.0') { + console.error('Current Mops CLI only supports pocket-ic 4.0.0'); + process.exit(1); + } + this.pocketIcServer = await PocketIcServer.start({ + binPath: pocketIcBin, + }); + this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl()); + } + } + + async stop() { + if (this.type == 'dfx') { + let dir = path.join(getRootDir(), '.mops/.bench'); + execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); + } + else if (this.pocketIc && this.pocketIcServer) { + await this.pocketIc.tearDown(); + await this.pocketIcServer.stop(); + } + } + + async deploy(name : string, wasm : string, cwd : string = process.cwd()) { + if (this.type === 'dfx') { + await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); + let canisterId = execSync(`dfx canister id ${name}`, {cwd}).toString().trim(); + let actor = await createActor(canisterId, { + agentOptions: { + host: 'http://127.0.0.1:4944', + }, + }); + this.canisters[name] = {cwd, canisterId, actor}; + } + else if (this.pocketIc) { + let {canisterId, actor} = await this.pocketIc.setupCanister({idlFactory, wasm}); + this.canisters[name] = { + cwd, + canisterId: canisterId.toText(), + actor, + }; + } + } + + getActor(name : string) : unknown { + return this.canisters[name]?.actor; + } + + getCanisterId(name : string) : string { + return this.canisters[name]?.canisterId || ''; + } + + dfxJson(canisterName : string) { + let canisters : Record = {}; + if (canisterName) { + canisters[canisterName] = { + type: 'custom', + wasm: 'canister.wasm', + candid: 'canister.did', + }; + } + + return { + version: 1, + canisters, + defaults: { + build: { + packtool: 'mops sources', + }, + }, + networks: { + local: { + type: 'ephemeral', + bind: '127.0.0.1:4944', + }, + }, + }; + } +} diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 81d66956..96f238bc 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -34,7 +34,7 @@ let globConfig = { }; type ReporterName = 'verbose' | 'files' | 'compact' | 'silent'; -type TestMode = 'interpreter' | 'wasi'; +type TestMode = 'interpreter' | 'wasi' | 'replica'; export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode} = {}) { let rootDir = getRootDir(); @@ -131,9 +131,17 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : await parallel(os.cpus().length, files, async (file : string) => { let mmf = new MMF1('store', absToRel(file)); - let wasiMode = mode === 'wasi' || fs.readFileSync(file, 'utf8').startsWith('// @testmode wasi'); - if (wasiMode && !wasmtimePath) { + // mode overrides + let lines = fs.readFileSync(file, 'utf8').split('\n'); + if (lines.includes('// @testmode wasi')) { + mode = 'wasi'; + } + else if (lines.includes('actor {')) { + mode = 'replica'; + } + + if (mode === 'wasi' && !wasmtimePath) { // ensure wasmtime is installed or specified in config if (config.toolchain?.wasmtime) { wasmtimePath = await toolchain.bin('wasmtime'); @@ -150,7 +158,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : let mocArgs = ['--hide-warnings', '--error-detail=2', ...sourcesArr.join(' ').split(' '), file].filter(x => x); // build and run wasm - if (wasiMode) { + if (mode === 'wasi') { let wasmFile = `${path.join(wasmDir, path.parse(file).name)}.wasm`; // build @@ -189,13 +197,18 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : }).then(resolve); } // interpret - else { + else if (mode === 'interpreter') { let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs]); pipeMMF(proc, mmf).then(resolve); } + // build and execute in replica + else if (mode === 'replica') { + let proc = spawn(mocPath, ['-r', '-replica-system-api', ...mocArgs]); + pipeMMF(proc, mmf).then(resolve); + } }); - reporter.addRun(file, mmf, promise, wasiMode); + reporter.addRun(file, mmf, promise, mode === 'wasi'); await promise; }); diff --git a/test/actor.test.mo b/test/actor-class.test.mo similarity index 100% rename from test/actor.test.mo rename to test/actor-class.test.mo diff --git a/test/storage-actor.test.mo b/test/storage-actor.test.mo new file mode 100644 index 00000000..a682406b --- /dev/null +++ b/test/storage-actor.test.mo @@ -0,0 +1,92 @@ +import {test; suite; skip} "mo:test/async"; +import Result "mo:base/Result"; +import Blob "mo:base/Blob"; + +import Storage "../backend/storage/storage-canister"; + +actor { + public func runTests() : async () { + var storage = await Storage.Storage(); + let fileId = "test"; + + // upload + await suite("storage upload", func() : async () { + await test("try to finish upload before upload start", func() : async () { + let res = await storage.finishUploads([fileId]); + assert Result.isErr(res); + }); + + await test("try to upload chunk before upload start", func() : async () { + assert Result.isErr(await storage.uploadChunk(fileId, 0, Blob.fromArray([]))); + }); + + await test("start upload", func() : async () { + assert Result.isOk(await storage.startUpload({ + id = fileId; + path = "test/test.mo"; + chunkCount = 1; + owners = []; + })); + }); + + await test("try to finish upload with unknown file id", func() : async () { + assert Result.isErr(await storage.finishUploads([fileId, "unknown-file-id"])); + }); + + await test("finish upload", func() : async () { + assert Result.isOk(await storage.finishUploads([fileId])); + }); + + await test("try to finish already finished upload", func() : async () { + assert Result.isErr(await storage.finishUploads([fileId])); + }); + + await test("try to start upload existing file", func() : async () { + assert Result.isErr(await storage.startUpload({ + id = fileId; + path = "test/test.mo"; + chunkCount = 1; + owners = []; + })); + }); + }); + + // download + await suite("storage download", func() : async () { + await test("get file meta", func() : async () { + let res = await storage.getFileMeta(fileId); + assert Result.isOk(res); + switch (res) { + case (#ok(fileMeta)) { + assert fileMeta.path == "test/test.mo"; + assert fileMeta.chunkCount == 1; + assert fileMeta.owners == []; + }; + case (_) {}; + }; + }); + + await test("try to get file meta of unknown file", func() : async () { + let res = await storage.getFileMeta("123"); + assert Result.isErr(res); + }); + + await test("upgrade storage canister", func() : async () { + storage := await (system Storage.Storage)(#upgrade(storage))(); + }); + + await test("get file meta after upgrade", func() : async () { + let res = await storage.getFileMeta(fileId); + assert Result.isOk(res); + switch (res) { + case (#ok(fileMeta)) { + assert fileMeta.path == "test/test.mo"; + assert fileMeta.chunkCount == 1; + assert fileMeta.owners == []; + }; + case (_) {}; + }; + }); + }); + }; +}; \ No newline at end of file From e5664b6d0aa59969edfaef53133cc71ad9b63c51 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Thu, 22 Aug 2024 15:32:19 +0400 Subject: [PATCH 03/13] [cli] actor test WIP --- cli/commands/replica.ts | 48 +++++++++++++++++++++++++++----------- cli/commands/test/test.ts | 48 ++++++++++++++++++++++++++++++++++++-- t.js | 20 ++++++++++++++++ test/storage-actor.test.mo | 3 +++ 4 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 t.js diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index 19699440..bd674827 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -3,21 +3,24 @@ import {execSync} from 'node:child_process'; import path from 'node:path'; import fs from 'node:fs'; import {execaCommand} from 'execa'; +import {IDL} from '@dfinity/candid'; +import {Actor, HttpAgent} from '@dfinity/agent'; import {PocketIc, PocketIcServer} from 'pic-ic'; import {getRootDir, readConfig} from '../mops.js'; -import {createActor, idlFactory} from '../declarations/bench/index.js'; import {toolchain} from './toolchain/index.js'; -export class BenchReplica { +export class Replica { type : 'dfx' | 'pocket-ic'; verbose = false; canisters : Record = {}; pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; + dir : string; // '.mops/.bench/' - constructor(type : 'dfx' | 'pocket-ic', verbose = false) { + constructor(type : 'dfx' | 'pocket-ic', dir : string, verbose = false) { this.type = type; this.verbose = verbose; + this.dir = dir; } async start({silent = false} = {}) { @@ -25,27 +28,33 @@ export class BenchReplica { if (this.type == 'dfx') { await this.stop(); - let dir = path.join(getRootDir(), '.mops/.bench'); + let dir = path.join(getRootDir(), this.dir); fs.writeFileSync(path.join(dir, 'dfx.json'), JSON.stringify(this.dfxJson(''), null, 2)); execSync('dfx start --background --clean --artificial-delay 0' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['inherit', this.verbose ? 'inherit' : 'ignore', 'inherit']}); } else { let pocketIcBin = await toolchain.bin('pocket-ic'); + + // eslint-disable-next-line let config = readConfig(); - if (config.toolchain?.['pocket-ic'] !== '4.0.0') { - console.error('Current Mops CLI only supports pocket-ic 4.0.0'); - process.exit(1); - } + // if (config.toolchain?.['pocket-ic'] !== '4.0.0') { + // console.error('Current Mops CLI only supports pocket-ic 4.0.0'); + // process.exit(1); + // } + this.pocketIcServer = await PocketIcServer.start({ + showRuntimeLogs: true, + showCanisterLogs: true, binPath: pocketIcBin, }); + this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl()); } } async stop() { if (this.type == 'dfx') { - let dir = path.join(getRootDir(), '.mops/.bench'); + let dir = path.join(getRootDir(), this.dir); execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); } else if (this.pocketIc && this.pocketIcServer) { @@ -54,19 +63,27 @@ export class BenchReplica { } } - async deploy(name : string, wasm : string, cwd : string = process.cwd()) { + async deploy(name : string, wasm : string, idlFactory : IDL.InterfaceFactory, cwd : string = process.cwd()) { if (this.type === 'dfx') { await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); let canisterId = execSync(`dfx canister id ${name}`, {cwd}).toString().trim(); - let actor = await createActor(canisterId, { - agentOptions: { + + let actor = Actor.createActor(idlFactory, { + agent: await HttpAgent.create({ host: 'http://127.0.0.1:4944', - }, + shouldFetchRootKey: true, + }), + canisterId, }); + this.canisters[name] = {cwd, canisterId, actor}; } else if (this.pocketIc) { - let {canisterId, actor} = await this.pocketIc.setupCanister({idlFactory, wasm}); + let {canisterId, actor} = await this.pocketIc.setupCanister({ + idlFactory, + wasm, + }); + await this.pocketIc.addCycles(canisterId, 1_000_000_000_000); this.canisters[name] = { cwd, canisterId: canisterId.toText(), @@ -76,6 +93,9 @@ export class BenchReplica { } getActor(name : string) : unknown { + if (!this.canisters[name]) { + throw new Error(`Canister ${name} not found`); + } return this.canisters[name]?.actor; } diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 96f238bc..ffc26fc0 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -20,6 +20,8 @@ import {FilesReporter} from './reporters/files-reporter.js'; import {CompactReporter} from './reporters/compact-reporter.js'; import {SilentReporter} from './reporters/silent-reporter.js'; import {toolchain} from '../toolchain/index.js'; +import {Replica} from '../replica.js'; +import {ActorMethod} from '@dfinity/agent'; let ignore = [ '**/node_modules/**', @@ -180,6 +182,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : wasmFile, ]; } + // backcompat else { wasmtimeArgs = [ '--max-wasm-stack=4000000', @@ -203,8 +206,49 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : } // build and execute in replica else if (mode === 'replica') { - let proc = spawn(mocPath, ['-r', '-replica-system-api', ...mocArgs]); - pipeMMF(proc, mmf).then(resolve); + let wasmFile = `${path.join(wasmDir, path.parse(file).name)}.wasm`; + + // build + // let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs]); + + console.log('Building...'); + // pipeMMF(buildProc, mmf).then(async () => { + (async () => { + if (mmf.failed > 0) { + return; + } + console.log('Running replica...'); + let replica = new Replica('pocket-ic', wasmDir); + await replica.start(); + + + let canisterName = path.parse(file).name; + let idlFactory = ({IDL} : any) => { + return IDL.Service({'runTests': IDL.Func([], [], [])}); + }; + interface _SERVICE {'runTests' : ActorMethod<[], undefined>;} + + console.log('Deploying...'); + await replica.deploy(canisterName, wasmFile, idlFactory); + + console.log('Getting actor...'); + console.log(replica.getCanisterId(canisterName)); + let actor = await replica.getActor(canisterName) as _SERVICE; + + console.log('Running tests...'); + await actor.runTests(); + + // // run + // let proc = spawn(wasmtimePath, wasmtimeArgs); + // await pipeMMF(proc, mmf); + + // run + // let proc = spawn(wasmtimePath, wasmtimeArgs); + // await pipeMMF(proc, mmf); + })().finally(() => { + // }).finally(() => { + // fs.rmSync(wasmFile, {force: true}); + }).then(resolve); } }); diff --git a/t.js b/t.js new file mode 100644 index 00000000..307ec24f --- /dev/null +++ b/t.js @@ -0,0 +1,20 @@ +import {PocketIc, PocketIcServer} from '@hadronous/pic'; + +let picServer = await PocketIcServer.start({ + showRuntimeLogs: true, + showCanisterLogs: true, +}); +let pic = await PocketIc.create(picServer.getUrl()); +let {canisterId, actor} = await pic.setupCanister({ + idlFactory: ({IDL}) => { + return IDL.Service({'runTests': IDL.Func([], [], [])}); + }, + wasm: '.mops/.test/storage-actor.test.wasm', + cycles: 1_000_000_000_000_000_000n, // doesn't help +}); + +await pic.addCycles(canisterId, 1_000_000_000_000); // doesn't help + +await actor.runTests(); // error + +await picServer.stop(); \ No newline at end of file diff --git a/test/storage-actor.test.mo b/test/storage-actor.test.mo index a682406b..1e039ff5 100644 --- a/test/storage-actor.test.mo +++ b/test/storage-actor.test.mo @@ -1,6 +1,7 @@ import {test; suite; skip} "mo:test/async"; import Result "mo:base/Result"; import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; import Storage "../backend/storage/storage-canister"; @@ -9,6 +10,8 @@ actor { var storage = await Storage.Storage(); let fileId = "test"; + Debug.print(debug_show("lalalalalalala")); + // upload await suite("storage upload", func() : async () { await test("try to finish upload before upload start", func() : async () { From bd1e870e9fd18ab5fb84090b9ad5d7ba4f2a2edf Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 23 Aug 2024 13:13:36 +0400 Subject: [PATCH 04/13] [cli] actor test WIP --- cli/api/actors.ts | 2 +- cli/commands/replica.ts | 57 ++++++++++++++++++++++++++------------ cli/commands/test/test.ts | 28 +++++++++++++------ test/storage-actor.test.mo | 7 ++++- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/cli/api/actors.ts b/cli/api/actors.ts index 288a8f01..c055d13e 100644 --- a/cli/api/actors.ts +++ b/cli/api/actors.ts @@ -8,7 +8,7 @@ import {_SERVICE as _STORAGE_SERVICE} from '../declarations/storage/storage.did. import {getEndpoint} from './network.js'; import {getNetwork} from './network.js'; -let agentPromiseByPrincipal = new Map >(); +let agentPromiseByPrincipal = new Map>(); let getAgent = async (identity ?: Identity) : Promise => { let principal = identity ? identity?.getPrincipal().toText() : ''; diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index bd674827..03232db1 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -5,8 +5,9 @@ import fs from 'node:fs'; import {execaCommand} from 'execa'; import {IDL} from '@dfinity/candid'; import {Actor, HttpAgent} from '@dfinity/agent'; +// import {PocketIc} from 'pic-ic'; import {PocketIc, PocketIcServer} from 'pic-ic'; -import {getRootDir, readConfig} from '../mops.js'; +import {readConfig} from '../mops.js'; import {toolchain} from './toolchain/index.js'; export class Replica { @@ -15,7 +16,7 @@ export class Replica { canisters : Record = {}; pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; - dir : string; // '.mops/.bench/' + dir : string; // absolute path (.mops/.test/) constructor(type : 'dfx' | 'pocket-ic', dir : string, verbose = false) { this.type = type; @@ -27,46 +28,67 @@ export class Replica { silent || console.log(`Starting ${this.type} replica...`); if (this.type == 'dfx') { + fs.mkdirSync(this.dir, {recursive: true}); + fs.writeFileSync(path.join(this.dir, 'dfx.json'), JSON.stringify(this.dfxJson(''), null, 2)); + fs.writeFileSync(path.join(this.dir, 'canister.did'), 'service : { runTests: () -> (); }'); + await this.stop(); - let dir = path.join(getRootDir(), this.dir); - fs.writeFileSync(path.join(dir, 'dfx.json'), JSON.stringify(this.dfxJson(''), null, 2)); - execSync('dfx start --background --clean --artificial-delay 0' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['inherit', this.verbose ? 'inherit' : 'ignore', 'inherit']}); + execSync('dfx start --background --clean --artificial-delay 0' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['inherit', this.verbose ? 'inherit' : 'ignore', 'inherit']}); } else { let pocketIcBin = await toolchain.bin('pocket-ic'); // eslint-disable-next-line let config = readConfig(); - // if (config.toolchain?.['pocket-ic'] !== '4.0.0') { - // console.error('Current Mops CLI only supports pocket-ic 4.0.0'); - // process.exit(1); - // } + if (config.toolchain?.['pocket-ic'] !== '4.0.0') { + console.error('Current Mops CLI only supports pocket-ic 4.0.0'); + process.exit(1); + } + + // this.pocketIc = await PocketIc.create(pocketIcBin); this.pocketIcServer = await PocketIcServer.start({ showRuntimeLogs: true, showCanisterLogs: true, binPath: pocketIcBin, }); - this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl()); } } async stop() { if (this.type == 'dfx') { - let dir = path.join(getRootDir(), this.dir); - execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); + execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); } + // else if (this.pocketIc) { else if (this.pocketIc && this.pocketIcServer) { + console.log('stop'); await this.pocketIc.tearDown(); + console.log('stopped'); await this.pocketIcServer.stop(); } } async deploy(name : string, wasm : string, idlFactory : IDL.InterfaceFactory, cwd : string = process.cwd()) { if (this.type === 'dfx') { - await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); - let canisterId = execSync(`dfx canister id ${name}`, {cwd}).toString().trim(); + // prepare dfx.json for current canister + let dfxJson = path.join(this.dir, 'dfx.json'); + + let oldDfxJsonData; + if (fs.existsSync(dfxJson)) { + oldDfxJsonData = JSON.parse(fs.readFileSync(dfxJson).toString()); + } + let newDfxJsonData = this.dfxJson(name, name + '.wasm'); + + if (oldDfxJsonData.canisters) { + newDfxJsonData.canisters = Object.assign(oldDfxJsonData.canisters, newDfxJsonData.canisters); + } + + fs.mkdirSync(this.dir, {recursive: true}); + fs.writeFileSync(dfxJson, JSON.stringify(newDfxJsonData, null, 2)); + + await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); + let canisterId = execSync(`dfx canister id ${name}`, {cwd: this.dir}).toString().trim(); let actor = Actor.createActor(idlFactory, { agent: await HttpAgent.create({ @@ -79,6 +101,7 @@ export class Replica { this.canisters[name] = {cwd, canisterId, actor}; } else if (this.pocketIc) { + // let {canisterId, actor} = await this.pocketIc.setupCanister(idlFactory, wasm); let {canisterId, actor} = await this.pocketIc.setupCanister({ idlFactory, wasm, @@ -103,13 +126,13 @@ export class Replica { return this.canisters[name]?.canisterId || ''; } - dfxJson(canisterName : string) { + dfxJson(canisterName : string, wasmPath = 'canister.wasm', didPath = 'canister.did') { let canisters : Record = {}; if (canisterName) { canisters[canisterName] = { type: 'custom', - wasm: 'canister.wasm', - candid: 'canister.did', + wasm: wasmPath, + candid: didPath, }; } diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index ffc26fc0..4409e951 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -128,9 +128,21 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : mocPath = await toolchain.bin('moc', {fallback: true}); } - let wasmDir = `${getRootDir()}/.mops/.test/`; + let wasmDir = path.join(getRootDir(), '.mops/.test/'); fs.mkdirSync(wasmDir, {recursive: true}); + let replica = new Replica('pocket-ic', wasmDir); + let replicaStartPromise : Promise | undefined; + + async function startReplicaOnce(replica : Replica) { + if (!replicaStartPromise) { + replicaStartPromise = new Promise((resolve) => { + replica.start().then(resolve); + }); + } + return replicaStartPromise; + } + await parallel(os.cpus().length, files, async (file : string) => { let mmf = new MMF1('store', absToRel(file)); @@ -139,7 +151,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : if (lines.includes('// @testmode wasi')) { mode = 'wasi'; } - else if (lines.includes('actor {')) { + else if (lines.includes('actor {') || lines.includes('// @testmode replica')) { mode = 'replica'; } @@ -218,9 +230,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : return; } console.log('Running replica...'); - let replica = new Replica('pocket-ic', wasmDir); - await replica.start(); - + await startReplicaOnce(replica); let canisterName = path.parse(file).name; let idlFactory = ({IDL} : any) => { @@ -237,6 +247,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : console.log('Running tests...'); await actor.runTests(); + console.log('Ready'); // // run // let proc = spawn(wasmtimePath, wasmtimeArgs); @@ -245,9 +256,10 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : // run // let proc = spawn(wasmtimePath, wasmtimeArgs); // await pipeMMF(proc, mmf); - })().finally(() => { - // }).finally(() => { + })().finally(async () => { + // }).finally(async () => { // fs.rmSync(wasmFile, {force: true}); + await replica.stop(); }).then(resolve); } }); @@ -257,7 +269,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : await promise; }); - fs.rmSync(wasmDir, {recursive: true, force: true}); + // fs.rmSync(wasmDir, {recursive: true, force: true}); return reporter.done(); } diff --git a/test/storage-actor.test.mo b/test/storage-actor.test.mo index 1e039ff5..1c0e1530 100644 --- a/test/storage-actor.test.mo +++ b/test/storage-actor.test.mo @@ -2,12 +2,15 @@ import {test; suite; skip} "mo:test/async"; import Result "mo:base/Result"; import Blob "mo:base/Blob"; import Debug "mo:base/Debug"; +import ExperimentalCycles "mo:base/ExperimentalCycles"; import Storage "../backend/storage/storage-canister"; actor { - public func runTests() : async () { + public func runTests() : async () { + ExperimentalCycles.add(1_000_000_000_000); var storage = await Storage.Storage(); + let fileId = "test"; Debug.print(debug_show("lalalalalalala")); @@ -19,6 +22,8 @@ actor { assert Result.isErr(res); }); + // assert false; + await test("try to upload chunk before upload start", func() : async () { assert Result.isErr(await storage.uploadChunk(fileId, 0, Blob.fromArray([]))); }); From 4b8640d735aa7c0a7c5cab940143a8b33b6f7562 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 23 Aug 2024 16:07:03 +0400 Subject: [PATCH 05/13] [cli] actor test WIP --- cli/commands/replica.ts | 61 ++++++- .../test/reporters/compact-reporter.ts | 3 +- cli/commands/test/reporters/files-reporter.ts | 5 +- cli/commands/test/reporters/reporter.ts | 3 +- .../test/reporters/silent-reporter.ts | 3 +- .../test/reporters/verbose-reporter.ts | 5 +- cli/commands/test/test.ts | 158 +++++++++--------- cli/types.ts | 4 +- test/storage-actor.test.mo | 6 +- 9 files changed, 152 insertions(+), 96 deletions(-) diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index 03232db1..a6e2abd3 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -5,15 +5,15 @@ import fs from 'node:fs'; import {execaCommand} from 'execa'; import {IDL} from '@dfinity/candid'; import {Actor, HttpAgent} from '@dfinity/agent'; -// import {PocketIc} from 'pic-ic'; import {PocketIc, PocketIcServer} from 'pic-ic'; import {readConfig} from '../mops.js'; import {toolchain} from './toolchain/index.js'; +import {PassThrough} from 'node:stream'; export class Replica { type : 'dfx' | 'pocket-ic'; verbose = false; - canisters : Record = {}; + canisters : Record = {}; pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; dir : string; // absolute path (.mops/.test/) @@ -45,14 +45,34 @@ export class Replica { process.exit(1); } - // this.pocketIc = await PocketIc.create(pocketIcBin); - this.pocketIcServer = await PocketIcServer.start({ - showRuntimeLogs: true, - showCanisterLogs: true, + showRuntimeLogs: false, + showCanisterLogs: false, binPath: pocketIcBin, }); this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl()); + + // process canister logs + let curData = ''; + this.pocketIcServer.serverProcess.stderr.on('data', (data) => { + curData = curData + data.toString(); + + if (curData.includes('\n')) { + let m = curData.match(/\[Canister ([a-z0-9-]+)\] (.*)/); + if (!m) { + return; + } + let [, canisterId, msg] = m; + + let stream = this.getCanisterStream(canisterId || ''); + if (stream) { + stream.write(msg); + } + + curData = ''; + } + + }); } } @@ -62,9 +82,7 @@ export class Replica { } // else if (this.pocketIc) { else if (this.pocketIc && this.pocketIcServer) { - console.log('stop'); await this.pocketIc.tearDown(); - console.log('stopped'); await this.pocketIcServer.stop(); } } @@ -98,7 +116,12 @@ export class Replica { canisterId, }); - this.canisters[name] = {cwd, canisterId, actor}; + this.canisters[name] = { + cwd, + canisterId, + actor, + stream: new PassThrough(), + }; } else if (this.pocketIc) { // let {canisterId, actor} = await this.pocketIc.setupCanister(idlFactory, wasm); @@ -111,8 +134,15 @@ export class Replica { cwd, canisterId: canisterId.toText(), actor, + stream: new PassThrough(), }; } + + if (!this.canisters[name]) { + throw new Error(`Canister ${name} not found`); + } + + return this.canisters[name]; } getActor(name : string) : unknown { @@ -122,10 +152,23 @@ export class Replica { return this.canisters[name]?.actor; } + getCanister(name : string) { + return this.canisters[name]; + } + getCanisterId(name : string) : string { return this.canisters[name]?.canisterId || ''; } + getCanisterStream(canisterId : string) : PassThrough | null { + for (let canister of Object.values(this.canisters)) { + if (canister.canisterId === canisterId) { + return canister.stream; + } + } + return null; + } + dfxJson(canisterName : string, wasmPath = 'canister.wasm', didPath = 'canister.did') { let canisters : Record = {}; if (canisterName) { diff --git a/cli/commands/test/reporters/compact-reporter.ts b/cli/commands/test/reporters/compact-reporter.ts index d153ed80..86d1d79f 100644 --- a/cli/commands/test/reporters/compact-reporter.ts +++ b/cli/commands/test/reporters/compact-reporter.ts @@ -3,6 +3,7 @@ import logUpdate from 'log-update'; import {absToRel} from '../utils.js'; import {MMF1} from '../mmf1.js'; import {Reporter} from './reporter.js'; +import {TestMode} from '../../../types.js'; export class CompactReporter implements Reporter { passed = 0; @@ -23,7 +24,7 @@ export class CompactReporter implements Reporter { this.#startTimer(); } - addRun(file : string, mmf : MMF1, state : Promise, _wasiMode : boolean) { + addRun(file : string, mmf : MMF1, state : Promise, _mode : TestMode) { this.#runningFiles.add(file); this.#log(); diff --git a/cli/commands/test/reporters/files-reporter.ts b/cli/commands/test/reporters/files-reporter.ts index bc2818ea..9f525dc4 100644 --- a/cli/commands/test/reporters/files-reporter.ts +++ b/cli/commands/test/reporters/files-reporter.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import {absToRel} from '../utils.js'; import {MMF1} from '../mmf1.js'; import {Reporter} from './reporter.js'; +import {TestMode} from '../../../types.js'; export class FilesReporter implements Reporter { passed = 0; @@ -15,7 +16,7 @@ export class FilesReporter implements Reporter { console.log('='.repeat(50)); } - addRun(file : string, mmf : MMF1, state : Promise, wasiMode : boolean) { + addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode) { state.then(() => { this.passed += Number(mmf.failed === 0); this.failed += Number(mmf.failed !== 0); @@ -27,7 +28,7 @@ export class FilesReporter implements Reporter { console.log('-'.repeat(50)); } else { - console.log(`${chalk.green('✓')} ${absToRel(file)} ${wasiMode ? chalk.gray('(wasi)') : ''}`); + console.log(`${chalk.green('✓')} ${absToRel(file)} ${mode === 'interpreter' ? '' : chalk.gray(`(${mode})`)}`); } }); } diff --git a/cli/commands/test/reporters/reporter.ts b/cli/commands/test/reporters/reporter.ts index 4aae1256..8859269b 100644 --- a/cli/commands/test/reporters/reporter.ts +++ b/cli/commands/test/reporters/reporter.ts @@ -1,7 +1,8 @@ +import {TestMode} from '../../../types.js'; import {MMF1} from '../mmf1.js'; export interface Reporter { addFiles(files : string[]) : void; - addRun(file : string, mmf : MMF1, state : Promise, wasiMode : boolean) : void; + addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode) : void; done() : boolean; } \ No newline at end of file diff --git a/cli/commands/test/reporters/silent-reporter.ts b/cli/commands/test/reporters/silent-reporter.ts index 0315235b..7b4cd4e8 100644 --- a/cli/commands/test/reporters/silent-reporter.ts +++ b/cli/commands/test/reporters/silent-reporter.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import {absToRel} from '../utils.js'; import {MMF1} from '../mmf1.js'; import {Reporter} from './reporter.js'; +import {TestMode} from '../../../types.js'; export class SilentReporter implements Reporter { passed = 0; @@ -13,7 +14,7 @@ export class SilentReporter implements Reporter { addFiles(_files : string[]) {} - addRun(file : string, mmf : MMF1, state : Promise, _wasiMode : boolean) { + addRun(file : string, mmf : MMF1, state : Promise, _mode : TestMode) { state.then(() => { this.passed += mmf.passed; this.failed += mmf.failed; diff --git a/cli/commands/test/reporters/verbose-reporter.ts b/cli/commands/test/reporters/verbose-reporter.ts index cf6c1c93..15c5f861 100644 --- a/cli/commands/test/reporters/verbose-reporter.ts +++ b/cli/commands/test/reporters/verbose-reporter.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import {absToRel} from '../utils.js'; import {MMF1} from '../mmf1.js'; import {Reporter} from './reporter.js'; +import {TestMode} from '../../../types.js'; export class VerboseReporter implements Reporter { passed = 0; @@ -19,7 +20,7 @@ export class VerboseReporter implements Reporter { console.log('='.repeat(50)); } - addRun(file : string, mmf : MMF1, state : Promise, wasiMode : boolean) { + addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode) { state.then(() => { this.passed += mmf.passed; this.failed += mmf.failed; @@ -30,7 +31,7 @@ export class VerboseReporter implements Reporter { } this.#curFileIndex++ && console.log('-'.repeat(50)); - console.log(`Running ${chalk.gray(absToRel(file))} ${wasiMode ? chalk.gray('(wasi)') : ''}`); + console.log(`Running ${chalk.gray(absToRel(file))} ${mode === 'interpreter' ? '' : chalk.gray(`(${mode})`)}`); mmf.flush(); }); } diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 4409e951..8c23f40c 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -22,6 +22,8 @@ import {SilentReporter} from './reporters/silent-reporter.js'; import {toolchain} from '../toolchain/index.js'; import {Replica} from '../replica.js'; import {ActorMethod} from '@dfinity/agent'; +import {PassThrough, Readable} from 'node:stream'; +import {TestMode} from '../../types.js'; let ignore = [ '**/node_modules/**', @@ -36,7 +38,6 @@ let globConfig = { }; type ReporterName = 'verbose' | 'files' | 'compact' | 'silent'; -type TestMode = 'interpreter' | 'wasi' | 'replica'; export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode} = {}) { let rootDir = getRootDir(); @@ -95,7 +96,7 @@ export async function runAll(reporterName : ReporterName = 'verbose', filter = ' return done; } -export async function testWithReporter(reporter : Reporter, filter = '', mode : TestMode = 'interpreter') : Promise { +export async function testWithReporter(reporter : Reporter, filter = '', defaultMode : TestMode = 'interpreter') : Promise { let rootDir = getRootDir(); let files : string[] = []; let libFiles = globSync('**/test?(s)/lib.mo', globConfig); @@ -137,7 +138,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : async function startReplicaOnce(replica : Replica) { if (!replicaStartPromise) { replicaStartPromise = new Promise((resolve) => { - replica.start().then(resolve); + replica.start({silent: true}).then(resolve); }); } return replicaStartPromise; @@ -148,6 +149,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : // mode overrides let lines = fs.readFileSync(file, 'utf8').split('\n'); + let mode = defaultMode; if (lines.includes('// @testmode wasi')) { mode = 'wasi'; } @@ -171,8 +173,13 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : let promise = new Promise((resolve) => { let mocArgs = ['--hide-warnings', '--error-detail=2', ...sourcesArr.join(' ').split(' '), file].filter(x => x); + // interpret + if (mode === 'interpreter') { + let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs]); + pipeMMF(proc, mmf).then(resolve); + } // build and run wasm - if (mode === 'wasi') { + else if (mode === 'wasi') { let wasmFile = `${path.join(wasmDir, path.parse(file).name)}.wasm`; // build @@ -211,25 +218,18 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : fs.rmSync(wasmFile, {force: true}); }).then(resolve); } - // interpret - else if (mode === 'interpreter') { - let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs]); - pipeMMF(proc, mmf).then(resolve); - } // build and execute in replica else if (mode === 'replica') { let wasmFile = `${path.join(wasmDir, path.parse(file).name)}.wasm`; // build - // let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs]); + let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs]); - console.log('Building...'); - // pipeMMF(buildProc, mmf).then(async () => { - (async () => { + pipeMMF(buildProc, mmf).then(async () => { if (mmf.failed > 0) { return; } - console.log('Running replica...'); + await startReplicaOnce(replica); let canisterName = path.parse(file).name; @@ -238,86 +238,94 @@ export async function testWithReporter(reporter : Reporter, filter = '', mode : }; interface _SERVICE {'runTests' : ActorMethod<[], undefined>;} - console.log('Deploying...'); - await replica.deploy(canisterName, wasmFile, idlFactory); + let {stream} = await replica.deploy(canisterName, wasmFile, idlFactory); - console.log('Getting actor...'); - console.log(replica.getCanisterId(canisterName)); - let actor = await replica.getActor(canisterName) as _SERVICE; - - console.log('Running tests...'); - await actor.runTests(); - console.log('Ready'); + pipeStdoutToMMF(stream, mmf); - // // run - // let proc = spawn(wasmtimePath, wasmtimeArgs); - // await pipeMMF(proc, mmf); + let actor = await replica.getActor(canisterName) as _SERVICE; - // run - // let proc = spawn(wasmtimePath, wasmtimeArgs); - // await pipeMMF(proc, mmf); - })().finally(async () => { - // }).finally(async () => { - // fs.rmSync(wasmFile, {force: true}); - await replica.stop(); + try { + await actor.runTests(); + mmf.pass(); + } + catch (e : any) { + let stderrStream = new PassThrough(); + pipeStderrToMMF(stderrStream, mmf, path.dirname(file)); + stderrStream.write(e.message); + } + }).finally(async () => { + fs.rmSync(wasmFile, {force: true}); }).then(resolve); } }); - reporter.addRun(file, mmf, promise, mode === 'wasi'); + reporter.addRun(file, mmf, promise, mode); await promise; }); - // fs.rmSync(wasmDir, {recursive: true, force: true}); + fs.rmSync(wasmDir, {recursive: true, force: true}); + if (replicaStartPromise) { + await replica.stop(); + } + return reporter.done(); } -function pipeMMF(proc : ChildProcessWithoutNullStreams, mmf : MMF1) { - return new Promise((resolve) => { - // stdout - proc.stdout.on('data', (data) => { - for (let line of data.toString().split('\n')) { - line = line.trim(); - if (line) { - mmf.parseLine(line); - } +function pipeStdoutToMMF(stdout : Readable, mmf : MMF1) { + stdout.on('data', (data) => { + for (let line of data.toString().split('\n')) { + line = line.trim(); + if (line) { + mmf.parseLine(line); } - }); + } + }); +} - // stderr - proc.stderr.on('data', (data) => { - let text : string = data.toString().trim(); - let failedLine = ''; - text = text.replace(/([\w+._/-]+):(\d+).(\d+)(-\d+.\d+)/g, (_m0, m1 : string, m2 : string, m3 : string) => { - // change absolute file path to relative - // change :line:col-line:col to :line:col to work in vscode - let res = `${absToRel(m1)}:${m2}:${m3}`; - - if (!fs.existsSync(m1)) { - return res; - } - - // show failed line - let content = fs.readFileSync(m1); - let lines = content.toString().split('\n') || []; - failedLine += chalk.dim('\n ...'); - let lineBefore = lines[+m2 - 2]; - if (lineBefore) { - failedLine += chalk.dim(`\n ${+m2 - 1}\t| ${lineBefore.replaceAll('\t', ' ')}`); - } - failedLine += `\n${chalk.redBright`->`} ${m2}\t| ${lines[+m2 - 1]?.replaceAll('\t', ' ')}`; - if (lines.length > +m2) { - failedLine += chalk.dim(`\n ${+m2 + 1}\t| ${lines[+m2]?.replaceAll('\t', ' ')}`); - } - failedLine += chalk.dim('\n ...'); +function pipeStderrToMMF(stderr : Readable, mmf : MMF1, dir = '') { + stderr.on('data', (data) => { + let text : string = data.toString().trim(); + let failedLine = ''; + + text = text.replace(/([\w+._/-]+):(\d+).(\d+)(-\d+.\d+)/g, (_m0, m1 : string, m2 : string, m3 : string) => { + // change absolute file path to relative + // change :line:col-line:col to :line:col to work in vscode + let res = `${absToRel(m1)}:${m2}:${m3}`; + let file = path.join(dir, m1); + + if (!fs.existsSync(file)) { return res; - }); - if (failedLine) { - text += failedLine; } - mmf.fail(text); + + // show failed line + let content = fs.readFileSync(file); + let lines = content.toString().split('\n') || []; + + failedLine += chalk.dim('\n ...'); + + let lineBefore = lines[+m2 - 2]; + if (lineBefore) { + failedLine += chalk.dim(`\n ${+m2 - 1}\t| ${lineBefore.replaceAll('\t', ' ')}`); + } + failedLine += `\n${chalk.redBright`->`} ${m2}\t| ${lines[+m2 - 1]?.replaceAll('\t', ' ')}`; + if (lines.length > +m2) { + failedLine += chalk.dim(`\n ${+m2 + 1}\t| ${lines[+m2]?.replaceAll('\t', ' ')}`); + } + failedLine += chalk.dim('\n ...'); + return res; }); + if (failedLine) { + text += failedLine; + } + mmf.fail(text); + }); +} + +function pipeMMF(proc : ChildProcessWithoutNullStreams, mmf : MMF1) { + return new Promise((resolve) => { + pipeStdoutToMMF(proc.stdout, mmf); + pipeStderrToMMF(proc.stderr, mmf); // exit proc.on('close', (code) => { diff --git a/cli/types.ts b/cli/types.ts index 92437d9a..f50221b6 100644 --- a/cli/types.ts +++ b/cli/types.ts @@ -40,4 +40,6 @@ export type Tool = 'moc' | 'wasmtime' | 'pocket-ic'; export type Requirements = { moc ?: string; -}; \ No newline at end of file +}; + +export type TestMode = 'interpreter' | 'wasi' | 'replica'; \ No newline at end of file diff --git a/test/storage-actor.test.mo b/test/storage-actor.test.mo index 1c0e1530..81fb84f9 100644 --- a/test/storage-actor.test.mo +++ b/test/storage-actor.test.mo @@ -7,13 +7,13 @@ import ExperimentalCycles "mo:base/ExperimentalCycles"; import Storage "../backend/storage/storage-canister"; actor { - public func runTests() : async () { + public func runTests() : async () { ExperimentalCycles.add(1_000_000_000_000); var storage = await Storage.Storage(); let fileId = "test"; - Debug.print(debug_show("lalalalalalala")); + Debug.print("lalalalalalala"); // upload await suite("storage upload", func() : async () { @@ -22,8 +22,6 @@ actor { assert Result.isErr(res); }); - // assert false; - await test("try to upload chunk before upload start", func() : async () { assert Result.isErr(await storage.uploadChunk(fileId, 0, Blob.fromArray([]))); }); From 92f4ba4fb123e35a71f3f9504bc0175f721f98eb Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 23 Aug 2024 16:27:37 +0400 Subject: [PATCH 06/13] [cli] actor test: reuse replica in watch mode --- cli/commands/replica.ts | 13 +++++++-- cli/commands/test/test.ts | 59 +++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index a6e2abd3..76ccea97 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -16,9 +16,9 @@ export class Replica { canisters : Record = {}; pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; - dir : string; // absolute path (.mops/.test/) + dir : string; // absolute path (/.../.mops/.test/) - constructor(type : 'dfx' | 'pocket-ic', dir : string, verbose = false) { + constructor(type : 'dfx' | 'pocket-ic', dir = '', verbose = false) { this.type = type; this.verbose = verbose; this.dir = dir; @@ -76,6 +76,15 @@ export class Replica { } } + stopSync() { + if (this.type == 'dfx') { + execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); + } + else if (this.pocketIc) { + throw new Error('Not supported'); + } + } + async stop() { if (this.type == 'dfx') { execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 8c23f40c..62a53383 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -39,16 +39,43 @@ let globConfig = { type ReporterName = 'verbose' | 'files' | 'compact' | 'silent'; +let replica = new Replica('pocket-ic'); +let replicaStartPromise : Promise | undefined; + +async function startReplicaOnce(replica : Replica) { + if (!replicaStartPromise) { + replicaStartPromise = new Promise((resolve) => { + replica.start({silent: true}).then(resolve); + }); + } + return replicaStartPromise; +} + export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode} = {}) { let rootDir = getRootDir(); if (watch) { + let sigint = false; + process.on('SIGINT', () => { + if (sigint) { + process.exit(0); + } + sigint = true; + + if (replicaStartPromise) { + if (replica.type == 'dfx') { + replica.stopSync(); + } + process.exit(0); + } + }); + // todo: run only changed for *.test.mo? // todo: run all for *.mo? let run = debounce(async () => { console.clear(); process.stdout.write('\x1Bc'); - await runAll(reporter, filter, mode); + await runAll(reporter, filter, mode, true); console.log('-'.repeat(50)); console.log('Waiting for file changes...'); console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`))); @@ -78,7 +105,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose' as let mocPath = ''; let wasmtimePath = ''; -export async function runAll(reporterName : ReporterName = 'verbose', filter = '', mode : TestMode = 'interpreter') : Promise { +export async function runAll(reporterName : ReporterName = 'verbose', filter = '', mode : TestMode = 'interpreter', watch = false) : Promise { let reporter : Reporter; if (reporterName == 'compact') { reporter = new CompactReporter; @@ -92,11 +119,11 @@ export async function runAll(reporterName : ReporterName = 'verbose', filter = ' else { reporter = new VerboseReporter; } - let done = await testWithReporter(reporter, filter, mode); + let done = await testWithReporter(reporter, filter, mode, watch); return done; } -export async function testWithReporter(reporter : Reporter, filter = '', defaultMode : TestMode = 'interpreter') : Promise { +export async function testWithReporter(reporter : Reporter, filter = '', defaultMode : TestMode = 'interpreter', watch = false) : Promise { let rootDir = getRootDir(); let files : string[] = []; let libFiles = globSync('**/test?(s)/lib.mo', globConfig); @@ -129,20 +156,10 @@ export async function testWithReporter(reporter : Reporter, filter = '', default mocPath = await toolchain.bin('moc', {fallback: true}); } - let wasmDir = path.join(getRootDir(), '.mops/.test/'); - fs.mkdirSync(wasmDir, {recursive: true}); + let testTempDir = path.join(getRootDir(), '.mops/.test/'); + replica.dir = testTempDir; - let replica = new Replica('pocket-ic', wasmDir); - let replicaStartPromise : Promise | undefined; - - async function startReplicaOnce(replica : Replica) { - if (!replicaStartPromise) { - replicaStartPromise = new Promise((resolve) => { - replica.start({silent: true}).then(resolve); - }); - } - return replicaStartPromise; - } + fs.mkdirSync(testTempDir, {recursive: true}); await parallel(os.cpus().length, files, async (file : string) => { let mmf = new MMF1('store', absToRel(file)); @@ -180,7 +197,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', default } // build and run wasm else if (mode === 'wasi') { - let wasmFile = `${path.join(wasmDir, path.parse(file).name)}.wasm`; + let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`; // build let buildProc = spawn(mocPath, [`-o=${wasmFile}`, '-wasi-system-api', ...mocArgs]); @@ -220,7 +237,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', default } // build and execute in replica else if (mode === 'replica') { - let wasmFile = `${path.join(wasmDir, path.parse(file).name)}.wasm`; + let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`; // build let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs]); @@ -264,8 +281,8 @@ export async function testWithReporter(reporter : Reporter, filter = '', default await promise; }); - fs.rmSync(wasmDir, {recursive: true, force: true}); - if (replicaStartPromise) { + fs.rmSync(testTempDir, {recursive: true, force: true}); + if (replicaStartPromise && !watch) { await replica.stop(); } From f9ecc6e2fe7d0df788d39baf095c7d49d60c5ae6 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 26 Aug 2024 16:23:23 +0400 Subject: [PATCH 07/13] [cli] actor test WIP --- cli/cli.ts | 5 +- cli/commands/publish.ts | 2 +- cli/commands/replica.ts | 118 ++++++++++++++++++++------------ cli/commands/test/mmf1.ts | 10 +-- cli/commands/test/test.ts | 54 ++++++++++----- cli/package-lock.json | 8 +-- cli/package.json | 4 +- test/storage-actor22222.test.mo | 100 +++++++++++++++++++++++++++ test/storage-actor33333.test.mo | 100 +++++++++++++++++++++++++++ 9 files changed, 327 insertions(+), 74 deletions(-) create mode 100644 test/storage-actor22222.test.mo create mode 100644 test/storage-actor33333.test.mo diff --git a/cli/cli.ts b/cli/cli.ts index d41aa2ec..22d0cc78 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -32,6 +32,8 @@ import * as self from './commands/self.js'; declare global { // eslint-disable-next-line no-var var MOPS_NETWORK : string; + // eslint-disable-next-line no-var + var mopsReplicaTestRunning : boolean; } let networkFile = getNetworkFile(); @@ -223,7 +225,8 @@ program .command('test [filter]') .description('Run tests') .addOption(new Option('-r, --reporter ', 'Test reporter').choices(['verbose', 'compact', 'files', 'silent']).default('verbose')) - .addOption(new Option('--mode ', 'Test mode').choices(['interpreter', 'wasi']).default('interpreter')) + .addOption(new Option('--mode ', 'Test mode').choices(['interpreter', 'wasi', 'replica']).default('interpreter')) + .addOption(new Option('--replica ', 'Which replica to use to run tests in replica mode').choices(['dfx', 'pocket-ic']).default('dfx')) .option('-w, --watch', 'Enable watch mode') .action(async (filter, options) => { await installAll({silent: true, lock: 'ignore'}); diff --git a/cli/commands/publish.ts b/cli/commands/publish.ts index 70ecf776..c29e3c48 100644 --- a/cli/commands/publish.ts +++ b/cli/commands/publish.ts @@ -271,7 +271,7 @@ export async function publish(options : {docs ?: boolean, test ?: boolean, bench let reporter = new SilentReporter; if (options.test) { console.log('Running tests...'); - await testWithReporter(reporter); + await testWithReporter(reporter, '', 'interpreter', config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx'); if (reporter.failed > 0) { console.log(chalk.red('Error: ') + 'tests failed'); process.exit(1); diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index 76ccea97..0d6c9070 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -1,30 +1,37 @@ import process from 'node:process'; -import {execSync} from 'node:child_process'; +import {ChildProcessWithoutNullStreams, execSync, spawn} from 'node:child_process'; import path from 'node:path'; import fs from 'node:fs'; -import {execaCommand} from 'execa'; +import {PassThrough} from 'node:stream'; + import {IDL} from '@dfinity/candid'; import {Actor, HttpAgent} from '@dfinity/agent'; import {PocketIc, PocketIcServer} from 'pic-ic'; + import {readConfig} from '../mops.js'; import {toolchain} from './toolchain/index.js'; -import {PassThrough} from 'node:stream'; + +type StartOptions = { + type ?: 'dfx' | 'pocket-ic'; + dir ?: string; + verbose ?: boolean; + silent ?: boolean; +}; export class Replica { - type : 'dfx' | 'pocket-ic'; + type : 'dfx' | 'pocket-ic' = 'dfx'; verbose = false; canisters : Record = {}; pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; - dir : string; // absolute path (/.../.mops/.test/) + dir : string = ''; // absolute path (/.../.mops/.test/) + ttl = 60; - constructor(type : 'dfx' | 'pocket-ic', dir = '', verbose = false) { - this.type = type; - this.verbose = verbose; - this.dir = dir; - } + async start({type, dir, verbose, silent} : StartOptions = {}) { + this.type = type ?? this.type; + this.verbose = verbose ?? this.verbose; + this.dir = dir ?? this.dir; - async start({silent = false} = {}) { silent || console.log(`Starting ${this.type} replica...`); if (this.type == 'dfx') { @@ -33,7 +40,30 @@ export class Replica { fs.writeFileSync(path.join(this.dir, 'canister.did'), 'service : { runTests: () -> (); }'); await this.stop(); - execSync('dfx start --background --clean --artificial-delay 0' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['inherit', this.verbose ? 'inherit' : 'ignore', 'inherit']}); + + let proc = spawn('dfx', ['start', '--clean', '--background', '--artificial-delay', '0', (this.verbose ? '' : '-qqqq')].filter(x => x), {cwd: this.dir}); + + // process canister logs + this._attachCanisterLogHandler(proc); + + proc.stdout.on('data', (data) => { + console.log('DFX:', data.toString()); + }); + + // await for dfx to start + let ok = false; + while (!ok) { + await fetch('http://127.0.0.1:4945/api/v2/status') + .then(res => { + if (res.status === 200) { + ok = true; + } + }) + .catch(() => {}) + .finally(() => { + return new Promise(resolve => setTimeout(resolve, 1000)); + }); + } } else { let pocketIcBin = await toolchain.bin('pocket-ic'); @@ -49,49 +79,45 @@ export class Replica { showRuntimeLogs: false, showCanisterLogs: false, binPath: pocketIcBin, + ttl: this.ttl, }); this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl()); // process canister logs - let curData = ''; - this.pocketIcServer.serverProcess.stderr.on('data', (data) => { - curData = curData + data.toString(); - - if (curData.includes('\n')) { - let m = curData.match(/\[Canister ([a-z0-9-]+)\] (.*)/); - if (!m) { - return; - } - let [, canisterId, msg] = m; - - let stream = this.getCanisterStream(canisterId || ''); - if (stream) { - stream.write(msg); - } - - curData = ''; - } - - }); + this._attachCanisterLogHandler(this.pocketIcServer.serverProcess as ChildProcessWithoutNullStreams); } } - stopSync() { - if (this.type == 'dfx') { - execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); - } - else if (this.pocketIc) { - throw new Error('Not supported'); - } + _attachCanisterLogHandler(proc : ChildProcessWithoutNullStreams) { + let curData = ''; + proc.stderr.on('data', (data) => { + curData = curData + data.toString(); + + if (curData.includes('\n')) { + let m = curData.match(/\[Canister ([a-z0-9-]+)\] (.*)/); + if (!m) { + return; + } + let [, canisterId, msg] = m; + + let stream = this.getCanisterStream(canisterId || ''); + if (stream) { + stream.write(msg); + } + + curData = ''; + } + }); } - async stop() { + async stop(sigint = false) { if (this.type == 'dfx') { execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); } - // else if (this.pocketIc) { else if (this.pocketIc && this.pocketIcServer) { - await this.pocketIc.tearDown(); + if (!sigint) { + await this.pocketIc.tearDown(); // error 'fetch failed' if run on SIGINT + } await this.pocketIcServer.stop(); } } @@ -114,12 +140,14 @@ export class Replica { fs.mkdirSync(this.dir, {recursive: true}); fs.writeFileSync(dfxJson, JSON.stringify(newDfxJsonData, null, 2)); - await execaCommand(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); + execSync(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); + execSync(`dfx ledger fabricate-cycles --canister ${name} --t 100`, {cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}); + let canisterId = execSync(`dfx canister id ${name}`, {cwd: this.dir}).toString().trim(); let actor = Actor.createActor(idlFactory, { agent: await HttpAgent.create({ - host: 'http://127.0.0.1:4944', + host: 'http://127.0.0.1:4945', shouldFetchRootKey: true, }), canisterId, @@ -199,7 +227,7 @@ export class Replica { networks: { local: { type: 'ephemeral', - bind: '127.0.0.1:4944', + bind: '127.0.0.1:4945', }, }, }; diff --git a/cli/commands/test/mmf1.ts b/cli/commands/test/mmf1.ts index 565ef246..cbe74d79 100644 --- a/cli/commands/test/mmf1.ts +++ b/cli/commands/test/mmf1.ts @@ -15,7 +15,7 @@ export class MMF1 { failed = 0; passed = 0; skipped = 0; - srategy : Strategy; + strategy : Strategy; output : { type : MessageType; message : string; @@ -27,19 +27,19 @@ export class MMF1 { // or ... passedNamesFlat : string[] = []; - constructor(srategy : Strategy, file : string) { - this.srategy = srategy; + constructor(strategy : Strategy, file : string) { + this.strategy = strategy; this.file = file; } _log(type : MessageType, ...args : string[]) { - if (this.srategy === 'store') { + if (this.strategy === 'store') { this.output.push({ type, message: args.join(' '), }); } - else if (this.srategy === 'print') { + else if (this.strategy === 'print') { console.log(...args); } } diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 62a53383..ec355958 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -3,6 +3,7 @@ import {spawn, ChildProcessWithoutNullStreams} from 'node:child_process'; import path from 'node:path'; import fs from 'node:fs'; import os from 'node:os'; + import chalk from 'chalk'; import {globSync} from 'glob'; import chokidar from 'chokidar'; @@ -38,35 +39,41 @@ let globConfig = { }; type ReporterName = 'verbose' | 'files' | 'compact' | 'silent'; +type ReplicaName = 'dfx' | 'pocket-ic'; -let replica = new Replica('pocket-ic'); +let replica = new Replica(); let replicaStartPromise : Promise | undefined; -async function startReplicaOnce(replica : Replica) { +async function startReplicaOnce(replica : Replica, type : ReplicaName) { if (!replicaStartPromise) { replicaStartPromise = new Promise((resolve) => { - replica.start({silent: true}).then(resolve); + replica.start({type, silent: true}).then(resolve); }); } return replicaStartPromise; } -export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode} = {}) { +export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode, replica : replicaType = 'dfx' as ReplicaName} = {}) { + replica.type = replicaType; + let rootDir = getRootDir(); if (watch) { + replica.ttl = 60 * 15; // 15 minutes + let sigint = false; process.on('SIGINT', () => { if (sigint) { + console.log('Force exit'); process.exit(0); } sigint = true; if (replicaStartPromise) { - if (replica.type == 'dfx') { - replica.stopSync(); - } - process.exit(0); + console.log('Stopping replica...'); + replica.stop(true).then(() => { + process.exit(0); + }); } }); @@ -75,7 +82,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose' as let run = debounce(async () => { console.clear(); process.stdout.write('\x1Bc'); - await runAll(reporter, filter, mode, true); + await runAll(reporter, filter, mode, replicaType, true); console.log('-'.repeat(50)); console.log('Waiting for file changes...'); console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`))); @@ -95,7 +102,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose' as run(); } else { - let passed = await runAll(reporter, filter, mode); + let passed = await runAll(reporter, filter, mode, replicaType); if (!passed) { process.exit(1); } @@ -105,7 +112,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose' as let mocPath = ''; let wasmtimePath = ''; -export async function runAll(reporterName : ReporterName = 'verbose', filter = '', mode : TestMode = 'interpreter', watch = false) : Promise { +export async function runAll(reporterName : ReporterName = 'verbose', filter = '', mode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false) : Promise { let reporter : Reporter; if (reporterName == 'compact') { reporter = new CompactReporter; @@ -119,11 +126,11 @@ export async function runAll(reporterName : ReporterName = 'verbose', filter = ' else { reporter = new VerboseReporter; } - let done = await testWithReporter(reporter, filter, mode, watch); + let done = await testWithReporter(reporter, filter, mode, replicaType, watch); return done; } -export async function testWithReporter(reporter : Reporter, filter = '', defaultMode : TestMode = 'interpreter', watch = false) : Promise { +export async function testWithReporter(reporter : Reporter, filter = '', defaultMode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false) : Promise { let rootDir = getRootDir(); let files : string[] = []; let libFiles = globSync('**/test?(s)/lib.mo', globConfig); @@ -237,6 +244,8 @@ export async function testWithReporter(reporter : Reporter, filter = '', default } // build and execute in replica else if (mode === 'replica') { + mmf.strategy = 'print'; // because we run replica tests one-by-one + let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`; // build @@ -247,7 +256,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', default return; } - await startReplicaOnce(replica); + await startReplicaOnce(replica, replicaType); let canisterName = path.parse(file).name; let idlFactory = ({IDL} : any) => { @@ -262,7 +271,20 @@ export async function testWithReporter(reporter : Reporter, filter = '', default let actor = await replica.getActor(canisterName) as _SERVICE; try { + if (globalThis.mopsReplicaTestRunning) { + await new Promise((resolve) => { + setInterval(() => { + if (!globalThis.mopsReplicaTestRunning) { + resolve(); + } + }, Math.random() * 1000 |0); + }); + } + + globalThis.mopsReplicaTestRunning = true; await actor.runTests(); + globalThis.mopsReplicaTestRunning = false; + mmf.pass(); } catch (e : any) { @@ -281,8 +303,8 @@ export async function testWithReporter(reporter : Reporter, filter = '', default await promise; }); - fs.rmSync(testTempDir, {recursive: true, force: true}); if (replicaStartPromise && !watch) { + fs.rmSync(testTempDir, {recursive: true, force: true}); await replica.stop(); } @@ -347,7 +369,7 @@ function pipeMMF(proc : ChildProcessWithoutNullStreams, mmf : MMF1) { // exit proc.on('close', (code) => { if (code === 0) { - mmf.pass(); + mmf.strategy !== 'print' && mmf.pass(); } else if (code !== 1) { mmf.fail(`unknown exit code: ${code}`); diff --git a/cli/package-lock.json b/cli/package-lock.json index d503f1f8..dd644d6c 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -43,7 +43,7 @@ "node-fetch": "3.3.2", "octokit": "3.1.2", "pem-file": "1.0.1", - "pic-ic": "0.4.0", + "pic-ic": "0.5.1", "prompts": "2.4.2", "semver": "7.6.3", "stream-to-promise": "3.0.0", @@ -5839,9 +5839,9 @@ "license": "MIT" }, "node_modules/pic-ic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/pic-ic/-/pic-ic-0.4.0.tgz", - "integrity": "sha512-JgEPBSSL2L81udOO7IQ1tr69sXg0tP/EwYtm3c6BXN5kYjrApFoUUPX/cn2dm+wmoLOlpL3p5yY2dwqytz3Nuw==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/pic-ic/-/pic-ic-0.5.1.tgz", + "integrity": "sha512-eapdYK9WUzm9MnfNDPutw4rLlURiYneiJA/SOcDuj86Vx7BR9S2mGFd4Nhku1FTGoNX3JWSGb7ibq+tvlITgAQ==", "license": "Apache-2.0", "dependencies": { "bip39": "^3.1.0" diff --git a/cli/package.json b/cli/package.json index 659a69c0..43813bba 100644 --- a/cli/package.json +++ b/cli/package.json @@ -73,7 +73,7 @@ "node-fetch": "3.3.2", "octokit": "3.1.2", "pem-file": "1.0.1", - "pic-ic": "0.4.0", + "pic-ic": "0.5.1", "prompts": "2.4.2", "semver": "7.6.3", "stream-to-promise": "3.0.0", @@ -92,9 +92,9 @@ "@types/semver": "7.5.8", "@types/stream-to-promise": "2.2.4", "@types/tar": "6.1.13", - "eslint": "8.57.0", "bun": "1.0.35", "esbuild": "0.23.0", + "eslint": "8.57.0", "tsx": "4.16.5", "typescript": "5.5.4" }, diff --git a/test/storage-actor22222.test.mo b/test/storage-actor22222.test.mo new file mode 100644 index 00000000..f7c4bac8 --- /dev/null +++ b/test/storage-actor22222.test.mo @@ -0,0 +1,100 @@ +import {test; suite; skip} "mo:test/async"; +import Result "mo:base/Result"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import ExperimentalCycles "mo:base/ExperimentalCycles"; + +import Storage "../backend/storage/storage-canister"; + +actor { + public func runTests() : async () { + ExperimentalCycles.add(1_000_000_000_000); + var storage = await Storage.Storage(); + + let fileId = "test"; + + Debug.print(debug_show("lalalalalalala")); + + // upload + await suite("storage upload", func() : async () { + await test("try to finish upload before upload start", func() : async () { + let res = await storage.finishUploads([fileId]); + assert Result.isErr(res); + }); + + // assert false; + + await test("try to upload chunk before upload start", func() : async () { + assert Result.isErr(await storage.uploadChunk(fileId, 0, Blob.fromArray([]))); + }); + + await test("start upload", func() : async () { + assert Result.isOk(await storage.startUpload({ + id = fileId; + path = "test/test.mo"; + chunkCount = 1; + owners = []; + })); + }); + + await test("try to finish upload with unknown file id", func() : async () { + assert Result.isErr(await storage.finishUploads([fileId, "unknown-file-id"])); + }); + + await test("finish upload", func() : async () { + assert Result.isOk(await storage.finishUploads([fileId])); + }); + + await test("try to finish already finished upload", func() : async () { + assert Result.isErr(await storage.finishUploads([fileId])); + }); + + await test("try to start upload existing file", func() : async () { + assert Result.isErr(await storage.startUpload({ + id = fileId; + path = "test/test.mo"; + chunkCount = 1; + owners = []; + })); + }); + }); + + // download + await suite("storage download", func() : async () { + await test("get file meta", func() : async () { + let res = await storage.getFileMeta(fileId); + assert Result.isOk(res); + switch (res) { + case (#ok(fileMeta)) { + assert fileMeta.path == "test/test.mo"; + assert fileMeta.chunkCount == 1; + assert fileMeta.owners == []; + }; + case (_) {}; + }; + }); + + await test("try to get file meta of unknown file", func() : async () { + let res = await storage.getFileMeta("123"); + assert Result.isErr(res); + }); + + await test("upgrade storage canister", func() : async () { + storage := await (system Storage.Storage)(#upgrade(storage))(); + }); + + await test("get file meta after upgrade", func() : async () { + let res = await storage.getFileMeta(fileId); + assert Result.isOk(res); + switch (res) { + case (#ok(fileMeta)) { + assert fileMeta.path == "test/test.mo"; + assert fileMeta.chunkCount == 1; + assert fileMeta.owners == []; + }; + case (_) {}; + }; + }); + }); + }; +}; \ No newline at end of file diff --git a/test/storage-actor33333.test.mo b/test/storage-actor33333.test.mo new file mode 100644 index 00000000..f7c4bac8 --- /dev/null +++ b/test/storage-actor33333.test.mo @@ -0,0 +1,100 @@ +import {test; suite; skip} "mo:test/async"; +import Result "mo:base/Result"; +import Blob "mo:base/Blob"; +import Debug "mo:base/Debug"; +import ExperimentalCycles "mo:base/ExperimentalCycles"; + +import Storage "../backend/storage/storage-canister"; + +actor { + public func runTests() : async () { + ExperimentalCycles.add(1_000_000_000_000); + var storage = await Storage.Storage(); + + let fileId = "test"; + + Debug.print(debug_show("lalalalalalala")); + + // upload + await suite("storage upload", func() : async () { + await test("try to finish upload before upload start", func() : async () { + let res = await storage.finishUploads([fileId]); + assert Result.isErr(res); + }); + + // assert false; + + await test("try to upload chunk before upload start", func() : async () { + assert Result.isErr(await storage.uploadChunk(fileId, 0, Blob.fromArray([]))); + }); + + await test("start upload", func() : async () { + assert Result.isOk(await storage.startUpload({ + id = fileId; + path = "test/test.mo"; + chunkCount = 1; + owners = []; + })); + }); + + await test("try to finish upload with unknown file id", func() : async () { + assert Result.isErr(await storage.finishUploads([fileId, "unknown-file-id"])); + }); + + await test("finish upload", func() : async () { + assert Result.isOk(await storage.finishUploads([fileId])); + }); + + await test("try to finish already finished upload", func() : async () { + assert Result.isErr(await storage.finishUploads([fileId])); + }); + + await test("try to start upload existing file", func() : async () { + assert Result.isErr(await storage.startUpload({ + id = fileId; + path = "test/test.mo"; + chunkCount = 1; + owners = []; + })); + }); + }); + + // download + await suite("storage download", func() : async () { + await test("get file meta", func() : async () { + let res = await storage.getFileMeta(fileId); + assert Result.isOk(res); + switch (res) { + case (#ok(fileMeta)) { + assert fileMeta.path == "test/test.mo"; + assert fileMeta.chunkCount == 1; + assert fileMeta.owners == []; + }; + case (_) {}; + }; + }); + + await test("try to get file meta of unknown file", func() : async () { + let res = await storage.getFileMeta("123"); + assert Result.isErr(res); + }); + + await test("upgrade storage canister", func() : async () { + storage := await (system Storage.Storage)(#upgrade(storage))(); + }); + + await test("get file meta after upgrade", func() : async () { + let res = await storage.getFileMeta(fileId); + assert Result.isOk(res); + switch (res) { + case (#ok(fileMeta)) { + assert fileMeta.path == "test/test.mo"; + assert fileMeta.chunkCount == 1; + assert fileMeta.owners == []; + }; + case (_) {}; + }; + }); + }); + }; +}; \ No newline at end of file From 9c6b8f29b7b71563e11e00feb2f25e7a0f5952cc Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Mon, 26 Aug 2024 16:36:07 +0400 Subject: [PATCH 08/13] [cli] replica test: disable progressive print --- cli/commands/test/reporters/reporter.ts | 2 +- cli/commands/test/reporters/verbose-reporter.ts | 17 +++++++++++++---- cli/commands/test/test.ts | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cli/commands/test/reporters/reporter.ts b/cli/commands/test/reporters/reporter.ts index 8859269b..f7cba144 100644 --- a/cli/commands/test/reporters/reporter.ts +++ b/cli/commands/test/reporters/reporter.ts @@ -3,6 +3,6 @@ import {MMF1} from '../mmf1.js'; export interface Reporter { addFiles(files : string[]) : void; - addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode) : void; + addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode, progressive ?: boolean) : void; done() : boolean; } \ No newline at end of file diff --git a/cli/commands/test/reporters/verbose-reporter.ts b/cli/commands/test/reporters/verbose-reporter.ts index 15c5f861..4ac169da 100644 --- a/cli/commands/test/reporters/verbose-reporter.ts +++ b/cli/commands/test/reporters/verbose-reporter.ts @@ -20,7 +20,11 @@ export class VerboseReporter implements Reporter { console.log('='.repeat(50)); } - addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode) { + addRun(file : string, mmf : MMF1, state : Promise, mode : TestMode, progressive = false) { + if (progressive) { + this._printStart(file, mode); + } + state.then(() => { this.passed += mmf.passed; this.failed += mmf.failed; @@ -29,9 +33,9 @@ export class VerboseReporter implements Reporter { if (mmf.passed === 0 && mmf.failed === 0) { this.passed++; } - - this.#curFileIndex++ && console.log('-'.repeat(50)); - console.log(`Running ${chalk.gray(absToRel(file))} ${mode === 'interpreter' ? '' : chalk.gray(`(${mode})`)}`); + if (!progressive) { + this._printStart(file, mode); + } mmf.flush(); }); } @@ -53,4 +57,9 @@ export class VerboseReporter implements Reporter { return this.failed === 0; } + + _printStart(file : string, mode : TestMode) { + this.#curFileIndex++ && console.log('-'.repeat(50)); + console.log(`Running ${chalk.gray(absToRel(file))} ${mode === 'interpreter' ? '' : chalk.gray(`(${mode})`)}`); + } } \ No newline at end of file diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index ec355958..6ec0efd6 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -244,7 +244,7 @@ export async function testWithReporter(reporter : Reporter, filter = '', default } // build and execute in replica else if (mode === 'replica') { - mmf.strategy = 'print'; // because we run replica tests one-by-one + // mmf.strategy = 'print'; // because we run replica tests one-by-one let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`; From 0b0fdf3f8fe9a6d430be9c78655775e523d4b5da Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Tue, 27 Aug 2024 14:06:49 +0400 Subject: [PATCH 09/13] [cli] default replica `pocket-ic` if presented in `[toolchain]` --- cli/cli.ts | 12 +++++++++--- cli/mops.ts | 5 ++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index 22d0cc78..9e8d3c92 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -7,7 +7,7 @@ import {init} from './commands/init.js'; import {publish} from './commands/publish.js'; import {importPem} from './commands/import-identity.js'; import {sources} from './commands/sources.js'; -import {checkApiCompatibility, setNetwork, apiVersion, checkConfigFile, getNetworkFile, version} from './mops.js'; +import {checkApiCompatibility, setNetwork, apiVersion, checkConfigFile, getNetworkFile, version, readConfig} from './mops.js'; import {getNetwork} from './api/network.js'; import {whoami} from './commands/whoami.js'; import {installAll} from './commands/install/install-all.js'; @@ -226,10 +226,13 @@ program .description('Run tests') .addOption(new Option('-r, --reporter ', 'Test reporter').choices(['verbose', 'compact', 'files', 'silent']).default('verbose')) .addOption(new Option('--mode ', 'Test mode').choices(['interpreter', 'wasi', 'replica']).default('interpreter')) - .addOption(new Option('--replica ', 'Which replica to use to run tests in replica mode').choices(['dfx', 'pocket-ic']).default('dfx')) + .addOption(new Option('--replica ', 'Which replica to use to run tests in replica mode').choices(['dfx', 'pocket-ic']).default('pocket-ic')) .option('-w, --watch', 'Enable watch mode') .action(async (filter, options) => { + checkConfigFile(true); await installAll({silent: true, lock: 'ignore'}); + let config = readConfig(); + options.replica = config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx'; await test(filter, options); }); @@ -237,14 +240,17 @@ program program .command('bench [filter]') .description('Run benchmarks') - .addOption(new Option('--replica ', 'Which replica to use to run benchmarks').choices(['dfx', 'pocket-ic']).default('dfx')) + .addOption(new Option('--replica ', 'Which replica to use to run benchmarks').choices(['dfx', 'pocket-ic']).default('pocket-ic')) .addOption(new Option('--gc ', 'Garbage collector').choices(['copying', 'compacting', 'generational', 'incremental']).default('copying')) .addOption(new Option('--save', 'Save benchmark results to .bench/.json')) .addOption(new Option('--compare', 'Run benchmark and compare results with .bench/.json')) // .addOption(new Option('--force-gc', 'Force GC')) .addOption(new Option('--verbose', 'Show more information')) .action(async (filter, options) => { + checkConfigFile(true); await installAll({silent: true, lock: 'ignore'}); + let config = readConfig(); + options.replica = config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx'; await bench(filter, options); }); diff --git a/cli/mops.ts b/cli/mops.ts index 2e14b099..3ff5832a 100644 --- a/cli/mops.ts +++ b/cli/mops.ts @@ -127,10 +127,13 @@ export function getRootDir() { return path.dirname(configFile); } -export function checkConfigFile() { +export function checkConfigFile(exit = false) { let configFile = getClosestConfigFile(); if (!configFile) { console.log(chalk.red('Error: ') + `Config file 'mops.toml' not found. Please run ${chalk.green('mops init')} first`); + if (exit) { + process.exit(1); + } return false; } return true; From 92afddb65daa9891f857d0894e3af5ec7e5fe3fb Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Tue, 27 Aug 2024 14:32:31 +0400 Subject: [PATCH 10/13] [cli] default replica fix --- cli/cli.ts | 10 +++------- cli/commands/bench.ts | 5 +++-- cli/commands/test/test.ts | 22 ++++++++++++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cli/cli.ts b/cli/cli.ts index 9e8d3c92..77345591 100755 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -7,7 +7,7 @@ import {init} from './commands/init.js'; import {publish} from './commands/publish.js'; import {importPem} from './commands/import-identity.js'; import {sources} from './commands/sources.js'; -import {checkApiCompatibility, setNetwork, apiVersion, checkConfigFile, getNetworkFile, version, readConfig} from './mops.js'; +import {checkApiCompatibility, setNetwork, apiVersion, checkConfigFile, getNetworkFile, version} from './mops.js'; import {getNetwork} from './api/network.js'; import {whoami} from './commands/whoami.js'; import {installAll} from './commands/install/install-all.js'; @@ -226,13 +226,11 @@ program .description('Run tests') .addOption(new Option('-r, --reporter ', 'Test reporter').choices(['verbose', 'compact', 'files', 'silent']).default('verbose')) .addOption(new Option('--mode ', 'Test mode').choices(['interpreter', 'wasi', 'replica']).default('interpreter')) - .addOption(new Option('--replica ', 'Which replica to use to run tests in replica mode').choices(['dfx', 'pocket-ic']).default('pocket-ic')) + .addOption(new Option('--replica ', 'Which replica to use to run tests in replica mode').choices(['dfx', 'pocket-ic'])) .option('-w, --watch', 'Enable watch mode') .action(async (filter, options) => { checkConfigFile(true); await installAll({silent: true, lock: 'ignore'}); - let config = readConfig(); - options.replica = config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx'; await test(filter, options); }); @@ -240,7 +238,7 @@ program program .command('bench [filter]') .description('Run benchmarks') - .addOption(new Option('--replica ', 'Which replica to use to run benchmarks').choices(['dfx', 'pocket-ic']).default('pocket-ic')) + .addOption(new Option('--replica ', 'Which replica to use to run benchmarks').choices(['dfx', 'pocket-ic'])) .addOption(new Option('--gc ', 'Garbage collector').choices(['copying', 'compacting', 'generational', 'incremental']).default('copying')) .addOption(new Option('--save', 'Save benchmark results to .bench/.json')) .addOption(new Option('--compare', 'Run benchmark and compare results with .bench/.json')) @@ -249,8 +247,6 @@ program .action(async (filter, options) => { checkConfigFile(true); await installAll({silent: true, lock: 'ignore'}); - let config = readConfig(); - options.replica = config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx'; await bench(filter, options); }); diff --git a/cli/commands/bench.ts b/cli/commands/bench.ts index 09643e3d..e4b68d08 100644 --- a/cli/commands/bench.ts +++ b/cli/commands/bench.ts @@ -48,8 +48,10 @@ type BenchOptions = { }; export async function bench(filter = '', optionsArg : Partial = {}) : Promise { + let config = readConfig(); + let defaultOptions : BenchOptions = { - replica: 'dfx', + replica: config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx', replicaVersion: '', compiler: 'moc', compilerVersion: getMocVersion(), @@ -67,7 +69,6 @@ export async function bench(filter = '', optionsArg : Partial = {} options.replicaVersion = getDfxVersion(); } else if (options.replica == 'pocket-ic') { - let config = readConfig(); options.replicaVersion = config.toolchain?.['pocket-ic'] || ''; } diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 6ec0efd6..e889b6d5 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -41,6 +41,14 @@ let globConfig = { type ReporterName = 'verbose' | 'files' | 'compact' | 'silent'; type ReplicaName = 'dfx' | 'pocket-ic'; +type TestOptions = { + watch : boolean; + reporter : ReporterName; + mode : TestMode; + replica : ReplicaName; +}; + + let replica = new Replica(); let replicaStartPromise : Promise | undefined; @@ -53,12 +61,14 @@ async function startReplicaOnce(replica : Replica, type : ReplicaName) { return replicaStartPromise; } -export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode, replica : replicaType = 'dfx' as ReplicaName} = {}) { - replica.type = replicaType; - +export async function test(filter = '', options : Partial = {}) { + let config = readConfig(); let rootDir = getRootDir(); - if (watch) { + let replicaType = options.replica ?? (config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx' as ReplicaName); + replica.type = replicaType; + + if (options.watch) { replica.ttl = 60 * 15; // 15 minutes let sigint = false; @@ -82,7 +92,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose' as let run = debounce(async () => { console.clear(); process.stdout.write('\x1Bc'); - await runAll(reporter, filter, mode, replicaType, true); + await runAll(options.reporter, filter, options.mode, replicaType, true); console.log('-'.repeat(50)); console.log('Waiting for file changes...'); console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`))); @@ -102,7 +112,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose' as run(); } else { - let passed = await runAll(reporter, filter, mode, replicaType); + let passed = await runAll(options.reporter, filter, options.mode, replicaType); if (!passed) { process.exit(1); } From 1996af9103b39d0db4d8cb3f142145dd36cf3437 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Tue, 27 Aug 2024 15:14:54 +0400 Subject: [PATCH 11/13] [cli] fix test with dfx replica --- cli/commands/replica.ts | 10 ++++++---- cli/commands/test/test.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cli/commands/replica.ts b/cli/commands/replica.ts index 0d6c9070..d3c2ecae 100644 --- a/cli/commands/replica.ts +++ b/cli/commands/replica.ts @@ -24,6 +24,7 @@ export class Replica { canisters : Record = {}; pocketIcServer ?: PocketIcServer; pocketIc ?: PocketIc; + dfxProcess ?: ChildProcessWithoutNullStreams; dir : string = ''; // absolute path (/.../.mops/.test/) ttl = 60; @@ -41,12 +42,12 @@ export class Replica { await this.stop(); - let proc = spawn('dfx', ['start', '--clean', '--background', '--artificial-delay', '0', (this.verbose ? '' : '-qqqq')].filter(x => x), {cwd: this.dir}); + this.dfxProcess = spawn('dfx', ['start', '--clean', '--artificial-delay', '0', (this.verbose ? '' : '-qqqq')].filter(x => x), {cwd: this.dir}); // process canister logs - this._attachCanisterLogHandler(proc); + this._attachCanisterLogHandler(this.dfxProcess); - proc.stdout.on('data', (data) => { + this.dfxProcess.stdout.on('data', (data) => { console.log('DFX:', data.toString()); }); @@ -112,7 +113,8 @@ export class Replica { async stop(sigint = false) { if (this.type == 'dfx') { - execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); + this.dfxProcess?.kill(); + // execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, timeout: 10_000, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']}); } else if (this.pocketIc && this.pocketIcServer) { if (!sigint) { diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index e889b6d5..3e35fde2 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -314,8 +314,8 @@ export async function testWithReporter(reporter : Reporter, filter = '', default }); if (replicaStartPromise && !watch) { - fs.rmSync(testTempDir, {recursive: true, force: true}); await replica.stop(); + fs.rmSync(testTempDir, {recursive: true, force: true}); } return reporter.done(); From 7aabe4f2a71fe1bd28c19570fdeea8cf88746019 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 30 Aug 2024 12:09:49 +0400 Subject: [PATCH 12/13] - --- t.js | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 t.js diff --git a/t.js b/t.js deleted file mode 100644 index 307ec24f..00000000 --- a/t.js +++ /dev/null @@ -1,20 +0,0 @@ -import {PocketIc, PocketIcServer} from '@hadronous/pic'; - -let picServer = await PocketIcServer.start({ - showRuntimeLogs: true, - showCanisterLogs: true, -}); -let pic = await PocketIc.create(picServer.getUrl()); -let {canisterId, actor} = await pic.setupCanister({ - idlFactory: ({IDL}) => { - return IDL.Service({'runTests': IDL.Func([], [], [])}); - }, - wasm: '.mops/.test/storage-actor.test.wasm', - cycles: 1_000_000_000_000_000_000n, // doesn't help -}); - -await pic.addCycles(canisterId, 1_000_000_000_000); // doesn't help - -await actor.runTests(); // error - -await picServer.stop(); \ No newline at end of file From 242731f3484d28335713601493f9b62947e1ae32 Mon Sep 17 00:00:00 2001 From: Zen Voich Date: Fri, 30 Aug 2024 14:08:24 +0400 Subject: [PATCH 13/13] [cli] fix `mops test` hang on replica test --- cli/commands/test/test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 298d1a27..4537af31 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -295,9 +295,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter | try { if (globalThis.mopsReplicaTestRunning) { await new Promise((resolve) => { - setInterval(() => { + let timerId = setInterval(() => { if (!globalThis.mopsReplicaTestRunning) { resolve(); + clearInterval(timerId); } }, Math.random() * 1000 |0); });