diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 4d042b3..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -tests/bin/chip-all-clusters-minimal-app-* filter=lfs diff=lfs merge=lfs -text diff --git a/tests/README.md b/tests/README.md index 0593eb3..319ec7f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,8 +9,19 @@ where: - `-failfast` makes the test stop after first failure - `-count 1` is to avoid Go test caching for example when testing a rebuilt snap + +## Run Thread tests +For running Thread tests, two Radio Co-Processors (RCPs) are needed for both local and remote machines. + +For building and flashing RCP firmware, please refer to [Build and flash RCP firmware on nRF52480 dongle](https://github.com/canonical/openthread-border-router-snap/wiki/Setup-OpenThread-Border-Router-with-nRF52840-Dongle#build-and-flash-rcp-firmware-on-nrf52480-dongle). + +```bash +LOCAL_INFRA_IF="eno1" REMOTE_INFRA_IF="eth0" REMOTE_USER="ubuntu" REMOTE_PASSWORD="abcdef" REMOTE_HOST="192.168.178.95" go test -v -failfast -count 1 ./thread_tests +``` + ## Environment variables Some environment variables can modify the test functionality. Refer to these in [the documentation](https://pkg.go.dev/github.com/canonical/matter-snap-testing/env) of the `matter-snap-testing` Go package. + diff --git a/tests/bin/chip-all-clusters-minimal-app-commit-1536ca2 b/tests/bin/chip-all-clusters-minimal-app-commit-1536ca2 deleted file mode 100755 index 2e456dc..0000000 --- a/tests/bin/chip-all-clusters-minimal-app-commit-1536ca2 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:29a03592355d375f14e8d5102d8b17c82c6c80ff0a8698adc79833d0360618e1 -size 98795128 diff --git a/tests/common.go b/tests/common.go new file mode 100644 index 0000000..7996236 --- /dev/null +++ b/tests/common.go @@ -0,0 +1,33 @@ +package tests + +import ( + "testing" + + "github.com/canonical/matter-snap-testing/utils" + "github.com/stretchr/testify/require" +) + +func InstallChipTool(t *testing.T) { + const chipToolSnap = "chip-tool" + + // clean + utils.SnapRemove(t, chipToolSnap) + + if utils.LocalServiceSnap() { + require.NoError(t, + utils.SnapInstallFromFile(nil, utils.LocalServiceSnapPath), + ) + } else { + require.NoError(t, + utils.SnapInstallFromStore(nil, chipToolSnap, utils.ServiceChannel), + ) + } + t.Cleanup(func() { + utils.SnapRemove(t, chipToolSnap) + }) + + // connect interfaces + utils.SnapConnect(t, chipToolSnap+":avahi-observe", "") + utils.SnapConnect(t, chipToolSnap+":bluez", "") + utils.SnapConnect(t, chipToolSnap+":process-control", "") +} diff --git a/tests/go.mod b/tests/go.mod index 89e656e..c5068f7 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -5,11 +5,13 @@ go 1.21.6 require ( github.com/canonical/matter-snap-testing v1.0.0-beta.1 github.com/stretchr/testify v1.8.4 + golang.org/x/crypto v0.20.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.17.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tests/go.sum b/tests/go.sum index 498734b..cf5ed90 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -11,6 +11,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tests/snap_test.go b/tests/snap_test.go deleted file mode 100644 index b8d5cd6..0000000 --- a/tests/snap_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package tests - -import ( - "context" - "log" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/canonical/matter-snap-testing/utils" - "github.com/stretchr/testify/assert" -) - -const ( - allClustersAppBin = "bin/chip-all-clusters-minimal-app-commit-1536ca2" - allClustersAppLog = "chip-all-clusters-minimal-app.log" -) - -func TestMain(m *testing.M) { - teardown, err := setup() - if err != nil { - log.Fatalf("Failed to setup tests: %s", err) - } - - code := m.Run() - teardown() - - os.Exit(code) -} - -func TestAllClustersApp(t *testing.T) { - startAllClustersApp(t) - - // wait for startup - waitForLogMessage(t, - allClustersAppLog, "CHIP minimal mDNS started advertising") - - t.Run("Commission", func(t *testing.T) { - stdout, _, _ := utils.Exec(t, "sudo chip-tool pairing onnetwork 110 20202021 2>&1") - assert.NoError(t, - os.WriteFile("chip-tool-pairing.log", []byte(stdout), 0644), - ) - }) - - t.Run("Control", func(t *testing.T) { - stdout, _, _ := utils.Exec(t, "sudo chip-tool onoff toggle 110 1 2>&1") - assert.NoError(t, - os.WriteFile("chip-tool-onoff.log", []byte(stdout), 0644), - ) - - waitForLogMessage(t, - allClustersAppLog, "CHIP:ZCL: Toggle ep1 on/off") - }) - -} - -func setup() (teardown func(), err error) { - const chipToolSnap = "chip-tool" - - log.Println("[CLEAN]") - utils.SnapRemove(nil, chipToolSnap) - - log.Println("[SETUP]") - - teardown = func() { - log.Println("[TEARDOWN]") - - log.Println("Removing installed snap:", !utils.SkipTeardownRemoval) - if !utils.SkipTeardownRemoval { - utils.SnapRemove(nil, chipToolSnap) - } - } - - if utils.LocalServiceSnap() { - err = utils.SnapInstallFromFile(nil, utils.LocalServiceSnapPath) - } else { - err = utils.SnapInstallFromStore(nil, chipToolSnap, utils.ServiceChannel) - } - if err != nil { - teardown() - return - } - - // connect interfaces - utils.SnapConnect(nil, chipToolSnap+":avahi-observe", "") - utils.SnapConnect(nil, chipToolSnap+":bluez", "") - utils.SnapConnect(nil, chipToolSnap+":process-control", "") - - return -} - -func startAllClustersApp(t *testing.T) { - // remove existing temp files - utils.Exec(t, "rm -fr /tmp/chip_*") - - logFile, err := os.Create(allClustersAppLog) - if err != nil { - t.Fatalf("Error creating log file: %s", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - cmd := exec.CommandContext(ctx, allClustersAppBin) - cmd.Stdout = logFile - cmd.Stderr = logFile - - if err := cmd.Start(); err != nil { - t.Fatalf("Error starting application: %s", err) - } - - t.Cleanup(func() { - utils.Exec(t, "rm -f /tmp/chip_*") - }) -} - -func waitForLogMessage(t *testing.T, logPath, expectedMsg string) { - const maxRetry = 10 - - for i := 1; i <= maxRetry; i++ { - time.Sleep(1 * time.Second) - t.Logf("Retry %d/%d: Find log message: '%s'", i, maxRetry, expectedMsg) - - logs, err := os.ReadFile(logPath) - if err != nil { - t.Fatalf("Error reading log file: %s\n", err) - } - - if strings.Contains(string(logs), expectedMsg) { - t.Logf("Found log message: '%s'", expectedMsg) - return - } - } - - t.Fatalf("Time out: reached max %d retries.", maxRetry) -} diff --git a/tests/thread_tests/local.go b/tests/thread_tests/local.go new file mode 100644 index 0000000..e0e58ed --- /dev/null +++ b/tests/thread_tests/local.go @@ -0,0 +1,93 @@ +package thread_tests + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/canonical/matter-snap-testing/utils" + + tests "chip-tool-snap-tests" +) + +const ( + otbrSnap = "openthread-border-router" + OTCTL = otbrSnap + ".ot-ctl" +) + +func setup(t *testing.T) { + tests.InstallChipTool(t) + + const ( + defaultInfraInterfaceValue = "wlan0" + infraInterfaceKey = "infra-if" + localInfraInterfaceEnv = "LOCAL_INFRA_IF" + ) + + // Clean + utils.SnapRemove(t, otbrSnap) + + // Install OTBR + utils.SnapInstallFromStore(t, otbrSnap, utils.ServiceChannel) + t.Cleanup(func() { + utils.SnapRemove(t, otbrSnap) + }) + + // Connect interfaces + snapInterfaces := []string{"avahi-control", "firewall-control", "raw-usb", "network-control", "bluetooth-control", "bluez"} + for _, interfaceSlot := range snapInterfaces { + utils.SnapConnect(nil, otbrSnap+":"+interfaceSlot, "") + } + + // Set infra interface + if v := os.Getenv(localInfraInterfaceEnv); v != "" { + infraInterfaceValue := v + utils.SnapSet(nil, otbrSnap, infraInterfaceKey, infraInterfaceValue) + } else { + utils.SnapSet(nil, otbrSnap, infraInterfaceKey, defaultInfraInterfaceValue) + } + + // Start OTBR + start := time.Now() + utils.SnapStart(t, otbrSnap) + waitForLogMessage(t, otbrSnap, "Start Thread Border Agent: OK", start) + + // Form Thread network + utils.Exec(t, "sudo "+OTCTL+" dataset init new") + utils.Exec(t, "sudo "+OTCTL+" dataset commit active") + utils.Exec(t, "sudo "+OTCTL+" ifconfig up") + utils.Exec(t, "sudo "+OTCTL+" thread start") + utils.WaitForLogMessage(t, otbrSnap, "Thread Network", start) +} + +func getActiveDataset(t *testing.T) string { + activeDataset, _, _ := utils.Exec(t, "sudo "+OTCTL+" dataset active -x | awk '{print $NF}' | grep --invert-match \"Done\"") + trimmedActiveDataset := strings.TrimSpace(activeDataset) + + return trimmedActiveDataset +} + +// TODO: update the library function to print the tail before failing: +// https://github.com/canonical/matter-snap-testing/blob/abae29ac5e865f0c5208350bdab63cecb3bdcc5a/utils/config.go#L54-L69 +func waitForLogMessage(t *testing.T, snap, expectedLog string, since time.Time) { + const maxRetry = 10 + + for i := 1; i <= maxRetry; i++ { + time.Sleep(1 * time.Second) + t.Logf("Retry %d/%d: Waiting for expected content in logs: %s", i, maxRetry, expectedLog) + + logs := utils.SnapLogs(t, since, snap) + if strings.Contains(logs, expectedLog) { + t.Logf("Found expected content in logs: %s", expectedLog) + return + } + } + + t.Logf("Time out: reached max %d retries.", maxRetry) + stdout, _, _ := utils.Exec(t, + fmt.Sprintf("sudo journalctl --lines=10 --no-pager --unit=snap.\"%s\".otbr-agent --priority=notice", snap)) + t.Log(stdout) + t.FailNow() +} diff --git a/tests/thread_tests/remote.go b/tests/thread_tests/remote.go new file mode 100644 index 0000000..8247ab2 --- /dev/null +++ b/tests/thread_tests/remote.go @@ -0,0 +1,193 @@ +package thread_tests + +import ( + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "golang.org/x/crypto/ssh" +) + +var ( + remoteUser = "" + remotePassword = "" + remoteHost = "" + remoteInfraInterface = "" + + SSHClient *ssh.Client +) + +func remote_setup(t *testing.T) { + remote_loadEnvVars() + + connectSSH(t) + + remote_deployOTBRAgent(t) + + remote_deployAllClustersApp(t) +} + +func remote_loadEnvVars() { + const ( + remoteUserEnv = "REMOTE_USER" + remotePasswordEnv = "REMOTE_PASSWORD" + remoteHostEnv = "REMOTE_HOST" + remoteInfraInterfaceEnv = "REMOTE_INFRA_IF" + ) + + if v := os.Getenv(remoteUserEnv); v != "" { + remoteUser = v + } + + if v := os.Getenv(remotePasswordEnv); v != "" { + remotePassword = v + } + + if v := os.Getenv(remoteHostEnv); v != "" { + remoteHost = v + } + + if v := os.Getenv(remoteInfraInterfaceEnv); v != "" { + remoteInfraInterface = v + } +} + +func connectSSH(t *testing.T) { + if SSHClient != nil { + return + } + + config := &ssh.ClientConfig{ + User: remoteUser, + Auth: []ssh.AuthMethod{ + ssh.Password(remotePassword), + }, + Timeout: 10 * time.Second, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + var err error + SSHClient, err = ssh.Dial("tcp", remoteHost+":22", config) + if err != nil { + t.Fatalf("Failed to dial: %s", err) + } + + t.Cleanup(func() { + SSHClient.Close() + }) + + t.Logf("SSH: connected to %s", remoteHost) +} + +func remote_deployOTBRAgent(t *testing.T) { + + t.Cleanup(func() { + remote_exec(t, "sudo snap remove --purge openthread-border-router") + }) + + start := time.Now().UTC() + + commands := []string{ + "sudo snap remove --purge openthread-border-router", + "sudo snap install openthread-border-router --edge", + "sudo snap set openthread-border-router infra-if='" + remoteInfraInterface + "'", + // "sudo snap connect openthread-border-router:avahi-control", + "sudo snap connect openthread-border-router:firewall-control", + "sudo snap connect openthread-border-router:raw-usb", + "sudo snap connect openthread-border-router:network-control", + // "sudo snap connect openthread-border-router:bluetooth-control", + // "sudo snap connect openthread-border-router:bluez", + "sudo snap start openthread-border-router", + } + for _, cmd := range commands { + remote_exec(t, cmd) + } + + remote_waitForLogMessage(t, otbrSnap, "Start Thread Border Agent: OK", start) + t.Log("OTBR on remote device is ready") +} + +func remote_deployAllClustersApp(t *testing.T) { + + t.Cleanup(func() { + remote_exec(t, "sudo snap remove --purge matter-all-clusters-app") + }) + + start := time.Now().UTC() + + commands := []string{ + "sudo apt install bluez", + "sudo snap remove --purge matter-all-clusters-app", + "sudo snap install matter-all-clusters-app --edge", + "sudo snap set matter-all-clusters-app args='--thread'", + "sudo snap connect matter-all-clusters-app:avahi-control", + "sudo snap connect matter-all-clusters-app:bluez", + "sudo snap connect matter-all-clusters-app:otbr-dbus-wpan0 openthread-border-router:dbus-wpan0", + "sudo snap start matter-all-clusters-app", + } + for _, cmd := range commands { + remote_exec(t, cmd) + } + + remote_waitForLogMessage(t, "matter-all-clusters-app", "CHIP minimal mDNS started advertising", start) + t.Log("Matter All Clusters App is ready") +} + +func remote_exec(t *testing.T, command string) string { + t.Helper() + + t.Logf("[exec-ssh] %s", command) + + if SSHClient == nil { + t.Fatalf("SSH client not initialized. Please connect to remote device first") + } + + session, err := SSHClient.NewSession() + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + stdout, err := session.StdoutPipe() + if err != nil { + t.Fatalf("Failed to create stdout pipe: %v", err) + } + + if err := session.Start(command); err != nil { + t.Fatalf("Failed to start session with command '%s': %v", command, err) + } + + output, err := io.ReadAll(stdout) + if err != nil { + t.Fatalf("Failed to read command output: %v", err) + } + + if err := session.Wait(); err != nil { + t.Fatalf("Command '%s' failed: %v", command, err) + } + + return string(output) +} + +func remote_waitForLogMessage(t *testing.T, snap string, expectedLog string, start time.Time) { + t.Helper() + + const maxRetry = 10 + for i := 1; i <= maxRetry; i++ { + time.Sleep(1 * time.Second) + t.Logf("Retry %d/%d: Waiting for expected content in logs: '%s'", i, maxRetry, expectedLog) + + command := fmt.Sprintf("sudo journalctl --utc --since \"%s\" --no-pager | grep \"%s\"|| true", start.UTC().Format("2006-01-02 15:04:05"), snap) + logs := remote_exec(t, command) + if strings.Contains(logs, expectedLog) { + t.Logf("Found expected content in logs: '%s'", expectedLog) + return + } + } + + t.Logf("Time out: reached max %d retries.", maxRetry) + t.Log(remote_exec(t, "journalctl --no-pager --lines=10 --unit=snap.openthread-border-router.otbr-agent --priority=notice")) + t.FailNow() +} diff --git a/tests/thread_tests/thread_test.go b/tests/thread_tests/thread_test.go new file mode 100644 index 0000000..2b54743 --- /dev/null +++ b/tests/thread_tests/thread_test.go @@ -0,0 +1,36 @@ +package thread_tests + +import ( + "os" + "testing" + "time" + + "github.com/canonical/matter-snap-testing/utils" + "github.com/stretchr/testify/assert" +) + +func TestAllClustersAppThread(t *testing.T) { + setup(t) + + trimmedActiveDataset := getActiveDataset(t) + + remote_setup(t) + + t.Run("Commission", func(t *testing.T) { + stdout, _, _ := utils.Exec(t, "sudo chip-tool pairing code-thread 110 hex:"+trimmedActiveDataset+" 34970112332 2>&1") + assert.NoError(t, + os.WriteFile("chip-tool-thread-pairing.log", []byte(stdout), 0644), + ) + }) + + t.Run("Control", func(t *testing.T) { + start := time.Now() + stdout, _, _ := utils.Exec(t, "sudo chip-tool onoff toggle 110 1 2>&1") + assert.NoError(t, + os.WriteFile("chip-tool-thread-onoff.log", []byte(stdout), 0644), + ) + + remote_waitForLogMessage(t, "matter-all-clusters-app", "CHIP:ZCL: Toggle ep1 on/off", start) + }) + +} diff --git a/tests/wifi_test.go b/tests/wifi_test.go new file mode 100644 index 0000000..2830437 --- /dev/null +++ b/tests/wifi_test.go @@ -0,0 +1,57 @@ +package tests + +import ( + "os" + "testing" + "time" + + "github.com/canonical/matter-snap-testing/utils" + "github.com/stretchr/testify/assert" +) + +const allClusterSnap = "matter-all-clusters-app" + +func TestAllClustersAppWiFi(t *testing.T) { + InstallChipTool(t) + + start := time.Now() + + // Start clean + utils.SnapRemove(t, allClusterSnap) + + t.Cleanup(func() { + utils.SnapRemove(t, allClusterSnap) + utils.SnapDumpLogs(nil, start, allClusterSnap) + }) + + // Install all clusters app + utils.SnapInstallFromStore(t, allClusterSnap, utils.ServiceChannel) + + // Setup all clusters app + utils.SnapSet(t, allClusterSnap, "args", "--wifi") + utils.SnapConnect(t, allClusterSnap+":avahi-control", "") + utils.SnapConnect(t, allClusterSnap+":bluez", "") + + // Start all clusters app + utils.SnapStart(t, allClusterSnap) + utils.WaitForLogMessage(t, + allClusterSnap, "CHIP minimal mDNS started advertising", start) + + t.Run("Commission", func(t *testing.T) { + stdout, _, _ := utils.Exec(t, "sudo chip-tool pairing onnetwork 110 20202021 2>&1") + assert.NoError(t, + os.WriteFile("chip-tool-pairing.log", []byte(stdout), 0644), + ) + }) + + t.Run("Control", func(t *testing.T) { + stdout, _, _ := utils.Exec(t, "sudo chip-tool onoff toggle 110 1 2>&1") + assert.NoError(t, + os.WriteFile("chip-tool-onoff.log", []byte(stdout), 0644), + ) + + utils.WaitForLogMessage(t, + allClusterSnap, "CHIP:ZCL: Toggle ep1 on/off", start) + }) + +}