1 # Copyright (c) 2020 ARM Limited
4 # The license below extends only to copyright in the software and shall
5 # not be construed as granting a license to any other intellectual
6 # property including but not limited to intellectual property relating
7 # to a hardware implementation of the functionality of the software
8 # licensed hereunder. You may use the software subject to the license
9 # terms below provided that you ensure that this notice is replicated
10 # unmodified and in its entirety in all distributions of the software,
11 # modified or unmodified, in source code or in binary form.
13 # Copyright (c) 2017 Mark D. Hill and David A. Wood
14 # All rights reserved.
16 # Redistribution and use in source and binary forms, with or without
17 # modification, are permitted provided that the following conditions are
18 # met: redistributions of source code must retain the above copyright
19 # notice, this list of conditions and the following disclaimer;
20 # redistributions in binary form must reproduce the above copyright
21 # notice, this list of conditions and the following disclaimer in the
22 # documentation and/or other materials provided with the distribution;
23 # neither the name of the copyright holders nor the names of its
24 # contributors may be used to endorse or promote products derived from
25 # this software without specific prior written permission.
27 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39 # Authors: Sean Wilson
42 Global configuration module which exposes two types of configuration
46 2. constants (Also attached to the config variable as an attribute)
48 The main motivation for this module is to have a centralized location for
49 defaults and configuration by command line and files for the test framework.
51 A secondary goal is to reduce programming errors by providing common constant
52 strings and values as python attributes to simplify detection of typos.
53 A simple typo in a string can take a lot of debugging to uncover the issue,
54 attribute errors are easier to notice and most autocompletion systems detect
57 The config variable is initialzed by calling :func:`initialize_config`.
58 Before this point only ``constants`` will be availaible. This is to ensure
59 that library function writers never accidentally get stale config attributes.
61 Program arguments/flag arguments are available from the config as attributes.
62 If an attribute was not set by the command line or the optional config file,
63 then it will fallback to the `_defaults` value, if still the value is not
64 found an AttributeError will be raised.
66 :func define_defaults:
67 Provided by the config if the attribute is not found in the config or
68 commandline. For instance, if we are using the list command fixtures might
69 not be able to count on the build_dir being provided since we aren't going
73 Values not directly exposed by the config, but are attached to the object
74 for centralized access. I.E. you can reach them with
75 :code:`config.constants.attribute`. These should be used for setting
76 common string names used across the test framework.
77 :code:`_defaults.build_dir = None` Once this module has been imported
78 constants should not be modified and their base attributes are frozen.
86 from six
import add_metaclass
87 from pickle
import HIGHEST_PROTOCOL
as highest_pickle_protocol
89 from testlib
.helper
import absdirpath
, AttrDict
, FrozenAttrDict
91 class UninitialzedAttributeException(Exception):
93 Signals that an attribute in the config file was not initialized.
97 class UninitializedConfigException(Exception):
99 Signals that the config was not initialized before trying to access an
104 class TagRegex(object):
105 def __init__(self
, include
, regex
):
106 self
.include
= include
107 self
.regex
= re
.compile(regex
)
110 type_
= 'Include' if self
.include
else 'Remove'
111 return '%10s: %s' % (type_
, self
.regex
.pattern
)
113 class _Config(object):
118 constants
= AttrDict()
119 _defaults
= AttrDict()
123 _post_processors
= {}
126 # This object will act as if it were a singleton.
127 self
.__dict
__ = self
.__shared
_dict
129 def _init(self
, parser
):
130 self
._parse
_commandline
_args
(parser
)
131 self
._run
_post
_processors
()
132 self
._initialized
= True
134 def _add_post_processor(self
, attr
, post_processor
):
136 :param attr: Attribute to pass to and recieve from the
137 :func:`post_processor`.
139 :param post_processor: A callback functions called in a chain to
140 perform additional setup for a config argument. Should return a
141 tuple containing the new value for the config attr.
143 if attr
not in self
._post
_processors
:
144 self
._post
_processors
[attr
] = []
145 self
._post
_processors
[attr
].append(post_processor
)
147 def _set(self
, name
, value
):
148 self
._config
[name
] = value
150 def _parse_commandline_args(self
, parser
):
151 args
= parser
.parse_args()
153 self
._config
_file
_args
= {}
155 for attr
in dir(args
):
156 # Ignore non-argument attributes.
157 if not attr
.startswith('_'):
158 self
._config
_file
_args
[attr
] = getattr(args
, attr
)
159 self
._config
.update(self
._config
_file
_args
)
161 def _run_post_processors(self
):
162 for attr
, callbacks
in self
._post
_processors
.items():
163 newval
= self
._lookup
_val
(attr
)
164 for callback
in callbacks
:
165 newval
= callback(newval
)
166 if newval
is not None:
168 self
._set
(attr
, newval
)
171 def _lookup_val(self
, attr
):
173 Get the attribute from the config or fallback to defaults.
175 :returns: If the value is not stored return None. Otherwise a tuple
176 containing the value.
178 if attr
in self
._config
:
179 return (self
._config
[attr
],)
180 elif hasattr(self
._defaults
, attr
):
181 return (getattr(self
._defaults
, attr
),)
183 def __getattr__(self
, attr
):
184 if attr
in dir(super(_Config
, self
)):
185 return getattr(super(_Config
, self
), attr
)
186 elif not self
._initialized
:
187 raise UninitializedConfigException(
188 'Cannot directly access elements from the config before it is'
191 val
= self
._lookup
_val
(attr
)
195 raise UninitialzedAttributeException(
196 '%s was not initialzed in the config.' % attr
)
199 d
= {typ
: set(self
.__getattr
__(typ
))
200 for typ
in self
.constants
.supported_tags
}
201 if any(map(lambda vals
: bool(vals
), d
.values())):
206 def define_defaults(defaults
):
208 Defaults are provided by the config if the attribute is not found in the
209 config or commandline. For instance, if we are using the list command
210 fixtures might not be able to count on the build_dir being provided since
211 we aren't going to build anything.
213 defaults
.base_dir
= os
.path
.abspath(os
.path
.join(absdirpath(__file__
),
216 defaults
.result_path
= os
.path
.join(os
.getcwd(), '.testing-results')
217 defaults
.resource_url
= 'http://dist.gem5.org/dist/develop'
218 defaults
.resource_path
= os
.path
.abspath(os
.path
.join(defaults
.base_dir
,
223 def define_constants(constants
):
225 'constants' are values not directly exposed by the config, but are attached
226 to the object for centralized access. These should be used for setting
227 common string names used across the test framework. A simple typo in
228 a string can take a lot of debugging to uncover the issue, attribute errors
229 are easier to notice and most autocompletion systems detect them.
231 constants
.system_out_name
= 'system-out'
232 constants
.system_err_name
= 'system-err'
234 constants
.isa_tag_type
= 'isa'
235 constants
.x86_tag
= 'X86'
236 constants
.sparc_tag
= 'SPARC'
237 constants
.riscv_tag
= 'RISCV'
238 constants
.arm_tag
= 'ARM'
239 constants
.mips_tag
= 'MIPS'
240 constants
.power_tag
= 'POWER'
241 constants
.null_tag
= 'NULL'
243 constants
.variant_tag_type
= 'variant'
244 constants
.opt_tag
= 'opt'
245 constants
.debug_tag
= 'debug'
246 constants
.fast_tag
= 'fast'
248 constants
.length_tag_type
= 'length'
249 constants
.quick_tag
= 'quick'
250 constants
.long_tag
= 'long'
252 constants
.host_isa_tag_type
= 'host'
253 constants
.host_x86_64_tag
= 'x86_64'
254 constants
.host_arm_tag
= 'aarch64'
256 constants
.supported_tags
= {
257 constants
.isa_tag_type
: (
266 constants
.variant_tag_type
: (
271 constants
.length_tag_type
: (
275 constants
.host_isa_tag_type
: (
276 constants
.host_x86_64_tag
,
277 constants
.host_arm_tag
,
281 # Binding target ISA with host ISA. This is useful for the
282 # case where host ISA and target ISA need to coincide
283 constants
.target_host
= {
284 constants
.arm_tag
: (constants
.host_arm_tag
,),
285 constants
.x86_tag
: (constants
.host_x86_64_tag
,),
286 constants
.sparc_tag
: (constants
.host_x86_64_tag
,),
287 constants
.riscv_tag
: (constants
.host_x86_64_tag
,),
288 constants
.mips_tag
: (constants
.host_x86_64_tag
,),
289 constants
.power_tag
: (constants
.host_x86_64_tag
,),
290 constants
.null_tag
: (None,)
293 constants
.supported_isas
= constants
.supported_tags
['isa']
294 constants
.supported_variants
= constants
.supported_tags
['variant']
295 constants
.supported_lengths
= constants
.supported_tags
['length']
296 constants
.supported_hosts
= constants
.supported_tags
['host']
298 constants
.tempdir_fixture_name
= 'tempdir'
299 constants
.gem5_simulation_stderr
= 'simerr'
300 constants
.gem5_simulation_stdout
= 'simout'
301 constants
.gem5_simulation_stats
= 'stats.txt'
302 constants
.gem5_simulation_config_ini
= 'config.ini'
303 constants
.gem5_simulation_config_json
= 'config.json'
304 constants
.gem5_returncode_fixture_name
= 'gem5-returncode'
305 constants
.gem5_binary_fixture_name
= 'gem5'
306 constants
.xml_filename
= 'results.xml'
307 constants
.pickle_filename
= 'results.pickle'
308 constants
.pickle_protocol
= highest_pickle_protocol
310 # The root directory which all test names will be based off of.
311 constants
.testing_base
= absdirpath(os
.path
.join(absdirpath(__file__
),
314 def define_post_processors(config
):
316 post_processors are used to do final configuration of variables. This is
317 useful if there is a dynamically set default, or some function that needs
318 to be applied after parsing in order to set a configration value.
320 Post processors must accept a single argument that will either be a tuple
321 containing the already set config value or ``None`` if the config value
322 has not been set to anything. They must return the modified value in the
326 def set_default_build_dir(build_dir
):
328 Post-processor to set the default build_dir based on the base_dir.
330 .. seealso :func:`~_Config._add_post_processor`
332 if not build_dir
or build_dir
[0] is None:
333 base_dir
= config
._lookup
_val
('base_dir')[0]
334 build_dir
= (os
.path
.join(base_dir
, 'build'),)
337 def fix_verbosity_hack(verbose
):
338 return (verbose
[0].val
,)
340 def threads_as_int(threads
):
341 if threads
is not None:
342 return (int(threads
[0]),)
344 def test_threads_as_int(test_threads
):
345 if test_threads
is not None:
346 return (int(test_threads
[0]),)
348 def default_isa(isa
):
350 return [constants
.supported_tags
[constants
.isa_tag_type
]]
354 def default_variant(variant
):
356 # Default variant is only opt. No need to run tests with multiple
357 # different compilation targets
358 return [[constants
.opt_tag
]]
362 def default_length(length
):
364 return [[constants
.quick_tag
]]
368 def default_host(host
):
372 host_machine
= platform
.machine()
373 if host_machine
not in constants
.supported_hosts
:
374 raise ValueError("Invalid host machine")
375 return [[host_machine
]]
377 return [[constants
.host_x86_64_tag
]]
381 def compile_tag_regex(positional_tags
):
382 if not positional_tags
:
383 return positional_tags
385 new_positional_tags_list
= []
386 positional_tags
= positional_tags
[0]
388 for flag
, regex
in positional_tags
:
389 if flag
== 'exclude_tags':
390 tag_regex
= TagRegex(False, regex
)
391 elif flag
== 'include_tags':
392 tag_regex
= TagRegex(True, regex
)
394 raise ValueError('Unsupported flag.')
395 new_positional_tags_list
.append(tag_regex
)
397 return (new_positional_tags_list
,)
399 config
._add
_post
_processor
('build_dir', set_default_build_dir
)
400 config
._add
_post
_processor
('verbose', fix_verbosity_hack
)
401 config
._add
_post
_processor
('isa', default_isa
)
402 config
._add
_post
_processor
('variant', default_variant
)
403 config
._add
_post
_processor
('length', default_length
)
404 config
._add
_post
_processor
('host', default_host
)
405 config
._add
_post
_processor
('threads', threads_as_int
)
406 config
._add
_post
_processor
('test_threads', test_threads_as_int
)
407 config
._add
_post
_processor
(StorePositionalTagsAction
.position_kword
,
409 class Argument(object):
411 Class represents a cli argument/flag for a argparse parser.
413 :attr name: The long name of this object that will be stored in the arg
414 output by the final parser.
416 def __init__(self
, *flags
, **kwargs
):
421 raise ValueError("Need at least one argument.")
422 elif 'dest' in kwargs
:
423 self
.name
= kwargs
['dest']
424 elif len(flags
) > 1 or flags
[0].startswith('-'):
426 if not flag
.startswith('-'):
427 raise ValueError("invalid option string %s: must start"
428 "with a character '-'" % flag
)
430 if flag
.startswith('--'):
431 if not hasattr(self
, 'name'):
432 self
.name
= flag
.lstrip('-')
434 if not hasattr(self
, 'name'):
435 self
.name
= flags
[0].lstrip('-')
436 self
.name
= self
.name
.replace('-', '_')
438 def add_to(self
, parser
):
439 '''Add this argument to the given parser.'''
440 parser
.add_argument(*self
.flags
, **self
.kwargs
)
443 '''Copy this argument so you might modify any of its kwargs.'''
444 return copy
.deepcopy(self
)
449 A class that is used to cheat the verbosity count incrementer by
450 pretending to be an int. This makes the int stay on the heap and eat other
451 real numbers when they are added to it.
453 We use this so we can allow the verbose flag to be provided before or after
454 the subcommand. This likely has no utility outside of this use case.
456 def __init__(self
, val
=0):
459 def __add__(self
, other
):
463 common_args
= NotImplemented
465 class StorePositionAction(argparse
.Action
):
466 '''Base class for classes wishing to create namespaces where
467 arguments are stored in the order provided via the command line.
469 position_kword
= 'positional'
471 def __call__(self
, parser
, namespace
, values
, option_string
=None):
472 if not self
.position_kword
in namespace
:
473 setattr(namespace
, self
.position_kword
, [])
474 previous
= getattr(namespace
, self
.position_kword
)
475 previous
.append((self
.dest
, values
))
476 setattr(namespace
, self
.position_kword
, previous
)
478 class StorePositionalTagsAction(StorePositionAction
):
479 position_kword
= 'tag_filters'
481 def define_common_args(config
):
483 Common args are arguments which are likely to be simular between different
484 subcommands, so they are available to all by placing their definitions
489 # A list of common arguments/flags used across cli parsers.
495 help='Directory to start searching for tests in'),
498 action
=StorePositionalTagsAction
,
499 help='A tag comparison used to select tests.'),
502 action
=StorePositionalTagsAction
,
503 help='A tag comparison used to select tests.'),
508 help="Only tests that are valid with one of these ISAs. "
514 help="Only tests that are valid with one of these binary variants"
515 "(e.g., opt, debug). Comma separated."),
520 help="Only tests that are one of these lengths. Comma separated."),
525 help="Only tests that are meant to runnable on the selected host"),
530 help='UID of a specific test item to run.'),
534 help='Build directory for SCons'),
538 default
=config
._defaults
.base_dir
,
539 help='Directory to change to in order to exec scons.'),
544 help='Number of threads to run SCons with.'),
546 '-t', '--test-threads',
549 help='Number of threads to spawn to run concurrent tests with.'),
554 default
=_StickyInt(),
555 help='Increase verbosity'),
560 help='Path to read a testing.ini config in'
566 help='Skip the building component of SCons targets.'
571 help='The path to store results in.'
576 default
=config
._defaults
.resource_path
,
577 help='Path where resources are stored (downloaded if not present)'
582 default
=config
._defaults
.resource_url
,
583 help='The URL where the resources reside.'
588 # NOTE: There is a limitation which arises due to this format. If you have
589 # multiple arguments with the same name only the final one in the list
592 # e.g. if you have a -v argument which increments verbosity level and
593 # a separate --verbose flag which 'store's verbosity level. the final
594 # one in the list will be saved.
595 common_args
= AttrDict({arg
.name
:arg
for arg
in common_args
})
597 @add_metaclass(abc
.ABCMeta
)
598 class ArgParser(object):
600 def __init__(self
, parser
):
601 # Copy public methods of the parser.
602 for attr
in dir(parser
):
603 if not attr
.startswith('_'):
604 setattr(self
, attr
, getattr(parser
, attr
))
606 self
.add_argument
= self
.parser
.add_argument
608 # Argument will be added to all parsers and subparsers.
609 common_args
.verbose
.add_to(parser
)
612 class CommandParser(ArgParser
):
614 Main parser which parses command strings and uses those to direct to
618 parser
= argparse
.ArgumentParser()
619 super(CommandParser
, self
).__init
__(parser
)
620 self
.subparser
= self
.add_subparsers(dest
='command')
623 class RunParser(ArgParser
):
625 Parser for the \'run\' command.
627 def __init__(self
, subparser
):
628 parser
= subparser
.add_parser(
630 help='''Run Tests.'''
633 super(RunParser
, self
).__init
__(parser
)
635 common_args
.uid
.add_to(parser
)
636 common_args
.skip_build
.add_to(parser
)
637 common_args
.directory
.add_to(parser
)
638 common_args
.build_dir
.add_to(parser
)
639 common_args
.base_dir
.add_to(parser
)
640 common_args
.bin_path
.add_to(parser
)
641 common_args
.threads
.add_to(parser
)
642 common_args
.test_threads
.add_to(parser
)
643 common_args
.isa
.add_to(parser
)
644 common_args
.variant
.add_to(parser
)
645 common_args
.length
.add_to(parser
)
646 common_args
.host
.add_to(parser
)
647 common_args
.include_tags
.add_to(parser
)
648 common_args
.exclude_tags
.add_to(parser
)
651 class ListParser(ArgParser
):
653 Parser for the \'list\' command.
655 def __init__(self
, subparser
):
656 parser
= subparser
.add_parser(
658 help='''List and query test metadata.'''
660 super(ListParser
, self
).__init
__(parser
)
666 help='List all test suites.'
672 help='List all test cases.'
678 help='List all fixtures.'
684 help='List all tags.'
691 help='Quiet output (machine readable).'
694 common_args
.directory
.add_to(parser
)
695 common_args
.bin_path
.add_to(parser
)
696 common_args
.isa
.add_to(parser
)
697 common_args
.variant
.add_to(parser
)
698 common_args
.length
.add_to(parser
)
699 common_args
.host
.add_to(parser
)
700 common_args
.include_tags
.add_to(parser
)
701 common_args
.exclude_tags
.add_to(parser
)
704 class RerunParser(ArgParser
):
705 def __init__(self
, subparser
):
706 parser
= subparser
.add_parser(
708 help='''Rerun failed tests.'''
710 super(RerunParser
, self
).__init
__(parser
)
712 common_args
.skip_build
.add_to(parser
)
713 common_args
.directory
.add_to(parser
)
714 common_args
.build_dir
.add_to(parser
)
715 common_args
.base_dir
.add_to(parser
)
716 common_args
.bin_path
.add_to(parser
)
717 common_args
.threads
.add_to(parser
)
718 common_args
.test_threads
.add_to(parser
)
719 common_args
.isa
.add_to(parser
)
720 common_args
.variant
.add_to(parser
)
721 common_args
.length
.add_to(parser
)
722 common_args
.host
.add_to(parser
)
725 define_constants(config
.constants
)
727 # Constants are directly exposed and available once this module is created.
728 # All constants MUST be defined before this point.
729 config
.constants
= FrozenAttrDict(config
.constants
.__dict
__)
730 constants
= config
.constants
733 This config object is the singleton config object available throughout the
736 def initialize_config():
738 Parse the commandline arguments and setup the config varibles.
742 # Setup constants and defaults
743 define_defaults(config
._defaults
)
744 define_post_processors(config
)
745 define_common_args(config
)
747 # Setup parser and subcommands
748 baseparser
= CommandParser()
749 runparser
= RunParser(baseparser
.subparser
)
750 listparser
= ListParser(baseparser
.subparser
)
751 rerunparser
= RerunParser(baseparser
.subparser
)
753 # Initialize the config by parsing args and running callbacks.
754 config
._init
(baseparser
)