blob: 2687626fc021a9046ecbc28769f900314c06fd51 [file] [edit]
# Copyright 2026 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 argparse
import asyncio
import collections
import dataclasses
import json
import os
import shutil
import subprocess
import sys
import tempfile
import typing
import build_dir
import fx_cmd
import statusinfo
HTTP_PORT = 6240
@dataclasses.dataclass
class StatsData:
categories: dict[str, dict[str, int]] = dataclasses.field(
default_factory=dict
)
tags: dict[str, int] = dataclasses.field(default_factory=dict)
class Options(argparse.Namespace):
def __init__(self) -> None:
super().__init__()
self.web: bool = False
self.stats: bool = False
self.paths: list[str] = []
self.port: int = HTTP_PORT
async def main(arg_override: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Manage test categories")
parser.add_argument(
"paths",
nargs="*",
help="Paths to query or show stats for",
)
parser.add_argument(
"--stats",
action="store_true",
help="Output stats instead of details",
)
parser.add_argument(
"--web",
action="store_true",
help="Run the web server to visualize test categories",
)
parser.add_argument(
"-p",
"--port",
type=int,
default=HTTP_PORT,
help=f"Port to run the web server on (default: {HTTP_PORT})",
)
args = parser.parse_args(args=arg_override, namespace=Options())
fuchsia_dir = os.environ.get("FUCHSIA_DIR")
if not fuchsia_dir:
print("FUCHSIA_DIR environment variable not set.")
sys.exit(1)
out_dir = os.path.join(fuchsia_dir, "out", "test-categories-only")
# Check if metadata exists in current build directory
current_build_dir = build_dir.get_build_directory()
metadata_path = os.path.join(current_build_dir, "testing_metadata.json")
if os.path.exists(metadata_path):
important(f"Using existing metadata from {metadata_path}")
else:
metadata_path = os.path.join(out_dir, "testing_metadata.json")
important(f"Metadata not found in build dir. Generating in {out_dir}")
await ensure_metadata(out_dir)
metadata = load_metadata_from_path(metadata_path)
if args.web:
await run_web_server(fuchsia_dir, metadata_path, args.port)
elif args.stats:
show_stats(metadata, args.paths, fuchsia_dir=fuchsia_dir)
else:
show_details(metadata, args.paths, fuchsia_dir=fuchsia_dir)
def important(text: str) -> None:
print(statusinfo.highlight(text))
def green(text: str) -> None:
print(statusinfo.green_highlight(text))
def dim(text: str) -> None:
print(statusinfo.dim(text))
async def ensure_metadata(out_dir: str) -> None:
fx = fx_cmd.FxCmd(build_directory=out_dir)
set_cmd = [
"set",
"--no-change-env",
"minimal.x64",
"--with",
"//tools/testing_metadata",
]
important(f"Running: fx --dir {out_dir} " + " ".join(set_cmd))
def output_callback(label: str, event: typing.Any) -> None:
print(
f"[{label}] {event.text.decode('utf-8', errors='replace')}",
end="",
file=sys.stderr,
)
await fx.sync(
*set_cmd,
stdout_callback=lambda e: output_callback("set", e),
stderr_callback=lambda e: output_callback("set", e),
)
build_cmd = ["build", "//tools/testing_metadata"]
important(f"Running: fx --dir {out_dir} " + " ".join(build_cmd))
await fx.sync(
*build_cmd,
stdout_callback=lambda e: output_callback("build", e),
stderr_callback=lambda e: output_callback("build", e),
)
def load_metadata_from_path(path: str) -> dict[str, typing.Any]:
if not os.path.exists(path):
print(f"Error: Metadata file not found at {path}")
sys.exit(1)
with open(path, "r") as f:
return json.load(f)
def show_details(
data: dict[str, typing.Any], paths: list[str], fuchsia_dir: str
) -> None:
metadata = data.get("metadata", {})
paths = [p.rstrip("/") for p in paths]
if not paths:
# If no paths listed, show current directory only.
paths = [os.path.relpath(os.getcwd(), fuchsia_dir)]
if paths == ["."]:
paths = [""]
for path in paths:
info = metadata.get(path)
if info:
print(f"Path: {path}")
print(json.dumps(info, indent=2))
else:
print(f"Path: {path} (No metadata found)")
def calculate_stats(
data: dict[str, typing.Any], paths: list[str], fuchsia_dir: str
) -> StatsData:
metadata = data.get("metadata", {})
paths = [p.rstrip("/") for p in paths]
# Filter metadata by paths
filtered_metadata: dict[str, typing.Any] = {}
if not paths:
# If no paths listed, show current directory only.
paths = [os.path.relpath(os.getcwd(), fuchsia_dir)]
if paths == ["."]:
filtered_metadata = metadata
else:
for p in paths:
for k, v in metadata.items():
if k.startswith(p):
filtered_metadata[k] = v
cat_subcat: dict[str, collections.Counter[str]] = collections.defaultdict(
collections.Counter
)
tags: collections.Counter[str] = collections.Counter()
for k, v in filtered_metadata.items():
coverage = v.get("coverage", {})
cat = coverage.get("category")
subcat = coverage.get("subcategory") or "None"
tgs = coverage.get("tags", [])
if cat:
cat_subcat[cat][subcat] += 1
for t in tgs:
tags[t] += 1
return StatsData(
categories={cat: dict(subcats) for cat, subcats in cat_subcat.items()},
tags=dict(tags),
)
def show_stats(
data: dict[str, typing.Any], paths: list[str], fuchsia_dir: str
) -> None:
stats = calculate_stats(data, paths, fuchsia_dir)
print("Categories and Subcategories:")
categories = stats.categories
sorted_cats = sorted(
categories.items(), key=lambda x: sum(x[1].values()), reverse=True
)
for cat, subcats in sorted_cats:
total = sum(subcats.values())
print(f" {cat}: {total}")
sorted_subcats = sorted(
subcats.items(), key=lambda x: x[1], reverse=True
)
for subcat, count in sorted_subcats:
print(f" {subcat}: {count}")
print("\nTags:")
tags = stats.tags
sorted_tags = sorted(tags.items(), key=lambda x: x[1], reverse=True)
for k, v in sorted_tags:
print(f" {k}: {v}")
async def run_web_server(
fuchsia_dir: str, metadata_path: str, port: int
) -> None:
tmpdir = tempfile.mkdtemp()
try:
shutil.copy(metadata_path, os.path.join(tmpdir, "metadata.json"))
shutil.copy(
os.path.join(
fuchsia_dir, "tools/testing_metadata/visualization/index.html"
),
os.path.join(tmpdir, "index.html"),
)
server_process = subprocess.Popen(
[
sys.executable,
"-m",
"http.server",
"-b",
"127.0.0.1",
str(port),
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=tmpdir,
)
green(f"View categories at http://localhost:{port}?files=metadata.json")
dim(
"Not seeing the categories you expect? Run `touch BUILD.gn` and then `fx build`"
)
print("")
print("Press enter to stop the server.")
await asyncio.to_thread(input)
server_process.terminate()
server_process.wait()
finally:
shutil.rmtree(tmpdir)