diff --git a/.github/workflows/test-update.yml b/.github/workflows/test-update.yml new file mode 100644 index 00000000..9b5dc6bf --- /dev/null +++ b/.github/workflows/test-update.yml @@ -0,0 +1,29 @@ +name: test the system update flow + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-and-update: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run dep package update test + env: + GH_TOKEN: ${{ secrets.ARDUINOBOT_TOKEN }} + run: | + go tool task test:update diff --git a/Taskfile.yml b/Taskfile.yml index d11bae23..8f2e49c0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -50,9 +50,13 @@ tasks: test:internal: cmds: - - go build ./cmd/arduino-app-cli # needed for e2e tests + - go build ./cmd/arduino-app-cli - task: generate - - go test ./internal/... ./cmd/... -v -race {{ .CLI_ARGS }} + - go test $(go list ./internal/... ./cmd/... | grep -v internal/e2e/updatetest) -v -race {{ .CLI_ARGS }} + + test:update: + cmds: + - go test --timeout 30m -v ./internal/e2e/updatetest test:pkg: desc: Run only tests in the pkg directory @@ -102,9 +106,10 @@ tasks: deps: - build-deb:clone-examples cmds: - - 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 . + - 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 . vars: ARCH: '{{.ARCH | default "arm64"}}' + OUTPUT: '{{.OUTPUT | default "./build"}}' build-deb:clone-examples: desc: "Clones the examples repo directly into the debian structure" diff --git a/cmd/arduino-app-cli/app/app.go b/cmd/arduino-app-cli/app/app.go index 3d6b6341..ba7ffb69 100644 --- a/cmd/arduino-app-cli/app/app.go +++ b/cmd/arduino-app-cli/app/app.go @@ -38,7 +38,6 @@ func NewAppCmd(cfg config.Configuration) *cobra.Command { appCmd.AddCommand(newRestartCmd(cfg)) appCmd.AddCommand(newLogsCmd(cfg)) appCmd.AddCommand(newListCmd(cfg)) - appCmd.AddCommand(newPsCmd()) appCmd.AddCommand(newMonitorCmd(cfg)) return appCmd diff --git a/cmd/arduino-app-cli/app/ps.go b/cmd/arduino-app-cli/app/ps.go deleted file mode 100644 index 6549b2b9..00000000 --- a/cmd/arduino-app-cli/app/ps.go +++ /dev/null @@ -1,30 +0,0 @@ -// This file is part of arduino-app-cli. -// -// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) -// -// This software is released under the GNU General Public License version 3, -// which covers the main part of arduino-app-cli. -// The terms of this license can be found at: -// https://www.gnu.org/licenses/gpl-3.0.en.html -// -// You can be released from the requirements of the above licenses by purchasing -// a commercial license. Buying such a license is mandatory if you want to -// modify or otherwise use the software for commercial activities involving the -// Arduino software without disclosing the source code of your own applications. -// To purchase a commercial license, send an email to license@arduino.cc. - -package app - -import ( - "github.com/spf13/cobra" -) - -func newPsCmd() *cobra.Command { - return &cobra.Command{ - Use: "ps", - Short: "Shows the list of running Arduino Apps", - RunE: func(cmd *cobra.Command, args []string) error { - panic("not implemented") - }, - } -} diff --git a/cmd/arduino-app-cli/config/config.go b/cmd/arduino-app-cli/config/config.go index 635ad2bd..f6367fd3 100644 --- a/cmd/arduino-app-cli/config/config.go +++ b/cmd/arduino-app-cli/config/config.go @@ -29,7 +29,7 @@ import ( func NewConfigCmd(cfg config.Configuration) *cobra.Command { appCmd := &cobra.Command{ Use: "config", - Short: "Manage arduino-app-cli config", + Short: "Manage Arduino App CLI config", } appCmd.AddCommand(newConfigGetCmd(cfg)) diff --git a/cmd/arduino-app-cli/daemon/daemon.go b/cmd/arduino-app-cli/daemon/daemon.go index f96a4e0c..eeac105c 100644 --- a/cmd/arduino-app-cli/daemon/daemon.go +++ b/cmd/arduino-app-cli/daemon/daemon.go @@ -38,7 +38,7 @@ import ( func NewDaemonCmd(cfg config.Configuration, version string) *cobra.Command { daemonCmd := &cobra.Command{ Use: "daemon", - Short: "Run an HTTP server to expose arduino-app-cli functionality through REST API", + Short: "Run the Arduino App CLI as an HTTP daemon", Run: func(cmd *cobra.Command, args []string) { daemonPort, _ := cmd.Flags().GetString("port") diff --git a/cmd/arduino-app-cli/main.go b/cmd/arduino-app-cli/main.go index e859aae2..c9dfddca 100644 --- a/cmd/arduino-app-cli/main.go +++ b/cmd/arduino-app-cli/main.go @@ -48,7 +48,7 @@ func run(configuration cfg.Configuration) error { defer func() { _ = servicelocator.CloseDockerClient() }() rootCmd := &cobra.Command{ Use: "arduino-app-cli", - Short: "A CLI to manage the Python app", + Short: "A CLI to manage Arduino Apps", PersistentPreRun: func(cmd *cobra.Command, args []string) { format, ok := feedback.ParseOutputFormat(format) if !ok { diff --git a/cmd/arduino-app-cli/system/system.go b/cmd/arduino-app-cli/system/system.go index dd9986de..54c89128 100644 --- a/cmd/arduino-app-cli/system/system.go +++ b/cmd/arduino-app-cli/system/system.go @@ -37,7 +37,8 @@ import ( func NewSystemCmd(cfg config.Configuration) *cobra.Command { cmd := &cobra.Command{ - Use: "system", + Use: "system", + Short: "Manage the board’s system configuration", } cmd.AddCommand(newDownloadImageCmd(cfg)) diff --git a/cmd/arduino-app-cli/version/version.go b/cmd/arduino-app-cli/version/version.go index 86ed7b3c..1cb06d05 100644 --- a/cmd/arduino-app-cli/version/version.go +++ b/cmd/arduino-app-cli/version/version.go @@ -16,34 +16,95 @@ package version import ( + "encoding/json" "fmt" + "net" + "net/http" + "net/url" + "time" "github.com/spf13/cobra" "github.com/arduino/arduino-app-cli/cmd/feedback" ) -func NewVersionCmd(version string) *cobra.Command { +// The actual listening address for the daemon +// is defined in the installation package +const ( + DefaultHostname = "localhost" + DefaultPort = "8800" + ProgramName = "Arduino App CLI" +) + +func NewVersionCmd(clientVersion string) *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: "Print the version number of Arduino App CLI", Run: func(cmd *cobra.Command, args []string) { - feedback.PrintResult(versionResult{ - AppName: "Arduino App CLI", - Version: version, - }) + port, _ := cmd.Flags().GetString("port") + + daemonVersion, err := getDaemonVersion(http.Client{}, port) + if err != nil { + feedback.Warnf("Warning: cannot get the running daemon version on %s:%s\n", DefaultHostname, port) + } + + result := versionResult{ + Name: ProgramName, + Version: clientVersion, + DaemonVersion: daemonVersion, + } + + feedback.PrintResult(result) }, } + cmd.Flags().String("port", DefaultPort, "The daemon network port") return cmd } +func getDaemonVersion(httpClient http.Client, port string) (string, error) { + + httpClient.Timeout = time.Second + + url := url.URL{ + Scheme: "http", + Host: net.JoinHostPort(DefaultHostname, port), + Path: "/v1/version", + } + + resp, err := httpClient.Get(url.String()) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code received") + } + + var daemonResponse struct { + Version string `json:"version"` + } + if err := json.NewDecoder(resp.Body).Decode(&daemonResponse); err != nil { + return "", err + } + + return daemonResponse.Version, nil +} + type versionResult struct { - AppName string `json:"appName"` - Version string `json:"version"` + Name string `json:"name"` + Version string `json:"version"` + DaemonVersion string `json:"daemon_version,omitempty"` } func (r versionResult) String() string { - return fmt.Sprintf("%s v%s", r.AppName, r.Version) + resultMessage := fmt.Sprintf("%s version %s", ProgramName, r.Version) + + if r.DaemonVersion != "" { + resultMessage = fmt.Sprintf("%s\ndaemon version: %s", + resultMessage, r.DaemonVersion) + } + return resultMessage } func (r versionResult) Data() interface{} { diff --git a/cmd/arduino-app-cli/version/version_test.go b/cmd/arduino-app-cli/version/version_test.go new file mode 100644 index 00000000..39617968 --- /dev/null +++ b/cmd/arduino-app-cli/version/version_test.go @@ -0,0 +1,125 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-app-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package version + +import ( + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDaemonVersion(t *testing.T) { + testCases := []struct { + name string + serverStub Tripper + port string + expectedResult string + expectedErrorMessage string + }{ + { + name: "return the server version when the server is up", + serverStub: successServer, + port: "8800", + expectedResult: "3.0-server", + expectedErrorMessage: "", + }, + { + name: "return error if default server is not listening on default port", + serverStub: failureServer, + port: "8800", + expectedResult: "", + expectedErrorMessage: `Get "http://localhost:8800/v1/version": connection refused`, + }, + { + name: "return error if provided server is not listening on provided port", + serverStub: failureServer, + port: "1234", + expectedResult: "", + expectedErrorMessage: `Get "http://localhost:1234/v1/version": connection refused`, + }, + { + name: "return error for server response 500 Internal Server Error", + serverStub: failureInternalServerError, + port: "0", + expectedResult: "", + expectedErrorMessage: "unexpected status code received", + }, + + { + name: "return error for server up and wrong json response", + serverStub: successServerWrongJson, + port: "8800", + expectedResult: "", + expectedErrorMessage: "invalid character '<' looking for beginning of value", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // arrange + httpClient := http.Client{} + httpClient.Transport = tc.serverStub + + // act + result, err := getDaemonVersion(httpClient, tc.port) + + // assert + require.Equal(t, tc.expectedResult, result) + if err != nil { + require.Equal(t, tc.expectedErrorMessage, err.Error()) + } + }) + } +} + +// Leverage the http.Client's RoundTripper +// to return a canned response and bypass network calls. +type Tripper func(*http.Request) (*http.Response, error) + +func (t Tripper) RoundTrip(request *http.Request) (*http.Response, error) { + return t(request) +} + +var successServer = Tripper(func(*http.Request) (*http.Response, error) { + body := io.NopCloser(strings.NewReader(`{"version":"3.0-server"}`)) + return &http.Response{ + StatusCode: http.StatusOK, + Body: body, + }, nil +}) + +var successServerWrongJson = Tripper(func(*http.Request) (*http.Response, error) { + body := io.NopCloser(strings.NewReader(`&2 + exit 1 +fi + + +if [ ! -w "$TARGET_FILE" ]; then + echo "Error: Target file $TARGET_FILE not found or not writable." >&2 + exit 1 +fi + +SERIAL_NUMBER=$(cat "$SERIAL_NUMBER_PATH") + +if [ -z "$SERIAL_NUMBER" ]; then + echo "Error: Serial number file is empty." >&2 + exit 1 +fi + +if grep -q "serial_number=" "$TARGET_FILE"; then + echo "Serial number ($SERIAL_NUMBER) already configured." + exit 0 +fi + +echo "Adding serial number to $TARGET_FILE..." +sed -i "/<\/service>/i serial_number=${SERIAL_NUMBER}<\/txt-record>" "$TARGET_FILE" + +echo "Avahi configuration attempt finished." +exit 0 \ No newline at end of file diff --git a/internal/e2e/updatetest/helpers.go b/internal/e2e/updatetest/helpers.go new file mode 100644 index 00000000..410a9999 --- /dev/null +++ b/internal/e2e/updatetest/helpers.go @@ -0,0 +1,331 @@ +package updatetest + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "iter" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func fetchDebPackageLatest(t *testing.T, path, repo string) string { + t.Helper() + + repo = fmt.Sprintf("github.com/arduino/%s", repo) + cmd := exec.Command( + "gh", "release", "list", + "--repo", repo, + "--exclude-pre-releases", + "--limit", "1", + ) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("command failed: %v\nOutput: %s", err, output) + } + + fmt.Println(string(output)) + + fields := strings.Fields(string(output)) + if len(fields) == 0 { + log.Fatal("could not parse tag from gh release list output") + } + tag := fields[0] + + fmt.Println("Detected tag:", tag) + cmd2 := exec.Command( + "gh", "release", "download", + tag, + "--repo", repo, + "--pattern", "*.deb", + "--dir", path, + ) + + out, err := cmd2.CombinedOutput() + if err != nil { + log.Fatalf("download failed: %v\nOutput: %s", err, out) + } + + return tag + +} + +func buildDebVersion(t *testing.T, storePath, tagVersion, arch string) { + t.Helper() + cwd, err := os.Getwd() + if err != nil { + panic(err) + } + outputDir := filepath.Join(cwd, storePath) + + tagVersion = fmt.Sprintf("VERSION=%s", tagVersion) + arch = fmt.Sprintf("ARCH=%s", arch) + outputDir = fmt.Sprintf("OUTPUT=%s", outputDir) + + cmd := exec.Command( + "go", "tool", "task", "build-deb", + tagVersion, + arch, + outputDir, + ) + + if err := cmd.Run(); err != nil { + log.Fatalf("failed to run build command: %v", err) + } +} + +func genMajorTag(t *testing.T, tag string) string { + t.Helper() + + parts := strings.Split(tag, ".") + last := parts[len(parts)-1] + + lastNum, _ := strconv.Atoi(strings.TrimPrefix(last, "v")) + lastNum++ + + parts[len(parts)-1] = strconv.Itoa(lastNum) + newTag := strings.Join(parts, ".") + + return newTag +} + +func genMinorTag(t *testing.T, tag string) string { + t.Helper() + + parts := strings.Split(tag, ".") + last := parts[len(parts)-1] + + lastNum, _ := strconv.Atoi(strings.TrimPrefix(last, "v")) + if lastNum > 0 { + lastNum-- + } + + parts[len(parts)-1] = strconv.Itoa(lastNum) + newTag := strings.Join(parts, ".") + + if !strings.HasPrefix(newTag, "v") { + newTag = "v" + newTag + } + return newTag +} + +func buildDockerImage(t *testing.T, dockerfile, name, arch string) { + t.Helper() + + arch = fmt.Sprintf("ARCH=%s", arch) + + cmd := exec.Command("docker", "build", "--build-arg", arch, "-t", name, "-f", dockerfile, ".") + // Capture both stdout and stderr + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + fmt.Printf("❌ Docker build failed: %v\n", err) + fmt.Printf("---- STDERR ----\n%s\n", stderr.String()) + fmt.Printf("---- STDOUT ----\n%s\n", out.String()) + return + } + + fmt.Println("✅ Docker build succeeded!") +} + +func startDockerContainer(t *testing.T, containerName string, containerImageName string) { + t.Helper() + + cmd := exec.Command( + "docker", "run", "--rm", "-d", + "-p", "8800:8800", + "--privileged", + "--cgroupns=host", + "--network", "host", + "-v", "/sys/fs/cgroup:/sys/fs/cgroup:rw", + "-v", "/var/run/docker.sock:/var/run/docker.sock", + "-e", "DOCKER_HOST=unix:///var/run/docker.sock", + "--name", containerName, + containerImageName, + ) + + if err := cmd.Run(); err != nil { + t.Fatalf("failed to run container: %v", err) + } + +} + +func getAppCliVersion(t *testing.T, containerName string) string { + t.Helper() + + cmd := exec.Command( + "docker", "exec", + "--user", "arduino", + containerName, + "arduino-app-cli", "version", "--format", "json", + ) + output, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("command failed: %v\nOutput: %s", err, output) + } + + var version struct { + Version string `json:"version"` + DaemonVersion string `json:"daemon_version"` + } + err = json.Unmarshal(output, &version) + require.NoError(t, err) + // TODO to enable after 0.6.7 + // require.Equal(t, version.Version, version.DaemonVersion, "client and daemon versions should match") + require.NotEmpty(t, version.Version) + return version.Version + +} + +func runSystemUpdate(t *testing.T, containerName string) { + t.Helper() + + cmd := exec.Command( + "docker", "exec", + "--user", "arduino", + containerName, + "arduino-app-cli", "system", "update", "--yes", + ) + output, err := cmd.CombinedOutput() + require.NoError(t, err, "system update failed: %s", output) + t.Logf("system update output: %s", output) +} + +func stopDockerContainer(t *testing.T, containerName string) { + t.Helper() + + cleanupCmd := exec.Command("docker", "rm", "-f", containerName) + + fmt.Println("🧹 Removing Docker container " + containerName) + if err := cleanupCmd.Run(); err != nil { + fmt.Printf("⚠️ Warning: could not remove container (might not exist): %v\n", err) + } + +} + +func putUpdateRequest(t *testing.T, host string) { + + t.Helper() + + url := fmt.Sprintf("http://%s/v1/system/update/apply", host) + + req, err := http.NewRequest(http.MethodPut, url, nil) + if err != nil { + log.Fatalf("Error creating request: %v", err) + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + } + defer resp.Body.Close() + + require.Equal(t, 202, resp.StatusCode) + +} + +func NewSSEClient(ctx context.Context, method, url string) iter.Seq2[Event, error] { + return func(yield func(Event, error) bool) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + _ = yield(Event{}, err) + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + _ = yield(Event{}, err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + _ = yield(Event{}, fmt.Errorf("got response status code %d", resp.StatusCode)) + return + } + + reader := bufio.NewReader(resp.Body) + + evt := Event{} + for { + line, err := reader.ReadString('\n') + if err != nil { + _ = yield(Event{}, err) + return + } + switch { + case strings.HasPrefix(line, "data:"): + evt.Data = []byte(strings.TrimSpace(strings.TrimPrefix(line, "data:"))) + case strings.HasPrefix(line, "event:"): + evt.Event = strings.TrimSpace(strings.TrimPrefix(line, "event:")) + case strings.HasPrefix(line, "id:"): + evt.ID = strings.TrimSpace(strings.TrimPrefix(line, "id:")) + case strings.HasPrefix(line, "\n"): + if !yield(evt, nil) { + return + } + evt = Event{} + default: + _ = yield(Event{}, fmt.Errorf("unknown line: '%s'", line)) + return + } + } + } +} + +type Event struct { + ID string + Event string + Data []byte // json +} + +func waitForPort(t *testing.T, host string, timeout time.Duration) { // nolint:unparam + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", host, 500*time.Millisecond) + if err == nil { + _ = conn.Close() + t.Logf("Server is up on %s", host) + return + } + time.Sleep(200 * time.Millisecond) + } + t.Fatalf("Server at %s did not start within %v", host, timeout) +} + +func waitForUpgrade(t *testing.T, host string) { + t.Helper() + + url := fmt.Sprintf("http://%s/v1/system/update/events", host) + + itr := NewSSEClient(t.Context(), "GET", url) + for event, err := range itr { + require.NoError(t, err) + t.Logf("Received event: ID=%s, Event=%s, Data=%s\n", event.ID, event.Event, string(event.Data)) + if event.Event == "restarting" { + break + } + } + +} diff --git a/internal/e2e/updatetest/test.Dockerfile b/internal/e2e/updatetest/test.Dockerfile new file mode 100644 index 00000000..578e71c0 --- /dev/null +++ b/internal/e2e/updatetest/test.Dockerfile @@ -0,0 +1,33 @@ +FROM debian:trixie + +RUN apt update && \ + apt install -y systemd systemd-sysv dbus \ + sudo docker.io ca-certificates curl gnupg \ + dpkg-dev apt-utils adduser gzip && \ + rm -rf /var/lib/apt/lists/* + +ARG ARCH=amd64 + +COPY build/stable/arduino-app-cli*_${ARCH}.deb /tmp/stable.deb +COPY build/arduino-app-cli*_${ARCH}.deb /tmp/unstable.deb +COPY build/stable/arduino-router*_${ARCH}.deb /tmp/router.deb + +RUN apt update && apt install -y /tmp/stable.deb /tmp/router.deb \ + && rm /tmp/stable.deb /tmp/router.deb \ + && mkdir -p /var/www/html/myrepo/dists/trixie/main/binary-${ARCH} \ + && mv /tmp/unstable.deb /var/www/html/myrepo/dists/trixie/main/binary-${ARCH}/ + +WORKDIR /var/www/html/myrepo +RUN dpkg-scanpackages dists/trixie/main/binary-${ARCH} /dev/null | gzip -9c > dists/trixie/main/binary-${ARCH}/Packages.gz +WORKDIR / + +RUN usermod -s /bin/bash arduino || true +RUN mkdir -p /home/arduino && chown -R arduino:arduino /home/arduino +RUN usermod -aG docker arduino + +RUN echo "deb [trusted=yes arch=${ARCH}] file:/var/www/html/myrepo trixie main" \ + > /etc/apt/sources.list.d/my-mock-repo.list + +EXPOSE 8800 +# CMD: systemd must be PID 1 +CMD ["/sbin/init"] diff --git a/internal/e2e/updatetest/update_test.go b/internal/e2e/updatetest/update_test.go new file mode 100644 index 00000000..061a8ff6 --- /dev/null +++ b/internal/e2e/updatetest/update_test.go @@ -0,0 +1,122 @@ +package updatetest + +import ( + "fmt" + "os" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var arch = runtime.GOARCH + +const dockerFile = "test.Dockerfile" +const daemonHost = "127.0.0.1:8800" + +func TestUpdatePackage(t *testing.T) { + fmt.Printf("***** ARCH %s ***** \n", arch) + + t.Run("Stable To Current", func(t *testing.T) { + t.Cleanup(func() { os.RemoveAll("build") }) + + tagAppCli := fetchDebPackageLatest(t, "build/stable", "arduino-app-cli") + fetchDebPackageLatest(t, "build/stable", "arduino-router") + majorTag := genMajorTag(t, tagAppCli) + + fmt.Printf("Updating from stable version %s to unstable version %s \n", tagAppCli, majorTag) + fmt.Printf("Building local deb version %s \n", majorTag) + buildDebVersion(t, "build", majorTag, arch) + + const dockerImageName = "apt-test-update-image" + fmt.Println("**** BUILD docker image *****") + buildDockerImage(t, dockerFile, dockerImageName, arch) + //TODO: t cleanup remove docker image + + t.Run("CLI Command", func(t *testing.T) { + const containerName = "apt-test-update" + t.Cleanup(func() { stopDockerContainer(t, containerName) }) + + fmt.Println("**** RUN docker image *****") + startDockerContainer(t, containerName, dockerImageName) + waitForPort(t, daemonHost, 5*time.Second) + + preUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+preUpdateVersion, tagAppCli) + runSystemUpdate(t, containerName) + postUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+postUpdateVersion, majorTag) + }) + + t.Run("HTTP Request", func(t *testing.T) { + const containerName = "apt-test-update-http" + t.Cleanup(func() { stopDockerContainer(t, containerName) }) + + startDockerContainer(t, containerName, dockerImageName) + waitForPort(t, daemonHost, 5*time.Second) + + preUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+preUpdateVersion, tagAppCli) + + putUpdateRequest(t, daemonHost) + waitForUpgrade(t, daemonHost) + + postUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+postUpdateVersion, majorTag) + }) + + }) + + t.Run("CurrentToStable", func(t *testing.T) { + t.Cleanup(func() { os.RemoveAll("build") }) + + tagAppCli := fetchDebPackageLatest(t, "build", "arduino-app-cli") + fetchDebPackageLatest(t, "build/stable", "arduino-router") + minorTag := genMinorTag(t, tagAppCli) + + fmt.Printf("Updating from unstable version %s to stable version %s \n", minorTag, tagAppCli) + fmt.Printf("Building local deb version %s \n", minorTag) + buildDebVersion(t, "build/stable", minorTag, arch) + + fmt.Println("**** BUILD docker image *****") + const dockerImageName = "test-apt-update-unstable-image" + + buildDockerImage(t, dockerFile, dockerImageName, arch) + //TODO: t cleanup remove docker image + + t.Run("CLI Command", func(t *testing.T) { + const containerName = "apt-test-update-unstable" + t.Cleanup(func() { stopDockerContainer(t, containerName) }) + + fmt.Println("**** RUN docker image *****") + startDockerContainer(t, containerName, dockerImageName) + waitForPort(t, daemonHost, 5*time.Second) + + preUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+preUpdateVersion, minorTag) + runSystemUpdate(t, containerName) + postUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+postUpdateVersion, tagAppCli) + }) + + t.Run("HTTP Request", func(t *testing.T) { + const containerName = "apt-test-update--unstable-http" + t.Cleanup(func() { stopDockerContainer(t, containerName) }) + + startDockerContainer(t, containerName, dockerImageName) + waitForPort(t, daemonHost, 5*time.Second) + + preUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+preUpdateVersion, minorTag) + + putUpdateRequest(t, daemonHost) + waitForUpgrade(t, daemonHost) + + postUpdateVersion := getAppCliVersion(t, containerName) + require.Equal(t, "v"+postUpdateVersion, tagAppCli) + }) + + }) + +} diff --git a/pkg/board/board.go b/pkg/board/board.go index 9669a3b3..3dac4af2 100644 --- a/pkg/board/board.go +++ b/pkg/board/board.go @@ -186,7 +186,6 @@ func FromFQBN(ctx context.Context, fqbn string) ([]Board, error) { switch port.GetPort().GetProtocol() { case SerialProtocol: serial := strings.ToLower(port.GetPort().GetHardwareId()) // in windows this is uppercase. - // TODO: we should store the board custom name in the product id so we can get it from the discovery service. var customName string if conn, err := adb.FromSerial(serial, ""); err == nil { @@ -211,10 +210,15 @@ func FromFQBN(ctx context.Context, fqbn string) ([]Board, error) { } customName = name[:idx] } + var serial string + if sn, ok := port.GetPort().GetProperties()["serial_number"]; ok { + serial = sn + } boards = append(boards, Board{ Protocol: NetworkProtocol, Address: port.GetPort().GetAddress(), + Serial: serial, BoardName: boardName, CustomName: customName, })