covertable
Version:
Efficient TypeScript library for pairwise testing, generating minimal covering arrays with constraint support.
749 lines (748 loc) • 24.3 kB
JavaScript
class z extends Error {
constructor(t, s = []) {
super(t), this.uncoveredPairs = s;
}
}
const b = (i, t, s = 1) => Array.from({ length: (t - i - 1) / s + 1 }, (e, n) => i + n * s), L = (...i) => {
const t = i[0].length;
return b(0, t).map((s) => i.map((e) => e[s]));
}, v = (i, t) => {
const s = t - 1, e = [], n = b(0, t);
for (; n[0] < i.length - s; ) {
e.push(n.map((r) => i[r])), n[s]++;
for (let r = s; r > 0; r--)
n[r] >= i.length - (s - r) && n[r - 1]++;
for (let r = 1; r <= s; r++)
n[r] >= i.length - (s - r) && (n[r] = n[r - 1] + 1);
}
return e;
}, A = (...i) => {
const t = [], s = (e, n) => {
if (e.length === i.length) {
t.push(e);
return;
}
for (let r of i[n])
s([...e, r], n + 1);
};
return s([], 0), t;
}, B = (i) => Array.isArray(i) ? i.length : Object.keys(i).length, S = (i) => Array.isArray(i) ? i.map((t, s) => [s, t]) : i instanceof Map ? [...i.entries()] : [...Object.entries(i)], N = (i, t) => {
const s = i.map((e) => t.get(e) || 0);
return L(s, i);
}, x = (i, t) => i > t ? 1 : -1, k = (i) => {
const t = i.reduce((s, e) => s * e);
return Number.isSafeInteger(t) ? t : i.sort(x).toString();
}, T = (i) => {
if (i % 2 === 0)
return !1;
let t = b(3, Math.sqrt(i) + 1, 2);
for (; t.length > 0; ) {
const s = t[0];
if (i % s === 0)
return !1;
t = t.filter((e) => e % s !== 0);
}
return !0;
};
function* U() {
yield 2;
for (let i = 3; ; i += 2)
T(i) && (yield i);
}
const F = (i) => {
let t = 2166136261;
for (let s = 0; s < i.length; s++)
t ^= i.charCodeAt(s), t = Math.imul(t, 16777619);
return (t >>> 0).toString(16).padStart(8, "0");
};
function W(i, t) {
const { salt: s, indices: e } = t, n = (r, o) => {
const l = `${r.map((c) => e.get(c))} ${s}`, a = `${o.map((c) => e.get(c))} ${s}`;
return F(l) > F(a) ? 1 : -1;
};
return i.sort(n);
}
const $ = (i, t, s, e) => {
let n = 0;
for (const r of s)
if (r === 2) {
const o = [...i], l = o.length;
for (let a = 0; a < l - 1; a++) {
const c = o[a];
for (let h = a + 1; h < l; h++) {
const f = c * o[h];
t.has(f) && (!e || !e.has(f)) && n++;
}
}
} else
for (let o of v([...i], r)) {
const l = k(o);
t.has(l) && (!e || !e.has(l)) && n++;
}
return n;
};
function* D(i) {
var s, e;
const t = (((e = (s = i.options) == null ? void 0 : s.constraints) == null ? void 0 : e.length) ?? 0) > 0;
for (; ; ) {
let n = null, r = null;
const o = /* @__PURE__ */ new Set();
if (i.row.size > 0)
for (const l of i.allStrengths)
for (const a of v([...i.row.values()], l))
o.add(k(a));
for (const [l, a] of i.incomplete.entries()) {
const c = i.row.size;
if (i.isFilled(i.row))
break;
if (o.has(l) || i.row.invalidPairs.has(l)) continue;
const h = c === 0 ? a.length : i.isCompatible(a);
if (h === null || h === 0)
continue;
let f = h;
if (t) {
const m = i.storable(i.getCandidate(a));
if (m === null || m === 0)
continue;
f = m;
}
const u = Math.abs(f), { tolerance: y = 0 } = i.options, g = $(
/* @__PURE__ */ new Set([...i.row.values(), ...a]),
i.incomplete,
i.allStrengths,
o
);
if (g + y > c * u) {
r = a;
break;
}
(n === null || n < g) && (n = g, r = a);
}
if (r === null)
break;
yield r;
}
}
function q(i, t) {
const s = t.split(".");
let e = i;
for (const n of s) {
if (e == null) return;
e = e[n];
}
return e;
}
const V = {
add: (i, t) => i + t,
sub: (i, t) => i - t,
mul: (i, t) => i * t,
div: (i, t) => i / t,
mod: (i, t) => i % t,
pow: (i, t) => i ** t
};
function K(i, t) {
if (typeof t == "string")
return q(i, t);
const s = K(i, t.left);
if (s === void 0) return;
const e = "right" in t ? K(i, t.right) : t.value;
if (e === void 0) return;
const n = V[t.operator];
return n(s, e);
}
function j(i) {
if (typeof i == "string")
return /* @__PURE__ */ new Set([i.split(".")[0]]);
const t = j(i.left);
if ("right" in i)
for (const s of j(i.right)) t.add(s);
return t;
}
function M(i) {
switch (i.operator) {
case "not":
return M(i.condition);
case "and":
case "or": {
const t = /* @__PURE__ */ new Set();
for (const s of i.conditions)
for (const e of M(s)) t.add(e);
return t;
}
case "fn":
return new Set(i.requires);
default: {
const t = j(i.left);
if ("right" in i)
for (const s of j(i.right)) t.add(s);
return t;
}
}
}
const R = {
eq: (i, t) => i === t,
ne: (i, t) => i !== t,
gt: (i, t) => i > t,
lt: (i, t) => i < t,
gte: (i, t) => i >= t,
lte: (i, t) => i <= t,
in: (i, t) => t.has(i)
};
function d(i, t, s = {}) {
switch (i.operator) {
// -- logical --
case "not": {
const e = d(i.condition, t, s);
return e === null ? null : !e;
}
case "and": {
let e = !1;
for (const n of i.conditions) {
const r = d(n, t, s);
if (r === !1) return !1;
r === null && (e = !0);
}
return e ? null : !0;
}
case "or": {
let e = !1;
for (const n of i.conditions) {
const r = d(n, t, s);
if (r === !0) return !0;
r === null && (e = !0);
}
return e ? null : !1;
}
// -- custom --
case "fn": {
for (const e of i.requires)
if (q(t, e) === void 0) return null;
return i.evaluate(t);
}
// -- in --
case "in": {
const e = K(t, i.left);
return e === void 0 ? null : (s.in ?? R.in)(e, i.values);
}
// -- comparison --
default: {
const e = K(t, i.left);
if (e === void 0) return null;
let n;
if ("right" in i) {
if (n = K(t, i.right), n === void 0) return null;
} else
n = i.value;
return (s[i.operator] ?? R[i.operator])(e, n);
}
}
}
class p extends Map {
constructor(t) {
super(), this.invalidPairs = /* @__PURE__ */ new Set();
for (const [s, e] of t)
this.set(s, e);
}
getPairKey(...t) {
const s = [...this.values(), ...t];
return k(s);
}
copy(t) {
for (let [s, e] of t.entries())
this.set(s, e);
}
}
class _ {
constructor(t, s = {}) {
this.factors = t, this.options = s, this.serials = /* @__PURE__ */ new Map(), this.parents = /* @__PURE__ */ new Map(), this.indices = /* @__PURE__ */ new Map(), this.incomplete = /* @__PURE__ */ new Map(), this.rejected = /* @__PURE__ */ new Set(), this._totalPairs = 0, this._prunedPairs = 0, this._rowCount = 0, this._uncoveredPairs = [], this._completions = {}, this.constraints = [], this.constraintsByKey = /* @__PURE__ */ new Map(), this.passedIndexes = /* @__PURE__ */ new Set(), this.comparer = s.comparer ?? {}, this.serialize(t), this.factorLength = B(t), this.factorIsArray = t instanceof Array, this.resolveConstraints(), this.setIncomplete(), this._totalPairs = this.incomplete.size, this.row = new p([]);
for (const [e, n] of this.incomplete.entries()) {
const r = this.getCandidate(n);
if (this.storableCheck(r) === !1)
this.incomplete.delete(e);
else if (this.constraints.length > 0) {
const o = new p(r);
this.forwardCheck(o) || this.incomplete.delete(e);
}
}
this._prunedPairs = this._totalPairs - this.incomplete.size;
}
get stats() {
return {
totalPairs: this._totalPairs,
prunedPairs: this._prunedPairs,
coveredPairs: this._totalPairs - this._prunedPairs - this.incomplete.size,
progress: this._totalPairs === 0 ? 0 : 1 - this.incomplete.size / this._totalPairs,
rowCount: this._rowCount,
uncoveredPairs: this._uncoveredPairs,
completions: this._completions
};
}
/** Normalize `in` conditions: convert `values` arrays to Sets for O(1) lookup. */
static normalizeCondition(t) {
return t.operator === "in" && Array.isArray(t.values) ? { ...t, values: new Set(t.values) } : t.operator === "and" || t.operator === "or" ? { ...t, conditions: t.conditions.map(_.normalizeCondition) } : t.operator === "not" ? { ...t, condition: _.normalizeCondition(t.condition) } : t;
}
resolveConstraints() {
const t = this.options.constraints ?? [];
for (let s = 0; s < t.length; s++) {
const e = _.normalizeCondition(t[s]), n = M(e);
this.constraints.push({ condition: e, keys: n });
for (const r of n) {
let o = this.constraintsByKey.get(r);
o || (o = /* @__PURE__ */ new Set(), this.constraintsByKey.set(r, o)), o.add(s);
}
}
}
serialize(t) {
let s = 0;
const e = U();
S(t).map(([n, r]) => {
const o = B(r), l = [];
b(s, s + o).map((a) => {
const c = e.next().value;
l.push(c), this.parents.set(c, n), this.indices.set(c, a);
}), this.serials.set(n, l), s += o;
});
}
setIncomplete() {
const { sorter: t = W, salt: s = "" } = this.options, e = [], n = S(this.serials).map(([a, c]) => a), r = this.options.subModels ?? [], o = r.map((a) => new Set(a.fields)), l = (a) => o.some((c) => a.every((h) => c.has(h)));
for (const a of v(n, this.strength)) {
if (l(a)) continue;
const c = b(0, this.strength).map((h) => this.serials.get(a[h]));
for (let h of A(...c))
e.push(h.sort(x));
}
for (const a of r)
for (const c of v(a.fields, a.strength)) {
const h = b(0, a.strength).map((f) => this.serials.get(c[f]));
for (let f of A(...h))
e.push(f.sort(x));
}
for (let a of t(e, { salt: s, indices: this.indices }))
this.incomplete.set(k(a), a);
}
/**
* Try to add a candidate pair to the current row. Evaluates constraints
* against a snapshot (row + pair) without mutating `this.row`. If all
* constraints pass (or are unknown), the pair is committed to `this.row`
* and `true` is returned. If any constraint definitively fails, `this.row`
* is unchanged and `false` is returned.
*/
setPair(t) {
const s = this.getCandidate(t), e = new p([...this.row.entries(), ...s]), n = this.toObject(e);
for (let r = 0; r < this.constraints.length; r++) {
if (this.passedIndexes.has(r)) continue;
if (d(this.constraints[r].condition, n, this.comparer) === !1) return !1;
}
if (!this.forwardCheck(e)) return !1;
for (const [r, o] of s)
this.row.set(r, o);
return this.markPassedConstraints(this.row), !0;
}
consumePairs(t) {
for (const s of this.allStrengths)
for (let e of v([...t.values()], s)) {
const n = k(e);
this.incomplete.delete(n);
}
}
getCandidate(t) {
return N(t, this.parents);
}
isCompatible(t) {
let s = 0;
for (const e of t) {
const n = this.parents.get(e), r = this.row.get(n);
if (typeof r > "u")
s++;
else if (r !== e)
return null;
}
return s;
}
/**
* Check whether adding `candidate` to `row` would violate any constraint.
* Returns `true` (OK), `false` (definitively violated), or `null`
* (some dependency is still missing — defer).
*
* Constraints already in `passedIndexes` are skipped.
*/
storableCheck(t, s = this.row) {
if (this.constraints.length === 0) return !0;
let e = null;
for (let n = 0; n < this.constraints.length; n++) {
if (this.passedIndexes.has(n)) continue;
if (e === null) {
const o = new p([...s.entries(), ...t]);
e = this.toObject(o);
}
if (d(this.constraints[n].condition, e, this.comparer) === !1) return !1;
}
return !0;
}
/**
* Returns the number of new keys this candidate would add to `row`, or
* `null` if the candidate is incompatible or would definitively violate a
* constraint. `null` results from three-valued evaluation are treated
* as "OK for now" — they will be rechecked once more keys are known.
*/
storable(t, s = this.row) {
let e = 0;
for (let [r, o] of t) {
let l = s.get(r);
if (typeof l > "u")
e++;
else if (l != o)
return null;
}
return this.storableCheck(t, s) === !1 ? null : e;
}
/**
* Evaluate constraints against `row` and mark those that pass as done.
* Returns `false` if any constraint definitively fails (= the row is
* unsalvageable and should be abandoned), `true` otherwise.
*/
markPassedConstraints(t) {
if (this.constraints.length === 0) return !0;
let s = null;
for (let e = 0; e < this.constraints.length; e++) {
if (this.passedIndexes.has(e)) continue;
s === null && (s = this.toObject(t));
const n = d(this.constraints[e].condition, s, this.comparer);
if (n === !0)
this.passedIndexes.add(e);
else if (n === !1)
return !1;
}
return !0;
}
/**
* Forward checking: given a snapshot row, propagate constraints to prune
* domains of unfilled factors. If any factor's domain becomes empty, the
* current assignment is unsolvable — return false.
*
* This is read-only: it does NOT modify this.row. It builds a temporary
* domain map and iteratively narrows it by evaluating constraints with
* each candidate value.
*/
forwardCheck(t) {
if (this.constraints.length === 0) return !0;
const s = /* @__PURE__ */ new Map();
for (const [o, l] of this.serials.entries())
t.has(o) || s.set(o, [...l]);
if (s.size === 0) return !0;
const e = new Set(this.constraints.map((o, l) => l)), n = new p([...t.entries()]);
let r = !0;
for (; r; ) {
r = !1;
for (const [o, l] of s.entries()) {
if (l.length === 0) return !1;
const a = this.constraintsByKey.get(o);
if (!a) continue;
let c = !1;
for (const f of a)
if (e.has(f)) {
c = !0;
break;
}
if (!c) continue;
const h = [];
for (const f of l) {
n.set(o, f);
const u = this.toObject(n);
let y = !0;
for (const g of a)
if (!this.passedIndexes.has(g) && d(this.constraints[g].condition, u, this.comparer) === !1) {
y = !1;
break;
}
y && h.push(f);
}
if (n.delete(o), h.length === 0) return !1;
if (h.length < l.length && (s.set(o, h), r = !0, h.length === 1)) {
n.set(o, h[0]), s.delete(o);
const f = this.constraintsByKey.get(o);
if (f)
for (const u of f) e.add(u);
}
if (h.length > 1)
for (const [f, u] of s.entries()) {
if (f === o) continue;
const y = this.constraintsByKey.get(f);
if (!y) continue;
let g = !1;
for (const w of y)
if (a.has(w)) {
g = !0;
break;
}
if (!g) continue;
const m = /* @__PURE__ */ new Set();
for (const w of h) {
n.set(o, w);
for (const C of u) {
if (m.has(C)) continue;
n.set(f, C);
const E = this.toObject(n);
let O = !0;
for (const I of y)
if (!this.passedIndexes.has(I) && d(this.constraints[I].condition, E, this.comparer) === !1) {
O = !1;
break;
}
O && m.add(C);
}
n.delete(f);
}
n.delete(o);
const P = u.filter((w) => m.has(w));
if (P.length === 0) return !1;
if (P.length < u.length && (s.set(f, P), r = !0, P.length === 1)) {
n.set(f, P[0]), s.delete(f);
const w = this.constraintsByKey.get(f);
if (w)
for (const C of w) e.add(C);
}
}
}
}
return !0;
}
isFilled(t) {
return t.size === this.factorLength;
}
toMap(t) {
const s = /* @__PURE__ */ new Map();
for (let [e, n] of t.entries()) {
const r = this.indices.get(n), o = this.indices.get(this.serials.get(e)[0]);
s.set(e, this.factors[e][r - o]);
}
return s;
}
toObject(t) {
const s = {};
for (let [e, n] of this.toMap(t).entries())
s[e] = n;
return s;
}
reset() {
this.row = new p([]), this.passedIndexes.clear();
}
restore() {
const t = this.row;
if (this.row = new p([]), this.passedIndexes.clear(), this.factorIsArray) {
const s = this.toMap(t);
return S(s).sort((e, n) => e[0] > n[0] ? 1 : -1).map(([e, n]) => n);
}
return this.toObject(t);
}
/**
* Fill the remaining unfilled factors of `this.row` and check constraints.
* Uses depth-first backtracking: each unfilled factor tries its values in
* order (weight-sorted on the first pass). When a value causes a
* constraint to evaluate to `false` (via three-valued `storable`), the
* next candidate is tried; if all candidates are exhausted, the previous
* factor is backtracked.
*
* Returns `true` when a valid completion is found (the row is updated in
* place), or `false` when no valid completion exists.
*/
/**
* Result of close(): `true` = valid completion found; `false` = failed;
* or an object with the conflict keys from the first failing constraint.
*/
close() {
const t = S(this.serials), s = [];
for (const [a, c] of t)
this.row.has(a) || s.push({ key: a, values: this.orderByWeight(a, c) });
if (s.length === 0)
return this.passedIndexes.clear(), this.markPassedConstraints(this.row), this.isComplete ? !0 : { conflictKeys: this.findConflictKeys() };
const e = new p([...this.row.entries()]), n = s.length, r = new Array(n).fill(0);
let o = 0, l = null;
for (e.set(s[0].key, s[0].values[0]); ; ) {
const a = s[o], c = a.values[r[o]], h = [[a.key, c]];
if (this.storable(h, e) !== null)
if (o === n - 1) {
if (this.row.copy(e), this.passedIndexes.clear(), this.markPassedConstraints(this.row), this.isComplete) return !0;
l = this.findConflictKeys();
} else {
o++, r[o] = 0, e.set(s[o].key, s[o].values[0]);
continue;
}
for (; ; ) {
if (r[o]++, r[o] < s[o].values.length) {
e.set(s[o].key, s[o].values[r[o]]);
break;
}
if (e.delete(s[o].key), o--, o < 0)
return this.reset(), { conflictKeys: l };
}
}
}
get strength() {
return this.options.strength || 2;
}
get allStrengths() {
const t = /* @__PURE__ */ new Set([this.strength]);
for (const s of this.options.subModels ?? [])
t.add(s.strength);
return [...t];
}
valueToSerial(t, s) {
const e = this.factors[t];
if (!e) return null;
const n = e.indexOf(s);
if (n === -1) return null;
const r = this.serials.get(t);
return r ? r[n] : null;
}
applyPreset(t) {
const s = [];
for (const [e, n] of S(t)) {
const r = this.valueToSerial(e, n);
if (r === null) return !1;
s.push([e, r]);
}
if (s.length === 0) return !1;
for (const [e, n] of s)
this.row.set(e, n);
return !0;
}
orderByWeight(t, s) {
const e = this.options.weights;
if (!e) return s;
const n = e[t];
if (!n) return s;
const r = this.indices.get(s[0]);
return [...s].sort((o, l) => {
const a = n[this.indices.get(o) - r] ?? 1;
return (n[this.indices.get(l) - r] ?? 1) - a;
});
}
get isComplete() {
if (!this.isFilled(this.row)) return !1;
if (this.constraints.length === 0) return !0;
const t = this.toObject(this.row);
for (let s = 0; s < this.constraints.length; s++) {
if (this.passedIndexes.has(s)) continue;
if (d(this.constraints[s].condition, t, this.comparer) === !1) return !1;
}
return !0;
}
/**
* Find the keys of the first failing constraint on the current row.
* Returns the set of factor keys that participate in the conflict,
* or null if the row passes all constraints.
*/
findConflictKeys() {
if (!this.isFilled(this.row)) return null;
const t = this.toObject(this.row);
for (let s = 0; s < this.constraints.length; s++)
if (!this.passedIndexes.has(s) && d(this.constraints[s].condition, t, this.comparer) === !1)
return this.constraints[s].keys;
return null;
}
/**
* Analyse remaining incomplete pairs and identify which constraint(s)
* make each pair infeasible. Used to build a diagnostic when throwing
* NeverMatch.
*/
diagnoseUncoveredPairs() {
const t = [];
for (const [, s] of this.incomplete.entries()) {
const e = this.getCandidate(s), n = {};
for (const [a, c] of e) {
const h = this.indices.get(c), f = this.serials.get(a), u = this.indices.get(f[0]);
n[a] = this.factors[a][h - u];
}
const r = new p(e), o = this.toObject(r), l = [];
for (let a = 0; a < this.constraints.length; a++)
d(this.constraints[a].condition, o, this.comparer) === !1 && l.push(a);
if (l.length === 0) {
const a = new Set(Object.keys(n));
for (let c = 0; c < this.constraints.length; c++)
for (const h of this.constraints[c].keys)
if (a.has(h)) {
l.push(c);
break;
}
}
t.push({ pair: n, constraints: l });
}
return t;
}
/**
* Record which factor values were filled by close() (completion) rather
* than by greedy. `greedyKeys` are the keys that were already in the row
* before close() ran.
*/
recordCompletions(t, s) {
for (const [e, n] of t.entries()) {
if (s.has(e)) continue;
const r = this.indices.get(n), o = this.serials.get(e), l = this.indices.get(o[0]), a = String(this.factors[e][r - l]), c = String(e);
this._completions[c] || (this._completions[c] = {}), this._completions[c][a] = (this._completions[c][a] ?? 0) + 1;
}
}
get progress() {
return this.stats.progress;
}
make() {
return [...this.makeAsync()];
}
*makeAsync() {
const { criterion: t = D, presets: s = [] } = this.options;
for (const n of s) {
if (!this.applyPreset(n)) continue;
const r = new Set(this.row.keys());
try {
this.close() === !0 && (this.recordCompletions(this.row, r), this.consumePairs(this.row), this._rowCount++, yield this.restore());
} catch (o) {
if (o instanceof z)
this.row = new p([]), this.passedIndexes.clear();
else
throw o;
}
}
let e = 0;
for (; this.incomplete.size; ) {
for (let r of t(this)) {
if (this.isFilled(this.row)) break;
const o = k(r);
this.row.invalidPairs.has(o) || this.setPair(r) || this.row.invalidPairs.add(o);
}
const n = new Set(this.row.keys());
try {
if (this.close() === !0)
this.recordCompletions(this.row, n), this.consumePairs(this.row), this._rowCount++, yield this.restore(), e = 0;
else {
if (e++, e > this.incomplete.size)
break;
const o = this.row.invalidPairs;
for (const l of this.allStrengths)
for (let a of v([...this.row.values()], l))
o.add(k(a));
this.reset(), this.rejected.clear();
for (const l of o)
this.row.invalidPairs.add(l);
}
} catch (r) {
if (r instanceof z) {
this.reset(), this.rejected.clear();
continue;
}
throw r;
}
}
for (const [, n] of [...this.incomplete.entries()]) {
if (this.reset(), !this.setPair(n)) continue;
const r = new Set(this.row.keys());
this.close() === !0 ? (this.recordCompletions(this.row, r), this.consumePairs(this.row), this._rowCount++, yield this.restore()) : this.reset();
}
this.incomplete.size > 0 && (this._uncoveredPairs = this.diagnoseUncoveredPairs());
}
}
export {
_ as C,
z as N,
D as g,
W as h
};