blob: 9f3d8051c613ed488798d6f88dd74709e0ad27e2 [file] [log] [blame]
# Copyright 2022 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 ipaddress
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
import textwrap
from threading import Event, Thread
from typing import Any
import unittest
TEST_DATA_DIR = "host_x64/test_data/fidlcat_e2e_tests" # relative to $PWD
FIDLCAT_TIMEOUT = 60 # timeout when invoking fidlcat
# Convert FUCHSIA_SSH_KEY into an absolute path. Otherwise ffx cannot find
# key and complains "Timeout attempting to reach target".
# See https://fxbug.dev/42051867.
os.environ.update(
FUCHSIA_ANALYTICS_DISABLED="1",
FUCHSIA_SSH_KEY=os.path.abspath(os.environ["FUCHSIA_SSH_KEY"]),
)
class Ffx:
# Relative to outdir.
_path = "host-tools/ffx"
# Automatically deleted when |self| is destructed.
_isolate_dir = tempfile.TemporaryDirectory()
# The isolate args are kept separately from the rest of the FFX configuration to make adding the
# target in |init_isolate| simpler.
_isolate_args = [
"--isolate-dir",
_isolate_dir.name,
]
# General FFX configuration.
_ffx_config = [
"--config",
"ffx.subtool-search-paths=" + os.getcwd() + "/host-tools",
"--config",
"log.level=DEBUG,log.dir=" + os.environ["FUCHSIA_TEST_OUTDIR"],
"--config",
"fastboot.usb.disabled=true",
"--config",
"discovery.mdns.enabled=false",
]
# The target address will be in environment variables and determined at initialization.
_target = ["--target"]
# The actual ffx command to run.
_command: list[str] = []
def __init__(self, *args: str) -> None:
self._command = list(args)
self._target.append(self.get_target())
@staticmethod
def get_target() -> str:
if not "FUCHSIA_DEVICE_ADDR" in os.environ.keys():
raise RuntimeError("FUCHSIA_DEVICE_ADDR must be specified.")
addr = ipaddress.ip_address(os.environ["FUCHSIA_DEVICE_ADDR"])
target = ""
# Fixup IPv6 address.
if addr.version == 6:
target = "[" + str(addr) + "]"
else:
target = str(addr)
# FUCHSIA_SSH_PORT is set when the test is run from `fx test`.
if "FUCHSIA_SSH_PORT" in os.environ.keys():
port = os.environ["FUCHSIA_SSH_PORT"]
target = target + ":" + port
return target
# Initialize the ffx isolate with the connected target device indicated by the environment
# variables FUCHSIA_DEVICE_ADDR and FUCHSIA_SSH_PORT.
def init_isolate(self, addr: str) -> None:
# Add the target to the isolate.
target_add_process = subprocess.Popen(
[self._path] + self._isolate_args + ["target", "add", addr],
)
target_add_process.wait()
if target_add_process.returncode != 0:
raise RuntimeError(
"Failed to spawn FFX isolate "
+ str(target_add_process.returncode)
)
# Run the requested ffx command.
def start(self) -> None:
self.init_isolate(self._target[-1])
self.process = subprocess.Popen(
[self._path]
+ self._isolate_args
+ self._ffx_config
+ self._target
+ self._command,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def wait(self) -> int:
self.process.communicate()
return self.process.returncode
class Fidlcat:
_path = "host_x64/fidlcat"
_arg: list[str] = []
_ffx_bridge = None
_debug_agent_socket_path = None
def __init__(self, *args: str, merge_stderr: bool = False) -> None:
"""
merge_stderr: whether to merge stderr to stdout.
"""
assert self._ffx_bridge is not None, "must call setup first"
stderr = subprocess.PIPE
if merge_stderr:
stderr = subprocess.STDOUT
self.process: subprocess.Popen[str] = subprocess.Popen(
[self._path] + self._args + list(args),
text=True,
stdout=subprocess.PIPE,
stderr=stderr,
)
self.stdout: str = (
"" # Contains both stdout and stderr, if merge_stderr.
)
self.stderr: str = ""
self._timeout_cancel: Event = Event()
Thread(target=self._timeout_thread).start()
def _timeout_thread(self) -> None:
self._timeout_cancel.wait(FIDLCAT_TIMEOUT)
if not self._timeout_cancel.is_set():
self.process.kill()
self.wait()
raise TimeoutError("Fidlcat timeouts\n" + self.get_diagnose_msg())
def wait(self) -> int:
"""Wait for the process to terminate, assert the returncode, fill the stdout and stderr."""
(stdout, stderr) = self.process.communicate()
self.stdout += stdout
if stderr: # None if merge_stderr
self.stderr += stderr
self._timeout_cancel.set()
return self.process.returncode
def read_until(self, pattern: str) -> bool:
"""
Read the stdout until EOF or a line contains pattern. Returns whether the pattern matches.
Note: A deadlock could happen if we only read from stdout but the stderr buffer is full.
Consider setting merge_stderr if you want to use this function.
"""
stdout = self.process.stdout
if not stdout:
return False
while True:
line = stdout.readline()
if not line:
return False
self.stdout += line
if pattern in line:
return True
def get_diagnose_msg(self) -> str:
return (
"\n=== stdout ===\n"
+ self.stdout
+ "\n\n=== stderr===\n"
+ self.stderr
+ "\n"
)
@classmethod
def setup(cls: Any) -> None:
cls._ffx_bridge = Ffx("debug", "connect", "--agent-only")
cls._ffx_bridge.start()
cls._debug_agent_socket_path = (
cls._ffx_bridge.process.stdout.readline().strip()
if cls._ffx_bridge.process.stdout is not None
else ""
)
assert os.path.exists(cls._debug_agent_socket_path)
cls._args = [
"--unix-connect",
cls._debug_agent_socket_path,
"--fidl-ir-path",
TEST_DATA_DIR,
"--symbol-path",
TEST_DATA_DIR,
]
@classmethod
def teardown(cls: Any) -> None:
if cls._ffx_bridge:
cls._ffx_bridge.process.terminate()
if cls._debug_agent_socket_path is None:
return
socket_path = pathlib.Path(cls._debug_agent_socket_path)
# The host end of debug_agent's socket is supposed to be cleaned up when the ffx isolate is
# destroyed, but sometimes doesn't for unknown reasons. Clean it up explicitly here just in
# case.
socket_path.unlink(missing_ok=True)
# fuchsia-pkg URL for an echo realm. The echo realm contains echo client and echo server components.
# The echo client is an eager child of the realm and will start when the realm is started/run.
#
# Note that the actual echo client is in a standalone component echo_client.cm so we almost always
# need to specify "--remote-component=echo_client.cm" in the test cases below.
ECHO_REALM_URL = (
"fuchsia-pkg://fuchsia.com/echo_realm_placeholder#meta/echo_realm.cm"
)
ECHO_REALM_MONIKER = "/core/ffx-laboratory:fidlcat_test_echo_realm"
class FidlcatE2eTests(unittest.TestCase):
@classmethod
def setUpClass(cls: Any) -> None:
Fidlcat.setup()
@classmethod
def tearDownClass(cls: Any) -> None:
Fidlcat.teardown()
# Ensure debug_agent exits correctly after each test case. See https://fxbug.dev/42051863.
def tearDown(self) -> None:
# FUCHSIA_DEVICE_ADDR and FUCHSIA_SSH_KEY must be defined.
# FUCHSIA_SSH_PORT is only defined when invoked from `fx test`.
cmd = [
"ssh",
"-F",
"none",
"-o",
"CheckHostIP=no",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-i",
os.environ["FUCHSIA_SSH_KEY"],
]
if os.environ.get("FUCHSIA_SSH_PORT"):
cmd += ["-p", os.environ["FUCHSIA_SSH_PORT"]]
cmd += [
os.environ["FUCHSIA_DEVICE_ADDR"],
"killall /pkg/bin/debug_agent",
]
res = subprocess.run(
cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
if res.returncode == 0 and "Killed" in res.stdout:
print("Killed dangling debug_agent", file=sys.stderr)
else:
# The return code will be 255 if no task found so don't check it.
assert "no tasks found" in res.stdout, res.stdout
def test_run_echo(self) -> None:
fidlcat = Fidlcat(
"--remote-component=echo_client.cm", "run", ECHO_REALM_URL
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertIn(
"sent request test.placeholders/Echo.EchoString = {\n"
' value: string = "hello world"\n'
" }",
fidlcat.stdout,
)
# TODO(https://fxbug.dev/42064761): This test flakes on core.x64-debug, where fidlcat fails to exit after
# receiving the SIGTERM signal.
def disabled_test_stay_alive(self) -> None:
fidlcat = Fidlcat(
"--remote-name=echo_client", "--stay-alive", merge_stderr=True
)
fidlcat.read_until("Connected!")
self.assertEqual(
Ffx("component", "run", ECHO_REALM_MONIKER, ECHO_REALM_URL).wait(),
0,
)
self.assertEqual(
Ffx("component", "destroy", ECHO_REALM_MONIKER).wait(), 0
)
fidlcat.read_until("Waiting for more processes to monitor.")
# Because, with the --stay-alive version, fidlcat never ends,
# we need to kill it to end the test.
fidlcat.process.terminate()
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
def test_extra_component(self) -> None:
fidlcat = Fidlcat(
"--remote-component=echo_client.cm",
"--extra-component=echo_server.cm",
"run",
ECHO_REALM_URL,
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertIn("Monitoring echo_server.cm koid=", fidlcat.stdout)
def test_trigger(self) -> None:
fidlcat = Fidlcat(
"--remote-component=echo_client.cm",
"--trigger=.*EchoString",
"run",
ECHO_REALM_URL,
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
# The first displayed message must be EchoString.
lines = fidlcat.stdout.split("\n\n")
self.assertIn(
"sent request test.placeholders/Echo.EchoString = {", lines[2]
)
def test_messages(self) -> None:
fidlcat = Fidlcat(
"--remote-component=echo_client.cm",
"--messages=.*EchoString",
"run",
ECHO_REALM_URL,
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
# The first and second displayed messages must be EchoString (everything else has been
# filtered out).
lines = fidlcat.stdout.split("\n\n")
self.assertIn(
"sent request test.placeholders/Echo.EchoString = {\n"
' value: string = "hello world"\n'
" }",
lines[2],
)
self.assertIn(
"received response test.placeholders/Echo.EchoString = {\n"
' response: string = "hello world"\n'
" }",
lines[3],
)
def test_save_replay(self) -> None:
save_path = tempfile.NamedTemporaryFile(suffix="_save.pb")
fidlcat = Fidlcat(
"--remote-component=echo_client.cm",
"--to",
save_path.name,
"run",
ECHO_REALM_URL,
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertIn(
"sent request test.placeholders/Echo.EchoString = {\n"
' value: string = "hello world"\n'
" }",
fidlcat.stdout,
)
fidlcat = Fidlcat("--from", save_path.name)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertIn(
"sent request test.placeholders/Echo.EchoString = {\n"
' value: string = "hello world"\n'
" }",
fidlcat.stdout,
)
def test_with_summary(self) -> None:
fidlcat = Fidlcat(
"--with=summary", "--from", TEST_DATA_DIR + "/echo.pb"
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertEqual(
fidlcat.stdout,
"""\
--------------------------------------------------------------------------------echo_client.cm 1934080: 19 handles
Process:eb13d6eb(proc-self)
startup Thread:72a3d5c3(thread-self)
startup Vmar:7ab3d41f(vmar-root)
startup Channel:4eb3d2ab(dir:/svc)
21320.052636 write request fuchsia.io/Openable.Open
startup Channel:8db3d62f(dir:/pkg)
startup Channel:2f53d2bb(directory-request:/)
21320.389674 read request fuchsia.io/Node1.Clone
closed by zx_handle_close
startup Clock:7343d0fb()
startup Socket:1363d7e7(fd:1)
closed by zx_handle_close
startup Socket:0793d49b(fd:2)
closed by zx_handle_close
startup Job:0673d5f7(job-default)
startup Vmo:6f43c91b(vdso-vmo)
startup Vmo:6c83ca87(stack-vmo)
Port:60a3d6c7(port:0)
created by zx_port_create
closed by zx_handle_close
Timer:4863d187(timer:0)
created by zx_timer_create
closed by zx_handle_close
Channel:c633d2db(channel:0)
linked to Channel:fdd3d09f(channel:1)
created by zx_channel_create
closed by Channel:4eb3d2ab(dir:/svc) sending fuchsia.io/Openable.Open
Channel:fdd3d09f(channel:1)
linked to Channel:c633d2db(channel:0)
created by zx_channel_create
21320.136348 write request fuchsia.io/Openable.Open
closed by zx_handle_close
Channel:7663d53b(channel:2)
linked to Channel:7063d25b(channel:3)
which is Channel:60dc2bb3() in process echo_server.cm:1934409
created by zx_channel_create
21320.157131 write request test.placeholders/Echo.EchoString
21321.018177 read response test.placeholders/Echo.EchoString
closed by zx_handle_close
Channel:7063d25b(channel:3)
linked to Channel:7663d53b(channel:2)
created by zx_channel_create
closed by Channel:fdd3d09f(channel:1) sending fuchsia.io/Openable.Open
Channel:31c3d79b()
created by Channel:2f53d2bb(directory-request:/) receiving fuchsia.io/Node1.Clone
closed by zx_handle_close
--------------------------------------------------------------------------------echo_server.cm 1934409: 18 handles
Process:e19c2d4f(proc-self)
startup Thread:4a8c356b(thread-self)
startup Vmar:da0c2e4b(vmar-root)
startup Channel:5e0c228f(dir:/svc)
21320.611044 write request fuchsia.io/Openable.Open
startup Channel:c75c3537(dir:/pkg)
startup Channel:dedc3503(directory-request:/)
21320.679108 read request fuchsia.io/Node1.Clone
21320.814595 read request fuchsia.io/Openable.Open
startup Clock:8efc2deb()
startup Socket:dcbc2b1b(fd:1)
startup Socket:e3dc2843(fd:2)
startup Job:d73c2ff7(job-default)
startup Vmo:d10c3563(vdso-vmo)
startup Vmo:db8c349f(stack-vmo)
Port:c2cc378f(port:1)
created by zx_port_create
Timer:90bc2937(timer:1)
created by zx_timer_create
Channel:f2ec2f3b(channel:4)
linked to Channel:d75c352b(channel:5)
created by zx_channel_create
closed by Channel:5e0c228f(dir:/svc) sending fuchsia.io/Openable.Open
Channel:d75c352b(channel:5)
linked to Channel:f2ec2f3b(channel:4)
created by zx_channel_create
Channel:aa3c2a07()
created by Channel:dedc3503(directory-request:/) receiving fuchsia.io/Node1.Clone
closed by zx_handle_close
Channel:60dc2bb3()
linked to Channel:7663d53b(channel:2) in process echo_client.cm:1934080
created by Channel:dedc3503(directory-request:/) receiving fuchsia.io/Openable.Open
21320.901025 read request test.placeholders/Echo.EchoString
21320.992007 write response test.placeholders/Echo.EchoString
closed by zx_handle_close
""",
)
def test_with_top(self) -> None:
fidlcat = Fidlcat("--with=top", "--from", TEST_DATA_DIR + "/echo.pb")
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertEqual(
fidlcat.stdout,
"""\
--------------------------------------------------------------------------------echo_client.cm 1934080: 5 events
fuchsia.io/Openable: 2 events
Open: 2 events
21320.052636 write request fuchsia.io/Openable.Open(Channel:4eb3d2ab(dir:/svc))
21320.136348 write request fuchsia.io/Openable.Open(Channel:fdd3d09f(channel:1))
test.placeholders/Echo: 2 events
EchoString: 2 events
21320.157131 write request test.placeholders/Echo.EchoString(Channel:7663d53b(channel:2))
21321.018177 read response test.placeholders/Echo.EchoString(Channel:7663d53b(channel:2))
fuchsia.io/Node1: 1 event
Clone: 1 event
21320.389674 read request fuchsia.io/Node1.Clone(Channel:2f53d2bb(directory-request:/))
--------------------------------------------------------------------------------echo_server.cm 1934409: 5 events
fuchsia.io/Openable: 2 events
Open: 2 events
21320.611044 write request fuchsia.io/Openable.Open(Channel:5e0c228f(dir:/svc))
21320.814595 read request fuchsia.io/Openable.Open(Channel:dedc3503(directory-request:/))
test.placeholders/Echo: 2 events
EchoString: 2 events
21320.901025 read request test.placeholders/Echo.EchoString(Channel:60dc2bb3())
21320.992007 write response test.placeholders/Echo.EchoString(Channel:60dc2bb3())
fuchsia.io/Node1: 1 event
Clone: 1 event
21320.679108 read request fuchsia.io/Node1.Clone(Channel:dedc3503(directory-request:/))
""",
)
def test_with_top_and_unknown_message(self) -> None:
fidlcat = Fidlcat(
"--with=top", "--from", TEST_DATA_DIR + "/snapshot.pb"
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
self.assertIn(
" unknown interfaces: : 1 event\n"
" 6862061079.791403 call ordinal=36dadb5482dc1d55("
"Channel:9b71d5c7(dir:/svc/fuchsia.feedback.DataProvider))\n",
fidlcat.stdout,
)
def test_with_messages_and_unknown_message(self) -> None:
fidlcat = Fidlcat(
"--messages=.*x.*", "--from", TEST_DATA_DIR + "/snapshot.pb"
)
self.assertEqual(fidlcat.wait(), 0, fidlcat.get_diagnose_msg())
# We only check that fidlcat didn't crash.
self.assertIn(
"Stop monitoring exceptions.cmx koid 19884\n", fidlcat.stdout
)
if __name__ == "__main__":
unittest.main()