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