diff --git a/cli/install/install.go b/cli/install/install.go index 863255c..513a338 100644 --- a/cli/install/install.go +++ b/cli/install/install.go @@ -193,9 +193,11 @@ func (i *installer) installFromRepo(ctx context.Context, name string, archs []st if pi.Ver == "" { var err error - if pi.Ver, _, pi.Arch, err = client.FindRepoLatest(pi, i.repoMap, archs); err != nil { + var spec *goolib.PkgSpec + if spec, _, pi.Arch, err = client.FindRepoLatest(pi, i.repoMap, archs); err != nil { return fmt.Errorf("can't resolve version for package %q: %v", pi.Name, err) } + pi.Ver = spec.Version } if _, err := goolib.ParseVersion(pi.Ver); err != nil { return fmt.Errorf("invalid package version %q: %v", pi.Ver, err) diff --git a/cli/latest/latest.go b/cli/latest/latest.go index b5d7c72..0830fbf 100644 --- a/cli/latest/latest.go +++ b/cli/latest/latest.go @@ -88,11 +88,12 @@ func (cmd *latestCmd) Execute(ctx context.Context, flags *flag.FlagSet, _ ...int } rm := downloader.AvailableVersions(ctx, repos, settings.CacheDir(), settings.CacheLife) - v, _, a, err := client.FindRepoLatest(pi, rm, settings.Archs) + spec, _, a, err := client.FindRepoLatest(pi, rm, settings.Archs) if err != nil { logger.Errorf("Failed to find package: %v", err) return subcommands.ExitFailure } + v := spec.Version if !cmd.compare { fmt.Println(v) return subcommands.ExitSuccess diff --git a/cli/update/update.go b/cli/update/update.go index faac18d..23b9138 100644 --- a/cli/update/update.go +++ b/cli/update/update.go @@ -129,13 +129,13 @@ func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { var ud []goolib.PackageInfo for p, ver := range pm { pi := goolib.PkgNameSplit(p) - v, r, _, err := client.FindRepoLatest(pi, rm, settings.Archs) + spec, r, _, err := client.FindRepoLatest(pi, rm, settings.Archs) if err != nil { // This error is because this installed package is not available in a repo. logger.Info(err) continue } - c, err := goolib.ComparePriorityVersion(rm[r].Priority, v, priority.Default, ver) + c, err := goolib.ComparePriorityVersion(rm[r].Priority, spec.Version, priority.Default, ver) if err != nil { logger.Error(err) continue @@ -146,7 +146,7 @@ func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { } // The versions might actually be the same even though the priorities are different, // so do another check to skip reinstall of the same version. - c, err = goolib.Compare(v, ver) + c, err = goolib.Compare(spec.Version, ver) if err != nil { logger.Error(err) continue @@ -159,9 +159,9 @@ func updates(pm client.PackageMap, rm client.RepoMap) []goolib.PackageInfo { if c == -1 { op = "Downgrade" } - fmt.Printf(" %s, %s --> %s from %s\n", p, ver, v, r) - logger.Infof("%s for package %s, %s installed and %s available from %s.", op, p, ver, v, r) - ud = append(ud, goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: v}) + fmt.Printf(" %s, %s --> %s from %s\n", p, ver, spec.Version, r) + logger.Infof("%s for package %s, %s installed and %s available from %s.", op, p, ver, spec.Version, r) + ud = append(ud, goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: spec.Version}) } return ud } diff --git a/client/client.go b/client/client.go index dd56c58..c8ab0e6 100644 --- a/client/client.go +++ b/client/client.go @@ -378,56 +378,111 @@ func FindRepoSpec(pi goolib.PackageInfo, repo Repo) (goolib.RepoSpec, error) { return goolib.RepoSpec{}, fmt.Errorf("no match found for package %s.%s.%s in repo", pi.Name, pi.Arch, pi.Ver) } -// latest returns the version and repo having the greatest (priority, version) from the set of +// latest returns the package spec and repo having the greatest (priority, version) from the set of // package specs in psm. -func latest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (string, string) { - var ver, repoURL string +func latest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (*goolib.PkgSpec, string) { + var bestPkg *goolib.PkgSpec + var repoURL string var pri priority.Value for u, pl := range psm { for _, pkg := range pl { q := rm[u].Priority c := 1 - if ver != "" { + if bestPkg != nil { var err error - if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, ver); err != nil { - logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, ver, err) + if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, bestPkg.Version); err != nil { + logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, bestPkg.Version, err) continue } } if c == 1 { repoURL = u - ver = pkg.Version + bestPkg = pkg pri = q } } } - return ver, repoURL + return bestPkg, repoURL } // FindRepoLatest returns the latest version of a package along with its repo and arch. +// It checks both direct name matches and "Provides" entries. // The archs are searched in order; if a matching package is found for any arch, it is // returned immediately even if a later arch might have a later version. -func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (string, string, string, error) { - psm := make(map[string][]*goolib.PkgSpec) +func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (*goolib.PkgSpec, string, string, error) { name := pi.Name if pi.Arch != "" { archs = []string{pi.Arch} name = fmt.Sprintf("%s.%s", pi.Name, pi.Arch) } + for _, a := range archs { + psmDirect := make(map[string][]*goolib.PkgSpec) + psmProvides := make(map[string][]*goolib.PkgSpec) + for u, r := range rm { for _, p := range r.Packages { - if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == a { - psm[u] = append(psm[u], p.PackageSpec) + ps := p.PackageSpec + if ps.Arch != a { + continue + } + + // Check exact match + if ps.Name == pi.Name { + if satisfiesVersion(ps.Version, pi.Ver) { + psmDirect[u] = append(psmDirect[u], ps) + } + // Skip checking Provides if the package itself is a direct match. + continue + } + + // Check provides + for _, prov := range ps.Provides { + if SatisfiesProvider(prov, pi.Name, pi.Ver) { + psmProvides[u] = append(psmProvides[u], ps) + break + } } } } - if len(psm) != 0 { - v, r := latest(psm, rm) - return v, r, a, nil + + // Prioritize direct package matches over virtual package providers. + if len(psmDirect) > 0 { + pkg, repo := latest(psmDirect, rm) + if pkg != nil { + return pkg, repo, a, nil + } } + + // If no direct matches, check providers. + // Note: This matches Arch behavior (prefer real package). + if len(psmProvides) > 0 { + pkg, repo := latest(psmProvides, rm) + if pkg != nil { + return pkg, repo, a, nil + } + } + } + return nil, "", "", fmt.Errorf("no package found satisfying %s in any repo", name) +} + +func satisfiesVersion(pkgVer, reqVer string) bool { + if reqVer == "" || pkgVer == "" { + return true } - return "", "", "", fmt.Errorf("no versions of package %s found in any repo", name) + c, err := goolib.Compare(pkgVer, reqVer) + if err != nil { + logger.Errorf("Error comparing versions %s vs %s: %v", pkgVer, reqVer, err) + return false + } + return c >= 0 +} + +// SatisfiesProvider checks if a provider string satisfies a requirement. +func SatisfiesProvider(prov, reqName, reqVer string) bool { + pName, pVer, _ := strings.Cut(prov, "=") + + return pName == reqName && satisfiesVersion(pVer, reqVer) } // WhatRepo returns what repo a package is in. diff --git a/client/client_test.go b/client/client_test.go index dae7542..7a134c6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -32,6 +32,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/googet/v2/goolib" "github.com/google/googet/v2/oswrap" + "github.com/google/googet/v2/priority" "github.com/google/logger" ) @@ -235,14 +236,17 @@ func TestFindRepoLatest(t *testing.T) { }, } { t.Run(tt.desc, func(t *testing.T) { - gotVersion, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs) + gotSpec, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs) if err != nil && !tt.wantErr { t.Fatalf("FindRepoLatest(%v, %v, %v) failed: %v", tt.pi, tt.rm, tt.archs, err) } else if err == nil && tt.wantErr { t.Fatalf("FindRepoLatest(%v, %v, %v) got nil error, wanted non-nil", tt.pi, tt.rm, tt.archs) } - if gotVersion != tt.wantVersion { - t.Errorf("FindRepoLatest(%v, %v, %v) got version: %q, want %q", tt.pi, tt.rm, tt.archs, gotVersion, tt.wantVersion) + if err != nil { + return + } + if gotSpec.Version != tt.wantVersion { + t.Errorf("FindRepoLatest(%v, %v, %v) got version: %q, want %q", tt.pi, tt.rm, tt.archs, gotSpec.Version, tt.wantVersion) } if gotArch != tt.wantArch { t.Errorf("FindRepoLatest(%v, %v, %v) got arch: %q, want %q", tt.pi, tt.rm, tt.archs, gotArch, tt.wantArch) @@ -409,3 +413,144 @@ func TestFindRepoSpecNoMatch(t *testing.T) { t.Error("did not get expected error when running FindRepoSpec") } } + +func TestFindRepoLatest_Provides(t *testing.T) { + rm := RepoMap{ + "repo1": Repo{ + Priority: priority.Value(500), + Packages: []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg", + Version: "2.0.0", + Arch: "noarch", + Provides: []string{ + "virtual_pkg", + "virtual_versioned=1.0.0", + }, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg_old", + Version: "1.0.0", + Arch: "noarch", + Provides: []string{ + "virtual_pkg", + }, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "other_pkg", + Version: "1.0.0", + Arch: "noarch", + }, + }, + }, + }, + } + + tests := []struct { + name string + pi goolib.PackageInfo + wantName string + wantVer string + wantError bool + }{ + { + name: "Direct match", + pi: goolib.PackageInfo{Name: "other_pkg", Arch: "noarch"}, + wantName: "other_pkg", + wantVer: "1.0.0", + }, + { + name: "Provider match unversioned", + pi: goolib.PackageInfo{Name: "virtual_pkg", Arch: "noarch"}, + wantName: "real_pkg", + wantVer: "2.0.0", // latest real_pkg + }, + { + name: "Provider match matched version", + pi: goolib.PackageInfo{Name: "virtual_versioned", Ver: "1.0.0", Arch: "noarch"}, + wantName: "real_pkg", + wantVer: "2.0.0", + }, + { + name: "Provider match unsatisfied version", + pi: goolib.PackageInfo{Name: "virtual_versioned", Ver: "2.0.0", Arch: "noarch"}, + wantError: true, + }, + { + name: "No match", + pi: goolib.PackageInfo{Name: "missing_pkg", Arch: "noarch"}, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spec, _, _, err := FindRepoLatest(tt.pi, rm, []string{"noarch"}) + if tt.wantError { + if err == nil { + t.Errorf("FindRepoLatest(%v) wanted error, got nil", tt.pi) + } + return + } + if err != nil { + t.Fatalf("FindRepoLatest(%v) unexpected error: %v", tt.pi, err) + } + if spec.Name != tt.wantName { + t.Errorf("FindRepoLatest(%v) name = %q, want %q", tt.pi, spec.Name, tt.wantName) + } + if spec.Version != tt.wantVer { // Simplified check, assumes simple version strings in test + t.Errorf("FindRepoLatest(%v) version = %q, want %q", tt.pi, spec.Version, tt.wantVer) + } + }) + } +} + +func TestFindRepoLatest_Priority(t *testing.T) { + // Setup repo with both direct match and provider. + // direct match: version 1.0.0 + // provider: version 2.0.0 (provides it) + // direct match should win despite lower version. + + rm := RepoMap{ + "repo1": Repo{ + Priority: priority.Value(500), + Packages: []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg", + Version: "1.0.0", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "2.0.0", + Arch: "noarch", + Provides: []string{ + "real_pkg", + }, + }, + }, + }, + }, + } + + pi := goolib.PackageInfo{Name: "real_pkg", Arch: "noarch"} + spec, _, _, err := FindRepoLatest(pi, rm, []string{"noarch"}) + if err != nil { + t.Fatalf("FindRepoLatest failed: %v", err) + } + + if spec.Name != "real_pkg" { + t.Errorf("Expected direct match 'real_pkg', got '%s'", spec.Name) + } + if spec.Version != "1.0.0" { + t.Errorf("Expected version '1.0.0', got '%s'", spec.Version) + } +} diff --git a/download/download.go b/download/download.go index 97d24fe..6c8b2b9 100644 --- a/download/download.go +++ b/download/download.go @@ -151,11 +151,11 @@ func FromRepo(ctx context.Context, rs goolib.RepoSpec, repo, dir string, downloa // Latest downloads the latest available version of a package. func Latest(ctx context.Context, name, dir string, rm client.RepoMap, archs []string, downloader *client.Downloader) (string, string, error) { - ver, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{Name: name, Arch: "", Ver: ""}, rm, archs) + spec, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{Name: name, Arch: "", Ver: ""}, rm, archs) if err != nil { return "", "", err } - rs, err := client.FindRepoSpec(goolib.PackageInfo{Name: name, Arch: arch, Ver: ver}, rm[repo]) + rs, err := client.FindRepoSpec(goolib.PackageInfo{Name: name, Arch: arch, Ver: spec.Version}, rm[repo]) if err != nil { return "", "", err } diff --git a/googet.goospec b/googet.goospec index d0a3739..fa262a4 100644 --- a/googet.goospec +++ b/googet.goospec @@ -1,4 +1,4 @@ -{{$version := "3.1.0@0" -}} +{{$version := "3.2.0@0" -}} { "name": "googet", "version": "{{$version}}", @@ -15,6 +15,7 @@ "path": "install.ps1" }, "releaseNotes": [ + "3.2.0 - Add Provides functionality and field to the GooGet PkgSpec.", "3.1.0 - Introduce a dry_run flag for update, install, and remove subcommands.", "3.0.0 - Replace googet state file with sqlite database. Add json output for installed command.", "2.21.0 - Add arm64 arch support.", diff --git a/goolib/goospec.go b/goolib/goospec.go index c4943f2..7a54209 100644 --- a/goolib/goospec.go +++ b/goolib/goospec.go @@ -89,6 +89,7 @@ type PkgSpec struct { PkgDependencies map[string]string `json:",omitempty"` Replaces []string Conflicts []string + Provides []string Install ExecFile Uninstall ExecFile Verify ExecFile diff --git a/goolib/goospec_test.go b/goolib/goospec_test.go index 17274b3..e0641a3 100644 --- a/goolib/goospec_test.go +++ b/goolib/goospec_test.go @@ -454,6 +454,7 @@ func TestMarshal(t *testing.T) { Source: "github", Replaces: []string{"foo"}, Conflicts: []string{"bar"}, + Provides: []string{"baz"}, Install: ExecFile{ Path: "install.ps1", }, @@ -479,6 +480,9 @@ func TestMarshal(t *testing.T) { "Conflicts": [ "bar" ], + "Provides": [ + "baz" + ], "Install": { "Path": "install.ps1" }, diff --git a/install/install.go b/install/install.go index d471c7f..eab1bf2 100644 --- a/install/install.go +++ b/install/install.go @@ -56,18 +56,49 @@ func minInstalled(pi goolib.PackageInfo, db *googetdb.GooDB) (bool, error) { return false, nil } +// isSatisfied reports whether the package dependency is satisfied by an installed package +// or a package that provides it. +func isSatisfied(pi goolib.PackageInfo, db *googetdb.GooDB) (bool, error) { + // First check if the package itself is installed. + ok, err := minInstalled(pi, db) + if err != nil { + return false, err + } + if ok { + return true, nil + } + + // Check if any installed package provides this dependency. + pkgs, err := db.FetchPkgs("") + if err != nil { + return false, err + } + for _, p := range pkgs { + if p.PackageSpec == nil { + continue + } + for _, prov := range p.PackageSpec.Provides { + if client.SatisfiesProvider(prov, pi.Name, pi.Ver) { + return true, nil + } + } + } + return false, nil +} + func resolveConflicts(ps *goolib.PkgSpec, db *googetdb.GooDB) error { // Check for any conflicting packages. // TODO(ajackura): Make sure no conflicting packages are listed as // dependencies or subdependancies. for _, pkg := range ps.Conflicts { pi := goolib.PkgNameSplit(pkg) - ins, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, db) + // Check if the conflicting package or a provider of it is installed. + sat, err := isSatisfied(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: pi.Ver}, db) if err != nil { return err } - if ins { - return fmt.Errorf("cannot install, conflict with installed package: %s", pi) + if sat { + return fmt.Errorf("cannot install, conflict with installed package or provider: %s", pi) } } return nil @@ -106,29 +137,23 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien // Check for and install any dependencies. for p, ver := range ps.PkgDependencies { pi := goolib.PkgNameSplit(p) - ok, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db) + ok, err := isSatisfied(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db) if err != nil { return err } else if ok { - logger.Infof("Dependency met: %s.%s with version greater than %s installed", pi.Name, pi.Arch, ver) + logger.Infof("Dependency met: %s.%s with version greater than %s installed or provided", pi.Name, pi.Arch, ver) continue } - v, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ""}, rm, archs) + + spec, repo, _, err := client.FindRepoLatest(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, rm, archs) if err != nil { return err } - c, err := goolib.Compare(v, ver) - if err != nil { + + logger.Infof("Dependency found: %s.%s %s (provides %s) is available", spec.Name, spec.Arch, spec.Version, pi.Name) + if err := FromRepo(ctx, goolib.PackageInfo{Name: spec.Name, Arch: spec.Arch, Ver: spec.Version}, repo, cache, rm, archs, dbOnly, downloader, db); err != nil { return err } - if c > -1 { - logger.Infof("Dependency found: %s.%s %s is available", pi.Name, arch, v) - if err := FromRepo(ctx, goolib.PackageInfo{Name: pi.Name, Arch: arch, Ver: v}, repo, cache, rm, archs, dbOnly, downloader, db); err != nil { - return err - } - continue - } - return fmt.Errorf("cannot resolve dependency, %s.%s version %s or greater not installed and not available in any repo", pi.Name, arch, ver) } return resolveReplacements(ctx, ps, dbOnly, downloader, db) } @@ -137,7 +162,27 @@ func installDeps(ctx context.Context, ps *goolib.PkgSpec, cache string, rm clien func FromRepo(ctx context.Context, pi goolib.PackageInfo, repo, cache string, rm client.RepoMap, archs []string, dbOnly bool, downloader *client.Downloader, db *googetdb.GooDB) error { logger.Infof("Starting install of %s.%s.%s", pi.Name, pi.Arch, pi.Ver) fmt.Printf("Installing %s.%s.%s and dependencies...\n", pi.Name, pi.Arch, pi.Ver) - rs, err := client.FindRepoSpec(pi, rm[repo]) + var rs goolib.RepoSpec + var err error + // If no version is specified, resolve the latest version handling both + // direct matches and providers. + if pi.Ver == "" { + spec, repoURL, _, err := client.FindRepoLatest(pi, rm, archs) + if err != nil { + return err + } + repo = repoURL + // Use the resolved real package name and version. + pi.Name = spec.Name + pi.Arch = spec.Arch + pi.Ver = spec.Version + rs, err = client.FindRepoSpec(goolib.PackageInfo{Name: spec.Name, Arch: spec.Arch, Ver: spec.Version}, rm[repo]) + } else { + // When a specific version is requested, look for an exact match in the repository. + // Virtual package resolution is not currently supported for specific versions. + rs, err = client.FindRepoSpec(pi, rm[repo]) + } + if err != nil { return err } @@ -202,13 +247,13 @@ func FromDisk(pkgPath, cache string, dbOnly, shouldReinstall bool, db *googetdb. } for p, ver := range zs.PkgDependencies { pi := goolib.PkgNameSplit(p) - if ok, err := minInstalled(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db); err != nil { + if ok, err := isSatisfied(goolib.PackageInfo{Name: pi.Name, Arch: pi.Arch, Ver: ver}, db); err != nil { return err } else if ok { - logger.Infof("Dependency met: %s.%s with version greater than %s installed", pi.Name, pi.Arch, ver) + logger.Infof("Dependency met: %s.%s with version greater than %s installed or provided", pi.Name, pi.Arch, ver) continue } - return fmt.Errorf("package dependency %s %s (min version %s) not installed", pi.Name, pi.Arch, ver) + return fmt.Errorf("package dependency %s %s (min version %s) not installed and not provided", pi.Name, pi.Arch, ver) } for _, pkg := range zs.Replaces { pi := goolib.PkgNameSplit(pkg) @@ -514,19 +559,19 @@ func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib dl = append(dl, pi) for d, v := range rs.PackageSpec.PkgDependencies { di := goolib.PkgNameSplit(d) - ver, repo, arch, err := client.FindRepoLatest(di, rm, archs) + spec, repo, arch, err := client.FindRepoLatest(di, rm, archs) di.Arch = arch if err != nil { return nil, fmt.Errorf("cannot resolve dependency %s.%s.%s: %v", di.Name, di.Arch, di.Ver, err) } - c, err := goolib.Compare(ver, v) + c, err := goolib.Compare(spec.Version, v) if err != nil { return nil, err } if c == -1 { return nil, fmt.Errorf("cannot resolve dependency, %s.%s version %s or greater not installed and not available in any repo", pi.Name, pi.Arch, pi.Ver) } - di.Ver = ver + di.Ver = spec.Version dl, err = listDeps(di, rm, repo, dl, archs) if err != nil { return nil, err diff --git a/install/install_test.go b/install/install_test.go index 7d7a914..3561ef2 100644 --- a/install/install_test.go +++ b/install/install_test.go @@ -28,6 +28,7 @@ import ( "github.com/google/googet/v2/googetdb" "github.com/google/googet/v2/goolib" "github.com/google/googet/v2/oswrap" + "github.com/google/googet/v2/priority" "github.com/google/googet/v2/settings" "github.com/google/logger" ) @@ -288,3 +289,209 @@ func TestResolveDst(t *testing.T) { } } } + +func TestIsSatisfied(t *testing.T) { + settings.Initialize(t.TempDir(), false) + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libfoo", "libbar=1.5.0"}, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "real_pkg", + Version: "2.0.0@1", + Arch: "noarch", + }, + }, + } + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + if err := db.WriteStateToDB(state); err != nil { + t.Fatalf("WriteStateToDB: %v", err) + } + + tests := []struct { + name string + pi goolib.PackageInfo + want bool + }{ + { + name: "Directly installed package", + pi: goolib.PackageInfo{Name: "real_pkg", Arch: "noarch", Ver: "1.0.0"}, + want: true, + }, + { + name: "Provided package without version", + pi: goolib.PackageInfo{Name: "libfoo", Arch: "noarch", Ver: "1.0.0"}, + want: true, + }, + { + name: "Provided package with satisfied version", + pi: goolib.PackageInfo{Name: "libbar", Arch: "noarch", Ver: "1.0.0"}, + want: true, + }, + { + name: "Provided package with unsatisfied version", + pi: goolib.PackageInfo{Name: "libbar", Arch: "noarch", Ver: "2.0.0"}, + want: false, + }, + { + name: "Not installed and not provided", + pi: goolib.PackageInfo{Name: "missing_pkg", Arch: "noarch", Ver: "1.0.0"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := isSatisfied(tt.pi, db) + if err != nil { + t.Fatalf("isSatisfied error: %v", err) + } + if got != tt.want { + t.Errorf("isSatisfied(%v) = %v, want %v", tt.pi, got, tt.want) + } + }) + } +} + +func TestResolveConflicts_Provides(t *testing.T) { + settings.Initialize(t.TempDir(), false) + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libconflict"}, + }, + }, + } + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + if err := db.WriteStateToDB(state); err != nil { + t.Fatalf("WriteStateToDB: %v", err) + } + + ps := &goolib.PkgSpec{ + Name: "conflicting_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Conflicts: []string{"libconflict"}, + } + + err = resolveConflicts(ps, db) + if err == nil { + t.Error("resolveConflicts expected error, got nil") + } else { + expectedErr := "cannot install, conflict with installed package or provider: libconflict" + if err.Error() != expectedErr { + t.Errorf("resolveConflicts error = %q, want %q", err.Error(), expectedErr) + } + } +} + +func TestFromRepo_SatisfiedByProvider(t *testing.T) { + // This is a more integration-level test to ensure installDeps uses isSatisfied. + // We mock the DB state and call installDeps directly or via a wrapper if accessible. + // installDeps is unexported, but we are in package install. + + settings.Initialize(t.TempDir(), false) + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libvirt"}, + }, + }, + } + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + if err := db.WriteStateToDB(state); err != nil { + t.Fatalf("WriteStateToDB: %v", err) + } + + // Package wanting libvirt + ps := &goolib.PkgSpec{ + Name: "consumer_pkg", + Version: "1.0.0@1", + Arch: "noarch", + PkgDependencies: map[string]string{"libvirt": "1.0.0"}, + } + + // We pass empty repo map and downloader because we expect it NOT to try downloading deps + // since they are satisfied. + err = installDeps(nil, ps, "", nil, nil, false, nil, db) + if err != nil { + t.Errorf("installDeps failed: %v", err) + } +} + +func TestFromRepo_SatisfiedByUninstalledProvider(t *testing.T) { + settings.Initialize(t.TempDir(), false) + db, err := googetdb.NewDB(settings.DBFile()) + if err != nil { + t.Fatalf("googetdb.NewDB: %v", err) + } + defer db.Close() + + // Repo state with provider + rm := client.RepoMap{ + "repo1": client.Repo{ + Priority: priority.Value(500), + Packages: []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "provider_pkg", + Version: "1.0.0@1", + Arch: "noarch", + Provides: []string{"libvirt"}, + }, + }, + }, + }, + } + + // Package wanting libvirt + ps := &goolib.PkgSpec{ + Name: "consumer_pkg", + Version: "1.0.0@1", + Arch: "noarch", + PkgDependencies: map[string]string{"libvirt": "1.0.0"}, + } + + // Verify that dependency resolution succeeds (finding provider_pkg); the download + // is expected to fail due to an invalid repository URL. + downloader, _ := client.NewDownloader("") + err = installDeps(nil, ps, "", rm, []string{"noarch"}, false, downloader, db) + + // We expect an error because download will fail (invalid URL/Source). + if err == nil { + t.Error("installDeps expected error, got nil") + } else { + // Verify that the error is not a resolution error. + // Any other error implies resolution succeeded and it failed at the download stage. + errMsg := err.Error() + if errMsg == "cannot resolve dependency, libvirt.noarch version 1.0.0 or greater not installed and not available in any repo" { + t.Errorf("installDeps failed to resolve provider: %v", err) + } + // Any other error means it TRIED to install it (provider found). + t.Logf("Got expected error (confirming resolution success): %v", err) + } +}