1#!/usr/bin/env python3 2 3# devtool stress tester 4# 5# Written by: Paul Eggleton <paul.eggleton@linux.intel.com> 6# 7# Copyright 2015 Intel Corporation 8# 9# This program is free software; you can redistribute it and/or modify 10# it under the terms of the GNU General Public License version 2 as 11# published by the Free Software Foundation. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License along 19# with this program; if not, write to the Free Software Foundation, Inc., 20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21# 22 23import sys 24import os 25import os.path 26import subprocess 27import re 28import argparse 29import logging 30import tempfile 31import shutil 32import signal 33import fnmatch 34 35scripts_lib_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'lib')) 36sys.path.insert(0, scripts_lib_path) 37import scriptutils 38import argparse_oe 39logger = scriptutils.logger_create('devtool-stress') 40 41def select_recipes(args): 42 import bb.tinfoil 43 tinfoil = bb.tinfoil.Tinfoil() 44 tinfoil.prepare(False) 45 46 pkg_pn = tinfoil.cooker.recipecaches[''].pkg_pn 47 (latest_versions, preferred_versions) = bb.providers.findProviders(tinfoil.config_data, tinfoil.cooker.recipecaches[''], pkg_pn) 48 49 skip_classes = args.skip_classes.split(',') 50 51 recipelist = [] 52 for pn in sorted(pkg_pn): 53 pref = preferred_versions[pn] 54 inherits = [os.path.splitext(os.path.basename(f))[0] for f in tinfoil.cooker.recipecaches[''].inherits[pref[1]]] 55 for cls in skip_classes: 56 if cls in inherits: 57 break 58 else: 59 recipelist.append(pn) 60 61 tinfoil.shutdown() 62 63 resume_from = args.resume_from 64 if resume_from: 65 if not resume_from in recipelist: 66 print('%s is not a testable recipe' % resume_from) 67 return 1 68 if args.only: 69 only = args.only.split(',') 70 for onlyitem in only: 71 for pn in recipelist: 72 if fnmatch.fnmatch(pn, onlyitem): 73 break 74 else: 75 print('%s does not match any testable recipe' % onlyitem) 76 return 1 77 else: 78 only = None 79 if args.skip: 80 skip = args.skip.split(',') 81 else: 82 skip = [] 83 84 recipes = [] 85 for pn in recipelist: 86 if resume_from: 87 if pn == resume_from: 88 resume_from = None 89 else: 90 continue 91 92 if args.only: 93 for item in only: 94 if fnmatch.fnmatch(pn, item): 95 break 96 else: 97 continue 98 99 skipit = False 100 for item in skip: 101 if fnmatch.fnmatch(pn, item): 102 skipit = True 103 if skipit: 104 continue 105 106 recipes.append(pn) 107 108 return recipes 109 110 111def stress_extract(args): 112 import bb.process 113 114 recipes = select_recipes(args) 115 116 failures = 0 117 tmpdir = tempfile.mkdtemp() 118 os.setpgrp() 119 try: 120 for pn in recipes: 121 sys.stdout.write('Testing %s ' % (pn + ' ').ljust(40, '.')) 122 sys.stdout.flush() 123 failed = False 124 skipped = None 125 126 srctree = os.path.join(tmpdir, pn) 127 try: 128 bb.process.run('devtool extract %s %s' % (pn, srctree)) 129 except bb.process.ExecutionError as exc: 130 if exc.exitcode == 4: 131 skipped = 'incompatible' 132 else: 133 failed = True 134 with open('stress_%s_extract.log' % pn, 'w') as f: 135 f.write(str(exc)) 136 137 if os.path.exists(srctree): 138 shutil.rmtree(srctree) 139 140 if failed: 141 print('failed') 142 failures += 1 143 elif skipped: 144 print('skipped (%s)' % skipped) 145 else: 146 print('ok') 147 except KeyboardInterrupt: 148 # We want any child processes killed. This is crude, but effective. 149 os.killpg(0, signal.SIGTERM) 150 151 if failures: 152 return 1 153 else: 154 return 0 155 156 157def stress_modify(args): 158 import bb.process 159 160 recipes = select_recipes(args) 161 162 failures = 0 163 tmpdir = tempfile.mkdtemp() 164 os.setpgrp() 165 try: 166 for pn in recipes: 167 sys.stdout.write('Testing %s ' % (pn + ' ').ljust(40, '.')) 168 sys.stdout.flush() 169 failed = False 170 reset = True 171 skipped = None 172 173 srctree = os.path.join(tmpdir, pn) 174 try: 175 bb.process.run('devtool modify -x %s %s' % (pn, srctree)) 176 except bb.process.ExecutionError as exc: 177 if exc.exitcode == 4: 178 skipped = 'incompatible' 179 else: 180 with open('stress_%s_modify.log' % pn, 'w') as f: 181 f.write(str(exc)) 182 failed = 'modify' 183 reset = False 184 185 if not skipped: 186 if not failed: 187 try: 188 bb.process.run('bitbake -c install %s' % pn) 189 except bb.process.CmdError as exc: 190 with open('stress_%s_install.log' % pn, 'w') as f: 191 f.write(str(exc)) 192 failed = 'build' 193 if reset: 194 try: 195 bb.process.run('devtool reset %s' % pn) 196 except bb.process.CmdError as exc: 197 print('devtool reset failed: %s' % str(exc)) 198 break 199 200 if os.path.exists(srctree): 201 shutil.rmtree(srctree) 202 203 if failed: 204 print('failed (%s)' % failed) 205 failures += 1 206 elif skipped: 207 print('skipped (%s)' % skipped) 208 else: 209 print('ok') 210 except KeyboardInterrupt: 211 # We want any child processes killed. This is crude, but effective. 212 os.killpg(0, signal.SIGTERM) 213 214 if failures: 215 return 1 216 else: 217 return 0 218 219 220def main(): 221 parser = argparse_oe.ArgumentParser(description="devtool stress tester", 222 epilog="Use %(prog)s <subcommand> --help to get help on a specific command") 223 parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true') 224 parser.add_argument('-r', '--resume-from', help='Resume from specified recipe', metavar='PN') 225 parser.add_argument('-o', '--only', help='Only test specified recipes (comma-separated without spaces, wildcards allowed)', metavar='PNLIST') 226 parser.add_argument('-s', '--skip', help='Skip specified recipes (comma-separated without spaces, wildcards allowed)', metavar='PNLIST', default='gcc-source-*,kernel-devsrc,package-index,perf,meta-world-pkgdata,glibc-locale,glibc-mtrace,glibc-scripts,os-release') 227 parser.add_argument('-c', '--skip-classes', help='Skip recipes inheriting specified classes (comma-separated) - default %(default)s', metavar='CLASSLIST', default='native,nativesdk,cross,cross-canadian,image,populate_sdk,meta,packagegroup') 228 subparsers = parser.add_subparsers(title='subcommands', metavar='<subcommand>') 229 subparsers.required = True 230 231 parser_modify = subparsers.add_parser('modify', 232 help='Run "devtool modify" followed by a build with bitbake on matching recipes', 233 description='Runs "devtool modify" followed by a build with bitbake on matching recipes') 234 parser_modify.set_defaults(func=stress_modify) 235 236 parser_extract = subparsers.add_parser('extract', 237 help='Run "devtool extract" on matching recipes', 238 description='Runs "devtool extract" on matching recipes') 239 parser_extract.set_defaults(func=stress_extract) 240 241 args = parser.parse_args() 242 243 if args.debug: 244 logger.setLevel(logging.DEBUG) 245 246 import scriptpath 247 bitbakepath = scriptpath.add_bitbake_lib_path() 248 if not bitbakepath: 249 logger.error("Unable to find bitbake by searching parent directory of this script or PATH") 250 return 1 251 logger.debug('Found bitbake path: %s' % bitbakepath) 252 253 ret = args.func(args) 254 255if __name__ == "__main__": 256 main() 257