| # Copyright 2018 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Tests for the build_api script covering the base Build API functionality.""" | |
| import os | |
| from typing import Callable | |
| from chromite.third_party.google.protobuf import json_format | |
| from chromite.api import api_config | |
| from chromite.api import message_util | |
| from chromite.api import router | |
| from chromite.api.gen.chromite.api import build_api_test_pb2 | |
| from chromite.lib import chroot_lib | |
| from chromite.lib import constants | |
| from chromite.lib import cros_build_lib | |
| from chromite.lib import cros_test_lib | |
| from chromite.lib import osutils | |
| class RouterTest( | |
| cros_test_lib.RunCommandTempDirTestCase, api_config.ApiConfigMixin | |
| ): | |
| """Test Router functionality.""" | |
| def setUp(self): | |
| self.router = router.Router() | |
| self.router.Register(build_api_test_pb2) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.chroot_dir = self.tempdir / "chroot" | |
| self.out_dir = self.tempdir / "out" | |
| chroot = chroot_lib.Chroot( | |
| path=self.chroot_dir, | |
| out_path=self.out_dir, | |
| ) | |
| # Make sure basic path structures exist. | |
| osutils.SafeMakedirs(self.chroot_dir) | |
| osutils.SafeMakedirs(self.out_dir) | |
| # Make the tmp dir for the re-exec inside chroot input/output files. | |
| osutils.SafeMakedirs(chroot.tmp) | |
| # Build the input/output/config paths we'll be using in the tests. | |
| self.json_input_file = os.path.join(self.tempdir, "input.json") | |
| self.json_output_file = os.path.join(self.tempdir, "output.json") | |
| self.json_config_file = os.path.join(self.tempdir, "config.json") | |
| self.binary_input_file = os.path.join(self.tempdir, "input.bin") | |
| self.binary_output_file = os.path.join(self.tempdir, "output.bin") | |
| self.binary_config_file = os.path.join(self.tempdir, "config.bin") | |
| # The message handlers for the respective files. | |
| self.json_input_handler = message_util.get_message_handler( | |
| self.json_input_file, message_util.FORMAT_JSON | |
| ) | |
| self.json_output_handler = message_util.get_message_handler( | |
| self.json_output_file, message_util.FORMAT_JSON | |
| ) | |
| self.json_config_handler = message_util.get_message_handler( | |
| self.json_config_file, message_util.FORMAT_JSON | |
| ) | |
| self.binary_input_handler = message_util.get_message_handler( | |
| self.binary_input_file, message_util.FORMAT_BINARY | |
| ) | |
| self.binary_output_handler = message_util.get_message_handler( | |
| self.binary_output_file, message_util.FORMAT_BINARY | |
| ) | |
| self.binary_config_handler = message_util.get_message_handler( | |
| self.binary_config_file, message_util.FORMAT_BINARY | |
| ) | |
| # Build an input message to use. | |
| self.expected_id = "input id" | |
| input_msg = build_api_test_pb2.TestRequestMessage() | |
| input_msg.id = self.expected_id | |
| input_msg.chroot.path = str(self.chroot_dir) | |
| input_msg.chroot.out_path = str(self.out_dir) | |
| # Write out base input and config messages. | |
| osutils.WriteFile( | |
| self.json_input_file, json_format.MessageToJson(input_msg) | |
| ) | |
| osutils.WriteFile( | |
| self.binary_input_file, input_msg.SerializeToString(), mode="wb" | |
| ) | |
| config_msg = self.api_config.get_proto() | |
| osutils.WriteFile( | |
| self.json_config_file, json_format.MessageToJson(config_msg) | |
| ) | |
| osutils.WriteFile( | |
| self.binary_config_file, config_msg.SerializeToString(), mode="wb" | |
| ) | |
| self.subprocess_tempdir = os.path.join(self.chroot_dir, "tempdir") | |
| osutils.SafeMakedirs(self.subprocess_tempdir) | |
| def testJsonInputOutputMethod(self): | |
| """Test json input/output handling.""" | |
| def impl(input_msg, output_msg, config): | |
| self.assertIsInstance( | |
| input_msg, build_api_test_pb2.TestRequestMessage | |
| ) | |
| self.assertIsInstance( | |
| output_msg, build_api_test_pb2.TestResultMessage | |
| ) | |
| self.assertIsInstance(config, api_config.ApiConfig) | |
| self.assertEqual(config, self.api_config) | |
| self.PatchObject(self.router, "_GetMethod", return_value=impl) | |
| self.router.Route( | |
| "chromite.api.TestApiService", | |
| "InputOutputMethod", | |
| self.api_config, | |
| self.json_input_handler, | |
| [self.json_output_handler], | |
| self.json_config_handler, | |
| ) | |
| def testBinaryInputOutputMethod(self): | |
| """Test binary input/output handling.""" | |
| def impl(input_msg, output_msg, config): | |
| self.assertIsInstance( | |
| input_msg, build_api_test_pb2.TestRequestMessage | |
| ) | |
| self.assertIsInstance( | |
| output_msg, build_api_test_pb2.TestResultMessage | |
| ) | |
| self.assertIsInstance(config, api_config.ApiConfig) | |
| self.assertEqual(config, self.api_config) | |
| self.PatchObject(self.router, "_GetMethod", return_value=impl) | |
| self.router.Route( | |
| "chromite.api.TestApiService", | |
| "InputOutputMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testMultipleOutputHandling(self): | |
| """Test multiple output handling.""" | |
| expected_result = "Success!" | |
| def impl(input_msg, output_msg, config): | |
| self.assertIsInstance( | |
| input_msg, build_api_test_pb2.TestRequestMessage | |
| ) | |
| self.assertIsInstance( | |
| output_msg, build_api_test_pb2.TestResultMessage | |
| ) | |
| self.assertIsInstance(config, api_config.ApiConfig) | |
| self.assertEqual(config, self.api_config) | |
| # Set the property on the output to test against. | |
| output_msg.result = expected_result | |
| self.PatchObject(self.router, "_GetMethod", return_value=impl) | |
| self.router.Route( | |
| "chromite.api.TestApiService", | |
| "InputOutputMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler, self.json_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| # Make sure it did write out all the expected files. | |
| self.assertExists(self.binary_output_file) | |
| self.assertExists(self.json_output_file) | |
| # Parse the output files back into a message. | |
| binary_msg = build_api_test_pb2.TestResultMessage() | |
| json_msg = build_api_test_pb2.TestResultMessage() | |
| self.binary_output_handler.read_into(binary_msg) | |
| self.json_output_handler.read_into(json_msg) | |
| # Make sure the parsed messages have the expected content. | |
| self.assertEqual(binary_msg.result, expected_result) | |
| self.assertEqual(json_msg.result, expected_result) | |
| def testRenameMethod(self): | |
| """Test implementation name config.""" | |
| def _GetMethod(_, method_name): | |
| self.assertEqual("CorrectName", method_name) | |
| return lambda x, y, z: None | |
| self.PatchObject(self.router, "_GetMethod", side_effect=_GetMethod) | |
| self.router.Route( | |
| "chromite.api.TestApiService", | |
| "RenamedMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def _mock_callable(self, expect_called: bool) -> Callable: | |
| """Helper to create the implementation mock to test chroot assertions. | |
| Args: | |
| expect_called: Whether the implementation should be called. | |
| When False, an assertion will fail if it is called. | |
| Returns: | |
| The implementation. | |
| """ | |
| def impl(_input_msg, _output_msg, _config): | |
| self.assertTrue( | |
| expect_called, "The implementation should not have been called." | |
| ) | |
| return impl | |
| def _writeChrootCallOutput(self, content="{}", mode="w"): | |
| def impl(*_args, **_kwargs): | |
| """Side effect for inside-chroot calls to the API.""" | |
| osutils.WriteFile( | |
| os.path.join( | |
| self.subprocess_tempdir, router.Router.REEXEC_OUTPUT_FILE | |
| ), | |
| content, | |
| mode=mode, | |
| ) | |
| return impl | |
| def testInsideServiceInsideMethodInsideChroot(self): | |
| """Test inside/inside/inside works correctly.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=True), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=True) | |
| self.router.Route( | |
| "chromite.api.InsideChrootApiService", | |
| "InsideServiceInsideMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testInsideServiceOutsideMethodOutsideChroot(self): | |
| """Test the outside method override works as expected.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=True), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.router.Route( | |
| "chromite.api.InsideChrootApiService", | |
| "InsideServiceOutsideMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testInsideServiceInsideMethodOutsideChroot(self): | |
| """Test calling an inside method from outside the chroot.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| service = "chromite.api.InsideChrootApiService" | |
| method = "InsideServiceInsideMethod" | |
| service_method = "%s/%s" % (service, method) | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| ["build_api", service_method], enter_chroot=True | |
| ) | |
| def testInsideServiceOutsideMethodInsideChroot(self): | |
| """Test inside chroot for outside method raises an error.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=True) | |
| with self.assertRaises(cros_build_lib.DieSystemExit): | |
| self.router.Route( | |
| "chromite.api.InsideChrootApiService", | |
| "InsideServiceOutsideMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testOutsideServiceOutsideMethodOutsideChroot(self): | |
| """Test outside/outside/outside works correctly.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=True), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.router.Route( | |
| "chromite.api.OutsideChrootApiService", | |
| "OutsideServiceOutsideMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testOutsideServiceInsideMethodInsideChroot(self): | |
| """Test the inside method assertion override works properly.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=True), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=True) | |
| self.router.Route( | |
| "chromite.api.OutsideChrootApiService", | |
| "OutsideServiceInsideMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testOutsideServiceInsideMethodOutsideChroot(self): | |
| """Test calling an inside override method from outside the chroot.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| service = "chromite.api.OutsideChrootApiService" | |
| method = "OutsideServiceInsideMethod" | |
| service_method = "%s/%s" % (service, method) | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| ["build_api", service_method], enter_chroot=True | |
| ) | |
| def testReexecNonemptyOutput(self): | |
| """Test calling an inside chroot method that produced output.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| # Patch the chroot tempdir method to return a tempdir with our | |
| # subprocess tempdir so the output file will be in the expected | |
| # location. | |
| tempdir = osutils.TempDir() | |
| original = tempdir.tempdir | |
| tempdir.tempdir = self.subprocess_tempdir | |
| self.PatchObject(chroot_lib.Chroot, "tempdir", return_value=tempdir) | |
| expected_output_msg = build_api_test_pb2.TestResultMessage() | |
| expected_output_msg.result = "foo" | |
| # Set the command side effect to write out our expected output to the | |
| # output file for the inside the chroot reexecution of the endpoint. | |
| # This lets us make sure the logic moving everything out works as | |
| # intended. | |
| self.rc.SetDefaultCmdResult( | |
| side_effect=self._writeChrootCallOutput( | |
| content=expected_output_msg.SerializeToString(), mode="wb" | |
| ) | |
| ) | |
| service = "chromite.api.OutsideChrootApiService" | |
| method = "OutsideServiceInsideMethod" | |
| service_method = "%s/%s" % (service, method) | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| ["build_api", service_method], enter_chroot=True | |
| ) | |
| # It should be writing the result out to our output file. | |
| output_msg = build_api_test_pb2.TestResultMessage() | |
| self.binary_output_handler.read_into(output_msg) | |
| self.assertEqual(expected_output_msg, output_msg) | |
| tempdir.tempdir = original | |
| del tempdir | |
| def testReexecEmptyOutput(self): | |
| """Test calling an inside chroot method that produced no output.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| expected_output_msg = build_api_test_pb2.TestResultMessage() | |
| # Set the command side effect to write out our expected output to the | |
| # output file for the inside the chroot reexecution of the endpoint. | |
| # This lets us make sure the logic moving everything out works as | |
| # intended. | |
| self.rc.SetDefaultCmdResult( | |
| side_effect=self._writeChrootCallOutput( | |
| content=expected_output_msg.SerializeToString(), mode="wb" | |
| ) | |
| ) | |
| service = "chromite.api.OutsideChrootApiService" | |
| method = "OutsideServiceInsideMethod" | |
| service_method = "%s/%s" % (service, method) | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| ["build_api", service_method], enter_chroot=True | |
| ) | |
| output_msg = build_api_test_pb2.TestResultMessage() | |
| self.binary_output_handler.read_into(output_msg) | |
| self.assertEqual(expected_output_msg, output_msg) | |
| def testReexecNoOutput(self): | |
| """Test calling an inside chroot method that produced no output.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.rc.SetDefaultCmdResult(returncode=1) | |
| service = "chromite.api.OutsideChrootApiService" | |
| method = "OutsideServiceInsideMethod" | |
| service_method = "%s/%s" % (service, method) | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| ["build_api", service_method], enter_chroot=True | |
| ) | |
| output_msg = build_api_test_pb2.TestResultMessage() | |
| empty_msg = build_api_test_pb2.TestResultMessage() | |
| self.binary_output_handler.read_into(output_msg) | |
| self.assertEqual(empty_msg, output_msg) | |
| def test_tot_service_tot_method(self): | |
| """Test no re-exec for ToT->ToT.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=True), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.PatchObject(constants, "IS_BRANCHED_CHROMITE", new=False) | |
| self.router.Route( | |
| "chromite.api.TotExecutionService", | |
| "TotServiceTotMethod", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def test_tot_service_tot_method_inside(self): | |
| """Test error raised when method runs ToT & inside the SDK.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.PatchObject(constants, "IS_BRANCHED_CHROMITE", new=False) | |
| with self.assertRaises(router.TotSdkError): | |
| self.router.Route( | |
| "chromite.api.TotExecutionService", | |
| "TotServiceTotMethodInside", | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def test_tot_service_branched_method(self): | |
| """Re-execute branched BAPI.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.PatchObject(constants, "IS_BRANCHED_CHROMITE", new=False) | |
| self.PatchObject(constants, "BRANCHED_CHROMITE_DIR", new=self.tempdir) | |
| service = "chromite.api.TotExecutionService" | |
| method = "TotServiceBranchedMethod" | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| [self.tempdir / "bin" / "build_api", f"{service}/{method}"] | |
| ) | |
| def test_branched_call_no_config(self): | |
| """Re-execute branched BAPI with no config passed.""" | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.PatchObject(constants, "IS_BRANCHED_CHROMITE", new=False) | |
| self.PatchObject(constants, "BRANCHED_CHROMITE_DIR", new=self.tempdir) | |
| service = "chromite.api.TotExecutionService" | |
| method = "TotServiceBranchedMethod" | |
| config_handler = message_util.get_message_handler( | |
| None, message_util.FORMAT_BINARY | |
| ) | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| config_handler, | |
| ) | |
| self.assertCommandContains( | |
| [self.tempdir / "bin" / "build_api", f"{service}/{method}"] | |
| ) | |
| # Branched BAPI calls don't parse and rewrite the messages, so when no | |
| # config is given, an empty one isn't written out like with SDK reexecs. | |
| self.assertCommandContains( | |
| ["--config-binary", "--config-json"], expected=False | |
| ) | |
| def test_tot_service_branched_method_inside(self): | |
| """Re-execute branched BAPI. | |
| Chroot handling delegated to the branched BAPI, so should be identical | |
| to the branched method case above. | |
| """ | |
| self.PatchObject( | |
| self.router, | |
| "_GetMethod", | |
| return_value=self._mock_callable(expect_called=False), | |
| ) | |
| self.PatchObject(cros_build_lib, "IsInsideChroot", return_value=False) | |
| self.PatchObject(constants, "IS_BRANCHED_CHROMITE", new=False) | |
| self.PatchObject(constants, "BRANCHED_CHROMITE_DIR", new=self.tempdir) | |
| service = "chromite.api.TotExecutionService" | |
| method = "TotServiceBranchedMethodInside" | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| self.assertCommandContains( | |
| [self.tempdir / "bin" / "build_api", f"{service}/{method}"] | |
| ) | |
| def testInvalidService(self): | |
| """Test invalid service call.""" | |
| service = "chromite.api.DoesNotExist" | |
| method = "OutsideServiceInsideMethod" | |
| with self.assertRaises(router.UnknownServiceError): | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testInvalidMethod(self): | |
| """Test invalid method call.""" | |
| service = "chromite.api.OutsideChrootApiService" | |
| method = "DoesNotExist" | |
| with self.assertRaises(router.UnknownMethodError): | |
| self.router.Route( | |
| service, | |
| method, | |
| self.api_config, | |
| self.binary_input_handler, | |
| [self.binary_output_handler], | |
| self.binary_config_handler, | |
| ) | |
| def testListVisibility(self): | |
| """Test visibility options.""" | |
| service = "HiddenService" | |
| method = "HiddenMethod" | |
| for current in self.router.ListMethods(): | |
| self.assertNotIn(service, current) | |
| self.assertNotIn(method, current) |