| #!/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()) |