UNPKG

covertable

Version:

Efficient TypeScript library for pairwise testing, generating minimal covering arrays with constraint support.

749 lines (748 loc) 24.3 kB
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 };