diff --git a/Taskfile.yml b/Taskfile.yml index 8f2e49c0..a0effb5b 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -57,6 +57,10 @@ tasks: test:update: cmds: - go test --timeout 30m -v ./internal/e2e/updatetest + #todo se funziona rimuovere e integrare con il comando qui sopra! + test:start: + cmds: + - go test --timeout 10m -v ./internal/e2e/startapp test:pkg: desc: Run only tests in the pkg directory diff --git a/internal/e2e/startapp/create-mock-devices.sh b/internal/e2e/startapp/create-mock-devices.sh new file mode 100644 index 00000000..7035d550 --- /dev/null +++ b/internal/e2e/startapp/create-mock-devices.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +# Mock GPU path +mkdir -p /dev/dri || true +if [ ! -e /dev/dri/renderD128 ]; then + mknod /dev/dri/renderD128 c 226 128 || true +fi + +# Mock camera +if [ ! -e /dev/video0 ]; then + mknod /dev/video0 c 81 0 || true +fi + +# Continue with the actual CMD +exec "$@" diff --git a/internal/e2e/startapp/helper.go b/internal/e2e/startapp/helper.go new file mode 100644 index 00000000..ba0cdc89 --- /dev/null +++ b/internal/e2e/startapp/helper.go @@ -0,0 +1,279 @@ +package startapp + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "iter" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "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 buildDockerImage(t *testing.T, dockerfile, name, arch string) { + t.Helper() + + archArg := fmt.Sprintf("ARCH=%s", arch) + + cmd := exec.Command("docker", "build", + "--build-arg", archArg, + "-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:/host/docker.sock", + "-e", "DOCKER_HOST=unix:///host/docker.sock", + "--name", containerName, + containerImageName, + ) + + if err := cmd.Run(); err != nil { + t.Fatalf("failed to run container: %v", err) + } + +} + +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 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 runAppStart(t *testing.T, containerName, appName string) { + t.Helper() + fixCmd := exec.Command("docker", "exec", containerName, + "chmod", "666", "/host/docker.sock") + + outputFix, errFix := fixCmd.CombinedOutput() + require.NoError(t, errFix, "Failed to chmod docker socket: %s", outputFix) + appCommand := fmt.Sprintf("/usr/bin/arduino-app-cli app start %s", appName) + + cmd := exec.Command( + "docker", "exec", + containerName, + "su", "arduino", "-c", appCommand, + ) + + output, err := cmd.CombinedOutput() + require.NoError(t, err, "Start command failed: %s", output) + t.Logf("Output comando 'start': %s", output) +} + +func checkContainerRunningOnHost(t *testing.T, appContainerName string) bool { + t.Helper() + cmd := exec.Command( + "docker", "ps", + "--filter", "name=^/"+appContainerName+"$", + "--format", "{{.Names}}", + ) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + + err := cmd.Run() + require.NoError(t, err, "fallito controllo 'docker ps' sull'host") + + return strings.TrimSpace(stdout.String()) == appContainerName +} +func stopAppContainer(t *testing.T, appContainerName string) { + t.Helper() + stopDockerContainer(t, appContainerName) +} + +func postCreateApp(t *testing.T, host string) { + t.Helper() + + url := fmt.Sprintf("http://%s/v1/apps?skip-python=false&skip-sketch=true", host) + + payload := `{"name": "HelloWorld","description": "My HelloWorld description","icon": ""}` + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(payload)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, 200, resp.StatusCode) + + body, _ := io.ReadAll(resp.Body) + t.Logf("Response body: %s", body) +} diff --git a/internal/e2e/startapp/start_test.go b/internal/e2e/startapp/start_test.go new file mode 100644 index 00000000..a22e571f --- /dev/null +++ b/internal/e2e/startapp/start_test.go @@ -0,0 +1,67 @@ +package startapp + +import ( + "fmt" + "os" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var arch = runtime.GOARCH + +const ( + dockerFile = "test.Dockerfile" + daemonHost = "127.0.0.1:8800" + versionToTest = "v1.0.0" +) + +func TestStartApp(t *testing.T) { + fmt.Printf("***** ARCH %s ***** \n", arch) + + t.Cleanup(func() { os.RemoveAll("build") }) + + fmt.Printf("Building local deb version %s \n", versionToTest) + buildDebVersion(t, "build", versionToTest, arch) + + fmt.Println("Fetching 'arduino-router' dependency...") + fetchDebPackageLatest(t, "build", "arduino-router") + + const dockerImageName = "e2e-start-test-image" + fmt.Println("**** BUILD docker image (e2e-test-runner) *****") + buildDockerImage(t, dockerFile, dockerImageName, arch) + + t.Run("Test App Start Command", func(t *testing.T) { + + const containerA_Name = "e2e-test-runner" + const appToStart = "user:helloworld" + + t.Cleanup(func() { + //stopDockerContainer(t, containerA_Name) + //stopAppContainer(t, appToStart) + }) + + fmt.Println("**** RUN docker image (board container) *****") + startDockerContainer(t, containerA_Name, dockerImageName) + + waitForPort(t, daemonHost, 20*time.Second) + + fmt.Println("**** Creating user app 'user:helloworld' *****") + postCreateApp(t, daemonHost) + + fmt.Printf("**** Telling e2e-test-runner to start app '%s' *****\n", appToStart) + runAppStart(t, containerA_Name, appToStart) + + fmt.Printf("**** Verifying on HOST if '%s' ( app to start) is running *****\n", appToStart) + + time.Sleep(1 * time.Second) + + isRunning := checkContainerRunningOnHost(t, appToStart) + + require.True(t, isRunning, "Il container B (%s) not foud", appToStart) + + fmt.Printf("Success: e2e-test-runner successfully launched app to start (%s) on host.\n", appToStart) + }) +} diff --git a/internal/e2e/startapp/test.Dockerfile b/internal/e2e/startapp/test.Dockerfile new file mode 100644 index 00000000..690a1a30 --- /dev/null +++ b/internal/e2e/startapp/test.Dockerfile @@ -0,0 +1,31 @@ +FROM debian:trixie + +# Install packages first +RUN apt update && \ + apt install -y systemd systemd-sysv dbus \ + sudo docker.io docker-compose \ + ca-certificates curl gnupg \ + dpkg-dev apt-utils adduser gzip && \ + rm -rf /var/lib/apt/lists/* + +ARG ARCH=amd64 + +COPY build/arduino-app-cli*_${ARCH}.deb /tmp/app.deb +COPY build/arduino-router*_${ARCH}.deb /tmp/router.deb + +RUN apt update && apt install -y /tmp/router.deb /tmp/app.deb \ + && rm /tmp/app.deb /tmp/router.deb + +RUN usermod -s /bin/bash arduino || true +RUN mkdir -p /home/arduino && chown -R arduino:arduino /home/arduino +RUN usermod -aG docker arduino + +# Copy + enable mock devices script +COPY create-mock-devices.sh /usr/local/bin/create-mock-devices.sh +RUN chmod +x /usr/local/bin/create-mock-devices.sh + +EXPOSE 8800 + +# ENTRYPOINT must remain last +ENTRYPOINT ["/usr/local/bin/create-mock-devices.sh"] +CMD ["/sbin/init"]