mmt-testlight 0.4.3 → 0.4.5

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/src/pkg-entry.cjs CHANGED
@@ -165,7 +165,7 @@ function createPkgJsRunner() {
165
165
  '__checkLogMode',
166
166
  `${helperDecls}\n${randomDecls}\n` +
167
167
  `const report_ = (...args) => mmtHelper.reportWithContext_ ? mmtHelper.reportWithContext_(__reporter, __runId, __id, ...args) : undefined;\n` +
168
- `const setenv_ = (name, value) => { try { envVariables[name] = value; } catch (_e) {} if (mmtHelper.setenvWithContext_) mmtHelper.setenvWithContext_(__reporter, __runId, __id, name, value); };\n` +
168
+ `const setenv_ = (vars) => { if (vars && typeof vars === 'object') { for (const [k, v] of Object.entries(vars)) { try { envVariables[k] = v; } catch (_e) {} } } if (mmtHelper.setenvWithContext_) mmtHelper.setenvWithContext_(__reporter, __runId, __id, vars); };\n` +
169
169
  `const check_ = (passed, type, raw, reportLevel, title, details, actual, expected) => mmtHelper.check_(passed, type, raw, reportLevel, title, details, actual, expected, report_, console, __checkLogMode);\n` +
170
170
  `const checkExpects_ = (items, type, reportLevel, title, details) => mmtHelper.checkExpects_ ? mmtHelper.checkExpects_(items, type, reportLevel, title, details, report_, console, __checkLogMode) : undefined;\n` +
171
171
  `const checkAbort_ = () => { if (__abortSignal && __abortSignal.aborted) { const e = new Error('Test run was stopped'); e.name = 'TestAbortError'; throw e; } };\n` +
@@ -522,16 +522,16 @@ function printHelp() {
522
522
  const options = formatHelpRows([
523
523
  ['-h, --help', 'Show help'],
524
524
  ['-v, --version', 'Show version'],
525
- ['--log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
525
+ ['-L, --log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
526
526
  ]);
527
527
  const runOptions = formatHelpRows([
528
528
  ['-q, --quiet', 'Minimal output'],
529
529
  ['-o, --out <file>', 'Write result JSON to file'],
530
530
  ['-i, --input <k=v...>', 'Input variables (repeatable)'],
531
531
  ['-e, --env <k=v...>', 'Environment variables (repeatable)'],
532
- ['--env-file <path>', 'Environment file (.mmt/.yaml)'],
533
- ['--preset <name>', 'Preset from env file (e.g. runner.dev)'],
534
- ['--example <name|#n>', 'Named example or index (#1 is first)'],
532
+ ['-F, --env-file <path>', 'Environment file (.mmt/.yaml)'],
533
+ ['-P, --preset <name>', 'Preset from env file (repeatable)'],
534
+ ['-x, --example <name|#n>', 'Named example or index (#1 is first)'],
535
535
  ['--print-js', 'Print generated JS before executing'],
536
536
  ['--debug-env', 'Print resolved env vars'],
537
537
  ]);
@@ -567,12 +567,12 @@ function printRunHelp() {
567
567
  ['-o, --out <file>', 'Write result JSON to file'],
568
568
  ['-i, --input <k=v...>', 'Input variables (repeatable)'],
569
569
  ['-e, --env <k=v...>', 'Environment variables (repeatable)'],
570
- ['--env-file <path>', 'Environment file (.mmt/.yaml)'],
571
- ['--preset <name>', 'Preset from env file (e.g. runner.dev)'],
572
- ['--example <name|#n>', 'Named example or index (#1 is first)'],
570
+ ['-F, --env-file <path>', 'Environment file (.mmt/.yaml)'],
571
+ ['-P, --preset <name>', 'Preset from env file (repeatable)'],
572
+ ['-x, --example <name|#n>', 'Named example or index (#1 is first)'],
573
573
  ['--print-js', 'Print generated JS before executing'],
574
574
  ['--debug-env', 'Print resolved env vars'],
575
- ['--log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
575
+ ['-L, --log-level <level>', 'Set log level (error|warn|info|debug|trace)'],
576
576
  ]);
577
577
  const examples = formatHelpRows([
578
578
  ['testlight run path/to/test.mmt'],
@@ -598,9 +598,9 @@ function printPrintJsHelp() {
598
598
  ['-h, --help', 'Show help'],
599
599
  ['-i, --input <k=v...>', 'Input variables (repeatable)'],
600
600
  ['-e, --env <k=v...>', 'Environment variables (repeatable)'],
601
- ['--env-file <path>', 'Environment file (.mmt/.yaml)'],
602
- ['--preset <name>', 'Preset from env file (e.g. runner.dev)'],
603
- ['--example <name|#n>', 'Named example or index (#1 is first)'],
601
+ ['-F, --env-file <path>', 'Environment file (.mmt/.yaml)'],
602
+ ['-P, --preset <name>', 'Preset from env file (repeatable)'],
603
+ ['-x, --example <name|#n>', 'Named example or index (#1 is first)'],
604
604
  ]);
605
605
  const out = [
606
606
  'Usage: testlight print-js [options] <file>',
@@ -653,6 +653,39 @@ function parsePairs(tokens) {
653
653
  return out;
654
654
  }
655
655
 
656
+ function pushPresetOpt(opts, value) {
657
+ if (!Array.isArray(opts.preset)) {
658
+ opts.preset = opts.preset ? [opts.preset] : [];
659
+ }
660
+ if (value == null) {
661
+ return;
662
+ }
663
+ for (const part of String(value).split(',')) {
664
+ const name = part.trim();
665
+ if (name) {
666
+ opts.preset.push(name);
667
+ }
668
+ }
669
+ }
670
+
671
+ function resolvePresetsFromDoc(runConfig, doc, presetNames) {
672
+ if (!runConfig || !doc) {
673
+ return {};
674
+ }
675
+ if (typeof runConfig.resolvePresetsEnv === 'function') {
676
+ return runConfig.resolvePresetsEnv(doc, presetNames) || {};
677
+ }
678
+ const list = Array.isArray(presetNames) ? presetNames :
679
+ (presetNames ? [presetNames] : []);
680
+ let out = {};
681
+ if (typeof runConfig.resolvePresetEnv === 'function') {
682
+ for (const name of list) {
683
+ Object.assign(out, runConfig.resolvePresetEnv(doc, name) || {});
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+
656
689
  function loadEnvDoc(envPath) {
657
690
  try {
658
691
  // eslint-disable-next-line global-require
@@ -690,7 +723,7 @@ function parseRunArgv(argv) {
690
723
  input: [],
691
724
  env: [],
692
725
  envFile: undefined,
693
- preset: undefined,
726
+ preset: [],
694
727
  example: undefined,
695
728
  printJs: false,
696
729
  debugEnv: false,
@@ -711,7 +744,7 @@ function parseRunArgv(argv) {
711
744
  if (a === '-h' || a === '--help') {
712
745
  continue;
713
746
  }
714
- if (a === '--log-level') {
747
+ if (a === '--log-level' || a === '-L') {
715
748
  consume(i + 1);
716
749
  i += 1;
717
750
  continue;
@@ -758,17 +791,17 @@ function parseRunArgv(argv) {
758
791
  }
759
792
  continue;
760
793
  }
761
- if (a === '--env-file') {
794
+ if (a === '--env-file' || a === '-F') {
762
795
  consume(i + 1);
763
796
  opts.envFile = argv[++i];
764
797
  continue;
765
798
  }
766
- if (a === '--preset') {
799
+ if (a === '--preset' || a === '-P') {
767
800
  consume(i + 1);
768
- opts.preset = argv[++i];
801
+ pushPresetOpt(opts, argv[++i]);
769
802
  continue;
770
803
  }
771
- if (a === '--example') {
804
+ if (a === '--example' || a === '-x') {
772
805
  consume(i + 1);
773
806
  opts.example = argv[++i];
774
807
  continue;
@@ -802,7 +835,7 @@ function parsePrintJsArgv(argv) {
802
835
  input: [],
803
836
  env: [],
804
837
  envFile: undefined,
805
- preset: undefined,
838
+ preset: [],
806
839
  example: undefined,
807
840
  };
808
841
  let filePath;
@@ -834,15 +867,15 @@ function parsePrintJsArgv(argv) {
834
867
  }
835
868
  continue;
836
869
  }
837
- if (a === '--env-file') {
870
+ if (a === '--env-file' || a === '-F') {
838
871
  opts.envFile = argv[++i];
839
872
  continue;
840
873
  }
841
- if (a === '--preset') {
842
- opts.preset = argv[++i];
874
+ if (a === '--preset' || a === '-P') {
875
+ pushPresetOpt(opts, argv[++i]);
843
876
  continue;
844
877
  }
845
- if (a === '--example') {
878
+ if (a === '--example' || a === '-x') {
846
879
  opts.example = argv[++i];
847
880
  continue;
848
881
  }
@@ -857,17 +890,19 @@ function parsePrintJsArgv(argv) {
857
890
  }
858
891
 
859
892
  function parseLogLevel(argv) {
860
- const idx = argv.indexOf('--log-level');
861
- if (idx < 0) {
862
- return 'info';
863
- }
864
- const v = argv[idx + 1];
865
- if (!v) {
866
- return 'info';
867
- }
868
- const level = String(v).toLowerCase();
869
- if (['error', 'warn', 'info', 'debug', 'trace'].includes(level)) {
870
- return level;
893
+ for (let i = 1; i < argv.length; i++) {
894
+ const a = argv[i];
895
+ if (a === '--log-level' || a === '-L') {
896
+ const v = argv[i + 1];
897
+ if (!v) {
898
+ return 'info';
899
+ }
900
+ const level = String(v).toLowerCase();
901
+ if (['error', 'warn', 'info', 'debug', 'trace'].includes(level)) {
902
+ return level;
903
+ }
904
+ return 'info';
905
+ }
871
906
  }
872
907
  return 'info';
873
908
  }
@@ -1023,9 +1058,8 @@ async function main() {
1023
1058
  const doc = loadEnvDoc(p);
1024
1059
  // Defaults come from env file variables; optional preset overlays them.
1025
1060
  let baseEnv = (doc && doc.variables) ? {...doc.variables} : {};
1026
- if (parsed.opts.preset && runConfig &&
1027
- typeof runConfig.resolvePresetEnv === 'function') {
1028
- const presetEnv = runConfig.resolvePresetEnv(doc, parsed.opts.preset) || {};
1061
+ if (parsed.opts.preset.length && runConfig) {
1062
+ const presetEnv = resolvePresetsFromDoc(runConfig, doc, parsed.opts.preset);
1029
1063
  baseEnv = {...baseEnv, ...presetEnv};
1030
1064
  }
1031
1065
  envvar = runConfig.mergeEnv({ envvar: baseEnv, manualEnvvars });
@@ -1033,7 +1067,7 @@ async function main() {
1033
1067
  try {
1034
1068
  process.stdout.write(`debug envFile raw: ${envFileRaw}\n`);
1035
1069
  process.stdout.write(`debug envFile resolved: ${p}\n`);
1036
- process.stdout.write(`debug preset: ${parsed.opts.preset || '(none)'}\n`);
1070
+ process.stdout.write(`debug preset: ${parsed.opts.preset.length ? parsed.opts.preset.join(',') : '(none)'}\n`);
1037
1071
  process.stdout.write(`debug envDoc variables keys: ${doc && doc.variables ? Object.keys(doc.variables).join(',') : '(none)'}\n`);
1038
1072
  process.stdout.write(`debug envDoc presets keys: ${doc && doc.presets ? Object.keys(doc.presets).join(',') : '(none)'}\n`);
1039
1073
  process.stdout.write(`debug baseEnv keys: ${baseEnv ? Object.keys(baseEnv).join(',') : '(none)'}\n`);
@@ -1123,8 +1157,9 @@ async function main() {
1123
1157
  envFileForPreset = loadEnvDoc(p);
1124
1158
  }
1125
1159
  }
1126
- if (envFileForPreset && coreRunConfig.resolvePresetEnv) {
1127
- suitePresetEnv = coreRunConfig.resolvePresetEnv(envFileForPreset, suiteEnv.preset) || {};
1160
+ if (envFileForPreset && coreRunConfig) {
1161
+ suitePresetEnv = resolvePresetsFromDoc(
1162
+ coreRunConfig, envFileForPreset, suiteEnv.preset) || {};
1128
1163
  }
1129
1164
  }
1130
1165
 
package/src/runArgs.ts CHANGED
@@ -18,7 +18,7 @@ const {resolveUserPath, resolveUserPathPreferExisting} = requireFromRunArgs('../
18
18
 
19
19
  export type ReportFormat = 'junit' | 'mmt' | 'html' | 'md' | 'md-detailed';
20
20
 
21
- const {mergeEnv, resolvePresetEnv} =
21
+ const {mergeEnv, resolvePresetsEnv, normalizePresetNames} =
22
22
  ((mmtcore as any).runConfig || {}) as any;
23
23
  const OMIT_SENTINEL =
24
24
  ((mmtcore as any).omitKeyword || {}).OMIT_SENTINEL || '__MMT_OMIT__';
@@ -347,7 +347,7 @@ export async function buildCliRunArgs(file: string, opts: AnyOpts): Promise<Pars
347
347
  let envvar: Record<string, any>|undefined = undefined;
348
348
  let networkConfig: NetworkConfig|undefined = undefined;
349
349
  const envFileOpt = opts.envFile as string | undefined;
350
- const presetName = opts.preset as string | undefined;
350
+ const presetNames = normalizePresetNames(opts.preset as string | string[] | undefined);
351
351
  let envFileDir = dir;
352
352
 
353
353
  // Detect if this is a suite file and check for suite environment config
@@ -368,7 +368,7 @@ export async function buildCliRunArgs(file: string, opts: AnyOpts): Promise<Pars
368
368
  envFileDir = path.dirname(p);
369
369
  const doc = await loadEnvDoc(p, findProjectRootForCli(p) || undefined);
370
370
  const defaultEnv = resolveDefaultEnvVariables(doc.variables);
371
- const presetEnv = resolvePresetEnv(doc, presetName);
371
+ const presetEnv = resolvePresetsEnv(doc, presetNames);
372
372
  envvar = mergeEnv({baseEnv: defaultEnv, envvar: presetEnv, manualEnvvars});
373
373
  // Build network config from file-driven HTTP settings and certificates.
374
374
  if (doc.certificates || doc.setting) {
@@ -403,7 +403,7 @@ export async function buildCliRunArgs(file: string, opts: AnyOpts): Promise<Pars
403
403
  if (suiteEnvFilePath && fs.existsSync(suiteEnvFilePath)) {
404
404
  const suiteEnvDoc =
405
405
  await loadEnvDoc(suiteEnvFilePath, projectRoot || undefined);
406
- suitePresetEnv = resolvePresetEnv(suiteEnvDoc, suiteEnvConfig.preset);
406
+ suitePresetEnv = resolvePresetsEnv(suiteEnvDoc, suiteEnvConfig.preset);
407
407
  }
408
408
  }
409
409
 
@@ -0,0 +1,79 @@
1
+ import {
2
+ buildDownloadUrl,
3
+ compareVersions,
4
+ detectInstallChannel,
5
+ detectPlatform,
6
+ normalizeVersion,
7
+ planUpdate,
8
+ } from './selfUpdate';
9
+
10
+ describe('selfUpdate helpers', () => {
11
+ it('normalizes and compares versions', () => {
12
+ expect(normalizeVersion('v1.2.3')).toBe('1.2.3');
13
+ expect(compareVersions('1.2.3', '1.2.3')).toBe(0);
14
+ expect(compareVersions('1.2.3', '1.2.4')).toBe(-1);
15
+ expect(compareVersions('1.3.0', '1.2.9')).toBe(1);
16
+ expect(compareVersions('1.2.3-beta.1', '1.2.3')).toBe(-1);
17
+ });
18
+
19
+ it('detects platforms', () => {
20
+ expect(detectPlatform('darwin', 'arm64')).toBe('macos-arm64');
21
+ expect(detectPlatform('linux', 'x64')).toBe('linux-x64');
22
+ expect(detectPlatform('win32', 'arm64')).toBe('win-x64');
23
+ });
24
+
25
+ it('detects install channels', () => {
26
+ expect(detectInstallChannel('/usr/local/bin/testlight')).toBe('standalone');
27
+ expect(detectInstallChannel(
28
+ '/opt/homebrew/Cellar/mmt-testlight/0.4.4/bin/testlight'))
29
+ .toBe('homebrew');
30
+ expect(detectInstallChannel(
31
+ '/usr/local/bin/node',
32
+ '/usr/local/lib/node_modules/mmt-testlight/dist/cli.js'))
33
+ .toBe('npm');
34
+ });
35
+
36
+ it('builds github and portal download urls', () => {
37
+ expect(buildDownloadUrl({version: '1.2.3', platform: 'linux-x64'}))
38
+ .toBe(
39
+ 'https://github.com/mshobeyri/multimeter/releases/download/v1.2.3/testlight-linux-x64.tar.gz');
40
+ expect(buildDownloadUrl({
41
+ version: '1.2.3',
42
+ platform: 'win-x64',
43
+ releaseBaseUrl: 'https://portal.example/testlight',
44
+ })).toBe('https://portal.example/testlight/v1.2.3/testlight-win-x64.zip');
45
+ });
46
+
47
+ it('plans npm advice instead of binary replace', async () => {
48
+ const plan = await planUpdate({
49
+ currentVersion: '0.4.0',
50
+ version: '0.4.4',
51
+ execPath: '/usr/local/bin/node',
52
+ scriptPath: '/usr/local/lib/node_modules/mmt-testlight/dist/cli.js',
53
+ fetchJson: async () => ({tag_name: 'v0.4.4'}),
54
+ });
55
+ expect(plan.action).toBe('advise-npm');
56
+ expect(plan.advice).toContain('npm install -g mmt-testlight@0.4.4');
57
+ });
58
+
59
+ it('plans noop when already current', async () => {
60
+ const plan = await planUpdate({
61
+ currentVersion: '1.2.3',
62
+ version: '1.2.3',
63
+ execPath: '/usr/local/bin/testlight',
64
+ fetchJson: async () => ({tag_name: 'v1.2.3'}),
65
+ });
66
+ expect(plan.action).toBe('noop');
67
+ });
68
+
69
+ it('plans download for standalone when newer', async () => {
70
+ const plan = await planUpdate({
71
+ currentVersion: '1.0.0',
72
+ version: '1.2.3',
73
+ execPath: '/opt/portal/bin/testlight',
74
+ fetchJson: async () => ({tag_name: 'v1.2.3'}),
75
+ });
76
+ expect(plan.action).toBe('download');
77
+ expect(plan.downloadUrl).toContain('/v1.2.3/testlight-');
78
+ });
79
+ });