knots-test-complexity
A Rust-based test quality analyzer for C projects that validates unit tests have sufficient complexity to thoroughly exercise source code.
Motivation
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:
- Overflow scenarios:
uint16_ttimer wrapping at 65535 - Boundary conditions: Off-by-one errors at array bounds
- State transitions: Complex state machines with temporal dependencies
- Error paths: Multiple error conditions not all tested
This tool enforces that tests have sufficient cyclomatic complexity to exercise all logical paths and boundary value testing to catch edge cases.
Philosophy
"A test with lower complexity than its source code is likely not testing all scenarios."
Example: The Overflow Bug
// Source: Cyclomatic Complexity = 2
uint16_t timer_ms = 0;
void periodic_1ms() {
timer_ms++; // Overflows at 65535!
}
bool is_timeout(uint16_t start_ms, uint16_t duration_ms) {
return (timer_ms - start_ms) >= duration_ms;
}
Traditional Coverage (100% line, 100% branch):
void test_timeout() {
timer_ms = 0;
TEST_ASSERT_TRUE(is_timeout(0, 100)); // Happy path
TEST_ASSERT_FALSE(is_timeout(0, 1)); // Boundary
}
// PASSES coverage but MISSES overflow bug!
Tool Enforcement - Would detect:
- Test complexity (2) barely meets source complexity (2)
- Missing boundary tests: 0, 65535, wrap-around scenarios
- Missing state variation: timer_ms at different values
Better Tests (Higher Complexity):
void test_timeout_boundaries() {
// Boundary: timer at 0
timer_ms = 0;
TEST_ASSERT_TRUE(is_timeout(0, 100));
// Boundary: timer near max
timer_ms = 65530;
TEST_ASSERT_TRUE(is_timeout(65520, 100));
// CRITICAL: Overflow scenario
timer_ms = 5; // Wrapped from 65535
TEST_ASSERT_TRUE(is_timeout(65530, 100)); // Catches overflow!
// Multiple start/duration combinations
for (int i = 0; i < 5; i++) {
test_scenario(scenarios[i]);
}
}
// Higher complexity test catches the bug!
Features
Core Metrics
-
Test-to-Source Complexity Ratio
- Aggregate cyclomatic complexity of all test functions
- Compare to aggregate complexity of source functions
- Default threshold: 70% (configurable)
-
Boundary Value Detection
- Detects integer types:
uint8_t,uint16_t,uint32_t,int8_t, etc. - Identifies range checks:
if (x > MAX),if (x < MIN) - Counts required boundary tests
- Validates tests cover: MIN, MIN-1, MAX, MAX+1
- Detects integer types:
-
State Variable Tracking (Future Enhancement)
- Identifies
static,volatile, and global variables - Requires multiple test scenarios per state variable
- Validates state transitions are tested
- Identifies
Output Modes
- Warning Mode (default): Reports violations but doesn't fail pre-commit
- Error Mode: Fails pre-commit on violations
- Verbose Mode: Shows detailed per-function complexity breakdown
Building
cargo build --release --workspace
The binary will be at target/release/knots-test-complexity
Installation
From Source
cd knots
cargo build --release --workspace
# Binary is at target/release/knots-test-complexity
Add to your PATH or copy to a location in your PATH.
Pre-Commit Integration
Add to your project's .pre-commit-config.yaml:
repos:
- repo: https://github.com/brandon-arrendondo/knots
rev: v0.3.0 # Use specific version tag
hooks:
- id: test-complexity
args:
- --threshold=0.70
- --boundary-threshold=0.80
- --level=warn
- --framework=ceedling
- --test-dir=Test
Configuration Options:
--threshold=0.70: Minimum test-to-source complexity ratio (default: 0.70 = 70%)--level=warn: Enforcement level (warnorerror, default:warn)--no-check-boundaries: Disable boundary value detection (enabled by default)--verbose: Show detailed per-file analysis
Example: Strict Enforcement
args:
- --threshold=0.80
- --level=error
- --verbose
Example: Warning Only (No Boundaries)
args:
- --threshold=0.70
- --level=warn
- --no-check-boundaries
Usage
Command Line
Analyze a test file and its corresponding source:
knots-test-complexity Test/test_battery_service.c Core/Src/modules/battery_service/battery_service.c
With verbose output:
knots-test-complexity -v Test/test_battery_service.c Core/Src/modules/battery_service/battery_service.c
With custom thresholds:
knots-test-complexity \
--threshold=0.70 \
--boundary-threshold=0.80 \
--level=error \
Test/test_timer.c Core/Src/timer.c
Output Example
Analyzing Test Quality: test_battery_service.c
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Source File: battery_service.c
Functions: 15
Total Cyclomatic Complexity: 87
Boundary Values Detected: 12
- uint8_t variables: 4 (boundaries: 0, 255)
- Range checks: 8 (if (x > MAX), etc.)
Test File: test_battery_service.c
Functions: 58
Total Cyclomatic Complexity: 74
Boundary Tests Found: 15
Complexity Analysis:
Test/Source Ratio: 85% ✓ (threshold: 70%)
Test Complexity: 74
Source Complexity: 87
Ratio: 74/87 = 0.85
Boundary Analysis:
Required Boundary Tests: 12
Found Boundary Tests: 15 ✓
Coverage: 125%
Result: ✓ PASS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Failure Example
Analyzing Test Quality: test_lin_comm_service.c
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Source File: lin_comm_service.c
Functions: 8
Total Cyclomatic Complexity: 54
Boundary Values Detected: 8
Test File: test_lin_comm_service.c
Functions: 12
Total Cyclomatic Complexity: 28
Boundary Tests Found: 3
Complexity Analysis:
Test/Source Ratio: 52% ✗ (threshold: 70%)
Test Complexity: 28
Source Complexity: 54
Ratio: 28/54 = 0.52
Boundary Analysis:
Required Boundary Tests: 8
Found Boundary Tests: 3 ✗
Missing Boundaries:
- rxByteCounter: 0, 3, 11, 12 (RX_HEADER_SIZE boundaries)
- rxFrameId: 0, 0x3F (FRAME_MASK boundary)
Recommendations:
1. Add tests for edge cases and error paths
2. Test boundary conditions: 0, max values, overflow
3. Add state transition tests (4 static variables detected)
4. Consider parametrized tests or loops in test code
Result: ✗ FAIL (--level=error)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Algorithm Details
1. Complexity Ratio Calculation
For each test file:
1. Parse test file with tree-sitter-c
2. Calculate cyclomatic complexity for all test functions
- Count: if, while, for, switch, &&, ||, ?:
- Include test helper functions
3. Sum total test complexity
4. Find corresponding source file
5. Calculate cyclomatic complexity for all source functions
6. Sum total source complexity
7. Calculate ratio = test_complexity / source_complexity
8. Compare ratio >= threshold (default 70%)
9. Report: PASS or FAIL with recommendations
2. Boundary Value Detection
For source file:
1. Find all integer type declarations
- uint8_t → boundaries: 0, 255
- uint16_t → boundaries: 0, 65535
- int8_t → boundaries: -128, 127
2. Find all range checks
- if (x > MAX) → test MAX, MAX+1
- if (x < MIN) → test MIN-1, MIN
- if (x >= threshold) → test threshold-1, threshold
3. Count required boundary tests
For test file:
1. Find all numeric literals in assertions
2. Match literals to source boundaries
3. Count covered boundaries
4. Report: boundary_coverage = found / required
5. Warn if coverage < 100%
3. Test Helper Function Handling
Test helpers ARE included in complexity calculation:
// Helper complexity counts!
void simulate_frame(uint8_t id) {
setup_mocks();
if (id == SPECIAL) { // +1 complexity
special_handling();
}
verify_results();
}
// Test also counts
void test_multiple_frames() {
for (int i = 0; i < 10; i++) { // +1 complexity
simulate_frame(i); // Helper complexity included
}
}
// Total test complexity = 2 (loop + helper's if)
This encourages well-structured tests with reusable helpers.
Integration with knots
knots-test-complexity complements knots:
| Tool | Purpose | Applied To | Metric |
|---|---|---|---|
| knots | Source code quality | Production code (.c, .h) |
McCabe & Cognitive complexity per function |
| knots-test-complexity | Test quality | Test code (test_*.c) |
Aggregate complexity ratio & boundary coverage |
Example Combined Workflow:
- repo: local
hooks:
# Check source code complexity (per-function limits)
- id: knots
name: Code Complexity Check
entry: hooks/pre-commit-wrapper.sh
language: script
files: \.(c|h)$
exclude: ^Test/
args: [--mccabe-threshold=15, --cognitive-threshold=15]
# Check test quality (aggregate complexity ratio)
- id: test-complexity
name: Test Quality Check
entry: hooks/test-complexity-wrapper.sh
language: script
files: ^Test/test_.*\.c$
args: [--threshold=70, --level=warn]
Future: Unified Tool
A future enhancement could merge both tools:
# Unified complexity tool
complexity-check --source <file.c> --test <test_file.c>
--source-mccabe-max=15
--source-cognitive-max=15
--test-ratio-min=0.70
--check-boundaries
Dependencies
[dependencies]
tree-sitter = "0.22"
tree-sitter-c = "0.21"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
regex = "1.10"
Project Structure
knots-test-complexity/
├── Cargo.toml # Rust project manifest
├── README.md # This file
├── src/
│ ├── main.rs # CLI entry point
│ ├── analyzer.rs # Test quality analyzer
│ ├── boundary.rs # Boundary value detector
│ └── reporter.rs # Output formatting
└── examples/
├── test_timer_good.c # Example: sufficient complexity
├── test_timer_bad.c # Example: insufficient complexity
└── README.md # Example documentation
Testing
Run the test suite:
cargo test
Run with verbose output:
cargo test -- --nocapture
License
MIT License. See LICENSE file.
See Also
- knots: Source code complexity analyzer
- pmccabe: Industry-standard McCabe complexity tool (validation reference)
- Cognitive Complexity: SonarSource specification
- Mutation Testing: Alternative approach for test quality (future consideration)
Metadata
Release files for knots-test-complexity 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| knots_test_complexity-2.0.0.tar.gz | 336.6 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| knots_test_complexity-2.0.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| knots_test_complexity-2.0.0-py3-none-musllinux_1_2_x86_64.whl | Python 3 | none | Linux musl 1.2+ x86-64 | Details |
| knots_test_complexity-2.0.0-py3-none-musllinux_1_2_aarch64.whl | Python 3 | none | Linux musl 1.2+ ARM64 | Details |
| knots_test_complexity-2.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| knots_test_complexity-2.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| knots_test_complexity-2.0.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| knots_test_complexity-2.0.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 31.3 MB
Release files / knots_test_complexity-2.0.0.tar.gz
| Download URL | knots_test_complexity-2.0.0.tar.gz |
|---|---|
| Size | 336.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
21ce034945b725511c3501d99941295262c49d3f0dac6ace2eecab36fa0321f9
|
|
BLAKE2b-256 checksum How to use checksums |
f06d28b39c4f1bc7274190b76d820c416659ec3171ea4d12b9b0db8bfa616d34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-win_amd64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-win_amd64.whl |
|---|---|
| Size | 4.2 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
ddbf043b2767545ab2cf64f863fe20812cffa7676bc614e953055cea6127dfa9
|
|
BLAKE2b-256 checksum How to use checksums |
f3076f290991389acd21825c3c81bc68e00ebc87c71c193887a81435e0401ca5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 4.6 MB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
7f8455721b9a6810599ef16491c5fccfa9617e33da34a02bc931688f3ccf9f18
|
|
BLAKE2b-256 checksum How to use checksums |
c495de9a539cc5bd1cdf5f6d9c429618576c236f093ea95b1cf81cdc3bb8ad4c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-musllinux_1_2_aarch64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 4.4 MB |
| Tags | Linux musl 1.2+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
45370761664425bd57c628736bd9afa84d3de1d7c4d664221dc5505862bade18
|
|
BLAKE2b-256 checksum How to use checksums |
fef5bb59ef69fac7ebe863a4b4d483afd07b14af6928b96b2d44df8476ea7d10
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.5 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
404eab94823a8aabba49fad500562570681391d1cd819ddfb32cf33a3835baa5
|
|
BLAKE2b-256 checksum How to use checksums |
ddea057d05f9f4f1af9739b3c97a8d3a20471ed57c96460bbb36c184a5bb78ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 4.5 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
3ae1bb0992dbc9de8c17feeee30e0be22c69f69cb483ffc126a7ab0bf48c6539
|
|
BLAKE2b-256 checksum How to use checksums |
9641448b7903f5392397d7e77846161da8e2f4f6c82f641cefba4359b8794cf8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-macosx_11_0_arm64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 4.5 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
8d7e6ac1943341eaf4513847be8ff20625f732b5e2789c4e79099c786eb6f76c
|
|
BLAKE2b-256 checksum How to use checksums |
43e981891355e120580d67734bce26f3120796919e733c8573d46879bd3b81f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / knots_test_complexity-2.0.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | knots_test_complexity-2.0.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 4.4 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
2656680fbe55c9300bb9ae21b7fbbc8eda62c5e30f441d7237bd72d26aae965a
|
|
BLAKE2b-256 checksum How to use checksums |
f5e7fd7a9a29eb9655ecd62a4187ff95b5f2ce708783b3c5be7b5fdefb9a9b88
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for knots-test-complexity 2.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 8 files in this release have been uploaded with provenance metadata.
Attestation for knots_test_complexity-2.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.