diff --git a/pkg/db/mock/executor.go b/pkg/db/mock/executor.go index 3de6cde0..be205b70 100644 --- a/pkg/db/mock/executor.go +++ b/pkg/db/mock/executor.go @@ -167,7 +167,8 @@ func (t *DBExecutor) Repos() []string { if t.ReposFn != nil { return t.ReposFn() } - panic("implement me") + // Tests that don't care about repo ordering shouldn't need to stub this out. + return nil } func (t *DBExecutor) SatisfierFromDB(s, s2 string) (IPackage, error) { diff --git a/pkg/query/metric.go b/pkg/query/metric.go index ddf3b4af..e858a3c4 100644 --- a/pkg/query/metric.go +++ b/pkg/query/metric.go @@ -1,13 +1,16 @@ package query import ( - "hash/fnv" "strings" "github.com/adrg/strutil" ) const minVotes = 30 +const ( + separateSourceMax = 45.0 + separateSourceMin = 5.0 +) // TODO: Add support for Popularity and LastModified func (a *abstractResults) aurSortByMetric(pkg *abstractResult) float64 { @@ -58,29 +61,35 @@ func (a *abstractResults) separateSourceScore(source string, score float64) floa return 50 } - switch source { - case sourceAUR: - return 0 - case "core": - return 40 - case "extra": - return 30 - case "community": - return 20 - case "multilib": - return 10 - } - if v, ok := a.separateSourceCache[source]; ok { return v } - h := fnv.New32a() - h.Write([]byte(source)) - sourceScore := float64(int(h.Sum32())%9 + 2) - a.separateSourceCache[source] = sourceScore + // AUR is always lowest priority + if source == sourceAUR { + return 0 + } - return sourceScore + // Score sync repositories based on pacman.conf order (as reflected by dbExecutor.Repos()). + // First repo gets max, last repo gets min, evenly distributed across the range. + for i, repo := range a.repoOrder { + if repo != source { + continue + } + + n := len(a.repoOrder) + if n == 1 { + a.separateSourceCache[source] = separateSourceMax + return separateSourceMax + } + + step := (separateSourceMax - separateSourceMin) / float64(n-1) + sourceScore := separateSourceMax - (float64(i) * step) + a.separateSourceCache[source] = sourceScore + return sourceScore + } + + return 0 } func (a *abstractResults) calculateMetric(pkg *abstractResult) float64 { diff --git a/pkg/query/metric_test.go b/pkg/query/metric_test.go new file mode 100644 index 00000000..710ff966 --- /dev/null +++ b/pkg/query/metric_test.go @@ -0,0 +1,47 @@ +//go:build !integration +// +build !integration + +package query + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSeparateSourceScore_UsesRepoOrderEvenlyDistributed(t *testing.T) { + t.Parallel() + + // Any non-1.0 score avoids the special-case 50 return. + const sim = 0.5 + const delta = 1e-1 + + t.Run("arch repos (core/extra/community/multilib)", func(t *testing.T) { + a := &abstractResults{ + separateSources: true, + repoOrder: []string{"core", "extra", "community", "multilib"}, + separateSourceCache: map[string]float64{}, + } + + assert.InDelta(t, 45.0, a.separateSourceScore("core", sim), delta) + assert.InDelta(t, 31.6, a.separateSourceScore("extra", sim), delta) + assert.InDelta(t, 18.3, a.separateSourceScore("community", sim), delta) + assert.InDelta(t, 5.0, a.separateSourceScore("multilib", sim), delta) + assert.Equal(t, 0.0, a.separateSourceScore(sourceAUR, sim)) + }) + + t.Run("arch arm repos (core/extra/alarm/aur)", func(t *testing.T) { + a := &abstractResults{ + separateSources: true, + repoOrder: []string{"core", "extra", "alarm", "aur"}, + separateSourceCache: map[string]float64{}, + } + + // Note: AUR is not a sync repository; it is always lowest priority (0) regardless of repo order. + assert.InDelta(t, 45.0, a.separateSourceScore("core", sim), delta) + assert.InDelta(t, 31.6, a.separateSourceScore("extra", sim), delta) + assert.InDelta(t, 18.3, a.separateSourceScore("alarm", sim), delta) + assert.InDelta(t, 5.0, a.separateSourceScore("aur", sim), delta) + assert.Equal(t, 0.0, a.separateSourceScore(sourceAUR, sim)) + }) +} diff --git a/pkg/query/query_builder.go b/pkg/query/query_builder.go index e91730f6..09bf8bf0 100644 --- a/pkg/query/query_builder.go +++ b/pkg/query/query_builder.go @@ -20,7 +20,7 @@ import ( "github.com/Jguer/yay/v12/pkg/text" ) -const sourceAUR = "aur" +const sourceAUR = "AUR" type SearchVerbosity int @@ -91,6 +91,7 @@ type abstractResults struct { metric strutil.StringMetric separateSources bool sortBy string + repoOrder []string distanceCache map[string]float64 separateSourceCache map[string]float64 @@ -143,9 +144,38 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor metric: metric, separateSources: s.separateSources, sortBy: s.sortBy, + repoOrder: dbExecutor.Repos(), distanceCache: map[string]float64{}, separateSourceCache: map[string]float64{}, } + var repoResults []alpm.IPackage + if s.targetMode.AtLeastRepo() { + repoResults = dbExecutor.SyncPackages(pkgS...) + + for i := range repoResults { + dbName := repoResults[i].DB().Name() + if s.queryMap[dbName] == nil { + s.queryMap[dbName] = map[string]any{} + } + + s.queryMap[dbName][repoResults[i].Name()] = repoResults[i] + + rawProvides := repoResults[i].Provides().Slice() + + provides := make([]string, len(rawProvides)) + for j := range rawProvides { + provides[j] = rawProvides[j].Name + } + + sortableResults.results = append(sortableResults.results, abstractResult{ + source: repoResults[i].DB().Name(), + name: repoResults[i].Name(), + description: repoResults[i].Description(), + provides: provides, + votes: -1, + }) + } + } if s.targetMode.AtLeastAUR() { var aurResults []aur.Pkg @@ -175,35 +205,6 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor } } - var repoResults []alpm.IPackage - if s.targetMode.AtLeastRepo() { - repoResults = dbExecutor.SyncPackages(pkgS...) - - for i := range repoResults { - dbName := repoResults[i].DB().Name() - if s.queryMap[dbName] == nil { - s.queryMap[dbName] = map[string]any{} - } - - s.queryMap[dbName][repoResults[i].Name()] = repoResults[i] - - rawProvides := repoResults[i].Provides().Slice() - - provides := make([]string, len(rawProvides)) - for j := range rawProvides { - provides[j] = rawProvides[j].Name - } - - sortableResults.results = append(sortableResults.results, abstractResult{ - source: repoResults[i].DB().Name(), - name: repoResults[i].Name(), - description: repoResults[i].Description(), - provides: provides, - votes: -1, - }) - } - } - sort.Sort(sortableResults) s.results = sortableResults.results diff --git a/pkg/query/query_builder_test.go b/pkg/query/query_builder_test.go index ab4560d4..3d012785 100644 --- a/pkg/query/query_builder_test.go +++ b/pkg/query/query_builder_test.go @@ -289,6 +289,10 @@ func TestSourceQueryBuilder(t *testing.T) { } mockDB := &mock.DBExecutor{ + ReposFn: func() []string { + // Match pacman.conf parsing order for source separation. + return []string{"core"} + }, SyncPackagesFn: func(pkgs ...string) []mock.IPackage { mockDB := mock.NewDB("core") return []mock.IPackage{