ic-mops 1.1.0-pre.0 → 1.1.1

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 (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bin/moc-wrapper.sh +1 -1
  3. package/bundle/cli.tgz +0 -0
  4. package/check-requirements.ts +1 -1
  5. package/cli.ts +18 -0
  6. package/commands/bench.ts +1 -1
  7. package/commands/install/install-all.ts +2 -2
  8. package/commands/replica.ts +33 -3
  9. package/commands/sources.ts +1 -1
  10. package/commands/sync.ts +4 -19
  11. package/commands/test/mmf1.ts +4 -0
  12. package/commands/test/reporters/silent-reporter.ts +22 -4
  13. package/commands/test/test.ts +74 -10
  14. package/commands/watch/deployer.ts +155 -0
  15. package/commands/watch/error-checker.ts +87 -0
  16. package/commands/watch/generator.ts +99 -0
  17. package/commands/watch/globMoFiles.ts +16 -0
  18. package/commands/watch/parseDfxJson.ts +64 -0
  19. package/commands/watch/tester.ts +81 -0
  20. package/commands/watch/warning-checker.ts +133 -0
  21. package/commands/watch/watch.ts +90 -0
  22. package/declarations/main/main.did +16 -10
  23. package/declarations/main/main.did.d.ts +19 -10
  24. package/declarations/main/main.did.js +25 -11
  25. package/dist/bin/moc-wrapper.sh +1 -1
  26. package/dist/check-requirements.js +1 -1
  27. package/dist/cli.js +16 -0
  28. package/dist/commands/bench.js +1 -1
  29. package/dist/commands/install/install-all.js +2 -2
  30. package/dist/commands/replica.d.ts +2 -2
  31. package/dist/commands/replica.js +26 -3
  32. package/dist/commands/sources.d.ts +1 -1
  33. package/dist/commands/sources.js +1 -1
  34. package/dist/commands/sync.js +3 -18
  35. package/dist/commands/test/mmf1.d.ts +1 -0
  36. package/dist/commands/test/mmf1.js +3 -0
  37. package/dist/commands/test/reporters/silent-reporter.d.ts +6 -1
  38. package/dist/commands/test/reporters/silent-reporter.js +18 -5
  39. package/dist/commands/test/test.d.ts +1 -1
  40. package/dist/commands/test/test.js +62 -10
  41. package/dist/commands/watch/deployer.d.ts +24 -0
  42. package/dist/commands/watch/deployer.js +125 -0
  43. package/dist/commands/watch/error-checker.d.ts +13 -0
  44. package/dist/commands/watch/error-checker.js +76 -0
  45. package/dist/commands/watch/generator.d.ts +21 -0
  46. package/dist/commands/watch/generator.js +79 -0
  47. package/dist/commands/watch/globMoFiles.d.ts +1 -0
  48. package/dist/commands/watch/globMoFiles.js +14 -0
  49. package/dist/commands/watch/parseDfxJson.d.ts +2 -0
  50. package/dist/commands/watch/parseDfxJson.js +22 -0
  51. package/dist/commands/watch/tester.d.ts +19 -0
  52. package/dist/commands/watch/tester.js +63 -0
  53. package/dist/commands/watch/warning-checker.d.ts +20 -0
  54. package/dist/commands/watch/warning-checker.js +111 -0
  55. package/dist/commands/watch/watch.d.ts +7 -0
  56. package/dist/commands/watch/watch.js +79 -0
  57. package/dist/declarations/main/main.did +16 -10
  58. package/dist/declarations/main/main.did.d.ts +19 -10
  59. package/dist/declarations/main/main.did.js +25 -11
  60. package/dist/helpers/get-moc-path.d.ts +1 -1
  61. package/dist/helpers/get-moc-path.js +17 -3
  62. package/dist/helpers/get-moc-version.d.ts +1 -1
  63. package/dist/helpers/get-moc-version.js +17 -5
  64. package/dist/integrity.js +3 -2
  65. package/dist/package.json +3 -2
  66. package/dist/parallel.d.ts +1 -1
  67. package/dist/templates/mops-test.yml +4 -2
  68. package/helpers/get-moc-path.ts +19 -3
  69. package/helpers/get-moc-version.ts +17 -5
  70. package/integrity.ts +3 -2
  71. package/package.json +3 -2
  72. package/parallel.ts +2 -2
  73. package/templates/mops-test.yml +4 -2
  74. package/test +0 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Mops CLI Changelog
2
2
 
3
+ ## 1.1.1
4
+ - `moc-wrapper` now adds hostname to the moc path cache(`.mops/moc-*` filename) to avoid errors when running in Dev Containers
5
+ - `mops watch` now deploys canisters with the `--yes` flag to skip data loss confirmation
6
+
7
+ ## 1.1.0
8
+ - New `mops watch` command to check for syntax errors, show warnings, run tests, generate declarations and deploy canisters ([docs](https://docs.mops.one/cli/mops-watch))
9
+ - New flag `--no-toolchain` in `mops install` command to skip toolchain installation
10
+ - New lock file format v3 ([docs](https://docs.mops.one/mops.lock))
11
+ - Faster `mops install` from lock file when lock file is up-to-date and there are no cached packages
12
+ - Fixed replica test hanging in watch mode bug
13
+ - Fixed mops failing when dfx is not installed
14
+ - Fixed `mops test` Github Action template
15
+
3
16
  ## 1.0.1
4
17
  - Fixed `mops user *` commands
5
18
 
@@ -22,7 +22,7 @@ else
22
22
  mopsTomlHash=$(shasum $mopsToml -a 256 | awk -F' ' '{print $1}')
23
23
  fi;
24
24
 
25
- cached="$rootDir/.mops/moc-$mopsTomlHash"
25
+ cached="$rootDir/.mops/moc-$(uname -n)-$mopsTomlHash"
26
26
 
27
27
  if [ -f $cached ]; then
28
28
  mocPath=$(cat $cached)
package/bundle/cli.tgz CHANGED
Binary file
@@ -11,7 +11,7 @@ export async function checkRequirements({verbose = false} = {}) {
11
11
  let config = readConfig();
12
12
  let mocVersion = config.toolchain?.moc;
13
13
  if (!mocVersion) {
14
- mocVersion = getMocVersion();
14
+ mocVersion = getMocVersion(false);
15
15
  }
16
16
  if (!mocVersion) {
17
17
  return;
package/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import fs from 'node:fs';
3
+ import events from 'node:events';
3
4
  import {Command, Argument, Option} from 'commander';
4
5
 
5
6
  import {init} from './commands/init.js';
@@ -25,6 +26,7 @@ import {toolchain} from './commands/toolchain/index.js';
25
26
  import {Tool} from './types.js';
26
27
  import * as self from './commands/self.js';
27
28
  import {resolvePackages} from './resolve-packages.js';
29
+ import {watch} from './commands/watch/watch.js';
28
30
 
29
31
  declare global {
30
32
  // eslint-disable-next-line no-var
@@ -33,6 +35,8 @@ declare global {
33
35
  var mopsReplicaTestRunning : boolean;
34
36
  }
35
37
 
38
+ events.setMaxListeners(20);
39
+
36
40
  let networkFile = getNetworkFile();
37
41
  if (fs.existsSync(networkFile)) {
38
42
  globalThis.MOPS_NETWORK = fs.readFileSync(networkFile).toString() || 'ic';
@@ -398,4 +402,18 @@ selfCommand
398
402
 
399
403
  program.addCommand(selfCommand);
400
404
 
405
+ // watch
406
+ program
407
+ .command('watch')
408
+ .description('Watch *.mo files and check for syntax errors, warnings, run tests, generate declarations and deploy canisters')
409
+ .option('-e, --error', 'Check Motoko canisters or *.mo files for syntax errors')
410
+ .option('-w, --warning', 'Check Motoko canisters or *.mo files for warnings')
411
+ .option('-t, --test', 'Run tests')
412
+ .option('-g, --generate', 'Generate declarations for Motoko canisters')
413
+ .option('-d, --deploy', 'Deploy Motoko canisters')
414
+ .action(async (options) => {
415
+ checkConfigFile(true);
416
+ await watch(options);
417
+ });
418
+
401
419
  program.parse();
package/commands/bench.ts CHANGED
@@ -55,7 +55,7 @@ export async function bench(filter = '', optionsArg : Partial<BenchOptions> = {}
55
55
  replica: config.toolchain?.['pocket-ic'] ? 'pocket-ic' : 'dfx',
56
56
  replicaVersion: '',
57
57
  compiler: 'moc',
58
- compilerVersion: getMocVersion(),
58
+ compilerVersion: getMocVersion(true),
59
59
  gc: 'copying',
60
60
  forceGc: true,
61
61
  save: false,
@@ -34,10 +34,10 @@ export async function installAll({verbose = false, silent = false, threads, lock
34
34
  if (lockFileJson && lockFileJson.version === 3) {
35
35
  verbose && console.log('Installing from lock file...');
36
36
  installedFromLockFile = true;
37
- let deps = Object.entries(lockFileJson.deps).map(([name, version]) => {
37
+ let lockedDeps = Object.entries(lockFileJson.deps).map(([name, version]) => {
38
38
  return parseDepValue(name, version);
39
39
  });
40
- let ok = await installDeps(deps, {silent, verbose, threads, ignoreTransitive: true});
40
+ let ok = await installDeps(lockedDeps, {silent, verbose, threads, ignoreTransitive: true});
41
41
  if (!ok) {
42
42
  return false;
43
43
  }
@@ -3,6 +3,7 @@ import {ChildProcessWithoutNullStreams, execSync, spawn} from 'node:child_proces
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
 
7
8
  import {IDL} from '@dfinity/candid';
8
9
  import {Actor, HttpAgent} from '@dfinity/agent';
@@ -126,7 +127,7 @@ export class Replica {
126
127
  }
127
128
  }
128
129
 
129
- async deploy(name : string, wasm : string, idlFactory : IDL.InterfaceFactory, cwd : string = process.cwd()) {
130
+ async deploy(name : string, wasm : string, idlFactory : IDL.InterfaceFactory, cwd : string = process.cwd(), signal ?: AbortSignal) {
130
131
  if (this.type === 'dfx') {
131
132
  // prepare dfx.json for current canister
132
133
  let dfxJson = path.join(this.dir, 'dfx.json');
@@ -144,8 +145,27 @@ export class Replica {
144
145
  fs.mkdirSync(this.dir, {recursive: true});
145
146
  fs.writeFileSync(dfxJson, JSON.stringify(newDfxJsonData, null, 2));
146
147
 
147
- execSync(`dfx deploy ${name} --mode reinstall --yes --identity anonymous`, {cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']});
148
- execSync(`dfx ledger fabricate-cycles --canister ${name} --t 100`, {cwd: this.dir, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']});
148
+ await spawnAsync('dfx', ['deploy', name, '--mode', 'reinstall', '--yes', '--identity', 'anonymous'], {cwd: this.dir, signal, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}).catch((error) => {
149
+ if (error.code === 'ABORT_ERR') {
150
+ return {stderr: ''};
151
+ }
152
+ throw error;
153
+ });
154
+
155
+ if (signal?.aborted) {
156
+ return;
157
+ }
158
+
159
+ await spawnAsync('dfx', ['ledger', 'fabricate-cycles', '--canister', name, '--t', '100'], {cwd: this.dir, signal, stdio: this.verbose ? 'pipe' : ['pipe', 'ignore', 'pipe']}).catch((error) => {
160
+ if (error.code === 'ABORT_ERR') {
161
+ return {stderr: ''};
162
+ }
163
+ throw error;
164
+ });
165
+
166
+ if (signal?.aborted) {
167
+ return;
168
+ }
149
169
 
150
170
  let canisterId = execSync(`dfx canister id ${name}`, {cwd: this.dir}).toString().trim();
151
171
 
@@ -170,7 +190,17 @@ export class Replica {
170
190
  idlFactory,
171
191
  wasm,
172
192
  });
193
+
194
+ if (signal?.aborted) {
195
+ return;
196
+ }
197
+
173
198
  await this.pocketIc.addCycles(canisterId, 1_000_000_000_000);
199
+
200
+ if (signal?.aborted) {
201
+ return;
202
+ }
203
+
174
204
  this.canisters[name] = {
175
205
  cwd,
176
206
  canisterId: canisterId.toText(),
@@ -45,5 +45,5 @@ export async function sources({conflicts = 'ignore' as 'warning' | 'error' | 'ig
45
45
  }
46
46
 
47
47
  return `--package ${name} ${pkgBaseDir}`;
48
- });
48
+ }).filter(x => x != null);
49
49
  }
package/commands/sync.ts CHANGED
@@ -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
 
11
11
  type SyncOptions = {
12
12
  lock ?: 'update' | 'ignore';
@@ -48,25 +48,10 @@ let ignore = [
48
48
  '**/.mops/**',
49
49
  ];
50
50
 
51
- let mocPath = '';
52
- function getMocPath() : string {
53
- if (!mocPath) {
54
- mocPath = process.env.DFX_MOC_PATH || '';
55
- }
56
- if (!mocPath) {
57
- try {
58
- mocPath = execSync('dfx cache show').toString().trim() + '/moc';
59
- }
60
- catch {}
61
- }
62
- if (!mocPath) {
63
- mocPath = 'moc';
64
- }
65
- return mocPath;
66
- }
67
-
68
51
  async function getUsedPackages() : Promise<string[]> {
69
52
  let rootDir = getRootDir();
53
+ let mocPath = getMocPath();
54
+
70
55
  let files = globSync('**/*.mo', {
71
56
  cwd: rootDir,
72
57
  nocase: true,
@@ -76,7 +61,7 @@ async function getUsedPackages() : Promise<string[]> {
76
61
  let packages : Set<string> = new Set;
77
62
 
78
63
  for (let file of files) {
79
- let deps : string[] = execSync(`${getMocPath()} --print-deps ${path.join(rootDir, file)}`).toString().trim().split('\n');
64
+ let deps : string[] = execSync(`${mocPath} --print-deps ${path.join(rootDir, file)}`).toString().trim().split('\n');
80
65
 
81
66
  for (let dep of deps) {
82
67
  if (dep.startsWith('mo:') && !dep.startsWith('mo:prim') && !dep.startsWith('mo:⛔')) {
@@ -53,6 +53,10 @@ export class MMF1 {
53
53
  this.output = [];
54
54
  }
55
55
 
56
+ getErrorMessages() {
57
+ return this.output.filter(out => out.type === 'fail').map(out => out.message);
58
+ }
59
+
56
60
  parseLine(line : string) {
57
61
  if (line.startsWith('mops:1:start ')) {
58
62
  this._testStart(line.split('mops:1:start ')[1] || '');
@@ -5,14 +5,25 @@ import {Reporter} from './reporter.js';
5
5
  import {TestMode} from '../../../types.js';
6
6
 
7
7
  export class SilentReporter implements Reporter {
8
+ total = 0;
8
9
  passed = 0;
9
10
  failed = 0;
10
11
  skipped = 0;
11
12
  passedFiles = 0;
12
13
  failedFiles = 0;
13
14
  passedNamesFlat : string[] = [];
15
+ flushOnError = true;
16
+ errorOutput = '';
17
+ onProgress = () => {};
14
18
 
15
- addFiles(_files : string[]) {}
19
+ constructor(flushOnError = true, onProgress = () => {}) {
20
+ this.flushOnError = flushOnError;
21
+ this.onProgress = onProgress;
22
+ }
23
+
24
+ addFiles(files : string[]) {
25
+ this.total = files.length;
26
+ }
16
27
 
17
28
  addRun(file : string, mmf : MMF1, state : Promise<void>, _mode : TestMode) {
18
29
  state.then(() => {
@@ -30,10 +41,17 @@ export class SilentReporter implements Reporter {
30
41
  this.failedFiles += Number(mmf.failed !== 0);
31
42
 
32
43
  if (mmf.failed) {
33
- console.log(chalk.red('✖'), absToRel(file));
34
- mmf.flush('fail');
35
- console.log('-'.repeat(50));
44
+ let output = `${chalk.red('✖')} ${absToRel(file)}\n${mmf.getErrorMessages().join('\n')}\n${'-'.repeat(50)}`;
45
+
46
+ if (this.flushOnError) {
47
+ console.log(output);
48
+ }
49
+ else {
50
+ this.errorOutput = `${this.errorOutput}\n${output}`.trim();
51
+ }
36
52
  }
53
+
54
+ this.onProgress();
37
55
  });
38
56
  }
39
57
 
@@ -85,14 +85,28 @@ export async function test(filter = '', options : Partial<TestOptions> = {}) {
85
85
  process.exit(0);
86
86
  });
87
87
  }
88
+ else {
89
+ process.exit(0);
90
+ }
88
91
  });
89
92
 
90
93
  // todo: run only changed for *.test.mo?
91
94
  // todo: run all for *.mo?
95
+
96
+ let curRun = Promise.resolve(true);
97
+ let controller = new AbortController();
98
+
92
99
  let run = debounce(async () => {
100
+ controller.abort();
101
+ await curRun;
102
+
93
103
  console.clear();
94
104
  process.stdout.write('\x1Bc');
95
- await runAll(options.reporter, filter, options.mode, replicaType, true);
105
+
106
+ controller = new AbortController();
107
+ curRun = runAll(options.reporter, filter, options.mode, replicaType, true, controller.signal);
108
+ await curRun;
109
+
96
110
  console.log('-'.repeat(50));
97
111
  console.log('Waiting for file changes...');
98
112
  console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`)));
@@ -122,12 +136,12 @@ export async function test(filter = '', options : Partial<TestOptions> = {}) {
122
136
  let mocPath = '';
123
137
  let wasmtimePath = '';
124
138
 
125
- async function runAll(reporterName : ReporterName | undefined, filter = '', mode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false) : Promise<boolean> {
126
- let done = await testWithReporter(reporterName, filter, mode, replicaType, watch);
139
+ async function runAll(reporterName : ReporterName | undefined, filter = '', mode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false, signal ?: AbortSignal) : Promise<boolean> {
140
+ let done = await testWithReporter(reporterName, filter, mode, replicaType, watch, signal);
127
141
  return done;
128
142
  }
129
143
 
130
- export async function testWithReporter(reporterName : ReporterName | Reporter | undefined, filter = '', defaultMode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false) : Promise<boolean> {
144
+ export async function testWithReporter(reporterName : ReporterName | Reporter | undefined, filter = '', defaultMode : TestMode = 'interpreter', replicaType : ReplicaName, watch = false, signal ?: AbortSignal) : Promise<boolean> {
131
145
  let rootDir = getRootDir();
132
146
  let files : string[] = [];
133
147
  let libFiles = globSync('**/test?(s)/lib.mo', globConfig);
@@ -191,6 +205,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
191
205
  fs.mkdirSync(testTempDir, {recursive: true});
192
206
 
193
207
  await parallel(os.cpus().length, files, async (file : string) => {
208
+ if (signal?.aborted) {
209
+ return;
210
+ }
211
+
194
212
  let mmf = new MMF1('store', absToRel(file));
195
213
 
196
214
  // mode overrides
@@ -221,7 +239,13 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
221
239
 
222
240
  // interpret
223
241
  if (mode === 'interpreter') {
224
- let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs]);
242
+ let proc = spawn(mocPath, ['-r', '-ref-system-api', ...mocArgs], {signal});
243
+ proc.addListener('error', (error : any) => {
244
+ if (error?.code === 'ABORT_ERR') {
245
+ return;
246
+ }
247
+ throw error;
248
+ });
225
249
  pipeMMF(proc, mmf).then(resolve);
226
250
  }
227
251
  // build and run wasm
@@ -229,7 +253,13 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
229
253
  let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`;
230
254
 
231
255
  // build
232
- let buildProc = spawn(mocPath, [`-o=${wasmFile}`, '-wasi-system-api', ...mocArgs]);
256
+ let buildProc = spawn(mocPath, [`-o=${wasmFile}`, '-wasi-system-api', ...mocArgs], {signal});
257
+ buildProc.addListener('error', (error : any) => {
258
+ if (error?.code === 'ABORT_ERR') {
259
+ return;
260
+ }
261
+ throw error;
262
+ });
233
263
  pipeMMF(buildProc, mmf).then(async () => {
234
264
  if (mmf.failed > 0) {
235
265
  return;
@@ -252,7 +282,14 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
252
282
  process.exit(1);
253
283
  }
254
284
 
255
- let proc = spawn(wasmtimePath, wasmtimeArgs);
285
+ let proc = spawn(wasmtimePath, wasmtimeArgs, {signal});
286
+ proc.addListener('error', (error : any) => {
287
+ if (error?.code === 'ABORT_ERR') {
288
+ return;
289
+ }
290
+ throw error;
291
+ });
292
+
256
293
  await pipeMMF(proc, mmf);
257
294
  }).finally(() => {
258
295
  fs.rmSync(wasmFile, {force: true});
@@ -265,7 +302,13 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
265
302
  let wasmFile = `${path.join(testTempDir, path.parse(file).name)}.wasm`;
266
303
 
267
304
  // build
268
- let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs]);
305
+ let buildProc = spawn(mocPath, [`-o=${wasmFile}`, ...mocArgs], {signal});
306
+ buildProc.addListener('error', (error : any) => {
307
+ if (error?.code === 'ABORT_ERR') {
308
+ return;
309
+ }
310
+ throw error;
311
+ });
269
312
 
270
313
  pipeMMF(buildProc, mmf).then(async () => {
271
314
  if (mmf.failed > 0) {
@@ -274,15 +317,23 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
274
317
 
275
318
  await startReplicaOnce(replica, replicaType);
276
319
 
320
+ if (signal?.aborted) {
321
+ return;
322
+ }
323
+
277
324
  let canisterName = path.parse(file).name;
278
325
  let idlFactory = ({IDL} : any) => {
279
326
  return IDL.Service({'runTests': IDL.Func([], [], [])});
280
327
  };
281
328
  interface _SERVICE {'runTests' : ActorMethod<[], undefined>;}
282
329
 
283
- let {stream} = await replica.deploy(canisterName, wasmFile, idlFactory);
330
+ let canister = await replica.deploy(canisterName, wasmFile, idlFactory, undefined, signal);
284
331
 
285
- pipeStdoutToMMF(stream, mmf);
332
+ if (signal?.aborted || !canister) {
333
+ return;
334
+ }
335
+
336
+ pipeStdoutToMMF(canister.stream, mmf);
286
337
 
287
338
  let actor = await replica.getActor(canisterName) as _SERVICE;
288
339
 
@@ -298,6 +349,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
298
349
  });
299
350
  }
300
351
 
352
+ if (signal?.aborted) {
353
+ return;
354
+ }
355
+
301
356
  globalThis.mopsReplicaTestRunning = true;
302
357
  await actor.runTests();
303
358
  globalThis.mopsReplicaTestRunning = false;
@@ -310,11 +365,16 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
310
365
  stderrStream.write(e.message);
311
366
  }
312
367
  }).finally(async () => {
368
+ globalThis.mopsReplicaTestRunning = false;
313
369
  fs.rmSync(wasmFile, {force: true});
314
370
  }).then(resolve);
315
371
  }
316
372
  });
317
373
 
374
+ if (signal?.aborted) {
375
+ return;
376
+ }
377
+
318
378
  reporter.addRun(file, mmf, promise, mode);
319
379
 
320
380
  await promise;
@@ -325,6 +385,10 @@ export async function testWithReporter(reporterName : ReporterName | Reporter |
325
385
  fs.rmSync(testTempDir, {recursive: true, force: true});
326
386
  }
327
387
 
388
+ if (signal?.aborted) {
389
+ return false;
390
+ }
391
+
328
392
  return reporter.done();
329
393
  }
330
394
 
@@ -0,0 +1,155 @@
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
+
6
+ import {ErrorChecker} from './error-checker.js';
7
+ import {Generator} from './generator.js';
8
+ import {parallel} from '../../parallel.js';
9
+ import {getRootDir} from '../../mops.js';
10
+
11
+ export class Deployer {
12
+ verbose = false;
13
+ canisters : Record<string, string> = {};
14
+ status : 'pending' | 'running' | 'syntax-error' | 'dfx-error' | 'error' | 'success' = 'pending';
15
+ errorChecker : ErrorChecker;
16
+ generator : Generator;
17
+ success = 0;
18
+ errors : string[] = [];
19
+ aborted = false;
20
+ controllers = new Map<string, AbortController>();
21
+ currentRun : Promise<any> | undefined;
22
+
23
+ constructor({verbose, canisters, errorChecker, generator} : {verbose : boolean, canisters : Record<string, string>, errorChecker : ErrorChecker, generator : Generator}) {
24
+ this.verbose = verbose;
25
+ this.canisters = canisters;
26
+ this.errorChecker = errorChecker;
27
+ this.generator = generator;
28
+ }
29
+
30
+ reset() {
31
+ this.status = 'pending';
32
+ this.success = 0;
33
+ this.errors = [];
34
+ }
35
+
36
+ async abortCurrent() {
37
+ this.aborted = true;
38
+ for (let controller of this.controllers.values()) {
39
+ controller.abort();
40
+ }
41
+ this.controllers.clear();
42
+ await this.currentRun;
43
+ this.reset();
44
+ this.aborted = false;
45
+ }
46
+
47
+ async run(onProgress : () => void) {
48
+ await this.abortCurrent();
49
+
50
+ if (this.errorChecker.status === 'error') {
51
+ this.status = 'syntax-error';
52
+ onProgress();
53
+ return;
54
+ }
55
+
56
+ if (Object.keys(this.canisters).length === 0) {
57
+ this.status = 'success';
58
+ onProgress();
59
+ return;
60
+ }
61
+
62
+ let rootDir = getRootDir();
63
+
64
+ try {
65
+ execSync('dfx ping', {cwd: rootDir});
66
+ }
67
+ catch (error) {
68
+ this.status = 'dfx-error';
69
+ onProgress();
70
+ return;
71
+ }
72
+
73
+ this.status = 'running';
74
+ onProgress();
75
+
76
+ // create canisters (sequentially to avoid DFX errors)
77
+ let resolve : (() => void) | undefined;
78
+ this.currentRun = new Promise<void>((res) => resolve = res);
79
+ for (let canister of Object.keys(this.canisters)) {
80
+ let controller = new AbortController();
81
+ let {signal} = controller;
82
+ this.controllers.set(canister, controller);
83
+
84
+ await promisify(execFile)('dfx', ['canister', 'create', canister], {cwd: rootDir, signal}).catch((error) => {
85
+ if (error.code === 'ABORT_ERR') {
86
+ return {stderr: ''};
87
+ }
88
+ throw error;
89
+ });
90
+
91
+ this.controllers.delete(canister);
92
+ }
93
+
94
+ resolve?.();
95
+
96
+ if (this.aborted) {
97
+ return;
98
+ }
99
+
100
+ this.currentRun = parallel(os.cpus().length, [...Object.keys(this.canisters)], async (canister) => {
101
+ let controller = new AbortController();
102
+ let {signal} = controller;
103
+ this.controllers.set(canister, controller);
104
+
105
+ // build
106
+ if (this.generator.status !== 'success' || !this.generator.canisters[canister]) {
107
+ await promisify(execFile)('dfx', ['build', canister], {cwd: rootDir, signal}).catch((error) => {
108
+ if (error.code === 'ABORT_ERR') {
109
+ return {stderr: ''};
110
+ }
111
+ throw error;
112
+ });
113
+ }
114
+
115
+ // install
116
+ await promisify(execFile)('dfx', ['canister', 'install', '--mode=auto', '--yes', canister], {cwd: rootDir, signal}).catch((error) => {
117
+ if (error.code === 'ABORT_ERR') {
118
+ return {stderr: ''};
119
+ }
120
+ throw error;
121
+ });
122
+
123
+ this.success += 1;
124
+ this.controllers.delete(canister);
125
+ onProgress();
126
+ });
127
+
128
+ await this.currentRun;
129
+
130
+ if (!this.aborted) {
131
+ this.status = 'success';
132
+ }
133
+ onProgress();
134
+ }
135
+
136
+ getOutput() : string {
137
+ let get = (v : number) => v.toString();
138
+ let count = (this.status === 'running' ? get : chalk.bold[this.errors.length > 0 ? 'redBright' : 'green'])(this.errors.length || this.success);
139
+
140
+ if (this.status === 'pending') {
141
+ return `Deploy: ${chalk.gray('(pending)')}`;
142
+ }
143
+ if (this.status === 'running') {
144
+ return `Deploy: ${count}/${Object.keys(this.canisters).length} ${chalk.gray('(running)')}`;
145
+ }
146
+ if (this.status === 'syntax-error') {
147
+ return `Deploy: ${chalk.gray('(errors)')}`;
148
+ }
149
+ if (this.status === 'dfx-error') {
150
+ return `Deploy: ${chalk.gray('(dfx not running)')}`;
151
+ }
152
+
153
+ return `Deploy: ${count}`;
154
+ }
155
+ }