From 71d23978901580e658d5b501905995ebf604deba Mon Sep 17 00:00:00 2001 From: perdasilva Date: Thu, 15 Dec 2022 15:37:59 +0100 Subject: [PATCH 1/3] refactor packages Signed-off-by: perdasilva --- cmd/dimacs/cmd.go | 4 +- cmd/dimacs/dimacs_constraints.go | 29 ++- cmd/dimacs/dimacs_source.go | 15 +- cmd/sudoku/cmd.go | 4 +- cmd/sudoku/sudoku.go | 51 +++-- pkg/entitysource/entity.go | 35 ---- pkg/entitysource/entity_test.go | 26 --- pkg/ext/olm/constraints.go | 124 ++++++------ pkg/ext/olm/constraints_test.go | 97 ++++----- .../cache_entity_source.go} | 14 +- pkg/input/entity.go | 21 ++ pkg/{entitysource => input}/entity_source.go | 6 +- .../entity_source_test.go | 60 +++--- pkg/input/entity_test.go | 27 +++ pkg/{entitysource => input}/query.go | 14 +- pkg/input/solver.go | 59 ++++++ pkg/{solver => input}/solver_suite_test.go | 2 +- pkg/input/solver_test.go | 183 +++++++++++++++++ .../source_suite_test.go | 2 +- pkg/input/variable_source.go | 38 ++++ pkg/{sat => solver}/bench_test.go | 2 +- pkg/{sat => solver}/constraints.go | 2 +- pkg/{sat => solver}/constraints_test.go | 2 +- pkg/{sat => solver}/doc.go | 2 +- pkg/{sat => solver}/lit_mapping.go | 2 +- pkg/{sat => solver}/search.go | 2 +- pkg/{sat => solver}/search_test.go | 2 +- pkg/{sat => solver}/solve.go | 2 +- pkg/{sat => solver}/solve_test.go | 2 +- pkg/solver/solver.go | 67 ------- pkg/solver/solver_test.go | 184 ------------------ pkg/{sat => solver}/tracer.go | 2 +- pkg/{sat => solver}/variable.go | 2 +- pkg/{sat => solver}/zz_search_test.go | 2 +- pkg/variablesource/variable.go | 30 --- pkg/variablesource/variable_source.go | 13 -- 36 files changed, 554 insertions(+), 575 deletions(-) delete mode 100644 pkg/entitysource/entity.go delete mode 100644 pkg/entitysource/entity_test.go rename pkg/{entitysource/cache_querier.go => input/cache_entity_source.go} (77%) create mode 100644 pkg/input/entity.go rename pkg/{entitysource => input}/entity_source.go (88%) rename pkg/{entitysource => input}/entity_source_test.go (68%) create mode 100644 pkg/input/entity_test.go rename pkg/{entitysource => input}/query.go (81%) create mode 100644 pkg/input/solver.go rename pkg/{solver => input}/solver_suite_test.go (89%) create mode 100644 pkg/input/solver_test.go rename pkg/{entitysource => input}/source_suite_test.go (87%) create mode 100644 pkg/input/variable_source.go rename pkg/{sat => solver}/bench_test.go (99%) rename pkg/{sat => solver}/constraints.go (99%) rename pkg/{sat => solver}/constraints_test.go (97%) rename pkg/{sat => solver}/doc.go (87%) rename pkg/{sat => solver}/lit_mapping.go (99%) rename pkg/{sat => solver}/search.go (99%) rename pkg/{sat => solver}/search_test.go (99%) rename pkg/{sat => solver}/solve.go (99%) rename pkg/{sat => solver}/solve_test.go (99%) delete mode 100644 pkg/solver/solver.go delete mode 100644 pkg/solver/solver_test.go rename pkg/{sat => solver}/tracer.go (97%) rename pkg/{sat => solver}/variable.go (98%) rename pkg/{sat => solver}/zz_search_test.go (99%) delete mode 100644 pkg/variablesource/variable.go delete mode 100644 pkg/variablesource/variable_source.go diff --git a/cmd/dimacs/cmd.go b/cmd/dimacs/cmd.go index acef3b3..704900e 100644 --- a/cmd/dimacs/cmd.go +++ b/cmd/dimacs/cmd.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/input" ) func NewDimacsCommand() *cobra.Command { @@ -53,7 +53,7 @@ func solve(path string) error { } // build solver - so, err := solver.NewDeppySolver(NewDimacsEntitySource(dimacs), NewDimacsVariableSource(dimacs)) + so, err := input.NewDeppySolver(NewDimacsEntitySource(dimacs), NewDimacsVariableSource(dimacs)) if err != nil { return err } diff --git a/cmd/dimacs/dimacs_constraints.go b/cmd/dimacs/dimacs_constraints.go index 38822da..775d88f 100644 --- a/cmd/dimacs/dimacs_constraints.go +++ b/cmd/dimacs/dimacs_constraints.go @@ -4,12 +4,11 @@ import ( "context" "strings" - "github.com/operator-framework/deppy/pkg/entitysource" - "github.com/operator-framework/deppy/pkg/sat" - "github.com/operator-framework/deppy/pkg/variablesource" + "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/solver" ) -var _ variablesource.VariableSource = &ConstraintGenerator{} +var _ input.VariableSource = &ConstraintGenerator{} type ConstraintGenerator struct { dimacs *Dimacs @@ -21,13 +20,13 @@ func NewDimacsVariableSource(dimacs *Dimacs) *ConstraintGenerator { } } -func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource entitysource.EntitySource) ([]sat.Variable, error) { - varMap := make(map[entitysource.EntityID]*variablesource.Variable, len(d.dimacs.variables)) - variables := make([]sat.Variable, 0, len(d.dimacs.variables)) - if err := entitySource.Iterate(ctx, func(entity *entitysource.Entity) error { - variable := variablesource.NewVariable(sat.Identifier(entity.ID())) +func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { + varMap := make(map[solver.Identifier]*input.SimpleVariable, len(d.dimacs.variables)) + variables := make([]solver.Variable, 0, len(d.dimacs.variables)) + if err := entitySource.Iterate(ctx, func(entity *input.Entity) error { + variable := input.NewSimpleVariable(entity.Identifier()) variables = append(variables, variable) - varMap[entity.ID()] = variable + varMap[entity.Identifier()] = variable return nil }); err != nil { return nil, err @@ -43,23 +42,23 @@ func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource ent if len(terms) == 1 { // TODO: check constraints haven't already been added to the variable - variable := varMap[entitysource.EntityID(strings.TrimPrefix(first, "-"))] + variable := varMap[solver.Identifier(strings.TrimPrefix(first, "-"))] if strings.HasPrefix(first, "-") { - variable.AddConstraint(sat.Not()) + variable.AddConstraint(solver.Not()) } else { // TODO: is this the right constraint here? (given that its an achoring constraint?) - variable.AddConstraint(sat.Mandatory()) + variable.AddConstraint(solver.Mandatory()) } continue } for i := 1; i < len(terms); i++ { - variable := varMap[entitysource.EntityID(strings.TrimPrefix(first, "-"))] + variable := varMap[solver.Identifier(strings.TrimPrefix(first, "-"))] second := terms[i] negSubject := strings.HasPrefix(first, "-") negOperand := strings.HasPrefix(second, "-") // TODO: this Or constraint is hacky as hell - variable.AddConstraint(sat.Or(sat.Identifier(strings.TrimPrefix(second, "-")), negSubject, negOperand)) + variable.AddConstraint(solver.Or(solver.Identifier(strings.TrimPrefix(second, "-")), negSubject, negOperand)) first = second } } diff --git a/cmd/dimacs/dimacs_source.go b/cmd/dimacs/dimacs_source.go index 80ee624..fcd8c2d 100644 --- a/cmd/dimacs/dimacs_source.go +++ b/cmd/dimacs/dimacs_source.go @@ -1,23 +1,24 @@ package dimacs import ( - "github.com/operator-framework/deppy/pkg/entitysource" + "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/solver" ) -var _ entitysource.EntitySource = &EntitySource{} +var _ input.EntitySource = &EntitySource{} type EntitySource struct { - *entitysource.CacheEntitySource + *input.CacheEntitySource } func NewDimacsEntitySource(dimacs *Dimacs) *EntitySource { - entities := make(map[entitysource.EntityID]entitysource.Entity, len(dimacs.Variables())) + entities := make(map[solver.Identifier]input.Entity, len(dimacs.Variables())) for _, variable := range dimacs.Variables() { - id := entitysource.EntityID(variable) - entities[id] = *entitysource.NewEntity(entitysource.EntityID(variable), nil) + id := solver.Identifier(variable) + entities[id] = *input.NewEntity(id, nil) } return &EntitySource{ - CacheEntitySource: entitysource.NewCacheQuerier(entities), + CacheEntitySource: input.NewCacheQuerier(entities), } } diff --git a/cmd/sudoku/cmd.go b/cmd/sudoku/cmd.go index 35a9172..7ae843a 100644 --- a/cmd/sudoku/cmd.go +++ b/cmd/sudoku/cmd.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/input" ) func NewSudokuCommand() *cobra.Command { @@ -22,7 +22,7 @@ func NewSudokuCommand() *cobra.Command { func solve() error { // build solver sudoku := NewSudoku() - so, err := solver.NewDeppySolver(sudoku, sudoku) + so, err := input.NewDeppySolver(sudoku, sudoku) if err != nil { return err } diff --git a/cmd/sudoku/sudoku.go b/cmd/sudoku/sudoku.go index 6670f5e..404d9d5 100644 --- a/cmd/sudoku/sudoku.go +++ b/cmd/sudoku/sudoku.go @@ -7,32 +7,31 @@ import ( "strconv" "time" - "github.com/operator-framework/deppy/pkg/entitysource" - "github.com/operator-framework/deppy/pkg/sat" - "github.com/operator-framework/deppy/pkg/variablesource" + "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/solver" ) -var _ entitysource.EntitySource = &Sudoku{} -var _ variablesource.VariableSource = &Sudoku{} +var _ input.EntitySource = &Sudoku{} +var _ input.VariableSource = &Sudoku{} type Sudoku struct { - *entitysource.CacheEntitySource + *input.CacheEntitySource } -func GetID(row int, col int, num int) entitysource.EntityID { +func GetID(row int, col int, num int) solver.Identifier { n := num n += col * 9 n += row * 81 - return entitysource.EntityID(fmt.Sprintf("%03d", n)) + return solver.Identifier(fmt.Sprintf("%03d", n)) } func NewSudoku() *Sudoku { - var entities = make(map[entitysource.EntityID]entitysource.Entity, 9*9*9) + var entities = make(map[solver.Identifier]input.Entity, 9*9*9) for row := 0; row < 9; row++ { for col := 0; col < 9; col++ { for num := 0; num < 9; num++ { id := GetID(row, col, num) - entities[id] = *entitysource.NewEntity(id, map[string]string{ + entities[id] = *input.NewEntity(id, map[string]string{ "row": strconv.Itoa(row), "col": strconv.Itoa(col), "num": strconv.Itoa(num), @@ -41,21 +40,21 @@ func NewSudoku() *Sudoku { } } return &Sudoku{ - CacheEntitySource: entitysource.NewCacheQuerier(entities), + CacheEntitySource: input.NewCacheQuerier(entities), } } -func (s Sudoku) GetVariables(ctx context.Context, _ entitysource.EntitySource) ([]sat.Variable, error) { +func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]solver.Variable, error) { // adapted from: https://github.com/go-air/gini/blob/871d828a26852598db2b88f436549634ba9533ff/sudoku_test.go#L10 - variables := make(map[sat.Identifier]*variablesource.Variable, 0) - inorder := make([]sat.Variable, 0) + variables := make(map[solver.Identifier]*input.SimpleVariable, 0) + inorder := make([]solver.Variable, 0) rand.Seed(time.Now().UnixNano()) // create variables for all number in all positions of the board for row := 0; row < 9; row++ { for col := 0; col < 9; col++ { for n := 0; n < 9; n++ { - variable := variablesource.NewVariable(sat.Identifier(GetID(row, col, n))) + variable := input.NewSimpleVariable(solver.Identifier(GetID(row, col, n))) variables[variable.Identifier()] = variable inorder = append(inorder, variable) } @@ -65,16 +64,16 @@ func (s Sudoku) GetVariables(ctx context.Context, _ entitysource.EntitySource) ( // add a clause stating that every position on the board has a number for row := 0; row < 9; row++ { for col := 0; col < 9; col++ { - ids := make([]sat.Identifier, 9) + ids := make([]solver.Identifier, 9) for n := 0; n < 9; n++ { - ids[n] = sat.Identifier(GetID(row, col, n)) + ids[n] = solver.Identifier(GetID(row, col, n)) } // randomize order to create new sudoku boards every run rand.Shuffle(len(ids), func(i, j int) { ids[i], ids[j] = ids[j], ids[i] }) // create clause that the particular position has a number - varID := sat.Identifier(fmt.Sprintf("%d-%d has a number", row, col)) - variable := variablesource.NewVariable(varID, sat.Mandatory(), sat.Dependency(ids...)) + varID := solver.Identifier(fmt.Sprintf("%d-%d has a number", row, col)) + variable := input.NewSimpleVariable(varID, solver.Mandatory(), solver.Dependency(ids...)) variables[varID] = variable inorder = append(inorder, variable) } @@ -84,10 +83,10 @@ func (s Sudoku) GetVariables(ctx context.Context, _ entitysource.EntitySource) ( for n := 0; n < 9; n++ { for row := 0; row < 9; row++ { for colA := 0; colA < 9; colA++ { - idA := sat.Identifier(GetID(row, colA, n)) + idA := solver.Identifier(GetID(row, colA, n)) variable := variables[idA] for colB := colA + 1; colB < 9; colB++ { - variable.AddConstraint(sat.Conflict(sat.Identifier(GetID(row, colB, n)))) + variable.AddConstraint(solver.Conflict(solver.Identifier(GetID(row, colB, n)))) } } } @@ -97,10 +96,10 @@ func (s Sudoku) GetVariables(ctx context.Context, _ entitysource.EntitySource) ( for n := 0; n < 9; n++ { for col := 0; col < 9; col++ { for rowA := 0; rowA < 9; rowA++ { - idA := sat.Identifier(GetID(rowA, col, n)) + idA := solver.Identifier(GetID(rowA, col, n)) variable := variables[idA] for rowB := rowA + 1; rowB < 9; rowB++ { - variable.AddConstraint(sat.Conflict(sat.Identifier(GetID(rowB, col, n)))) + variable.AddConstraint(solver.Conflict(solver.Identifier(GetID(rowB, col, n)))) } } } @@ -114,12 +113,12 @@ func (s Sudoku) GetVariables(ctx context.Context, _ entitysource.EntitySource) ( // all numbers for n := 0; n < 9; n++ { for i, offA := range offs { - idA := sat.Identifier(GetID(x+offA.x, y+offA.y, n)) + idA := solver.Identifier(GetID(x+offA.x, y+offA.y, n)) variable := variables[idA] for j := i + 1; j < len(offs); j++ { offB := offs[j] - idB := sat.Identifier(GetID(x+offB.x, y+offB.y, n)) - variable.AddConstraint(sat.Conflict(idB)) + idB := solver.Identifier(GetID(x+offB.x, y+offB.y, n)) + variable.AddConstraint(solver.Conflict(idB)) } } } diff --git a/pkg/entitysource/entity.go b/pkg/entitysource/entity.go deleted file mode 100644 index c5bfd0a..0000000 --- a/pkg/entitysource/entity.go +++ /dev/null @@ -1,35 +0,0 @@ -package entitysource - -import "fmt" - -type EntityID string - -type EntityPropertyNotFoundError string - -func (p EntityPropertyNotFoundError) Error() string { - return fmt.Sprintf("Property '(%s)' Not Found", string(p)) -} - -type Entity struct { - id EntityID - properties map[string]string -} - -func NewEntity(id EntityID, properties map[string]string) *Entity { - return &Entity{ - id: id, - properties: properties, - } -} - -func (e *Entity) ID() EntityID { - return e.id -} - -func (e *Entity) GetProperty(key string) (string, error) { - value, ok := e.properties[key] - if !ok { - return "", EntityPropertyNotFoundError(key) - } - return value, nil -} diff --git a/pkg/entitysource/entity_test.go b/pkg/entitysource/entity_test.go deleted file mode 100644 index ec65272..0000000 --- a/pkg/entitysource/entity_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package entitysource_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/entitysource" -) - -var _ = Describe("Entity", func() { - It("stores id and properties", func() { - entity := entitysource.NewEntity("id", map[string]string{"prop": "value"}) - Expect(entity.ID()).To(Equal(entitysource.EntityID("id"))) - - value, err := entity.GetProperty("prop") - Expect(err).To(BeNil()) - Expect(value).To(Equal("value")) - }) - - It("returns not found error when property is not found", func() { - entity := entitysource.NewEntity("id", map[string]string{"foo": "value"}) - value, err := entity.GetProperty("bar") - Expect(value).To(Equal("")) - Expect(err).To(MatchError(entitysource.EntityPropertyNotFoundError("bar"))) - }) -}) diff --git a/pkg/ext/olm/constraints.go b/pkg/ext/olm/constraints.go index 46b861c..c09de54 100644 --- a/pkg/ext/olm/constraints.go +++ b/pkg/ext/olm/constraints.go @@ -9,9 +9,9 @@ import ( "github.com/blang/semver/v4" "github.com/tidwall/gjson" - "github.com/operator-framework/deppy/pkg/entitysource" - "github.com/operator-framework/deppy/pkg/sat" - "github.com/operator-framework/deppy/pkg/variablesource" + "github.com/operator-framework/deppy/pkg/solver" + + "github.com/operator-framework/deppy/pkg/input" ) const ( @@ -24,7 +24,7 @@ const ( propertyNotFound = "" ) -var _ variablesource.VariableSource = &requirePackage{} +var _ input.VariableSource = &requirePackage{} type requirePackage struct { packageName string @@ -32,8 +32,8 @@ type requirePackage struct { channel string } -func (r *requirePackage) GetVariables(ctx context.Context, entitySource entitysource.EntitySource) ([]sat.Variable, error) { - resultSet, err := entitySource.Filter(ctx, entitysource.And( +func (r *requirePackage) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { + resultSet, err := entitySource.Filter(ctx, input.And( withPackageName(r.packageName), withinVersion(r.versionRange), withChannel(r.channel))) @@ -42,13 +42,13 @@ func (r *requirePackage) GetVariables(ctx context.Context, entitySource entityso } ids := resultSet.Sort(byChannelAndVersion).CollectIds() subject := subject("require", r.packageName, r.versionRange, r.channel) - return []sat.Variable{ - variablesource.NewVariable(subject, sat.Mandatory(), sat.Dependency(toSatIdentifier(ids...)...)), + return []solver.Variable{ + input.NewSimpleVariable(subject, solver.Mandatory(), solver.Dependency(ids...)), }, nil } // RequirePackage creates a constraint generator to describe that a package is wanted for installation -func RequirePackage(packageName string, versionRange string, channel string) variablesource.VariableSource { +func RequirePackage(packageName string, versionRange string, channel string) input.VariableSource { return &requirePackage{ packageName: packageName, versionRange: versionRange, @@ -56,30 +56,30 @@ func RequirePackage(packageName string, versionRange string, channel string) var } } -var _ variablesource.VariableSource = &uniqueness{} +var _ input.VariableSource = &uniqueness{} -type subjectFormatFn func(key string) sat.Identifier +type subjectFormatFn func(key string) solver.Identifier type uniqueness struct { subject subjectFormatFn - groupByFn entitysource.GroupByFunction + groupByFn input.GroupByFunction } -func (u *uniqueness) GetVariables(ctx context.Context, entitySource entitysource.EntitySource) ([]sat.Variable, error) { +func (u *uniqueness) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { resultSet, err := entitySource.GroupBy(ctx, u.groupByFn) if err != nil || len(resultSet) == 0 { return nil, err } - variables := make([]sat.Variable, 0, len(resultSet)) + variables := make([]solver.Variable, 0, len(resultSet)) for key, entities := range resultSet { - ids := toSatIdentifier(entities.Sort(byChannelAndVersion).CollectIds()...) - variables = append(variables, variablesource.NewVariable(u.subject(key), sat.AtMost(1, ids...))) + ids := entities.Sort(byChannelAndVersion).CollectIds() + variables = append(variables, input.NewSimpleVariable(u.subject(key), solver.AtMost(1, ids...))) } return variables, nil } // GVKUniqueness generates constraints describing that only a single bundle / gvk can be selected -func GVKUniqueness() variablesource.VariableSource { +func GVKUniqueness() input.VariableSource { return &uniqueness{ subject: uniquenessSubjectFormat, groupByFn: gvkGroupFunction, @@ -87,36 +87,36 @@ func GVKUniqueness() variablesource.VariableSource { } // PackageUniqueness generates constraints describing that only a single bundle / package can be selected -func PackageUniqueness() variablesource.VariableSource { +func PackageUniqueness() input.VariableSource { return &uniqueness{ subject: uniquenessSubjectFormat, groupByFn: packageGroupFunction, } } -func uniquenessSubjectFormat(key string) sat.Identifier { - return sat.IdentifierFromString(fmt.Sprintf("%s uniqueness", key)) +func uniquenessSubjectFormat(key string) solver.Identifier { + return solver.IdentifierFromString(fmt.Sprintf("%s uniqueness", key)) } -var _ variablesource.VariableSource = &packageDependency{} +var _ input.VariableSource = &packageDependency{} type packageDependency struct { - subject sat.Identifier + subject solver.Identifier packageName string versionRange string } -func (p *packageDependency) GetVariables(ctx context.Context, entitySource entitysource.EntitySource) ([]sat.Variable, error) { - entities, err := entitySource.Filter(ctx, entitysource.And(withPackageName(p.packageName), withinVersion(p.versionRange))) +func (p *packageDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { + entities, err := entitySource.Filter(ctx, input.And(withPackageName(p.packageName), withinVersion(p.versionRange))) if err != nil || len(entities) == 0 { return nil, err } - ids := toSatIdentifier(entities.Sort(byChannelAndVersion).CollectIds()...) - return []sat.Variable{variablesource.NewVariable(p.subject, sat.Dependency(ids...))}, nil + ids := entities.Sort(byChannelAndVersion).CollectIds() + return []solver.Variable{input.NewSimpleVariable(p.subject, solver.Dependency(ids...))}, nil } // PackageDependency generates constraints to describe a package's dependency on another package -func PackageDependency(subject sat.Identifier, packageName string, versionRange string) variablesource.VariableSource { +func PackageDependency(subject solver.Identifier, packageName string, versionRange string) input.VariableSource { return &packageDependency{ subject: subject, packageName: packageName, @@ -124,26 +124,26 @@ func PackageDependency(subject sat.Identifier, packageName string, versionRange } } -var _ variablesource.VariableSource = &gvkDependency{} +var _ input.VariableSource = &gvkDependency{} type gvkDependency struct { - subject sat.Identifier + subject solver.Identifier group string version string kind string } -func (g *gvkDependency) GetVariables(ctx context.Context, entitySource entitysource.EntitySource) ([]sat.Variable, error) { - entities, err := entitySource.Filter(ctx, entitysource.And(withExportsGVK(g.group, g.version, g.kind))) +func (g *gvkDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { + entities, err := entitySource.Filter(ctx, input.And(withExportsGVK(g.group, g.version, g.kind))) if err != nil || len(entities) == 0 { return nil, err } - ids := toSatIdentifier(entities.Sort(byChannelAndVersion).CollectIds()...) - return []sat.Variable{variablesource.NewVariable(g.subject, sat.Dependency(ids...))}, nil + ids := entities.Sort(byChannelAndVersion).CollectIds() + return []solver.Variable{input.NewSimpleVariable(g.subject, solver.Dependency(ids...))}, nil } // GVKDependency generates constraints to describe a package's dependency on a gvk -func GVKDependency(subject sat.Identifier, group string, version string, kind string) variablesource.VariableSource { +func GVKDependency(subject solver.Identifier, group string, version string, kind string) input.VariableSource { return &gvkDependency{ subject: subject, group: group, @@ -152,18 +152,18 @@ func GVKDependency(subject sat.Identifier, group string, version string, kind st } } -func withPackageName(packageName string) entitysource.Predicate { - return func(entity *entitysource.Entity) bool { - if pkgName, err := entity.GetProperty(PropertyOLMPackageName); err == nil { +func withPackageName(packageName string) input.Predicate { + return func(entity *input.Entity) bool { + if pkgName, ok := entity.Properties[PropertyOLMPackageName]; ok { return pkgName == packageName } return false } } -func withinVersion(semverRange string) entitysource.Predicate { - return func(entity *entitysource.Entity) bool { - if v, err := entity.GetProperty(PropertyOLMVersion); err == nil { +func withinVersion(semverRange string) input.Predicate { + return func(entity *input.Entity) bool { + if v, ok := entity.Properties[PropertyOLMVersion]; ok { vrange, err := semver.ParseRange(semverRange) if err != nil { return false @@ -178,21 +178,21 @@ func withinVersion(semverRange string) entitysource.Predicate { } } -func withChannel(channel string) entitysource.Predicate { - return func(entity *entitysource.Entity) bool { +func withChannel(channel string) input.Predicate { + return func(entity *input.Entity) bool { if channel == "" { return true } - if c, err := entity.GetProperty(PropertyOLMChannel); err == nil { + if c, ok := entity.Properties[PropertyOLMChannel]; ok { return c == channel } return false } } -func withExportsGVK(group string, version string, kind string) entitysource.Predicate { - return func(entity *entitysource.Entity) bool { - if g, err := entity.GetProperty(PropertyOLMGVK); err == nil { +func withExportsGVK(group string, version string, kind string) input.Predicate { + return func(entity *input.Entity) bool { + if g, ok := entity.Properties[PropertyOLMGVK]; ok { for _, gvk := range gjson.Parse(g).Array() { if gjson.Get(gvk.String(), "group").String() == group && gjson.Get(gvk.String(), "version").String() == version && gjson.Get(gvk.String(), "kind").String() == kind { return true @@ -207,8 +207,8 @@ func withExportsGVK(group string, version string, kind string) entitysource.Pred // package, channel (default channel at the head), and inverse version (higher versions on top) // if a property does not exist for one of the entities, the one missing the property is pushed down // if both entities are missing the same property they are ordered by id -func byChannelAndVersion(e1 *entitysource.Entity, e2 *entitysource.Entity) bool { - idOrder := e1.ID() < e2.ID() +func byChannelAndVersion(e1 *input.Entity, e2 *input.Entity) bool { + idOrder := e1.Identifier() < e2.Identifier() // first sort package lexical order pkgOrder := compareProperty(getPropertyOrNotFound(e1, PropertyOLMPackageName), getPropertyOrNotFound(e2, PropertyOLMPackageName)) @@ -243,7 +243,7 @@ func byChannelAndVersion(e1 *entitysource.Entity, e2 *entitysource.Entity) bool e1Version := getPropertyOrNotFound(e1, PropertyOLMVersion) e2Version := getPropertyOrNotFound(e2, PropertyOLMVersion) - // if neither has a version property, sort in ID order + // if neither has a version property, sort in Identifier order if e1Version == propertyNotFound && e2Version == propertyNotFound { return idOrder } @@ -267,8 +267,8 @@ func byChannelAndVersion(e1 *entitysource.Entity, e2 *entitysource.Entity) bool return v1.GT(v2) } -func gvkGroupFunction(entity *entitysource.Entity) []string { - if gvks, err := entity.GetProperty(PropertyOLMGVK); err == nil { +func gvkGroupFunction(entity *input.Entity) []string { + if gvks, ok := entity.Properties[PropertyOLMGVK]; ok { gvkArray := gjson.Parse(gvks).Array() keys := make([]string, 0, len(gvkArray)) for _, val := range gvkArray { @@ -285,28 +285,20 @@ func gvkGroupFunction(entity *entitysource.Entity) []string { return nil } -func packageGroupFunction(entity *entitysource.Entity) []string { - if packageName, err := entity.GetProperty(PropertyOLMPackageName); err == nil { +func packageGroupFunction(entity *input.Entity) []string { + if packageName, ok := entity.Properties[PropertyOLMPackageName]; ok { return []string{packageName} } return nil } -func subject(str ...string) sat.Identifier { - return sat.Identifier(regexp.MustCompile(`\\s`).ReplaceAllString(strings.Join(str, "-"), "")) +func subject(str ...string) solver.Identifier { + return solver.Identifier(regexp.MustCompile(`\\s`).ReplaceAllString(strings.Join(str, "-"), "")) } -func toSatIdentifier(ids ...entitysource.EntityID) []sat.Identifier { - satIds := make([]sat.Identifier, len(ids)) - for i := range ids { - satIds[i] = sat.Identifier(ids[i]) - } - return satIds -} - -func getPropertyOrNotFound(entity *entitysource.Entity, propertyName string) string { - value, err := entity.GetProperty(propertyName) - if err != nil { +func getPropertyOrNotFound(entity *input.Entity, propertyName string) string { + value, ok := entity.Properties[propertyName] + if !ok { return propertyNotFound } return value diff --git a/pkg/ext/olm/constraints_test.go b/pkg/ext/olm/constraints_test.go index dff0df3..c8f48c9 100644 --- a/pkg/ext/olm/constraints_test.go +++ b/pkg/ext/olm/constraints_test.go @@ -11,9 +11,12 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/operator-framework/deppy/pkg/solver" + + "github.com/operator-framework/deppy/pkg/input" + . "github.com/onsi/gomega/gstruct" - "github.com/operator-framework/deppy/pkg/entitysource" "github.com/operator-framework/deppy/pkg/ext/olm" ) @@ -22,27 +25,27 @@ func TestConstraints(t *testing.T) { RunSpecs(t, "Constraints Suite") } -func defaultTestEntityList() entitysource.EntityList { - return entitysource.EntityList{ - *entitysource.NewEntity("cool-package-1-entity", map[string]string{ +func defaultTestEntityList() input.EntityList { + return input.EntityList{ + *input.NewEntity("cool-package-1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "2.0.1", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", }), - *entitysource.NewEntity("cool-package-2-0-entity", map[string]string{ + *input.NewEntity("cool-package-2-0-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-2", olm.PropertyOLMVersion: "2.0.3", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", }), - *entitysource.NewEntity("cool-package-2-1-entity", map[string]string{ + *input.NewEntity("cool-package-2-1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-2", olm.PropertyOLMVersion: "2.1.0", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMGVK: "{\"group\":\"my-other-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", }), - *entitysource.NewEntity("cool-package-3-entity", map[string]string{ + *input.NewEntity("cool-package-3-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-3", olm.PropertyOLMVersion: "3.1.2", olm.PropertyOLMChannel: "channel-2", @@ -54,17 +57,17 @@ func defaultTestEntityList() entitysource.EntityList { // MockQuerier type to mock the entity querier type MockQuerier struct { testError error - testEntityList entitysource.EntityList + testEntityList input.EntityList } -func (t MockQuerier) Get(_ context.Context, _ entitysource.EntityID) *entitysource.Entity { - return &entitysource.Entity{} +func (t MockQuerier) Get(_ context.Context, _ solver.Identifier) *input.Entity { + return &input.Entity{} } -func (t MockQuerier) Filter(_ context.Context, filter entitysource.Predicate) (entitysource.EntityList, error) { +func (t MockQuerier) Filter(_ context.Context, filter input.Predicate) (input.EntityList, error) { if t.testError != nil { return nil, t.testError } - ret := entitysource.EntityList{} + ret := input.EntityList{} for _, entity := range t.testEntityList { if filter(&entity) { ret = append(ret, entity) @@ -72,23 +75,23 @@ func (t MockQuerier) Filter(_ context.Context, filter entitysource.Predicate) (e } return ret, nil } -func (t MockQuerier) GroupBy(_ context.Context, id entitysource.GroupByFunction) (entitysource.EntityListMap, error) { +func (t MockQuerier) GroupBy(_ context.Context, id input.GroupByFunction) (input.EntityListMap, error) { if t.testError != nil { return nil, t.testError } - ret := entitysource.EntityListMap{} + ret := input.EntityListMap{} for _, entity := range t.testEntityList { keys := id(&entity) for _, key := range keys { if _, ok := ret[key]; !ok { - ret[key] = entitysource.EntityList{} + ret[key] = input.EntityList{} } ret[key] = append(ret[key], entity) } } return ret, nil } -func (t MockQuerier) Iterate(_ context.Context, id entitysource.IteratorFunction) error { +func (t MockQuerier) Iterate(_ context.Context, id input.IteratorFunction) error { if t.testError != nil { return t.testError } @@ -129,8 +132,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("finds no candidates to satisfy the dependency when no entries contain the 'olm.packageName' key", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-3-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-3-entity", map[string]string{ "wrong-key": "cool-package-1", olm.PropertyOLMVersion: "2.1.2", olm.PropertyOLMChannel: "channel-1", @@ -152,8 +155,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("finds no candidates to satisfy the dependency when no entries have a valid semver value", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-1-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "abcdefg", olm.PropertyOLMChannel: "channel-1", @@ -164,8 +167,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("finds no candidates to satisfy the dependency when no entries contain the 'olm.version' key", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-1-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", "wrong-key": "2.1.2", olm.PropertyOLMChannel: "channel-1", @@ -177,8 +180,8 @@ var _ = Describe("Constraints", func() { }) // channel It("returns one satVar entry describing no possible dependency candidates when the entry has an empty channel name", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-1-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "2.1.2", olm.PropertyOLMChannel: "", @@ -201,8 +204,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("returns one satVar entry describing no possible dependency candidates when no entries contain the 'olm.channel' key", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-1-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "2.1.2", "wrong-key": "channel-1", @@ -257,8 +260,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("returns an empty sat.Variable slice when package name key is missing from all entities", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-3-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-3-entity", map[string]string{ "wrong-key": "cool-package-3", olm.PropertyOLMVersion: "3.1.2", olm.PropertyOLMChannel: "channel-2", @@ -301,8 +304,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("returns an empty sat.Variable slice when gvk key is missing from all entities", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-3-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-3-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-3", "wrong-key": "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", }), @@ -312,8 +315,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("returns an empty sat.Variable slice when gvk field is malformed in all entities", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-3-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-3-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-3", olm.PropertyOLMGVK: "abcdefg", }), @@ -323,8 +326,8 @@ var _ = Describe("Constraints", func() { Expect(satVars).Should(HaveLen(0)) }) It("does not panic but returns an empty result set when gvk json is missing fields", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-3-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-3-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-3", olm.PropertyOLMGVK: "{}", }), @@ -407,14 +410,14 @@ var _ = Describe("Constraints", func() { } }) DescribeTable("package name ordering", func(pkg1NameKey string, pkg2NameKey string, matchElements Elements) { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-entity-1", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-entity-1", map[string]string{ pkg1NameKey: "cool-package-1", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", olm.PropertyOLMDefaultChannel: "channel-1", }), - *entitysource.NewEntity("cool-package-entity-2", map[string]string{ + *input.NewEntity("cool-package-entity-2", map[string]string{ pkg2NameKey: "cool-package-2", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMGVK: "{\"group\":\"my-group\",\"version\":\"my-version\",\"kind\":\"my-kind\"}", @@ -442,51 +445,51 @@ var _ = Describe("Constraints", func() { ) Describe("channel and version ordering", func() { It("orders sat vars with identical packageName by channel and version in that order of priority", func() { - mockQuerier.testEntityList = entitysource.EntityList{ - *entitysource.NewEntity("cool-package-1-ch1-1.0-entity", map[string]string{ + mockQuerier.testEntityList = input.EntityList{ + *input.NewEntity("cool-package-1-ch1-1.0-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "1.0.1", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMDefaultChannel: "channel-2", }), - *entitysource.NewEntity("cool-package-1-ch1-invalid-version-a-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch1-invalid-version-a-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "abcdefg", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMDefaultChannel: "channel-2", }), - *entitysource.NewEntity("cool-package-1-ch2-versionless-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch2-versionless-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMChannel: "channel-2", olm.PropertyOLMDefaultChannel: "channel-2", }), - *entitysource.NewEntity("cool-package-1-ch1-1.1-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch1-1.1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "1.1.3", olm.PropertyOLMChannel: "channel-1", }), - *entitysource.NewEntity("cool-package-1-ch1-invalid-version-b-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch1-invalid-version-b-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "abcdefg", olm.PropertyOLMChannel: "channel-1", }), - *entitysource.NewEntity("cool-package-1-ch2-1.2-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch2-1.2-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "1.2.3", olm.PropertyOLMChannel: "channel-2", olm.PropertyOLMDefaultChannel: "channel-2", }), - *entitysource.NewEntity("cool-package-1-ch3-1.2-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch3-1.2-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "1.2.3", olm.PropertyOLMChannel: "channel-3", olm.PropertyOLMDefaultChannel: "channel-2", }), - *entitysource.NewEntity("cool-package-1-channelless-1.1-entity", map[string]string{ + *input.NewEntity("cool-package-1-channelless-1.1-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMVersion: "1.1.3", }), - *entitysource.NewEntity("cool-package-1-ch1-versionless-entity", map[string]string{ + *input.NewEntity("cool-package-1-ch1-versionless-entity", map[string]string{ olm.PropertyOLMPackageName: "cool-package-1", olm.PropertyOLMChannel: "channel-1", olm.PropertyOLMDefaultChannel: "channel-2", diff --git a/pkg/entitysource/cache_querier.go b/pkg/input/cache_entity_source.go similarity index 77% rename from pkg/entitysource/cache_querier.go rename to pkg/input/cache_entity_source.go index b6dc523..2f7de8e 100644 --- a/pkg/entitysource/cache_querier.go +++ b/pkg/input/cache_entity_source.go @@ -1,21 +1,25 @@ -package entitysource +package input -import "context" +import ( + "context" + + "github.com/operator-framework/deppy/pkg/solver" +) var _ EntitySource = &CacheEntitySource{} type CacheEntitySource struct { // TODO: separate out a cache - entities map[EntityID]Entity + entities map[solver.Identifier]Entity } -func NewCacheQuerier(entities map[EntityID]Entity) *CacheEntitySource { +func NewCacheQuerier(entities map[solver.Identifier]Entity) *CacheEntitySource { return &CacheEntitySource{ entities: entities, } } -func (c CacheEntitySource) Get(_ context.Context, id EntityID) *Entity { +func (c CacheEntitySource) Get(_ context.Context, id solver.Identifier) *Entity { if entity, ok := c.entities[id]; ok { return &entity } diff --git a/pkg/input/entity.go b/pkg/input/entity.go new file mode 100644 index 0000000..74c2985 --- /dev/null +++ b/pkg/input/entity.go @@ -0,0 +1,21 @@ +package input + +import ( + "github.com/operator-framework/deppy/pkg/solver" +) + +type Entity struct { + ID solver.Identifier `json:"identifier"` + Properties map[string]string `json:"properties"` +} + +func (e *Entity) Identifier() solver.Identifier { + return e.ID +} + +func NewEntity(id solver.Identifier, properties map[string]string) *Entity { + return &Entity{ + ID: id, + Properties: properties, + } +} diff --git a/pkg/entitysource/entity_source.go b/pkg/input/entity_source.go similarity index 88% rename from pkg/entitysource/entity_source.go rename to pkg/input/entity_source.go index 30864ff..bb3b2c9 100644 --- a/pkg/entitysource/entity_source.go +++ b/pkg/input/entity_source.go @@ -1,7 +1,9 @@ -package entitysource +package input import ( "context" + + "github.com/operator-framework/deppy/pkg/solver" ) // IteratorFunction is executed for each entity when iterating over all entities @@ -22,7 +24,7 @@ type EntityListMap map[string]EntityList // EntitySource provides a query and content acquisition interface for arbitrary entity stores type EntitySource interface { - Get(ctx context.Context, id EntityID) *Entity + Get(ctx context.Context, id solver.Identifier) *Entity Filter(ctx context.Context, filter Predicate) (EntityList, error) GroupBy(ctx context.Context, fn GroupByFunction) (EntityListMap, error) Iterate(ctx context.Context, fn IteratorFunction) error diff --git a/pkg/entitysource/entity_source_test.go b/pkg/input/entity_source_test.go similarity index 68% rename from pkg/entitysource/entity_source_test.go rename to pkg/input/entity_source_test.go index 2f0fb98..3638f52 100644 --- a/pkg/entitysource/entity_source_test.go +++ b/pkg/input/entity_source_test.go @@ -1,4 +1,4 @@ -package entitysource_test +package input_test import ( "context" @@ -7,16 +7,18 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" + "github.com/operator-framework/deppy/pkg/input" + + "github.com/operator-framework/deppy/pkg/solver" - "github.com/operator-framework/deppy/pkg/entitysource" + . "github.com/onsi/gomega/gstruct" ) // Test functions for filter -func byIndex(index string) entitysource.Predicate { - return func(entity *entitysource.Entity) bool { - i, err := entity.GetProperty("index") - if err != nil { +func byIndex(index string) input.Predicate { + return func(entity *input.Entity) bool { + i, ok := entity.Properties["index"] + if !ok { return false } if i == index { @@ -25,10 +27,10 @@ func byIndex(index string) entitysource.Predicate { return false } } -func bySource(source string) entitysource.Predicate { - return func(entity *entitysource.Entity) bool { - i, err := entity.GetProperty("source") - if err != nil { +func bySource(source string) input.Predicate { + return func(entity *input.Entity) bool { + i, ok := entity.Properties["source"] + if !ok { return false } if i == source { @@ -39,19 +41,19 @@ func bySource(source string) entitysource.Predicate { } // Test function for iterate -var entityCheck map[entitysource.EntityID]bool +var entityCheck map[solver.Identifier]bool -func check(entity *entitysource.Entity) error { - checked, ok := entityCheck[entity.ID()] +func check(entity *input.Entity) error { + checked, ok := entityCheck[entity.Identifier()] Expect(ok).Should(BeTrue()) Expect(checked).Should(BeFalse()) - entityCheck[entity.ID()] = true + entityCheck[entity.Identifier()] = true return nil } // Test function for GroupBy -func bySourceAndIndex(entity *entitysource.Entity) []string { - switch entity.ID() { +func bySourceAndIndex(entity *input.Entity) []string { + switch entity.Identifier() { case "1-1": return []string{"source 1", "index 1"} case "1-2": @@ -67,24 +69,24 @@ func bySourceAndIndex(entity *entitysource.Entity) []string { var _ = Describe("EntitySource", func() { When("a group is created with multiple entity sources", func() { var ( - entitySource entitysource.EntitySource + entitySource input.EntitySource ) BeforeEach(func() { - entities := map[entitysource.EntityID]entitysource.Entity{ - entitysource.EntityID("1-1"): *entitysource.NewEntity("1-1", map[string]string{"source": "1", "index": "1"}), - entitysource.EntityID("1-2"): *entitysource.NewEntity("1-2", map[string]string{"source": "1", "index": "2"}), - entitysource.EntityID("2-1"): *entitysource.NewEntity("2-1", map[string]string{"source": "2", "index": "1"}), - entitysource.EntityID("2-2"): *entitysource.NewEntity("2-2", map[string]string{"source": "2", "index": "2"}), + entities := map[solver.Identifier]input.Entity{ + solver.Identifier("1-1"): *input.NewEntity("1-1", map[string]string{"source": "1", "index": "1"}), + solver.Identifier("1-2"): *input.NewEntity("1-2", map[string]string{"source": "1", "index": "2"}), + solver.Identifier("2-1"): *input.NewEntity("2-1", map[string]string{"source": "2", "index": "1"}), + solver.Identifier("2-2"): *input.NewEntity("2-2", map[string]string{"source": "2", "index": "2"}), } - entitySource = entitysource.NewCacheQuerier(entities) + entitySource = input.NewCacheQuerier(entities) }) Describe("Get", func() { It("should return requested entity", func() { e := entitySource.Get(context.Background(), "2-2") Expect(e).NotTo(BeNil()) - Expect(e.ID()).To(Equal(entitysource.EntityID("2-2"))) + Expect(e.Identifier()).To(Equal(solver.Identifier("2-2"))) }) }) @@ -93,7 +95,7 @@ var _ = Describe("EntitySource", func() { id := func(element interface{}) string { return fmt.Sprintf("%v", element) } - el, err := entitySource.Filter(context.Background(), entitysource.Or(byIndex("2"), bySource("1"))) + el, err := entitySource.Filter(context.Background(), input.Or(byIndex("2"), bySource("1"))) Expect(err).To(BeNil()) Expect(el).To(MatchAllElements(id, Elements{ "{1-2 map[index:2 source:1]}": Not(BeNil()), @@ -108,7 +110,7 @@ var _ = Describe("EntitySource", func() { "1-1": Not(BeNil()), })) - el, err = entitySource.Filter(context.Background(), entitysource.And(byIndex("2"), bySource("1"))) + el, err = entitySource.Filter(context.Background(), input.And(byIndex("2"), bySource("1"))) Expect(err).To(BeNil()) Expect(el).To(MatchAllElements(id, Elements{ "{1-2 map[index:2 source:1]}": Not(BeNil()), @@ -119,7 +121,7 @@ var _ = Describe("EntitySource", func() { "1-2": Not(BeNil()), })) - el, err = entitySource.Filter(context.Background(), entitysource.And(byIndex("2"), entitysource.Not(bySource("1")))) + el, err = entitySource.Filter(context.Background(), input.And(byIndex("2"), input.Not(bySource("1")))) Expect(err).To(BeNil()) Expect(el).To(MatchAllElements(id, Elements{ "{2-2 map[index:2 source:2]}": Not(BeNil()), @@ -135,7 +137,7 @@ var _ = Describe("EntitySource", func() { Describe("Iterate", func() { It("should go through all entities", func() { - entityCheck = map[entitysource.EntityID]bool{"1-1": false, "1-2": false, "2-1": false, "2-2": false} + entityCheck = map[solver.Identifier]bool{"1-1": false, "1-2": false, "2-1": false, "2-2": false} err := entitySource.Iterate(context.Background(), check) Expect(err).To(BeNil()) for _, value := range entityCheck { diff --git a/pkg/input/entity_test.go b/pkg/input/entity_test.go new file mode 100644 index 0000000..2a52eae --- /dev/null +++ b/pkg/input/entity_test.go @@ -0,0 +1,27 @@ +package input_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/solver" +) + +var _ = Describe("Entity", func() { + It("stores id and properties", func() { + entity := input.NewEntity("id", map[string]string{"prop": "value"}) + Expect(entity.Identifier()).To(Equal(solver.Identifier("id"))) + + value, ok := entity.Properties["prop"] + Expect(ok).To(BeTrue()) + Expect(value).To(Equal("value")) + }) + + It("returns not found error when property is not found", func() { + entity := input.NewEntity("id", map[string]string{"foo": "value"}) + value, ok := entity.Properties["bar"] + Expect(value).To(Equal("")) + Expect(ok).To(BeFalse()) + }) +}) diff --git a/pkg/entitysource/query.go b/pkg/input/query.go similarity index 81% rename from pkg/entitysource/query.go rename to pkg/input/query.go index 1b97d53..72714dc 100644 --- a/pkg/entitysource/query.go +++ b/pkg/input/query.go @@ -1,6 +1,10 @@ -package entitysource +package input -import "sort" +import ( + "sort" + + "github.com/operator-framework/deppy/pkg/solver" +) func (r EntityList) Sort(fn SortFunction) EntityList { sort.SliceStable(r, func(i, j int) bool { @@ -9,10 +13,10 @@ func (r EntityList) Sort(fn SortFunction) EntityList { return r } -func (r EntityList) CollectIds() []EntityID { - ids := make([]EntityID, len(r)) +func (r EntityList) CollectIds() []solver.Identifier { + ids := make([]solver.Identifier, len(r)) for i := range r { - ids[i] = r[i].ID() + ids[i] = r[i].Identifier() } return ids } diff --git a/pkg/input/solver.go b/pkg/input/solver.go new file mode 100644 index 0000000..85f74e4 --- /dev/null +++ b/pkg/input/solver.go @@ -0,0 +1,59 @@ +package input + +import ( + "context" + + "github.com/operator-framework/deppy/pkg/solver" +) + +// TODO: should disambiguate between solver errors due to constraints +// and other generic errors (e.g. entity source not reachable, etc.) + +// Solution is returned by the Solver when a solution could be found +// it can be queried by Identifier to see if the entity was selected (true) or not (false) +// by the solver +type Solution map[solver.Identifier]bool + +// DeppySolver is a simple solver implementation that takes an entity source group and a constraint aggregator +// to produce a Solution (or error if no solution can be found) +type DeppySolver struct { + entitySource EntitySource + variableSource VariableSource +} + +func NewDeppySolver(entitySource EntitySource, variableSource VariableSource) (*DeppySolver, error) { + return &DeppySolver{ + entitySource: entitySource, + variableSource: variableSource, + }, nil +} + +func (d DeppySolver) Solve(ctx context.Context) (Solution, error) { + vars, err := d.variableSource.GetVariables(ctx, d.entitySource) + if err != nil { + return nil, err + } + + satSolver, err := solver.NewSolver(solver.WithInput(vars)) + if err != nil { + return nil, err + } + + selection, err := satSolver.Solve(ctx) + if err != nil { + return nil, err + } + + solution := Solution{} + for _, variable := range vars { + if entity := d.entitySource.Get(ctx, variable.Identifier()); entity != nil { + solution[entity.Identifier()] = false + } + } + for _, variable := range selection { + if entity := d.entitySource.Get(ctx, variable.Identifier()); entity != nil { + solution[entity.Identifier()] = true + } + } + return solution, nil +} diff --git a/pkg/solver/solver_suite_test.go b/pkg/input/solver_suite_test.go similarity index 89% rename from pkg/solver/solver_suite_test.go rename to pkg/input/solver_suite_test.go index 766e4bd..a939d07 100644 --- a/pkg/solver/solver_suite_test.go +++ b/pkg/input/solver_suite_test.go @@ -1,4 +1,4 @@ -package solver_test +package input_test import ( "testing" diff --git a/pkg/input/solver_test.go b/pkg/input/solver_test.go new file mode 100644 index 0000000..d919cde --- /dev/null +++ b/pkg/input/solver_test.go @@ -0,0 +1,183 @@ +package input_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/operator-framework/deppy/pkg/input" + + . "github.com/onsi/gomega/gstruct" + + "github.com/operator-framework/deppy/pkg/solver" +) + +type EntitySourceStruct struct { + variables []solver.Variable + input.EntitySource +} + +func (c EntitySourceStruct) GetVariables(_ context.Context, _ input.EntitySource) ([]solver.Variable, error) { + return c.variables, nil +} + +func NewEntitySource(variables []solver.Variable) *EntitySourceStruct { + entities := make(map[solver.Identifier]input.Entity, len(variables)) + for _, variable := range variables { + entityID := variable.Identifier() + entities[entityID] = *input.NewEntity(entityID, map[string]string{"x": "y"}) + } + return &EntitySourceStruct{ + variables: variables, + EntitySource: input.NewCacheQuerier(entities), + } +} + +var _ = Describe("Entity", func() { + It("should select a mandatory entity", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Mandatory()), + input.NewSimpleVariable("2"), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(true), + solver.Identifier("2"): Equal(false), + })) + }) + + It("should select two mandatory entities", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Mandatory()), + input.NewSimpleVariable("2", solver.Mandatory()), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(true), + solver.Identifier("2"): Equal(true), + })) + }) + + It("should select a mandatory entity and its dependency", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Mandatory(), solver.Dependency("2")), + input.NewSimpleVariable("2"), + input.NewSimpleVariable("3"), + } + s := NewEntitySource(variables) + + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(true), + solver.Identifier("2"): Equal(true), + solver.Identifier("3"): Equal(false), + })) + }) + + It("should fail when a dependency is prohibited", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Mandatory(), solver.Dependency("2")), + input.NewSimpleVariable("2", solver.Prohibited()), + input.NewSimpleVariable("3"), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + _, err = so.Solve(context.Background()) + Expect(err).Should(HaveOccurred()) + }) + + It("should select a mandatory entity and its dependency and ignore a non-mandatory prohibited variable", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Mandatory(), solver.Dependency("2")), + input.NewSimpleVariable("2"), + input.NewSimpleVariable("3", solver.Prohibited()), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(true), + solver.Identifier("2"): Equal(true), + solver.Identifier("3"): Equal(false), + })) + }) + + It("should not select 'or' paths that are prohibited", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Or("2", false, false), solver.Dependency("3")), + input.NewSimpleVariable("2", solver.Dependency("4")), + input.NewSimpleVariable("3", solver.Prohibited()), + input.NewSimpleVariable("4"), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(false), + solver.Identifier("2"): Equal(true), + solver.Identifier("3"): Equal(false), + solver.Identifier("4"): Equal(true), + })) + }) + + It("should respect atMost constraint", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Or("2", false, false), solver.Dependency("3"), solver.Dependency("4")), + input.NewSimpleVariable("2", solver.Dependency("3")), + input.NewSimpleVariable("3", solver.AtMost(1, "3", "4")), + input.NewSimpleVariable("4"), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(false), + solver.Identifier("2"): Equal(true), + solver.Identifier("3"): Equal(true), + solver.Identifier("4"): Equal(false), + })) + }) + + It("should respect dependency conflicts", func() { + variables := []solver.Variable{ + input.NewSimpleVariable("1", solver.Or("2", false, false), solver.Dependency("3"), solver.Dependency("4")), + input.NewSimpleVariable("2", solver.Dependency("4"), solver.Dependency("5")), + input.NewSimpleVariable("3", solver.Conflict("6")), + input.NewSimpleVariable("4", solver.Dependency("6")), + input.NewSimpleVariable("5"), + input.NewSimpleVariable("6"), + } + s := NewEntitySource(variables) + so, err := input.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + solver.Identifier("1"): Equal(false), + solver.Identifier("2"): Equal(true), + solver.Identifier("3"): Equal(false), + solver.Identifier("4"): Equal(true), + solver.Identifier("5"): Equal(true), + solver.Identifier("6"): Equal(true), + })) + }) +}) diff --git a/pkg/entitysource/source_suite_test.go b/pkg/input/source_suite_test.go similarity index 87% rename from pkg/entitysource/source_suite_test.go rename to pkg/input/source_suite_test.go index 52b0965..495973d 100644 --- a/pkg/entitysource/source_suite_test.go +++ b/pkg/input/source_suite_test.go @@ -1,4 +1,4 @@ -package entitysource_test +package input_test import ( "testing" diff --git a/pkg/input/variable_source.go b/pkg/input/variable_source.go new file mode 100644 index 0000000..f94fd5e --- /dev/null +++ b/pkg/input/variable_source.go @@ -0,0 +1,38 @@ +package input + +import ( + "context" + + "github.com/operator-framework/deppy/pkg/solver" +) + +// VariableSource generates solver constraints given an entity querier interface +type VariableSource interface { + GetVariables(ctx context.Context, entitySource EntitySource) ([]solver.Variable, error) +} + +var _ solver.Variable = &SimpleVariable{} + +type SimpleVariable struct { + id solver.Identifier + constraints []solver.Constraint +} + +func (s *SimpleVariable) Identifier() solver.Identifier { + return s.id +} + +func (s *SimpleVariable) Constraints() []solver.Constraint { + return s.constraints +} + +func (s *SimpleVariable) AddConstraint(constraint solver.Constraint) { + s.constraints = append(s.constraints, constraint) +} + +func NewSimpleVariable(id solver.Identifier, constraints ...solver.Constraint) *SimpleVariable { + return &SimpleVariable{ + id: id, + constraints: constraints, + } +} diff --git a/pkg/sat/bench_test.go b/pkg/solver/bench_test.go similarity index 99% rename from pkg/sat/bench_test.go rename to pkg/solver/bench_test.go index e477454..05e95fe 100644 --- a/pkg/sat/bench_test.go +++ b/pkg/solver/bench_test.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "context" diff --git a/pkg/sat/constraints.go b/pkg/solver/constraints.go similarity index 99% rename from pkg/sat/constraints.go rename to pkg/solver/constraints.go index a48ef7d..3361d80 100644 --- a/pkg/sat/constraints.go +++ b/pkg/solver/constraints.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "fmt" diff --git a/pkg/sat/constraints_test.go b/pkg/solver/constraints_test.go similarity index 97% rename from pkg/sat/constraints_test.go rename to pkg/solver/constraints_test.go index ea95664..4bd0a2e 100644 --- a/pkg/sat/constraints_test.go +++ b/pkg/solver/constraints_test.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "testing" diff --git a/pkg/sat/doc.go b/pkg/solver/doc.go similarity index 87% rename from pkg/sat/doc.go rename to pkg/solver/doc.go index 00e207a..12f2745 100644 --- a/pkg/sat/doc.go +++ b/pkg/solver/doc.go @@ -1,3 +1,3 @@ // Package solver implements a general-purpose solver for boolean // constraint satisfiability problems. -package sat +package solver diff --git a/pkg/sat/lit_mapping.go b/pkg/solver/lit_mapping.go similarity index 99% rename from pkg/sat/lit_mapping.go rename to pkg/solver/lit_mapping.go index a27809c..eb7a739 100644 --- a/pkg/sat/lit_mapping.go +++ b/pkg/solver/lit_mapping.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "fmt" diff --git a/pkg/sat/search.go b/pkg/solver/search.go similarity index 99% rename from pkg/sat/search.go rename to pkg/solver/search.go index b4621be..523b311 100644 --- a/pkg/sat/search.go +++ b/pkg/solver/search.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "context" diff --git a/pkg/sat/search_test.go b/pkg/solver/search_test.go similarity index 99% rename from pkg/sat/search_test.go rename to pkg/solver/search_test.go index 57dc68d..fd9221a 100644 --- a/pkg/sat/search_test.go +++ b/pkg/solver/search_test.go @@ -1,6 +1,6 @@ //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o zz_search_test.go ../../../../../vendor/github.com/go-air/gini/inter S -package sat +package solver import ( "context" diff --git a/pkg/sat/solve.go b/pkg/solver/solve.go similarity index 99% rename from pkg/sat/solve.go rename to pkg/solver/solve.go index 53f3115..76f22c1 100644 --- a/pkg/sat/solve.go +++ b/pkg/solver/solve.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "context" diff --git a/pkg/sat/solve_test.go b/pkg/solver/solve_test.go similarity index 99% rename from pkg/sat/solve_test.go rename to pkg/solver/solve_test.go index 4a32719..e84c465 100644 --- a/pkg/sat/solve_test.go +++ b/pkg/solver/solve_test.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "bytes" diff --git a/pkg/solver/solver.go b/pkg/solver/solver.go deleted file mode 100644 index 3dedb1c..0000000 --- a/pkg/solver/solver.go +++ /dev/null @@ -1,67 +0,0 @@ -package solver - -import ( - "context" - - "github.com/operator-framework/deppy/pkg/entitysource" - "github.com/operator-framework/deppy/pkg/sat" - "github.com/operator-framework/deppy/pkg/variablesource" -) - -var _ Solver = &DeppySolver{} - -// TODO: should be disambiguate between solver errors due to constraints -// and other generic errors (e.g. entity source not reachable, etc.) - -// Solution is returned by the Solver when a solution could be found -// it can be queried by EntityID to see if the entity was selected (true) or not (false) -// by the solver -type Solution map[entitysource.EntityID]bool - -type Solver interface { - Solve(ctx context.Context) (Solution, error) -} - -// DeppySolver is a simple solver implementation that takes an entity source group and a constraint aggregator -// to produce a Solution (or error if no solution can be found) -type DeppySolver struct { - entitySource entitysource.EntitySource - variableSource variablesource.VariableSource -} - -func NewDeppySolver(entitySource entitysource.EntitySource, variableSource variablesource.VariableSource) (*DeppySolver, error) { - return &DeppySolver{ - entitySource: entitySource, - variableSource: variableSource, - }, nil -} - -func (d DeppySolver) Solve(ctx context.Context) (Solution, error) { - vars, err := d.variableSource.GetVariables(ctx, d.entitySource) - if err != nil { - return nil, err - } - - satSolver, err := sat.NewSolver(sat.WithInput(vars)) - if err != nil { - return nil, err - } - - selection, err := satSolver.Solve(ctx) - if err != nil { - return nil, err - } - - solution := Solution{} - for _, variable := range vars { - if entity := d.entitySource.Get(ctx, entitysource.EntityID(variable.Identifier())); entity != nil { - solution[entity.ID()] = false - } - } - for _, variable := range selection { - if entity := d.entitySource.Get(ctx, entitysource.EntityID(variable.Identifier())); entity != nil { - solution[entity.ID()] = true - } - } - return solution, nil -} diff --git a/pkg/solver/solver_test.go b/pkg/solver/solver_test.go deleted file mode 100644 index 20ae2db..0000000 --- a/pkg/solver/solver_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package solver_test - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - . "github.com/onsi/gomega/gstruct" - - "github.com/operator-framework/deppy/pkg/entitysource" - "github.com/operator-framework/deppy/pkg/sat" - "github.com/operator-framework/deppy/pkg/solver" - "github.com/operator-framework/deppy/pkg/variablesource" -) - -type EntitySourceStruct struct { - variables []sat.Variable - entitysource.EntitySource -} - -func (c EntitySourceStruct) GetVariables(_ context.Context, _ entitysource.EntitySource) ([]sat.Variable, error) { - return c.variables, nil -} - -func NewEntitySource(variables []sat.Variable) *EntitySourceStruct { - entities := make(map[entitysource.EntityID]entitysource.Entity, len(variables)) - for _, variable := range variables { - entityID := entitysource.EntityID(variable.Identifier()) - entities[entityID] = *entitysource.NewEntity(entityID, map[string]string{"x": "y"}) - } - return &EntitySourceStruct{ - variables: variables, - EntitySource: entitysource.NewCacheQuerier(entities), - } -} - -var _ = Describe("Entity", func() { - It("should select a mandatory entity", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Mandatory()), - variablesource.NewVariable("2"), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(true), - entitysource.EntityID("2"): Equal(false), - })) - }) - - It("should select two mandatory entities", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Mandatory()), - variablesource.NewVariable("2", sat.Mandatory()), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(true), - entitysource.EntityID("2"): Equal(true), - })) - }) - - It("should select a mandatory entity and its dependency", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Mandatory(), sat.Dependency("2")), - variablesource.NewVariable("2"), - variablesource.NewVariable("3"), - } - s := NewEntitySource(variables) - - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(true), - entitysource.EntityID("2"): Equal(true), - entitysource.EntityID("3"): Equal(false), - })) - }) - - It("should fail when a dependency is prohibited", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Mandatory(), sat.Dependency("2")), - variablesource.NewVariable("2", sat.Prohibited()), - variablesource.NewVariable("3"), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - _, err = so.Solve(context.Background()) - Expect(err).Should(HaveOccurred()) - }) - - It("should select a mandatory entity and its dependency and ignore a non-mandatory prohibited variable", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Mandatory(), sat.Dependency("2")), - variablesource.NewVariable("2"), - variablesource.NewVariable("3", sat.Prohibited()), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(true), - entitysource.EntityID("2"): Equal(true), - entitysource.EntityID("3"): Equal(false), - })) - }) - - It("should not select 'or' paths that are prohibited", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Or("2", false, false), sat.Dependency("3")), - variablesource.NewVariable("2", sat.Dependency("4")), - variablesource.NewVariable("3", sat.Prohibited()), - variablesource.NewVariable("4"), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(false), - entitysource.EntityID("2"): Equal(true), - entitysource.EntityID("3"): Equal(false), - entitysource.EntityID("4"): Equal(true), - })) - }) - - It("should respect atMost constraint", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Or("2", false, false), sat.Dependency("3"), sat.Dependency("4")), - variablesource.NewVariable("2", sat.Dependency("3")), - variablesource.NewVariable("3", sat.AtMost(1, "3", "4")), - variablesource.NewVariable("4"), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(false), - entitysource.EntityID("2"): Equal(true), - entitysource.EntityID("3"): Equal(true), - entitysource.EntityID("4"): Equal(false), - })) - }) - - It("should respect dependency conflicts", func() { - variables := []sat.Variable{ - variablesource.NewVariable("1", sat.Or("2", false, false), sat.Dependency("3"), sat.Dependency("4")), - variablesource.NewVariable("2", sat.Dependency("4"), sat.Dependency("5")), - variablesource.NewVariable("3", sat.Conflict("6")), - variablesource.NewVariable("4", sat.Dependency("6")), - variablesource.NewVariable("5"), - variablesource.NewVariable("6"), - } - s := NewEntitySource(variables) - so, err := solver.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - entitysource.EntityID("1"): Equal(false), - entitysource.EntityID("2"): Equal(true), - entitysource.EntityID("3"): Equal(false), - entitysource.EntityID("4"): Equal(true), - entitysource.EntityID("5"): Equal(true), - entitysource.EntityID("6"): Equal(true), - })) - }) -}) diff --git a/pkg/sat/tracer.go b/pkg/solver/tracer.go similarity index 97% rename from pkg/sat/tracer.go rename to pkg/solver/tracer.go index f551da5..b103536 100644 --- a/pkg/sat/tracer.go +++ b/pkg/solver/tracer.go @@ -1,4 +1,4 @@ -package sat +package solver import ( "fmt" diff --git a/pkg/sat/variable.go b/pkg/solver/variable.go similarity index 98% rename from pkg/sat/variable.go rename to pkg/solver/variable.go index 4a1d846..32afab7 100644 --- a/pkg/sat/variable.go +++ b/pkg/solver/variable.go @@ -1,4 +1,4 @@ -package sat +package solver // Identifier values uniquely identify particular Variables within // the input to a single call to Solve. diff --git a/pkg/sat/zz_search_test.go b/pkg/solver/zz_search_test.go similarity index 99% rename from pkg/sat/zz_search_test.go rename to pkg/solver/zz_search_test.go index 0dcc427..6fbdbcc 100644 --- a/pkg/sat/zz_search_test.go +++ b/pkg/solver/zz_search_test.go @@ -1,5 +1,5 @@ // Code generated by counterfeiter. DO NOT EDIT. -package sat +package solver import ( "sync" diff --git a/pkg/variablesource/variable.go b/pkg/variablesource/variable.go deleted file mode 100644 index 772f8f5..0000000 --- a/pkg/variablesource/variable.go +++ /dev/null @@ -1,30 +0,0 @@ -package variablesource - -import "github.com/operator-framework/deppy/pkg/sat" - -var _ sat.Variable = &Variable{} - -// Variable is a simple implementation of sat.Variable -type Variable struct { - id sat.Identifier - constraints []sat.Constraint -} - -func (v *Variable) Identifier() sat.Identifier { - return v.id -} - -func (v *Variable) Constraints() []sat.Constraint { - return v.constraints -} - -func (v *Variable) AddConstraint(constraint ...sat.Constraint) { - v.constraints = append(v.constraints, constraint...) -} - -func NewVariable(id sat.Identifier, constraints ...sat.Constraint) *Variable { - return &Variable{ - id: id, - constraints: constraints, - } -} diff --git a/pkg/variablesource/variable_source.go b/pkg/variablesource/variable_source.go deleted file mode 100644 index 836d744..0000000 --- a/pkg/variablesource/variable_source.go +++ /dev/null @@ -1,13 +0,0 @@ -package variablesource - -import ( - "context" - - "github.com/operator-framework/deppy/pkg/entitysource" - "github.com/operator-framework/deppy/pkg/sat" -) - -// VariableSource generates solver constraints given an entity querier interface -type VariableSource interface { - GetVariables(ctx context.Context, entitySource entitysource.EntitySource) ([]sat.Variable, error) -} From 77a3b8b3c4edf6b2356fbe89f07155456dbf2e4f Mon Sep 17 00:00:00 2001 From: perdasilva Date: Thu, 15 Dec 2022 16:59:26 +0100 Subject: [PATCH 2/3] move solver to internal Signed-off-by: perdasilva --- cmd/dimacs/cmd.go | 4 +- cmd/dimacs/dimacs_constraints.go | 21 +- cmd/dimacs/dimacs_source.go | 8 +- cmd/sudoku/cmd.go | 4 +- cmd/sudoku/sudoku.go | 41 ++- {pkg => internal}/solver/bench_test.go | 21 +- internal/solver/constraints.go | 28 ++ internal/solver/constraints_test.go | 43 +++ {pkg => internal}/solver/doc.go | 0 {pkg => internal}/solver/lit_mapping.go | 58 +-- {pkg => internal}/solver/search.go | 12 +- {pkg => internal}/solver/search_test.go | 24 +- {pkg => internal}/solver/solve.go | 16 +- internal/solver/solve_test.go | 369 +++++++++++++++++++ {pkg => internal}/solver/tracer.go | 15 +- internal/solver/variable.go | 18 + {pkg => internal}/solver/zz_search_test.go | 0 pkg/deppy/api.go | 62 ++++ pkg/deppy/constraint/constraint.go | 208 +++++++++++ pkg/{ => deppy}/input/cache_entity_source.go | 8 +- pkg/deppy/input/entity.go | 21 ++ pkg/{ => deppy}/input/entity_source.go | 4 +- pkg/{ => deppy}/input/entity_source_test.go | 20 +- pkg/{ => deppy}/input/entity_test.go | 7 +- pkg/{ => deppy}/input/query.go | 6 +- pkg/deppy/input/variable_source.go | 38 ++ pkg/{input => deppy/solver}/solver.go | 14 +- pkg/deppy/solver/solver_test.go | 187 ++++++++++ pkg/deppy/tracer.go | 10 + pkg/ext/olm/constraints.go | 44 +-- pkg/ext/olm/constraints_test.go | 7 +- pkg/input/entity.go | 21 -- pkg/input/solver_suite_test.go | 13 - pkg/input/solver_test.go | 183 --------- pkg/input/source_suite_test.go | 13 - pkg/input/variable_source.go | 38 -- pkg/solver/constraints.go | 250 ------------- pkg/solver/constraints_test.go | 39 -- pkg/solver/solve_test.go | 365 ------------------ pkg/solver/variable.go | 40 -- 40 files changed, 1160 insertions(+), 1120 deletions(-) rename {pkg => internal}/solver/bench_test.go (74%) create mode 100644 internal/solver/constraints.go create mode 100644 internal/solver/constraints_test.go rename {pkg => internal}/solver/doc.go (100%) rename {pkg => internal}/solver/lit_mapping.go (74%) rename {pkg => internal}/solver/search.go (93%) rename {pkg => internal}/solver/search_test.go (77%) rename {pkg => internal}/solver/solve.go (90%) create mode 100644 internal/solver/solve_test.go rename {pkg => internal}/solver/tracer.go (60%) create mode 100644 internal/solver/variable.go rename {pkg => internal}/solver/zz_search_test.go (100%) create mode 100644 pkg/deppy/api.go create mode 100644 pkg/deppy/constraint/constraint.go rename pkg/{ => deppy}/input/cache_entity_source.go (80%) create mode 100644 pkg/deppy/input/entity.go rename pkg/{ => deppy}/input/entity_source.go (89%) rename pkg/{ => deppy}/input/entity_source_test.go (86%) rename pkg/{ => deppy}/input/entity_test.go (77%) rename pkg/{ => deppy}/input/query.go (87%) create mode 100644 pkg/deppy/input/variable_source.go rename pkg/{input => deppy/solver}/solver.go (77%) create mode 100644 pkg/deppy/solver/solver_test.go create mode 100644 pkg/deppy/tracer.go delete mode 100644 pkg/input/entity.go delete mode 100644 pkg/input/solver_suite_test.go delete mode 100644 pkg/input/solver_test.go delete mode 100644 pkg/input/source_suite_test.go delete mode 100644 pkg/input/variable_source.go delete mode 100644 pkg/solver/constraints.go delete mode 100644 pkg/solver/constraints_test.go delete mode 100644 pkg/solver/solve_test.go delete mode 100644 pkg/solver/variable.go diff --git a/cmd/dimacs/cmd.go b/cmd/dimacs/cmd.go index 704900e..dd23e10 100644 --- a/cmd/dimacs/cmd.go +++ b/cmd/dimacs/cmd.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" - "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/deppy/solver" ) func NewDimacsCommand() *cobra.Command { @@ -53,7 +53,7 @@ func solve(path string) error { } // build solver - so, err := input.NewDeppySolver(NewDimacsEntitySource(dimacs), NewDimacsVariableSource(dimacs)) + so, err := solver.NewDeppySolver(NewDimacsEntitySource(dimacs), NewDimacsVariableSource(dimacs)) if err != nil { return err } diff --git a/cmd/dimacs/dimacs_constraints.go b/cmd/dimacs/dimacs_constraints.go index 775d88f..3b87989 100644 --- a/cmd/dimacs/dimacs_constraints.go +++ b/cmd/dimacs/dimacs_constraints.go @@ -4,8 +4,9 @@ import ( "context" "strings" - "github.com/operator-framework/deppy/pkg/input" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/constraint" + "github.com/operator-framework/deppy/pkg/deppy/input" ) var _ input.VariableSource = &ConstraintGenerator{} @@ -20,9 +21,9 @@ func NewDimacsVariableSource(dimacs *Dimacs) *ConstraintGenerator { } } -func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { - varMap := make(map[solver.Identifier]*input.SimpleVariable, len(d.dimacs.variables)) - variables := make([]solver.Variable, 0, len(d.dimacs.variables)) +func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { + varMap := make(map[deppy.Identifier]*input.SimpleVariable, len(d.dimacs.variables)) + variables := make([]deppy.Variable, 0, len(d.dimacs.variables)) if err := entitySource.Iterate(ctx, func(entity *input.Entity) error { variable := input.NewSimpleVariable(entity.Identifier()) variables = append(variables, variable) @@ -42,23 +43,23 @@ func (d *ConstraintGenerator) GetVariables(ctx context.Context, entitySource inp if len(terms) == 1 { // TODO: check constraints haven't already been added to the variable - variable := varMap[solver.Identifier(strings.TrimPrefix(first, "-"))] + variable := varMap[deppy.Identifier(strings.TrimPrefix(first, "-"))] if strings.HasPrefix(first, "-") { - variable.AddConstraint(solver.Not()) + variable.AddConstraint(constraint.Not()) } else { // TODO: is this the right constraint here? (given that its an achoring constraint?) - variable.AddConstraint(solver.Mandatory()) + variable.AddConstraint(constraint.Mandatory()) } continue } for i := 1; i < len(terms); i++ { - variable := varMap[solver.Identifier(strings.TrimPrefix(first, "-"))] + variable := varMap[deppy.Identifier(strings.TrimPrefix(first, "-"))] second := terms[i] negSubject := strings.HasPrefix(first, "-") negOperand := strings.HasPrefix(second, "-") // TODO: this Or constraint is hacky as hell - variable.AddConstraint(solver.Or(solver.Identifier(strings.TrimPrefix(second, "-")), negSubject, negOperand)) + variable.AddConstraint(constraint.Or(deppy.Identifier(strings.TrimPrefix(second, "-")), negSubject, negOperand)) first = second } } diff --git a/cmd/dimacs/dimacs_source.go b/cmd/dimacs/dimacs_source.go index fcd8c2d..0808528 100644 --- a/cmd/dimacs/dimacs_source.go +++ b/cmd/dimacs/dimacs_source.go @@ -1,8 +1,8 @@ package dimacs import ( - "github.com/operator-framework/deppy/pkg/input" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/input" ) var _ input.EntitySource = &EntitySource{} @@ -12,9 +12,9 @@ type EntitySource struct { } func NewDimacsEntitySource(dimacs *Dimacs) *EntitySource { - entities := make(map[solver.Identifier]input.Entity, len(dimacs.Variables())) + entities := make(map[deppy.Identifier]input.Entity, len(dimacs.Variables())) for _, variable := range dimacs.Variables() { - id := solver.Identifier(variable) + id := deppy.Identifier(variable) entities[id] = *input.NewEntity(id, nil) } diff --git a/cmd/sudoku/cmd.go b/cmd/sudoku/cmd.go index 7ae843a..73a79ae 100644 --- a/cmd/sudoku/cmd.go +++ b/cmd/sudoku/cmd.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" - "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/deppy/solver" ) func NewSudokuCommand() *cobra.Command { @@ -22,7 +22,7 @@ func NewSudokuCommand() *cobra.Command { func solve() error { // build solver sudoku := NewSudoku() - so, err := input.NewDeppySolver(sudoku, sudoku) + so, err := solver.NewDeppySolver(sudoku, sudoku) if err != nil { return err } diff --git a/cmd/sudoku/sudoku.go b/cmd/sudoku/sudoku.go index 404d9d5..f68897e 100644 --- a/cmd/sudoku/sudoku.go +++ b/cmd/sudoku/sudoku.go @@ -7,8 +7,9 @@ import ( "strconv" "time" - "github.com/operator-framework/deppy/pkg/input" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/constraint" + "github.com/operator-framework/deppy/pkg/deppy/input" ) var _ input.EntitySource = &Sudoku{} @@ -18,15 +19,15 @@ type Sudoku struct { *input.CacheEntitySource } -func GetID(row int, col int, num int) solver.Identifier { +func GetID(row int, col int, num int) deppy.Identifier { n := num n += col * 9 n += row * 81 - return solver.Identifier(fmt.Sprintf("%03d", n)) + return deppy.Identifier(fmt.Sprintf("%03d", n)) } func NewSudoku() *Sudoku { - var entities = make(map[solver.Identifier]input.Entity, 9*9*9) + var entities = make(map[deppy.Identifier]input.Entity, 9*9*9) for row := 0; row < 9; row++ { for col := 0; col < 9; col++ { for num := 0; num < 9; num++ { @@ -44,17 +45,17 @@ func NewSudoku() *Sudoku { } } -func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]solver.Variable, error) { +func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]deppy.Variable, error) { // adapted from: https://github.com/go-air/gini/blob/871d828a26852598db2b88f436549634ba9533ff/sudoku_test.go#L10 - variables := make(map[solver.Identifier]*input.SimpleVariable, 0) - inorder := make([]solver.Variable, 0) + variables := make(map[deppy.Identifier]*input.SimpleVariable, 0) + inorder := make([]deppy.Variable, 0) rand.Seed(time.Now().UnixNano()) // create variables for all number in all positions of the board for row := 0; row < 9; row++ { for col := 0; col < 9; col++ { for n := 0; n < 9; n++ { - variable := input.NewSimpleVariable(solver.Identifier(GetID(row, col, n))) + variable := input.NewSimpleVariable(GetID(row, col, n)) variables[variable.Identifier()] = variable inorder = append(inorder, variable) } @@ -64,16 +65,16 @@ func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]solve // add a clause stating that every position on the board has a number for row := 0; row < 9; row++ { for col := 0; col < 9; col++ { - ids := make([]solver.Identifier, 9) + ids := make([]deppy.Identifier, 9) for n := 0; n < 9; n++ { - ids[n] = solver.Identifier(GetID(row, col, n)) + ids[n] = GetID(row, col, n) } // randomize order to create new sudoku boards every run rand.Shuffle(len(ids), func(i, j int) { ids[i], ids[j] = ids[j], ids[i] }) // create clause that the particular position has a number - varID := solver.Identifier(fmt.Sprintf("%d-%d has a number", row, col)) - variable := input.NewSimpleVariable(varID, solver.Mandatory(), solver.Dependency(ids...)) + varID := deppy.Identifier(fmt.Sprintf("%d-%d has a number", row, col)) + variable := input.NewSimpleVariable(varID, constraint.Mandatory(), constraint.Dependency(ids...)) variables[varID] = variable inorder = append(inorder, variable) } @@ -83,10 +84,10 @@ func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]solve for n := 0; n < 9; n++ { for row := 0; row < 9; row++ { for colA := 0; colA < 9; colA++ { - idA := solver.Identifier(GetID(row, colA, n)) + idA := GetID(row, colA, n) variable := variables[idA] for colB := colA + 1; colB < 9; colB++ { - variable.AddConstraint(solver.Conflict(solver.Identifier(GetID(row, colB, n)))) + variable.AddConstraint(constraint.Conflict(GetID(row, colB, n))) } } } @@ -96,10 +97,10 @@ func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]solve for n := 0; n < 9; n++ { for col := 0; col < 9; col++ { for rowA := 0; rowA < 9; rowA++ { - idA := solver.Identifier(GetID(rowA, col, n)) + idA := GetID(rowA, col, n) variable := variables[idA] for rowB := rowA + 1; rowB < 9; rowB++ { - variable.AddConstraint(solver.Conflict(solver.Identifier(GetID(rowB, col, n)))) + variable.AddConstraint(constraint.Conflict(GetID(rowB, col, n))) } } } @@ -113,12 +114,12 @@ func (s Sudoku) GetVariables(ctx context.Context, _ input.EntitySource) ([]solve // all numbers for n := 0; n < 9; n++ { for i, offA := range offs { - idA := solver.Identifier(GetID(x+offA.x, y+offA.y, n)) + idA := GetID(x+offA.x, y+offA.y, n) variable := variables[idA] for j := i + 1; j < len(offs); j++ { offB := offs[j] - idB := solver.Identifier(GetID(x+offB.x, y+offB.y, n)) - variable.AddConstraint(solver.Conflict(idB)) + idB := GetID(x+offB.x, y+offB.y, n) + variable.AddConstraint(constraint.Conflict(idB)) } } } diff --git a/pkg/solver/bench_test.go b/internal/solver/bench_test.go similarity index 74% rename from pkg/solver/bench_test.go rename to internal/solver/bench_test.go index 05e95fe..977f694 100644 --- a/pkg/solver/bench_test.go +++ b/internal/solver/bench_test.go @@ -5,9 +5,12 @@ import ( "math/rand" "strconv" "testing" + + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/constraint" ) -var BenchmarkInput = func() []Variable { +var BenchmarkInput = func() []deppy.Variable { const ( length = 256 seed = 9 @@ -18,18 +21,18 @@ var BenchmarkInput = func() []Variable { nConflict = 3 ) - id := func(i int) Identifier { - return Identifier(strconv.Itoa(i)) + id := func(i int) deppy.Identifier { + return deppy.Identifier(strconv.Itoa(i)) } variable := func(i int) TestVariable { - var c []Constraint + var c []deppy.Constraint if rand.Float64() < pMandatory { - c = append(c, Mandatory()) + c = append(c, constraint.Mandatory()) } if rand.Float64() < pDependency { n := rand.Intn(nDependency-1) + 1 - var d []Identifier + var d []deppy.Identifier for x := 0; x < n; x++ { y := i for y == i { @@ -37,7 +40,7 @@ var BenchmarkInput = func() []Variable { } d = append(d, id(y)) } - c = append(c, Dependency(d...)) + c = append(c, constraint.Dependency(d...)) } if rand.Float64() < pConflict { n := rand.Intn(nConflict-1) + 1 @@ -46,7 +49,7 @@ var BenchmarkInput = func() []Variable { for y == i { y = rand.Intn(length) } - c = append(c, Conflict(id(y))) + c = append(c, constraint.Conflict(id(y))) } } return TestVariable{ @@ -56,7 +59,7 @@ var BenchmarkInput = func() []Variable { } rand.Seed(seed) - result := make([]Variable, length) + result := make([]deppy.Variable, length) for i := range result { result[i] = variable(i) } diff --git a/internal/solver/constraints.go b/internal/solver/constraints.go new file mode 100644 index 0000000..1d78a6e --- /dev/null +++ b/internal/solver/constraints.go @@ -0,0 +1,28 @@ +package solver + +import ( + "github.com/go-air/gini/z" + + "github.com/operator-framework/deppy/pkg/deppy" +) + +// zeroConstraint is returned by ConstraintOf in error cases. +type zeroConstraint struct{} + +var _ deppy.Constraint = zeroConstraint{} + +func (zeroConstraint) String(subject deppy.Identifier) string { + return "" +} + +func (zeroConstraint) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + return z.LitNull +} + +func (zeroConstraint) Order() []deppy.Identifier { + return nil +} + +func (zeroConstraint) Anchor() bool { + return false +} diff --git a/internal/solver/constraints_test.go b/internal/solver/constraints_test.go new file mode 100644 index 0000000..92d58c1 --- /dev/null +++ b/internal/solver/constraints_test.go @@ -0,0 +1,43 @@ +package solver + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/deppy/pkg/deppy/constraint" + + "github.com/operator-framework/deppy/pkg/deppy" +) + +func TestOrder(t *testing.T) { + type tc struct { + Name string + Constraint deppy.Constraint + Expected []deppy.Identifier + } + + for _, tt := range []tc{ + { + Name: "mandatory", + Constraint: constraint.Mandatory(), + }, + { + Name: "prohibited", + Constraint: constraint.Prohibited(), + }, + { + Name: "dependency", + Constraint: constraint.Dependency("a", "b", "c"), + Expected: []deppy.Identifier{"a", "b", "c"}, + }, + { + Name: "conflict", + Constraint: constraint.Conflict("a"), + }, + } { + t.Run(tt.Name, func(t *testing.T) { + assert.Equal(t, tt.Expected, tt.Constraint.Order()) + }) + } +} diff --git a/pkg/solver/doc.go b/internal/solver/doc.go similarity index 100% rename from pkg/solver/doc.go rename to internal/solver/doc.go diff --git a/pkg/solver/lit_mapping.go b/internal/solver/lit_mapping.go similarity index 74% rename from pkg/solver/lit_mapping.go rename to internal/solver/lit_mapping.go index eb7a739..64ff27c 100644 --- a/pkg/solver/lit_mapping.go +++ b/internal/solver/lit_mapping.go @@ -7,12 +7,14 @@ import ( "github.com/go-air/gini/inter" "github.com/go-air/gini/logic" "github.com/go-air/gini/z" + + "github.com/operator-framework/deppy/pkg/deppy" ) -type DuplicateIdentifier Identifier +type DuplicateIdentifier deppy.Identifier func (e DuplicateIdentifier) Error() string { - return fmt.Sprintf("duplicate identifier %q in input", Identifier(e)) + return fmt.Sprintf("duplicate identifier %q in input", deppy.Identifier(e)) } type inconsistentLitMapping []error @@ -25,10 +27,10 @@ func (inconsistentLitMapping) Error() string { // Solve (Constraints, Variables, etc.) and the variables that // appear in the SAT formula. type litMapping struct { - inorder []Variable - variables map[z.Lit]Variable - lits map[Identifier]z.Lit - constraints map[z.Lit]AppliedConstraint + inorder []deppy.Variable + variables map[z.Lit]deppy.Variable + lits map[deppy.Identifier]z.Lit + constraints map[z.Lit]deppy.AppliedConstraint c *logic.C errs inconsistentLitMapping } @@ -37,12 +39,12 @@ type litMapping struct { // the provided slice of Variables. This includes construction of // the translation tables between Variables/Constraints and the // inputs to the underlying solver. -func newLitMapping(variables []Variable) (*litMapping, error) { +func newLitMapping(variables []deppy.Variable) (*litMapping, error) { d := litMapping{ inorder: variables, - variables: make(map[z.Lit]Variable, len(variables)), - lits: make(map[Identifier]z.Lit, len(variables)), - constraints: make(map[z.Lit]AppliedConstraint), + variables: make(map[z.Lit]deppy.Variable, len(variables)), + lits: make(map[deppy.Identifier]z.Lit, len(variables)), + constraints: make(map[z.Lit]deppy.AppliedConstraint), c: logic.NewCCap(len(variables)), } @@ -58,7 +60,7 @@ func newLitMapping(variables []Variable) (*litMapping, error) { for _, variable := range variables { for _, constraint := range variable.Constraints() { - m := constraint.apply(d.c, &d, variable.Identifier()) + m := constraint.Apply(&d, variable.Identifier()) if m == z.LitNull { // This constraint doesn't have a // useful representation in the SAT @@ -66,7 +68,7 @@ func newLitMapping(variables []Variable) (*litMapping, error) { continue } - d.constraints[m] = AppliedConstraint{ + d.constraints[m] = deppy.AppliedConstraint{ Variable: variable, Constraint: constraint, } @@ -76,9 +78,15 @@ func newLitMapping(variables []Variable) (*litMapping, error) { return &d, nil } +// LogicCircuit returns the lit mappings internal logic circuit +// used by constraint for translation into boolean expressions processed by the solver +func (d *litMapping) LogicCircuit() *logic.C { + return d.c +} + // LitOf returns the positive literal corresponding to the Variable // with the given Identifier. -func (d *litMapping) LitOf(id Identifier) z.Lit { +func (d *litMapping) LitOf(id deppy.Identifier) z.Lit { m, ok := d.lits[id] if ok { return m @@ -89,7 +97,7 @@ func (d *litMapping) LitOf(id Identifier) z.Lit { // VariableOf returns the Variable corresponding to the provided // literal, or a zeroVariable if no such Variable exists. -func (d *litMapping) VariableOf(m z.Lit) Variable { +func (d *litMapping) VariableOf(m z.Lit) deppy.Variable { i, ok := d.variables[m] if ok { return i @@ -101,12 +109,12 @@ func (d *litMapping) VariableOf(m z.Lit) Variable { // ConstraintOf returns the constraint application corresponding to // the provided literal, or a zeroConstraint if no such constraint // exists. -func (d *litMapping) ConstraintOf(m z.Lit) AppliedConstraint { +func (d *litMapping) ConstraintOf(m z.Lit) deppy.AppliedConstraint { if a, ok := d.constraints[m]; ok { return a } d.errs = append(d.errs, fmt.Errorf("no constraint corresponding to %s", m)) - return AppliedConstraint{ + return deppy.AppliedConstraint{ Variable: zeroVariable{}, Constraint: zeroConstraint{}, } @@ -158,13 +166,13 @@ func (d *litMapping) CardinalityConstrainer(g inter.Adder, ms []z.Lit) *logic.Ca } // AnchorIdentifiers returns a slice containing the Identifiers of -// every Variable with at least one "anchor" constraint, in the -// order they appear in the input. -func (d *litMapping) AnchorIdentifiers() []Identifier { - var ids []Identifier +// every Variable with at least one "Anchor" constraint, in the +// Order they appear in the input. +func (d *litMapping) AnchorIdentifiers() []deppy.Identifier { + var ids []deppy.Identifier for _, variable := range d.inorder { for _, constraint := range variable.Constraints() { - if constraint.anchor() { + if constraint.Anchor() { ids = append(ids, variable.Identifier()) break } @@ -173,8 +181,8 @@ func (d *litMapping) AnchorIdentifiers() []Identifier { return ids } -func (d *litMapping) Variables(g inter.S) []Variable { - var result []Variable +func (d *litMapping) Variables(g inter.S) []deppy.Variable { + var result []deppy.Variable for _, i := range d.inorder { if g.Value(d.LitOf(i.Identifier())) { result = append(result, i) @@ -195,9 +203,9 @@ func (d *litMapping) Lits(dst []z.Lit) []z.Lit { return dst } -func (d *litMapping) Conflicts(g inter.Assumable) []AppliedConstraint { +func (d *litMapping) Conflicts(g inter.Assumable) []deppy.AppliedConstraint { whys := g.Why(nil) - as := make([]AppliedConstraint, 0, len(whys)) + as := make([]deppy.AppliedConstraint, 0, len(whys)) for _, why := range whys { if a, ok := d.constraints[why]; ok { as = append(as, a) diff --git a/pkg/solver/search.go b/internal/solver/search.go similarity index 93% rename from pkg/solver/search.go rename to internal/solver/search.go index 523b311..02c0fad 100644 --- a/pkg/solver/search.go +++ b/internal/solver/search.go @@ -5,6 +5,8 @@ import ( "github.com/go-air/gini/inter" "github.com/go-air/gini/z" + + "github.com/operator-framework/deppy/pkg/deppy" ) type choice struct { @@ -26,7 +28,7 @@ type search struct { assumptions map[z.Lit]struct{} // set of assumed lits - duplicates guess stack - for fast lookup guesses []guess // stack of assumed guesses headChoice, tailChoice *choice // deque of unmade choices - tracer Tracer + tracer deppy.Tracer result int buffer []z.Lit } @@ -59,7 +61,7 @@ func (h *search) PushGuess() { variable := h.lits.VariableOf(g.m) for _, constraint := range variable.Constraints() { var ms []z.Lit - for _, dependency := range constraint.order() { + for _, dependency := range constraint.Order() { ms = append(ms, h.lits.LitOf(dependency)) } if len(ms) > 0 { @@ -202,8 +204,8 @@ func (h *search) Do(ctx context.Context, anchors []z.Lit) (int, []z.Lit, map[z.L return result, lits, set } -func (h *search) Variables() []Variable { - result := make([]Variable, 0, len(h.guesses)) +func (h *search) Variables() []deppy.Variable { + result := make([]deppy.Variable, 0, len(h.guesses)) for _, g := range h.guesses { if g.m != z.LitNull { result = append(result, h.lits.VariableOf(g.candidates[g.index])) @@ -212,6 +214,6 @@ func (h *search) Variables() []Variable { return result } -func (h *search) Conflicts() []AppliedConstraint { +func (h *search) Conflicts() []deppy.AppliedConstraint { return h.lits.Conflicts(h.s) } diff --git a/pkg/solver/search_test.go b/internal/solver/search_test.go similarity index 77% rename from pkg/solver/search_test.go rename to internal/solver/search_test.go index fd9221a..2b9446f 100644 --- a/pkg/solver/search_test.go +++ b/internal/solver/search_test.go @@ -9,6 +9,10 @@ import ( "github.com/go-air/gini/inter" "github.com/go-air/gini/z" "github.com/stretchr/testify/assert" + + "github.com/operator-framework/deppy/pkg/deppy/constraint" + + "github.com/operator-framework/deppy/pkg/deppy" ) type TestScopeCounter struct { @@ -31,19 +35,19 @@ func (c *TestScopeCounter) Untest() (result int) { func TestSearch(t *testing.T) { type tc struct { Name string - Variables []Variable + Variables []deppy.Variable TestReturns []int UntestReturns []int Result int - Assumptions []Identifier + Assumptions []deppy.Identifier } for _, tt := range []tc{ { Name: "children popped from back of deque when guess popped", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("c")), - variable("b", Mandatory()), + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("c")), + variable("b", constraint.Mandatory()), variable("c"), }, TestReturns: []int{0, -1}, @@ -53,16 +57,16 @@ func TestSearch(t *testing.T) { }, { Name: "candidates exhausted", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("x")), - variable("b", Mandatory(), Dependency("y")), + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("x")), + variable("b", constraint.Mandatory(), constraint.Dependency("y")), variable("x"), variable("y"), }, TestReturns: []int{0, 0, -1, 1}, UntestReturns: []int{0}, Result: 1, - Assumptions: []Identifier{"a", "b", "y"}, + Assumptions: []deppy.Identifier{"a", "b", "y"}, }, } { t.Run(tt.Name, func(t *testing.T) { @@ -95,7 +99,7 @@ func TestSearch(t *testing.T) { result, ms, _ := h.Do(context.Background(), anchors) assert.Equal(tt.Result, result) - var ids []Identifier + var ids []deppy.Identifier for _, m := range ms { ids = append(ids, lits.VariableOf(m).Identifier()) } diff --git a/pkg/solver/solve.go b/internal/solver/solve.go similarity index 90% rename from pkg/solver/solve.go rename to internal/solver/solve.go index 76f22c1..c738601 100644 --- a/pkg/solver/solve.go +++ b/internal/solver/solve.go @@ -9,13 +9,15 @@ import ( "github.com/go-air/gini" "github.com/go-air/gini/inter" "github.com/go-air/gini/z" + + "github.com/operator-framework/deppy/pkg/deppy" ) var ErrIncomplete = errors.New("cancelled before a solution could be found") // NotSatisfiable is an error composed of a minimal set of applied // constraints that is sufficient to make a solution impossible. -type NotSatisfiable []AppliedConstraint +type NotSatisfiable []deppy.AppliedConstraint func (e NotSatisfiable) Error() string { const msg = "constraints not satisfiable" @@ -30,13 +32,13 @@ func (e NotSatisfiable) Error() string { } type Solver interface { - Solve(context.Context) ([]Variable, error) + Solve(context.Context) ([]deppy.Variable, error) } type solver struct { g inter.S litMap *litMapping - tracer Tracer + tracer deppy.Tracer buffer []z.Lit } @@ -50,7 +52,7 @@ const ( // containing only those Variables that were selected for // installation. If no solution is possible, or if the provided // Context times out or is cancelled, an error is returned. -func (s *solver) Solve(ctx context.Context) (result []Variable, err error) { +func (s *solver) Solve(ctx context.Context) (result []deppy.Variable, err error) { defer func() { // This likely indicates a bug, so discard whatever // return values were produced. @@ -78,7 +80,7 @@ func (s *solver) Solve(ctx context.Context) (result []Variable, err error) { // push a new test scope with the baseline assumptions, to prevent them from being cleared during search outcome, _ := s.g.Test(nil) if outcome != satisfiable && outcome != unsatisfiable { - // searcher for solutions in input order, so that preferences + // searcher for solutions in input Order, so that preferences // can be taken into acount (i.e. prefer one catalog to another) outcome, assumptions, aset = (&search{s: s.g, lits: s.litMap, tracer: s.tracer}).Do(context.Background(), assumptions) } @@ -130,7 +132,7 @@ func NewSolver(options ...Option) (Solver, error) { type Option func(s *solver) error -func WithInput(input []Variable) Option { +func WithInput(input []deppy.Variable) Option { return func(s *solver) error { var err error s.litMap, err = newLitMapping(input) @@ -138,7 +140,7 @@ func WithInput(input []Variable) Option { } } -func WithTracer(t Tracer) Option { +func WithTracer(t deppy.Tracer) Option { return func(s *solver) error { s.tracer = t return nil diff --git a/internal/solver/solve_test.go b/internal/solver/solve_test.go new file mode 100644 index 0000000..e8847bd --- /dev/null +++ b/internal/solver/solve_test.go @@ -0,0 +1,369 @@ +package solver + +import ( + "bytes" + "context" + "errors" + "fmt" + "reflect" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/operator-framework/deppy/pkg/deppy/constraint" + + "github.com/operator-framework/deppy/pkg/deppy" +) + +type TestVariable struct { + identifier deppy.Identifier + constraints []deppy.Constraint +} + +func (i TestVariable) Identifier() deppy.Identifier { + return i.identifier +} + +func (i TestVariable) Constraints() []deppy.Constraint { + return i.constraints +} + +func (i TestVariable) GoString() string { + return fmt.Sprintf("%q", i.Identifier()) +} + +func variable(id deppy.Identifier, constraints ...deppy.Constraint) deppy.Variable { + return TestVariable{ + identifier: id, + constraints: constraints, + } +} + +func TestNotSatisfiableError(t *testing.T) { + type tc struct { + Name string + Error NotSatisfiable + String string + } + + for _, tt := range []tc{ + { + Name: "nil", + String: "constraints not satisfiable", + }, + { + Name: "empty", + String: "constraints not satisfiable", + Error: NotSatisfiable{}, + }, + { + Name: "single failure", + Error: NotSatisfiable{ + deppy.AppliedConstraint{ + Variable: variable("a", constraint.Mandatory()), + Constraint: constraint.Mandatory(), + }, + }, + String: fmt.Sprintf("constraints not satisfiable: %s", + constraint.Mandatory().String("a")), + }, + { + Name: "multiple failures", + Error: NotSatisfiable{ + deppy.AppliedConstraint{ + Variable: variable("a", constraint.Mandatory()), + Constraint: constraint.Mandatory(), + }, + deppy.AppliedConstraint{ + Variable: variable("b", constraint.Prohibited()), + Constraint: constraint.Prohibited(), + }, + }, + String: fmt.Sprintf("constraints not satisfiable: %s, %s", + constraint.Mandatory().String("a"), constraint.Prohibited().String("b")), + }, + } { + t.Run(tt.Name, func(t *testing.T) { + assert.Equal(t, tt.String, tt.Error.Error()) + }) + } +} + +func TestSolve(t *testing.T) { + type tc struct { + Name string + Variables []deppy.Variable + Installed []deppy.Identifier + Error error + } + + for _, tt := range []tc{ + { + Name: "no variables", + }, + { + Name: "unnecessary variable is not installed", + Variables: []deppy.Variable{variable("a")}, + }, + { + Name: "single mandatory variable is installed", + Variables: []deppy.Variable{variable("a", constraint.Mandatory())}, + Installed: []deppy.Identifier{"a"}, + }, + { + Name: "both mandatory and prohibited produce error", + Variables: []deppy.Variable{variable("a", constraint.Mandatory(), constraint.Prohibited())}, + Error: NotSatisfiable{ + { + Variable: variable("a", constraint.Mandatory(), constraint.Prohibited()), + Constraint: constraint.Mandatory(), + }, + { + Variable: variable("a", constraint.Mandatory(), constraint.Prohibited()), + Constraint: constraint.Prohibited(), + }, + }, + }, + { + Name: "dependency is installed", + Variables: []deppy.Variable{ + variable("a"), + variable("b", constraint.Mandatory(), constraint.Dependency("a")), + }, + Installed: []deppy.Identifier{"a", "b"}, + }, + { + Name: "transitive dependency is installed", + Variables: []deppy.Variable{ + variable("a"), + variable("b", constraint.Dependency("a")), + variable("c", constraint.Mandatory(), constraint.Dependency("b")), + }, + Installed: []deppy.Identifier{"a", "b", "c"}, + }, + { + Name: "both dependencies are installed", + Variables: []deppy.Variable{ + variable("a"), + variable("b"), + variable("c", constraint.Mandatory(), constraint.Dependency("a"), constraint.Dependency("b")), + }, + Installed: []deppy.Identifier{"a", "b", "c"}, + }, + { + Name: "solution with first dependency is selected", + Variables: []deppy.Variable{ + variable("a"), + variable("b", constraint.Conflict("a")), + variable("c", constraint.Mandatory(), constraint.Dependency("a", "b")), + }, + Installed: []deppy.Identifier{"a", "c"}, + }, + { + Name: "solution with only first dependency is selected", + Variables: []deppy.Variable{ + variable("a"), + variable("b"), + variable("c", constraint.Mandatory(), constraint.Dependency("a", "b")), + }, + Installed: []deppy.Identifier{"a", "c"}, + }, + { + Name: "solution with first dependency is selected (reverse)", + Variables: []deppy.Variable{ + variable("a"), + variable("b", constraint.Conflict("a")), + variable("c", constraint.Mandatory(), constraint.Dependency("b", "a")), + }, + Installed: []deppy.Identifier{"b", "c"}, + }, + { + Name: "two mandatory but conflicting packages", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory()), + variable("b", constraint.Mandatory(), constraint.Conflict("a")), + }, + Error: NotSatisfiable{ + { + Variable: variable("a", constraint.Mandatory()), + Constraint: constraint.Mandatory(), + }, + { + Variable: variable("b", constraint.Mandatory(), constraint.Conflict("a")), + Constraint: constraint.Mandatory(), + }, + { + Variable: variable("b", constraint.Mandatory(), constraint.Conflict("a")), + Constraint: constraint.Conflict("a"), + }, + }, + }, + { + Name: "irrelevant dependencies don't influence search Order", + Variables: []deppy.Variable{ + variable("a", constraint.Dependency("x", "y")), + variable("b", constraint.Mandatory(), constraint.Dependency("y", "x")), + variable("x"), + variable("y"), + }, + Installed: []deppy.Identifier{"b", "y"}, + }, + { + Name: "cardinality constraint prevents resolution", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("x", "y"), constraint.AtMost(1, "x", "y")), + variable("x", constraint.Mandatory()), + variable("y", constraint.Mandatory()), + }, + Error: NotSatisfiable{ + { + Variable: variable("a", constraint.Mandatory(), constraint.Dependency("x", "y"), constraint.AtMost(1, "x", "y")), + Constraint: constraint.AtMost(1, "x", "y"), + }, + { + Variable: variable("x", constraint.Mandatory()), + Constraint: constraint.Mandatory(), + }, + { + Variable: variable("y", constraint.Mandatory()), + Constraint: constraint.Mandatory(), + }, + }, + }, + { + Name: "cardinality constraint forces alternative", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("x", "y"), constraint.AtMost(1, "x", "y")), + variable("b", constraint.Mandatory(), constraint.Dependency("y")), + variable("x"), + variable("y"), + }, + Installed: []deppy.Identifier{"a", "b", "y"}, + }, + { + Name: "two dependencies satisfied by one variable", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("y")), + variable("b", constraint.Mandatory(), constraint.Dependency("x", "y")), + variable("x"), + variable("y"), + }, + Installed: []deppy.Identifier{"a", "b", "y"}, + }, + { + Name: "foo two dependencies satisfied by one variable", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("y", "z", "m")), + variable("b", constraint.Mandatory(), constraint.Dependency("x", "y")), + variable("x"), + variable("y"), + variable("z"), + variable("m"), + }, + Installed: []deppy.Identifier{"a", "b", "y"}, + }, + { + Name: "result size larger than minimum due to preference", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("x", "y")), + variable("b", constraint.Mandatory(), constraint.Dependency("y")), + variable("x"), + variable("y"), + }, + Installed: []deppy.Identifier{"a", "b", "x", "y"}, + }, + { + Name: "only the least preferable choice is acceptable", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("a1", "a2")), + variable("a1", constraint.Conflict("c1"), constraint.Conflict("c2")), + variable("a2", constraint.Conflict("c1")), + variable("b", constraint.Mandatory(), constraint.Dependency("b1", "b2")), + variable("b1", constraint.Conflict("c1"), constraint.Conflict("c2")), + variable("b2", constraint.Conflict("c1")), + variable("c", constraint.Mandatory(), constraint.Dependency("c1", "c2")), + variable("c1"), + variable("c2"), + }, + Installed: []deppy.Identifier{"a", "a2", "b", "b2", "c", "c2"}, + }, + { + Name: "preferences respected with multiple dependencies per variable", + Variables: []deppy.Variable{ + variable("a", constraint.Mandatory(), constraint.Dependency("x1", "x2"), constraint.Dependency("y1", "y2")), + variable("x1"), + variable("x2"), + variable("y1"), + variable("y2"), + }, + Installed: []deppy.Identifier{"a", "x1", "y1"}, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + assert := assert.New(t) + + var traces bytes.Buffer + s, err := NewSolver(WithInput(tt.Variables), WithTracer(LoggingTracer{Writer: &traces})) + if err != nil { + t.Fatalf("failed to initialize solver: %s", err) + } + + installed, err := s.Solve(context.TODO()) + + if installed != nil { + sort.SliceStable(installed, func(i, j int) bool { + return installed[i].Identifier() < installed[j].Identifier() + }) + } + + // Failed constraints are sorted in lexically + // increasing Order of the identifier of the + // constraint's variable, with ties broken + // in favor of the constraint that appears + // earliest in the variable's list of + // constraints. + var ns NotSatisfiable + if errors.As(err, &ns) { + sort.SliceStable(ns, func(i, j int) bool { + if ns[i].Variable.Identifier() != ns[j].Variable.Identifier() { + return ns[i].Variable.Identifier() < ns[j].Variable.Identifier() + } + var x, y int + for ii, c := range ns[i].Variable.Constraints() { + if reflect.DeepEqual(c, ns[i].Constraint) { + x = ii + break + } + } + for ij, c := range ns[j].Variable.Constraints() { + if reflect.DeepEqual(c, ns[j].Constraint) { + y = ij + break + } + } + return x < y + }) + } + + var ids []deppy.Identifier + for _, variable := range installed { + ids = append(ids, variable.Identifier()) + } + assert.Equal(tt.Installed, ids) + assert.Equal(tt.Error, err) + + if t.Failed() { + t.Logf("\n%s", traces.String()) + } + }) + } +} + +func TestDuplicateIdentifier(t *testing.T) { + _, err := NewSolver(WithInput([]deppy.Variable{ + variable("a"), + variable("a"), + })) + assert.Equal(t, DuplicateIdentifier("a"), err) +} diff --git a/pkg/solver/tracer.go b/internal/solver/tracer.go similarity index 60% rename from pkg/solver/tracer.go rename to internal/solver/tracer.go index b103536..3d26e59 100644 --- a/pkg/solver/tracer.go +++ b/internal/solver/tracer.go @@ -3,27 +3,20 @@ package solver import ( "fmt" "io" -) - -type SearchPosition interface { - Variables() []Variable - Conflicts() []AppliedConstraint -} -type Tracer interface { - Trace(p SearchPosition) -} + "github.com/operator-framework/deppy/pkg/deppy" +) type DefaultTracer struct{} -func (DefaultTracer) Trace(_ SearchPosition) { +func (DefaultTracer) Trace(_ deppy.SearchPosition) { } type LoggingTracer struct { Writer io.Writer } -func (t LoggingTracer) Trace(p SearchPosition) { +func (t LoggingTracer) Trace(p deppy.SearchPosition) { fmt.Fprintf(t.Writer, "---\nAssumptions:\n") for _, i := range p.Variables() { fmt.Fprintf(t.Writer, "- %s\n", i.Identifier()) diff --git a/internal/solver/variable.go b/internal/solver/variable.go new file mode 100644 index 0000000..eb61997 --- /dev/null +++ b/internal/solver/variable.go @@ -0,0 +1,18 @@ +package solver + +import ( + "github.com/operator-framework/deppy/pkg/deppy" +) + +// zeroVariable is returned by VariableOf in error cases. +type zeroVariable struct{} + +var _ deppy.Variable = zeroVariable{} + +func (zeroVariable) Identifier() deppy.Identifier { + return "" +} + +func (zeroVariable) Constraints() []deppy.Constraint { + return nil +} diff --git a/pkg/solver/zz_search_test.go b/internal/solver/zz_search_test.go similarity index 100% rename from pkg/solver/zz_search_test.go rename to internal/solver/zz_search_test.go diff --git a/pkg/deppy/api.go b/pkg/deppy/api.go new file mode 100644 index 0000000..a98a719 --- /dev/null +++ b/pkg/deppy/api.go @@ -0,0 +1,62 @@ +package deppy + +import ( + "github.com/go-air/gini/logic" + "github.com/go-air/gini/z" +) + +// Identifier values uniquely identify particular Variables within +// the input to a single call to Solve. +type Identifier string + +func (id Identifier) String() string { + return string(id) +} + +// IdentifierFromString returns an Identifier based on a provided +// string. +func IdentifierFromString(s string) Identifier { + return Identifier(s) +} + +// Variable values are the basic unit of problems and solutions +// understood by this package. +type Variable interface { + // Identifier returns the Identifier that uniquely identifies + // this Variable among all other Variables in a given + // problem. + Identifier() Identifier + // Constraints returns the set of constraints that apply to + // this Variable. + Constraints() []Constraint +} + +// LitMapping performs translation between the input and output types of +// Solve (Constraints, Variables, etc.) and the variables that +// appear in the SAT formula. +type LitMapping interface { + LitOf(subject Identifier) z.Lit + LogicCircuit() *logic.C +} + +// Constraint implementations limit the circumstances under which a +// particular Variable can appear in a solution. +type Constraint interface { + String(subject Identifier) string + Apply(lm LitMapping, subject Identifier) z.Lit + Order() []Identifier + Anchor() bool +} + +// AppliedConstraint values compose a single Constraint with the +// Variable it applies to. +type AppliedConstraint struct { + Variable Variable + Constraint Constraint +} + +// String implements fmt.Stringer and returns a human-readable message +// representing the receiver. +func (a AppliedConstraint) String() string { + return a.Constraint.String(a.Variable.Identifier()) +} diff --git a/pkg/deppy/constraint/constraint.go b/pkg/deppy/constraint/constraint.go new file mode 100644 index 0000000..0134c2c --- /dev/null +++ b/pkg/deppy/constraint/constraint.go @@ -0,0 +1,208 @@ +package constraint + +import ( + "fmt" + "strings" + + "github.com/go-air/gini/z" + + "github.com/operator-framework/deppy/pkg/deppy" +) + +type mandatory struct{} + +func (constraint mandatory) String(subject deppy.Identifier) string { + return fmt.Sprintf("%s is mandatory", subject) +} + +func (constraint mandatory) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + return lm.LitOf(subject) +} + +func (constraint mandatory) Order() []deppy.Identifier { + return nil +} + +func (constraint mandatory) Anchor() bool { + return true +} + +// Mandatory returns a Constraint that will permit only solutions that +// contain a particular Variable. +func Mandatory() deppy.Constraint { + return mandatory{} +} + +type prohibited struct{} + +func (constraint prohibited) String(subject deppy.Identifier) string { + return fmt.Sprintf("%s is prohibited", subject) +} + +func (constraint prohibited) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + return lm.LitOf(subject).Not() +} + +func (constraint prohibited) Order() []deppy.Identifier { + return nil +} + +func (constraint prohibited) Anchor() bool { + return false +} + +// Prohibited returns a Constraint that will reject any solution that +// contains a particular Variable. Callers may also decide to omit +// an Variable from input to Solve rather than Apply such a +// Constraint. +func Prohibited() deppy.Constraint { + return prohibited{} +} + +func Not() deppy.Constraint { + return prohibited{} +} + +type dependency []deppy.Identifier + +func (constraint dependency) String(subject deppy.Identifier) string { + if len(constraint) == 0 { + return fmt.Sprintf("%s has a dependency without any candidates to satisfy it", subject) + } + s := make([]string, len(constraint)) + for i, each := range constraint { + s[i] = string(each) + } + return fmt.Sprintf("%s requires at least one of %s", subject, strings.Join(s, ", ")) +} + +func (constraint dependency) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + m := lm.LitOf(subject).Not() + for _, each := range constraint { + m = lm.LogicCircuit().Or(m, lm.LitOf(each)) + } + return m +} + +func (constraint dependency) Order() []deppy.Identifier { + return constraint +} + +func (constraint dependency) Anchor() bool { + return false +} + +// Dependency returns a Constraint that will only permit solutions +// containing a given Variable on the condition that at least one +// of the Variables identified by the given Identifiers also +// appears in the solution. Identifiers appearing earlier in the +// argument list have higher preference than those appearing later. +func Dependency(ids ...deppy.Identifier) deppy.Constraint { + return dependency(ids) +} + +type conflict deppy.Identifier + +func (constraint conflict) String(subject deppy.Identifier) string { + return fmt.Sprintf("%s conflicts with %s", subject, constraint) +} + +func (constraint conflict) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + return lm.LogicCircuit().Or(lm.LitOf(subject).Not(), lm.LitOf(deppy.Identifier(constraint)).Not()) +} + +func (constraint conflict) Order() []deppy.Identifier { + return nil +} + +func (constraint conflict) Anchor() bool { + return false +} + +// Conflict returns a Constraint that will permit solutions containing +// either the constrained Variable, the Variable identified by +// the given Identifier, or neither, but not both. +func Conflict(id deppy.Identifier) deppy.Constraint { + return conflict(id) +} + +type leq struct { + ids []deppy.Identifier + n int +} + +func (constraint leq) String(subject deppy.Identifier) string { + s := make([]string, len(constraint.ids)) + for i, each := range constraint.ids { + s[i] = string(each) + } + return fmt.Sprintf("%s permits at most %d of %s", subject, constraint.n, strings.Join(s, ", ")) +} + +func (constraint leq) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + ms := make([]z.Lit, len(constraint.ids)) + for i, each := range constraint.ids { + ms[i] = lm.LitOf(each) + } + return lm.LogicCircuit().CardSort(ms).Leq(constraint.n) +} + +func (constraint leq) Order() []deppy.Identifier { + return nil +} + +func (constraint leq) Anchor() bool { + return false +} + +// AtMost returns a Constraint that forbids solutions that contain +// more than n of the Variables identified by the given +// Identifiers. +func AtMost(n int, ids ...deppy.Identifier) deppy.Constraint { + return leq{ + ids: ids, + n: n, + } +} + +type or struct { + operand deppy.Identifier + isSubjectNegated bool + isOperandNegated bool +} + +func (constraint or) String(subject deppy.Identifier) string { + return fmt.Sprintf("%s is prohibited", subject) +} + +func (constraint or) Apply(lm deppy.LitMapping, subject deppy.Identifier) z.Lit { + subjectLit := lm.LitOf(subject) + if constraint.isSubjectNegated { + subjectLit = subjectLit.Not() + } + operandLit := lm.LitOf(constraint.operand) + if constraint.isOperandNegated { + operandLit = operandLit.Not() + } + return lm.LogicCircuit().Or(subjectLit, operandLit) +} + +func (constraint or) Order() []deppy.Identifier { + return nil +} + +func (constraint or) Anchor() bool { + return false +} + +// Or returns a constraints in the form subject OR identifier +// if isSubjectNegated = true, ~subject OR identifier +// if isOperandNegated = true, subject OR ~identifier +// if both are true: ~subject OR ~identifier +func Or(identifier deppy.Identifier, isSubjectNegated bool, isOperandNegated bool) deppy.Constraint { + return or{ + operand: identifier, + isSubjectNegated: isSubjectNegated, + isOperandNegated: isOperandNegated, + } +} diff --git a/pkg/input/cache_entity_source.go b/pkg/deppy/input/cache_entity_source.go similarity index 80% rename from pkg/input/cache_entity_source.go rename to pkg/deppy/input/cache_entity_source.go index 2f7de8e..b0095f0 100644 --- a/pkg/input/cache_entity_source.go +++ b/pkg/deppy/input/cache_entity_source.go @@ -3,23 +3,23 @@ package input import ( "context" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" ) var _ EntitySource = &CacheEntitySource{} type CacheEntitySource struct { // TODO: separate out a cache - entities map[solver.Identifier]Entity + entities map[deppy.Identifier]Entity } -func NewCacheQuerier(entities map[solver.Identifier]Entity) *CacheEntitySource { +func NewCacheQuerier(entities map[deppy.Identifier]Entity) *CacheEntitySource { return &CacheEntitySource{ entities: entities, } } -func (c CacheEntitySource) Get(_ context.Context, id solver.Identifier) *Entity { +func (c CacheEntitySource) Get(_ context.Context, id deppy.Identifier) *Entity { if entity, ok := c.entities[id]; ok { return &entity } diff --git a/pkg/deppy/input/entity.go b/pkg/deppy/input/entity.go new file mode 100644 index 0000000..6bcd0b3 --- /dev/null +++ b/pkg/deppy/input/entity.go @@ -0,0 +1,21 @@ +package input + +import ( + "github.com/operator-framework/deppy/pkg/deppy" +) + +type Entity struct { + ID deppy.Identifier `json:"identifier"` + Properties map[string]string `json:"properties"` +} + +func (e *Entity) Identifier() deppy.Identifier { + return e.ID +} + +func NewEntity(id deppy.Identifier, properties map[string]string) *Entity { + return &Entity{ + ID: id, + Properties: properties, + } +} diff --git a/pkg/input/entity_source.go b/pkg/deppy/input/entity_source.go similarity index 89% rename from pkg/input/entity_source.go rename to pkg/deppy/input/entity_source.go index bb3b2c9..a21a85d 100644 --- a/pkg/input/entity_source.go +++ b/pkg/deppy/input/entity_source.go @@ -3,7 +3,7 @@ package input import ( "context" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" ) // IteratorFunction is executed for each entity when iterating over all entities @@ -24,7 +24,7 @@ type EntityListMap map[string]EntityList // EntitySource provides a query and content acquisition interface for arbitrary entity stores type EntitySource interface { - Get(ctx context.Context, id solver.Identifier) *Entity + Get(ctx context.Context, id deppy.Identifier) *Entity Filter(ctx context.Context, filter Predicate) (EntityList, error) GroupBy(ctx context.Context, fn GroupByFunction) (EntityListMap, error) Iterate(ctx context.Context, fn IteratorFunction) error diff --git a/pkg/input/entity_source_test.go b/pkg/deppy/input/entity_source_test.go similarity index 86% rename from pkg/input/entity_source_test.go rename to pkg/deppy/input/entity_source_test.go index 3638f52..f4c6fd7 100644 --- a/pkg/input/entity_source_test.go +++ b/pkg/deppy/input/entity_source_test.go @@ -7,9 +7,9 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" . "github.com/onsi/gomega/gstruct" ) @@ -41,7 +41,7 @@ func bySource(source string) input.Predicate { } // Test function for iterate -var entityCheck map[solver.Identifier]bool +var entityCheck map[deppy.Identifier]bool func check(entity *input.Entity) error { checked, ok := entityCheck[entity.Identifier()] @@ -73,11 +73,11 @@ var _ = Describe("EntitySource", func() { ) BeforeEach(func() { - entities := map[solver.Identifier]input.Entity{ - solver.Identifier("1-1"): *input.NewEntity("1-1", map[string]string{"source": "1", "index": "1"}), - solver.Identifier("1-2"): *input.NewEntity("1-2", map[string]string{"source": "1", "index": "2"}), - solver.Identifier("2-1"): *input.NewEntity("2-1", map[string]string{"source": "2", "index": "1"}), - solver.Identifier("2-2"): *input.NewEntity("2-2", map[string]string{"source": "2", "index": "2"}), + entities := map[deppy.Identifier]input.Entity{ + deppy.Identifier("1-1"): *input.NewEntity("1-1", map[string]string{"source": "1", "index": "1"}), + deppy.Identifier("1-2"): *input.NewEntity("1-2", map[string]string{"source": "1", "index": "2"}), + deppy.Identifier("2-1"): *input.NewEntity("2-1", map[string]string{"source": "2", "index": "1"}), + deppy.Identifier("2-2"): *input.NewEntity("2-2", map[string]string{"source": "2", "index": "2"}), } entitySource = input.NewCacheQuerier(entities) }) @@ -86,7 +86,7 @@ var _ = Describe("EntitySource", func() { It("should return requested entity", func() { e := entitySource.Get(context.Background(), "2-2") Expect(e).NotTo(BeNil()) - Expect(e.Identifier()).To(Equal(solver.Identifier("2-2"))) + Expect(e.Identifier()).To(Equal(deppy.Identifier("2-2"))) }) }) @@ -137,7 +137,7 @@ var _ = Describe("EntitySource", func() { Describe("Iterate", func() { It("should go through all entities", func() { - entityCheck = map[solver.Identifier]bool{"1-1": false, "1-2": false, "2-1": false, "2-2": false} + entityCheck = map[deppy.Identifier]bool{"1-1": false, "1-2": false, "2-1": false, "2-2": false} err := entitySource.Iterate(context.Background(), check) Expect(err).To(BeNil()) for _, value := range entityCheck { diff --git a/pkg/input/entity_test.go b/pkg/deppy/input/entity_test.go similarity index 77% rename from pkg/input/entity_test.go rename to pkg/deppy/input/entity_test.go index 2a52eae..c926d6d 100644 --- a/pkg/input/entity_test.go +++ b/pkg/deppy/input/entity_test.go @@ -4,14 +4,15 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/input" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy/input" + + "github.com/operator-framework/deppy/pkg/deppy" ) var _ = Describe("Entity", func() { It("stores id and properties", func() { entity := input.NewEntity("id", map[string]string{"prop": "value"}) - Expect(entity.Identifier()).To(Equal(solver.Identifier("id"))) + Expect(entity.Identifier()).To(Equal(deppy.Identifier("id"))) value, ok := entity.Properties["prop"] Expect(ok).To(BeTrue()) diff --git a/pkg/input/query.go b/pkg/deppy/input/query.go similarity index 87% rename from pkg/input/query.go rename to pkg/deppy/input/query.go index 72714dc..4838545 100644 --- a/pkg/input/query.go +++ b/pkg/deppy/input/query.go @@ -3,7 +3,7 @@ package input import ( "sort" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy" ) func (r EntityList) Sort(fn SortFunction) EntityList { @@ -13,8 +13,8 @@ func (r EntityList) Sort(fn SortFunction) EntityList { return r } -func (r EntityList) CollectIds() []solver.Identifier { - ids := make([]solver.Identifier, len(r)) +func (r EntityList) CollectIds() []deppy.Identifier { + ids := make([]deppy.Identifier, len(r)) for i := range r { ids[i] = r[i].Identifier() } diff --git a/pkg/deppy/input/variable_source.go b/pkg/deppy/input/variable_source.go new file mode 100644 index 0000000..2490831 --- /dev/null +++ b/pkg/deppy/input/variable_source.go @@ -0,0 +1,38 @@ +package input + +import ( + "context" + + "github.com/operator-framework/deppy/pkg/deppy" +) + +// VariableSource generates solver constraints given an entity querier interface +type VariableSource interface { + GetVariables(ctx context.Context, entitySource EntitySource) ([]deppy.Variable, error) +} + +var _ deppy.Variable = &SimpleVariable{} + +type SimpleVariable struct { + id deppy.Identifier + constraints []deppy.Constraint +} + +func (s *SimpleVariable) Identifier() deppy.Identifier { + return s.id +} + +func (s *SimpleVariable) Constraints() []deppy.Constraint { + return s.constraints +} + +func (s *SimpleVariable) AddConstraint(constraint deppy.Constraint) { + s.constraints = append(s.constraints, constraint) +} + +func NewSimpleVariable(id deppy.Identifier, constraints ...deppy.Constraint) *SimpleVariable { + return &SimpleVariable{ + id: id, + constraints: constraints, + } +} diff --git a/pkg/input/solver.go b/pkg/deppy/solver/solver.go similarity index 77% rename from pkg/input/solver.go rename to pkg/deppy/solver/solver.go index 85f74e4..c0f88ce 100644 --- a/pkg/input/solver.go +++ b/pkg/deppy/solver/solver.go @@ -1,9 +1,11 @@ -package input +package solver import ( "context" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/internal/solver" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/input" ) // TODO: should disambiguate between solver errors due to constraints @@ -12,16 +14,16 @@ import ( // Solution is returned by the Solver when a solution could be found // it can be queried by Identifier to see if the entity was selected (true) or not (false) // by the solver -type Solution map[solver.Identifier]bool +type Solution map[deppy.Identifier]bool // DeppySolver is a simple solver implementation that takes an entity source group and a constraint aggregator // to produce a Solution (or error if no solution can be found) type DeppySolver struct { - entitySource EntitySource - variableSource VariableSource + entitySource input.EntitySource + variableSource input.VariableSource } -func NewDeppySolver(entitySource EntitySource, variableSource VariableSource) (*DeppySolver, error) { +func NewDeppySolver(entitySource input.EntitySource, variableSource input.VariableSource) (*DeppySolver, error) { return &DeppySolver{ entitySource: entitySource, variableSource: variableSource, diff --git a/pkg/deppy/solver/solver_test.go b/pkg/deppy/solver/solver_test.go new file mode 100644 index 0000000..1731027 --- /dev/null +++ b/pkg/deppy/solver/solver_test.go @@ -0,0 +1,187 @@ +package solver_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/operator-framework/deppy/pkg/deppy/input" + + "github.com/operator-framework/deppy/pkg/deppy/solver" + + "github.com/operator-framework/deppy/pkg/deppy/constraint" + + "github.com/operator-framework/deppy/pkg/deppy" + + . "github.com/onsi/gomega/gstruct" +) + +type EntitySourceStruct struct { + variables []deppy.Variable + input.EntitySource +} + +func (c EntitySourceStruct) GetVariables(_ context.Context, _ input.EntitySource) ([]deppy.Variable, error) { + return c.variables, nil +} + +func NewEntitySource(variables []deppy.Variable) *EntitySourceStruct { + entities := make(map[deppy.Identifier]input.Entity, len(variables)) + for _, variable := range variables { + entityID := variable.Identifier() + entities[entityID] = *input.NewEntity(entityID, map[string]string{"x": "y"}) + } + return &EntitySourceStruct{ + variables: variables, + EntitySource: input.NewCacheQuerier(entities), + } +} + +var _ = Describe("Entity", func() { + It("should select a mandatory entity", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Mandatory()), + input.NewSimpleVariable("2"), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(true), + deppy.Identifier("2"): Equal(false), + })) + }) + + It("should select two mandatory entities", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Mandatory()), + input.NewSimpleVariable("2", constraint.Mandatory()), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(true), + deppy.Identifier("2"): Equal(true), + })) + }) + + It("should select a mandatory entity and its dependency", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Mandatory(), constraint.Dependency("2")), + input.NewSimpleVariable("2"), + input.NewSimpleVariable("3"), + } + s := NewEntitySource(variables) + + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(true), + deppy.Identifier("2"): Equal(true), + deppy.Identifier("3"): Equal(false), + })) + }) + + It("should fail when a dependency is prohibited", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Mandatory(), constraint.Dependency("2")), + input.NewSimpleVariable("2", constraint.Prohibited()), + input.NewSimpleVariable("3"), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + _, err = so.Solve(context.Background()) + Expect(err).Should(HaveOccurred()) + }) + + It("should select a mandatory entity and its dependency and ignore a non-mandatory prohibited variable", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Mandatory(), constraint.Dependency("2")), + input.NewSimpleVariable("2"), + input.NewSimpleVariable("3", constraint.Prohibited()), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(true), + deppy.Identifier("2"): Equal(true), + deppy.Identifier("3"): Equal(false), + })) + }) + + It("should not select 'or' paths that are prohibited", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Or("2", false, false), constraint.Dependency("3")), + input.NewSimpleVariable("2", constraint.Dependency("4")), + input.NewSimpleVariable("3", constraint.Prohibited()), + input.NewSimpleVariable("4"), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(false), + deppy.Identifier("2"): Equal(true), + deppy.Identifier("3"): Equal(false), + deppy.Identifier("4"): Equal(true), + })) + }) + + It("should respect atMost constraint", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Or("2", false, false), constraint.Dependency("3"), constraint.Dependency("4")), + input.NewSimpleVariable("2", constraint.Dependency("3")), + input.NewSimpleVariable("3", constraint.AtMost(1, "3", "4")), + input.NewSimpleVariable("4"), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(false), + deppy.Identifier("2"): Equal(true), + deppy.Identifier("3"): Equal(true), + deppy.Identifier("4"): Equal(false), + })) + }) + + It("should respect dependency conflicts", func() { + variables := []deppy.Variable{ + input.NewSimpleVariable("1", constraint.Or("2", false, false), constraint.Dependency("3"), constraint.Dependency("4")), + input.NewSimpleVariable("2", constraint.Dependency("4"), constraint.Dependency("5")), + input.NewSimpleVariable("3", constraint.Conflict("6")), + input.NewSimpleVariable("4", constraint.Dependency("6")), + input.NewSimpleVariable("5"), + input.NewSimpleVariable("6"), + } + s := NewEntitySource(variables) + so, err := solver.NewDeppySolver(s, s) + Expect(err).To(BeNil()) + solution, err := so.Solve(context.Background()) + Expect(err).To(BeNil()) + Expect(solution).To(MatchAllKeys(Keys{ + deppy.Identifier("1"): Equal(false), + deppy.Identifier("2"): Equal(true), + deppy.Identifier("3"): Equal(false), + deppy.Identifier("4"): Equal(true), + deppy.Identifier("5"): Equal(true), + deppy.Identifier("6"): Equal(true), + })) + }) +}) diff --git a/pkg/deppy/tracer.go b/pkg/deppy/tracer.go new file mode 100644 index 0000000..5d33fbe --- /dev/null +++ b/pkg/deppy/tracer.go @@ -0,0 +1,10 @@ +package deppy + +type SearchPosition interface { + Variables() []Variable + Conflicts() []AppliedConstraint +} + +type Tracer interface { + Trace(p SearchPosition) +} diff --git a/pkg/ext/olm/constraints.go b/pkg/ext/olm/constraints.go index c09de54..adaae3e 100644 --- a/pkg/ext/olm/constraints.go +++ b/pkg/ext/olm/constraints.go @@ -9,9 +9,11 @@ import ( "github.com/blang/semver/v4" "github.com/tidwall/gjson" - "github.com/operator-framework/deppy/pkg/solver" + "github.com/operator-framework/deppy/pkg/deppy/input" - "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/deppy/constraint" + + "github.com/operator-framework/deppy/pkg/deppy" ) const ( @@ -32,7 +34,7 @@ type requirePackage struct { channel string } -func (r *requirePackage) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { +func (r *requirePackage) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { resultSet, err := entitySource.Filter(ctx, input.And( withPackageName(r.packageName), withinVersion(r.versionRange), @@ -42,8 +44,8 @@ func (r *requirePackage) GetVariables(ctx context.Context, entitySource input.En } ids := resultSet.Sort(byChannelAndVersion).CollectIds() subject := subject("require", r.packageName, r.versionRange, r.channel) - return []solver.Variable{ - input.NewSimpleVariable(subject, solver.Mandatory(), solver.Dependency(ids...)), + return []deppy.Variable{ + input.NewSimpleVariable(subject, constraint.Mandatory(), constraint.Dependency(ids...)), }, nil } @@ -58,22 +60,22 @@ func RequirePackage(packageName string, versionRange string, channel string) inp var _ input.VariableSource = &uniqueness{} -type subjectFormatFn func(key string) solver.Identifier +type subjectFormatFn func(key string) deppy.Identifier type uniqueness struct { subject subjectFormatFn groupByFn input.GroupByFunction } -func (u *uniqueness) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { +func (u *uniqueness) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { resultSet, err := entitySource.GroupBy(ctx, u.groupByFn) if err != nil || len(resultSet) == 0 { return nil, err } - variables := make([]solver.Variable, 0, len(resultSet)) + variables := make([]deppy.Variable, 0, len(resultSet)) for key, entities := range resultSet { ids := entities.Sort(byChannelAndVersion).CollectIds() - variables = append(variables, input.NewSimpleVariable(u.subject(key), solver.AtMost(1, ids...))) + variables = append(variables, input.NewSimpleVariable(u.subject(key), constraint.AtMost(1, ids...))) } return variables, nil } @@ -94,29 +96,29 @@ func PackageUniqueness() input.VariableSource { } } -func uniquenessSubjectFormat(key string) solver.Identifier { - return solver.IdentifierFromString(fmt.Sprintf("%s uniqueness", key)) +func uniquenessSubjectFormat(key string) deppy.Identifier { + return deppy.IdentifierFromString(fmt.Sprintf("%s uniqueness", key)) } var _ input.VariableSource = &packageDependency{} type packageDependency struct { - subject solver.Identifier + subject deppy.Identifier packageName string versionRange string } -func (p *packageDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { +func (p *packageDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { entities, err := entitySource.Filter(ctx, input.And(withPackageName(p.packageName), withinVersion(p.versionRange))) if err != nil || len(entities) == 0 { return nil, err } ids := entities.Sort(byChannelAndVersion).CollectIds() - return []solver.Variable{input.NewSimpleVariable(p.subject, solver.Dependency(ids...))}, nil + return []deppy.Variable{input.NewSimpleVariable(p.subject, constraint.Dependency(ids...))}, nil } // PackageDependency generates constraints to describe a package's dependency on another package -func PackageDependency(subject solver.Identifier, packageName string, versionRange string) input.VariableSource { +func PackageDependency(subject deppy.Identifier, packageName string, versionRange string) input.VariableSource { return &packageDependency{ subject: subject, packageName: packageName, @@ -127,23 +129,23 @@ func PackageDependency(subject solver.Identifier, packageName string, versionRan var _ input.VariableSource = &gvkDependency{} type gvkDependency struct { - subject solver.Identifier + subject deppy.Identifier group string version string kind string } -func (g *gvkDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]solver.Variable, error) { +func (g *gvkDependency) GetVariables(ctx context.Context, entitySource input.EntitySource) ([]deppy.Variable, error) { entities, err := entitySource.Filter(ctx, input.And(withExportsGVK(g.group, g.version, g.kind))) if err != nil || len(entities) == 0 { return nil, err } ids := entities.Sort(byChannelAndVersion).CollectIds() - return []solver.Variable{input.NewSimpleVariable(g.subject, solver.Dependency(ids...))}, nil + return []deppy.Variable{input.NewSimpleVariable(g.subject, constraint.Dependency(ids...))}, nil } // GVKDependency generates constraints to describe a package's dependency on a gvk -func GVKDependency(subject solver.Identifier, group string, version string, kind string) input.VariableSource { +func GVKDependency(subject deppy.Identifier, group string, version string, kind string) input.VariableSource { return &gvkDependency{ subject: subject, group: group, @@ -292,8 +294,8 @@ func packageGroupFunction(entity *input.Entity) []string { return nil } -func subject(str ...string) solver.Identifier { - return solver.Identifier(regexp.MustCompile(`\\s`).ReplaceAllString(strings.Join(str, "-"), "")) +func subject(str ...string) deppy.Identifier { + return deppy.Identifier(regexp.MustCompile(`\\s`).ReplaceAllString(strings.Join(str, "-"), "")) } func getPropertyOrNotFound(entity *input.Entity, propertyName string) string { diff --git a/pkg/ext/olm/constraints_test.go b/pkg/ext/olm/constraints_test.go index c8f48c9..b1c3bd6 100644 --- a/pkg/ext/olm/constraints_test.go +++ b/pkg/ext/olm/constraints_test.go @@ -11,9 +11,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/operator-framework/deppy/pkg/solver" - - "github.com/operator-framework/deppy/pkg/input" + "github.com/operator-framework/deppy/pkg/deppy" + "github.com/operator-framework/deppy/pkg/deppy/input" . "github.com/onsi/gomega/gstruct" @@ -60,7 +59,7 @@ type MockQuerier struct { testEntityList input.EntityList } -func (t MockQuerier) Get(_ context.Context, _ solver.Identifier) *input.Entity { +func (t MockQuerier) Get(_ context.Context, _ deppy.Identifier) *input.Entity { return &input.Entity{} } func (t MockQuerier) Filter(_ context.Context, filter input.Predicate) (input.EntityList, error) { diff --git a/pkg/input/entity.go b/pkg/input/entity.go deleted file mode 100644 index 74c2985..0000000 --- a/pkg/input/entity.go +++ /dev/null @@ -1,21 +0,0 @@ -package input - -import ( - "github.com/operator-framework/deppy/pkg/solver" -) - -type Entity struct { - ID solver.Identifier `json:"identifier"` - Properties map[string]string `json:"properties"` -} - -func (e *Entity) Identifier() solver.Identifier { - return e.ID -} - -func NewEntity(id solver.Identifier, properties map[string]string) *Entity { - return &Entity{ - ID: id, - Properties: properties, - } -} diff --git a/pkg/input/solver_suite_test.go b/pkg/input/solver_suite_test.go deleted file mode 100644 index a939d07..0000000 --- a/pkg/input/solver_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package input_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestSolver(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Solver Suite") -} diff --git a/pkg/input/solver_test.go b/pkg/input/solver_test.go deleted file mode 100644 index d919cde..0000000 --- a/pkg/input/solver_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package input_test - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/operator-framework/deppy/pkg/input" - - . "github.com/onsi/gomega/gstruct" - - "github.com/operator-framework/deppy/pkg/solver" -) - -type EntitySourceStruct struct { - variables []solver.Variable - input.EntitySource -} - -func (c EntitySourceStruct) GetVariables(_ context.Context, _ input.EntitySource) ([]solver.Variable, error) { - return c.variables, nil -} - -func NewEntitySource(variables []solver.Variable) *EntitySourceStruct { - entities := make(map[solver.Identifier]input.Entity, len(variables)) - for _, variable := range variables { - entityID := variable.Identifier() - entities[entityID] = *input.NewEntity(entityID, map[string]string{"x": "y"}) - } - return &EntitySourceStruct{ - variables: variables, - EntitySource: input.NewCacheQuerier(entities), - } -} - -var _ = Describe("Entity", func() { - It("should select a mandatory entity", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Mandatory()), - input.NewSimpleVariable("2"), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(true), - solver.Identifier("2"): Equal(false), - })) - }) - - It("should select two mandatory entities", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Mandatory()), - input.NewSimpleVariable("2", solver.Mandatory()), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(true), - solver.Identifier("2"): Equal(true), - })) - }) - - It("should select a mandatory entity and its dependency", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Mandatory(), solver.Dependency("2")), - input.NewSimpleVariable("2"), - input.NewSimpleVariable("3"), - } - s := NewEntitySource(variables) - - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(true), - solver.Identifier("2"): Equal(true), - solver.Identifier("3"): Equal(false), - })) - }) - - It("should fail when a dependency is prohibited", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Mandatory(), solver.Dependency("2")), - input.NewSimpleVariable("2", solver.Prohibited()), - input.NewSimpleVariable("3"), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - _, err = so.Solve(context.Background()) - Expect(err).Should(HaveOccurred()) - }) - - It("should select a mandatory entity and its dependency and ignore a non-mandatory prohibited variable", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Mandatory(), solver.Dependency("2")), - input.NewSimpleVariable("2"), - input.NewSimpleVariable("3", solver.Prohibited()), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(true), - solver.Identifier("2"): Equal(true), - solver.Identifier("3"): Equal(false), - })) - }) - - It("should not select 'or' paths that are prohibited", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Or("2", false, false), solver.Dependency("3")), - input.NewSimpleVariable("2", solver.Dependency("4")), - input.NewSimpleVariable("3", solver.Prohibited()), - input.NewSimpleVariable("4"), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(false), - solver.Identifier("2"): Equal(true), - solver.Identifier("3"): Equal(false), - solver.Identifier("4"): Equal(true), - })) - }) - - It("should respect atMost constraint", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Or("2", false, false), solver.Dependency("3"), solver.Dependency("4")), - input.NewSimpleVariable("2", solver.Dependency("3")), - input.NewSimpleVariable("3", solver.AtMost(1, "3", "4")), - input.NewSimpleVariable("4"), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(false), - solver.Identifier("2"): Equal(true), - solver.Identifier("3"): Equal(true), - solver.Identifier("4"): Equal(false), - })) - }) - - It("should respect dependency conflicts", func() { - variables := []solver.Variable{ - input.NewSimpleVariable("1", solver.Or("2", false, false), solver.Dependency("3"), solver.Dependency("4")), - input.NewSimpleVariable("2", solver.Dependency("4"), solver.Dependency("5")), - input.NewSimpleVariable("3", solver.Conflict("6")), - input.NewSimpleVariable("4", solver.Dependency("6")), - input.NewSimpleVariable("5"), - input.NewSimpleVariable("6"), - } - s := NewEntitySource(variables) - so, err := input.NewDeppySolver(s, s) - Expect(err).To(BeNil()) - solution, err := so.Solve(context.Background()) - Expect(err).To(BeNil()) - Expect(solution).To(MatchAllKeys(Keys{ - solver.Identifier("1"): Equal(false), - solver.Identifier("2"): Equal(true), - solver.Identifier("3"): Equal(false), - solver.Identifier("4"): Equal(true), - solver.Identifier("5"): Equal(true), - solver.Identifier("6"): Equal(true), - })) - }) -}) diff --git a/pkg/input/source_suite_test.go b/pkg/input/source_suite_test.go deleted file mode 100644 index 495973d..0000000 --- a/pkg/input/source_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package input_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestSource(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Source Suite") -} diff --git a/pkg/input/variable_source.go b/pkg/input/variable_source.go deleted file mode 100644 index f94fd5e..0000000 --- a/pkg/input/variable_source.go +++ /dev/null @@ -1,38 +0,0 @@ -package input - -import ( - "context" - - "github.com/operator-framework/deppy/pkg/solver" -) - -// VariableSource generates solver constraints given an entity querier interface -type VariableSource interface { - GetVariables(ctx context.Context, entitySource EntitySource) ([]solver.Variable, error) -} - -var _ solver.Variable = &SimpleVariable{} - -type SimpleVariable struct { - id solver.Identifier - constraints []solver.Constraint -} - -func (s *SimpleVariable) Identifier() solver.Identifier { - return s.id -} - -func (s *SimpleVariable) Constraints() []solver.Constraint { - return s.constraints -} - -func (s *SimpleVariable) AddConstraint(constraint solver.Constraint) { - s.constraints = append(s.constraints, constraint) -} - -func NewSimpleVariable(id solver.Identifier, constraints ...solver.Constraint) *SimpleVariable { - return &SimpleVariable{ - id: id, - constraints: constraints, - } -} diff --git a/pkg/solver/constraints.go b/pkg/solver/constraints.go deleted file mode 100644 index 3361d80..0000000 --- a/pkg/solver/constraints.go +++ /dev/null @@ -1,250 +0,0 @@ -package solver - -import ( - "fmt" - "strings" - - "github.com/go-air/gini/logic" - "github.com/go-air/gini/z" -) - -// Constraint implementations limit the circumstances under which a -// particular Variable can appear in a solution. -type Constraint interface { - String(subject Identifier) string - apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit - order() []Identifier - anchor() bool -} - -// zeroConstraint is returned by ConstraintOf in error cases. -type zeroConstraint struct{} - -var _ Constraint = zeroConstraint{} - -func (zeroConstraint) String(subject Identifier) string { - return "" -} - -func (zeroConstraint) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit { - return z.LitNull -} - -func (zeroConstraint) order() []Identifier { - return nil -} - -func (zeroConstraint) anchor() bool { - return false -} - -// AppliedConstraint values compose a single Constraint with the -// Variable it applies to. -type AppliedConstraint struct { - Variable Variable - Constraint Constraint -} - -// String implements fmt.Stringer and returns a human-readable message -// representing the receiver. -func (a AppliedConstraint) String() string { - return a.Constraint.String(a.Variable.Identifier()) -} - -type mandatory struct{} - -func (constraint mandatory) String(subject Identifier) string { - return fmt.Sprintf("%s is mandatory", subject) -} - -func (constraint mandatory) apply(_ *logic.C, lm *litMapping, subject Identifier) z.Lit { - return lm.LitOf(subject) -} - -func (constraint mandatory) order() []Identifier { - return nil -} - -func (constraint mandatory) anchor() bool { - return true -} - -// Mandatory returns a Constraint that will permit only solutions that -// contain a particular Variable. -func Mandatory() Constraint { - return mandatory{} -} - -type prohibited struct{} - -func (constraint prohibited) String(subject Identifier) string { - return fmt.Sprintf("%s is prohibited", subject) -} - -func (constraint prohibited) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit { - return lm.LitOf(subject).Not() -} - -func (constraint prohibited) order() []Identifier { - return nil -} - -func (constraint prohibited) anchor() bool { - return false -} - -// Prohibited returns a Constraint that will reject any solution that -// contains a particular Variable. Callers may also decide to omit -// an Variable from input to Solve rather than apply such a -// Constraint. -func Prohibited() Constraint { - return prohibited{} -} - -func Not() Constraint { - return prohibited{} -} - -type dependency []Identifier - -func (constraint dependency) String(subject Identifier) string { - if len(constraint) == 0 { - return fmt.Sprintf("%s has a dependency without any candidates to satisfy it", subject) - } - s := make([]string, len(constraint)) - for i, each := range constraint { - s[i] = string(each) - } - return fmt.Sprintf("%s requires at least one of %s", subject, strings.Join(s, ", ")) -} - -func (constraint dependency) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit { - m := lm.LitOf(subject).Not() - for _, each := range constraint { - m = c.Or(m, lm.LitOf(each)) - } - return m -} - -func (constraint dependency) order() []Identifier { - return constraint -} - -func (constraint dependency) anchor() bool { - return false -} - -// Dependency returns a Constraint that will only permit solutions -// containing a given Variable on the condition that at least one -// of the Variables identified by the given Identifiers also -// appears in the solution. Identifiers appearing earlier in the -// argument list have higher preference than those appearing later. -func Dependency(ids ...Identifier) Constraint { - return dependency(ids) -} - -type conflict Identifier - -func (constraint conflict) String(subject Identifier) string { - return fmt.Sprintf("%s conflicts with %s", subject, constraint) -} - -func (constraint conflict) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit { - return c.Or(lm.LitOf(subject).Not(), lm.LitOf(Identifier(constraint)).Not()) -} - -func (constraint conflict) order() []Identifier { - return nil -} - -func (constraint conflict) anchor() bool { - return false -} - -// Conflict returns a Constraint that will permit solutions containing -// either the constrained Variable, the Variable identified by -// the given Identifier, or neither, but not both. -func Conflict(id Identifier) Constraint { - return conflict(id) -} - -type leq struct { - ids []Identifier - n int -} - -func (constraint leq) String(subject Identifier) string { - s := make([]string, len(constraint.ids)) - for i, each := range constraint.ids { - s[i] = string(each) - } - return fmt.Sprintf("%s permits at most %d of %s", subject, constraint.n, strings.Join(s, ", ")) -} - -func (constraint leq) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit { - ms := make([]z.Lit, len(constraint.ids)) - for i, each := range constraint.ids { - ms[i] = lm.LitOf(each) - } - return c.CardSort(ms).Leq(constraint.n) -} - -func (constraint leq) order() []Identifier { - return nil -} - -func (constraint leq) anchor() bool { - return false -} - -// AtMost returns a Constraint that forbids solutions that contain -// more than n of the Variables identified by the given -// Identifiers. -func AtMost(n int, ids ...Identifier) Constraint { - return leq{ - ids: ids, - n: n, - } -} - -type or struct { - operand Identifier - isSubjectNegated bool - isOperandNegated bool -} - -func (constraint or) String(subject Identifier) string { - return fmt.Sprintf("%s is prohibited", subject) -} - -func (constraint or) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit { - subjectLit := lm.LitOf(subject) - if constraint.isSubjectNegated { - subjectLit = subjectLit.Not() - } - operandLit := lm.LitOf(constraint.operand) - if constraint.isOperandNegated { - operandLit = operandLit.Not() - } - return c.Or(subjectLit, operandLit) -} - -func (constraint or) order() []Identifier { - return nil -} - -func (constraint or) anchor() bool { - return false -} - -// Or returns a constraints in the form subject OR identifier -// if isSubjectNegated = true, ~subject OR identifier -// if isOperandNegated = true, subject OR ~identifier -// if both are true: ~subject OR ~identifier -func Or(identifier Identifier, isSubjectNegated bool, isOperandNegated bool) Constraint { - return or{ - operand: identifier, - isSubjectNegated: isSubjectNegated, - isOperandNegated: isOperandNegated, - } -} diff --git a/pkg/solver/constraints_test.go b/pkg/solver/constraints_test.go deleted file mode 100644 index 4bd0a2e..0000000 --- a/pkg/solver/constraints_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package solver - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOrder(t *testing.T) { - type tc struct { - Name string - Constraint Constraint - Expected []Identifier - } - - for _, tt := range []tc{ - { - Name: "mandatory", - Constraint: Mandatory(), - }, - { - Name: "prohibited", - Constraint: Prohibited(), - }, - { - Name: "dependency", - Constraint: Dependency("a", "b", "c"), - Expected: []Identifier{"a", "b", "c"}, - }, - { - Name: "conflict", - Constraint: Conflict("a"), - }, - } { - t.Run(tt.Name, func(t *testing.T) { - assert.Equal(t, tt.Expected, tt.Constraint.order()) - }) - } -} diff --git a/pkg/solver/solve_test.go b/pkg/solver/solve_test.go deleted file mode 100644 index e84c465..0000000 --- a/pkg/solver/solve_test.go +++ /dev/null @@ -1,365 +0,0 @@ -package solver - -import ( - "bytes" - "context" - "errors" - "fmt" - "reflect" - "sort" - "testing" - - "github.com/stretchr/testify/assert" -) - -type TestVariable struct { - identifier Identifier - constraints []Constraint -} - -func (i TestVariable) Identifier() Identifier { - return i.identifier -} - -func (i TestVariable) Constraints() []Constraint { - return i.constraints -} - -func (i TestVariable) GoString() string { - return fmt.Sprintf("%q", i.Identifier()) -} - -func variable(id Identifier, constraints ...Constraint) Variable { - return TestVariable{ - identifier: id, - constraints: constraints, - } -} - -func TestNotSatisfiableError(t *testing.T) { - type tc struct { - Name string - Error NotSatisfiable - String string - } - - for _, tt := range []tc{ - { - Name: "nil", - String: "constraints not satisfiable", - }, - { - Name: "empty", - String: "constraints not satisfiable", - Error: NotSatisfiable{}, - }, - { - Name: "single failure", - Error: NotSatisfiable{ - AppliedConstraint{ - Variable: variable("a", Mandatory()), - Constraint: Mandatory(), - }, - }, - String: fmt.Sprintf("constraints not satisfiable: %s", - Mandatory().String("a")), - }, - { - Name: "multiple failures", - Error: NotSatisfiable{ - AppliedConstraint{ - Variable: variable("a", Mandatory()), - Constraint: Mandatory(), - }, - AppliedConstraint{ - Variable: variable("b", Prohibited()), - Constraint: Prohibited(), - }, - }, - String: fmt.Sprintf("constraints not satisfiable: %s, %s", - Mandatory().String("a"), Prohibited().String("b")), - }, - } { - t.Run(tt.Name, func(t *testing.T) { - assert.Equal(t, tt.String, tt.Error.Error()) - }) - } -} - -func TestSolve(t *testing.T) { - type tc struct { - Name string - Variables []Variable - Installed []Identifier - Error error - } - - for _, tt := range []tc{ - { - Name: "no variables", - }, - { - Name: "unnecessary variable is not installed", - Variables: []Variable{variable("a")}, - }, - { - Name: "single mandatory variable is installed", - Variables: []Variable{variable("a", Mandatory())}, - Installed: []Identifier{"a"}, - }, - { - Name: "both mandatory and prohibited produce error", - Variables: []Variable{variable("a", Mandatory(), Prohibited())}, - Error: NotSatisfiable{ - { - Variable: variable("a", Mandatory(), Prohibited()), - Constraint: Mandatory(), - }, - { - Variable: variable("a", Mandatory(), Prohibited()), - Constraint: Prohibited(), - }, - }, - }, - { - Name: "dependency is installed", - Variables: []Variable{ - variable("a"), - variable("b", Mandatory(), Dependency("a")), - }, - Installed: []Identifier{"a", "b"}, - }, - { - Name: "transitive dependency is installed", - Variables: []Variable{ - variable("a"), - variable("b", Dependency("a")), - variable("c", Mandatory(), Dependency("b")), - }, - Installed: []Identifier{"a", "b", "c"}, - }, - { - Name: "both dependencies are installed", - Variables: []Variable{ - variable("a"), - variable("b"), - variable("c", Mandatory(), Dependency("a"), Dependency("b")), - }, - Installed: []Identifier{"a", "b", "c"}, - }, - { - Name: "solution with first dependency is selected", - Variables: []Variable{ - variable("a"), - variable("b", Conflict("a")), - variable("c", Mandatory(), Dependency("a", "b")), - }, - Installed: []Identifier{"a", "c"}, - }, - { - Name: "solution with only first dependency is selected", - Variables: []Variable{ - variable("a"), - variable("b"), - variable("c", Mandatory(), Dependency("a", "b")), - }, - Installed: []Identifier{"a", "c"}, - }, - { - Name: "solution with first dependency is selected (reverse)", - Variables: []Variable{ - variable("a"), - variable("b", Conflict("a")), - variable("c", Mandatory(), Dependency("b", "a")), - }, - Installed: []Identifier{"b", "c"}, - }, - { - Name: "two mandatory but conflicting packages", - Variables: []Variable{ - variable("a", Mandatory()), - variable("b", Mandatory(), Conflict("a")), - }, - Error: NotSatisfiable{ - { - Variable: variable("a", Mandatory()), - Constraint: Mandatory(), - }, - { - Variable: variable("b", Mandatory(), Conflict("a")), - Constraint: Mandatory(), - }, - { - Variable: variable("b", Mandatory(), Conflict("a")), - Constraint: Conflict("a"), - }, - }, - }, - { - Name: "irrelevant dependencies don't influence search order", - Variables: []Variable{ - variable("a", Dependency("x", "y")), - variable("b", Mandatory(), Dependency("y", "x")), - variable("x"), - variable("y"), - }, - Installed: []Identifier{"b", "y"}, - }, - { - Name: "cardinality constraint prevents resolution", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")), - variable("x", Mandatory()), - variable("y", Mandatory()), - }, - Error: NotSatisfiable{ - { - Variable: variable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")), - Constraint: AtMost(1, "x", "y"), - }, - { - Variable: variable("x", Mandatory()), - Constraint: Mandatory(), - }, - { - Variable: variable("y", Mandatory()), - Constraint: Mandatory(), - }, - }, - }, - { - Name: "cardinality constraint forces alternative", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")), - variable("b", Mandatory(), Dependency("y")), - variable("x"), - variable("y"), - }, - Installed: []Identifier{"a", "b", "y"}, - }, - { - Name: "two dependencies satisfied by one variable", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("y")), - variable("b", Mandatory(), Dependency("x", "y")), - variable("x"), - variable("y"), - }, - Installed: []Identifier{"a", "b", "y"}, - }, - { - Name: "foo two dependencies satisfied by one variable", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("y", "z", "m")), - variable("b", Mandatory(), Dependency("x", "y")), - variable("x"), - variable("y"), - variable("z"), - variable("m"), - }, - Installed: []Identifier{"a", "b", "y"}, - }, - { - Name: "result size larger than minimum due to preference", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("x", "y")), - variable("b", Mandatory(), Dependency("y")), - variable("x"), - variable("y"), - }, - Installed: []Identifier{"a", "b", "x", "y"}, - }, - { - Name: "only the least preferable choice is acceptable", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("a1", "a2")), - variable("a1", Conflict("c1"), Conflict("c2")), - variable("a2", Conflict("c1")), - variable("b", Mandatory(), Dependency("b1", "b2")), - variable("b1", Conflict("c1"), Conflict("c2")), - variable("b2", Conflict("c1")), - variable("c", Mandatory(), Dependency("c1", "c2")), - variable("c1"), - variable("c2"), - }, - Installed: []Identifier{"a", "a2", "b", "b2", "c", "c2"}, - }, - { - Name: "preferences respected with multiple dependencies per variable", - Variables: []Variable{ - variable("a", Mandatory(), Dependency("x1", "x2"), Dependency("y1", "y2")), - variable("x1"), - variable("x2"), - variable("y1"), - variable("y2"), - }, - Installed: []Identifier{"a", "x1", "y1"}, - }, - } { - t.Run(tt.Name, func(t *testing.T) { - assert := assert.New(t) - - var traces bytes.Buffer - s, err := NewSolver(WithInput(tt.Variables), WithTracer(LoggingTracer{Writer: &traces})) - if err != nil { - t.Fatalf("failed to initialize solver: %s", err) - } - - installed, err := s.Solve(context.TODO()) - - if installed != nil { - sort.SliceStable(installed, func(i, j int) bool { - return installed[i].Identifier() < installed[j].Identifier() - }) - } - - // Failed constraints are sorted in lexically - // increasing order of the identifier of the - // constraint's variable, with ties broken - // in favor of the constraint that appears - // earliest in the variable's list of - // constraints. - var ns NotSatisfiable - if errors.As(err, &ns) { - sort.SliceStable(ns, func(i, j int) bool { - if ns[i].Variable.Identifier() != ns[j].Variable.Identifier() { - return ns[i].Variable.Identifier() < ns[j].Variable.Identifier() - } - var x, y int - for ii, c := range ns[i].Variable.Constraints() { - if reflect.DeepEqual(c, ns[i].Constraint) { - x = ii - break - } - } - for ij, c := range ns[j].Variable.Constraints() { - if reflect.DeepEqual(c, ns[j].Constraint) { - y = ij - break - } - } - return x < y - }) - } - - var ids []Identifier - for _, variable := range installed { - ids = append(ids, variable.Identifier()) - } - assert.Equal(tt.Installed, ids) - assert.Equal(tt.Error, err) - - if t.Failed() { - t.Logf("\n%s", traces.String()) - } - }) - } -} - -func TestDuplicateIdentifier(t *testing.T) { - _, err := NewSolver(WithInput([]Variable{ - variable("a"), - variable("a"), - })) - assert.Equal(t, DuplicateIdentifier("a"), err) -} diff --git a/pkg/solver/variable.go b/pkg/solver/variable.go deleted file mode 100644 index 32afab7..0000000 --- a/pkg/solver/variable.go +++ /dev/null @@ -1,40 +0,0 @@ -package solver - -// Identifier values uniquely identify particular Variables within -// the input to a single call to Solve. -type Identifier string - -func (id Identifier) String() string { - return string(id) -} - -// IdentifierFromString returns an Identifier based on a provided -// string. -func IdentifierFromString(s string) Identifier { - return Identifier(s) -} - -// Variable values are the basic unit of problems and solutions -// understood by this package. -type Variable interface { - // Identifier returns the Identifier that uniquely identifies - // this Variable among all other Variables in a given - // problem. - Identifier() Identifier - // Constraints returns the set of constraints that apply to - // this Variable. - Constraints() []Constraint -} - -// zeroVariable is returned by VariableOf in error cases. -type zeroVariable struct{} - -var _ Variable = zeroVariable{} - -func (zeroVariable) Identifier() Identifier { - return "" -} - -func (zeroVariable) Constraints() []Constraint { - return nil -} From d6bd646a524664d13998ed526caa1c850680d070 Mon Sep 17 00:00:00 2001 From: perdasilva Date: Mon, 2 Jan 2023 17:09:12 +0100 Subject: [PATCH 3/3] remove superfluous solve unit test Signed-off-by: perdasilva --- internal/solver/solve_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/internal/solver/solve_test.go b/internal/solver/solve_test.go index e8847bd..b92a587 100644 --- a/internal/solver/solve_test.go +++ b/internal/solver/solve_test.go @@ -241,16 +241,6 @@ func TestSolve(t *testing.T) { }, Installed: []deppy.Identifier{"a", "b", "y"}, }, - { - Name: "two dependencies satisfied by one variable", - Variables: []deppy.Variable{ - variable("a", constraint.Mandatory(), constraint.Dependency("y")), - variable("b", constraint.Mandatory(), constraint.Dependency("x", "y")), - variable("x"), - variable("y"), - }, - Installed: []deppy.Identifier{"a", "b", "y"}, - }, { Name: "foo two dependencies satisfied by one variable", Variables: []deppy.Variable{