generate_scripts.py revision 99b7e65094726c8d7d4c7cc5b6e4d4165e4ce239
1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4from __future__ import print_function 5from collections import namedtuple 6import json, os, sys 7 8AUTOTEST_NAME = 'graphics_PiglitBVT' 9INPUT_DIR = './piglit_logs/' 10OUTPUT_DIR = './test_scripts/' 11OUTPUT_FILE_PATTERN = OUTPUT_DIR + '/%s/' + AUTOTEST_NAME + '_%d.sh' 12OUTPUT_FILE_SLICES = 20 13PIGLIT_PATH = '/usr/local/autotest/deps/piglit/piglit/' 14# Do not generate scripts with "bash -e" as we want to handle errors ourself. 15FILE_HEADER = '#!/bin/bash\n\n' 16 17# Script fragment function that kicks off individual piglit tests. 18FILE_RUN_TEST = '\n\ 19function run_test()\n\ 20{\n\ 21 local name="$1"\n\ 22 local time="$2"\n\ 23 local command="$3"\n\ 24 echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"\n\ 25 echo "Running test \"$name\" of expected runtime $time sec: $command"\n\ 26 sync\n\ 27 $command\n\ 28 if [ $? == 0 ] ; then\n\ 29 let "need_pass--"\n\ 30 else\n\ 31 let "failures++"\n\ 32 fi\n\ 33}\n\ 34' 35 36# Script fragment that sumarizes the overall status. 37FILE_SUMMARY = 'popd\n\ 38\n\ 39if [ $need_pass == 0 ] ; then\n\ 40 echo "+---------------------------------------------+"\n\ 41 echo "| Overall pass, as all %d tests have passed. |"\n\ 42 echo "+---------------------------------------------+"\n\ 43else\n\ 44 echo "+-----------------------------------------------------------+"\n\ 45 echo "| Overall failure, as $need_pass tests did not pass and $failures failed. |"\n\ 46 echo "+-----------------------------------------------------------+"\n\ 47fi\n\ 48exit $need_pass\n\ 49' 50 51# Control file template for executing a slice. 52CONTROL_FILE = "\ 53# Copyright 2014 The Chromium OS Authors. All rights reserved.\n\ 54# Use of this source code is governed by a BSD-style license that can be\n\ 55# found in the LICENSE file.\n\ 56\n\ 57NAME = '" + AUTOTEST_NAME + "'\n\ 58AUTHOR = 'chromeos-gfx'\n\ 59PURPOSE = 'Collection of automated tests for OpenGL implementations.'\n\ 60CRITERIA = 'All tests in a slice have to pass, otherwise it will fail.'\n\ 61SUITE = 'bvt'\n\ 62EXPERIMENTAL = 'True'\n\ 63TIME='SHORT'\n\ 64TEST_CATEGORY = 'Functional'\n\ 65TEST_CLASS = 'graphics'\n\ 66TEST_TYPE = 'client'\n\ 67\n\ 68DOC = \"\"\"\n\ 69Piglit is a collection of automated tests for OpenGL implementations.\n\ 70\n\ 71The goal of Piglit is to help improve the quality of open source OpenGL drivers\n\ 72by providing developers with a simple means to perform regression tests.\n\ 73\n\ 74This control file runs slice %d out of %d slices of a passing subset of the\n\ 75original collection.\n\ 76\n\ 77http://people.freedesktop.org/~nh/piglit/\n\ 78\"\"\"\n\ 79\n\ 80job.run_test('" + AUTOTEST_NAME + "', test_slice=%d)\ 81" 82 83def output_control_file(sl, slices): 84 """ 85 Write control file for slice sl to disk. 86 """ 87 filename = 'control.%d' % sl 88 with open(filename, 'w+') as f: 89 print(CONTROL_FILE % (sl, slices, sl), file=f) 90 91 92def append_script_header(f, need_pass): 93 """ 94 Write the beginning of the test script to f. 95 """ 96 print(FILE_HEADER, file=f) 97 # need_pass is the script variable that counts down to zero and gets returned. 98 print('need_pass=%d' % need_pass, file=f) 99 print('failures=0', file=f) 100 print('PIGLIT_PATH=%s' % PIGLIT_PATH, file=f) 101 print('export PIGLIT_SOURCE_DIR=%s' % PIGLIT_PATH, file=f) 102 print('export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PIGLIT_PATH/lib', file=f) 103 print('', file=f) 104 print(FILE_RUN_TEST, file=f) 105 print('', file=f) 106 print('pushd $PIGLIT_PATH', file=f) 107 108 109def append_script_summary(f, need_pass): 110 """ 111 Append the summary to the test script f with a required pass count. 112 """ 113 print(FILE_SUMMARY % need_pass, file=f) 114 115 116def mkdir_p(path): 117 """ 118 Create all directories in path. 119 """ 120 try: 121 os.makedirs(path) 122 except OSError: 123 if os.path.isdir(path): 124 pass 125 else: 126 raise 127 128def get_log_filepaths(family_root): 129 """ 130 Find all log files (*main.txt) that were placed into family_root. 131 """ 132 main_files = [] 133 for root, _, files in os.walk(family_root): 134 for filename in files: 135 if filename.endswith('main.txt'): 136 main_files.append(os.path.join(root, filename)) 137 return main_files 138 139 140def load_log_files(main_files): 141 """ 142 The log files are just python dictionaries, load them from disk. 143 """ 144 d = {} 145 for main_file in main_files: 146 #print('Loading file %s' % main_file, file=sys.stderr) 147 d[main_file] = json.loads(open(main_file).read()) 148 return d 149 150 151# Define a Test data structure containing the command line and runtime. 152Test = namedtuple('Test', 'command time passing_count not_passing_count') 153 154def get_test_statistics(d): 155 """ 156 Figures out for each test how often is passed/failed, the command line and 157 how long it runs. 158 """ 159 statistics = {} 160 for main_file in d: 161 for test in d[main_file]['tests']: 162 # Initialize for all known test names to zero stats. 163 statistics[test] = Test(None, 0.0, 0, 0) 164 165 for main_file in d: 166 print('Updating statistics from %s.' % main_file, file=sys.stderr) 167 tests = d[main_file]['tests'] 168 for test in tests: 169 command = statistics[test].command 170 if tests[test]['result'] == 'pass': 171 # A passing test expectation is a no-op and must be ignored. 172 if 'expectation' not in main_file: 173 if 'command' in tests[test]: 174 command = tests[test]['command'] 175 statistics[test] = Test(command, 176 max(tests[test]['time'], 177 statistics[test].time), 178 statistics[test].passing_count + 1, 179 statistics[test].not_passing_count) 180 else: 181 # TODO(ihf): We get a bump due to flaky tests in the expectations file. 182 # While this is intended it should be handled cleaner as it impacts 183 # the computed pass rate. 184 statistics[test] = Test(command, 185 statistics[test].time, 186 statistics[test].passing_count, 187 statistics[test].not_passing_count + 1) 188 189 return statistics 190 191 192def get_max_passing(statistics): 193 """ 194 Gets the maximum count of passes a test has. 195 """ 196 max_passing_count = 0 197 for test in statistics: 198 max_passing_count = max(statistics[test].passing_count, max_passing_count) 199 return max_passing_count 200 201 202def get_passing_tests(statistics): 203 """ 204 Gets a list of all tests that never failed and have a maximum pass count. 205 """ 206 tests = [] 207 max_passing_count = get_max_passing(statistics) 208 for test in statistics: 209 if (statistics[test].passing_count == max_passing_count and 210 statistics[test].not_passing_count == 0): 211 tests.append(test) 212 return sorted(tests) 213 214 215def get_intermittent_tests(statistics): 216 """ 217 Gets tests that failed at least once and passed at least once. 218 """ 219 tests = [] 220 max_passing_count = get_max_passing(statistics) 221 for test in statistics: 222 if (statistics[test].passing_count > 0 and 223 statistics[test].passing_count < max_passing_count and 224 statistics[test].not_passing_count > 0): 225 tests.append(test) 226 return sorted(tests) 227 228 229def process_gpu_family(family, family_root): 230 """ 231 This takes a directory with log files from the same gpu family and processes 232 the result log into |slices| runable scripts. 233 """ 234 print('--> Processing "%s".' % family, file=sys.stderr) 235 main_files = get_log_filepaths(family_root) 236 d = load_log_files(main_files) 237 statistics = get_test_statistics(d) 238 passing_tests = get_passing_tests(statistics) 239 240 slices = OUTPUT_FILE_SLICES 241 current_slice = 1 242 slice_tests = [] 243 time_slice = 0 244 num_processed = 0 245 num_pass_total = len(passing_tests) 246 time_total = 0 247 for test in passing_tests: 248 time_total += statistics[test].time 249 250 # Generate one script containing all tests. This can be used as a simpler way 251 # to run everything, but also to have an easier diff when updating piglit. 252 filename = OUTPUT_FILE_PATTERN % (family, 0) 253 # Ensure the output directory for this family exists. 254 mkdir_p(os.path.dirname(os.path.realpath(filename))) 255 if passing_tests: 256 with open(filename, 'w+') as f: 257 append_script_header(f, num_pass_total) 258 for test in passing_tests: 259 # Make script less location dependent by stripping path from commands. 260 cmd = statistics[test].command.replace(PIGLIT_PATH, '') 261 time_test = statistics[test].time 262 print('run_test "%s" %.1f "%s"' % (test, 0.0, cmd), file=f) 263 append_script_summary(f, num_pass_total) 264 265 # Slice passing tests into several pieces to get below BVT's 20 minute limit. 266 # TODO(ihf): If we ever get into the situation that one test takes more than 267 # time_total / slice we would get an empty slice afterward. Fortunately the 268 # stderr spew should warn the operator of this. 269 for test in passing_tests: 270 # We are still writing all the tests that belong in the current slice. 271 if time_slice < time_total / slices: 272 slice_tests.append(test) 273 time_test = statistics[test].time 274 time_slice += time_test 275 num_processed += 1 276 277 # We finished the slice. Now output the file with all tests in this slice. 278 if time_slice >= time_total / slices or num_processed == num_pass_total: 279 filename = OUTPUT_FILE_PATTERN % (family, current_slice) 280 with open(filename, 'w+') as f: 281 need_pass = len(slice_tests) 282 append_script_header(f, need_pass) 283 for test in slice_tests: 284 # Make script less location dependent by stripping path from commands. 285 cmd = statistics[test].command.replace(PIGLIT_PATH, '') 286 time_test = statistics[test].time 287 # TODO(ihf): Pass proper time_test instead of 0.0 once we can use it. 288 print('run_test "%s" %.1f "%s"' 289 % (test, 0.0, cmd), file=f) 290 append_script_summary(f, need_pass) 291 output_control_file(current_slice, slices) 292 293 print('Slice %d: max runtime for %d passing tests is %.1f seconds.' 294 % (current_slice, need_pass, time_slice), file=sys.stderr) 295 current_slice += 1 296 slice_tests = [] 297 time_slice = 0 298 299 print('Total max runtime on "%s" for %d passing tests is %.1f seconds.' % 300 (family, num_pass_total, time_total), file=sys.stderr) 301 302 # Try to help the person updating piglit by collecting the variance 303 # across different log files into one expectations file per family. 304 output_suggested_expectations(statistics, family, family_root) 305 306 307def output_suggested_expectations(statistics, family, family_root): 308 """ 309 Analyze intermittency and output suggested test expectations. 310 Test expectations are dictionaries with the same structure as logs. 311 """ 312 flaky_tests = get_intermittent_tests(statistics) 313 print('Encountered %d tests that do not always pass in "%s" logs.' % 314 (len(flaky_tests), family), file=sys.stderr) 315 316 if not flaky_tests: 317 return 318 319 max_passing = get_max_passing(statistics) 320 expectations = {} 321 for test in flaky_tests: 322 pass_rate = statistics[test].passing_count / float(max_passing) 323 # Loading a json converts everything to string anyways, so save it as such 324 # and make it only 2 significiant digits. 325 expectations[test] = {'result': 'flaky', 'pass rate' : '%.2f' % pass_rate} 326 327 filename = os.path.join(family_root, 'expectations_%s_main.txt' % family) 328 with open(filename, 'w+') as f: 329 json.dump({'tests': expectations}, f, indent=2, sort_keys=True) 330 331 332def get_gpu_families(root): 333 """ 334 We consider each directory under root a possible gpu family. 335 """ 336 files = os.listdir(root) 337 families = [] 338 for f in files: 339 if os.path.isdir(os.path.join(root, f)): 340 families.append(f) 341 return families 342 343 344def generate_scripts(root): 345 """ 346 For each family under root create the corresponding set of passing test 347 scripts. 348 """ 349 families = get_gpu_families(root) 350 for family in families: 351 process_gpu_family(family, os.path.join(root, family)) 352 353 354# We check the log files in as highly compressed binaries. 355print('Uncompressing log files...', file=sys.stderr) 356os.system('bunzip2 ' + INPUT_DIR + '/*/*/*main.txt.bz2') 357 358# Generate the scripts. 359generate_scripts(INPUT_DIR) 360 361# Binary should remain the same, otherwise use 362# git checkout -- piglit_output 363# or similar to reverse. 364print('Recompressing log files...', file=sys.stderr) 365os.system('bzip2 -9 ' + INPUT_DIR + '/*/*/*main.txt') 366