Skip to content

Commit 1565c46

Browse files
committed
feat(validator): add optional constraints parameter to analyzeCoverage
Previously, analyzing a constrained test suite reported constraint-impossible tuples as uncovered, making it impossible to distinguish real coverage gaps from impossible combinations. Analyzing the output of a constrained generate() call could never yield coverageRatio === 1.0 even when coverage was complete. When constraints are supplied to analyzeCoverage, tuples that violate any constraint are excluded from the coverage universe entirely — they do not appear in totalTuples, coveredTuples, or uncovered. This matches ExcludeInvalidTuples semantics used in the generator, so a generated test suite analyzed against the same model + constraints always reports full coverage. Changes: - src/validator/coverage_validator.{h,cpp}: add optional constraints vector to ValidateCoverage; exclude invalid tuples before scoring - src/wasm/bindings.cpp: expose constraints arg in WASM binding - js/types.ts: add constraints field to AnalyzeCoverageOptions - js/index.ts, js/pure/index.ts: forward constraints through JS APIs - src/ts/validator/coverage-validator.ts: pure TS validator updated - docs/en/js-api.md, docs/ja/js-api.md, README.npm.md: document the new parameter Tests: 240 C++ (ctest) + 536 JS (vitest) all pass Version: 1.0.2 → 1.1.0 (backwards-compatible addition)
1 parent ae8fe3a commit 1565c46

20 files changed

Lines changed: 248 additions & 49 deletions

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.16)
2-
project(coverwise VERSION 1.0.2 LANGUAGES CXX)
2+
project(coverwise VERSION 1.1.0 LANGUAGES CXX)
33

44
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)

README.npm.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const result = cw.generate({ parameters: [/* ... */] });
8989
| Method | Description |
9090
|--------|-------------|
9191
| `Coverwise.create()` | Create instance (loads WASM once) |
92-
| `cw.analyzeCoverage(params, tests, strength?)` | Measure t-wise coverage, list uncovered combinations |
92+
| `cw.analyzeCoverage(params, tests, strength?, constraints?)` | Measure t-wise coverage, list uncovered combinations (constraint-excluded tuples are removed from the universe) |
9393
| `cw.extendTests(existing, input)` | Add only the tests needed to close coverage gaps |
9494
| `cw.generate(input)` | Generate minimal covering array from scratch |
9595
| `cw.estimateModel(input)` | Preview model statistics |

docs/en/js-api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ interface ClassCoverage {
185185
}
186186
```
187187

188-
## `analyzeCoverage(parameters, tests, strength?)`
188+
## `analyzeCoverage(parameters, tests, strength?, constraints?)`
189189

190190
Analyze the t-wise coverage of an existing test suite. Independent of the generatorvalidates any set of tests.
191191

@@ -194,9 +194,12 @@ function analyzeCoverage(
194194
parameters: Parameter[],
195195
tests: TestCase[],
196196
strength?: number, // Default: 2
197+
constraints?: string[], // Optional constraint DSL strings
197198
): CoverageReport
198199
```
199200

201+
When `constraints` is supplied, tuples that violate any constraint are **removed from the coverage universe entirely**they do not count toward `totalTuples`, `coveredTuples`, or `uncovered`. This matches the generator's semantics, so analyzing a generated suite always yields `coverageRatio === 1.0`.
202+
200203
### CoverageReport
201204

202205
```typescript

docs/ja/js-api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ interface ClassCoverage {
185185
}
186186
```
187187

188-
## `analyzeCoverage(parameters, tests, strength?)`
188+
## `analyzeCoverage(parameters, tests, strength?, constraints?)`
189189

190190
既存テストスイートの t-wise カバレッジを分析します。ジェネレータとは独立しており、任意のテストセットを検証できます。
191191

@@ -194,9 +194,12 @@ function analyzeCoverage(
194194
parameters: Parameter[],
195195
tests: TestCase[],
196196
strength?: number, // デフォルト: 2
197+
constraints?: string[], // 制約 DSL 文字列(省略可)
197198
): CoverageReport
198199
```
199200

201+
`constraints` を渡すと、制約に違反する tuple**カバレッジ universe から完全に除外**されます(`totalTuples` / `coveredTuples` / `uncovered` のいずれにも計上されません)。これはジェネレータと同じセマンティクスで、生成済みテスト集合を解析すると常に `coverageRatio === 1.0` になります。
202+
200203
### CoverageReport
201204

202205
```typescript

js/index.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,23 @@ describe('analyzeCoverage()', () => {
197197
// 2-wise for 2 params = 4 tuples
198198
expect(report.totalTuples).toBe(4);
199199
});
200+
201+
it('excludes constraint-invalid tuples from the coverage universe', () => {
202+
const params: Parameter[] = [
203+
{ name: 'os', values: ['win', 'mac'] },
204+
{ name: 'browser', values: ['chrome', 'ie'] },
205+
];
206+
// IF os=mac THEN browser!=ie -> removes (mac, ie). 3 valid tuples remain.
207+
const tests: TestCase[] = [
208+
{ os: 'win', browser: 'chrome' },
209+
{ os: 'mac', browser: 'chrome' },
210+
];
211+
const report = analyzeCoverage(params, tests, 2, ['IF os=mac THEN browser!=ie']);
212+
expect(report.totalTuples).toBe(3);
213+
expect(report.coveredTuples).toBe(2);
214+
expect(report.uncovered).toHaveLength(1);
215+
expect(report.uncovered[0].tuple).toEqual(expect.arrayContaining(['os=win', 'browser=ie']));
216+
});
200217
});
201218

202219
describe('extendTests()', () => {

js/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface WasmModule {
5050
params: Parameter[],
5151
tests: TestCase[],
5252
strength: number,
53+
constraints?: string[],
5354
): CoverageReport | { error: true; code?: string; message?: string };
5455
extendTests(
5556
existing: TestCase[],
@@ -151,9 +152,12 @@ export function analyzeCoverage(
151152
parameters: Parameter[],
152153
tests: TestCase[],
153154
strength?: number,
155+
constraints?: string[],
154156
): CoverageReport {
155157
const mod = getModule();
156-
const result = checkResult<CoverageReport>(mod.analyzeCoverage(parameters, tests, strength ?? 2));
158+
const result = checkResult<CoverageReport>(
159+
mod.analyzeCoverage(parameters, tests, strength ?? 2, constraints ?? []),
160+
);
157161
// When there are no tuples (e.g. fewer parameters than strength), coverage is vacuously 1.0.
158162
if (result.totalTuples === 0) {
159163
result.coverageRatio = 1.0;
@@ -225,9 +229,14 @@ export class Coverwise {
225229
/**
226230
* Analyze t-wise coverage of an existing test suite.
227231
*/
228-
analyzeCoverage(parameters: Parameter[], tests: TestCase[], strength?: number): CoverageReport {
232+
analyzeCoverage(
233+
parameters: Parameter[],
234+
tests: TestCase[],
235+
strength?: number,
236+
constraints?: string[],
237+
): CoverageReport {
229238
const result = checkResult<CoverageReport>(
230-
this.module.analyzeCoverage(parameters, tests, strength ?? 2),
239+
this.module.analyzeCoverage(parameters, tests, strength ?? 2, constraints ?? []),
231240
);
232241
if (result.totalTuples === 0) {
233242
result.coverageRatio = 1.0;

js/pure/index.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
extend as internalExtend,
3434
generate as internalGenerate,
3535
} from '../../src/ts/core/generator.js';
36+
import type { ConstraintNode } from '../../src/ts/model/constraint-ast.js';
37+
import { parseConstraint } from '../../src/ts/model/constraint-parser.js';
3638
import {
3739
annotateClassCoverage,
3840
validateCoverage as internalValidateCoverage,
@@ -144,12 +146,30 @@ export function analyzeCoverage(
144146
parameters: Parameter[],
145147
tests: TestCase[],
146148
strength?: number,
149+
constraints?: string[],
147150
): CoverageReport {
148151
validateParameters(parameters);
149152
const s = validateStrength(strength);
150153
const params = toInternalParams(parameters);
151154
const internalTests = tests.map((tc) => toInternalTestCase(tc, params));
152-
const report = internalValidateCoverage(params, internalTests, s);
155+
156+
// Parse optional constraint expressions.
157+
const parsedConstraints: ConstraintNode[] = [];
158+
if (constraints && constraints.length > 0) {
159+
for (const expr of constraints) {
160+
const parseResult = parseConstraint(expr, params);
161+
if (parseResult.error.code !== 0 || !parseResult.constraint) {
162+
throw new Error(
163+
`Invalid constraint "${expr}": ${parseResult.error.message}${
164+
parseResult.error.detail ? ` — ${parseResult.error.detail}` : ''
165+
}`,
166+
);
167+
}
168+
parsedConstraints.push(parseResult.constraint);
169+
}
170+
}
171+
172+
const report = internalValidateCoverage(params, internalTests, s, parsedConstraints);
153173
const result = toPublicCoverageReport(report);
154174
// When there are no tuples, coverage is vacuously 1.0.
155175
if (result.totalTuples === 0) {
@@ -219,8 +239,13 @@ export class Coverwise {
219239
/**
220240
* Analyze t-wise coverage of an existing test suite.
221241
*/
222-
analyzeCoverage(parameters: Parameter[], tests: TestCase[], strength?: number): CoverageReport {
223-
return analyzeCoverage(parameters, tests, strength);
242+
analyzeCoverage(
243+
parameters: Parameter[],
244+
tests: TestCase[],
245+
strength?: number,
246+
constraints?: string[],
247+
): CoverageReport {
248+
return analyzeCoverage(parameters, tests, strength, constraints);
224249
}
225250

226251
/**

js/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ export interface UncoveredTuple {
4949
tuple: string[];
5050
/** Parameter names involved. */
5151
params: string[];
52-
/** Why this tuple is uncovered. */
52+
/**
53+
* Why this tuple is uncovered.
54+
*
55+
* Currently always `"never covered"` — tuples that are impossible due to
56+
* constraints are removed from the coverage universe entirely (they do not
57+
* appear here and are not counted in `totalTuples`), matching the
58+
* generator's `excludeInvalidTuples` semantics.
59+
*/
5360
reason: string;
5461
/** Human-readable display string. */
5562
display: string;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@libraz/coverwise",
3-
"version": "1.0.2",
3+
"version": "1.1.0",
44
"type": "module",
55
"packageManager": "yarn@4.12.0",
66
"description": "Combinatorial test coverage engine — analyze, generate, and extend t-wise test suites via WASM",

src/core/generator.cpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,10 @@ model::GenerateResult Generate(const GenerateOptions& options) {
320320
if (!AllComplete(coverage, sub_engines)) {
321321
if (opts.max_tests > 0 && result.tests.size() >= static_cast<size_t>(opts.max_tests)) {
322322
result.warnings.push_back("Generation stopped at max_tests (" +
323-
std::to_string(opts.max_tests) +
324-
") before reaching 100% coverage");
323+
std::to_string(opts.max_tests) + ") before reaching 100% coverage");
325324
} else {
326-
result.warnings.push_back(
327-
"Generation stopped before reaching 100% coverage after " +
328-
std::to_string(kMaxRetries) + " consecutive zero-score candidates");
325+
result.warnings.push_back("Generation stopped before reaching 100% coverage after " +
326+
std::to_string(kMaxRetries) + " consecutive zero-score candidates");
329327
}
330328
}
331329

0 commit comments

Comments
 (0)