@flakiness/sdk 0.129.4 → 0.131.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/cli.js +179 -90
- package/lib/cli/cmd-convert.js +35 -24
- package/lib/cli/cmd-link.js +79 -4
- package/lib/cli/cmd-login.js +6 -3
- package/lib/cli/cmd-show-report.js +45 -21
- package/lib/junit.js +3 -3
- package/lib/localReportApi.js +207 -5
- package/lib/localReportServer.js +76 -52
- package/lib/playwright-test.js +87 -58
- package/lib/utils.js +24 -0
- package/package.json +10 -5
- package/types/tsconfig.tsbuildinfo +1 -1
package/lib/cli/cli.js
CHANGED
|
@@ -737,8 +737,50 @@ var Ranges;
|
|
|
737
737
|
import { TypedHTTP as TypedHTTP4 } from "@flakiness/shared/common/typedHttp.js";
|
|
738
738
|
import assert4 from "assert";
|
|
739
739
|
import { Command, Option } from "commander";
|
|
740
|
-
import
|
|
741
|
-
|
|
740
|
+
import path11 from "path";
|
|
741
|
+
|
|
742
|
+
// ../package.json
|
|
743
|
+
var package_default = {
|
|
744
|
+
name: "flakiness",
|
|
745
|
+
version: "0.131.0",
|
|
746
|
+
private: true,
|
|
747
|
+
scripts: {
|
|
748
|
+
minor: "./version.mjs minor",
|
|
749
|
+
patch: "./version.mjs patch",
|
|
750
|
+
dev: "npx kubik --env-file=.env.dev -w $(find . -name build.mts) ./app.mts ./stripe.mts",
|
|
751
|
+
"dev+billing": "npx kubik --env-file=.env.dev+billing -w $(find . -name build.mts) ./app.mts ./stripe.mts",
|
|
752
|
+
prod: "npx kubik --env-file=.env.prodlocal -w ./server.mts ./web/build.mts ./experimental/build.mts ./landing/build.mts",
|
|
753
|
+
build: "npx kubik $(find . -name build.mts)",
|
|
754
|
+
perf: "node --max-old-space-size=10240 --enable-source-maps --env-file=.env.prodlocal experimental/lib/perf_filter.js"
|
|
755
|
+
},
|
|
756
|
+
engines: {
|
|
757
|
+
node: ">=24"
|
|
758
|
+
},
|
|
759
|
+
author: "Degu Labs, Inc",
|
|
760
|
+
license: "Fair Source 100",
|
|
761
|
+
workspaces: [
|
|
762
|
+
"./report",
|
|
763
|
+
"./sdk",
|
|
764
|
+
"./docs",
|
|
765
|
+
"./landing",
|
|
766
|
+
"./devenv",
|
|
767
|
+
"./database",
|
|
768
|
+
"./server",
|
|
769
|
+
"./shared",
|
|
770
|
+
"./experimental",
|
|
771
|
+
"./e2e",
|
|
772
|
+
"./web"
|
|
773
|
+
],
|
|
774
|
+
devDependencies: {
|
|
775
|
+
"@playwright/test": "^1.54.0",
|
|
776
|
+
"@types/node": "^22.10.2",
|
|
777
|
+
esbuild: "^0.27.0",
|
|
778
|
+
glob: "^10.3.10",
|
|
779
|
+
kubik: "^0.24.0",
|
|
780
|
+
tsx: "^4.19.2",
|
|
781
|
+
typescript: "^5.6.2"
|
|
782
|
+
}
|
|
783
|
+
};
|
|
742
784
|
|
|
743
785
|
// src/flakinessConfig.ts
|
|
744
786
|
import fs2 from "fs";
|
|
@@ -869,6 +911,29 @@ var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:
|
|
|
869
911
|
function stripAnsi(str) {
|
|
870
912
|
return str.replace(ansiRegex, "");
|
|
871
913
|
}
|
|
914
|
+
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
915
|
+
const reportPath = path.join(outputFolder, "report.json");
|
|
916
|
+
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
917
|
+
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
918
|
+
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
919
|
+
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
920
|
+
if (attachments.length)
|
|
921
|
+
await fs.promises.mkdir(attachmentsFolder);
|
|
922
|
+
const movedAttachments = [];
|
|
923
|
+
for (const attachment of attachments) {
|
|
924
|
+
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
925
|
+
if (attachment.path)
|
|
926
|
+
await fs.promises.cp(attachment.path, attachmentPath);
|
|
927
|
+
else if (attachment.body)
|
|
928
|
+
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
929
|
+
movedAttachments.push({
|
|
930
|
+
contentType: attachment.contentType,
|
|
931
|
+
id: attachment.id,
|
|
932
|
+
path: attachmentPath
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return movedAttachments;
|
|
936
|
+
}
|
|
872
937
|
function shell(command, args, options) {
|
|
873
938
|
try {
|
|
874
939
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
@@ -1158,7 +1223,7 @@ import fs5 from "fs/promises";
|
|
|
1158
1223
|
import path5 from "path";
|
|
1159
1224
|
|
|
1160
1225
|
// src/junit.ts
|
|
1161
|
-
import { ReportUtils as ReportUtils2
|
|
1226
|
+
import { ReportUtils as ReportUtils2 } from "@flakiness/report";
|
|
1162
1227
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
1163
1228
|
import assert2 from "assert";
|
|
1164
1229
|
import fs4 from "fs";
|
|
@@ -1236,7 +1301,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
1236
1301
|
file,
|
|
1237
1302
|
line,
|
|
1238
1303
|
column: 1
|
|
1239
|
-
} :
|
|
1304
|
+
} : void 0,
|
|
1240
1305
|
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
1241
1306
|
suites: [],
|
|
1242
1307
|
tests: []
|
|
@@ -1298,7 +1363,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
1298
1363
|
file,
|
|
1299
1364
|
line,
|
|
1300
1365
|
column: 1
|
|
1301
|
-
} :
|
|
1366
|
+
} : void 0,
|
|
1302
1367
|
attempts: [{
|
|
1303
1368
|
environmentIdx: currentEnvIndex,
|
|
1304
1369
|
expectedStatus,
|
|
@@ -1394,21 +1459,8 @@ async function cmdConvert(junitPath, options) {
|
|
|
1394
1459
|
runStartTimestamp: Date.now(),
|
|
1395
1460
|
runDuration: 0
|
|
1396
1461
|
});
|
|
1397
|
-
await
|
|
1398
|
-
console.log(
|
|
1399
|
-
if (attachments.length > 0) {
|
|
1400
|
-
await fs5.mkdir("fkattachments", { recursive: true });
|
|
1401
|
-
for (const attachment of attachments) {
|
|
1402
|
-
if (attachment.path) {
|
|
1403
|
-
const destPath = path5.join("fkattachments", attachment.id);
|
|
1404
|
-
await fs5.copyFile(attachment.path, destPath);
|
|
1405
|
-
} else if (attachment.body) {
|
|
1406
|
-
const destPath = path5.join("fkattachments", attachment.id);
|
|
1407
|
-
await fs5.writeFile(destPath, attachment.body);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
|
|
1411
|
-
}
|
|
1462
|
+
await saveReportAndAttachments(report, attachments, options.outputDir);
|
|
1463
|
+
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
1412
1464
|
}
|
|
1413
1465
|
async function findXmlFiles(dir, result = []) {
|
|
1414
1466
|
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
@@ -1463,28 +1515,6 @@ async function cmdDownload(session2, project, runId) {
|
|
|
1463
1515
|
console.log(`\u2714\uFE0F Saved as ${rootDir}`);
|
|
1464
1516
|
}
|
|
1465
1517
|
|
|
1466
|
-
// src/cli/cmd-link.ts
|
|
1467
|
-
async function cmdLink(slug) {
|
|
1468
|
-
const session2 = await FlakinessSession.load();
|
|
1469
|
-
if (!session2) {
|
|
1470
|
-
console.log(`Please login first`);
|
|
1471
|
-
process.exit(1);
|
|
1472
|
-
}
|
|
1473
|
-
const [orgSlug, projectSlug] = slug.split("/");
|
|
1474
|
-
const project = await session2.api.project.findProject.GET({
|
|
1475
|
-
orgSlug,
|
|
1476
|
-
projectSlug
|
|
1477
|
-
});
|
|
1478
|
-
if (!project) {
|
|
1479
|
-
console.log(`Failed to find project ${slug}`);
|
|
1480
|
-
process.exit(1);
|
|
1481
|
-
}
|
|
1482
|
-
const config = FlakinessConfig.createEmpty();
|
|
1483
|
-
config.setProjectPublicId(project.projectPublicId);
|
|
1484
|
-
await config.save();
|
|
1485
|
-
console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1518
|
// ../server/lib/common/knownClientIds.js
|
|
1489
1519
|
var KNOWN_CLIENT_IDS = {
|
|
1490
1520
|
OFFICIAL_WEB: "flakiness-io-official-cli",
|
|
@@ -1507,7 +1537,8 @@ async function cmdLogout() {
|
|
|
1507
1537
|
}
|
|
1508
1538
|
|
|
1509
1539
|
// src/cli/cmd-login.ts
|
|
1510
|
-
|
|
1540
|
+
var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
|
|
1541
|
+
async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
|
|
1511
1542
|
await cmdLogout();
|
|
1512
1543
|
const api = createServerAPI(endpoint);
|
|
1513
1544
|
const data = await api.deviceauth.createRequest.POST({
|
|
@@ -1521,8 +1552,8 @@ async function cmdLogin(endpoint) {
|
|
|
1521
1552
|
await new Promise((x) => setTimeout(x, 2e3));
|
|
1522
1553
|
const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
|
|
1523
1554
|
if (!result) {
|
|
1524
|
-
console.
|
|
1525
|
-
|
|
1555
|
+
console.error(`Authorization request was rejected.`);
|
|
1556
|
+
process.exit(1);
|
|
1526
1557
|
}
|
|
1527
1558
|
token = result.token;
|
|
1528
1559
|
if (token)
|
|
@@ -1544,12 +1575,47 @@ async function cmdLogin(endpoint) {
|
|
|
1544
1575
|
const message = e instanceof Error ? e.message : String(e);
|
|
1545
1576
|
console.error(`x Failed to login:`, message);
|
|
1546
1577
|
}
|
|
1578
|
+
return session2;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/cli/cmd-link.ts
|
|
1582
|
+
async function cmdLink(slugOrUrl) {
|
|
1583
|
+
let slug = slugOrUrl;
|
|
1584
|
+
let endpoint = DEFAULT_FLAKINESS_ENDPOINT;
|
|
1585
|
+
if (slugOrUrl.startsWith("http://") || slugOrUrl.startsWith("https://")) {
|
|
1586
|
+
const url = URL.parse(slugOrUrl);
|
|
1587
|
+
if (!url) {
|
|
1588
|
+
console.error(`Invalid URL: ${slugOrUrl}`);
|
|
1589
|
+
process.exit(1);
|
|
1590
|
+
}
|
|
1591
|
+
slug = url.pathname.substring(1);
|
|
1592
|
+
endpoint = url.origin;
|
|
1593
|
+
} else if (slugOrUrl.startsWith("flakiness.io/")) {
|
|
1594
|
+
endpoint = "https://flakiness.io";
|
|
1595
|
+
slug = slugOrUrl.substring("flakiness.io/".length);
|
|
1596
|
+
}
|
|
1597
|
+
let session2 = await FlakinessSession.load();
|
|
1598
|
+
if (!session2 || session2.endpoint() !== endpoint)
|
|
1599
|
+
session2 = await cmdLogin(endpoint);
|
|
1600
|
+
const [orgSlug, projectSlug] = slug.split("/");
|
|
1601
|
+
const project = await session2.api.project.findProject.GET({
|
|
1602
|
+
orgSlug,
|
|
1603
|
+
projectSlug
|
|
1604
|
+
});
|
|
1605
|
+
if (!project) {
|
|
1606
|
+
console.log(`Failed to find project ${slug}`);
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1609
|
+
const config = FlakinessConfig.createEmpty();
|
|
1610
|
+
config.setProjectPublicId(project.projectPublicId);
|
|
1611
|
+
await config.save();
|
|
1612
|
+
console.log(`\u2713 Linked to ${session2.endpoint()}/${project.org.orgSlug}/${project.projectSlug}`);
|
|
1547
1613
|
}
|
|
1548
1614
|
|
|
1549
1615
|
// src/cli/cmd-show-report.ts
|
|
1550
1616
|
import chalk from "chalk";
|
|
1551
1617
|
import open2 from "open";
|
|
1552
|
-
import
|
|
1618
|
+
import path8 from "path";
|
|
1553
1619
|
|
|
1554
1620
|
// src/localReportServer.ts
|
|
1555
1621
|
import { TypedHTTP as TypedHTTP3 } from "@flakiness/shared/common/typedHttp.js";
|
|
@@ -1560,9 +1626,14 @@ import compression from "compression";
|
|
|
1560
1626
|
import debug from "debug";
|
|
1561
1627
|
import express from "express";
|
|
1562
1628
|
import "express-async-errors";
|
|
1563
|
-
import fs8 from "fs";
|
|
1564
1629
|
import http2 from "http";
|
|
1565
1630
|
|
|
1631
|
+
// src/localReportApi.ts
|
|
1632
|
+
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
1633
|
+
import fs7 from "fs";
|
|
1634
|
+
import path7 from "path";
|
|
1635
|
+
import { z } from "zod/v4";
|
|
1636
|
+
|
|
1566
1637
|
// src/localGit.ts
|
|
1567
1638
|
import { exec } from "child_process";
|
|
1568
1639
|
import { promisify } from "util";
|
|
@@ -1604,9 +1675,37 @@ async function listLocalCommits(gitRoot, head, count) {
|
|
|
1604
1675
|
}
|
|
1605
1676
|
|
|
1606
1677
|
// src/localReportApi.ts
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1678
|
+
var ReportInfo = class {
|
|
1679
|
+
constructor(_options) {
|
|
1680
|
+
this._options = _options;
|
|
1681
|
+
}
|
|
1682
|
+
report;
|
|
1683
|
+
attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
1684
|
+
commits = [];
|
|
1685
|
+
async refresh() {
|
|
1686
|
+
const report = await fs7.promises.readFile(this._options.reportPath, "utf-8").then((x) => JSON.parse(x)).catch((e) => void 0);
|
|
1687
|
+
if (!report) {
|
|
1688
|
+
this.report = void 0;
|
|
1689
|
+
this.commits = [];
|
|
1690
|
+
this.attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (JSON.stringify(report) === JSON.stringify(this.report))
|
|
1694
|
+
return;
|
|
1695
|
+
this.report = report;
|
|
1696
|
+
this.commits = await listLocalCommits(path7.dirname(this._options.reportPath), report.commitId, 100);
|
|
1697
|
+
const attachmentsDir = this._options.attachmentsFolder;
|
|
1698
|
+
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1699
|
+
if (missingAttachments.length) {
|
|
1700
|
+
const first = missingAttachments.slice(0, 3);
|
|
1701
|
+
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
1702
|
+
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
1703
|
+
if (missingAttachments.length > 3)
|
|
1704
|
+
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
1705
|
+
}
|
|
1706
|
+
this.attachmentIdToPath = attachmentIdToPath;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1610
1709
|
var t = TypedHTTP2.Router.create();
|
|
1611
1710
|
var localReportRouter = {
|
|
1612
1711
|
ping: t.get({
|
|
@@ -1616,7 +1715,7 @@ var localReportRouter = {
|
|
|
1616
1715
|
}),
|
|
1617
1716
|
lastCommits: t.get({
|
|
1618
1717
|
handler: async ({ ctx }) => {
|
|
1619
|
-
return ctx.commits;
|
|
1718
|
+
return ctx.reportInfo.commits;
|
|
1620
1719
|
}
|
|
1621
1720
|
}),
|
|
1622
1721
|
report: {
|
|
@@ -1625,7 +1724,7 @@ var localReportRouter = {
|
|
|
1625
1724
|
attachmentId: z.string().min(1).max(100).transform((id) => id)
|
|
1626
1725
|
}),
|
|
1627
1726
|
handler: async ({ ctx, input }) => {
|
|
1628
|
-
const idx = ctx.attachmentIdToPath.get(input.attachmentId);
|
|
1727
|
+
const idx = ctx.reportInfo.attachmentIdToPath.get(input.attachmentId);
|
|
1629
1728
|
if (!idx)
|
|
1630
1729
|
throw TypedHTTP2.HttpError.withCode("NOT_FOUND");
|
|
1631
1730
|
const buffer = await fs7.promises.readFile(idx.path);
|
|
@@ -1634,7 +1733,8 @@ var localReportRouter = {
|
|
|
1634
1733
|
}),
|
|
1635
1734
|
json: t.get({
|
|
1636
1735
|
handler: async ({ ctx }) => {
|
|
1637
|
-
|
|
1736
|
+
await ctx.reportInfo.refresh();
|
|
1737
|
+
return ctx.reportInfo.report;
|
|
1638
1738
|
}
|
|
1639
1739
|
})
|
|
1640
1740
|
}
|
|
@@ -1649,17 +1749,6 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
1649
1749
|
this._authToken = _authToken;
|
|
1650
1750
|
}
|
|
1651
1751
|
static async create(options) {
|
|
1652
|
-
const report = JSON.parse(await fs8.promises.readFile(options.reportPath, "utf-8"));
|
|
1653
|
-
const attachmentsDir = options.attachmentsFolder;
|
|
1654
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1655
|
-
if (missingAttachments.length) {
|
|
1656
|
-
const first = missingAttachments.slice(0, 3);
|
|
1657
|
-
for (let i = 0; i < 3 && i < missingAttachments.length; ++i)
|
|
1658
|
-
console.warn(`Missing attachment with id ${missingAttachments[i]}`);
|
|
1659
|
-
if (missingAttachments.length > 3)
|
|
1660
|
-
console.warn(`...and ${missingAttachments.length - 3} more missing attachments.`);
|
|
1661
|
-
}
|
|
1662
|
-
const commits = await listLocalCommits(process.cwd(), report.commitId, 100);
|
|
1663
1752
|
const app = express();
|
|
1664
1753
|
app.set("etag", false);
|
|
1665
1754
|
const authToken = randomUUIDBase62();
|
|
@@ -1682,9 +1771,10 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
1682
1771
|
});
|
|
1683
1772
|
next();
|
|
1684
1773
|
});
|
|
1774
|
+
const reportInfo = new ReportInfo(options);
|
|
1685
1775
|
app.use("/" + authToken, createTypedHttpExpressMiddleware({
|
|
1686
1776
|
router: localReportRouter,
|
|
1687
|
-
createRootContext: async ({ req, res, input }) => ({
|
|
1777
|
+
createRootContext: async ({ req, res, input }) => ({ reportInfo })
|
|
1688
1778
|
}));
|
|
1689
1779
|
app.use((err2, req, res, next) => {
|
|
1690
1780
|
if (err2 instanceof TypedHTTP3.HttpError)
|
|
@@ -1718,7 +1808,7 @@ var LocalReportServer = class _LocalReportServer {
|
|
|
1718
1808
|
|
|
1719
1809
|
// src/cli/cmd-show-report.ts
|
|
1720
1810
|
async function cmdShowReport(reportFolder) {
|
|
1721
|
-
const reportPath =
|
|
1811
|
+
const reportPath = path8.join(reportFolder, "report.json");
|
|
1722
1812
|
const session2 = await FlakinessSession.load();
|
|
1723
1813
|
const config = await FlakinessConfig.load();
|
|
1724
1814
|
const projectPublicId = config.projectPublicId();
|
|
@@ -1768,8 +1858,8 @@ async function cmdUnlink() {
|
|
|
1768
1858
|
}
|
|
1769
1859
|
|
|
1770
1860
|
// src/cli/cmd-upload-playwright-json.ts
|
|
1771
|
-
import
|
|
1772
|
-
import
|
|
1861
|
+
import fs9 from "fs/promises";
|
|
1862
|
+
import path9 from "path";
|
|
1773
1863
|
|
|
1774
1864
|
// src/playwrightJSONReport.ts
|
|
1775
1865
|
import { FlakinessReport as FK2 } from "@flakiness/report";
|
|
@@ -1950,7 +2040,7 @@ function parseJSONError(context, error) {
|
|
|
1950
2040
|
// src/reportUploader.ts
|
|
1951
2041
|
import { compressTextAsync, compressTextSync } from "@flakiness/shared/node/compression.js";
|
|
1952
2042
|
import assert3 from "assert";
|
|
1953
|
-
import
|
|
2043
|
+
import fs8 from "fs";
|
|
1954
2044
|
import { URL as URL2 } from "url";
|
|
1955
2045
|
var ReportUploader = class _ReportUploader {
|
|
1956
2046
|
static optionsFromEnv(overrides) {
|
|
@@ -2048,16 +2138,16 @@ var ReportUpload = class {
|
|
|
2048
2138
|
url: uploadUrl,
|
|
2049
2139
|
headers: {
|
|
2050
2140
|
"Content-Type": attachment.contentType,
|
|
2051
|
-
"Content-Length": (await
|
|
2141
|
+
"Content-Length": (await fs8.promises.stat(attachmentPath)).size + ""
|
|
2052
2142
|
},
|
|
2053
2143
|
method: "put"
|
|
2054
2144
|
});
|
|
2055
|
-
|
|
2145
|
+
fs8.createReadStream(attachmentPath).pipe(request);
|
|
2056
2146
|
await responseDataPromise;
|
|
2057
2147
|
}, HTTP_BACKOFF);
|
|
2058
2148
|
return;
|
|
2059
2149
|
}
|
|
2060
|
-
let buffer = attachment.body ? attachment.body : attachment.path ? await
|
|
2150
|
+
let buffer = attachment.body ? attachment.body : attachment.path ? await fs8.promises.readFile(attachment.path) : void 0;
|
|
2061
2151
|
assert3(buffer);
|
|
2062
2152
|
const encoding = compressable ? "br" : void 0;
|
|
2063
2153
|
if (compressable)
|
|
@@ -2082,12 +2172,12 @@ var ReportUpload = class {
|
|
|
2082
2172
|
|
|
2083
2173
|
// src/cli/cmd-upload-playwright-json.ts
|
|
2084
2174
|
async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
2085
|
-
const fullPath =
|
|
2086
|
-
if (!await
|
|
2175
|
+
const fullPath = path9.resolve(relativePath);
|
|
2176
|
+
if (!await fs9.access(fullPath, fs9.constants.F_OK).then(() => true).catch(() => false)) {
|
|
2087
2177
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
2088
2178
|
process.exit(1);
|
|
2089
2179
|
}
|
|
2090
|
-
const text = await
|
|
2180
|
+
const text = await fs9.readFile(fullPath, "utf-8");
|
|
2091
2181
|
const playwrightJson = JSON.parse(text);
|
|
2092
2182
|
const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
|
|
2093
2183
|
extractAttachments: true
|
|
@@ -2109,20 +2199,20 @@ async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
|
2109
2199
|
|
|
2110
2200
|
// src/cli/cmd-upload.ts
|
|
2111
2201
|
import chalk2 from "chalk";
|
|
2112
|
-
import
|
|
2113
|
-
import
|
|
2202
|
+
import fs10 from "fs/promises";
|
|
2203
|
+
import path10 from "path";
|
|
2114
2204
|
var warn = (txt) => console.warn(chalk2.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
2115
2205
|
var err = (txt) => console.error(chalk2.red(`[flakiness.io] Error: ${txt}`));
|
|
2116
2206
|
var log = (txt) => console.log(`[flakiness.io] ${txt}`);
|
|
2117
2207
|
async function cmdUpload(relativePath, options) {
|
|
2118
|
-
const fullPath =
|
|
2119
|
-
if (!await
|
|
2208
|
+
const fullPath = path10.resolve(relativePath);
|
|
2209
|
+
if (!await fs10.access(fullPath, fs10.constants.F_OK).then(() => true).catch(() => false)) {
|
|
2120
2210
|
err(`Path ${fullPath} is not accessible!`);
|
|
2121
2211
|
process.exit(1);
|
|
2122
2212
|
}
|
|
2123
|
-
const text = await
|
|
2213
|
+
const text = await fs10.readFile(fullPath, "utf-8");
|
|
2124
2214
|
const report = JSON.parse(text);
|
|
2125
|
-
const attachmentsDir = options.attachmentsDir ??
|
|
2215
|
+
const attachmentsDir = options.attachmentsDir ?? path10.dirname(fullPath);
|
|
2126
2216
|
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
2127
2217
|
if (missingAttachments.length) {
|
|
2128
2218
|
warn(`Missing ${missingAttachments.length} attachments`);
|
|
@@ -2155,7 +2245,7 @@ async function cmdWhoami() {
|
|
|
2155
2245
|
// src/cli/cli.ts
|
|
2156
2246
|
var session = await FlakinessSession.load();
|
|
2157
2247
|
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
2158
|
-
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ??
|
|
2248
|
+
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
|
|
2159
2249
|
var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
|
|
2160
2250
|
async function runCommand(callback) {
|
|
2161
2251
|
try {
|
|
@@ -2167,8 +2257,7 @@ async function runCommand(callback) {
|
|
|
2167
2257
|
process.exit(1);
|
|
2168
2258
|
}
|
|
2169
2259
|
}
|
|
2170
|
-
var
|
|
2171
|
-
var program = new Command().name("flakiness").description("Flakiness CLI tool").version(PACKAGE_JSON.version);
|
|
2260
|
+
var program = new Command().name("flakiness").description("Flakiness CLI tool").version(package_default.version);
|
|
2172
2261
|
async function ensureAccessToken(options) {
|
|
2173
2262
|
let accessToken = options.accessToken;
|
|
2174
2263
|
if (!accessToken) {
|
|
@@ -2195,7 +2284,7 @@ program.command("upload-playwright-json", { hidden: true }).description("Upload
|
|
|
2195
2284
|
await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
|
|
2196
2285
|
}));
|
|
2197
2286
|
var optLink = new Option("--link <org/proj>", "A project to link to");
|
|
2198
|
-
program.command("login").description("Login to the
|
|
2287
|
+
program.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).addOption(optLink).action(async (options) => runCommand(async () => {
|
|
2199
2288
|
await cmdLogin(options.endpoint);
|
|
2200
2289
|
if (options.link)
|
|
2201
2290
|
await cmdLink(options.link);
|
|
@@ -2206,11 +2295,11 @@ program.command("logout").description("Logout from current session").action(asyn
|
|
|
2206
2295
|
program.command("whoami").description("Show current logged in user information").action(async () => runCommand(async () => {
|
|
2207
2296
|
await cmdWhoami();
|
|
2208
2297
|
}));
|
|
2209
|
-
program.command("link").description("Link repository to the flakiness project").addOption(optEndpoint).argument("org/project",
|
|
2298
|
+
program.command("link").description("Link repository to the flakiness project").addOption(optEndpoint).argument("flakiness.io/org/project", "A URL of the Flakiness.io project").action(async (slugOrUrl, options) => runCommand(async () => {
|
|
2210
2299
|
const session2 = await FlakinessSession.load();
|
|
2211
2300
|
if (!session2)
|
|
2212
2301
|
await cmdLogin(options.endpoint);
|
|
2213
|
-
await cmdLink(
|
|
2302
|
+
await cmdLink(slugOrUrl);
|
|
2214
2303
|
}));
|
|
2215
2304
|
program.command("unlink").description("Unlink repository from the flakiness project").action(async () => runCommand(async () => {
|
|
2216
2305
|
await cmdUnlink();
|
|
@@ -2268,11 +2357,11 @@ program.command("upload").description("Upload Flakiness report to the flakiness.
|
|
|
2268
2357
|
await cmdUpload(relativePath, await ensureAccessToken(options));
|
|
2269
2358
|
});
|
|
2270
2359
|
});
|
|
2271
|
-
program.command("show
|
|
2272
|
-
const dir =
|
|
2360
|
+
program.command("show").description("Show flakiness report").argument("[relative-path]", "Path to the Flakiness report file or folder that contains `report.json`. (default: flakiness-report)").action(async (arg) => runCommand(async () => {
|
|
2361
|
+
const dir = path11.resolve(arg ?? "flakiness-report");
|
|
2273
2362
|
await cmdShowReport(dir);
|
|
2274
2363
|
}));
|
|
2275
|
-
program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "
|
|
2364
|
+
program.command("convert-junit").description("Convert JUnit XML report(s) to Flakiness report format").argument("<junit-root-dir-path>", "Path to JUnit XML file or directory containing XML files").option("--env-name <name>", "Environment name for the report", "junit").option("--commit-id <id>", "Git commit ID (auto-detected if not provided)").option("--output-dir <dir>", "Output directory for the report", "flakiness-report").action(async (junitPath, options) => {
|
|
2276
2365
|
await runCommand(async () => {
|
|
2277
2366
|
await cmdConvert(junitPath, options);
|
|
2278
2367
|
});
|
package/lib/cli/cmd-convert.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/cmd-convert.ts
|
|
4
4
|
import fs3 from "fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
|
|
7
7
|
// src/junit.ts
|
|
8
|
-
import { ReportUtils as ReportUtils2
|
|
8
|
+
import { ReportUtils as ReportUtils2 } from "@flakiness/report";
|
|
9
9
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
10
10
|
import assert2 from "assert";
|
|
11
11
|
import fs2 from "fs";
|
|
12
|
-
import
|
|
12
|
+
import path2 from "path";
|
|
13
13
|
|
|
14
14
|
// src/utils.ts
|
|
15
15
|
import { ReportUtils } from "@flakiness/report";
|
|
@@ -19,6 +19,7 @@ import crypto from "crypto";
|
|
|
19
19
|
import fs from "fs";
|
|
20
20
|
import http from "http";
|
|
21
21
|
import https from "https";
|
|
22
|
+
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
22
23
|
function sha1File(filePath) {
|
|
23
24
|
return new Promise((resolve, reject) => {
|
|
24
25
|
const hash = crypto.createHash("sha1");
|
|
@@ -122,6 +123,29 @@ var httpUtils;
|
|
|
122
123
|
httpUtils2.postJSON = postJSON;
|
|
123
124
|
})(httpUtils || (httpUtils = {}));
|
|
124
125
|
var ansiRegex = new RegExp("[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", "g");
|
|
126
|
+
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
127
|
+
const reportPath = path.join(outputFolder, "report.json");
|
|
128
|
+
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
129
|
+
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
130
|
+
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
131
|
+
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
132
|
+
if (attachments.length)
|
|
133
|
+
await fs.promises.mkdir(attachmentsFolder);
|
|
134
|
+
const movedAttachments = [];
|
|
135
|
+
for (const attachment of attachments) {
|
|
136
|
+
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
137
|
+
if (attachment.path)
|
|
138
|
+
await fs.promises.cp(attachment.path, attachmentPath);
|
|
139
|
+
else if (attachment.body)
|
|
140
|
+
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
141
|
+
movedAttachments.push({
|
|
142
|
+
contentType: attachment.contentType,
|
|
143
|
+
id: attachment.id,
|
|
144
|
+
path: attachmentPath
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return movedAttachments;
|
|
148
|
+
}
|
|
125
149
|
function shell(command, args, options) {
|
|
126
150
|
try {
|
|
127
151
|
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
@@ -187,7 +211,7 @@ function extractStdout(testcase, stdio) {
|
|
|
187
211
|
}));
|
|
188
212
|
}
|
|
189
213
|
async function parseAttachment(value) {
|
|
190
|
-
let absolutePath =
|
|
214
|
+
let absolutePath = path2.resolve(process.cwd(), value);
|
|
191
215
|
if (fs2.existsSync(absolutePath)) {
|
|
192
216
|
const id = await sha1File(absolutePath);
|
|
193
217
|
return {
|
|
@@ -219,7 +243,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
219
243
|
file,
|
|
220
244
|
line,
|
|
221
245
|
column: 1
|
|
222
|
-
} :
|
|
246
|
+
} : void 0,
|
|
223
247
|
type: name ? "suite" : file ? "file" : "anonymous suite",
|
|
224
248
|
suites: [],
|
|
225
249
|
tests: []
|
|
@@ -260,7 +284,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
260
284
|
id: attachment.id,
|
|
261
285
|
contentType: attachment.contentType,
|
|
262
286
|
//TODO: better default names for attachments?
|
|
263
|
-
name: attachment.path ?
|
|
287
|
+
name: attachment.path ? path2.basename(attachment.path) : `attachment`
|
|
264
288
|
});
|
|
265
289
|
} else {
|
|
266
290
|
annotations.push({
|
|
@@ -281,7 +305,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
281
305
|
file,
|
|
282
306
|
line,
|
|
283
307
|
column: 1
|
|
284
|
-
} :
|
|
308
|
+
} : void 0,
|
|
285
309
|
attempts: [{
|
|
286
310
|
environmentIdx: currentEnvIndex,
|
|
287
311
|
expectedStatus,
|
|
@@ -335,7 +359,7 @@ async function parseJUnit(xmls, options) {
|
|
|
335
359
|
|
|
336
360
|
// src/cli/cmd-convert.ts
|
|
337
361
|
async function cmdConvert(junitPath, options) {
|
|
338
|
-
const fullPath =
|
|
362
|
+
const fullPath = path3.resolve(junitPath);
|
|
339
363
|
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
340
364
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
341
365
|
process.exit(1);
|
|
@@ -377,26 +401,13 @@ async function cmdConvert(junitPath, options) {
|
|
|
377
401
|
runStartTimestamp: Date.now(),
|
|
378
402
|
runDuration: 0
|
|
379
403
|
});
|
|
380
|
-
await
|
|
381
|
-
console.log(
|
|
382
|
-
if (attachments.length > 0) {
|
|
383
|
-
await fs3.mkdir("fkattachments", { recursive: true });
|
|
384
|
-
for (const attachment of attachments) {
|
|
385
|
-
if (attachment.path) {
|
|
386
|
-
const destPath = path2.join("fkattachments", attachment.id);
|
|
387
|
-
await fs3.copyFile(attachment.path, destPath);
|
|
388
|
-
} else if (attachment.body) {
|
|
389
|
-
const destPath = path2.join("fkattachments", attachment.id);
|
|
390
|
-
await fs3.writeFile(destPath, attachment.body);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
console.log(`\u2713 Saved ${attachments.length} attachments to fkattachments/`);
|
|
394
|
-
}
|
|
404
|
+
await saveReportAndAttachments(report, attachments, options.outputDir);
|
|
405
|
+
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
395
406
|
}
|
|
396
407
|
async function findXmlFiles(dir, result = []) {
|
|
397
408
|
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
398
409
|
for (const entry of entries) {
|
|
399
|
-
const fullPath =
|
|
410
|
+
const fullPath = path3.join(dir, entry.name);
|
|
400
411
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
401
412
|
result.push(fullPath);
|
|
402
413
|
else if (entry.isDirectory())
|