blob: 480d662dcac18f916d027047a38e3440a1ce7641 [file] [log] [blame]
# 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 errno
import filecmp
import os
import sys
import plasa_differ
from enum import Enum
class Policy(Enum):
# update_golden tells this script to overwrite the existing golden with
# the current golden. This is useful for batch updates.
update_golden = "update_golden"
# ack_changes tells this script to fail if any changes are detected.
# It will tell the user how to update the golden to acknowledge the
# changes.
ack_changes = "ack_changes"
# no_breaking_changes tells this script to fail if breaking changes
# are detected. If non-breaking changes are detected it will tell
# the user how to update the golden to acknowledge the changes.
no_breaking_changes = "no_breaking_changes"
# no_changes tells this script to fail if any changes are detected.
# It will not ask the user to update the golden.
no_changes = "no_changes"
def __str__(self):
return self.value
FIDL_VERSIONING_DOCS_URL = (
"https://fuchsia.dev/fuchsia-src/reference/fidl/language/versioning"
)
FIDL_AVAILABILITY_HINT = (
f"Are you missing an @available annotation?\n"
f"For more information see {FIDL_VERSIONING_DOCS_URL}\n"
)
class CompatibilityError(Exception):
"""Exception raised when breaking API changes are detected"""
def __init__(self, api_level, breaking_changes, current, golden):
self.api_level = api_level
self.breaking_changes = breaking_changes
self.current = current
self.golden = golden
def __str__(self):
formatted_breaking_changes = "\n - ".join(self.breaking_changes)
return (
f"These changes are incompatible with API level {self.api_level}\n"
f"{formatted_breaking_changes}\n\n"
f"{FIDL_AVAILABILITY_HINT}"
)
class GoldenMismatchError(Exception):
"""Exception raised when a stale golden file is detected."""
def __init__(
self,
api_level,
current,
golden,
show_update_hint=False,
show_fidl_availability_hint=True,
):
self.api_level = api_level
self.current = current
self.golden = golden
self.show_update_hint = show_update_hint
self.show_fidl_availability_hint = show_fidl_availability_hint
def __str__(self):
hints = []
if self.show_fidl_availability_hint:
hints.append(FIDL_AVAILABILITY_HINT)
if self.show_update_hint:
cmd = update_cmd(self.current, self.golden)
hints.append(
f"Please acknowledge this change by updating the golden.\n"
f"To do this, please run:\n"
f" {cmd}\n"
)
hint_lines = "\n".join(hints)
return f"Detected changes to API level {self.api_level}\n" + hint_lines
def golden_not_found_error(filename):
message = (
f"The golden file {filename} does not exist.\n"
f"If this is a new FIDL API you must first create this file.\n"
f"To do so, run:\n"
f" touch {os.path.abspath(filename)}\n"
)
return FileNotFoundError(errno.ENOENT, message, filename)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--policy",
help="How to handle failures",
type=Policy,
default=Policy.no_breaking_changes,
choices=list(Policy),
)
parser.add_argument(
"--depfile", help="Where to write the depfile", required=True
)
parser.add_argument(
"--api-level", help="The API level being tested", required=True
)
parser.add_argument(
"--golden", help="Path to the golden file", required=True
)
parser.add_argument(
"--current", help="Path to the local file", required=True
)
parser.add_argument(
"--stamp", help="Path to the victory file", required=True
)
parser.add_argument(
"--fidl_api_diff_path",
help="Path to the fidl_api_diff binary",
required=True,
)
args = parser.parse_args()
dependencies = [args.current, args.golden]
if os.path.getsize(args.current) == 0 and not os.path.exists(args.golden):
# Skip testing if current is empty and golden is missing.
# Also, do not generate such files when updating goldens.
# Do not depend on files that do not and should not exist.
dependencies.remove(args.golden)
err = None
elif args.policy == Policy.update_golden:
err = update_golden(args)
elif not os.path.exists(args.golden):
# In all remaining cases, the golden must exist.
err = golden_not_found_error(args.golden)
elif args.policy == Policy.no_breaking_changes:
err = fail_on_breaking_changes(args)
elif args.policy == Policy.no_changes:
err = fail_on_changes(args)
elif args.policy == Policy.ack_changes:
err = fail_on_unacknowledged_changes(args)
else:
raise ValueError("unknown policy: {}".format(args.policy))
with open(args.depfile, "w") as f:
f.write("{}: {}\n".format(args.stamp, " ".join(dependencies)))
with open(args.stamp, "w") as stamp_file:
stamp_file.write("Golden!\n")
if not err:
return 0
print("\nERROR: ", err)
return 1
def update_golden(args):
import subprocess
c = update_cmd(args.current, args.golden)
if c is not None:
subprocess.run(c.split())
return None
def fail_on_breaking_changes(args):
"""Fails if current is not backward compatible with golden or if
current and golden aren't identical.
"""
differ = plasa_differ.PlasaDiffer(args.fidl_api_diff_path)
breaking_changes = differ.find_breaking_changes_in_fragment_file(
args.golden, args.current
)
if breaking_changes:
return CompatibilityError(
api_level=args.api_level,
breaking_changes=breaking_changes,
current=args.current,
golden=args.golden,
)
if not filecmp.cmp(args.golden, args.current):
return GoldenMismatchError(
api_level=args.api_level,
current=args.current,
golden=args.golden,
show_update_hint=True,
# No breaking changes so the user must be using `@available` correctly.
show_fidl_availability_hint=False,
)
return None
def fail_on_unacknowledged_changes(args):
"""Asks the user to fix the golden if current and golden aren't identical."""
if not filecmp.cmp(args.golden, args.current):
return GoldenMismatchError(
api_level=args.api_level,
current=args.current,
golden=args.golden,
show_update_hint=True,
)
return None
def fail_on_changes(args):
"""Fails if current and golden aren't identical."""
if not filecmp.cmp(args.golden, args.current):
return GoldenMismatchError(
api_level=args.api_level,
current=args.current,
golden=args.golden,
)
return None
# Updates `golden` if it does not exist or is different from `current`.
def update_cmd(current, golden):
# Disable shallow comparison which doesn't prevent unnecessary writes. It
# ignores file contents and only compares file type, size and mod time.
if not os.path.exists(golden) or not filecmp.cmp(
current, golden, shallow=False
):
return "cp {} {}".format(
os.path.abspath(current), os.path.abspath(golden)
)
if __name__ == "__main__":
sys.exit(main())