deflake 1.1.6 → 1.2.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/cli.js +49 -26
- package/client.js +3 -2
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -177,35 +177,50 @@ function stripAnsi(str) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
function parsePlaywrightLogs(fullLog) {
|
|
180
|
-
// Strip ANSI codes to reliable regex matching
|
|
181
180
|
const cleanLog = stripAnsi(fullLog);
|
|
182
181
|
const errorBlocks = [];
|
|
183
|
-
|
|
184
|
-
// Regex to find start of a test failure: " 1) [chromium] › ..."
|
|
185
182
|
const errorHeaderRegex = /^\s*(\d+)\)\s+\[(.*?)\]\s+/gm;
|
|
186
183
|
let match;
|
|
187
184
|
|
|
188
|
-
// Find all start indices
|
|
189
185
|
const indices = [];
|
|
190
186
|
while ((match = errorHeaderRegex.exec(cleanLog)) !== null) {
|
|
191
|
-
indices.push({ index: match.index,
|
|
187
|
+
indices.push({ index: match.index, name: match[0].trim() });
|
|
192
188
|
}
|
|
193
189
|
|
|
194
|
-
// Slice log into blocks
|
|
195
190
|
for (let i = 0; i < indices.length; i++) {
|
|
196
191
|
const start = indices[i].index;
|
|
197
192
|
const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
|
|
198
|
-
const content = cleanLog.slice(start, end);
|
|
199
193
|
errorBlocks.push({
|
|
200
|
-
|
|
201
|
-
content:
|
|
194
|
+
name: indices[i].name,
|
|
195
|
+
content: cleanLog.slice(start, end)
|
|
202
196
|
});
|
|
203
197
|
}
|
|
204
|
-
|
|
205
198
|
return errorBlocks;
|
|
206
199
|
}
|
|
207
200
|
|
|
208
|
-
|
|
201
|
+
function parseCypressLogs(fullLog) {
|
|
202
|
+
const cleanLog = stripAnsi(fullLog);
|
|
203
|
+
const specBlocks = [];
|
|
204
|
+
const specHeaderRegex = /Running:\s+([^\s]+)\s+\(\d+\s+of\s+\d+\)/g;
|
|
205
|
+
let match;
|
|
206
|
+
|
|
207
|
+
const indices = [];
|
|
208
|
+
while ((match = specHeaderRegex.exec(cleanLog)) !== null) {
|
|
209
|
+
indices.push({ index: match.index, spec: match[1] });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (let i = 0; i < indices.length; i++) {
|
|
213
|
+
const start = indices[i].index;
|
|
214
|
+
const end = (i + 1 < indices.length) ? indices[i + 1].index : cleanLog.length;
|
|
215
|
+
specBlocks.push({
|
|
216
|
+
spec: indices[i].spec,
|
|
217
|
+
content: cleanLog.slice(start, end)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return specBlocks;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function runHealer(logContent, htmlPath, apiUrl, testName, applyFix = false) {
|
|
209
224
|
// Check file size limit (Basic check to avoid 429s on huge files)
|
|
210
225
|
if (htmlPath && fs.existsSync(htmlPath)) {
|
|
211
226
|
const stats = fs.statSync(htmlPath);
|
|
@@ -267,7 +282,7 @@ async function runHealer(logContent, htmlPath, apiUrl, testName) {
|
|
|
267
282
|
}
|
|
268
283
|
|
|
269
284
|
try {
|
|
270
|
-
const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent);
|
|
285
|
+
const result = await client.heal(finalLogPath, htmlPath, failureLoc, sourceCodeContent, applyFix);
|
|
271
286
|
|
|
272
287
|
if (result && result.status === 'success') {
|
|
273
288
|
// Return structured object for grouping
|
|
@@ -625,7 +640,10 @@ async function runDoctor(argv) {
|
|
|
625
640
|
if (result.status === 'success') {
|
|
626
641
|
const usage = result.data;
|
|
627
642
|
process.stdout.write(`\r ✅ API Connected! (Tier: ${C.GREEN}${usage.tier.toUpperCase()}${C.RESET}) \n`);
|
|
628
|
-
|
|
643
|
+
const u = usage;
|
|
644
|
+
const remaining = u.limit - u.usage;
|
|
645
|
+
console.log(` Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining${C.RESET}`);
|
|
646
|
+
console.log(` Analysis: ${C.GREEN}FREE${C.RESET} (Pay-per-fix model enabled)`);
|
|
629
647
|
} else {
|
|
630
648
|
process.stdout.write(`\r ❌ ${C.RED}API Connectivity Failed${C.RESET} \n`);
|
|
631
649
|
if (result.code === 401 || result.code === 403) {
|
|
@@ -715,23 +733,30 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
715
733
|
console.log(` (Processing the first ${limit}...)\n`);
|
|
716
734
|
}
|
|
717
735
|
|
|
718
|
-
const
|
|
719
|
-
const results = [];
|
|
720
|
-
const processLimit = Math.min(artifacts.length, limit);
|
|
721
|
-
const batchArtifacts = artifacts.slice(0, processLimit);
|
|
736
|
+
const framework = artifacts.length > 0 && artifacts[0].htmlPath?.includes('cypress') ? 'Cypress' : 'Playwright';
|
|
722
737
|
|
|
723
738
|
console.log(`🔍 Analyzing ${batchArtifacts.length} failure(s)...`);
|
|
724
739
|
|
|
740
|
+
const playwrightBlocks = (framework === 'Playwright') ? parsePlaywrightLogs(fullLog || "") : [];
|
|
741
|
+
const cypressBlocks = (framework === 'Cypress') ? parseCypressLogs(fullLog || "") : [];
|
|
742
|
+
|
|
725
743
|
for (const art of batchArtifacts) {
|
|
726
744
|
let specificLog = fullLog;
|
|
727
|
-
|
|
745
|
+
|
|
746
|
+
if (framework === 'Cypress' && cypressBlocks.length > 0) {
|
|
747
|
+
// Match screenshot path to spec log
|
|
748
|
+
// Screenshot typically: cypress/screenshots/folder/file.cy.ts/Test Name (failed).png
|
|
749
|
+
const match = cypressBlocks.find(b => art.htmlPath && art.htmlPath.includes(b.spec));
|
|
750
|
+
if (match) {
|
|
751
|
+
specificLog = match.content;
|
|
752
|
+
}
|
|
753
|
+
} else if (framework === 'Playwright' && playwrightBlocks.length > 0) {
|
|
754
|
+
// Heuristic matching for Playwright error blocks
|
|
728
755
|
let bestMatch = null;
|
|
729
756
|
let bestScore = -1;
|
|
730
|
-
const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3
|
|
731
|
-
const browser = art.name.toLowerCase().includes('firefox') ? 'firefox' : art.name.toLowerCase().includes('webkit') ? 'webkit' : 'chromium';
|
|
732
|
-
const candidates = errorBlocks.filter(b => b.browser.toLowerCase().includes(browser));
|
|
757
|
+
const artifactTokens = art.name.toLowerCase().replace(/[-_]/g, ' ').split(' ').filter(w => w.length > 3);
|
|
733
758
|
|
|
734
|
-
for (const block of
|
|
759
|
+
for (const block of playwrightBlocks) {
|
|
735
760
|
let score = 0;
|
|
736
761
|
const blockLower = block.content.toLowerCase();
|
|
737
762
|
for (const token of artifactTokens) { if (blockLower.includes(token)) score++; }
|
|
@@ -739,8 +764,6 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
739
764
|
}
|
|
740
765
|
if (bestMatch && bestScore > 0) {
|
|
741
766
|
specificLog = bestMatch.content;
|
|
742
|
-
const idx = errorBlocks.indexOf(bestMatch);
|
|
743
|
-
if (idx > -1) errorBlocks.splice(idx, 1);
|
|
744
767
|
}
|
|
745
768
|
}
|
|
746
769
|
|
|
@@ -748,7 +771,7 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
748
771
|
process.stdout.write(`\r⏳ Analyzing ${displayName}... `);
|
|
749
772
|
|
|
750
773
|
try {
|
|
751
|
-
const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name);
|
|
774
|
+
const result = await runHealer(specificLog, art.htmlPath, argv.apiUrl, art.name, argv.fix);
|
|
752
775
|
|
|
753
776
|
if (result && result.status === 'success') {
|
|
754
777
|
results.push(result);
|
|
@@ -804,7 +827,7 @@ async function analyzeFailures(artifacts, fullLog, client) {
|
|
|
804
827
|
if (updatedUsage.status === 'success') {
|
|
805
828
|
const u = updatedUsage.data;
|
|
806
829
|
const remaining = u.limit - u.usage;
|
|
807
|
-
console.log(` -
|
|
830
|
+
console.log(` - Fix Quota: ${C.GREEN}${remaining}/${u.limit} remaining (Analysis is FREE)${C.RESET}`);
|
|
808
831
|
}
|
|
809
832
|
} catch (e) {
|
|
810
833
|
// usage fetch failed is non-critical
|
package/client.js
CHANGED
|
@@ -94,7 +94,7 @@ class DeFlakeClient {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async heal(logPath, htmlPath, failureLocation = null, sourceCode = null) {
|
|
97
|
+
async heal(logPath, htmlPath, failureLocation = null, sourceCode = null, applyFix = false) {
|
|
98
98
|
try {
|
|
99
99
|
// ... (rest of the check logic) ...
|
|
100
100
|
if (!fs.existsSync(logPath)) throw new Error(`Log file not found: ${logPath}`);
|
|
@@ -119,7 +119,8 @@ class DeFlakeClient {
|
|
|
119
119
|
html_snapshot: htmlContent || "",
|
|
120
120
|
failing_line: failureLocation ? `Line ${failureLocation.rootLine}` : "",
|
|
121
121
|
source_code: sourceCode || "",
|
|
122
|
-
framework: this.framework
|
|
122
|
+
framework: this.framework,
|
|
123
|
+
apply_fix: applyFix
|
|
123
124
|
};
|
|
124
125
|
|
|
125
126
|
const response = await axios.post(this.apiUrl, payload, {
|