flakiness 0.147.0 → 0.149.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 +36 -411
- package/lib/cli/cmd-convert.js +16 -60
- package/lib/cli/cmd-login.js +0 -2
- package/lib/cli/cmd-logout.js +0 -2
- package/lib/cli/cmd-status.js +0 -2
- package/lib/cli/cmd-upload.js +8 -17
- package/lib/cli/cmd-whoami.js +0 -2
- package/lib/flakinessSession.js +0 -2
- package/lib/junit.js +5 -5
- package/lib/serverapi.js +0 -2
- package/lib/utils.js +3 -190
- package/package.json +4 -4
- package/types/tsconfig.tsbuildinfo +1 -1
- package/lib/cli/cmd-upload-playwright-json.js +0 -463
- package/lib/playwrightJSONReport.js +0 -429
package/lib/cli/cli.js
CHANGED
|
@@ -738,16 +738,16 @@ var Ranges;
|
|
|
738
738
|
|
|
739
739
|
// src/cli/cli.ts
|
|
740
740
|
import { TypedHTTP as TypedHTTP2 } from "@flakiness/shared/common/typedHttp.js";
|
|
741
|
-
import
|
|
741
|
+
import assert2 from "assert";
|
|
742
742
|
import { Command, Option } from "commander";
|
|
743
|
-
import
|
|
743
|
+
import fs7 from "fs";
|
|
744
744
|
import ora from "ora";
|
|
745
|
-
import
|
|
745
|
+
import path7 from "path";
|
|
746
746
|
|
|
747
747
|
// ../package.json
|
|
748
748
|
var package_default = {
|
|
749
749
|
name: "flakiness",
|
|
750
|
-
version: "0.
|
|
750
|
+
version: "0.149.0",
|
|
751
751
|
private: true,
|
|
752
752
|
scripts: {
|
|
753
753
|
minor: "./version.mjs minor",
|
|
@@ -764,7 +764,6 @@ var package_default = {
|
|
|
764
764
|
author: "Degu Labs, Inc",
|
|
765
765
|
license: "Fair Source 100",
|
|
766
766
|
workspaces: [
|
|
767
|
-
"./report",
|
|
768
767
|
"./sdk",
|
|
769
768
|
"./cli",
|
|
770
769
|
"./playwright",
|
|
@@ -791,7 +790,7 @@ var package_default = {
|
|
|
791
790
|
|
|
792
791
|
// src/flakinessSession.ts
|
|
793
792
|
import fs2 from "fs/promises";
|
|
794
|
-
import
|
|
793
|
+
import os from "os";
|
|
795
794
|
import path2 from "path";
|
|
796
795
|
|
|
797
796
|
// src/serverapi.ts
|
|
@@ -799,23 +798,11 @@ import { TypedHTTP } from "@flakiness/shared/common/typedHttp.js";
|
|
|
799
798
|
|
|
800
799
|
// src/utils.ts
|
|
801
800
|
import { ReportUtils } from "@flakiness/sdk";
|
|
802
|
-
import assert from "assert";
|
|
803
|
-
import { spawnSync } from "child_process";
|
|
804
801
|
import crypto from "crypto";
|
|
805
802
|
import fs from "fs";
|
|
806
803
|
import http from "http";
|
|
807
804
|
import https from "https";
|
|
808
|
-
import
|
|
809
|
-
import path, { posix as posixPath, win32 as win32Path } from "path";
|
|
810
|
-
async function existsAsync(aPath) {
|
|
811
|
-
return fs.promises.stat(aPath).then(() => true).catch((e) => false);
|
|
812
|
-
}
|
|
813
|
-
function extractEnvConfiguration() {
|
|
814
|
-
const ENV_PREFIX = "FK_ENV_";
|
|
815
|
-
return Object.fromEntries(
|
|
816
|
-
Object.entries(process.env).filter(([key]) => key.toUpperCase().startsWith(ENV_PREFIX.toUpperCase())).map(([key, value]) => [key.substring(ENV_PREFIX.length).toLowerCase(), (value ?? "").trim().toLowerCase()])
|
|
817
|
-
);
|
|
818
|
-
}
|
|
805
|
+
import path from "path";
|
|
819
806
|
function sha1File(filePath) {
|
|
820
807
|
return new Promise((resolve, reject) => {
|
|
821
808
|
const hash = crypto.createHash("sha1");
|
|
@@ -919,96 +906,6 @@ var httpUtils;
|
|
|
919
906
|
httpUtils2.postJSON = postJSON;
|
|
920
907
|
})(httpUtils || (httpUtils = {}));
|
|
921
908
|
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");
|
|
922
|
-
function stripAnsi(str) {
|
|
923
|
-
return str.replace(ansiRegex, "");
|
|
924
|
-
}
|
|
925
|
-
async function saveReportAndAttachments(report, attachments, outputFolder) {
|
|
926
|
-
const reportPath = path.join(outputFolder, "report.json");
|
|
927
|
-
const attachmentsFolder = path.join(outputFolder, "attachments");
|
|
928
|
-
await fs.promises.rm(outputFolder, { recursive: true, force: true });
|
|
929
|
-
await fs.promises.mkdir(outputFolder, { recursive: true });
|
|
930
|
-
await fs.promises.writeFile(reportPath, JSON.stringify(report), "utf-8");
|
|
931
|
-
if (attachments.length)
|
|
932
|
-
await fs.promises.mkdir(attachmentsFolder);
|
|
933
|
-
const movedAttachments = [];
|
|
934
|
-
for (const attachment of attachments) {
|
|
935
|
-
const attachmentPath = path.join(attachmentsFolder, attachment.id);
|
|
936
|
-
if (attachment.path)
|
|
937
|
-
await fs.promises.cp(attachment.path, attachmentPath);
|
|
938
|
-
else if (attachment.body)
|
|
939
|
-
await fs.promises.writeFile(attachmentPath, attachment.body);
|
|
940
|
-
movedAttachments.push({
|
|
941
|
-
contentType: attachment.contentType,
|
|
942
|
-
id: attachment.id,
|
|
943
|
-
path: attachmentPath
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
return movedAttachments;
|
|
947
|
-
}
|
|
948
|
-
function shell(command, args, options) {
|
|
949
|
-
try {
|
|
950
|
-
const result = spawnSync(command, args, { encoding: "utf-8", ...options });
|
|
951
|
-
if (result.status !== 0) {
|
|
952
|
-
return void 0;
|
|
953
|
-
}
|
|
954
|
-
return result.stdout.trim();
|
|
955
|
-
} catch (e) {
|
|
956
|
-
console.error(e);
|
|
957
|
-
return void 0;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
function readLinuxOSRelease() {
|
|
961
|
-
const osReleaseText = fs.readFileSync("/etc/os-release", "utf-8");
|
|
962
|
-
return new Map(osReleaseText.toLowerCase().split("\n").filter((line) => line.includes("=")).map((line) => {
|
|
963
|
-
line = line.trim();
|
|
964
|
-
let [key, value] = line.split("=");
|
|
965
|
-
if (value.startsWith('"') && value.endsWith('"'))
|
|
966
|
-
value = value.substring(1, value.length - 1);
|
|
967
|
-
return [key, value];
|
|
968
|
-
}));
|
|
969
|
-
}
|
|
970
|
-
function osLinuxInfo() {
|
|
971
|
-
const arch = shell(`uname`, [`-m`]);
|
|
972
|
-
const osReleaseMap = readLinuxOSRelease();
|
|
973
|
-
const name = osReleaseMap.get("name") ?? shell(`uname`);
|
|
974
|
-
const version = osReleaseMap.get("version_id");
|
|
975
|
-
return { name, arch, version };
|
|
976
|
-
}
|
|
977
|
-
function osDarwinInfo() {
|
|
978
|
-
const name = "macos";
|
|
979
|
-
const arch = shell(`uname`, [`-m`]);
|
|
980
|
-
const version = shell(`sw_vers`, [`-productVersion`]);
|
|
981
|
-
return { name, arch, version };
|
|
982
|
-
}
|
|
983
|
-
function osWinInfo() {
|
|
984
|
-
const name = "win";
|
|
985
|
-
const arch = process.arch;
|
|
986
|
-
const version = os.release();
|
|
987
|
-
return { name, arch, version };
|
|
988
|
-
}
|
|
989
|
-
function getOSInfo() {
|
|
990
|
-
if (process.platform === "darwin")
|
|
991
|
-
return osDarwinInfo();
|
|
992
|
-
if (process.platform === "win32")
|
|
993
|
-
return osWinInfo();
|
|
994
|
-
return osLinuxInfo();
|
|
995
|
-
}
|
|
996
|
-
function inferRunUrl() {
|
|
997
|
-
if (process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
|
998
|
-
return `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
999
|
-
return void 0;
|
|
1000
|
-
}
|
|
1001
|
-
function parseStringDate(dateString) {
|
|
1002
|
-
return +new Date(dateString);
|
|
1003
|
-
}
|
|
1004
|
-
function gitCommitInfo(gitRepo) {
|
|
1005
|
-
const sha = shell(`git`, ["rev-parse", "HEAD"], {
|
|
1006
|
-
cwd: gitRepo,
|
|
1007
|
-
encoding: "utf-8"
|
|
1008
|
-
});
|
|
1009
|
-
assert(sha, `FAILED: git rev-parse HEAD @ ${gitRepo}`);
|
|
1010
|
-
return sha.trim();
|
|
1011
|
-
}
|
|
1012
909
|
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
1013
910
|
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
1014
911
|
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
@@ -1024,7 +921,8 @@ async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
|
1024
921
|
attachmentIdToPath.set(attachment.id, {
|
|
1025
922
|
contentType: attachment.contentType,
|
|
1026
923
|
id: attachment.id,
|
|
1027
|
-
path: attachmentPath
|
|
924
|
+
path: attachmentPath,
|
|
925
|
+
type: "file"
|
|
1028
926
|
});
|
|
1029
927
|
}
|
|
1030
928
|
}
|
|
@@ -1043,65 +941,6 @@ async function listFilesRecursively(dir, result = []) {
|
|
|
1043
941
|
}
|
|
1044
942
|
return result;
|
|
1045
943
|
}
|
|
1046
|
-
function computeGitRoot(somePathInsideGitRepo) {
|
|
1047
|
-
const root = shell(`git`, ["rev-parse", "--show-toplevel"], {
|
|
1048
|
-
cwd: somePathInsideGitRepo,
|
|
1049
|
-
encoding: "utf-8"
|
|
1050
|
-
});
|
|
1051
|
-
assert(root, `FAILED: git rev-parse --show-toplevel HEAD @ ${somePathInsideGitRepo}`);
|
|
1052
|
-
return normalizePath(root);
|
|
1053
|
-
}
|
|
1054
|
-
var IS_WIN32_PATH = new RegExp("^[a-zA-Z]:\\\\", "i");
|
|
1055
|
-
var IS_ALMOST_POSIX_PATH = new RegExp("^[a-zA-Z]:/", "i");
|
|
1056
|
-
function normalizePath(aPath) {
|
|
1057
|
-
if (IS_WIN32_PATH.test(aPath)) {
|
|
1058
|
-
aPath = aPath.split(win32Path.sep).join(posixPath.sep);
|
|
1059
|
-
}
|
|
1060
|
-
if (IS_ALMOST_POSIX_PATH.test(aPath))
|
|
1061
|
-
return "/" + aPath[0] + aPath.substring(2);
|
|
1062
|
-
return aPath;
|
|
1063
|
-
}
|
|
1064
|
-
function gitFilePath(gitRoot, absolutePath) {
|
|
1065
|
-
return posixPath.relative(gitRoot, absolutePath);
|
|
1066
|
-
}
|
|
1067
|
-
function parseDurationMS(value) {
|
|
1068
|
-
if (isNaN(value))
|
|
1069
|
-
throw new Error("Duration cannot be NaN");
|
|
1070
|
-
if (value < 0)
|
|
1071
|
-
throw new Error(`Duration cannot be less than 0, found ${value}`);
|
|
1072
|
-
return value | 0;
|
|
1073
|
-
}
|
|
1074
|
-
function createEnvironments(projects) {
|
|
1075
|
-
const envConfiguration = extractEnvConfiguration();
|
|
1076
|
-
const osInfo = getOSInfo();
|
|
1077
|
-
let uniqueNames = /* @__PURE__ */ new Set();
|
|
1078
|
-
const result = /* @__PURE__ */ new Map();
|
|
1079
|
-
for (const project of projects) {
|
|
1080
|
-
let defaultName = project.name;
|
|
1081
|
-
if (!defaultName.trim())
|
|
1082
|
-
defaultName = "anonymous";
|
|
1083
|
-
let name = defaultName;
|
|
1084
|
-
for (let i = 2; uniqueNames.has(name); ++i)
|
|
1085
|
-
name = `${defaultName}-${i}`;
|
|
1086
|
-
uniqueNames.add(defaultName);
|
|
1087
|
-
result.set(project, {
|
|
1088
|
-
name,
|
|
1089
|
-
systemData: {
|
|
1090
|
-
osArch: osInfo.arch,
|
|
1091
|
-
osName: osInfo.name,
|
|
1092
|
-
osVersion: osInfo.version
|
|
1093
|
-
},
|
|
1094
|
-
userSuppliedData: {
|
|
1095
|
-
...envConfiguration,
|
|
1096
|
-
...project.metadata
|
|
1097
|
-
},
|
|
1098
|
-
opaqueData: {
|
|
1099
|
-
project
|
|
1100
|
-
}
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
return result;
|
|
1104
|
-
}
|
|
1105
944
|
|
|
1106
945
|
// src/serverapi.ts
|
|
1107
946
|
function createServerAPI(endpoint, options) {
|
|
@@ -1120,7 +959,7 @@ function createServerAPI(endpoint, options) {
|
|
|
1120
959
|
|
|
1121
960
|
// src/flakinessSession.ts
|
|
1122
961
|
var CONFIG_DIR = (() => {
|
|
1123
|
-
const configDir = process.platform === "darwin" ? path2.join(
|
|
962
|
+
const configDir = process.platform === "darwin" ? path2.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path2.join(os.homedir(), "AppData", "Roaming", "flakiness") : path2.join(os.homedir(), ".config", "flakiness");
|
|
1124
963
|
return configDir;
|
|
1125
964
|
})();
|
|
1126
965
|
var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
|
|
@@ -1162,13 +1001,14 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
1162
1001
|
};
|
|
1163
1002
|
|
|
1164
1003
|
// src/cli/cmd-convert.ts
|
|
1004
|
+
import { GitWorktree, writeReport } from "@flakiness/sdk";
|
|
1165
1005
|
import fs4 from "fs/promises";
|
|
1166
1006
|
import path4 from "path";
|
|
1167
1007
|
|
|
1168
1008
|
// src/junit.ts
|
|
1169
1009
|
import { ReportUtils as ReportUtils2 } from "@flakiness/sdk";
|
|
1170
1010
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
1171
|
-
import
|
|
1011
|
+
import assert from "assert";
|
|
1172
1012
|
import fs3 from "fs";
|
|
1173
1013
|
import path3 from "path";
|
|
1174
1014
|
function getProperties(element) {
|
|
@@ -1218,13 +1058,15 @@ async function parseAttachment(value) {
|
|
|
1218
1058
|
return {
|
|
1219
1059
|
contentType: "image/png",
|
|
1220
1060
|
path: absolutePath,
|
|
1221
|
-
id
|
|
1061
|
+
id,
|
|
1062
|
+
type: "file"
|
|
1222
1063
|
};
|
|
1223
1064
|
}
|
|
1224
1065
|
return {
|
|
1225
1066
|
contentType: "text/plain",
|
|
1226
1067
|
id: sha1Buffer(value),
|
|
1227
|
-
body: Buffer.from(value)
|
|
1068
|
+
body: Buffer.from(value),
|
|
1069
|
+
type: "buffer"
|
|
1228
1070
|
};
|
|
1229
1071
|
}
|
|
1230
1072
|
async function traverseJUnitReport(context, node) {
|
|
@@ -1265,7 +1107,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
1265
1107
|
currentEnvIndex = report.environments.push(currentEnv) - 1;
|
|
1266
1108
|
}
|
|
1267
1109
|
} else if (element.name === "testcase") {
|
|
1268
|
-
|
|
1110
|
+
assert(currentSuite);
|
|
1269
1111
|
const file = element.attributes["file"];
|
|
1270
1112
|
const name = element.attributes["name"];
|
|
1271
1113
|
const line = parseInt(element.attributes["line"], 10);
|
|
@@ -1285,7 +1127,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
1285
1127
|
id: attachment.id,
|
|
1286
1128
|
contentType: attachment.contentType,
|
|
1287
1129
|
//TODO: better default names for attachments?
|
|
1288
|
-
name: attachment.
|
|
1130
|
+
name: attachment.type === "file" ? path3.basename(attachment.path) : `attachment`
|
|
1289
1131
|
});
|
|
1290
1132
|
} else {
|
|
1291
1133
|
annotations.push({
|
|
@@ -1390,7 +1232,8 @@ async function cmdConvert(junitPath, options) {
|
|
|
1390
1232
|
commitId = options.commitId;
|
|
1391
1233
|
} else {
|
|
1392
1234
|
try {
|
|
1393
|
-
|
|
1235
|
+
const worktree = GitWorktree.create(process.cwd());
|
|
1236
|
+
commitId = worktree.headCommitId();
|
|
1394
1237
|
} catch (e) {
|
|
1395
1238
|
console.error("Failed to get git commit info. Please provide --commit-id option.");
|
|
1396
1239
|
process.exit(1);
|
|
@@ -1402,7 +1245,7 @@ async function cmdConvert(junitPath, options) {
|
|
|
1402
1245
|
runStartTimestamp: Date.now(),
|
|
1403
1246
|
runDuration: 0
|
|
1404
1247
|
});
|
|
1405
|
-
await
|
|
1248
|
+
await writeReport(report, attachments, options.outputDir);
|
|
1406
1249
|
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
1407
1250
|
}
|
|
1408
1251
|
async function findXmlFiles(dir, result = []) {
|
|
@@ -1482,7 +1325,7 @@ var KNOWN_CLIENT_IDS = {
|
|
|
1482
1325
|
|
|
1483
1326
|
// src/cli/cmd-login.ts
|
|
1484
1327
|
import open from "open";
|
|
1485
|
-
import
|
|
1328
|
+
import os2 from "os";
|
|
1486
1329
|
|
|
1487
1330
|
// src/cli/cmd-logout.ts
|
|
1488
1331
|
async function cmdLogout() {
|
|
@@ -1502,7 +1345,7 @@ async function cmdLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
|
|
|
1502
1345
|
const api = createServerAPI(endpoint);
|
|
1503
1346
|
const data = await api.deviceauth.createRequest.POST({
|
|
1504
1347
|
clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
|
|
1505
|
-
name:
|
|
1348
|
+
name: os2.hostname()
|
|
1506
1349
|
});
|
|
1507
1350
|
await open(new URL(data.verificationUrl, endpoint).href);
|
|
1508
1351
|
console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
|
|
@@ -1567,246 +1410,31 @@ async function cmdUnlink() {
|
|
|
1567
1410
|
await config.save();
|
|
1568
1411
|
}
|
|
1569
1412
|
|
|
1570
|
-
// src/cli/cmd-upload-playwright-json.ts
|
|
1571
|
-
import { ReportUploader } from "@flakiness/sdk";
|
|
1572
|
-
import fs6 from "fs/promises";
|
|
1573
|
-
import path6 from "path";
|
|
1574
|
-
|
|
1575
|
-
// src/playwrightJSONReport.ts
|
|
1576
|
-
import { FlakinessReport as FK2, ReportUtils as ReportUtils3 } from "@flakiness/sdk";
|
|
1577
|
-
import debug from "debug";
|
|
1578
|
-
import { posix as posixPath2 } from "path";
|
|
1579
|
-
var dlog = debug("flakiness:json-report");
|
|
1580
|
-
var PlaywrightJSONReport;
|
|
1581
|
-
((PlaywrightJSONReport2) => {
|
|
1582
|
-
function collectMetadata(somePathInsideProject = process.cwd()) {
|
|
1583
|
-
const commitId = gitCommitInfo(somePathInsideProject);
|
|
1584
|
-
const osInfo = getOSInfo();
|
|
1585
|
-
const metadata = {
|
|
1586
|
-
gitRoot: computeGitRoot(somePathInsideProject),
|
|
1587
|
-
commitId,
|
|
1588
|
-
osName: osInfo.name,
|
|
1589
|
-
arch: osInfo.arch,
|
|
1590
|
-
osVersion: osInfo.version,
|
|
1591
|
-
runURL: inferRunUrl()
|
|
1592
|
-
};
|
|
1593
|
-
dlog(`metadata directory: ${somePathInsideProject}`);
|
|
1594
|
-
dlog(`metadata: ${JSON.stringify(metadata)}`);
|
|
1595
|
-
dlog(`commit info: ${JSON.stringify(commitId)}`);
|
|
1596
|
-
dlog(`os info: ${JSON.stringify(osInfo)}`);
|
|
1597
|
-
return metadata;
|
|
1598
|
-
}
|
|
1599
|
-
PlaywrightJSONReport2.collectMetadata = collectMetadata;
|
|
1600
|
-
async function parse(metadata, jsonReport, options) {
|
|
1601
|
-
const context = {
|
|
1602
|
-
projectId2environmentIdx: /* @__PURE__ */ new Map(),
|
|
1603
|
-
testBaseDir: normalizePath(jsonReport.config.rootDir),
|
|
1604
|
-
gitRoot: metadata.gitRoot,
|
|
1605
|
-
attachments: /* @__PURE__ */ new Map(),
|
|
1606
|
-
unaccessibleAttachmentPaths: [],
|
|
1607
|
-
extractAttachments: options.extractAttachments
|
|
1608
|
-
};
|
|
1609
|
-
const configPath = jsonReport.config.configFile ? gitFilePath(context.gitRoot, normalizePath(jsonReport.config.configFile)) : void 0;
|
|
1610
|
-
const report = {
|
|
1611
|
-
category: FK2.CATEGORY_PLAYWRIGHT,
|
|
1612
|
-
commitId: metadata.commitId,
|
|
1613
|
-
configPath,
|
|
1614
|
-
url: metadata.runURL,
|
|
1615
|
-
environments: [],
|
|
1616
|
-
suites: [],
|
|
1617
|
-
opaqueData: jsonReport.config,
|
|
1618
|
-
unattributedErrors: jsonReport.errors.map((error) => parseJSONError(context, error)),
|
|
1619
|
-
// The report.stats is a releatively new addition to Playwright's JSONReport,
|
|
1620
|
-
// so we have to polyfill with some reasonable values when it's missing.
|
|
1621
|
-
duration: jsonReport.stats?.duration && jsonReport.stats?.duration > 0 ? parseDurationMS(jsonReport.stats.duration) : 0,
|
|
1622
|
-
startTimestamp: jsonReport.stats && jsonReport.stats.startTime ? parseStringDate(jsonReport.stats.startTime) : Date.now()
|
|
1623
|
-
};
|
|
1624
|
-
report.environments = [...createEnvironments(jsonReport.config.projects).values()];
|
|
1625
|
-
for (let envIdx = 0; envIdx < report.environments.length; ++envIdx)
|
|
1626
|
-
context.projectId2environmentIdx.set(jsonReport.config.projects[envIdx].id, envIdx);
|
|
1627
|
-
report.suites = await Promise.all(jsonReport.suites.map((suite) => parseJSONSuite(context, suite)));
|
|
1628
|
-
return {
|
|
1629
|
-
report: ReportUtils3.normalizeReport(report),
|
|
1630
|
-
attachments: [...context.attachments.values()],
|
|
1631
|
-
unaccessibleAttachmentPaths: context.unaccessibleAttachmentPaths
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
PlaywrightJSONReport2.parse = parse;
|
|
1635
|
-
})(PlaywrightJSONReport || (PlaywrightJSONReport = {}));
|
|
1636
|
-
async function parseJSONSuite(context, jsonSuite) {
|
|
1637
|
-
let type = "suite";
|
|
1638
|
-
if (jsonSuite.column === 0 && jsonSuite.line === 0)
|
|
1639
|
-
type = "file";
|
|
1640
|
-
else if (!jsonSuite.title)
|
|
1641
|
-
type = "anonymous suite";
|
|
1642
|
-
const suite = {
|
|
1643
|
-
type,
|
|
1644
|
-
title: jsonSuite.title,
|
|
1645
|
-
location: {
|
|
1646
|
-
file: gitFilePath(context.gitRoot, normalizePath(jsonSuite.file)),
|
|
1647
|
-
line: jsonSuite.line,
|
|
1648
|
-
column: jsonSuite.column
|
|
1649
|
-
}
|
|
1650
|
-
};
|
|
1651
|
-
if (jsonSuite.suites && jsonSuite.suites.length)
|
|
1652
|
-
suite.suites = await Promise.all(jsonSuite.suites.map((suite2) => parseJSONSuite(context, suite2)));
|
|
1653
|
-
if (jsonSuite.specs && jsonSuite.specs.length)
|
|
1654
|
-
suite.tests = await Promise.all(jsonSuite.specs.map((spec) => parseJSONSpec(context, spec)));
|
|
1655
|
-
return suite;
|
|
1656
|
-
}
|
|
1657
|
-
async function parseJSONSpec(context, jsonSpec) {
|
|
1658
|
-
const test = {
|
|
1659
|
-
title: jsonSpec.title,
|
|
1660
|
-
tags: jsonSpec.tags,
|
|
1661
|
-
location: {
|
|
1662
|
-
file: gitFilePath(context.gitRoot, normalizePath(posixPath2.join(context.testBaseDir, normalizePath(jsonSpec.file)))),
|
|
1663
|
-
line: jsonSpec.line,
|
|
1664
|
-
column: jsonSpec.column
|
|
1665
|
-
},
|
|
1666
|
-
attempts: []
|
|
1667
|
-
};
|
|
1668
|
-
for (const jsonTest of jsonSpec.tests) {
|
|
1669
|
-
const environmentIdx = context.projectId2environmentIdx.get(jsonTest.projectId);
|
|
1670
|
-
if (environmentIdx === void 0)
|
|
1671
|
-
throw new Error("Inconsistent report - no project for a test found!");
|
|
1672
|
-
const testResults = jsonTest.results.filter((result) => result.status !== void 0);
|
|
1673
|
-
if (!testResults.length)
|
|
1674
|
-
continue;
|
|
1675
|
-
test.attempts.push(...await Promise.all(testResults.map((jsonTestResult) => parseJSONTestResult(context, jsonTest, environmentIdx, jsonTestResult))));
|
|
1676
|
-
}
|
|
1677
|
-
return test;
|
|
1678
|
-
}
|
|
1679
|
-
function createLocation(context, location) {
|
|
1680
|
-
return {
|
|
1681
|
-
file: gitFilePath(context.gitRoot, normalizePath(location.file)),
|
|
1682
|
-
line: location.line,
|
|
1683
|
-
column: location.column
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
async function parseJSONTestResult(context, jsonTest, environmentIdx, jsonTestResult) {
|
|
1687
|
-
const attachments = [];
|
|
1688
|
-
const attempt = {
|
|
1689
|
-
timeout: parseDurationMS(jsonTest.timeout),
|
|
1690
|
-
annotations: jsonTest.annotations.map((annotation) => ({
|
|
1691
|
-
type: annotation.type,
|
|
1692
|
-
description: annotation.description,
|
|
1693
|
-
location: annotation.location ? createLocation(context, annotation.location) : void 0
|
|
1694
|
-
})),
|
|
1695
|
-
environmentIdx,
|
|
1696
|
-
expectedStatus: jsonTest.expectedStatus,
|
|
1697
|
-
parallelIndex: jsonTestResult.parallelIndex,
|
|
1698
|
-
status: jsonTestResult.status,
|
|
1699
|
-
errors: jsonTestResult.errors && jsonTestResult.errors.length ? jsonTestResult.errors.map((error) => parseJSONError(context, error)) : void 0,
|
|
1700
|
-
stdout: jsonTestResult.stdout && jsonTestResult.stdout.length ? jsonTestResult.stdout : void 0,
|
|
1701
|
-
stderr: jsonTestResult.stderr && jsonTestResult.stderr.length ? jsonTestResult.stderr : void 0,
|
|
1702
|
-
steps: jsonTestResult.steps ? jsonTestResult.steps.map((jsonTestStep) => parseJSONTestStep(context, jsonTestStep)) : void 0,
|
|
1703
|
-
startTimestamp: parseStringDate(jsonTestResult.startTime),
|
|
1704
|
-
duration: jsonTestResult.duration && jsonTestResult.duration > 0 ? parseDurationMS(jsonTestResult.duration) : 0,
|
|
1705
|
-
attachments
|
|
1706
|
-
};
|
|
1707
|
-
if (context.extractAttachments) {
|
|
1708
|
-
await Promise.all((jsonTestResult.attachments ?? []).map(async (jsonAttachment) => {
|
|
1709
|
-
if (jsonAttachment.path && !await existsAsync(jsonAttachment.path)) {
|
|
1710
|
-
context.unaccessibleAttachmentPaths.push(jsonAttachment.path);
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
const id = jsonAttachment.path ? await sha1File(jsonAttachment.path) : sha1Buffer(jsonAttachment.body ?? "");
|
|
1714
|
-
context.attachments.set(id, {
|
|
1715
|
-
contentType: jsonAttachment.contentType,
|
|
1716
|
-
id,
|
|
1717
|
-
body: jsonAttachment.body ? Buffer.from(jsonAttachment.body) : void 0,
|
|
1718
|
-
path: jsonAttachment.path
|
|
1719
|
-
});
|
|
1720
|
-
attachments.push({
|
|
1721
|
-
id,
|
|
1722
|
-
name: jsonAttachment.name,
|
|
1723
|
-
contentType: jsonAttachment.contentType
|
|
1724
|
-
});
|
|
1725
|
-
}));
|
|
1726
|
-
}
|
|
1727
|
-
return attempt;
|
|
1728
|
-
}
|
|
1729
|
-
function parseJSONTestStep(context, jsonStep) {
|
|
1730
|
-
const step = {
|
|
1731
|
-
// NOTE: jsonStep.duration was -1 in some playwright versions
|
|
1732
|
-
duration: parseDurationMS(Math.max(jsonStep.duration, 0)),
|
|
1733
|
-
title: jsonStep.title
|
|
1734
|
-
};
|
|
1735
|
-
if (jsonStep.error)
|
|
1736
|
-
step.error = parseJSONError(context, jsonStep.error);
|
|
1737
|
-
if (jsonStep.steps)
|
|
1738
|
-
step.steps = jsonStep.steps.map((childJSONStep) => parseJSONTestStep(context, childJSONStep));
|
|
1739
|
-
return step;
|
|
1740
|
-
}
|
|
1741
|
-
function parseJSONError(context, error) {
|
|
1742
|
-
return {
|
|
1743
|
-
location: error.location ? createLocation(context, error.location) : void 0,
|
|
1744
|
-
message: error.message ? stripAnsi(error.message).split("\n")[0] : void 0,
|
|
1745
|
-
stack: error.stack,
|
|
1746
|
-
value: error.value
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
// src/cli/cmd-upload-playwright-json.ts
|
|
1751
|
-
async function cmdUploadPlaywrightJson(relativePath, options) {
|
|
1752
|
-
const fullPath = path6.resolve(relativePath);
|
|
1753
|
-
if (!await fs6.access(fullPath, fs6.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1754
|
-
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1755
|
-
process.exit(1);
|
|
1756
|
-
}
|
|
1757
|
-
const text = await fs6.readFile(fullPath, "utf-8");
|
|
1758
|
-
const playwrightJson = JSON.parse(text);
|
|
1759
|
-
const { attachments, report, unaccessibleAttachmentPaths } = await PlaywrightJSONReport.parse(PlaywrightJSONReport.collectMetadata(), playwrightJson, {
|
|
1760
|
-
extractAttachments: true
|
|
1761
|
-
});
|
|
1762
|
-
for (const unaccessibleAttachment of unaccessibleAttachmentPaths)
|
|
1763
|
-
console.warn(`WARN: cannot access attachment ${unaccessibleAttachment}`);
|
|
1764
|
-
const uploader = new ReportUploader({
|
|
1765
|
-
flakinessAccessToken: options.accessToken,
|
|
1766
|
-
flakinessEndpoint: options.endpoint
|
|
1767
|
-
});
|
|
1768
|
-
const upload = uploader.createUpload(report, attachments);
|
|
1769
|
-
const uploadResult = await upload.upload();
|
|
1770
|
-
if (!uploadResult.success) {
|
|
1771
|
-
console.log(`[flakiness.io] X Failed to upload to ${options.endpoint}: ${uploadResult.message}`);
|
|
1772
|
-
} else {
|
|
1773
|
-
console.log(`[flakiness.io] \u2713 Report uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
1413
|
// src/cli/cmd-upload.ts
|
|
1778
|
-
import {
|
|
1414
|
+
import { uploadReport } from "@flakiness/sdk";
|
|
1779
1415
|
import chalk from "chalk";
|
|
1780
|
-
import
|
|
1781
|
-
import
|
|
1416
|
+
import fs6 from "fs/promises";
|
|
1417
|
+
import path6 from "path";
|
|
1782
1418
|
var warn = (txt) => console.warn(chalk.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
1783
1419
|
var err = (txt) => console.error(chalk.red(`[flakiness.io] Error: ${txt}`));
|
|
1784
|
-
var log = (txt) => console.log(`[flakiness.io] ${txt}`);
|
|
1785
1420
|
async function cmdUpload(relativePaths, options) {
|
|
1786
|
-
const uploader = new ReportUploader2({
|
|
1787
|
-
flakinessAccessToken: options.accessToken,
|
|
1788
|
-
flakinessEndpoint: options.endpoint
|
|
1789
|
-
});
|
|
1790
1421
|
for (const relativePath of relativePaths) {
|
|
1791
|
-
const fullPath =
|
|
1792
|
-
if (!await
|
|
1422
|
+
const fullPath = path6.resolve(relativePath);
|
|
1423
|
+
if (!await fs6.access(fullPath, fs6.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1793
1424
|
err(`Path ${fullPath} is not accessible!`);
|
|
1794
1425
|
process.exit(1);
|
|
1795
1426
|
}
|
|
1796
|
-
const text = await
|
|
1427
|
+
const text = await fs6.readFile(fullPath, "utf-8");
|
|
1797
1428
|
const report = JSON.parse(text);
|
|
1798
|
-
const attachmentsDir = options.attachmentsDir ??
|
|
1429
|
+
const attachmentsDir = options.attachmentsDir ?? path6.dirname(fullPath);
|
|
1799
1430
|
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1800
1431
|
if (missingAttachments.length) {
|
|
1801
1432
|
warn(`Missing ${missingAttachments.length} attachments`);
|
|
1802
1433
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
} else {
|
|
1808
|
-
log(`\u2713 Uploaded ${uploadResult.reportUrl ?? uploadResult.message ?? ""}`);
|
|
1809
|
-
}
|
|
1434
|
+
await uploadReport(report, Array.from(attachmentIdToPath.values()), {
|
|
1435
|
+
flakinessAccessToken: options.accessToken,
|
|
1436
|
+
flakinessEndpoint: options.endpoint
|
|
1437
|
+
});
|
|
1810
1438
|
}
|
|
1811
1439
|
}
|
|
1812
1440
|
|
|
@@ -1854,15 +1482,12 @@ async function ensureAccessToken(options) {
|
|
|
1854
1482
|
}
|
|
1855
1483
|
}
|
|
1856
1484
|
}
|
|
1857
|
-
|
|
1485
|
+
assert2(accessToken, `Please either pass FLAKINESS_ACCESS_TOKEN or run login + link`);
|
|
1858
1486
|
return {
|
|
1859
1487
|
...options,
|
|
1860
1488
|
accessToken
|
|
1861
1489
|
};
|
|
1862
1490
|
}
|
|
1863
|
-
program.command("upload-playwright-json", { hidden: true }).description("Upload Playwright Test JSON report to the flakiness.io service").argument("<relative-path-to-json>", "Path to the Playwright JSON report file").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePath, options) => runCommand(async () => {
|
|
1864
|
-
await cmdUploadPlaywrightJson(relativePath, await ensureAccessToken(options));
|
|
1865
|
-
}));
|
|
1866
1491
|
program.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
|
|
1867
1492
|
await cmdLogin(options.endpoint);
|
|
1868
1493
|
}));
|
|
@@ -1939,7 +1564,7 @@ program.command("download").description("Download run").addOption(optSince).addO
|
|
|
1939
1564
|
});
|
|
1940
1565
|
console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
|
|
1941
1566
|
}
|
|
1942
|
-
const alreadyExisting =
|
|
1567
|
+
const alreadyExisting = fs7.readdirSync(process.cwd());
|
|
1943
1568
|
const downloadedRuns = alreadyExisting.filter((entry) => entry.startsWith("fkrun-")).map((run) => parseInt(run.substring(`fkrun-`.length), 10)).filter((runId) => !isNaN(runId));
|
|
1944
1569
|
console.log(`Found ${downloadedRuns.length} locally downloaded reports`);
|
|
1945
1570
|
const toBeDownloaded = Ranges.subtract(runIds, Ranges.fromList(downloadedRuns));
|
|
@@ -1968,7 +1593,7 @@ program.command("upload").description("Upload Flakiness report to the flakiness.
|
|
|
1968
1593
|
});
|
|
1969
1594
|
});
|
|
1970
1595
|
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 () => {
|
|
1971
|
-
const dir =
|
|
1596
|
+
const dir = path7.resolve(arg ?? "flakiness-report");
|
|
1972
1597
|
await showReport(dir);
|
|
1973
1598
|
}));
|
|
1974
1599
|
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) => {
|