ic-mops 2.16.0 → 2.16.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.16.1
6
+
7
+ - Fix `mops bench` crashing on moc 0.15+ with the default `--gc copying`: `--copying-gc` is rejected under enhanced orthogonal persistence, which became the default persistence mode in 2.16.0. The default GC is now `incremental` (moc's default and the only collector available under EOP). Selecting a legacy collector (`copying`, `compacting`, `generational`) now implies `--legacy-persistence`, since moc only accepts them there.
8
+ - `mops build`, `mops check`, `mops generate`, and `mops check-stable` now tunnel `moc`'s exit code instead of always exiting `1`. moc exits `2` on a compiler crash (uncaught internal error) versus `1` for normal user errors (type/compile errors, stable-compat mismatch, bad args), so CI and scripts can now distinguish "file a Motoko bug" from "fix your code" via `$?`. `mops test` likewise exits `2` when a `moc` process crashes during a test run, instead of reporting it as an ordinary test failure.
9
+
5
10
  ## 2.16.0
6
11
 
7
12
  - `mops bench` now compiles benchmark canisters under **enhanced orthogonal persistence** (moc's default) instead of forcing `--legacy-persistence` — measuring the persistence mode real canisters run. Pass `--legacy-persistence` to opt back into legacy persistence.
package/bundle/cli.tgz CHANGED
Binary file
package/cli.ts CHANGED
@@ -544,9 +544,12 @@ program
544
544
  ).choices(["dfx", "pocket-ic"]),
545
545
  )
546
546
  .addOption(
547
- new Option("--gc <gc>", "Garbage collector")
547
+ new Option(
548
+ "--gc <gc>",
549
+ "Garbage collector. Under enhanced orthogonal persistence (the default) the GC is fixed to `incremental`; selecting `copying`, `compacting`, or `generational` implies `--legacy-persistence`",
550
+ )
548
551
  .choices(["copying", "compacting", "generational", "incremental"])
549
- .default("copying"),
552
+ .default("incremental"),
550
553
  )
551
554
  .addOption(
552
555
  new Option("--save", "Save benchmark results to .bench/<filename>.json"),
@@ -573,7 +576,7 @@ program
573
576
  .addOption(
574
577
  new Option(
575
578
  "--verbose",
576
- "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings",
579
+ "Print the benchmark pipeline (compiler, replica, GC, context, persistence, profile, optimization) and stream compiler/replica output, including dfx optimization warnings",
577
580
  ),
578
581
  )
579
582
  .action(async (filter, options) => {
package/commands/bench.ts CHANGED
@@ -61,7 +61,7 @@ export async function bench(
61
61
  replicaVersion: "",
62
62
  compiler: "moc",
63
63
  compilerVersion: getMocVersion(true),
64
- gc: "copying",
64
+ gc: "incremental",
65
65
  forceGc: true,
66
66
  query: false,
67
67
  legacyPersistence: false,
@@ -112,6 +112,8 @@ export async function bench(
112
112
  replicaType === "dfx" || replicaType === "dfx-pocket-ic"
113
113
  ? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
114
114
  : "none (raw moc output)";
115
+ let legacyGc = isLegacyGc(options.gc);
116
+ let effectiveLegacyPersistence = options.legacyPersistence || legacyGc;
115
117
  console.log(chalk.gray("Benchmark pipeline:"));
116
118
  console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
117
119
  console.log(
@@ -129,9 +131,16 @@ export async function bench(
129
131
  );
130
132
  console.log(
131
133
  chalk.gray(
132
- ` persistence: ${options.legacyPersistence ? "legacy" : "enhanced"}`,
134
+ ` persistence: ${effectiveLegacyPersistence ? "legacy" : "enhanced"}`,
133
135
  ),
134
136
  );
137
+ if (legacyGc && !options.legacyPersistence) {
138
+ console.log(
139
+ chalk.gray(
140
+ ` (gc '${options.gc}' only exists under legacy persistence; enabling --legacy-persistence)`,
141
+ ),
142
+ );
143
+ }
135
144
  console.log(chalk.gray(` profile: ${options.profile}`));
136
145
  console.log(chalk.gray(` optimize: ${optimize}`));
137
146
  }
@@ -279,18 +288,25 @@ function computeDiffAll(
279
288
  return diff;
280
289
  }
281
290
 
291
+ // Collectors that only exist under legacy persistence. moc 0.15+ fixes the GC to
292
+ // incremental under enhanced orthogonal persistence and rejects these there.
293
+ function isLegacyGc(gc: BenchOptions["gc"]): boolean {
294
+ return gc === "copying" || gc === "compacting" || gc === "generational";
295
+ }
296
+
282
297
  function getMocArgs(options: BenchOptions): string {
283
298
  let args = "";
284
299
 
285
- // Benchmarks compile under enhanced orthogonal persistence (moc's default
286
- // since 0.15) — the mode real canisters run. Pass `--legacy-persistence`
287
- // only when the user opts in, and only where moc supports the flag (>= 0.15;
288
- // legacy is already the default below it).
289
- if (
290
- options.legacyPersistence &&
291
- options.compilerVersion &&
292
- new SemVer(options.compilerVersion).compare("0.15.0") >= 0
293
- ) {
300
+ let mocAtLeast015 =
301
+ !!options.compilerVersion &&
302
+ new SemVer(options.compilerVersion).compare("0.15.0") >= 0;
303
+
304
+ // Legacy collectors require legacy persistence; moc < 0.15 is already legacy
305
+ // and has no --legacy-persistence flag.
306
+ let useLegacyPersistence =
307
+ options.legacyPersistence || isLegacyGc(options.gc);
308
+
309
+ if (useLegacyPersistence && mocAtLeast015) {
294
310
  args += " --legacy-persistence";
295
311
  }
296
312
 
@@ -298,7 +314,8 @@ function getMocArgs(options: BenchOptions): string {
298
314
  args += " --force-gc";
299
315
  }
300
316
 
301
- if (options.gc) {
317
+ // Under EOP the GC is fixed; only pass a collector flag where it's selectable.
318
+ if (options.gc && (useLegacyPersistence || !mocAtLeast015)) {
302
319
  args += ` --${options.gc}-gc`;
303
320
  }
304
321
 
package/commands/build.ts CHANGED
@@ -4,7 +4,7 @@ import { exists } from "fs-extra";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { lock, unlockSync } from "proper-lockfile";
7
- import { cliError } from "../error.js";
7
+ import { cliError, cliExit } from "../error.js";
8
8
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
9
9
  import {
10
10
  filterCanisters,
@@ -136,7 +136,8 @@ export async function build(
136
136
  console.error(result.stdout);
137
137
  }
138
138
  }
139
- cliError(
139
+ cliExit(
140
+ result.exitCode ?? 1,
140
141
  `Build failed for canister ${canisterName} (exit code: ${result.exitCode})`,
141
142
  );
142
143
  }
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import { cliError } from "../error.js";
6
+ import { cliError, cliExit } from "../error.js";
7
7
  import {
8
8
  getCheckLimitPendingIssue,
9
9
  prepareMigrationArgs,
@@ -261,7 +261,8 @@ export async function runStableCheck(
261
261
  if (result.stderr) {
262
262
  console.error(result.stderr);
263
263
  }
264
- cliError(
264
+ cliExit(
265
+ result.exitCode ?? 1,
265
266
  `✗ Stable compatibility check failed for canister '${canisterName}'`,
266
267
  );
267
268
  }
@@ -314,7 +315,8 @@ async function generateStableTypes(
314
315
  if (result.stderr) {
315
316
  console.error(result.stderr);
316
317
  }
317
- cliError(
318
+ cliExit(
319
+ result.exitCode ?? 1,
318
320
  `Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`,
319
321
  );
320
322
  }
package/commands/check.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
- import { cliError } from "../error.js";
4
+ import { cliError, cliExit } from "../error.js";
5
5
  import {
6
6
  getGlobalMocArgs,
7
7
  getRootDir,
@@ -208,7 +208,8 @@ async function checkCanisters(
208
208
  });
209
209
 
210
210
  if (result.exitCode !== 0) {
211
- cliError(
211
+ cliExit(
212
+ result.exitCode ?? 1,
212
213
  `✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`,
213
214
  );
214
215
  }
@@ -285,7 +286,8 @@ async function checkFiles(
285
286
  });
286
287
 
287
288
  if (result.exitCode !== 0) {
288
- cliError(
289
+ cliExit(
290
+ result.exitCode ?? 1,
289
291
  `✗ Check failed for file ${file} (exit code: ${result.exitCode})`,
290
292
  );
291
293
  }
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { execa } from "execa";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { cliError } from "../error.js";
5
+ import { cliError, cliExit } from "../error.js";
6
6
  import {
7
7
  filterCanisters,
8
8
  resolveCanisterConfigs,
@@ -104,7 +104,8 @@ export async function generateCandid(
104
104
  console.error(result.stdout);
105
105
  }
106
106
  }
107
- cliError(
107
+ cliExit(
108
+ result.exitCode ?? 1,
108
109
  `Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`,
109
110
  );
110
111
  }
@@ -159,7 +159,7 @@ export async function test(filter = "", options: Partial<TestOptions> = {}) {
159
159
  explicitReplica,
160
160
  );
161
161
  if (!passed) {
162
- process.exit(1);
162
+ process.exit(maxMocExit >= 2 ? 2 : 1);
163
163
  }
164
164
  }
165
165
  }
@@ -167,6 +167,13 @@ export async function test(filter = "", options: Partial<TestOptions> = {}) {
167
167
  let mocPath = "";
168
168
  let wasmtimePath = "";
169
169
 
170
+ let maxMocExit = 0;
171
+ const trackMocExit = (code: number | null) => {
172
+ if (code && code >= 2) {
173
+ maxMocExit = Math.max(maxMocExit, code);
174
+ }
175
+ };
176
+
170
177
  async function runAll(
171
178
  reporterName: ReporterName | undefined,
172
179
  filter = "",
@@ -197,6 +204,7 @@ export async function testWithReporter(
197
204
  signal?: AbortSignal,
198
205
  explicitReplica = false,
199
206
  ): Promise<boolean> {
207
+ maxMocExit = 0;
200
208
  let rootDir = getRootDir();
201
209
  let files: string[] = [];
202
210
  let libFiles = globSync("**/test?(s)/lib.mo", MOTOKO_GLOB_CONFIG);
@@ -328,7 +336,7 @@ export async function testWithReporter(
328
336
  }
329
337
  throw error;
330
338
  });
331
- pipeMMF(proc, mmf).then(resolve);
339
+ pipeMMF(proc, mmf, trackMocExit).then(resolve);
332
340
  }
333
341
  // build and run wasm
334
342
  else if (mode === "wasi") {
@@ -346,7 +354,7 @@ export async function testWithReporter(
346
354
  }
347
355
  throw error;
348
356
  });
349
- pipeMMF(buildProc, mmf)
357
+ pipeMMF(buildProc, mmf, trackMocExit)
350
358
  .then(async () => {
351
359
  if (mmf.failed > 0) {
352
360
  return;
@@ -413,7 +421,7 @@ export async function testWithReporter(
413
421
  throw error;
414
422
  });
415
423
 
416
- pipeMMF(buildProc, mmf)
424
+ pipeMMF(buildProc, mmf, trackMocExit)
417
425
  .then(async () => {
418
426
  if (mmf.failed > 0) {
419
427
  return;
@@ -68,18 +68,22 @@ export function pipeStderrToMMF(stderr: Readable, mmf: MMF1, dir = "") {
68
68
  });
69
69
  }
70
70
 
71
- export function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1) {
71
+ export function pipeMMF(
72
+ proc: ChildProcessWithoutNullStreams,
73
+ mmf: MMF1,
74
+ onClose?: (code: number | null) => void,
75
+ ) {
72
76
  return new Promise<void>((resolve) => {
73
77
  pipeStdoutToMMF(proc.stdout, mmf);
74
78
  pipeStderrToMMF(proc.stderr, mmf);
75
79
 
76
- // exit
77
80
  proc.on("close", (code) => {
78
81
  if (code === 0) {
79
82
  mmf.strategy !== "print" && mmf.pass();
80
83
  } else if (code !== 1) {
81
84
  mmf.fail(`unknown exit code: ${code}`);
82
85
  }
86
+ onClose?.(code ?? null);
83
87
  resolve();
84
88
  });
85
89
  });
package/dist/cli.js CHANGED
@@ -395,15 +395,15 @@ program
395
395
  .command("bench [filter]")
396
396
  .description("Run benchmarks")
397
397
  .addOption(new Option("--replica <replica>", "Which replica to use to run benchmarks (`dfx` is deprecated; prefer `pocket-ic`)").choices(["dfx", "pocket-ic"]))
398
- .addOption(new Option("--gc <gc>", "Garbage collector")
398
+ .addOption(new Option("--gc <gc>", "Garbage collector. Under enhanced orthogonal persistence (the default) the GC is fixed to `incremental`; selecting `copying`, `compacting`, or `generational` implies `--legacy-persistence`")
399
399
  .choices(["copying", "compacting", "generational", "incremental"])
400
- .default("copying"))
400
+ .default("incremental"))
401
401
  .addOption(new Option("--save", "Save benchmark results to .bench/<filename>.json"))
402
402
  .addOption(new Option("--compare", "Run benchmark and compare results with .bench/<filename>.json"))
403
403
  // .addOption(new Option('--force-gc', 'Force GC'))
404
404
  .addOption(new Option("--query", "Measure each cell in a query call (how `query` methods run on the IC: no GC). Only for benchmarks whose runner is synchronous (no inter-canister calls)"))
405
405
  .addOption(new Option("--legacy-persistence", "Compile benchmark canisters under legacy persistence instead of enhanced orthogonal persistence (the default)"))
406
- .addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings"))
406
+ .addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, context, persistence, profile, optimization) and stream compiler/replica output, including dfx optimization warnings"))
407
407
  .action(async (filter, options) => {
408
408
  checkConfigFile(true);
409
409
  await installAll({
@@ -29,7 +29,7 @@ export async function bench(filter = "", optionsArg = {}) {
29
29
  replicaVersion: "",
30
30
  compiler: "moc",
31
31
  compilerVersion: getMocVersion(true),
32
- gc: "copying",
32
+ gc: "incremental",
33
33
  forceGc: true,
34
34
  query: false,
35
35
  legacyPersistence: false,
@@ -69,12 +69,17 @@ export async function bench(filter = "", optionsArg = {}) {
69
69
  let optimize = replicaType === "dfx" || replicaType === "dfx-pocket-ic"
70
70
  ? 'dfx `optimize: "cycles"` (ic-wasm) on deploy'
71
71
  : "none (raw moc output)";
72
+ let legacyGc = isLegacyGc(options.gc);
73
+ let effectiveLegacyPersistence = options.legacyPersistence || legacyGc;
72
74
  console.log(chalk.gray("Benchmark pipeline:"));
73
75
  console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
74
76
  console.log(chalk.gray(` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`));
75
77
  console.log(chalk.gray(` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`));
76
78
  console.log(chalk.gray(` context: ${options.query ? "query" : "update"}`));
77
- console.log(chalk.gray(` persistence: ${options.legacyPersistence ? "legacy" : "enhanced"}`));
79
+ console.log(chalk.gray(` persistence: ${effectiveLegacyPersistence ? "legacy" : "enhanced"}`));
80
+ if (legacyGc && !options.legacyPersistence) {
81
+ console.log(chalk.gray(` (gc '${options.gc}' only exists under legacy persistence; enabling --legacy-persistence)`));
82
+ }
78
83
  console.log(chalk.gray(` profile: ${options.profile}`));
79
84
  console.log(chalk.gray(` optimize: ${optimize}`));
80
85
  }
@@ -190,21 +195,26 @@ function computeDiffAll(currentResults, prevResults, rows, cols) {
190
195
  }
191
196
  return diff;
192
197
  }
198
+ // Collectors that only exist under legacy persistence. moc 0.15+ fixes the GC to
199
+ // incremental under enhanced orthogonal persistence and rejects these there.
200
+ function isLegacyGc(gc) {
201
+ return gc === "copying" || gc === "compacting" || gc === "generational";
202
+ }
193
203
  function getMocArgs(options) {
194
204
  let args = "";
195
- // Benchmarks compile under enhanced orthogonal persistence (moc's default
196
- // since 0.15) the mode real canisters run. Pass `--legacy-persistence`
197
- // only when the user opts in, and only where moc supports the flag (>= 0.15;
198
- // legacy is already the default below it).
199
- if (options.legacyPersistence &&
200
- options.compilerVersion &&
201
- new SemVer(options.compilerVersion).compare("0.15.0") >= 0) {
205
+ let mocAtLeast015 = !!options.compilerVersion &&
206
+ new SemVer(options.compilerVersion).compare("0.15.0") >= 0;
207
+ // Legacy collectors require legacy persistence; moc < 0.15 is already legacy
208
+ // and has no --legacy-persistence flag.
209
+ let useLegacyPersistence = options.legacyPersistence || isLegacyGc(options.gc);
210
+ if (useLegacyPersistence && mocAtLeast015) {
202
211
  args += " --legacy-persistence";
203
212
  }
204
213
  if (options.forceGc) {
205
214
  args += " --force-gc";
206
215
  }
207
- if (options.gc) {
216
+ // Under EOP the GC is fixed; only pass a collector flag where it's selectable.
217
+ if (options.gc && (useLegacyPersistence || !mocAtLeast015)) {
208
218
  args += ` --${options.gc}-gc`;
209
219
  }
210
220
  if (options.profile === "Debug") {
@@ -4,7 +4,7 @@ import { exists } from "fs-extra";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { lock, unlockSync } from "proper-lockfile";
7
- import { cliError } from "../error.js";
7
+ import { cliError, cliExit } from "../error.js";
8
8
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
9
9
  import { filterCanisters, resolveCanisterConfigs, } from "../helpers/resolve-canisters.js";
10
10
  import { BUILD_MANAGED_FLAGS, prepareMocArgs } from "../helpers/moc-args.js";
@@ -107,7 +107,7 @@ export async function build(canisterNames, options) {
107
107
  console.error(result.stdout);
108
108
  }
109
109
  }
110
- cliError(`Build failed for canister ${canisterName} (exit code: ${result.exitCode})`);
110
+ cliExit(result.exitCode ?? 1, `Build failed for canister ${canisterName} (exit code: ${result.exitCode})`);
111
111
  }
112
112
  if (options.verbose && result.stdout && result.stdout.trim()) {
113
113
  console.log(result.stdout);
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
- import { cliError } from "../error.js";
6
+ import { cliError, cliExit } from "../error.js";
7
7
  import { getCheckLimitPendingIssue, prepareMigrationArgs, reportCheckLimitPendingIssue, } from "../helpers/migrations.js";
8
8
  import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
9
9
  import { filterCanisters, looksLikeFile, resolveCanisterConfigs, resolveSingleCanister, validateCanisterArgs, } from "../helpers/resolve-canisters.js";
@@ -146,7 +146,7 @@ export async function runStableCheck(params) {
146
146
  if (result.stderr) {
147
147
  console.error(result.stderr);
148
148
  }
149
- cliError(`✗ Stable compatibility check failed for canister '${canisterName}'`);
149
+ cliExit(result.exitCode ?? 1, `✗ Stable compatibility check failed for canister '${canisterName}'`);
150
150
  }
151
151
  console.log(chalk.green(`✓ Stable compatibility check passed for canister '${canisterName}'`));
152
152
  }
@@ -178,7 +178,7 @@ async function generateStableTypes(mocPath, moFile, outputPath, sources, globalM
178
178
  if (result.stderr) {
179
179
  console.error(result.stderr);
180
180
  }
181
- cliError(`Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`);
181
+ cliExit(result.exitCode ?? 1, `Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`);
182
182
  }
183
183
  await rm(wasmPath, { force: true });
184
184
  return outputPath;
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
- import { cliError } from "../error.js";
4
+ import { cliError, cliExit } from "../error.js";
5
5
  import { getGlobalMocArgs, getRootDir, readConfig, resolveConfigPath, } from "../mops.js";
6
6
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
7
7
  import { withFixLock } from "../helpers/fix-lock.js";
@@ -127,7 +127,7 @@ async function checkCanisters(config, canisters, options) {
127
127
  reject: false,
128
128
  });
129
129
  if (result.exitCode !== 0) {
130
- cliError(`✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`);
130
+ cliExit(result.exitCode ?? 1, `✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`);
131
131
  }
132
132
  console.log(chalk.green(`✓ ${canisterName}`));
133
133
  }
@@ -189,7 +189,7 @@ async function checkFiles(config, files, options) {
189
189
  reject: false,
190
190
  });
191
191
  if (result.exitCode !== 0) {
192
- cliError(`✗ Check failed for file ${file} (exit code: ${result.exitCode})`);
192
+ cliExit(result.exitCode ?? 1, `✗ Check failed for file ${file} (exit code: ${result.exitCode})`);
193
193
  }
194
194
  console.log(chalk.green(`✓ ${file}`));
195
195
  }
@@ -2,7 +2,7 @@ import chalk from "chalk";
2
2
  import { execa } from "execa";
3
3
  import { mkdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { cliError } from "../error.js";
5
+ import { cliError, cliExit } from "../error.js";
6
6
  import { filterCanisters, resolveCanisterConfigs, } from "../helpers/resolve-canisters.js";
7
7
  import { GENERATE_CANDID_MANAGED_FLAGS, prepareMocArgs, } from "../helpers/moc-args.js";
8
8
  import { getRootDir, readConfig, resolveConfigPath, writeConfig, } from "../mops.js";
@@ -60,7 +60,7 @@ export async function generateCandid(canisterNames, options) {
60
60
  console.error(result.stdout);
61
61
  }
62
62
  }
63
- cliError(`Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`);
63
+ cliExit(result.exitCode ?? 1, `Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`);
64
64
  }
65
65
  if (dest.configPath !== null) {
66
66
  const c = (config.canisters ??= {});
@@ -98,17 +98,24 @@ export async function test(filter = "", options = {}) {
98
98
  else {
99
99
  let passed = await runAll(options.reporter, filter, options.mode, replicaType, false, undefined, explicitReplica);
100
100
  if (!passed) {
101
- process.exit(1);
101
+ process.exit(maxMocExit >= 2 ? 2 : 1);
102
102
  }
103
103
  }
104
104
  }
105
105
  let mocPath = "";
106
106
  let wasmtimePath = "";
107
+ let maxMocExit = 0;
108
+ const trackMocExit = (code) => {
109
+ if (code && code >= 2) {
110
+ maxMocExit = Math.max(maxMocExit, code);
111
+ }
112
+ };
107
113
  async function runAll(reporterName, filter = "", mode = "interpreter", replicaType, watch = false, signal, explicitReplica = false) {
108
114
  let done = await testWithReporter(reporterName, filter, mode, replicaType, watch, signal, explicitReplica);
109
115
  return done;
110
116
  }
111
117
  export async function testWithReporter(reporterName, filter = "", defaultMode = "interpreter", replicaType, watch = false, signal, explicitReplica = false) {
118
+ maxMocExit = 0;
112
119
  let rootDir = getRootDir();
113
120
  let files = [];
114
121
  let libFiles = globSync("**/test?(s)/lib.mo", MOTOKO_GLOB_CONFIG);
@@ -217,7 +224,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
217
224
  }
218
225
  throw error;
219
226
  });
220
- pipeMMF(proc, mmf).then(resolve);
227
+ pipeMMF(proc, mmf, trackMocExit).then(resolve);
221
228
  }
222
229
  // build and run wasm
223
230
  else if (mode === "wasi") {
@@ -230,7 +237,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
230
237
  }
231
238
  throw error;
232
239
  });
233
- pipeMMF(buildProc, mmf)
240
+ pipeMMF(buildProc, mmf, trackMocExit)
234
241
  .then(async () => {
235
242
  if (mmf.failed > 0) {
236
243
  return;
@@ -288,7 +295,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
288
295
  }
289
296
  throw error;
290
297
  });
291
- pipeMMF(buildProc, mmf)
298
+ pipeMMF(buildProc, mmf, trackMocExit)
292
299
  .then(async () => {
293
300
  if (mmf.failed > 0) {
294
301
  return;
@@ -4,4 +4,4 @@ import { MMF1 } from "./mmf1.js";
4
4
  export declare function absToRel(p: string): string;
5
5
  export declare function pipeStdoutToMMF(stdout: Readable, mmf: MMF1): void;
6
6
  export declare function pipeStderrToMMF(stderr: Readable, mmf: MMF1, dir?: string): void;
7
- export declare function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1): Promise<void>;
7
+ export declare function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1, onClose?: (code: number | null) => void): Promise<void>;
@@ -49,11 +49,10 @@ export function pipeStderrToMMF(stderr, mmf, dir = "") {
49
49
  mmf.fail(text);
50
50
  });
51
51
  }
52
- export function pipeMMF(proc, mmf) {
52
+ export function pipeMMF(proc, mmf, onClose) {
53
53
  return new Promise((resolve) => {
54
54
  pipeStdoutToMMF(proc.stdout, mmf);
55
55
  pipeStderrToMMF(proc.stderr, mmf);
56
- // exit
57
56
  proc.on("close", (code) => {
58
57
  if (code === 0) {
59
58
  mmf.strategy !== "print" && mmf.pass();
@@ -61,6 +60,7 @@ export function pipeMMF(proc, mmf) {
61
60
  else if (code !== 1) {
62
61
  mmf.fail(`unknown exit code: ${code}`);
63
62
  }
63
+ onClose?.(code ?? null);
64
64
  resolve();
65
65
  });
66
66
  });
package/dist/error.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export declare function cliError(...args: unknown[]): never;
2
+ export declare function cliExit(code: number, ...args: unknown[]): never;
package/dist/error.js CHANGED
@@ -3,3 +3,7 @@ export function cliError(...args) {
3
3
  console.error(chalk.red(...args));
4
4
  process.exit(1);
5
5
  }
6
+ export function cliExit(code, ...args) {
7
+ console.error(chalk.red(...args));
8
+ process.exit(code || 1);
9
+ }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.16.0",
3
+ "version": "2.16.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { rmSync } from "node:fs";
3
+ import path from "path";
4
+ import { cli } from "./helpers";
5
+ // Pin moc 1.3.0 (≥ 0.15) to exercise the EOP path — this repo's own mops.toml
6
+ // uses moc 0.14.14, so the default bench run is never EOP-tested here.
7
+ describe("bench", () => {
8
+ jest.setTimeout(180_000);
9
+ test("runs under EOP with the default gc", async () => {
10
+ const cwd = path.join(import.meta.dirname, "bench");
11
+ try {
12
+ const result = await cli(["bench"], { cwd });
13
+ expect(result.stderr).not.toContain("--copying-gc is not supported");
14
+ expect(result.stderr).not.toContain("Invalid compiler flag combination");
15
+ expect(result.exitCode).toBe(0);
16
+ }
17
+ finally {
18
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
19
+ }
20
+ });
21
+ });
package/error.ts CHANGED
@@ -4,3 +4,8 @@ export function cliError(...args: unknown[]): never {
4
4
  console.error(chalk.red(...args));
5
5
  process.exit(1);
6
6
  }
7
+
8
+ export function cliExit(code: number, ...args: unknown[]): never {
9
+ console.error(chalk.red(...args));
10
+ process.exit(code || 1);
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.16.0",
3
+ "version": "2.16.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -0,0 +1,27 @@
1
+ // Exercises the bench pipeline under EOP; the template only needs
2
+ // init() -> { getVersion; getSchema; runCell }, so no mo:bench dep.
3
+ module {
4
+ type Schema = {
5
+ name : Text;
6
+ description : Text;
7
+ rows : [Text];
8
+ cols : [Text];
9
+ };
10
+
11
+ class Bench(schema : Schema, run : (Nat, Nat) -> ()) {
12
+ public func getVersion() : Nat = 1;
13
+ public func getSchema() : Schema = schema;
14
+ public let runCell = run;
15
+ };
16
+
17
+ public func init() : Bench {
18
+ let schema : Schema = {
19
+ name = "Sanity";
20
+ description = "Trivial bench to exercise the mops bench pipeline under EOP";
21
+ rows = ["a"];
22
+ cols = ["1"];
23
+ };
24
+ func run(_ri : Nat, _ci : Nat) {};
25
+ Bench(schema, run);
26
+ };
27
+ };
@@ -0,0 +1,6 @@
1
+ [dependencies]
2
+ core = "2.0.0"
3
+
4
+ [toolchain]
5
+ moc = "1.3.0"
6
+ pocket-ic = "12.0.0"
@@ -0,0 +1,22 @@
1
+ import { describe, expect, jest, test } from "@jest/globals";
2
+ import { rmSync } from "node:fs";
3
+ import path from "path";
4
+ import { cli } from "./helpers";
5
+
6
+ // Pin moc 1.3.0 (≥ 0.15) to exercise the EOP path — this repo's own mops.toml
7
+ // uses moc 0.14.14, so the default bench run is never EOP-tested here.
8
+ describe("bench", () => {
9
+ jest.setTimeout(180_000);
10
+
11
+ test("runs under EOP with the default gc", async () => {
12
+ const cwd = path.join(import.meta.dirname, "bench");
13
+ try {
14
+ const result = await cli(["bench"], { cwd });
15
+ expect(result.stderr).not.toContain("--copying-gc is not supported");
16
+ expect(result.stderr).not.toContain("Invalid compiler flag combination");
17
+ expect(result.exitCode).toBe(0);
18
+ } finally {
19
+ rmSync(path.join(cwd, ".mops"), { recursive: true, force: true });
20
+ }
21
+ });
22
+ });