pi-python-helper 0.3.0 → 0.4.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/CHANGELOG.md +18 -0
- package/docs/tools.md +4 -3
- package/extensions/tools/dependencies.ts +1 -1
- package/extensions/tools/environment.ts +6 -2
- package/extensions/tools/test-config.ts +1 -1
- package/extensions/tools/validation.ts +5 -1
- package/package.json +4 -1
- package/src/build/selection.ts +28 -256
- package/src/build/staleness.ts +28 -104
- package/src/core/result.ts +23 -123
- package/src/core/runner.ts +3 -94
- package/src/core/safety.ts +32 -104
- package/src/validation/bundle.ts +40 -75
- package/src/validation/evidence.ts +25 -21
- package/src/validation/tdd.ts +21 -130
package/src/validation/tdd.ts
CHANGED
|
@@ -1,135 +1,26 @@
|
|
|
1
|
-
import { isTestFile } from '../project/paths.ts';
|
|
2
|
-
|
|
3
|
-
/** A production file and a test file that were matched to each other. */
|
|
4
|
-
export interface TddAssociation {
|
|
5
|
-
source: string;
|
|
6
|
-
test: string;
|
|
7
|
-
/** Tokens both paths share, so the caller can judge the match. */
|
|
8
|
-
sharedTokens: string[];
|
|
9
|
-
/**
|
|
10
|
-
* `module` when the match goes beyond the path prefix every file shares,
|
|
11
|
-
* `package` when only the common package prefix matched. A package-level match
|
|
12
|
-
* still passes the checkpoint, but it is weak evidence and is disclosed.
|
|
13
|
-
*/
|
|
14
|
-
strength: 'module' | 'package';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TddCheckpoint {
|
|
18
|
-
ok: boolean;
|
|
19
|
-
reasons: string[];
|
|
20
|
-
sourceChanges: string[];
|
|
21
|
-
testChanges: string[];
|
|
22
|
-
associations: TddAssociation[];
|
|
23
|
-
/** True when a match rested only on the shared package prefix. */
|
|
24
|
-
weakAssociation: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Tokens shorter than this cannot distinguish two module names. */
|
|
28
|
-
const MIN_TOKEN_LENGTH = 4;
|
|
29
|
-
|
|
30
|
-
/** Tokens that appear in the prefix of every file and so carry no meaning. */
|
|
31
|
-
const PREFIX_TOKENS = new Set(['test', 'tests', 'testing', 'src', 'lib']);
|
|
32
|
-
|
|
33
|
-
function tokens(path: string): string[] {
|
|
34
|
-
return path
|
|
35
|
-
.replace(/\.py$/i, '')
|
|
36
|
-
.split(/[^A-Za-z0-9]+/)
|
|
37
|
-
.map((token) => token.toLowerCase())
|
|
38
|
-
.filter((token) => token.length >= MIN_TOKEN_LENGTH && !PREFIX_TOKENS.has(token));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function sharedTokens(source: string, test: string): string[] {
|
|
42
|
-
const testTokens = tokens(test);
|
|
43
|
-
const shared: string[] = [];
|
|
44
|
-
for (const sourceToken of new Set(tokens(source))) {
|
|
45
|
-
if (
|
|
46
|
-
testTokens.some(
|
|
47
|
-
(testToken) =>
|
|
48
|
-
sourceToken === testToken ||
|
|
49
|
-
sourceToken.startsWith(testToken) ||
|
|
50
|
-
testToken.startsWith(sourceToken),
|
|
51
|
-
)
|
|
52
|
-
) {
|
|
53
|
-
shared.push(sourceToken);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return shared;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
1
|
/**
|
|
60
|
-
*
|
|
2
|
+
* Python's view of the shared TDD checkpoint.
|
|
61
3
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* directory) yields no exclusions, so an exact name match is never downgraded.
|
|
4
|
+
* The ordering and token-overlap logic live in `pi-helper-core`; this module
|
|
5
|
+
* only supplies which files count as production code or tests in a Python
|
|
6
|
+
* project, so call sites keep their two-argument signature.
|
|
66
7
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
8
|
+
import { checkTdd as coreCheckTdd, type TddSignals } from 'pi-helper-core';
|
|
9
|
+
import { isPythonFile, isTestFile } from '../project/paths.ts';
|
|
10
|
+
|
|
11
|
+
/** Tokens that appear in the prefix of most paths and so cannot distinguish modules. */
|
|
12
|
+
const PYTHON_TDD_SIGNALS: TddSignals = {
|
|
13
|
+
isSourceFile: isPythonFile,
|
|
14
|
+
isTestFile,
|
|
15
|
+
prefixTokens: new Set(['test', 'tests', 'testing', 'src', 'lib']),
|
|
16
|
+
minTokenLength: 4,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function checkTdd(
|
|
20
|
+
changedPaths: string[],
|
|
21
|
+
testChangedPaths: string[] = [],
|
|
22
|
+
): import('pi-helper-core').TddCheckpoint {
|
|
23
|
+
return coreCheckTdd(changedPaths, testChangedPaths, PYTHON_TDD_SIGNALS);
|
|
78
24
|
}
|
|
79
25
|
|
|
80
|
-
|
|
81
|
-
* Check that production changes are accompanied by a plausibly related test
|
|
82
|
-
* change.
|
|
83
|
-
*
|
|
84
|
-
* Matching is name-based on purpose: it is cheap, deterministic, and only used
|
|
85
|
-
* to decide whether to run the heavier verification bundle. Because it is only
|
|
86
|
-
* name-based, it also reports *why* each pair matched and downgrades a match
|
|
87
|
-
* that rests solely on the package prefix instead of silently counting it as
|
|
88
|
-
* strong evidence.
|
|
89
|
-
*/
|
|
90
|
-
export function checkTdd(changedPaths: string[], testChangedPaths: string[] = []): TddCheckpoint {
|
|
91
|
-
const all = [...new Set([...changedPaths, ...testChangedPaths])].map((path) =>
|
|
92
|
-
path.replace(/\\/g, '/'),
|
|
93
|
-
);
|
|
94
|
-
const sourceChanges = all.filter((path) => path.endsWith('.py') && !isTestFile(path));
|
|
95
|
-
const testChanges = all.filter((path) => path.endsWith('.py') && isTestFile(path));
|
|
96
|
-
|
|
97
|
-
// `fastapi_server/db/database.py` and `fastapi_server/tests/unit/test_db.py`
|
|
98
|
-
// share `fastapi` and `server` with every other file in the project, so those
|
|
99
|
-
// tokens must not be treated as evidence of a real relationship.
|
|
100
|
-
const ubiquitous = commonPrefixTokens([...sourceChanges, ...testChanges]);
|
|
101
|
-
|
|
102
|
-
const associations: TddAssociation[] = [];
|
|
103
|
-
for (const source of sourceChanges) {
|
|
104
|
-
for (const test of testChanges) {
|
|
105
|
-
const shared = sharedTokens(source, test);
|
|
106
|
-
if (shared.length === 0) continue;
|
|
107
|
-
const discriminating = shared.filter((token) => !ubiquitous.has(token));
|
|
108
|
-
associations.push({
|
|
109
|
-
source,
|
|
110
|
-
test,
|
|
111
|
-
sharedTokens: shared,
|
|
112
|
-
strength: discriminating.length > 0 ? 'module' : 'package',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const strong = associations.some((entry) => entry.strength === 'module');
|
|
118
|
-
const weakAssociation = associations.length > 0 && !strong;
|
|
119
|
-
|
|
120
|
-
const reasons: string[] = [];
|
|
121
|
-
if (sourceChanges.length > 0 && testChanges.length === 0) {
|
|
122
|
-
reasons.push('Production Python files changed without any test file change.');
|
|
123
|
-
} else if (sourceChanges.length > 0 && associations.length === 0) {
|
|
124
|
-
reasons.push('Changed test files do not appear related to the changed production modules.');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
ok: reasons.length === 0,
|
|
129
|
-
reasons,
|
|
130
|
-
sourceChanges,
|
|
131
|
-
testChanges,
|
|
132
|
-
associations,
|
|
133
|
-
weakAssociation,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
26
|
+
export type { TddAssociation, TddCheckpoint } from 'pi-helper-core';
|