ic-mops 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bundle/cli.tgz +0 -0
  3. package/check-requirements.ts +1 -1
  4. package/cli.ts +29 -5
  5. package/commands/bench.ts +1 -1
  6. package/commands/install/install-all.ts +28 -6
  7. package/commands/install/install-dep.ts +5 -4
  8. package/commands/install/install-deps.ts +3 -2
  9. package/commands/install/install-local-dep.ts +11 -5
  10. package/commands/install/install-mops-dep.ts +8 -5
  11. package/commands/replica.ts +33 -3
  12. package/commands/sources.ts +1 -1
  13. package/commands/sync.ts +4 -19
  14. package/commands/test/mmf1.ts +4 -0
  15. package/commands/test/reporters/silent-reporter.ts +22 -4
  16. package/commands/test/test.ts +74 -10
  17. package/commands/watch/deployer.ts +155 -0
  18. package/commands/watch/error-checker.ts +87 -0
  19. package/commands/watch/generator.ts +99 -0
  20. package/commands/watch/globMoFiles.ts +16 -0
  21. package/commands/watch/parseDfxJson.ts +64 -0
  22. package/commands/watch/tester.ts +81 -0
  23. package/commands/watch/warning-checker.ts +133 -0
  24. package/commands/watch/watch.ts +90 -0
  25. package/declarations/main/main.did +16 -10
  26. package/declarations/main/main.did.d.ts +19 -10
  27. package/declarations/main/main.did.js +25 -11
  28. package/dist/check-requirements.js +1 -1
  29. package/dist/cli.js +26 -5
  30. package/dist/commands/bench.js +1 -1
  31. package/dist/commands/install/install-all.d.ts +2 -1
  32. package/dist/commands/install/install-all.js +24 -6
  33. package/dist/commands/install/install-dep.d.ts +2 -1
  34. package/dist/commands/install/install-dep.js +4 -4
  35. package/dist/commands/install/install-deps.d.ts +2 -1
  36. package/dist/commands/install/install-deps.js +2 -2
  37. package/dist/commands/install/install-local-dep.d.ts +2 -1
  38. package/dist/commands/install/install-local-dep.js +9 -4
  39. package/dist/commands/install/install-mops-dep.d.ts +2 -1
  40. package/dist/commands/install/install-mops-dep.js +7 -5
  41. package/dist/commands/replica.d.ts +2 -2
  42. package/dist/commands/replica.js +26 -3
  43. package/dist/commands/sources.d.ts +1 -1
  44. package/dist/commands/sources.js +1 -1
  45. package/dist/commands/sync.js +3 -18
  46. package/dist/commands/test/mmf1.d.ts +1 -0
  47. package/dist/commands/test/mmf1.js +3 -0
  48. package/dist/commands/test/reporters/silent-reporter.d.ts +6 -1
  49. package/dist/commands/test/reporters/silent-reporter.js +18 -5
  50. package/dist/commands/test/test.d.ts +1 -1
  51. package/dist/commands/test/test.js +62 -10
  52. package/dist/commands/watch/deployer.d.ts +24 -0
  53. package/dist/commands/watch/deployer.js +125 -0
  54. package/dist/commands/watch/error-checker.d.ts +13 -0
  55. package/dist/commands/watch/error-checker.js +76 -0
  56. package/dist/commands/watch/generator.d.ts +21 -0
  57. package/dist/commands/watch/generator.js +79 -0
  58. package/dist/commands/watch/globMoFiles.d.ts +1 -0
  59. package/dist/commands/watch/globMoFiles.js +14 -0
  60. package/dist/commands/watch/parseDfxJson.d.ts +2 -0
  61. package/dist/commands/watch/parseDfxJson.js +22 -0
  62. package/dist/commands/watch/tester.d.ts +19 -0
  63. package/dist/commands/watch/tester.js +63 -0
  64. package/dist/commands/watch/warning-checker.d.ts +20 -0
  65. package/dist/commands/watch/warning-checker.js +111 -0
  66. package/dist/commands/watch/watch.d.ts +7 -0
  67. package/dist/commands/watch/watch.js +79 -0
  68. package/dist/declarations/main/main.did +16 -10
  69. package/dist/declarations/main/main.did.d.ts +19 -10
  70. package/dist/declarations/main/main.did.js +25 -11
  71. package/dist/helpers/get-moc-path.d.ts +1 -1
  72. package/dist/helpers/get-moc-path.js +17 -3
  73. package/dist/helpers/get-moc-version.d.ts +1 -1
  74. package/dist/helpers/get-moc-version.js +17 -5
  75. package/dist/integrity.d.ts +20 -0
  76. package/dist/integrity.js +42 -10
  77. package/dist/mops.d.ts +2 -1
  78. package/dist/mops.js +13 -10
  79. package/dist/package.json +3 -2
  80. package/dist/parallel.d.ts +1 -1
  81. package/dist/resolve-packages.js +7 -0
  82. package/dist/templates/mops-test.yml +4 -2
  83. package/dist/vessel.d.ts +2 -1
  84. package/dist/vessel.js +4 -1
  85. package/helpers/get-moc-path.ts +19 -3
  86. package/helpers/get-moc-version.ts +17 -5
  87. package/integrity.ts +56 -13
  88. package/mops.ts +15 -11
  89. package/package.json +3 -2
  90. package/parallel.ts +2 -2
  91. package/resolve-packages.ts +9 -0
  92. package/templates/mops-test.yml +4 -2
  93. package/vessel.ts +5 -1
@@ -1,6 +1,7 @@
1
1
  type InstallLocalDepOptions = {
2
2
  verbose?: boolean;
3
3
  silent?: boolean;
4
+ ignoreTransitive?: boolean;
4
5
  };
5
- export declare function installLocalDep(pkg: string, pkgPath?: string, { verbose, silent }?: InstallLocalDepOptions): Promise<boolean>;
6
+ export declare function installLocalDep(pkg: string, pkgPath?: string, { verbose, silent, ignoreTransitive }?: InstallLocalDepOptions): Promise<boolean>;
6
7
  export {};
@@ -5,7 +5,7 @@ import { getRootDir, readConfig } from '../../mops.js';
5
5
  import { installDeps } from './install-deps.js';
6
6
  // skip install and just find non-local dependencies to install
7
7
  // pkgPath should be relative to the current root dir or absolute
8
- export async function installLocalDep(pkg, pkgPath = '', { verbose, silent } = {}) {
8
+ export async function installLocalDep(pkg, pkgPath = '', { verbose, silent, ignoreTransitive } = {}) {
9
9
  if (!silent) {
10
10
  let logUpdate = createLogUpdate(process.stdout, { showCursor: true });
11
11
  logUpdate(`Local dependency ${pkg} = "${pkgPath}"`);
@@ -17,7 +17,12 @@ export async function installLocalDep(pkg, pkgPath = '', { verbose, silent } = {
17
17
  }
18
18
  }
19
19
  // install dependencies
20
- let dir = path.resolve(getRootDir(), pkgPath);
21
- let config = readConfig(path.join(dir, 'mops.toml'));
22
- return installDeps(Object.values(config.dependencies || {}), { silent, verbose }, pkgPath);
20
+ if (!ignoreTransitive) {
21
+ let dir = path.resolve(getRootDir(), pkgPath);
22
+ let config = readConfig(path.join(dir, 'mops.toml'));
23
+ return installDeps(Object.values(config.dependencies || {}), { silent, verbose }, pkgPath);
24
+ }
25
+ else {
26
+ return true;
27
+ }
23
28
  }
@@ -3,6 +3,7 @@ type InstallMopsDepOptions = {
3
3
  silent?: boolean;
4
4
  dep?: boolean;
5
5
  threads?: number;
6
+ ignoreTransitive?: boolean;
6
7
  };
7
- export declare function installMopsDep(pkg: string, version?: string, { verbose, silent, dep, threads }?: InstallMopsDepOptions): Promise<boolean>;
8
+ export declare function installMopsDep(pkg: string, version?: string, { verbose, silent, dep, threads, ignoreTransitive }?: InstallMopsDepOptions): Promise<boolean>;
8
9
  export {};
@@ -13,7 +13,7 @@ import { getDepCacheDir, getMopsDepCacheName, isDepCached } from '../../cache.js
13
13
  import { downloadFile, getPackageFilesInfo } from '../../api/downloadPackageFiles.js';
14
14
  import { installDeps } from './install-deps.js';
15
15
  import { getDepName } from '../../helpers/get-dep-name.js';
16
- export async function installMopsDep(pkg, version = '', { verbose, silent, dep, threads } = {}) {
16
+ export async function installMopsDep(pkg, version = '', { verbose, silent, dep, threads, ignoreTransitive } = {}) {
17
17
  threads = threads || 12;
18
18
  let depName = getDepName(pkg);
19
19
  if (!checkConfigFile()) {
@@ -90,10 +90,12 @@ export async function installMopsDep(pkg, version = '', { verbose, silent, dep,
90
90
  logUpdate.clear();
91
91
  }
92
92
  // install dependencies
93
- let config = readConfig(path.join(cacheDir, 'mops.toml'));
94
- let res = await installDeps(Object.values(config.dependencies || {}), { silent, verbose });
95
- if (!res) {
96
- return false;
93
+ if (!ignoreTransitive) {
94
+ let config = readConfig(path.join(cacheDir, 'mops.toml'));
95
+ let res = await installDeps(Object.values(config.dependencies || {}), { silent, verbose });
96
+ if (!res) {
97
+ return false;
98
+ }
97
99
  }
98
100
  return true;
99
101
  }
@@ -25,12 +25,12 @@ export declare class Replica {
25
25
  start({ type, dir, verbose, silent }?: StartOptions): Promise<void>;
26
26
  _attachCanisterLogHandler(proc: ChildProcessWithoutNullStreams): void;
27
27
  stop(sigint?: boolean): Promise<void>;
28
- deploy(name: string, wasm: string, idlFactory: IDL.InterfaceFactory, cwd?: string): Promise<{
28
+ deploy(name: string, wasm: string, idlFactory: IDL.InterfaceFactory, cwd?: string, signal?: AbortSignal): Promise<{
29
29
  cwd: string;
30
30
  canisterId: string;
31
31
  actor: any;
32
32
  stream: PassThrough;
33
- }>;
33
+ } | undefined>;
34
34
  getActor(name: string): unknown;
35
35
  getCanister(name: string): {
36
36
  cwd: string;
@@ -3,6 +3,7 @@ import { execSync, spawn } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import fs from 'node:fs';
5
5
  import { PassThrough } from 'node:stream';
6
+ import { spawn as spawnAsync } from 'promisify-child-process';
6
7
  import { Actor, HttpAgent } from '@dfinity/agent';
7
8
  import { PocketIc, PocketIcServer } from 'pic-ic';
8
9
  import { readConfig } from '../mops.js';
@@ -97,7 +98,7 @@ export class Replica {
97
98
  await this.pocketIcServer.stop();
98
99
  }
99
100
  }
100
- async deploy(name, wasm, idlFactory, cwd = process.cwd()) {
101
+ async deploy(name, wasm, idlFactory, cwd = process.cwd(), signal) {
101
102
  if (this.type === 'dfx') {
102
103
  // prepare dfx.json for current canister
103
104
  let dfxJson = path.join(this.dir, 'dfx.json');
@@ -111,8 +112,24 @@ export class Replica {
111
112
  }
112
113
  fs.mkdirSync(this.dir, { recursive: true });
113
114
  fs.writeFileSync(dfxJson, JSON.stringify(newDfxJsonData, null, 2));
114
- execSync(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, { cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe'] });
115
- execSync(`dfx ledger fabricate-cycles --canister ${name} --t 100`, { cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe'] });
115
+ await spawnAsync('dfx', ['deploy', name, '--mode', 'reinstall', '--yes', '--identity', 'anonymous'], { cwd: this.dir, signal, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe'] }).catch((error) => {
116
+ if (error.code === 'ABORT_ERR') {
117
+ return { stderr: '' };
118
+ }
119
+ throw error;
120
+ });
121
+ if (signal?.aborted) {
122
+ return;
123
+ }
124
+ await spawnAsync('dfx', ['ledger', 'fabricate-cycles', '--canister', name, '--t', '100'], { cwd: this.dir, signal, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe'] }).catch((error) => {
125
+ if (error.code === 'ABORT_ERR') {
126
+ return { stderr: '' };
127
+ }
128
+ throw error;
129
+ });
130
+ if (signal?.aborted) {
131
+ return;
132
+ }
116
133
  let canisterId = execSync(`dfx canister id ${name}`, { cwd: this.dir }).toString().trim();
117
134
  let actor = Actor.createActor(idlFactory, {
118
135
  agent: await HttpAgent.create({
@@ -134,7 +151,13 @@ export class Replica {
134
151
  idlFactory,
135
152
  wasm,
136
153
  });
154
+ if (signal?.aborted) {
155
+ return;
156
+ }
137
157
  await this.pocketIc.addCycles(canisterId, 1_000_000_000_000);
158
+ if (signal?.aborted) {
159
+ return;
160
+ }
138
161
  this.canisters[name] = {
139
162
  cwd,
140
163
  canisterId: canisterId.toText(),
@@ -1,4 +1,4 @@
1
1
  export declare function sources({ conflicts, cwd }?: {
2
2
  conflicts?: "ignore" | "error" | "warning" | undefined;
3
3
  cwd?: string | undefined;
4
- }): Promise<(string | undefined)[]>;
4
+ }): Promise<string[]>;
@@ -38,5 +38,5 @@ export async function sources({ conflicts = 'ignore', cwd = process.cwd() } = {}
38
38
  pkgBaseDir = pkgDir;
39
39
  }
40
40
  return `--package ${name} ${pkgBaseDir}`;
41
- });
41
+ }).filter(x => x != null);
42
42
  }
@@ -1,4 +1,3 @@
1
- import process from 'node:process';
2
1
  import path from 'node:path';
3
2
  import { execSync } from 'node:child_process';
4
3
  import { globSync } from 'glob';
@@ -7,6 +6,7 @@ import { checkConfigFile, getRootDir, readConfig } from '../mops.js';
7
6
  import { add } from './add.js';
8
7
  import { remove } from './remove.js';
9
8
  import { checkIntegrity } from '../integrity.js';
9
+ import { getMocPath } from '../helpers/get-moc-path.js';
10
10
  export async function sync({ lock } = {}) {
11
11
  if (!checkConfigFile()) {
12
12
  return;
@@ -35,24 +35,9 @@ let ignore = [
35
35
  '**/.git/**',
36
36
  '**/.mops/**',
37
37
  ];
38
- let mocPath = '';
39
- function getMocPath() {
40
- if (!mocPath) {
41
- mocPath = process.env.DFX_MOC_PATH || '';
42
- }
43
- if (!mocPath) {
44
- try {
45
- mocPath = execSync('dfx cache show').toString().trim() + '/moc';
46
- }
47
- catch { }
48
- }
49
- if (!mocPath) {
50
- mocPath = 'moc';
51
- }
52
- return mocPath;
53
- }
54
38
  async function getUsedPackages() {
55
39
  let rootDir = getRootDir();
40
+ let mocPath = getMocPath();
56
41
  let files = globSync('**/*.mo', {
57
42
  cwd: rootDir,
58
43
  nocase: true,
@@ -60,7 +45,7 @@ async function getUsedPackages() {
60
45
  });
61
46
  let packages = new Set;
62
47
  for (let file of files) {
63
- let deps = execSync(`${getMocPath()} --print-deps ${path.join(rootDir, file)}`).toString().trim().split('\n');
48
+ let deps = execSync(`${mocPath} --print-deps ${path.join(rootDir, file)}`).toString().trim().split('\n');
64
49
  for (let dep of deps) {
65
50
  if (dep.startsWith('mo:') && !dep.startsWith('mo:prim') && !dep.startsWith('mo:⛔')) {
66
51
  packages.add(dep.replace(/^mo:([^/]+).*$/, '$1'));
@@ -18,6 +18,7 @@ export declare class MMF1 {
18
18
  constructor(strategy: Strategy, file: string);
19
19
  _log(type: MessageType, ...args: string[]): void;
20
20
  flush(messageType?: MessageType): void;
21
+ getErrorMessages(): string[];
21
22
  parseLine(line: string): void;
22
23
  _testStart(name: string): void;
23
24
  _testEnd(name: string): void;
@@ -39,6 +39,9 @@ export class MMF1 {
39
39
  }
40
40
  this.output = [];
41
41
  }
42
+ getErrorMessages() {
43
+ return this.output.filter(out => out.type === 'fail').map(out => out.message);
44
+ }
42
45
  parseLine(line) {
43
46
  if (line.startsWith('mops:1:start ')) {
44
47
  this._testStart(line.split('mops:1:start ')[1] || '');
@@ -2,13 +2,18 @@ import { MMF1 } from '../mmf1.js';
2
2
  import { Reporter } from './reporter.js';
3
3
  import { TestMode } from '../../../types.js';
4
4
  export declare class SilentReporter implements Reporter {
5
+ total: number;
5
6
  passed: number;
6
7
  failed: number;
7
8
  skipped: number;
8
9
  passedFiles: number;
9
10
  failedFiles: number;
10
11
  passedNamesFlat: string[];
11
- addFiles(_files: string[]): void;
12
+ flushOnError: boolean;
13
+ errorOutput: string;
14
+ onProgress: () => void;
15
+ constructor(flushOnError?: boolean, onProgress?: () => void);
16
+ addFiles(files: string[]): void;
12
17
  addRun(file: string, mmf: MMF1, state: Promise<void>, _mode: TestMode): void;
13
18
  done(): boolean;
14
19
  }
@@ -1,15 +1,23 @@
1
1
  import chalk from 'chalk';
2
2
  import { absToRel } from '../utils.js';
3
3
  export class SilentReporter {
4
- constructor() {
4
+ constructor(flushOnError = true, onProgress = () => { }) {
5
+ this.total = 0;
5
6
  this.passed = 0;
6
7
  this.failed = 0;
7
8
  this.skipped = 0;
8
9
  this.passedFiles = 0;
9
10
  this.failedFiles = 0;
10
11
  this.passedNamesFlat = [];
12
+ this.flushOnError = true;
13
+ this.errorOutput = '';
14
+ this.onProgress = () => { };
15
+ this.flushOnError = flushOnError;
16
+ this.onProgress = onProgress;
17
+ }
18
+ addFiles(files) {
19
+ this.total = files.length;
11
20
  }
12
- addFiles(_files) { }
13
21
  addRun(file, mmf, state, _mode) {
14
22
  state.then(() => {
15
23
  this.passed += mmf.passed;
@@ -23,10 +31,15 @@ export class SilentReporter {
23
31
  this.passedFiles += Number(mmf.failed === 0);
24
32
  this.failedFiles += Number(mmf.failed !== 0);
25
33
  if (mmf.failed) {
26
- console.log(chalk.red('✖'), absToRel(file));
27
- mmf.flush('fail');
28
- console.log('-'.repeat(50));
34
+ let output = `${chalk.red('✖')} ${absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;
35
+ if (this.flushOnError) {
36
+ console.log(output);
37
+ }
38
+ else {
39
+ this.errorOutput = `${this.errorOutput}\n${output}`.trim();
40
+ }
29
41
  }
42
+ this.onProgress();
30
43
  });
31
44
  }
32
45
  done() {
@@ -9,5 +9,5 @@ type TestOptions = {
9
9
  replica: ReplicaName;
10
10
  };
11
11
  export declare function test(filter?: string, options?: Partial<TestOptions>): Promise<void>;
12
- export declare function testWithReporter(reporterName: ReporterName | Reporter | undefined, filter: string | undefined, defaultMode: TestMode | undefined, replicaType: ReplicaName, watch?: boolean): Promise<boolean>;
12
+ export declare function testWithReporter(reporterName: ReporterName | Reporter | undefined, filter: string | undefined, defaultMode: TestMode | undefined, replicaType: ReplicaName, watch?: boolean, signal?: AbortSignal): Promise<boolean>;
13
13
  export {};
@@ -59,13 +59,22 @@ export async function test(filter = '', options = {}) {
59
59
  process.exit(0);
60
60
  });
61
61
  }
62
+ else {
63
+ process.exit(0);
64
+ }
62
65
  });
63
66
  // todo: run only changed for *.test.mo?
64
67
  // todo: run all for *.mo?
68
+ let curRun = Promise.resolve(true);
69
+ let controller = new AbortController();
65
70
  let run = debounce(async () => {
71
+ controller.abort();
72
+ await curRun;
66
73
  console.clear();
67
74
  process.stdout.write('\x1Bc');
68
- await runAll(options.reporter, filter, options.mode, replicaType, true);
75
+ controller = new AbortController();
76
+ curRun = runAll(options.reporter, filter, options.mode, replicaType, true, controller.signal);
77
+ await curRun;
69
78
  console.log('-'.repeat(50));
70
79
  console.log('Waiting for file changes...');
71
80
  console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`)));
@@ -91,11 +100,11 @@ export async function test(filter = '', options = {}) {
91
100
  }
92
101
  let mocPath = '';
93
102
  let wasmtimePath = '';
94
- async function runAll(reporterName, filter = '', mode = 'interpreter', replicaType, watch = false) {
95
- let done = await testWithReporter(reporterName, filter, mode, replicaType, watch);
103
+ async function runAll(reporterName, filter = '', mode = 'interpreter', replicaType, watch = false, signal) {
104
+ let done = await testWithReporter(reporterName, filter, mode, replicaType, watch, signal);
96
105
  return done;
97
106
  }
98
- export async function testWithReporter(reporterName, filter = '', defaultMode = 'interpreter', replicaType, watch = false) {
107
+ export async function testWithReporter(reporterName, filter = '', defaultMode = 'interpreter', replicaType, watch = false, signal) {
99
108
  let rootDir = getRootDir();
100
109
  let files = [];
101
110
  let libFiles = globSync('**/test?(s)/lib.mo', globConfig);
@@ -149,6 +158,9 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
149
158
  replica.dir = testTempDir;
150
159
  fs.mkdirSync(testTempDir, { recursive: true });
151
160
  await parallel(os.cpus().length, files, async (file) => {
161
+ if (signal?.aborted) {
162
+ return;
163
+ }
152
164
  let mmf = new MMF1('store', absToRel(file));
153
165
  // mode overrides
154
166
  let lines = fs.readFileSync(file, 'utf8').split('\n');
@@ -175,14 +187,26 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
175
187
  let mocArgs = ['--hide-warnings', '--error-detail=2', ...sourcesArr.join(' ').split(' '), file].filter(x => x);
176
188
  // interpret
177
189
  if (mode === 'interpreter') {
178
- let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs]);
190
+ let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs], { signal });
191
+ proc.addListener('error', (error) => {
192
+ if (error?.code === 'ABORT_ERR') {
193
+ return;
194
+ }
195
+ throw error;
196
+ });
179
197
  pipeMMF(proc, mmf).then(resolve);
180
198
  }
181
199
  // build and run wasm
182
200
  else if (mode === 'wasi') {
183
201
  let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`;
184
202
  // build
185
- let buildProc = spawn(mocPath, [`-o=${wasmFile}`, '-wasi-system-api', ...mocArgs]);
203
+ let buildProc = spawn(mocPath, [`-o=${wasmFile}`, '-wasi-system-api', ...mocArgs], { signal });
204
+ buildProc.addListener('error', (error) => {
205
+ if (error?.code === 'ABORT_ERR') {
206
+ return;
207
+ }
208
+ throw error;
209
+ });
186
210
  pipeMMF(buildProc, mmf).then(async () => {
187
211
  if (mmf.failed > 0) {
188
212
  return;
@@ -204,7 +228,13 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
204
228
  console.error(chalk.red('Minimum wasmtime version is 14.0.0. Please update wasmtime to the latest version'));
205
229
  process.exit(1);
206
230
  }
207
- let proc = spawn(wasmtimePath, wasmtimeArgs);
231
+ let proc = spawn(wasmtimePath, wasmtimeArgs, { signal });
232
+ proc.addListener('error', (error) => {
233
+ if (error?.code === 'ABORT_ERR') {
234
+ return;
235
+ }
236
+ throw error;
237
+ });
208
238
  await pipeMMF(proc, mmf);
209
239
  }).finally(() => {
210
240
  fs.rmSync(wasmFile, { force: true });
@@ -215,18 +245,30 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
215
245
  // mmf.strategy = 'print'; // because we run replica tests one-by-one
216
246
  let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`;
217
247
  // build
218
- let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs]);
248
+ let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs], { signal });
249
+ buildProc.addListener('error', (error) => {
250
+ if (error?.code === 'ABORT_ERR') {
251
+ return;
252
+ }
253
+ throw error;
254
+ });
219
255
  pipeMMF(buildProc, mmf).then(async () => {
220
256
  if (mmf.failed > 0) {
221
257
  return;
222
258
  }
223
259
  await startReplicaOnce(replica, replicaType);
260
+ if (signal?.aborted) {
261
+ return;
262
+ }
224
263
  let canisterName = path.parse(file).name;
225
264
  let idlFactory = ({ IDL }) => {
226
265
  return IDL.Service({ 'runTests': IDL.Func([], [], []) });
227
266
  };
228
- let { stream } = await replica.deploy(canisterName, wasmFile, idlFactory);
229
- pipeStdoutToMMF(stream, mmf);
267
+ let canister = await replica.deploy(canisterName, wasmFile, idlFactory, undefined, signal);
268
+ if (signal?.aborted || !canister) {
269
+ return;
270
+ }
271
+ pipeStdoutToMMF(canister.stream, mmf);
230
272
  let actor = await replica.getActor(canisterName);
231
273
  try {
232
274
  if (globalThis.mopsReplicaTestRunning) {
@@ -239,6 +281,9 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
239
281
  }, Math.random() * 1000 | 0);
240
282
  });
241
283
  }
284
+ if (signal?.aborted) {
285
+ return;
286
+ }
242
287
  globalThis.mopsReplicaTestRunning = true;
243
288
  await actor.runTests();
244
289
  globalThis.mopsReplicaTestRunning = false;
@@ -250,10 +295,14 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
250
295
  stderrStream.write(e.message);
251
296
  }
252
297
  }).finally(async () => {
298
+ globalThis.mopsReplicaTestRunning = false;
253
299
  fs.rmSync(wasmFile, { force: true });
254
300
  }).then(resolve);
255
301
  }
256
302
  });
303
+ if (signal?.aborted) {
304
+ return;
305
+ }
257
306
  reporter.addRun(file, mmf, promise, mode);
258
307
  await promise;
259
308
  });
@@ -261,6 +310,9 @@ export async function testWithReporter(reporterName, filter = '', defaultMode =
261
310
  await replica.stop();
262
311
  fs.rmSync(testTempDir, { recursive: true, force: true });
263
312
  }
313
+ if (signal?.aborted) {
314
+ return false;
315
+ }
264
316
  return reporter.done();
265
317
  }
266
318
  function pipeStdoutToMMF(stdout, mmf) {
@@ -0,0 +1,24 @@
1
+ import { ErrorChecker } from './error-checker.js';
2
+ import { Generator } from './generator.js';
3
+ export declare class Deployer {
4
+ verbose: boolean;
5
+ canisters: Record<string, string>;
6
+ status: 'pending' | 'running' | 'syntax-error' | 'dfx-error' | 'error' | 'success';
7
+ errorChecker: ErrorChecker;
8
+ generator: Generator;
9
+ success: number;
10
+ errors: string[];
11
+ aborted: boolean;
12
+ controllers: Map<string, AbortController>;
13
+ currentRun: Promise<any> | undefined;
14
+ constructor({ verbose, canisters, errorChecker, generator }: {
15
+ verbose: boolean;
16
+ canisters: Record<string, string>;
17
+ errorChecker: ErrorChecker;
18
+ generator: Generator;
19
+ });
20
+ reset(): void;
21
+ abortCurrent(): Promise<void>;
22
+ run(onProgress: () => void): Promise<void>;
23
+ getOutput(): string;
24
+ }
@@ -0,0 +1,125 @@
1
+ import chalk from 'chalk';
2
+ import os from 'node:os';
3
+ import { promisify } from 'node:util';
4
+ import { execFile, execSync } from 'node:child_process';
5
+ import { parallel } from '../../parallel.js';
6
+ import { getRootDir } from '../../mops.js';
7
+ export class Deployer {
8
+ constructor({ verbose, canisters, errorChecker, generator }) {
9
+ this.verbose = false;
10
+ this.canisters = {};
11
+ this.status = 'pending';
12
+ this.success = 0;
13
+ this.errors = [];
14
+ this.aborted = false;
15
+ this.controllers = new Map();
16
+ this.verbose = verbose;
17
+ this.canisters = canisters;
18
+ this.errorChecker = errorChecker;
19
+ this.generator = generator;
20
+ }
21
+ reset() {
22
+ this.status = 'pending';
23
+ this.success = 0;
24
+ this.errors = [];
25
+ }
26
+ async abortCurrent() {
27
+ this.aborted = true;
28
+ for (let controller of this.controllers.values()) {
29
+ controller.abort();
30
+ }
31
+ this.controllers.clear();
32
+ await this.currentRun;
33
+ this.reset();
34
+ this.aborted = false;
35
+ }
36
+ async run(onProgress) {
37
+ await this.abortCurrent();
38
+ if (this.errorChecker.status === 'error') {
39
+ this.status = 'syntax-error';
40
+ onProgress();
41
+ return;
42
+ }
43
+ if (Object.keys(this.canisters).length === 0) {
44
+ this.status = 'success';
45
+ onProgress();
46
+ return;
47
+ }
48
+ let rootDir = getRootDir();
49
+ try {
50
+ execSync('dfx ping', { cwd: rootDir });
51
+ }
52
+ catch (error) {
53
+ this.status = 'dfx-error';
54
+ onProgress();
55
+ return;
56
+ }
57
+ this.status = 'running';
58
+ onProgress();
59
+ // create canisters (sequentially to avoid DFX errors)
60
+ let resolve;
61
+ this.currentRun = new Promise((res) => resolve = res);
62
+ for (let canister of Object.keys(this.canisters)) {
63
+ let controller = new AbortController();
64
+ let { signal } = controller;
65
+ this.controllers.set(canister, controller);
66
+ await promisify(execFile)('dfx', ['canister', 'create', canister], { cwd: rootDir, signal }).catch((error) => {
67
+ if (error.code === 'ABORT_ERR') {
68
+ return { stderr: '' };
69
+ }
70
+ throw error;
71
+ });
72
+ this.controllers.delete(canister);
73
+ }
74
+ resolve?.();
75
+ if (this.aborted) {
76
+ return;
77
+ }
78
+ this.currentRun = parallel(os.cpus().length, [...Object.keys(this.canisters)], async (canister) => {
79
+ let controller = new AbortController();
80
+ let { signal } = controller;
81
+ this.controllers.set(canister, controller);
82
+ // build
83
+ if (this.generator.status !== 'success' || !this.generator.canisters[canister]) {
84
+ await promisify(execFile)('dfx', ['build', canister], { cwd: rootDir, signal }).catch((error) => {
85
+ if (error.code === 'ABORT_ERR') {
86
+ return { stderr: '' };
87
+ }
88
+ throw error;
89
+ });
90
+ }
91
+ // install
92
+ await promisify(execFile)('dfx', ['canister', 'install', '--mode=auto', canister], { cwd: rootDir, signal }).catch((error) => {
93
+ if (error.code === 'ABORT_ERR') {
94
+ return { stderr: '' };
95
+ }
96
+ throw error;
97
+ });
98
+ this.success += 1;
99
+ this.controllers.delete(canister);
100
+ onProgress();
101
+ });
102
+ await this.currentRun;
103
+ if (!this.aborted) {
104
+ this.status = 'success';
105
+ }
106
+ onProgress();
107
+ }
108
+ getOutput() {
109
+ let get = (v) => v.toString();
110
+ let count = (this.status === 'running' ? get : chalk.bold[this.errors.length > 0 ? 'redBright' : 'green'])(this.errors.length || this.success);
111
+ if (this.status === 'pending') {
112
+ return `Deploy: ${chalk.gray('(pending)')}`;
113
+ }
114
+ if (this.status === 'running') {
115
+ return `Deploy: ${count}/${Object.keys(this.canisters).length} ${chalk.gray('(running)')}`;
116
+ }
117
+ if (this.status === 'syntax-error') {
118
+ return `Deploy: ${chalk.gray('(errors)')}`;
119
+ }
120
+ if (this.status === 'dfx-error') {
121
+ return `Deploy: ${chalk.gray('(dfx not running)')}`;
122
+ }
123
+ return `Deploy: ${count}`;
124
+ }
125
+ }
@@ -0,0 +1,13 @@
1
+ export declare class ErrorChecker {
2
+ verbose: boolean;
3
+ canisters: Record<string, string>;
4
+ status: 'pending' | 'running' | 'error' | 'success';
5
+ errors: string[];
6
+ constructor({ verbose, canisters }: {
7
+ verbose: boolean;
8
+ canisters: Record<string, string>;
9
+ });
10
+ reset(): void;
11
+ run(onProgress: () => void): Promise<void>;
12
+ getOutput(): string;
13
+ }