diff --git a/e2etests/cvd/common/BUILD.bazel b/e2etests/cvd/common/BUILD.bazel index ee437f46f01..dcce582f594 100644 --- a/e2etests/cvd/common/BUILD.bazel +++ b/e2etests/cvd/common/BUILD.bazel @@ -12,14 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "common", - srcs = ["common.go"], + srcs = [ + "common.go", + "cvd.go", + ], importpath = "github.com/google/android-cuttlefish/e2etests/cvd/common", visibility = ["//visibility:public"], deps = [ "@rules_go//go/runfiles", ], -) \ No newline at end of file +) + +go_test( + name = "common_test", + srcs = ["cvd_test.go"], + embed = [":common"], +) diff --git a/e2etests/cvd/common/common.go b/e2etests/cvd/common/common.go index b4ef1cb3a44..27578821913 100644 --- a/e2etests/cvd/common/common.go +++ b/e2etests/cvd/common/common.go @@ -34,6 +34,7 @@ import ( "strings" "syscall" "testing" + "time" "github.com/bazelbuild/rules_go/go/runfiles" ) @@ -118,19 +119,42 @@ func (tc *TestContext) RunCmdWithEnv(command []string, envvars map[string]string // Waits for a device to be available via adb. func (tc *TestContext) RunAdbWaitForDevice() error { - adbCommand := []string{ - "timeout", - "--kill-after=30s", - "29s", - "adb", - "wait-for-device", - } - if _, err := tc.RunCmd(adbCommand...); err != nil { + ctx, cancel := context.WithTimeout(tc.context, 30*time.Second) + defer cancel() + if _, err := runCmdWithContextEnv(ctx, []string{"adb", "wait-for-device"}, map[string]string{}); err != nil { return fmt.Errorf("timed out waiting for Cuttlefish device to connect to adb: %w", err) } return nil } +// Waits for a specific device serial to be available via adb. +func (tc *TestContext) RunAdbWaitForDeviceSerial(serial string) error { + ctx, cancel := context.WithTimeout(tc.context, 30*time.Second) + defer cancel() + if _, err := runCmdWithContextEnv(ctx, []string{"adb", "-s", serial, "wait-for-device"}, map[string]string{}); err != nil { + return fmt.Errorf("timed out waiting for device %s to connect to adb: %w", serial, err) + } + return nil +} + +// Checks if adb shell is reachable for a given device serial. +func (tc *TestContext) IsAdbShellReachable(serial string) bool { + ctx, cancel := context.WithTimeout(tc.context, 5*time.Second) + defer cancel() + res, err := runCmdWithContextEnv(ctx, []string{"adb", "-s", serial, "shell", "echo", "ping"}, map[string]string{}) + return err == nil && strings.Contains(res.Stdout, "ping") +} + +// Waits for a device serial to disconnect from adb within the given timeout. +func (tc *TestContext) WaitForDeviceOffline(serial string, timeoutSeconds int) error { + ctx, cancel := context.WithTimeout(tc.context, time.Duration(timeoutSeconds)*time.Second) + defer cancel() + if _, err := runCmdWithContextEnv(ctx, []string{"adb", "-s", serial, "wait-for-disconnect"}, map[string]string{}); err != nil { + return fmt.Errorf("device %s did not disconnect within %d seconds: %w", serial, timeoutSeconds, err) + } + return nil +} + // Runs the given command with the existing envvars. func (tc *TestContext) RunCmd(args ...string) (CommandOutput, error) { command := []string{} @@ -191,7 +215,7 @@ func (tc *TestContext) CVDFetch(args FetchArgs) (CommandOutput, error) { if credentialArg != "" { fetchCmd = append(fetchCmd, fmt.Sprintf("--credential_source=%s", credentialArg)) } - res, err := tc.RunCmd(fetchCmd...); + res, err := tc.RunCmd(fetchCmd...) if err != nil { log.Printf("Failed to fetch: %w", err) return res, err @@ -233,19 +257,13 @@ func (tc *TestContext) CVDCreate(args CreateArgs) (CommandOutput, error) { return res, nil } -// Performs `cvd stop`. -func (tc *TestContext) CVDStop() error { +// Runs a cvd command with the test environment (HOME=tempdir). +func (tc *TestContext) RunCVD(args ...string) (CommandOutput, error) { tempdirEnv := map[string]string{ "HOME": tc.tempdir, } - - stopCmd := []string{tc.TargetBin(), "stop"} - if _, err := tc.RunCmdWithEnv(stopCmd, tempdirEnv); err != nil { - log.Printf("Failed to stop instance(s): %w", err) - return err - } - - return nil + cvdCmd := append([]string{tc.TargetBin()}, args...) + return tc.RunCmdWithEnv(cvdCmd, tempdirEnv) } // Performs `HOME= bin/launch_cvd `. @@ -363,6 +381,14 @@ func (tc *TestContext) GetSyspropString(key string) (string, error) { return strings.TrimSpace(res.Stdout), nil } +func (tc *TestContext) GetSyspropStringForDevice(serial, key string) (string, error) { + res, err := tc.RunCmd("adb", "-s", serial, "shell", "getprop", key) + if err != nil { + return "", fmt.Errorf("failed to get sysprop %s on device %s: %w", key, serial, err) + } + return strings.TrimSpace(res.Stdout), nil +} + // Creates a standard environment for an e2etests. func (tc *TestContext) SetUp(t *testing.T) { tc.t = t @@ -379,6 +405,11 @@ func (tc *TestContext) SetUp(t *testing.T) { } log.Printf("Finished cleaning up any pre-existing instances!") + log.Printf("Starting adb server...") + if _, err := tc.RunCmd("adb", "start-server"); err != nil { + log.Printf("Failed to start adb server: %v", err) + } + tc.tempdir = tc.t.TempDir() log.Printf("Chdir to %s", tc.tempdir) diff --git a/e2etests/cvd/common/cvd.go b/e2etests/cvd/common/cvd.go new file mode 100644 index 00000000000..3df5f5a54c9 --- /dev/null +++ b/e2etests/cvd/common/cvd.go @@ -0,0 +1,204 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2etests + +import ( + "encoding/json" + "fmt" + "log" + "strings" +) + +// CVDInstanceStatusEntry represents a single instance's status in `cvd status --print` JSON output. +type CVDInstanceStatusEntry struct { + AdbPort int `json:"adb_port"` + AdbSerial string `json:"adb_serial"` + AssemblyDir string `json:"assembly_dir"` + InstanceDir string `json:"instance_dir"` + InstanceName string `json:"instance_name"` + Status string `json:"status"` + WebAccess string `json:"web_access"` + WebRtcDeviceID string `json:"webrtc_device_id"` +} + +// ParseCVDStatusJSON parses the JSON output returned by `cvd status --print`. +// It handles single-instance object, multi-instance array `[...]`, and group object `{"instances": [...]}` formats. +func ParseCVDStatusJSON(output string) ([]CVDInstanceStatusEntry, error) { + // 1. Try group object format: {"instances": [...]} + startObj := strings.Index(output, "{") + endObj := strings.LastIndex(output, "}") + if startObj != -1 && endObj > startObj { + jsonStr := strings.TrimSpace(output[startObj : endObj+1]) + var group struct { + Instances *[]CVDInstanceStatusEntry `json:"instances"` + } + if err := json.Unmarshal([]byte(jsonStr), &group); err == nil && group.Instances != nil { + return *group.Instances, nil + } + } + + // 2. Try array format: [...] + // Sift past any potential leading bracketed log prefixes (e.g. "[INFO ...]") + for idx := strings.Index(output, "["); idx != -1; { + endArray := strings.LastIndex(output, "]") + if endArray > idx { + jsonStr := strings.TrimSpace(output[idx : endArray+1]) + var entries []CVDInstanceStatusEntry + if err := json.Unmarshal([]byte(jsonStr), &entries); err == nil && len(entries) > 0 { + return entries, nil + } + } + next := strings.Index(output[idx+1:], "[") + if next == -1 { + break + } + idx += 1 + next + } + + // 3. Try single instance object format: {"instance_name": ..., "status": ...} + if startObj != -1 && endObj > startObj { + jsonStr := strings.TrimSpace(output[startObj : endObj+1]) + var single CVDInstanceStatusEntry + if err := json.Unmarshal([]byte(jsonStr), &single); err == nil && (single.InstanceName != "" || single.Status != "") { + return []CVDInstanceStatusEntry{single}, nil + } + } + + return nil, fmt.Errorf("failed to parse cvd status json from output: %s", output) +} + +// CVDStop performs `cvd stop `. +func (tc *TestContext) CVDStop(args ...string) error { + stopCmd := append([]string{"stop"}, args...) + if _, err := tc.RunCVD(stopCmd...); err != nil { + log.Printf("Failed to stop instance(s): %v", err) + return err + } + return nil +} + +// CVDStart performs `cvd start `. +func (tc *TestContext) CVDStart(args ...string) error { + startCmd := append([]string{"start"}, args...) + if _, err := tc.RunCVD(startCmd...); err != nil { + log.Printf("Failed to start instance(s): %v", err) + return err + } + return nil +} + +// CVDRestart performs `cvd restart `. +func (tc *TestContext) CVDRestart(args ...string) error { + restartCmd := append([]string{"restart"}, args...) + if _, err := tc.RunCVD(restartCmd...); err != nil { + log.Printf("Failed to restart instance(s): %v", err) + return err + } + return nil +} + +// CVDStatus performs `cvd status --print ` and parses the status JSON. +func (tc *TestContext) CVDStatus(args ...string) ([]CVDInstanceStatusEntry, error) { + statusCmd := append([]string{"status", "--print"}, args...) + out, err := tc.RunCVD(statusCmd...) + if err != nil { + return nil, fmt.Errorf("cvd status failed: %w", err) + } + return ParseCVDStatusJSON(out.Stdout) +} + +// CVDInstanceStop performs `cvd --instance_name= stop `. +func (tc *TestContext) CVDInstanceStop(instanceName string, args ...string) error { + cmd := []string{fmt.Sprintf("--instance_name=%s", instanceName), "stop"} + cmd = append(cmd, args...) + if _, err := tc.RunCVD(cmd...); err != nil { + log.Printf("Failed to stop instance %s: %v", instanceName, err) + return err + } + return nil +} + +// CVDInstanceStart performs `cvd --instance_name= start `. +func (tc *TestContext) CVDInstanceStart(instanceName string, args ...string) error { + cmd := []string{fmt.Sprintf("--instance_name=%s", instanceName), "start"} + cmd = append(cmd, args...) + if _, err := tc.RunCVD(cmd...); err != nil { + log.Printf("Failed to start instance %s: %v", instanceName, err) + return err + } + return nil +} + +// CVDInstanceRestart performs `cvd --instance_name= restart `. +func (tc *TestContext) CVDInstanceRestart(instanceName string, args ...string) error { + cmd := []string{fmt.Sprintf("--instance_name=%s", instanceName), "restart"} + cmd = append(cmd, args...) + if _, err := tc.RunCVD(cmd...); err != nil { + log.Printf("Failed to restart instance %s: %v", instanceName, err) + return err + } + return nil +} + +// CVDInstanceStatus performs `cvd --instance_name= status --print ` and parses the status JSON. +func (tc *TestContext) CVDInstanceStatus(instanceName string, args ...string) ([]CVDInstanceStatusEntry, error) { + cmd := []string{fmt.Sprintf("--instance_name=%s", instanceName), "status", "--print"} + cmd = append(cmd, args...) + out, err := tc.RunCVD(cmd...) + if err != nil { + return nil, fmt.Errorf("cvd status failed for instance %s: %w", instanceName, err) + } + return ParseCVDStatusJSON(out.Stdout) +} + +// CVDStop performs `cvd stop ` on the given TestContext. +func CVDStop(tc *TestContext, args ...string) error { + return tc.CVDStop(args...) +} + +// CVDStart performs `cvd start ` on the given TestContext. +func CVDStart(tc *TestContext, args ...string) error { + return tc.CVDStart(args...) +} + +// CVDRestart performs `cvd restart ` on the given TestContext. +func CVDRestart(tc *TestContext, args ...string) error { + return tc.CVDRestart(args...) +} + +// CVDStatus performs `cvd status --print ` and parses the status JSON on the given TestContext. +func CVDStatus(tc *TestContext, args ...string) ([]CVDInstanceStatusEntry, error) { + return tc.CVDStatus(args...) +} + +// CVDInstanceStop performs `cvd --instance_name= stop ` on the given TestContext. +func CVDInstanceStop(tc *TestContext, instanceName string, args ...string) error { + return tc.CVDInstanceStop(instanceName, args...) +} + +// CVDInstanceStart performs `cvd --instance_name= start ` on the given TestContext. +func CVDInstanceStart(tc *TestContext, instanceName string, args ...string) error { + return tc.CVDInstanceStart(instanceName, args...) +} + +// CVDInstanceRestart performs `cvd --instance_name= restart ` on the given TestContext. +func CVDInstanceRestart(tc *TestContext, instanceName string, args ...string) error { + return tc.CVDInstanceRestart(instanceName, args...) +} + +// CVDInstanceStatus performs `cvd --instance_name= status --print ` and parses the status JSON on the given TestContext. +func CVDInstanceStatus(tc *TestContext, instanceName string, args ...string) ([]CVDInstanceStatusEntry, error) { + return tc.CVDInstanceStatus(instanceName, args...) +} diff --git a/e2etests/cvd/common/cvd_test.go b/e2etests/cvd/common/cvd_test.go new file mode 100644 index 00000000000..d79396fd397 --- /dev/null +++ b/e2etests/cvd/common/cvd_test.go @@ -0,0 +1,140 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2etests + +import ( + "testing" +) + +func TestParseCVDStatusJSON(t *testing.T) { + tests := []struct { + name string + input string + wantCount int + wantName string + wantStat string + wantErr bool + }{ + { + name: "Array format", + input: `[ + { + "adb_port": 6520, + "adb_serial": "127.0.0.1:6520", + "instance_name": "ins-1", + "status": "Running" + } + ]`, + wantCount: 1, + wantName: "ins-1", + wantStat: "Running", + }, + { + name: "Group object format", + input: `{ + "instances": [ + { + "adb_port": 6520, + "adb_serial": "127.0.0.1:6520", + "instance_name": "ins-1", + "status": "Running" + }, + { + "adb_port": 6521, + "adb_serial": "127.0.0.1:6521", + "instance_name": "ins-2", + "status": "Running" + } + ] + }`, + wantCount: 2, + wantName: "ins-1", + wantStat: "Running", + }, + { + name: "Log prefix with group object", + input: `[INFO 2026-09-10 12:34:56] cvd status output: + { + "instances": [ + { + "adb_port": 6520, + "adb_serial": "127.0.0.1:6520", + "instance_name": "ins-1", + "status": "Running" + } + ] + }`, + wantCount: 1, + wantName: "ins-1", + wantStat: "Running", + }, + { + name: "Log prefix with array", + input: `[INFO 2026-09-10 12:34:56] cvd status output: + [ + { + "adb_port": 6520, + "adb_serial": "127.0.0.1:6520", + "instance_name": "ins-1", + "status": "Running" + } + ]`, + wantCount: 1, + wantName: "ins-1", + wantStat: "Running", + }, + { + name: "Single object format", + input: `{ + "adb_port": 6520, + "adb_serial": "127.0.0.1:6520", + "instance_name": "ins-1", + "status": "Running" + }`, + wantCount: 1, + wantName: "ins-1", + wantStat: "Running", + }, + { + name: "Invalid output", + input: "error: cvd server is not running", + wantErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + entries, err := ParseCVDStatusJSON(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(entries) != tc.wantCount { + t.Fatalf("expected %d entries, got %d", tc.wantCount, len(entries)) + } + if entries[0].InstanceName != tc.wantName { + t.Errorf("expected instance name %q, got %q", tc.wantName, entries[0].InstanceName) + } + if entries[0].Status != tc.wantStat { + t.Errorf("expected status %q, got %q", tc.wantStat, entries[0].Status) + } + }) + } +} diff --git a/e2etests/cvd/sdv_tests/BUILD.bazel b/e2etests/cvd/sdv_tests/BUILD.bazel new file mode 100644 index 00000000000..77054e6ea6d --- /dev/null +++ b/e2etests/cvd/sdv_tests/BUILD.bazel @@ -0,0 +1,36 @@ +# Copyright (C) 2026 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@rules_go//go:def.bzl", "go_test") + +go_test( + name = "sdv_tests", + size = "large", + srcs = [ + "main_test.go", + "sdv_1vm_test.go", + "sdv_2vm_test.go", + ], + tags = [ + "exclusive", + "external", + "no-sandbox", + "podcvd_excluded", + "requires_ab", + "supports-graceful-termination", + ], + deps = [ + "//cvd/common:common", + ], +) diff --git a/e2etests/cvd/sdv_tests/main_test.go b/e2etests/cvd/sdv_tests/main_test.go new file mode 100644 index 00000000000..c9b130a1890 --- /dev/null +++ b/e2etests/cvd/sdv_tests/main_test.go @@ -0,0 +1,24 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "testing" +) + +func TestSdv(t *testing.T) { + t.Run("1VM", runSdv1VMTest) + t.Run("2VM", runSdv2VMTest) +} diff --git a/e2etests/cvd/sdv_tests/sdv_1vm_test.go b/e2etests/cvd/sdv_tests/sdv_1vm_test.go new file mode 100644 index 00000000000..17380313bdd --- /dev/null +++ b/e2etests/cvd/sdv_tests/sdv_1vm_test.go @@ -0,0 +1,157 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + "strings" + "testing" + + "github.com/google/android-cuttlefish/e2etests/cvd/common" +) + +const ( + sdvBranch = "git_main-swcar-dev" + sdvCoreTarget = "aosp_cf_x86_64_sdv_core-trunk_staging-userdebug" + defaultSerial = "127.0.0.1:6520" + expected1VMSysprop = "instance1" + expectedBootMode = "locked" + + // Bootconfig args matching sdv_default_extra_launch_args from CI configuration + // (configs/wireless/android/testing/atp/prod/aaos-bi-engprod/common/cvd_vms_setup/cvd_default_args.gcl). + extraBootconfigArgs = "--extra_bootconfig_args=" + + "androidboot.sdv.boot_mode=locked " + + "androidboot.sdv.ignore_avb_state=true " + + "androidboot.sdv.init_open_dice.sample_file=dice_handover_instance1 " + + "androidboot.sdv.instance_name=instance1 " + + "androidboot.sdv.keymint.rpc.hbk=799da7577efd41d5b27810c5952fcec0291cbcfd687e77ac9a6cec8370651b1d " + + "androidboot.sdv.preprovisioned_vvmtruststore=etc " + + "androidboot.sdv.someip.enable=true " + + "androidboot.sdv.vvmfactorytrust=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff " + + "androidboot.virt.address=3" +) + +func runSdv1VMTest(t *testing.T) { + c := e2etests.TestContext{} + c.SetUp(t) + defer c.TearDown() + + log.Printf("Fetching SDV Core image (%s/%s)...", sdvBranch, sdvCoreTarget) + if _, err := c.CVDFetch(e2etests.FetchArgs{ + DefaultBuildBranch: sdvBranch, + DefaultBuildTarget: sdvCoreTarget, + }); err != nil { + t.Fatalf("CVDFetch failed: %v", err) + } + + log.Printf("Creating 1-VM SDV Core instance...") + if _, err := c.CVDCreate(e2etests.CreateArgs{ + Args: []string{extraBootconfigArgs}, + }); err != nil { + t.Fatalf("CVDCreate failed: %v", err) + } + + log.Printf("Waiting for device %s to be available via adb...", defaultSerial) + if err := c.RunAdbWaitForDeviceSerial(defaultSerial); err != nil { + t.Fatalf("timed out waiting for device %s: %v", defaultSerial, err) + } + + initialName, err := c.GetSyspropStringForDevice(defaultSerial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read initial ro.boot.sdv.instance_name: %v", err) + } + if initialName != expected1VMSysprop { + t.Fatalf("unexpected instance name: expected %q, got %q", expected1VMSysprop, initialName) + } + log.Printf("Verified initial instance name: %s", initialName) + + initialBootMode, err := c.GetSyspropStringForDevice(defaultSerial, "ro.boot.sdv.boot_mode") + if err != nil { + t.Fatalf("failed to read initial ro.boot.sdv.boot_mode: %v", err) + } + if initialBootMode != expectedBootMode { + t.Fatalf("unexpected boot mode: expected %q, got %q", expectedBootMode, initialBootMode) + } + log.Printf("Verified initial boot mode: %s", initialBootMode) + + t.Run("Status", func(t *testing.T) { + entries, err := c.CVDStatus() + if err != nil { + t.Fatalf("CVDStatus failed: %v", err) + } + if len(entries) == 0 { + t.Fatalf("Expected at least 1 instance status entry, got 0") + } + if !strings.EqualFold(entries[0].Status, "Running") { + t.Fatalf("Expected instance status 'Running', got %q", entries[0].Status) + } + if !c.IsAdbShellReachable(defaultSerial) { + t.Fatalf("ADB shell is not reachable for device %s", defaultSerial) + } + log.Printf("Status check passed for 1-VM: %s", entries[0].Status) + }) + + t.Run("StopStart", func(t *testing.T) { + log.Printf("Stopping 1-VM instance...") + if err := c.CVDStop(); err != nil { + t.Fatalf("CVDStop failed: %v", err) + } + + log.Printf("Verifying device %s went offline...", defaultSerial) + if err := c.WaitForDeviceOffline(defaultSerial, 30); err != nil { + t.Fatalf("device did not go offline after CVDStop: %v", err) + } + + log.Printf("Starting 1-VM instance...") + if err := c.CVDStart(); err != nil { + t.Fatalf("CVDStart failed: %v", err) + } + + log.Printf("Waiting for device %s to reconnect...", defaultSerial) + if err := c.RunAdbWaitForDeviceSerial(defaultSerial); err != nil { + t.Fatalf("device did not come back online after CVDStart: %v", err) + } + + nameAfter, err := c.GetSyspropStringForDevice(defaultSerial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.instance_name after start: %v", err) + } + if nameAfter != expected1VMSysprop { + t.Fatalf("instance name changed after start: expected %q, got %q", expected1VMSysprop, nameAfter) + } + log.Printf("Stop/Start check passed for 1-VM; instance name preserved: %s", nameAfter) + }) + + t.Run("Restart", func(t *testing.T) { + log.Printf("Restarting 1-VM instance...") + if err := c.CVDRestart(); err != nil { + t.Fatalf("CVDRestart failed: %v", err) + } + + log.Printf("Waiting for device %s to be online and responsive after restart...", defaultSerial) + if err := c.RunAdbWaitForDeviceSerial(defaultSerial); err != nil { + t.Fatalf("device did not come back online after CVDRestart: %v", err) + } + + nameAfter, err := c.GetSyspropStringForDevice(defaultSerial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.instance_name after restart: %v", err) + } + if nameAfter != expected1VMSysprop { + t.Fatalf("instance name changed after restart: expected %q, got %q", expected1VMSysprop, nameAfter) + } + log.Printf("Restart check passed for 1-VM; instance name preserved: %s", nameAfter) + }) +} diff --git a/e2etests/cvd/sdv_tests/sdv_2vm_test.go b/e2etests/cvd/sdv_tests/sdv_2vm_test.go new file mode 100644 index 00000000000..6d3b60d2e43 --- /dev/null +++ b/e2etests/cvd/sdv_tests/sdv_2vm_test.go @@ -0,0 +1,262 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "log" + "strings" + "testing" + + "github.com/google/android-cuttlefish/e2etests/cvd/common" +) + +const ( + instance1Serial = "127.0.0.1:6520" + instance2Serial = "127.0.0.1:6521" + instance1Name = "ins-1" + instance2Name = "ins-2" + expectedInstance1Sysprop = "instance1" + expectedInstance2Sysprop = "instance2" + + sdv2VMLoadConfig = ` +{ + "instances": [ + { + "name": "ins-1", + "vm": { + "cpus": 2, + "memory_mb": 2048 + }, + "boot": { + "extra_bootconfig_args": "androidboot.sdv.boot_mode=locked androidboot.sdv.ignore_avb_state=true androidboot.sdv.init_open_dice.sample_file=dice_handover_instance1 androidboot.sdv.instance_name=instance1 androidboot.sdv.keymint.rpc.hbk=799da7577efd41d5b27810c5952fcec0291cbcfd687e77ac9a6cec8370651b1d androidboot.sdv.preprovisioned_vvmtruststore=etc androidboot.sdv.someip.enable=true androidboot.sdv.vvmfactorytrust=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff androidboot.virt.address=3" + }, + "security": { + "guest_enforce_security": false + }, + "disk": { + "default_build": "@ab/git_main-swcar-dev/aosp_cf_x86_64_sdv_core-trunk_staging-userdebug" + }, + "graphics": { + "gpu_mode": "none" + } + }, + { + "name": "ins-2", + "vm": { + "cpus": 4, + "memory_mb": 4096 + }, + "boot": { + "extra_bootconfig_args": "androidboot.sdv.boot_mode=locked androidboot.sdv.ignore_avb_state=true androidboot.sdv.init_open_dice.sample_file=dice_handover_instance2 androidboot.sdv.instance_name=instance2 androidboot.sdv.keymint.rpc.hbk=a3b876291ff7341d861e325406b96a0b58d0a3daf6b030681b8afc24b84d1160 androidboot.sdv.preprovisioned_vvmtruststore=etc androidboot.sdv.someip.enable=false androidboot.sdv.vvmfactorytrust=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff androidboot.virt.address=4" + }, + "security": { + "guest_enforce_security": false + }, + "disk": { + "default_build": "@ab/git_main-swcar-dev/aosp_cf_x86_64_sdv_media-trunk_staging-userdebug" + }, + "graphics": { + "displays": [ + { + "width": 1920, + "height": 1080 + } + ], + "gpu_mode": "gfxstream_guest_angle_host_swiftshader" + } + } + ], + "netsim_bt": false, + "metrics": { + "enable": true + }, + "common": { + "host_package": "@ab/git_main-swcar-dev/aosp_cf_x86_64_sdv_media-trunk_staging-userdebug" + } +}` +) + +func runSdv2VMTest(t *testing.T) { + c := e2etests.TestContext{} + c.SetUp(t) + defer c.TearDown() + + log.Printf("Creating 2-VM SDV environment via cvd create --config_file...") + if err := c.CVDCreateWithConfigFile(e2etests.LoadArgs{ + LoadConfig: sdv2VMLoadConfig, + }); err != nil { + t.Fatalf("CVDCreateWithConfigFile failed: %v", err) + } + + log.Printf("Waiting for instance 1 (%s) to connect via adb...", instance1Serial) + if err := c.RunAdbWaitForDeviceSerial(instance1Serial); err != nil { + t.Fatalf("Instance 1 (%s) failed to connect to adb: %v", instance1Serial, err) + } + + log.Printf("Waiting for instance 2 (%s) to connect via adb...", instance2Serial) + if err := c.RunAdbWaitForDeviceSerial(instance2Serial); err != nil { + t.Fatalf("Instance 2 (%s) failed to connect to adb: %v", instance2Serial, err) + } + + prop1, err := c.GetSyspropStringForDevice(instance1Serial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.instance_name on instance 1 (%s): %v", instance1Serial, err) + } + if prop1 != expectedInstance1Sysprop { + t.Fatalf("unexpected instance name on instance 1: expected %q, got %q", expectedInstance1Sysprop, prop1) + } + + bootMode1, err := c.GetSyspropStringForDevice(instance1Serial, "ro.boot.sdv.boot_mode") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.boot_mode on instance 1 (%s): %v", instance1Serial, err) + } + if bootMode1 != expectedBootMode { + t.Fatalf("unexpected boot mode on instance 1: expected %q, got %q", expectedBootMode, bootMode1) + } + + prop2, err := c.GetSyspropStringForDevice(instance2Serial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.instance_name on instance 2 (%s): %v", instance2Serial, err) + } + if prop2 != expectedInstance2Sysprop { + t.Fatalf("unexpected instance name on instance 2: expected %q, got %q", expectedInstance2Sysprop, prop2) + } + + bootMode2, err := c.GetSyspropStringForDevice(instance2Serial, "ro.boot.sdv.boot_mode") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.boot_mode on instance 2 (%s): %v", instance2Serial, err) + } + if bootMode2 != expectedBootMode { + t.Fatalf("unexpected boot mode on instance 2: expected %q, got %q", expectedBootMode, bootMode2) + } + log.Printf("Verified initial 2-VM state: %s (name=%s, mode=%s), %s (name=%s, mode=%s)", + instance1Serial, prop1, bootMode1, instance2Serial, prop2, bootMode2) + + verifyBothReachable := func(stage string) { + if !c.IsAdbShellReachable(instance1Serial) { + t.Fatalf("Device 1 (%s) is not reachable after %s", instance1Serial, stage) + } + if !c.IsAdbShellReachable(instance2Serial) { + t.Fatalf("Device 2 (%s) is not reachable after %s", instance2Serial, stage) + } + } + + t.Run("Status", func(t *testing.T) { + for _, inst := range []struct { + name string + serial string + }{ + {instance1Name, instance1Serial}, + {instance2Name, instance2Serial}, + } { + entries, err := c.CVDInstanceStatus(inst.name) + if err != nil { + t.Fatalf("CVDInstanceStatus failed for %s: %v", inst.name, err) + } + if len(entries) == 0 { + t.Fatalf("Expected at least 1 status entry for %s, got 0", inst.name) + } + found := false + for _, entry := range entries { + if entry.InstanceName == inst.name || entry.InstanceName == "" || len(entries) == 1 { + if !strings.EqualFold(entry.Status, "Running") { + t.Fatalf("Expected status 'Running' for %s, got %q", inst.name, entry.Status) + } + found = true + log.Printf("Status check passed for %s: %s", inst.name, entry.Status) + break + } + } + if !found { + t.Fatalf("No status entry matching %s found in: %v", inst.name, entries) + } + if !c.IsAdbShellReachable(inst.serial) { + t.Fatalf("ADB shell is not reachable for %s (%s)", inst.name, inst.serial) + } + } + verifyBothReachable("Status subtest") + }) + + // Note: In Cuttlefish multi-instance setups, Instance 1 (ins-1) hosts the common + // environment daemons (e.g. shared networking and sockets). As a result, + // Instance 1 cannot be stopped or restarted independently without disrupting the + // entire environment. Therefore, lifecycle operations (stop/start and restart) + // are performed on Instance 2 (ins-2), verifying that Instance 1 remains online + // and unaffected throughout. + t.Run("StopStartInstance2_IsolateInstance1", func(t *testing.T) { + log.Printf("Stopping %s...", instance2Name) + if err := c.CVDInstanceStop(instance2Name); err != nil { + t.Fatalf("CVDInstanceStop(%s) failed: %v", instance2Name, err) + } + + log.Printf("Verifying %s (%s) went offline...", instance2Name, instance2Serial) + if err := c.WaitForDeviceOffline(instance2Serial, 30); err != nil { + t.Fatalf("Instance 2 (%s) did not go offline after stop: %v", instance2Serial, err) + } + + // Verify non-targeted VM (ins-1) remains online and fully reachable + log.Printf("Verifying non-targeted instance %s (%s) remains online...", instance1Name, instance1Serial) + if !c.IsAdbShellReachable(instance1Serial) { + t.Fatalf("Isolation violation: Instance 1 (%s) became unreachable when Instance 2 was stopped", instance1Serial) + } + + log.Printf("Starting %s...", instance2Name) + if err := c.CVDInstanceStart(instance2Name); err != nil { + t.Fatalf("CVDInstanceStart(%s) failed: %v", instance2Name, err) + } + + log.Printf("Waiting for %s (%s) to come back online...", instance2Name, instance2Serial) + if err := c.RunAdbWaitForDeviceSerial(instance2Serial); err != nil { + t.Fatalf("Instance 2 (%s) did not come back online after start: %v", instance2Serial, err) + } + + nameAfter, err := c.GetSyspropStringForDevice(instance2Serial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.instance_name on %s: %v", instance2Serial, err) + } + if nameAfter != expectedInstance2Sysprop { + t.Fatalf("instance name changed on %s after start: expected %q, got %q", instance2Serial, expectedInstance2Sysprop, nameAfter) + } + log.Printf("Stop/Start check passed for %s with full isolation on %s", instance2Name, instance1Name) + verifyBothReachable("StopStartInstance2 subtest") + }) + + t.Run("RestartInstance2", func(t *testing.T) { + log.Printf("Restarting %s...", instance2Name) + if err := c.CVDInstanceRestart(instance2Name); err != nil { + t.Fatalf("CVDInstanceRestart(%s) failed: %v", instance2Name, err) + } + + // Verify non-targeted VM (ins-1) remains online and fully reachable + log.Printf("Verifying non-targeted instance %s (%s) remains online...", instance1Name, instance1Serial) + if !c.IsAdbShellReachable(instance1Serial) { + t.Fatalf("Isolation violation: Instance 1 (%s) became unreachable when Instance 2 was restarted", instance1Serial) + } + + log.Printf("Waiting for %s (%s) to come back online...", instance2Name, instance2Serial) + if err := c.RunAdbWaitForDeviceSerial(instance2Serial); err != nil { + t.Fatalf("Instance 2 (%s) did not come back online after restart: %v", instance2Serial, err) + } + nameAfter, err := c.GetSyspropStringForDevice(instance2Serial, "ro.boot.sdv.instance_name") + if err != nil { + t.Fatalf("failed to read ro.boot.sdv.instance_name on %s: %v", instance2Serial, err) + } + if nameAfter != expectedInstance2Sysprop { + t.Fatalf("instance name changed on %s after restart: expected %q, got %q", instance2Serial, expectedInstance2Sysprop, nameAfter) + } + log.Printf("Restart check passed for %s with full isolation on %s", instance2Name, instance1Name) + verifyBothReachable("RestartInstance2 subtest") + }) +}