1#!/usr/bin/env python3
2# Copyright 2016 The Chromium Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Helper script to update the test error expectations based on actual results.
17
18This is useful for regenerating test expectations after making changes to the
19error format.
20
21To use this run the affected tests, and then pass the input to this script
22(either via stdin, or as the first argument). For instance:
23
24  $ ./build/pki_test --gtest_filter="*ParseCertificate*:*ParsedCertificate*:" | \
25     pki/testdata/parse_certificate_unittest/rebase-errors.py
26
27The script works by scanning the stdout looking for gtest failures having a
28particular format. The C++ test side should have been instrumented to dump
29out the test file's path on mismatch.
30
31This script will then update the corresponding .pem file
32"""
33
34import base64
35import os
36import re
37import sys
38
39# Regular expression to find the failed errors in test stdout.
40#  * Group 1 of the match is file path (relative to //src) where the
41#    expected errors were read from.
42#  * Group 2 of the match is the actual error text
43failed_test_regex = re.compile(r"""
44Cert errors don't match expectations \((.+?)\)
45
46EXPECTED:
47
48(?:.|\n)*?
49ACTUAL:
50
51((?:.|\n)*?)
52===> Use pki/testdata/parse_certificate_unittest/rebase-errors.py to rebaseline.
53""", re.MULTILINE)
54
55
56# Regular expression to find the ERRORS block (and any text above it) in a PEM
57# file. The assumption is that ERRORS is not the very first block in the file
58# (since it looks for an -----END to precede it).
59#  * Group 1 of the match is the ERRORS block content and any comments
60#    immediately above it.
61errors_block_regex = re.compile(r""".*
62-----END .*?-----
63(.*?
64-----BEGIN ERRORS-----
65.*?
66-----END ERRORS-----)""", re.MULTILINE | re.DOTALL)
67
68
69def read_file_to_string(path):
70  """Reads a file entirely to a string"""
71  with open(path, 'r') as f:
72    return f.read()
73
74
75def write_string_to_file(data, path):
76  """Writes a string to a file"""
77  print("Writing file %s ..." % (path))
78  with open(path, "w") as f:
79    f.write(data)
80
81
82def replace_string(original, start, end, replacement):
83  """Replaces the specified range of |original| with |replacement|"""
84  return original[0:start] + replacement + original[end:]
85
86
87def text_data_to_pem(block_header, text_data):
88  # b64encode takes in bytes and returns bytes.
89  pem_data = base64.b64encode(text_data.encode('utf8')).decode('utf8')
90  return '%s\n-----BEGIN %s-----\n%s\n-----END %s-----\n' % (
91      text_data, block_header, pem_data, block_header)
92
93
94def fixup_pem_file(path, actual_errors):
95  """Updates the ERRORS block in the test .pem file"""
96  contents = read_file_to_string(path)
97
98  errors_block_text = '\n' + text_data_to_pem('ERRORS', actual_errors)
99  # Strip the trailing newline.
100  errors_block_text = errors_block_text[:-1]
101
102  m = errors_block_regex.search(contents)
103
104  if not m:
105    contents += errors_block_text
106  else:
107    contents = replace_string(contents, m.start(1), m.end(1),
108                              errors_block_text)
109
110  # Update the file.
111  write_string_to_file(contents, path)
112
113
114def get_src_root():
115  """Returns the path to BoringSSL source tree. This assumes the
116  current script is inside the source tree."""
117  cur_dir = os.path.dirname(os.path.realpath(__file__))
118
119  while True:
120    # Check if it looks like the BoringSSL root.
121    if os.path.isdir(os.path.join(cur_dir, "crypto")) and \
122       os.path.isdir(os.path.join(cur_dir, "pki")) and \
123       os.path.isdir(os.path.join(cur_dir, "ssl")):
124      return cur_dir
125    parent_dir, _ = os.path.split(cur_dir)
126    if not parent_dir or parent_dir == cur_dir:
127      break
128    cur_dir = parent_dir
129
130  print("Couldn't find src dir")
131  sys.exit(1)
132
133
134def get_abs_path(rel_path):
135  """Converts |rel_path| (relative to src) to a full path"""
136  return os.path.join(get_src_root(), rel_path)
137
138
139def main():
140  if len(sys.argv) > 2:
141    print('Usage: %s [path-to-unittest-stdout]' % (sys.argv[0]))
142    sys.exit(1)
143
144  # Read the input either from a file, or from stdin.
145  test_stdout = None
146  if len(sys.argv) == 2:
147    test_stdout = read_file_to_string(sys.argv[1])
148  else:
149    print('Reading input from stdin...')
150    test_stdout = sys.stdin.read()
151
152  for m in failed_test_regex.finditer(test_stdout):
153    src_relative_errors_path = "pki/" + m.group(1)
154    errors_path = get_abs_path(src_relative_errors_path)
155    actual_errors = m.group(2)
156
157    fixup_pem_file(errors_path, actual_errors)
158
159
160if __name__ == "__main__":
161  main()
162