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# SPDX-License-Identifier: GPL-2.0-only
10#
11
12import sys
13import os
14import os.path
15import subprocess
16import re
17import argparse
18import logging
19import tempfile
20import shutil
21import signal
22import fnmatch
23
24scripts_lib_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'lib'))
25sys.path.insert(0, scripts_lib_path)
26import scriptutils
27import argparse_oe
28logger = scriptutils.logger_create('devtool-stress')
29
30def select_recipes(args):
31    import bb.tinfoil
32    tinfoil = bb.tinfoil.Tinfoil()
33    tinfoil.prepare(False)
34
35    pkg_pn = tinfoil.cooker.recipecaches[''].pkg_pn
36    (latest_versions, preferred_versions) = bb.providers.findProviders(tinfoil.config_data, tinfoil.cooker.recipecaches[''], pkg_pn)
37
38    skip_classes = args.skip_classes.split(',')
39
40    recipelist = []
41    for pn in sorted(pkg_pn):
42        pref = preferred_versions[pn]
43        inherits = [os.path.splitext(os.path.basename(f))[0] for f in tinfoil.cooker.recipecaches[''].inherits[pref[1]]]
44        for cls in skip_classes:
45            if cls in inherits:
46                break
47        else:
48            recipelist.append(pn)
49
50    tinfoil.shutdown()
51
52    resume_from = args.resume_from
53    if resume_from:
54        if not resume_from in recipelist:
55            print('%s is not a testable recipe' % resume_from)
56            return 1
57    if args.only:
58        only = args.only.split(',')
59        for onlyitem in only:
60            for pn in recipelist:
61                if fnmatch.fnmatch(pn, onlyitem):
62                    break
63            else:
64                print('%s does not match any testable recipe' % onlyitem)
65                return 1
66    else:
67        only = None
68    if args.skip:
69        skip = args.skip.split(',')
70    else:
71        skip = []
72
73    recipes = []
74    for pn in recipelist:
75        if resume_from:
76            if pn == resume_from:
77                resume_from = None
78            else:
79                continue
80
81        if args.only:
82            for item in only:
83                if fnmatch.fnmatch(pn, item):
84                    break
85            else:
86                continue
87
88        skipit = False
89        for item in skip:
90            if fnmatch.fnmatch(pn, item):
91                skipit = True
92        if skipit:
93            continue
94
95        recipes.append(pn)
96
97    return recipes
98
99
100def stress_extract(args):
101    import bb.process
102
103    recipes = select_recipes(args)
104
105    failures = 0
106    tmpdir = tempfile.mkdtemp()
107    os.setpgrp()
108    try:
109        for pn in recipes:
110            sys.stdout.write('Testing %s ' % (pn + ' ').ljust(40, '.'))
111            sys.stdout.flush()
112            failed = False
113            skipped = None
114
115            srctree = os.path.join(tmpdir, pn)
116            try:
117                bb.process.run('devtool extract %s %s' % (pn, srctree))
118            except bb.process.ExecutionError as exc:
119                if exc.exitcode == 4:
120                    skipped = 'incompatible'
121                else:
122                    failed = True
123                    with open('stress_%s_extract.log' % pn, 'w') as f:
124                        f.write(str(exc))
125
126            if os.path.exists(srctree):
127                shutil.rmtree(srctree)
128
129            if failed:
130                print('failed')
131                failures += 1
132            elif skipped:
133                print('skipped (%s)' % skipped)
134            else:
135                print('ok')
136    except KeyboardInterrupt:
137        # We want any child processes killed. This is crude, but effective.
138        os.killpg(0, signal.SIGTERM)
139
140    if failures:
141        return 1
142    else:
143        return 0
144
145
146def stress_modify(args):
147    import bb.process
148
149    recipes = select_recipes(args)
150
151    failures = 0
152    tmpdir = tempfile.mkdtemp()
153    os.setpgrp()
154    try:
155        for pn in recipes:
156            sys.stdout.write('Testing %s ' % (pn + ' ').ljust(40, '.'))
157            sys.stdout.flush()
158            failed = False
159            reset = True
160            skipped = None
161
162            srctree = os.path.join(tmpdir, pn)
163            try:
164                bb.process.run('devtool modify -x %s %s' % (pn, srctree))
165            except bb.process.ExecutionError as exc:
166                if exc.exitcode == 4:
167                    skipped = 'incompatible'
168                else:
169                    with open('stress_%s_modify.log' % pn, 'w') as f:
170                        f.write(str(exc))
171                    failed = 'modify'
172                    reset = False
173
174            if not skipped:
175                if not failed:
176                    try:
177                        bb.process.run('bitbake -c install %s' % pn)
178                    except bb.process.CmdError as exc:
179                        with open('stress_%s_install.log' % pn, 'w') as f:
180                            f.write(str(exc))
181                        failed = 'build'
182                if reset:
183                    try:
184                        bb.process.run('devtool reset %s' % pn)
185                    except bb.process.CmdError as exc:
186                        print('devtool reset failed: %s' % str(exc))
187                        break
188
189            if os.path.exists(srctree):
190                shutil.rmtree(srctree)
191
192            if failed:
193                print('failed (%s)' % failed)
194                failures += 1
195            elif skipped:
196                print('skipped (%s)' % skipped)
197            else:
198                print('ok')
199    except KeyboardInterrupt:
200        # We want any child processes killed. This is crude, but effective.
201        os.killpg(0, signal.SIGTERM)
202
203    if failures:
204        return 1
205    else:
206        return 0
207
208
209def main():
210    parser = argparse_oe.ArgumentParser(description="devtool stress tester",
211                                        epilog="Use %(prog)s <subcommand> --help to get help on a specific command")
212    parser.add_argument('-d', '--debug', help='Enable debug output', action='store_true')
213    parser.add_argument('-r', '--resume-from', help='Resume from specified recipe', metavar='PN')
214    parser.add_argument('-o', '--only', help='Only test specified recipes (comma-separated without spaces, wildcards allowed)', metavar='PNLIST')
215    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')
216    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')
217    subparsers = parser.add_subparsers(title='subcommands', metavar='<subcommand>')
218    subparsers.required = True
219
220    parser_modify = subparsers.add_parser('modify',
221                                          help='Run "devtool modify" followed by a build with bitbake on matching recipes',
222                                          description='Runs "devtool modify" followed by a build with bitbake on matching recipes')
223    parser_modify.set_defaults(func=stress_modify)
224
225    parser_extract = subparsers.add_parser('extract',
226                                           help='Run "devtool extract" on matching recipes',
227                                           description='Runs "devtool extract" on matching recipes')
228    parser_extract.set_defaults(func=stress_extract)
229
230    args = parser.parse_args()
231
232    if args.debug:
233        logger.setLevel(logging.DEBUG)
234
235    import scriptpath
236    bitbakepath = scriptpath.add_bitbake_lib_path()
237    if not bitbakepath:
238        logger.error("Unable to find bitbake by searching parent directory of this script or PATH")
239        return 1
240    logger.debug('Found bitbake path: %s' % bitbakepath)
241
242    ret = args.func(args)
243
244if __name__ == "__main__":
245    main()
246