nip/tests/test_conflict_detection.nim

662 lines
21 KiB
Nim

## Unit Tests for Conflict Detection
##
## Tests for detecting and reporting various types of conflicts
## in the NIP dependency resolver.
##
## Requirements tested:
## - 7.1: Detect and report version conflicts
## - 7.2: Detect and report variant conflicts
## - 7.3: Detect and report circular dependencies
## - 7.4: Detect and report missing packages
## - 7.5: Provide actionable suggestions for resolution
import std/[unittest, tables, sets, options, sequtils, strutils]
import ../src/nip/resolver/conflict_detection
import ../src/nip/resolver/solver_types
import ../src/nip/resolver/variant_types
import ../src/nip/manifest_parser
suite "Conflict Detection Tests":
# --- Version Conflict Tests ---
test "Detect version conflict - exact versions":
## Test detecting conflicting exact version requirements
## Requirements: 7.1
let constraints = @[
VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)),
VersionConstraint(operator: OpExact, version: SemanticVersion(major: 2, minor: 0, patch: 0))
]
let conflict = detectVersionConflict("nginx", constraints)
check conflict.isSome
check conflict.get().kind == VersionConflict
check conflict.get().packages.contains("nginx")
check conflict.get().details.contains("1.0.0")
check conflict.get().details.contains("2.0.0")
check conflict.get().suggestions.len > 0
test "No version conflict - compatible versions":
## Test that compatible versions don't trigger conflict
## Requirements: 7.1
let constraints = @[
VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 1, minor: 0, patch: 0)),
VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 1, minor: 5, patch: 0))
]
let conflict = detectVersionConflict("nginx", constraints)
check conflict.isNone
test "No version conflict - single constraint":
## Test that single constraint doesn't trigger conflict
## Requirements: 7.1
let constraints = @[
VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0))
]
let conflict = detectVersionConflict("nginx", constraints)
check conflict.isNone
test "Detect version conflict - exact vs greater-equal":
## Test detecting conflict between exact and >= constraint
## Requirements: 7.1
let constraints = @[
VersionConstraint(operator: OpExact, version: SemanticVersion(major: 1, minor: 0, patch: 0)),
VersionConstraint(operator: OpGreaterEq, version: SemanticVersion(major: 2, minor: 0, patch: 0))
]
let conflict = detectVersionConflict("zlib", constraints)
check conflict.isSome
check conflict.get().kind == VersionConflict
check conflict.get().suggestions.len > 0
# --- Variant Conflict Tests ---
test "Detect variant conflict - exclusive domains":
## Test detecting conflicting exclusive variant flags
## Requirements: 7.2
var profile1 = newVariantProfile()
var domain1 = newVariantDomain("init", Exclusive)
domain1.flags.incl("systemd")
profile1.addDomain(domain1)
var profile2 = newVariantProfile()
var domain2 = newVariantDomain("init", Exclusive)
domain2.flags.incl("dinit")
profile2.addDomain(domain2)
let demands = @[
VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false),
VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false)
]
let conflict = detectVariantConflict("nginx", demands)
check conflict.isSome
check conflict.get().kind == conflict_detection.VariantConflict
check conflict.get().details.contains("init")
check conflict.get().suggestions.len > 0
test "No variant conflict - non-exclusive domains":
## Test that non-exclusive domains don't trigger conflict
## Requirements: 7.2
var profile1 = newVariantProfile()
var domain1 = newVariantDomain("features", NonExclusive)
domain1.flags.incl("wayland")
profile1.addDomain(domain1)
var profile2 = newVariantProfile()
var domain2 = newVariantDomain("features", NonExclusive)
domain2.flags.incl("x11")
profile2.addDomain(domain2)
let demands = @[
VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false),
VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false)
]
let conflict = detectVariantConflict("nginx", demands)
check conflict.isNone
test "No variant conflict - single demand":
## Test that single demand doesn't trigger conflict
## Requirements: 7.2
var profile = newVariantProfile()
var domain = newVariantDomain("init", Exclusive)
domain.flags.incl("systemd")
profile.addDomain(domain)
let demands = @[
VariantDemand(packageName: "nginx", variantProfile: profile, optional: false)
]
let conflict = detectVariantConflict("nginx", demands)
check conflict.isNone
test "Detect variant conflict - multiple exclusive domains":
## Test detecting conflicts in multiple exclusive domains
## Requirements: 7.2
var profile1 = newVariantProfile()
var domain1a = newVariantDomain("init", Exclusive)
domain1a.flags.incl("systemd")
profile1.addDomain(domain1a)
var domain1b = newVariantDomain("libc", Exclusive)
domain1b.flags.incl("glibc")
profile1.addDomain(domain1b)
var profile2 = newVariantProfile()
var domain2a = newVariantDomain("init", Exclusive)
domain2a.flags.incl("dinit")
profile2.addDomain(domain2a)
var domain2b = newVariantDomain("libc", Exclusive)
domain2b.flags.incl("musl")
profile2.addDomain(domain2b)
let demands = @[
VariantDemand(packageName: "nginx", variantProfile: profile1, optional: false),
VariantDemand(packageName: "nginx", variantProfile: profile2, optional: false)
]
let conflict = detectVariantConflict("nginx", demands)
check conflict.isSome
check conflict.get().kind == conflict_detection.VariantConflict
# --- Circular Dependency Tests ---
test "Detect circular dependency - simple cycle":
## Test detecting a simple circular dependency
## Requirements: 7.3
var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
graph["nginx"] = @["zlib"]
graph["zlib"] = @["nginx"]
let conflict = detectCircularDependency(graph, "nginx")
check conflict.isSome
check conflict.get().kind == CircularDependency
check conflict.get().cyclePath.isSome
check conflict.get().cyclePath.get().len >= 2
check conflict.get().suggestions.len > 0
test "Detect circular dependency - three-way cycle":
## Test detecting a three-way circular dependency
## Requirements: 7.3
var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
graph["nginx"] = @["zlib"]
graph["zlib"] = @["pcre"]
graph["pcre"] = @["nginx"]
let conflict = detectCircularDependency(graph, "nginx")
check conflict.isSome
check conflict.get().kind == CircularDependency
check conflict.get().cyclePath.isSome
let cycle = conflict.get().cyclePath.get()
check cycle.len >= 3
check cycle.contains("nginx")
check cycle.contains("zlib")
check cycle.contains("pcre")
test "No circular dependency - linear chain":
## Test that linear dependency chains don't trigger circular conflict
## Requirements: 7.3
var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
graph["nginx"] = @["zlib"]
graph["zlib"] = @["pcre"]
graph["pcre"] = @[]
let conflict = detectCircularDependency(graph, "nginx")
check conflict.isNone
test "No circular dependency - diamond dependency":
## Test that diamond dependencies don't trigger circular conflict
## Requirements: 7.3
var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
graph["nginx"] = @["zlib", "pcre"]
graph["zlib"] = @["openssl"]
graph["pcre"] = @["openssl"]
graph["openssl"] = @[]
let conflict = detectCircularDependency(graph, "nginx")
check conflict.isNone
test "Detect circular dependency - self-loop":
## Test detecting a package that depends on itself
## Requirements: 7.3
var graph: Table[string, seq[string]] = initTable[string, seq[string]]()
graph["nginx"] = @["nginx"]
let conflict = detectCircularDependency(graph, "nginx")
check conflict.isSome
check conflict.get().kind == CircularDependency
# --- Missing Package Tests ---
test "Detect missing package":
## Test detecting a missing package
## Requirements: 7.4
let available = toHashSet(["nginx", "zlib", "pcre"])
let conflict = detectMissingPackage("openssl", available)
check conflict.isSome
check conflict.get().kind == MissingPackage
check conflict.get().packages.contains("openssl")
check conflict.get().details.contains("openssl")
check conflict.get().suggestions.len > 0
test "No missing package - package exists":
## Test that existing packages don't trigger missing conflict
## Requirements: 7.4
let available = toHashSet(["nginx", "zlib", "pcre", "openssl"])
let conflict = detectMissingPackage("openssl", available)
check conflict.isNone
test "Detect missing package - suggest similar names":
## Test that similar package names are suggested
## Requirements: 7.4
let available = toHashSet(["nginx", "nginx-ssl", "nginx-http2"])
let conflict = detectMissingPackage("nginx-http3", available)
check conflict.isSome
check conflict.get().kind == MissingPackage
# Should suggest similar names
let suggestions = conflict.get().suggestions.join(" ")
check suggestions.contains("nginx") or suggestions.contains("Did you mean")
test "Detect missing package - empty repository":
## Test detecting missing package in empty repository
## Requirements: 7.4
let available: HashSet[string] = initHashSet[string]()
let conflict = detectMissingPackage("nginx", available)
check conflict.isSome
check conflict.get().kind == MissingPackage
# --- Build Hash Mismatch Tests ---
test "Detect build hash mismatch":
## Test detecting build hash mismatch
## Requirements: 7.5
let conflict = detectBuildHashMismatch(
"nginx",
"blake3-abc123def456",
"blake3-xyz789abc123"
)
check conflict.isSome
check conflict.get().kind == BuildHashMismatch
check conflict.get().packages.contains("nginx")
check conflict.get().details.contains("abc123def456")
check conflict.get().details.contains("xyz789abc123")
check conflict.get().suggestions.len > 0
test "No build hash mismatch - hashes match":
## Test that matching hashes don't trigger mismatch conflict
## Requirements: 7.5
let conflict = detectBuildHashMismatch(
"nginx",
"blake3-abc123def456",
"blake3-abc123def456"
)
check conflict.isNone
# --- Conflict Formatting Tests ---
test "Format version conflict":
## Test formatting a version conflict report
## Requirements: 7.1, 7.5
let report = ConflictReport(
kind: VersionConflict,
packages: @["nginx"],
details: "nginx requires version 1.0.0 and 2.0.0",
suggestions: @["Update to compatible version"],
conflictingTerms: @[],
cyclePath: none(seq[string])
)
let formatted = formatConflict(report)
check formatted.contains("VersionConflict")
check formatted.contains("nginx")
check formatted.contains("Update to compatible version")
test "Format variant conflict":
## Test formatting a variant conflict report
## Requirements: 7.2, 7.5
let report = ConflictReport(
kind: VariantConflict,
packages: @["nginx"],
details: "nginx requires +systemd and +dinit",
suggestions: @["Choose one variant"],
conflictingTerms: @[],
cyclePath: none(seq[string])
)
let formatted = formatConflict(report)
check formatted.contains("VariantConflict")
check formatted.contains("nginx")
check formatted.contains("Choose one variant")
test "Format circular dependency":
## Test formatting a circular dependency report
## Requirements: 7.3, 7.5
let report = ConflictReport(
kind: CircularDependency,
packages: @["nginx", "zlib", "nginx"],
details: "nginx -> zlib -> nginx",
suggestions: @["Break the cycle"],
conflictingTerms: @[],
cyclePath: some(@["nginx", "zlib", "nginx"])
)
let formatted = formatConflict(report)
check formatted.contains("CircularDependency")
check formatted.contains("nginx")
check formatted.contains("Break the cycle")
test "Format missing package":
## Test formatting a missing package report
## Requirements: 7.4, 7.5
let report = ConflictReport(
kind: MissingPackage,
packages: @["openssl"],
details: "openssl not found",
suggestions: @["Check package name", "Update repositories"],
conflictingTerms: @[],
cyclePath: none(seq[string])
)
let formatted = formatConflict(report)
check formatted.contains("MissingPackage")
check formatted.contains("openssl")
check formatted.contains("Check package name")
test "Format build hash mismatch":
## Test formatting a build hash mismatch report
## Requirements: 7.5
let report = ConflictReport(
kind: BuildHashMismatch,
packages: @["nginx"],
details: "Hash mismatch for nginx",
suggestions: @["Reinstall package"],
conflictingTerms: @[],
cyclePath: none(seq[string])
)
let formatted = formatConflict(report)
check formatted.contains("BuildHashMismatch")
check formatted.contains("nginx")
check formatted.contains("Reinstall package")
# --- Conflict Analysis Tests ---
test "Analyze conflict origins - version conflict":
## Test analyzing origins of a version conflict
## Requirements: 7.5
var manifests: Table[string, seq[VariantDemand]] = initTable[string, seq[VariantDemand]]()
manifests["nginx"] = @[
VariantDemand(packageName: "nginx", variantProfile: newVariantProfile(), optional: false),
VariantDemand(packageName: "nginx", variantProfile: newVariantProfile(), optional: false)
]
let report = ConflictReport(
kind: VersionConflict,
packages: @["nginx"],
details: "Version conflict",
suggestions: @[],
conflictingTerms: @[],
cyclePath: none(seq[string])
)
let analysis = analyzeConflictOrigins(report, manifests)
check analysis.len > 0
check analysis[0].contains("nginx")
check analysis[0].contains("2")
test "Analyze conflict origins - circular dependency":
## Test analyzing origins of a circular dependency
## Requirements: 7.5
let report = ConflictReport(
kind: CircularDependency,
packages: @["nginx", "zlib", "nginx"],
details: "Circular dependency",
suggestions: @[],
conflictingTerms: @[],
cyclePath: some(@["nginx", "zlib", "nginx"])
)
let analysis = analyzeConflictOrigins(report, initTable[string, seq[VariantDemand]]())
check analysis.len > 0
check analysis[0].contains("3")
# --- Minimal Conflict Extraction Tests ---
test "Extract minimal conflict":
## Test extracting minimal conflicting incompatibilities
## Requirements: 7.5
let incomp1 = Incompatibility(
terms: @[],
cause: Root,
externalContext: "Incomp 1",
fromPackage: none(string),
fromVersion: none(SemanticVersion)
)
let incomp2 = Incompatibility(
terms: @[],
cause: Dependency,
externalContext: "Incomp 2",
fromPackage: none(string),
fromVersion: none(SemanticVersion)
)
let minimal = extractMinimalConflict(@[incomp1, incomp2])
check minimal.isSome
check minimal.get().len == 2
test "Extract minimal conflict - empty list":
## Test extracting minimal conflict from empty list
## Requirements: 7.5
let minimal = extractMinimalConflict(@[])
check minimal.isNone
test "Extract minimal conflict - single incompatibility":
## Test extracting minimal conflict from single incompatibility
## Requirements: 7.5
let incomp = Incompatibility(
terms: @[],
cause: Root,
externalContext: "Single incompatibility",
fromPackage: none(string),
fromVersion: none(SemanticVersion)
)
let minimal = extractMinimalConflict(@[incomp])
check minimal.isSome
check minimal.get().len == 1
test "Extract minimal conflict - multiple incompatibilities":
## Test extracting minimal conflict from multiple incompatibilities
## Requirements: 7.5
let incomp1 = Incompatibility(
terms: @[],
cause: Root,
externalContext: "Root requirement",
fromPackage: some("nginx"),
fromVersion: none(SemanticVersion)
)
let incomp2 = Incompatibility(
terms: @[],
cause: Dependency,
externalContext: "Dependency conflict",
fromPackage: some("nginx"),
fromVersion: none(SemanticVersion)
)
let incomp3 = Incompatibility(
terms: @[],
cause: VariantConflict,
externalContext: "Variant conflict",
fromPackage: some("openssl"),
fromVersion: none(SemanticVersion)
)
let minimal = extractMinimalConflict(@[incomp1, incomp2, incomp3])
check minimal.isSome
# The minimal set should have at least 1 incompatibility
check minimal.get().len >= 1
# The minimal set should have at most all incompatibilities
check minimal.get().len <= 3
test "Extract minimal conflict - preserves root incompatibilities":
## Test that root incompatibilities are preserved in minimal set
## Requirements: 7.5
let rootIncompatibility = Incompatibility(
terms: @[],
cause: Root,
externalContext: "User requirement",
fromPackage: some("nginx"),
fromVersion: none(SemanticVersion)
)
let dependencyIncompatibility = Incompatibility(
terms: @[],
cause: Dependency,
externalContext: "Dependency",
fromPackage: some("zlib"),
fromVersion: none(SemanticVersion)
)
let minimal = extractMinimalConflict(@[rootIncompatibility, dependencyIncompatibility])
check minimal.isSome
# Root incompatibilities should be preserved
let hasRoot = minimal.get().anyIt(it.cause == Root)
check hasRoot
test "Extract minimal conflict - handles variant conflicts":
## Test that variant conflicts are handled correctly
## Requirements: 7.5
let variantConflict1 = Incompatibility(
terms: @[],
cause: IncompatibilityCause.VariantConflict,
externalContext: "Variant conflict 1",
fromPackage: some("nginx"),
fromVersion: none(SemanticVersion)
)
let variantConflict2 = Incompatibility(
terms: @[],
cause: IncompatibilityCause.VariantConflict,
externalContext: "Variant conflict 2",
fromPackage: some("nginx"),
fromVersion: none(SemanticVersion)
)
let minimal = extractMinimalConflict(@[variantConflict1, variantConflict2])
check minimal.isSome
# Should have at least one variant conflict
let hasVariantConflict = minimal.get().anyIt(it.cause == IncompatibilityCause.VariantConflict)
check hasVariantConflict
test "Extract minimal conflict - deterministic results":
## Test that minimal conflict extraction is deterministic
## Requirements: 7.5
let incomp1 = Incompatibility(
terms: @[],
cause: Root,
externalContext: "Incomp 1",
fromPackage: some("pkg1"),
fromVersion: none(SemanticVersion)
)
let incomp2 = Incompatibility(
terms: @[],
cause: Dependency,
externalContext: "Incomp 2",
fromPackage: some("pkg2"),
fromVersion: none(SemanticVersion)
)
let incomp3 = Incompatibility(
terms: @[],
cause: VariantConflict,
externalContext: "Incomp 3",
fromPackage: some("pkg3"),
fromVersion: none(SemanticVersion)
)
let incompatibilities = @[incomp1, incomp2, incomp3]
# Extract minimal conflict multiple times
let minimal1 = extractMinimalConflict(incompatibilities)
let minimal2 = extractMinimalConflict(incompatibilities)
# Results should be the same
check minimal1.isSome
check minimal2.isSome
check minimal1.get().len == minimal2.get().len