flakiness 0.158.0 → 0.160.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/README.md +0 -1
- package/lib/cli/cli.js +85 -222
- package/lib/junit.js +7 -130
- package/package.json +9 -6
- package/types/tsconfig.tsbuildinfo +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,6 @@ flakiness upload ./report.json --endpoint https://custom.flakiness.io
|
|
|
65
65
|
**Options:**
|
|
66
66
|
- `-t, --access-token <token>` — Read-write access token (env: `FLAKINESS_ACCESS_TOKEN`)
|
|
67
67
|
- `-e, --endpoint <url>` — Service endpoint (env: `FLAKINESS_ENDPOINT`)
|
|
68
|
-
- `--attachments-dir <dir>` — Directory containing attachments (defaults to report directory)
|
|
69
68
|
|
|
70
69
|
### Download Reports
|
|
71
70
|
|
package/lib/cli/cli.js
CHANGED
|
@@ -813,6 +813,11 @@ var TypedHTTP;
|
|
|
813
813
|
...TypedHTTP2.StatusCodes.ClientErrors,
|
|
814
814
|
...TypedHTTP2.StatusCodes.ServerErrors
|
|
815
815
|
};
|
|
816
|
+
function assert3(value, code, message) {
|
|
817
|
+
if (!value)
|
|
818
|
+
throw HttpError.withCode(code, message);
|
|
819
|
+
}
|
|
820
|
+
TypedHTTP2.assert = assert3;
|
|
816
821
|
class HttpError extends Error {
|
|
817
822
|
constructor(status, message) {
|
|
818
823
|
super(message);
|
|
@@ -901,8 +906,8 @@ var TypedHTTP;
|
|
|
901
906
|
}
|
|
902
907
|
TypedHTTP2.Router = Router;
|
|
903
908
|
function createClient(base, fetchCallback) {
|
|
904
|
-
function buildUrl(method,
|
|
905
|
-
const url = new URL(
|
|
909
|
+
function buildUrl(method, path7, input, options) {
|
|
910
|
+
const url = new URL(path7.join("/"), base);
|
|
906
911
|
const signal = options?.signal;
|
|
907
912
|
let body = void 0;
|
|
908
913
|
if (method === "GET" && input)
|
|
@@ -917,8 +922,8 @@ var TypedHTTP;
|
|
|
917
922
|
signal
|
|
918
923
|
};
|
|
919
924
|
}
|
|
920
|
-
function fetcher(method,
|
|
921
|
-
const options = buildUrl(method,
|
|
925
|
+
function fetcher(method, path7, input, methodOptions) {
|
|
926
|
+
const options = buildUrl(method, path7, input, methodOptions);
|
|
922
927
|
return fetchCallback(options.url, {
|
|
923
928
|
method: options.method,
|
|
924
929
|
body: options.body,
|
|
@@ -941,17 +946,17 @@ var TypedHTTP;
|
|
|
941
946
|
}
|
|
942
947
|
});
|
|
943
948
|
}
|
|
944
|
-
function createProxy(
|
|
949
|
+
function createProxy(path7 = []) {
|
|
945
950
|
return new Proxy({}, {
|
|
946
951
|
get(target, prop) {
|
|
947
952
|
if (typeof prop === "symbol")
|
|
948
953
|
return void 0;
|
|
949
954
|
if (allHttpMethods.includes(prop)) {
|
|
950
|
-
const f = fetcher.bind(null, prop,
|
|
951
|
-
f.prepare = buildUrl.bind(null, prop,
|
|
955
|
+
const f = fetcher.bind(null, prop, path7);
|
|
956
|
+
f.prepare = buildUrl.bind(null, prop, path7);
|
|
952
957
|
return f;
|
|
953
958
|
}
|
|
954
|
-
const newPath = [...
|
|
959
|
+
const newPath = [...path7, prop];
|
|
955
960
|
return createProxy(newPath);
|
|
956
961
|
}
|
|
957
962
|
});
|
|
@@ -964,14 +969,15 @@ var TypedHTTP;
|
|
|
964
969
|
// src/cli/cli.ts
|
|
965
970
|
import assert2 from "assert";
|
|
966
971
|
import { Command, Option } from "commander";
|
|
967
|
-
import
|
|
972
|
+
import debug2 from "debug";
|
|
973
|
+
import fs6 from "fs";
|
|
968
974
|
import ora from "ora";
|
|
969
|
-
import
|
|
975
|
+
import path6 from "path";
|
|
970
976
|
|
|
971
977
|
// ../package.json
|
|
972
978
|
var package_default = {
|
|
973
979
|
name: "flakiness",
|
|
974
|
-
version: "0.
|
|
980
|
+
version: "0.160.0",
|
|
975
981
|
type: "module",
|
|
976
982
|
private: true,
|
|
977
983
|
scripts: {
|
|
@@ -990,10 +996,10 @@ var package_default = {
|
|
|
990
996
|
license: "Fair Source 100",
|
|
991
997
|
devDependencies: {
|
|
992
998
|
"@flakiness/playwright": "^0.154.0",
|
|
993
|
-
"@playwright/test": "
|
|
999
|
+
"@playwright/test": "catalog:",
|
|
994
1000
|
"@types/node": "^22.19.3",
|
|
995
1001
|
esbuild: "^0.27.2",
|
|
996
|
-
glob: "
|
|
1002
|
+
glob: "catalog:",
|
|
997
1003
|
kubik: "^0.24.0",
|
|
998
1004
|
tsx: "^4.21.0",
|
|
999
1005
|
typescript: "^5.9.3"
|
|
@@ -1001,157 +1007,13 @@ var package_default = {
|
|
|
1001
1007
|
};
|
|
1002
1008
|
|
|
1003
1009
|
// src/flakinessSession.ts
|
|
1004
|
-
import
|
|
1010
|
+
import fs from "fs/promises";
|
|
1005
1011
|
import os from "os";
|
|
1006
|
-
import path2 from "path";
|
|
1007
|
-
|
|
1008
|
-
// src/utils.ts
|
|
1009
|
-
import { ReportUtils } from "@flakiness/sdk";
|
|
1010
|
-
import crypto from "crypto";
|
|
1011
|
-
import fs from "fs";
|
|
1012
|
-
import http from "http";
|
|
1013
|
-
import https from "https";
|
|
1014
1012
|
import path from "path";
|
|
1015
|
-
function sha1File(filePath) {
|
|
1016
|
-
return new Promise((resolve, reject) => {
|
|
1017
|
-
const hash = crypto.createHash("sha1");
|
|
1018
|
-
const stream = fs.createReadStream(filePath);
|
|
1019
|
-
stream.on("data", (chunk) => {
|
|
1020
|
-
hash.update(chunk);
|
|
1021
|
-
});
|
|
1022
|
-
stream.on("end", () => {
|
|
1023
|
-
resolve(hash.digest("hex"));
|
|
1024
|
-
});
|
|
1025
|
-
stream.on("error", (err2) => {
|
|
1026
|
-
reject(err2);
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
1031
|
-
function errorText(error) {
|
|
1032
|
-
return FLAKINESS_DBG ? error.stack : error.message;
|
|
1033
|
-
}
|
|
1034
|
-
function sha1Buffer(data) {
|
|
1035
|
-
const hash = crypto.createHash("sha1");
|
|
1036
|
-
hash.update(data);
|
|
1037
|
-
return hash.digest("hex");
|
|
1038
|
-
}
|
|
1039
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
1040
|
-
for (const timeout of backoff) {
|
|
1041
|
-
try {
|
|
1042
|
-
return await job();
|
|
1043
|
-
} catch (e) {
|
|
1044
|
-
if (e instanceof AggregateError)
|
|
1045
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
1046
|
-
else if (e instanceof Error)
|
|
1047
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
1048
|
-
else
|
|
1049
|
-
console.error(`[flakiness.io err]`, e);
|
|
1050
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
return await job();
|
|
1054
|
-
}
|
|
1055
|
-
var httpUtils;
|
|
1056
|
-
((httpUtils2) => {
|
|
1057
|
-
function createRequest({ url, method = "get", headers = {} }) {
|
|
1058
|
-
let resolve;
|
|
1059
|
-
let reject;
|
|
1060
|
-
const responseDataPromise = new Promise((a, b) => {
|
|
1061
|
-
resolve = a;
|
|
1062
|
-
reject = b;
|
|
1063
|
-
});
|
|
1064
|
-
const protocol = url.startsWith("https") ? https : http;
|
|
1065
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
1066
|
-
const request = protocol.request(url, { method, headers }, (res) => {
|
|
1067
|
-
const chunks = [];
|
|
1068
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
1069
|
-
res.on("end", () => {
|
|
1070
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
1071
|
-
resolve(Buffer.concat(chunks));
|
|
1072
|
-
else
|
|
1073
|
-
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
1074
|
-
});
|
|
1075
|
-
res.on("error", (error) => reject(error));
|
|
1076
|
-
});
|
|
1077
|
-
request.on("error", reject);
|
|
1078
|
-
return { request, responseDataPromise };
|
|
1079
|
-
}
|
|
1080
|
-
httpUtils2.createRequest = createRequest;
|
|
1081
|
-
async function getBuffer(url, backoff) {
|
|
1082
|
-
return await retryWithBackoff(async () => {
|
|
1083
|
-
const { request, responseDataPromise } = createRequest({ url });
|
|
1084
|
-
request.end();
|
|
1085
|
-
return await responseDataPromise;
|
|
1086
|
-
}, backoff);
|
|
1087
|
-
}
|
|
1088
|
-
httpUtils2.getBuffer = getBuffer;
|
|
1089
|
-
async function getText(url, backoff) {
|
|
1090
|
-
const buffer = await getBuffer(url, backoff);
|
|
1091
|
-
return buffer.toString("utf-8");
|
|
1092
|
-
}
|
|
1093
|
-
httpUtils2.getText = getText;
|
|
1094
|
-
async function getJSON(url) {
|
|
1095
|
-
return JSON.parse(await getText(url));
|
|
1096
|
-
}
|
|
1097
|
-
httpUtils2.getJSON = getJSON;
|
|
1098
|
-
async function postText(url, text, backoff) {
|
|
1099
|
-
const headers = {
|
|
1100
|
-
"Content-Type": "application/json",
|
|
1101
|
-
"Content-Length": Buffer.byteLength(text) + ""
|
|
1102
|
-
};
|
|
1103
|
-
return await retryWithBackoff(async () => {
|
|
1104
|
-
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
1105
|
-
request.write(text);
|
|
1106
|
-
request.end();
|
|
1107
|
-
return await responseDataPromise;
|
|
1108
|
-
}, backoff);
|
|
1109
|
-
}
|
|
1110
|
-
httpUtils2.postText = postText;
|
|
1111
|
-
async function postJSON(url, json, backoff) {
|
|
1112
|
-
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
1113
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
1114
|
-
}
|
|
1115
|
-
httpUtils2.postJSON = postJSON;
|
|
1116
|
-
})(httpUtils || (httpUtils = {}));
|
|
1117
|
-
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");
|
|
1118
|
-
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
1119
|
-
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
1120
|
-
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
1121
|
-
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
1122
|
-
const missingAttachments = /* @__PURE__ */ new Set();
|
|
1123
|
-
ReportUtils.visitTests(report, (test) => {
|
|
1124
|
-
for (const attempt of test.attempts) {
|
|
1125
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
1126
|
-
const attachmentPath = filenameToPath.get(attachment.id);
|
|
1127
|
-
if (!attachmentPath) {
|
|
1128
|
-
missingAttachments.add(attachment.id);
|
|
1129
|
-
} else {
|
|
1130
|
-
attachmentIdToPath.set(attachment.id, {
|
|
1131
|
-
contentType: attachment.contentType,
|
|
1132
|
-
id: attachment.id,
|
|
1133
|
-
path: attachmentPath,
|
|
1134
|
-
type: "file"
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1140
|
-
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
1141
|
-
}
|
|
1142
|
-
async function listFilesRecursively(dir, result = []) {
|
|
1143
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1144
|
-
for (const entry of entries) {
|
|
1145
|
-
const fullPath = path.join(dir, entry.name);
|
|
1146
|
-
if (entry.isDirectory())
|
|
1147
|
-
await listFilesRecursively(fullPath, result);
|
|
1148
|
-
else
|
|
1149
|
-
result.push(fullPath);
|
|
1150
|
-
}
|
|
1151
|
-
return result;
|
|
1152
|
-
}
|
|
1153
1013
|
|
|
1154
1014
|
// src/serverapi.ts
|
|
1015
|
+
import debug from "debug";
|
|
1016
|
+
var log = debug("fk:server_api");
|
|
1155
1017
|
function createServerAPI(endpoint, options) {
|
|
1156
1018
|
endpoint += "/api/";
|
|
1157
1019
|
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
@@ -1165,13 +1027,29 @@ function createServerAPI(endpoint, options) {
|
|
|
1165
1027
|
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
1166
1028
|
return TypedHTTP.createClient(endpoint, fetcher);
|
|
1167
1029
|
}
|
|
1030
|
+
async function retryWithBackoff(job, backoff = []) {
|
|
1031
|
+
for (const timeout of backoff) {
|
|
1032
|
+
try {
|
|
1033
|
+
return await job();
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
if (e instanceof AggregateError)
|
|
1036
|
+
console.error(`[flakiness.io err]`, log(e.errors[0]));
|
|
1037
|
+
else if (e instanceof Error)
|
|
1038
|
+
console.error(`[flakiness.io err]`, log(e));
|
|
1039
|
+
else
|
|
1040
|
+
console.error(`[flakiness.io err]`, e);
|
|
1041
|
+
await new Promise((x) => setTimeout(x, timeout));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return await job();
|
|
1045
|
+
}
|
|
1168
1046
|
|
|
1169
1047
|
// src/flakinessSession.ts
|
|
1170
1048
|
var CONFIG_DIR = (() => {
|
|
1171
|
-
const configDir = process.platform === "darwin" ?
|
|
1049
|
+
const configDir = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "flakiness") : path.join(os.homedir(), ".config", "flakiness");
|
|
1172
1050
|
return configDir;
|
|
1173
1051
|
})();
|
|
1174
|
-
var CONFIG_PATH =
|
|
1052
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
1175
1053
|
var FlakinessSession = class _FlakinessSession {
|
|
1176
1054
|
constructor(_config) {
|
|
1177
1055
|
this._config = _config;
|
|
@@ -1184,14 +1062,14 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
1184
1062
|
return session2;
|
|
1185
1063
|
}
|
|
1186
1064
|
static async load() {
|
|
1187
|
-
const data = await
|
|
1065
|
+
const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
1188
1066
|
if (!data)
|
|
1189
1067
|
return void 0;
|
|
1190
1068
|
const json = JSON.parse(data);
|
|
1191
1069
|
return new _FlakinessSession(json);
|
|
1192
1070
|
}
|
|
1193
1071
|
static async remove() {
|
|
1194
|
-
await
|
|
1072
|
+
await fs.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
1195
1073
|
}
|
|
1196
1074
|
api;
|
|
1197
1075
|
endpoint() {
|
|
@@ -1204,22 +1082,23 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
1204
1082
|
return this._config.token;
|
|
1205
1083
|
}
|
|
1206
1084
|
async save() {
|
|
1207
|
-
await
|
|
1208
|
-
await
|
|
1085
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
1086
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
1209
1087
|
}
|
|
1210
1088
|
};
|
|
1211
1089
|
|
|
1212
1090
|
// src/cli/cmd-convert.ts
|
|
1213
1091
|
import { GitWorktree, writeReport } from "@flakiness/sdk";
|
|
1214
|
-
import
|
|
1215
|
-
import
|
|
1092
|
+
import fs3 from "fs/promises";
|
|
1093
|
+
import path3 from "path";
|
|
1216
1094
|
|
|
1217
1095
|
// src/junit.ts
|
|
1218
|
-
import { ReportUtils
|
|
1096
|
+
import { ReportUtils } from "@flakiness/sdk";
|
|
1219
1097
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
1220
1098
|
import assert from "assert";
|
|
1221
|
-
import
|
|
1222
|
-
import
|
|
1099
|
+
import fs2 from "fs";
|
|
1100
|
+
import mime from "mime";
|
|
1101
|
+
import path2 from "path";
|
|
1223
1102
|
function getProperties(element) {
|
|
1224
1103
|
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
1225
1104
|
if (!propertiesNodes.length)
|
|
@@ -1261,22 +1140,10 @@ function extractStdout(testcase, stdio) {
|
|
|
1261
1140
|
}));
|
|
1262
1141
|
}
|
|
1263
1142
|
async function parseAttachment(value) {
|
|
1264
|
-
let absolutePath =
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
contentType: "image/png",
|
|
1269
|
-
path: absolutePath,
|
|
1270
|
-
id,
|
|
1271
|
-
type: "file"
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
return {
|
|
1275
|
-
contentType: "text/plain",
|
|
1276
|
-
id: sha1Buffer(value),
|
|
1277
|
-
body: Buffer.from(value),
|
|
1278
|
-
type: "buffer"
|
|
1279
|
-
};
|
|
1143
|
+
let absolutePath = path2.resolve(process.cwd(), value);
|
|
1144
|
+
if (fs2.existsSync(absolutePath))
|
|
1145
|
+
return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
|
|
1146
|
+
return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
|
|
1280
1147
|
}
|
|
1281
1148
|
async function traverseJUnitReport(context, node) {
|
|
1282
1149
|
const element = node;
|
|
@@ -1337,7 +1204,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
1337
1204
|
id: attachment.id,
|
|
1338
1205
|
contentType: attachment.contentType,
|
|
1339
1206
|
//TODO: better default names for attachments?
|
|
1340
|
-
name: attachment.type === "file" ?
|
|
1207
|
+
name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
|
|
1341
1208
|
});
|
|
1342
1209
|
} else {
|
|
1343
1210
|
annotations.push({
|
|
@@ -1405,22 +1272,22 @@ async function parseJUnit(xmls, options) {
|
|
|
1405
1272
|
await traverseJUnitReport(context, element);
|
|
1406
1273
|
}
|
|
1407
1274
|
return {
|
|
1408
|
-
report:
|
|
1275
|
+
report: ReportUtils.normalizeReport(report),
|
|
1409
1276
|
attachments: Array.from(context.attachments.values())
|
|
1410
1277
|
};
|
|
1411
1278
|
}
|
|
1412
1279
|
|
|
1413
1280
|
// src/cli/cmd-convert.ts
|
|
1414
1281
|
async function cmdConvert(junitPath, options) {
|
|
1415
|
-
const fullPath =
|
|
1416
|
-
if (!await
|
|
1282
|
+
const fullPath = path3.resolve(junitPath);
|
|
1283
|
+
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1417
1284
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1418
1285
|
process.exit(1);
|
|
1419
1286
|
}
|
|
1420
|
-
const stat = await
|
|
1287
|
+
const stat = await fs3.stat(fullPath);
|
|
1421
1288
|
let xmlContents = [];
|
|
1422
1289
|
if (stat.isFile()) {
|
|
1423
|
-
const xmlContent = await
|
|
1290
|
+
const xmlContent = await fs3.readFile(fullPath, "utf-8");
|
|
1424
1291
|
xmlContents.push(xmlContent);
|
|
1425
1292
|
} else if (stat.isDirectory()) {
|
|
1426
1293
|
const xmlFiles = await findXmlFiles(fullPath);
|
|
@@ -1430,7 +1297,7 @@ async function cmdConvert(junitPath, options) {
|
|
|
1430
1297
|
}
|
|
1431
1298
|
console.log(`Found ${xmlFiles.length} XML files`);
|
|
1432
1299
|
for (const xmlFile of xmlFiles) {
|
|
1433
|
-
const xmlContent = await
|
|
1300
|
+
const xmlContent = await fs3.readFile(xmlFile, "utf-8");
|
|
1434
1301
|
xmlContents.push(xmlContent);
|
|
1435
1302
|
}
|
|
1436
1303
|
} else {
|
|
@@ -1459,9 +1326,9 @@ async function cmdConvert(junitPath, options) {
|
|
|
1459
1326
|
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
1460
1327
|
}
|
|
1461
1328
|
async function findXmlFiles(dir, result = []) {
|
|
1462
|
-
const entries = await
|
|
1329
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1463
1330
|
for (const entry of entries) {
|
|
1464
|
-
const fullPath =
|
|
1331
|
+
const fullPath = path3.join(dir, entry.name);
|
|
1465
1332
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
1466
1333
|
result.push(fullPath);
|
|
1467
1334
|
else if (entry.isDirectory())
|
|
@@ -1471,10 +1338,10 @@ async function findXmlFiles(dir, result = []) {
|
|
|
1471
1338
|
}
|
|
1472
1339
|
|
|
1473
1340
|
// src/cli/cmd-download.ts
|
|
1474
|
-
import
|
|
1475
|
-
import
|
|
1341
|
+
import fs4 from "fs";
|
|
1342
|
+
import path4 from "path";
|
|
1476
1343
|
async function cmdDownload(session2, project, runId, rootDir) {
|
|
1477
|
-
if (
|
|
1344
|
+
if (fs4.existsSync(rootDir)) {
|
|
1478
1345
|
console.log(`Directory ${rootDir} already exists!`);
|
|
1479
1346
|
return;
|
|
1480
1347
|
}
|
|
@@ -1483,15 +1350,15 @@ async function cmdDownload(session2, project, runId, rootDir) {
|
|
|
1483
1350
|
projectSlug: project.projectSlug,
|
|
1484
1351
|
runId
|
|
1485
1352
|
});
|
|
1486
|
-
const attachmentsDir =
|
|
1487
|
-
await
|
|
1353
|
+
const attachmentsDir = path4.join(rootDir, "attachments");
|
|
1354
|
+
await fs4.promises.mkdir(rootDir, { recursive: true });
|
|
1488
1355
|
if (urls.attachmentURLs.length)
|
|
1489
|
-
await
|
|
1356
|
+
await fs4.promises.mkdir(attachmentsDir, { recursive: true });
|
|
1490
1357
|
const response = await fetch(urls.reportURL);
|
|
1491
1358
|
if (!response.ok)
|
|
1492
1359
|
throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
|
|
1493
1360
|
const reportContent = await response.text();
|
|
1494
|
-
await
|
|
1361
|
+
await fs4.promises.writeFile(path4.join(rootDir, "report.json"), reportContent);
|
|
1495
1362
|
const attachmentDownloader = async () => {
|
|
1496
1363
|
while (urls.attachmentURLs.length) {
|
|
1497
1364
|
const url = urls.attachmentURLs.pop();
|
|
@@ -1499,8 +1366,8 @@ async function cmdDownload(session2, project, runId, rootDir) {
|
|
|
1499
1366
|
if (!response2.ok)
|
|
1500
1367
|
throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
|
|
1501
1368
|
const fileBuffer = Buffer.from(await response2.arrayBuffer());
|
|
1502
|
-
const filename =
|
|
1503
|
-
await
|
|
1369
|
+
const filename = path4.basename(new URL(url).pathname);
|
|
1370
|
+
await fs4.promises.writeFile(path4.join(attachmentsDir, filename), fileBuffer);
|
|
1504
1371
|
}
|
|
1505
1372
|
};
|
|
1506
1373
|
const workerPromises = [];
|
|
@@ -1621,27 +1488,23 @@ async function cmdUnlink() {
|
|
|
1621
1488
|
}
|
|
1622
1489
|
|
|
1623
1490
|
// src/cli/cmd-upload.ts
|
|
1624
|
-
import { uploadReport } from "@flakiness/sdk";
|
|
1491
|
+
import { readReport, uploadReport } from "@flakiness/sdk";
|
|
1625
1492
|
import chalk from "chalk";
|
|
1626
|
-
import
|
|
1627
|
-
import
|
|
1493
|
+
import fs5 from "fs/promises";
|
|
1494
|
+
import path5 from "path";
|
|
1628
1495
|
var warn = (txt) => console.warn(chalk.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
1629
1496
|
var err = (txt) => console.error(chalk.red(`[flakiness.io] Error: ${txt}`));
|
|
1630
1497
|
async function cmdUpload(relativePaths, options) {
|
|
1631
1498
|
for (const relativePath of relativePaths) {
|
|
1632
|
-
const fullPath =
|
|
1633
|
-
if (!await
|
|
1499
|
+
const fullPath = path5.resolve(relativePath);
|
|
1500
|
+
if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1634
1501
|
err(`Path ${fullPath} is not accessible!`);
|
|
1635
1502
|
process.exit(1);
|
|
1636
1503
|
}
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
const attachmentsDir = options.attachmentsDir ?? path6.dirname(fullPath);
|
|
1640
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1641
|
-
if (missingAttachments.length) {
|
|
1504
|
+
const { report, attachments, missingAttachments } = await readReport(path5.dirname(fullPath));
|
|
1505
|
+
if (missingAttachments.length)
|
|
1642
1506
|
warn(`Missing ${missingAttachments.length} attachments`);
|
|
1643
|
-
|
|
1644
|
-
await uploadReport(report, Array.from(attachmentIdToPath.values()), {
|
|
1507
|
+
await uploadReport(report, attachments, {
|
|
1645
1508
|
flakinessAccessToken: options.accessToken,
|
|
1646
1509
|
flakinessEndpoint: options.endpoint
|
|
1647
1510
|
});
|
|
@@ -1661,17 +1524,17 @@ async function cmdWhoami() {
|
|
|
1661
1524
|
}
|
|
1662
1525
|
|
|
1663
1526
|
// src/cli/cli.ts
|
|
1527
|
+
var log2 = debug2("fk:cli");
|
|
1664
1528
|
var session = await FlakinessSession.load();
|
|
1665
1529
|
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
1666
1530
|
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
|
|
1667
|
-
var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
|
|
1668
1531
|
async function runCommand(callback) {
|
|
1669
1532
|
try {
|
|
1670
1533
|
await callback();
|
|
1671
1534
|
} catch (e) {
|
|
1672
1535
|
if (!(e instanceof Error))
|
|
1673
1536
|
throw e;
|
|
1674
|
-
|
|
1537
|
+
log2(e);
|
|
1675
1538
|
process.exit(1);
|
|
1676
1539
|
}
|
|
1677
1540
|
}
|
|
@@ -1775,7 +1638,7 @@ program.command("download").description("Download run").addOption(optSince).addO
|
|
|
1775
1638
|
});
|
|
1776
1639
|
console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
|
|
1777
1640
|
}
|
|
1778
|
-
const alreadyExisting =
|
|
1641
|
+
const alreadyExisting = fs6.readdirSync(process.cwd());
|
|
1779
1642
|
const downloadedRuns = alreadyExisting.filter((entry) => entry.startsWith("fkrun-")).map((run) => parseInt(run.substring(`fkrun-`.length), 10)).filter((runId) => !isNaN(runId));
|
|
1780
1643
|
console.log(`Found ${downloadedRuns.length} locally downloaded reports`);
|
|
1781
1644
|
const toBeDownloaded = Ranges.subtract(runIds, Ranges.fromList(downloadedRuns));
|
|
@@ -1798,13 +1661,13 @@ program.command("download").description("Download run").addOption(optSince).addO
|
|
|
1798
1661
|
await Promise.all(downloaders);
|
|
1799
1662
|
spinner.stop();
|
|
1800
1663
|
}));
|
|
1801
|
-
program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-paths...>", "Paths to the Flakiness report files").addOption(optAccessToken).addOption(optEndpoint).
|
|
1664
|
+
program.command("upload").description("Upload Flakiness report to the flakiness.io service").argument("<relative-paths...>", "Paths to the Flakiness report files").addOption(optAccessToken).addOption(optEndpoint).action(async (relativePaths, options) => {
|
|
1802
1665
|
await runCommand(async () => {
|
|
1803
1666
|
await cmdUpload(relativePaths, await ensureAccessToken(options));
|
|
1804
1667
|
});
|
|
1805
1668
|
});
|
|
1806
1669
|
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 () => {
|
|
1807
|
-
const dir =
|
|
1670
|
+
const dir = path6.resolve(arg ?? "flakiness-report");
|
|
1808
1671
|
await showReport(dir);
|
|
1809
1672
|
}));
|
|
1810
1673
|
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) => {
|