flakiness 0.159.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 +78 -220
- package/lib/junit.js +7 -130
- package/package.json +7 -4
- 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
|
@@ -906,8 +906,8 @@ var TypedHTTP;
|
|
|
906
906
|
}
|
|
907
907
|
TypedHTTP2.Router = Router;
|
|
908
908
|
function createClient(base, fetchCallback) {
|
|
909
|
-
function buildUrl(method,
|
|
910
|
-
const url = new URL(
|
|
909
|
+
function buildUrl(method, path7, input, options) {
|
|
910
|
+
const url = new URL(path7.join("/"), base);
|
|
911
911
|
const signal = options?.signal;
|
|
912
912
|
let body = void 0;
|
|
913
913
|
if (method === "GET" && input)
|
|
@@ -922,8 +922,8 @@ var TypedHTTP;
|
|
|
922
922
|
signal
|
|
923
923
|
};
|
|
924
924
|
}
|
|
925
|
-
function fetcher(method,
|
|
926
|
-
const options = buildUrl(method,
|
|
925
|
+
function fetcher(method, path7, input, methodOptions) {
|
|
926
|
+
const options = buildUrl(method, path7, input, methodOptions);
|
|
927
927
|
return fetchCallback(options.url, {
|
|
928
928
|
method: options.method,
|
|
929
929
|
body: options.body,
|
|
@@ -946,17 +946,17 @@ var TypedHTTP;
|
|
|
946
946
|
}
|
|
947
947
|
});
|
|
948
948
|
}
|
|
949
|
-
function createProxy(
|
|
949
|
+
function createProxy(path7 = []) {
|
|
950
950
|
return new Proxy({}, {
|
|
951
951
|
get(target, prop) {
|
|
952
952
|
if (typeof prop === "symbol")
|
|
953
953
|
return void 0;
|
|
954
954
|
if (allHttpMethods.includes(prop)) {
|
|
955
|
-
const f = fetcher.bind(null, prop,
|
|
956
|
-
f.prepare = buildUrl.bind(null, prop,
|
|
955
|
+
const f = fetcher.bind(null, prop, path7);
|
|
956
|
+
f.prepare = buildUrl.bind(null, prop, path7);
|
|
957
957
|
return f;
|
|
958
958
|
}
|
|
959
|
-
const newPath = [...
|
|
959
|
+
const newPath = [...path7, prop];
|
|
960
960
|
return createProxy(newPath);
|
|
961
961
|
}
|
|
962
962
|
});
|
|
@@ -969,14 +969,15 @@ var TypedHTTP;
|
|
|
969
969
|
// src/cli/cli.ts
|
|
970
970
|
import assert2 from "assert";
|
|
971
971
|
import { Command, Option } from "commander";
|
|
972
|
-
import
|
|
972
|
+
import debug2 from "debug";
|
|
973
|
+
import fs6 from "fs";
|
|
973
974
|
import ora from "ora";
|
|
974
|
-
import
|
|
975
|
+
import path6 from "path";
|
|
975
976
|
|
|
976
977
|
// ../package.json
|
|
977
978
|
var package_default = {
|
|
978
979
|
name: "flakiness",
|
|
979
|
-
version: "0.
|
|
980
|
+
version: "0.160.0",
|
|
980
981
|
type: "module",
|
|
981
982
|
private: true,
|
|
982
983
|
scripts: {
|
|
@@ -1006,157 +1007,13 @@ var package_default = {
|
|
|
1006
1007
|
};
|
|
1007
1008
|
|
|
1008
1009
|
// src/flakinessSession.ts
|
|
1009
|
-
import
|
|
1010
|
+
import fs from "fs/promises";
|
|
1010
1011
|
import os from "os";
|
|
1011
|
-
import path2 from "path";
|
|
1012
|
-
|
|
1013
|
-
// src/utils.ts
|
|
1014
|
-
import { ReportUtils } from "@flakiness/sdk";
|
|
1015
|
-
import crypto from "crypto";
|
|
1016
|
-
import fs from "fs";
|
|
1017
|
-
import http from "http";
|
|
1018
|
-
import https from "https";
|
|
1019
1012
|
import path from "path";
|
|
1020
|
-
function sha1File(filePath) {
|
|
1021
|
-
return new Promise((resolve, reject) => {
|
|
1022
|
-
const hash = crypto.createHash("sha1");
|
|
1023
|
-
const stream = fs.createReadStream(filePath);
|
|
1024
|
-
stream.on("data", (chunk) => {
|
|
1025
|
-
hash.update(chunk);
|
|
1026
|
-
});
|
|
1027
|
-
stream.on("end", () => {
|
|
1028
|
-
resolve(hash.digest("hex"));
|
|
1029
|
-
});
|
|
1030
|
-
stream.on("error", (err2) => {
|
|
1031
|
-
reject(err2);
|
|
1032
|
-
});
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
1036
|
-
function errorText(error) {
|
|
1037
|
-
return FLAKINESS_DBG ? error.stack : error.message;
|
|
1038
|
-
}
|
|
1039
|
-
function sha1Buffer(data) {
|
|
1040
|
-
const hash = crypto.createHash("sha1");
|
|
1041
|
-
hash.update(data);
|
|
1042
|
-
return hash.digest("hex");
|
|
1043
|
-
}
|
|
1044
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
1045
|
-
for (const timeout of backoff) {
|
|
1046
|
-
try {
|
|
1047
|
-
return await job();
|
|
1048
|
-
} catch (e) {
|
|
1049
|
-
if (e instanceof AggregateError)
|
|
1050
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
1051
|
-
else if (e instanceof Error)
|
|
1052
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
1053
|
-
else
|
|
1054
|
-
console.error(`[flakiness.io err]`, e);
|
|
1055
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
return await job();
|
|
1059
|
-
}
|
|
1060
|
-
var httpUtils;
|
|
1061
|
-
((httpUtils2) => {
|
|
1062
|
-
function createRequest({ url, method = "get", headers = {} }) {
|
|
1063
|
-
let resolve;
|
|
1064
|
-
let reject;
|
|
1065
|
-
const responseDataPromise = new Promise((a, b) => {
|
|
1066
|
-
resolve = a;
|
|
1067
|
-
reject = b;
|
|
1068
|
-
});
|
|
1069
|
-
const protocol = url.startsWith("https") ? https : http;
|
|
1070
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
1071
|
-
const request = protocol.request(url, { method, headers }, (res) => {
|
|
1072
|
-
const chunks = [];
|
|
1073
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
1074
|
-
res.on("end", () => {
|
|
1075
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
1076
|
-
resolve(Buffer.concat(chunks));
|
|
1077
|
-
else
|
|
1078
|
-
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
1079
|
-
});
|
|
1080
|
-
res.on("error", (error) => reject(error));
|
|
1081
|
-
});
|
|
1082
|
-
request.on("error", reject);
|
|
1083
|
-
return { request, responseDataPromise };
|
|
1084
|
-
}
|
|
1085
|
-
httpUtils2.createRequest = createRequest;
|
|
1086
|
-
async function getBuffer(url, backoff) {
|
|
1087
|
-
return await retryWithBackoff(async () => {
|
|
1088
|
-
const { request, responseDataPromise } = createRequest({ url });
|
|
1089
|
-
request.end();
|
|
1090
|
-
return await responseDataPromise;
|
|
1091
|
-
}, backoff);
|
|
1092
|
-
}
|
|
1093
|
-
httpUtils2.getBuffer = getBuffer;
|
|
1094
|
-
async function getText(url, backoff) {
|
|
1095
|
-
const buffer = await getBuffer(url, backoff);
|
|
1096
|
-
return buffer.toString("utf-8");
|
|
1097
|
-
}
|
|
1098
|
-
httpUtils2.getText = getText;
|
|
1099
|
-
async function getJSON(url) {
|
|
1100
|
-
return JSON.parse(await getText(url));
|
|
1101
|
-
}
|
|
1102
|
-
httpUtils2.getJSON = getJSON;
|
|
1103
|
-
async function postText(url, text, backoff) {
|
|
1104
|
-
const headers = {
|
|
1105
|
-
"Content-Type": "application/json",
|
|
1106
|
-
"Content-Length": Buffer.byteLength(text) + ""
|
|
1107
|
-
};
|
|
1108
|
-
return await retryWithBackoff(async () => {
|
|
1109
|
-
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
1110
|
-
request.write(text);
|
|
1111
|
-
request.end();
|
|
1112
|
-
return await responseDataPromise;
|
|
1113
|
-
}, backoff);
|
|
1114
|
-
}
|
|
1115
|
-
httpUtils2.postText = postText;
|
|
1116
|
-
async function postJSON(url, json, backoff) {
|
|
1117
|
-
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
1118
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
1119
|
-
}
|
|
1120
|
-
httpUtils2.postJSON = postJSON;
|
|
1121
|
-
})(httpUtils || (httpUtils = {}));
|
|
1122
|
-
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");
|
|
1123
|
-
async function resolveAttachmentPaths(report, attachmentsDir) {
|
|
1124
|
-
const attachmentFiles = await listFilesRecursively(attachmentsDir);
|
|
1125
|
-
const filenameToPath = new Map(attachmentFiles.map((file) => [path.basename(file), file]));
|
|
1126
|
-
const attachmentIdToPath = /* @__PURE__ */ new Map();
|
|
1127
|
-
const missingAttachments = /* @__PURE__ */ new Set();
|
|
1128
|
-
ReportUtils.visitTests(report, (test) => {
|
|
1129
|
-
for (const attempt of test.attempts) {
|
|
1130
|
-
for (const attachment of attempt.attachments ?? []) {
|
|
1131
|
-
const attachmentPath = filenameToPath.get(attachment.id);
|
|
1132
|
-
if (!attachmentPath) {
|
|
1133
|
-
missingAttachments.add(attachment.id);
|
|
1134
|
-
} else {
|
|
1135
|
-
attachmentIdToPath.set(attachment.id, {
|
|
1136
|
-
contentType: attachment.contentType,
|
|
1137
|
-
id: attachment.id,
|
|
1138
|
-
path: attachmentPath,
|
|
1139
|
-
type: "file"
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
});
|
|
1145
|
-
return { attachmentIdToPath, missingAttachments: Array.from(missingAttachments) };
|
|
1146
|
-
}
|
|
1147
|
-
async function listFilesRecursively(dir, result = []) {
|
|
1148
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
1149
|
-
for (const entry of entries) {
|
|
1150
|
-
const fullPath = path.join(dir, entry.name);
|
|
1151
|
-
if (entry.isDirectory())
|
|
1152
|
-
await listFilesRecursively(fullPath, result);
|
|
1153
|
-
else
|
|
1154
|
-
result.push(fullPath);
|
|
1155
|
-
}
|
|
1156
|
-
return result;
|
|
1157
|
-
}
|
|
1158
1013
|
|
|
1159
1014
|
// src/serverapi.ts
|
|
1015
|
+
import debug from "debug";
|
|
1016
|
+
var log = debug("fk:server_api");
|
|
1160
1017
|
function createServerAPI(endpoint, options) {
|
|
1161
1018
|
endpoint += "/api/";
|
|
1162
1019
|
const fetcher = options?.auth ? (url, init) => fetch(url, {
|
|
@@ -1170,13 +1027,29 @@ function createServerAPI(endpoint, options) {
|
|
|
1170
1027
|
return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
|
|
1171
1028
|
return TypedHTTP.createClient(endpoint, fetcher);
|
|
1172
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
|
+
}
|
|
1173
1046
|
|
|
1174
1047
|
// src/flakinessSession.ts
|
|
1175
1048
|
var CONFIG_DIR = (() => {
|
|
1176
|
-
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");
|
|
1177
1050
|
return configDir;
|
|
1178
1051
|
})();
|
|
1179
|
-
var CONFIG_PATH =
|
|
1052
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
1180
1053
|
var FlakinessSession = class _FlakinessSession {
|
|
1181
1054
|
constructor(_config) {
|
|
1182
1055
|
this._config = _config;
|
|
@@ -1189,14 +1062,14 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
1189
1062
|
return session2;
|
|
1190
1063
|
}
|
|
1191
1064
|
static async load() {
|
|
1192
|
-
const data = await
|
|
1065
|
+
const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
|
|
1193
1066
|
if (!data)
|
|
1194
1067
|
return void 0;
|
|
1195
1068
|
const json = JSON.parse(data);
|
|
1196
1069
|
return new _FlakinessSession(json);
|
|
1197
1070
|
}
|
|
1198
1071
|
static async remove() {
|
|
1199
|
-
await
|
|
1072
|
+
await fs.unlink(CONFIG_PATH).catch((e) => void 0);
|
|
1200
1073
|
}
|
|
1201
1074
|
api;
|
|
1202
1075
|
endpoint() {
|
|
@@ -1209,22 +1082,23 @@ var FlakinessSession = class _FlakinessSession {
|
|
|
1209
1082
|
return this._config.token;
|
|
1210
1083
|
}
|
|
1211
1084
|
async save() {
|
|
1212
|
-
await
|
|
1213
|
-
await
|
|
1085
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
1086
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
|
|
1214
1087
|
}
|
|
1215
1088
|
};
|
|
1216
1089
|
|
|
1217
1090
|
// src/cli/cmd-convert.ts
|
|
1218
1091
|
import { GitWorktree, writeReport } from "@flakiness/sdk";
|
|
1219
|
-
import
|
|
1220
|
-
import
|
|
1092
|
+
import fs3 from "fs/promises";
|
|
1093
|
+
import path3 from "path";
|
|
1221
1094
|
|
|
1222
1095
|
// src/junit.ts
|
|
1223
|
-
import { ReportUtils
|
|
1096
|
+
import { ReportUtils } from "@flakiness/sdk";
|
|
1224
1097
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
1225
1098
|
import assert from "assert";
|
|
1226
|
-
import
|
|
1227
|
-
import
|
|
1099
|
+
import fs2 from "fs";
|
|
1100
|
+
import mime from "mime";
|
|
1101
|
+
import path2 from "path";
|
|
1228
1102
|
function getProperties(element) {
|
|
1229
1103
|
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
1230
1104
|
if (!propertiesNodes.length)
|
|
@@ -1266,22 +1140,10 @@ function extractStdout(testcase, stdio) {
|
|
|
1266
1140
|
}));
|
|
1267
1141
|
}
|
|
1268
1142
|
async function parseAttachment(value) {
|
|
1269
|
-
let absolutePath =
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
contentType: "image/png",
|
|
1274
|
-
path: absolutePath,
|
|
1275
|
-
id,
|
|
1276
|
-
type: "file"
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
return {
|
|
1280
|
-
contentType: "text/plain",
|
|
1281
|
-
id: sha1Buffer(value),
|
|
1282
|
-
body: Buffer.from(value),
|
|
1283
|
-
type: "buffer"
|
|
1284
|
-
};
|
|
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));
|
|
1285
1147
|
}
|
|
1286
1148
|
async function traverseJUnitReport(context, node) {
|
|
1287
1149
|
const element = node;
|
|
@@ -1342,7 +1204,7 @@ async function traverseJUnitReport(context, node) {
|
|
|
1342
1204
|
id: attachment.id,
|
|
1343
1205
|
contentType: attachment.contentType,
|
|
1344
1206
|
//TODO: better default names for attachments?
|
|
1345
|
-
name: attachment.type === "file" ?
|
|
1207
|
+
name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
|
|
1346
1208
|
});
|
|
1347
1209
|
} else {
|
|
1348
1210
|
annotations.push({
|
|
@@ -1410,22 +1272,22 @@ async function parseJUnit(xmls, options) {
|
|
|
1410
1272
|
await traverseJUnitReport(context, element);
|
|
1411
1273
|
}
|
|
1412
1274
|
return {
|
|
1413
|
-
report:
|
|
1275
|
+
report: ReportUtils.normalizeReport(report),
|
|
1414
1276
|
attachments: Array.from(context.attachments.values())
|
|
1415
1277
|
};
|
|
1416
1278
|
}
|
|
1417
1279
|
|
|
1418
1280
|
// src/cli/cmd-convert.ts
|
|
1419
1281
|
async function cmdConvert(junitPath, options) {
|
|
1420
|
-
const fullPath =
|
|
1421
|
-
if (!await
|
|
1282
|
+
const fullPath = path3.resolve(junitPath);
|
|
1283
|
+
if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1422
1284
|
console.error(`Error: path ${fullPath} is not accessible`);
|
|
1423
1285
|
process.exit(1);
|
|
1424
1286
|
}
|
|
1425
|
-
const stat = await
|
|
1287
|
+
const stat = await fs3.stat(fullPath);
|
|
1426
1288
|
let xmlContents = [];
|
|
1427
1289
|
if (stat.isFile()) {
|
|
1428
|
-
const xmlContent = await
|
|
1290
|
+
const xmlContent = await fs3.readFile(fullPath, "utf-8");
|
|
1429
1291
|
xmlContents.push(xmlContent);
|
|
1430
1292
|
} else if (stat.isDirectory()) {
|
|
1431
1293
|
const xmlFiles = await findXmlFiles(fullPath);
|
|
@@ -1435,7 +1297,7 @@ async function cmdConvert(junitPath, options) {
|
|
|
1435
1297
|
}
|
|
1436
1298
|
console.log(`Found ${xmlFiles.length} XML files`);
|
|
1437
1299
|
for (const xmlFile of xmlFiles) {
|
|
1438
|
-
const xmlContent = await
|
|
1300
|
+
const xmlContent = await fs3.readFile(xmlFile, "utf-8");
|
|
1439
1301
|
xmlContents.push(xmlContent);
|
|
1440
1302
|
}
|
|
1441
1303
|
} else {
|
|
@@ -1464,9 +1326,9 @@ async function cmdConvert(junitPath, options) {
|
|
|
1464
1326
|
console.log(`\u2713 Saved to ${options.outputDir}`);
|
|
1465
1327
|
}
|
|
1466
1328
|
async function findXmlFiles(dir, result = []) {
|
|
1467
|
-
const entries = await
|
|
1329
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1468
1330
|
for (const entry of entries) {
|
|
1469
|
-
const fullPath =
|
|
1331
|
+
const fullPath = path3.join(dir, entry.name);
|
|
1470
1332
|
if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
|
|
1471
1333
|
result.push(fullPath);
|
|
1472
1334
|
else if (entry.isDirectory())
|
|
@@ -1476,10 +1338,10 @@ async function findXmlFiles(dir, result = []) {
|
|
|
1476
1338
|
}
|
|
1477
1339
|
|
|
1478
1340
|
// src/cli/cmd-download.ts
|
|
1479
|
-
import
|
|
1480
|
-
import
|
|
1341
|
+
import fs4 from "fs";
|
|
1342
|
+
import path4 from "path";
|
|
1481
1343
|
async function cmdDownload(session2, project, runId, rootDir) {
|
|
1482
|
-
if (
|
|
1344
|
+
if (fs4.existsSync(rootDir)) {
|
|
1483
1345
|
console.log(`Directory ${rootDir} already exists!`);
|
|
1484
1346
|
return;
|
|
1485
1347
|
}
|
|
@@ -1488,15 +1350,15 @@ async function cmdDownload(session2, project, runId, rootDir) {
|
|
|
1488
1350
|
projectSlug: project.projectSlug,
|
|
1489
1351
|
runId
|
|
1490
1352
|
});
|
|
1491
|
-
const attachmentsDir =
|
|
1492
|
-
await
|
|
1353
|
+
const attachmentsDir = path4.join(rootDir, "attachments");
|
|
1354
|
+
await fs4.promises.mkdir(rootDir, { recursive: true });
|
|
1493
1355
|
if (urls.attachmentURLs.length)
|
|
1494
|
-
await
|
|
1356
|
+
await fs4.promises.mkdir(attachmentsDir, { recursive: true });
|
|
1495
1357
|
const response = await fetch(urls.reportURL);
|
|
1496
1358
|
if (!response.ok)
|
|
1497
1359
|
throw new Error(`HTTP error ${response.status} for report URL: ${urls.reportURL}`);
|
|
1498
1360
|
const reportContent = await response.text();
|
|
1499
|
-
await
|
|
1361
|
+
await fs4.promises.writeFile(path4.join(rootDir, "report.json"), reportContent);
|
|
1500
1362
|
const attachmentDownloader = async () => {
|
|
1501
1363
|
while (urls.attachmentURLs.length) {
|
|
1502
1364
|
const url = urls.attachmentURLs.pop();
|
|
@@ -1504,8 +1366,8 @@ async function cmdDownload(session2, project, runId, rootDir) {
|
|
|
1504
1366
|
if (!response2.ok)
|
|
1505
1367
|
throw new Error(`HTTP error ${response2.status} for attachment URL: ${url}`);
|
|
1506
1368
|
const fileBuffer = Buffer.from(await response2.arrayBuffer());
|
|
1507
|
-
const filename =
|
|
1508
|
-
await
|
|
1369
|
+
const filename = path4.basename(new URL(url).pathname);
|
|
1370
|
+
await fs4.promises.writeFile(path4.join(attachmentsDir, filename), fileBuffer);
|
|
1509
1371
|
}
|
|
1510
1372
|
};
|
|
1511
1373
|
const workerPromises = [];
|
|
@@ -1626,27 +1488,23 @@ async function cmdUnlink() {
|
|
|
1626
1488
|
}
|
|
1627
1489
|
|
|
1628
1490
|
// src/cli/cmd-upload.ts
|
|
1629
|
-
import { uploadReport } from "@flakiness/sdk";
|
|
1491
|
+
import { readReport, uploadReport } from "@flakiness/sdk";
|
|
1630
1492
|
import chalk from "chalk";
|
|
1631
|
-
import
|
|
1632
|
-
import
|
|
1493
|
+
import fs5 from "fs/promises";
|
|
1494
|
+
import path5 from "path";
|
|
1633
1495
|
var warn = (txt) => console.warn(chalk.yellow(`[flakiness.io] WARN: ${txt}`));
|
|
1634
1496
|
var err = (txt) => console.error(chalk.red(`[flakiness.io] Error: ${txt}`));
|
|
1635
1497
|
async function cmdUpload(relativePaths, options) {
|
|
1636
1498
|
for (const relativePath of relativePaths) {
|
|
1637
|
-
const fullPath =
|
|
1638
|
-
if (!await
|
|
1499
|
+
const fullPath = path5.resolve(relativePath);
|
|
1500
|
+
if (!await fs5.access(fullPath, fs5.constants.F_OK).then(() => true).catch(() => false)) {
|
|
1639
1501
|
err(`Path ${fullPath} is not accessible!`);
|
|
1640
1502
|
process.exit(1);
|
|
1641
1503
|
}
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
const attachmentsDir = options.attachmentsDir ?? path6.dirname(fullPath);
|
|
1645
|
-
const { attachmentIdToPath, missingAttachments } = await resolveAttachmentPaths(report, attachmentsDir);
|
|
1646
|
-
if (missingAttachments.length) {
|
|
1504
|
+
const { report, attachments, missingAttachments } = await readReport(path5.dirname(fullPath));
|
|
1505
|
+
if (missingAttachments.length)
|
|
1647
1506
|
warn(`Missing ${missingAttachments.length} attachments`);
|
|
1648
|
-
|
|
1649
|
-
await uploadReport(report, Array.from(attachmentIdToPath.values()), {
|
|
1507
|
+
await uploadReport(report, attachments, {
|
|
1650
1508
|
flakinessAccessToken: options.accessToken,
|
|
1651
1509
|
flakinessEndpoint: options.endpoint
|
|
1652
1510
|
});
|
|
@@ -1666,17 +1524,17 @@ async function cmdWhoami() {
|
|
|
1666
1524
|
}
|
|
1667
1525
|
|
|
1668
1526
|
// src/cli/cli.ts
|
|
1527
|
+
var log2 = debug2("fk:cli");
|
|
1669
1528
|
var session = await FlakinessSession.load();
|
|
1670
1529
|
var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
|
|
1671
1530
|
var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(session?.endpoint() ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
|
|
1672
|
-
var optAttachmentsDir = new Option("--attachments-dir <dir>", "Directory containing attachments to upload. Defaults to the report directory");
|
|
1673
1531
|
async function runCommand(callback) {
|
|
1674
1532
|
try {
|
|
1675
1533
|
await callback();
|
|
1676
1534
|
} catch (e) {
|
|
1677
1535
|
if (!(e instanceof Error))
|
|
1678
1536
|
throw e;
|
|
1679
|
-
|
|
1537
|
+
log2(e);
|
|
1680
1538
|
process.exit(1);
|
|
1681
1539
|
}
|
|
1682
1540
|
}
|
|
@@ -1780,7 +1638,7 @@ program.command("download").description("Download run").addOption(optSince).addO
|
|
|
1780
1638
|
});
|
|
1781
1639
|
console.log(`Found ${Ranges.cardinality(runIds)} reports uploaded since ${options.since}`);
|
|
1782
1640
|
}
|
|
1783
|
-
const alreadyExisting =
|
|
1641
|
+
const alreadyExisting = fs6.readdirSync(process.cwd());
|
|
1784
1642
|
const downloadedRuns = alreadyExisting.filter((entry) => entry.startsWith("fkrun-")).map((run) => parseInt(run.substring(`fkrun-`.length), 10)).filter((runId) => !isNaN(runId));
|
|
1785
1643
|
console.log(`Found ${downloadedRuns.length} locally downloaded reports`);
|
|
1786
1644
|
const toBeDownloaded = Ranges.subtract(runIds, Ranges.fromList(downloadedRuns));
|
|
@@ -1803,13 +1661,13 @@ program.command("download").description("Download run").addOption(optSince).addO
|
|
|
1803
1661
|
await Promise.all(downloaders);
|
|
1804
1662
|
spinner.stop();
|
|
1805
1663
|
}));
|
|
1806
|
-
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) => {
|
|
1807
1665
|
await runCommand(async () => {
|
|
1808
1666
|
await cmdUpload(relativePaths, await ensureAccessToken(options));
|
|
1809
1667
|
});
|
|
1810
1668
|
});
|
|
1811
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 () => {
|
|
1812
|
-
const dir =
|
|
1670
|
+
const dir = path6.resolve(arg ?? "flakiness-report");
|
|
1813
1671
|
await showReport(dir);
|
|
1814
1672
|
}));
|
|
1815
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) => {
|
package/lib/junit.js
CHANGED
|
@@ -1,121 +1,10 @@
|
|
|
1
1
|
// src/junit.ts
|
|
2
|
-
import { ReportUtils
|
|
2
|
+
import { ReportUtils } from "@flakiness/sdk";
|
|
3
3
|
import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
|
|
4
4
|
import assert from "assert";
|
|
5
|
-
import fs2 from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
|
|
8
|
-
// src/utils.ts
|
|
9
|
-
import { ReportUtils } from "@flakiness/sdk";
|
|
10
|
-
import crypto from "crypto";
|
|
11
5
|
import fs from "fs";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
function sha1File(filePath) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const hash = crypto.createHash("sha1");
|
|
17
|
-
const stream = fs.createReadStream(filePath);
|
|
18
|
-
stream.on("data", (chunk) => {
|
|
19
|
-
hash.update(chunk);
|
|
20
|
-
});
|
|
21
|
-
stream.on("end", () => {
|
|
22
|
-
resolve(hash.digest("hex"));
|
|
23
|
-
});
|
|
24
|
-
stream.on("error", (err) => {
|
|
25
|
-
reject(err);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
var FLAKINESS_DBG = !!process.env.FLAKINESS_DBG;
|
|
30
|
-
function errorText(error) {
|
|
31
|
-
return FLAKINESS_DBG ? error.stack : error.message;
|
|
32
|
-
}
|
|
33
|
-
function sha1Buffer(data) {
|
|
34
|
-
const hash = crypto.createHash("sha1");
|
|
35
|
-
hash.update(data);
|
|
36
|
-
return hash.digest("hex");
|
|
37
|
-
}
|
|
38
|
-
async function retryWithBackoff(job, backoff = []) {
|
|
39
|
-
for (const timeout of backoff) {
|
|
40
|
-
try {
|
|
41
|
-
return await job();
|
|
42
|
-
} catch (e) {
|
|
43
|
-
if (e instanceof AggregateError)
|
|
44
|
-
console.error(`[flakiness.io err]`, errorText(e.errors[0]));
|
|
45
|
-
else if (e instanceof Error)
|
|
46
|
-
console.error(`[flakiness.io err]`, errorText(e));
|
|
47
|
-
else
|
|
48
|
-
console.error(`[flakiness.io err]`, e);
|
|
49
|
-
await new Promise((x) => setTimeout(x, timeout));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return await job();
|
|
53
|
-
}
|
|
54
|
-
var httpUtils;
|
|
55
|
-
((httpUtils2) => {
|
|
56
|
-
function createRequest({ url, method = "get", headers = {} }) {
|
|
57
|
-
let resolve;
|
|
58
|
-
let reject;
|
|
59
|
-
const responseDataPromise = new Promise((a, b) => {
|
|
60
|
-
resolve = a;
|
|
61
|
-
reject = b;
|
|
62
|
-
});
|
|
63
|
-
const protocol = url.startsWith("https") ? https : http;
|
|
64
|
-
headers = Object.fromEntries(Object.entries(headers).filter(([key, value]) => value !== void 0));
|
|
65
|
-
const request = protocol.request(url, { method, headers }, (res) => {
|
|
66
|
-
const chunks = [];
|
|
67
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
68
|
-
res.on("end", () => {
|
|
69
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
70
|
-
resolve(Buffer.concat(chunks));
|
|
71
|
-
else
|
|
72
|
-
reject(new Error(`Request to ${url} failed with ${res.statusCode}`));
|
|
73
|
-
});
|
|
74
|
-
res.on("error", (error) => reject(error));
|
|
75
|
-
});
|
|
76
|
-
request.on("error", reject);
|
|
77
|
-
return { request, responseDataPromise };
|
|
78
|
-
}
|
|
79
|
-
httpUtils2.createRequest = createRequest;
|
|
80
|
-
async function getBuffer(url, backoff) {
|
|
81
|
-
return await retryWithBackoff(async () => {
|
|
82
|
-
const { request, responseDataPromise } = createRequest({ url });
|
|
83
|
-
request.end();
|
|
84
|
-
return await responseDataPromise;
|
|
85
|
-
}, backoff);
|
|
86
|
-
}
|
|
87
|
-
httpUtils2.getBuffer = getBuffer;
|
|
88
|
-
async function getText(url, backoff) {
|
|
89
|
-
const buffer = await getBuffer(url, backoff);
|
|
90
|
-
return buffer.toString("utf-8");
|
|
91
|
-
}
|
|
92
|
-
httpUtils2.getText = getText;
|
|
93
|
-
async function getJSON(url) {
|
|
94
|
-
return JSON.parse(await getText(url));
|
|
95
|
-
}
|
|
96
|
-
httpUtils2.getJSON = getJSON;
|
|
97
|
-
async function postText(url, text, backoff) {
|
|
98
|
-
const headers = {
|
|
99
|
-
"Content-Type": "application/json",
|
|
100
|
-
"Content-Length": Buffer.byteLength(text) + ""
|
|
101
|
-
};
|
|
102
|
-
return await retryWithBackoff(async () => {
|
|
103
|
-
const { request, responseDataPromise } = createRequest({ url, headers, method: "post" });
|
|
104
|
-
request.write(text);
|
|
105
|
-
request.end();
|
|
106
|
-
return await responseDataPromise;
|
|
107
|
-
}, backoff);
|
|
108
|
-
}
|
|
109
|
-
httpUtils2.postText = postText;
|
|
110
|
-
async function postJSON(url, json, backoff) {
|
|
111
|
-
const buffer = await postText(url, JSON.stringify(json), backoff);
|
|
112
|
-
return JSON.parse(buffer.toString("utf-8"));
|
|
113
|
-
}
|
|
114
|
-
httpUtils2.postJSON = postJSON;
|
|
115
|
-
})(httpUtils || (httpUtils = {}));
|
|
116
|
-
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");
|
|
117
|
-
|
|
118
|
-
// src/junit.ts
|
|
6
|
+
import mime from "mime";
|
|
7
|
+
import path from "path";
|
|
119
8
|
function getProperties(element) {
|
|
120
9
|
const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
|
|
121
10
|
if (!propertiesNodes.length)
|
|
@@ -158,21 +47,9 @@ function extractStdout(testcase, stdio) {
|
|
|
158
47
|
}
|
|
159
48
|
async function parseAttachment(value) {
|
|
160
49
|
let absolutePath = path.resolve(process.cwd(), value);
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
contentType: "image/png",
|
|
165
|
-
path: absolutePath,
|
|
166
|
-
id,
|
|
167
|
-
type: "file"
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
contentType: "text/plain",
|
|
172
|
-
id: sha1Buffer(value),
|
|
173
|
-
body: Buffer.from(value),
|
|
174
|
-
type: "buffer"
|
|
175
|
-
};
|
|
50
|
+
if (fs.existsSync(absolutePath))
|
|
51
|
+
return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
|
|
52
|
+
return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
|
|
176
53
|
}
|
|
177
54
|
async function traverseJUnitReport(context, node) {
|
|
178
55
|
const element = node;
|
|
@@ -301,7 +178,7 @@ async function parseJUnit(xmls, options) {
|
|
|
301
178
|
await traverseJUnitReport(context, element);
|
|
302
179
|
}
|
|
303
180
|
return {
|
|
304
|
-
report:
|
|
181
|
+
report: ReportUtils.normalizeReport(report),
|
|
305
182
|
attachments: Array.from(context.attachments.values())
|
|
306
183
|
};
|
|
307
184
|
}
|