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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Mops CLI Changelog
2
2
 
3
+ ## 1.1.0
4
+ - 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))
5
+ - New flag `--no-toolchain` in `mops install` command to skip toolchain installation
6
+ - New lock file format v3 ([docs](https://docs.mops.one/mops.lock))
7
+ - Faster `mops install` from lock file when lock file is up-to-date and there are no cached packages
8
+ - Fixed replica test hanging in watch mode bug
9
+ - Fixed mops failing when dfx is not installed
10
+ - Fixed `mops test` Github Action template
11
+
3
12
  ## 1.0.1
4
13
  - Fixed `mops user *` commands
5
14
 
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';
@@ -89,6 +93,7 @@ program
89
93
  .command('install')
90
94
  .alias('i')
91
95
  .description('Install all dependencies specified in mops.toml')
96
+ .option('--no-toolchain', 'Do not install toolchain')
92
97
  .option('--verbose')
93
98
  .addOption(new Option('--lock <action>', 'Lockfile action').choices(['check', 'update', 'ignore']))
94
99
  .action(async (options) => {
@@ -101,10 +106,15 @@ program
101
106
  return;
102
107
  }
103
108
 
104
- await toolchain.ensureToolchainInited({strict: false});
109
+ if (options.toolchain) {
110
+ await toolchain.ensureToolchainInited({strict: false});
111
+ }
105
112
 
106
113
  let ok = await installAll(options);
107
- await toolchain.installAll(options);
114
+
115
+ if (options.toolchain) {
116
+ await toolchain.installAll(options);
117
+ }
108
118
 
109
119
  // check conflicts
110
120
  await resolvePackages({conflicts: 'warning'});
@@ -162,7 +172,7 @@ program
162
172
  process.exit(1);
163
173
  }
164
174
  if (options.install) {
165
- await installAll({silent: true, lock: 'ignore', threads: 6});
175
+ await installAll({silent: true, lock: 'ignore', threads: 6, installFromLockFile: true});
166
176
  }
167
177
  await toolchain.ensureToolchainInited({strict: false});
168
178
  let sourcesArr = await sources(options);
@@ -206,7 +216,7 @@ program
206
216
  .option('-w, --watch', 'Enable watch mode')
207
217
  .action(async (filter, options) => {
208
218
  checkConfigFile(true);
209
- await installAll({silent: true, lock: 'ignore'});
219
+ await installAll({silent: true, lock: 'ignore', installFromLockFile: true});
210
220
  await test(filter, options);
211
221
  });
212
222
 
@@ -222,7 +232,7 @@ program
222
232
  .addOption(new Option('--verbose', 'Show more information'))
223
233
  .action(async (filter, options) => {
224
234
  checkConfigFile(true);
225
- await installAll({silent: true, lock: 'ignore'});
235
+ await installAll({silent: true, lock: 'ignore', installFromLockFile: true});
226
236
  await bench(filter, options);
227
237
  });
228
238
 
@@ -392,4 +402,18 @@ selfCommand
392
402
 
393
403
  program.addCommand(selfCommand);
394
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
+
395
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,
@@ -1,8 +1,8 @@
1
1
  import process from 'node:process';
2
2
  import chalk from 'chalk';
3
3
  import {createLogUpdate} from 'log-update';
4
- import {checkConfigFile, readConfig} from '../../mops.js';
5
- import {checkIntegrity} from '../../integrity.js';
4
+ import {checkConfigFile, parseDepValue, readConfig} from '../../mops.js';
5
+ import {checkIntegrity, checkLockFileLight, readLockFile} from '../../integrity.js';
6
6
  import {installDeps} from './install-deps.js';
7
7
  import {checkRequirements} from '../../check-requirements.js';
8
8
  import {syncLocalCache} from './sync-local-cache.js';
@@ -13,9 +13,10 @@ type InstallAllOptions = {
13
13
  silent ?: boolean;
14
14
  lock ?: 'check' | 'update' | 'ignore';
15
15
  threads ?: number;
16
+ installFromLockFile ?: boolean;
16
17
  }
17
18
 
18
- export async function installAll({verbose = false, silent = false, threads, lock} : InstallAllOptions = {}) : Promise<boolean> {
19
+ export async function installAll({verbose = false, silent = false, threads, lock, installFromLockFile} : InstallAllOptions = {}) : Promise<boolean> {
19
20
  if (!checkConfigFile()) {
20
21
  return false;
21
22
  }
@@ -24,12 +25,33 @@ export async function installAll({verbose = false, silent = false, threads, lock
24
25
  let deps = Object.values(config.dependencies || {});
25
26
  let devDeps = Object.values(config['dev-dependencies'] || {});
26
27
  let allDeps = [...deps, ...devDeps];
28
+ let installedFromLockFile = false;
27
29
 
28
- let ok = await installDeps(allDeps, {silent, verbose, threads});
29
- if (!ok) {
30
- return false;
30
+ // install from lock file to avoid installing intermediate dependencies
31
+ if ((lock !== 'ignore' || installFromLockFile) && checkLockFileLight()) {
32
+ let lockFileJson = readLockFile();
33
+
34
+ if (lockFileJson && lockFileJson.version === 3) {
35
+ verbose && console.log('Installing from lock file...');
36
+ installedFromLockFile = true;
37
+ let lockedDeps = Object.entries(lockFileJson.deps).map(([name, version]) => {
38
+ return parseDepValue(name, version);
39
+ });
40
+ let ok = await installDeps(lockedDeps, {silent, verbose, threads, ignoreTransitive: true});
41
+ if (!ok) {
42
+ return false;
43
+ }
44
+ }
31
45
  }
32
46
 
47
+ if (!installedFromLockFile) {
48
+ let ok = await installDeps(allDeps, {silent, verbose, threads});
49
+ if (!ok) {
50
+ return false;
51
+ }
52
+ }
53
+
54
+
33
55
  let logUpdate = createLogUpdate(process.stdout, {showCursor: true});
34
56
 
35
57
  if (!silent && lock !== 'ignore') {
@@ -9,13 +9,14 @@ type InstallDepOptions = {
9
9
  verbose ?: boolean;
10
10
  silent ?: boolean;
11
11
  threads ?: number;
12
+ ignoreTransitive ?: boolean;
12
13
  };
13
14
 
14
15
  // install dependency
15
16
  // returns false if failed
16
- export async function installDep(dep : Dependency, {verbose, silent, threads} : InstallDepOptions = {}, parentPkgPath ?: string) : Promise<boolean> {
17
+ export async function installDep(dep : Dependency, {verbose, silent, threads, ignoreTransitive} : InstallDepOptions = {}, parentPkgPath ?: string) : Promise<boolean> {
17
18
  if (dep.repo) {
18
- await installFromGithub(dep.name, dep.repo, {silent, verbose});
19
+ await installFromGithub(dep.name, dep.repo, {silent, verbose, ignoreTransitive});
19
20
  return true;
20
21
  }
21
22
  else if (dep.path) {
@@ -24,10 +25,10 @@ export async function installDep(dep : Dependency, {verbose, silent, threads} :
24
25
  if (parentPkgPath) {
25
26
  depPath = path.resolve(parentPkgPath, dep.path);
26
27
  }
27
- return installLocalDep(dep.name, depPath, {silent, verbose});
28
+ return installLocalDep(dep.name, depPath, {silent, verbose, ignoreTransitive});
28
29
  }
29
30
  else if (dep.version) {
30
- return installMopsDep(dep.name, dep.version, {silent, verbose, threads});
31
+ return installMopsDep(dep.name, dep.version, {silent, verbose, threads, ignoreTransitive});
31
32
  }
32
33
 
33
34
  return true;
@@ -5,16 +5,17 @@ type InstallDepsOptions = {
5
5
  verbose ?: boolean;
6
6
  silent ?: boolean;
7
7
  threads ?: number;
8
+ ignoreTransitive ?: boolean;
8
9
  };
9
10
 
10
11
  // install all dependencies
11
12
  // returns actual installed dependencies
12
13
  // returns false if failed
13
- export async function installDeps(deps : Dependency[], {verbose, silent, threads} : InstallDepsOptions = {}, parentPkgPath ?: string) : Promise<boolean> {
14
+ export async function installDeps(deps : Dependency[], {verbose, silent, threads, ignoreTransitive} : InstallDepsOptions = {}, parentPkgPath ?: string) : Promise<boolean> {
14
15
  let ok = true;
15
16
 
16
17
  for (const dep of deps) {
17
- let res = await installDep(dep, {verbose, silent, threads}, parentPkgPath);
18
+ let res = await installDep(dep, {verbose, silent, threads, ignoreTransitive}, parentPkgPath);
18
19
  if (!res) {
19
20
  ok = false;
20
21
  }
@@ -7,11 +7,12 @@ import {installDeps} from './install-deps.js';
7
7
  type InstallLocalDepOptions = {
8
8
  verbose ?: boolean;
9
9
  silent ?: boolean;
10
+ ignoreTransitive ?: boolean;
10
11
  };
11
12
 
12
13
  // skip install and just find non-local dependencies to install
13
14
  // pkgPath should be relative to the current root dir or absolute
14
- export async function installLocalDep(pkg : string, pkgPath = '', {verbose, silent} : InstallLocalDepOptions = {}) : Promise<boolean> {
15
+ export async function installLocalDep(pkg : string, pkgPath = '', {verbose, silent, ignoreTransitive} : InstallLocalDepOptions = {}) : Promise<boolean> {
15
16
  if (!silent) {
16
17
  let logUpdate = createLogUpdate(process.stdout, {showCursor: true});
17
18
  logUpdate(`Local dependency ${pkg} = "${pkgPath}"`);
@@ -25,7 +26,12 @@ export async function installLocalDep(pkg : string, pkgPath = '', {verbose, sile
25
26
  }
26
27
 
27
28
  // install dependencies
28
- let dir = path.resolve(getRootDir(), pkgPath);
29
- let config = readConfig(path.join(dir, 'mops.toml'));
30
- return installDeps(Object.values(config.dependencies || {}), {silent, verbose}, pkgPath);
31
- }
29
+ if (!ignoreTransitive) {
30
+ let dir = path.resolve(getRootDir(), pkgPath);
31
+ let config = readConfig(path.join(dir, 'mops.toml'));
32
+ return installDeps(Object.values(config.dependencies || {}), {silent, verbose}, pkgPath);
33
+ }
34
+ else {
35
+ return true;
36
+ }
37
+ }
@@ -19,9 +19,10 @@ type InstallMopsDepOptions = {
19
19
  silent ?: boolean;
20
20
  dep ?: boolean;
21
21
  threads ?: number;
22
+ ignoreTransitive ?: boolean;
22
23
  };
23
24
 
24
- export async function installMopsDep(pkg : string, version = '', {verbose, silent, dep, threads} : InstallMopsDepOptions = {}) : Promise<boolean> {
25
+ export async function installMopsDep(pkg : string, version = '', {verbose, silent, dep, threads, ignoreTransitive} : InstallMopsDepOptions = {}) : Promise<boolean> {
25
26
  threads = threads || 12;
26
27
  let depName = getDepName(pkg);
27
28
 
@@ -113,11 +114,13 @@ export async function installMopsDep(pkg : string, version = '', {verbose, silen
113
114
  }
114
115
 
115
116
  // install dependencies
116
- let config = readConfig(path.join(cacheDir, 'mops.toml'));
117
- let res = await installDeps(Object.values(config.dependencies || {}), {silent, verbose});
117
+ if (!ignoreTransitive) {
118
+ let config = readConfig(path.join(cacheDir, 'mops.toml'));
119
+ let res = await installDeps(Object.values(config.dependencies || {}), {silent, verbose});
118
120
 
119
- if (!res) {
120
- return false;
121
+ if (!res) {
122
+ return false;
123
+ }
121
124
  }
122
125
 
123
126
  return true;
@@ -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