| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2016 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2016 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Helper script to update the test error expectations based on actual results. | 6 """Helper script to update the test error expectations based on actual results. |
| 7 | 7 |
| 8 This is useful for regenerating test expectations after making changes to the | 8 This is useful for regenerating test expectations after making changes to the |
| 9 error format. | 9 error format. |
| 10 | 10 |
| 11 To use this run the affected tests, and then pass the input to this script | 11 To use this run the affected tests, and then pass the input to this script |
| 12 (either via stdin, or as the first argument). For instance: | 12 (either via stdin, or as the first argument). For instance: |
| 13 | 13 |
| 14 $ ./out/Release/net_unittests --gtest_filter="*VerifyCertificateChain*" | \ | 14 $ ./out/Release/net_unittests --gtest_filter="*VerifyCertificateChain*" | \ |
| 15 net/data/verify_certificate_chain_unittest/rebase-errors.py | 15 net/data/verify_certificate_chain_unittest/rebase-errors.py |
| 16 | 16 |
| 17 The script will search the unit-test (results.txt in above example) and look | 17 The script works by scanning the stdout looking for gtest failures when |
| 18 for failure lines and the corresponding actual error string. | 18 comparing "errors.ToDebugString()". The C++ test side should have been |
| 19 instrumented to dump out the test file's path on mismatch. |
| 19 | 20 |
| 20 It will then go and update the corresponding .pem and .py file. | 21 This script will then update the corresponding file(s) -- a .pem file, and |
| 22 possibly an accompanying .py file. |
| 21 """ | 23 """ |
| 22 | 24 |
| 23 import common | 25 import common |
| 24 import glob | |
| 25 import os | 26 import os |
| 26 import sys | 27 import sys |
| 27 import re | 28 import re |
| 28 | 29 |
| 29 | 30 |
| 31 # Regular expression to find the failed errors in test stdout. |
| 32 # * Group 1 of the match is the actual error text (backslash-escaped) |
| 33 # * Group 2 of the match is file path (relative to //src) where the expected |
| 34 # errors were read from. |
| 35 failed_test_regex = re.compile(r""" |
| 36 Value of: errors.ToDebugString\(\) |
| 37 Actual: "(.*)" |
| 38 (?:.|\n)+? |
| 39 Test file: (.*) |
| 40 """, re.MULTILINE) |
| 41 |
| 42 |
| 43 # Regular expression to find the ERRORS block (and any text above it) in a PEM |
| 44 # file. The assumption is that ERRORS is not the very first block in the file |
| 45 # (since it looks for an -----END to precede it). |
| 46 # * Group 1 of the match is the ERRORS block content and any comments |
| 47 # immediately above it. |
| 48 errors_block_regex = re.compile(r""".* |
| 49 -----END .*?----- |
| 50 |
| 51 (.*? |
| 52 -----BEGIN ERRORS----- |
| 53 .*? |
| 54 -----END ERRORS----- |
| 55 )""", re.MULTILINE | re.DOTALL) |
| 56 |
| 57 |
| 30 def read_file_to_string(path): | 58 def read_file_to_string(path): |
| 31 """Reads a file entirely to a string""" | 59 """Reads a file entirely to a string""" |
| 32 with open(path, 'r') as f: | 60 with open(path, 'r') as f: |
| 33 return f.read() | 61 return f.read() |
| 34 | 62 |
| 35 | 63 |
| 36 def write_string_to_file(data, path): | 64 def write_string_to_file(data, path): |
| 37 """Writes a string to a file""" | 65 """Writes a string to a file""" |
| 38 print "Writing file %s ..." % (path) | 66 print "Writing file %s ..." % (path) |
| 39 with open(path, "w") as f: | 67 with open(path, "w") as f: |
| 40 f.write(data) | 68 f.write(data) |
| 41 | 69 |
| 42 | 70 |
| 43 def get_file_paths_for_test(test_name): | 71 def get_py_path(pem_path): |
| 44 """Returns the file paths (as a tuple) that define a particular unit test. | 72 """Returns the .py filepath used to generate the given .pem path, which may |
| 45 For instance given test name 'IntermediateLacksBasicConstraints' it would | 73 or may not exist. |
| 46 return the paths to: | |
| 47 | 74 |
| 48 * intermediate-lacks-basic-constraints.pem, | 75 Some test files (notably those in verify_certificate_chain_unittest/ have a |
| 49 * generate-intermediate-lacks-basic-constraints.py | 76 "generate-XXX.py" script that builds the "XXX.pem" file. Build the path to |
| 50 """ | 77 the corresponding "generate-XXX.py" (which may or may not exist).""" |
| 51 # The directory that this python script is stored in. | 78 file_name = os.path.basename(pem_path) |
| 52 base_dir = os.path.dirname(os.path.realpath(__file__)) | 79 file_name_no_extension = os.path.splitext(file_name)[0] |
| 53 | 80 py_file_name = 'generate-' + file_name_no_extension + '.py' |
| 54 # The C++ test name is just a camel case verson of the file name. Rather than | 81 return os.path.join(os.path.dirname(pem_path), py_file_name) |
| 55 # converting directly from camel case to a file name, it is simpler to just | |
| 56 # scan the file list and see which matches. (Not efficient but good enough). | |
| 57 paths = glob.glob(os.path.join(base_dir, '*.pem')) | |
| 58 | |
| 59 for pem_path in paths: | |
| 60 file_name = os.path.basename(pem_path) | |
| 61 file_name_no_extension = os.path.splitext(file_name)[0] | |
| 62 | |
| 63 # Strip the hyphens in file name to bring it closer to the camel case. | |
| 64 transformed = file_name_no_extension.replace('-', '') | |
| 65 | |
| 66 # Now all that differs is the case. | |
| 67 if transformed.lower() == test_name.lower(): | |
| 68 py_file_name = 'generate-' + file_name_no_extension + '.py' | |
| 69 py_path = os.path.join(base_dir, py_file_name) | |
| 70 return (pem_path, py_path) | |
| 71 | |
| 72 return None | |
| 73 | 82 |
| 74 | 83 |
| 75 def replace_string(original, start, end, replacement): | 84 def replace_string(original, start, end, replacement): |
| 76 """Replaces the specified range of |original| with |replacement|""" | 85 """Replaces the specified range of |original| with |replacement|""" |
| 77 return original[0:start] + replacement + original[end:] | 86 return original[0:start] + replacement + original[end:] |
| 78 | 87 |
| 79 | 88 |
| 80 def fixup_pem_file(path, actual_errors): | 89 def fixup_pem_file(path, actual_errors): |
| 81 """Updates the ERRORS block in the test .pem file""" | 90 """Updates the ERRORS block in the test .pem file""" |
| 82 contents = read_file_to_string(path) | 91 contents = read_file_to_string(path) |
| 83 | 92 |
| 84 # This assumes that ERRORS is the last thing in file, and comes after the | 93 m = errors_block_regex.search(contents) |
| 85 # VERIFY_RESULT block. | 94 |
| 86 kEndVerifyResult = '-----END VERIFY_RESULT-----' | 95 if not m: |
| 87 contents = contents[0:contents.index(kEndVerifyResult)] | 96 print "Couldn't find ERRORS block in %s" % (path) |
| 88 contents += kEndVerifyResult | 97 return |
| 89 contents += '\n' | 98 |
| 90 contents += '\n' | 99 contents = replace_string(contents, m.start(1), m.end(1), |
| 91 contents += common.text_data_to_pem('ERRORS', actual_errors) | 100 common.text_data_to_pem('ERRORS', actual_errors)) |
| 92 | 101 |
| 93 # Update the file. | 102 # Update the file. |
| 94 write_string_to_file(contents, path) | 103 write_string_to_file(contents, path) |
| 95 | 104 |
| 96 | 105 |
| 97 def fixup_py_file(path, actual_errors): | 106 def fixup_py_file(path, actual_errors): |
| 98 """Replaces the 'errors = XXX' section of the test's python script""" | 107 """Replaces the 'errors = XXX' section of the test's python script""" |
| 99 contents = read_file_to_string(path) | 108 contents = read_file_to_string(path) |
| 100 | 109 |
| 101 # This assumes that the errors variable uses triple quotes. | 110 # This assumes that the errors variable uses triple quotes. |
| 102 prog = re.compile(r'^errors = """(.*)"""', re.MULTILINE | re.DOTALL) | 111 prog = re.compile(r'^errors = """(.*?)"""', re.MULTILINE | re.DOTALL) |
| 103 result = prog.search(contents) | 112 result = prog.search(contents) |
| 104 | 113 |
| 105 # Replace the stuff in between the triple quotes with the actual errors. | 114 # Replace the stuff in between the triple quotes with the actual errors. |
| 106 contents = replace_string(contents, result.start(1), result.end(1), | 115 contents = replace_string(contents, result.start(1), result.end(1), |
| 107 actual_errors) | 116 actual_errors) |
| 108 | 117 |
| 109 # Update the file. | 118 # Update the file. |
| 110 write_string_to_file(contents, path) | 119 write_string_to_file(contents, path) |
| 111 | 120 |
| 112 | 121 |
| 113 def fixup_test(test_name, actual_errors): | 122 def get_src_root(): |
| 114 """Updates the test files used by |test_name|, setting the expected error to | 123 """Returns the path to the enclosing //src directory. This assumes the |
| 124 current script is inside the source tree.""" |
| 125 cur_dir = os.path.dirname(os.path.realpath(__file__)) |
| 126 |
| 127 while True: |
| 128 parent_dir, dirname = os.path.split(cur_dir) |
| 129 # Check if it looks like the src/ root. |
| 130 if dirname == "src" and os.path.isdir(os.path.join(cur_dir, "net")): |
| 131 return cur_dir |
| 132 if not parent_dir or parent_dir == cur_dir: |
| 133 break |
| 134 cur_dir = parent_dir |
| 135 |
| 136 print "Couldn't find src dir" |
| 137 sys.exit(1) |
| 138 |
| 139 |
| 140 def get_abs_path(rel_path): |
| 141 """Converts |rel_path| (relative to src) to a full path""" |
| 142 return os.path.join(get_src_root(), rel_path) |
| 143 |
| 144 |
| 145 def fixup_errors_for_file(actual_errors, pem_path): |
| 146 """Updates the errors in |test_file_path| (.pem file) to match |
| 115 |actual_errors|""" | 147 |actual_errors|""" |
| 116 | 148 |
| 117 # Determine the paths for the corresponding *.pem file and generate-*.py | 149 fixup_pem_file(pem_path, actual_errors) |
| 118 pem_path, py_path = get_file_paths_for_test(test_name) | |
| 119 | 150 |
| 120 fixup_pem_file(pem_path, actual_errors) | 151 # If the test has a generator script update it too. |
| 121 fixup_py_file(py_path, actual_errors) | 152 py_path = get_py_path(pem_path) |
| 122 | 153 if os.path.isfile(py_path): |
| 123 | 154 fixup_py_file(py_path, actual_errors) |
| 124 kTestNamePattern = (r'^\[ RUN \] VerifyCertificateChain/' | |
| 125 'VerifyCertificateChainSingleRootTest/0\.(.*)$') | |
| 126 kValueOfLine = 'Value of: errors.ToDebugString()' | |
| 127 kActualPattern = '^ Actual: "(.*)"$' | |
| 128 | 155 |
| 129 | 156 |
| 130 def main(): | 157 def main(): |
| 131 if len(sys.argv) > 2: | 158 if len(sys.argv) > 2: |
| 132 print 'Usage: %s [path-to-unittest-stdout]' % (sys.argv[0]) | 159 print 'Usage: %s [path-to-unittest-stdout]' % (sys.argv[0]) |
| 133 sys.exit(1) | 160 sys.exit(1) |
| 134 | 161 |
| 135 # Read the input either from a file, or from stdin. | 162 # Read the input either from a file, or from stdin. |
| 136 test_stdout = None | 163 test_stdout = None |
| 137 if len(sys.argv) == 2: | 164 if len(sys.argv) == 2: |
| 138 test_stdout = read_file_to_string(sys.argv[1]) | 165 test_stdout = read_file_to_string(sys.argv[1]) |
| 139 else: | 166 else: |
| 140 print 'Reading input from stdin...' | 167 print 'Reading input from stdin...' |
| 141 test_stdout = sys.stdin.read() | 168 test_stdout = sys.stdin.read() |
| 142 | 169 |
| 143 lines = test_stdout.split('\n') | 170 for m in failed_test_regex.finditer(test_stdout): |
| 144 | 171 actual_errors = m.group(1) |
| 145 # Iterate over each line of the unit test stdout. | 172 actual_errors = actual_errors.decode('string-escape') |
| 146 for i in range(len(lines) - 3): | 173 relative_test_path = m.group(2) |
| 147 # Figure out the name of the test. | 174 fixup_errors_for_file(actual_errors, get_abs_path(relative_test_path)) |
| 148 m = re.search(kTestNamePattern, lines[i]) | |
| 149 if not m: | |
| 150 continue | |
| 151 test_name = m.group(1) | |
| 152 | |
| 153 # Confirm that it is a failure having to do with the errors. | |
| 154 if lines[i + 2] != kValueOfLine: | |
| 155 continue | |
| 156 | |
| 157 # Get the actual error text (which in gtest output is escaped). | |
| 158 m = re.search(kActualPattern, lines[i + 3]) | |
| 159 if not m: | |
| 160 continue | |
| 161 actual = m.group(1) | |
| 162 actual = actual.decode('string-escape') | |
| 163 | |
| 164 fixup_test(test_name, actual) | |
| 165 | 175 |
| 166 | 176 |
| 167 if __name__ == "__main__": | 177 if __name__ == "__main__": |
| 168 main() | 178 main() |
| OLD | NEW |