doc-detective 3.5.0-dev.0 → 3.5.0-dev.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/.github/FUNDING.yml +14 -14
- package/.github/dependabot.yml +11 -11
- package/.github/workflows/auto-dev-release.yml +173 -173
- package/.github/workflows/npm-test.yaml +95 -96
- package/.github/workflows/update-core.yaml +131 -131
- package/CONTRIBUTIONS.md +27 -27
- package/LICENSE +661 -661
- package/README.md +110 -110
- package/dev/dev.config.json +3 -8
- package/dev/dev.spec.json +30 -30
- package/dev/index.js +5 -5
- package/package.json +47 -47
- package/reference.png +0 -0
- package/samples/.doc-detective.json +94 -94
- package/samples/doc-content-detect.md +10 -10
- package/samples/doc-content-inline-tests.md +23 -23
- package/samples/docker-hello.spec.json +15 -15
- package/samples/env +2 -2
- package/samples/http.spec.yaml +37 -37
- package/samples/kitten-search-detect.md +7 -7
- package/samples/kitten-search-inline.md +15 -15
- package/samples/kitten-search.spec.json +28 -28
- package/samples/local-gui.md +5 -5
- package/samples/tests.spec.json +70 -70
- package/samples/variables.env +4 -4
- package/scripts/bump-sync-version-core.js +108 -110
- package/src/checkDependencies.js +84 -84
- package/src/index.js +72 -72
- package/src/utils.js +1023 -1023
- package/test/artifacts/cleanup.spec.json +18 -18
- package/test/artifacts/config.json +6 -6
- package/test/artifacts/doc-content.md +23 -23
- package/test/artifacts/env +2 -2
- package/test/artifacts/httpRequest.spec.yaml +37 -37
- package/test/artifacts/runShell.spec.json +29 -29
- package/test/artifacts/setup.spec.json +18 -18
- package/test/artifacts/test.spec.json +46 -46
- package/test/resolvedTests.test.js +193 -193
- package/test/runTests.test.js +53 -53
- package/test/server/index.js +185 -185
- package/test/server/public/index.html +174 -174
- package/test/test-config.json +12 -12
- package/test/test-results.json +124 -124
- package/test/utils.test.js +634 -298
package/src/utils.js
CHANGED
|
@@ -1,1023 +1,1023 @@
|
|
|
1
|
-
const yargs = require("yargs/yargs");
|
|
2
|
-
const { hideBin } = require("yargs/helpers");
|
|
3
|
-
const { validate, resolvePaths, readFile } = require("doc-detective-common");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const { spawn } = require("child_process");
|
|
7
|
-
const os = require("os");
|
|
8
|
-
const axios = require("axios");
|
|
9
|
-
|
|
10
|
-
exports.setArgs = setArgs;
|
|
11
|
-
exports.setConfig = setConfig;
|
|
12
|
-
exports.outputResults = outputResults;
|
|
13
|
-
exports.spawnCommand = spawnCommand;
|
|
14
|
-
exports.setMeta = setMeta;
|
|
15
|
-
exports.getVersionData = getVersionData;
|
|
16
|
-
exports.log = log;
|
|
17
|
-
exports.getResolvedTestsFromEnv = getResolvedTestsFromEnv;
|
|
18
|
-
exports.reportResults = reportResults;
|
|
19
|
-
|
|
20
|
-
// Log function that respects logLevel
|
|
21
|
-
function log(message, level = "info", config = {}) {
|
|
22
|
-
const logLevels = ["silent", "error", "warning", "info", "debug"];
|
|
23
|
-
const currentLevel = config.logLevel || "info";
|
|
24
|
-
const currentLevelIndex = logLevels.indexOf(currentLevel);
|
|
25
|
-
const messageLevelIndex = logLevels.indexOf(level);
|
|
26
|
-
|
|
27
|
-
// Only log if the message level is at or above the current log level
|
|
28
|
-
if (currentLevelIndex >= messageLevelIndex && messageLevelIndex > 0) {
|
|
29
|
-
if (level === "error") {
|
|
30
|
-
console.error(message);
|
|
31
|
-
} else {
|
|
32
|
-
console.log(message);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Define args
|
|
38
|
-
function setArgs(args) {
|
|
39
|
-
if (!args) return {};
|
|
40
|
-
let argv = yargs(hideBin(args))
|
|
41
|
-
.option("config", {
|
|
42
|
-
alias: "c",
|
|
43
|
-
description: "Path to a `config.json` or `config.yaml` file.",
|
|
44
|
-
type: "string",
|
|
45
|
-
})
|
|
46
|
-
.option("input", {
|
|
47
|
-
alias: "i",
|
|
48
|
-
description:
|
|
49
|
-
"Path to test specifications and documentation source files. May be paths to specific files or to directories to scan for files.",
|
|
50
|
-
type: "string",
|
|
51
|
-
})
|
|
52
|
-
.option("output", {
|
|
53
|
-
alias: "o",
|
|
54
|
-
description:
|
|
55
|
-
"Path of the directory in which to store the output of Doc Detective commands.",
|
|
56
|
-
type: "string",
|
|
57
|
-
})
|
|
58
|
-
.option("logLevel", {
|
|
59
|
-
alias: "l",
|
|
60
|
-
description:
|
|
61
|
-
"Detail level of logging events. Accepted values: silent, error, warning, info (default), debug",
|
|
62
|
-
type: "string",
|
|
63
|
-
})
|
|
64
|
-
.option("allow-unsafe", {
|
|
65
|
-
description: "Allow execution of potentially unsafe tests",
|
|
66
|
-
type: "boolean",
|
|
67
|
-
})
|
|
68
|
-
.help()
|
|
69
|
-
.alias("help", "h").argv;
|
|
70
|
-
|
|
71
|
-
return argv;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Get resolved tests from environment variable, if set
|
|
75
|
-
async function getResolvedTestsFromEnv(config = {}) {
|
|
76
|
-
if (!process.env.DOC_DETECTIVE_API) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let resolvedTests = null;
|
|
81
|
-
let apiConfig = null;
|
|
82
|
-
try {
|
|
83
|
-
// Parse the environment variable as JSON
|
|
84
|
-
apiConfig = JSON.parse(process.env.DOC_DETECTIVE_API);
|
|
85
|
-
|
|
86
|
-
// Validate the structure: { accountId, url, token, contextIds }
|
|
87
|
-
if (!apiConfig.accountId || !apiConfig.url || !apiConfig.token || !apiConfig.contextIds) {
|
|
88
|
-
log(
|
|
89
|
-
"Invalid DOC_DETECTIVE_API: must contain 'accountId', 'url', 'token', and 'contextIds' properties",
|
|
90
|
-
"error",
|
|
91
|
-
config
|
|
92
|
-
);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
log(`CLI:Fetching resolved tests from ${apiConfig.url}/resolved-tests`, "debug", config);
|
|
97
|
-
|
|
98
|
-
// Make GET request to the specified URL with token in header
|
|
99
|
-
const response = await axios.get(`${apiConfig.url}/resolved-tests`, {
|
|
100
|
-
headers: {
|
|
101
|
-
"x-runner-token": apiConfig.token,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// The response is the resolvedTests
|
|
106
|
-
resolvedTests = response.data;
|
|
107
|
-
|
|
108
|
-
// Validate against resolvedTests_v3 schema
|
|
109
|
-
const validation = validate({
|
|
110
|
-
schemaKey: "resolvedTests_v3",
|
|
111
|
-
object: resolvedTests,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (!validation.valid) {
|
|
115
|
-
log(
|
|
116
|
-
"Invalid resolvedTests from API response. " + validation.errors,
|
|
117
|
-
"error",
|
|
118
|
-
config
|
|
119
|
-
);
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Get config from environment variable for merging
|
|
124
|
-
const envConfig = await getConfigFromEnv();
|
|
125
|
-
if (envConfig) {
|
|
126
|
-
// Apply config overrides to resolvedTests.config
|
|
127
|
-
if (resolvedTests.config) {
|
|
128
|
-
resolvedTests.config = { ...resolvedTests.config, ...envConfig };
|
|
129
|
-
} else {
|
|
130
|
-
resolvedTests.config = envConfig;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
log(
|
|
135
|
-
`CLI:RESOLVED_TESTS:\n${JSON.stringify(resolvedTests, null, 2)}`,
|
|
136
|
-
"debug",
|
|
137
|
-
config
|
|
138
|
-
);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
log(
|
|
141
|
-
`Error fetching resolved tests from DOC_DETECTIVE_API: ${error.message}`,
|
|
142
|
-
"error",
|
|
143
|
-
config
|
|
144
|
-
);
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
return { apiConfig, resolvedTests };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function getConfigFromEnv() {
|
|
151
|
-
if (!process.env.DOC_DETECTIVE_CONFIG) {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let envConfig = null;
|
|
156
|
-
try {
|
|
157
|
-
// Parse the environment variable as JSON
|
|
158
|
-
envConfig = JSON.parse(process.env.DOC_DETECTIVE_CONFIG);
|
|
159
|
-
|
|
160
|
-
// Validate the environment variable config
|
|
161
|
-
const envValidation = validate({
|
|
162
|
-
schemaKey: "config_v3",
|
|
163
|
-
object: envConfig,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (!envValidation.valid) {
|
|
167
|
-
console.error(
|
|
168
|
-
"Invalid config from DOC_DETECTIVE_CONFIG environment variable.",
|
|
169
|
-
envValidation.errors
|
|
170
|
-
);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
log(`CLI:ENV_CONFIG:\n${JSON.stringify(envConfig, null, 2)}`, "debug", envConfig);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
console.error(
|
|
177
|
-
`Error parsing DOC_DETECTIVE_CONFIG environment variable: ${error.message}`
|
|
178
|
-
);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
return envConfig;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Override config values based on args and validate the config
|
|
185
|
-
async function setConfig({ configPath, args }) {
|
|
186
|
-
if (args.config && !configPath) {
|
|
187
|
-
configPath = args.config;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// If config file exists, read it
|
|
191
|
-
let config = {};
|
|
192
|
-
if (configPath) {
|
|
193
|
-
try {
|
|
194
|
-
config = await readFile({ fileURLOrPath: configPath });
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error(`Error reading config file at ${configPath}: ${error}`);
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check for DOC_DETECTIVE_CONFIG environment variable
|
|
202
|
-
const envConfig = await getConfigFromEnv();
|
|
203
|
-
if (envConfig) {
|
|
204
|
-
// Merge with file config, preferring environment variable config (use raw envConfig, not validated with defaults)
|
|
205
|
-
config = { ...config, ...envConfig };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Validate config
|
|
209
|
-
const validation = validate({
|
|
210
|
-
schemaKey: "config_v3",
|
|
211
|
-
object: config,
|
|
212
|
-
});
|
|
213
|
-
if (!validation.valid) {
|
|
214
|
-
// Output validation errors
|
|
215
|
-
console.error("Invalid config.", validation.errors);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Accept coerced and defaulted values
|
|
220
|
-
config = validation.object;
|
|
221
|
-
// Set default values
|
|
222
|
-
config = {
|
|
223
|
-
...config,
|
|
224
|
-
input: config.input || ".",
|
|
225
|
-
output: config.output || ".",
|
|
226
|
-
recursive: config.recursive
|
|
227
|
-
relativePathBase: config.relativePathBase || "file",
|
|
228
|
-
loadVariables: config.loadVariables || ".env",
|
|
229
|
-
detectSteps: config.detectSteps
|
|
230
|
-
logLevel: config.logLevel || "info",
|
|
231
|
-
fileTypes: config.fileTypes || ["markdown", "asciidoc", "html"],
|
|
232
|
-
telemetry: config.telemetry || { send: true },
|
|
233
|
-
};
|
|
234
|
-
// Override config values
|
|
235
|
-
if (configPath) {
|
|
236
|
-
config.configPath = configPath;
|
|
237
|
-
}
|
|
238
|
-
if (args.input) {
|
|
239
|
-
// If input includes commas, split it into an array
|
|
240
|
-
args.input = args.input.split(",").map((item) => item.trim());
|
|
241
|
-
// Resolve paths
|
|
242
|
-
args.input = args.input.map((item) => {
|
|
243
|
-
if (item.startsWith("https://") || item.startsWith("http://")) {
|
|
244
|
-
return item; // Don't resolve URLs
|
|
245
|
-
}
|
|
246
|
-
return path.resolve(item);
|
|
247
|
-
});
|
|
248
|
-
// Add to config
|
|
249
|
-
config.input = args.input;
|
|
250
|
-
}
|
|
251
|
-
if (args.output) {
|
|
252
|
-
config.output = path.resolve(args.output);
|
|
253
|
-
}
|
|
254
|
-
if (args.logLevel) {
|
|
255
|
-
config.logLevel = args.logLevel;
|
|
256
|
-
}
|
|
257
|
-
if (typeof args.allowUnsafe === "boolean") {
|
|
258
|
-
config.allowUnsafeSteps = args.allowUnsafe;
|
|
259
|
-
}
|
|
260
|
-
// Resolve paths
|
|
261
|
-
config = await resolvePaths({
|
|
262
|
-
config: config,
|
|
263
|
-
object: config,
|
|
264
|
-
filePath: configPath || ".",
|
|
265
|
-
nested: false,
|
|
266
|
-
objectType: "config",
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
return config;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Internal reporters
|
|
273
|
-
const reporters = {
|
|
274
|
-
// JSON reporter: outputs results to a JSON file
|
|
275
|
-
jsonReporter: async (config = {}, outputPath, results, options = {}) => {
|
|
276
|
-
// Define supported output extensions
|
|
277
|
-
const outputExtensions = [".json"];
|
|
278
|
-
|
|
279
|
-
// Normalize output path
|
|
280
|
-
outputPath = path.resolve(outputPath);
|
|
281
|
-
|
|
282
|
-
let data = JSON.stringify(results, null, 2);
|
|
283
|
-
let outputFile = "";
|
|
284
|
-
let outputDir = "";
|
|
285
|
-
let reportType = "doc-detective-results";
|
|
286
|
-
if (options.command) {
|
|
287
|
-
if (options.command === "runCoverage") {
|
|
288
|
-
reportType = "coverageResults";
|
|
289
|
-
} else if (options.command === "runTests") {
|
|
290
|
-
reportType = "testResults";
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Detect if output ends with a supported extension
|
|
295
|
-
if (outputExtensions.some((ext) => outputPath.endsWith(ext))) {
|
|
296
|
-
outputDir = path.dirname(outputPath);
|
|
297
|
-
outputFile = outputPath;
|
|
298
|
-
// If outputFile already exists, add a counter to the filename
|
|
299
|
-
if (fs.existsSync(outputFile)) {
|
|
300
|
-
let counter = 0;
|
|
301
|
-
while (fs.existsSync(outputFile.replace(".json", `-${counter}.json`))) {
|
|
302
|
-
counter++;
|
|
303
|
-
}
|
|
304
|
-
outputFile = outputFile.replace(".json", `-${counter}.json`);
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
outputDir = outputPath;
|
|
308
|
-
outputFile = path.resolve(outputDir, `${reportType}-${Date.now()}.json`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
// Create output directory if it doesn't exist
|
|
313
|
-
if (!fs.existsSync(outputDir)) {
|
|
314
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Write results to output file
|
|
318
|
-
fs.writeFileSync(outputFile, data);
|
|
319
|
-
console.log(`See detailed results at ${outputFile}\n`);
|
|
320
|
-
return outputFile;
|
|
321
|
-
} catch (err) {
|
|
322
|
-
console.error(`Error writing results to ${outputFile}. ${err}`);
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
// Terminal reporter: outputs a summary to the terminal
|
|
328
|
-
terminalReporter: async (config = {}, outputPath, results, options = {}) => {
|
|
329
|
-
// Defines colors for terminal output
|
|
330
|
-
const colors = {
|
|
331
|
-
red: "\x1b[31m",
|
|
332
|
-
green: "\x1b[32m",
|
|
333
|
-
yellow: "\x1b[33m",
|
|
334
|
-
cyan: "\x1b[36m",
|
|
335
|
-
reset: "\x1b[0m",
|
|
336
|
-
bold: "\x1b[1m",
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// Check if we have the new results format with summary
|
|
340
|
-
if (!results) {
|
|
341
|
-
console.log("No results available.");
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Handle results that have a summary section
|
|
346
|
-
if (results.summary) {
|
|
347
|
-
// Extract summary data
|
|
348
|
-
const { specs, tests, contexts, steps } = results.summary;
|
|
349
|
-
|
|
350
|
-
// Calculate totals
|
|
351
|
-
const totalSpecs = specs
|
|
352
|
-
? specs.pass + specs.fail + specs.warning + specs.skipped
|
|
353
|
-
: 0;
|
|
354
|
-
const totalTests = tests
|
|
355
|
-
? tests.pass + tests.fail + tests.warning + tests.skipped
|
|
356
|
-
: 0;
|
|
357
|
-
const totalContexts = contexts
|
|
358
|
-
? contexts.pass + contexts.fail + contexts.warning + contexts.skipped
|
|
359
|
-
: 0;
|
|
360
|
-
const totalSteps = steps
|
|
361
|
-
? steps.pass + steps.fail + steps.warning + steps.skipped
|
|
362
|
-
: 0;
|
|
363
|
-
|
|
364
|
-
// Any failures overall?
|
|
365
|
-
const hasFailures =
|
|
366
|
-
(specs && specs.fail > 0) ||
|
|
367
|
-
(tests && tests.fail > 0) ||
|
|
368
|
-
(contexts && contexts.fail > 0) ||
|
|
369
|
-
(steps && steps.fail > 0);
|
|
370
|
-
|
|
371
|
-
// Any skipped overall?
|
|
372
|
-
const allSpecsSkipped =
|
|
373
|
-
specs && specs.pass === 0 && specs.fail === 0 && specs.skipped > 0;
|
|
374
|
-
|
|
375
|
-
console.log(
|
|
376
|
-
`\n${colors.bold}===== Doc Detective Results Summary =====${colors.reset}`
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
// Print specs summary if available
|
|
380
|
-
if (specs) {
|
|
381
|
-
console.log(`\n${colors.bold}Specs:${colors.reset}`);
|
|
382
|
-
console.log(`Total: ${totalSpecs}`);
|
|
383
|
-
if (specs.pass > 0) {
|
|
384
|
-
console.log(`${colors.green}Passed: ${specs.pass}${colors.reset}`);
|
|
385
|
-
} else {
|
|
386
|
-
console.log(`Passed: ${specs.pass}`);
|
|
387
|
-
}
|
|
388
|
-
console.log(
|
|
389
|
-
`${specs.fail > 0 ? colors.red : colors.green}Failed: ${specs.fail}${
|
|
390
|
-
colors.reset
|
|
391
|
-
}`
|
|
392
|
-
);
|
|
393
|
-
if (specs.warning > 0)
|
|
394
|
-
console.log(
|
|
395
|
-
`${colors.yellow}Warnings: ${specs.warning}${colors.reset}`
|
|
396
|
-
);
|
|
397
|
-
if (specs.skipped > 0)
|
|
398
|
-
console.log(
|
|
399
|
-
`${colors.yellow}Skipped: ${specs.skipped}${colors.reset}`
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Print tests summary if available
|
|
404
|
-
if (tests) {
|
|
405
|
-
console.log(`\n${colors.bold}Tests:${colors.reset}`);
|
|
406
|
-
console.log(`Total: ${totalTests}`);
|
|
407
|
-
if (tests.pass > 0) {
|
|
408
|
-
console.log(`${colors.green}Passed: ${tests.pass}${colors.reset}`);
|
|
409
|
-
} else {
|
|
410
|
-
console.log(`Passed: ${tests.pass}`);
|
|
411
|
-
}
|
|
412
|
-
console.log(
|
|
413
|
-
`${tests.fail > 0 ? colors.red : colors.green}Failed: ${tests.fail}${
|
|
414
|
-
colors.reset
|
|
415
|
-
}`
|
|
416
|
-
);
|
|
417
|
-
if (tests.warning > 0)
|
|
418
|
-
console.log(
|
|
419
|
-
`${colors.yellow}Warnings: ${tests.warning}${colors.reset}`
|
|
420
|
-
);
|
|
421
|
-
if (tests.skipped > 0)
|
|
422
|
-
console.log(
|
|
423
|
-
`${colors.yellow}Skipped: ${tests.skipped}${colors.reset}`
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Print contexts summary if available
|
|
428
|
-
if (contexts) {
|
|
429
|
-
console.log(`\n${colors.bold}Contexts:${colors.reset}`);
|
|
430
|
-
console.log(`Total: ${totalContexts}`);
|
|
431
|
-
if (contexts.pass > 0) {
|
|
432
|
-
console.log(`${colors.green}Passed: ${contexts.pass}${colors.reset}`);
|
|
433
|
-
} else {
|
|
434
|
-
console.log(`Passed: ${contexts.pass}`);
|
|
435
|
-
}
|
|
436
|
-
console.log(
|
|
437
|
-
`${contexts.fail > 0 ? colors.red : colors.green}Failed: ${
|
|
438
|
-
contexts.fail
|
|
439
|
-
}${colors.reset}`
|
|
440
|
-
);
|
|
441
|
-
if (contexts.warning > 0)
|
|
442
|
-
console.log(
|
|
443
|
-
`${colors.yellow}Warnings: ${contexts.warning}${colors.reset}`
|
|
444
|
-
);
|
|
445
|
-
if (contexts.skipped > 0)
|
|
446
|
-
console.log(
|
|
447
|
-
`${colors.yellow}Skipped: ${contexts.skipped}${colors.reset}`
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Print steps summary if available
|
|
452
|
-
if (steps) {
|
|
453
|
-
console.log(`\n${colors.bold}Steps:${colors.reset}`);
|
|
454
|
-
console.log(`Total: ${totalSteps}`);
|
|
455
|
-
if (steps.pass > 0) {
|
|
456
|
-
console.log(`${colors.green}Passed: ${steps.pass}${colors.reset}`);
|
|
457
|
-
} else {
|
|
458
|
-
console.log(`Passed: ${steps.pass}`);
|
|
459
|
-
}
|
|
460
|
-
console.log(
|
|
461
|
-
`${steps.fail > 0 ? colors.red : colors.green}Failed: ${steps.fail}${
|
|
462
|
-
colors.reset
|
|
463
|
-
}`
|
|
464
|
-
);
|
|
465
|
-
if (steps.warning > 0)
|
|
466
|
-
console.log(
|
|
467
|
-
`${colors.yellow}Warnings: ${steps.warning}${colors.reset}`
|
|
468
|
-
);
|
|
469
|
-
if (steps.skipped > 0)
|
|
470
|
-
console.log(
|
|
471
|
-
`${colors.yellow}Skipped: ${steps.skipped}${colors.reset}`
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// If all specs were skipped, call it out
|
|
476
|
-
if (allSpecsSkipped) {
|
|
477
|
-
console.log(
|
|
478
|
-
`\n${colors.yellow}⚠️ All items were skipped. No specs passed or failed. ⚠️${colors.reset}`
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// If we have specs with failures, display them
|
|
483
|
-
if (results.specs && hasFailures) {
|
|
484
|
-
console.log(
|
|
485
|
-
`\n${colors.bold}${colors.red}Failed Items:${colors.reset}`
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
// Collect failures
|
|
489
|
-
const failedSpecs = [];
|
|
490
|
-
const failedTests = [];
|
|
491
|
-
const failedContexts = [];
|
|
492
|
-
const failedSteps = [];
|
|
493
|
-
|
|
494
|
-
// Collect skipped
|
|
495
|
-
const skippedSpecs = [];
|
|
496
|
-
const skippedTests = [];
|
|
497
|
-
const skippedContexts = [];
|
|
498
|
-
const skippedSteps = [];
|
|
499
|
-
|
|
500
|
-
// Process specs array to collect failures and skipped
|
|
501
|
-
results.specs.forEach((spec, specIndex) => {
|
|
502
|
-
// Check if spec has failed
|
|
503
|
-
if (spec.result === "FAIL") {
|
|
504
|
-
failedSpecs.push({
|
|
505
|
-
index: specIndex,
|
|
506
|
-
id: spec.specId || `Spec ${specIndex + 1}`,
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
// Check if spec was skipped
|
|
510
|
-
if (spec.result === "SKIPPED") {
|
|
511
|
-
skippedSpecs.push({
|
|
512
|
-
index: specIndex,
|
|
513
|
-
id: spec.specId || `Spec ${specIndex + 1}`,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Process tests in this spec
|
|
518
|
-
if (spec.tests && spec.tests.length > 0) {
|
|
519
|
-
spec.tests.forEach((test, testIndex) => {
|
|
520
|
-
// Check if test has failed
|
|
521
|
-
if (test.result === "FAIL") {
|
|
522
|
-
failedTests.push({
|
|
523
|
-
specIndex,
|
|
524
|
-
testIndex,
|
|
525
|
-
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
526
|
-
id: test.testId || `Test ${testIndex + 1}`,
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
// Check if test was skipped
|
|
530
|
-
if (test.result === "SKIPPED") {
|
|
531
|
-
skippedTests.push({
|
|
532
|
-
specIndex,
|
|
533
|
-
testIndex,
|
|
534
|
-
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
535
|
-
id: test.testId || `Test ${testIndex + 1}`,
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Process contexts in this test
|
|
540
|
-
if (test.contexts && test.contexts.length > 0) {
|
|
541
|
-
test.contexts.forEach((context, contextIndex) => {
|
|
542
|
-
// Check if context has failed
|
|
543
|
-
if (
|
|
544
|
-
context.result === "FAIL" ||
|
|
545
|
-
(context.result && context.result.status === "FAIL")
|
|
546
|
-
) {
|
|
547
|
-
failedContexts.push({
|
|
548
|
-
specIndex,
|
|
549
|
-
testIndex,
|
|
550
|
-
contextIndex,
|
|
551
|
-
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
552
|
-
testId: test.testId || `Test ${testIndex + 1}`,
|
|
553
|
-
platform: context.platform || "unknown",
|
|
554
|
-
browser: context.browser
|
|
555
|
-
? context.browser.name
|
|
556
|
-
: "unknown",
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
// Check if context was skipped
|
|
560
|
-
if (
|
|
561
|
-
context.result === "SKIPPED" ||
|
|
562
|
-
(context.result && context.result.status === "SKIPPED")
|
|
563
|
-
) {
|
|
564
|
-
skippedContexts.push({
|
|
565
|
-
specIndex,
|
|
566
|
-
testIndex,
|
|
567
|
-
contextIndex,
|
|
568
|
-
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
569
|
-
testId: test.testId || `Test ${testIndex + 1}`,
|
|
570
|
-
platform: context.platform || "unknown",
|
|
571
|
-
browser: context.browser
|
|
572
|
-
? context.browser.name
|
|
573
|
-
: "unknown",
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Process steps in this context
|
|
578
|
-
if (context.steps && context.steps.length > 0) {
|
|
579
|
-
context.steps.forEach((step, stepIndex) => {
|
|
580
|
-
// Check if step has failed
|
|
581
|
-
if (step.result === "FAIL") {
|
|
582
|
-
failedSteps.push({
|
|
583
|
-
specIndex,
|
|
584
|
-
testIndex,
|
|
585
|
-
contextIndex,
|
|
586
|
-
stepIndex,
|
|
587
|
-
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
588
|
-
testId: test.testId || `Test ${testIndex + 1}`,
|
|
589
|
-
platform: context.platform || "unknown",
|
|
590
|
-
browser: context.browser
|
|
591
|
-
? context.browser.name
|
|
592
|
-
: "unknown",
|
|
593
|
-
stepId: step.stepId || `Step ${stepIndex + 1}`,
|
|
594
|
-
error: step.resultDescription || "Unknown error",
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
// Check if step was skipped
|
|
598
|
-
if (step.result === "SKIPPED") {
|
|
599
|
-
skippedSteps.push({
|
|
600
|
-
specIndex,
|
|
601
|
-
testIndex,
|
|
602
|
-
contextIndex,
|
|
603
|
-
stepIndex,
|
|
604
|
-
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
605
|
-
testId: test.testId || `Test ${testIndex + 1}`,
|
|
606
|
-
platform: context.platform || "unknown",
|
|
607
|
-
browser: context.browser
|
|
608
|
-
? context.browser.name
|
|
609
|
-
: "unknown",
|
|
610
|
-
stepId: step.stepId || `Step ${stepIndex + 1}`,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
// Display failures
|
|
622
|
-
if (failedSpecs.length > 0) {
|
|
623
|
-
console.log(`\n${colors.red}Failed Specs:${colors.reset}`);
|
|
624
|
-
failedSpecs.forEach((item, i) => {
|
|
625
|
-
console.log(`${colors.red}${i + 1}. ${item.id}${colors.reset}`);
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (failedTests.length > 0) {
|
|
630
|
-
console.log(`\n${colors.red}Failed Tests:${colors.reset}`);
|
|
631
|
-
failedTests.forEach((item, i) => {
|
|
632
|
-
console.log(
|
|
633
|
-
`${colors.red}${i + 1}. ${item.id} (from ${item.specId})${
|
|
634
|
-
colors.reset
|
|
635
|
-
}`
|
|
636
|
-
);
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (failedContexts.length > 0) {
|
|
641
|
-
console.log(`\n${colors.red}Failed Contexts:${colors.reset}`);
|
|
642
|
-
failedContexts.forEach((item, i) => {
|
|
643
|
-
console.log(
|
|
644
|
-
`${colors.red}${i + 1}. ${item.platform}/${item.browser} (from ${
|
|
645
|
-
item.testId
|
|
646
|
-
})${colors.reset}`
|
|
647
|
-
);
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (failedSteps.length > 0) {
|
|
652
|
-
console.log(`\n${colors.red}Failed Steps:${colors.reset}`);
|
|
653
|
-
failedSteps.forEach((item, i) => {
|
|
654
|
-
console.log(
|
|
655
|
-
`${colors.red}${i + 1}. ${item.platform}/${item.browser} - ${
|
|
656
|
-
item.stepId
|
|
657
|
-
}${colors.reset}`
|
|
658
|
-
);
|
|
659
|
-
console.log(` Error: ${item.error}`);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Display skipped items in yellow
|
|
664
|
-
if (skippedSpecs.length > 0) {
|
|
665
|
-
console.log(`\n${colors.yellow}Skipped Specs:${colors.reset}`);
|
|
666
|
-
skippedSpecs.forEach((item, i) => {
|
|
667
|
-
console.log(`${colors.yellow}${i + 1}. ${item.id}${colors.reset}`);
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
if (skippedTests.length > 0) {
|
|
671
|
-
console.log(`\n${colors.yellow}Skipped Tests:${colors.reset}`);
|
|
672
|
-
skippedTests.forEach((item, i) => {
|
|
673
|
-
console.log(
|
|
674
|
-
`${colors.yellow}${i + 1}. ${item.id} (from ${item.specId})${
|
|
675
|
-
colors.reset
|
|
676
|
-
}`
|
|
677
|
-
);
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
if (skippedContexts.length > 0) {
|
|
681
|
-
console.log(`\n${colors.yellow}Skipped Contexts:${colors.reset}`);
|
|
682
|
-
skippedContexts.forEach((item, i) => {
|
|
683
|
-
console.log(
|
|
684
|
-
`${colors.yellow}${i + 1}. ${item.platform}/${
|
|
685
|
-
item.browser
|
|
686
|
-
} (from ${item.testId})${colors.reset}`
|
|
687
|
-
);
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
if (skippedSteps.length > 0) {
|
|
691
|
-
console.log(`\n${colors.yellow}Skipped Steps:${colors.reset}`);
|
|
692
|
-
skippedSteps.forEach((item, i) => {
|
|
693
|
-
console.log(
|
|
694
|
-
`${colors.yellow}${i + 1}. ${item.platform}/${item.browser} - ${
|
|
695
|
-
item.stepId
|
|
696
|
-
}${colors.reset}`
|
|
697
|
-
);
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
} else if (!hasFailures && !allSpecsSkipped) {
|
|
701
|
-
// Celebration when all tests pass
|
|
702
|
-
console.log(`\n${colors.green}🎉 All items passed! 🎉${colors.reset}`);
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
console.log(
|
|
706
|
-
"No tests were executed or results are in an unknown format."
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
console.log("\n===============================\n");
|
|
711
|
-
},
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
// Export reporters for external use
|
|
715
|
-
exports.reporters = reporters;
|
|
716
|
-
|
|
717
|
-
// Helper function to register custom reporters
|
|
718
|
-
function registerReporter(name, reporterFunction) {
|
|
719
|
-
if (typeof reporterFunction !== "function") {
|
|
720
|
-
throw new Error("Reporter must be a function");
|
|
721
|
-
}
|
|
722
|
-
reporters[name] = reporterFunction;
|
|
723
|
-
return true;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Export the registerReporter function
|
|
727
|
-
exports.registerReporter = registerReporter;
|
|
728
|
-
|
|
729
|
-
async function reportResults({ apiConfig, results }) {
|
|
730
|
-
// Transform results into the required format for the API
|
|
731
|
-
// Extract contexts from the nested structure and format them
|
|
732
|
-
const contexts = [];
|
|
733
|
-
|
|
734
|
-
if (results.specs) {
|
|
735
|
-
results.specs.forEach((spec) => {
|
|
736
|
-
if (spec.tests) {
|
|
737
|
-
spec.tests.forEach((test) => {
|
|
738
|
-
if (test.contexts) {
|
|
739
|
-
test.contexts.forEach((context) => {
|
|
740
|
-
// Extract or generate contextId
|
|
741
|
-
const contextId =
|
|
742
|
-
context.contextId;
|
|
743
|
-
|
|
744
|
-
// Convert result status to lowercase (PASS -> passed, FAIL -> failed, etc.)
|
|
745
|
-
let status;
|
|
746
|
-
if (context.result === "PASS") {
|
|
747
|
-
status = "passed";
|
|
748
|
-
} else if (context.result === "FAIL") {
|
|
749
|
-
status = "failed";
|
|
750
|
-
} else if (context.result === "WARNING") {
|
|
751
|
-
status = "warning";
|
|
752
|
-
} else if (context.result === "SKIPPED") {
|
|
753
|
-
status = "skipped";
|
|
754
|
-
}
|
|
755
|
-
if (!status) {
|
|
756
|
-
log(config, "error", `Unknown context result status for context ID ${contextId}`);
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Build the context payload with the entire context object embedded
|
|
761
|
-
contexts.push({
|
|
762
|
-
contextId: contextId,
|
|
763
|
-
status: status,
|
|
764
|
-
result: context,
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// POST to the /contexts endpoint
|
|
774
|
-
try {
|
|
775
|
-
const url = `${apiConfig.url}/contexts`;
|
|
776
|
-
const payload = { contexts };
|
|
777
|
-
|
|
778
|
-
console.log(payload);
|
|
779
|
-
|
|
780
|
-
const response = await axios.post(url, payload, {
|
|
781
|
-
headers: {
|
|
782
|
-
"x-runner-token": apiConfig.token,
|
|
783
|
-
},
|
|
784
|
-
});
|
|
785
|
-
console.log("Results reported successfully:", response.data);
|
|
786
|
-
} catch (error) {
|
|
787
|
-
console.error(
|
|
788
|
-
`Error reporting results to ${apiConfig.url}/contexts: ${error.message}`
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
async function outputResults(config = {}, outputPath, results, options = {}) {
|
|
794
|
-
// Default to using both built-in reporters if none specified
|
|
795
|
-
const defaultReporters = ["terminal", "json"];
|
|
796
|
-
|
|
797
|
-
let activeReporters = options.reporters || defaultReporters;
|
|
798
|
-
|
|
799
|
-
// If the reporters option is provided as strings, normalize them
|
|
800
|
-
if (activeReporters.length > 0) {
|
|
801
|
-
// Convert any shorthand names to full reporter names
|
|
802
|
-
activeReporters = activeReporters.map((reporter) => {
|
|
803
|
-
if (typeof reporter === "string") {
|
|
804
|
-
// Convert shorthand names to actual reporter keys
|
|
805
|
-
switch (reporter.toLowerCase()) {
|
|
806
|
-
case "json":
|
|
807
|
-
return "jsonReporter";
|
|
808
|
-
case "terminal":
|
|
809
|
-
return "terminalReporter";
|
|
810
|
-
default:
|
|
811
|
-
return reporter;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return reporter;
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Execute each reporter
|
|
819
|
-
const reporterPromises = activeReporters.map((reporter) => {
|
|
820
|
-
if (typeof reporter === "function") {
|
|
821
|
-
// Direct function reference
|
|
822
|
-
return reporter(config, outputPath, results, options);
|
|
823
|
-
} else if (typeof reporter === "string" && reporters[reporter]) {
|
|
824
|
-
// String reference to built-in or registered reporter
|
|
825
|
-
return reporters[reporter](config, outputPath, results, options);
|
|
826
|
-
} else if (typeof reporter === "string" && !reporters[reporter]) {
|
|
827
|
-
console.error(
|
|
828
|
-
`Reporter "${reporter}" not found. Available reporters: ${Object.keys(
|
|
829
|
-
reporters
|
|
830
|
-
).join(", ")}`
|
|
831
|
-
);
|
|
832
|
-
return Promise.resolve();
|
|
833
|
-
} else {
|
|
834
|
-
console.error(`Invalid reporter: ${reporter}`);
|
|
835
|
-
return Promise.resolve();
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// Wait for all reporters to complete
|
|
840
|
-
return Promise.all(reporterPromises);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Perform a native command in the current working directory.
|
|
844
|
-
async function spawnCommand(cmd, args) {
|
|
845
|
-
// Split command into command and arguments
|
|
846
|
-
if (cmd.includes(" ")) {
|
|
847
|
-
const cmdArray = cmd.split(" ");
|
|
848
|
-
cmd = cmdArray[0];
|
|
849
|
-
cmdArgs = cmdArray.slice(1);
|
|
850
|
-
// Add arguments to args array
|
|
851
|
-
if (args) {
|
|
852
|
-
args = cmdArgs.concat(args);
|
|
853
|
-
} else {
|
|
854
|
-
args = cmdArgs;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const runCommand = spawn(cmd, args, {
|
|
859
|
-
env: process.env, // Explicitly pass environment variables
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
// Capture stdout
|
|
863
|
-
let stdout = "";
|
|
864
|
-
for await (const chunk of runCommand.stdout) {
|
|
865
|
-
stdout += chunk;
|
|
866
|
-
}
|
|
867
|
-
// Remove trailing newline
|
|
868
|
-
stdout = stdout.replace(/\n$/, "");
|
|
869
|
-
|
|
870
|
-
// Capture stderr
|
|
871
|
-
let stderr = "";
|
|
872
|
-
for await (const chunk of runCommand.stderr) {
|
|
873
|
-
stderr += chunk;
|
|
874
|
-
}
|
|
875
|
-
// Remove trailing newline
|
|
876
|
-
stderr = stderr.replace(/\n$/, "");
|
|
877
|
-
|
|
878
|
-
// Capture exit code
|
|
879
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
880
|
-
runCommand.on("close", resolve);
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
return { stdout, stderr, exitCode };
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function setMeta() {
|
|
887
|
-
const platformMap = {
|
|
888
|
-
win32: "windows",
|
|
889
|
-
darwin: "mac",
|
|
890
|
-
linux: "linux",
|
|
891
|
-
};
|
|
892
|
-
|
|
893
|
-
// Set meta
|
|
894
|
-
const meta =
|
|
895
|
-
process.env["DOC_DETECTIVE_META"] !== undefined
|
|
896
|
-
? JSON.parse(process.env["DOC_DETECTIVE_META"])
|
|
897
|
-
: {};
|
|
898
|
-
const package = require("../package.json");
|
|
899
|
-
meta.distribution = "doc-detective";
|
|
900
|
-
meta.dist_version = package.version;
|
|
901
|
-
meta.dist_platform = platformMap[os.platform()] || os.platform();
|
|
902
|
-
meta.dist_platform_version = os.release();
|
|
903
|
-
meta.dist_platform_arch = os.arch();
|
|
904
|
-
meta.dist_deployment = meta.dist_deployment || "node";
|
|
905
|
-
meta.dist_deployment_version =
|
|
906
|
-
meta.dist_deployment_version || process.version;
|
|
907
|
-
meta.dist_interface = meta.dist_interface || "cli";
|
|
908
|
-
process.env["DOC_DETECTIVE_META"] = JSON.stringify(meta);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// Get version data programmatically (no console output)
|
|
912
|
-
function getVersionData() {
|
|
913
|
-
try {
|
|
914
|
-
// Get main package version
|
|
915
|
-
const mainPackage = require("../package.json");
|
|
916
|
-
const versionData = {
|
|
917
|
-
main: {
|
|
918
|
-
"doc-detective": {
|
|
919
|
-
version: mainPackage.version,
|
|
920
|
-
expected: "main package",
|
|
921
|
-
},
|
|
922
|
-
},
|
|
923
|
-
dependencies: {},
|
|
924
|
-
context: {
|
|
925
|
-
executionMethod: "direct node execution",
|
|
926
|
-
nodeVersion: process.version,
|
|
927
|
-
platform: `${os.platform()} ${os.arch()}`,
|
|
928
|
-
timestamp: new Date().toISOString(),
|
|
929
|
-
},
|
|
930
|
-
locations: {},
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
// Auto-discover all doc-detective-* packages in node_modules
|
|
934
|
-
const nodeModulesPath = path.resolve(process.cwd(), "node_modules");
|
|
935
|
-
const dependenciesToCheck = [];
|
|
936
|
-
|
|
937
|
-
if (fs.existsSync(nodeModulesPath)) {
|
|
938
|
-
const nodeModulesContents = fs.readdirSync(nodeModulesPath);
|
|
939
|
-
nodeModulesContents.forEach((dir) => {
|
|
940
|
-
if (dir.startsWith("doc-detective-") && dir !== "doc-detective") {
|
|
941
|
-
dependenciesToCheck.push(dir);
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// Detect execution method
|
|
947
|
-
const isNpx =
|
|
948
|
-
process.env.npm_execpath && process.env.npm_execpath.includes("npx");
|
|
949
|
-
const isNpm = process.env.npm_execpath && !isNpx;
|
|
950
|
-
|
|
951
|
-
if (isNpx) {
|
|
952
|
-
versionData.context.executionMethod = "npx";
|
|
953
|
-
} else if (isNpm) {
|
|
954
|
-
versionData.context.executionMethod = "npm";
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Check installed versions of dependencies
|
|
958
|
-
dependenciesToCheck.sort().forEach((dep) => {
|
|
959
|
-
try {
|
|
960
|
-
// Try to read the dependency's package.json
|
|
961
|
-
const depPackagePath = path.resolve(
|
|
962
|
-
process.cwd(),
|
|
963
|
-
"node_modules",
|
|
964
|
-
dep,
|
|
965
|
-
"package.json"
|
|
966
|
-
);
|
|
967
|
-
if (fs.existsSync(depPackagePath)) {
|
|
968
|
-
const depPackage = JSON.parse(
|
|
969
|
-
fs.readFileSync(depPackagePath, "utf8")
|
|
970
|
-
);
|
|
971
|
-
const installedVersion = depPackage.version;
|
|
972
|
-
|
|
973
|
-
// Look for expected version in main package dependencies or devDependencies
|
|
974
|
-
const expectedVersion =
|
|
975
|
-
mainPackage.dependencies[dep] ||
|
|
976
|
-
mainPackage.devDependencies[dep] ||
|
|
977
|
-
"not specified in main package";
|
|
978
|
-
|
|
979
|
-
versionData.dependencies[dep] = {
|
|
980
|
-
installed: installedVersion,
|
|
981
|
-
expected: expectedVersion,
|
|
982
|
-
status:
|
|
983
|
-
expectedVersion !== "not specified in main package" &&
|
|
984
|
-
!expectedVersion.includes(installedVersion) &&
|
|
985
|
-
!installedVersion.includes(expectedVersion.replace(/[\^~]/, ""))
|
|
986
|
-
? "mismatch"
|
|
987
|
-
: "ok",
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
versionData.locations[dep] = path.resolve(
|
|
991
|
-
process.cwd(),
|
|
992
|
-
"node_modules",
|
|
993
|
-
dep
|
|
994
|
-
);
|
|
995
|
-
} else {
|
|
996
|
-
versionData.dependencies[dep] = {
|
|
997
|
-
installed: null,
|
|
998
|
-
expected:
|
|
999
|
-
mainPackage.dependencies[dep] ||
|
|
1000
|
-
mainPackage.devDependencies[dep] ||
|
|
1001
|
-
"not specified",
|
|
1002
|
-
status: "not found",
|
|
1003
|
-
error: "package.json not found",
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
} catch (error) {
|
|
1007
|
-
versionData.dependencies[dep] = {
|
|
1008
|
-
installed: null,
|
|
1009
|
-
expected:
|
|
1010
|
-
mainPackage.dependencies[dep] ||
|
|
1011
|
-
mainPackage.devDependencies[dep] ||
|
|
1012
|
-
"not specified",
|
|
1013
|
-
status: "error",
|
|
1014
|
-
error: error.message,
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
return versionData;
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
return { error: error.message };
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1
|
+
const yargs = require("yargs/yargs");
|
|
2
|
+
const { hideBin } = require("yargs/helpers");
|
|
3
|
+
const { validate, resolvePaths, readFile } = require("doc-detective-common");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { spawn } = require("child_process");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const axios = require("axios");
|
|
9
|
+
|
|
10
|
+
exports.setArgs = setArgs;
|
|
11
|
+
exports.setConfig = setConfig;
|
|
12
|
+
exports.outputResults = outputResults;
|
|
13
|
+
exports.spawnCommand = spawnCommand;
|
|
14
|
+
exports.setMeta = setMeta;
|
|
15
|
+
exports.getVersionData = getVersionData;
|
|
16
|
+
exports.log = log;
|
|
17
|
+
exports.getResolvedTestsFromEnv = getResolvedTestsFromEnv;
|
|
18
|
+
exports.reportResults = reportResults;
|
|
19
|
+
|
|
20
|
+
// Log function that respects logLevel
|
|
21
|
+
function log(message, level = "info", config = {}) {
|
|
22
|
+
const logLevels = ["silent", "error", "warning", "info", "debug"];
|
|
23
|
+
const currentLevel = config.logLevel || "info";
|
|
24
|
+
const currentLevelIndex = logLevels.indexOf(currentLevel);
|
|
25
|
+
const messageLevelIndex = logLevels.indexOf(level);
|
|
26
|
+
|
|
27
|
+
// Only log if the message level is at or above the current log level
|
|
28
|
+
if (currentLevelIndex >= messageLevelIndex && messageLevelIndex > 0) {
|
|
29
|
+
if (level === "error") {
|
|
30
|
+
console.error(message);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Define args
|
|
38
|
+
function setArgs(args) {
|
|
39
|
+
if (!args) return {};
|
|
40
|
+
let argv = yargs(hideBin(args))
|
|
41
|
+
.option("config", {
|
|
42
|
+
alias: "c",
|
|
43
|
+
description: "Path to a `config.json` or `config.yaml` file.",
|
|
44
|
+
type: "string",
|
|
45
|
+
})
|
|
46
|
+
.option("input", {
|
|
47
|
+
alias: "i",
|
|
48
|
+
description:
|
|
49
|
+
"Path to test specifications and documentation source files. May be paths to specific files or to directories to scan for files.",
|
|
50
|
+
type: "string",
|
|
51
|
+
})
|
|
52
|
+
.option("output", {
|
|
53
|
+
alias: "o",
|
|
54
|
+
description:
|
|
55
|
+
"Path of the directory in which to store the output of Doc Detective commands.",
|
|
56
|
+
type: "string",
|
|
57
|
+
})
|
|
58
|
+
.option("logLevel", {
|
|
59
|
+
alias: "l",
|
|
60
|
+
description:
|
|
61
|
+
"Detail level of logging events. Accepted values: silent, error, warning, info (default), debug",
|
|
62
|
+
type: "string",
|
|
63
|
+
})
|
|
64
|
+
.option("allow-unsafe", {
|
|
65
|
+
description: "Allow execution of potentially unsafe tests",
|
|
66
|
+
type: "boolean",
|
|
67
|
+
})
|
|
68
|
+
.help()
|
|
69
|
+
.alias("help", "h").argv;
|
|
70
|
+
|
|
71
|
+
return argv;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get resolved tests from environment variable, if set
|
|
75
|
+
async function getResolvedTestsFromEnv(config = {}) {
|
|
76
|
+
if (!process.env.DOC_DETECTIVE_API) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let resolvedTests = null;
|
|
81
|
+
let apiConfig = null;
|
|
82
|
+
try {
|
|
83
|
+
// Parse the environment variable as JSON
|
|
84
|
+
apiConfig = JSON.parse(process.env.DOC_DETECTIVE_API);
|
|
85
|
+
|
|
86
|
+
// Validate the structure: { accountId, url, token, contextIds }
|
|
87
|
+
if (!apiConfig.accountId || !apiConfig.url || !apiConfig.token || !apiConfig.contextIds) {
|
|
88
|
+
log(
|
|
89
|
+
"Invalid DOC_DETECTIVE_API: must contain 'accountId', 'url', 'token', and 'contextIds' properties",
|
|
90
|
+
"error",
|
|
91
|
+
config
|
|
92
|
+
);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log(`CLI:Fetching resolved tests from ${apiConfig.url}/resolved-tests`, "debug", config);
|
|
97
|
+
|
|
98
|
+
// Make GET request to the specified URL with token in header
|
|
99
|
+
const response = await axios.get(`${apiConfig.url}/resolved-tests`, {
|
|
100
|
+
headers: {
|
|
101
|
+
"x-runner-token": apiConfig.token,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// The response is the resolvedTests
|
|
106
|
+
resolvedTests = response.data;
|
|
107
|
+
|
|
108
|
+
// Validate against resolvedTests_v3 schema
|
|
109
|
+
const validation = validate({
|
|
110
|
+
schemaKey: "resolvedTests_v3",
|
|
111
|
+
object: resolvedTests,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!validation.valid) {
|
|
115
|
+
log(
|
|
116
|
+
"Invalid resolvedTests from API response. " + validation.errors,
|
|
117
|
+
"error",
|
|
118
|
+
config
|
|
119
|
+
);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get config from environment variable for merging
|
|
124
|
+
const envConfig = await getConfigFromEnv();
|
|
125
|
+
if (envConfig) {
|
|
126
|
+
// Apply config overrides to resolvedTests.config
|
|
127
|
+
if (resolvedTests.config) {
|
|
128
|
+
resolvedTests.config = { ...resolvedTests.config, ...envConfig };
|
|
129
|
+
} else {
|
|
130
|
+
resolvedTests.config = envConfig;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
log(
|
|
135
|
+
`CLI:RESOLVED_TESTS:\n${JSON.stringify(resolvedTests, null, 2)}`,
|
|
136
|
+
"debug",
|
|
137
|
+
config
|
|
138
|
+
);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
log(
|
|
141
|
+
`Error fetching resolved tests from DOC_DETECTIVE_API: ${error.message}`,
|
|
142
|
+
"error",
|
|
143
|
+
config
|
|
144
|
+
);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
return { apiConfig, resolvedTests };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function getConfigFromEnv() {
|
|
151
|
+
if (!process.env.DOC_DETECTIVE_CONFIG) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let envConfig = null;
|
|
156
|
+
try {
|
|
157
|
+
// Parse the environment variable as JSON
|
|
158
|
+
envConfig = JSON.parse(process.env.DOC_DETECTIVE_CONFIG);
|
|
159
|
+
|
|
160
|
+
// Validate the environment variable config
|
|
161
|
+
const envValidation = validate({
|
|
162
|
+
schemaKey: "config_v3",
|
|
163
|
+
object: envConfig,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!envValidation.valid) {
|
|
167
|
+
console.error(
|
|
168
|
+
"Invalid config from DOC_DETECTIVE_CONFIG environment variable.",
|
|
169
|
+
envValidation.errors
|
|
170
|
+
);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
log(`CLI:ENV_CONFIG:\n${JSON.stringify(envConfig, null, 2)}`, "debug", envConfig);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(
|
|
177
|
+
`Error parsing DOC_DETECTIVE_CONFIG environment variable: ${error.message}`
|
|
178
|
+
);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
return envConfig;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Override config values based on args and validate the config
|
|
185
|
+
async function setConfig({ configPath, args }) {
|
|
186
|
+
if (args.config && !configPath) {
|
|
187
|
+
configPath = args.config;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If config file exists, read it
|
|
191
|
+
let config = {};
|
|
192
|
+
if (configPath) {
|
|
193
|
+
try {
|
|
194
|
+
config = await readFile({ fileURLOrPath: configPath });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(`Error reading config file at ${configPath}: ${error}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for DOC_DETECTIVE_CONFIG environment variable
|
|
202
|
+
const envConfig = await getConfigFromEnv();
|
|
203
|
+
if (envConfig) {
|
|
204
|
+
// Merge with file config, preferring environment variable config (use raw envConfig, not validated with defaults)
|
|
205
|
+
config = { ...config, ...envConfig };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Validate config
|
|
209
|
+
const validation = validate({
|
|
210
|
+
schemaKey: "config_v3",
|
|
211
|
+
object: config,
|
|
212
|
+
});
|
|
213
|
+
if (!validation.valid) {
|
|
214
|
+
// Output validation errors
|
|
215
|
+
console.error("Invalid config.", validation.errors);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Accept coerced and defaulted values
|
|
220
|
+
config = validation.object;
|
|
221
|
+
// Set default values
|
|
222
|
+
config = {
|
|
223
|
+
...config,
|
|
224
|
+
input: config.input || ".",
|
|
225
|
+
output: config.output || ".",
|
|
226
|
+
recursive: config.recursive ?? true,
|
|
227
|
+
relativePathBase: config.relativePathBase || "file",
|
|
228
|
+
loadVariables: config.loadVariables || ".env",
|
|
229
|
+
detectSteps: config.detectSteps ?? true,
|
|
230
|
+
logLevel: config.logLevel || "info",
|
|
231
|
+
fileTypes: config.fileTypes || ["markdown", "asciidoc", "html"],
|
|
232
|
+
telemetry: config.telemetry || { send: true },
|
|
233
|
+
};
|
|
234
|
+
// Override config values
|
|
235
|
+
if (configPath) {
|
|
236
|
+
config.configPath = configPath;
|
|
237
|
+
}
|
|
238
|
+
if (args.input) {
|
|
239
|
+
// If input includes commas, split it into an array
|
|
240
|
+
args.input = args.input.split(",").map((item) => item.trim());
|
|
241
|
+
// Resolve paths
|
|
242
|
+
args.input = args.input.map((item) => {
|
|
243
|
+
if (item.startsWith("https://") || item.startsWith("http://")) {
|
|
244
|
+
return item; // Don't resolve URLs
|
|
245
|
+
}
|
|
246
|
+
return path.resolve(item);
|
|
247
|
+
});
|
|
248
|
+
// Add to config
|
|
249
|
+
config.input = args.input;
|
|
250
|
+
}
|
|
251
|
+
if (args.output) {
|
|
252
|
+
config.output = path.resolve(args.output);
|
|
253
|
+
}
|
|
254
|
+
if (args.logLevel) {
|
|
255
|
+
config.logLevel = args.logLevel;
|
|
256
|
+
}
|
|
257
|
+
if (typeof args.allowUnsafe === "boolean") {
|
|
258
|
+
config.allowUnsafeSteps = args.allowUnsafe;
|
|
259
|
+
}
|
|
260
|
+
// Resolve paths
|
|
261
|
+
config = await resolvePaths({
|
|
262
|
+
config: config,
|
|
263
|
+
object: config,
|
|
264
|
+
filePath: configPath || ".",
|
|
265
|
+
nested: false,
|
|
266
|
+
objectType: "config",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return config;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Internal reporters
|
|
273
|
+
const reporters = {
|
|
274
|
+
// JSON reporter: outputs results to a JSON file
|
|
275
|
+
jsonReporter: async (config = {}, outputPath, results, options = {}) => {
|
|
276
|
+
// Define supported output extensions
|
|
277
|
+
const outputExtensions = [".json"];
|
|
278
|
+
|
|
279
|
+
// Normalize output path
|
|
280
|
+
outputPath = path.resolve(outputPath);
|
|
281
|
+
|
|
282
|
+
let data = JSON.stringify(results, null, 2);
|
|
283
|
+
let outputFile = "";
|
|
284
|
+
let outputDir = "";
|
|
285
|
+
let reportType = "doc-detective-results";
|
|
286
|
+
if (options.command) {
|
|
287
|
+
if (options.command === "runCoverage") {
|
|
288
|
+
reportType = "coverageResults";
|
|
289
|
+
} else if (options.command === "runTests") {
|
|
290
|
+
reportType = "testResults";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Detect if output ends with a supported extension
|
|
295
|
+
if (outputExtensions.some((ext) => outputPath.endsWith(ext))) {
|
|
296
|
+
outputDir = path.dirname(outputPath);
|
|
297
|
+
outputFile = outputPath;
|
|
298
|
+
// If outputFile already exists, add a counter to the filename
|
|
299
|
+
if (fs.existsSync(outputFile)) {
|
|
300
|
+
let counter = 0;
|
|
301
|
+
while (fs.existsSync(outputFile.replace(".json", `-${counter}.json`))) {
|
|
302
|
+
counter++;
|
|
303
|
+
}
|
|
304
|
+
outputFile = outputFile.replace(".json", `-${counter}.json`);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
outputDir = outputPath;
|
|
308
|
+
outputFile = path.resolve(outputDir, `${reportType}-${Date.now()}.json`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// Create output directory if it doesn't exist
|
|
313
|
+
if (!fs.existsSync(outputDir)) {
|
|
314
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Write results to output file
|
|
318
|
+
fs.writeFileSync(outputFile, data);
|
|
319
|
+
console.log(`See detailed results at ${outputFile}\n`);
|
|
320
|
+
return outputFile;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error(`Error writing results to ${outputFile}. ${err}`);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// Terminal reporter: outputs a summary to the terminal
|
|
328
|
+
terminalReporter: async (config = {}, outputPath, results, options = {}) => {
|
|
329
|
+
// Defines colors for terminal output
|
|
330
|
+
const colors = {
|
|
331
|
+
red: "\x1b[31m",
|
|
332
|
+
green: "\x1b[32m",
|
|
333
|
+
yellow: "\x1b[33m",
|
|
334
|
+
cyan: "\x1b[36m",
|
|
335
|
+
reset: "\x1b[0m",
|
|
336
|
+
bold: "\x1b[1m",
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Check if we have the new results format with summary
|
|
340
|
+
if (!results) {
|
|
341
|
+
console.log("No results available.");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Handle results that have a summary section
|
|
346
|
+
if (results.summary) {
|
|
347
|
+
// Extract summary data
|
|
348
|
+
const { specs, tests, contexts, steps } = results.summary;
|
|
349
|
+
|
|
350
|
+
// Calculate totals
|
|
351
|
+
const totalSpecs = specs
|
|
352
|
+
? specs.pass + specs.fail + specs.warning + specs.skipped
|
|
353
|
+
: 0;
|
|
354
|
+
const totalTests = tests
|
|
355
|
+
? tests.pass + tests.fail + tests.warning + tests.skipped
|
|
356
|
+
: 0;
|
|
357
|
+
const totalContexts = contexts
|
|
358
|
+
? contexts.pass + contexts.fail + contexts.warning + contexts.skipped
|
|
359
|
+
: 0;
|
|
360
|
+
const totalSteps = steps
|
|
361
|
+
? steps.pass + steps.fail + steps.warning + steps.skipped
|
|
362
|
+
: 0;
|
|
363
|
+
|
|
364
|
+
// Any failures overall?
|
|
365
|
+
const hasFailures =
|
|
366
|
+
(specs && specs.fail > 0) ||
|
|
367
|
+
(tests && tests.fail > 0) ||
|
|
368
|
+
(contexts && contexts.fail > 0) ||
|
|
369
|
+
(steps && steps.fail > 0);
|
|
370
|
+
|
|
371
|
+
// Any skipped overall?
|
|
372
|
+
const allSpecsSkipped =
|
|
373
|
+
specs && specs.pass === 0 && specs.fail === 0 && specs.skipped > 0;
|
|
374
|
+
|
|
375
|
+
console.log(
|
|
376
|
+
`\n${colors.bold}===== Doc Detective Results Summary =====${colors.reset}`
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// Print specs summary if available
|
|
380
|
+
if (specs) {
|
|
381
|
+
console.log(`\n${colors.bold}Specs:${colors.reset}`);
|
|
382
|
+
console.log(`Total: ${totalSpecs}`);
|
|
383
|
+
if (specs.pass > 0) {
|
|
384
|
+
console.log(`${colors.green}Passed: ${specs.pass}${colors.reset}`);
|
|
385
|
+
} else {
|
|
386
|
+
console.log(`Passed: ${specs.pass}`);
|
|
387
|
+
}
|
|
388
|
+
console.log(
|
|
389
|
+
`${specs.fail > 0 ? colors.red : colors.green}Failed: ${specs.fail}${
|
|
390
|
+
colors.reset
|
|
391
|
+
}`
|
|
392
|
+
);
|
|
393
|
+
if (specs.warning > 0)
|
|
394
|
+
console.log(
|
|
395
|
+
`${colors.yellow}Warnings: ${specs.warning}${colors.reset}`
|
|
396
|
+
);
|
|
397
|
+
if (specs.skipped > 0)
|
|
398
|
+
console.log(
|
|
399
|
+
`${colors.yellow}Skipped: ${specs.skipped}${colors.reset}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Print tests summary if available
|
|
404
|
+
if (tests) {
|
|
405
|
+
console.log(`\n${colors.bold}Tests:${colors.reset}`);
|
|
406
|
+
console.log(`Total: ${totalTests}`);
|
|
407
|
+
if (tests.pass > 0) {
|
|
408
|
+
console.log(`${colors.green}Passed: ${tests.pass}${colors.reset}`);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(`Passed: ${tests.pass}`);
|
|
411
|
+
}
|
|
412
|
+
console.log(
|
|
413
|
+
`${tests.fail > 0 ? colors.red : colors.green}Failed: ${tests.fail}${
|
|
414
|
+
colors.reset
|
|
415
|
+
}`
|
|
416
|
+
);
|
|
417
|
+
if (tests.warning > 0)
|
|
418
|
+
console.log(
|
|
419
|
+
`${colors.yellow}Warnings: ${tests.warning}${colors.reset}`
|
|
420
|
+
);
|
|
421
|
+
if (tests.skipped > 0)
|
|
422
|
+
console.log(
|
|
423
|
+
`${colors.yellow}Skipped: ${tests.skipped}${colors.reset}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Print contexts summary if available
|
|
428
|
+
if (contexts) {
|
|
429
|
+
console.log(`\n${colors.bold}Contexts:${colors.reset}`);
|
|
430
|
+
console.log(`Total: ${totalContexts}`);
|
|
431
|
+
if (contexts.pass > 0) {
|
|
432
|
+
console.log(`${colors.green}Passed: ${contexts.pass}${colors.reset}`);
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`Passed: ${contexts.pass}`);
|
|
435
|
+
}
|
|
436
|
+
console.log(
|
|
437
|
+
`${contexts.fail > 0 ? colors.red : colors.green}Failed: ${
|
|
438
|
+
contexts.fail
|
|
439
|
+
}${colors.reset}`
|
|
440
|
+
);
|
|
441
|
+
if (contexts.warning > 0)
|
|
442
|
+
console.log(
|
|
443
|
+
`${colors.yellow}Warnings: ${contexts.warning}${colors.reset}`
|
|
444
|
+
);
|
|
445
|
+
if (contexts.skipped > 0)
|
|
446
|
+
console.log(
|
|
447
|
+
`${colors.yellow}Skipped: ${contexts.skipped}${colors.reset}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Print steps summary if available
|
|
452
|
+
if (steps) {
|
|
453
|
+
console.log(`\n${colors.bold}Steps:${colors.reset}`);
|
|
454
|
+
console.log(`Total: ${totalSteps}`);
|
|
455
|
+
if (steps.pass > 0) {
|
|
456
|
+
console.log(`${colors.green}Passed: ${steps.pass}${colors.reset}`);
|
|
457
|
+
} else {
|
|
458
|
+
console.log(`Passed: ${steps.pass}`);
|
|
459
|
+
}
|
|
460
|
+
console.log(
|
|
461
|
+
`${steps.fail > 0 ? colors.red : colors.green}Failed: ${steps.fail}${
|
|
462
|
+
colors.reset
|
|
463
|
+
}`
|
|
464
|
+
);
|
|
465
|
+
if (steps.warning > 0)
|
|
466
|
+
console.log(
|
|
467
|
+
`${colors.yellow}Warnings: ${steps.warning}${colors.reset}`
|
|
468
|
+
);
|
|
469
|
+
if (steps.skipped > 0)
|
|
470
|
+
console.log(
|
|
471
|
+
`${colors.yellow}Skipped: ${steps.skipped}${colors.reset}`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// If all specs were skipped, call it out
|
|
476
|
+
if (allSpecsSkipped) {
|
|
477
|
+
console.log(
|
|
478
|
+
`\n${colors.yellow}⚠️ All items were skipped. No specs passed or failed. ⚠️${colors.reset}`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// If we have specs with failures, display them
|
|
483
|
+
if (results.specs && hasFailures) {
|
|
484
|
+
console.log(
|
|
485
|
+
`\n${colors.bold}${colors.red}Failed Items:${colors.reset}`
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Collect failures
|
|
489
|
+
const failedSpecs = [];
|
|
490
|
+
const failedTests = [];
|
|
491
|
+
const failedContexts = [];
|
|
492
|
+
const failedSteps = [];
|
|
493
|
+
|
|
494
|
+
// Collect skipped
|
|
495
|
+
const skippedSpecs = [];
|
|
496
|
+
const skippedTests = [];
|
|
497
|
+
const skippedContexts = [];
|
|
498
|
+
const skippedSteps = [];
|
|
499
|
+
|
|
500
|
+
// Process specs array to collect failures and skipped
|
|
501
|
+
results.specs.forEach((spec, specIndex) => {
|
|
502
|
+
// Check if spec has failed
|
|
503
|
+
if (spec.result === "FAIL") {
|
|
504
|
+
failedSpecs.push({
|
|
505
|
+
index: specIndex,
|
|
506
|
+
id: spec.specId || `Spec ${specIndex + 1}`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// Check if spec was skipped
|
|
510
|
+
if (spec.result === "SKIPPED") {
|
|
511
|
+
skippedSpecs.push({
|
|
512
|
+
index: specIndex,
|
|
513
|
+
id: spec.specId || `Spec ${specIndex + 1}`,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Process tests in this spec
|
|
518
|
+
if (spec.tests && spec.tests.length > 0) {
|
|
519
|
+
spec.tests.forEach((test, testIndex) => {
|
|
520
|
+
// Check if test has failed
|
|
521
|
+
if (test.result === "FAIL") {
|
|
522
|
+
failedTests.push({
|
|
523
|
+
specIndex,
|
|
524
|
+
testIndex,
|
|
525
|
+
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
526
|
+
id: test.testId || `Test ${testIndex + 1}`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// Check if test was skipped
|
|
530
|
+
if (test.result === "SKIPPED") {
|
|
531
|
+
skippedTests.push({
|
|
532
|
+
specIndex,
|
|
533
|
+
testIndex,
|
|
534
|
+
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
535
|
+
id: test.testId || `Test ${testIndex + 1}`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Process contexts in this test
|
|
540
|
+
if (test.contexts && test.contexts.length > 0) {
|
|
541
|
+
test.contexts.forEach((context, contextIndex) => {
|
|
542
|
+
// Check if context has failed
|
|
543
|
+
if (
|
|
544
|
+
context.result === "FAIL" ||
|
|
545
|
+
(context.result && context.result.status === "FAIL")
|
|
546
|
+
) {
|
|
547
|
+
failedContexts.push({
|
|
548
|
+
specIndex,
|
|
549
|
+
testIndex,
|
|
550
|
+
contextIndex,
|
|
551
|
+
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
552
|
+
testId: test.testId || `Test ${testIndex + 1}`,
|
|
553
|
+
platform: context.platform || "unknown",
|
|
554
|
+
browser: context.browser
|
|
555
|
+
? context.browser.name
|
|
556
|
+
: "unknown",
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
// Check if context was skipped
|
|
560
|
+
if (
|
|
561
|
+
context.result === "SKIPPED" ||
|
|
562
|
+
(context.result && context.result.status === "SKIPPED")
|
|
563
|
+
) {
|
|
564
|
+
skippedContexts.push({
|
|
565
|
+
specIndex,
|
|
566
|
+
testIndex,
|
|
567
|
+
contextIndex,
|
|
568
|
+
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
569
|
+
testId: test.testId || `Test ${testIndex + 1}`,
|
|
570
|
+
platform: context.platform || "unknown",
|
|
571
|
+
browser: context.browser
|
|
572
|
+
? context.browser.name
|
|
573
|
+
: "unknown",
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Process steps in this context
|
|
578
|
+
if (context.steps && context.steps.length > 0) {
|
|
579
|
+
context.steps.forEach((step, stepIndex) => {
|
|
580
|
+
// Check if step has failed
|
|
581
|
+
if (step.result === "FAIL") {
|
|
582
|
+
failedSteps.push({
|
|
583
|
+
specIndex,
|
|
584
|
+
testIndex,
|
|
585
|
+
contextIndex,
|
|
586
|
+
stepIndex,
|
|
587
|
+
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
588
|
+
testId: test.testId || `Test ${testIndex + 1}`,
|
|
589
|
+
platform: context.platform || "unknown",
|
|
590
|
+
browser: context.browser
|
|
591
|
+
? context.browser.name
|
|
592
|
+
: "unknown",
|
|
593
|
+
stepId: step.stepId || `Step ${stepIndex + 1}`,
|
|
594
|
+
error: step.resultDescription || "Unknown error",
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
// Check if step was skipped
|
|
598
|
+
if (step.result === "SKIPPED") {
|
|
599
|
+
skippedSteps.push({
|
|
600
|
+
specIndex,
|
|
601
|
+
testIndex,
|
|
602
|
+
contextIndex,
|
|
603
|
+
stepIndex,
|
|
604
|
+
specId: spec.specId || `Spec ${specIndex + 1}`,
|
|
605
|
+
testId: test.testId || `Test ${testIndex + 1}`,
|
|
606
|
+
platform: context.platform || "unknown",
|
|
607
|
+
browser: context.browser
|
|
608
|
+
? context.browser.name
|
|
609
|
+
: "unknown",
|
|
610
|
+
stepId: step.stepId || `Step ${stepIndex + 1}`,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Display failures
|
|
622
|
+
if (failedSpecs.length > 0) {
|
|
623
|
+
console.log(`\n${colors.red}Failed Specs:${colors.reset}`);
|
|
624
|
+
failedSpecs.forEach((item, i) => {
|
|
625
|
+
console.log(`${colors.red}${i + 1}. ${item.id}${colors.reset}`);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (failedTests.length > 0) {
|
|
630
|
+
console.log(`\n${colors.red}Failed Tests:${colors.reset}`);
|
|
631
|
+
failedTests.forEach((item, i) => {
|
|
632
|
+
console.log(
|
|
633
|
+
`${colors.red}${i + 1}. ${item.id} (from ${item.specId})${
|
|
634
|
+
colors.reset
|
|
635
|
+
}`
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (failedContexts.length > 0) {
|
|
641
|
+
console.log(`\n${colors.red}Failed Contexts:${colors.reset}`);
|
|
642
|
+
failedContexts.forEach((item, i) => {
|
|
643
|
+
console.log(
|
|
644
|
+
`${colors.red}${i + 1}. ${item.platform}/${item.browser} (from ${
|
|
645
|
+
item.testId
|
|
646
|
+
})${colors.reset}`
|
|
647
|
+
);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (failedSteps.length > 0) {
|
|
652
|
+
console.log(`\n${colors.red}Failed Steps:${colors.reset}`);
|
|
653
|
+
failedSteps.forEach((item, i) => {
|
|
654
|
+
console.log(
|
|
655
|
+
`${colors.red}${i + 1}. ${item.platform}/${item.browser} - ${
|
|
656
|
+
item.stepId
|
|
657
|
+
}${colors.reset}`
|
|
658
|
+
);
|
|
659
|
+
console.log(` Error: ${item.error}`);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Display skipped items in yellow
|
|
664
|
+
if (skippedSpecs.length > 0) {
|
|
665
|
+
console.log(`\n${colors.yellow}Skipped Specs:${colors.reset}`);
|
|
666
|
+
skippedSpecs.forEach((item, i) => {
|
|
667
|
+
console.log(`${colors.yellow}${i + 1}. ${item.id}${colors.reset}`);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (skippedTests.length > 0) {
|
|
671
|
+
console.log(`\n${colors.yellow}Skipped Tests:${colors.reset}`);
|
|
672
|
+
skippedTests.forEach((item, i) => {
|
|
673
|
+
console.log(
|
|
674
|
+
`${colors.yellow}${i + 1}. ${item.id} (from ${item.specId})${
|
|
675
|
+
colors.reset
|
|
676
|
+
}`
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (skippedContexts.length > 0) {
|
|
681
|
+
console.log(`\n${colors.yellow}Skipped Contexts:${colors.reset}`);
|
|
682
|
+
skippedContexts.forEach((item, i) => {
|
|
683
|
+
console.log(
|
|
684
|
+
`${colors.yellow}${i + 1}. ${item.platform}/${
|
|
685
|
+
item.browser
|
|
686
|
+
} (from ${item.testId})${colors.reset}`
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (skippedSteps.length > 0) {
|
|
691
|
+
console.log(`\n${colors.yellow}Skipped Steps:${colors.reset}`);
|
|
692
|
+
skippedSteps.forEach((item, i) => {
|
|
693
|
+
console.log(
|
|
694
|
+
`${colors.yellow}${i + 1}. ${item.platform}/${item.browser} - ${
|
|
695
|
+
item.stepId
|
|
696
|
+
}${colors.reset}`
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
} else if (!hasFailures && !allSpecsSkipped) {
|
|
701
|
+
// Celebration when all tests pass
|
|
702
|
+
console.log(`\n${colors.green}🎉 All items passed! 🎉${colors.reset}`);
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
console.log(
|
|
706
|
+
"No tests were executed or results are in an unknown format."
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
console.log("\n===============================\n");
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// Export reporters for external use
|
|
715
|
+
exports.reporters = reporters;
|
|
716
|
+
|
|
717
|
+
// Helper function to register custom reporters
|
|
718
|
+
function registerReporter(name, reporterFunction) {
|
|
719
|
+
if (typeof reporterFunction !== "function") {
|
|
720
|
+
throw new Error("Reporter must be a function");
|
|
721
|
+
}
|
|
722
|
+
reporters[name] = reporterFunction;
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Export the registerReporter function
|
|
727
|
+
exports.registerReporter = registerReporter;
|
|
728
|
+
|
|
729
|
+
async function reportResults({ apiConfig, results }) {
|
|
730
|
+
// Transform results into the required format for the API
|
|
731
|
+
// Extract contexts from the nested structure and format them
|
|
732
|
+
const contexts = [];
|
|
733
|
+
|
|
734
|
+
if (results.specs) {
|
|
735
|
+
results.specs.forEach((spec) => {
|
|
736
|
+
if (spec.tests) {
|
|
737
|
+
spec.tests.forEach((test) => {
|
|
738
|
+
if (test.contexts) {
|
|
739
|
+
test.contexts.forEach((context) => {
|
|
740
|
+
// Extract or generate contextId
|
|
741
|
+
const contextId =
|
|
742
|
+
context.contextId;
|
|
743
|
+
|
|
744
|
+
// Convert result status to lowercase (PASS -> passed, FAIL -> failed, etc.)
|
|
745
|
+
let status;
|
|
746
|
+
if (context.result === "PASS") {
|
|
747
|
+
status = "passed";
|
|
748
|
+
} else if (context.result === "FAIL") {
|
|
749
|
+
status = "failed";
|
|
750
|
+
} else if (context.result === "WARNING") {
|
|
751
|
+
status = "warning";
|
|
752
|
+
} else if (context.result === "SKIPPED") {
|
|
753
|
+
status = "skipped";
|
|
754
|
+
}
|
|
755
|
+
if (!status) {
|
|
756
|
+
log(config, "error", `Unknown context result status for context ID ${contextId}`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Build the context payload with the entire context object embedded
|
|
761
|
+
contexts.push({
|
|
762
|
+
contextId: contextId,
|
|
763
|
+
status: status,
|
|
764
|
+
result: context,
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// POST to the /contexts endpoint
|
|
774
|
+
try {
|
|
775
|
+
const url = `${apiConfig.url}/contexts`;
|
|
776
|
+
const payload = { contexts };
|
|
777
|
+
|
|
778
|
+
console.log(payload);
|
|
779
|
+
|
|
780
|
+
const response = await axios.post(url, payload, {
|
|
781
|
+
headers: {
|
|
782
|
+
"x-runner-token": apiConfig.token,
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
console.log("Results reported successfully:", response.data);
|
|
786
|
+
} catch (error) {
|
|
787
|
+
console.error(
|
|
788
|
+
`Error reporting results to ${apiConfig.url}/contexts: ${error.message}`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function outputResults(config = {}, outputPath, results, options = {}) {
|
|
794
|
+
// Default to using both built-in reporters if none specified
|
|
795
|
+
const defaultReporters = ["terminal", "json"];
|
|
796
|
+
|
|
797
|
+
let activeReporters = options.reporters || defaultReporters;
|
|
798
|
+
|
|
799
|
+
// If the reporters option is provided as strings, normalize them
|
|
800
|
+
if (activeReporters.length > 0) {
|
|
801
|
+
// Convert any shorthand names to full reporter names
|
|
802
|
+
activeReporters = activeReporters.map((reporter) => {
|
|
803
|
+
if (typeof reporter === "string") {
|
|
804
|
+
// Convert shorthand names to actual reporter keys
|
|
805
|
+
switch (reporter.toLowerCase()) {
|
|
806
|
+
case "json":
|
|
807
|
+
return "jsonReporter";
|
|
808
|
+
case "terminal":
|
|
809
|
+
return "terminalReporter";
|
|
810
|
+
default:
|
|
811
|
+
return reporter;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return reporter;
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Execute each reporter
|
|
819
|
+
const reporterPromises = activeReporters.map((reporter) => {
|
|
820
|
+
if (typeof reporter === "function") {
|
|
821
|
+
// Direct function reference
|
|
822
|
+
return reporter(config, outputPath, results, options);
|
|
823
|
+
} else if (typeof reporter === "string" && reporters[reporter]) {
|
|
824
|
+
// String reference to built-in or registered reporter
|
|
825
|
+
return reporters[reporter](config, outputPath, results, options);
|
|
826
|
+
} else if (typeof reporter === "string" && !reporters[reporter]) {
|
|
827
|
+
console.error(
|
|
828
|
+
`Reporter "${reporter}" not found. Available reporters: ${Object.keys(
|
|
829
|
+
reporters
|
|
830
|
+
).join(", ")}`
|
|
831
|
+
);
|
|
832
|
+
return Promise.resolve();
|
|
833
|
+
} else {
|
|
834
|
+
console.error(`Invalid reporter: ${reporter}`);
|
|
835
|
+
return Promise.resolve();
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Wait for all reporters to complete
|
|
840
|
+
return Promise.all(reporterPromises);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Perform a native command in the current working directory.
|
|
844
|
+
async function spawnCommand(cmd, args) {
|
|
845
|
+
// Split command into command and arguments
|
|
846
|
+
if (cmd.includes(" ")) {
|
|
847
|
+
const cmdArray = cmd.split(" ");
|
|
848
|
+
cmd = cmdArray[0];
|
|
849
|
+
cmdArgs = cmdArray.slice(1);
|
|
850
|
+
// Add arguments to args array
|
|
851
|
+
if (args) {
|
|
852
|
+
args = cmdArgs.concat(args);
|
|
853
|
+
} else {
|
|
854
|
+
args = cmdArgs;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const runCommand = spawn(cmd, args, {
|
|
859
|
+
env: process.env, // Explicitly pass environment variables
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Capture stdout
|
|
863
|
+
let stdout = "";
|
|
864
|
+
for await (const chunk of runCommand.stdout) {
|
|
865
|
+
stdout += chunk;
|
|
866
|
+
}
|
|
867
|
+
// Remove trailing newline
|
|
868
|
+
stdout = stdout.replace(/\n$/, "");
|
|
869
|
+
|
|
870
|
+
// Capture stderr
|
|
871
|
+
let stderr = "";
|
|
872
|
+
for await (const chunk of runCommand.stderr) {
|
|
873
|
+
stderr += chunk;
|
|
874
|
+
}
|
|
875
|
+
// Remove trailing newline
|
|
876
|
+
stderr = stderr.replace(/\n$/, "");
|
|
877
|
+
|
|
878
|
+
// Capture exit code
|
|
879
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
880
|
+
runCommand.on("close", resolve);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
return { stdout, stderr, exitCode };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function setMeta() {
|
|
887
|
+
const platformMap = {
|
|
888
|
+
win32: "windows",
|
|
889
|
+
darwin: "mac",
|
|
890
|
+
linux: "linux",
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// Set meta
|
|
894
|
+
const meta =
|
|
895
|
+
process.env["DOC_DETECTIVE_META"] !== undefined
|
|
896
|
+
? JSON.parse(process.env["DOC_DETECTIVE_META"])
|
|
897
|
+
: {};
|
|
898
|
+
const package = require("../package.json");
|
|
899
|
+
meta.distribution = "doc-detective";
|
|
900
|
+
meta.dist_version = package.version;
|
|
901
|
+
meta.dist_platform = platformMap[os.platform()] || os.platform();
|
|
902
|
+
meta.dist_platform_version = os.release();
|
|
903
|
+
meta.dist_platform_arch = os.arch();
|
|
904
|
+
meta.dist_deployment = meta.dist_deployment || "node";
|
|
905
|
+
meta.dist_deployment_version =
|
|
906
|
+
meta.dist_deployment_version || process.version;
|
|
907
|
+
meta.dist_interface = meta.dist_interface || "cli";
|
|
908
|
+
process.env["DOC_DETECTIVE_META"] = JSON.stringify(meta);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Get version data programmatically (no console output)
|
|
912
|
+
function getVersionData() {
|
|
913
|
+
try {
|
|
914
|
+
// Get main package version
|
|
915
|
+
const mainPackage = require("../package.json");
|
|
916
|
+
const versionData = {
|
|
917
|
+
main: {
|
|
918
|
+
"doc-detective": {
|
|
919
|
+
version: mainPackage.version,
|
|
920
|
+
expected: "main package",
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
dependencies: {},
|
|
924
|
+
context: {
|
|
925
|
+
executionMethod: "direct node execution",
|
|
926
|
+
nodeVersion: process.version,
|
|
927
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
928
|
+
timestamp: new Date().toISOString(),
|
|
929
|
+
},
|
|
930
|
+
locations: {},
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// Auto-discover all doc-detective-* packages in node_modules
|
|
934
|
+
const nodeModulesPath = path.resolve(process.cwd(), "node_modules");
|
|
935
|
+
const dependenciesToCheck = [];
|
|
936
|
+
|
|
937
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
938
|
+
const nodeModulesContents = fs.readdirSync(nodeModulesPath);
|
|
939
|
+
nodeModulesContents.forEach((dir) => {
|
|
940
|
+
if (dir.startsWith("doc-detective-") && dir !== "doc-detective") {
|
|
941
|
+
dependenciesToCheck.push(dir);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Detect execution method
|
|
947
|
+
const isNpx =
|
|
948
|
+
process.env.npm_execpath && process.env.npm_execpath.includes("npx");
|
|
949
|
+
const isNpm = process.env.npm_execpath && !isNpx;
|
|
950
|
+
|
|
951
|
+
if (isNpx) {
|
|
952
|
+
versionData.context.executionMethod = "npx";
|
|
953
|
+
} else if (isNpm) {
|
|
954
|
+
versionData.context.executionMethod = "npm";
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Check installed versions of dependencies
|
|
958
|
+
dependenciesToCheck.sort().forEach((dep) => {
|
|
959
|
+
try {
|
|
960
|
+
// Try to read the dependency's package.json
|
|
961
|
+
const depPackagePath = path.resolve(
|
|
962
|
+
process.cwd(),
|
|
963
|
+
"node_modules",
|
|
964
|
+
dep,
|
|
965
|
+
"package.json"
|
|
966
|
+
);
|
|
967
|
+
if (fs.existsSync(depPackagePath)) {
|
|
968
|
+
const depPackage = JSON.parse(
|
|
969
|
+
fs.readFileSync(depPackagePath, "utf8")
|
|
970
|
+
);
|
|
971
|
+
const installedVersion = depPackage.version;
|
|
972
|
+
|
|
973
|
+
// Look for expected version in main package dependencies or devDependencies
|
|
974
|
+
const expectedVersion =
|
|
975
|
+
mainPackage.dependencies[dep] ||
|
|
976
|
+
mainPackage.devDependencies[dep] ||
|
|
977
|
+
"not specified in main package";
|
|
978
|
+
|
|
979
|
+
versionData.dependencies[dep] = {
|
|
980
|
+
installed: installedVersion,
|
|
981
|
+
expected: expectedVersion,
|
|
982
|
+
status:
|
|
983
|
+
expectedVersion !== "not specified in main package" &&
|
|
984
|
+
!expectedVersion.includes(installedVersion) &&
|
|
985
|
+
!installedVersion.includes(expectedVersion.replace(/[\^~]/, ""))
|
|
986
|
+
? "mismatch"
|
|
987
|
+
: "ok",
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
versionData.locations[dep] = path.resolve(
|
|
991
|
+
process.cwd(),
|
|
992
|
+
"node_modules",
|
|
993
|
+
dep
|
|
994
|
+
);
|
|
995
|
+
} else {
|
|
996
|
+
versionData.dependencies[dep] = {
|
|
997
|
+
installed: null,
|
|
998
|
+
expected:
|
|
999
|
+
mainPackage.dependencies[dep] ||
|
|
1000
|
+
mainPackage.devDependencies[dep] ||
|
|
1001
|
+
"not specified",
|
|
1002
|
+
status: "not found",
|
|
1003
|
+
error: "package.json not found",
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
versionData.dependencies[dep] = {
|
|
1008
|
+
installed: null,
|
|
1009
|
+
expected:
|
|
1010
|
+
mainPackage.dependencies[dep] ||
|
|
1011
|
+
mainPackage.devDependencies[dep] ||
|
|
1012
|
+
"not specified",
|
|
1013
|
+
status: "error",
|
|
1014
|
+
error: error.message,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
return versionData;
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
return { error: error.message };
|
|
1022
|
+
}
|
|
1023
|
+
}
|