blob: 1abacb6ce60de5a9bd1b645ca9ab65dedefc6440 [file] [log] [blame] [edit]
# 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 contextlib
import io
import json
import logging
import os
import tempfile
import typing
import unittest
from pathlib import Path
from unittest import mock
import data_for_test
import main
from async_utils.command import AsyncCommand, CommandOutput
from parameterized import parameterized
_LOGGER = logging.Logger(__file__)
class TestStatusCommand(unittest.TestCase):
def setUp(self) -> None:
self.tmp_dir = tempfile.TemporaryDirectory()
self.fuchsia_dir = Path(self.tmp_dir.name)
self.out_dir = self.fuchsia_dir / "out" / "x64"
os.makedirs(self.out_dir)
self.args_gn = self.out_dir / "args.gn"
with self.args_gn.open("w") as f:
f.write(data_for_test.ARGS_GN_BASE)
self.product_bundles_json = self.out_dir / "product_bundles.json"
with self.product_bundles_json.open("w") as f:
f.write(data_for_test.PRODUCT_BUNDLES_JSON)
self.async_patch = mock.patch(
"async_utils.command.AsyncCommand.create",
mock.AsyncMock(side_effect=self.mock_command_result),
)
self.async_mock = self.async_patch.start()
self.addCleanup(self.async_patch.stop)
self.env_patch = mock.patch.dict(
"os.environ", {"FUCHSIA_DIR": str(self.fuchsia_dir)}, clear=True
)
self.env_mock = self.env_patch.start()
self.addCleanup(self.env_patch.stop)
self.build_dir_patch = mock.patch(
"build_dir.get_build_directory",
mock.Mock(return_value=self.out_dir),
)
self.build_dir_mock = self.build_dir_patch.start()
self.addCleanup(self.build_dir_patch.stop)
# Appear to be two matching git hashes.
self.stdout_git_command = "12" * 20 + "\n" + "12" * 20
# By default, no overrides
self.stdout_jiri_overrides = ""
# Use the pre-computed output for args.gn formatting.
self.stdout_gn_format = data_for_test.ARGS_GN_JSON
def mock_command_result(
self, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
return_code: int = 255
stdout: str = ""
if args[0] == "git":
self.assertEqual(
args[1:],
(
"--no-optional-locks",
f"--git-dir={str(self.fuchsia_dir)}/.git",
"rev-parse",
"HEAD",
"JIRI_HEAD",
),
)
stdout = self.stdout_git_command
return_code = 0
elif args[0] == "fx" and args[3] == "gn":
self.assertEqual(
args[4:],
(
"format",
"--dump-tree=json",
str(self.out_dir / "args.gn"),
),
)
stdout = self.stdout_gn_format
return_code = 0
elif args[0] == "fx" and args[3:] == ("jiri", "override", "-list"):
stdout = self.stdout_jiri_overrides
return_code = 0
ret = mock.MagicMock(spec=AsyncCommand)
setattr(
ret,
"run_to_completion",
mock.AsyncMock(
return_value=CommandOutput(
stdout,
stderr="",
return_code=return_code,
runtime=0,
wrapper_return_code=None,
was_timeout=False,
)
),
)
_LOGGER.debug("Called with", args, kwargs)
_LOGGER.debug("Returning", ret)
return ret
@parameterized.expand(
[
("no arguments defaults to text", []),
("-f text", ["-f", "text"]),
("--format text", ["--format", "text"]),
]
)
def test_text_output(self, _name: str, flags: list[str]) -> None:
"""Check that text output is as expected"""
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main(flags)
self.maxDiff = None
self.assertEqual(
out.getvalue(),
EXPECTED_TEXT_OUTPUT.lstrip().replace(
"<<BUILD_DIR>>", str(self.out_dir)
),
f"Output to copy/paste:\n{out.getvalue()}",
)
@parameterized.expand(
[
("no arguments defaults to text", []),
("-f text", ["-f", "text"]),
("--format text", ["--format", "text"]),
]
)
def test_text_output_no_pb(self, _name: str, flags: list[str]) -> None:
"""Check that text output is as expected when no PB is set"""
with self.args_gn.open("w") as f:
f.write(
data_for_test.ARGS_GN_BASE.replace(
'main_pb_label = "//bundles:main"\n', ""
)
)
# Remove the main_pb_label from the mocked gn format output
gn_json = json.loads(data_for_test.ARGS_GN_JSON)
gn_json["child"] = [
c
for c in gn_json["child"]
if c.get("child")[0].get("value") != "main_pb_label"
]
self.stdout_gn_format = json.dumps(gn_json)
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main(flags)
self.maxDiff = None
self.assertEqual(
out.getvalue(),
EXPECTED_TEXT_OUTPUT_NO_PB.lstrip().replace(
"<<BUILD_DIR>>", str(self.out_dir)
),
f"Output to copy/paste:\n{out.getvalue()}",
)
@parameterized.expand(
[
("-f json", ["-f", "json"]),
("--format json", ["--format", "json"]),
]
)
def test_json_output(self, _name: str, flags: list[str]) -> None:
"""Check that json output is as expected"""
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main(["-f", "json"])
formatted_output = json.dumps(json.loads(out.getvalue()), indent=2)
self.maxDiff = None
self.assertEqual(
formatted_output,
EXPECTED_JSON_OUTPUT.lstrip().replace(
"<<BUILD_DIR>>", str(self.out_dir)
),
f"Output to copy/paste:\n{formatted_output}",
)
@parameterized.expand(
[
("-f json", ["-f", "json"]),
("--format json", ["--format", "json"]),
]
)
def test_json_output_no_pb(self, _name: str, flags: list[str]) -> None:
"""Check that json output is as expected when no PB is set"""
with self.args_gn.open("w") as f:
f.write(
data_for_test.ARGS_GN_BASE.replace(
'main_pb_label = "//bundles:main"\n', ""
)
)
# Remove the main_pb_label from the mocked gn format output
gn_json = json.loads(data_for_test.ARGS_GN_JSON)
gn_json["child"] = [
c
for c in gn_json["child"]
if c.get("child")[0].get("value") != "main_pb_label"
]
self.stdout_gn_format = json.dumps(gn_json)
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main(flags)
formatted_output = json.dumps(json.loads(out.getvalue()), indent=2)
self.maxDiff = None
self.assertEqual(
formatted_output,
EXPECTED_JSON_OUTPUT_NO_PB.lstrip().replace(
"<<BUILD_DIR>>", str(self.out_dir)
),
f"Output to copy/paste:\n{formatted_output}",
)
@parameterized.expand(
[
("-f json", ["-f", "json"]),
("--format json", ["--format", "json"]),
]
)
def test_extended_json_output(self, _name: str, flags: list[str]) -> None:
"""Check that json output is as expected for longer include paths and
non-alphanumeric names"""
out = io.StringIO()
self.stdout_gn_format = self.stdout_gn_format.replace(
"//boards/x64.gni", "//foo/bar/boards/x64-fb.gni"
)
with contextlib.redirect_stdout(out):
main.main(["-f", "json"])
formatted_output = json.dumps(json.loads(out.getvalue()), indent=2)
self.maxDiff = None
# Longer path
new_expected_json_output = EXPECTED_JSON_OUTPUT.replace(
"//boards/x64.gni", "//foo/bar/boards/x64.gni"
)
# Extended characters in name
new_expected_json_output = new_expected_json_output.replace(
"x64", "x64-fb"
)
self.assertEqual(
formatted_output,
new_expected_json_output.lstrip().replace(
"<<BUILD_DIR>>", str(self.out_dir)
),
f"Output to copy/paste:\n{formatted_output}",
)
def assertSetContains(self, input: set[str], *args: str) -> None:
self.assertEqual(
input,
input | {*args},
"\nSet was:\n " + "\n ".join(map(lambda x: f"'{x}'", input)),
)
def test_no_git_output(self) -> None:
"""When git returns no output, assume we're at JIRI_HEAD"""
self.stdout_git_command = ""
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main([])
lines = set(out.getvalue().splitlines())
self.assertSetContains(
lines,
" Is fuchsia source project in JIRI_HEAD?: true",
)
def test_git_mismatch(self) -> None:
"""Git reports we are not at JIRI_HEAD"""
self.stdout_git_command = "abcd\nefgh"
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main([])
lines = set(out.getvalue().splitlines())
self.assertSetContains(
lines,
" Is fuchsia source project in JIRI_HEAD?: false",
)
def test_jiri_override(self) -> None:
"""Jiri reports we have overrides"""
self.stdout_jiri_overrides = "override: foo"
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main([])
lines = set(out.getvalue().splitlines())
self.assertSetContains(
lines,
" Has Jiri overrides?: true (output of 'jiri override -list')",
)
def test_args_gn_empty(self) -> None:
"""When args.gn is empty, no Build info is available"""
self.stdout_gn_format = ""
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main([])
lines = set(out.getvalue().splitlines())
self.assertEqual(lines, lines - {"Build Info:"})
def test_device_name_from_environment(self) -> None:
"""Retrieve device name from environment variables"""
self.env_mock["FUCHSIA_NODENAME"] = "foo"
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main([])
lines = set(out.getvalue().splitlines())
self.assertSetContains(set(lines), " Device name: foo (set by fx -t)")
def test_device_name_from__build(self) -> None:
"""Retrieve device name from build file"""
# Note that this still obtains the nodename from environment,
# but we use an additional environment variable to determine
# the source of the data.
self.env_mock["FUCHSIA_NODENAME"] = "foo"
self.env_mock["FUCHSIA_NODENAME_IS_FROM_FILE"] = "true"
out = io.StringIO()
with contextlib.redirect_stdout(out):
main.main([])
lines = set(out.getvalue().splitlines())
self.assertSetContains(
set(lines), " Device name: foo (set by `fx set-device`)"
)
EXPECTED_TEXT_OUTPUT = """
Environment Info:
Current build directory: <<BUILD_DIR>>
Source Info:
Is fuchsia source project in JIRI_HEAD?: true
Has Jiri overrides?: false (output of 'jiri override -list')
Build Info:
Board: x64 (//boards/x64.gni)
Product: core (//products/core.gni)
Product bundle: main (set with `fx set-main-pb`)
Base packages: [//other:tests] (--with-base argument of `fx set`)
Cache packages: [//src/other:tests] (--with-cache argument of `fx set`)
Universe packages: [//scripts:tests, //tools/devshell/python:tests] (--with argument of `fx set`)
Developer tests: [//src/other:tests] (--with-test argument of `fx set`)
Compilation mode: debug
"""
EXPECTED_TEXT_OUTPUT_NO_PB = """
Environment Info:
Current build directory: <<BUILD_DIR>>
Source Info:
Is fuchsia source project in JIRI_HEAD?: true
Has Jiri overrides?: false (output of 'jiri override -list')
Build Info:
Board: x64 (//boards/x64.gni)
Product: core (//products/core.gni)
Base packages: [//other:tests] (--with-base argument of `fx set`)
Cache packages: [//src/other:tests] (--with-cache argument of `fx set`)
Universe packages: [//scripts:tests, //tools/devshell/python:tests] (--with argument of `fx set`)
Developer tests: [//src/other:tests] (--with-test argument of `fx set`)
Compilation mode: debug
"""
EXPECTED_JSON_OUTPUT = """
{
"environmentInfo": {
"name": "Environment Info",
"items": {
"build_dir": {
"title": "Current build directory",
"value": "<<BUILD_DIR>>",
"notes": null
}
}
},
"sourceInfo": {
"name": "Source Info",
"items": {
"is_in_jiri_head": {
"title": "Is fuchsia source project in JIRI_HEAD?",
"value": true,
"notes": null
},
"has_jiri_overrides": {
"title": "Has Jiri overrides?",
"value": false,
"notes": "output of 'jiri override -list'"
}
}
},
"buildInfo": {
"name": "Build Info",
"items": {
"boards": {
"title": "Board",
"value": "x64",
"notes": "//boards/x64.gni"
},
"products": {
"title": "Product",
"value": "core",
"notes": "//products/core.gni"
},
"main_pb_label": {
"title": "Product bundle",
"value": "main",
"notes": "set with `fx set-main-pb`"
},
"base_package_labels": {
"title": "Base packages",
"value": [
"//other:tests"
],
"notes": "--with-base argument of `fx set`"
},
"cache_package_labels": {
"title": "Cache packages",
"value": [
"//src/other:tests"
],
"notes": "--with-cache argument of `fx set`"
},
"universe_package_labels": {
"title": "Universe packages",
"value": [
"//scripts:tests",
"//tools/devshell/python:tests"
],
"notes": "--with argument of `fx set`"
},
"developer_test_labels": {
"title": "Developer tests",
"value": [
"//src/other:tests"
],
"notes": "--with-test argument of `fx set`"
},
"compilation_mode": {
"title": "Compilation mode",
"value": "debug",
"notes": null
}
}
}
}"""
EXPECTED_JSON_OUTPUT_NO_PB = """
{
"environmentInfo": {
"name": "Environment Info",
"items": {
"build_dir": {
"title": "Current build directory",
"value": "<<BUILD_DIR>>",
"notes": null
}
}
},
"sourceInfo": {
"name": "Source Info",
"items": {
"is_in_jiri_head": {
"title": "Is fuchsia source project in JIRI_HEAD?",
"value": true,
"notes": null
},
"has_jiri_overrides": {
"title": "Has Jiri overrides?",
"value": false,
"notes": "output of 'jiri override -list'"
}
}
},
"buildInfo": {
"name": "Build Info",
"items": {
"boards": {
"title": "Board",
"value": "x64",
"notes": "//boards/x64.gni"
},
"products": {
"title": "Product",
"value": "core",
"notes": "//products/core.gni"
},
"base_package_labels": {
"title": "Base packages",
"value": [
"//other:tests"
],
"notes": "--with-base argument of `fx set`"
},
"cache_package_labels": {
"title": "Cache packages",
"value": [
"//src/other:tests"
],
"notes": "--with-cache argument of `fx set`"
},
"universe_package_labels": {
"title": "Universe packages",
"value": [
"//scripts:tests",
"//tools/devshell/python:tests"
],
"notes": "--with argument of `fx set`"
},
"developer_test_labels": {
"title": "Developer tests",
"value": [
"//src/other:tests"
],
"notes": "--with-test argument of `fx set`"
},
"compilation_mode": {
"title": "Compilation mode",
"value": "debug",
"notes": null
}
}
}
}"""