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