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 will search the unit-test stdout and looking for a failed value of: |
mattm
2016/09/15 00:42:05
awkward wording
eroman
2016/09/16 01:14:29
Done.
| |
18 for failure lines and the corresponding actual error string. | 18 errors.ToDebugString() |
19 | 19 |
20 It will then go and update the corresponding .pem and .py file. | 20 It will then go and update the corresponding .pem and/or .py files. |
21 | |
22 This script works for tests described in .pem files which use a block | |
23 called "ERRORS", and in the C++ side check "errors.ToDebugString()" | |
21 """ | 24 """ |
22 | 25 |
23 import common | 26 import common |
24 import glob | |
25 import os | 27 import os |
26 import sys | 28 import sys |
27 import re | 29 import re |
28 | 30 |
29 | 31 |
32 kFailedTestPattern = r""" | |
mattm
2016/09/15 00:42:05
do the re.compile for these patterns here rather t
eroman
2016/09/16 01:14:29
Done. Agreed that is better! I also added an expla
| |
33 Value of: errors.ToDebugString\(\) | |
34 Actual: "(.*?)" | |
mattm
2016/09/15 00:42:05
probably should be greedy (safe since this pattern
eroman
2016/09/16 01:14:29
Done.
| |
35 (?:.|\n)+? | |
36 Test file: (.*?) | |
mattm
2016/09/15 00:42:05
here too
eroman
2016/09/16 01:14:29
Done.
| |
37 """ | |
38 | |
39 | |
40 kErrorsBlockPattern = r""" | |
41 -----END .*----- | |
mattm
2016/09/15 00:42:05
non-greedy?
eroman
2016/09/16 01:14:29
Done. (Thanks for the help over IM on this).
| |
42 | |
43 (.*? | |
44 -----BEGIN ERRORS----- | |
45 .*? | |
46 -----END ERRORS----- | |
47 )""" | |
48 | |
49 | |
30 def read_file_to_string(path): | 50 def read_file_to_string(path): |
31 """Reads a file entirely to a string""" | 51 """Reads a file entirely to a string""" |
32 with open(path, 'r') as f: | 52 with open(path, 'r') as f: |
33 return f.read() | 53 return f.read() |
34 | 54 |
35 | 55 |
36 def write_string_to_file(data, path): | 56 def write_string_to_file(data, path): |
37 """Writes a string to a file""" | 57 """Writes a string to a file""" |
38 print "Writing file %s ..." % (path) | 58 print "Writing file %s ..." % (path) |
39 with open(path, "w") as f: | 59 with open(path, "w") as f: |
40 f.write(data) | 60 f.write(data) |
41 | 61 |
42 | 62 |
43 def get_file_paths_for_test(test_name): | 63 def get_py_path(pem_path): |
44 """Returns the file paths (as a tuple) that define a particular unit test. | 64 """Returns the .py filepath used to generate the given .pem path if there is |
45 For instance given test name 'IntermediateLacksBasicConstraints' it would | 65 one. Otherwise returns None.""" |
46 return the paths to: | |
47 | 66 |
48 * intermediate-lacks-basic-constraints.pem, | 67 base_dir = os.path.dirname(pem_path) |
49 * generate-intermediate-lacks-basic-constraints.py | 68 if not base_dir.endswith('/verify_certificate_chain_unittest'): |
mattm
2016/09/15 00:42:05
reasoning for hard coding this? (Instead of say, b
mattm
2016/09/15 00:42:05
os.path.split(base_dir)[1]
eroman
2016/09/16 01:14:29
Done.
| |
50 """ | 69 return None |
51 # The directory that this python script is stored in. | |
52 base_dir = os.path.dirname(os.path.realpath(__file__)) | |
53 | 70 |
54 # The C++ test name is just a camel case verson of the file name. Rather than | 71 file_name = os.path.basename(pem_path) |
55 # converting directly from camel case to a file name, it is simpler to just | 72 file_name_no_extension = os.path.splitext(file_name)[0] |
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 | 73 |
59 for pem_path in paths: | 74 py_file_name = 'generate-' + file_name_no_extension + '.py' |
60 file_name = os.path.basename(pem_path) | 75 return os.path.join(base_dir, py_file_name) |
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 | 76 |
74 | 77 |
75 def replace_string(original, start, end, replacement): | 78 def replace_string(original, start, end, replacement): |
76 """Replaces the specified range of |original| with |replacement|""" | 79 """Replaces the specified range of |original| with |replacement|""" |
77 return original[0:start] + replacement + original[end:] | 80 return original[0:start] + replacement + original[end:] |
78 | 81 |
79 | 82 |
80 def fixup_pem_file(path, actual_errors): | 83 def fixup_pem_file(path, actual_errors): |
81 """Updates the ERRORS block in the test .pem file""" | 84 """Updates the ERRORS block in the test .pem file""" |
82 contents = read_file_to_string(path) | 85 contents = read_file_to_string(path) |
83 | 86 |
84 # This assumes that ERRORS is the last thing in file, and comes after the | 87 prog = re.compile(kErrorsBlockPattern, re.MULTILINE | re.DOTALL) |
85 # VERIFY_RESULT block. | 88 m = prog.search(contents) |
86 kEndVerifyResult = '-----END VERIFY_RESULT-----' | 89 |
87 contents = contents[0:contents.index(kEndVerifyResult)] | 90 if not m: |
88 contents += kEndVerifyResult | 91 print "Couldn't find ERRORS block in %s" % (path) |
89 contents += '\n' | 92 return |
90 contents += '\n' | 93 |
91 contents += common.text_data_to_pem('ERRORS', actual_errors) | 94 contents = replace_string(contents, m.start(1), m.end(1), |
95 common.text_data_to_pem('ERRORS', actual_errors)) | |
92 | 96 |
93 # Update the file. | 97 # Update the file. |
94 write_string_to_file(contents, path) | 98 write_string_to_file(contents, path) |
95 | 99 |
96 | 100 |
97 def fixup_py_file(path, actual_errors): | 101 def fixup_py_file(path, actual_errors): |
98 """Replaces the 'errors = XXX' section of the test's python script""" | 102 """Replaces the 'errors = XXX' section of the test's python script""" |
99 contents = read_file_to_string(path) | 103 contents = read_file_to_string(path) |
100 | 104 |
101 # This assumes that the errors variable uses triple quotes. | 105 # This assumes that the errors variable uses triple quotes. |
102 prog = re.compile(r'^errors = """(.*)"""', re.MULTILINE | re.DOTALL) | 106 prog = re.compile(r'^errors = """(.*)"""', re.MULTILINE | re.DOTALL) |
mattm
2016/09/15 00:42:05
guess this should be non-greedy too
eroman
2016/09/16 01:14:29
Done.
| |
103 result = prog.search(contents) | 107 result = prog.search(contents) |
104 | 108 |
105 # Replace the stuff in between the triple quotes with the actual errors. | 109 # Replace the stuff in between the triple quotes with the actual errors. |
106 contents = replace_string(contents, result.start(1), result.end(1), | 110 contents = replace_string(contents, result.start(1), result.end(1), |
107 actual_errors) | 111 actual_errors) |
108 | 112 |
109 # Update the file. | 113 # Update the file. |
110 write_string_to_file(contents, path) | 114 write_string_to_file(contents, path) |
111 | 115 |
112 | 116 |
113 def fixup_test(test_name, actual_errors): | 117 def get_abs_path(rel_path): |
114 """Updates the test files used by |test_name|, setting the expected error to | 118 """Converts |rel_path| (relative to src) to a full path""" |
119 # Assume the current script lives in a well known location. | |
120 kScriptDir = "net/data/verify_certificate_chain_unittest" | |
121 script_dir = os.path.dirname(os.path.realpath(__file__)) | |
122 if not script_dir.endswith(kScriptDir): | |
123 print "Script is not in expected location: " % (kScriptDir) | |
mattm
2016/09/15 00:42:05
format string doesn't contain %s
eroman
2016/09/16 01:14:29
Done
| |
124 exit(1) | |
mattm
2016/09/15 00:42:05
sys.exit
eroman
2016/09/16 01:14:29
... clearly I didn't test this failure case.
| |
125 | |
126 src_dir = script_dir[0:-len(kScriptDir)] | |
mattm
2016/09/15 00:42:05
instead of hardcoding all that, how about somethin
eroman
2016/09/16 01:14:29
Done.
| |
127 return src_dir + rel_path | |
mattm
2016/09/15 00:42:05
os.path.join
eroman
2016/09/16 01:14:29
Done.
| |
128 | |
129 | |
130 def fixup_errors_for_file(actual_errors, pem_path): | |
131 """Updates the errors in |test_file_path| (.pem file) to match | |
115 |actual_errors|""" | 132 |actual_errors|""" |
116 | 133 |
117 # Determine the paths for the corresponding *.pem file and generate-*.py | 134 fixup_pem_file(pem_path, actual_errors) |
118 pem_path, py_path = get_file_paths_for_test(test_name) | |
119 | 135 |
120 fixup_pem_file(pem_path, actual_errors) | 136 # Tests in verify_certificate_chain_unittests additionally have a .py |
121 fixup_py_file(py_path, actual_errors) | 137 # generator file to update. |
122 | 138 py_path = get_py_path(pem_path) |
123 | 139 if py_path: |
124 kTestNamePattern = (r'^\[ RUN \] VerifyCertificateChain/' | 140 fixup_py_file(py_path, actual_errors) |
125 'VerifyCertificateChainSingleRootTest/0\.(.*)$') | |
126 kValueOfLine = 'Value of: errors.ToDebugString()' | |
127 kActualPattern = '^ Actual: "(.*)"$' | |
128 | 141 |
129 | 142 |
130 def main(): | 143 def main(): |
131 if len(sys.argv) > 2: | 144 if len(sys.argv) > 2: |
132 print 'Usage: %s [path-to-unittest-stdout]' % (sys.argv[0]) | 145 print 'Usage: %s [path-to-unittest-stdout]' % (sys.argv[0]) |
133 sys.exit(1) | 146 sys.exit(1) |
134 | 147 |
135 # Read the input either from a file, or from stdin. | 148 # Read the input either from a file, or from stdin. |
136 test_stdout = None | 149 test_stdout = None |
137 if len(sys.argv) == 2: | 150 if len(sys.argv) == 2: |
138 test_stdout = read_file_to_string(sys.argv[1]) | 151 test_stdout = read_file_to_string(sys.argv[1]) |
139 else: | 152 else: |
140 print 'Reading input from stdin...' | 153 print 'Reading input from stdin...' |
141 test_stdout = sys.stdin.read() | 154 test_stdout = sys.stdin.read() |
142 | 155 |
143 lines = test_stdout.split('\n') | 156 prog = re.compile(kFailedTestPattern, re.MULTILINE) |
144 | 157 |
145 # Iterate over each line of the unit test stdout. | 158 for m in prog.finditer(test_stdout): |
146 for i in range(len(lines) - 3): | 159 actual_errors = m.group(1) |
147 # Figure out the name of the test. | 160 actual_errors = actual_errors.decode('string-escape') |
148 m = re.search(kTestNamePattern, lines[i]) | 161 relative_test_path = m.group(2) |
149 if not m: | 162 fixup_errors_for_file(actual_errors, get_abs_path(relative_test_path)) |
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 | 163 |
166 | 164 |
167 if __name__ == "__main__": | 165 if __name__ == "__main__": |
168 main() | 166 main() |
OLD | NEW |