Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 99 additions & 49 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,118 @@ import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
"strings"
"testing"
"time"
)

func TestErrorFuncs(t *testing.T) {
errFuncs := []func(ctx context.Context, unit string, opts Options) error{
func(ctx context.Context, unit string, opts Options) error { return Enable(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Disable(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Restart(ctx, unit, opts) },
func(ctx context.Context, unit string, opts Options) error { return Start(ctx, unit, opts) },
}
errCases := []struct {
unit string
err error
opts Options
runAsUser bool
portableUserUnit := userTestUnit(t)
testCases := []struct {
name string
fn func(ctx context.Context, unit string, opts Options) error
lifecycle bool
errCases []struct {
unit string
err error
opts Options
runAsUser bool
}
}{
/* Run these tests only as an unpriviledged user */

// try nonexistant unit in user mode as user
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
// try nonexisting unit in system mode as user
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
// try existing unit in system mode as user
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},

/* End user tests*/

/* Run these tests only as a superuser */

// try nonexistant unit in system mode as system
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false},
// try existing unit in system mode as system
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
// try existing unit in system mode as system
{"nginx", nil, Options{UserMode: false}, false},

/* End superuser tests*/

{
name: "Enable",
fn: func(ctx context.Context, unit string, opts Options) error { return Enable(ctx, unit, opts) },
lifecycle: false,
errCases: []struct {
unit string
err error
opts Options
runAsUser bool
}{
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
{portableUserUnit, nil, Options{UserMode: true}, true},
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false},
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{"nginx", nil, Options{UserMode: false}, false},
},
},
{
name: "Disable",
fn: func(ctx context.Context, unit string, opts Options) error { return Disable(ctx, unit, opts) },
lifecycle: false,
errCases: []struct {
unit string
err error
opts Options
runAsUser bool
}{
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
{portableUserUnit, nil, Options{UserMode: true}, true},
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false},
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{"nginx", nil, Options{UserMode: false}, false},
},
},
{
name: "Restart",
fn: func(ctx context.Context, unit string, opts Options) error { return Restart(ctx, unit, opts) },
lifecycle: true,
errCases: []struct {
unit string
err error
opts Options
runAsUser bool
}{
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
{portableUserUnit, nil, Options{UserMode: true}, true},
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false},
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{"nginx", nil, Options{UserMode: false}, false},
},
},
{
name: "Start",
fn: func(ctx context.Context, unit string, opts Options) error { return Start(ctx, unit, opts) },
lifecycle: true,
errCases: []struct {
unit string
err error
opts Options
runAsUser bool
}{
{"nonexistant", ErrDoesNotExist, Options{UserMode: true}, true},
{portableUserUnit, nil, Options{UserMode: true}, true},
{"nonexistant", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nginx", ErrInsufficientPermissions, Options{UserMode: false}, true},
{"nonexistant", ErrDoesNotExist, Options{UserMode: false}, false},
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{"nginx", nil, Options{UserMode: false}, false},
},
},
}

for _, f := range errFuncs {
fName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
fName = strings.TrimPrefix(fName, "github.com/taigrr/")
t.Run(fmt.Sprintf("Errorcheck %s", fName), func(t *testing.T) {
for _, tc := range errCases {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && tc.runAsUser {
for _, tc := range testCases {
t.Run(fmt.Sprintf("Errorcheck %s", tc.name), func(t *testing.T) {
for _, errCase := range tc.errCases {
t.Run(fmt.Sprintf("%s as %s", errCase.unit, userString), func(t *testing.T) {
if (userString == "root" || userString == "system") && errCase.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
} else if (userString != "root" && userString != "system") && !errCase.runAsUser {
t.Skip("skipping superuser test while running as user")
}
if tc.lifecycle && errCase.runAsUser && errCase.opts.UserMode {
requireUserBus(t)
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := f(ctx, tc.unit, tc.opts)
if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err)
err := tc.fn(ctx, errCase.unit, errCase.opts)
if !errors.Is(err, errCase.err) {
t.Errorf("error is %v, but should have been %v", err, errCase.err)
}
})
}
Expand Down
46 changes: 31 additions & 15 deletions helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func TestGetStartTime(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
portableUserUnit := userTestUnit(t)
readOnlySystemUnit := systemTestUnit(t)
testCases := []struct {
unit string
err error
Expand All @@ -31,18 +33,18 @@ func TestGetStartTime(t *testing.T) {
// try nonexistant unit in user mode as user
{"nonexistant", ErrUnitNotActive, Options{UserMode: false}, true},
// try existing unit in user mode as user
{"syncthing", ErrUnitNotActive, Options{UserMode: true}, true},
{portableUserUnit, ErrUnitNotActive, Options{UserMode: true}, true},
// try existing unit in system mode as user
{"nginx", nil, Options{UserMode: false}, true},
{readOnlySystemUnit, nil, Options{UserMode: false}, true},

// Run these tests only as a superuser

// try nonexistant unit in system mode as system
{"nonexistant", ErrUnitNotActive, Options{UserMode: false}, false},
// try existing unit in system mode as system
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{readOnlySystemUnit, ErrBusFailure, Options{UserMode: true}, false},
// try existing unit in system mode as system
{"nginx", nil, Options{UserMode: false}, false},
{readOnlySystemUnit, nil, Options{UserMode: false}, false},
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
Expand All @@ -59,6 +61,9 @@ func TestGetStartTime(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err := GetStartTime(ctx, tc.unit, tc.opts)
if tc.unit == portableUserUnit && tc.opts.UserMode && tc.runAsUser && err == nil {
return
}
if !errors.Is(err, tc.err) {
t.Errorf("error is %v, but should have been %v", err, tc.err)
}
Expand Down Expand Up @@ -94,6 +99,8 @@ func TestGetStartTime(t *testing.T) {
}

func TestGetNumRestarts(t *testing.T) {
readOnlySystemUnit := systemTestUnit(t)
portableUserUnit := userTestUnit(t)
type testCase struct {
unit string
err error
Expand All @@ -106,18 +113,18 @@ func TestGetNumRestarts(t *testing.T) {
// try nonexistant unit in user mode as user
{"nonexistant", ErrValueNotSet, Options{UserMode: false}, true},
// try existing unit in user mode as user (loaded, so NRestarts=0 is valid)
{"syncthing", nil, Options{UserMode: true}, true},
{portableUserUnit, nil, Options{UserMode: true}, true},
// try existing unit in system mode as user
{"nginx", nil, Options{UserMode: false}, true},
{readOnlySystemUnit, nil, Options{UserMode: false}, true},

// Run these tests only as a superuser

// try nonexistant unit in system mode as system
{"nonexistant", ErrValueNotSet, Options{UserMode: false}, false},
// try existing unit in system mode as system
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{readOnlySystemUnit, ErrBusFailure, Options{UserMode: true}, false},
// try existing unit in system mode as system
{"nginx", nil, Options{UserMode: false}, false},
{readOnlySystemUnit, nil, Options{UserMode: false}, false},
}
for _, tc := range testCases {
func(tc testCase) {
Expand Down Expand Up @@ -177,6 +184,8 @@ func TestGetNumRestarts(t *testing.T) {
}

func TestGetMemoryUsage(t *testing.T) {
readOnlySystemUnit := systemTestUnit(t)
portableUserUnit := userTestUnit(t)
type testCase struct {
unit string
err error
Expand All @@ -189,23 +198,26 @@ func TestGetMemoryUsage(t *testing.T) {
// try nonexistant unit in user mode as user
{"nonexistant", ErrValueNotSet, Options{UserMode: false}, true},
// try existing unit in user mode as user
{"syncthing", ErrValueNotSet, Options{UserMode: true}, true},
{portableUserUnit, ErrValueNotSet, Options{UserMode: true}, true},
// try existing unit in system mode as user
{"nginx", nil, Options{UserMode: false}, true},
{readOnlySystemUnit, nil, Options{UserMode: false}, true},

// Run these tests only as a superuser

// try nonexistant unit in system mode as system
{"nonexistant", ErrValueNotSet, Options{UserMode: false}, false},
// try existing unit in system mode as system
{"nginx", ErrBusFailure, Options{UserMode: true}, false},
{readOnlySystemUnit, ErrBusFailure, Options{UserMode: true}, false},
// try existing unit in system mode as system
{"nginx", nil, Options{UserMode: false}, false},
{readOnlySystemUnit, nil, Options{UserMode: false}, false},
}
for _, tc := range testCases {
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if tc.runAsUser && tc.opts.UserMode && tc.unit == portableUserUnit {
requireUserBus(t)
}
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
Expand All @@ -224,9 +236,9 @@ func TestGetMemoryUsage(t *testing.T) {
t.Run("prove memory usage values change across services", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
bytes, err := GetMemoryUsage(ctx, "nginx", Options{UserMode: false})
bytes, err := GetMemoryUsage(ctx, readOnlySystemUnit, Options{UserMode: false})
if err != nil {
t.Errorf("issue getting memory usage of nginx: %v", err)
t.Errorf("issue getting memory usage of %s: %v", readOnlySystemUnit, err)
}
secondBytes, err := GetMemoryUsage(ctx, "user.slice", Options{UserMode: false})
if err != nil {
Expand Down Expand Up @@ -287,6 +299,7 @@ func TestGetUnits(t *testing.T) {
}

func TestGetPID(t *testing.T) {
portableUserUnit := userTestUnit(t)
type testCase struct {
unit string
err error
Expand All @@ -300,7 +313,7 @@ func TestGetPID(t *testing.T) {
// try nonexistant unit in user mode as user
{"nonexistant", nil, Options{UserMode: false}, true},
// try existing unit in user mode as user
{"syncthing", nil, Options{UserMode: true}, true},
{portableUserUnit, nil, Options{UserMode: true}, true},
// try existing unit in system mode as user
{"nginx", nil, Options{UserMode: false}, true},

Expand All @@ -317,6 +330,9 @@ func TestGetPID(t *testing.T) {
func(tc testCase) {
t.Run(fmt.Sprintf("%s as %s", tc.unit, userString), func(t *testing.T) {
t.Parallel()
if tc.runAsUser && tc.opts.UserMode && tc.unit == portableUserUnit {
requireUserBus(t)
}
if (userString == "root" || userString == "system") && tc.runAsUser {
t.Skip("skipping user test while running as superuser")
} else if (userString != "root" && userString != "system") && !tc.runAsUser {
Expand Down
72 changes: 72 additions & 0 deletions portable_user_unit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package systemctl

import (
"context"
"os"
"path/filepath"
"testing"
"time"
)

func userBusAvailable(t *testing.T) bool {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err := GetUnits(ctx, Options{UserMode: true})
return err == nil
}

func requireUserBus(t *testing.T) {
t.Helper()
if !userBusAvailable(t) {
t.Skip("skipping user-mode lifecycle test without a reachable user systemd bus")
}
}

func userTestUnit(t *testing.T) string {
t.Helper()
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatalf("get user home dir: %v", err)
}
unitDir := filepath.Join(homeDir, ".local", "share", "systemd", "user")
if err := os.MkdirAll(unitDir, 0o755); err != nil {
t.Fatalf("create user systemd dir: %v", err)
}

unitName := "openclaw-test"
unitPath := filepath.Join(unitDir, unitName+".service")
unitFile := `[Unit]
Description=OpenClaw portable test service

[Service]
Type=simple
ExecStart=/bin/sh -c 'sleep 300'
Restart=on-failure

[Install]
WantedBy=default.target
`
if err := os.WriteFile(unitPath, []byte(unitFile), 0o644); err != nil {
t.Fatalf("write user test unit: %v", err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = Stop(ctx, unitName, Options{UserMode: true})
_ = Disable(ctx, unitName, Options{UserMode: true})
_ = Unmask(ctx, unitName, Options{UserMode: true})
_ = os.Remove(filepath.Join(unitDir, "default.target.wants", unitName+".service"))
_ = os.Remove(unitPath)
_ = DaemonReload(ctx, Options{UserMode: true})
})

if userBusAvailable(t) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := DaemonReload(ctx, Options{UserMode: true}); err != nil {
t.Fatalf("reload user daemon: %v", err)
}
}
return unitName
}
Loading
Loading