@arghajit/playwright-pulse-report 0.2.0 → 0.2.1
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 +3 -2
- package/dist/reporter/playwright-pulse-reporter.js +115 -107
- package/package.json +3 -3
- package/scripts/generate-static-report.mjs +945 -1058
- package/scripts/generate-trend.mjs +165 -0
- package/scripts/generate-trend-excel.mjs +0 -273
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// XLSX is NO LONGER NEEDED here
|
|
4
|
+
|
|
5
|
+
// Use dynamic import for chalk as it's ESM only for prettier console logs
|
|
6
|
+
let chalk;
|
|
7
|
+
try {
|
|
8
|
+
chalk = (await import("chalk")).default;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
chalk = {
|
|
11
|
+
green: (t) => t,
|
|
12
|
+
red: (t) => t,
|
|
13
|
+
yellow: (t) => t,
|
|
14
|
+
blue: (t) => t,
|
|
15
|
+
bold: (t) => t,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_OUTPUT_DIR = "pulse-report";
|
|
20
|
+
const CURRENT_RUN_JSON_FILE = "playwright-pulse-report.json"; // Source of the current run data
|
|
21
|
+
const HISTORY_SUBDIR = "history"; // Subdirectory for historical JSON files
|
|
22
|
+
const HISTORY_FILE_PREFIX = "trend-";
|
|
23
|
+
const MAX_HISTORY_FILES = 15; // Store last 15 runs
|
|
24
|
+
|
|
25
|
+
async function archiveCurrentRunData() {
|
|
26
|
+
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
27
|
+
const currentRunJsonPath = path.join(outputDir, CURRENT_RUN_JSON_FILE);
|
|
28
|
+
const historyDir = path.join(outputDir, HISTORY_SUBDIR);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 1. Ensure history directory exists
|
|
32
|
+
await fs.mkdir(historyDir, { recursive: true });
|
|
33
|
+
// console.log(chalk.blue(`History directory ensured at: ${historyDir}`));
|
|
34
|
+
|
|
35
|
+
// 2. Read the current run's JSON data
|
|
36
|
+
// console.log(chalk.blue(`Reading current run data from: ${currentRunJsonPath}`));
|
|
37
|
+
let currentReportData;
|
|
38
|
+
try {
|
|
39
|
+
const jsonData = await fs.readFile(currentRunJsonPath, "utf-8");
|
|
40
|
+
currentReportData = JSON.parse(jsonData);
|
|
41
|
+
if (
|
|
42
|
+
!currentReportData ||
|
|
43
|
+
!currentReportData.run ||
|
|
44
|
+
!currentReportData.run.timestamp
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Invalid current run JSON report structure. Missing 'run' or 'run.timestamp' data."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(
|
|
52
|
+
chalk.red(
|
|
53
|
+
`Error reading or parsing current run JSON report at ${currentRunJsonPath}: ${error.message}`
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
process.exit(1); // Exit if we can't read the source file
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Determine the filename for the new history file
|
|
60
|
+
// Ensure timestamp is a valid number before using getTime()
|
|
61
|
+
let runTimestampMs;
|
|
62
|
+
try {
|
|
63
|
+
runTimestampMs = new Date(currentReportData.run.timestamp).getTime();
|
|
64
|
+
if (isNaN(runTimestampMs)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Invalid timestamp value: ${currentReportData.run.timestamp}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
} catch (dateError) {
|
|
70
|
+
console.error(
|
|
71
|
+
chalk.red(
|
|
72
|
+
`Failed to parse timestamp '${currentReportData.run.timestamp}': ${dateError.message}`
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const newHistoryFileName = `${HISTORY_FILE_PREFIX}${runTimestampMs}.json`;
|
|
79
|
+
const newHistoryFilePath = path.join(historyDir, newHistoryFileName);
|
|
80
|
+
|
|
81
|
+
// 4. Write the current run's data to the new history file
|
|
82
|
+
// console.log(chalk.blue(`Saving current run data to: ${newHistoryFilePath}`));
|
|
83
|
+
await fs.writeFile(
|
|
84
|
+
newHistoryFilePath,
|
|
85
|
+
JSON.stringify(currentReportData, null, 2),
|
|
86
|
+
"utf-8"
|
|
87
|
+
);
|
|
88
|
+
console.log(chalk.green(`Archived current run to: ${newHistoryFilePath}`));
|
|
89
|
+
|
|
90
|
+
// 5. Prune old history files
|
|
91
|
+
await pruneOldHistoryFiles(historyDir);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(
|
|
94
|
+
chalk.red(`Error in archiveCurrentRunData: ${error.message}`)
|
|
95
|
+
);
|
|
96
|
+
// console.error(error.stack); // Uncomment for more detailed stack trace
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function pruneOldHistoryFiles(historyDir) {
|
|
102
|
+
// console.log(chalk.blue(`Pruning old history files in ${historyDir} (keeping last ${MAX_HISTORY_FILES})...`));
|
|
103
|
+
try {
|
|
104
|
+
const files = await fs.readdir(historyDir);
|
|
105
|
+
const historyJsonFiles = files
|
|
106
|
+
.filter(
|
|
107
|
+
(file) => file.startsWith(HISTORY_FILE_PREFIX) && file.endsWith(".json")
|
|
108
|
+
)
|
|
109
|
+
.map((file) => {
|
|
110
|
+
const timestampPart = file
|
|
111
|
+
.replace(HISTORY_FILE_PREFIX, "")
|
|
112
|
+
.replace(".json", "");
|
|
113
|
+
return { name: file, timestamp: parseInt(timestampPart, 10) };
|
|
114
|
+
})
|
|
115
|
+
.filter((file) => !isNaN(file.timestamp))
|
|
116
|
+
.sort((a, b) => a.timestamp - b.timestamp); // Sort ascending (oldest first)
|
|
117
|
+
|
|
118
|
+
if (historyJsonFiles.length > MAX_HISTORY_FILES) {
|
|
119
|
+
const filesToDelete = historyJsonFiles.slice(
|
|
120
|
+
0,
|
|
121
|
+
historyJsonFiles.length - MAX_HISTORY_FILES
|
|
122
|
+
);
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.yellow(
|
|
125
|
+
`Found ${historyJsonFiles.length} history files. Pruning ${filesToDelete.length} oldest file(s)...`
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
for (const fileMeta of filesToDelete) {
|
|
129
|
+
const filePathToDelete = path.join(historyDir, fileMeta.name);
|
|
130
|
+
try {
|
|
131
|
+
await fs.unlink(filePathToDelete);
|
|
132
|
+
// console.log(chalk.gray(`Deleted old history file: ${fileMeta.name}`));
|
|
133
|
+
} catch (deleteError) {
|
|
134
|
+
console.warn(
|
|
135
|
+
chalk.yellow(
|
|
136
|
+
`Could not delete old history file ${fileMeta.name}: ${deleteError.message}`
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// console.log(chalk.green(`Found ${historyJsonFiles.length} history files. No pruning needed.`));
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.warn(
|
|
146
|
+
chalk.yellow(
|
|
147
|
+
`Warning during history pruning in ${historyDir}: ${error.message}`
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
// Don't exit for pruning errors, as saving the current run is more critical
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Main execution
|
|
155
|
+
archiveCurrentRunData().catch((error) => {
|
|
156
|
+
// Fallback catch, though critical errors in archiveCurrentRunData should exit
|
|
157
|
+
if (process.exitCode === undefined || process.exitCode === 0) {
|
|
158
|
+
// check if not already exited
|
|
159
|
+
console.error(
|
|
160
|
+
chalk.red.bold("An unexpected error occurred in history archiving:"),
|
|
161
|
+
error
|
|
162
|
+
);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
// generate-trend-excel.mjs
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import * as XLSX from "xlsx";
|
|
5
|
-
|
|
6
|
-
// Use dynamic import for chalk as it's ESM only for prettier console logs
|
|
7
|
-
let chalk;
|
|
8
|
-
try {
|
|
9
|
-
chalk = (await import("chalk")).default;
|
|
10
|
-
} catch (e) {
|
|
11
|
-
chalk = { green: (t) => t, red: (t) => t, yellow: (t) => t, blue: (t) => t }; // Basic fallback
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DEFAULT_OUTPUT_DIR = "pulse-report"; // Should match reporter's outputDir
|
|
15
|
-
const DEFAULT_JSON_FILE = "playwright-pulse-report.json";
|
|
16
|
-
const TREND_EXCEL_FILE_NAME = "trend.xls";
|
|
17
|
-
|
|
18
|
-
class ExcelTrendManager {
|
|
19
|
-
_excelFilePath;
|
|
20
|
-
_maxRuns = 5;
|
|
21
|
-
|
|
22
|
-
constructor(outputDir, excelFileName = TREND_EXCEL_FILE_NAME) {
|
|
23
|
-
this._excelFilePath = path.join(outputDir, excelFileName);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
getExcelFilePath() {
|
|
27
|
-
return this._excelFilePath;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async _readExistingData() {
|
|
31
|
-
try {
|
|
32
|
-
await fs.access(this._excelFilePath);
|
|
33
|
-
const buffer = await fs.readFile(this._excelFilePath);
|
|
34
|
-
return XLSX.read(buffer, { type: "buffer" });
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_shiftOverallRuns(data, currentNumericRunId) {
|
|
41
|
-
const validData = Array.isArray(data) ? data : [];
|
|
42
|
-
const pastOrCurrentData = validData.filter(
|
|
43
|
-
(row) =>
|
|
44
|
-
row.hasOwnProperty("RUN_ID") &&
|
|
45
|
-
typeof row.RUN_ID === "number" &&
|
|
46
|
-
row.RUN_ID <= currentNumericRunId
|
|
47
|
-
);
|
|
48
|
-
const sortedData = [...pastOrCurrentData].sort(
|
|
49
|
-
(a, b) => a.RUN_ID - b.RUN_ID
|
|
50
|
-
);
|
|
51
|
-
if (sortedData.length > this._maxRuns) {
|
|
52
|
-
return sortedData.slice(sortedData.length - this._maxRuns);
|
|
53
|
-
}
|
|
54
|
-
return sortedData;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async updateTrendData(
|
|
58
|
-
runIdFromReport,
|
|
59
|
-
timestamp,
|
|
60
|
-
totalTests,
|
|
61
|
-
passed,
|
|
62
|
-
failed,
|
|
63
|
-
skipped,
|
|
64
|
-
duration,
|
|
65
|
-
testResultsForThisRun
|
|
66
|
-
) {
|
|
67
|
-
let workbook = await this._readExistingData();
|
|
68
|
-
const numericRunId = Math.floor(timestamp / 1000);
|
|
69
|
-
|
|
70
|
-
if (!workbook) {
|
|
71
|
-
workbook = XLSX.utils.book_new();
|
|
72
|
-
// If the workbook is new, SheetNames will be empty.
|
|
73
|
-
// We need to initialize it if it doesn't exist
|
|
74
|
-
if (!workbook.SheetNames) {
|
|
75
|
-
workbook.SheetNames = [];
|
|
76
|
-
}
|
|
77
|
-
} else {
|
|
78
|
-
// Ensure SheetNames exists even for existing workbooks (should, but defensive)
|
|
79
|
-
if (!workbook.SheetNames) {
|
|
80
|
-
workbook.SheetNames = [];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// --- Overall Data ---
|
|
85
|
-
let existingOverallData = [];
|
|
86
|
-
if (workbook.Sheets["overall"]) {
|
|
87
|
-
try {
|
|
88
|
-
existingOverallData = XLSX.utils.sheet_to_json(
|
|
89
|
-
workbook.Sheets["overall"]
|
|
90
|
-
);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.warn(
|
|
93
|
-
chalk.yellow(
|
|
94
|
-
"Could not parse existing 'overall' sheet. Starting fresh. Error:",
|
|
95
|
-
e.message
|
|
96
|
-
)
|
|
97
|
-
);
|
|
98
|
-
existingOverallData = [];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
existingOverallData = existingOverallData.filter(
|
|
102
|
-
(row) => row.RUN_ID !== numericRunId
|
|
103
|
-
);
|
|
104
|
-
const newOverallRow = {
|
|
105
|
-
RUN_ID: numericRunId,
|
|
106
|
-
DURATION: duration,
|
|
107
|
-
TIMESTAMP: timestamp,
|
|
108
|
-
TOTAL_TESTS: totalTests,
|
|
109
|
-
PASSED: passed,
|
|
110
|
-
FAILED: failed,
|
|
111
|
-
SKIPPED: skipped,
|
|
112
|
-
};
|
|
113
|
-
let updatedOverallData = [...existingOverallData, newOverallRow];
|
|
114
|
-
updatedOverallData = this._shiftOverallRuns(
|
|
115
|
-
updatedOverallData,
|
|
116
|
-
numericRunId
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const overallSheet = XLSX.utils.json_to_sheet(updatedOverallData);
|
|
120
|
-
|
|
121
|
-
// UPDATED: Use book_append_sheet for new sheets, or replace existing
|
|
122
|
-
if (!workbook.SheetNames.includes("overall")) {
|
|
123
|
-
XLSX.utils.book_append_sheet(workbook, overallSheet, "overall");
|
|
124
|
-
// Move "overall" to the beginning if it was just added and not already first
|
|
125
|
-
const overallIndex = workbook.SheetNames.indexOf("overall");
|
|
126
|
-
if (overallIndex > 0) {
|
|
127
|
-
const sheetName = workbook.SheetNames.splice(overallIndex, 1)[0];
|
|
128
|
-
workbook.SheetNames.unshift(sheetName);
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
workbook.Sheets["overall"] = overallSheet; // Replace existing
|
|
132
|
-
}
|
|
133
|
-
XLSX.utils.book_set_sheet_visibility(workbook, "overall", 0);
|
|
134
|
-
|
|
135
|
-
// --- Per-Test Data Sheet for the Current Run ---
|
|
136
|
-
const runKey = `test run ${numericRunId}`;
|
|
137
|
-
const currentRunTestData = testResultsForThisRun.map((test) => ({
|
|
138
|
-
TEST_NAME: test.name,
|
|
139
|
-
DURATION: test.duration,
|
|
140
|
-
STATUS: test.status,
|
|
141
|
-
TIMESTAMP: timestamp,
|
|
142
|
-
}));
|
|
143
|
-
const testRunSheet = XLSX.utils.json_to_sheet(currentRunTestData);
|
|
144
|
-
|
|
145
|
-
// UPDATED: Logic to add or replace the sheet and ensure it's in SheetNames
|
|
146
|
-
if (!workbook.SheetNames.includes(runKey)) {
|
|
147
|
-
XLSX.utils.book_append_sheet(workbook, testRunSheet, runKey); // This adds to Sheets and SheetNames
|
|
148
|
-
} else {
|
|
149
|
-
workbook.Sheets[runKey] = testRunSheet; // Just replace the sheet data
|
|
150
|
-
}
|
|
151
|
-
// Now that the sheet is guaranteed to be in SheetNames and workbook.Sheets, set visibility
|
|
152
|
-
XLSX.utils.book_set_sheet_visibility(workbook, runKey, 0);
|
|
153
|
-
|
|
154
|
-
// --- Maintain Max Sheet Count for Individual Test Runs ---
|
|
155
|
-
let testRunSheetNames = workbook.SheetNames.filter(
|
|
156
|
-
(name) => name.toLowerCase().startsWith("test run ") && name !== "overall"
|
|
157
|
-
);
|
|
158
|
-
testRunSheetNames.sort((a, b) => {
|
|
159
|
-
const matchA = a.match(/test run (\d+)$/i);
|
|
160
|
-
const matchB = b.match(/test run (\d+)$/i);
|
|
161
|
-
const idA = matchA && matchA[1] ? parseInt(matchA[1], 10) : 0;
|
|
162
|
-
const idB = matchB && matchB[1] ? parseInt(matchB[1], 10) : 0;
|
|
163
|
-
return idA - idB;
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (testRunSheetNames.length > this._maxRuns) {
|
|
167
|
-
const sheetsToRemoveCount = testRunSheetNames.length - this._maxRuns;
|
|
168
|
-
const removedSheetNames = [];
|
|
169
|
-
for (let i = 0; i < sheetsToRemoveCount; i++) {
|
|
170
|
-
const oldestSheetName = testRunSheetNames[i];
|
|
171
|
-
// Remove from workbook.Sheets
|
|
172
|
-
delete workbook.Sheets[oldestSheetName];
|
|
173
|
-
removedSheetNames.push(oldestSheetName);
|
|
174
|
-
}
|
|
175
|
-
// Rebuild SheetNames array without the removed sheets
|
|
176
|
-
workbook.SheetNames = workbook.SheetNames.filter(
|
|
177
|
-
(name) => !removedSheetNames.includes(name)
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// --- Write Workbook ---
|
|
182
|
-
try {
|
|
183
|
-
const buffer = XLSX.write(workbook, { bookType: "xls", type: "buffer" });
|
|
184
|
-
await fs.writeFile(this._excelFilePath, buffer);
|
|
185
|
-
console.log(
|
|
186
|
-
chalk.green(
|
|
187
|
-
`Excel trend report updated successfully at ${this._excelFilePath}`
|
|
188
|
-
)
|
|
189
|
-
);
|
|
190
|
-
} catch (writeError) {
|
|
191
|
-
console.error(
|
|
192
|
-
chalk.red(`Failed to write Excel file at ${this._excelFilePath}`)
|
|
193
|
-
);
|
|
194
|
-
console.error(chalk.red("Write Error Details:"), writeError);
|
|
195
|
-
throw writeError;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function generateTrendExcel() {
|
|
201
|
-
const outputDir = path.resolve(process.cwd(), DEFAULT_OUTPUT_DIR);
|
|
202
|
-
const jsonReportPath = path.join(outputDir, DEFAULT_JSON_FILE);
|
|
203
|
-
|
|
204
|
-
// Ensure output directory exists before any file operations
|
|
205
|
-
try {
|
|
206
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
207
|
-
} catch (mkdirError) {
|
|
208
|
-
console.error(
|
|
209
|
-
chalk.red(`Failed to create output directory ${outputDir}:`),
|
|
210
|
-
mkdirError
|
|
211
|
-
);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
console.log(chalk.blue(`Reading JSON report from: ${jsonReportPath}`));
|
|
216
|
-
let reportData;
|
|
217
|
-
try {
|
|
218
|
-
const jsonData = await fs.readFile(jsonReportPath, "utf-8");
|
|
219
|
-
reportData = JSON.parse(jsonData);
|
|
220
|
-
if (!reportData || !reportData.run || !Array.isArray(reportData.results)) {
|
|
221
|
-
throw new Error(
|
|
222
|
-
"Invalid JSON report structure. Missing 'run' or 'results' data."
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error(
|
|
227
|
-
chalk.red(`Error reading or parsing JSON report: ${error.message}`)
|
|
228
|
-
);
|
|
229
|
-
console.error(chalk.red("JSON Read/Parse Error Details:"), error);
|
|
230
|
-
process.exit(1);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const { run, results } = reportData;
|
|
234
|
-
if (!run.timestamp || isNaN(new Date(run.timestamp).getTime())) {
|
|
235
|
-
console.error(
|
|
236
|
-
chalk.red(`Invalid or missing run.timestamp in JSON: ${run.timestamp}`)
|
|
237
|
-
);
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
const runTimestamp = new Date(run.timestamp).getTime();
|
|
241
|
-
|
|
242
|
-
const testResultsForExcel = results.map((r) => ({
|
|
243
|
-
name: r.name,
|
|
244
|
-
duration: r.duration,
|
|
245
|
-
status: r.status,
|
|
246
|
-
}));
|
|
247
|
-
|
|
248
|
-
const excelManager = new ExcelTrendManager(outputDir);
|
|
249
|
-
try {
|
|
250
|
-
await excelManager.updateTrendData(
|
|
251
|
-
run.id,
|
|
252
|
-
runTimestamp,
|
|
253
|
-
run.totalTests,
|
|
254
|
-
run.passed,
|
|
255
|
-
run.failed,
|
|
256
|
-
run.skipped,
|
|
257
|
-
run.duration,
|
|
258
|
-
testResultsForExcel
|
|
259
|
-
);
|
|
260
|
-
} catch (excelError) {
|
|
261
|
-
console.error(chalk.red("Aborting due to error during Excel generation."));
|
|
262
|
-
console.error(chalk.red("Excel Generation Error Details:"), excelError);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
generateTrendExcel().catch((error) => {
|
|
268
|
-
console.error(
|
|
269
|
-
chalk.red("An unexpected error occurred in generate-trend-excel:"),
|
|
270
|
-
error
|
|
271
|
-
);
|
|
272
|
-
process.exit(1);
|
|
273
|
-
});
|