halcanary@google.com | fed3037 | 2013-10-04 12:46:45 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | # Copyright 2013 Google Inc. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """ |
| 8 | This script will take as an argument either a list of skp files or a |
| 9 | set of directories that contains skp files. It will then test each |
| 10 | skp file with the `render_pictures` program. If that program either |
| 11 | spits out any unexpected output or doesn't return 0, I will flag that |
| 12 | skp file as problematic. We then extract all of the embedded images |
| 13 | inside the skp and test each one of them against the |
| 14 | SkImageDecoder::DecodeFile function. Again, we consider any |
| 15 | extraneous output or a bad return value an error. In the event of an |
| 16 | error, we retain the image and print out information about the error. |
| 17 | The output (on stdout) is formatted as a csv document. |
| 18 | |
| 19 | A copy of each bad image is left in a directory created by |
| 20 | tempfile.mkdtemp(). |
| 21 | """ |
| 22 | |
| 23 | import glob |
| 24 | import os |
| 25 | import re |
| 26 | import shutil |
| 27 | import subprocess |
| 28 | import sys |
| 29 | import tempfile |
| 30 | import threading |
| 31 | |
| 32 | import test_rendering # skia/trunk/tools. reuse FindPathToProgram() |
| 33 | |
| 34 | USAGE = """ |
| 35 | Usage: |
| 36 | {command} SKP_FILE [SKP_FILES] |
| 37 | {command} SKP_DIR [SKP_DIRS]\n |
| 38 | Environment variables: |
| 39 | To run multiple worker threads, set NUM_THREADS. |
| 40 | To use a different temporary storage location, set TMPDIR. |
| 41 | |
| 42 | """ |
| 43 | |
| 44 | def execute_program(args, ignores=None): |
| 45 | """ |
| 46 | Execute a process and waits for it to complete. Returns all |
| 47 | output (stderr and stdout) after (optional) filtering. |
| 48 | |
| 49 | @param args is passed into subprocess.Popen(). |
| 50 | |
| 51 | @param ignores (optional) is a list of regular expression strings |
| 52 | that will be ignored in the output. |
| 53 | |
| 54 | @returns a tuple (returncode, output) |
| 55 | """ |
| 56 | if ignores is None: |
| 57 | ignores = [] |
| 58 | else: |
| 59 | ignores = [re.compile(ignore) for ignore in ignores] |
| 60 | proc = subprocess.Popen( |
| 61 | args, |
| 62 | stdout=subprocess.PIPE, |
| 63 | stderr=subprocess.STDOUT) |
| 64 | output = ''.join( |
| 65 | line for line in proc.stdout |
| 66 | if not any(bool(ignore.match(line)) for ignore in ignores)) |
| 67 | returncode = proc.wait() |
| 68 | return (returncode, output) |
| 69 | |
| 70 | |
| 71 | def list_files(paths): |
| 72 | """ |
| 73 | Accepts a list of directories or filenames on the command line. |
| 74 | We do not choose to recurse into directories beyond one level. |
| 75 | """ |
| 76 | class NotAFileException(Exception): |
| 77 | pass |
| 78 | for path in paths: |
| 79 | for globbedpath in glob.iglob(path): # useful on win32 |
| 80 | if os.path.isdir(globbedpath): |
| 81 | for filename in os.listdir(globbedpath): |
| 82 | newpath = os.path.join(globbedpath, filename) |
| 83 | if os.path.isfile(newpath): |
| 84 | yield newpath |
| 85 | elif os.path.isfile(globbedpath): |
| 86 | yield globbedpath |
| 87 | else: |
| 88 | raise NotAFileException('{} is not a file'.format(globbedpath)) |
| 89 | |
| 90 | |
| 91 | class BadImageFinder(object): |
| 92 | |
| 93 | def __init__(self, directory=None): |
| 94 | self.render_pictures = test_rendering.FindPathToProgram( |
| 95 | 'render_pictures') |
| 96 | self.test_image_decoder = test_rendering.FindPathToProgram( |
| 97 | 'test_image_decoder') |
| 98 | assert os.path.isfile(self.render_pictures) |
| 99 | assert os.path.isfile(self.test_image_decoder) |
| 100 | if directory is None: |
| 101 | self.saved_image_dir = tempfile.mkdtemp(prefix='skia_skp_test_') |
| 102 | else: |
| 103 | assert os.path.isdir(directory) |
| 104 | self.saved_image_dir = directory |
| 105 | self.bad_image_count = 0 |
| 106 | |
| 107 | def process_files(self, skp_files): |
| 108 | for path in skp_files: |
| 109 | self.process_file(path) |
| 110 | |
| 111 | def process_file(self, skp_file): |
| 112 | assert self.saved_image_dir is not None |
| 113 | assert os.path.isfile(skp_file) |
| 114 | args = [self.render_pictures, '--readPath', skp_file] |
| 115 | ignores = ['^process_in', '^deserializ', '^drawing...', '^Non-defaul'] |
| 116 | returncode, output = execute_program(args, ignores) |
| 117 | if (returncode == 0) and not output: |
| 118 | return |
| 119 | temp_image_dir = tempfile.mkdtemp(prefix='skia_skp_test___') |
| 120 | args = [ self.render_pictures, '--readPath', skp_file, |
| 121 | '--writePath', temp_image_dir, '--writeEncodedImages'] |
| 122 | subprocess.call(args, stderr=open(os.devnull,'w'), |
| 123 | stdout=open(os.devnull,'w')) |
| 124 | for image_name in os.listdir(temp_image_dir): |
| 125 | image_path = os.path.join(temp_image_dir, image_name) |
| 126 | assert(os.path.isfile(image_path)) |
| 127 | args = [self.test_image_decoder, image_path] |
| 128 | returncode, output = execute_program(args, []) |
| 129 | if (returncode == 0) and not output: |
| 130 | os.remove(image_path) |
| 131 | continue |
| 132 | try: |
| 133 | shutil.move(image_path, self.saved_image_dir) |
| 134 | except (shutil.Error,): |
| 135 | # If this happens, don't stop the entire process, |
| 136 | # just warn the user. |
| 137 | os.remove(image_path) |
| 138 | sys.stderr.write('{0} is a repeat.\n'.format(image_name)) |
| 139 | self.bad_image_count += 1 |
| 140 | if returncode == 2: |
| 141 | returncode = 'SkImageDecoder::DecodeFile returns false' |
| 142 | elif returncode == 0: |
| 143 | returncode = 'extra verbosity' |
| 144 | assert output |
| 145 | elif returncode == -11: |
| 146 | returncode = 'segmentation violation' |
| 147 | else: |
| 148 | returncode = 'returncode: {}'.format(returncode) |
| 149 | output = output.strip().replace('\n',' ').replace('"','\'') |
| 150 | suffix = image_name[-3:] |
| 151 | output_line = '"{0}","{1}","{2}","{3}","{4}"\n'.format( |
| 152 | returncode, suffix, skp_file, image_name, output) |
| 153 | sys.stdout.write(output_line) |
| 154 | sys.stdout.flush() |
| 155 | os.rmdir(temp_image_dir) |
| 156 | return |
| 157 | |
| 158 | def main(main_argv): |
| 159 | if not main_argv or main_argv[0] in ['-h', '-?', '-help', '--help']: |
| 160 | sys.stderr.write(USAGE.format(command=__file__)) |
| 161 | return 1 |
| 162 | if 'NUM_THREADS' in os.environ: |
| 163 | number_of_threads = int(os.environ['NUM_THREADS']) |
| 164 | if number_of_threads < 1: |
| 165 | number_of_threads = 1 |
| 166 | else: |
| 167 | number_of_threads = 1 |
| 168 | os.environ['skia_images_png_suppressDecoderWarnings'] = 'true' |
| 169 | os.environ['skia_images_jpeg_suppressDecoderWarnings'] = 'true' |
| 170 | |
| 171 | temp_dir = tempfile.mkdtemp(prefix='skia_skp_test_') |
| 172 | sys.stderr.write('Directory for bad images: {}\n'.format(temp_dir)) |
| 173 | sys.stdout.write('"Error","Filetype","SKP File","Image File","Output"\n') |
| 174 | sys.stdout.flush() |
| 175 | |
| 176 | finders = [ |
| 177 | BadImageFinder(temp_dir) for index in xrange(number_of_threads)] |
| 178 | arguments = [[] for index in xrange(number_of_threads)] |
| 179 | for index, item in enumerate(list_files(main_argv)): |
| 180 | ## split up the given targets among the worker threads |
| 181 | arguments[index % number_of_threads].append(item) |
| 182 | threads = [ |
| 183 | threading.Thread( |
| 184 | target=BadImageFinder.process_files, args=(finder,argument)) |
| 185 | for finder, argument in zip(finders, arguments)] |
| 186 | for thread in threads: |
| 187 | thread.start() |
| 188 | for thread in threads: |
| 189 | thread.join() |
| 190 | number = sum(finder.bad_image_count for finder in finders) |
| 191 | sys.stderr.write('Number of bad images found: {}\n'.format(number)) |
| 192 | return 0 |
| 193 | |
| 194 | if __name__ == '__main__': |
| 195 | exit(main(sys.argv[1:])) |
| 196 | |
| 197 | # LocalWords: skp stdout csv |