golden_test.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. #!/usr/bin/env python3
  2. """Compare a command's output against an expected "golden" output file.
  3. Usage:
  4. golden_test.py <golden path> <command> [--update]
  5. <golden path> is the path to the golden file, and <command> is
  6. the command to run, including any arguments. If --update is specified,
  7. the command will be run and its output stored in the golden file.
  8. Otherwise, the command will be run and its output compared against
  9. the contents of the golden file.
  10. For these purposes, the command's output consists of the interleaved
  11. contents of stdout and stderr, as well as the command's exit code. Thus,
  12. golden tests can provide coverage of cases where the command is expected
  13. to fail, as well as cases where it's expected to succeed.
  14. This script is designed to be run by a `golden_test` Bazel rule,
  15. and may not work when run outside that context.
  16. """
  17. __copyright__ = """
  18. Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  19. Exceptions. See /LICENSE for license information.
  20. SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  21. """
  22. import argparse
  23. import difflib
  24. import os
  25. import subprocess
  26. import sys
  27. _ERROR_MESSAGE = """When running under:
  28. {dir}
  29. the golden contents of:
  30. {golden_path}
  31. do not match generated output of:
  32. {subject_cmd_args}
  33. """
  34. _UPDATE_MESSAGE = """To update the golden file, run the following:
  35. bazel run {test_target} -- --update
  36. """
  37. def _parse_args():
  38. """Parses command line arguments, returning the result."""
  39. arg_parser = argparse.ArgumentParser(description=__doc__)
  40. arg_parser.add_argument("golden_path", help="The path to the golden file.")
  41. arg_parser.add_argument(
  42. "subject_command", help="The command line to compare output with."
  43. )
  44. arg_parser.add_argument(
  45. "--golden_is_subset",
  46. action="store_true",
  47. help="Indicates that the golden file will be a subset of output, "
  48. "rather than full output.",
  49. )
  50. arg_parser.add_argument(
  51. "--update",
  52. action="store_true",
  53. help="Whether to update the golden file.",
  54. )
  55. return arg_parser.parse_args()
  56. def _get_subject_output(args):
  57. """Returns output from the subject command."""
  58. subject_cmd = subprocess.run(
  59. args=args.subject_command.split(),
  60. stdout=subprocess.PIPE, # Capture stdout as a string
  61. stderr=subprocess.STDOUT, # Send stderr to the same place as stdout
  62. universal_newlines=True,
  63. )
  64. subject = subject_cmd.stdout
  65. if subject_cmd.returncode != 0:
  66. subject += "EXIT CODE: {0}\n".format(subject_cmd.returncode)
  67. return subject
  68. def _check_diff(args, subject):
  69. """Prints and checks the diff. Returns the appropriate exit code."""
  70. subject_lines = subject.splitlines(keepends=True)
  71. with open(args.golden_path) as golden:
  72. golden_lines = list(golden.readlines())
  73. if args.golden_is_subset:
  74. golden_set = frozenset(golden_lines)
  75. subject_lines = [line for line in subject_lines if line in golden_set]
  76. context_diff = list(
  77. difflib.context_diff(
  78. subject_lines, golden_lines, fromfile="subject", tofile="golden"
  79. )
  80. )
  81. if context_diff:
  82. if args.golden_is_subset:
  83. # Print subject output for context, because it may be useful in
  84. # debugging.
  85. print("=" * 80)
  86. print("Subject output (including ignored lines)")
  87. print("=" * 80)
  88. print(subject)
  89. print("=" * 80)
  90. print("Output diff")
  91. print("=" * 80)
  92. sys.stdout.writelines(context_diff)
  93. print("=" * 80)
  94. print(
  95. _ERROR_MESSAGE.format(
  96. dir=os.getenv("TEST_SRCDIR"),
  97. golden_path=args.golden_path,
  98. subject_cmd_args=args.subject_command,
  99. )
  100. )
  101. if not args.golden_is_subset:
  102. print(
  103. _UPDATE_MESSAGE.format(
  104. test_target=os.getenv("TEST_TARGET"),
  105. )
  106. )
  107. return 1
  108. else:
  109. print("PASS")
  110. return 0
  111. def main():
  112. args = _parse_args()
  113. subject = _get_subject_output(args)
  114. if args.update:
  115. with open(args.golden_path, "w") as golden:
  116. golden.write(subject)
  117. return 0
  118. return _check_diff(args, subject)
  119. if __name__ == "__main__":
  120. sys.exit(main())