xref: /aosp_15_r20/external/cronet/third_party/libc++/src/utils/libcxx/test/dsl.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1# ===----------------------------------------------------------------------===##
2#
3# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4# See https://llvm.org/LICENSE.txt for license information.
5# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6#
7# ===----------------------------------------------------------------------===##
8
9import os
10import pickle
11import pipes
12import platform
13import shutil
14import tempfile
15
16import libcxx.test.format
17import lit
18import lit.LitConfig
19import lit.Test
20import lit.TestRunner
21import lit.util
22
23
24class ConfigurationError(Exception):
25    pass
26
27
28class ConfigurationCompilationError(ConfigurationError):
29    pass
30
31
32class ConfigurationRuntimeError(ConfigurationError):
33    pass
34
35
36def _memoizeExpensiveOperation(extractCacheKey):
37    """
38    Allows memoizing a very expensive operation.
39
40    We pickle the cache key to make sure we store an immutable representation
41    of it. If we stored an object and the object was referenced elsewhere, it
42    could be changed from under our feet, which would break the cache.
43
44    We also store the cache for a given function persistently across invocations
45    of Lit. This dramatically speeds up the configuration of the test suite when
46    invoking Lit repeatedly, which is important for developer workflow. However,
47    with the current implementation that does not synchronize updates to the
48    persistent cache, this also means that one should not call a memoized
49    operation from multiple threads. This should normally not be a problem
50    since Lit configuration is single-threaded.
51    """
52
53    def decorator(function):
54        def f(config, *args, **kwargs):
55            cacheRoot = os.path.join(config.test_exec_root, "__config_cache__")
56            persistentCache = os.path.join(cacheRoot, function.__name__)
57            if not os.path.exists(cacheRoot):
58                os.makedirs(cacheRoot)
59
60            cache = {}
61            # Load a cache from a previous Lit invocation if there is one.
62            if os.path.exists(persistentCache):
63                with open(persistentCache, "rb") as cacheFile:
64                    cache = pickle.load(cacheFile)
65
66            cacheKey = pickle.dumps(extractCacheKey(config, *args, **kwargs))
67            if cacheKey not in cache:
68                cache[cacheKey] = function(config, *args, **kwargs)
69                # Update the persistent cache so it knows about the new key
70                # We write to a PID-suffixed file and rename the result to
71                # ensure that the cache is not corrupted when running the test
72                # suite with multiple shards. Since this file is in the same
73                # directory as the destination, os.replace() will be atomic.
74                unique_suffix = ".tmp." + str(os.getpid())
75                with open(persistentCache + unique_suffix, "wb") as cacheFile:
76                    pickle.dump(cache, cacheFile)
77                os.replace(persistentCache + unique_suffix, persistentCache)
78            return cache[cacheKey]
79
80        return f
81
82    return decorator
83
84
85def _executeWithFakeConfig(test, commands):
86    """
87    Returns (stdout, stderr, exitCode, timeoutInfo, parsedCommands)
88    """
89    litConfig = lit.LitConfig.LitConfig(
90        progname="lit",
91        path=[],
92        quiet=False,
93        useValgrind=False,
94        valgrindLeakCheck=False,
95        valgrindArgs=[],
96        noExecute=False,
97        debug=False,
98        isWindows=platform.system() == "Windows",
99        order="smart",
100        params={},
101    )
102    return libcxx.test.format._executeScriptInternal(test, litConfig, commands)
103
104
105def _makeConfigTest(config):
106    # Make sure the support directories exist, which is needed to create
107    # the temporary file %t below.
108    sourceRoot = os.path.join(config.test_exec_root, "__config_src__")
109    execRoot = os.path.join(config.test_exec_root, "__config_exec__")
110    for supportDir in (sourceRoot, execRoot):
111        if not os.path.exists(supportDir):
112            os.makedirs(supportDir)
113
114    # Create a dummy test suite and single dummy test inside it. As part of
115    # the Lit configuration, automatically do the equivalent of 'mkdir %T'
116    # and 'rm -r %T' to avoid cluttering the build directory.
117    suite = lit.Test.TestSuite("__config__", sourceRoot, execRoot, config)
118    tmp = tempfile.NamedTemporaryFile(dir=sourceRoot, delete=False, suffix=".cpp")
119    tmp.close()
120    pathInSuite = [os.path.relpath(tmp.name, sourceRoot)]
121
122    class TestWrapper(lit.Test.Test):
123        def __enter__(self):
124            testDir, _ = libcxx.test.format._getTempPaths(self)
125            os.makedirs(testDir)
126            return self
127
128        def __exit__(self, *args):
129            testDir, _ = libcxx.test.format._getTempPaths(self)
130            shutil.rmtree(testDir)
131            os.remove(tmp.name)
132
133    return TestWrapper(suite, pathInSuite, config)
134
135
136@_memoizeExpensiveOperation(lambda c, s, f=[]: (c.substitutions, c.environment, s, f))
137def sourceBuilds(config, source, additionalFlags=[]):
138    """
139    Return whether the program in the given string builds successfully.
140
141    This is done by compiling and linking a program that consists of the given
142    source with the %{cxx} substitution, and seeing whether that succeeds. If
143    any additional flags are passed, they are appended to the compiler invocation.
144    """
145    with _makeConfigTest(config) as test:
146        with open(test.getSourcePath(), "w") as sourceFile:
147            sourceFile.write(source)
148        _, _, exitCode, _, _ = _executeWithFakeConfig(
149            test, ["%{{build}} {}".format(" ".join(additionalFlags))]
150        )
151        return exitCode == 0
152
153
154@_memoizeExpensiveOperation(
155    lambda c, p, args=None: (c.substitutions, c.environment, p, args)
156)
157def programOutput(config, program, args=None):
158    """
159    Compiles a program for the test target, run it on the test target and return
160    the output.
161
162    Note that execution of the program is done through the %{exec} substitution,
163    which means that the program may be run on a remote host depending on what
164    %{exec} does.
165    """
166    if args is None:
167        args = []
168    with _makeConfigTest(config) as test:
169        with open(test.getSourcePath(), "w") as source:
170            source.write(program)
171        _, err, exitCode, _, buildcmd = _executeWithFakeConfig(test, ["%{build}"])
172        if exitCode != 0:
173            raise ConfigurationCompilationError(
174                "Failed to build program, cmd:\n{}\nstderr is:\n{}".format(
175                    buildcmd, err
176                )
177            )
178
179        out, err, exitCode, _, runcmd = _executeWithFakeConfig(
180            test, ["%{{run}} {}".format(" ".join(args))]
181        )
182        if exitCode != 0:
183            raise ConfigurationRuntimeError(
184                "Failed to run program, cmd:\n{}\nstderr is:\n{}".format(runcmd, err)
185            )
186
187        return out
188
189
190@_memoizeExpensiveOperation(
191    lambda c, p, args=None: (c.substitutions, c.environment, p, args)
192)
193def programSucceeds(config, program, args=None):
194    """
195    Compiles a program for the test target, run it on the test target and return
196    whether it completed successfully.
197
198    Note that execution of the program is done through the %{exec} substitution,
199    which means that the program may be run on a remote host depending on what
200    %{exec} does.
201    """
202    try:
203        programOutput(config, program, args)
204    except ConfigurationRuntimeError:
205        return False
206    return True
207
208
209@_memoizeExpensiveOperation(lambda c, f: (c.substitutions, c.environment, f))
210def tryCompileFlag(config, flag):
211    """
212    Try using the given compiler flag and return the exit code along with stdout and stderr.
213    """
214    # fmt: off
215    with _makeConfigTest(config) as test:
216        out, err, exitCode, timeoutInfo, _ = _executeWithFakeConfig(test, [
217            "%{{cxx}} -xc++ {} -Werror -fsyntax-only %{{flags}} %{{compile_flags}} {}".format(os.devnull, flag)
218        ])
219        return exitCode, out, err
220    # fmt: on
221
222
223def hasCompileFlag(config, flag):
224    """
225    Return whether the compiler in the configuration supports a given compiler flag.
226
227    This is done by executing the %{cxx} substitution with the given flag and
228    checking whether that succeeds.
229    """
230    (exitCode, _, _) = tryCompileFlag(config, flag)
231    return exitCode == 0
232
233
234@_memoizeExpensiveOperation(lambda c, s: (c.substitutions, c.environment, s))
235def runScriptExitCode(config, script):
236    """
237    Runs the given script as a Lit test, and returns the exit code of the execution.
238
239    The script must be a list of commands, each of which being something that
240    could appear on the right-hand-side of a `RUN:` keyword.
241    """
242    with _makeConfigTest(config) as test:
243        _, _, exitCode, _, _ = _executeWithFakeConfig(test, script)
244        return exitCode
245
246
247@_memoizeExpensiveOperation(lambda c, s: (c.substitutions, c.environment, s))
248def commandOutput(config, command):
249    """
250    Runs the given script as a Lit test, and returns the output.
251    If the exit code isn't 0 an exception is raised.
252
253    The script must be a list of commands, each of which being something that
254    could appear on the right-hand-side of a `RUN:` keyword.
255    """
256    with _makeConfigTest(config) as test:
257        out, err, exitCode, _, cmd = _executeWithFakeConfig(test, command)
258        if exitCode != 0:
259            raise ConfigurationRuntimeError(
260                "Failed to run command: {}\nstderr is:\n{}".format(cmd, err)
261            )
262        return out
263
264
265@_memoizeExpensiveOperation(lambda c, l: (c.substitutions, c.environment, l))
266def hasAnyLocale(config, locales):
267    """
268    Return whether the runtime execution environment supports a given locale.
269    Different systems may use different names for a locale, so this function checks
270    whether any of the passed locale names is supported by setlocale() and returns
271    true if one of them works.
272
273    This is done by executing a program that tries to set the given locale using
274    %{exec} -- this means that the command may be executed on a remote host
275    depending on the %{exec} substitution.
276    """
277    program = """
278    #include <stddef.h>
279    #if defined(_LIBCPP_HAS_NO_LOCALIZATION)
280      int main(int, char**) { return 1; }
281    #else
282      #include <locale.h>
283      int main(int argc, char** argv) {
284        for (int i = 1; i < argc; i++) {
285          if (::setlocale(LC_ALL, argv[i]) != NULL) {
286            return 0;
287          }
288        }
289        return 1;
290      }
291    #endif
292  """
293    return programSucceeds(config, program, args=[pipes.quote(l) for l in locales])
294
295
296@_memoizeExpensiveOperation(lambda c, flags="": (c.substitutions, c.environment, flags))
297def compilerMacros(config, flags=""):
298    """
299    Return a dictionary of predefined compiler macros.
300
301    The keys are strings representing macros, and the values are strings
302    representing what each macro is defined to.
303
304    If the optional `flags` argument (a string) is provided, these flags will
305    be added to the compiler invocation when generating the macros.
306    """
307    with _makeConfigTest(config) as test:
308        with open(test.getSourcePath(), "w") as sourceFile:
309            sourceFile.write(
310                """
311      #if __has_include(<__config_site>)
312      #  include <__config_site>
313      #endif
314      """
315            )
316        unparsedOutput, err, exitCode, _, cmd = _executeWithFakeConfig(
317            test, ["%{{cxx}} %s -dM -E %{{flags}} %{{compile_flags}} {}".format(flags)]
318        )
319        if exitCode != 0:
320            raise ConfigurationCompilationError(
321                "Failed to retrieve compiler macros, compiler invocation is:\n{}\nstderr is:\n{}".format(
322                    cmd, err
323                )
324            )
325        parsedMacros = dict()
326        defines = (
327            l.strip() for l in unparsedOutput.split("\n") if l.startswith("#define ")
328        )
329        for line in defines:
330            line = line[len("#define ") :]
331            macro, _, value = line.partition(" ")
332            parsedMacros[macro] = value
333        return parsedMacros
334
335
336def featureTestMacros(config, flags=""):
337    """
338    Return a dictionary of feature test macros.
339
340    The keys are strings representing feature test macros, and the values are
341    integers representing the value of the macro.
342    """
343    allMacros = compilerMacros(config, flags)
344    return {
345        m: int(v.rstrip("LlUu"))
346        for (m, v) in allMacros.items()
347        if m.startswith("__cpp_")
348    }
349
350
351def _getSubstitution(substitution, config):
352  for (orig, replacement) in config.substitutions:
353    if orig == substitution:
354      return replacement
355  raise ValueError('Substitution {} is not in the config.'.format(substitution))
356
357def _appendToSubstitution(substitutions, key, value):
358    return [(k, v + " " + value) if k == key else (k, v) for (k, v) in substitutions]
359
360def _prependToSubstitution(substitutions, key, value):
361    return [(k, value + " " + v) if k == key else (k, v) for (k, v) in substitutions]
362
363def _ensureFlagIsSupported(config, flag):
364    (exitCode, out, err) = tryCompileFlag(config, flag)
365    assert (
366        exitCode == 0
367    ), f"Trying to enable compiler flag {flag}, which is not supported. stdout was:\n{out}\n\nstderr was:\n{err}"
368
369
370class ConfigAction(object):
371    """
372    This class represents an action that can be performed on a Lit TestingConfig
373    object.
374
375    Examples of such actions are adding or modifying substitutions, Lit features,
376    etc. This class only provides the interface of such actions, and it is meant
377    to be subclassed appropriately to create new actions.
378    """
379
380    def applyTo(self, config):
381        """
382        Applies the action to the given configuration.
383
384        This should modify the configuration object in place, and return nothing.
385
386        If applying the action to the configuration would yield an invalid
387        configuration, and it is possible to diagnose it here, this method
388        should produce an error. For example, it should be an error to modify
389        a substitution in a way that we know for sure is invalid (e.g. adding
390        a compiler flag when we know the compiler doesn't support it). Failure
391        to do so early may lead to difficult-to-diagnose issues down the road.
392        """
393        pass
394
395    def pretty(self, config, litParams):
396        """
397        Returns a short and human-readable string describing what this action does.
398
399        This is used for logging purposes when running the test suite, so it should
400        be kept concise.
401        """
402        pass
403
404
405class AddFeature(ConfigAction):
406    """
407    This action defines the given Lit feature when running the test suite.
408
409    The name of the feature can be a string or a callable, in which case it is
410    called with the configuration to produce the feature name (as a string).
411    """
412
413    def __init__(self, name):
414        self._name = name
415
416    def _getName(self, config):
417        name = self._name(config) if callable(self._name) else self._name
418        if not isinstance(name, str):
419            raise ValueError(
420                "Lit feature did not resolve to a string (got {})".format(name)
421            )
422        return name
423
424    def applyTo(self, config):
425        config.available_features.add(self._getName(config))
426
427    def pretty(self, config, litParams):
428        return "add Lit feature {}".format(self._getName(config))
429
430
431class AddFlag(ConfigAction):
432    """
433    This action adds the given flag to the %{flags} substitution.
434
435    The flag can be a string or a callable, in which case it is called with the
436    configuration to produce the actual flag (as a string).
437    """
438
439    def __init__(self, flag):
440        self._getFlag = lambda config: flag(config) if callable(flag) else flag
441
442    def applyTo(self, config):
443        flag = self._getFlag(config)
444        _ensureFlagIsSupported(config, flag)
445        config.substitutions = _appendToSubstitution(
446            config.substitutions, "%{flags}", flag
447        )
448
449    def pretty(self, config, litParams):
450        return "add {} to %{{flags}}".format(self._getFlag(config))
451
452class AddFlagIfSupported(ConfigAction):
453    """
454    This action adds the given flag to the %{flags} substitution, only if
455    the compiler supports the flag.
456
457    The flag can be a string or a callable, in which case it is called with the
458    configuration to produce the actual flag (as a string).
459    """
460
461    def __init__(self, flag):
462        self._getFlag = lambda config: flag(config) if callable(flag) else flag
463
464    def applyTo(self, config):
465        flag = self._getFlag(config)
466        if hasCompileFlag(config, flag):
467            config.substitutions = _appendToSubstitution(
468                config.substitutions, "%{flags}", flag
469            )
470
471    def pretty(self, config, litParams):
472        return "add {} to %{{flags}}".format(self._getFlag(config))
473
474
475class AddCompileFlag(ConfigAction):
476    """
477    This action adds the given flag to the %{compile_flags} substitution.
478
479    The flag can be a string or a callable, in which case it is called with the
480    configuration to produce the actual flag (as a string).
481    """
482
483    def __init__(self, flag):
484        self._getFlag = lambda config: flag(config) if callable(flag) else flag
485
486    def applyTo(self, config):
487        flag = self._getFlag(config)
488        _ensureFlagIsSupported(config, flag)
489        config.substitutions = _appendToSubstitution(
490            config.substitutions, "%{compile_flags}", flag
491        )
492
493    def pretty(self, config, litParams):
494        return "add {} to %{{compile_flags}}".format(self._getFlag(config))
495
496
497class AddLinkFlag(ConfigAction):
498    """
499    This action appends the given flag to the %{link_flags} substitution.
500
501    The flag can be a string or a callable, in which case it is called with the
502    configuration to produce the actual flag (as a string).
503    """
504
505    def __init__(self, flag):
506        self._getFlag = lambda config: flag(config) if callable(flag) else flag
507
508    def applyTo(self, config):
509        flag = self._getFlag(config)
510        _ensureFlagIsSupported(config, flag)
511        config.substitutions = _appendToSubstitution(
512            config.substitutions, "%{link_flags}", flag
513        )
514
515    def pretty(self, config, litParams):
516        return "append {} to %{{link_flags}}".format(self._getFlag(config))
517
518
519class PrependLinkFlag(ConfigAction):
520    """
521    This action prepends the given flag to the %{link_flags} substitution.
522
523    The flag can be a string or a callable, in which case it is called with the
524    configuration to produce the actual flag (as a string).
525    """
526
527    def __init__(self, flag):
528        self._getFlag = lambda config: flag(config) if callable(flag) else flag
529
530    def applyTo(self, config):
531        flag = self._getFlag(config)
532        _ensureFlagIsSupported(config, flag)
533        config.substitutions = _prependToSubstitution(
534            config.substitutions, "%{link_flags}", flag
535        )
536
537    def pretty(self, config, litParams):
538        return "prepend {} to %{{link_flags}}".format(self._getFlag(config))
539
540
541class AddOptionalWarningFlag(ConfigAction):
542    """
543    This action adds the given warning flag to the %{compile_flags} substitution,
544    if it is supported by the compiler.
545
546    The flag can be a string or a callable, in which case it is called with the
547    configuration to produce the actual flag (as a string).
548    """
549
550    def __init__(self, flag):
551        self._getFlag = lambda config: flag(config) if callable(flag) else flag
552
553    def applyTo(self, config):
554        flag = self._getFlag(config)
555        # Use -Werror to make sure we see an error about the flag being unsupported.
556        if hasCompileFlag(config, "-Werror " + flag):
557            config.substitutions = _appendToSubstitution(
558                config.substitutions, "%{compile_flags}", flag
559            )
560
561    def pretty(self, config, litParams):
562        return "add {} to %{{compile_flags}}".format(self._getFlag(config))
563
564
565class AddSubstitution(ConfigAction):
566    """
567    This action adds the given substitution to the Lit configuration.
568
569    The substitution can be a string or a callable, in which case it is called
570    with the configuration to produce the actual substitution (as a string).
571    """
572
573    def __init__(self, key, substitution):
574        self._key = key
575        self._getSub = (
576            lambda config: substitution(config)
577            if callable(substitution)
578            else substitution
579        )
580
581    def applyTo(self, config):
582        key = self._key
583        sub = self._getSub(config)
584        config.substitutions.append((key, sub))
585
586    def pretty(self, config, litParams):
587        return "add substitution {} = {}".format(self._key, self._getSub(config))
588
589
590class Feature(object):
591    """
592    Represents a Lit available feature that is enabled whenever it is supported.
593
594    A feature like this informs the test suite about a capability of the compiler,
595    platform, etc. Unlike Parameters, it does not make sense to explicitly
596    control whether a Feature is enabled -- it should be enabled whenever it
597    is supported.
598    """
599
600    def __init__(self, name, actions=None, when=lambda _: True):
601        """
602        Create a Lit feature for consumption by a test suite.
603
604        - name
605            The name of the feature. This is what will end up in Lit's available
606            features if the feature is enabled. This can be either a string or a
607            callable, in which case it is passed the TestingConfig and should
608            generate a string representing the name of the feature.
609
610        - actions
611            An optional list of ConfigActions to apply when the feature is supported.
612            An AddFeature action is always created regardless of any actions supplied
613            here -- these actions are meant to perform more than setting a corresponding
614            Lit feature (e.g. adding compiler flags). If 'actions' is a callable, it
615            is called with the current configuration object to generate the actual
616            list of actions.
617
618        - when
619            A callable that gets passed a TestingConfig and should return a
620            boolean representing whether the feature is supported in that
621            configuration. For example, this can use `hasCompileFlag` to
622            check whether the compiler supports the flag that the feature
623            represents. If omitted, the feature will always be considered
624            supported.
625        """
626        self._name = name
627        self._actions = [] if actions is None else actions
628        self._isSupported = when
629
630    def _getName(self, config):
631        name = self._name(config) if callable(self._name) else self._name
632        if not isinstance(name, str):
633            raise ValueError(
634                "Feature did not resolve to a name that's a string, got {}".format(name)
635            )
636        return name
637
638    def getActions(self, config):
639        """
640        Return the list of actions associated to this feature.
641
642        If the feature is not supported, an empty list is returned.
643        If the feature is supported, an `AddFeature` action is automatically added
644        to the returned list of actions, in addition to any actions provided on
645        construction.
646        """
647        if not self._isSupported(config):
648            return []
649        else:
650            actions = (
651                self._actions(config) if callable(self._actions) else self._actions
652            )
653            return [AddFeature(self._getName(config))] + actions
654
655    def pretty(self, config):
656        """
657        Returns the Feature's name.
658        """
659        return self._getName(config)
660
661
662def _str_to_bool(s):
663    """
664    Convert a string value to a boolean.
665
666    True values are "y", "yes", "t", "true", "on" and "1", regardless of capitalization.
667    False values are "n", "no", "f", "false", "off" and "0", regardless of capitalization.
668    """
669    trueVals = ["y", "yes", "t", "true", "on", "1"]
670    falseVals = ["n", "no", "f", "false", "off", "0"]
671    lower = s.lower()
672    if lower in trueVals:
673        return True
674    elif lower in falseVals:
675        return False
676    else:
677        raise ValueError("Got string '{}', which isn't a valid boolean".format(s))
678
679
680def _parse_parameter(s, type):
681    if type is bool and isinstance(s, str):
682        return _str_to_bool(s)
683    elif type is list and isinstance(s, str):
684        return [x.strip() for x in s.split(",") if x.strip()]
685    return type(s)
686
687
688class Parameter(object):
689    """
690    Represents a parameter of a Lit test suite.
691
692    Parameters are used to customize the behavior of test suites in a user
693    controllable way. There are two ways of setting the value of a Parameter.
694    The first one is to pass `--param <KEY>=<VALUE>` when running Lit (or
695    equivalently to set `litConfig.params[KEY] = VALUE` somewhere in the
696    Lit configuration files. This method will set the parameter globally for
697    all test suites being run.
698
699    The second method is to set `config.KEY = VALUE` somewhere in the Lit
700    configuration files, which sets the parameter only for the test suite(s)
701    that use that `config` object.
702
703    Parameters can have multiple possible values, and they can have a default
704    value when left unspecified. They can also have any number of ConfigActions
705    associated to them, in which case the actions will be performed on the
706    TestingConfig if the parameter is enabled. Depending on the actions
707    associated to a Parameter, it may be an error to enable the Parameter
708    if some actions are not supported in the given configuration. For example,
709    trying to set the compilation standard to C++23 when `-std=c++23` is not
710    supported by the compiler would be an error.
711    """
712
713    def __init__(self, name, type, help, actions, choices=None, default=None):
714        """
715        Create a Lit parameter to customize the behavior of a test suite.
716
717        - name
718            The name of the parameter that can be used to set it on the command-line.
719            On the command-line, the parameter can be set using `--param <name>=<value>`
720            when running Lit. This must be non-empty.
721
722        - choices
723            An optional non-empty set of possible values for this parameter. If provided,
724            this must be anything that can be iterated. It is an error if the parameter
725            is given a value that is not in that set, whether explicitly or through a
726            default value.
727
728        - type
729            A callable that can be used to parse the value of the parameter given
730            on the command-line. As a special case, using the type `bool` also
731            allows parsing strings with boolean-like contents, and the type `list`
732            will parse a string delimited by commas into a list of the substrings.
733
734        - help
735            A string explaining the parameter, for documentation purposes.
736            TODO: We should be able to surface those from the Lit command-line.
737
738        - actions
739            A callable that gets passed the parsed value of the parameter (either
740            the one passed on the command-line or the default one), and that returns
741            a list of ConfigAction to perform given the value of the parameter.
742            All the ConfigAction must be supported in the given configuration.
743
744        - default
745            An optional default value to use for the parameter when no value is
746            provided on the command-line. If the default value is a callable, it
747            is called with the TestingConfig and should return the default value
748            for the parameter. Whether the default value is computed or specified
749            directly, it must be in the 'choices' provided for that Parameter.
750        """
751        self._name = name
752        if len(self._name) == 0:
753            raise ValueError("Parameter name must not be the empty string")
754
755        if choices is not None:
756            self._choices = list(choices)  # should be finite
757            if len(self._choices) == 0:
758                raise ValueError(
759                    "Parameter '{}' must be given at least one possible value".format(
760                        self._name
761                    )
762                )
763        else:
764            self._choices = None
765
766        self._parse = lambda x: _parse_parameter(x, type)
767        self._help = help
768        self._actions = actions
769        self._default = default
770
771    def _getValue(self, config, litParams):
772        """
773        Return the value of the parameter given the configuration objects.
774        """
775        param = getattr(config, self.name, None)
776        param = litParams.get(self.name, param)
777        if param is None and self._default is None:
778            raise ValueError(
779                "Parameter {} doesn't have a default value, but it was not specified in the Lit parameters or in the Lit config".format(
780                    self.name
781                )
782            )
783        getDefault = (
784            lambda: self._default(config) if callable(self._default) else self._default
785        )
786
787        if param is not None:
788            (pretty, value) = (param, self._parse(param))
789        else:
790            value = getDefault()
791            pretty = "{} (default)".format(value)
792
793        if self._choices and value not in self._choices:
794            raise ValueError(
795                "Got value '{}' for parameter '{}', which is not in the provided set of possible choices: {}".format(
796                    value, self.name, self._choices
797                )
798            )
799        return (pretty, value)
800
801    @property
802    def name(self):
803        """
804        Return the name of the parameter.
805
806        This is the name that can be used to set the parameter on the command-line
807        when running Lit.
808        """
809        return self._name
810
811    def getActions(self, config, litParams):
812        """
813        Return the list of actions associated to this value of the parameter.
814        """
815        (_, parameterValue) = self._getValue(config, litParams)
816        return self._actions(parameterValue)
817
818    def pretty(self, config, litParams):
819        """
820        Return a pretty representation of the parameter's name and value.
821        """
822        (prettyParameterValue, _) = self._getValue(config, litParams)
823        return "{}={}".format(self.name, prettyParameterValue)
824