@gabrielerandelli/minus-tracker 0.5.6 → 0.5.8
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 +331 -41
- package/dist/cli/index.js +440 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/data/stress-manifest.json +13387 -0
- package/dist/index.cjs +24 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/package.json +20 -2
- package/samples/sample-trades.csv +6 -0
package/dist/cli/index.js
CHANGED
|
@@ -36,7 +36,7 @@ var it = {
|
|
|
36
36
|
ratesGaps: (list) => `Lacune: ${list}`,
|
|
37
37
|
ratesUpdateFetching: "Recupero dati BCE SDMX in corso...",
|
|
38
38
|
ratesUpdateDone: (n) => `Completato. Aggiunte ${n} nuove date.`,
|
|
39
|
-
ratesSnapshotWritten: (
|
|
39
|
+
ratesSnapshotWritten: (path5) => `Snapshot scritto in ${path5}`,
|
|
40
40
|
ratesAutoUpdateStart: "Le rate BCE non sono aggiornate. Aggiornamento automatico in corso...",
|
|
41
41
|
ratesAutoUpdateFailed: "Aggiornamento automatico fallito. Si procede con le rate disponibili.",
|
|
42
42
|
configLangSet: (lang) => `Lingua impostata su: ${lang}`,
|
|
@@ -77,7 +77,7 @@ var en = {
|
|
|
77
77
|
ratesGaps: (list) => `Gaps: ${list}`,
|
|
78
78
|
ratesUpdateFetching: "Fetching ECB SDMX...",
|
|
79
79
|
ratesUpdateDone: (n) => `Done. Added ${n} new dates.`,
|
|
80
|
-
ratesSnapshotWritten: (
|
|
80
|
+
ratesSnapshotWritten: (path5) => `Snapshot written to ${path5}`,
|
|
81
81
|
ratesAutoUpdateStart: "ECB rates are stale. Auto-updating\u2026",
|
|
82
82
|
ratesAutoUpdateFailed: "Auto-update failed. Proceeding with available rates.",
|
|
83
83
|
configLangSet: (lang) => `Language set to: ${lang}`,
|
|
@@ -319,6 +319,17 @@ var DEGIROParser = class {
|
|
|
319
319
|
constructor(snapshot) {
|
|
320
320
|
this._snapshot = snapshot;
|
|
321
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Parse a DEGIRO Transactions CSV export string.
|
|
324
|
+
*
|
|
325
|
+
* @param csv - Raw UTF-8 string from the DEGIRO Transactions export.
|
|
326
|
+
* @returns Array of normalised Transaction objects. Empty if no data rows are found.
|
|
327
|
+
* @throws {ParseError} code `"INVALID_CSV"` — malformed CSV or binary content.
|
|
328
|
+
* @throws {ParseError} code `"MISSING_COLUMN"` (+ `columnName`) — required column absent.
|
|
329
|
+
*
|
|
330
|
+
* Rows with missing ISIN, unsupported currency, zero quantity, or no ECB rate within
|
|
331
|
+
* 3 trading days are skipped silently. Inspect `parser.warnings` for details.
|
|
332
|
+
*/
|
|
322
333
|
parse(csv) {
|
|
323
334
|
this._warningEntries = [];
|
|
324
335
|
if (typeof csv !== "string" || csv.includes("\0")) {
|
|
@@ -442,10 +453,23 @@ function inferTaxYear(transactions) {
|
|
|
442
453
|
var Calculator = class {
|
|
443
454
|
_transactions;
|
|
444
455
|
_parseWarnings;
|
|
456
|
+
/**
|
|
457
|
+
* @param transactions - Normalised Transaction objects from the CSV import step.
|
|
458
|
+
* @param parseWarnings - Warnings collected during the import step; forwarded into the report.
|
|
459
|
+
*/
|
|
445
460
|
constructor(transactions, parseWarnings) {
|
|
446
461
|
this._transactions = transactions;
|
|
447
462
|
this._parseWarnings = parseWarnings ?? [];
|
|
448
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* Run LIFO or FIFO lot-matching over the transaction list.
|
|
466
|
+
*
|
|
467
|
+
* @param method - `"LIFO"` or `"FIFO"`.
|
|
468
|
+
* @returns GainsReport with `plusvalenze`, `minusvalenze`, `netResult`, per-lot breakdown,
|
|
469
|
+
* ECB rates used, and any accumulated warnings.
|
|
470
|
+
* @throws {CalculationError} when a SELL has no matching open buy lots.
|
|
471
|
+
* `error.isin` and `error.date` identify the problematic transaction.
|
|
472
|
+
*/
|
|
449
473
|
calculateGains(method) {
|
|
450
474
|
const warnings = [...this._parseWarnings];
|
|
451
475
|
const sorted = [...this._transactions].sort((a, b) => {
|
|
@@ -843,6 +867,412 @@ async function runConfig(positional, flags, s, stdout, stderr) {
|
|
|
843
867
|
return 2;
|
|
844
868
|
}
|
|
845
869
|
|
|
870
|
+
// src/cli/commands/stress-test.ts
|
|
871
|
+
import * as fs6 from "fs";
|
|
872
|
+
import * as path4 from "path";
|
|
873
|
+
import * as os3 from "os";
|
|
874
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
875
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
876
|
+
|
|
877
|
+
// src/stress/generator.ts
|
|
878
|
+
function formatDate(isoDate) {
|
|
879
|
+
const [yyyy, mm, dd] = isoDate.split("-");
|
|
880
|
+
return `${dd}-${mm}-${yyyy}`;
|
|
881
|
+
}
|
|
882
|
+
function generateCsv(scenario) {
|
|
883
|
+
if (scenario.special === "invalid_csv") {
|
|
884
|
+
return "NOT_A_CSV\0\ngarbage data";
|
|
885
|
+
}
|
|
886
|
+
const allColumns = [
|
|
887
|
+
"Date",
|
|
888
|
+
"Time",
|
|
889
|
+
"Product",
|
|
890
|
+
"ISIN",
|
|
891
|
+
"Exchange",
|
|
892
|
+
"Execution centre",
|
|
893
|
+
"Quantity",
|
|
894
|
+
"Price",
|
|
895
|
+
"Local value",
|
|
896
|
+
"Local value currency",
|
|
897
|
+
"Value",
|
|
898
|
+
"Value currency",
|
|
899
|
+
"Exchange rate",
|
|
900
|
+
"Transaction costs",
|
|
901
|
+
"Transaction costs currency",
|
|
902
|
+
"Total",
|
|
903
|
+
"Total currency",
|
|
904
|
+
"Order ID"
|
|
905
|
+
];
|
|
906
|
+
let columns = allColumns;
|
|
907
|
+
if (scenario.special === "missing_col_isin") {
|
|
908
|
+
columns = columns.filter((c) => c !== "ISIN");
|
|
909
|
+
} else if (scenario.special === "missing_col_quantity") {
|
|
910
|
+
columns = columns.filter((c) => c !== "Quantity");
|
|
911
|
+
} else if (scenario.special === "missing_col_price") {
|
|
912
|
+
columns = columns.filter((c) => c !== "Price");
|
|
913
|
+
}
|
|
914
|
+
const buildRow = (values, includeColumns) => {
|
|
915
|
+
return includeColumns.map((col) => values[col] ?? "").join(",");
|
|
916
|
+
};
|
|
917
|
+
const header = columns.join(",");
|
|
918
|
+
const rows = [header];
|
|
919
|
+
for (let rowIndex = 0; rowIndex < scenario.transactions.length; rowIndex++) {
|
|
920
|
+
const tx = scenario.transactions[rowIndex];
|
|
921
|
+
const localValue = -tx.qty * tx.price;
|
|
922
|
+
const total = localValue - tx.fee;
|
|
923
|
+
const rowValues = {
|
|
924
|
+
Date: formatDate(tx.date),
|
|
925
|
+
Time: "09:00",
|
|
926
|
+
Product: tx.product,
|
|
927
|
+
ISIN: tx.isin,
|
|
928
|
+
Exchange: "XEUR",
|
|
929
|
+
"Execution centre": "XEUR",
|
|
930
|
+
Quantity: String(tx.qty),
|
|
931
|
+
Price: tx.price.toFixed(2),
|
|
932
|
+
"Local value": localValue.toFixed(2),
|
|
933
|
+
"Local value currency": tx.currency,
|
|
934
|
+
Value: localValue.toFixed(2),
|
|
935
|
+
"Value currency": "EUR",
|
|
936
|
+
"Exchange rate": "1",
|
|
937
|
+
"Transaction costs": (-Math.abs(tx.fee)).toFixed(2),
|
|
938
|
+
"Transaction costs currency": "EUR",
|
|
939
|
+
Total: total.toFixed(2),
|
|
940
|
+
"Total currency": "EUR",
|
|
941
|
+
"Order ID": `stress-${scenario.id}-${rowIndex}`
|
|
942
|
+
};
|
|
943
|
+
const row = buildRow(rowValues, columns);
|
|
944
|
+
rows.push(row);
|
|
945
|
+
}
|
|
946
|
+
return rows.join("\n");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/stress/runner.ts
|
|
950
|
+
import { spawnSync } from "child_process";
|
|
951
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
952
|
+
import { join as join4 } from "path";
|
|
953
|
+
function countWarnings(text) {
|
|
954
|
+
if (!text) return 0;
|
|
955
|
+
const match = text.match(/(?:warnings|avvertenze)\s*:\s*(\d+)/i);
|
|
956
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
957
|
+
}
|
|
958
|
+
function isValidJson(jsonStr) {
|
|
959
|
+
try {
|
|
960
|
+
const obj = JSON.parse(jsonStr);
|
|
961
|
+
return typeof obj === "object" && obj !== null && "plusvalenze" in obj && "minusvalenze" in obj && "lots" in obj;
|
|
962
|
+
} catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function runCommand(cliBin, args, cmdLabel, expectedExitCode, outputShapeCheck) {
|
|
967
|
+
const isJsFile = cliBin.endsWith(".js");
|
|
968
|
+
let result;
|
|
969
|
+
if (isJsFile) {
|
|
970
|
+
result = spawnSync("node", [cliBin, ...args], {
|
|
971
|
+
encoding: "utf8",
|
|
972
|
+
timeout: 15e3,
|
|
973
|
+
env: { ...process.env, MINUS_TRACKER_LANG: void 0 }
|
|
974
|
+
});
|
|
975
|
+
} else {
|
|
976
|
+
result = spawnSync(cliBin, args, {
|
|
977
|
+
encoding: "utf8",
|
|
978
|
+
timeout: 15e3,
|
|
979
|
+
env: { ...process.env, MINUS_TRACKER_LANG: void 0 }
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
const exitCode = result.status ?? 1;
|
|
983
|
+
const stdout = result.stdout || "";
|
|
984
|
+
const stderr = result.stderr || "";
|
|
985
|
+
const exitCodeMatch = exitCode === expectedExitCode;
|
|
986
|
+
const shapeCheck = outputShapeCheck(stdout, stderr, exitCode);
|
|
987
|
+
let pass = exitCodeMatch && shapeCheck.valid;
|
|
988
|
+
let failure;
|
|
989
|
+
if (!exitCodeMatch) {
|
|
990
|
+
failure = `exit code mismatch: expected ${expectedExitCode}, got ${exitCode}`;
|
|
991
|
+
} else if (!shapeCheck.valid) {
|
|
992
|
+
failure = shapeCheck.reason;
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
cmd: cmdLabel,
|
|
996
|
+
exitCode,
|
|
997
|
+
stdout,
|
|
998
|
+
stderr,
|
|
999
|
+
pass,
|
|
1000
|
+
failure
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function runScenario(scenario, csvContent, outputDir, cliBin) {
|
|
1004
|
+
const csvFileName = `${scenario.id}-${scenario.slug}.csv`;
|
|
1005
|
+
const csvFile = join4(outputDir, csvFileName);
|
|
1006
|
+
writeFileSync3(csvFile, csvContent, "utf8");
|
|
1007
|
+
const results = [];
|
|
1008
|
+
results.push(
|
|
1009
|
+
runCommand(
|
|
1010
|
+
cliBin,
|
|
1011
|
+
["calc", csvFile],
|
|
1012
|
+
"calc",
|
|
1013
|
+
scenario.expect.calc_exit,
|
|
1014
|
+
(stdout) => {
|
|
1015
|
+
if (scenario.expect.calc_exit === 0) {
|
|
1016
|
+
const hasPlus = /plusvalenze|plusvalues/i.test(stdout);
|
|
1017
|
+
return hasPlus ? { valid: true } : {
|
|
1018
|
+
valid: false,
|
|
1019
|
+
reason: "calc output missing plusvalenze/plusvalues (exit 0)"
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return { valid: true };
|
|
1023
|
+
}
|
|
1024
|
+
)
|
|
1025
|
+
);
|
|
1026
|
+
results.push(
|
|
1027
|
+
runCommand(
|
|
1028
|
+
cliBin,
|
|
1029
|
+
["calc", "--method", "FIFO", csvFile],
|
|
1030
|
+
"calc --method FIFO",
|
|
1031
|
+
scenario.expect.fifo_exit,
|
|
1032
|
+
(stdout) => {
|
|
1033
|
+
if (scenario.expect.fifo_exit === 0) {
|
|
1034
|
+
const hasPlus = /plusvalenze|plusvalues/i.test(stdout);
|
|
1035
|
+
return hasPlus ? { valid: true } : {
|
|
1036
|
+
valid: false,
|
|
1037
|
+
reason: "calc --method FIFO output missing plusvalenze/plusvalues (exit 0)"
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
return { valid: true };
|
|
1041
|
+
}
|
|
1042
|
+
)
|
|
1043
|
+
);
|
|
1044
|
+
results.push(
|
|
1045
|
+
runCommand(
|
|
1046
|
+
cliBin,
|
|
1047
|
+
["calc", "--json", csvFile],
|
|
1048
|
+
"calc --json",
|
|
1049
|
+
scenario.expect.json_exit,
|
|
1050
|
+
(stdout) => {
|
|
1051
|
+
if (scenario.expect.json_exit === 0) {
|
|
1052
|
+
const isValid = isValidJson(stdout);
|
|
1053
|
+
return isValid ? { valid: true } : {
|
|
1054
|
+
valid: false,
|
|
1055
|
+
reason: "calc --json output is not valid JSON with required keys (exit 0)"
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
return { valid: true };
|
|
1059
|
+
}
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
results.push(
|
|
1063
|
+
runCommand(
|
|
1064
|
+
cliBin,
|
|
1065
|
+
["calc", "--lang", "en", csvFile],
|
|
1066
|
+
"calc --lang en",
|
|
1067
|
+
scenario.expect.en_exit,
|
|
1068
|
+
(stdout) => {
|
|
1069
|
+
if (scenario.expect.en_exit === 0) {
|
|
1070
|
+
const nonEmpty = stdout.trim().length > 0;
|
|
1071
|
+
return nonEmpty ? { valid: true } : {
|
|
1072
|
+
valid: false,
|
|
1073
|
+
reason: "calc --lang en output is empty (exit 0)"
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return { valid: true };
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
);
|
|
1080
|
+
results.push(
|
|
1081
|
+
runCommand(
|
|
1082
|
+
cliBin,
|
|
1083
|
+
["validate", csvFile],
|
|
1084
|
+
"validate",
|
|
1085
|
+
scenario.expect.validate_exit,
|
|
1086
|
+
(stdout) => {
|
|
1087
|
+
if (scenario.expect.validate_exit !== 0) return { valid: true };
|
|
1088
|
+
const nonEmpty = stdout.trim().length > 0;
|
|
1089
|
+
return nonEmpty ? { valid: true } : {
|
|
1090
|
+
valid: false,
|
|
1091
|
+
reason: "validate output is empty"
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
)
|
|
1095
|
+
);
|
|
1096
|
+
const enCalcResult = results[3];
|
|
1097
|
+
const actualWarningCount = countWarnings(enCalcResult.stdout);
|
|
1098
|
+
const warningCheckPass = actualWarningCount >= scenario.expect.warning_count;
|
|
1099
|
+
const pass = results.every((r) => r.pass) && warningCheckPass;
|
|
1100
|
+
return {
|
|
1101
|
+
id: scenario.id,
|
|
1102
|
+
category: scenario.category,
|
|
1103
|
+
slug: scenario.slug,
|
|
1104
|
+
description: scenario.description,
|
|
1105
|
+
csvFile,
|
|
1106
|
+
results,
|
|
1107
|
+
pass,
|
|
1108
|
+
warningCheckPass,
|
|
1109
|
+
expectedWarningCount: scenario.expect.warning_count,
|
|
1110
|
+
actualWarningCount
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/stress/reporter.ts
|
|
1115
|
+
function buildReport(results, rangeStart, rangeEnd) {
|
|
1116
|
+
const passed = results.filter((r) => r.pass).length;
|
|
1117
|
+
const failed = results.length - passed;
|
|
1118
|
+
return {
|
|
1119
|
+
totalScenarios: results.length,
|
|
1120
|
+
passed,
|
|
1121
|
+
failed,
|
|
1122
|
+
results,
|
|
1123
|
+
rangeStart,
|
|
1124
|
+
rangeEnd
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function formatJson(report) {
|
|
1128
|
+
return JSON.stringify(report, null, 2);
|
|
1129
|
+
}
|
|
1130
|
+
function formatTable(report) {
|
|
1131
|
+
const lines = [];
|
|
1132
|
+
lines.push("STRESS TEST \u2014 minus-tracker");
|
|
1133
|
+
lines.push(
|
|
1134
|
+
`Scenari: ${report.totalScenarios} | Passati: ${report.passed} | Falliti: ${report.failed}`
|
|
1135
|
+
);
|
|
1136
|
+
lines.push("");
|
|
1137
|
+
const idCol = "ID";
|
|
1138
|
+
const categoryCol = "CATEGORIA";
|
|
1139
|
+
const descCol = "DESCRIZIONE";
|
|
1140
|
+
const resultCol = "ESITO";
|
|
1141
|
+
lines.push(formatRow(idCol, categoryCol, descCol, resultCol));
|
|
1142
|
+
for (const result of report.results) {
|
|
1143
|
+
const idStr = result.id.padStart(3, "0");
|
|
1144
|
+
const status = result.pass ? "\u2713 PASS" : "\u2717 FAIL";
|
|
1145
|
+
lines.push(formatRow(idStr, result.category, result.description, status));
|
|
1146
|
+
if (!result.pass) {
|
|
1147
|
+
for (const cmdResult of result.results) {
|
|
1148
|
+
if (!cmdResult.pass) {
|
|
1149
|
+
if (cmdResult.failure) {
|
|
1150
|
+
let failureText = "";
|
|
1151
|
+
if (cmdResult.failure.includes("exit code mismatch")) {
|
|
1152
|
+
const match = cmdResult.failure.match(
|
|
1153
|
+
/expected (\d+), got (\d+)/
|
|
1154
|
+
);
|
|
1155
|
+
if (match) {
|
|
1156
|
+
failureText = `atteso exit ${match[1]}, ottenuto exit ${match[2]}`;
|
|
1157
|
+
}
|
|
1158
|
+
} else {
|
|
1159
|
+
failureText = `output shape non valido \u2014 ${cmdResult.failure}`;
|
|
1160
|
+
}
|
|
1161
|
+
lines.push(` \u2192 ${cmdResult.cmd}: ${failureText}`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (!result.warningCheckPass) {
|
|
1166
|
+
lines.push(
|
|
1167
|
+
` \u2192 avvertenze: attese \u2265${result.expectedWarningCount}, trovate ${result.actualWarningCount}`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
lines.push("");
|
|
1173
|
+
lines.push(`PASSATI: ${report.passed}`);
|
|
1174
|
+
lines.push(`FALLITI: ${report.failed}`);
|
|
1175
|
+
lines.push("");
|
|
1176
|
+
return lines.join("\n");
|
|
1177
|
+
}
|
|
1178
|
+
function formatRow(id, category, description, esito) {
|
|
1179
|
+
const idWidth = 4;
|
|
1180
|
+
const categoryWidth = 22;
|
|
1181
|
+
const descWidth = 40;
|
|
1182
|
+
const esitoWidth = 8;
|
|
1183
|
+
const idCol = id.padEnd(idWidth);
|
|
1184
|
+
const categoryCol = truncateOrPad(category, categoryWidth);
|
|
1185
|
+
const descCol = truncateOrPad(description, descWidth);
|
|
1186
|
+
const esitoCol = esito.padEnd(esitoWidth);
|
|
1187
|
+
return `${idCol} ${categoryCol} ${descCol} ${esitoCol}`;
|
|
1188
|
+
}
|
|
1189
|
+
function truncateOrPad(str, maxWidth) {
|
|
1190
|
+
if (str.length > maxWidth) {
|
|
1191
|
+
return str.slice(0, maxWidth - 2) + "..";
|
|
1192
|
+
}
|
|
1193
|
+
return str.padEnd(maxWidth);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/cli/commands/stress-test.ts
|
|
1197
|
+
async function runStressTest(_positionals, flags, stdout, stderr) {
|
|
1198
|
+
const rangeStr = flags["range"] ?? "1-100";
|
|
1199
|
+
const rangeParts = rangeStr.split("-");
|
|
1200
|
+
if (rangeParts.length !== 2) {
|
|
1201
|
+
stderr.write(
|
|
1202
|
+
`Error: --range must be in the form N-M (e.g. 1-100), got: "${rangeStr}"
|
|
1203
|
+
`
|
|
1204
|
+
);
|
|
1205
|
+
return 2;
|
|
1206
|
+
}
|
|
1207
|
+
const rangeStart = parseInt(rangeParts[0], 10);
|
|
1208
|
+
const rangeEnd = parseInt(rangeParts[1], 10);
|
|
1209
|
+
if (isNaN(rangeStart) || isNaN(rangeEnd) || rangeStart <= 0 || rangeEnd <= 0 || rangeStart > rangeEnd || rangeStart > 100 || rangeEnd > 100) {
|
|
1210
|
+
stderr.write(
|
|
1211
|
+
`Error: --range values must be positive integers between 1 and 100 with start \u2264 end, got: "${rangeStr}"
|
|
1212
|
+
`
|
|
1213
|
+
);
|
|
1214
|
+
return 2;
|
|
1215
|
+
}
|
|
1216
|
+
const __dirname = path4.dirname(fileURLToPath2(import.meta.url));
|
|
1217
|
+
const manifestPath = path4.join(__dirname, "../data/stress-manifest.json");
|
|
1218
|
+
let manifest;
|
|
1219
|
+
try {
|
|
1220
|
+
const raw = fs6.readFileSync(manifestPath, "utf8");
|
|
1221
|
+
manifest = JSON.parse(raw);
|
|
1222
|
+
} catch {
|
|
1223
|
+
stderr.write(`Error: could not load stress manifest at ${manifestPath}
|
|
1224
|
+
`);
|
|
1225
|
+
return 1;
|
|
1226
|
+
}
|
|
1227
|
+
const cliBin = process.argv[1];
|
|
1228
|
+
let outputDir;
|
|
1229
|
+
const outputDirFlag = flags["output-dir"];
|
|
1230
|
+
if (outputDirFlag) {
|
|
1231
|
+
outputDir = outputDirFlag;
|
|
1232
|
+
fs6.mkdirSync(outputDir, { recursive: true });
|
|
1233
|
+
} else {
|
|
1234
|
+
outputDir = fs6.mkdtempSync(path4.join(os3.tmpdir(), "minus-tracker-stress-"));
|
|
1235
|
+
}
|
|
1236
|
+
const ratesCheck = spawnSync2("node", [cliBin, "rates", "--check"], {
|
|
1237
|
+
encoding: "utf8",
|
|
1238
|
+
timeout: 15e3
|
|
1239
|
+
});
|
|
1240
|
+
if ((ratesCheck.status ?? 1) !== 0) {
|
|
1241
|
+
stderr.write("Warning: rates --check failed; ECB rates may be stale.\n");
|
|
1242
|
+
}
|
|
1243
|
+
const configShow = spawnSync2("node", [cliBin, "config", "--show"], {
|
|
1244
|
+
encoding: "utf8",
|
|
1245
|
+
timeout: 15e3
|
|
1246
|
+
});
|
|
1247
|
+
if ((configShow.status ?? 1) !== 0) {
|
|
1248
|
+
stderr.write("Warning: config --show failed.\n");
|
|
1249
|
+
}
|
|
1250
|
+
const scenarios = manifest.scenarios.filter((s) => {
|
|
1251
|
+
const id = parseInt(s.id, 10);
|
|
1252
|
+
return id >= rangeStart && id <= rangeEnd;
|
|
1253
|
+
});
|
|
1254
|
+
const results = [];
|
|
1255
|
+
for (const scenario of scenarios) {
|
|
1256
|
+
const csv = generateCsv(scenario);
|
|
1257
|
+
const result = runScenario(scenario, csv, outputDir, cliBin);
|
|
1258
|
+
results.push(result);
|
|
1259
|
+
stdout.write(
|
|
1260
|
+
` [${result.pass ? "\u2713" : "\u2717"}] ${scenario.id} ${scenario.slug}
|
|
1261
|
+
`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const report = buildReport(results, rangeStart, rangeEnd);
|
|
1265
|
+
if (flags["json"]) {
|
|
1266
|
+
stdout.write(formatJson(report) + "\n");
|
|
1267
|
+
} else {
|
|
1268
|
+
stdout.write(formatTable(report) + "\n");
|
|
1269
|
+
}
|
|
1270
|
+
if (!flags["keep"]) {
|
|
1271
|
+
fs6.rmSync(outputDir, { recursive: true, force: true });
|
|
1272
|
+
}
|
|
1273
|
+
return report.failed === 0 ? 0 : 1;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
846
1276
|
// src/cli/index.ts
|
|
847
1277
|
async function main() {
|
|
848
1278
|
const { values, positionals } = parseArgs({
|
|
@@ -854,7 +1284,10 @@ async function main() {
|
|
|
854
1284
|
json: { type: "boolean", default: false },
|
|
855
1285
|
check: { type: "boolean", default: false },
|
|
856
1286
|
update: { type: "boolean", default: false },
|
|
857
|
-
show: { type: "boolean", default: false }
|
|
1287
|
+
show: { type: "boolean", default: false },
|
|
1288
|
+
range: { type: "string" },
|
|
1289
|
+
keep: { type: "boolean", default: false },
|
|
1290
|
+
"output-dir": { type: "string" }
|
|
858
1291
|
},
|
|
859
1292
|
allowPositionals: true,
|
|
860
1293
|
strict: false
|
|
@@ -881,9 +1314,12 @@ async function main() {
|
|
|
881
1314
|
case "config":
|
|
882
1315
|
exitCode = await runConfig(restPositionals, flags, s, stdout, stderr);
|
|
883
1316
|
break;
|
|
1317
|
+
case "stress-test":
|
|
1318
|
+
exitCode = await runStressTest(restPositionals, flags, stdout, stderr);
|
|
1319
|
+
break;
|
|
884
1320
|
default:
|
|
885
1321
|
stderr.write(
|
|
886
|
-
"Usage: minus-tracker <calc|validate|rates|config> [options] [file]\n"
|
|
1322
|
+
"Usage: minus-tracker <calc|validate|rates|config|stress-test> [options] [file]\n"
|
|
887
1323
|
);
|
|
888
1324
|
exitCode = 2;
|
|
889
1325
|
}
|