blob: 62f0f9f61262155b0533fbee21b559c345acd2f0 [file] [log] [blame]
# 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)