From 9f1049e51af6ffba483f4e3301aff23bef0d9be7 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 21 Nov 2023 10:07:19 +0100 Subject: [PATCH 1/5] Implemented dependency-templated resolver --- resolver.go | 113 +++++++++++++++++++++++++++-------------------- resolver_test.go | 42 ++++++++++-------- 2 files changed, 89 insertions(+), 66 deletions(-) diff --git a/resolver.go b/resolver.go index 7857230..14ee94d 100644 --- a/resolver.go +++ b/resolver.go @@ -15,24 +15,21 @@ type Dependency interface { } // Release represents a release, it must provide methods to return Name, Version and Dependencies -type Release interface { +type Release[D Dependency] interface { GetName() string GetVersion() *Version - GetDependencies() []Dependency + GetDependencies() []D } -func match(r Release, dep Dependency) bool { - return r.GetName() == dep.GetName() && dep.GetConstraint().Match(r.GetVersion()) -} - -// Releases is a list of Release -type Releases []Release +// Releases is a list of Release of the same package (all releases with +// the same Name but different Version) +type Releases[R Release[D], D Dependency] []R -// FilterBy return a subset of the Releases matching the provided Dependency -func (set Releases) FilterBy(dep Dependency) Releases { - res := []Release{} +// FilterBy return a subset of the Releases matching the provided Constraint +func (set Releases[R, D]) FilterBy(c Constraint) Releases[R, D] { + var res Releases[R, D] for _, r := range set { - if match(r, dep) { + if c.Match(r.GetVersion()) { res = append(res, r) } } @@ -41,49 +38,69 @@ func (set Releases) FilterBy(dep Dependency) Releases { // SortDescent sort the Releases in this set in descending order (the lastest // release is the first) -func (set Releases) SortDescent() { +func (set Releases[R, D]) SortDescent() { sort.Slice(set, func(i, j int) bool { return set[i].GetVersion().GreaterThan(set[j].GetVersion()) }) } // Archive contains all Releases set to consider for dependency resolution -type Archive struct { - Releases map[string]Releases +type Archive[R Release[D], D Dependency] struct { + releases map[string]Releases[R, D] } -// Resolve will try to depp-resolve dependencies from the Release passed as -// arguent using a backtracking algorithm. -func (ar *Archive) Resolve(release Release) []Release { - mainDep := &bareDependency{ - name: release.GetName(), - version: release.GetVersion(), +// NewArchive creates a new archive +func NewArchive[R Release[D], D Dependency]() *Archive[R, D] { + return &Archive[R, D]{ + releases: map[string]Releases[R, D]{}, } - return ar.resolve(map[string]Release{}, []Dependency{mainDep}, map[Dependency]int{}) } -type bareDependency struct { - name string - version *Version +// AddRelease adds a release to this archive +func (ar *Archive[R, D]) AddRelease(rel R) { + relName := rel.GetName() + ar.releases[relName] = append(ar.releases[relName], rel) } -func (b *bareDependency) GetName() string { - return b.name +// AddReleases adds all the releases to this archive +func (ar *Archive[R, D]) AddReleases(rels ...R) { + for _, rel := range rels { + relName := rel.GetName() + ar.releases[relName] = append(ar.releases[relName], rel) + } } -func (b *bareDependency) GetConstraint() Constraint { - return &Equals{Version: b.version} +// Resolve will try to depp-resolve dependencies from the Release passed as +// arguent using a backtracking algorithm. +func (ar *Archive[R, D]) Resolve(release R) Releases[R, D] { + // Initial empty state of the resolver + solution := map[string]R{} + depsToProcess := []D{} + problematicDeps := map[dependencyHash]int{} + + // Check if the release is in the archive + if len(ar.releases[release.GetName()].FilterBy(&Equals{Version: release.GetVersion()})) == 0 { + return nil + } + + // Add the requested release to the solution and proceed + // with the dependencies resolution + solution[release.GetName()] = release + depsToProcess = append(depsToProcess, release.GetDependencies()...) + return ar.resolve(solution, depsToProcess, problematicDeps) } -func (b *bareDependency) String() string { - return b.GetName() + b.GetConstraint().String() +type dependencyHash string + +func hashDependency[D Dependency](dep D) dependencyHash { + return dependencyHash(dep.GetName() + "/" + dep.GetConstraint().String()) } -func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependency, problematicDeps map[Dependency]int) []Release { +func (ar *Archive[R, D]) resolve(solution map[string]R, depsToProcess []D, problematicDeps map[dependencyHash]int) Releases[R, D] { debug("deps to process: %s", depsToProcess) if len(depsToProcess) == 0 { debug("All dependencies have been resolved.") - res := []Release{} + var res Releases[R, D] for _, v := range solution { res = append(res, v) } @@ -97,7 +114,7 @@ func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependen // If a release is already picked in the solution check if it match the dep if existingRelease, has := solution[depName]; has { - if match(existingRelease, dep) { + if dep.GetConstraint().Match(existingRelease.GetVersion()) { debug("%s already in solution and matching", existingRelease) return ar.resolve(solution, depsToProcess[1:], problematicDeps) } @@ -106,27 +123,25 @@ func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependen } // Otherwise start backtracking the dependency - releases := ar.Releases[dep.GetName()].FilterBy(dep) + releases := ar.releases[dep.GetName()].FilterBy(dep.GetConstraint()) // Consider the latest versions first releases.SortDescent() - findMissingDeps := func(deps []Dependency) Dependency { - for _, dep := range deps { - if _, ok := ar.Releases[dep.GetName()]; !ok { - return dep - } - } - return nil - } - debug("releases matching criteria: %s", releases) for _, release := range releases { deps := release.GetDependencies() debug("try with %s %s", release, deps) - if missingDep := findMissingDeps(deps); missingDep != nil { - debug("%s did not work, becuase his dependency %s does not exists", release, missingDep.GetName()) + missingDep := false + for _, dep := range deps { + if _, ok := ar.releases[dep.GetName()]; !ok { + debug("%s did not work, becuase his dependency %s does not exists", release, dep.GetName()) + missingDep = true + break + } + } + if missingDep { continue } @@ -134,7 +149,9 @@ func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependen newDepsToProcess := append(depsToProcess[1:], deps...) // bubble up problematics deps so they are processed first sort.Slice(newDepsToProcess, func(i, j int) bool { - return problematicDeps[newDepsToProcess[i]] > problematicDeps[newDepsToProcess[j]] + ci := hashDependency(newDepsToProcess[i]) + cj := hashDependency(newDepsToProcess[j]) + return problematicDeps[ci] > problematicDeps[cj] }) if res := ar.resolve(solution, newDepsToProcess, problematicDeps); res != nil { return res @@ -143,6 +160,6 @@ func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependen delete(solution, depName) } - problematicDeps[dep]++ + problematicDeps[hashDependency(dep)]++ return nil } diff --git a/resolver_test.go b/resolver_test.go index 4d905ab..e33a66f 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -19,10 +19,12 @@ type customDep struct { cond Constraint } +// GetName return the name of the dependency (implements the Dependency interface) func (c *customDep) GetName() string { return c.name } +// GetConstraint return the version contraints of the dependency (implements the Dependency interface) func (c *customDep) GetConstraint() Constraint { return c.cond } @@ -34,18 +36,20 @@ func (c *customDep) String() string { type customRel struct { name string vers *Version - deps []Dependency + deps []*customDep } +// GetName return the name of the release (implements the Release interface) func (r *customRel) GetName() string { return r.name } +// GetVersion return the version of the release (implements the Release interface) func (r *customRel) GetVersion() *Version { return r.vers } -func (r *customRel) GetDependencies() []Dependency { +func (r *customRel) GetDependencies() []*customDep { return r.deps } @@ -53,7 +57,7 @@ func (r *customRel) String() string { return r.name + "@" + r.vers.String() } -func d(dep string) Dependency { +func d(dep string) *customDep { name := dep[0:1] cond, err := ParseConstraint(dep[1:]) if err != nil { @@ -62,15 +66,15 @@ func d(dep string) Dependency { return &customDep{name: name, cond: cond} } -func deps(deps ...string) []Dependency { - res := []Dependency{} +func deps(deps ...string) []*customDep { + var res []*customDep for _, dep := range deps { res = append(res, d(dep)) } return res } -func rel(name, ver string, deps []Dependency) Release { +func rel(name, ver string, deps []*customDep) *customRel { return &customRel{name: name, vers: v(ver), deps: deps} } @@ -119,18 +123,17 @@ func TestResolver(t *testing.T) { i160 := rel("I", "1.6.0", deps()) i170 := rel("I", "1.7.0", deps()) i180 := rel("I", "1.8.0", deps()) - arch := &Archive{ - Releases: map[string]Releases{ - "A": {a100, a110, a111, a120, a121}, - "B": {b131, b130, b121, b120, b111, b110, b100}, - "C": {c200, c120, c111, c110, c102, c101, c100, c021, c020, c010}, - "D": {d100, d120}, - "E": {e100, e101}, - "G": {g130, g140, g150, g160, g170, g180}, - "H": {h130, h140, h150, h160, h170, h180}, - "I": {i130, i140, i150, i160, i170, i180}, - }, - } + arch := NewArchive[*customRel, *customDep]() + arch.AddReleases( + a100, a110, a111, a120, a121, + b131, b130, b121, b120, b111, b110, b100, + c200, c120, c111, c110, c102, c101, c100, c021, c020, c010, + d100, d120, + e100, e101, + g130, g140, g150, g160, g170, g180, + h130, h140, h150, h160, h170, h180, + i130, i140, i150, i160, i170, i180, + ) a130 := rel("A", "1.3.0", deps()) r0 := arch.Resolve(a130) // Non-existent in archive @@ -179,4 +182,7 @@ func TestResolver(t *testing.T) { case <-time.After(time.Second): require.FailNow(t, "test didn't complete in the allocated time") } + + r7 := arch.Resolve(e101) + require.Nil(t, r7) } From 8711f89cb5bf7fe4e23993489d5e1f38d7bae9c7 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 21 Nov 2023 23:45:56 +0100 Subject: [PATCH 2/5] Renamed Archive -> Resolver --- resolver.go | 21 +++++++++++---------- resolver_test.go | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/resolver.go b/resolver.go index 14ee94d..c3ed979 100644 --- a/resolver.go +++ b/resolver.go @@ -44,26 +44,27 @@ func (set Releases[R, D]) SortDescent() { }) } -// Archive contains all Releases set to consider for dependency resolution -type Archive[R Release[D], D Dependency] struct { +// Resolver is a container with references to all Releases to consider for +// dependency resolution +type Resolver[R Release[D], D Dependency] struct { releases map[string]Releases[R, D] } -// NewArchive creates a new archive -func NewArchive[R Release[D], D Dependency]() *Archive[R, D] { - return &Archive[R, D]{ +// NewResolver creates a new archive +func NewResolver[R Release[D], D Dependency]() *Resolver[R, D] { + return &Resolver[R, D]{ releases: map[string]Releases[R, D]{}, } } // AddRelease adds a release to this archive -func (ar *Archive[R, D]) AddRelease(rel R) { +func (ar *Resolver[R, D]) AddRelease(rel R) { relName := rel.GetName() ar.releases[relName] = append(ar.releases[relName], rel) } // AddReleases adds all the releases to this archive -func (ar *Archive[R, D]) AddReleases(rels ...R) { +func (ar *Resolver[R, D]) AddReleases(rels ...R) { for _, rel := range rels { relName := rel.GetName() ar.releases[relName] = append(ar.releases[relName], rel) @@ -71,8 +72,8 @@ func (ar *Archive[R, D]) AddReleases(rels ...R) { } // Resolve will try to depp-resolve dependencies from the Release passed as -// arguent using a backtracking algorithm. -func (ar *Archive[R, D]) Resolve(release R) Releases[R, D] { +// arguent using a backtracking algorithm. This function is NOT thread-safe. +func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] { // Initial empty state of the resolver solution := map[string]R{} depsToProcess := []D{} @@ -96,7 +97,7 @@ func hashDependency[D Dependency](dep D) dependencyHash { return dependencyHash(dep.GetName() + "/" + dep.GetConstraint().String()) } -func (ar *Archive[R, D]) resolve(solution map[string]R, depsToProcess []D, problematicDeps map[dependencyHash]int) Releases[R, D] { +func (ar *Resolver[R, D]) resolve(solution map[string]R, depsToProcess []D, problematicDeps map[dependencyHash]int) Releases[R, D] { debug("deps to process: %s", depsToProcess) if len(depsToProcess) == 0 { debug("All dependencies have been resolved.") diff --git a/resolver_test.go b/resolver_test.go index e33a66f..7d405ff 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -123,7 +123,7 @@ func TestResolver(t *testing.T) { i160 := rel("I", "1.6.0", deps()) i170 := rel("I", "1.7.0", deps()) i180 := rel("I", "1.8.0", deps()) - arch := NewArchive[*customRel, *customDep]() + arch := NewResolver[*customRel, *customDep]() arch.AddReleases( a100, a110, a111, a120, a121, b131, b130, b121, b120, b111, b110, b100, From 00b35162796fb149ee923fa693abf8503fbac37b Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 21 Nov 2023 23:47:32 +0100 Subject: [PATCH 3/5] Simplified loop --- resolver.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/resolver.go b/resolver.go index c3ed979..fac1210 100644 --- a/resolver.go +++ b/resolver.go @@ -130,21 +130,17 @@ func (ar *Resolver[R, D]) resolve(solution map[string]R, depsToProcess []D, prob releases.SortDescent() debug("releases matching criteria: %s", releases) +backtracking_loop: for _, release := range releases { - deps := release.GetDependencies() - debug("try with %s %s", release, deps) - - missingDep := false - for _, dep := range deps { - if _, ok := ar.releases[dep.GetName()]; !ok { - debug("%s did not work, becuase his dependency %s does not exists", release, dep.GetName()) - missingDep = true - break + releaseDeps := release.GetDependencies() + debug("try with %s %s", release, releaseDeps) + + for _, releaseDep := range releaseDeps { + if _, ok := ar.releases[releaseDep.GetName()]; !ok { + debug("%s did not work, becuase his dependency %s does not exists", release, releaseDep.GetName()) + continue backtracking_loop } } - if missingDep { - continue - } solution[depName] = release newDepsToProcess := append(depsToProcess[1:], deps...) From a325c5222911fefe26aef5ac398a68ee19ed969e Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 21 Nov 2023 23:48:47 +0100 Subject: [PATCH 4/5] Made Resolver state variables as fields --- resolver.go | 61 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/resolver.go b/resolver.go index fac1210..4344075 100644 --- a/resolver.go +++ b/resolver.go @@ -48,6 +48,11 @@ func (set Releases[R, D]) SortDescent() { // dependency resolution type Resolver[R Release[D], D Dependency] struct { releases map[string]Releases[R, D] + + // resolver state + solution map[string]R + depsToProcess []D + problematicDeps map[dependencyHash]int } // NewResolver creates a new archive @@ -75,9 +80,9 @@ func (ar *Resolver[R, D]) AddReleases(rels ...R) { // arguent using a backtracking algorithm. This function is NOT thread-safe. func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] { // Initial empty state of the resolver - solution := map[string]R{} - depsToProcess := []D{} - problematicDeps := map[dependencyHash]int{} + ar.solution = map[string]R{} + ar.depsToProcess = []D{} + ar.problematicDeps = map[dependencyHash]int{} // Check if the release is in the archive if len(ar.releases[release.GetName()].FilterBy(&Equals{Version: release.GetVersion()})) == 0 { @@ -86,9 +91,9 @@ func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] { // Add the requested release to the solution and proceed // with the dependencies resolution - solution[release.GetName()] = release - depsToProcess = append(depsToProcess, release.GetDependencies()...) - return ar.resolve(solution, depsToProcess, problematicDeps) + ar.solution[release.GetName()] = release + ar.depsToProcess = append(ar.depsToProcess, release.GetDependencies()...) + return ar.resolve() } type dependencyHash string @@ -97,39 +102,45 @@ func hashDependency[D Dependency](dep D) dependencyHash { return dependencyHash(dep.GetName() + "/" + dep.GetConstraint().String()) } -func (ar *Resolver[R, D]) resolve(solution map[string]R, depsToProcess []D, problematicDeps map[dependencyHash]int) Releases[R, D] { - debug("deps to process: %s", depsToProcess) - if len(depsToProcess) == 0 { +func (ar *Resolver[R, D]) resolve() Releases[R, D] { + debug("deps to process: %s", ar.depsToProcess) + if len(ar.depsToProcess) == 0 { debug("All dependencies have been resolved.") var res Releases[R, D] - for _, v := range solution { + for _, v := range ar.solution { res = append(res, v) } return res } // Pick the first dependency in the deps to process - dep := depsToProcess[0] + dep := ar.depsToProcess[0] depName := dep.GetName() debug("Considering next dep: %s", depName) // If a release is already picked in the solution check if it match the dep - if existingRelease, has := solution[depName]; has { + if existingRelease, has := ar.solution[depName]; has { if dep.GetConstraint().Match(existingRelease.GetVersion()) { debug("%s already in solution and matching", existingRelease) - return ar.resolve(solution, depsToProcess[1:], problematicDeps) + oldDepsToProcess := ar.depsToProcess + ar.depsToProcess = ar.depsToProcess[1:] + if res := ar.resolve(); res != nil { + return res + } + ar.depsToProcess = oldDepsToProcess + return nil } debug("%s already in solution do not match... rollingback", existingRelease) return nil } // Otherwise start backtracking the dependency - releases := ar.releases[dep.GetName()].FilterBy(dep.GetConstraint()) + releases := ar.releases[depName].FilterBy(dep.GetConstraint()) // Consider the latest versions first releases.SortDescent() - debug("releases matching criteria: %s", releases) + backtracking_loop: for _, release := range releases { releaseDeps := release.GetDependencies() @@ -142,21 +153,23 @@ backtracking_loop: } } - solution[depName] = release - newDepsToProcess := append(depsToProcess[1:], deps...) + ar.solution[depName] = release + oldDepsToProcess := ar.depsToProcess + ar.depsToProcess = append(ar.depsToProcess[1:], releaseDeps...) // bubble up problematics deps so they are processed first - sort.Slice(newDepsToProcess, func(i, j int) bool { - ci := hashDependency(newDepsToProcess[i]) - cj := hashDependency(newDepsToProcess[j]) - return problematicDeps[ci] > problematicDeps[cj] + sort.Slice(ar.depsToProcess, func(i, j int) bool { + ci := hashDependency(ar.depsToProcess[i]) + cj := hashDependency(ar.depsToProcess[j]) + return ar.problematicDeps[ci] > ar.problematicDeps[cj] }) - if res := ar.resolve(solution, newDepsToProcess, problematicDeps); res != nil { + if res := ar.resolve(); res != nil { return res } + ar.depsToProcess = oldDepsToProcess debug("%s did not work...", release) - delete(solution, depName) + delete(ar.solution, depName) } - problematicDeps[hashDependency(dep)]++ + ar.problematicDeps[hashDependency(dep)]++ return nil } From ae9c7ba27be36f96550ba016f4c530d6a516d2cd Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Tue, 21 Nov 2023 23:51:15 +0100 Subject: [PATCH 5/5] coverage to 100% again --- resolver_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resolver_test.go b/resolver_test.go index 7d405ff..e59ef36 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -129,11 +129,12 @@ func TestResolver(t *testing.T) { b131, b130, b121, b120, b111, b110, b100, c200, c120, c111, c110, c102, c101, c100, c021, c020, c010, d100, d120, - e100, e101, g130, g140, g150, g160, g170, g180, h130, h140, h150, h160, h170, h180, i130, i140, i150, i160, i170, i180, ) + arch.AddRelease(e100) // use this method for 100% code coverage + arch.AddRelease(e101) a130 := rel("A", "1.3.0", deps()) r0 := arch.Resolve(a130) // Non-existent in archive