blob: 281a4d6e2702f2bb8902d268ad1cf5cf906676bd [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# 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 argparse
import json
import sys
from typing import Any, TextIO
# Create new dict/lists containers that are hashable, but first remove any
# |prefixes| from nested strings.
def make_hashable(item: Any, prefixes: list[str]) -> Any:
if isinstance(item, dict):
item = hashabledict(item, prefixes)
elif isinstance(item, list):
item = hashablelist(item, prefixes)
elif isinstance(item, str):
for prefix in prefixes:
item = item.removeprefix(prefix)
return item
# A list that is hashable so it can be added to a set.
class hashablelist(list[Any]):
def __init__(self, list_items: list[Any], prefixes: list[str]) -> None:
for v in list_items:
self.append(make_hashable(v, prefixes))
def __hash__(self) -> int: # type: ignore[override]
return hash(tuple(sorted(self)))
def __eq__(self, other: Any) -> bool:
if isinstance(other, hashablelist):
return sorted(self) == sorted(other)
return False
# A dictionary that is hashable so it can be added to a set.
class hashabledict(dict[str, object]):
def __init__(self, dict: dict[str, object], prefixes: list[str]) -> None:
for k, v in dict.items():
self[k] = make_hashable(v, prefixes)
def __hash__(self) -> int: # type: ignore[override]
return hash(tuple(sorted(self.items())))
# Loads a set of items from a json file and optionally, uses |key| to get the list.
# Any |prefixes| will be stripped from strings in each item.
def load_set_from_json(
log: TextIO, key: str, prefixes: list[str]
) -> set[object]:
items = json.load(log)
if key:
items = items.get(key, [])
return set(make_hashable(i, prefixes) for i in items)
def main() -> int:
parser = argparse.ArgumentParser(
description="Compare two json lists generated by different assembly invocations"
"and assert that they are functionally equivalent"
)
parser.add_argument(
"--reference", type=argparse.FileType("r"), required=True
)
parser.add_argument(
"--comparison", type=argparse.FileType("r"), required=True
)
parser.add_argument("--stamp", type=argparse.FileType("w"), required=True)
parser.add_argument("--strip-prefix", nargs="*")
parser.add_argument("--list-key", type=str)
args = parser.parse_args()
reference = load_set_from_json(
args.reference, args.list_key, args.strip_prefix
)
comparison = load_set_from_json(
args.comparison, args.list_key, args.strip_prefix
)
# Checks for missing commands.
missing = reference.difference(comparison)
extra = comparison.difference(reference)
if missing:
print("Items in the reference were missing from the comparison")
for cmd in missing:
print("Missing: " + str(cmd))
if extra:
print("Items in the comparison were not found in the reference")
for cmd in extra:
print("Extra: " + str(cmd))
if missing or extra:
return len(missing) + len(extra)
# Writes an empty file because GN requires targets to have an output.
with args.stamp as stamp:
stamp.write("")
return 0
if __name__ == "__main__":
sys.exit(main())