From bfda38ab0af3bf06fbd74e5dcae2433b510f9462 Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 14 Aug 2026 15:53:25 -0700 Subject: [PATCH 1/6] bin: simplifiy / harmonize scripts - remove `test` which used outdated `go fmt` command --- bin/build | 15 +++++++-------- bin/build-linux-amd64 | 8 ++++---- bin/test | 10 ---------- bin/test-unit | 11 +++++++---- 4 files changed, 18 insertions(+), 26 deletions(-) delete mode 100755 bin/test diff --git a/bin/build b/bin/build index b0a4b37c..5c4766bf 100755 --- a/bin/build +++ b/bin/build @@ -1,11 +1,10 @@ -#!/bin/bash +#!/usr/bin/env bash +set -eu -o pipefail -set -e +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" -bin=$(dirname $0) +( + cd "${ROOT_DIR}" -if [[ "$GOOS" == 'linux' ]] && [[ "$GOARCH" == 'amd64' ]]; then - export GOTOOLDIR=$(go env GOROOT)/pkg/linux_amd64 -fi - -CGO_ENABLED=0 go build -o $bin/../out/verify-multidigest github.com/cloudfoundry/bosh-utils/main + CGO_ENABLED=0 go build -o out/verify-multidigest github.com/cloudfoundry/bosh-utils/main +) diff --git a/bin/build-linux-amd64 b/bin/build-linux-amd64 index dcf747f8..374698bd 100755 --- a/bin/build-linux-amd64 +++ b/bin/build-linux-amd64 @@ -1,9 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash +set -eu -o pipefail -set -e +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" export GOARCH=amd64 export GOOS=linux -export GOTOOLDIR=$(go env GOROOT)/pkg/linux_amd64 -$(dirname $0)/build +"${ROOT_DIR}/bin/build" diff --git a/bin/test b/bin/test deleted file mode 100755 index 00a36e19..00000000 --- a/bin/test +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -e - -bin=$(dirname $0) - -echo "Formatting packages..." -go fmt $(go list github.com/cloudfoundry/bosh-utils/... | grep -v vendor) - -$bin/test-unit diff --git a/bin/test-unit b/bin/test-unit index 0837e9de..1fe13d79 100755 --- a/bin/test-unit +++ b/bin/test-unit @@ -1,7 +1,10 @@ -#!/bin/bash +#!/usr/bin/env bash +set -eu -o pipefail -set -e -bin=$(dirname $0) +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" echo -e "\n Testing packages..." -go run github.com/onsi/ginkgo/v2/ginkgo run -p -r --race --trace --keep-going "${@}" +( + cd "${ROOT_DIR}" + go run github.com/onsi/ginkgo/v2/ginkgo run -p -r --race --trace --keep-going ${1+"${@}"} +) From ad426fd1a03b02d4943f2ef8830ddebc25b586b5 Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 14 Aug 2026 15:54:38 -0700 Subject: [PATCH 2/6] ci: introduce lint task - adds `bin/lint` --- bin/lint | 23 +++++++++++++++++++++++ ci/pipeline.yml | 9 +++++++++ ci/tasks/lint.sh | 6 ++++++ ci/tasks/lint.yml | 8 ++++++++ 4 files changed, 46 insertions(+) create mode 100755 bin/lint create mode 100755 ci/tasks/lint.sh create mode 100644 ci/tasks/lint.yml diff --git a/bin/lint b/bin/lint new file mode 100755 index 00000000..3a6884ed --- /dev/null +++ b/bin/lint @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" + +go_bin_path="$(go env GOBIN)" +export PATH=${go_bin_path}:${PATH} +( + cd "${ROOT_DIR}" + if ! command -v golangci-lint &> /dev/null; then + echo "Installing golangci-lint@latest..." + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + fi + + golangci-lint version + + linted_os_list=(linux windows) + + for os in "${linted_os_list[@]}"; do + echo "lint-ing with GOOS=${os}..." + GOOS="${os}" golangci-lint run ./... + done +) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 066c42a1..ef2add58 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -42,7 +42,16 @@ jobs: GOOS_LIST: linux,windows GOPROXY: ((repository_mirrors.goproxy)) GOSUMDB: ((repository_mirrors.gosumdb)) + BUMP_TEST_DEPS: linux,windows - in_parallel: + - task: lint + input_mapping: + bosh-utils: bumped-bosh-utils + file: bosh-utils/ci/tasks/lint.yml + image: bosh-utils-registry-image + params: + GOPROXY: ((repository_mirrors.goproxy)) + GOSUMDB: ((repository_mirrors.gosumdb)) - task: test-unit input_mapping: bosh-utils: bumped-bosh-utils diff --git a/ci/tasks/lint.sh b/ci/tasks/lint.sh new file mode 100755 index 00000000..b9f7ae08 --- /dev/null +++ b/ci/tasks/lint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +cd bosh-utils +bin/lint diff --git a/ci/tasks/lint.yml b/ci/tasks/lint.yml new file mode 100644 index 00000000..b502abc5 --- /dev/null +++ b/ci/tasks/lint.yml @@ -0,0 +1,8 @@ +--- +platform: linux + +inputs: +- name: bosh-utils + +run: + path: bosh-utils/ci/tasks/lint.sh From ae5a1dc3cb2007023494434341e25873058b1cf0 Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 14 Aug 2026 15:55:56 -0700 Subject: [PATCH 3/6] Use `tool` directive instead of tools module pattern --- go.mod | 2 ++ tools/tools.go | 11 ----------- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 tools/tools.go diff --git a/go.mod b/go.mod index c7f2c5f5..033521a5 100644 --- a/go.mod +++ b/go.mod @@ -37,3 +37,5 @@ require ( google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +tool github.com/maxbrunsfeld/counterfeiter/v6 diff --git a/tools/tools.go b/tools/tools.go deleted file mode 100644 index e6fa2b8f..00000000 --- a/tools/tools.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build tools -// +build tools - -package tools - -import ( - _ "github.com/maxbrunsfeld/counterfeiter/v6" -) - -// This file imports packages that are used when running go generate, or used -// during the development process but not otherwise depended on by built code. From b6cf51b718d2bfebf3eae8214301c4e588b65eab Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 17 Aug 2026 12:05:32 -0700 Subject: [PATCH 4/6] Cleanup async logger spec - remove dot-import - rename package-shadowning vars --- logger/async_test.go | 98 +++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/logger/async_test.go b/logger/async_test.go index 05f5b2b9..e96e102a 100644 --- a/logger/async_test.go +++ b/logger/async_test.go @@ -9,7 +9,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/cloudfoundry/bosh-utils/logger" + "github.com/cloudfoundry/bosh-utils/logger" ) type intervalWriter struct { @@ -52,33 +52,27 @@ func (w *blockingWriter) String() string { } var _ = Describe("Logger", func() { - var ( - outBuf = new(bytes.Buffer) - ) - BeforeEach(func() { - outBuf.Reset() - }) - Describe("Async Logger", func() { It("logs the formatted message to Logger.err at the debug level", func() { - logger := NewAsyncWriterLogger(LevelDebug, outBuf) - logger.Debug("TAG", "some %s info to log", "awesome") - logger.Flush() + out := new(bytes.Buffer) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) + asyncWriterLogger.Debug("TAG", "some %s info to log", "awesome") + asyncWriterLogger.Flush() expectedContent := expectedLogFormat("TAG", "DEBUG - some awesome info to log") - Expect(outBuf).To(MatchRegexp(expectedContent)) + Expect(out).To(MatchRegexp(expectedContent)) }) It("does not block when its writer is blocked", func() { out := new(blockingWriter) - logger := NewAsyncWriterLogger(LevelDebug, out) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) out.Lock() ch := make(chan struct{}, 1) go func() { - for i := 0; i < 10; i++ { - logger.Info("TAG", "Make sure we are not just buffering bytes: %s", strings.Repeat("A", 4096)) - logger.Error("TAG", "Make sure we are not just buffering bytes: %s", strings.Repeat("A", 4096)) + for range 10 { + asyncWriterLogger.Info("TAG", "Make sure we are not just buffering bytes: %s", strings.Repeat("A", 4096)) + asyncWriterLogger.Error("TAG", "Make sure we are not just buffering bytes: %s", strings.Repeat("A", 4096)) } ch <- struct{}{} }() @@ -90,46 +84,46 @@ var _ = Describe("Logger", func() { const s0 = "ABCDEFGHIJ" const s1 = "abcdefghij" - outBuf := new(blockingWriter) - logger := NewAsyncWriterLogger(LevelDebug, outBuf) + out := new(blockingWriter) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) - outBuf.Lock() - logger.Debug("TAG", s0) - logger.Debug("TAG", s1) - outBuf.Unlock() + out.Lock() + asyncWriterLogger.Debug("TAG", s0) + asyncWriterLogger.Debug("TAG", s1) + out.Unlock() - Expect(logger.Flush()).To(Succeed()) + Expect(asyncWriterLogger.Flush()).To(Succeed()) - lines := strings.Split(strings.TrimSpace(outBuf.buf.String()), "\n") + lines := strings.Split(strings.TrimSpace(out.buf.String()), "\n") Expect(lines).To(HaveLen(2)) Expect(lines[0]).To(HaveSuffix(s0)) Expect(lines[1]).To(HaveSuffix(s1)) }) It("continuously flushes queued log messages", func() { - outBuf := new(blockingWriter) - logger := NewAsyncWriterLogger(LevelDebug, outBuf) + out := new(blockingWriter) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) - outBuf.Lock() - for i := 0; i < 10; i++ { - logger.Debug("TAG", "Queued log message") + out.Lock() + for range 10 { + asyncWriterLogger.Debug("TAG", "Queued log message") } - Expect(outBuf.buf.Len()).To(Equal(0)) - outBuf.Unlock() - Eventually(outBuf.Len).ShouldNot(Equal(0)) + Expect(out.buf.Len()).To(Equal(0)) + out.Unlock() + Eventually(out.Len).ShouldNot(Equal(0)) }) It("flushes with a timeout", func() { - outBuf := new(blockingWriter) - logger := NewAsyncWriterLogger(LevelDebug, outBuf) - logger.Debug("TAG", "something") + out := new(blockingWriter) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) + asyncWriterLogger.Debug("TAG", "something") - outBuf.Lock() - Expect(logger.FlushTimeout(time.Millisecond * 10)).ToNot(Succeed()) + out.Lock() + Expect(asyncWriterLogger.FlushTimeout(time.Millisecond * 10)).ToNot(Succeed()) - outBuf.Unlock() - Expect(logger.FlushTimeout(time.Millisecond * 10)).To(Succeed()) - Expect(strings.TrimSpace(outBuf.buf.String())).To(HaveSuffix("something")) + out.Unlock() + Expect(asyncWriterLogger.FlushTimeout(time.Millisecond * 10)).To(Succeed()) + Expect(strings.TrimSpace(out.buf.String())).To(HaveSuffix("something")) }) It("flush doesn't block writes", func() { @@ -140,21 +134,21 @@ var _ = Describe("Logger", func() { ) out := &intervalWriter{dur: WriteInterval} - logger := NewAsyncWriterLogger(LevelDebug, out) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) // add some messages to the queue out.Lock() - for i := 0; i < MessageCount; i++ { - logger.Debug("NEW", "message") + for range MessageCount { + asyncWriterLogger.Debug("NEW", "message") } out.Unlock() - go logger.Flush() + go asyncWriterLogger.Flush() ch := make(chan struct{}, 1) go func() { - for i := 0; i < MessageCount; i++ { - logger.Debug("NEW", "message") + for range MessageCount { + asyncWriterLogger.Debug("NEW", "message") } ch <- struct{}{} }() @@ -169,12 +163,12 @@ var _ = Describe("Logger", func() { ) out := &intervalWriter{dur: WriteInterval} - logger := NewAsyncWriterLogger(LevelDebug, out) + asyncWriterLogger := logger.NewAsyncWriterLogger(logger.LevelDebug, out) // add some messages to the queue out.Lock() - for i := 0; i < MessageCount; i++ { - logger.Debug("QUEUED", "queued") + for range MessageCount { + asyncWriterLogger.Debug("QUEUED", "queued") } out.Unlock() @@ -183,13 +177,13 @@ var _ = Describe("Logger", func() { defer tick.Stop() go func() { for range tick.C { - logger.Debug("NEW", "new") + asyncWriterLogger.Debug("NEW", "new") } }() ch := make(chan struct{}, 1) go func() { - logger.Flush() + asyncWriterLogger.Flush() ch <- struct{}{} }() @@ -200,7 +194,7 @@ var _ = Describe("Logger", func() { It("prints the correct prefix during concurrent writes", func() { ch := make(chan struct{}, 1) go func() { - testConcurrentPrefix(NewAsyncWriterLogger) + testConcurrentPrefix(logger.NewAsyncWriterLogger) ch <- struct{}{} }() Eventually(ch, time.Second*5).Should(Receive()) From fa6ef98e3f62f9005e8ecfca3e78977ee9d70663 Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 17 Aug 2026 11:14:15 -0700 Subject: [PATCH 5/6] Replace call to `nice` with bespoke binary This change, and the additiona of a darwin-specific `getProcessPriority()` allows tests to be run on macOS. --- .../priority/priority_unix.go | 27 ++++++++ .../priority/priority_windows.go | 35 ++++++++++ system/exec_cmd_runner_test.go | 65 ++++++------------- system/process_priority_darwin.go | 12 ++++ system/process_priority_linux.go | 18 +++++ system/process_priority_unix.go | 16 +---- system/system_suite_test.go | 37 +++++++---- 7 files changed, 137 insertions(+), 73 deletions(-) create mode 100644 system/exec_cmd_runner_fixtures/priority/priority_unix.go create mode 100644 system/exec_cmd_runner_fixtures/priority/priority_windows.go create mode 100644 system/process_priority_darwin.go create mode 100644 system/process_priority_linux.go diff --git a/system/exec_cmd_runner_fixtures/priority/priority_unix.go b/system/exec_cmd_runner_fixtures/priority/priority_unix.go new file mode 100644 index 00000000..2a9fd94e --- /dev/null +++ b/system/exec_cmd_runner_fixtures/priority/priority_unix.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "syscall" + "time" +) + +func main() { + time.Sleep(100 * time.Millisecond) + + pid := os.Getpid() + niceValue, err := syscall.Getpriority(syscall.PRIO_PROCESS, pid) + if err != nil { + fmt.Printf("error getting priority: %s\n", err) + os.Exit(1) + } + + // Linux: convert knice => unice see https://linux.die.net/man/2/getpriority + if runtime.GOOS == "linux" { + fmt.Printf("%d\n", (niceValue-20)*-1) + } else { + fmt.Printf("%d\n", niceValue) + } +} diff --git a/system/exec_cmd_runner_fixtures/priority/priority_windows.go b/system/exec_cmd_runner_fixtures/priority/priority_windows.go new file mode 100644 index 00000000..140147e5 --- /dev/null +++ b/system/exec_cmd_runner_fixtures/priority/priority_windows.go @@ -0,0 +1,35 @@ +//go:build windows + +package main + +import ( + "fmt" + "os" + "time" + + "golang.org/x/sys/windows" +) + +func main() { + time.Sleep(100 * time.Millisecond) + + // Open a handle to the current process with permission to query its info + pid := uint32(os.Getpid()) + handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, pid) + if err != nil { + fmt.Printf("error opening process: %s\n", err) + os.Exit(1) + } + // Ensure the handle is closed when we're done + defer windows.CloseHandle(handle) //nolint:errcheck + + // Get the priority class + priorityClass, err := windows.GetPriorityClass(handle) + if err != nil { + fmt.Printf("error getting priority: %s\n", err) + os.Exit(1) + } + + // Prints the raw Windows priority class integer + fmt.Printf("%d\n", priorityClass) +} diff --git a/system/exec_cmd_runner_test.go b/system/exec_cmd_runner_test.go index 0bb4cc25..517ce3bd 100644 --- a/system/exec_cmd_runner_test.go +++ b/system/exec_cmd_runner_test.go @@ -3,10 +3,9 @@ package system_test import ( "fmt" "os" - "os/exec" "runtime" - "strconv" "strings" + "syscall" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -111,6 +110,13 @@ func parseEnvFields(envDump string, convertKeysToUpper bool) map[string]string { return fields } +func normalizeNiceLevel(kernelNice int) int { + if runtime.GOOS == "linux" { + return (kernelNice - 20) * -1 + } + return kernelNice +} + var _ = Describe("execCmdRunner", func() { var ( runner CmdRunner @@ -184,62 +190,31 @@ var _ = Describe("execCmdRunner", func() { }) It("runs a command nicer than itself", func() { - // Write script that echos its nice value - // Sleep briefly to ensure parent has time to set priority - script := "#!/bin/bash\nsleep 0.1\nnice\n" - tmpFile, err := os.CreateTemp("", "tmp-script-*.sh") - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(tmpFile.Name()) - _, err = tmpFile.WriteString(script) - Expect(err).ToNot(HaveOccurred()) - err = tmpFile.Close() + // Calculate what we expect the priority to be + parentPid := os.Getpid() + parentKnice, err := syscall.Getpriority(syscall.PRIO_PROCESS, parentPid) Expect(err).ToNot(HaveOccurred()) - niceOut, err := exec.Command("nice").Output() + stdout, _, _, err := runner.RunComplexCommand(Command{Name: priorityPath, SpawnWithLowerPriority: true}) Expect(err).ToNot(HaveOccurred()) - parentNice, err := strconv.Atoi(strings.TrimSpace(string(niceOut))) - Expect(err).ToNot(HaveOccurred()) - expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) - // Run script with SpawnWithLowerPriority - cmd := Command{ - Name: "bash", - Args: []string{tmpFile.Name()}, - SpawnWithLowerPriority: true, - } - stdout, _, _, err := runner.RunComplexCommand(cmd) - - Expect(err).ToNot(HaveOccurred()) + parentNice := normalizeNiceLevel(parentKnice) + expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) Expect(stdout).To(Equal(expectedOutput)) }) It("runs an async command nicer than itself", func() { - // Write script that echos its nice value - script := "#!/bin/bash\nsleep 0.1\nnice\n" - tmpFile, err := os.CreateTemp("", "tmp-script-*.sh") - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(tmpFile.Name()) - _, err = tmpFile.WriteString(script) - Expect(err).ToNot(HaveOccurred()) - err = tmpFile.Close() + parentPid := os.Getpid() + parentKnice, err := syscall.Getpriority(syscall.PRIO_PROCESS, parentPid) Expect(err).ToNot(HaveOccurred()) - niceOut, err := exec.Command("nice").Output() - Expect(err).ToNot(HaveOccurred()) - parentNice, err := strconv.Atoi(strings.TrimSpace(string(niceOut))) + process, err := runner.RunComplexCommandAsync(Command{Name: priorityPath, SpawnWithLowerPriority: true}) Expect(err).ToNot(HaveOccurred()) - expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) - - cmd := Command{ - Name: "bash", - Args: []string{tmpFile.Name()}, - SpawnWithLowerPriority: true, - } - process, err := runner.RunComplexCommandAsync(cmd) - Expect(err).ToNot(HaveOccurred()) - result := <-process.Wait() Expect(result.Error).ToNot(HaveOccurred()) + + parentNice := normalizeNiceLevel(parentKnice) + expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) Expect(result.Stdout).To(Equal(expectedOutput)) }) }) diff --git a/system/process_priority_darwin.go b/system/process_priority_darwin.go new file mode 100644 index 00000000..6e087413 --- /dev/null +++ b/system/process_priority_darwin.go @@ -0,0 +1,12 @@ +//go:build darwin + +package system + +import ( + "syscall" +) + +// getProcessPriority returns the nice value of the process with the given pid. +func getProcessPriority(pid int) (int, error) { + return syscall.Getpriority(syscall.PRIO_PROCESS, pid) +} diff --git a/system/process_priority_linux.go b/system/process_priority_linux.go new file mode 100644 index 00000000..e8a7cd7d --- /dev/null +++ b/system/process_priority_linux.go @@ -0,0 +1,18 @@ +package system + +import ( + "syscall" +) + +// getProcessPriority returns the nice value of the process with the given pid. +func getProcessPriority(pid int) (int, error) { + knice, err := syscall.Getpriority(syscall.PRIO_PROCESS, pid) + if err != nil { + return 0, err + } + + // Linux: convert syscall.Getpriority()'s "kernel nice" to "user nice" + // => unice = 20 - knice + // See https://linux.die.net/man/2/getpriority + return ((knice - 20) * -1), nil +} diff --git a/system/process_priority_unix.go b/system/process_priority_unix.go index 05bcbd85..fc24b454 100644 --- a/system/process_priority_unix.go +++ b/system/process_priority_unix.go @@ -1,27 +1,13 @@ -//go:build !windows +package system // Inspired by github.com/hekmon/processpriority (MIT, Copyright 2024 Edouard Hur). // Reimplemented inline to avoid the external dependency. -package system - import ( "os" "syscall" ) -// getProcessPriority returns the nice value of the process with the given pid. -func getProcessPriority(pid int) (int, error) { - // syscall.Getpriority returns the "kernel nice" (20 - nice), so we convert. - // See https://linux.die.net/man/2/getpriority - knice, err := syscall.Getpriority(syscall.PRIO_PROCESS, pid) - if err != nil { - return 0, err - } - nice := (knice - 20) * -1 - return nice, nil -} - // setProcessPriority sets the nice value of the process with the given pid. func setProcessPriority(pid int, nice int) error { return syscall.Setpriority(syscall.PRIO_PROCESS, pid, nice) diff --git a/system/system_suite_test.go b/system/system_suite_test.go index 9ffbdc3d..a26daa0d 100644 --- a/system/system_suite_test.go +++ b/system/system_suite_test.go @@ -3,6 +3,7 @@ package system_test import ( "bytes" "math/rand" + "os" "path/filepath" "runtime" "strings" @@ -23,37 +24,47 @@ func TestSystem(t *testing.T) { var catPath string var falsePath string var windowsExePath string +var priorityPath string var _ = SynchronizedBeforeSuite(func() []byte { var paths []string - catBin, err := gexec.Build("exec_cmd_runner_fixtures/cat/cat.go") - Expect(err).ToNot(HaveOccurred()) - paths = append(paths, catBin) - - falseBin, err := gexec.Build("exec_cmd_runner_fixtures/false/false.go") - Expect(err).ToNot(HaveOccurred()) - paths = append(paths, falseBin) + paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/cat/")) + paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/false/")) + paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/windows_exe/")) + paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/priority")) - windowsExeBin, err := gexec.Build("exec_cmd_runner_fixtures/windows_exe/windows_exe.go") - Expect(err).ToNot(HaveOccurred()) - paths = append(paths, windowsExeBin) - - Expect(paths).To(HaveLen(3)) + Expect(paths).To(HaveLen(4)) return []byte(strings.Join(paths, "|")) }, func(data []byte) { paths := strings.Split(string(data), "|") - Expect(paths).To(HaveLen(3)) + Expect(paths).To(HaveLen(4)) catPath = paths[0] falsePath = paths[1] windowsExePath = paths[2] + priorityPath = paths[3] }) var _ = SynchronizedAfterSuite(func() {}, func() { gexec.CleanupBuildArtifacts() }) +func buildFixtureCmd(fixtureSrcPath string) string { + ex, err := os.Executable() + if err != nil { + panic(err) + } + workingDir := filepath.Dir(ex) + + Expect(os.Chdir(fixtureSrcPath)).To(Succeed()) + fixtureBinPath, err := gexec.Build("./...") + Expect(err).ToNot(HaveOccurred()) + Expect(os.Chdir(workingDir)).To(Succeed()) + + return fixtureBinPath +} + func randSeq(n int) string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, n) From 0252e6454a878851aa2a5bf39e19e96a751e3139 Mon Sep 17 00:00:00 2001 From: aram price Date: Mon, 17 Aug 2026 11:52:07 -0700 Subject: [PATCH 6/6] Split tests into os-specific files This change allows linters for supported GOOS values to pass and reduces the number of programmatically skipped tests. --- .github/workflows/go.yml | 4 +- main/verify_multidigest_test.go | 3 +- .../priority/priority_unix.go | 2 + .../priority/priority_windows.go | 23 +- system/exec_cmd_runner_unix_test.go | 350 ++++++++++++++++++ ...est.go => exec_cmd_runner_windows_test.go} | 209 +---------- system/os_file_system_test.go | 139 +------ system/os_file_system_unix_test.go | 109 +++++- system/os_file_system_windows_test.go | 170 +++++++-- system/process_priority_darwin.go | 12 - system/process_priority_linux.go | 18 - system/process_priority_unix.go | 20 + system/system_suite_test.go | 42 ++- 13 files changed, 700 insertions(+), 401 deletions(-) create mode 100644 system/exec_cmd_runner_unix_test.go rename system/{exec_cmd_runner_test.go => exec_cmd_runner_windows_test.go} (65%) delete mode 100644 system/process_priority_darwin.go delete mode 100644 system/process_priority_linux.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c4dd833a..62043068 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,6 +19,6 @@ jobs: - name: Unit Tests (Windows) if: runner.os == 'Windows' run: ./bin/test-unit.ps1 - - name: Unit Tests (Linux) - if: runner.os == 'Linux' + - name: Unit Tests (Linux/macOS) + if: runner.os != 'Windows' run: ./bin/test-unit diff --git a/main/verify_multidigest_test.go b/main/verify_multidigest_test.go index 063f5d7a..00a6dadb 100644 --- a/main/verify_multidigest_test.go +++ b/main/verify_multidigest_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -31,7 +32,7 @@ var _ = Describe("VerifyMultidigest", func() { It("has a version flag", func() { session, err := runVerifyMultidigest("--version") Expect(err).ToNot(HaveOccurred()) - Eventually(session).Should(gexec.Exit(0)) + Eventually(session).WithTimeout(3 * time.Second).Should(gexec.Exit(0)) Eventually(session.Out).Should(gbytes.Say("version \\[DEV BUILD\\]")) }) }) diff --git a/system/exec_cmd_runner_fixtures/priority/priority_unix.go b/system/exec_cmd_runner_fixtures/priority/priority_unix.go index 2a9fd94e..01ca24d5 100644 --- a/system/exec_cmd_runner_fixtures/priority/priority_unix.go +++ b/system/exec_cmd_runner_fixtures/priority/priority_unix.go @@ -1,3 +1,5 @@ +//go:build !windows + package main import ( diff --git a/system/exec_cmd_runner_fixtures/priority/priority_windows.go b/system/exec_cmd_runner_fixtures/priority/priority_windows.go index 140147e5..b5ed58d3 100644 --- a/system/exec_cmd_runner_fixtures/priority/priority_windows.go +++ b/system/exec_cmd_runner_fixtures/priority/priority_windows.go @@ -30,6 +30,25 @@ func main() { os.Exit(1) } - // Prints the raw Windows priority class integer - fmt.Printf("%d\n", priorityClass) + var priorityClassName string + switch priorityClass { + case windows.NORMAL_PRIORITY_CLASS: + priorityClassName = "NORMAL_PRIORITY_CLASS" + case windows.IDLE_PRIORITY_CLASS: + priorityClassName = "IDLE_PRIORITY_CLASS" + case windows.HIGH_PRIORITY_CLASS: + priorityClassName = "HIGH_PRIORITY_CLASS" + case windows.REALTIME_PRIORITY_CLASS: + priorityClassName = "REALTIME_PRIORITY_CLASS" + case windows.BELOW_NORMAL_PRIORITY_CLASS: + priorityClassName = "BELOW_NORMAL_PRIORITY_CLASS" + case windows.ABOVE_NORMAL_PRIORITY_CLASS: + priorityClassName = "ABOVE_NORMAL_PRIORITY_CLASS" + default: + // Fallback for any unknown values + priorityClassName = fmt.Sprintf("UNKNOWN_PRIORITY_CLASS (%d)", priorityClass) + } + + // Prints the Windows priority class name + fmt.Printf("%s\r\n", priorityClassName) } diff --git a/system/exec_cmd_runner_unix_test.go b/system/exec_cmd_runner_unix_test.go new file mode 100644 index 00000000..b0452f45 --- /dev/null +++ b/system/exec_cmd_runner_unix_test.go @@ -0,0 +1,350 @@ +//go:build !windows + +package system_test + +import ( + "fmt" + "os" + "runtime" + "strings" + "syscall" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + boshlog "github.com/cloudfoundry/bosh-utils/logger" + "github.com/cloudfoundry/bosh-utils/logger/loggerfakes" + . "github.com/cloudfoundry/bosh-utils/system" + fakesys "github.com/cloudfoundry/bosh-utils/system/fakes" +) + +const ErrExitCode = 14 + +func unixCommand(cmdName string) Command { + return map[string]Command{ + "pwd": { + Name: "bash", + Args: []string{"-c", "echo $PWD"}, + WorkingDir: `/tmp`, + }, + "stderr": { + Name: "bash", + Args: []string{"-c", "echo error-output >&2"}, + }, + "exit": { + Name: "bash", + Args: []string{"-c", fmt.Sprintf("exit %d", ErrExitCode)}, + }, + "ls": { + Name: "ls", + Args: []string{"-l"}, + WorkingDir: ".", + }, + "env": { + Name: "env", + Env: map[string]string{ + "FOO": "BAR", + }, + }, + "echo": { + Name: "echo", + Args: []string{"Hello World!"}, + }, + }[cmdName] +} + +func normalizeNiceLevel(kernelNice int) int { + if runtime.GOOS == "linux" { + return (kernelNice - 20) * -1 + } + return kernelNice +} + +var _ = Describe("execCmdRunner", func() { + var ( + runner CmdRunner + ) + + BeforeEach(func() { + runner = NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) + }) + + Describe("RunComplexCommand", func() { + It("run complex command with working directory", func() { + cmd := unixCommand("ls") + stdout, stderr, status, err := runner.RunComplexCommand(cmd) + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(ContainSubstring("exec_cmd_runner_fixtures")) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + }) + + It("run complex command with env", func() { + cmd := unixCommand("env") + stdout, stderr, status, err := runner.RunComplexCommand(cmd) + Expect(err).ToNot(HaveOccurred()) + + envVars := parseEnvFields(stdout, true) + Expect(envVars).To(HaveKeyWithValue("FOO", "BAR")) + Expect(envVars).To(HaveKey("PATH")) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + }) + + It("uses the env vars specified in the Command", func() { + GinkgoT().Setenv("_FOO", "BAR") + + cmd := unixCommand("env") + cmd.Env = map[string]string{ + "_FOO": "BAZZZ", + } + stdout, _, _, err := runner.RunComplexCommand(cmd) + Expect(err).ToNot(HaveOccurred()) + + envVars := parseEnvFields(stdout, false) + Expect(envVars).To(HaveKeyWithValue("_FOO", "BAZZZ")) + }) + + Context("unix specific behavior", func() { + It("performs a case-sensitive comparison of env vars when on *Nix", func() { + GinkgoT().Setenv("_FOO", "BAR") + + cmd := unixCommand("env") + cmd.Env = map[string]string{ + "_foo": "BAZZZ", + "ABC": "XYZ", + "abc": "xyz", + } + stdout, _, _, err := runner.RunComplexCommand(cmd) + Expect(err).ToNot(HaveOccurred()) + + envVars := parseEnvFields(stdout, false) + Expect(envVars).To(HaveKeyWithValue("_FOO", "BAR")) + Expect(envVars).To(HaveKeyWithValue("_foo", "BAZZZ")) + Expect(err).ToNot(HaveOccurred()) + Expect(envVars).To(HaveKeyWithValue("ABC", "XYZ")) + Expect(envVars).To(HaveKeyWithValue("abc", "xyz")) + }) + + It("runs a command nicer than itself", func() { + // Calculate what we expect the priority to be + parentPid := os.Getpid() + parentKnice, err := syscall.Getpriority(syscall.PRIO_PROCESS, parentPid) + Expect(err).ToNot(HaveOccurred()) + + stdout, _, _, err := runner.RunComplexCommand(Command{Name: priorityPath, SpawnWithLowerPriority: true}) + Expect(err).ToNot(HaveOccurred()) + + parentNice := normalizeNiceLevel(parentKnice) + expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) + Expect(stdout).To(Equal(expectedOutput)) + }) + + It("runs an async command nicer than itself", func() { + parentPid := os.Getpid() + parentKnice, err := syscall.Getpriority(syscall.PRIO_PROCESS, parentPid) + Expect(err).ToNot(HaveOccurred()) + + process, err := runner.RunComplexCommandAsync(Command{Name: priorityPath, SpawnWithLowerPriority: true}) + Expect(err).ToNot(HaveOccurred()) + result := <-process.Wait() + Expect(result.Error).ToNot(HaveOccurred()) + + parentNice := normalizeNiceLevel(parentKnice) + expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) + Expect(result.Stdout).To(Equal(expectedOutput)) + }) + }) + + It("run complex command with stdin", func() { + input := "This is STDIN\nWith another line." + cmd := Command{ + Name: catPath, + Stdin: strings.NewReader(input), + } + stdout, stderr, status, err := runner.RunComplexCommand(cmd) + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(Equal(input)) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + }) + + It("prints stdout/stderr to provided I/O object", func() { + fs := fakesys.NewFakeFileSystem() + stdoutFile, err := fs.OpenFile("/fake-stdout-path", os.O_RDWR, os.FileMode(0644)) + Expect(err).ToNot(HaveOccurred()) + + stderrFile, err := fs.OpenFile("/fake-stderr-path", os.O_RDWR, os.FileMode(0644)) + Expect(err).ToNot(HaveOccurred()) + + cmd := Command{ + Name: catPath, + Args: []string{"-stdout", "fake-out", "-stderr", "fake-err"}, + Stdout: stdoutFile, + Stderr: stderrFile, + } + + stdout, stderr, status, err := runner.RunComplexCommand(cmd) + Expect(err).ToNot(HaveOccurred()) + + Expect(stdout).To(BeEmpty()) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + + stdoutContents := make([]byte, 1024) + _, err = stdoutFile.Read(stdoutContents) + Expect(err).ToNot(HaveOccurred()) + Expect(string(stdoutContents)).To(ContainSubstring("fake-out")) + + stderrContents := make([]byte, 1024) + _, err = stderrFile.Read(stderrContents) + Expect(err).ToNot(HaveOccurred()) + Expect(string(stderrContents)).To(ContainSubstring("fake-err")) + }) + }) + + Describe("RunComplexCommandAsync", func() { + It("populates stdout and stderr", func() { + cmd := unixCommand("ls") + process, err := runner.RunComplexCommandAsync(cmd) + Expect(err).ToNot(HaveOccurred()) + + result := <-process.Wait() + Expect(result.Error).ToNot(HaveOccurred()) + Expect(result.ExitStatus).To(Equal(0)) + }) + + It("populates stdout and stderr", func() { + cmd := Command{ + Name: catPath, + Args: []string{"-stdout", "STDOUT", "-stderr", "STDERR"}, + } + process, err := runner.RunComplexCommandAsync(cmd) + Expect(err).ToNot(HaveOccurred()) + + result := <-process.Wait() + Expect(result.Error).ToNot(HaveOccurred()) + Expect(result.Stdout).To(Equal("STDOUT\n")) + Expect(result.Stderr).To(Equal("STDERR\n")) + }) + + It("returns error and sets status to exit status of command if it exits with non-0 status", func() { + cmd := unixCommand("exit") + process, err := runner.RunComplexCommandAsync(cmd) + Expect(err).ToNot(HaveOccurred()) + + result := <-process.Wait() + Expect(result.Error).To(HaveOccurred()) + Expect(result.ExitStatus).To(Equal(ErrExitCode)) + }) + + It("allows setting custom env variable in addition to inheriting process env variables", func() { + cmd := unixCommand("env") + + process, err := runner.RunComplexCommandAsync(cmd) + Expect(err).ToNot(HaveOccurred()) + + result := <-process.Wait() + Expect(result.Error).ToNot(HaveOccurred()) + Expect(result.Stdout).To(ContainSubstring("FOO=BAR")) + Expect(result.Stdout).To(ContainSubstring("PATH=")) + }) + + It("changes working dir", func() { + cmd := unixCommand("pwd") + process, err := runner.RunComplexCommandAsync(cmd) + Expect(err).ToNot(HaveOccurred()) + + result := <-process.Wait() + Expect(result.Error).ToNot(HaveOccurred()) + Expect(result.Stdout).To(ContainSubstring(cmd.WorkingDir)) + }) + }) + + Describe("RunCommand", func() { + It("run command", func() { + cmd := unixCommand("echo") + stdout, stderr, status, err := runner.RunCommand(cmd.Name, cmd.Args...) + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(Equal("Hello World!\n")) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + }) + + It("run command with error output", func() { + cmd := unixCommand("stderr") + stdout, stderr, status, err := runner.RunCommand(cmd.Name, cmd.Args...) + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(BeEmpty()) + Expect(stderr).To(ContainSubstring("error-output")) + Expect(status).To(Equal(0)) + }) + + It("run command with non-0 exit status", func() { + cmd := unixCommand("exit") + stdout, stderr, status, err := runner.RunCommand(cmd.Name, cmd.Args...) + Expect(err).To(HaveOccurred()) + Expect(stdout).To(BeEmpty()) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(ErrExitCode)) + }) + + It("run command with error", func() { + stdout, stderr, status, err := runner.RunCommand(falsePath) + Expect(err).To(HaveOccurred()) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + Expect(status).To(Equal(1)) + }) + + It("run command with error with args", func() { + stdout, stderr, status, err := runner.RunCommand(falsePath, "second arg") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf("Running command: '%s second arg', stdout: '', stderr: '': exit status 1", falsePath))) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + Expect(status).To(Equal(1)) + }) + + It("run command with cmd not found", func() { + stdout, stderr, status, err := runner.RunCommand("something that does not exist") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Or(ContainSubstring("not found"), ContainSubstring("ObjectNotFound"))) + Expect(stderr).To(BeEmpty()) + Expect(stdout).To(BeEmpty()) + Expect(status).ToNot(Equal(0)) + }) + }) + + Describe("RunCommandWithInput", func() { + It("run command with input", func() { + stdout, stderr, status, err := runner.RunCommandWithInput("foo\nbar\nbaz", catPath) + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(Equal("foo\nbar\nbaz")) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + }) + }) + + Describe("RunCommandQuietly", func() { + It("run command with input", func() { + logger := &loggerfakes.FakeLogger{} + runner = NewExecCmdRunner(logger) + + cmd := unixCommand("echo") + stdout, stderr, status, err := runner.RunCommandQuietly(cmd.Name, cmd.Args...) + Expect(err).ToNot(HaveOccurred()) + Expect(logger.DebugCallCount()).To(Equal(2)) + Expect(stdout).To(Equal("Hello World!\n")) + Expect(stderr).To(BeEmpty()) + Expect(status).To(Equal(0)) + }) + }) + + Describe("CommandExists", func() { + It("command exists", func() { + Expect(runner.CommandExists("env")).To(BeTrue()) + Expect(runner.CommandExists("absolutely-does-not-exist-ever-please-unicorns")).To(BeFalse()) + }) + }) +}) diff --git a/system/exec_cmd_runner_test.go b/system/exec_cmd_runner_windows_test.go similarity index 65% rename from system/exec_cmd_runner_test.go rename to system/exec_cmd_runner_windows_test.go index 517ce3bd..d1a0be48 100644 --- a/system/exec_cmd_runner_test.go +++ b/system/exec_cmd_runner_windows_test.go @@ -3,9 +3,7 @@ package system_test import ( "fmt" "os" - "runtime" "strings" - "syscall" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -18,13 +16,6 @@ import ( const ErrExitCode = 14 -func osSpecificCommand(cmdName string) Command { - if isWindows { - return windowsCommand(cmdName) - } - return unixCommand(cmdName) -} - func windowsCommand(cmdName string) Command { return map[string]Command{ "pwd": { @@ -59,64 +50,6 @@ func windowsCommand(cmdName string) Command { }[cmdName] } -func unixCommand(cmdName string) Command { - return map[string]Command{ - "pwd": { - Name: "bash", - Args: []string{"-c", "echo $PWD"}, - WorkingDir: `/tmp`, - }, - "stderr": { - Name: "bash", - Args: []string{"-c", "echo error-output >&2"}, - }, - "exit": { - Name: "bash", - Args: []string{"-c", fmt.Sprintf("exit %d", ErrExitCode)}, - }, - "ls": { - Name: "ls", - Args: []string{"-l"}, - WorkingDir: ".", - }, - "env": { - Name: "env", - Env: map[string]string{ - "FOO": "BAR", - }, - }, - "echo": { - Name: "echo", - Args: []string{"Hello World!"}, - }, - }[cmdName] -} - -func parseEnvFields(envDump string, convertKeysToUpper bool) map[string]string { - fields := make(map[string]string) - envDump = strings.ReplaceAll(envDump, "\r", "") - for _, line := range strings.Split(envDump, "\n") { - // don't split on '=' as '=' is allowed in the value on Windows - if n := strings.IndexByte(line, '='); n != -1 { - key := line[:n] // key - val := line[n+1:] // key - if convertKeysToUpper { - fields[strings.ToUpper(key)] = val - } else { - fields[key] = val - } - } - } - return fields -} - -func normalizeNiceLevel(kernelNice int) int { - if runtime.GOOS == "linux" { - return (kernelNice - 20) * -1 - } - return kernelNice -} - var _ = Describe("execCmdRunner", func() { var ( runner CmdRunner @@ -128,7 +61,7 @@ var _ = Describe("execCmdRunner", func() { Describe("RunComplexCommand", func() { It("run complex command with working directory", func() { - cmd := osSpecificCommand("ls") + cmd := windowsCommand("ls") stdout, stderr, status, err := runner.RunComplexCommand(cmd) Expect(err).ToNot(HaveOccurred()) Expect(stdout).To(ContainSubstring("exec_cmd_runner_fixtures")) @@ -137,7 +70,7 @@ var _ = Describe("execCmdRunner", func() { }) It("run complex command with env", func() { - cmd := osSpecificCommand("env") + cmd := windowsCommand("env") stdout, stderr, status, err := runner.RunComplexCommand(cmd) Expect(err).ToNot(HaveOccurred()) @@ -151,7 +84,7 @@ var _ = Describe("execCmdRunner", func() { It("uses the env vars specified in the Command", func() { GinkgoT().Setenv("_FOO", "BAR") - cmd := osSpecificCommand("env") + cmd := windowsCommand("env") cmd.Env = map[string]string{ "_FOO": "BAZZZ", } @@ -162,75 +95,12 @@ var _ = Describe("execCmdRunner", func() { Expect(envVars).To(HaveKeyWithValue("_FOO", "BAZZZ")) }) - Context("unix specific behavior", func() { - BeforeEach(func() { - if isWindows { - Skip("unix only test") - } - }) - - It("performs a case-sensitive comparison of env vars when on *Nix", func() { - GinkgoT().Setenv("_FOO", "BAR") - - cmd := osSpecificCommand("env") - cmd.Env = map[string]string{ - "_foo": "BAZZZ", - "ABC": "XYZ", - "abc": "xyz", - } - stdout, _, _, err := runner.RunComplexCommand(cmd) - Expect(err).ToNot(HaveOccurred()) - - envVars := parseEnvFields(stdout, false) - Expect(envVars).To(HaveKeyWithValue("_FOO", "BAR")) - Expect(envVars).To(HaveKeyWithValue("_foo", "BAZZZ")) - Expect(err).ToNot(HaveOccurred()) - Expect(envVars).To(HaveKeyWithValue("ABC", "XYZ")) - Expect(envVars).To(HaveKeyWithValue("abc", "xyz")) - }) - - It("runs a command nicer than itself", func() { - // Calculate what we expect the priority to be - parentPid := os.Getpid() - parentKnice, err := syscall.Getpriority(syscall.PRIO_PROCESS, parentPid) - Expect(err).ToNot(HaveOccurred()) - - stdout, _, _, err := runner.RunComplexCommand(Command{Name: priorityPath, SpawnWithLowerPriority: true}) - Expect(err).ToNot(HaveOccurred()) - - parentNice := normalizeNiceLevel(parentKnice) - expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) - Expect(stdout).To(Equal(expectedOutput)) - }) - - It("runs an async command nicer than itself", func() { - parentPid := os.Getpid() - parentKnice, err := syscall.Getpriority(syscall.PRIO_PROCESS, parentPid) - Expect(err).ToNot(HaveOccurred()) - - process, err := runner.RunComplexCommandAsync(Command{Name: priorityPath, SpawnWithLowerPriority: true}) - Expect(err).ToNot(HaveOccurred()) - result := <-process.Wait() - Expect(result.Error).ToNot(HaveOccurred()) - - parentNice := normalizeNiceLevel(parentKnice) - expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19)) - Expect(result.Stdout).To(Equal(expectedOutput)) - }) - }) - Context("windows specific behavior", func() { - BeforeEach(func() { - if !isWindows { - Skip("Windows only test") - } - }) - setupWindowsEnvTest := func(cmdVars map[string]string) (map[string]string, error) { os.Setenv("_FOO", "BAR") //nolint:errcheck defer os.Unsetenv("_FOO") - cmd := osSpecificCommand("env") + cmd := windowsCommand("env") cmd.Env = cmdVars stdout, _, _, err := runner.RunComplexCommand(cmd) if err != nil { @@ -286,56 +156,21 @@ var _ = Describe("execCmdRunner", func() { }) It("runs a command nicer than itself", func() { - // Write script that echos its priority class - // Sleep briefly to ensure parent has time to set priority - script := "Start-Sleep -Milliseconds 100\n$proc = Get-Process -Id $PID\nWrite-Output $proc.PriorityClass" - - tmpFile, err := os.CreateTemp("", "tmp-script-*.ps1") - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(tmpFile.Name()) - _, err = tmpFile.WriteString(script) - Expect(err).ToNot(HaveOccurred()) - err = tmpFile.Close() - Expect(err).ToNot(HaveOccurred()) - err = os.Chmod(tmpFile.Name(), 0700) - Expect(err).ToNot(HaveOccurred()) - - // Run script with SpawnWithLowerPriority - cmd := Command{ - Name: "powershell", - Args: []string{"-ExecutionPolicy", "Bypass", "-File", tmpFile.Name()}, - SpawnWithLowerPriority: true, - } + cmd := Command{Name: priorityPath, SpawnWithLowerPriority: true} stdout, _, _, err := runner.RunComplexCommand(cmd) Expect(err).ToNot(HaveOccurred()) - Expect(stdout).To(Equal("BelowNormal\r\n")) + Expect(stdout).To(Equal("BELOW_NORMAL_PRIORITY_CLASS\r\n")) }) It("runs an async command nicer than itself", func() { - script := "Start-Sleep -Milliseconds 100\n$proc = Get-Process -Id $PID\nWrite-Output $proc.PriorityClass" - - tmpFile, err := os.CreateTemp("", "tmp-script-*.ps1") - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(tmpFile.Name()) - _, err = tmpFile.WriteString(script) - Expect(err).ToNot(HaveOccurred()) - err = tmpFile.Close() - Expect(err).ToNot(HaveOccurred()) - err = os.Chmod(tmpFile.Name(), 0700) - Expect(err).ToNot(HaveOccurred()) - - cmd := Command{ - Name: "powershell", - Args: []string{"-ExecutionPolicy", "Bypass", "-File", tmpFile.Name()}, - SpawnWithLowerPriority: true, - } + cmd := Command{Name: priorityPath, SpawnWithLowerPriority: true} process, err := runner.RunComplexCommandAsync(cmd) Expect(err).ToNot(HaveOccurred()) result := <-process.Wait() Expect(result.Error).ToNot(HaveOccurred()) - Expect(result.Stdout).To(Equal("BelowNormal\r\n")) + Expect(result.Stdout).To(Equal("BELOW_NORMAL_PRIORITY_CLASS\r\n")) }) }) @@ -388,7 +223,7 @@ var _ = Describe("execCmdRunner", func() { Describe("RunComplexCommandAsync", func() { It("populates stdout and stderr", func() { - cmd := osSpecificCommand("ls") + cmd := windowsCommand("ls") process, err := runner.RunComplexCommandAsync(cmd) Expect(err).ToNot(HaveOccurred()) @@ -412,7 +247,7 @@ var _ = Describe("execCmdRunner", func() { }) It("returns error and sets status to exit status of command if it exits with non-0 status", func() { - cmd := osSpecificCommand("exit") + cmd := windowsCommand("exit") process, err := runner.RunComplexCommandAsync(cmd) Expect(err).ToNot(HaveOccurred()) @@ -422,7 +257,7 @@ var _ = Describe("execCmdRunner", func() { }) It("allows setting custom env variable in addition to inheriting process env variables", func() { - cmd := osSpecificCommand("env") + cmd := windowsCommand("env") process, err := runner.RunComplexCommandAsync(cmd) Expect(err).ToNot(HaveOccurred()) @@ -434,7 +269,7 @@ var _ = Describe("execCmdRunner", func() { }) It("changes working dir", func() { - cmd := osSpecificCommand("pwd") + cmd := windowsCommand("pwd") process, err := runner.RunComplexCommandAsync(cmd) Expect(err).ToNot(HaveOccurred()) @@ -446,7 +281,7 @@ var _ = Describe("execCmdRunner", func() { Describe("RunCommand", func() { It("run command", func() { - cmd := osSpecificCommand("echo") + cmd := windowsCommand("echo") stdout, stderr, status, err := runner.RunCommand(cmd.Name, cmd.Args...) Expect(err).ToNot(HaveOccurred()) Expect(stdout).To(Equal("Hello World!\n")) @@ -455,7 +290,7 @@ var _ = Describe("execCmdRunner", func() { }) It("run command with error output", func() { - cmd := osSpecificCommand("stderr") + cmd := windowsCommand("stderr") stdout, stderr, status, err := runner.RunCommand(cmd.Name, cmd.Args...) Expect(err).ToNot(HaveOccurred()) Expect(stdout).To(BeEmpty()) @@ -464,7 +299,7 @@ var _ = Describe("execCmdRunner", func() { }) It("run command with non-0 exit status", func() { - cmd := osSpecificCommand("exit") + cmd := windowsCommand("exit") stdout, stderr, status, err := runner.RunCommand(cmd.Name, cmd.Args...) Expect(err).To(HaveOccurred()) Expect(stdout).To(BeEmpty()) @@ -493,9 +328,7 @@ var _ = Describe("execCmdRunner", func() { stdout, stderr, status, err := runner.RunCommand("something that does not exist") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Or(ContainSubstring("not found"), ContainSubstring("ObjectNotFound"))) - if runtime.GOOS != "windows" { - Expect(stderr).To(BeEmpty()) - } + Expect(stderr).To(BeEmpty()) Expect(stdout).To(BeEmpty()) Expect(status).ToNot(Equal(0)) }) @@ -516,7 +349,7 @@ var _ = Describe("execCmdRunner", func() { logger := &loggerfakes.FakeLogger{} runner = NewExecCmdRunner(logger) - cmd := osSpecificCommand("echo") + cmd := windowsCommand("echo") stdout, stderr, status, err := runner.RunCommandQuietly(cmd.Name, cmd.Args...) Expect(err).ToNot(HaveOccurred()) Expect(logger.DebugCallCount()).To(Equal(2)) @@ -528,13 +361,7 @@ var _ = Describe("execCmdRunner", func() { Describe("CommandExists", func() { It("command exists", func() { - var cmd string - if runtime.GOOS == "windows" { - cmd = "cmd.exe" - } else { - cmd = "env" - } - Expect(runner.CommandExists(cmd)).To(BeTrue()) + Expect(runner.CommandExists("cmd.exe")).To(BeTrue()) Expect(runner.CommandExists("absolutely-does-not-exist-ever-please-unicorns")).To(BeFalse()) }) }) diff --git a/system/os_file_system_test.go b/system/os_file_system_test.go index 03b8d8e7..18403512 100644 --- a/system/os_file_system_test.go +++ b/system/os_file_system_test.go @@ -2,13 +2,10 @@ package system_test import ( "bytes" - "fmt" "io" "os" - "os/exec" "os/user" "path/filepath" - "slices" "strings" . "github.com/onsi/ginkgo/v2" @@ -22,6 +19,12 @@ import ( . "github.com/cloudfoundry/bosh-utils/system" ) +var fixtureFiles = []string{ + "foo.txt", + "bar/bar.txt", + "bar/baz/.gitkeep", +} + func createOsFs() (fs FileSystem) { logger := boshlog.NewLogger(boshlog.LevelNone) fs = NewOsFileSystem(logger) @@ -52,36 +55,6 @@ var _ = Describe("OS FileSystem", func() { os.RemoveAll(TempDir) //nolint:errcheck }) - It("home dir", func() { - if isWindows { - currentUser, err := user.Current() - Expect(err).ToNot(HaveOccurred()) - - // If a regular user, the home directory will end with the username - expDir := fmt.Sprintf(`\%s`, filepath.Base(currentUser.Username)) - - // If a System or LocalSystem user, the home directory will be different - // ref: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers - groupIds, err := currentUser.GroupIds() - Expect(err).ToNot(HaveOccurred()) - if slices.Contains(groupIds, "S-1-5-18") { - expDir = `C:\Windows\system32\config\systemprofile` - } - - homeDir, err := createOsFs().HomeDir(currentUser.Name) - Expect(err).ToNot(HaveOccurred()) - - Expect(strings.ToLower(homeDir)).To(ContainSubstring(strings.ToLower(expDir))) - } else { - superuser := "root" - expDir := "/root" - homeDir, err := createOsFs().HomeDir(superuser) - Expect(err).ToNot(HaveOccurred()) - - Expect(homeDir).To(ContainSubstring(expDir)) - } - }) - It("expand path", func() { osFs := createOsFs() @@ -824,57 +797,9 @@ var _ = Describe("OS FileSystem", func() { Expect(err).ToNot(HaveOccurred()) Expect(fooContent).To(Equal(srcContent)) }) - - It("does not leak file descriptors", func() { - cmdName := "lsof" - if isWindows { - if _, err := exec.LookPath("handle.exe"); err != nil { - Skip("This test requires handle.exe it can be downloaded here:\n" + - "https://technet.microsoft.com/en-us/sysinternals/handle.aspx") - } - cmdName = "handle.exe" - } - osFs := createOsFs() - - srcFile, err := osFs.TempFile("srcPath") - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(srcFile.Name()) - - err = srcFile.Close() - Expect(err).ToNot(HaveOccurred()) - - dstFile, err := osFs.TempFile("dstPath") - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(dstFile.Name()) - - err = dstFile.Close() - Expect(err).ToNot(HaveOccurred()) - - err = osFs.CopyFile(srcFile.Name(), dstFile.Name()) - Expect(err).ToNot(HaveOccurred()) - - runner := NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) - stdout, _, _, err := runner.RunCommand(cmdName) - Expect(err).ToNot(HaveOccurred()) - - for _, line := range strings.Split(stdout, "\n") { - if strings.Contains(line, srcFile.Name()) { - Fail(fmt.Sprintf("CopyFile did not close: srcFile: %s", srcFile.Name())) - } - if strings.Contains(line, dstFile.Name()) { - Fail(fmt.Sprintf("CopyFile did not close: dstFile: %s", dstFile.Name())) - } - } - }) }) Describe("CopyDir", func() { - var fixtureFiles = []string{ - "foo.txt", - "bar/bar.txt", - "bar/baz/.gitkeep", - } - It("recursively copies directory contents", func() { osFs := createOsFs() srcPath := "test_assets/test_copy_dir_entries" @@ -895,58 +820,6 @@ var _ = Describe("OS FileSystem", func() { Expect(srcContents).To(Equal(dstContents), "Copied file does not match source file: '%s", fixtureFile) } }) - - It("does not leak file descriptors", func() { - cmdName := "lsof" - if isWindows { - if _, err := exec.LookPath("handle.exe"); err != nil { - Skip("This test requires handle.exe it can be downloaded here:\n" + - "https://technet.microsoft.com/en-us/sysinternals/handle.aspx") - } - cmdName = "handle.exe" - } - - osFs := createOsFs() - srcPath := "test_assets/test_copy_dir_entries" - dstPath, err := osFs.TempDir("CopyDirTestDir") - Expect(err).ToNot(HaveOccurred()) - defer osFs.RemoveAll(dstPath) //nolint:errcheck - - err = osFs.CopyDir(srcPath, dstPath) - Expect(err).ToNot(HaveOccurred()) - - runner := NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) - stdout, _, _, err := runner.RunCommand(cmdName) - Expect(err).ToNot(HaveOccurred()) - - // lsof and handle use absolute paths - srcPath, err = filepath.Abs(srcPath) - Expect(err).ToNot(HaveOccurred()) - - for _, line := range strings.Split(stdout, "\n") { - for _, fixtureFile := range fixtureFiles { - srcFilePath := filepath.Join(srcPath, fixtureFile) - if strings.Contains(line, srcFilePath) { - Fail(fmt.Sprintf("CopyDir did not close source file: %s", srcFilePath)) - } - - srcFileDirPath := filepath.Dir(srcFilePath) - if strings.Contains(line, srcFileDirPath) { - Fail(fmt.Sprintf("CopyDir did not close source dir: %s", srcFileDirPath)) - } - - dstFilePath := filepath.Join(dstPath, fixtureFile) - if strings.Contains(line, dstFilePath) { - Fail(fmt.Sprintf("CopyDir did not close destination file: %s", dstFilePath)) - } - - dstFileDirPath := filepath.Dir(dstFilePath) - if strings.Contains(line, dstFileDirPath) { - Fail(fmt.Sprintf("CopyDir did not close destination dir: %s", dstFileDirPath)) - } - } - } - }) }) It("remove all", func() { diff --git a/system/os_file_system_unix_test.go b/system/os_file_system_unix_test.go index de0dafb7..1550b405 100644 --- a/system/os_file_system_unix_test.go +++ b/system/os_file_system_unix_test.go @@ -4,16 +4,22 @@ package system_test import ( + "fmt" "os" "path/filepath" "runtime" + "strings" "syscall" + boshlog "github.com/cloudfoundry/bosh-utils/logger" + . "github.com/cloudfoundry/bosh-utils/system" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("OS FileSystem", func() { + const lsofCmd = "lsof" + Describe("chown", func() { var testPath string @@ -24,7 +30,13 @@ var _ = Describe("OS FileSystem", func() { Expect(err).ToNot(HaveOccurred()) }) - if runtime.GOOS == "linux" && os.Getenv("USER") == "root" { + Context("when running as root on linux", func() { + BeforeEach(func() { + if runtime.GOOS != "linux" || os.Geteuid() != 0 { + Skip("This test can only run as `root` on Linux") + } + }) + It("should chown file with owner:group syntax", func() { osFs := createOsFs() @@ -54,7 +66,7 @@ var _ = Describe("OS FileSystem", func() { Expect(testPathStat.Sys().(*syscall.Stat_t).Uid).To(Equal(uint32(0))) Expect(testPathStat.Sys().(*syscall.Stat_t).Gid).To(Equal(uint32(0))) }) - } + }) Context("given an empty owner", func() { It("should return an error", func() { @@ -97,7 +109,86 @@ var _ = Describe("OS FileSystem", func() { }) }) + Describe("CopyFile", func() { + It("does not leak file descriptors", func() { + osFs := createOsFs() + + srcFile, err := osFs.TempFile("srcPath") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(srcFile.Name()) + + err = srcFile.Close() + Expect(err).ToNot(HaveOccurred()) + + dstFile, err := osFs.TempFile("dstPath") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dstFile.Name()) + + err = dstFile.Close() + Expect(err).ToNot(HaveOccurred()) + + err = osFs.CopyFile(srcFile.Name(), dstFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + runner := NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) + stdout, _, _, err := runner.RunCommand(lsofCmd, "-p", fmt.Sprintf("%d", os.Getpid())) + Expect(err).ToNot(HaveOccurred()) + + for _, line := range strings.Split(stdout, "\n") { + if strings.Contains(line, srcFile.Name()) { + Fail(fmt.Sprintf("CopyFile did not close: srcFile: %s", srcFile.Name())) + } + if strings.Contains(line, dstFile.Name()) { + Fail(fmt.Sprintf("CopyFile did not close: dstFile: %s", dstFile.Name())) + } + } + }) + }) + Describe("CopyDir", func() { + It("does not leak file descriptors", func() { + osFs := createOsFs() + srcPath := "test_assets/test_copy_dir_entries" + dstPath, err := osFs.TempDir("CopyDirTestDir") + Expect(err).ToNot(HaveOccurred()) + defer osFs.RemoveAll(dstPath) //nolint:errcheck + + err = osFs.CopyDir(srcPath, dstPath) + Expect(err).ToNot(HaveOccurred()) + + runner := NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) + stdout, _, _, err := runner.RunCommand(lsofCmd, "-p", fmt.Sprintf("%d", os.Getpid())) + Expect(err).ToNot(HaveOccurred()) + + // lsof and handle use absolute paths + srcPath, err = filepath.Abs(srcPath) + Expect(err).ToNot(HaveOccurred()) + + for _, line := range strings.Split(stdout, "\n") { + for _, fixtureFile := range fixtureFiles { + srcFilePath := filepath.Join(srcPath, fixtureFile) + if strings.Contains(line, srcFilePath) { + Fail(fmt.Sprintf("CopyDir did not close source file: %s", srcFilePath)) + } + + srcFileDirPath := filepath.Dir(srcFilePath) + if strings.Contains(line, srcFileDirPath) { + Fail(fmt.Sprintf("CopyDir did not close source dir: %s", srcFileDirPath)) + } + + dstFilePath := filepath.Join(dstPath, fixtureFile) + if strings.Contains(line, dstFilePath) { + Fail(fmt.Sprintf("CopyDir did not close destination file: %s", dstFilePath)) + } + + dstFileDirPath := filepath.Dir(dstFilePath) + if strings.Contains(line, dstFileDirPath) { + Fail(fmt.Sprintf("CopyDir did not close destination dir: %s", dstFileDirPath)) + } + } + } + }) + It("keeps the permissions", func() { osFs := createOsFs() srcPath, err := osFs.TempDir("CopyDirTestSrc") @@ -123,4 +214,18 @@ var _ = Describe("OS FileSystem", func() { Expect(fi.Mode()).To(Equal(os.FileMode(0400))) }) }) + + Describe("home dir", func() { + It("home dir", func() { + superuser := "root" + expDir := "/root" + if runtime.GOOS == "darwin" { + expDir = "/var/root" + } + homeDir, err := createOsFs().HomeDir(superuser) + Expect(err).ToNot(HaveOccurred()) + + Expect(homeDir).To(Equal(expDir)) + }) + }) }) diff --git a/system/os_file_system_windows_test.go b/system/os_file_system_windows_test.go index 1d91d855..55cf26e8 100644 --- a/system/os_file_system_windows_test.go +++ b/system/os_file_system_windows_test.go @@ -1,27 +1,104 @@ package system_test import ( + "fmt" "os" + "os/exec" + "os/user" "path" "path/filepath" + "slices" + "strings" "syscall" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + boshlog "github.com/cloudfoundry/bosh-utils/logger" + . "github.com/cloudfoundry/bosh-utils/system" ) var _ = Describe("Windows Specific tests", func() { - It("HomeDir returns an error if 'username' is not the current user", func() { - if !isWindows { - Skip("Windows only test") - } - osFs := createOsFs() - - _, err := osFs.HomeDir("Non-Existent User Name 1234") - Expect(err).To(HaveOccurred()) + Describe("home dir", func() { + It("home dir", func() { + currentUser, err := user.Current() + Expect(err).ToNot(HaveOccurred()) + + // If a regular user, the home directory will end with the username + expDir := fmt.Sprintf(`\%s`, filepath.Base(currentUser.Username)) + + // If a System or LocalSystem user, the home directory will be different + // ref: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers + groupIds, err := currentUser.GroupIds() + Expect(err).ToNot(HaveOccurred()) + if slices.Contains(groupIds, "S-1-5-18") { + expDir = `C:\Windows\system32\config\systemprofile` + } + + homeDir, err := createOsFs().HomeDir(currentUser.Name) + Expect(err).ToNot(HaveOccurred()) + + Expect(strings.ToLower(homeDir)).To(ContainSubstring(strings.ToLower(expDir))) + }) + + It("returns an error if 'username' is not the current user", func() { + osFs := createOsFs() + + _, err := osFs.HomeDir("Non-Existent User Name 1234") + Expect(err).To(HaveOccurred()) + }) }) Describe("CopyDir", func() { + It("does not leak file descriptors", func() { + cmdName := "handle.exe" + if _, err := exec.LookPath(cmdName); err != nil { + Skip("This test requires handle.exe it can be downloaded here:\n" + + "https://technet.microsoft.com/en-us/sysinternals/handle.aspx") + } + + osFs := createOsFs() + srcPath := "test_assets/test_copy_dir_entries" + dstPath, err := osFs.TempDir("CopyDirTestDir") + Expect(err).ToNot(HaveOccurred()) + defer osFs.RemoveAll(dstPath) //nolint:errcheck + + err = osFs.CopyDir(srcPath, dstPath) + Expect(err).ToNot(HaveOccurred()) + + runner := NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) + stdout, _, _, err := runner.RunCommand(cmdName) + Expect(err).ToNot(HaveOccurred()) + + // lsof and handle use absolute paths + srcPath, err = filepath.Abs(srcPath) + Expect(err).ToNot(HaveOccurred()) + + for _, line := range strings.Split(stdout, "\n") { + for _, fixtureFile := range fixtureFiles { + srcFilePath := filepath.Join(srcPath, fixtureFile) + if strings.Contains(line, srcFilePath) { + Fail(fmt.Sprintf("CopyDir did not close source file: %s", srcFilePath)) + } + + srcFileDirPath := filepath.Dir(srcFilePath) + if strings.Contains(line, srcFileDirPath) { + Fail(fmt.Sprintf("CopyDir did not close source dir: %s", srcFileDirPath)) + } + + dstFilePath := filepath.Join(dstPath, fixtureFile) + if strings.Contains(line, dstFilePath) { + Fail(fmt.Sprintf("CopyDir did not close destination file: %s", dstFilePath)) + } + + dstFileDirPath := filepath.Dir(dstFilePath) + if strings.Contains(line, dstFileDirPath) { + Fail(fmt.Sprintf("CopyDir did not close destination dir: %s", dstFileDirPath)) + } + } + } + }) + It("doesn't keep the permissions because they do not behave the same in windows", func() { osFs := createOsFs() srcPath, err := osFs.TempDir("CopyDirTestSrc") @@ -48,29 +125,72 @@ var _ = Describe("Windows Specific tests", func() { }) }) - It("can remove a directory long path", func() { - osFs := createOsFs() + Describe("CopyFile", func() { + It("does not leak file descriptors", func() { + cmdName := "handle.exe" + if _, err := exec.LookPath(cmdName); err != nil { + Skip("This test requires handle.exe it can be downloaded here:\n" + + "https://technet.microsoft.com/en-us/sysinternals/handle.aspx") + } + osFs := createOsFs() + + srcFile, err := osFs.TempFile("srcPath") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(srcFile.Name()) + + err = srcFile.Close() + Expect(err).ToNot(HaveOccurred()) + + dstFile, err := osFs.TempFile("dstPath") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(dstFile.Name()) + + err = dstFile.Close() + Expect(err).ToNot(HaveOccurred()) + + err = osFs.CopyFile(srcFile.Name(), dstFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + runner := NewExecCmdRunner(boshlog.NewLogger(boshlog.LevelNone)) + stdout, _, _, err := runner.RunCommand(cmdName) + Expect(err).ToNot(HaveOccurred()) + + for _, line := range strings.Split(stdout, "\n") { + if strings.Contains(line, srcFile.Name()) { + Fail(fmt.Sprintf("CopyFile did not close: srcFile: %s", srcFile.Name())) + } + if strings.Contains(line, dstFile.Name()) { + Fail(fmt.Sprintf("CopyFile did not close: dstFile: %s", dstFile.Name())) + } + } + }) + }) + + Describe("RemoveAll", func() { + It("can remove a directory long path", func() { + osFs := createOsFs() - longPath := randLongPath(GinkgoT().TempDir()) - err := os.MkdirAll(longPath, 0755) - Expect(err).ToNot(HaveOccurred()) + longPath := randLongPath(GinkgoT().TempDir()) + err := os.MkdirAll(longPath, 0755) + Expect(err).ToNot(HaveOccurred()) - dstFile, err := os.CreateTemp(`\\?\`+longPath, "") - Expect(err).ToNot(HaveOccurred()) + dstFile, err := os.CreateTemp(`\\?\`+longPath, "") + Expect(err).ToNot(HaveOccurred()) - dstPath := path.Join(longPath, filepath.Base(dstFile.Name())) - defer os.Remove(dstPath) - dstFile.Close() + dstPath := path.Join(longPath, filepath.Base(dstFile.Name())) + defer os.Remove(dstPath) + dstFile.Close() - fileInfo, err := osFs.Stat(dstPath) - Expect(fileInfo).ToNot(BeNil()) - Expect(os.IsNotExist(err)).To(BeFalse()) + fileInfo, err := osFs.Stat(dstPath) + Expect(fileInfo).ToNot(BeNil()) + Expect(os.IsNotExist(err)).To(BeFalse()) - err = osFs.RemoveAll(dstPath) - Expect(err).ToNot(HaveOccurred()) + err = osFs.RemoveAll(dstPath) + Expect(err).ToNot(HaveOccurred()) - _, err = osFs.Stat(dstPath) - Expect(os.IsNotExist(err)).To(BeTrue()) + _, err = osFs.Stat(dstPath) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) }) // Alert future developers that a previously unimplemented diff --git a/system/process_priority_darwin.go b/system/process_priority_darwin.go deleted file mode 100644 index 6e087413..00000000 --- a/system/process_priority_darwin.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build darwin - -package system - -import ( - "syscall" -) - -// getProcessPriority returns the nice value of the process with the given pid. -func getProcessPriority(pid int) (int, error) { - return syscall.Getpriority(syscall.PRIO_PROCESS, pid) -} diff --git a/system/process_priority_linux.go b/system/process_priority_linux.go deleted file mode 100644 index e8a7cd7d..00000000 --- a/system/process_priority_linux.go +++ /dev/null @@ -1,18 +0,0 @@ -package system - -import ( - "syscall" -) - -// getProcessPriority returns the nice value of the process with the given pid. -func getProcessPriority(pid int) (int, error) { - knice, err := syscall.Getpriority(syscall.PRIO_PROCESS, pid) - if err != nil { - return 0, err - } - - // Linux: convert syscall.Getpriority()'s "kernel nice" to "user nice" - // => unice = 20 - knice - // See https://linux.die.net/man/2/getpriority - return ((knice - 20) * -1), nil -} diff --git a/system/process_priority_unix.go b/system/process_priority_unix.go index fc24b454..70f24864 100644 --- a/system/process_priority_unix.go +++ b/system/process_priority_unix.go @@ -1,3 +1,5 @@ +//go:build !windows + package system // Inspired by github.com/hekmon/processpriority (MIT, Copyright 2024 Edouard Hur). @@ -5,6 +7,7 @@ package system import ( "os" + "runtime" "syscall" ) @@ -13,6 +16,23 @@ func setProcessPriority(pid int, nice int) error { return syscall.Setpriority(syscall.PRIO_PROCESS, pid, nice) } +// getProcessPriority returns the nice value of the process with the given pid. +func getProcessPriority(pid int) (int, error) { + if runtime.GOOS == "linux" { + knice, err := syscall.Getpriority(syscall.PRIO_PROCESS, pid) + if err != nil { + return 0, err + } + + // Linux: convert syscall.Getpriority()'s "kernel nice" to "user nice" + // => unice = 20 - knice + // See https://linux.die.net/man/2/getpriority + return (knice - 20) * -1, nil + } + + return syscall.Getpriority(syscall.PRIO_PROCESS, pid) +} + // lowerProcessPriority sets the child process nice value to parent + 5, clamped at 19. func (r execCmdRunner) lowerProcessPriority(logTag string, processPid int) error { parentPid := os.Getpid() diff --git a/system/system_suite_test.go b/system/system_suite_test.go index a26daa0d..2b05c0c1 100644 --- a/system/system_suite_test.go +++ b/system/system_suite_test.go @@ -27,18 +27,18 @@ var windowsExePath string var priorityPath string var _ = SynchronizedBeforeSuite(func() []byte { - var paths []string + workingDir, err := filepath.Abs(".") + Expect(err).ToNot(HaveOccurred()) - paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/cat/")) - paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/false/")) - paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/windows_exe/")) - paths = append(paths, buildFixtureCmd("exec_cmd_runner_fixtures/priority")) + var paths []string + paths = append(paths, buildFixtureCmd(workingDir, "exec_cmd_runner_fixtures/cat/")) + paths = append(paths, buildFixtureCmd(workingDir, "exec_cmd_runner_fixtures/false/")) + paths = append(paths, buildFixtureCmd(workingDir, "exec_cmd_runner_fixtures/windows_exe/")) + paths = append(paths, buildFixtureCmd(workingDir, "exec_cmd_runner_fixtures/priority")) - Expect(paths).To(HaveLen(4)) return []byte(strings.Join(paths, "|")) }, func(data []byte) { paths := strings.Split(string(data), "|") - Expect(paths).To(HaveLen(4)) catPath = paths[0] falsePath = paths[1] @@ -50,13 +50,7 @@ var _ = SynchronizedAfterSuite(func() {}, func() { gexec.CleanupBuildArtifacts() }) -func buildFixtureCmd(fixtureSrcPath string) string { - ex, err := os.Executable() - if err != nil { - panic(err) - } - workingDir := filepath.Dir(ex) - +func buildFixtureCmd(workingDir string, fixtureSrcPath string) string { Expect(os.Chdir(fixtureSrcPath)).To(Succeed()) fixtureBinPath, err := gexec.Build("./...") Expect(err).ToNot(HaveOccurred()) @@ -78,7 +72,7 @@ func randSeq(n int) string { func randLongPath(tmpDir string) string { volume := tmpDir + string(filepath.Separator) buf := bytes.NewBufferString(volume) - for i := 0; i < 2; i++ { + for range 2 { for i := byte('A'); i <= 'Z'; i++ { buf.Write(bytes.Repeat([]byte{i}, 4)) buf.WriteRune(filepath.Separator) @@ -88,3 +82,21 @@ func randLongPath(tmpDir string) string { buf.WriteRune(filepath.Separator) return filepath.Clean(buf.String()) } + +func parseEnvFields(envDump string, convertKeysToUpper bool) map[string]string { + fields := make(map[string]string) + for line := range strings.SplitSeq(envDump, "\n") { + line = strings.TrimSuffix(line, "\r") + // don't split on '=' as '=' is allowed in the value on Windows + if before, after, ok := strings.Cut(line, "="); ok { + varName := before + varValue := after + if convertKeysToUpper { + fields[strings.ToUpper(varName)] = varValue + } else { + fields[varName] = varValue + } + } + } + return fields +}