465 lines
13 KiB
Go
465 lines
13 KiB
Go
package installer
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"zsvo/pkg/packager"
|
|
"zsvo/pkg/recipe"
|
|
"zsvo/pkg/types"
|
|
)
|
|
|
|
func buildTestPackage(t *testing.T, workDir string, r *recipe.Recipe, files map[string][]byte) string {
|
|
t.Helper()
|
|
|
|
stagingDir := r.GetStagingDir(workDir)
|
|
for relPath, content := range files {
|
|
path := filepath.Join(stagingDir, relPath)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll(%s) error = %v", path, err)
|
|
}
|
|
if err := os.WriteFile(path, content, 0o755); err != nil {
|
|
t.Fatalf("WriteFile(%s) error = %v", path, err)
|
|
}
|
|
}
|
|
|
|
p := packager.NewPackager(workDir)
|
|
if err := p.Package(r); err != nil {
|
|
t.Fatalf("Package(%s) error = %v", r.GetPackageName(), err)
|
|
}
|
|
return p.GetPackageFile(r)
|
|
}
|
|
|
|
func TestInstallPackageCopiesFilesAndSymlinks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
r := &recipe.Recipe{Name: "demo", Version: "1.0.0", Build: []string{"true"}, Install: []string{"true"}}
|
|
|
|
stagingDir := r.GetStagingDir(workDir)
|
|
if err := os.MkdirAll(filepath.Join(stagingDir, "usr", "bin"), 0o755); err != nil {
|
|
t.Fatalf("MkdirAll() error = %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(stagingDir, "usr", "bin", "demo"), []byte("demo\n"), 0o755); err != nil {
|
|
t.Fatalf("WriteFile() error = %v", err)
|
|
}
|
|
if err := os.Symlink("demo", filepath.Join(stagingDir, "usr", "bin", "demo-link")); err != nil {
|
|
t.Fatalf("Symlink() error = %v", err)
|
|
}
|
|
|
|
p := packager.NewPackager(workDir)
|
|
if err := p.Package(r); err != nil {
|
|
t.Fatalf("Package() error = %v", err)
|
|
}
|
|
|
|
ins := NewInstaller(rootDir)
|
|
if err := ins.Install(p.GetPackageFile(r)); err != nil {
|
|
t.Fatalf("Install() error = %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(rootDir, "usr", "bin", "demo")); err != nil {
|
|
t.Fatalf("installed file missing: %v", err)
|
|
}
|
|
if target, err := os.Readlink(filepath.Join(rootDir, "usr", "bin", "demo-link")); err != nil {
|
|
t.Fatalf("Readlink() error = %v", err)
|
|
} else if target != "demo" {
|
|
t.Fatalf("unexpected symlink target: %s", target)
|
|
}
|
|
}
|
|
|
|
func TestInstallManyOrdersByDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "libfoo",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
}, map[string][]byte{"usr/lib/libfoo.so": []byte("lib\n")})
|
|
|
|
appPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "app",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"libfoo"},
|
|
}, map[string][]byte{"usr/bin/app": []byte("app\n")})
|
|
|
|
// app передан первым, installer должен поставить libfoo раньше.
|
|
if err := ins.InstallMany([]string{appPkg, libPkg}); err != nil {
|
|
t.Fatalf("InstallMany() error = %v", err)
|
|
}
|
|
|
|
if !ins.IsInstalled("libfoo") || !ins.IsInstalled("app") {
|
|
t.Fatalf("expected libfoo and app to be installed")
|
|
}
|
|
}
|
|
|
|
func TestInstallManyRollsBackOnFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "libfoo",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
}, map[string][]byte{"usr/lib/libfoo.so": []byte("lib\n")})
|
|
|
|
appPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "app",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"libfoo"},
|
|
}, map[string][]byte{"usr/bin/app": []byte("app\n")})
|
|
|
|
brokenPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "broken",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"missing"},
|
|
}, map[string][]byte{"usr/bin/broken": []byte("broken\n")})
|
|
|
|
err := ins.InstallMany([]string{appPkg, libPkg, brokenPkg})
|
|
if err == nil {
|
|
t.Fatalf("expected InstallMany() to fail")
|
|
}
|
|
|
|
if ins.IsInstalled("libfoo") || ins.IsInstalled("app") || ins.IsInstalled("broken") {
|
|
t.Fatalf("expected transaction rollback")
|
|
}
|
|
}
|
|
|
|
func TestInstallManyRespectsVersionConstraints(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "libfoo",
|
|
Version: "2.1.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
}, map[string][]byte{"usr/lib/libfoo.so": []byte("lib2\n")})
|
|
|
|
appPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "app",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"libfoo>=2.0.0"},
|
|
}, map[string][]byte{"usr/bin/app": []byte("app\n")})
|
|
|
|
if err := ins.InstallMany([]string{appPkg, libPkg}); err != nil {
|
|
t.Fatalf("InstallMany() error = %v", err)
|
|
}
|
|
if !ins.IsInstalled("libfoo") || !ins.IsInstalled("app") {
|
|
t.Fatalf("expected libfoo and app to be installed")
|
|
}
|
|
}
|
|
|
|
func TestInstallManyFailsOnUnsatisfiedVersionConstraint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libOld := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "libfoo",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
}, map[string][]byte{"usr/lib/libfoo.so": []byte("lib1\n")})
|
|
if err := ins.Install(libOld); err != nil {
|
|
t.Fatalf("Install(libOld) error = %v", err)
|
|
}
|
|
|
|
appPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "app",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"libfoo>=2.0.0"},
|
|
}, map[string][]byte{"usr/bin/app": []byte("app\n")})
|
|
|
|
err := ins.Install(appPkg)
|
|
if err == nil {
|
|
t.Fatalf("expected install error for unsatisfied version constraint")
|
|
}
|
|
if !strings.Contains(err.Error(), "libfoo>=2.0.0") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallManySupportsAlternativeDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libBarPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "libbar",
|
|
Version: "1.5.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
}, map[string][]byte{"usr/lib/libbar.so": []byte("bar\n")})
|
|
|
|
appPkg := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "app",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"libfoo | libbar>=1.2.0"},
|
|
}, map[string][]byte{"usr/bin/app": []byte("app\n")})
|
|
|
|
if err := ins.InstallMany([]string{appPkg, libBarPkg}); err != nil {
|
|
t.Fatalf("InstallMany() error = %v", err)
|
|
}
|
|
if !ins.IsInstalled("libbar") || !ins.IsInstalled("app") {
|
|
t.Fatalf("expected libbar and app to be installed")
|
|
}
|
|
}
|
|
|
|
func TestInstallManyDetectsDependencyCycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
pkgA := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "a",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"b>=1.0"},
|
|
}, map[string][]byte{"usr/bin/a": []byte("a\n")})
|
|
|
|
pkgB := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "b",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
Deps: []string{"a>=1.0"},
|
|
}, map[string][]byte{"usr/bin/b": []byte("b\n")})
|
|
|
|
err := ins.InstallMany([]string{pkgA, pkgB})
|
|
if err == nil {
|
|
t.Fatalf("expected cycle error")
|
|
}
|
|
if !strings.Contains(err.Error(), "dependency cycle") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInstallRespectsRootFlag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
workDir := t.TempDir()
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
pkgFile := buildTestPackage(t, workDir, &recipe.Recipe{
|
|
Name: "rooted",
|
|
Version: "1.0.0",
|
|
Build: []string{"true"},
|
|
Install: []string{"true"},
|
|
}, map[string][]byte{"usr/bin/rooted": []byte("ok\n")})
|
|
|
|
if err := ins.Install(pkgFile); err != nil {
|
|
t.Fatalf("Install() error = %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(rootDir, "usr", "bin", "rooted")); err != nil {
|
|
t.Fatalf("expected file in custom root: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemoveManyWithoutCascadeFails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
lib := &types.PkgInfo{Name: "libfoo", Version: "1.0.0", Files: []string{"usr/lib/libfoo.so"}}
|
|
app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo"}, Files: []string{"usr/bin/app"}}
|
|
|
|
if err := ins.registerPackage(lib); err != nil {
|
|
t.Fatalf("registerPackage(lib) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(app); err != nil {
|
|
t.Fatalf("registerPackage(app) error = %v", err)
|
|
}
|
|
|
|
_, err := ins.RemoveMany([]string{"libfoo"}, RemoveOptions{})
|
|
if err == nil {
|
|
t.Fatalf("expected RemoveMany() to fail")
|
|
}
|
|
if !strings.Contains(err.Error(), "required by") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemoveManyCascade(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
lib := &types.PkgInfo{Name: "libfoo", Version: "1.0.0", Files: []string{"usr/lib/libfoo.so"}}
|
|
app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo"}, Files: []string{"usr/bin/app"}}
|
|
|
|
if err := ins.registerPackage(lib); err != nil {
|
|
t.Fatalf("registerPackage(lib) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(app); err != nil {
|
|
t.Fatalf("registerPackage(app) error = %v", err)
|
|
}
|
|
|
|
removed, err := ins.RemoveMany([]string{"libfoo"}, RemoveOptions{Cascade: true})
|
|
if err != nil {
|
|
t.Fatalf("RemoveMany(cascade) error = %v", err)
|
|
}
|
|
|
|
if len(removed) != 2 {
|
|
t.Fatalf("expected two removed packages, got %d", len(removed))
|
|
}
|
|
if ins.IsInstalled("libfoo") || ins.IsInstalled("app") {
|
|
t.Fatalf("expected packages to be removed")
|
|
}
|
|
}
|
|
|
|
func TestRemoveManyAllowsAlternativeDependencyProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libFoo := &types.PkgInfo{Name: "libfoo", Version: "1.0.0", Files: []string{"usr/lib/libfoo.so"}}
|
|
libBar := &types.PkgInfo{Name: "libbar", Version: "1.0.0", Files: []string{"usr/lib/libbar.so"}}
|
|
app := &types.PkgInfo{
|
|
Name: "app",
|
|
Version: "1.0.0",
|
|
Dependencies: []string{"libfoo | libbar"},
|
|
Files: []string{"usr/bin/app"},
|
|
}
|
|
|
|
if err := ins.registerPackage(libFoo); err != nil {
|
|
t.Fatalf("registerPackage(libFoo) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(libBar); err != nil {
|
|
t.Fatalf("registerPackage(libBar) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(app); err != nil {
|
|
t.Fatalf("registerPackage(app) error = %v", err)
|
|
}
|
|
|
|
removed, err := ins.RemoveMany([]string{"libfoo"}, RemoveOptions{})
|
|
if err != nil {
|
|
t.Fatalf("RemoveMany() error = %v", err)
|
|
}
|
|
if len(removed) != 1 || removed[0] != "libfoo" {
|
|
t.Fatalf("unexpected removed list: %#v", removed)
|
|
}
|
|
if !ins.IsInstalled("app") {
|
|
t.Fatalf("app should remain installed because libbar satisfies dependency")
|
|
}
|
|
}
|
|
|
|
func TestListOrphans(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
lib := &types.PkgInfo{Name: "libfoo", Version: "1.0.0"}
|
|
app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo"}}
|
|
tool := &types.PkgInfo{Name: "tool", Version: "1.0.0"}
|
|
|
|
if err := ins.registerPackage(lib); err != nil {
|
|
t.Fatalf("registerPackage(lib) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(app); err != nil {
|
|
t.Fatalf("registerPackage(app) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(tool); err != nil {
|
|
t.Fatalf("registerPackage(tool) error = %v", err)
|
|
}
|
|
|
|
orphans, err := ins.ListOrphans()
|
|
if err != nil {
|
|
t.Fatalf("ListOrphans() error = %v", err)
|
|
}
|
|
|
|
set := make(map[string]struct{}, len(orphans))
|
|
for _, name := range orphans {
|
|
set[name] = struct{}{}
|
|
}
|
|
|
|
if _, ok := set["app"]; !ok {
|
|
t.Fatalf("expected app to be orphan")
|
|
}
|
|
if _, ok := set["tool"]; !ok {
|
|
t.Fatalf("expected tool to be orphan")
|
|
}
|
|
if _, ok := set["libfoo"]; ok {
|
|
t.Fatalf("did not expect libfoo to be orphan")
|
|
}
|
|
}
|
|
|
|
func TestListOrphansAlternativeDependencyMarksChosenProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
rootDir := t.TempDir()
|
|
ins := NewInstaller(rootDir)
|
|
|
|
libFoo := &types.PkgInfo{Name: "libfoo", Version: "1.0.0"}
|
|
libBar := &types.PkgInfo{Name: "libbar", Version: "1.0.0"}
|
|
app := &types.PkgInfo{Name: "app", Version: "1.0.0", Dependencies: []string{"libfoo | libbar"}}
|
|
|
|
if err := ins.registerPackage(libFoo); err != nil {
|
|
t.Fatalf("registerPackage(libFoo) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(libBar); err != nil {
|
|
t.Fatalf("registerPackage(libBar) error = %v", err)
|
|
}
|
|
if err := ins.registerPackage(app); err != nil {
|
|
t.Fatalf("registerPackage(app) error = %v", err)
|
|
}
|
|
|
|
orphans, err := ins.ListOrphans()
|
|
if err != nil {
|
|
t.Fatalf("ListOrphans() error = %v", err)
|
|
}
|
|
|
|
set := make(map[string]struct{}, len(orphans))
|
|
for _, name := range orphans {
|
|
set[name] = struct{}{}
|
|
}
|
|
|
|
if _, ok := set["app"]; !ok {
|
|
t.Fatalf("expected app to be orphan")
|
|
}
|
|
if _, ok := set["libbar"]; !ok {
|
|
t.Fatalf("expected libbar to be orphan when libfoo is first satisfiable provider")
|
|
}
|
|
if _, ok := set["libfoo"]; ok {
|
|
t.Fatalf("did not expect libfoo to be orphan")
|
|
}
|
|
}
|