| # Copyright 2023 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import asyncio | |
| import os | |
| import os.path | |
| import struct | |
| import sys | |
| import typing | |
| import unittest | |
| import fidl_fuchsia_controller_test as fc_test | |
| import fidl_fuchsia_developer_ffx as ffx_fidl | |
| from fuchsia_controller_py import ( | |
| Channel, | |
| Context, | |
| FcTransportStatus, | |
| IsolateDir, | |
| ) | |
| from fidl import AsyncChannel, DomainError | |
| class TestSocketServer(fc_test.TestingServer): | |
| def __init__(self, ctx: Context, ch: Channel): | |
| client, server = ctx.socket_create() | |
| self.client = client | |
| self.server = server | |
| super().__init__(ch) | |
| def return_socket_or_error( | |
| self, | |
| ) -> fc_test.TestingServer.ReturnSocketOrErrorResponse: | |
| if self.server.as_int() == 0: | |
| return DomainError(FcTransportStatus.FC_ERR_NOT_SUPPORTED) | |
| return fc_test.TestingReturnSocketOrErrorResponse( | |
| reader_thing=self.server.take() | |
| ) | |
| def return_union(self) -> fc_test.TestingServer.ReturnUnionResponse: | |
| res = fc_test.TestingReturnUnionResponse(x=10) | |
| return res | |
| def return_union_with_table( | |
| self, | |
| ) -> fc_test.TestingServer.ReturnUnionWithTableResponse: | |
| x = fc_test.NoopUnion(union_str="foobar") | |
| res = fc_test.TestingReturnUnionWithTableResponse(x=x) | |
| return res | |
| def return_other_composed_protocol( | |
| self, | |
| ) -> fc_test.TestingServer.ReturnOtherComposedProtocolResponse: | |
| return fc_test.TestingReturnOtherComposedProtocolResponse( | |
| client_thing=0 | |
| ) | |
| def return_possible_error( | |
| self, | |
| ) -> fc_test.TestingServer.ReturnPossibleErrorResponse: | |
| return DomainError("not implemented") | |
| def return_possible_error2( | |
| self, | |
| ) -> fc_test.TestingServer.ReturnPossibleError2Response: | |
| return DomainError("not implemented") | |
| class EndToEnd(unittest.IsolatedAsyncioTestCase): | |
| def _get_default_config(self) -> typing.Dict[str, str]: | |
| return {} | |
| def _get_isolate_dir(self) -> IsolateDir: | |
| isolation_path = None | |
| tmp_path = os.getenv("TEST_UNDECLARED_OUTPUTS_DIR") | |
| if tmp_path: | |
| isolation_path = os.path.join(tmp_path, "isolate") | |
| return IsolateDir(dir=isolation_path) | |
| def test_config_get_nonexistent(self) -> None: | |
| ctx = Context() | |
| self.assertEqual(ctx.config_get_string("foobarzzzzzzo==?"), None) | |
| def test_config_get_exists(self) -> None: | |
| config = self._get_default_config() | |
| key = "foobar" | |
| expect = "bazmumble" | |
| config[key] = expect | |
| ctx = Context(config=config, isolate_dir=self._get_isolate_dir()) | |
| self.assertEqual(ctx.config_get_string(key), expect) | |
| def test_config_get_too_long(self) -> None: | |
| config = self._get_default_config() | |
| key = "foobarzington" | |
| expect = "b" * 50000 | |
| config[key] = expect | |
| ctx = Context(config=config, isolate_dir=self._get_isolate_dir()) | |
| with self.assertRaises(BufferError): | |
| ctx.config_get_string(key) | |
| async def test_client_sends_message_before_coro_await(self) -> None: | |
| ctx = Context() | |
| (ch0, ch1) = ctx.channel_create() | |
| async_ch1 = AsyncChannel(ch1) | |
| echo_proxy = ffx_fidl.EchoClient(ch0) | |
| coro = echo_proxy.echo_string(value="foo") | |
| buf, _ = await async_ch1.read() | |
| txid = int.from_bytes(buf[0:4], sys.byteorder) | |
| ordinal = next( | |
| ord | |
| for ord, info in ffx_fidl.EchoServer.method_map.items() | |
| if info.name == "echo_string" | |
| ) | |
| def encode_message(obj: typing.Any) -> tuple[bytes, list[typing.Any]]: | |
| if obj is not None: | |
| payload, handles = obj.encode() | |
| else: | |
| payload, handles = b"", [] | |
| header = struct.pack("<IHBBQ", txid, 0x02, 0x00, 0x01, ordinal) | |
| return header + payload, handles | |
| encoded_bytes, _ = encode_message( | |
| ffx_fidl.EchoEchoStringRequest(value="foo") | |
| ) | |
| self.assertEqual(buf, encoded_bytes) | |
| msg = encode_message( | |
| ffx_fidl.EchoEchoStringResponse(response="otherthing") | |
| ) | |
| async_ch1.write(msg) | |
| result = await coro | |
| self.assertEqual(result.response, "otherthing") | |
| def test_context_creation_duplicate_target_raises_exception(self) -> None: | |
| with self.assertRaises(RuntimeError): | |
| _ctx = Context(target="foo", config={"target.default": "bar"}) | |
| with self.assertRaises(RuntimeError): | |
| _ctx = Context(target="foo", config={"target": {"default": "bar"}}) | |
| with self.assertRaises(RuntimeError): | |
| _ctx = Context(target="foo", config={"target": "bar"}) | |
| def test_context_creation_invalid_config_type(self) -> None: | |
| with self.assertRaises(TypeError): | |
| Context(config="not a dict") # type: ignore | |
| def test_context_creation_conflicting_config_raises_error( | |
| self, | |
| ) -> None: | |
| with self.assertRaises(RuntimeError): | |
| Context(config={"foo": 1, "foo.bar": 2}) | |
| with self.assertRaises(RuntimeError): | |
| Context(config={"foo.bar": 2, "foo": 1}) | |
| with self.assertRaises(RuntimeError): | |
| Context(config={"foo": {"bar": 1}, "foo.bar": {"baz": 2}}) | |
| def test_config_get_nested(self) -> None: | |
| config = self._get_default_config() | |
| config["some.nested.key"] = "nested_val" | |
| ctx = Context(config=config, isolate_dir=self._get_isolate_dir()) | |
| self.assertEqual(ctx.config_get_string("some.nested.key"), "nested_val") | |
| def test_config_get_dict_nested(self) -> None: | |
| config: typing.Dict[str, typing.Any] = self._get_default_config() | |
| config["some"] = {"dict": {"nested": "dict_val"}} | |
| ctx = Context(config=config, isolate_dir=self._get_isolate_dir()) | |
| self.assertEqual(ctx.config_get_string("some.dict.nested"), "dict_val") | |
| def test_context_creation_no_args(self) -> None: | |
| Context() | |
| async def test_sending_fidl_protocol(self) -> None: | |
| ctx = Context() | |
| tc_server, tc_client = ctx.channel_create() | |
| list_server, list_client = ctx.channel_create() | |
| tc_proxy = ffx_fidl.TargetCollectionClient(tc_client) | |
| query = ffx_fidl.TargetQuery(string_matcher="foobar") | |
| tc_proxy.list_targets(query=query, reader=list_client.take()) | |
| async_tc_server = AsyncChannel(tc_server) | |
| buf, hdls = await async_tc_server.read() | |
| self.assertEqual(len(hdls), 1) | |
| new_list_client = Channel(hdls[0]) | |
| new_list_client.write((bytearray([5, 6, 7]), [])) | |
| async_list_server = AsyncChannel(list_server) | |
| list_buf, list_hdls = await async_list_server.read() | |
| self.assertEqual(len(list_hdls), 0) | |
| self.assertEqual(list_buf, bytearray([5, 6, 7])) | |
| async def test_sending_socket_as_result(self) -> None: | |
| ctx = Context() | |
| t_server, t_client = ctx.channel_create() | |
| server = TestSocketServer(ctx, t_server) | |
| client = fc_test.TestingClient(t_client) | |
| server_task = asyncio.get_running_loop().create_task(server.serve()) | |
| res1 = await client.return_union() | |
| self.assertEqual(res1.x, 10) | |
| res2 = await client.return_union_with_table() | |
| # This is to placate mypy | |
| assert res2.x is not None | |
| self.assertEqual(res2.x.union_str, "foobar") | |
| res3 = await client.return_socket_or_error() | |
| # TODO(b/415379365): Given this is intended to be a socket, it should return | |
| # the appropriate type here. | |
| assert res3.response is not None | |
| self.assertNotEqual(res3.response.reader_thing, 0) | |
| res4 = await client.return_socket_or_error() | |
| self.assertEqual(res4.err, FcTransportStatus.FC_ERR_NOT_SUPPORTED) | |
| server_task.cancel() |