ic-mops 0.45.4-pre.0 → 1.0.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 (76) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +1 -1
  3. package/api/actors.ts +1 -1
  4. package/bundle/cli.tgz +0 -0
  5. package/cache.ts +15 -10
  6. package/cli.ts +27 -27
  7. package/commands/add.ts +4 -0
  8. package/commands/bench/bench-canister.mo +34 -8
  9. package/commands/bench-replica.ts +11 -6
  10. package/commands/bench.ts +29 -3
  11. package/commands/publish.ts +1 -1
  12. package/commands/replica.ts +239 -0
  13. package/commands/sources.ts +2 -3
  14. package/commands/test/mmf1.ts +10 -6
  15. package/commands/test/reporters/compact-reporter.ts +2 -1
  16. package/commands/test/reporters/files-reporter.ts +3 -2
  17. package/commands/test/reporters/reporter.ts +2 -1
  18. package/commands/test/reporters/silent-reporter.ts +2 -1
  19. package/commands/test/reporters/verbose-reporter.ts +14 -4
  20. package/commands/test/test.ts +214 -81
  21. package/commands/user.ts +71 -1
  22. package/declarations/bench/bench.did +6 -2
  23. package/declarations/bench/bench.did.d.ts +6 -2
  24. package/declarations/bench/bench.did.js +6 -2
  25. package/dist/cache.js +14 -10
  26. package/dist/cli.d.ts +1 -0
  27. package/dist/cli.js +23 -27
  28. package/dist/commands/add.js +3 -0
  29. package/dist/commands/bench/bench-canister.mo +34 -8
  30. package/dist/commands/bench-replica.d.ts +2 -1
  31. package/dist/commands/bench-replica.js +10 -6
  32. package/dist/commands/bench.js +27 -3
  33. package/dist/commands/publish.js +1 -1
  34. package/dist/commands/replica.d.ts +59 -0
  35. package/dist/commands/replica.js +195 -0
  36. package/dist/commands/sources.d.ts +2 -2
  37. package/dist/commands/sources.js +2 -3
  38. package/dist/commands/test/mmf1.d.ts +2 -2
  39. package/dist/commands/test/mmf1.js +9 -5
  40. package/dist/commands/test/reporters/compact-reporter.d.ts +2 -1
  41. package/dist/commands/test/reporters/compact-reporter.js +1 -1
  42. package/dist/commands/test/reporters/files-reporter.d.ts +2 -1
  43. package/dist/commands/test/reporters/files-reporter.js +2 -2
  44. package/dist/commands/test/reporters/reporter.d.ts +2 -1
  45. package/dist/commands/test/reporters/silent-reporter.d.ts +2 -1
  46. package/dist/commands/test/reporters/silent-reporter.js +1 -1
  47. package/dist/commands/test/reporters/verbose-reporter.d.ts +3 -1
  48. package/dist/commands/test/reporters/verbose-reporter.js +12 -4
  49. package/dist/commands/test/test.d.ts +10 -8
  50. package/dist/commands/test/test.js +171 -78
  51. package/dist/commands/user.d.ts +6 -0
  52. package/dist/commands/user.js +59 -1
  53. package/dist/declarations/bench/bench.did +6 -2
  54. package/dist/declarations/bench/bench.did.d.ts +6 -2
  55. package/dist/declarations/bench/bench.did.js +6 -2
  56. package/dist/mops.d.ts +1 -1
  57. package/dist/mops.js +5 -29
  58. package/dist/package.json +20 -21
  59. package/dist/release-cli.js +5 -2
  60. package/dist/resolve-packages.d.ts +2 -2
  61. package/dist/resolve-packages.js +29 -7
  62. package/dist/types.d.ts +1 -0
  63. package/dist/vessel.js +1 -1
  64. package/mops.ts +5 -32
  65. package/package.json +24 -24
  66. package/release-cli.ts +7 -2
  67. package/resolve-packages.ts +39 -8
  68. package/types.ts +3 -1
  69. package/vessel.ts +1 -1
  70. package/DEVELOPMENT.md +0 -25
  71. package/commands/import-identity.ts +0 -62
  72. package/commands/whoami.ts +0 -12
  73. package/dist/commands/import-identity.d.ts +0 -5
  74. package/dist/commands/import-identity.js +0 -51
  75. package/dist/commands/whoami.d.ts +0 -1
  76. package/dist/commands/whoami.js +0 -11
@@ -0,0 +1,239 @@
1
+ import process from 'node:process';
2
+ import {ChildProcessWithoutNullStreams, execSync, spawn} from 'node:child_process';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import {PassThrough} from 'node:stream';
6
+
7
+ import {IDL} from '@dfinity/candid';
8
+ import {Actor, HttpAgent} from '@dfinity/agent';
9
+ import {PocketIc, PocketIcServer} from 'pic-ic';
10
+
11
+ import {readConfig} from '../mops.js';
12
+ import {toolchain} from './toolchain/index.js';
13
+
14
+ type StartOptions = {
15
+ type ?: 'dfx' | 'pocket-ic';
16
+ dir ?: string;
17
+ verbose ?: boolean;
18
+ silent ?: boolean;
19
+ };
20
+
21
+ export class Replica {
22
+ type : 'dfx' | 'pocket-ic' = 'dfx';
23
+ verbose = false;
24
+ canisters : Record<string, {cwd : string; canisterId : string; actor : any; stream : PassThrough;}> = {};
25
+ pocketIcServer ?: PocketIcServer;
26
+ pocketIc ?: PocketIc;
27
+ dfxProcess ?: ChildProcessWithoutNullStreams;
28
+ dir : string = ''; // absolute path (/.../.mops/.test/)
29
+ ttl = 60;
30
+
31
+ async start({type, dir, verbose, silent} : StartOptions = {}) {
32
+ this.type = type ?? this.type;
33
+ this.verbose = verbose ?? this.verbose;
34
+ this.dir = dir ?? this.dir;
35
+
36
+ silent || console.log(`Starting ${this.type} replica...`);
37
+
38
+ if (this.type == 'dfx') {
39
+ fs.mkdirSync(this.dir, {recursive: true});
40
+ fs.writeFileSync(path.join(this.dir, 'dfx.json'), JSON.stringify(this.dfxJson(''), null, 2));
41
+ fs.writeFileSync(path.join(this.dir, 'canister.did'), 'service : { runTests: () -> (); }');
42
+
43
+ await this.stop();
44
+
45
+ this.dfxProcess = spawn('dfx', ['start', '--clean', '--artificial-delay', '0', (this.verbose ? '' : '-qqqq')].filter(x => x), {cwd: this.dir});
46
+
47
+ // process canister logs
48
+ this._attachCanisterLogHandler(this.dfxProcess);
49
+
50
+ this.dfxProcess.stdout.on('data', (data) => {
51
+ console.log('DFX:', data.toString());
52
+ });
53
+
54
+ // await for dfx to start
55
+ let ok = false;
56
+ while (!ok) {
57
+ await fetch('http://127.0.0.1:4945/api/v2/status')
58
+ .then(res => {
59
+ if (res.status === 200) {
60
+ ok = true;
61
+ }
62
+ })
63
+ .catch(() => {})
64
+ .finally(() => {
65
+ return new Promise(resolve => setTimeout(resolve, 1000));
66
+ });
67
+ }
68
+ }
69
+ else {
70
+ let pocketIcBin = await toolchain.bin('pocket-ic');
71
+
72
+ // eslint-disable-next-line
73
+ let config = readConfig();
74
+ if (config.toolchain?.['pocket-ic'] !== '4.0.0') {
75
+ console.error('Current Mops CLI only supports pocket-ic 4.0.0');
76
+ process.exit(1);
77
+ }
78
+
79
+ this.pocketIcServer = await PocketIcServer.start({
80
+ showRuntimeLogs: false,
81
+ showCanisterLogs: false,
82
+ binPath: pocketIcBin,
83
+ ttl: this.ttl,
84
+ });
85
+ this.pocketIc = await PocketIc.create(this.pocketIcServer.getUrl());
86
+
87
+ // process canister logs
88
+ this._attachCanisterLogHandler(this.pocketIcServer.serverProcess as ChildProcessWithoutNullStreams);
89
+ }
90
+ }
91
+
92
+ _attachCanisterLogHandler(proc : ChildProcessWithoutNullStreams) {
93
+ let curData = '';
94
+ proc.stderr.on('data', (data) => {
95
+ curData = curData + data.toString();
96
+
97
+ if (curData.includes('\n')) {
98
+ let chunk = curData.split('\n').slice(0, -1).join('\n');
99
+ let matches = [...chunk.matchAll(/\[Canister ([a-z0-9-]+)\] (.*)/g)];
100
+
101
+ for (let match of matches) {
102
+ let [, canisterId, msg] = match;
103
+ let stream = this.getCanisterStream(canisterId || '');
104
+ if (stream) {
105
+ stream.write(msg);
106
+ }
107
+ }
108
+
109
+ if (matches.length) {
110
+ curData = curData.split('\n').slice(-1).join('\n');
111
+ }
112
+ }
113
+ });
114
+ }
115
+
116
+ async stop(sigint = false) {
117
+ if (this.type == 'dfx') {
118
+ this.dfxProcess?.kill();
119
+ // execSync('dfx stop' + (this.verbose ? '' : ' -qqqq'), {cwd: this.dir, timeout: 10_000, stdio: ['pipe', this.verbose ? 'inherit' : 'ignore', 'pipe']});
120
+ }
121
+ else if (this.pocketIc && this.pocketIcServer) {
122
+ if (!sigint) {
123
+ await this.pocketIc.tearDown(); // error 'fetch failed' if run on SIGINT
124
+ }
125
+ await this.pocketIcServer.stop();
126
+ }
127
+ }
128
+
129
+ async deploy(name : string, wasm : string, idlFactory : IDL.InterfaceFactory, cwd : string = process.cwd()) {
130
+ if (this.type === 'dfx') {
131
+ // prepare dfx.json for current canister
132
+ let dfxJson = path.join(this.dir, 'dfx.json');
133
+
134
+ let oldDfxJsonData;
135
+ if (fs.existsSync(dfxJson)) {
136
+ oldDfxJsonData = JSON.parse(fs.readFileSync(dfxJson).toString());
137
+ }
138
+ let newDfxJsonData = this.dfxJson(name, name + '.wasm');
139
+
140
+ if (oldDfxJsonData.canisters) {
141
+ newDfxJsonData.canisters = Object.assign(oldDfxJsonData.canisters, newDfxJsonData.canisters);
142
+ }
143
+
144
+ fs.mkdirSync(this.dir, {recursive: true});
145
+ fs.writeFileSync(dfxJson, JSON.stringify(newDfxJsonData, null, 2));
146
+
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']});
149
+
150
+ let canisterId = execSync(`dfx canister id ${name}`, {cwd: this.dir}).toString().trim();
151
+
152
+ let actor = Actor.createActor(idlFactory, {
153
+ agent: await HttpAgent.create({
154
+ host: 'http://127.0.0.1:4945',
155
+ shouldFetchRootKey: true,
156
+ }),
157
+ canisterId,
158
+ });
159
+
160
+ this.canisters[name] = {
161
+ cwd,
162
+ canisterId,
163
+ actor,
164
+ stream: new PassThrough(),
165
+ };
166
+ }
167
+ else if (this.pocketIc) {
168
+ // let {canisterId, actor} = await this.pocketIc.setupCanister(idlFactory, wasm);
169
+ let {canisterId, actor} = await this.pocketIc.setupCanister({
170
+ idlFactory,
171
+ wasm,
172
+ });
173
+ await this.pocketIc.addCycles(canisterId, 1_000_000_000_000);
174
+ this.canisters[name] = {
175
+ cwd,
176
+ canisterId: canisterId.toText(),
177
+ actor,
178
+ stream: new PassThrough(),
179
+ };
180
+ }
181
+
182
+ if (!this.canisters[name]) {
183
+ throw new Error(`Canister ${name} not found`);
184
+ }
185
+
186
+ return this.canisters[name];
187
+ }
188
+
189
+ getActor(name : string) : unknown {
190
+ if (!this.canisters[name]) {
191
+ throw new Error(`Canister ${name} not found`);
192
+ }
193
+ return this.canisters[name]?.actor;
194
+ }
195
+
196
+ getCanister(name : string) {
197
+ return this.canisters[name];
198
+ }
199
+
200
+ getCanisterId(name : string) : string {
201
+ return this.canisters[name]?.canisterId || '';
202
+ }
203
+
204
+ getCanisterStream(canisterId : string) : PassThrough | null {
205
+ for (let canister of Object.values(this.canisters)) {
206
+ if (canister.canisterId === canisterId) {
207
+ return canister.stream;
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+
213
+ dfxJson(canisterName : string, wasmPath = 'canister.wasm', didPath = 'canister.did') {
214
+ let canisters : Record<string, any> = {};
215
+ if (canisterName) {
216
+ canisters[canisterName] = {
217
+ type: 'custom',
218
+ wasm: wasmPath,
219
+ candid: didPath,
220
+ };
221
+ }
222
+
223
+ return {
224
+ version: 1,
225
+ canisters,
226
+ defaults: {
227
+ build: {
228
+ packtool: 'mops sources',
229
+ },
230
+ },
231
+ networks: {
232
+ local: {
233
+ type: 'ephemeral',
234
+ bind: '127.0.0.1:4945',
235
+ },
236
+ },
237
+ };
238
+ }
239
+ }
@@ -4,13 +4,12 @@ import fs from 'node:fs';
4
4
  import {checkConfigFile, formatDir, formatGithubDir, getDependencyType, readConfig} from '../mops.js';
5
5
  import {resolvePackages} from '../resolve-packages.js';
6
6
 
7
- // TODO: resolve conflicts
8
- export async function sources({verbose = false, cwd = process.cwd()} = {}) {
7
+ export async function sources({conflicts = 'ignore' as 'warning' | 'error' | 'ignore', cwd = process.cwd()} = {}) {
9
8
  if (!checkConfigFile()) {
10
9
  return [];
11
10
  }
12
11
 
13
- let resolvedPackages = await resolvePackages({verbose});
12
+ let resolvedPackages = await resolvePackages({conflicts});
14
13
 
15
14
  // sources
16
15
  return Object.entries(resolvedPackages).map(([name, version]) => {
@@ -15,7 +15,7 @@ export class MMF1 {
15
15
  failed = 0;
16
16
  passed = 0;
17
17
  skipped = 0;
18
- srategy : Strategy;
18
+ strategy : Strategy;
19
19
  output : {
20
20
  type : MessageType;
21
21
  message : string;
@@ -27,19 +27,19 @@ export class MMF1 {
27
27
  // or <file> › <suite> › <test> › <nested-test>...
28
28
  passedNamesFlat : string[] = [];
29
29
 
30
- constructor(srategy : Strategy, file : string) {
31
- this.srategy = srategy;
30
+ constructor(strategy : Strategy, file : string) {
31
+ this.strategy = strategy;
32
32
  this.file = file;
33
33
  }
34
34
 
35
35
  _log(type : MessageType, ...args : string[]) {
36
- if (this.srategy === 'store') {
36
+ if (this.strategy === 'store') {
37
37
  this.output.push({
38
38
  type,
39
39
  message: args.join(' '),
40
40
  });
41
41
  }
42
- else if (this.srategy === 'print') {
42
+ else if (this.strategy === 'print') {
43
43
  console.log(...args);
44
44
  }
45
45
  }
@@ -83,7 +83,11 @@ export class MMF1 {
83
83
  }
84
84
 
85
85
  _testEnd(name : string) {
86
- if (name !== this.stack.pop()) {
86
+ let last = this.stack.pop();
87
+ if (name !== last) {
88
+ console.error(`Expected test name "${last}" but got "${name}"`);
89
+ console.error(`Stack: ${this.stack.join(' › ')}`);
90
+ console.error(`File: ${this.file}`);
87
91
  throw 'mmf1._testEnd: start and end test mismatch';
88
92
  }
89
93
  this._status(name, 'pass');
@@ -3,6 +3,7 @@ import logUpdate from 'log-update';
3
3
  import {absToRel} from '../utils.js';
4
4
  import {MMF1} from '../mmf1.js';
5
5
  import {Reporter} from './reporter.js';
6
+ import {TestMode} from '../../../types.js';
6
7
 
7
8
  export class CompactReporter implements Reporter {
8
9
  passed = 0;
@@ -23,7 +24,7 @@ export class CompactReporter implements Reporter {
23
24
  this.#startTimer();
24
25
  }
25
26
 
26
- addRun(file : string, mmf : MMF1, state : Promise<void>, _wasiMode : boolean) {
27
+ addRun(file : string, mmf : MMF1, state : Promise<void>, _mode : TestMode) {
27
28
  this.#runningFiles.add(file);
28
29
  this.#log();
29
30
 
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import {absToRel} from '../utils.js';
3
3
  import {MMF1} from '../mmf1.js';
4
4
  import {Reporter} from './reporter.js';
5
+ import {TestMode} from '../../../types.js';
5
6
 
6
7
  export class FilesReporter implements Reporter {
7
8
  passed = 0;
@@ -15,7 +16,7 @@ export class FilesReporter implements Reporter {
15
16
  console.log('='.repeat(50));
16
17
  }
17
18
 
18
- addRun(file : string, mmf : MMF1, state : Promise<void>, wasiMode : boolean) {
19
+ addRun(file : string, mmf : MMF1, state : Promise<void>, mode : TestMode) {
19
20
  state.then(() => {
20
21
  this.passed += Number(mmf.failed === 0);
21
22
  this.failed += Number(mmf.failed !== 0);
@@ -27,7 +28,7 @@ export class FilesReporter implements Reporter {
27
28
  console.log('-'.repeat(50));
28
29
  }
29
30
  else {
30
- console.log(`${chalk.green('✓')} ${absToRel(file)} ${wasiMode ? chalk.gray('(wasi)') : ''}`);
31
+ console.log(`${chalk.green('✓')} ${absToRel(file)} ${mode === 'interpreter' ? '' : chalk.gray(`(${mode})`)}`);
31
32
  }
32
33
  });
33
34
  }
@@ -1,7 +1,8 @@
1
+ import {TestMode} from '../../../types.js';
1
2
  import {MMF1} from '../mmf1.js';
2
3
 
3
4
  export interface Reporter {
4
5
  addFiles(files : string[]) : void;
5
- addRun(file : string, mmf : MMF1, state : Promise<void>, wasiMode : boolean) : void;
6
+ addRun(file : string, mmf : MMF1, state : Promise<void>, mode : TestMode, progressive ?: boolean) : void;
6
7
  done() : boolean;
7
8
  }
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import {absToRel} from '../utils.js';
3
3
  import {MMF1} from '../mmf1.js';
4
4
  import {Reporter} from './reporter.js';
5
+ import {TestMode} from '../../../types.js';
5
6
 
6
7
  export class SilentReporter implements Reporter {
7
8
  passed = 0;
@@ -13,7 +14,7 @@ export class SilentReporter implements Reporter {
13
14
 
14
15
  addFiles(_files : string[]) {}
15
16
 
16
- addRun(file : string, mmf : MMF1, state : Promise<void>, _wasiMode : boolean) {
17
+ addRun(file : string, mmf : MMF1, state : Promise<void>, _mode : TestMode) {
17
18
  state.then(() => {
18
19
  this.passed += mmf.passed;
19
20
  this.failed += mmf.failed;
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import {absToRel} from '../utils.js';
3
3
  import {MMF1} from '../mmf1.js';
4
4
  import {Reporter} from './reporter.js';
5
+ import {TestMode} from '../../../types.js';
5
6
 
6
7
  export class VerboseReporter implements Reporter {
7
8
  passed = 0;
@@ -19,7 +20,11 @@ export class VerboseReporter implements Reporter {
19
20
  console.log('='.repeat(50));
20
21
  }
21
22
 
22
- addRun(file : string, mmf : MMF1, state : Promise<void>, wasiMode : boolean) {
23
+ addRun(file : string, mmf : MMF1, state : Promise<void>, mode : TestMode, progressive = false) {
24
+ if (progressive) {
25
+ this._printStart(file, mode);
26
+ }
27
+
23
28
  state.then(() => {
24
29
  this.passed += mmf.passed;
25
30
  this.failed += mmf.failed;
@@ -28,9 +33,9 @@ export class VerboseReporter implements Reporter {
28
33
  if (mmf.passed === 0 && mmf.failed === 0) {
29
34
  this.passed++;
30
35
  }
31
-
32
- this.#curFileIndex++ && console.log('-'.repeat(50));
33
- console.log(`Running ${chalk.gray(absToRel(file))} ${wasiMode ? chalk.gray('(wasi)') : ''}`);
36
+ if (!progressive) {
37
+ this._printStart(file, mode);
38
+ }
34
39
  mmf.flush();
35
40
  });
36
41
  }
@@ -52,4 +57,9 @@ export class VerboseReporter implements Reporter {
52
57
 
53
58
  return this.failed === 0;
54
59
  }
60
+
61
+ _printStart(file : string, mode : TestMode) {
62
+ this.#curFileIndex++ && console.log('-'.repeat(50));
63
+ console.log(`Running ${chalk.gray(absToRel(file))} ${mode === 'interpreter' ? '' : chalk.gray(`(${mode})`)}`);
64
+ }
55
65
  }