From d557fc68cc91edb6836e70b0643bf32d0159758f Mon Sep 17 00:00:00 2001 From: Davide N Date: Thu, 12 Jun 2025 16:47:56 +0200 Subject: [PATCH 1/4] wip: tests --- apt.go | 17 +++++++- apt_test.go | 68 ++++++++++++++++++++++++++++- testdata/apt-list-upgradable.golden | 16 +++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 testdata/apt-list-upgradable.golden diff --git a/apt.go b/apt.go index 5f0c591..5fae2c1 100644 --- a/apt.go +++ b/apt.go @@ -22,6 +22,7 @@ import ( "bufio" "bytes" "fmt" + "io" "os/exec" "regexp" "strconv" @@ -64,6 +65,8 @@ func Search(pattern string) ([]*Package, error) { func parseDpkgQueryOutput(out []byte) []*Package { res := []*Package{} scanner := bufio.NewScanner(bytes.NewReader(out)) + buf := make([]byte, 0, 1024*1024) // 1 MB buffer + scanner.Buffer(buf, 1024*1024) // Set max buffer size to 1 MB for scanner.Scan() { data := strings.Split(scanner.Text(), "\t") size, err := strconv.Atoi(data[4]) @@ -98,10 +101,20 @@ func ListUpgradable() ([]*Package, error) { if err != nil { return nil, fmt.Errorf("running apt list: %s", err) } + + res := parseListUpgradableOutput(bytes.NewReader(out)) + return res, nil +} + +func parseListUpgradableOutput(r io.Reader) []*Package { + // Example of matched line: + // xserver-xorg-core/focal-updates 2:1.20.13-1ubuntu1~20.04.20 amd64 [upgradable from: 2:1.20.13-1ubuntu1~20.04.19] re := regexp.MustCompile(`^([^ ]+) ([^ ]+) ([^ ]+)( \[upgradable from: [^\[\]]*\])?`) res := []*Package{} - scanner := bufio.NewScanner(bytes.NewReader(out)) + scanner := bufio.NewScanner(r) + buf := make([]byte, 0, 1024*1024) // 1 MB buffer + scanner.Buffer(buf, 1024*1024) // Set max buffer size to 1 MB for scanner.Scan() { matches := re.FindAllStringSubmatch(scanner.Text(), -1) if len(matches) == 0 { @@ -120,7 +133,7 @@ func ListUpgradable() ([]*Package, error) { Architecture: matches[0][3], }) } - return res, nil + return res } // Upgrade runs the upgrade for a set of packages diff --git a/apt_test.go b/apt_test.go index 542a125..74fb5e6 100644 --- a/apt_test.go +++ b/apt_test.go @@ -22,12 +22,13 @@ import ( "encoding/json" "fmt" "os" + "strings" "testing" "github.com/stretchr/testify/require" ) -func TestList(t *testing.T) { +func TestParseDpkgQueryOutput(t *testing.T) { out, err := os.ReadFile("testdata/dpkg-query-output-1.txt") require.NoError(t, err, "Reading test input data") list := parseDpkgQueryOutput(out) @@ -68,3 +69,68 @@ func TestCheckForUpdates(t *testing.T) { fmt.Printf(">>>\n%s\n<<<\n", string(out)) fmt.Println("ERR:", err) } + +func TestParseListUpgradableOutput(t *testing.T) { + t.Run("special cases", func(t *testing.T) { + tests := []struct { + name string + input string + expected []*Package + }{ + { + name: "empty input", + input: "", + expected: []*Package{}, + }, + { + name: "line not matching regex", + input: "this-is-not-a-valid-line\n", + expected: []*Package{}, + }, + { + name: "upgradable package without [upgradable from]", + input: "nano/bionic-updates 2.9.3-2 amd64\n", + expected: []*Package{ + { + Name: "nano", + Status: "upgradable", + Version: "2.9.3-2", + Architecture: "amd64", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := parseListUpgradableOutput(strings.NewReader(tt.input)) + require.Equal(t, tt.expected, res) + }) + } + }) + + t.Run("golden file: list-upgradable.golden", func(t *testing.T) { + data, err := os.ReadFile("testdata/apt-list-upgradable.golden") + require.NoError(t, err, "Reading golden file") + result := parseListUpgradableOutput(strings.NewReader(string(data))) + + want := []*Package{ + {Name: "apt-transport-https", Status: "upgradable", Version: "2.0.11", Architecture: "all"}, + {Name: "apt-utils", Status: "upgradable", Version: "2.0.11", Architecture: "amd64"}, + {Name: "apt", Status: "upgradable", Version: "2.0.11", Architecture: "amd64"}, + {Name: "code-insiders", Status: "upgradable", Version: "1.101.0-1749657374", Architecture: "amd64"}, + {Name: "code", Status: "upgradable", Version: "1.100.3-1748872405", Architecture: "amd64"}, + {Name: "containerd.io", Status: "upgradable", Version: "1.7.27-1", Architecture: "amd64"}, + {Name: "distro-info-data", Status: "upgradable", Version: "0.43ubuntu1.18", Architecture: "all"}, + {Name: "docker-ce-cli", Status: "upgradable", Version: "5:28.1.1-1~ubuntu.20.04~focal", Architecture: "amd64"}, + {Name: "python3.12", Status: "upgradable", Version: "3.12.11-1+focal1", Architecture: "amd64"}, + {Name: "xdg-desktop-portal", Status: "upgradable", Version: "1.14.3-1~flatpak1~20.04", Architecture: "amd64"}, + {Name: "xserver-common", Status: "upgradable", Version: "2:1.20.13-1ubuntu1~20.04.20", Architecture: "all"}, + {Name: "xserver-xephyr", Status: "upgradable", Version: "2:1.20.13-1ubuntu1~20.04.20", Architecture: "amd64"}, + {Name: "xserver-xorg-core", Status: "upgradable", Version: "2:1.20.13-1ubuntu1~20.04.20", Architecture: "amd64"}, + {Name: "xserver-xorg-legacy", Status: "upgradable", Version: "2:1.20.13-1ubuntu1~20.04.20", Architecture: "amd64"}, + {Name: "xwayland", Status: "upgradable", Version: "2:1.20.13-1ubuntu1~20.04.20", Architecture: "amd64"}, + } + require.NotNil(t, result) + require.Equal(t, want, result, "Parsed result should match expected from golden file") + }) +} diff --git a/testdata/apt-list-upgradable.golden b/testdata/apt-list-upgradable.golden new file mode 100644 index 0000000..81e13e1 --- /dev/null +++ b/testdata/apt-list-upgradable.golden @@ -0,0 +1,16 @@ +Listing... Done +apt-transport-https/focal-updates,focal-updates 2.0.11 all [upgradable from: 2.0.10] +apt-utils/focal-updates 2.0.11 amd64 [upgradable from: 2.0.10] +apt/focal-updates 2.0.11 amd64 [upgradable from: 2.0.10] +code-insiders/stable 1.101.0-1749657374 amd64 [upgradable from: 1.100.0-1743745333] +code/stable 1.100.3-1748872405 amd64 [upgradable from: 1.100.2-1747260578] +containerd.io/focal 1.7.27-1 amd64 [upgradable from: 1.7.25-1] +distro-info-data/focal-updates,focal-updates 0.43ubuntu1.18 all [upgradable from: 0.43ubuntu1.16] +docker-ce-cli/focal 5:28.1.1-1~ubuntu.20.04~focal amd64 [upgradable from: 5:28.0.1-1~ubuntu.20.04~focal] +python3.12/focal 3.12.11-1+focal1 amd64 [upgradable from: 3.12.5-1+focal1] +xdg-desktop-portal/focal 1.14.3-1~flatpak1~20.04 amd64 [upgradable from: 1.6.0-1ubuntu2] +xserver-common/focal-updates,focal-updates 2:1.20.13-1ubuntu1~20.04.20 all [upgradable from: 2:1.20.13-1ubuntu1~20.04.19] +xserver-xephyr/focal-updates 2:1.20.13-1ubuntu1~20.04.20 amd64 [upgradable from: 2:1.20.13-1ubuntu1~20.04.19] +xserver-xorg-core/focal-updates 2:1.20.13-1ubuntu1~20.04.20 amd64 [upgradable from: 2:1.20.13-1ubuntu1~20.04.19] +xserver-xorg-legacy/focal-updates 2:1.20.13-1ubuntu1~20.04.20 amd64 [upgradable from: 2:1.20.13-1ubuntu1~20.04.19] +xwayland/focal-updates 2:1.20.13-1ubuntu1~20.04.20 amd64 [upgradable from: 2:1.20.13-1ubuntu1~20.04.19] From 6243bb6ede823e28636ad8046369322d613971cc Mon Sep 17 00:00:00 2001 From: Davide N Date: Thu, 12 Jun 2025 17:38:34 +0200 Subject: [PATCH 2/4] task test refactor --- .github/workflows/test-go-task.yml | 3 +-- Taskfile.yaml | 19 +------------------ apt_test.go | 4 ++-- testdata/Dockerfile | 4 ++-- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test-go-task.yml b/.github/workflows/test-go-task.yml index 1910f39..6e09309 100644 --- a/.github/workflows/test-go-task.yml +++ b/.github/workflows/test-go-task.yml @@ -99,8 +99,7 @@ jobs: - name: Run tests env: GO_MODULE_PATH: ${{ matrix.module.path }} - # run: task go:test ## TODO: refactor the tests - run: task go:test:docker + run: task go:test - name: Send unit tests coverage to Codecov if: runner.os == 'Linux' diff --git a/Taskfile.yaml b/Taskfile.yaml index bd96060..fedfadd 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -1,4 +1,3 @@ - version: "3" vars: @@ -11,9 +10,6 @@ vars: go list ./... | tr '\n' ' ' || echo '"ERROR: Unable to discover Go packages"' ) - # `-ldflags` flag to use for `go build` command - # TODO: define flag if required by the project, or leave empty if not needed. - LDFLAGS: tasks: # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-go-task/Taskfile.yml @@ -47,19 +43,6 @@ tasks: go:test: desc: Run unit tests dir: "{{default .DEFAULT_GO_MODULE_PATH .GO_MODULE_PATH}}" - cmds: - - | - go test \ - -v \ - -short \ - -run '{{default ".*" .GO_TEST_REGEX}}' \ - {{default "-timeout 10m -coverpkg=./... -covermode=atomic" .GO_TEST_FLAGS}} \ - -coverprofile=coverage_unit.txt \ - {{.TEST_LDFLAGS}} \ - {{default .DEFAULT_GO_PACKAGES .GO_PACKAGES}} - - go:test:docker: - desc: Run the tests inside a docker image cmds: - docker build -f testdata/Dockerfile -t go-apt-test:latest . - - docker run --rm go-apt-test + - docker run --rm -v "$PWD":/app go-apt-test -v -short -run '{{default ".*" .GO_TEST_REGEX}}' {{default "-timeout 10m -coverpkg=./... -covermode=atomic" .GO_TEST_FLAGS}} -coverprofile=coverage_unit.txt {{default .DEFAULT_GO_PACKAGES .GO_PACKAGES}} diff --git a/apt_test.go b/apt_test.go index 74fb5e6..45ba1de 100644 --- a/apt_test.go +++ b/apt_test.go @@ -71,7 +71,7 @@ func TestCheckForUpdates(t *testing.T) { } func TestParseListUpgradableOutput(t *testing.T) { - t.Run("special cases", func(t *testing.T) { + t.Run("edges cases", func(t *testing.T) { tests := []struct { name string input string @@ -84,7 +84,7 @@ func TestParseListUpgradableOutput(t *testing.T) { }, { name: "line not matching regex", - input: "this-is-not-a-valid-line\n", + input: "this-is-not a-valid-line\n", expected: []*Package{}, }, { diff --git a/testdata/Dockerfile b/testdata/Dockerfile index 3340a79..7472609 100644 --- a/testdata/Dockerfile +++ b/testdata/Dockerfile @@ -10,10 +10,10 @@ RUN apt-get update && apt-get install -y \ WORKDIR /app # Copy everything (mod files + code) -COPY . . +COPY go.mod go.sum ./ # Download modules RUN go mod download # Run tests in apt package -CMD ["go", "test", "-v", "./"] +ENTRYPOINT ["go", "test"] From 8c8b5d266e71716bdbe558704837fc617c0e7871 Mon Sep 17 00:00:00 2001 From: Davide N Date: Thu, 12 Jun 2025 17:40:34 +0200 Subject: [PATCH 3/4] apt --- apt.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apt.go b/apt.go index 5fae2c1..919ccb7 100644 --- a/apt.go +++ b/apt.go @@ -65,8 +65,6 @@ func Search(pattern string) ([]*Package, error) { func parseDpkgQueryOutput(out []byte) []*Package { res := []*Package{} scanner := bufio.NewScanner(bytes.NewReader(out)) - buf := make([]byte, 0, 1024*1024) // 1 MB buffer - scanner.Buffer(buf, 1024*1024) // Set max buffer size to 1 MB for scanner.Scan() { data := strings.Split(scanner.Text(), "\t") size, err := strconv.Atoi(data[4]) @@ -113,8 +111,6 @@ func parseListUpgradableOutput(r io.Reader) []*Package { res := []*Package{} scanner := bufio.NewScanner(r) - buf := make([]byte, 0, 1024*1024) // 1 MB buffer - scanner.Buffer(buf, 1024*1024) // Set max buffer size to 1 MB for scanner.Scan() { matches := re.FindAllStringSubmatch(scanner.Text(), -1) if len(matches) == 0 { From f97662597e1eaff416e4b82d93286d83f67fa113 Mon Sep 17 00:00:00 2001 From: Davide N Date: Thu, 12 Jun 2025 17:44:17 +0200 Subject: [PATCH 4/4] remove docker --- Taskfile.yaml | 15 +++++++++++++-- apt.go | 1 + apt_test.go | 7 ------- testdata/Dockerfile | 19 ------------------- 4 files changed, 14 insertions(+), 28 deletions(-) delete mode 100644 testdata/Dockerfile diff --git a/Taskfile.yaml b/Taskfile.yaml index fedfadd..dad1f36 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -1,3 +1,4 @@ + version: "3" vars: @@ -10,6 +11,9 @@ vars: go list ./... | tr '\n' ' ' || echo '"ERROR: Unable to discover Go packages"' ) + # `-ldflags` flag to use for `go build` command + # TODO: define flag if required by the project, or leave empty if not needed. + LDFLAGS: tasks: # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-go-task/Taskfile.yml @@ -44,5 +48,12 @@ tasks: desc: Run unit tests dir: "{{default .DEFAULT_GO_MODULE_PATH .GO_MODULE_PATH}}" cmds: - - docker build -f testdata/Dockerfile -t go-apt-test:latest . - - docker run --rm -v "$PWD":/app go-apt-test -v -short -run '{{default ".*" .GO_TEST_REGEX}}' {{default "-timeout 10m -coverpkg=./... -covermode=atomic" .GO_TEST_FLAGS}} -coverprofile=coverage_unit.txt {{default .DEFAULT_GO_PACKAGES .GO_PACKAGES}} + - | + go test \ + -v \ + -short \ + -run '{{default ".*" .GO_TEST_REGEX}}' \ + {{default "-timeout 10m -coverpkg=./... -covermode=atomic" .GO_TEST_FLAGS}} \ + -coverprofile=coverage_unit.txt \ + {{.TEST_LDFLAGS}} \ + {{default .DEFAULT_GO_PACKAGES .GO_PACKAGES}} diff --git a/apt.go b/apt.go index 919ccb7..20d2171 100644 --- a/apt.go +++ b/apt.go @@ -86,6 +86,7 @@ func parseDpkgQueryOutput(out []byte) []*Package { // CheckForUpdates runs an apt update to retrieve new packages available // from the repositories +// NOTE: it requires root privileges to run func CheckForUpdates() (output []byte, err error) { cmd := exec.Command("apt-get", "update", "-q") return cmd.CombinedOutput() diff --git a/apt_test.go b/apt_test.go index 45ba1de..63e2209 100644 --- a/apt_test.go +++ b/apt_test.go @@ -63,13 +63,6 @@ func TestListUpgradable(t *testing.T) { require.NoError(t, err, "running List command") } -func TestCheckForUpdates(t *testing.T) { - out, err := CheckForUpdates() - require.NoError(t, err, "running CheckForUpdate command") - fmt.Printf(">>>\n%s\n<<<\n", string(out)) - fmt.Println("ERR:", err) -} - func TestParseListUpgradableOutput(t *testing.T) { t.Run("edges cases", func(t *testing.T) { tests := []struct { diff --git a/testdata/Dockerfile b/testdata/Dockerfile deleted file mode 100644 index 7472609..0000000 --- a/testdata/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM golang:1.24 - -# Install dpkg/apt tools -RUN apt-get update && apt-get install -y \ - dpkg \ - apt \ - && rm -rf /var/lib/apt/lists/* - -# Set working directory inside container -WORKDIR /app - -# Copy everything (mod files + code) -COPY go.mod go.sum ./ - -# Download modules -RUN go mod download - -# Run tests in apt package -ENTRYPOINT ["go", "test"]