knots-test-complexity 1.11.0__py3-none-win_amd64.whl
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.
- knots_test_complexity-1.11.0.data/scripts/knots-test-complexity.exe +0 -0
- knots_test_complexity-1.11.0.dist-info/METADATA +447 -0
- knots_test_complexity-1.11.0.dist-info/RECORD +5 -0
- knots_test_complexity-1.11.0.dist-info/WHEEL +4 -0
- knots_test_complexity-1.11.0.dist-info/sboms/knots-test-complexity.cyclonedx.json +1846 -0
|
Binary file
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: knots-test-complexity
|
|
3
|
+
Version: 1.11.0
|
|
4
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
9
|
+
Classifier: Topic :: Software Development :: Testing
|
|
10
|
+
Summary: Test quality analyzer — validates unit tests have sufficient complexity and boundary coverage (companion to knots)
|
|
11
|
+
Keywords: testing,complexity,static-analysis,coverage
|
|
12
|
+
Author: Brandon Arrendondo
|
|
13
|
+
License: MIT
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
16
|
+
Project-URL: Homepage, https://github.com/brandon-arrendondo/knots
|
|
17
|
+
Project-URL: Repository, https://github.com/brandon-arrendondo/knots
|
|
18
|
+
|
|
19
|
+
# knots-test-complexity
|
|
20
|
+
|
|
21
|
+
A Rust-based test quality analyzer for C projects that validates unit tests have sufficient complexity to thoroughly exercise source code.
|
|
22
|
+
|
|
23
|
+
## Motivation
|
|
24
|
+
|
|
25
|
+
Traditional code coverage metrics (line, branch, function) can be misleading. A test can achieve 100% branch coverage with simple assertions while missing critical edge cases like:
|
|
26
|
+
|
|
27
|
+
- **Overflow scenarios**: `uint16_t` timer wrapping at 65535
|
|
28
|
+
- **Boundary conditions**: Off-by-one errors at array bounds
|
|
29
|
+
- **State transitions**: Complex state machines with temporal dependencies
|
|
30
|
+
- **Error paths**: Multiple error conditions not all tested
|
|
31
|
+
|
|
32
|
+
This tool enforces that tests have sufficient **cyclomatic complexity** to exercise all logical paths and **boundary value testing** to catch edge cases.
|
|
33
|
+
|
|
34
|
+
## Philosophy
|
|
35
|
+
|
|
36
|
+
> "A test with lower complexity than its source code is likely not testing all scenarios."
|
|
37
|
+
|
|
38
|
+
### Example: The Overflow Bug
|
|
39
|
+
|
|
40
|
+
```c
|
|
41
|
+
// Source: Cyclomatic Complexity = 2
|
|
42
|
+
uint16_t timer_ms = 0;
|
|
43
|
+
|
|
44
|
+
void periodic_1ms() {
|
|
45
|
+
timer_ms++; // Overflows at 65535!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bool is_timeout(uint16_t start_ms, uint16_t duration_ms) {
|
|
49
|
+
return (timer_ms - start_ms) >= duration_ms;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Traditional Coverage** (100% line, 100% branch):
|
|
54
|
+
```c
|
|
55
|
+
void test_timeout() {
|
|
56
|
+
timer_ms = 0;
|
|
57
|
+
TEST_ASSERT_TRUE(is_timeout(0, 100)); // Happy path
|
|
58
|
+
TEST_ASSERT_FALSE(is_timeout(0, 1)); // Boundary
|
|
59
|
+
}
|
|
60
|
+
// PASSES coverage but MISSES overflow bug!
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Tool Enforcement** - Would detect:
|
|
64
|
+
- Test complexity (2) barely meets source complexity (2)
|
|
65
|
+
- Missing boundary tests: 0, 65535, wrap-around scenarios
|
|
66
|
+
- Missing state variation: timer_ms at different values
|
|
67
|
+
|
|
68
|
+
**Better Tests** (Higher Complexity):
|
|
69
|
+
```c
|
|
70
|
+
void test_timeout_boundaries() {
|
|
71
|
+
// Boundary: timer at 0
|
|
72
|
+
timer_ms = 0;
|
|
73
|
+
TEST_ASSERT_TRUE(is_timeout(0, 100));
|
|
74
|
+
|
|
75
|
+
// Boundary: timer near max
|
|
76
|
+
timer_ms = 65530;
|
|
77
|
+
TEST_ASSERT_TRUE(is_timeout(65520, 100));
|
|
78
|
+
|
|
79
|
+
// CRITICAL: Overflow scenario
|
|
80
|
+
timer_ms = 5; // Wrapped from 65535
|
|
81
|
+
TEST_ASSERT_TRUE(is_timeout(65530, 100)); // Catches overflow!
|
|
82
|
+
|
|
83
|
+
// Multiple start/duration combinations
|
|
84
|
+
for (int i = 0; i < 5; i++) {
|
|
85
|
+
test_scenario(scenarios[i]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Higher complexity test catches the bug!
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Features
|
|
92
|
+
|
|
93
|
+
### Core Metrics
|
|
94
|
+
|
|
95
|
+
1. **Test-to-Source Complexity Ratio**
|
|
96
|
+
- Aggregate cyclomatic complexity of all test functions
|
|
97
|
+
- Compare to aggregate complexity of source functions
|
|
98
|
+
- Default threshold: 70% (configurable)
|
|
99
|
+
|
|
100
|
+
2. **Boundary Value Detection**
|
|
101
|
+
- Detects integer types: `uint8_t`, `uint16_t`, `uint32_t`, `int8_t`, etc.
|
|
102
|
+
- Identifies range checks: `if (x > MAX)`, `if (x < MIN)`
|
|
103
|
+
- Counts required boundary tests
|
|
104
|
+
- Validates tests cover: MIN, MIN-1, MAX, MAX+1
|
|
105
|
+
|
|
106
|
+
3. **State Variable Tracking** (Future Enhancement)
|
|
107
|
+
- Identifies `static`, `volatile`, and global variables
|
|
108
|
+
- Requires multiple test scenarios per state variable
|
|
109
|
+
- Validates state transitions are tested
|
|
110
|
+
|
|
111
|
+
### Output Modes
|
|
112
|
+
|
|
113
|
+
- **Warning Mode** (default): Reports violations but doesn't fail pre-commit
|
|
114
|
+
- **Error Mode**: Fails pre-commit on violations
|
|
115
|
+
- **Verbose Mode**: Shows detailed per-function complexity breakdown
|
|
116
|
+
|
|
117
|
+
## Building
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cargo build --release --workspace
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The binary will be at `target/release/knots-test-complexity`
|
|
124
|
+
|
|
125
|
+
## Installation
|
|
126
|
+
|
|
127
|
+
### From Source
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
cd knots
|
|
131
|
+
cargo build --release --workspace
|
|
132
|
+
# Binary is at target/release/knots-test-complexity
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Add to your PATH or copy to a location in your PATH.
|
|
136
|
+
|
|
137
|
+
### Pre-Commit Integration
|
|
138
|
+
|
|
139
|
+
Add to your project's `.pre-commit-config.yaml`:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
repos:
|
|
143
|
+
- repo: https://github.com/brandon-arrendondo/knots
|
|
144
|
+
rev: v0.3.0 # Use specific version tag
|
|
145
|
+
hooks:
|
|
146
|
+
- id: test-complexity
|
|
147
|
+
args:
|
|
148
|
+
- --threshold=0.70
|
|
149
|
+
- --boundary-threshold=0.80
|
|
150
|
+
- --level=warn
|
|
151
|
+
- --framework=ceedling
|
|
152
|
+
- --test-dir=Test
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Configuration Options:**
|
|
156
|
+
|
|
157
|
+
- `--threshold=0.70`: Minimum test-to-source complexity ratio (default: 0.70 = 70%)
|
|
158
|
+
- `--level=warn`: Enforcement level (`warn` or `error`, default: `warn`)
|
|
159
|
+
- `--no-check-boundaries`: Disable boundary value detection (enabled by default)
|
|
160
|
+
- `--verbose`: Show detailed per-file analysis
|
|
161
|
+
|
|
162
|
+
**Example: Strict Enforcement**
|
|
163
|
+
```yaml
|
|
164
|
+
args:
|
|
165
|
+
- --threshold=0.80
|
|
166
|
+
- --level=error
|
|
167
|
+
- --verbose
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Example: Warning Only (No Boundaries)**
|
|
171
|
+
```yaml
|
|
172
|
+
args:
|
|
173
|
+
- --threshold=0.70
|
|
174
|
+
- --level=warn
|
|
175
|
+
- --no-check-boundaries
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Usage
|
|
179
|
+
|
|
180
|
+
### Command Line
|
|
181
|
+
|
|
182
|
+
Analyze a test file and its corresponding source:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
knots-test-complexity Test/test_battery_service.c Core/Src/modules/battery_service/battery_service.c
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With verbose output:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
knots-test-complexity -v Test/test_battery_service.c Core/Src/modules/battery_service/battery_service.c
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
With custom thresholds:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
knots-test-complexity \
|
|
198
|
+
--threshold=0.70 \
|
|
199
|
+
--boundary-threshold=0.80 \
|
|
200
|
+
--level=error \
|
|
201
|
+
Test/test_timer.c Core/Src/timer.c
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Output Example
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
Analyzing Test Quality: test_battery_service.c
|
|
208
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
209
|
+
|
|
210
|
+
Source File: battery_service.c
|
|
211
|
+
Functions: 15
|
|
212
|
+
Total Cyclomatic Complexity: 87
|
|
213
|
+
Boundary Values Detected: 12
|
|
214
|
+
- uint8_t variables: 4 (boundaries: 0, 255)
|
|
215
|
+
- Range checks: 8 (if (x > MAX), etc.)
|
|
216
|
+
|
|
217
|
+
Test File: test_battery_service.c
|
|
218
|
+
Functions: 58
|
|
219
|
+
Total Cyclomatic Complexity: 74
|
|
220
|
+
Boundary Tests Found: 15
|
|
221
|
+
|
|
222
|
+
Complexity Analysis:
|
|
223
|
+
Test/Source Ratio: 85% ✓ (threshold: 70%)
|
|
224
|
+
Test Complexity: 74
|
|
225
|
+
Source Complexity: 87
|
|
226
|
+
Ratio: 74/87 = 0.85
|
|
227
|
+
|
|
228
|
+
Boundary Analysis:
|
|
229
|
+
Required Boundary Tests: 12
|
|
230
|
+
Found Boundary Tests: 15 ✓
|
|
231
|
+
Coverage: 125%
|
|
232
|
+
|
|
233
|
+
Result: ✓ PASS
|
|
234
|
+
|
|
235
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Failure Example
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
Analyzing Test Quality: test_lin_comm_service.c
|
|
242
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
|
+
|
|
244
|
+
Source File: lin_comm_service.c
|
|
245
|
+
Functions: 8
|
|
246
|
+
Total Cyclomatic Complexity: 54
|
|
247
|
+
Boundary Values Detected: 8
|
|
248
|
+
|
|
249
|
+
Test File: test_lin_comm_service.c
|
|
250
|
+
Functions: 12
|
|
251
|
+
Total Cyclomatic Complexity: 28
|
|
252
|
+
Boundary Tests Found: 3
|
|
253
|
+
|
|
254
|
+
Complexity Analysis:
|
|
255
|
+
Test/Source Ratio: 52% ✗ (threshold: 70%)
|
|
256
|
+
Test Complexity: 28
|
|
257
|
+
Source Complexity: 54
|
|
258
|
+
Ratio: 28/54 = 0.52
|
|
259
|
+
|
|
260
|
+
Boundary Analysis:
|
|
261
|
+
Required Boundary Tests: 8
|
|
262
|
+
Found Boundary Tests: 3 ✗
|
|
263
|
+
Missing Boundaries:
|
|
264
|
+
- rxByteCounter: 0, 3, 11, 12 (RX_HEADER_SIZE boundaries)
|
|
265
|
+
- rxFrameId: 0, 0x3F (FRAME_MASK boundary)
|
|
266
|
+
|
|
267
|
+
Recommendations:
|
|
268
|
+
1. Add tests for edge cases and error paths
|
|
269
|
+
2. Test boundary conditions: 0, max values, overflow
|
|
270
|
+
3. Add state transition tests (4 static variables detected)
|
|
271
|
+
4. Consider parametrized tests or loops in test code
|
|
272
|
+
|
|
273
|
+
Result: ✗ FAIL (--level=error)
|
|
274
|
+
|
|
275
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Algorithm Details
|
|
279
|
+
|
|
280
|
+
### 1. Complexity Ratio Calculation
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
For each test file:
|
|
284
|
+
1. Parse test file with tree-sitter-c
|
|
285
|
+
2. Calculate cyclomatic complexity for all test functions
|
|
286
|
+
- Count: if, while, for, switch, &&, ||, ?:
|
|
287
|
+
- Include test helper functions
|
|
288
|
+
3. Sum total test complexity
|
|
289
|
+
|
|
290
|
+
4. Find corresponding source file
|
|
291
|
+
5. Calculate cyclomatic complexity for all source functions
|
|
292
|
+
6. Sum total source complexity
|
|
293
|
+
|
|
294
|
+
7. Calculate ratio = test_complexity / source_complexity
|
|
295
|
+
8. Compare ratio >= threshold (default 70%)
|
|
296
|
+
|
|
297
|
+
9. Report: PASS or FAIL with recommendations
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 2. Boundary Value Detection
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
For source file:
|
|
304
|
+
1. Find all integer type declarations
|
|
305
|
+
- uint8_t → boundaries: 0, 255
|
|
306
|
+
- uint16_t → boundaries: 0, 65535
|
|
307
|
+
- int8_t → boundaries: -128, 127
|
|
308
|
+
|
|
309
|
+
2. Find all range checks
|
|
310
|
+
- if (x > MAX) → test MAX, MAX+1
|
|
311
|
+
- if (x < MIN) → test MIN-1, MIN
|
|
312
|
+
- if (x >= threshold) → test threshold-1, threshold
|
|
313
|
+
|
|
314
|
+
3. Count required boundary tests
|
|
315
|
+
|
|
316
|
+
For test file:
|
|
317
|
+
1. Find all numeric literals in assertions
|
|
318
|
+
2. Match literals to source boundaries
|
|
319
|
+
3. Count covered boundaries
|
|
320
|
+
|
|
321
|
+
4. Report: boundary_coverage = found / required
|
|
322
|
+
5. Warn if coverage < 100%
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### 3. Test Helper Function Handling
|
|
326
|
+
|
|
327
|
+
Test helpers ARE included in complexity calculation:
|
|
328
|
+
|
|
329
|
+
```c
|
|
330
|
+
// Helper complexity counts!
|
|
331
|
+
void simulate_frame(uint8_t id) {
|
|
332
|
+
setup_mocks();
|
|
333
|
+
if (id == SPECIAL) { // +1 complexity
|
|
334
|
+
special_handling();
|
|
335
|
+
}
|
|
336
|
+
verify_results();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Test also counts
|
|
340
|
+
void test_multiple_frames() {
|
|
341
|
+
for (int i = 0; i < 10; i++) { // +1 complexity
|
|
342
|
+
simulate_frame(i); // Helper complexity included
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Total test complexity = 2 (loop + helper's if)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
This encourages well-structured tests with reusable helpers.
|
|
349
|
+
|
|
350
|
+
## Integration with knots
|
|
351
|
+
|
|
352
|
+
`knots-test-complexity` complements `knots`:
|
|
353
|
+
|
|
354
|
+
| Tool | Purpose | Applied To | Metric |
|
|
355
|
+
|------|---------|------------|--------|
|
|
356
|
+
| **knots** | Source code quality | Production code (`.c`, `.h`) | McCabe & Cognitive complexity per function |
|
|
357
|
+
| **knots-test-complexity** | Test quality | Test code (`test_*.c`) | Aggregate complexity ratio & boundary coverage |
|
|
358
|
+
|
|
359
|
+
**Example Combined Workflow:**
|
|
360
|
+
|
|
361
|
+
```yaml
|
|
362
|
+
- repo: local
|
|
363
|
+
hooks:
|
|
364
|
+
# Check source code complexity (per-function limits)
|
|
365
|
+
- id: knots
|
|
366
|
+
name: Code Complexity Check
|
|
367
|
+
entry: hooks/pre-commit-wrapper.sh
|
|
368
|
+
language: script
|
|
369
|
+
files: \.(c|h)$
|
|
370
|
+
exclude: ^Test/
|
|
371
|
+
args: [--mccabe-threshold=15, --cognitive-threshold=15]
|
|
372
|
+
|
|
373
|
+
# Check test quality (aggregate complexity ratio)
|
|
374
|
+
- id: test-complexity
|
|
375
|
+
name: Test Quality Check
|
|
376
|
+
entry: hooks/test-complexity-wrapper.sh
|
|
377
|
+
language: script
|
|
378
|
+
files: ^Test/test_.*\.c$
|
|
379
|
+
args: [--threshold=70, --level=warn]
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Future: Unified Tool
|
|
383
|
+
|
|
384
|
+
A future enhancement could merge both tools:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
# Unified complexity tool
|
|
388
|
+
complexity-check --source <file.c> --test <test_file.c>
|
|
389
|
+
--source-mccabe-max=15
|
|
390
|
+
--source-cognitive-max=15
|
|
391
|
+
--test-ratio-min=0.70
|
|
392
|
+
--check-boundaries
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Dependencies
|
|
396
|
+
|
|
397
|
+
```toml
|
|
398
|
+
[dependencies]
|
|
399
|
+
tree-sitter = "0.22"
|
|
400
|
+
tree-sitter-c = "0.21"
|
|
401
|
+
anyhow = "1.0"
|
|
402
|
+
clap = { version = "4.5", features = ["derive"] }
|
|
403
|
+
regex = "1.10"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Project Structure
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
knots-test-complexity/
|
|
410
|
+
├── Cargo.toml # Rust project manifest
|
|
411
|
+
├── README.md # This file
|
|
412
|
+
├── src/
|
|
413
|
+
│ ├── main.rs # CLI entry point
|
|
414
|
+
│ ├── analyzer.rs # Test quality analyzer
|
|
415
|
+
│ ├── boundary.rs # Boundary value detector
|
|
416
|
+
│ └── reporter.rs # Output formatting
|
|
417
|
+
└── examples/
|
|
418
|
+
├── test_timer_good.c # Example: sufficient complexity
|
|
419
|
+
├── test_timer_bad.c # Example: insufficient complexity
|
|
420
|
+
└── README.md # Example documentation
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Testing
|
|
424
|
+
|
|
425
|
+
Run the test suite:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
cargo test
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Run with verbose output:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
cargo test -- --nocapture
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
MIT License. See LICENSE file.
|
|
440
|
+
|
|
441
|
+
## See Also
|
|
442
|
+
|
|
443
|
+
- **knots**: Source code complexity analyzer
|
|
444
|
+
- **pmccabe**: Industry-standard McCabe complexity tool (validation reference)
|
|
445
|
+
- **Cognitive Complexity**: [SonarSource specification](https://www.sonarsource.com/resources/cognitive-complexity/)
|
|
446
|
+
- **Mutation Testing**: Alternative approach for test quality (future consideration)
|
|
447
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
knots_test_complexity-1.11.0.data/scripts/knots-test-complexity.exe,sha256=nkfqNx9_E6JmI1qr0Cq1RSaBM5hENFBViw7URfFpiA8,11164160
|
|
2
|
+
knots_test_complexity-1.11.0.dist-info/METADATA,sha256=RXJcV_eP_6iAWPkhk_VIoyqdFxtB73JQq_1ZGl01U00,12908
|
|
3
|
+
knots_test_complexity-1.11.0.dist-info/WHEEL,sha256=2zDlIYIdD4m4N3p5DVEG3iJhGLdhsBQgdH-FqVkAur8,94
|
|
4
|
+
knots_test_complexity-1.11.0.dist-info/sboms/knots-test-complexity.cyclonedx.json,sha256=2T8Oevq-Nha23xUb0Qco6Zu6pWwqTReCWUwJvuADwhg,57590
|
|
5
|
+
knots_test_complexity-1.11.0.dist-info/RECORD,,
|