blob: d33a5a170fc736338ad40ec16855d66e4f2d73f0 [file] [log] [blame]
# Copyright 2024 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 pathlib
import tempfile
import typing
import unittest
import unittest.mock as mock
import fx_cmd
from async_utils.command import (
CommandEvent,
CommandOutput,
StderrEvent,
StdoutEvent,
)
real_subprocess_exec = asyncio.create_subprocess_exec
class TestFxCmd(unittest.TestCase):
@mock.patch.dict(
"os.environ", {"FUCHSIA_BUILD_DIR_FROM_FX": "/tmp/fuchsia/global"}
)
def test_command_line(self) -> None:
"""command line respects build directory specification"""
cmd = fx_cmd.FxCmd()
self.assertEqual(
cmd.command_line("format-code"),
["fx", "--dir", "/tmp/fuchsia/global", "format-code"],
)
cmd = fx_cmd.FxCmd(build_directory=pathlib.Path("/fuchsia"))
self.assertEqual(
cmd.command_line("format-code"),
["fx", "--dir", "/fuchsia", "format-code"],
)
# This mock captures the --dir argument to an `fx` command and lists
# that directory rather than running the original command.
@mock.patch(
"asyncio.subprocess.create_subprocess_exec",
mock.Mock(
side_effect=lambda *args, **kwargs: real_subprocess_exec(
"ls", args[2], **kwargs
),
),
)
def test_sync_stdout(self) -> None:
"""sync execution returns stdout"""
with tempfile.TemporaryDirectory() as td:
path = pathlib.Path(td)
f1 = path / "foo.txt"
f2 = path / "bar.txt"
f1.touch()
f2.touch()
lines: list[str] = []
def callback(x: StdoutEvent) -> None:
lines.append(x.text.decode(errors="ignore").strip())
result = fx_cmd.FxCmd(build_directory=path).sync(
"build", stdout_callback=callback
)
self.assertEqual(result.return_code, 0)
self.assertTrue(
any(["bar.txt" in l for l in lines]),
f"Missing bar.txt in {lines}",
)
self.assertTrue(
any(["foo.txt" in l for l in lines]),
f"Missing foo.txt in {lines}",
)
@mock.patch(
"asyncio.subprocess.create_subprocess_exec",
mock.Mock(
side_effect=lambda *args, **kwargs: real_subprocess_exec(
"ls", "/tmp/does-not-exist12345", **kwargs
),
),
)
def test_sync_stderr(self) -> None:
"""sync execution returns stderr"""
lines: list[str] = []
def callback(x: StderrEvent) -> None:
lines.append(x.text.decode(errors="ignore").strip())
result = fx_cmd.FxCmd(build_directory=pathlib.Path("/missing")).sync(
"build", stderr_callback=callback
)
self.assertNotEqual(result.return_code, 0)
self.assertNotEqual(lines, [])
class TestCommandTransformers(unittest.TestCase):
class OutputCountTransformer(fx_cmd.CommandTransformer[str, int]):
"""Simple transformer that counts the number of lines in a test execution."""
def _handle_event(
self, event: CommandEvent, callback: typing.Callable[[str], None]
) -> None:
if isinstance(event, StdoutEvent):
callback(event.text.decode(errors="ignore").strip())
def _to_output(self, output: CommandOutput) -> int:
return output.stdout.count("\n")
@mock.patch(
"asyncio.subprocess.create_subprocess_exec",
mock.Mock(
side_effect=lambda *args, **kwargs: real_subprocess_exec(
"ls", args[2], **kwargs
),
),
)
def test_counting_transformer(self) -> None:
"""A basic transformer works correctly"""
with tempfile.TemporaryDirectory() as td:
path = pathlib.Path(td)
f1 = path / "foo.txt"
f2 = path / "bar.txt"
f1.touch()
f2.touch()
lines: list[str] = []
result = self.OutputCountTransformer(
"ignored", inner=fx_cmd.FxCmd(build_directory=path)
).sync(event_callback=lambda x: lines.append(x))
self.assertEqual(result, 2)
self.assertSetEqual(set(lines), {"bar.txt", "foo.txt"})
# Test that we get an error reading a directory that doesn't exist.
self.assertRaises(
fx_cmd.CommandFailed,
lambda: self.OutputCountTransformer(
"ignored",
inner=fx_cmd.FxCmd(build_directory=path / "does-not-exist"),
).sync(),
)
class FailingOutputTransformer(fx_cmd.CommandTransformer[None, int]):
def _to_output(self, output: CommandOutput) -> int:
raise RuntimeError("failed to convert")
class FailingEventTransformer(fx_cmd.CommandTransformer[str, int]):
def _handle_event(
self,
event: CommandEvent,
callback: typing.Callable[[str], None],
) -> None:
raise RuntimeError("failed to handle event")
def _to_output(self, output: CommandOutput) -> int:
return 0
@mock.patch(
"asyncio.subprocess.create_subprocess_exec",
mock.Mock(
side_effect=lambda *_args, **kwargs: real_subprocess_exec(
"echo", "hello world\n", **kwargs
),
),
)
def test_transformer_errors(self) -> None:
"""Exceptions in the transformer are caught"""
self.assertRaises(
fx_cmd.CommandTransformFailed,
lambda: self.FailingOutputTransformer(
"ignored", inner=fx_cmd.FxCmd(build_directory="/fuchsia")
).sync(),
)
self.assertRaises(
fx_cmd.CommandTransformFailed,
lambda: self.FailingEventTransformer(
"ignored", inner=fx_cmd.FxCmd(build_directory="/fuchsia")
).sync(),
)
@mock.patch(
"asyncio.subprocess.create_subprocess_exec",
mock.Mock(
side_effect=lambda *_args, **kwargs: real_subprocess_exec(
"sleep", "10", **kwargs
),
),
)
def test_transformer_timeout(self) -> None:
"""Timeouts are caught in the transformer"""
self.assertRaises(
fx_cmd.CommandTimeout,
lambda: self.OutputCountTransformer(
"ignored",
inner=fx_cmd.FxCmd(build_directory="/fuchsia", timeout=0.1),
).sync(),
)