tewip-ai 0.17.2 → 0.18.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.
|
@@ -47,7 +47,37 @@ function relativeFile(file, workspace) {
|
|
|
47
47
|
return f.slice(ws.length);
|
|
48
48
|
return f;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Does this report come from a run that re-ran tests? Either the runner said so with a rerun
|
|
52
|
+
* element, or some test in it is written twice with a failure recorded - both of which pytest's
|
|
53
|
+
* rerun plugin does and an ordinary passing run never does.
|
|
54
|
+
*/
|
|
55
|
+
function looksLikeReruns(root) {
|
|
56
|
+
let rerunElements = false;
|
|
57
|
+
let failureSeen = false;
|
|
58
|
+
const names = new Set();
|
|
59
|
+
let duplicateWithFailure = false;
|
|
60
|
+
const walk = (node) => {
|
|
61
|
+
for (const child of node.children) {
|
|
62
|
+
if (child.name === 'testcase') {
|
|
63
|
+
const key = `${child.attrs.classname ?? ''}::${child.attrs.name ?? ''}`;
|
|
64
|
+
const failed = child.children.some((c) => FAILURE.has(c.name));
|
|
65
|
+
if (child.children.some((c) => RETRY_FAILURE.has(c.name)))
|
|
66
|
+
rerunElements = true;
|
|
67
|
+
if (failed)
|
|
68
|
+
failureSeen = true;
|
|
69
|
+
if (names.has(key) && failureSeen)
|
|
70
|
+
duplicateWithFailure = true;
|
|
71
|
+
names.add(key);
|
|
72
|
+
}
|
|
73
|
+
walk(child);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
walk(root);
|
|
77
|
+
return rerunElements || duplicateWithFailure;
|
|
78
|
+
}
|
|
50
79
|
function junitCases(root, workspace) {
|
|
80
|
+
const rerunning = looksLikeReruns(root);
|
|
51
81
|
const out = [];
|
|
52
82
|
const walk = (e, suite) => {
|
|
53
83
|
for (const c of e.children) {
|
|
@@ -70,10 +100,16 @@ function junitCases(root, workspace) {
|
|
|
70
100
|
const previous = out.at(-1);
|
|
71
101
|
if (previous?.testId === testId) {
|
|
72
102
|
// Rerun plugins (pytest-rerunfailures and others) write every attempt as its own entry and
|
|
73
|
-
// only re-run after a failure, so an earlier entry of the same test
|
|
74
|
-
//
|
|
103
|
+
// only re-run after a failure, so an earlier entry of the same test can be a failed attempt
|
|
104
|
+
// whose error the plugin left out. Only adjacent entries are merged.
|
|
105
|
+
//
|
|
106
|
+
// The inference is only safe where the report shows this runner re-runs tests at all. Without
|
|
107
|
+
// that condition two adjacent entries of the same name were enough to invent a failure, and
|
|
108
|
+
// real suites produce those innocently: node:test repeats a name for a subtest and its
|
|
109
|
+
// parent, so piscina's own 152 passing tests came back as two failing ones, diagnosed as
|
|
110
|
+
// flaky. A test that passed must never be reported as having failed.
|
|
75
111
|
const earlier = previous.attempts.at(-1);
|
|
76
|
-
if (!earlier.failed)
|
|
112
|
+
if (!earlier.failed && rerunning)
|
|
77
113
|
previous.attempts[previous.attempts.length - 1] = { failed: true, message: '', stack: '' };
|
|
78
114
|
previous.attempts.push(...attempts);
|
|
79
115
|
return;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { truncate } from './signature.js';
|
|
2
|
+
/** The module name each runner prints when it cannot load something. */
|
|
3
|
+
const PATTERNS = [
|
|
4
|
+
{ language: 'Python', pattern: /ModuleNotFoundError:\s*No module named ['"]([^'"]+)['"]/ },
|
|
5
|
+
{ language: 'Node', pattern: /Cannot find module ['"]([^'"]+)['"]/ },
|
|
6
|
+
{ language: 'Node', pattern: /ERR_MODULE_NOT_FOUND[^'"]*['"]([^'"]+)['"]/ },
|
|
7
|
+
{ language: 'Java', pattern: /(?:NoClassDefFoundError|ClassNotFoundException):\s*([\w.$]+)/ },
|
|
8
|
+
{ language: 'Ruby', pattern: /cannot load such file --\s*(\S+)/ },
|
|
9
|
+
{ language: 'Go', pattern: /cannot find package ['"]([^'"]+)['"]/ },
|
|
10
|
+
{ language: '.NET', pattern: /Could not load file or assembly ['"]([^'",]+)/ },
|
|
11
|
+
{ language: 'PHP', pattern: /Failed opening required ['"]([^'"]+)['"]|Class ['"]([^'"]+)['"] not found/ },
|
|
12
|
+
];
|
|
13
|
+
// `ImportError: cannot import name 'x'` is deliberately not here. It usually means the code asked
|
|
14
|
+
// for something that does not exist - a rename that missed a caller - which is a real bug, and only
|
|
15
|
+
// sometimes a version mismatch. "No module named" says plainly that nothing could be found.
|
|
16
|
+
/** A module that is part of this project rather than something it depends on. */
|
|
17
|
+
const LOOKS_INTERNAL = /^\.|^(?:src|tests?|app|lib|internal|pkg|main)\b|_test$|\.spec$|\.test$/i;
|
|
18
|
+
/** The file could be found but not understood, which is a mistake in the code, not the machine. */
|
|
19
|
+
const A_CODE_ERROR = /\b(?:SyntaxError|IndentationError|TabError|TypeError|NameError|AttributeError|error TS\d+|Unexpected token)\b/;
|
|
20
|
+
/**
|
|
21
|
+
* pytest reports a missing plugin as a missing fixture, which on its own is ambiguous: a typo in a
|
|
22
|
+
* fixture name reads exactly the same. What separates them is how many tests it hits. A typo breaks
|
|
23
|
+
* the one test that made it; an uninstalled plugin breaks every test that asks for its fixture.
|
|
24
|
+
*
|
|
25
|
+
* Found on humanize's real suite, where 15 tests failed with "fixture 'benchmark' not found". The
|
|
26
|
+
* model answered those 15 identical failures with 13 ENV and 2 TEST_DATA - the same input getting
|
|
27
|
+
* different answers, which is worse than either answer, and exactly what a rule is for.
|
|
28
|
+
*/
|
|
29
|
+
const MISSING_FIXTURE = /fixture ['"]([^'"]+)['"] not found/;
|
|
30
|
+
export const FIXTURE_SHARED_BY = 3;
|
|
31
|
+
export const fixtureMissing = (errorMessage) => MISSING_FIXTURE.exec(errorMessage)?.[1] ?? null;
|
|
32
|
+
export function missingDependency(errorMessage) {
|
|
33
|
+
if (A_CODE_ERROR.test(errorMessage))
|
|
34
|
+
return null;
|
|
35
|
+
for (const { language, pattern } of PATTERNS) {
|
|
36
|
+
const found = pattern.exec(errorMessage);
|
|
37
|
+
const module = found?.slice(1).find(Boolean);
|
|
38
|
+
if (!module)
|
|
39
|
+
continue;
|
|
40
|
+
if (LOOKS_INTERNAL.test(module))
|
|
41
|
+
return null;
|
|
42
|
+
return { language, module };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Nothing about the test ran, so nothing about the test or the code has been shown. The suite needs
|
|
48
|
+
* a dependency the machine does not have.
|
|
49
|
+
*/
|
|
50
|
+
export const envMissingDependency = {
|
|
51
|
+
id: 'env-missing-dependency',
|
|
52
|
+
evaluate({ record, run }) {
|
|
53
|
+
const fixture = fixtureMissing(record.errorMessage);
|
|
54
|
+
if (fixture) {
|
|
55
|
+
const sharing = run.failures.filter((f) => fixtureMissing(f.errorMessage) === fixture).length;
|
|
56
|
+
if (sharing < FIXTURE_SHARED_BY)
|
|
57
|
+
return null; // one test only: a typo is as likely, so say nothing
|
|
58
|
+
return {
|
|
59
|
+
category: 'ENV',
|
|
60
|
+
confidence: 0.85,
|
|
61
|
+
evidence: [
|
|
62
|
+
{
|
|
63
|
+
source: 'env-missing-dependency',
|
|
64
|
+
message: `${sharing} tests in this run all need the fixture "${truncate(fixture, 40)}" and none can find it: the plugin that provides it is not installed, so no test code ran`,
|
|
65
|
+
data: { fixture, sharing },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const found = missingDependency(record.errorMessage);
|
|
71
|
+
if (!found)
|
|
72
|
+
return null;
|
|
73
|
+
// When the whole run failed this way, the install step is the obvious suspect and there is
|
|
74
|
+
// nothing test-specific left to doubt.
|
|
75
|
+
const wholeRun = run.failures.length >= 3 && run.failures.every((f) => missingDependency(f.errorMessage) !== null);
|
|
76
|
+
return {
|
|
77
|
+
category: 'ENV',
|
|
78
|
+
confidence: wholeRun ? 0.92 : 0.85,
|
|
79
|
+
evidence: [
|
|
80
|
+
{
|
|
81
|
+
source: 'env-missing-dependency',
|
|
82
|
+
message: wholeRun
|
|
83
|
+
? `every failure in this run is a module that could not be loaded (${found.language}: "${truncate(found.module, 60)}"): the dependencies were not installed, so no test code ran`
|
|
84
|
+
: `the test could not be loaded because ${found.language} could not find "${truncate(found.module, 60)}": a dependency is missing, so nothing about the test itself has been shown`,
|
|
85
|
+
data: { ...found, wholeRun },
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
package/dist/engine/rules.js
CHANGED
|
@@ -2,6 +2,7 @@ import { deviceEnvSignal } from './device-errors.js';
|
|
|
2
2
|
import { INFRA_MEANING, infraSignal } from './infra-errors.js';
|
|
3
3
|
import { isElementNotFound, networkSignal, valueMismatch } from './error-parse.js';
|
|
4
4
|
import { locatorDrift, locatorDriftStrict } from './drift-rules.js';
|
|
5
|
+
import { envMissingDependency } from './missing-dependency.js';
|
|
5
6
|
import { timingFlaky, timingIntercepted, timingIntermittent, timingKnownFlaky, timingRetryPass, timingStaleElement } from './timing-rules.js';
|
|
6
7
|
import { errorScenarioOf } from './llm/context.js';
|
|
7
8
|
import { errorShape, truncate } from './signature.js';
|
|
@@ -191,4 +192,4 @@ const realBugAssertion = {
|
|
|
191
192
|
* Priority order: the first matching rule decides. Signals from outside the test (ENV)
|
|
192
193
|
* come first, so a 5xx that also fails an assertion is ENV, not REAL_BUG.
|
|
193
194
|
*/
|
|
194
|
-
export const RULES = [envMassFailure, envNetwork, envInfra, envDevice, testDataCollision, envServerError, locatorDriftStrict, timingIntercepted, timingStaleElement, timingRetryPass, locatorDrift, timingFlaky, timingIntermittent, timingKnownFlaky, realBugAssertion];
|
|
195
|
+
export const RULES = [envMissingDependency, envMassFailure, envNetwork, envInfra, envDevice, testDataCollision, envServerError, locatorDriftStrict, timingIntercepted, timingStaleElement, timingRetryPass, locatorDrift, timingFlaky, timingIntermittent, timingKnownFlaky, realBugAssertion];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test-failures",
|