llvm/libcxx/test/libcxx/selftest/dsl/dsl.sh.py

# ===----------------------------------------------------------------------===##
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------===##

# With picolibc, test_program_stderr_is_not_conflated_with_stdout fails
# because stdout & stderr are treated as the same.
# XFAIL: LIBCXX-PICOLIBC-FIXME

# Note: We prepend arguments with 'x' to avoid thinking there are too few
#       arguments in case an argument is an empty string.
# RUN: %{python} %s x%S x%T x%{substitutions}

import base64
import copy
import os
import pickle
import platform
import subprocess
import sys
import unittest
from os.path import dirname

# Allow importing 'lit' and the 'libcxx' module. Make sure we put the lit
# path first so we don't find any system-installed version.
monorepoRoot = dirname(dirname(dirname(dirname(dirname(dirname(__file__))))))
sys.path = [
    os.path.join(monorepoRoot, "libcxx", "utils"),
    os.path.join(monorepoRoot, "llvm", "utils", "lit"),
] + sys.path
import libcxx.test.dsl as dsl
import lit.LitConfig
import lit.util

# Steal some parameters from the config running this test so that we can
# bootstrap our own TestingConfig.
args = list(map(lambda s: s[1:], sys.argv[1:8]))  # Remove the leading 'x'
SOURCE_ROOT, EXEC_PATH, SUBSTITUTIONS = args
sys.argv[1:8] = []

# Decode the substitutions.
SUBSTITUTIONS = pickle.loads(base64.b64decode(SUBSTITUTIONS))
for s, sub in SUBSTITUTIONS:
    print("Substitution '{}' is '{}'".format(s, sub))


class SetupConfigs(unittest.TestCase):
    """
    Base class for the tests below -- it creates a fake TestingConfig.
    """

    def setUp(self):
        """
        Create a fake TestingConfig that can be populated however we wish for
        the purpose of running unit tests below. We pre-populate it with the
        minimum required substitutions.
        """
        self.litConfig = lit.LitConfig.LitConfig(
            progname="lit",
            path=[],
            quiet=False,
            useValgrind=False,
            valgrindLeakCheck=False,
            valgrindArgs=[],
            noExecute=False,
            debug=False,
            isWindows=platform.system() == "Windows",
            order="smart",
            params={},
        )

        self.config = lit.TestingConfig.TestingConfig.fromdefaults(self.litConfig)
        self.config.environment = dict(os.environ)
        self.config.test_source_root = SOURCE_ROOT
        self.config.test_exec_root = EXEC_PATH
        self.config.recursiveExpansionLimit = 10
        self.config.substitutions = copy.deepcopy(SUBSTITUTIONS)

    def getSubstitution(self, substitution):
        """
        Return a given substitution from the TestingConfig. It is an error if
        there is no such substitution.
        """
        found = [x for (s, x) in self.config.substitutions if s == substitution]
        assert len(found) == 1
        return found[0]


def findIndex(list, pred):
    """Finds the index of the first element satisfying 'pred' in a list, or
    'len(list)' if there is no such element."""
    index = 0
    for x in list:
        if pred(x):
            break
        else:
            index += 1
    return index


class TestHasCompileFlag(SetupConfigs):
    """
    Tests for libcxx.test.dsl.hasCompileFlag
    """

    def test_no_flag_should_work(self):
        self.assertTrue(dsl.hasCompileFlag(self.config, ""))

    def test_flag_exists(self):
        self.assertTrue(dsl.hasCompileFlag(self.config, "-O1"))

    def test_nonexistent_flag(self):
        self.assertFalse(
            dsl.hasCompileFlag(self.config, "-this_is_not_a_flag_any_compiler_has")
        )

    def test_multiple_flags(self):
        self.assertTrue(dsl.hasCompileFlag(self.config, "-O1 -Dhello"))


class TestSourceBuilds(SetupConfigs):
    """
    Tests for libcxx.test.dsl.sourceBuilds
    """

    def test_valid_program_builds(self):
        source = """int main(int, char**) { return 0; }"""
        self.assertTrue(dsl.sourceBuilds(self.config, source))

    def test_compilation_error_fails(self):
        source = """int main(int, char**) { this does not compile }"""
        self.assertFalse(dsl.sourceBuilds(self.config, source))

    def test_link_error_fails(self):
        source = """extern void this_isnt_defined_anywhere();
                    int main(int, char**) { this_isnt_defined_anywhere(); return 0; }"""
        self.assertFalse(dsl.sourceBuilds(self.config, source))


class TestProgramOutput(SetupConfigs):
    """
    Tests for libcxx.test.dsl.programOutput
    """

    def test_valid_program_returns_output(self):
        source = """
        #include <cstdio>
        int main(int, char**) { std::printf("FOOBAR"); return 0; }
        """
        self.assertEqual(dsl.programOutput(self.config, source), "FOOBAR")

    def test_valid_program_returns_output_newline_handling(self):
        source = """
        #include <cstdio>
        int main(int, char**) { std::printf("FOOBAR\\n"); return 0; }
        """
        self.assertEqual(dsl.programOutput(self.config, source), "FOOBAR\n")

    def test_valid_program_returns_no_output(self):
        source = """
        int main(int, char**) { return 0; }
        """
        self.assertEqual(dsl.programOutput(self.config, source), "")

    def test_program_that_fails_to_run_raises_runtime_error(self):
        # The program compiles, but exits with an error
        source = """
        int main(int, char**) { return 1; }
        """
        self.assertRaises(
            dsl.ConfigurationRuntimeError,
            lambda: dsl.programOutput(self.config, source),
        )

    def test_program_that_fails_to_compile_raises_compilation_error(self):
        # The program doesn't compile
        source = """
        int main(int, char**) { this doesnt compile }
        """
        self.assertRaises(
            dsl.ConfigurationCompilationError,
            lambda: dsl.programOutput(self.config, source),
        )

    def test_pass_arguments_to_program(self):
        source = """
        #include <cassert>
        #include <string>
        int main(int argc, char** argv) {
            assert(argc == 3);
            assert(argv[1] == std::string("first-argument"));
            assert(argv[2] == std::string("second-argument"));
            return 0;
        }
        """
        args = ["first-argument", "second-argument"]
        self.assertEqual(dsl.programOutput(self.config, source, args=args), "")

    def test_caching_is_not_too_aggressive(self):
        # Run a program, then change the substitutions and run it again.
        # Make sure the program is run the second time and the right result
        # is given, to ensure we're not incorrectly caching the result of the
        # first program run.
        source = """
        #include <cstdio>
        int main(int, char**) {
            std::printf("MACRO=%u\\n", MACRO);
            return 0;
        }
        """
        compileFlagsIndex = findIndex(
            self.config.substitutions, lambda x: x[0] == "%{compile_flags}"
        )
        compileFlags = self.config.substitutions[compileFlagsIndex][1]

        self.config.substitutions[compileFlagsIndex] = (
            "%{compile_flags}",
            compileFlags + " -DMACRO=1",
        )
        output1 = dsl.programOutput(self.config, source)
        self.assertEqual(output1, "MACRO=1\n")

        self.config.substitutions[compileFlagsIndex] = (
            "%{compile_flags}",
            compileFlags + " -DMACRO=2",
        )
        output2 = dsl.programOutput(self.config, source)
        self.assertEqual(output2, "MACRO=2\n")

    def test_program_stderr_is_not_conflated_with_stdout(self):
        # Run a program that produces stdout output and stderr output too, making
        # sure the stderr output does not pollute the stdout output.
        source = """
        #include <cstdio>
        int main(int, char**) {
            std::fprintf(stdout, "STDOUT-OUTPUT");
            std::fprintf(stderr, "STDERR-OUTPUT");
            return 0;
        }
        """
        self.assertEqual(dsl.programOutput(self.config, source), "STDOUT-OUTPUT")


class TestProgramSucceeds(SetupConfigs):
    """
    Tests for libcxx.test.dsl.programSucceeds
    """

    def test_success(self):
        source = """
        int main(int, char**) { return 0; }
        """
        self.assertTrue(dsl.programSucceeds(self.config, source))

    def test_failure(self):
        source = """
        int main(int, char**) { return 1; }
        """
        self.assertFalse(dsl.programSucceeds(self.config, source))

    def test_compile_failure(self):
        source = """
        this does not compile
        """
        self.assertRaises(
            dsl.ConfigurationCompilationError,
            lambda: dsl.programSucceeds(self.config, source),
        )


class TestHasLocale(SetupConfigs):
    """
    Tests for libcxx.test.dsl.hasLocale
    """

    def test_doesnt_explode(self):
        # It's really hard to test that a system has a given locale, so at least
        # make sure we don't explode when we try to check it.
        try:
            dsl.hasAnyLocale(self.config, ["en_US.UTF-8"])
        except subprocess.CalledProcessError:
            self.fail("checking for hasLocale should not explode")

    def test_nonexistent_locale(self):
        self.assertFalse(
            dsl.hasAnyLocale(self.config, ["forsurethisisnotanexistinglocale"])
        )

    def test_localization_program_doesnt_compile(self):
        compilerIndex = findIndex(self.config.substitutions, lambda x: x[0] == "%{cxx}")
        self.config.substitutions[compilerIndex] = (
            "%{cxx}",
            "this-is-certainly-not-a-valid-compiler!!",
        )
        self.assertRaises(
            dsl.ConfigurationCompilationError,
            lambda: dsl.hasAnyLocale(self.config, ["en_US.UTF-8"]),
        )


class TestCompilerMacros(SetupConfigs):
    """
    Tests for libcxx.test.dsl.compilerMacros
    """

    def test_basic(self):
        macros = dsl.compilerMacros(self.config)
        self.assertIsInstance(macros, dict)
        self.assertGreater(len(macros), 0)
        for (k, v) in macros.items():
            self.assertIsInstance(k, str)
            self.assertIsInstance(v, str)

    def test_no_flag(self):
        macros = dsl.compilerMacros(self.config)
        self.assertIn("__cplusplus", macros.keys())

    def test_empty_flag(self):
        macros = dsl.compilerMacros(self.config, "")
        self.assertIn("__cplusplus", macros.keys())

    def test_with_flag(self):
        macros = dsl.compilerMacros(self.config, "-DFOO=3")
        self.assertIn("__cplusplus", macros.keys())
        self.assertEqual(macros["FOO"], "3")

    def test_with_flags(self):
        macros = dsl.compilerMacros(self.config, "-DFOO=3 -DBAR=hello")
        self.assertIn("__cplusplus", macros.keys())
        self.assertEqual(macros["FOO"], "3")
        self.assertEqual(macros["BAR"], "hello")


class TestFeatureTestMacros(SetupConfigs):
    """
    Tests for libcxx.test.dsl.featureTestMacros
    """

    def test_basic(self):
        macros = dsl.featureTestMacros(self.config)
        self.assertIsInstance(macros, dict)
        self.assertGreater(len(macros), 0)
        for (k, v) in macros.items():
            self.assertIsInstance(k, str)
            self.assertIsInstance(v, int)


class TestFeature(SetupConfigs):
    """
    Tests for libcxx.test.dsl.Feature
    """

    def test_trivial(self):
        feature = dsl.Feature(name="name")
        origSubstitutions = copy.deepcopy(self.config.substitutions)
        actions = feature.getActions(self.config)
        self.assertTrue(len(actions) == 1)
        for a in actions:
            a.applyTo(self.config)
        self.assertEqual(origSubstitutions, self.config.substitutions)
        self.assertIn("name", self.config.available_features)

    def test_name_can_be_a_callable(self):
        feature = dsl.Feature(name=lambda cfg: "name")
        for a in feature.getActions(self.config):
            a.applyTo(self.config)
        self.assertIn("name", self.config.available_features)

    def test_name_is_not_a_string_1(self):
        feature = dsl.Feature(name=None)
        self.assertRaises(ValueError, lambda: feature.getActions(self.config))
        self.assertRaises(ValueError, lambda: feature.pretty(self.config))

    def test_name_is_not_a_string_2(self):
        feature = dsl.Feature(name=lambda cfg: None)
        self.assertRaises(ValueError, lambda: feature.getActions(self.config))
        self.assertRaises(ValueError, lambda: feature.pretty(self.config))

    def test_adding_action(self):
        feature = dsl.Feature(name="name", actions=[dsl.AddCompileFlag("-std=c++03")])
        origLinkFlags = copy.deepcopy(self.getSubstitution("%{link_flags}"))
        for a in feature.getActions(self.config):
            a.applyTo(self.config)
        self.assertIn("name", self.config.available_features)
        self.assertIn("-std=c++03", self.getSubstitution("%{compile_flags}"))
        self.assertEqual(origLinkFlags, self.getSubstitution("%{link_flags}"))

    def test_actions_can_be_a_callable(self):
        feature = dsl.Feature(
            name="name",
            actions=lambda cfg: (
                self.assertIs(self.config, cfg),
                [dsl.AddCompileFlag("-std=c++03")],
            )[1],
        )
        for a in feature.getActions(self.config):
            a.applyTo(self.config)
        self.assertIn("-std=c++03", self.getSubstitution("%{compile_flags}"))

    def test_unsupported_feature(self):
        feature = dsl.Feature(name="name", when=lambda _: False)
        self.assertEqual(feature.getActions(self.config), [])

    def test_is_supported_gets_passed_the_config(self):
        feature = dsl.Feature(
            name="name", when=lambda cfg: (self.assertIs(self.config, cfg), True)[1]
        )
        self.assertEqual(len(feature.getActions(self.config)), 1)


def _throw():
    raise ValueError()


class TestParameter(SetupConfigs):
    """
    Tests for libcxx.test.dsl.Parameter
    """

    def test_empty_name_should_blow_up(self):
        self.assertRaises(
            ValueError,
            lambda: dsl.Parameter(
                name="", choices=["c++03"], type=str, help="", actions=lambda _: []
            ),
        )

    def test_empty_choices_should_blow_up(self):
        self.assertRaises(
            ValueError,
            lambda: dsl.Parameter(
                name="std", choices=[], type=str, help="", actions=lambda _: []
            ),
        )

    def test_no_choices_is_ok(self):
        param = dsl.Parameter(name="triple", type=str, help="", actions=lambda _: [])
        self.assertEqual(param.name, "triple")

    def test_name_is_set_correctly(self):
        param = dsl.Parameter(
            name="std", choices=["c++03"], type=str, help="", actions=lambda _: []
        )
        self.assertEqual(param.name, "std")

    def test_no_value_provided_and_no_default_value(self):
        param = dsl.Parameter(
            name="std", choices=["c++03"], type=str, help="", actions=lambda _: []
        )
        self.assertRaises(
            ValueError, lambda: param.getActions(self.config, self.litConfig.params)
        )

    def test_no_value_provided_and_default_value(self):
        param = dsl.Parameter(
            name="std",
            choices=["c++03"],
            type=str,
            help="",
            default="c++03",
            actions=lambda std: [dsl.AddFeature(std)],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("c++03", self.config.available_features)

    def test_value_provided_on_command_line_and_no_default_value(self):
        self.litConfig.params["std"] = "c++03"
        param = dsl.Parameter(
            name="std",
            choices=["c++03"],
            type=str,
            help="",
            actions=lambda std: [dsl.AddFeature(std)],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("c++03", self.config.available_features)

    def test_value_provided_on_command_line_and_default_value(self):
        """The value provided on the command line should override the default value"""
        self.litConfig.params["std"] = "c++11"
        param = dsl.Parameter(
            name="std",
            choices=["c++03", "c++11"],
            type=str,
            default="c++03",
            help="",
            actions=lambda std: [dsl.AddFeature(std)],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("c++11", self.config.available_features)
        self.assertNotIn("c++03", self.config.available_features)

    def test_value_provided_in_config_and_default_value(self):
        """The value provided in the config should override the default value"""
        self.config.std = "c++11"
        param = dsl.Parameter(
            name="std",
            choices=["c++03", "c++11"],
            type=str,
            default="c++03",
            help="",
            actions=lambda std: [dsl.AddFeature(std)],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("c++11", self.config.available_features)
        self.assertNotIn("c++03", self.config.available_features)

    def test_value_provided_in_config_and_on_command_line(self):
        """The value on the command line should override the one in the config"""
        self.config.std = "c++11"
        self.litConfig.params["std"] = "c++03"
        param = dsl.Parameter(
            name="std",
            choices=["c++03", "c++11"],
            type=str,
            help="",
            actions=lambda std: [dsl.AddFeature(std)],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("c++03", self.config.available_features)
        self.assertNotIn("c++11", self.config.available_features)

    def test_no_actions(self):
        self.litConfig.params["std"] = "c++03"
        param = dsl.Parameter(
            name="std", choices=["c++03"], type=str, help="", actions=lambda _: []
        )
        actions = param.getActions(self.config, self.litConfig.params)
        self.assertEqual(actions, [])

    def test_boolean_value_parsed_from_trueish_string_parameter(self):
        self.litConfig.params["enable_exceptions"] = "True"
        param = dsl.Parameter(
            name="enable_exceptions",
            choices=[True, False],
            type=bool,
            help="",
            actions=lambda exceptions: [] if exceptions else _throw(),
        )
        self.assertEqual(param.getActions(self.config, self.litConfig.params), [])

    def test_boolean_value_from_true_boolean_parameter(self):
        self.litConfig.params["enable_exceptions"] = True
        param = dsl.Parameter(
            name="enable_exceptions",
            choices=[True, False],
            type=bool,
            help="",
            actions=lambda exceptions: [] if exceptions else _throw(),
        )
        self.assertEqual(param.getActions(self.config, self.litConfig.params), [])

    def test_boolean_value_parsed_from_falseish_string_parameter(self):
        self.litConfig.params["enable_exceptions"] = "False"
        param = dsl.Parameter(
            name="enable_exceptions",
            choices=[True, False],
            type=bool,
            help="",
            actions=lambda exceptions: []
            if exceptions
            else [dsl.AddFeature("-fno-exceptions")],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("-fno-exceptions", self.config.available_features)

    def test_boolean_value_from_false_boolean_parameter(self):
        self.litConfig.params["enable_exceptions"] = False
        param = dsl.Parameter(
            name="enable_exceptions",
            choices=[True, False],
            type=bool,
            help="",
            actions=lambda exceptions: []
            if exceptions
            else [dsl.AddFeature("-fno-exceptions")],
        )
        for a in param.getActions(self.config, self.litConfig.params):
            a.applyTo(self.config)
        self.assertIn("-fno-exceptions", self.config.available_features)

    def test_list_parsed_from_comma_delimited_string_empty(self):
        self.litConfig.params["additional_features"] = ""
        param = dsl.Parameter(
            name="additional_features", type=list, help="", actions=lambda f: f
        )
        self.assertEqual(param.getActions(self.config, self.litConfig.params), [])

    def test_list_parsed_from_comma_delimited_string_1(self):
        self.litConfig.params["additional_features"] = "feature1"
        param = dsl.Parameter(
            name="additional_features", type=list, help="", actions=lambda f: f
        )
        self.assertEqual(
            param.getActions(self.config, self.litConfig.params), ["feature1"]
        )

    def test_list_parsed_from_comma_delimited_string_2(self):
        self.litConfig.params["additional_features"] = "feature1,feature2"
        param = dsl.Parameter(
            name="additional_features", type=list, help="", actions=lambda f: f
        )
        self.assertEqual(
            param.getActions(self.config, self.litConfig.params),
            ["feature1", "feature2"],
        )

    def test_list_parsed_from_comma_delimited_string_3(self):
        self.litConfig.params["additional_features"] = "feature1,feature2, feature3"
        param = dsl.Parameter(
            name="additional_features", type=list, help="", actions=lambda f: f
        )
        self.assertEqual(
            param.getActions(self.config, self.litConfig.params),
            ["feature1", "feature2", "feature3"],
        )


if __name__ == "__main__":
    unittest.main(verbosity=2)