ortoni-report 4.0.2-beta.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +22 -0
- package/dist/{chunk-INS3E7E6.mjs → chunk-4RZ5C7KY.mjs} +402 -296
- package/dist/{cli/cli.js → cli.js} +382 -144
- package/dist/cli.mjs +208 -0
- package/dist/index.html +2 -2
- package/dist/ortoni-report.d.mts +8 -1
- package/dist/ortoni-report.d.ts +8 -1
- package/dist/ortoni-report.js +220 -96
- package/dist/ortoni-report.mjs +34 -22
- package/package.json +11 -4
- package/readme.md +60 -75
- package/dist/chunk-45EJSEX2.mjs +0 -632
- package/dist/chunk-75EAJL2U.mjs +0 -632
- package/dist/chunk-A6HCKATU.mjs +0 -76
- package/dist/chunk-FGIYOFIC.mjs +0 -632
- package/dist/chunk-FHKWBHU6.mjs +0 -633
- package/dist/chunk-GLICR3VS.mjs +0 -637
- package/dist/chunk-HFO6XSKC.mjs +0 -633
- package/dist/chunk-HOZD6YIV.mjs +0 -634
- package/dist/chunk-IJO2YIFE.mjs +0 -637
- package/dist/chunk-JEIWNUQY.mjs +0 -632
- package/dist/chunk-JPLAGYR7.mjs +0 -632
- package/dist/chunk-NM6ULN2O.mjs +0 -632
- package/dist/chunk-OZS6QIJS.mjs +0 -638
- package/dist/chunk-P57227VN.mjs +0 -633
- package/dist/chunk-QMTRYN5N.js +0 -635
- package/dist/chunk-TI33PMMQ.mjs +0 -639
- package/dist/chunk-Z5NBP5TS.mjs +0 -635
- package/dist/cli/cli.cjs +0 -678
- package/dist/cli/cli.d.cts +0 -1
- package/dist/cli/cli.mjs +0 -80
- package/dist/ortoni-report.cjs +0 -2134
- package/dist/ortoni-report.d.cts +0 -111
- /package/dist/{cli/cli.d.mts → cli.d.mts} +0 -0
- /package/dist/{cli/cli.d.ts → cli.d.ts} +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
5
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -23,72 +22,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
22
|
mod
|
|
24
23
|
));
|
|
25
24
|
|
|
26
|
-
// src/cli
|
|
25
|
+
// src/cli.ts
|
|
27
26
|
var import_commander = require("commander");
|
|
27
|
+
var fs4 = __toESM(require("fs"));
|
|
28
|
+
var path5 = __toESM(require("path"));
|
|
28
29
|
|
|
29
|
-
// src/
|
|
30
|
-
var
|
|
31
|
-
var import_path = __toESM(require("path"));
|
|
32
|
-
var import_child_process = require("child_process");
|
|
33
|
-
function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
|
|
34
|
-
const app = (0, import_express.default)();
|
|
35
|
-
app.use(import_express.default.static(reportFolder));
|
|
36
|
-
app.get("/", (_req, res) => {
|
|
37
|
-
try {
|
|
38
|
-
res.sendFile(import_path.default.resolve(reportFolder, reportFilename));
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error("Ortoni-Report: Error sending report file:", error);
|
|
41
|
-
res.status(500).send("Error loading report");
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
try {
|
|
45
|
-
const server = app.listen(port, () => {
|
|
46
|
-
console.log(
|
|
47
|
-
`Server is running at http://localhost:${port}
|
|
48
|
-
Press Ctrl+C to stop.`
|
|
49
|
-
);
|
|
50
|
-
if (open2 === "always" || open2 === "on-failure") {
|
|
51
|
-
try {
|
|
52
|
-
openBrowser(`http://localhost:${port}`);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error("Ortoni-Report: Error opening browser:", error);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
server.on("error", (error) => {
|
|
59
|
-
if (error.code === "EADDRINUSE") {
|
|
60
|
-
console.error(
|
|
61
|
-
`Ortoni-Report: Port ${port} is already in use. Trying a different port...`
|
|
62
|
-
);
|
|
63
|
-
} else {
|
|
64
|
-
console.error("Ortoni-Report: Server error:", error);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error("Ortoni-Report: Error starting the server:", error);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function openBrowser(url) {
|
|
72
|
-
const platform = process.platform;
|
|
73
|
-
let command;
|
|
74
|
-
try {
|
|
75
|
-
if (platform === "win32") {
|
|
76
|
-
command = "cmd";
|
|
77
|
-
(0, import_child_process.spawn)(command, ["/c", "start", url]);
|
|
78
|
-
} else if (platform === "darwin") {
|
|
79
|
-
command = "open";
|
|
80
|
-
(0, import_child_process.spawn)(command, [url]);
|
|
81
|
-
} else {
|
|
82
|
-
command = "xdg-open";
|
|
83
|
-
(0, import_child_process.spawn)(command, [url]);
|
|
84
|
-
}
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error("Ortoni-Report: Error opening the browser:", error);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// src/cli/cli.ts
|
|
91
|
-
var fs2 = __toESM(require("fs"));
|
|
30
|
+
// src/mergeData.ts
|
|
31
|
+
var fs3 = __toESM(require("fs"));
|
|
92
32
|
var path3 = __toESM(require("path"));
|
|
93
33
|
|
|
94
34
|
// src/helpers/databaseManager.ts
|
|
@@ -426,13 +366,46 @@ var HTMLGenerator = class {
|
|
|
426
366
|
);
|
|
427
367
|
return data;
|
|
428
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Return safe analytics/report data.
|
|
371
|
+
* If no dbManager is provided, return empty defaults and a note explaining why.
|
|
372
|
+
*/
|
|
429
373
|
async getReportData() {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
374
|
+
if (!this.dbManager) {
|
|
375
|
+
return {
|
|
376
|
+
summary: {},
|
|
377
|
+
trends: {},
|
|
378
|
+
flakyTests: [],
|
|
379
|
+
slowTests: [],
|
|
380
|
+
note: "Test history/trends are unavailable (saveHistory disabled or DB not initialized)."
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const [summary, trends, flakyTests, slowTests] = await Promise.all([
|
|
385
|
+
this.dbManager.getSummaryData ? this.dbManager.getSummaryData() : Promise.resolve({}),
|
|
386
|
+
this.dbManager.getTrends ? this.dbManager.getTrends() : Promise.resolve({}),
|
|
387
|
+
this.dbManager.getFlakyTests ? this.dbManager.getFlakyTests() : Promise.resolve([]),
|
|
388
|
+
this.dbManager.getSlowTests ? this.dbManager.getSlowTests() : Promise.resolve([])
|
|
389
|
+
]);
|
|
390
|
+
return {
|
|
391
|
+
summary: summary ?? {},
|
|
392
|
+
trends: trends ?? {},
|
|
393
|
+
flakyTests: flakyTests ?? [],
|
|
394
|
+
slowTests: slowTests ?? []
|
|
395
|
+
};
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.warn(
|
|
398
|
+
"HTMLGenerator: failed to read analytics from DB, continuing without history.",
|
|
399
|
+
err
|
|
400
|
+
);
|
|
401
|
+
return {
|
|
402
|
+
summary: {},
|
|
403
|
+
trends: {},
|
|
404
|
+
flakyTests: [],
|
|
405
|
+
slowTests: [],
|
|
406
|
+
note: "Test history/trends could not be loaded due to a DB error."
|
|
407
|
+
};
|
|
408
|
+
}
|
|
436
409
|
}
|
|
437
410
|
async prepareReportData(filteredResults, totalDuration, results, projectSet) {
|
|
438
411
|
const totalTests = filteredResults.length;
|
|
@@ -441,10 +414,10 @@ var HTMLGenerator = class {
|
|
|
441
414
|
const failed = filteredResults.filter(
|
|
442
415
|
(r) => r.status === "failed" || r.status === "timedOut"
|
|
443
416
|
).length;
|
|
444
|
-
const successRate = ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
|
|
417
|
+
const successRate = totalTests === 0 ? "0.00" : ((passedTests + flakyTests) / totalTests * 100).toFixed(2);
|
|
445
418
|
const allTags = /* @__PURE__ */ new Set();
|
|
446
419
|
results.forEach(
|
|
447
|
-
(result) => result.testTags.forEach((tag) => allTags.add(tag))
|
|
420
|
+
(result) => (result.testTags || []).forEach((tag) => allTags.add(tag))
|
|
448
421
|
);
|
|
449
422
|
const projectResults = this.calculateProjectResults(
|
|
450
423
|
filteredResults,
|
|
@@ -455,13 +428,31 @@ var HTMLGenerator = class {
|
|
|
455
428
|
const testHistories = await Promise.all(
|
|
456
429
|
results.map(async (result) => {
|
|
457
430
|
const testId = `${result.filePath}:${result.projectName}:${result.title}`;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
431
|
+
if (!this.dbManager || !this.dbManager.getTestHistory) {
|
|
432
|
+
return {
|
|
433
|
+
testId,
|
|
434
|
+
history: []
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const history = await this.dbManager.getTestHistory(testId);
|
|
439
|
+
return {
|
|
440
|
+
testId,
|
|
441
|
+
history: history ?? []
|
|
442
|
+
};
|
|
443
|
+
} catch (err) {
|
|
444
|
+
console.warn(
|
|
445
|
+
`HTMLGenerator: failed to read history for ${testId}`,
|
|
446
|
+
err
|
|
447
|
+
);
|
|
448
|
+
return {
|
|
449
|
+
testId,
|
|
450
|
+
history: []
|
|
451
|
+
};
|
|
452
|
+
}
|
|
463
453
|
})
|
|
464
454
|
);
|
|
455
|
+
const reportData = await this.getReportData();
|
|
465
456
|
return {
|
|
466
457
|
summary: {
|
|
467
458
|
overAllResult: {
|
|
@@ -497,7 +488,7 @@ var HTMLGenerator = class {
|
|
|
497
488
|
showProject: this.ortoniConfig.showProject || false
|
|
498
489
|
},
|
|
499
490
|
analytics: {
|
|
500
|
-
reportData
|
|
491
|
+
reportData
|
|
501
492
|
}
|
|
502
493
|
};
|
|
503
494
|
}
|
|
@@ -536,38 +527,100 @@ var HTMLGenerator = class {
|
|
|
536
527
|
};
|
|
537
528
|
|
|
538
529
|
// src/helpers/fileManager.ts
|
|
539
|
-
var
|
|
530
|
+
var import_fs2 = __toESM(require("fs"));
|
|
540
531
|
var import_path2 = __toESM(require("path"));
|
|
532
|
+
|
|
533
|
+
// src/helpers/templateLoader.ts
|
|
534
|
+
var import_fs = __toESM(require("fs"));
|
|
535
|
+
var import_path = __toESM(require("path"));
|
|
536
|
+
var import_meta = {};
|
|
537
|
+
async function readBundledTemplate(pkgName = "ortoni-report") {
|
|
538
|
+
const packagedRel = "dist/index.html";
|
|
539
|
+
try {
|
|
540
|
+
if (typeof require === "function") {
|
|
541
|
+
const resolved = require.resolve(`${pkgName}/${packagedRel}`);
|
|
542
|
+
if (import_fs.default.existsSync(resolved)) {
|
|
543
|
+
return import_fs.default.readFileSync(resolved, "utf-8");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const moduleNS = await import("module");
|
|
550
|
+
if (moduleNS && typeof moduleNS.createRequire === "function") {
|
|
551
|
+
const createRequire = moduleNS.createRequire;
|
|
552
|
+
const req = createRequire(
|
|
553
|
+
// @ts-ignore
|
|
554
|
+
typeof __filename !== "undefined" ? __filename : import_meta.url
|
|
555
|
+
);
|
|
556
|
+
const resolved = req.resolve(`${pkgName}/${packagedRel}`);
|
|
557
|
+
if (import_fs.default.existsSync(resolved)) {
|
|
558
|
+
return import_fs.default.readFileSync(resolved, "utf-8");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
const here = import_path.default.resolve(__dirname, "../dist/index.html");
|
|
565
|
+
if (import_fs.default.existsSync(here)) return import_fs.default.readFileSync(here, "utf-8");
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const nm = import_path.default.join(process.cwd(), "node_modules", pkgName, packagedRel);
|
|
570
|
+
if (import_fs.default.existsSync(nm)) return import_fs.default.readFileSync(nm, "utf-8");
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
const alt = import_path.default.join(process.cwd(), "dist", "index.html");
|
|
575
|
+
if (import_fs.default.existsSync(alt)) return import_fs.default.readFileSync(alt, "utf-8");
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
throw new Error(
|
|
579
|
+
`ortoni-report template not found (tried:
|
|
580
|
+
- require.resolve('${pkgName}/${packagedRel}')
|
|
581
|
+
- import('module').createRequire(...).resolve('${pkgName}/${packagedRel}')
|
|
582
|
+
- relative ../dist/index.html
|
|
583
|
+
- ${import_path.default.join(
|
|
584
|
+
process.cwd(),
|
|
585
|
+
"node_modules",
|
|
586
|
+
pkgName,
|
|
587
|
+
packagedRel
|
|
588
|
+
)}
|
|
589
|
+
- ${import_path.default.join(process.cwd(), "dist", "index.html")}
|
|
590
|
+
Ensure 'dist/index.html' is present in the published package and package.json 'files' includes 'dist/'.`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/helpers/fileManager.ts
|
|
541
595
|
var FileManager = class {
|
|
542
596
|
constructor(folderPath) {
|
|
543
597
|
this.folderPath = folderPath;
|
|
544
598
|
}
|
|
545
599
|
ensureReportDirectory() {
|
|
546
600
|
const ortoniDataFolder = import_path2.default.join(this.folderPath, "ortoni-data");
|
|
547
|
-
if (!
|
|
548
|
-
|
|
601
|
+
if (!import_fs2.default.existsSync(this.folderPath)) {
|
|
602
|
+
import_fs2.default.mkdirSync(this.folderPath, { recursive: true });
|
|
549
603
|
} else {
|
|
550
|
-
if (
|
|
551
|
-
|
|
604
|
+
if (import_fs2.default.existsSync(ortoniDataFolder)) {
|
|
605
|
+
import_fs2.default.rmSync(ortoniDataFolder, { recursive: true, force: true });
|
|
552
606
|
}
|
|
553
607
|
}
|
|
554
608
|
}
|
|
555
|
-
writeReportFile(filename, data) {
|
|
556
|
-
|
|
557
|
-
console.log("temp path - " + templatePath);
|
|
558
|
-
let html = import_fs.default.readFileSync(templatePath, "utf-8");
|
|
609
|
+
async writeReportFile(filename, data) {
|
|
610
|
+
let html = await readBundledTemplate();
|
|
559
611
|
const reportJSON = JSON.stringify({
|
|
560
612
|
data
|
|
561
613
|
});
|
|
562
614
|
html = html.replace("__ORTONI_TEST_REPORTDATA__", reportJSON);
|
|
563
|
-
|
|
564
|
-
|
|
615
|
+
const outputPath = import_path2.default.join(process.cwd(), this.folderPath, filename);
|
|
616
|
+
import_fs2.default.writeFileSync(outputPath, html);
|
|
617
|
+
return outputPath;
|
|
565
618
|
}
|
|
566
619
|
writeRawFile(filename, data) {
|
|
567
620
|
const outputPath = import_path2.default.join(process.cwd(), this.folderPath, filename);
|
|
568
|
-
|
|
621
|
+
import_fs2.default.mkdirSync(import_path2.default.dirname(outputPath), { recursive: true });
|
|
569
622
|
const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
570
|
-
|
|
623
|
+
import_fs2.default.writeFileSync(outputPath, content, "utf-8");
|
|
571
624
|
return outputPath;
|
|
572
625
|
}
|
|
573
626
|
copyTraceViewerAssets(skip) {
|
|
@@ -584,19 +637,19 @@ var FileManager = class {
|
|
|
584
637
|
traceViewerTargetFolder,
|
|
585
638
|
"assets"
|
|
586
639
|
);
|
|
587
|
-
|
|
588
|
-
for (const file of
|
|
640
|
+
import_fs2.default.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
|
|
641
|
+
for (const file of import_fs2.default.readdirSync(traceViewerFolder)) {
|
|
589
642
|
if (file.endsWith(".map") || file.includes("watch") || file.includes("assets"))
|
|
590
643
|
continue;
|
|
591
|
-
|
|
644
|
+
import_fs2.default.copyFileSync(
|
|
592
645
|
import_path2.default.join(traceViewerFolder, file),
|
|
593
646
|
import_path2.default.join(traceViewerTargetFolder, file)
|
|
594
647
|
);
|
|
595
648
|
}
|
|
596
649
|
const assetsFolder = import_path2.default.join(traceViewerFolder, "assets");
|
|
597
|
-
for (const file of
|
|
650
|
+
for (const file of import_fs2.default.readdirSync(assetsFolder)) {
|
|
598
651
|
if (file.endsWith(".map") || file.includes("xtermModule")) continue;
|
|
599
|
-
|
|
652
|
+
import_fs2.default.copyFileSync(
|
|
600
653
|
import_path2.default.join(assetsFolder, file),
|
|
601
654
|
import_path2.default.join(traceViewerAssetsTargetFolder, file)
|
|
602
655
|
);
|
|
@@ -604,9 +657,228 @@ var FileManager = class {
|
|
|
604
657
|
}
|
|
605
658
|
};
|
|
606
659
|
|
|
607
|
-
// src/
|
|
608
|
-
|
|
609
|
-
|
|
660
|
+
// src/mergeData.ts
|
|
661
|
+
async function mergeAllData(options = {}) {
|
|
662
|
+
const folderPath = options.dir || "ortoni-report";
|
|
663
|
+
console.info(`Ortoni Report: Merging shard files in folder: ${folderPath}`);
|
|
664
|
+
if (!fs3.existsSync(folderPath)) {
|
|
665
|
+
console.error(`Ortoni Report: folder "${folderPath}" does not exist.`);
|
|
666
|
+
process.exitCode = 1;
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const filenames = fs3.readdirSync(folderPath).filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
|
|
670
|
+
if (filenames.length === 0) {
|
|
671
|
+
console.error("Ortoni Report: \u274C No shard files found to merge.");
|
|
672
|
+
process.exitCode = 1;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const shardFileIndex = (name) => {
|
|
676
|
+
const m = name.match(/ortoni-shard-(\d+)-of-(\d+)\.json$/);
|
|
677
|
+
return m ? parseInt(m[1], 10) : null;
|
|
678
|
+
};
|
|
679
|
+
const sortedFiles = filenames.map((f) => ({ f, idx: shardFileIndex(f) })).sort((a, b) => {
|
|
680
|
+
if (a.idx === null && b.idx === null) return a.f.localeCompare(b.f);
|
|
681
|
+
if (a.idx === null) return 1;
|
|
682
|
+
if (b.idx === null) return -1;
|
|
683
|
+
return a.idx - b.idx;
|
|
684
|
+
}).map((x) => x.f);
|
|
685
|
+
const allResults = [];
|
|
686
|
+
const projectSet = /* @__PURE__ */ new Set();
|
|
687
|
+
let totalDurationSum = 0;
|
|
688
|
+
let mergedUserConfig = null;
|
|
689
|
+
let mergedUserMeta = null;
|
|
690
|
+
const badShards = [];
|
|
691
|
+
const shardCounts = {};
|
|
692
|
+
const shardDurMap = {};
|
|
693
|
+
for (const file of sortedFiles) {
|
|
694
|
+
const fullPath = path3.join(folderPath, file);
|
|
695
|
+
try {
|
|
696
|
+
const shardRaw = fs3.readFileSync(fullPath, "utf-8");
|
|
697
|
+
const shardData = JSON.parse(shardRaw);
|
|
698
|
+
if (!Array.isArray(shardData.results)) {
|
|
699
|
+
console.warn(
|
|
700
|
+
`Ortoni Report: Shard ${file} missing results array \u2014 skipping.`
|
|
701
|
+
);
|
|
702
|
+
badShards.push(file);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
shardData.results.forEach((r) => allResults.push(r));
|
|
706
|
+
shardCounts[file] = shardData.results.length;
|
|
707
|
+
if (Array.isArray(shardData.projectSet)) {
|
|
708
|
+
shardData.projectSet.forEach((p) => projectSet.add(p));
|
|
709
|
+
}
|
|
710
|
+
const rawShardDur = shardData.totalDuration;
|
|
711
|
+
let durToAdd = 0;
|
|
712
|
+
let perTestSum = 0;
|
|
713
|
+
if (typeof rawShardDur === "number") {
|
|
714
|
+
durToAdd = rawShardDur;
|
|
715
|
+
} else {
|
|
716
|
+
perTestSum = Array.isArray(shardData.results) ? shardData.results.reduce(
|
|
717
|
+
(acc, t) => acc + (Number(t?.duration) || 0),
|
|
718
|
+
0
|
|
719
|
+
) : 0;
|
|
720
|
+
durToAdd = perTestSum;
|
|
721
|
+
}
|
|
722
|
+
totalDurationSum += durToAdd;
|
|
723
|
+
shardDurMap[file] = durToAdd;
|
|
724
|
+
if (shardData.userConfig) {
|
|
725
|
+
if (!mergedUserConfig) mergedUserConfig = { ...shardData.userConfig };
|
|
726
|
+
else {
|
|
727
|
+
Object.keys(shardData.userConfig).forEach((k) => {
|
|
728
|
+
if (mergedUserConfig[k] === void 0 || mergedUserConfig[k] === null || mergedUserConfig[k] === "") {
|
|
729
|
+
mergedUserConfig[k] = shardData.userConfig[k];
|
|
730
|
+
} else if (shardData.userConfig[k] !== mergedUserConfig[k]) {
|
|
731
|
+
console.warn(
|
|
732
|
+
`Ortoni Report: userConfig mismatch for key "${k}" between shards. Using first value "${mergedUserConfig[k]}".`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (shardData.userMeta) {
|
|
739
|
+
if (!mergedUserMeta) mergedUserMeta = { ...shardData.userMeta };
|
|
740
|
+
else {
|
|
741
|
+
mergedUserMeta.meta = {
|
|
742
|
+
...mergedUserMeta.meta || {},
|
|
743
|
+
...shardData.userMeta.meta || {}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch (err) {
|
|
748
|
+
console.error(`Ortoni Report: Failed to parse shard ${file}:`, err);
|
|
749
|
+
badShards.push(file);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (badShards.length > 0) {
|
|
754
|
+
console.warn(
|
|
755
|
+
`Ortoni Report: Completed merge with ${badShards.length} bad shard(s) skipped:`,
|
|
756
|
+
badShards
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const totalDuration = totalDurationSum;
|
|
760
|
+
const saveHistoryFromOptions = typeof options.saveHistory === "boolean" ? options.saveHistory : void 0;
|
|
761
|
+
const saveHistoryFromShard = mergedUserConfig && typeof mergedUserConfig.saveHistory === "boolean" ? mergedUserConfig.saveHistory : void 0;
|
|
762
|
+
const saveHistory = saveHistoryFromOptions ?? saveHistoryFromShard ?? true;
|
|
763
|
+
let dbManager;
|
|
764
|
+
let runId;
|
|
765
|
+
if (saveHistory) {
|
|
766
|
+
try {
|
|
767
|
+
dbManager = new DatabaseManager();
|
|
768
|
+
const dbPath = path3.join(folderPath, "ortoni-data-history.sqlite");
|
|
769
|
+
await dbManager.initialize(dbPath);
|
|
770
|
+
runId = await dbManager.saveTestRun();
|
|
771
|
+
if (typeof runId === "number") {
|
|
772
|
+
await dbManager.saveTestResults(runId, allResults);
|
|
773
|
+
console.info(
|
|
774
|
+
`Ortoni Report: Saved ${allResults.length} results to DB (runId=${runId}).`
|
|
775
|
+
);
|
|
776
|
+
} else {
|
|
777
|
+
console.warn(
|
|
778
|
+
"Ortoni Report: Failed to create test run in DB; proceeding without saving results."
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
} catch (err) {
|
|
782
|
+
console.error(
|
|
783
|
+
"Ortoni Report: Error while saving history to DB. Proceeding without DB:",
|
|
784
|
+
err
|
|
785
|
+
);
|
|
786
|
+
dbManager = void 0;
|
|
787
|
+
runId = void 0;
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
console.info(
|
|
791
|
+
"Ortoni Report: Skipping history save (saveHistory=false). (Typical for CI runs)"
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const htmlGenerator = new HTMLGenerator(
|
|
795
|
+
{ ...mergedUserConfig || {}, meta: mergedUserMeta?.meta },
|
|
796
|
+
dbManager
|
|
797
|
+
);
|
|
798
|
+
const finalReportData = await htmlGenerator.generateFinalReport(
|
|
799
|
+
// filteredResults: typically filter out skipped for display (keeps existing behavior)
|
|
800
|
+
allResults.filter((r) => r.status !== "skipped"),
|
|
801
|
+
totalDuration,
|
|
802
|
+
allResults,
|
|
803
|
+
projectSet
|
|
804
|
+
// pass Set<string> as original generateFinalReport expects
|
|
805
|
+
);
|
|
806
|
+
const fileManager = new FileManager(folderPath);
|
|
807
|
+
const outputFileName = options.file || "ortoni-report.html";
|
|
808
|
+
const outputPath = fileManager.writeReportFile(
|
|
809
|
+
outputFileName,
|
|
810
|
+
finalReportData
|
|
811
|
+
);
|
|
812
|
+
console.log(`\u2705 Final merged report generated at ${await outputPath}`);
|
|
813
|
+
console.log(`\u2705 Shards merged: ${sortedFiles.length}`);
|
|
814
|
+
console.log(`\u2705 Tests per shard:`, shardCounts);
|
|
815
|
+
console.log(`\u2705 Total tests merged ${allResults.length}`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/utils/expressServer.ts
|
|
819
|
+
var import_express = __toESM(require("express"));
|
|
820
|
+
var import_path3 = __toESM(require("path"));
|
|
821
|
+
var import_child_process = require("child_process");
|
|
822
|
+
function startReportServer(reportFolder, reportFilename, port = 2004, open2) {
|
|
823
|
+
const app = (0, import_express.default)();
|
|
824
|
+
app.use(import_express.default.static(reportFolder, { index: false }));
|
|
825
|
+
app.get("/", (_req, res) => {
|
|
826
|
+
try {
|
|
827
|
+
res.sendFile(import_path3.default.resolve(reportFolder, reportFilename));
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.error("Ortoni Report: Error sending report file:", error);
|
|
830
|
+
res.status(500).send("Error loading report");
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
try {
|
|
834
|
+
const server = app.listen(port, () => {
|
|
835
|
+
console.log(
|
|
836
|
+
`Server is running at http://localhost:${port}
|
|
837
|
+
Press Ctrl+C to stop.`
|
|
838
|
+
);
|
|
839
|
+
if (open2 === "always" || open2 === "on-failure") {
|
|
840
|
+
try {
|
|
841
|
+
openBrowser(`http://localhost:${port}`);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.error("Ortoni Report: Error opening browser:", error);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
server.on("error", (error) => {
|
|
848
|
+
if (error.code === "EADDRINUSE") {
|
|
849
|
+
console.error(
|
|
850
|
+
`Ortoni Report: Port ${port} is already in use. Trying a different port...`
|
|
851
|
+
);
|
|
852
|
+
} else {
|
|
853
|
+
console.error("Ortoni Report: Server error:", error);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error("Ortoni Report: Error starting the server:", error);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function openBrowser(url) {
|
|
861
|
+
const platform = process.platform;
|
|
862
|
+
let command;
|
|
863
|
+
try {
|
|
864
|
+
if (platform === "win32") {
|
|
865
|
+
command = "cmd";
|
|
866
|
+
(0, import_child_process.spawn)(command, ["/c", "start", url]);
|
|
867
|
+
} else if (platform === "darwin") {
|
|
868
|
+
command = "open";
|
|
869
|
+
(0, import_child_process.spawn)(command, [url]);
|
|
870
|
+
} else {
|
|
871
|
+
command = "xdg-open";
|
|
872
|
+
(0, import_child_process.spawn)(command, [url]);
|
|
873
|
+
}
|
|
874
|
+
} catch (error) {
|
|
875
|
+
console.error("Ortoni Report: Error opening the browser:", error);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/cli.ts
|
|
880
|
+
import_commander.program.version("4.0.1").description("Ortoni Report - CLI");
|
|
881
|
+
import_commander.program.command("show-report").description("Open Ortoni Report").option(
|
|
610
882
|
"-d, --dir <path>",
|
|
611
883
|
"Path to the folder containing the report",
|
|
612
884
|
"ortoni-report"
|
|
@@ -616,60 +888,26 @@ import_commander.program.command("show-report").description("Open the generated
|
|
|
616
888
|
"ortoni-report.html"
|
|
617
889
|
).option("-p, --port <port>", "Port to run the server", "2004").action((options) => {
|
|
618
890
|
const projectRoot = process.cwd();
|
|
619
|
-
const folderPath =
|
|
620
|
-
const filePath =
|
|
891
|
+
const folderPath = path5.resolve(projectRoot, options.dir);
|
|
892
|
+
const filePath = path5.resolve(folderPath, options.file);
|
|
621
893
|
const port = parseInt(options.port) || 2004;
|
|
622
|
-
if (!
|
|
894
|
+
if (!fs4.existsSync(filePath)) {
|
|
623
895
|
console.error(
|
|
624
896
|
`\u274C Error: The file "${filePath}" does not exist in "${folderPath}".`
|
|
625
897
|
);
|
|
626
898
|
process.exit(1);
|
|
627
899
|
}
|
|
628
|
-
startReportServer(folderPath,
|
|
900
|
+
startReportServer(folderPath, path5.basename(filePath), port, "always");
|
|
629
901
|
});
|
|
630
|
-
import_commander.program.command("merge-
|
|
902
|
+
import_commander.program.command("merge-report").description("Merge sharded reports into one final report").option(
|
|
631
903
|
"-d, --dir <path>",
|
|
632
904
|
"Path to the folder containing shard files",
|
|
633
905
|
"ortoni-report"
|
|
634
906
|
).option("-f, --file <filename>", "Output report file", "ortoni-report.html").action(async (options) => {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const shardFiles = fs2.readdirSync(folderPath).filter((f) => f.startsWith("ortoni-shard-") && f.endsWith(".json"));
|
|
641
|
-
if (shardFiles.length === 0) {
|
|
642
|
-
console.error("\u274C No shard files found to merge.");
|
|
643
|
-
process.exit(1);
|
|
644
|
-
}
|
|
645
|
-
let allResults = [];
|
|
646
|
-
let projectSet = /* @__PURE__ */ new Set();
|
|
647
|
-
let totalDuration = 0;
|
|
648
|
-
for (const file of shardFiles) {
|
|
649
|
-
const shardData = JSON.parse(
|
|
650
|
-
fs2.readFileSync(path3.join(folderPath, file), "utf-8")
|
|
651
|
-
);
|
|
652
|
-
allResults.push(...shardData.results);
|
|
653
|
-
shardData.projectSet.forEach((p) => projectSet.add(p));
|
|
654
|
-
totalDuration += shardData.duration;
|
|
655
|
-
}
|
|
656
|
-
const dbManager = new DatabaseManager();
|
|
657
|
-
await dbManager.initialize(
|
|
658
|
-
path3.join(folderPath, "ortoni-data-history.sqlite")
|
|
659
|
-
);
|
|
660
|
-
const runId = await dbManager.saveTestRun();
|
|
661
|
-
if (typeof runId === "number") {
|
|
662
|
-
await dbManager.saveTestResults(runId, allResults);
|
|
663
|
-
}
|
|
664
|
-
const htmlGenerator = new HTMLGenerator({}, dbManager);
|
|
665
|
-
const finalReportData = await htmlGenerator.generateFinalReport(
|
|
666
|
-
allResults.filter((r) => r.status !== "skipped"),
|
|
667
|
-
totalDuration,
|
|
668
|
-
allResults,
|
|
669
|
-
projectSet
|
|
670
|
-
);
|
|
671
|
-
const fileManager = new FileManager(folderPath);
|
|
672
|
-
const outputPath = fileManager.writeReportFile(filePath, finalReportData);
|
|
673
|
-
console.log(`\u2705 Final merged report generated at ${outputPath}`);
|
|
907
|
+
await mergeAllData({
|
|
908
|
+
dir: options.dir,
|
|
909
|
+
file: options.file,
|
|
910
|
+
saveHistory: false
|
|
911
|
+
});
|
|
674
912
|
});
|
|
675
913
|
import_commander.program.parse(process.argv);
|