From d64b17aecc8c1ff7e016c2ba5ddad870eabf3b8f Mon Sep 17 00:00:00 2001 From: Tulip Blossom Date: Sun, 15 Dec 2024 19:02:35 -0300 Subject: [PATCH 1/3] feat: initial testing for system and filelock + adaptations --- cmd/update.go | 16 +++++++----- drv/brew.go | 33 +++++------------------ drv/generic.go | 5 ---- drv/system.go | 39 ++++++++++++++++------------ drv/system_test.go | 46 ++++++++++++++++++++++++++++++++ justfile | 8 ++++++ pkg/filelock/filelock.go | 49 ++++++++++++++++++++--------------- pkg/filelock/filelock_test.go | 48 ++++++++++++++++++++++++++++++++++ 8 files changed, 169 insertions(+), 75 deletions(-) create mode 100644 drv/system_test.go create mode 100644 pkg/filelock/filelock_test.go diff --git a/cmd/update.go b/cmd/update.go index 84c13f3..d0f636c 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -15,17 +15,21 @@ import ( ) func Update(cmd *cobra.Command, args []string) { - lock, err := filelock.AcquireLock() + lockfile, err := filelock.OpenLockfile(filelock.GetDefaultLockfile()) if err != nil { - slog.Error(fmt.Sprintf("%v, is uupd already running?", err)) + slog.Error("Failed creating and opening lockfile. Is uupd already running?", slog.Any("error", err)) return } - defer func() { - err := filelock.ReleaseLock(lock) + defer func(lockfile *os.File) { + err := filelock.ReleaseLock(lockfile) if err != nil { - slog.Error("Failed releasing lock") + slog.Error("Failed releasing lock", slog.Any("error", err)) } - }() + }(lockfile) + if err := filelock.AcquireLock(lockfile, filelock.TimeoutConfig{Tries: 5}); err != nil { + slog.Error(fmt.Sprintf("%v, is uupd already running?", err)) + return + } hwCheck, err := cmd.Flags().GetBool("hw-check") if err != nil { diff --git a/drv/brew.go b/drv/brew.go index f934d91..5f880e5 100644 --- a/drv/brew.go +++ b/drv/brew.go @@ -50,8 +50,9 @@ func (up BrewUpdater) Update() (*[]CommandOutput, error) { tmpout.Context = "Brew Update" tmpout.Cli = cli tmpout.Failure = err != nil - if err != nil { - tmpout.SetFailureContext("Brew update") + if failure := err != nil; failure { + tmpout.Failure = failure + tmpout.Context = "Brew Update" final_output = append(final_output, *tmpout) return &final_output, err } @@ -86,30 +87,10 @@ func (up BrewUpdater) New(config UpdaterInitConfiguration) (BrewUpdater, error) } up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) - brewPrefix, exists := up.Config.Environment["HOMEBREW_PREFIX"] - if !exists || brewPrefix == "" { - up.BrewPrefix = "/home/linuxbrew/.linuxbrew" - } else { - up.BrewPrefix = brewPrefix - } - brewRepo, exists := up.Config.Environment["HOMEBREW_REPOSITORY"] - if !exists || brewRepo == "" { - up.BrewRepo = fmt.Sprintf("%s/Homebrew", up.BrewPrefix) - } else { - up.BrewRepo = brewRepo - } - brewCellar, exists := up.Config.Environment["HOMEBREW_CELLAR"] - if !exists || brewCellar == "" { - up.BrewCellar = fmt.Sprintf("%s/Cellar", up.BrewPrefix) - } else { - up.BrewCellar = brewCellar - } - brewPath, exists := up.Config.Environment["HOMEBREW_PATH"] - if !exists || brewPath == "" { - up.BrewPath = fmt.Sprintf("%s/bin/brew", up.BrewPrefix) - } else { - up.BrewPath = brewPath - } + up.BrewPrefix = EnvOrFallback(up.Config.Environment, "HOMEBREW_PREFIX", "/home/linuxbrew/.linuxbrew") + up.BrewPrefix = EnvOrFallback(up.Config.Environment, "HOMEBREW_REPOSITORY", fmt.Sprintf("%s/Homebrew", up.BrewPrefix)) + up.BrewPrefix = EnvOrFallback(up.Config.Environment, "HOMEBREW_CELLAR", fmt.Sprintf("%s/Cellar", up.BrewPrefix)) + up.BrewPrefix = EnvOrFallback(up.Config.Environment, "HOMEBREW_PATH", fmt.Sprintf("%s/bin/brew", up.BrewPrefix)) if up.Config.DryRun { return up, nil diff --git a/drv/generic.go b/drv/generic.go index ae0e4c7..70ec784 100644 --- a/drv/generic.go +++ b/drv/generic.go @@ -60,11 +60,6 @@ func (output CommandOutput) New(out []byte, err error) *CommandOutput { } } -func (out *CommandOutput) SetFailureContext(context string) { - out.Failure = true - out.Context = context -} - type DriverConfiguration struct { Title string Description string diff --git a/drv/system.go b/drv/system.go index 3cf6cc6..9b13c1d 100644 --- a/drv/system.go +++ b/drv/system.go @@ -43,27 +43,33 @@ type SystemUpdater struct { BinaryPath string } +// Checks if it is at least a month old considering how that works +func IsOutdatedOneMonthTimestamp(current time.Time, target time.Time) bool { + return target.Before(current.AddDate(0, -1, 0)) +} + func (up SystemUpdater) Outdated() (bool, error) { if up.Config.DryRun { return false, nil } - oneMonthAgo := time.Now().AddDate(0, -1, 0) - var timestamp time.Time + cmd := exec.Command(up.BinaryPath, "status", "--format=json") out, err := cmd.CombinedOutput() if err != nil { return false, err } + var status bootcStatus err = json.Unmarshal(out, &status) if err != nil { return false, err } - timestamp, err = time.Parse(time.RFC3339Nano, status.Status.Booted.Image.Timestamp) + + timestamp, err := time.Parse(time.RFC3339Nano, status.Status.Booted.Image.Timestamp) if err != nil { return false, nil } - return timestamp.Before(oneMonthAgo), nil + return IsOutdatedOneMonthTimestamp(time.Now(), timestamp), nil } func (up SystemUpdater) Update() (*[]CommandOutput, error) { @@ -75,9 +81,8 @@ func (up SystemUpdater) Update() (*[]CommandOutput, error) { cmd = exec.Command(cli[0], cli[1:]...) out, err := session.RunLog(up.Config.logger, slog.LevelDebug, cmd) tmpout := CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext("System update") - } + tmpout.Failure = err != nil + tmpout.Context = "System Update" finalOutput = append(finalOutput, *tmpout) return &finalOutput, err } @@ -89,6 +94,14 @@ func (up SystemUpdater) Steps() int { return 0 } +func EnvOrFallback(environment EnvironmentMap, key string, fallback string) string { + validCase, exists := environment[key] + if exists && validCase != "" { + return validCase + } + return fallback +} + func (up SystemUpdater) New(config UpdaterInitConfiguration) (SystemUpdater, error) { up.Config = DriverConfiguration{ Title: "System", @@ -98,16 +111,7 @@ func (up SystemUpdater) New(config UpdaterInitConfiguration) (SystemUpdater, err Environment: config.Environment, } up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) - if up.Config.DryRun { - return up, nil - } - - bootcBinaryPath, exists := up.Config.Environment["UUPD_BOOTC_BINARY"] - if !exists || bootcBinaryPath == "" { - up.BinaryPath = "/usr/bin/bootc" - } else { - up.BinaryPath = bootcBinaryPath - } + up.BinaryPath = EnvOrFallback(config.Environment, "UUPD_BOOTC_BINARY", "/usr/bin/bootc") return up, nil } @@ -122,6 +126,7 @@ func (up SystemUpdater) Check() (bool, error) { if err != nil { return true, err } + updateNecessary := !strings.Contains(string(out), "No changes in:") up.Config.logger.Debug("Executed update check", slog.String("output", string(out)), slog.Bool("update", updateNecessary)) return updateNecessary, nil diff --git a/drv/system_test.go b/drv/system_test.go new file mode 100644 index 0000000..8893dc9 --- /dev/null +++ b/drv/system_test.go @@ -0,0 +1,46 @@ +package drv_test + +import ( + "testing" + + "github.com/ublue-os/uupd/drv" + appLogging "github.com/ublue-os/uupd/pkg/logging" +) + +func InitBaseConfig() drv.SystemUpdater { + var initConfiguration = drv.UpdaterInitConfiguration{ + DryRun: false, + Ci: false, + Verbose: false, + Environment: nil, + Logger: appLogging.NewMuteLogger(), + } + driv, _ := drv.SystemUpdater{}.New(initConfiguration) + return driv +} + +func TestProperSteps(t *testing.T) { + systemUpdater := InitBaseConfig() + systemUpdater.Config.Enabled = false + + if systemUpdater.Steps() != 0 { + t.Fatalf("Expected no steps when module is disabled") + } + + systemUpdater.Config.Enabled = true + if systemUpdater.Steps() == 0 { + t.Fatalf("Expected steps to be added") + } +} + +func TestFallBack(t *testing.T) { + var environment drv.EnvironmentMap = drv.EnvironmentMap{ + "TEST_FALLBACK_GOOD": "true", + } + if value := drv.EnvOrFallback(environment, "TEST_FALLBACK_GOOD", "FALSE"); value != "true" { + t.Fatalf("Getting the proper value fails, %s", value) + } + if value := drv.EnvOrFallback(environment, "TEST_FALLBACK_BAD", "FALSE"); value != "FALSE" { + t.Fatalf("Getting the fallback fails, %s", value) + } +} diff --git a/justfile b/justfile index 1d16408..dea3859 100644 --- a/justfile +++ b/justfile @@ -76,3 +76,11 @@ lint: release: goreleaser + +test: + go test -v -cover ./... + +test-interactive: + #!/usr/bin/env bash + t="/tmp/go-cover.$$.tmp" + go test -v -coverprofile=$t ./... $@ && go tool cover -html=$t && unlink $t diff --git a/pkg/filelock/filelock.go b/pkg/filelock/filelock.go index 88d6147..a9ffe76 100644 --- a/pkg/filelock/filelock.go +++ b/pkg/filelock/filelock.go @@ -1,20 +1,14 @@ package filelock import ( - "fmt" - "io" + "errors" "os" "syscall" "time" ) -const fileLockPath string = "/run/uupd.lock" - func IsFileLocked(file *os.File) bool { - lock := syscall.Flock_t{ - Type: syscall.F_WRLCK, - Whence: io.SeekStart, - } + lock := syscall.Flock_t{} err := syscall.FcntlFlock(file.Fd(), syscall.F_GETLK, &lock) if err != nil { return false @@ -22,34 +16,47 @@ func IsFileLocked(file *os.File) bool { return lock.Type != syscall.F_UNLCK } -func AcquireLock() (*os.File, error) { - file, err := os.OpenFile(fileLockPath, os.O_RDONLY|os.O_CREATE|os.O_TRUNC, 0600) +func GetDefaultLockfile() string { + return "/run/uupd.lock" +} + +func OpenLockfile(filepath string) (*os.File, error) { + file, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return nil, err } + return file, err +} + +type TimeoutConfig struct { + Tries int +} - timeout := 5.0 - startTime := time.Now() - var lockFile *os.File +func AcquireLock(file *os.File, timeout TimeoutConfig) error { + maxTries := timeout.Tries + tries := 0 + fileLocked := false - for time.Since(startTime).Seconds() < timeout { - err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) - if err == nil { - lockFile = file + for tries < maxTries { + err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if fileLocked = err == nil; fileLocked { break } + tries += 1 time.Sleep(1 * time.Second) } - if lockFile == nil { - file.Close() - return nil, fmt.Errorf("Could not acquire lock at %s", fileLockPath) + if !fileLocked { + return errors.New("Could not acquire lock to file") } - return lockFile, nil + return nil } func ReleaseLock(file *os.File) error { + if err := syscall.Flock(int(file.Fd()), syscall.LOCK_UN); err != nil { + return err + } return syscall.Close(int(file.Fd())) } diff --git a/pkg/filelock/filelock_test.go b/pkg/filelock/filelock_test.go new file mode 100644 index 0000000..86d5373 --- /dev/null +++ b/pkg/filelock/filelock_test.go @@ -0,0 +1,48 @@ +package filelock_test + +import ( + "testing" + + "github.com/ublue-os/uupd/pkg/filelock" +) + +const LockForTest = "/tmp/uupd-sigmatest.lock" + +var defaultTimeout = filelock.TimeoutConfig{1} + +func TestFullLock(t *testing.T) { + file, err := filelock.OpenLockfile(LockForTest) + if err != nil { + t.Fatalf("Failed even opening the file, %v", err) + } + defer file.Close() + err = filelock.AcquireLock(file, defaultTimeout) + if err != nil { + t.Fatalf("Failed acquiring lock file, %v", err) + } + err = filelock.ReleaseLock(file) + if err != nil { + t.Fatal("Failed releasing lock file") + } +} + +func TestLockAcquired(t *testing.T) { + file1, err := filelock.OpenLockfile(LockForTest) + if err != nil { + t.Fatalf("Failed even opening the file, %v", err) + } + defer file1.Close() + err = filelock.AcquireLock(file1, defaultTimeout) + if err != nil { + t.Fatalf("Failed acquiring lock file, %v", err) + } + file2, err := filelock.OpenLockfile(LockForTest) + if err != nil { + t.Fatalf("Failed even opening the file, %v", err) + } + defer file2.Close() + err = filelock.AcquireLock(file2, defaultTimeout) + if err == nil { + t.Fatalf("Expected failing to lock file, %v", err) + } +} From 8731b3cb8aeeb825d25e1fbec915eabda2571ae5 Mon Sep 17 00:00:00 2001 From: Tulip Blossom Date: Sun, 15 Dec 2024 22:41:40 -0300 Subject: [PATCH 2/3] feat: enable both system modules for imageOutdated and updateCheck + move drivers to their own modules --- cmd/imageOutdated.go | 20 ++++- cmd/update.go | 56 +++++--------- cmd/updateCheck.go | 40 +++------- drv/{ => brew}/brew.go | 17 ++--- drv/{ => distrobox}/distrobox.go | 17 ++--- drv/{ => flatpak}/flatpak.go | 17 ++--- drv/{ => generic}/generic.go | 12 ++- drv/generic/generic_test.go | 19 +++++ drv/{rpm-ostree.go => rpmostree/rpmostree.go} | 36 ++------- drv/{ => system}/system.go | 75 ++++++++++++++----- drv/system/system_test.go | 35 +++++++++ drv/system_test.go | 46 ------------ uupd.spec | 3 +- 13 files changed, 186 insertions(+), 207 deletions(-) rename drv/{ => brew}/brew.go (87%) rename drv/{ => distrobox}/distrobox.go (88%) rename drv/{ => flatpak}/flatpak.go (89%) rename drv/{ => generic}/generic.go (87%) create mode 100644 drv/generic/generic_test.go rename drv/{rpm-ostree.go => rpmostree/rpmostree.go} (75%) rename drv/{ => system}/system.go (60%) create mode 100644 drv/system/system_test.go delete mode 100644 drv/system_test.go diff --git a/cmd/imageOutdated.go b/cmd/imageOutdated.go index 2e3e9bb..43b90a3 100644 --- a/cmd/imageOutdated.go +++ b/cmd/imageOutdated.go @@ -4,15 +4,27 @@ import ( "log/slog" "github.com/spf13/cobra" - "github.com/ublue-os/uupd/drv" + "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/drv/system" ) func ImageOutdated(cmd *cobra.Command, args []string) { - systemUpdater, err := drv.SystemUpdater{}.New(drv.UpdaterInitConfiguration{}) + initConfiguration := generic.UpdaterInitConfiguration{}.New() + initConfiguration.Ci = false + initConfiguration.DryRun = false + initConfiguration.Verbose = false + + mainSystemDriver, _, _, err := system.InitializeSystemDriver(*initConfiguration) if err != nil { - slog.Error("Failed getting system driver", slog.Any("error", err)) + slog.Error("Failed") return } - println(systemUpdater.Outdated) + systemOutdated, err := mainSystemDriver.Outdated() + + if err != nil { + slog.Error("Failed checking if system is out of date") + } + + println(systemOutdated) } diff --git a/cmd/update.go b/cmd/update.go index d0f636c..28ede9d 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -8,7 +8,12 @@ import ( "github.com/jedib0t/go-pretty/v6/progress" "github.com/spf13/cobra" "github.com/ublue-os/uupd/checks" - "github.com/ublue-os/uupd/drv" + "github.com/ublue-os/uupd/drv/brew" + "github.com/ublue-os/uupd/drv/distrobox" + "github.com/ublue-os/uupd/drv/flatpak" + drv "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/drv/system" + "github.com/ublue-os/uupd/pkg/filelock" "github.com/ublue-os/uupd/pkg/percent" "github.com/ublue-os/uupd/pkg/session" @@ -68,56 +73,29 @@ func Update(cmd *cobra.Command, args []string) { initConfiguration.DryRun = dryRun initConfiguration.Verbose = verboseRun - brewUpdater, err := drv.BrewUpdater{}.New(*initConfiguration) + brewUpdater, err := brew.BrewUpdater{}.New(*initConfiguration) brewUpdater.Config.Enabled = err == nil - flatpakUpdater, err := drv.FlatpakUpdater{}.New(*initConfiguration) + flatpakUpdater, err := flatpak.FlatpakUpdater{}.New(*initConfiguration) flatpakUpdater.Config.Enabled = err == nil flatpakUpdater.SetUsers(users) - distroboxUpdater, err := drv.DistroboxUpdater{}.New(*initConfiguration) + distroboxUpdater, err := distrobox.DistroboxUpdater{}.New(*initConfiguration) distroboxUpdater.Config.Enabled = err == nil distroboxUpdater.SetUsers(users) - var enableUpd bool = true - - rpmOstreeUpdater, err := drv.RpmOstreeUpdater{}.New(*initConfiguration) - if err != nil { - enableUpd = false - } - - systemUpdater, err := drv.SystemUpdater{}.New(*initConfiguration) - if err != nil { - enableUpd = false - } - - isBootc, err := drv.BootcCompatible(systemUpdater.BinaryPath) - if err != nil { - isBootc = false - } - - if !isBootc { - slog.Debug("Using rpm-ostree fallback as system driver") - } - - systemUpdater.Config.Enabled = isBootc && enableUpd - rpmOstreeUpdater.Config.Enabled = !isBootc && enableUpd - - // The system driver to be applied needs to have the correct "enabled" value since it will NOT update from here onwards. - var mainSystemDriver drv.SystemUpdateDriver = &systemUpdater - if !isBootc { - mainSystemDriver = &rpmOstreeUpdater - } + mainSystemDriver, mainSystemDriverConfig, _, err := system.InitializeSystemDriver(*initConfiguration) - enableUpd, err = mainSystemDriver.Check() + enableUpd, err := mainSystemDriver.Check() if err != nil { slog.Error("Failed checking for updates") } + mainSystemDriverConfig.Enabled = mainSystemDriverConfig.Enabled && enableUpd - slog.Debug("System Updater module status", slog.Bool("enabled", enableUpd)) + slog.Debug("System Updater module status", slog.Bool("enabled", mainSystemDriverConfig.Enabled)) totalSteps := brewUpdater.Steps() + flatpakUpdater.Steps() + distroboxUpdater.Steps() - if enableUpd { + if mainSystemDriverConfig.Enabled { totalSteps += mainSystemDriver.Steps() } pw := percent.NewProgressWriter() @@ -169,9 +147,9 @@ func Update(cmd *cobra.Command, args []string) { // This section is ugly but we cant really do much about it. // Using interfaces doesn't preserve the "Config" struct state and I dont know any other way to make this work without cursed workarounds. - if enableUpd { - slog.Debug(fmt.Sprintf("%s module", systemUpdater.Config.Title), slog.String("module_name", systemUpdater.Config.Title), slog.Any("module_configuration", systemUpdater.Config)) - percent.ChangeTrackerMessageFancy(pw, tracker, progressEnabled, percent.TrackerMessage{Title: systemUpdater.Config.Title, Description: systemUpdater.Config.Description}) + if mainSystemDriverConfig.Enabled { + slog.Debug(fmt.Sprintf("%s module", mainSystemDriverConfig.Title), slog.String("module_name", mainSystemDriverConfig.Title), slog.Any("module_configuration", mainSystemDriverConfig)) + percent.ChangeTrackerMessageFancy(pw, tracker, progressEnabled, percent.TrackerMessage{Title: mainSystemDriverConfig.Title, Description: mainSystemDriverConfig.Description}) var out *[]drv.CommandOutput out, err = mainSystemDriver.Update() outputs = append(outputs, *out...) diff --git a/cmd/updateCheck.go b/cmd/updateCheck.go index b8950b4..a2d4f68 100644 --- a/cmd/updateCheck.go +++ b/cmd/updateCheck.go @@ -4,42 +4,20 @@ import ( "log/slog" "github.com/spf13/cobra" - "github.com/ublue-os/uupd/drv" + "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/drv/system" ) func UpdateCheck(cmd *cobra.Command, args []string) { - var enableUpd bool = true + initConfiguration := generic.UpdaterInitConfiguration{}.New() + initConfiguration.Ci = false + initConfiguration.DryRun = false + initConfiguration.Verbose = false - initConfiguration := drv.UpdaterInitConfiguration{}.New() - rpmOstreeUpdater, err := drv.RpmOstreeUpdater{}.New(*initConfiguration) + mainSystemDriver, _, _, err := system.InitializeSystemDriver(*initConfiguration) if err != nil { - enableUpd = false - } - - systemUpdater, err := drv.SystemUpdater{}.New(*initConfiguration) - if err != nil { - enableUpd = false - } - - isBootc, err := drv.BootcCompatible(systemUpdater.BinaryPath) - if err != nil { - isBootc = false - } - - if !isBootc { - slog.Debug("Using rpm-ostree fallback as system driver") - } - - systemUpdater.Config.Enabled = isBootc && enableUpd - rpmOstreeUpdater.Config.Enabled = !isBootc && enableUpd - - var mainSystemDriver drv.SystemUpdateDriver - if !isBootc { - slog.Debug("Using the rpm-ostree driver") - mainSystemDriver = &rpmOstreeUpdater - } else { - slog.Debug("Using the bootc driver") - mainSystemDriver = &systemUpdater + slog.Error("Failed") + return } updateAvailable, err := mainSystemDriver.Check() diff --git a/drv/brew.go b/drv/brew/brew.go similarity index 87% rename from drv/brew.go rename to drv/brew/brew.go index 5f880e5..1abf928 100644 --- a/drv/brew.go +++ b/drv/brew/brew.go @@ -1,4 +1,4 @@ -package drv +package brew import ( "fmt" @@ -7,6 +7,7 @@ import ( "strings" "syscall" + . "github.com/ublue-os/uupd/drv/generic" "github.com/ublue-os/uupd/pkg/session" ) @@ -45,7 +46,7 @@ func (up BrewUpdater) Update() (*[]CommandOutput, error) { } cli := []string{up.BrewPath, "update"} - out, err := session.RunUID(up.Config.logger, slog.LevelDebug, up.BaseUser, cli, up.Config.Environment) + out, err := session.RunUID(up.Config.Logger, slog.LevelDebug, up.BaseUser, cli, up.Config.Environment) tmpout := CommandOutput{}.New(out, err) tmpout.Context = "Brew Update" tmpout.Cli = cli @@ -58,7 +59,7 @@ func (up BrewUpdater) Update() (*[]CommandOutput, error) { } cli = []string{up.BrewPath, "upgrade"} - out, err = session.RunUID(up.Config.logger, slog.LevelDebug, up.BaseUser, cli, up.Config.Environment) + out, err = session.RunUID(up.Config.Logger, slog.LevelDebug, up.BaseUser, cli, up.Config.Environment) tmpout = CommandOutput{}.New(out, err) tmpout.Context = "Brew Upgrade" tmpout.Cli = cli @@ -85,7 +86,7 @@ func (up BrewUpdater) New(config UpdaterInitConfiguration) (BrewUpdater, error) DryRun: config.DryRun, Environment: config.Environment, } - up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) + up.Config.Logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) up.BrewPrefix = EnvOrFallback(up.Config.Environment, "HOMEBREW_PREFIX", "/home/linuxbrew/.linuxbrew") up.BrewPrefix = EnvOrFallback(up.Config.Environment, "HOMEBREW_REPOSITORY", fmt.Sprintf("%s/Homebrew", up.BrewPrefix)) @@ -104,11 +105,3 @@ func (up BrewUpdater) New(config UpdaterInitConfiguration) (BrewUpdater, error) return up, nil } - -func (up *BrewUpdater) Logger() *slog.Logger { - return up.Config.logger -} - -func (up *BrewUpdater) SetLogger(logger *slog.Logger) { - up.Config.logger = logger -} diff --git a/drv/distrobox.go b/drv/distrobox/distrobox.go similarity index 88% rename from drv/distrobox.go rename to drv/distrobox/distrobox.go index 5db751c..213d6ac 100644 --- a/drv/distrobox.go +++ b/drv/distrobox/distrobox.go @@ -1,9 +1,10 @@ -package drv +package distrobox import ( "log/slog" "strings" + . "github.com/ublue-os/uupd/drv/generic" "github.com/ublue-os/uupd/pkg/percent" "github.com/ublue-os/uupd/pkg/session" ) @@ -38,7 +39,7 @@ func (up DistroboxUpdater) New(config UpdaterInitConfiguration) (DistroboxUpdate DryRun: config.DryRun, Environment: config.Environment, } - up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) + up.Config.Logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) up.usersEnabled = false up.Tracker = nil @@ -78,7 +79,7 @@ func (up DistroboxUpdater) Update() (*[]CommandOutput, error) { percent.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, percent.TrackerMessage{Title: up.Config.Title, Description: up.Config.Description}) cli := []string{up.binaryPath, "upgrade", "-a"} - out, err := session.RunUID(up.Config.logger, slog.LevelDebug, 0, cli, nil) + out, err := session.RunUID(up.Config.Logger, slog.LevelDebug, 0, cli, nil) tmpout := CommandOutput{}.New(out, err) tmpout.Context = up.Config.Description tmpout.Cli = cli @@ -91,7 +92,7 @@ func (up DistroboxUpdater) Update() (*[]CommandOutput, error) { context := *up.Config.UserDescription + " " + user.Name percent.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, percent.TrackerMessage{Title: up.Config.Title, Description: *up.Config.UserDescription + " " + user.Name}) cli := []string{up.binaryPath, "upgrade", "-a"} - out, err := session.RunUID(up.Config.logger, slog.LevelDebug, user.UID, cli, nil) + out, err := session.RunUID(up.Config.Logger, slog.LevelDebug, user.UID, cli, nil) tmpout = CommandOutput{}.New(out, err) tmpout.Context = context tmpout.Cli = cli @@ -100,11 +101,3 @@ func (up DistroboxUpdater) Update() (*[]CommandOutput, error) { } return &finalOutput, nil } - -func (up *DistroboxUpdater) Logger() *slog.Logger { - return up.Config.logger -} - -func (up *DistroboxUpdater) SetLogger(logger *slog.Logger) { - up.Config.logger = logger -} diff --git a/drv/flatpak.go b/drv/flatpak/flatpak.go similarity index 89% rename from drv/flatpak.go rename to drv/flatpak/flatpak.go index 1c42e1c..e8cc311 100644 --- a/drv/flatpak.go +++ b/drv/flatpak/flatpak.go @@ -1,10 +1,11 @@ -package drv +package flatpak import ( "log/slog" "os/exec" "strings" + . "github.com/ublue-os/uupd/drv/generic" "github.com/ublue-os/uupd/pkg/percent" "github.com/ublue-os/uupd/pkg/session" ) @@ -39,7 +40,7 @@ func (up FlatpakUpdater) New(config UpdaterInitConfiguration) (FlatpakUpdater, e DryRun: config.DryRun, Environment: config.Environment, } - up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) + up.Config.Logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) up.usersEnabled = false up.Tracker = nil @@ -80,7 +81,7 @@ func (up FlatpakUpdater) Update() (*[]CommandOutput, error) { percent.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, percent.TrackerMessage{Title: up.Config.Title, Description: up.Config.Description}) cli := []string{up.binaryPath, "update", "-y", "--noninteractive"} flatpakCmd := exec.Command(cli[0], cli[1:]...) - out, err := session.RunLog(up.Config.logger, slog.LevelDebug, flatpakCmd) + out, err := session.RunLog(up.Config.Logger, slog.LevelDebug, flatpakCmd) tmpout := CommandOutput{}.New(out, err) tmpout.Context = up.Config.Description tmpout.Cli = cli @@ -93,7 +94,7 @@ func (up FlatpakUpdater) Update() (*[]CommandOutput, error) { context := *up.Config.UserDescription + " " + user.Name percent.ChangeTrackerMessageFancy(*up.Tracker.Writer, up.Tracker.Tracker, up.Tracker.Progress, percent.TrackerMessage{Title: up.Config.Title, Description: context}) cli := []string{up.binaryPath, "update", "-y"} - out, err := session.RunUID(up.Config.logger, slog.LevelDebug, user.UID, cli, nil) + out, err := session.RunUID(up.Config.Logger, slog.LevelDebug, user.UID, cli, nil) tmpout = CommandOutput{}.New(out, err) tmpout.Context = context tmpout.Cli = cli @@ -102,11 +103,3 @@ func (up FlatpakUpdater) Update() (*[]CommandOutput, error) { } return &finalOutput, nil } - -func (up *FlatpakUpdater) Logger() *slog.Logger { - return up.Config.logger -} - -func (up *FlatpakUpdater) SetLogger(logger *slog.Logger) { - up.Config.logger = logger -} diff --git a/drv/generic.go b/drv/generic/generic.go similarity index 87% rename from drv/generic.go rename to drv/generic/generic.go index 70ec784..73f36f5 100644 --- a/drv/generic.go +++ b/drv/generic/generic.go @@ -1,4 +1,4 @@ -package drv +package generic import ( "log/slog" @@ -20,6 +20,14 @@ type UpdaterInitConfiguration struct { Logger *slog.Logger } +func EnvOrFallback(environment EnvironmentMap, key string, fallback string) string { + validCase, exists := environment[key] + if exists && validCase != "" { + return validCase + } + return fallback +} + func GetEnvironment(data []string, getkeyval func(item string) (key, val string)) map[string]string { items := make(map[string]string) for _, item := range data { @@ -67,7 +75,7 @@ type DriverConfiguration struct { MultiUser bool DryRun bool Environment EnvironmentMap `json:"-"` - logger *slog.Logger `json:"-"` + Logger *slog.Logger `json:"-"` UserDescription *string } diff --git a/drv/generic/generic_test.go b/drv/generic/generic_test.go new file mode 100644 index 0000000..94d45fb --- /dev/null +++ b/drv/generic/generic_test.go @@ -0,0 +1,19 @@ +package generic_test + +import ( + "testing" + + "github.com/ublue-os/uupd/drv/generic" +) + +func TestFallBack(t *testing.T) { + var environment generic.EnvironmentMap = generic.EnvironmentMap{ + "TEST_FALLBACK_GOOD": "true", + } + if value := generic.EnvOrFallback(environment, "TEST_FALLBACK_GOOD", "FALSE"); value != "true" { + t.Fatalf("Getting the proper value fails, %s", value) + } + if value := generic.EnvOrFallback(environment, "TEST_FALLBACK_BAD", "FALSE"); value != "FALSE" { + t.Fatalf("Getting the fallback fails, %s", value) + } +} diff --git a/drv/rpm-ostree.go b/drv/rpmostree/rpmostree.go similarity index 75% rename from drv/rpm-ostree.go rename to drv/rpmostree/rpmostree.go index 23cb540..e401a0a 100644 --- a/drv/rpm-ostree.go +++ b/drv/rpmostree/rpmostree.go @@ -1,15 +1,17 @@ -package drv +package rpmostree // Temporary: WILL get removed at some point. // FIXME: Remove this on Spring 2025 when we all move to dnf5 and bootc ideally import ( "encoding/json" - "github.com/ublue-os/uupd/pkg/session" "log/slog" "os/exec" "strings" "time" + + . "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/pkg/session" ) type rpmOstreeStatus struct { @@ -31,7 +33,7 @@ func (up RpmOstreeUpdater) Outdated() (bool, error) { var timestamp time.Time cmd := exec.Command(up.BinaryPath, "status", "--json", "--booted") - out, err := session.RunLog(up.Config.logger, slog.LevelDebug, cmd) + out, err := session.RunLog(up.Config.Logger, slog.LevelDebug, cmd) if err != nil { return false, err } @@ -50,7 +52,7 @@ func (up RpmOstreeUpdater) Update() (*[]CommandOutput, error) { var cmd *exec.Cmd binaryPath := up.BinaryPath cli := []string{binaryPath, "upgrade"} - out, err := session.RunLog(up.Config.logger, slog.LevelDebug, cmd) + out, err := session.RunLog(up.Config.Logger, slog.LevelDebug, cmd) tmpout := CommandOutput{}.New(out, err) tmpout.Cli = cli tmpout.Failure = err != nil @@ -66,20 +68,6 @@ func (up RpmOstreeUpdater) Steps() int { return 0 } -func BootcCompatible(binaryPath string) (bool, error) { - cmd := exec.Command(binaryPath, "status", "--format=json") - out, err := cmd.CombinedOutput() - if err != nil { - return false, nil - } - var status bootcStatus - err = json.Unmarshal(out, &status) - if err != nil { - return false, nil - } - return !(status.Status.Booted.Incompatible || status.Status.Staged.Incompatible), nil -} - func (up RpmOstreeUpdater) New(config UpdaterInitConfiguration) (RpmOstreeUpdater, error) { up.Config = DriverConfiguration{ Title: "System", @@ -88,7 +76,7 @@ func (up RpmOstreeUpdater) New(config UpdaterInitConfiguration) (RpmOstreeUpdate DryRun: config.DryRun, Environment: config.Environment, } - up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) + up.Config.Logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) if up.Config.DryRun { return up, nil } @@ -117,14 +105,6 @@ func (up RpmOstreeUpdater) Check() (bool, error) { } updateNecessary := strings.Contains(string(out), "AvailableUpdate") - up.Config.logger.Debug("Executed update check", slog.String("output", string(out)), slog.Bool("update", updateNecessary)) + up.Config.Logger.Debug("Executed update check", slog.String("output", string(out)), slog.Bool("update", updateNecessary)) return updateNecessary, nil } - -func (up *RpmOstreeUpdater) Logger() *slog.Logger { - return up.Config.logger -} - -func (up *RpmOstreeUpdater) SetLogger(logger *slog.Logger) { - up.Config.logger = logger -} diff --git a/drv/system.go b/drv/system/system.go similarity index 60% rename from drv/system.go rename to drv/system/system.go index 9b13c1d..a12a604 100644 --- a/drv/system.go +++ b/drv/system/system.go @@ -1,4 +1,4 @@ -package drv +package system import ( "encoding/json" @@ -7,6 +7,8 @@ import ( "strings" "time" + . "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/drv/rpmostree" "github.com/ublue-os/uupd/pkg/session" ) @@ -34,8 +36,6 @@ type SystemUpdateDriver interface { Outdated() (bool, error) Check() (bool, error) Update() (*[]CommandOutput, error) - Logger() *slog.Logger - SetLogger(value *slog.Logger) } type SystemUpdater struct { @@ -77,9 +77,9 @@ func (up SystemUpdater) Update() (*[]CommandOutput, error) { var cmd *exec.Cmd binaryPath := up.BinaryPath cli := []string{binaryPath, "upgrade", "--quiet"} - up.Config.logger.Debug("Executing update", slog.Any("cli", cli)) + up.Config.Logger.Debug("Executing update", slog.Any("cli", cli)) cmd = exec.Command(cli[0], cli[1:]...) - out, err := session.RunLog(up.Config.logger, slog.LevelDebug, cmd) + out, err := session.RunLog(up.Config.Logger, slog.LevelDebug, cmd) tmpout := CommandOutput{}.New(out, err) tmpout.Failure = err != nil tmpout.Context = "System Update" @@ -94,14 +94,6 @@ func (up SystemUpdater) Steps() int { return 0 } -func EnvOrFallback(environment EnvironmentMap, key string, fallback string) string { - validCase, exists := environment[key] - if exists && validCase != "" { - return validCase - } - return fallback -} - func (up SystemUpdater) New(config UpdaterInitConfiguration) (SystemUpdater, error) { up.Config = DriverConfiguration{ Title: "System", @@ -110,7 +102,7 @@ func (up SystemUpdater) New(config UpdaterInitConfiguration) (SystemUpdater, err DryRun: config.DryRun, Environment: config.Environment, } - up.Config.logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) + up.Config.Logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) up.BinaryPath = EnvOrFallback(config.Environment, "UUPD_BOOTC_BINARY", "/usr/bin/bootc") return up, nil @@ -128,14 +120,59 @@ func (up SystemUpdater) Check() (bool, error) { } updateNecessary := !strings.Contains(string(out), "No changes in:") - up.Config.logger.Debug("Executed update check", slog.String("output", string(out)), slog.Bool("update", updateNecessary)) + up.Config.Logger.Debug("Executed update check", slog.String("output", string(out)), slog.Bool("update", updateNecessary)) return updateNecessary, nil } -func (up *SystemUpdater) Logger() *slog.Logger { - return up.Config.logger +func BootcCompatible(binaryPath string) (bool, error) { + cmd := exec.Command(binaryPath, "status", "--format=json") + out, err := cmd.CombinedOutput() + if err != nil { + return false, nil + } + var status bootcStatus + err = json.Unmarshal(out, &status) + if err != nil { + return false, nil + } + return !(status.Status.Booted.Incompatible || status.Status.Staged.Incompatible), nil } -func (up *SystemUpdater) SetLogger(logger *slog.Logger) { - up.Config.logger = logger +func InitializeSystemDriver(initConfiguration UpdaterInitConfiguration) (SystemUpdateDriver, DriverConfiguration, bool, error) { + var enableUpd bool = true + + rpmOstreeUpdater, err := rpmostree.RpmOstreeUpdater{}.New(initConfiguration) + if err != nil { + enableUpd = false + } + + systemUpdater, err := SystemUpdater{}.New(initConfiguration) + if err != nil { + enableUpd = false + } + + isBootc, err := BootcCompatible(systemUpdater.BinaryPath) + if err != nil { + isBootc = false + } + + if !isBootc { + slog.Debug("Using rpm-ostree fallback as system driver") + } + + // The system driver to be applied needs to have the correct "enabled" value since it will NOT update from here onwards. + systemUpdater.Config.Enabled = systemUpdater.Config.Enabled && isBootc && enableUpd + rpmOstreeUpdater.Config.Enabled = rpmOstreeUpdater.Config.Enabled && !isBootc && enableUpd + + var finalConfig DriverConfiguration + var mainSystemDriver SystemUpdateDriver + if isBootc { + mainSystemDriver = &systemUpdater + finalConfig = systemUpdater.Config + } else { + mainSystemDriver = &rpmOstreeUpdater + finalConfig = systemUpdater.Config + } + + return mainSystemDriver, finalConfig, isBootc, err } diff --git a/drv/system/system_test.go b/drv/system/system_test.go new file mode 100644 index 0000000..ce162e0 --- /dev/null +++ b/drv/system/system_test.go @@ -0,0 +1,35 @@ +package system_test + +import ( + "testing" + + "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/drv/system" + appLogging "github.com/ublue-os/uupd/pkg/logging" +) + +func InitBaseConfig() system.SystemUpdater { + var initConfiguration = generic.UpdaterInitConfiguration{ + DryRun: false, + Ci: false, + Verbose: false, + Environment: nil, + Logger: appLogging.NewMuteLogger(), + } + driv, _ := system.SystemUpdater{}.New(initConfiguration) + return driv +} + +func TestProperSteps(t *testing.T) { + systemUpdater := InitBaseConfig() + systemUpdater.Config.Enabled = false + + if systemUpdater.Steps() != 0 { + t.Fatalf("Expected no steps when module is disabled") + } + + systemUpdater.Config.Enabled = true + if systemUpdater.Steps() == 0 { + t.Fatalf("Expected steps to be added") + } +} diff --git a/drv/system_test.go b/drv/system_test.go deleted file mode 100644 index 8893dc9..0000000 --- a/drv/system_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package drv_test - -import ( - "testing" - - "github.com/ublue-os/uupd/drv" - appLogging "github.com/ublue-os/uupd/pkg/logging" -) - -func InitBaseConfig() drv.SystemUpdater { - var initConfiguration = drv.UpdaterInitConfiguration{ - DryRun: false, - Ci: false, - Verbose: false, - Environment: nil, - Logger: appLogging.NewMuteLogger(), - } - driv, _ := drv.SystemUpdater{}.New(initConfiguration) - return driv -} - -func TestProperSteps(t *testing.T) { - systemUpdater := InitBaseConfig() - systemUpdater.Config.Enabled = false - - if systemUpdater.Steps() != 0 { - t.Fatalf("Expected no steps when module is disabled") - } - - systemUpdater.Config.Enabled = true - if systemUpdater.Steps() == 0 { - t.Fatalf("Expected steps to be added") - } -} - -func TestFallBack(t *testing.T) { - var environment drv.EnvironmentMap = drv.EnvironmentMap{ - "TEST_FALLBACK_GOOD": "true", - } - if value := drv.EnvOrFallback(environment, "TEST_FALLBACK_GOOD", "FALSE"); value != "true" { - t.Fatalf("Getting the proper value fails, %s", value) - } - if value := drv.EnvOrFallback(environment, "TEST_FALLBACK_BAD", "FALSE"); value != "FALSE" { - t.Fatalf("Getting the fallback fails, %s", value) - } -} diff --git a/uupd.spec b/uupd.spec index af02be7..f57ed99 100644 --- a/uupd.spec +++ b/uupd.spec @@ -39,8 +39,7 @@ install -Dpm 644 %{name}.timer %{buildroot}%{_unitdir}/%{name}.timer install -Dpm 644 %{name}.rules %{buildroot}%{_sysconfdir}/polkit-1/rules.d/%{name}.rules %check -# go test should be here if you have tests, e.g. -# go test -v ./... +go test -v ./... %post %systemd_post %{name}.timer From e4b2d7892635e6cb4c185926f56f7aba15c584d3 Mon Sep 17 00:00:00 2001 From: Tulip Blossom Date: Mon, 16 Dec 2024 02:38:59 -0300 Subject: [PATCH 3/3] feat(testing): add basic tests for most modules --- cmd/update.go | 6 +-- drv/brew/brew_test.go | 35 +++++++++++++++ drv/distrobox/distrobox.go | 7 +-- drv/distrobox/distrobox_test.go | 56 ++++++++++++++++++++++++ drv/flatpak/flatpak.go | 7 +-- drv/flatpak/flatpak_test.go | 56 ++++++++++++++++++++++++ drv/generic/generic.go | 13 ++---- drv/generic/generic_test.go | 21 +++++++++ drv/rpmostree/rpmostree.go | 19 +++------ drv/rpmostree/rpmostree_test.go | 35 +++++++++++++++ drv/system/system.go | 7 +-- justfile | 18 ++++++-- pkg/filelock/filelock.go | 6 +-- pkg/percent/incrementer.go | 29 +++++++++++++ pkg/percent/incrementer_test.go | 52 +++++++++++++++++++++++ pkg/percent/progressmanager.go | 31 +------------- pkg/session/session.go | 58 +++++++++++++------------ pkg/session/session_test.go | 75 +++++++++++++++++++++++++++++++++ 18 files changed, 422 insertions(+), 109 deletions(-) create mode 100644 drv/brew/brew_test.go create mode 100644 drv/distrobox/distrobox_test.go create mode 100644 drv/flatpak/flatpak_test.go create mode 100644 drv/rpmostree/rpmostree_test.go create mode 100644 pkg/percent/incrementer.go create mode 100644 pkg/percent/incrementer_test.go create mode 100644 pkg/session/session_test.go diff --git a/cmd/update.go b/cmd/update.go index 28ede9d..055efad 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -84,7 +84,7 @@ func Update(cmd *cobra.Command, args []string) { distroboxUpdater.Config.Enabled = err == nil distroboxUpdater.SetUsers(users) - mainSystemDriver, mainSystemDriverConfig, _, err := system.InitializeSystemDriver(*initConfiguration) + mainSystemDriver, mainSystemDriverConfig, _, _ := system.InitializeSystemDriver(*initConfiguration) enableUpd, err := mainSystemDriver.Check() if err != nil { @@ -137,7 +137,7 @@ func Update(cmd *cobra.Command, args []string) { if systemOutdated { const OUTDATED_WARNING = "There hasn't been an update in over a month. Consider rebooting or running updates manually" - err := session.Notify("System Warning", OUTDATED_WARNING) + err := session.Notify(users, "System Warning", OUTDATED_WARNING) if err != nil { slog.Error("Failed showing warning notification") } @@ -167,7 +167,6 @@ func Update(cmd *cobra.Command, args []string) { if flatpakUpdater.Config.Enabled { slog.Debug(fmt.Sprintf("%s module", flatpakUpdater.Config.Title), slog.String("module_name", flatpakUpdater.Config.Title), slog.Any("module_configuration", flatpakUpdater.Config)) - percent.ChangeTrackerMessageFancy(pw, tracker, progressEnabled, percent.TrackerMessage{Title: flatpakUpdater.Config.Title, Description: flatpakUpdater.Config.Description}) var out *[]drv.CommandOutput out, err = flatpakUpdater.Update() outputs = append(outputs, *out...) @@ -176,7 +175,6 @@ func Update(cmd *cobra.Command, args []string) { if distroboxUpdater.Config.Enabled { slog.Debug(fmt.Sprintf("%s module", distroboxUpdater.Config.Title), slog.String("module_name", distroboxUpdater.Config.Title), slog.Any("module_configuration", distroboxUpdater.Config)) - percent.ChangeTrackerMessageFancy(pw, tracker, progressEnabled, percent.TrackerMessage{Title: distroboxUpdater.Config.Title, Description: distroboxUpdater.Config.Description}) var out *[]drv.CommandOutput out, err = distroboxUpdater.Update() outputs = append(outputs, *out...) diff --git a/drv/brew/brew_test.go b/drv/brew/brew_test.go new file mode 100644 index 0000000..d622250 --- /dev/null +++ b/drv/brew/brew_test.go @@ -0,0 +1,35 @@ +package brew_test + +import ( + "testing" + + "github.com/ublue-os/uupd/drv/brew" + "github.com/ublue-os/uupd/drv/generic" + appLogging "github.com/ublue-os/uupd/pkg/logging" +) + +func InitBaseConfig() brew.BrewUpdater { + var initConfiguration = generic.UpdaterInitConfiguration{ + DryRun: false, + Ci: false, + Verbose: false, + Environment: nil, + Logger: appLogging.NewMuteLogger(), + } + driv, _ := brew.BrewUpdater{}.New(initConfiguration) + return driv +} + +func TestProperSteps(t *testing.T) { + brewUpdater := InitBaseConfig() + brewUpdater.Config.Enabled = false + + if brewUpdater.Steps() != 0 { + t.Fatalf("Expected no steps when module is disabled") + } + + brewUpdater.Config.Enabled = true + if brewUpdater.Steps() == 0 { + t.Fatalf("Expected steps to be added") + } +} diff --git a/drv/distrobox/distrobox.go b/drv/distrobox/distrobox.go index 213d6ac..85818ed 100644 --- a/drv/distrobox/distrobox.go +++ b/drv/distrobox/distrobox.go @@ -43,12 +43,7 @@ func (up DistroboxUpdater) New(config UpdaterInitConfiguration) (DistroboxUpdate up.usersEnabled = false up.Tracker = nil - binaryPath, exists := up.Config.Environment["UUPD_DISTROBOX_BINARY"] - if !exists || binaryPath == "" { - up.binaryPath = "/usr/bin/distrobox" - } else { - up.binaryPath = binaryPath - } + up.binaryPath = EnvOrFallback(up.Config.Environment, "UUPD_DISTROBOX_BINARY", "/usr/bin/distrobox") return up, nil } diff --git a/drv/distrobox/distrobox_test.go b/drv/distrobox/distrobox_test.go new file mode 100644 index 0000000..77d3b27 --- /dev/null +++ b/drv/distrobox/distrobox_test.go @@ -0,0 +1,56 @@ +package distrobox_test + +import ( + "log" + "testing" + + "github.com/ublue-os/uupd/drv/distrobox" + "github.com/ublue-os/uupd/drv/generic" + appLogging "github.com/ublue-os/uupd/pkg/logging" + "github.com/ublue-os/uupd/pkg/session" +) + +func InitBaseConfig() distrobox.DistroboxUpdater { + var initConfiguration = generic.UpdaterInitConfiguration{ + DryRun: false, + Ci: false, + Verbose: false, + Environment: nil, + Logger: appLogging.NewMuteLogger(), + } + driv, _ := distrobox.DistroboxUpdater{}.New(initConfiguration) + return driv +} + +func TestProperSteps(t *testing.T) { + updater := InitBaseConfig() + updater.Config.Enabled = false + + if updater.Steps() != 0 { + t.Fatalf("Expected no steps when module is disabled") + } + + updater.Config.Enabled = true + if updater.Steps() == 0 { + t.Fatalf("Expected steps to be added") + } +} + +func TestProperUserSteps(t *testing.T) { + updater := InitBaseConfig() + + mockUser := []session.User{ + {UID: 0, Name: "root"}, + {UID: 1, Name: "roote"}, + {UID: 2, Name: "rooto"}, + } + updater.SetUsers(mockUser) + + if reported := updater.Steps(); reported != 1+len(mockUser) { + log.Fatalf("Incorrect number of steps for users: %d", reported) + } + updater.Config.Enabled = false + if reported := updater.Steps(); reported != 0 { + log.Fatalf("Incorrect number of steps for users: %d", reported) + } +} diff --git a/drv/flatpak/flatpak.go b/drv/flatpak/flatpak.go index e8cc311..3a99c56 100644 --- a/drv/flatpak/flatpak.go +++ b/drv/flatpak/flatpak.go @@ -44,12 +44,7 @@ func (up FlatpakUpdater) New(config UpdaterInitConfiguration) (FlatpakUpdater, e up.usersEnabled = false up.Tracker = nil - binaryPath, exists := up.Config.Environment["UUPD_FLATPAK_BINARY"] - if !exists || binaryPath == "" { - up.binaryPath = "/usr/bin/flatpak" - } else { - up.binaryPath = binaryPath - } + up.binaryPath = EnvOrFallback(up.Config.Environment, "UUPD_FLATPAK_BINARY", "/usr/bin/flatpak") return up, nil } diff --git a/drv/flatpak/flatpak_test.go b/drv/flatpak/flatpak_test.go new file mode 100644 index 0000000..6974b6f --- /dev/null +++ b/drv/flatpak/flatpak_test.go @@ -0,0 +1,56 @@ +package flatpak_test + +import ( + "log" + "testing" + + "github.com/ublue-os/uupd/drv/flatpak" + "github.com/ublue-os/uupd/drv/generic" + appLogging "github.com/ublue-os/uupd/pkg/logging" + "github.com/ublue-os/uupd/pkg/session" +) + +func InitBaseConfig() flatpak.FlatpakUpdater { + var initConfiguration = generic.UpdaterInitConfiguration{ + DryRun: false, + Ci: false, + Verbose: false, + Environment: nil, + Logger: appLogging.NewMuteLogger(), + } + driv, _ := flatpak.FlatpakUpdater{}.New(initConfiguration) + return driv +} + +func TestProperSteps(t *testing.T) { + updater := InitBaseConfig() + updater.Config.Enabled = false + + if updater.Steps() != 0 { + t.Fatalf("Expected no steps when module is disabled") + } + + updater.Config.Enabled = true + if updater.Steps() == 0 { + t.Fatalf("Expected steps to be added") + } +} + +func TestProperUserSteps(t *testing.T) { + updater := InitBaseConfig() + + mockUser := []session.User{ + {UID: 0, Name: "root"}, + {UID: 1, Name: "roote"}, + {UID: 2, Name: "rooto"}, + } + updater.SetUsers(mockUser) + + if reported := updater.Steps(); reported != 1+len(mockUser) { + log.Fatalf("Incorrect number of steps for users: %d", reported) + } + updater.Config.Enabled = false + if reported := updater.Steps(); reported != 0 { + log.Fatalf("Incorrect number of steps for users: %d", reported) + } +} diff --git a/drv/generic/generic.go b/drv/generic/generic.go index 73f36f5..e885f48 100644 --- a/drv/generic/generic.go +++ b/drv/generic/generic.go @@ -28,11 +28,11 @@ func EnvOrFallback(environment EnvironmentMap, key string, fallback string) stri return fallback } -func GetEnvironment(data []string, getkeyval func(item string) (key, val string)) map[string]string { +func GetEnvironment(data []string) map[string]string { items := make(map[string]string) for _, item := range data { - key, val := getkeyval(item) - items[key] = val + splits := strings.Split(item, "=") + items[splits[0]] = splits[1] } return items } @@ -40,12 +40,7 @@ func GetEnvironment(data []string, getkeyval func(item string) (key, val string) func (up UpdaterInitConfiguration) New() *UpdaterInitConfiguration { up.DryRun = false up.Ci = false - up.Environment = GetEnvironment(os.Environ(), func(item string) (key, val string) { - splits := strings.Split(item, "=") - key = splits[0] - val = splits[1] - return - }) + up.Environment = GetEnvironment(os.Environ()) up.Logger = slog.Default() return &up diff --git a/drv/generic/generic_test.go b/drv/generic/generic_test.go index 94d45fb..e07b3d2 100644 --- a/drv/generic/generic_test.go +++ b/drv/generic/generic_test.go @@ -1,6 +1,7 @@ package generic_test import ( + "log" "testing" "github.com/ublue-os/uupd/drv/generic" @@ -17,3 +18,23 @@ func TestFallBack(t *testing.T) { t.Fatalf("Getting the fallback fails, %s", value) } } + +func TestProperEnvironment(t *testing.T) { + valuesExpected := map[string]string{ + "SIGMA": "true", + "CHUD": "false", + "AMOGUS": "sus", + "NOTHING": "", + } + + for key, value := range valuesExpected { + testmap := generic.GetEnvironment([]string{key + "=" + value}) + valuegot, exists := testmap[key] + if !exists { + log.Fatalf("Could not get environment variable at all: %s", key) + } + if valuegot != value { + log.Fatalf("Value gotten from variable was not expected: Got %s, Expected %s", valuegot, value) + } + } +} diff --git a/drv/rpmostree/rpmostree.go b/drv/rpmostree/rpmostree.go index e401a0a..637b786 100644 --- a/drv/rpmostree/rpmostree.go +++ b/drv/rpmostree/rpmostree.go @@ -25,11 +25,15 @@ type RpmOstreeUpdater struct { BinaryPath string } +// Checks if it is at least a month old considering how that works +func IsOutdatedOneMonthTimestamp(current time.Time, target time.Time) bool { + return target.Before(current.AddDate(0, -1, 0)) +} + func (up RpmOstreeUpdater) Outdated() (bool, error) { if up.Config.DryRun { return false, nil } - oneMonthAgo := time.Now().AddDate(0, -1, 0) var timestamp time.Time cmd := exec.Command(up.BinaryPath, "status", "--json", "--booted") @@ -44,7 +48,7 @@ func (up RpmOstreeUpdater) Outdated() (bool, error) { } timestamp = time.Unix(status.Deployments[0].Timestamp, 0).UTC() - return timestamp.Before(oneMonthAgo), nil + return IsOutdatedOneMonthTimestamp(time.Now(), timestamp), nil } func (up RpmOstreeUpdater) Update() (*[]CommandOutput, error) { @@ -77,16 +81,7 @@ func (up RpmOstreeUpdater) New(config UpdaterInitConfiguration) (RpmOstreeUpdate Environment: config.Environment, } up.Config.Logger = config.Logger.With(slog.String("module", strings.ToLower(up.Config.Title))) - if up.Config.DryRun { - return up, nil - } - - binaryPath, exists := up.Config.Environment["UUPD_RPMOSTREE_BINARY"] - if !exists || binaryPath == "" { - up.BinaryPath = "/usr/bin/rpm-ostree" - } else { - up.BinaryPath = binaryPath - } + up.BinaryPath = EnvOrFallback(up.Config.Environment, "UUPD_RPMOSTREE_BINARY", "/usr/bin/rpm-ostree") return up, nil } diff --git a/drv/rpmostree/rpmostree_test.go b/drv/rpmostree/rpmostree_test.go new file mode 100644 index 0000000..56c1522 --- /dev/null +++ b/drv/rpmostree/rpmostree_test.go @@ -0,0 +1,35 @@ +package rpmostree_test + +import ( + "testing" + + "github.com/ublue-os/uupd/drv/generic" + "github.com/ublue-os/uupd/drv/rpmostree" + appLogging "github.com/ublue-os/uupd/pkg/logging" +) + +func InitBaseConfig() rpmostree.RpmOstreeUpdater { + var initConfiguration = generic.UpdaterInitConfiguration{ + DryRun: false, + Ci: false, + Verbose: false, + Environment: nil, + Logger: appLogging.NewMuteLogger(), + } + driv, _ := rpmostree.RpmOstreeUpdater{}.New(initConfiguration) + return driv +} + +func TestProperSteps(t *testing.T) { + rpmostreeUpdater := InitBaseConfig() + rpmostreeUpdater.Config.Enabled = false + + if rpmostreeUpdater.Steps() != 0 { + t.Fatalf("Expected no steps when module is disabled") + } + + rpmostreeUpdater.Config.Enabled = true + if rpmostreeUpdater.Steps() == 0 { + t.Fatalf("Expected steps to be added") + } +} diff --git a/drv/system/system.go b/drv/system/system.go index a12a604..55649c3 100644 --- a/drv/system/system.go +++ b/drv/system/system.go @@ -43,11 +43,6 @@ type SystemUpdater struct { BinaryPath string } -// Checks if it is at least a month old considering how that works -func IsOutdatedOneMonthTimestamp(current time.Time, target time.Time) bool { - return target.Before(current.AddDate(0, -1, 0)) -} - func (up SystemUpdater) Outdated() (bool, error) { if up.Config.DryRun { return false, nil @@ -69,7 +64,7 @@ func (up SystemUpdater) Outdated() (bool, error) { if err != nil { return false, nil } - return IsOutdatedOneMonthTimestamp(time.Now(), timestamp), nil + return rpmostree.IsOutdatedOneMonthTimestamp(time.Now(), timestamp), nil } func (up SystemUpdater) Update() (*[]CommandOutput, error) { diff --git a/justfile b/justfile index dea3859..f66e4f4 100644 --- a/justfile +++ b/justfile @@ -77,10 +77,20 @@ lint: release: goreleaser -test: - go test -v -cover ./... +test directory="": + #!/usr/bin/env bash + if [ "{{directory}}" != "" ] ; then + go test -v -cover ./{{directory}}/... + else + go test -v -cover ./... + fi -test-interactive: +test-coverage directory="": #!/usr/bin/env bash t="/tmp/go-cover.$$.tmp" - go test -v -coverprofile=$t ./... $@ && go tool cover -html=$t && unlink $t + + if [ "{{directory}}" != "" ] ; then + go test -v -coverprofile=$t ./{{directory}}/... $@ && go tool cover -html=$t && unlink $t + else + go test -v -coverprofile=$t ./... $@ && go tool cover -html=$t && unlink $t + fi diff --git a/pkg/filelock/filelock.go b/pkg/filelock/filelock.go index a9ffe76..3300a53 100644 --- a/pkg/filelock/filelock.go +++ b/pkg/filelock/filelock.go @@ -21,11 +21,7 @@ func GetDefaultLockfile() string { } func OpenLockfile(filepath string) (*os.File, error) { - file, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return nil, err - } - return file, err + return os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) } type TimeoutConfig struct { diff --git a/pkg/percent/incrementer.go b/pkg/percent/incrementer.go new file mode 100644 index 0000000..ebbf36f --- /dev/null +++ b/pkg/percent/incrementer.go @@ -0,0 +1,29 @@ +package percent + +import "github.com/jedib0t/go-pretty/v6/progress" + +type Incrementer struct { + DoneIncrements int + MaxIncrements int +} + +type IncrementTracker struct { + Tracker *progress.Tracker + Incrementer *Incrementer +} + +func (it *IncrementTracker) IncrementSection(err error) { + if int64(it.Incrementer.DoneIncrements)+int64(1) > int64(it.Incrementer.MaxIncrements) { + return + } + it.Incrementer.DoneIncrements += 1 + if err == nil { + it.Tracker.Increment(1) + } else { + it.Tracker.IncrementWithError(1) + } +} + +func (it *IncrementTracker) CurrentStep() int { + return it.Incrementer.DoneIncrements +} diff --git a/pkg/percent/incrementer_test.go b/pkg/percent/incrementer_test.go new file mode 100644 index 0000000..954f430 --- /dev/null +++ b/pkg/percent/incrementer_test.go @@ -0,0 +1,52 @@ +package percent_test + +import ( + "math" + "testing" + + "github.com/jedib0t/go-pretty/v6/progress" + "github.com/ublue-os/uupd/pkg/percent" +) + +func InitIncrementer(max int) percent.IncrementTracker { + tracker := progress.Tracker{Message: "Updating", Units: progress.UnitsDefault, Total: int64(max)} + incrementer := percent.Incrementer{ + MaxIncrements: max, + DoneIncrements: 0, + } + return percent.IncrementTracker{ + Tracker: &tracker, + Incrementer: &incrementer, + } +} + +func TestOverflow(t *testing.T) { + max := 3 + tracker := InitIncrementer(max) + + iter := 0 + for iter < max { + tracker.IncrementSection(nil) + iter++ + } + // +1 so that it overflows + tracker.IncrementSection(nil) + + if tracker.CurrentStep() > max { + t.Fatalf("Incremented with overflow. Expected: %d, Got: %d", max, tracker.CurrentStep()) + } +} + +func TestProperIncrement(t *testing.T) { + num := math.MaxInt8 + tracker := InitIncrementer(num) + + iter := 0 + for iter < num { + if tracker.CurrentStep() != iter { + t.Fatalf("Misstep increment. Expected: %d, Got: %d", iter, tracker.CurrentStep()) + } + tracker.IncrementSection(nil) + iter++ + } +} diff --git a/pkg/percent/progressmanager.go b/pkg/percent/progressmanager.go index d85aee2..ab945a1 100644 --- a/pkg/percent/progressmanager.go +++ b/pkg/percent/progressmanager.go @@ -14,16 +14,6 @@ import ( "github.com/ublue-os/uupd/pkg/session" ) -type Incrementer struct { - doneIncrements int - MaxIncrements int -} - -type IncrementTracker struct { - Tracker *progress.Tracker - incrementer *Incrementer -} - var CuteColors = progress.StyleColors{ Message: text.Colors{text.FgWhite}, Error: text.Colors{text.FgRed}, @@ -100,7 +90,7 @@ func NewProgressWriter() progress.Writer { func NewIncrementTracker(tracker *progress.Tracker, max_increments int) *IncrementTracker { return &IncrementTracker{ Tracker: tracker, - incrementer: &Incrementer{MaxIncrements: max_increments}, + Incrementer: &Incrementer{MaxIncrements: max_increments}, } } @@ -126,25 +116,6 @@ func ChangeTrackerMessageFancy(writer progress.Writer, tracker *IncrementTracker tracker.Tracker.UpdateMessage(finalMessage) } -func (it *IncrementTracker) IncrementSection(err error) { - var increment_step float64 - if it.incrementer.doneIncrements == 0 { - increment_step = 1 - } else { - increment_step = float64(it.Tracker.Total / int64(it.incrementer.MaxIncrements)) - } - if err == nil { - it.Tracker.Increment(int64(increment_step)) - } else { - it.Tracker.IncrementWithError(int64(increment_step)) - } - it.incrementer.doneIncrements++ -} - -func (it *IncrementTracker) CurrentStep() int { - return it.incrementer.doneIncrements -} - func ResetOscProgress() { // OSC escape sequence to reset all previous OSC progress hints to 0%. // Documentation is on https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC diff --git a/pkg/session/session.go b/pkg/session/session.go index bbc5c1f..5f9bacc 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -2,6 +2,7 @@ package session import ( "bufio" + "context" "fmt" "io" "log/slog" @@ -26,13 +27,17 @@ func RunLog(logger *slog.Logger, level slog.Level, command *exec.Cmd) ([]byte, e stderr, _ := command.StderrPipe() multiReader := io.MultiReader(stdout, stderr) - command.Start() + if err := command.Wait(); err != nil { + logger.Warn("Error occoured starting external command", slog.Any("error", err)) + } scanner := bufio.NewScanner(multiReader) scanner.Split(bufio.ScanLines) for scanner.Scan() { - logger.With(slog.Bool("subcommand", true)).Log(nil, level, scanner.Text()) + logger.With(slog.Bool("subcommand", true)).Log(context.TODO(), level, scanner.Text()) + } + if err := command.Wait(); err != nil { + logger.Warn("Error occoured while waiting for external command", slog.Any("error", err)) } - command.Wait() return scanner.Bytes(), scanner.Err() } @@ -56,6 +61,23 @@ func RunUID(logger *slog.Logger, level slog.Level, uid int, command []string, en return RunLog(logger, level, cmd) } +func ParseUserFromVariant(uidVariant dbus.Variant, nameVariant dbus.Variant) (User, error) { + uid, ok := uidVariant.Value().(uint32) + if !ok { + return User{}, fmt.Errorf("invalid UID type, expected uint32") + } + + name, ok := nameVariant.Value().(string) + if !ok { + return User{}, fmt.Errorf("invalid Name type, expected string") + } + + return User{ + UID: int(uid), + Name: name, + }, nil +} + func ListUsers() ([]User, error) { conn, err := dbus.SystemBus() if err != nil { @@ -72,38 +94,20 @@ func ListUsers() ([]User, error) { var users []User for _, data := range resp { - if len(data) < 2 { - return []User{}, fmt.Errorf("Malformed dbus response") - } - uidVariant := data[0] - nameVariant := data[1] - - uid, ok := uidVariant.Value().(uint32) - if !ok { - return []User{}, fmt.Errorf("invalid UID type, expected uint32") + parsed, err := ParseUserFromVariant(data[0], data[1]) + if err != nil { + return nil, err } - name, ok := nameVariant.Value().(string) - if !ok { - return []User{}, fmt.Errorf("invalid Name type, expected string") - } - - users = append(users, User{ - UID: int(uid), - Name: name, - }) + users = append(users, parsed) } return users, nil } -func Notify(summary string, body string) error { - users, err := ListUsers() - if err != nil { - return err - } +func Notify(users []User, summary string, body string) error { for _, user := range users { // we don't care if these exit - _, _ = RunUID(slog.Default(), slog.LevelDebug, user.UID, []string{"/usr/bin/notify-send", "--app-name", "uupd", summary, body}, nil) + _, _ = RunUID(nil, slog.LevelDebug, user.UID, []string{"/usr/bin/notify-send", "--app-name", "uupd", summary, body}, nil) } return nil } diff --git a/pkg/session/session_test.go b/pkg/session/session_test.go new file mode 100644 index 0000000..68935c8 --- /dev/null +++ b/pkg/session/session_test.go @@ -0,0 +1,75 @@ +package session_test + +import ( + "fmt" + "math" + "testing" + + "github.com/godbus/dbus/v5" + "github.com/ublue-os/uupd/pkg/session" +) + +func TestUserParsingInvalidUID(t *testing.T) { + t.Parallel() + testVariants := []dbus.Variant{ + dbus.MakeVariant(0.3), + dbus.MakeVariant(-1), + dbus.MakeVariant(math.MaxInt), + dbus.MakeVariant(math.MinInt), + } + + userName := dbus.MakeVariant("root") + for _, uidVariant := range testVariants { + t.Run(fmt.Sprintf("variant: %v", uidVariant.Value()), func(t *testing.T) { + t.Parallel() + _, err := session.ParseUserFromVariant(uidVariant, userName) + if err == nil { + t.Fatalf("Parser accepted invalid input: %v %v", uidVariant, userName) + } + }) + } +} + +func TestUserParsingInvalidName(t *testing.T) { + t.Parallel() + testVariants := []dbus.Variant{ + dbus.MakeVariant(0.3), + dbus.MakeVariant(-1), + dbus.MakeVariant(math.MaxInt), + dbus.MakeVariant(math.MinInt), + } + + uidVariant := dbus.MakeVariant(uint32(0)) + for _, nameVariant := range testVariants { + t.Run(fmt.Sprintf("variant: %v", uidVariant.Value()), func(t *testing.T) { + t.Parallel() + _, err := session.ParseUserFromVariant(uidVariant, nameVariant) + if err == nil { + t.Fatalf("Parser accepted invalid input: %v", err) + } + }) + } +} + +func TestUserParsingValidUser(t *testing.T) { + t.Parallel() + testVariants := []struct { + UidVariant dbus.Variant + NameVariant dbus.Variant + }{ + {dbus.MakeVariant(uint32(0)), dbus.MakeVariant("root")}, + {dbus.MakeVariant(uint32(10)), dbus.MakeVariant("bob")}, + {dbus.MakeVariant(uint32(20)), dbus.MakeVariant("beatryz")}, + {dbus.MakeVariant(uint32(math.MaxUint16)), dbus.MakeVariant("zorg_the_destroyer")}, + } + + for _, variant := range testVariants { + t.Run(fmt.Sprintf("variant: %v", variant.NameVariant.Value()), func(t *testing.T) { + t.Parallel() + _, err := session.ParseUserFromVariant(variant.UidVariant, variant.NameVariant) + if err != nil { + t.Fatalf("Parser rejected valid input: %v", err) + } + }) + } +}