Skip to content

Commit 62e5944

Browse files
giulio93lucarin91
andauthored
Test CI package update (#72)
* test .deb update * task test * dind * taskfile udpate * using a go script to test update * no parameters * define arch * unstable test * delete major * Client test * Client test * using a helper file * using run * path in the building --------- Co-authored-by: Giulio Pilotto <pilotto.giulio@gmail.com> Co-authored-by: Luca Rinaldi <l.rinaldi@arduino.cc>
1 parent 5bcedf4 commit 62e5944

File tree

5 files changed

+523
-3
lines changed

5 files changed

+523
-3
lines changed

.github/workflows/test-update.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: test the system update flow
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build-and-update:
14+
runs-on: ubuntu-22.04
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Go
21+
uses: actions/setup-go@v5
22+
with:
23+
go-version-file: go.mod
24+
25+
- name: Run dep package update test
26+
env:
27+
GH_TOKEN: ${{ secrets.ARDUINOBOT_TOKEN }}
28+
run: |
29+
go tool task test:update

Taskfile.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ tasks:
5050

5151
test:internal:
5252
cmds:
53-
- go build ./cmd/arduino-app-cli # needed for e2e tests
53+
- go build ./cmd/arduino-app-cli
5454
- task: generate
55-
- go test ./internal/... ./cmd/... -v -race {{ .CLI_ARGS }}
55+
- go test $(go list ./internal/... ./cmd/... | grep -v internal/e2e/updatetest) -v -race {{ .CLI_ARGS }}
56+
57+
test:update:
58+
cmds:
59+
- go test --timeout 30m -v ./internal/e2e/updatetest
5660

5761
test:pkg:
5862
desc: Run only tests in the pkg directory
@@ -102,9 +106,10 @@ tasks:
102106
deps:
103107
- build-deb:clone-examples
104108
cmds:
105-
- docker build --build-arg BINARY_NAME=arduino-app-cli --build-arg DEB_NAME=arduino-app-cli --build-arg VERSION={{ .VERSION }} --build-arg ARCH={{ .ARCH }} --build-arg RELEASE={{ .RELEASE }} --output=./build -f debian/Dockerfile .
109+
- docker build --build-arg BINARY_NAME=arduino-app-cli --build-arg DEB_NAME=arduino-app-cli --build-arg VERSION={{ .VERSION }} --build-arg ARCH={{ .ARCH }} --build-arg RELEASE={{ .RELEASE }} --output={{ .OUTPUT }} -f debian/Dockerfile .
106110
vars:
107111
ARCH: '{{.ARCH | default "arm64"}}'
112+
OUTPUT: '{{.OUTPUT | default "./build"}}'
108113

109114
build-deb:clone-examples:
110115
desc: "Clones the examples repo directly into the debian structure"

internal/e2e/updatetest/helpers.go

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
package updatetest
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"iter"
10+
"log"
11+
"net"
12+
"net/http"
13+
"os"
14+
"os/exec"
15+
"path/filepath"
16+
"strconv"
17+
"strings"
18+
"testing"
19+
"time"
20+
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
func fetchDebPackageLatest(t *testing.T, path, repo string) string {
25+
t.Helper()
26+
27+
repo = fmt.Sprintf("github.com/arduino/%s", repo)
28+
cmd := exec.Command(
29+
"gh", "release", "list",
30+
"--repo", repo,
31+
"--exclude-pre-releases",
32+
"--limit", "1",
33+
)
34+
35+
output, err := cmd.CombinedOutput()
36+
if err != nil {
37+
log.Fatalf("command failed: %v\nOutput: %s", err, output)
38+
}
39+
40+
fmt.Println(string(output))
41+
42+
fields := strings.Fields(string(output))
43+
if len(fields) == 0 {
44+
log.Fatal("could not parse tag from gh release list output")
45+
}
46+
tag := fields[0]
47+
48+
fmt.Println("Detected tag:", tag)
49+
cmd2 := exec.Command(
50+
"gh", "release", "download",
51+
tag,
52+
"--repo", repo,
53+
"--pattern", "*.deb",
54+
"--dir", path,
55+
)
56+
57+
out, err := cmd2.CombinedOutput()
58+
if err != nil {
59+
log.Fatalf("download failed: %v\nOutput: %s", err, out)
60+
}
61+
62+
return tag
63+
64+
}
65+
66+
func buildDebVersion(t *testing.T, storePath, tagVersion, arch string) {
67+
t.Helper()
68+
cwd, err := os.Getwd()
69+
if err != nil {
70+
panic(err)
71+
}
72+
outputDir := filepath.Join(cwd, storePath)
73+
74+
tagVersion = fmt.Sprintf("VERSION=%s", tagVersion)
75+
arch = fmt.Sprintf("ARCH=%s", arch)
76+
outputDir = fmt.Sprintf("OUTPUT=%s", outputDir)
77+
78+
cmd := exec.Command(
79+
"go", "tool", "task", "build-deb",
80+
tagVersion,
81+
arch,
82+
outputDir,
83+
)
84+
85+
if err := cmd.Run(); err != nil {
86+
log.Fatalf("failed to run build command: %v", err)
87+
}
88+
}
89+
90+
func genMajorTag(t *testing.T, tag string) string {
91+
t.Helper()
92+
93+
parts := strings.Split(tag, ".")
94+
last := parts[len(parts)-1]
95+
96+
lastNum, _ := strconv.Atoi(strings.TrimPrefix(last, "v"))
97+
lastNum++
98+
99+
parts[len(parts)-1] = strconv.Itoa(lastNum)
100+
newTag := strings.Join(parts, ".")
101+
102+
return newTag
103+
}
104+
105+
func genMinorTag(t *testing.T, tag string) string {
106+
t.Helper()
107+
108+
parts := strings.Split(tag, ".")
109+
last := parts[len(parts)-1]
110+
111+
lastNum, _ := strconv.Atoi(strings.TrimPrefix(last, "v"))
112+
if lastNum > 0 {
113+
lastNum--
114+
}
115+
116+
parts[len(parts)-1] = strconv.Itoa(lastNum)
117+
newTag := strings.Join(parts, ".")
118+
119+
if !strings.HasPrefix(newTag, "v") {
120+
newTag = "v" + newTag
121+
}
122+
return newTag
123+
}
124+
125+
func buildDockerImage(t *testing.T, dockerfile, name, arch string) {
126+
t.Helper()
127+
128+
arch = fmt.Sprintf("ARCH=%s", arch)
129+
130+
cmd := exec.Command("docker", "build", "--build-arg", arch, "-t", name, "-f", dockerfile, ".")
131+
// Capture both stdout and stderr
132+
var out bytes.Buffer
133+
var stderr bytes.Buffer
134+
cmd.Stdout = &out
135+
cmd.Stderr = &stderr
136+
137+
err := cmd.Run()
138+
if err != nil {
139+
fmt.Printf("❌ Docker build failed: %v\n", err)
140+
fmt.Printf("---- STDERR ----\n%s\n", stderr.String())
141+
fmt.Printf("---- STDOUT ----\n%s\n", out.String())
142+
return
143+
}
144+
145+
fmt.Println("✅ Docker build succeeded!")
146+
}
147+
148+
func startDockerContainer(t *testing.T, containerName string, containerImageName string) {
149+
t.Helper()
150+
151+
cmd := exec.Command(
152+
"docker", "run", "--rm", "-d",
153+
"-p", "8800:8800",
154+
"--privileged",
155+
"--cgroupns=host",
156+
"--network", "host",
157+
"-v", "/sys/fs/cgroup:/sys/fs/cgroup:rw",
158+
"-v", "/var/run/docker.sock:/var/run/docker.sock",
159+
"-e", "DOCKER_HOST=unix:///var/run/docker.sock",
160+
"--name", containerName,
161+
containerImageName,
162+
)
163+
164+
if err := cmd.Run(); err != nil {
165+
t.Fatalf("failed to run container: %v", err)
166+
}
167+
168+
}
169+
170+
func getAppCliVersion(t *testing.T, containerName string) string {
171+
t.Helper()
172+
173+
cmd := exec.Command(
174+
"docker", "exec",
175+
"--user", "arduino",
176+
containerName,
177+
"arduino-app-cli", "version", "--format", "json",
178+
)
179+
output, err := cmd.CombinedOutput()
180+
if err != nil {
181+
log.Fatalf("command failed: %v\nOutput: %s", err, output)
182+
}
183+
184+
var version struct {
185+
Version string `json:"version"`
186+
DaemonVersion string `json:"daemon_version"`
187+
}
188+
err = json.Unmarshal(output, &version)
189+
require.NoError(t, err)
190+
// TODO to enable after 0.6.7
191+
// require.Equal(t, version.Version, version.DaemonVersion, "client and daemon versions should match")
192+
require.NotEmpty(t, version.Version)
193+
return version.Version
194+
195+
}
196+
197+
func runSystemUpdate(t *testing.T, containerName string) {
198+
t.Helper()
199+
200+
cmd := exec.Command(
201+
"docker", "exec",
202+
"--user", "arduino",
203+
containerName,
204+
"arduino-app-cli", "system", "update", "--yes",
205+
)
206+
output, err := cmd.CombinedOutput()
207+
require.NoError(t, err, "system update failed: %s", output)
208+
t.Logf("system update output: %s", output)
209+
}
210+
211+
func stopDockerContainer(t *testing.T, containerName string) {
212+
t.Helper()
213+
214+
cleanupCmd := exec.Command("docker", "rm", "-f", containerName)
215+
216+
fmt.Println("🧹 Removing Docker container " + containerName)
217+
if err := cleanupCmd.Run(); err != nil {
218+
fmt.Printf("⚠️ Warning: could not remove container (might not exist): %v\n", err)
219+
}
220+
221+
}
222+
223+
func putUpdateRequest(t *testing.T, host string) {
224+
225+
t.Helper()
226+
227+
url := fmt.Sprintf("http://%s/v1/system/update/apply", host)
228+
229+
req, err := http.NewRequest(http.MethodPut, url, nil)
230+
if err != nil {
231+
log.Fatalf("Error creating request: %v", err)
232+
}
233+
234+
req.Header.Set("Content-Type", "application/json")
235+
236+
client := &http.Client{}
237+
resp, err := client.Do(req)
238+
if err != nil {
239+
log.Fatalf("Error sending request: %v", err)
240+
}
241+
defer resp.Body.Close()
242+
243+
require.Equal(t, 202, resp.StatusCode)
244+
245+
}
246+
247+
func NewSSEClient(ctx context.Context, method, url string) iter.Seq2[Event, error] {
248+
return func(yield func(Event, error) bool) {
249+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
250+
if err != nil {
251+
_ = yield(Event{}, err)
252+
return
253+
}
254+
255+
resp, err := http.DefaultClient.Do(req)
256+
if err != nil {
257+
_ = yield(Event{}, err)
258+
return
259+
}
260+
defer resp.Body.Close()
261+
262+
if resp.StatusCode != 200 {
263+
_ = yield(Event{}, fmt.Errorf("got response status code %d", resp.StatusCode))
264+
return
265+
}
266+
267+
reader := bufio.NewReader(resp.Body)
268+
269+
evt := Event{}
270+
for {
271+
line, err := reader.ReadString('\n')
272+
if err != nil {
273+
_ = yield(Event{}, err)
274+
return
275+
}
276+
switch {
277+
case strings.HasPrefix(line, "data:"):
278+
evt.Data = []byte(strings.TrimSpace(strings.TrimPrefix(line, "data:")))
279+
case strings.HasPrefix(line, "event:"):
280+
evt.Event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
281+
case strings.HasPrefix(line, "id:"):
282+
evt.ID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
283+
case strings.HasPrefix(line, "\n"):
284+
if !yield(evt, nil) {
285+
return
286+
}
287+
evt = Event{}
288+
default:
289+
_ = yield(Event{}, fmt.Errorf("unknown line: '%s'", line))
290+
return
291+
}
292+
}
293+
}
294+
}
295+
296+
type Event struct {
297+
ID string
298+
Event string
299+
Data []byte // json
300+
}
301+
302+
func waitForPort(t *testing.T, host string, timeout time.Duration) { // nolint:unparam
303+
t.Helper()
304+
deadline := time.Now().Add(timeout)
305+
for time.Now().Before(deadline) {
306+
conn, err := net.DialTimeout("tcp", host, 500*time.Millisecond)
307+
if err == nil {
308+
_ = conn.Close()
309+
t.Logf("Server is up on %s", host)
310+
return
311+
}
312+
time.Sleep(200 * time.Millisecond)
313+
}
314+
t.Fatalf("Server at %s did not start within %v", host, timeout)
315+
}
316+
317+
func waitForUpgrade(t *testing.T, host string) {
318+
t.Helper()
319+
320+
url := fmt.Sprintf("http://%s/v1/system/update/events", host)
321+
322+
itr := NewSSEClient(t.Context(), "GET", url)
323+
for event, err := range itr {
324+
require.NoError(t, err)
325+
t.Logf("Received event: ID=%s, Event=%s, Data=%s\n", event.ID, event.Event, string(event.Data))
326+
if event.Event == "restarting" {
327+
break
328+
}
329+
}
330+
331+
}

0 commit comments

Comments
 (0)