1#!/usr/bin/python2.7 2 3# Copyright 2010, The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17 18"""RenderScript Compiler Test. 19 20Runs subdirectories of tests for the RenderScript compiler. 21""" 22 23import filecmp 24import glob 25import os 26import re 27import shutil 28import subprocess 29import sys 30import unittest 31 32 33__author__ = 'Android' 34 35 36DOTTED_LINE = '................' 37 38 39class SlangTests(unittest.TestCase): 40 """Class to contain all the unittest test cases. 41 42 Tests will be dynamically added to this class as methods. 43 No static tests, so this class is initially empty. 44 See GenerateSlangTest() and AddSlangUnitTests(). 45 46 """ 47 pass 48 49 50def GenerateSlangTest(dir_name): 51 """Creates a test method that can be added as method to SlangTests.""" 52 cwd = os.getcwd() 53 def SlangTest(self): 54 os.chdir(cwd) 55 ExecTest(dir_name, self) 56 return SlangTest 57 58 59def AddSlangUnitTests(test_dirs): 60 """Adds a test to SlangTests for each directory in test_dirs.""" 61 62 for t in test_dirs: 63 # Must start with 'test_' according to unittest 64 test_name = 'test_%s' % t 65 test = GenerateSlangTest(t) 66 # Add test as method to SlangTests with test_name as method name 67 setattr(SlangTests, test_name, test) 68 69 70class Options(object): 71 verbose = 0 72 cleanup = 1 73 update_cts = 0 74 zero_return = 0 75 76 77def CompareFiles(actual, expect): 78 """Compares actual and expect for equality.""" 79 if not os.path.isfile(actual): 80 if Options.verbose: 81 print 'Could not find %s' % actual 82 return False 83 if not os.path.isfile(expect): 84 if Options.verbose: 85 print 'Could not find %s' % expect 86 return False 87 88 return filecmp.cmp(actual, expect, False) 89 90 91def CopyIfDifferent(src, dst): 92 """Updates dst if it is different from src.""" 93 if not CompareFiles(src, dst): 94 if Options.verbose: 95 print 'Copying from %s to %s' % (src, dst) 96 shutil.copyfile(src, dst) 97 98 99def GetCommandLineArgs(filename): 100 """Extracts command line arguments from first comment line in a file.""" 101 f = open(filename, 'r') 102 line = f.readline() 103 f.close() 104 if line[0] == '/' and line[1] == '/': 105 return line[2:].strip() 106 else: 107 return '' 108 109 110def ReadFileToStr(filename): 111 """Returns contents of file as a str.""" 112 with open(filename, 'r') as f: 113 return f.read() 114 115 116def ReportIfDifferFromExpected(tests, name, file1, file2): 117 """Fails tests if file1 and file2 differ.""" 118 if not CompareFiles(file1, file2): 119 if Options.verbose: 120 err_message = ('%s is different:\n' 121 'expected:\n%s\n%s%s\n\n' 122 'actual:\n%s\n%s%s\n') % ( 123 name, 124 DOTTED_LINE, ReadFileToStr(file1), DOTTED_LINE, 125 DOTTED_LINE, ReadFileToStr(file2), DOTTED_LINE) 126 else: 127 err_message = '%s is different' % name 128 tests.fail(err_message) 129 130 131def GetRSFiles(): 132 """Returns a list of files in cwd with extension '.rscript' or '.fs'.""" 133 rs_files = glob.glob('*.rscript') 134 fs_files = glob.glob('*.fs') 135 rs_files += fs_files 136 rs_files.sort() 137 return rs_files 138 139 140def GetOutDir(): 141 """Returns the directory with llvm-rs-cc.""" 142 # If cache has not yet been calculated, do that 143 if GetOutDir.cache is None: 144 try: 145 # ANDROID_HOST_OUT is set after `lunch` on local builds 146 GetOutDir.cache = os.environ['ANDROID_HOST_OUT'] 147 except KeyError: 148 # On build server, we need to get the HOST_OUT Makefile variable 149 # because ANDROID_HOST_OUT is not exported on build server 150 GetOutDir.cache = subprocess.check_output(['bash', '-c', 151 'cd ../../../../.. ; ' 152 'source build/envsetup.sh ' 153 '1> /dev/null 2> /dev/null ; ' 154 'realpath `get_build_var HOST_OUT`']) 155 GetOutDir.cache = GetOutDir.cache.strip() 156 return GetOutDir.cache 157 158 159# Declare/define cache variable for GetOutDir to cache results 160# This way we only need to call subprocesses once to get the directory 161GetOutDir.cache = None 162 163 164def CreateCmd(): 165 """Creates the test command to run for the current test.""" 166 cmd_string = ('%s/bin/llvm-rs-cc -o tmp/ -p tmp/ -MD ' 167 '-I ../../../../../frameworks/rs/script_api/include/ ' 168 '-I ../../../../../external/clang/lib/Headers/') % GetOutDir() 169 base_args = cmd_string.split() 170 rs_files = GetRSFiles() 171 172 # Extra command line arguments can be placed as // comments at the start of 173 # any .rscript file. We automatically bundle up all of these extra args and invoke 174 # llvm-rs-cc with them. 175 extra_args_str = '' 176 for rs_file in rs_files: 177 extra_args_str += GetCommandLineArgs(rs_file) 178 extra_args = extra_args_str.split() 179 180 args = base_args + extra_args + rs_files 181 return args 182 183 184def UpdateCTS(): 185 """Copies resulting files to appropriate CTS directory (if different).""" 186 if glob.glob('IN_CTS'): 187 cts_path = '../../../../../cts/' 188 cts_res_raw_path = cts_path + 'tests/tests/renderscriptlegacy/res/raw/' 189 cts_src_path = cts_path + 'tests/tests/renderscript/src/' 190 for bc_src in glob.glob('tmp/*.bc'): 191 bc_dst = re.sub(r'tmp\/', cts_res_raw_path, bc_src, 1) 192 CopyIfDifferent(bc_src, bc_dst) 193 for java_src in glob.glob('tmp/android/renderscript/cts/*.java'): 194 java_dst = re.sub(r'tmp\/', cts_src_path, java_src, 1) 195 CopyIfDifferent(java_src, java_dst) 196 197 198def Cleanup(): 199 """Cleans up the cwd of any tmp files created in current test.""" 200 try: 201 os.remove('stdout.txt') 202 os.remove('stderr.txt') 203 shutil.rmtree('tmp/') 204 except OSError: 205 pass 206 207 208def CheckTestResult(dir_name, subprocess_ret, tests, args): 209 """Checks the result of the subprocess command to see if it passed/failed. 210 211 If dir_name starts with 'F_', then subprocess is expected to fail. 212 If it instead succeeded, then this test is failed. 213 Vice versa with a dir_name starting with 'P_'. 214 215 Args: 216 dir_name: name of current directory/test name 217 subprocess_ret: return code of subprocess 218 tests: unittest, call tests.fail(reason) when failure 219 args: the arguments for the command that was run 220 """ 221 if dir_name[0:2] == 'F_': 222 if subprocess_ret == 0: 223 if Options.verbose: 224 err_message = ('Command (%s) passed on invalid input\n' 225 'stdout:\n%s\n%s%s\n') % ( 226 ' '.join(args), 227 DOTTED_LINE, ReadFileToStr('stdout.txt'), DOTTED_LINE 228 ) 229 else: 230 err_message = 'Command passed on invalid input' 231 tests.fail(err_message) 232 elif dir_name[0:2] == 'P_': 233 if subprocess_ret != 0: 234 if Options.verbose: 235 err_message = ('Command (%s) failed on valid input\n' 236 'stderr:\n%s\n%s%s\n') % ( 237 ' '.join(args), 238 DOTTED_LINE, ReadFileToStr('stderr.txt'), DOTTED_LINE 239 ) 240 else: 241 err_message = 'Command failed on valid input' 242 tests.fail(err_message) 243 else: 244 tests.fail('Invalid test name: ' + dir_name + 245 ', should start with F_ or P_') 246 247 248def CheckJavaOutput(tests): 249 """Check that the Java output files are as expected. 250 251 Each 'Script*.java.expect' file should have exactly one corresponding file. 252 The two files should match exactly. 253 254 Args: 255 tests: unittest, call tests.fail(reason) when failure 256 """ 257 java_expect = glob.glob('Script*.java.expect') 258 for expect in java_expect: 259 expect_base = expect[:-7] # strip ".expect" suffix 260 find = 'tmp/*/' + expect_base 261 found = glob.glob(find) 262 if len(found) != 1: 263 if not found: 264 tests.fail('%s not found' % find) 265 else: 266 tests.fail('multiple %s found' % find) 267 elif not CompareFiles(found[0], expect): 268 tests.fail('%s and %s are different' % (found[0], expect)) 269 270 271def ExecTest(dir_name, tests): 272 """Executes an llvm-rs-cc test from dir_name.""" 273 274 os.chdir(dir_name) 275 stdout_file = open('stdout.txt', 'w+') 276 stderr_file = open('stderr.txt', 'w+') 277 278 args = CreateCmd() 279 280 if Options.verbose > 1: 281 print 'Executing:', ' '.join(args) 282 283 # Execute the command and check the resulting shell return value. 284 # All tests that are expected to FAIL have directory names that 285 # start with 'F_'. Other tests that are expected to PASS have 286 # directory names that start with 'P_'. 287 ret = 0 288 try: 289 ret = subprocess.call(args, stdout=stdout_file, stderr=stderr_file) 290 except OSError: 291 tests.fail('subprocess.call failed: ' + ' '.join(args)) 292 293 stdout_file.close() 294 stderr_file.close() 295 296 CheckTestResult(dir_name, ret, tests, args) 297 298 ReportIfDifferFromExpected(tests, 'stdout', 'stdout.txt.expect', 'stdout.txt') 299 ReportIfDifferFromExpected(tests, 'stderr', 'stderr.txt.expect', 'stderr.txt') 300 301 CheckJavaOutput(tests) 302 303 if Options.update_cts: 304 UpdateCTS() 305 306 if Options.cleanup: 307 Cleanup() 308 309 310def Usage(): 311 """Print out usage information.""" 312 print ('Usage: %s [OPTION]... [TESTNAME]...' 313 'Renderscript Compiler Test Harness\n' 314 'Runs TESTNAMEs (all tests by default)\n' 315 'Available Options:\n' 316 ' -h, --help Help message\n' 317 ' -n, --no-cleanup Don\'t clean up after running tests\n' 318 ' -u, --update-cts Update CTS test versions\n' 319 ' -v, --verbose Verbose output. Enter multiple -v to get more verbose.\n' 320 ' -z, --zero-return Return 0 as exit code no matter if tests fail. Required for TreeHugger.\n' 321 ) % (sys.argv[0]), 322 return 323 324 325def main(): 326 """Runs the unittest suite. 327 328 Parses command line arguments, adds test directories as tests. 329 330 Returns: 331 0 if '-z' flag is set. 332 Else unittest.main() returns with its own error code. 333 """ 334 335 # Chdir to the directory this file is in since tests are in this directory 336 os.chdir(os.path.dirname(os.path.abspath(__file__))) 337 files = [] 338 for arg in sys.argv[1:]: 339 if arg in ('-h', '--help'): 340 Usage() 341 return 0 342 elif arg in ('-n', '--no-cleanup'): 343 Options.cleanup = 0 344 elif arg in ('-u', '--update-cts'): 345 Options.update_cts = 1 346 elif arg in ('-v', '--verbose'): 347 Options.verbose += 1 348 elif arg in ('-z', '--zero-return'): 349 Options.zero_return = 1 350 else: 351 # Test list to run 352 if os.path.isdir(arg): 353 files.append(arg) 354 else: 355 print >> sys.stderr, 'Invalid test or option: %s' % arg 356 return 1 357 358 if not files: 359 file_names = os.listdir('.') 360 # Test names must start with 'F_' or 'P_' 361 # 'F_' tests are expected to fail 362 # 'P_' tests are expected to pass 363 for f in file_names: 364 if os.path.isdir(f) and (f[0:2] == 'F_' or f[0:2] == 'P_'): 365 files.append(f) 366 files.sort() 367 368 AddSlangUnitTests(files) 369 370 # verbosity=2 is necessary for PythonUnitTestRunner to parse the results 371 # Otherwise verbosity does not matter 372 # If Options.zero_return is set, do not let unittest.main() exit 373 # This is necessary in TreeHugger to distinguish between failing tests and 374 # failing to execute the python script 375 # If Options.zero_return is not set, let unittest.main() exit 376 # In this case it will return a non-zero code if any tests fail 377 unittest_exit = Options.zero_return == 0 378 unittest.main(verbosity=2, 379 argv=[sys.argv[0]] + ['SlangTests'], 380 exit=unittest_exit) 381 382 return 0 383 384 385if __name__ == '__main__': 386 sys.exit(main()) 387 388