ic-mops 0.25.3 → 0.26.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.
package/README.md CHANGED
@@ -17,7 +17,20 @@ npm i -g ic-mops
17
17
 
18
18
  ## Install Packages
19
19
 
20
- ### 1. Initialize
20
+ ### 1. Configure dfx.json
21
+ Add `mops` as a packtool to your `dfx.json`
22
+
23
+ ```json
24
+ {
25
+ "defaults": {
26
+ "build": {
27
+ "packtool": "mops sources"
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### 2. Initialize
21
34
  Run this command in the root directory of your project (where is `dfx.json` placed)
22
35
 
23
36
  If there are Vessel config files, mops will migrate packages from `vessel.dhall` to `mops.toml`
@@ -26,7 +39,7 @@ If there are Vessel config files, mops will migrate packages from `vessel.dhall`
26
39
  mops init
27
40
  ```
28
41
 
29
- ### 2. Install Motoko Packages
42
+ ### 3. Install Motoko Packages
30
43
  Use `mops add <package_name>` to install a specific package and save it to `mops.toml`
31
44
 
32
45
  ```
@@ -53,7 +66,7 @@ Use `mops install` to install all packages specified in `mops.toml`
53
66
  mops install
54
67
  ```
55
68
 
56
- ### 3. Import Package
69
+ ### 4. Import Package
57
70
  Now you can import installed packages in your Motoko code
58
71
 
59
72
  ```motoko
package/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from 'node:fs';
4
- import {program, Argument} from 'commander';
4
+ import {program, Argument, Option} from 'commander';
5
5
  import chalk from 'chalk';
6
6
  import {Principal} from '@dfinity/principal';
7
7
 
@@ -100,6 +100,7 @@ program
100
100
  .command('publish')
101
101
  .description('Publish package to the mops registry')
102
102
  .option('--no-docs', 'Do not generate docs')
103
+ .option('--no-test', 'Do not run tests')
103
104
  .action(async (options) => {
104
105
  if (!checkConfigFile()) {
105
106
  process.exit(1);
@@ -188,14 +189,10 @@ program
188
189
  program
189
190
  .command('test [filter]')
190
191
  .description('Run tests')
191
- .option('-r, --reporter <reporter>', 'Choose reporter: verbose, compact, files')
192
+ .addOption(new Option('-r, --reporter <reporter>', 'Test reporter').choices(['verbose', 'compact', 'files', 'silent']).default('verbose'))
193
+ .addOption(new Option('--mode <mode>', 'Test mode').choices(['interpreter', 'wasi']).default('interpreter'))
192
194
  .option('-w, --watch', 'Enable watch mode')
193
- .option('--mode <mode>', 'Test mode: \'interpreter\' or \'wasi\' (default \'interpreter\'')
194
195
  .action(async (filter, options) => {
195
- if (options.mode && !['interpreter', 'wasi'].includes(options.mode)) {
196
- console.log(`Unknown --mode value '${options.mode}'. Allowed: interpreter, wasi`);
197
- process.exit(1);
198
- }
199
196
  await test(filter, options);
200
197
  });
201
198
 
@@ -10,8 +10,10 @@ import {parallel} from '../parallel.js';
10
10
  import {docs} from './docs.js';
11
11
  import {DependencyV2, PackageConfigV2} from '../declarations/main/main.did.js';
12
12
  import {Dependency} from '../types.js';
13
+ import {testWithReporter} from './test/test.js';
14
+ import {SilentReporter} from './test/reporters/silent-reporter.js';
13
15
 
14
- export async function publish({noDocs = false} = {}) {
16
+ export async function publish(options: {docs?: boolean, test?: boolean} = {}) {
15
17
  if (!checkConfigFile()) {
16
18
  return;
17
19
  }
@@ -19,24 +21,26 @@ export async function publish({noDocs = false} = {}) {
19
21
  let rootDir = getRootDir();
20
22
  let config = readConfig();
21
23
 
24
+ console.log(`Publishing ${config.package?.name}@${config.package?.version}`);
25
+
22
26
  // validate
23
27
  for (let key of Object.keys(config)) {
24
28
  if (!['package', 'dependencies', 'dev-dependencies', 'scripts'].includes(key)) {
25
29
  console.log(chalk.red('Error: ') + `Unknown config section [${key}]`);
26
- return;
30
+ process.exit(1);
27
31
  }
28
32
  }
29
33
 
30
34
  // required fields
31
35
  if (!config.package) {
32
36
  console.log(chalk.red('Error: ') + 'Please specify [package] section in your mops.toml');
33
- return;
37
+ process.exit(1);
34
38
  }
35
39
  for (let key of ['name', 'version']) {
36
40
  // @ts-ignore
37
41
  if (!config.package[key]) {
38
42
  console.log(chalk.red('Error: ') + `Please specify "${key}" in [config] section in your mops.toml`);
39
- return;
43
+ process.exit(1);
40
44
  }
41
45
  }
42
46
 
@@ -74,7 +78,7 @@ export async function publish({noDocs = false} = {}) {
74
78
  for (let key of Object.keys(config.package)) {
75
79
  if (!packageKeys.includes(key)) {
76
80
  console.log(chalk.red('Error: ') + `Unknown config key 'package.${key}'`);
77
- return;
81
+ process.exit(1);
78
82
  }
79
83
  }
80
84
 
@@ -100,20 +104,20 @@ export async function publish({noDocs = false} = {}) {
100
104
  // @ts-ignore
101
105
  if (config.package[key] && config.package[key].length > max) {
102
106
  console.log(chalk.red('Error: ') + `package.${key} value max length is ${max}`);
103
- return;
107
+ process.exit(1);
104
108
  }
105
109
  }
106
110
 
107
111
  if (config.dependencies) {
108
112
  if (Object.keys(config.dependencies).length > 100) {
109
113
  console.log(chalk.red('Error: ') + 'max dependencies is 100');
110
- return;
114
+ process.exit(1);
111
115
  }
112
116
 
113
117
  for (let dep of Object.values(config.dependencies)) {
114
118
  if (dep.path) {
115
119
  console.log(chalk.red('Error: ') + 'you can\'t publish packages with local dependencies');
116
- return;
120
+ process.exit(1);
117
121
  }
118
122
  delete dep.path;
119
123
  }
@@ -122,13 +126,13 @@ export async function publish({noDocs = false} = {}) {
122
126
  if (config['dev-dependencies']) {
123
127
  if (Object.keys(config['dev-dependencies']).length > 100) {
124
128
  console.log(chalk.red('Error: ') + 'max dev-dependencies is 100');
125
- return;
129
+ process.exit(1);
126
130
  }
127
131
 
128
132
  for (let dep of Object.values(config['dev-dependencies'])) {
129
133
  if (dep.path) {
130
134
  console.log(chalk.red('Error: ') + 'you can\'t publish packages with local dev-dependencies');
131
- return;
135
+ process.exit(1);
132
136
  }
133
137
  delete dep.path;
134
138
  }
@@ -197,7 +201,8 @@ export async function publish({noDocs = false} = {}) {
197
201
 
198
202
  // generate docs
199
203
  let docsFile = path.join(rootDir, '.mops/.docs/docs.tgz');
200
- if (!noDocs) {
204
+ if (options.docs) {
205
+ console.log('Generating documentation...');
201
206
  await docs({silent: true});
202
207
  if (fs.existsSync(docsFile)) {
203
208
  files.unshift(docsFile);
@@ -207,18 +212,29 @@ export async function publish({noDocs = false} = {}) {
207
212
  // check required files
208
213
  if (!files.includes('mops.toml')) {
209
214
  console.log(chalk.red('Error: ') + ' please add mops.toml file');
210
- return;
215
+ process.exit(1);
211
216
  }
212
217
  if (!files.includes('README.md')) {
213
218
  console.log(chalk.red('Error: ') + ' please add README.md file');
214
- return;
219
+ process.exit(1);
215
220
  }
216
221
 
217
222
  // check allowed exts
218
223
  for (let file of files) {
219
224
  if (!minimatch(file, '**/*.{mo,did,md,toml}') && !file.toLowerCase().endsWith('license') && !file.toLowerCase().endsWith('notice') && file !== docsFile) {
220
225
  console.log(chalk.red('Error: ') + `file ${file} has unsupported extension. Allowed: .mo, .did, .md, .toml`);
221
- return;
226
+ process.exit(1);
227
+ }
228
+ }
229
+
230
+ // test
231
+ let reporter = new SilentReporter;
232
+ if (options.test) {
233
+ console.log('Running tests...');
234
+ await testWithReporter(reporter);
235
+ if (reporter.failed > 0) {
236
+ console.log(chalk.red('Error: ') + 'tests failed');
237
+ process.exit(1);
222
238
  }
223
239
  }
224
240
 
@@ -227,7 +243,7 @@ export async function publish({noDocs = false} = {}) {
227
243
  let step = 0;
228
244
  function progress() {
229
245
  step++;
230
- logUpdate(`Publishing ${config.package?.name}@${config.package?.version} ${progressBar(step, total)}`);
246
+ logUpdate(`Uploading files ${progressBar(step, total)}`);
231
247
  }
232
248
 
233
249
  // upload config
@@ -241,6 +257,14 @@ export async function publish({noDocs = false} = {}) {
241
257
  }
242
258
  let puiblishingId = publishing.ok;
243
259
 
260
+ // upload test stats
261
+ if (options.test) {
262
+ await actor.uploadTestStats(puiblishingId, {
263
+ passed: BigInt(reporter.passed),
264
+ passedNames: reporter.passedNamesFlat,
265
+ });
266
+ }
267
+
244
268
  // upload files
245
269
  await parallel(8, files, async (file: string) => {
246
270
  progress();
@@ -9,6 +9,7 @@ type TestStatus = 'pass' | 'fail' | 'skip';
9
9
  type MessageType = 'pass' | 'fail' | 'skip' | 'suite' | 'stdout';
10
10
 
11
11
  export class MMF1 {
12
+ file: string;
12
13
  stack: string[] = [];
13
14
  currSuite: string = '';
14
15
  failed = 0;
@@ -19,9 +20,16 @@ export class MMF1 {
19
20
  type: MessageType;
20
21
  message: string;
21
22
  }[] = [];
23
+ nestingSymbol = ' › ';
24
+ // or <file>
25
+ // or <file> › <test>
26
+ // or <file> › <suite> › <test>
27
+ // or <file> › <suite> › <test> › <nested-test>...
28
+ passedNamesFlat: string[] = [];
22
29
 
23
- constructor(srategy: Strategy) {
30
+ constructor(srategy: Strategy, file: string) {
24
31
  this.srategy = srategy;
32
+ this.file = file;
25
33
  }
26
34
 
27
35
  _log(type: MessageType, ...args: string[]) {
@@ -90,6 +98,7 @@ export class MMF1 {
90
98
  }
91
99
  this.passed++;
92
100
  this._log(status, ' '.repeat(this.stack.length * 2), chalk.green('✓'), name);
101
+ this.passedNamesFlat.push([this.file, ...this.stack, name].join(this.nestingSymbol));
93
102
  }
94
103
  else if (status === 'fail') {
95
104
  this.failed++;
@@ -32,6 +32,10 @@ export class CompactReporter implements Reporter {
32
32
  this.failed += mmf.failed;
33
33
  this.skipped += mmf.skipped;
34
34
 
35
+ if (mmf.passed === 0 && mmf.failed === 0) {
36
+ this.passed++;
37
+ }
38
+
35
39
  this.passedFiles += Number(mmf.failed === 0);
36
40
  this.failedFiles += Number(mmf.failed !== 0);
37
41
 
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import {absToRel} from '../utils.js';
3
+ import {MMF1} from '../mmf1.js';
4
+ import {Reporter} from './reporter.js';
5
+
6
+ export class SilentReporter implements Reporter {
7
+ passed = 0;
8
+ failed = 0;
9
+ skipped = 0;
10
+ passedFiles = 0;
11
+ failedFiles = 0;
12
+ passedNamesFlat: string[] = [];
13
+
14
+ addFiles(_files: string[]) {}
15
+
16
+ addRun(file: string, mmf: MMF1, state: Promise<void>, _wasiMode: boolean) {
17
+ state.then(() => {
18
+ this.passed += mmf.passed;
19
+ this.failed += mmf.failed;
20
+ this.skipped += mmf.skipped;
21
+ this.passedNamesFlat = [...this.passedNamesFlat, ...mmf.passedNamesFlat];
22
+
23
+ if (mmf.passed === 0 && mmf.failed === 0) {
24
+ this.passed++;
25
+ this.passedNamesFlat.push(absToRel(file));
26
+ }
27
+
28
+ this.passedFiles += Number(mmf.failed === 0);
29
+ this.failedFiles += Number(mmf.failed !== 0);
30
+
31
+ if (mmf.failed) {
32
+ console.log(chalk.red('✖'), absToRel(file));
33
+ mmf.flush('fail');
34
+ console.log('-'.repeat(50));
35
+ }
36
+ });
37
+ }
38
+
39
+ done(): boolean {
40
+ return this.failed === 0;
41
+ }
42
+ }
@@ -25,6 +25,10 @@ export class VerboseReporter implements Reporter {
25
25
  this.failed += mmf.failed;
26
26
  this.skipped += mmf.skipped;
27
27
 
28
+ if (mmf.passed === 0 && mmf.failed === 0) {
29
+ this.passed++;
30
+ }
31
+
28
32
  this.#curFileIndex++ && console.log('-'.repeat(50));
29
33
  console.log(`Running ${chalk.gray(absToRel(file))} ${wasiMode ? chalk.gray('(wasi)') : ''}`);
30
34
  mmf.flush();
@@ -13,10 +13,11 @@ import {parallel} from '../../parallel.js';
13
13
 
14
14
  import {MMF1} from './mmf1.js';
15
15
  import {absToRel} from './utils.js';
16
+ import {Reporter} from './reporters/reporter.js';
16
17
  import {VerboseReporter} from './reporters/verbose-reporter.js';
17
18
  import {FilesReporter} from './reporters/files-reporter.js';
18
19
  import {CompactReporter} from './reporters/compact-reporter.js';
19
- import {Reporter} from './reporters/reporter.js';
20
+ import {SilentReporter} from './reporters/silent-reporter.js';
20
21
 
21
22
  let ignore = [
22
23
  '**/node_modules/**',
@@ -30,9 +31,10 @@ let globConfig = {
30
31
  ignore: ignore,
31
32
  };
32
33
 
34
+ type ReporterName = 'verbose' | 'files' | 'compact' | 'silent';
33
35
  type TestMode = 'interpreter' | 'wasi';
34
36
 
35
- export async function test(filter = '', {watch = false, reporter = 'verbose', mode = 'interpreter' as TestMode} = {}) {
37
+ export async function test(filter = '', {watch = false, reporter = 'verbose' as ReporterName, mode = 'interpreter' as TestMode} = {}) {
36
38
  let rootDir = getRootDir();
37
39
 
38
40
  if (watch) {
@@ -41,7 +43,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose', mo
41
43
  let run = debounce(async () => {
42
44
  console.clear();
43
45
  process.stdout.write('\x1Bc');
44
- await runAll(filter, reporter);
46
+ await runAll(reporter, filter, mode);
45
47
  console.log('-'.repeat(50));
46
48
  console.log('Waiting for file changes...');
47
49
  console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`)));
@@ -61,7 +63,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose', mo
61
63
  run();
62
64
  }
63
65
  else {
64
- let passed = await runAll(filter, reporter, mode);
66
+ let passed = await runAll(reporter, filter, mode);
65
67
  if (!passed) {
66
68
  process.exit(1);
67
69
  }
@@ -70,7 +72,7 @@ export async function test(filter = '', {watch = false, reporter = 'verbose', mo
70
72
 
71
73
  let mocPath = process.env.DFX_MOC_PATH;
72
74
 
73
- export async function runAll(filter = '', reporterName = 'verbose', mode: TestMode = 'interpreter') {
75
+ export async function runAll(reporterName: ReporterName = 'verbose', filter = '', mode: TestMode = 'interpreter'): Promise<boolean> {
74
76
  let reporter: Reporter;
75
77
  if (reporterName == 'compact') {
76
78
  reporter = new CompactReporter;
@@ -78,10 +80,17 @@ export async function runAll(filter = '', reporterName = 'verbose', mode: TestMo
78
80
  else if (reporterName == 'files') {
79
81
  reporter = new FilesReporter;
80
82
  }
83
+ else if (reporterName == 'silent') {
84
+ reporter = new SilentReporter;
85
+ }
81
86
  else {
82
87
  reporter = new VerboseReporter;
83
88
  }
89
+ let done = await testWithReporter(reporter, filter, mode);
90
+ return done;
91
+ }
84
92
 
93
+ export async function testWithReporter(reporter: Reporter, filter = '', mode: TestMode = 'interpreter'): Promise<boolean> {
85
94
  let rootDir = getRootDir();
86
95
  let files: string[] = [];
87
96
  let libFiles = globSync('**/test?(s)/lib.mo', globConfig);
@@ -98,11 +107,11 @@ export async function runAll(filter = '', reporterName = 'verbose', mode: TestMo
98
107
  if (!files.length) {
99
108
  if (filter) {
100
109
  console.log(`No test files found for filter '${filter}'`);
101
- return;
110
+ return false;
102
111
  }
103
112
  console.log('No test files found');
104
113
  console.log('Put your tests in \'test\' directory in *.test.mo files');
105
- return;
114
+ return false;
106
115
  }
107
116
 
108
117
  reporter.addFiles(files);
@@ -117,7 +126,7 @@ export async function runAll(filter = '', reporterName = 'verbose', mode: TestMo
117
126
  fs.mkdirSync(wasmDir, {recursive: true});
118
127
 
119
128
  await parallel(os.cpus().length, files, async (file: string) => {
120
- let mmf = new MMF1('store');
129
+ let mmf = new MMF1('store', absToRel(file));
121
130
  let wasiMode = mode === 'wasi' || fs.readFileSync(file, 'utf8').startsWith('// @testmode wasi');
122
131
 
123
132
  let promise = new Promise<void>((resolve) => {
@@ -26,6 +26,16 @@ type User =
26
26
  };
27
27
  type Time = int;
28
28
  type Text = text;
29
+ type TestStats__1 =
30
+ record {
31
+ passed: nat;
32
+ passedNames: vec text;
33
+ };
34
+ type TestStats =
35
+ record {
36
+ passed: nat;
37
+ passedNames: vec text;
38
+ };
29
39
  type StorageStats =
30
40
  record {
31
41
  cyclesBalance: nat;
@@ -133,6 +143,7 @@ type PackageDetails =
133
143
  owner: principal;
134
144
  ownerInfo: User;
135
145
  publication: PackagePublication;
146
+ testStats: TestStats__1;
136
147
  versionHistory: vec PackageSummary__1;
137
148
  };
138
149
  type PackageConfigV2__1 =
@@ -242,4 +253,5 @@ service : {
242
253
  startPublish: (PackageConfigV2) -> (Result_1);
243
254
  takeAirdropSnapshot: () -> () oneway;
244
255
  uploadFileChunk: (PublishingId, FileId, nat, blob) -> (Result);
256
+ uploadTestStats: (PublishingId, TestStats) -> (Result);
245
257
  }
@@ -58,6 +58,7 @@ export interface PackageDetails {
58
58
  'ownerInfo' : User,
59
59
  'owner' : Principal,
60
60
  'deps' : Array<PackageSummary__1>,
61
+ 'testStats' : TestStats__1,
61
62
  'downloadsTotal' : bigint,
62
63
  'downloadsInLast30Days' : bigint,
63
64
  'downloadTrend' : Array<DownloadsSnapshot>,
@@ -124,6 +125,11 @@ export interface StorageStats {
124
125
  'cyclesBalance' : bigint,
125
126
  'memorySize' : bigint,
126
127
  }
128
+ export interface TestStats { 'passedNames' : Array<string>, 'passed' : bigint }
129
+ export interface TestStats__1 {
130
+ 'passedNames' : Array<string>,
131
+ 'passed' : bigint,
132
+ }
127
133
  export type Text = string;
128
134
  export type Time = bigint;
129
135
  export interface User {
@@ -206,4 +212,5 @@ export interface _SERVICE {
206
212
  [PublishingId, FileId, bigint, Uint8Array | number[]],
207
213
  Result
208
214
  >,
215
+ 'uploadTestStats' : ActorMethod<[PublishingId, TestStats], Result>,
209
216
  }
@@ -84,6 +84,10 @@ export const idlFactory = ({ IDL }) => {
84
84
  'config' : PackageConfigV2__1,
85
85
  'publication' : PackagePublication,
86
86
  });
87
+ const TestStats__1 = IDL.Record({
88
+ 'passedNames' : IDL.Vec(IDL.Text),
89
+ 'passed' : IDL.Nat,
90
+ });
87
91
  const DownloadsSnapshot = IDL.Record({
88
92
  'startTime' : Time,
89
93
  'endTime' : Time,
@@ -93,6 +97,7 @@ export const idlFactory = ({ IDL }) => {
93
97
  'ownerInfo' : User,
94
98
  'owner' : IDL.Principal,
95
99
  'deps' : IDL.Vec(PackageSummary__1),
100
+ 'testStats' : TestStats__1,
96
101
  'downloadsTotal' : IDL.Nat,
97
102
  'downloadsInLast30Days' : IDL.Nat,
98
103
  'downloadTrend' : IDL.Vec(DownloadsSnapshot),
@@ -145,6 +150,10 @@ export const idlFactory = ({ IDL }) => {
145
150
  });
146
151
  const PublishingErr = IDL.Text;
147
152
  const Result_1 = IDL.Variant({ 'ok' : PublishingId, 'err' : PublishingErr });
153
+ const TestStats = IDL.Record({
154
+ 'passedNames' : IDL.Vec(IDL.Text),
155
+ 'passed' : IDL.Nat,
156
+ });
148
157
  return IDL.Service({
149
158
  'backup' : IDL.Func([], [], []),
150
159
  'claimAirdrop' : IDL.Func([IDL.Principal], [IDL.Text], []),
@@ -237,6 +246,7 @@ export const idlFactory = ({ IDL }) => {
237
246
  [Result],
238
247
  [],
239
248
  ),
249
+ 'uploadTestStats' : IDL.Func([PublishingId, TestStats], [Result], []),
240
250
  });
241
251
  };
242
252
  export const init = ({ IDL }) => { return []; };
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
- import { program, Argument } from 'commander';
3
+ import { program, Argument, Option } from 'commander';
4
4
  import chalk from 'chalk';
5
5
  import { Principal } from '@dfinity/principal';
6
6
  import { init } from './commands/init.js';
@@ -89,6 +89,7 @@ program
89
89
  .command('publish')
90
90
  .description('Publish package to the mops registry')
91
91
  .option('--no-docs', 'Do not generate docs')
92
+ .option('--no-test', 'Do not run tests')
92
93
  .action(async (options) => {
93
94
  if (!checkConfigFile()) {
94
95
  process.exit(1);
@@ -169,14 +170,10 @@ program
169
170
  program
170
171
  .command('test [filter]')
171
172
  .description('Run tests')
172
- .option('-r, --reporter <reporter>', 'Choose reporter: verbose, compact, files')
173
+ .addOption(new Option('-r, --reporter <reporter>', 'Test reporter').choices(['verbose', 'compact', 'files', 'silent']).default('verbose'))
174
+ .addOption(new Option('--mode <mode>', 'Test mode').choices(['interpreter', 'wasi']).default('interpreter'))
173
175
  .option('-w, --watch', 'Enable watch mode')
174
- .option('--mode <mode>', 'Test mode: \'interpreter\' or \'wasi\' (default \'interpreter\'')
175
176
  .action(async (filter, options) => {
176
- if (options.mode && !['interpreter', 'wasi'].includes(options.mode)) {
177
- console.log(`Unknown --mode value '${options.mode}'. Allowed: interpreter, wasi`);
178
- process.exit(1);
179
- }
180
177
  await test(filter, options);
181
178
  });
182
179
  // template
@@ -1,3 +1,4 @@
1
- export declare function publish({ noDocs }?: {
2
- noDocs?: boolean | undefined;
1
+ export declare function publish(options?: {
2
+ docs?: boolean;
3
+ test?: boolean;
3
4
  }): Promise<void>;
@@ -8,29 +8,32 @@ import prompts from 'prompts';
8
8
  import { checkConfigFile, getRootDir, mainActor, progressBar, readConfig } from '../mops.js';
9
9
  import { parallel } from '../parallel.js';
10
10
  import { docs } from './docs.js';
11
- export async function publish({ noDocs = false } = {}) {
11
+ import { testWithReporter } from './test/test.js';
12
+ import { SilentReporter } from './test/reporters/silent-reporter.js';
13
+ export async function publish(options = {}) {
12
14
  if (!checkConfigFile()) {
13
15
  return;
14
16
  }
15
17
  let rootDir = getRootDir();
16
18
  let config = readConfig();
19
+ console.log(`Publishing ${config.package?.name}@${config.package?.version}`);
17
20
  // validate
18
21
  for (let key of Object.keys(config)) {
19
22
  if (!['package', 'dependencies', 'dev-dependencies', 'scripts'].includes(key)) {
20
23
  console.log(chalk.red('Error: ') + `Unknown config section [${key}]`);
21
- return;
24
+ process.exit(1);
22
25
  }
23
26
  }
24
27
  // required fields
25
28
  if (!config.package) {
26
29
  console.log(chalk.red('Error: ') + 'Please specify [package] section in your mops.toml');
27
- return;
30
+ process.exit(1);
28
31
  }
29
32
  for (let key of ['name', 'version']) {
30
33
  // @ts-ignore
31
34
  if (!config.package[key]) {
32
35
  console.log(chalk.red('Error: ') + `Please specify "${key}" in [config] section in your mops.toml`);
33
- return;
36
+ process.exit(1);
34
37
  }
35
38
  }
36
39
  // desired fields
@@ -66,7 +69,7 @@ export async function publish({ noDocs = false } = {}) {
66
69
  for (let key of Object.keys(config.package)) {
67
70
  if (!packageKeys.includes(key)) {
68
71
  console.log(chalk.red('Error: ') + `Unknown config key 'package.${key}'`);
69
- return;
72
+ process.exit(1);
70
73
  }
71
74
  }
72
75
  // check lengths
@@ -90,18 +93,18 @@ export async function publish({ noDocs = false } = {}) {
90
93
  // @ts-ignore
91
94
  if (config.package[key] && config.package[key].length > max) {
92
95
  console.log(chalk.red('Error: ') + `package.${key} value max length is ${max}`);
93
- return;
96
+ process.exit(1);
94
97
  }
95
98
  }
96
99
  if (config.dependencies) {
97
100
  if (Object.keys(config.dependencies).length > 100) {
98
101
  console.log(chalk.red('Error: ') + 'max dependencies is 100');
99
- return;
102
+ process.exit(1);
100
103
  }
101
104
  for (let dep of Object.values(config.dependencies)) {
102
105
  if (dep.path) {
103
106
  console.log(chalk.red('Error: ') + 'you can\'t publish packages with local dependencies');
104
- return;
107
+ process.exit(1);
105
108
  }
106
109
  delete dep.path;
107
110
  }
@@ -109,12 +112,12 @@ export async function publish({ noDocs = false } = {}) {
109
112
  if (config['dev-dependencies']) {
110
113
  if (Object.keys(config['dev-dependencies']).length > 100) {
111
114
  console.log(chalk.red('Error: ') + 'max dev-dependencies is 100');
112
- return;
115
+ process.exit(1);
113
116
  }
114
117
  for (let dep of Object.values(config['dev-dependencies'])) {
115
118
  if (dep.path) {
116
119
  console.log(chalk.red('Error: ') + 'you can\'t publish packages with local dev-dependencies');
117
- return;
120
+ process.exit(1);
118
121
  }
119
122
  delete dep.path;
120
123
  }
@@ -177,7 +180,8 @@ export async function publish({ noDocs = false } = {}) {
177
180
  files = globbySync([...files, ...defaultFiles]);
178
181
  // generate docs
179
182
  let docsFile = path.join(rootDir, '.mops/.docs/docs.tgz');
180
- if (!noDocs) {
183
+ if (options.docs) {
184
+ console.log('Generating documentation...');
181
185
  await docs({ silent: true });
182
186
  if (fs.existsSync(docsFile)) {
183
187
  files.unshift(docsFile);
@@ -186,17 +190,27 @@ export async function publish({ noDocs = false } = {}) {
186
190
  // check required files
187
191
  if (!files.includes('mops.toml')) {
188
192
  console.log(chalk.red('Error: ') + ' please add mops.toml file');
189
- return;
193
+ process.exit(1);
190
194
  }
191
195
  if (!files.includes('README.md')) {
192
196
  console.log(chalk.red('Error: ') + ' please add README.md file');
193
- return;
197
+ process.exit(1);
194
198
  }
195
199
  // check allowed exts
196
200
  for (let file of files) {
197
201
  if (!minimatch(file, '**/*.{mo,did,md,toml}') && !file.toLowerCase().endsWith('license') && !file.toLowerCase().endsWith('notice') && file !== docsFile) {
198
202
  console.log(chalk.red('Error: ') + `file ${file} has unsupported extension. Allowed: .mo, .did, .md, .toml`);
199
- return;
203
+ process.exit(1);
204
+ }
205
+ }
206
+ // test
207
+ let reporter = new SilentReporter;
208
+ if (options.test) {
209
+ console.log('Running tests...');
210
+ await testWithReporter(reporter);
211
+ if (reporter.failed > 0) {
212
+ console.log(chalk.red('Error: ') + 'tests failed');
213
+ process.exit(1);
200
214
  }
201
215
  }
202
216
  // progress
@@ -204,7 +218,7 @@ export async function publish({ noDocs = false } = {}) {
204
218
  let step = 0;
205
219
  function progress() {
206
220
  step++;
207
- logUpdate(`Publishing ${config.package?.name}@${config.package?.version} ${progressBar(step, total)}`);
221
+ logUpdate(`Uploading files ${progressBar(step, total)}`);
208
222
  }
209
223
  // upload config
210
224
  let actor = await mainActor(true);
@@ -215,6 +229,13 @@ export async function publish({ noDocs = false } = {}) {
215
229
  return;
216
230
  }
217
231
  let puiblishingId = publishing.ok;
232
+ // upload test stats
233
+ if (options.test) {
234
+ await actor.uploadTestStats(puiblishingId, {
235
+ passed: BigInt(reporter.passed),
236
+ passedNames: reporter.passedNamesFlat,
237
+ });
238
+ }
218
239
  // upload files
219
240
  await parallel(8, files, async (file) => {
220
241
  progress();
@@ -2,6 +2,7 @@ type Strategy = 'store' | 'print';
2
2
  type TestStatus = 'pass' | 'fail' | 'skip';
3
3
  type MessageType = 'pass' | 'fail' | 'skip' | 'suite' | 'stdout';
4
4
  export declare class MMF1 {
5
+ file: string;
5
6
  stack: string[];
6
7
  currSuite: string;
7
8
  failed: number;
@@ -12,7 +13,9 @@ export declare class MMF1 {
12
13
  type: MessageType;
13
14
  message: string;
14
15
  }[];
15
- constructor(srategy: Strategy);
16
+ nestingSymbol: string;
17
+ passedNamesFlat: string[];
18
+ constructor(srategy: Strategy, file: string);
16
19
  _log(type: MessageType, ...args: string[]): void;
17
20
  flush(messageType?: MessageType): void;
18
21
  parseLine(line: string): void;
@@ -4,14 +4,21 @@
4
4
  // mops:1:skip
5
5
  import chalk from 'chalk';
6
6
  export class MMF1 {
7
- constructor(srategy) {
7
+ constructor(srategy, file) {
8
8
  this.stack = [];
9
9
  this.currSuite = '';
10
10
  this.failed = 0;
11
11
  this.passed = 0;
12
12
  this.skipped = 0;
13
13
  this.output = [];
14
+ this.nestingSymbol = ' › ';
15
+ // or <file>
16
+ // or <file> › <test>
17
+ // or <file> › <suite> › <test>
18
+ // or <file> › <suite> › <test> › <nested-test>...
19
+ this.passedNamesFlat = [];
14
20
  this.srategy = srategy;
21
+ this.file = file;
15
22
  }
16
23
  _log(type, ...args) {
17
24
  if (this.srategy === 'store') {
@@ -73,6 +80,7 @@ export class MMF1 {
73
80
  }
74
81
  this.passed++;
75
82
  this._log(status, ' '.repeat(this.stack.length * 2), chalk.green('✓'), name);
83
+ this.passedNamesFlat.push([this.file, ...this.stack, name].join(this.nestingSymbol));
76
84
  }
77
85
  else if (status === 'fail') {
78
86
  this.failed++;
@@ -40,6 +40,9 @@ export class CompactReporter {
40
40
  this.passed += mmf.passed;
41
41
  this.failed += mmf.failed;
42
42
  this.skipped += mmf.skipped;
43
+ if (mmf.passed === 0 && mmf.failed === 0) {
44
+ this.passed++;
45
+ }
43
46
  this.passedFiles += Number(mmf.failed === 0);
44
47
  this.failedFiles += Number(mmf.failed !== 0);
45
48
  if (mmf.failed) {
@@ -0,0 +1,13 @@
1
+ import { MMF1 } from '../mmf1.js';
2
+ import { Reporter } from './reporter.js';
3
+ export declare class SilentReporter implements Reporter {
4
+ passed: number;
5
+ failed: number;
6
+ skipped: number;
7
+ passedFiles: number;
8
+ failedFiles: number;
9
+ passedNamesFlat: string[];
10
+ addFiles(_files: string[]): void;
11
+ addRun(file: string, mmf: MMF1, state: Promise<void>, _wasiMode: boolean): void;
12
+ done(): boolean;
13
+ }
@@ -0,0 +1,35 @@
1
+ import chalk from 'chalk';
2
+ import { absToRel } from '../utils.js';
3
+ export class SilentReporter {
4
+ constructor() {
5
+ this.passed = 0;
6
+ this.failed = 0;
7
+ this.skipped = 0;
8
+ this.passedFiles = 0;
9
+ this.failedFiles = 0;
10
+ this.passedNamesFlat = [];
11
+ }
12
+ addFiles(_files) { }
13
+ addRun(file, mmf, state, _wasiMode) {
14
+ state.then(() => {
15
+ this.passed += mmf.passed;
16
+ this.failed += mmf.failed;
17
+ this.skipped += mmf.skipped;
18
+ this.passedNamesFlat = [...this.passedNamesFlat, ...mmf.passedNamesFlat];
19
+ if (mmf.passed === 0 && mmf.failed === 0) {
20
+ this.passed++;
21
+ this.passedNamesFlat.push(absToRel(file));
22
+ }
23
+ this.passedFiles += Number(mmf.failed === 0);
24
+ this.failedFiles += Number(mmf.failed !== 0);
25
+ if (mmf.failed) {
26
+ console.log(chalk.red('✖'), absToRel(file));
27
+ mmf.flush('fail');
28
+ console.log('-'.repeat(50));
29
+ }
30
+ });
31
+ }
32
+ done() {
33
+ return this.failed === 0;
34
+ }
35
+ }
@@ -33,6 +33,9 @@ export class VerboseReporter {
33
33
  this.passed += mmf.passed;
34
34
  this.failed += mmf.failed;
35
35
  this.skipped += mmf.skipped;
36
+ if (mmf.passed === 0 && mmf.failed === 0) {
37
+ this.passed++;
38
+ }
36
39
  (__classPrivateFieldSet(this, _VerboseReporter_curFileIndex, (_b = __classPrivateFieldGet(this, _VerboseReporter_curFileIndex, "f"), _a = _b++, _b), "f"), _a) && console.log('-'.repeat(50));
37
40
  console.log(`Running ${chalk.gray(absToRel(file))} ${wasiMode ? chalk.gray('(wasi)') : ''}`);
38
41
  mmf.flush();
@@ -1,8 +1,11 @@
1
+ import { Reporter } from './reporters/reporter.js';
2
+ type ReporterName = 'verbose' | 'files' | 'compact' | 'silent';
1
3
  type TestMode = 'interpreter' | 'wasi';
2
4
  export declare function test(filter?: string, { watch, reporter, mode }?: {
3
5
  watch?: boolean | undefined;
4
- reporter?: string | undefined;
6
+ reporter?: ReporterName | undefined;
5
7
  mode?: TestMode | undefined;
6
8
  }): Promise<void>;
7
- export declare function runAll(filter?: string, reporterName?: string, mode?: TestMode): Promise<boolean | undefined>;
9
+ export declare function runAll(reporterName?: ReporterName, filter?: string, mode?: TestMode): Promise<boolean>;
10
+ export declare function testWithReporter(reporter: Reporter, filter?: string, mode?: TestMode): Promise<boolean>;
8
11
  export {};
@@ -14,6 +14,7 @@ import { absToRel } from './utils.js';
14
14
  import { VerboseReporter } from './reporters/verbose-reporter.js';
15
15
  import { FilesReporter } from './reporters/files-reporter.js';
16
16
  import { CompactReporter } from './reporters/compact-reporter.js';
17
+ import { SilentReporter } from './reporters/silent-reporter.js';
17
18
  let ignore = [
18
19
  '**/node_modules/**',
19
20
  '**/.mops/**',
@@ -32,7 +33,7 @@ export async function test(filter = '', { watch = false, reporter = 'verbose', m
32
33
  let run = debounce(async () => {
33
34
  console.clear();
34
35
  process.stdout.write('\x1Bc');
35
- await runAll(filter, reporter);
36
+ await runAll(reporter, filter, mode);
36
37
  console.log('-'.repeat(50));
37
38
  console.log('Waiting for file changes...');
38
39
  console.log(chalk.gray((`Press ${chalk.gray('Ctrl+C')} to exit.`)));
@@ -50,14 +51,14 @@ export async function test(filter = '', { watch = false, reporter = 'verbose', m
50
51
  run();
51
52
  }
52
53
  else {
53
- let passed = await runAll(filter, reporter, mode);
54
+ let passed = await runAll(reporter, filter, mode);
54
55
  if (!passed) {
55
56
  process.exit(1);
56
57
  }
57
58
  }
58
59
  }
59
60
  let mocPath = process.env.DFX_MOC_PATH;
60
- export async function runAll(filter = '', reporterName = 'verbose', mode = 'interpreter') {
61
+ export async function runAll(reporterName = 'verbose', filter = '', mode = 'interpreter') {
61
62
  let reporter;
62
63
  if (reporterName == 'compact') {
63
64
  reporter = new CompactReporter;
@@ -65,9 +66,16 @@ export async function runAll(filter = '', reporterName = 'verbose', mode = 'inte
65
66
  else if (reporterName == 'files') {
66
67
  reporter = new FilesReporter;
67
68
  }
69
+ else if (reporterName == 'silent') {
70
+ reporter = new SilentReporter;
71
+ }
68
72
  else {
69
73
  reporter = new VerboseReporter;
70
74
  }
75
+ let done = await testWithReporter(reporter, filter, mode);
76
+ return done;
77
+ }
78
+ export async function testWithReporter(reporter, filter = '', mode = 'interpreter') {
71
79
  let rootDir = getRootDir();
72
80
  let files = [];
73
81
  let libFiles = globSync('**/test?(s)/lib.mo', globConfig);
@@ -84,11 +92,11 @@ export async function runAll(filter = '', reporterName = 'verbose', mode = 'inte
84
92
  if (!files.length) {
85
93
  if (filter) {
86
94
  console.log(`No test files found for filter '${filter}'`);
87
- return;
95
+ return false;
88
96
  }
89
97
  console.log('No test files found');
90
98
  console.log('Put your tests in \'test\' directory in *.test.mo files');
91
- return;
99
+ return false;
92
100
  }
93
101
  reporter.addFiles(files);
94
102
  let sourcesArr = await sources();
@@ -98,7 +106,7 @@ export async function runAll(filter = '', reporterName = 'verbose', mode = 'inte
98
106
  let wasmDir = `${getRootDir()}/.mops/.test/`;
99
107
  fs.mkdirSync(wasmDir, { recursive: true });
100
108
  await parallel(os.cpus().length, files, async (file) => {
101
- let mmf = new MMF1('store');
109
+ let mmf = new MMF1('store', absToRel(file));
102
110
  let wasiMode = mode === 'wasi' || fs.readFileSync(file, 'utf8').startsWith('// @testmode wasi');
103
111
  let promise = new Promise((resolve) => {
104
112
  if (!mocPath) {
@@ -26,6 +26,16 @@ type User =
26
26
  };
27
27
  type Time = int;
28
28
  type Text = text;
29
+ type TestStats__1 =
30
+ record {
31
+ passed: nat;
32
+ passedNames: vec text;
33
+ };
34
+ type TestStats =
35
+ record {
36
+ passed: nat;
37
+ passedNames: vec text;
38
+ };
29
39
  type StorageStats =
30
40
  record {
31
41
  cyclesBalance: nat;
@@ -133,6 +143,7 @@ type PackageDetails =
133
143
  owner: principal;
134
144
  ownerInfo: User;
135
145
  publication: PackagePublication;
146
+ testStats: TestStats__1;
136
147
  versionHistory: vec PackageSummary__1;
137
148
  };
138
149
  type PackageConfigV2__1 =
@@ -242,4 +253,5 @@ service : {
242
253
  startPublish: (PackageConfigV2) -> (Result_1);
243
254
  takeAirdropSnapshot: () -> () oneway;
244
255
  uploadFileChunk: (PublishingId, FileId, nat, blob) -> (Result);
256
+ uploadTestStats: (PublishingId, TestStats) -> (Result);
245
257
  }
@@ -58,6 +58,7 @@ export interface PackageDetails {
58
58
  'ownerInfo' : User,
59
59
  'owner' : Principal,
60
60
  'deps' : Array<PackageSummary__1>,
61
+ 'testStats' : TestStats__1,
61
62
  'downloadsTotal' : bigint,
62
63
  'downloadsInLast30Days' : bigint,
63
64
  'downloadTrend' : Array<DownloadsSnapshot>,
@@ -124,6 +125,11 @@ export interface StorageStats {
124
125
  'cyclesBalance' : bigint,
125
126
  'memorySize' : bigint,
126
127
  }
128
+ export interface TestStats { 'passedNames' : Array<string>, 'passed' : bigint }
129
+ export interface TestStats__1 {
130
+ 'passedNames' : Array<string>,
131
+ 'passed' : bigint,
132
+ }
127
133
  export type Text = string;
128
134
  export type Time = bigint;
129
135
  export interface User {
@@ -206,4 +212,5 @@ export interface _SERVICE {
206
212
  [PublishingId, FileId, bigint, Uint8Array | number[]],
207
213
  Result
208
214
  >,
215
+ 'uploadTestStats' : ActorMethod<[PublishingId, TestStats], Result>,
209
216
  }
@@ -84,6 +84,10 @@ export const idlFactory = ({ IDL }) => {
84
84
  'config' : PackageConfigV2__1,
85
85
  'publication' : PackagePublication,
86
86
  });
87
+ const TestStats__1 = IDL.Record({
88
+ 'passedNames' : IDL.Vec(IDL.Text),
89
+ 'passed' : IDL.Nat,
90
+ });
87
91
  const DownloadsSnapshot = IDL.Record({
88
92
  'startTime' : Time,
89
93
  'endTime' : Time,
@@ -93,6 +97,7 @@ export const idlFactory = ({ IDL }) => {
93
97
  'ownerInfo' : User,
94
98
  'owner' : IDL.Principal,
95
99
  'deps' : IDL.Vec(PackageSummary__1),
100
+ 'testStats' : TestStats__1,
96
101
  'downloadsTotal' : IDL.Nat,
97
102
  'downloadsInLast30Days' : IDL.Nat,
98
103
  'downloadTrend' : IDL.Vec(DownloadsSnapshot),
@@ -145,6 +150,10 @@ export const idlFactory = ({ IDL }) => {
145
150
  });
146
151
  const PublishingErr = IDL.Text;
147
152
  const Result_1 = IDL.Variant({ 'ok' : PublishingId, 'err' : PublishingErr });
153
+ const TestStats = IDL.Record({
154
+ 'passedNames' : IDL.Vec(IDL.Text),
155
+ 'passed' : IDL.Nat,
156
+ });
148
157
  return IDL.Service({
149
158
  'backup' : IDL.Func([], [], []),
150
159
  'claimAirdrop' : IDL.Func([IDL.Principal], [IDL.Text], []),
@@ -237,6 +246,7 @@ export const idlFactory = ({ IDL }) => {
237
246
  [Result],
238
247
  [],
239
248
  ),
249
+ 'uploadTestStats' : IDL.Func([PublishingId, TestStats], [Result], []),
240
250
  });
241
251
  };
242
252
  export const init = ({ IDL }) => { return []; };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "0.25.3",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/cli.js"
@@ -7,7 +7,7 @@ module {
7
7
  /// Example:
8
8
  /// ```motoko
9
9
  /// assert add(1, 2) == 3;
10
- /// assert add(-5, 5) == 0;
10
+ /// assert add(7, 3) == 10;
11
11
  /// ```
12
12
  public func add(x : Nat, y : Nat) : Nat {
13
13
  return x + y;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "0.25.3",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/cli.js"
@@ -7,7 +7,7 @@ module {
7
7
  /// Example:
8
8
  /// ```motoko
9
9
  /// assert add(1, 2) == 3;
10
- /// assert add(-5, 5) == 0;
10
+ /// assert add(7, 3) == 10;
11
11
  /// ```
12
12
  public func add(x : Nat, y : Nat) : Nat {
13
13
  return x + y;