Skip to content

Commit

Permalink
Merge pull request #19 from bugst/templated_resolver
Browse files Browse the repository at this point in the history
Generics-based resolver
  • Loading branch information
cmaglie authored Nov 23, 2023
2 parents e8b9735 + ae9c7ba commit 391e859
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 82 deletions.
155 changes: 91 additions & 64 deletions resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -41,108 +38,138 @@ 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
// 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]

// resolver state
solution map[string]R
depsToProcess []D
problematicDeps map[dependencyHash]int
}

// 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(),
// 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]{},
}
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 *Resolver[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 *Resolver[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. This function is NOT thread-safe.
func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] {
// Initial empty state of the resolver
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 {
return nil
}

// Add the requested release to the solution and proceed
// with the dependencies resolution
ar.solution[release.GetName()] = release
ar.depsToProcess = append(ar.depsToProcess, release.GetDependencies()...)
return ar.resolve()
}

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 {
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.")
res := []Release{}
for _, v := range solution {
var res Releases[R, D]
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 match(existingRelease, dep) {
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)
releases := ar.releases[depName].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)

backtracking_loop:
for _, release := range releases {
deps := release.GetDependencies()
debug("try with %s %s", release, deps)
releaseDeps := release.GetDependencies()
debug("try with %s %s", release, releaseDeps)

if missingDep := findMissingDeps(deps); missingDep != nil {
debug("%s did not work, becuase his dependency %s does not exists", release, missingDep.GetName())
continue
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
}
}

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 {
return problematicDeps[newDepsToProcess[i]] > problematicDeps[newDepsToProcess[j]]
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[dep]++
ar.problematicDeps[hashDependency(dep)]++
return nil
}
43 changes: 25 additions & 18 deletions resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -34,26 +36,28 @@ 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
}

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 {
Expand All @@ -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}
}

Expand Down Expand Up @@ -119,18 +123,18 @@ 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 := NewResolver[*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,
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
Expand Down Expand Up @@ -179,4 +183,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)
}

0 comments on commit 391e859

Please sign in to comment.