blob: d9cef27fc9fe950cbf85e0b2509c7662f0401d10 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2015 The ChromiumOS Authors
Yiming Chen3c0103a2015-03-31 11:32:35 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module for integration VM tests for CLI commands.
6
7This module contains the basic functionalities for setting up a VM and testing
8the CLI commands.
9"""
10
Chris McDonald14ac61d2021-07-21 11:49:56 -060011import logging
12
Ralph Nathan90475a12015-05-20 13:19:01 -070013from chromite.cli import deploy
Achuith Bhandarkar075d2062019-01-04 12:38:42 -080014from chromite.lib import constants
Yiming Chen3c0103a2015-03-31 11:32:35 -070015from chromite.lib import cros_build_lib
Yiming Chen3c0103a2015-03-31 11:32:35 -070016from chromite.lib import remote_access
17from chromite.lib import vm
Mike Frysinger99d9ab02019-10-22 20:21:20 -040018from chromite.utils import outcap
Yiming Chen3c0103a2015-03-31 11:32:35 -070019
20
21class Error(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060022 """Base exception for CLI command VM tests."""
Yiming Chen3c0103a2015-03-31 11:32:35 -070023
24
Yiming Chen818d8f22015-04-29 11:25:24 -070025class SetupError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060026 """Raised when error occurs during test environment setup."""
Yiming Chen818d8f22015-04-29 11:25:24 -070027
28
29class TestError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060030 """Raised when a command test has failed."""
Yiming Chen818d8f22015-04-29 11:25:24 -070031
32
Yiming Chen3c0103a2015-03-31 11:32:35 -070033class CommandError(Error):
Alex Klein1699fab2022-09-08 08:46:06 -060034 """Raised when error occurs during a command test."""
Yiming Chen3c0103a2015-03-31 11:32:35 -070035
36
37def _PrintCommandLog(command, content):
Alex Klein1699fab2022-09-08 08:46:06 -060038 """Print out the log |content| for |command|."""
39 if content:
40 logging.info(
41 "\n----------- Start of %s log -----------\n%s\n"
42 "----------- End of %s log -----------",
43 command,
44 content.rstrip(),
45 command,
46 )
Yiming Chen3c0103a2015-03-31 11:32:35 -070047
48
Mike Frysinger61cf22d2021-12-15 00:37:54 -050049def test_command_decorator(command_name):
Alex Klein1699fab2022-09-08 08:46:06 -060050 """Decorator that runs the command test function."""
Yiming Chen3c0103a2015-03-31 11:32:35 -070051
Alex Klein1699fab2022-09-08 08:46:06 -060052 def Decorator(test_function):
53 """Inner decorator that actually wraps the function."""
Yiming Chen3c0103a2015-03-31 11:32:35 -070054
Alex Klein1699fab2022-09-08 08:46:06 -060055 def Wrapper(command_test):
56 """Wrapper for the test function."""
57 command = cros_build_lib.CmdToStr(
58 command_test.BuildCommand(command_name)
59 )
60 logging.info("Running test for %s.", command)
61 try:
62 test_function(command_test)
63 logging.info("Test for %s passed.", command)
64 except CommandError as e:
65 _PrintCommandLog(command, str(e))
66 raise TestError("Test for %s failed." % command)
Yiming Chen3c0103a2015-03-31 11:32:35 -070067
Alex Klein1699fab2022-09-08 08:46:06 -060068 return Wrapper
Yiming Chen3c0103a2015-03-31 11:32:35 -070069
Alex Klein1699fab2022-09-08 08:46:06 -060070 return Decorator
Yiming Chen3c0103a2015-03-31 11:32:35 -070071
72
Alex Klein074f94f2023-06-22 10:32:06 -060073class CommandVMTest:
Alex Klein1699fab2022-09-08 08:46:06 -060074 """Base class for CLI command VM tests.
Yiming Chen3c0103a2015-03-31 11:32:35 -070075
Alex Klein1699fab2022-09-08 08:46:06 -060076 This class provides the abstract interface for testing CLI commands on a VM.
77 The sub-class must define the BuildCommand method in order to be usable. And
78 the test functions must use the test_command_decorator decorator.
Yiming Chen3c0103a2015-03-31 11:32:35 -070079 """
Yiming Chen3c0103a2015-03-31 11:32:35 -070080
Alex Klein1699fab2022-09-08 08:46:06 -060081 def __init__(self, board, image_path):
82 """Initializes CommandVMTest.
Yiming Chen3c0103a2015-03-31 11:32:35 -070083
Alex Klein1699fab2022-09-08 08:46:06 -060084 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060085 board: Board for the VM to run tests.
86 image_path: Path to the image for the VM to run tests.
Alex Klein1699fab2022-09-08 08:46:06 -060087 """
88 self.board = board
89 self.image_path = image_path
90 self.port = None
91 self.device_addr = None
Yiming Chen3c0103a2015-03-31 11:32:35 -070092
Alex Klein1699fab2022-09-08 08:46:06 -060093 def BuildCommand(self, command, device=None, pos_args=None, opt_args=None):
94 """Builds a CLI command.
Yiming Chen3c0103a2015-03-31 11:32:35 -070095
Alex Klein1699fab2022-09-08 08:46:06 -060096 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060097 command: The sub-command to build on (e.g. 'flash', 'deploy').
98 device: The device's address for the command.
99 pos_args: A list of positional arguments for the command.
100 opt_args: A list of optional arguments for the command.
Alex Klein1699fab2022-09-08 08:46:06 -0600101 """
102 raise NotImplementedError()
Yiming Chen3c0103a2015-03-31 11:32:35 -0700103
Alex Klein1699fab2022-09-08 08:46:06 -0600104 def SetUp(self):
105 """Creates and starts the VM instance for testing."""
106 self.port = remote_access.GetUnusedPort()
107 self.device_addr = "ssh://%s:%d" % (remote_access.LOCALHOST, self.port)
108 vm_path = vm.CreateVMImage(
109 image=self.image_path, board=self.board, updatable=True
110 )
111 vm_cmd = [
112 "./cros_vm",
113 "--ssh-port=%d" % self.port,
114 "--copy-on-write",
115 "--board=%s" % self.board,
116 "--image-path=%s" % vm_path,
117 "--start",
118 ]
119 cros_build_lib.run(vm_cmd, cwd=constants.CHROMITE_BIN_DIR)
Yiming Chen320f7ac2015-04-02 16:14:00 -0700120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 def TearDown(self):
122 """Stops the VM instance after testing."""
123 if not self.port:
124 return
125 cros_build_lib.run(
126 ["./cros_vm", "--stop", "--ssh-port=%d" % self.port],
127 cwd=constants.CHROMITE_BIN_DIR,
128 check=False,
129 )
Yiming Chen320f7ac2015-04-02 16:14:00 -0700130
Alex Klein1699fab2022-09-08 08:46:06 -0600131 @test_command_decorator("shell")
132 def TestShell(self):
133 """Tests the shell command."""
134 # The path and content of a temporary file for testing shell command.
135 path = "/tmp/shell-test"
136 content = "shell command test file"
Yiming Chen320f7ac2015-04-02 16:14:00 -0700137
Alex Klein1699fab2022-09-08 08:46:06 -0600138 cmd = self.BuildCommand(
139 "shell", device=self.device_addr, opt_args=["--no-known-hosts"]
140 )
Yiming Chen320f7ac2015-04-02 16:14:00 -0700141
Alex Klein1699fab2022-09-08 08:46:06 -0600142 logging.info(
143 "Test to use shell command to write a file to the VM device."
144 )
145 write_cmd = cmd + ["--", 'echo "%s" > %s' % (content, path)]
146 result = cros_build_lib.run(write_cmd, capture_output=True, check=False)
147 if result.returncode:
148 logging.error("Failed to write the file to the VM device.")
149 raise CommandError(result.stderr)
Yiming Chen320f7ac2015-04-02 16:14:00 -0700150
Alex Klein1699fab2022-09-08 08:46:06 -0600151 logging.info(
152 "Test to use shell command to read a file on the VM device."
153 )
154 read_cmd = cmd + ["--", "cat %s" % path]
155 result = cros_build_lib.run(
156 read_cmd, capture_output=True, encoding="utf-8", check=False
157 )
158 if result.returncode or result.stdout.rstrip() != content:
159 logging.error("Failed to read the file on the VM device.")
160 raise CommandError(result.stderr)
Yiming Chen665e3e22015-04-09 16:42:49 -0700161
Alex Klein1699fab2022-09-08 08:46:06 -0600162 logging.info(
163 "Test to use shell command to remove a file on the VM device."
164 )
165 remove_cmd = cmd + ["--", "rm %s" % path]
166 result = cros_build_lib.run(
167 remove_cmd, capture_output=True, check=False
168 )
169 if result.returncode:
170 logging.error("Failed to remove the file on the VM device.")
171 raise CommandError(result.stderr)
Yiming Chen665e3e22015-04-09 16:42:49 -0700172
Alex Klein1699fab2022-09-08 08:46:06 -0600173 @test_command_decorator("debug")
174 def TestDebug(self):
175 """Tests the debug command."""
176 logging.info("Test to start and debug a new process on the VM device.")
177 exe_path = "/bin/bash"
178 start_cmd = self.BuildCommand(
179 "debug", device=self.device_addr, opt_args=["--exe", exe_path]
180 )
181 result = cros_build_lib.run(
182 start_cmd, capture_output=True, check=False, input="\n"
183 )
184 if result.returncode:
185 logging.error(
186 "Failed to start and debug a new process on the VM device."
187 )
188 raise CommandError(result.stderr)
Yiming Chen3c0103a2015-03-31 11:32:35 -0700189
Alex Klein1699fab2022-09-08 08:46:06 -0600190 logging.info("Test to attach a running process on the VM device.")
191 with remote_access.ChromiumOSDeviceHandler(
192 remote_access.LOCALHOST, port=self.port
193 ) as device:
194 exe = "update_engine"
195 pids = device.GetRunningPids(exe, full_path=False)
196 if not pids:
197 logging.error("Failed to find any running process to debug.")
198 raise CommandError()
199 pid = pids[0]
200 attach_cmd = self.BuildCommand(
201 "debug", device=self.device_addr, opt_args=["--pid", str(pid)]
202 )
203 result = cros_build_lib.run(
204 attach_cmd, capture_output=True, check=False, input="\n"
205 )
206 if result.returncode:
207 logging.error(
208 "Failed to attach a running process on the VM device."
209 )
210 raise CommandError(result.stderr)
Yiming Chen3c0103a2015-03-31 11:32:35 -0700211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 @test_command_decorator("flash")
213 def TestFlash(self):
214 """Tests the flash command."""
215 # We explicitly disable reboot after the update because VMs sometimes do
216 # not come back after reboot. The flash command does not need to verify
217 # the integrity of the updated image. We have AU tests for that.
218 cmd = self.BuildCommand(
219 "flash",
220 device=self.device_addr,
221 pos_args=["latest"],
222 opt_args=["--no-wipe", "--no-reboot"],
223 )
Yiming Chen3c0103a2015-03-31 11:32:35 -0700224
Alex Klein1699fab2022-09-08 08:46:06 -0600225 logging.info("Test to flash the VM device with the latest image.")
226 result = cros_build_lib.run(cmd, capture_output=True, check=False)
227 if result.returncode:
228 logging.error("Failed to flash the VM device.")
229 raise CommandError(result.stderr)
Ralph Nathan90475a12015-05-20 13:19:01 -0700230
Alex Klein1699fab2022-09-08 08:46:06 -0600231 @test_command_decorator("deploy")
232 def TestDeploy(self):
233 """Tests the deploy command."""
234 packages = ["dev-python/cherrypy", "app-portage/portage-utils"]
235 # Set the installation root to /usr/local so that the command does not
236 # attempt to remount rootfs (which leads to VM reboot).
237 cmd = self.BuildCommand(
238 "deploy",
239 device=self.device_addr,
240 pos_args=packages,
241 opt_args=["--log-level=info", "--root=/usr/local"],
242 )
Yiming Chen3c0103a2015-03-31 11:32:35 -0700243
Alex Klein1699fab2022-09-08 08:46:06 -0600244 logging.info("Test to uninstall packages on the VM device.")
245 with outcap.OutputCapturer() as output:
246 result = cros_build_lib.run(cmd + ["--unmerge"], check=False)
Ralph Nathan90475a12015-05-20 13:19:01 -0700247
Alex Klein1699fab2022-09-08 08:46:06 -0600248 if result.returncode:
249 logging.error("Failed to uninstall packages on the VM device.")
250 raise CommandError(result.stderr)
Ralph Nathan90475a12015-05-20 13:19:01 -0700251
Alex Klein1699fab2022-09-08 08:46:06 -0600252 captured_output = output.GetStdout() + output.GetStderr()
253 for event in deploy.BrilloDeployOperation.UNMERGE_EVENTS:
254 if event not in captured_output:
255 logging.error(
256 "Strings used by deploy.BrilloDeployOperation to update "
257 "the progress bar have been changed. Please update the "
258 "strings in UNMERGE_EVENTS"
259 )
260 raise CommandError()
Yiming Chen3c0103a2015-03-31 11:32:35 -0700261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 logging.info("Test to install packages on the VM device.")
263 with outcap.OutputCapturer() as output:
264 result = cros_build_lib.run(cmd, check=False)
Ralph Nathan90475a12015-05-20 13:19:01 -0700265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 if result.returncode:
267 logging.error("Failed to install packages on the VM device.")
268 raise CommandError(result.stderr)
Yiming Chen3c0103a2015-03-31 11:32:35 -0700269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 captured_output = output.GetStdout() + output.GetStderr()
271 for event in deploy.BrilloDeployOperation.MERGE_EVENTS:
272 if event not in captured_output:
273 logging.error(
274 "Strings used by deploy.BrilloDeployOperation to update "
275 "the progress bar have been changed. Please update the "
276 "strings in MERGE_EVENTS"
277 )
278 raise CommandError()
Yiming Chen3c0103a2015-03-31 11:32:35 -0700279
Alex Klein1699fab2022-09-08 08:46:06 -0600280 # Verify that the packages are installed.
281 with remote_access.ChromiumOSDeviceHandler(
282 remote_access.LOCALHOST, port=self.port
283 ) as device:
284 try:
285 device.run(["python", "-c", '"import cherrypy"'])
286 device.run(["qmerge", "-h"])
287 except cros_build_lib.RunCommandError as e:
288 logging.error(
289 "Unable to verify packages installed on VM: %s", e
290 )
291 raise CommandError()
292
293 def RunTests(self):
294 """Calls the test functions."""
295 self.TestShell()
296 # TestDebug broken (crbug.com/863122)
297 self.TestFlash()
298 self.TestDeploy()
299
300 def Run(self):
301 """Runs the tests."""
302 try:
303 self.SetUp()
304 self.RunTests()
305 logging.info("All tests completed successfully.")
306 finally:
307 self.TearDown()