Skip to content

Commit 6d58030

Browse files
committed
[libc++] Create a small DSL for defining Lit features and parameters
This allows defining Lit features that can be enabled or disabled based on compiler support, and parameters that are passed on the command line. The main benefits are: - Feature detection is entirely based on the substitutions provided in the TestingConfig object, which is simpler and decouples it from the complicated compiler emulation infrastructure. - The syntax is declarative, which makes it easy to see what features and parameters are accepted by the test suite. This is significantly less entangled than the current config.py logic. - Since feature detection is based on substitutions, it works really well on top of the new format, and custom Lit configurations can be created easily without being based on `config.py`. Differential Revision: https://reviews.llvm.org/D78381
1 parent 9671f6e commit 6d58030

File tree

3 files changed

+600
-0
lines changed

3 files changed

+600
-0
lines changed
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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+
# RUN: %{python} %s '%S' '%T' '%{escaped_exec}' \
9+
# RUN: '%{escaped_cxx}' \
10+
# RUN: '%{escaped_flags}' \
11+
# RUN: '%{escaped_compile_flags}' \
12+
# RUN: '%{escaped_link_flags}'
13+
# END.
14+
15+
import base64
16+
import copy
17+
import os
18+
import platform
19+
import subprocess
20+
import sys
21+
import unittest
22+
from os.path import dirname
23+
24+
# Allow importing 'lit' and the 'libcxx' module. Make sure we put the lit
25+
# path first so we don't find any system-installed version.
26+
monorepoRoot = dirname(dirname(dirname(dirname(dirname(dirname(__file__))))))
27+
sys.path = [os.path.join(monorepoRoot, 'libcxx', 'utils'),
28+
os.path.join(monorepoRoot, 'llvm', 'utils', 'lit')] + sys.path
29+
import libcxx.test.dsl as dsl
30+
import lit.LitConfig
31+
32+
# Steal some parameters from the config running this test so that we can
33+
# bootstrap our own TestingConfig.
34+
SOURCE_ROOT, EXEC_PATH, EXEC, CXX, FLAGS, COMPILE_FLAGS, LINK_FLAGS = sys.argv[1:]
35+
sys.argv = sys.argv[:1]
36+
37+
class SetupConfigs(unittest.TestCase):
38+
"""
39+
Base class for the tests below -- it creates a fake TestingConfig.
40+
"""
41+
def setUp(self):
42+
"""
43+
Create a fake TestingConfig that can be populated however we wish for
44+
the purpose of running unit tests below. We pre-populate it with the
45+
minimum required substitutions.
46+
"""
47+
self.litConfig = lit.LitConfig.LitConfig(
48+
progname='lit',
49+
path=[],
50+
quiet=False,
51+
useValgrind=False,
52+
valgrindLeakCheck=False,
53+
valgrindArgs=[],
54+
noExecute=False,
55+
debug=False,
56+
isWindows=platform.system() == 'Windows',
57+
params={})
58+
59+
self.config = lit.TestingConfig.TestingConfig.fromdefaults(self.litConfig)
60+
self.config.test_source_root = SOURCE_ROOT
61+
self.config.test_exec_root = EXEC_PATH
62+
self.config.substitutions = [
63+
('%{cxx}', base64.b64decode(CXX)),
64+
('%{flags}', base64.b64decode(FLAGS)),
65+
('%{compile_flags}', base64.b64decode(COMPILE_FLAGS)),
66+
('%{link_flags}', base64.b64decode(LINK_FLAGS)),
67+
('%{exec}', base64.b64decode(EXEC))
68+
]
69+
70+
def getSubstitution(self, substitution):
71+
"""
72+
Return a given substitution from the TestingConfig. It is an error if
73+
there is no such substitution.
74+
"""
75+
found = [x for (s, x) in self.config.substitutions if s == substitution]
76+
assert len(found) == 1
77+
return found[0]
78+
79+
80+
class TestHasCompileFlag(SetupConfigs):
81+
"""
82+
Tests for libcxx.test.dsl.hasCompileFlag
83+
"""
84+
def test_no_flag_should_work(self):
85+
self.assertTrue(dsl.hasCompileFlag(self.config, ''))
86+
87+
def test_flag_exists(self):
88+
self.assertTrue(dsl.hasCompileFlag(self.config, '-O1'))
89+
90+
def test_nonexistent_flag(self):
91+
self.assertFalse(dsl.hasCompileFlag(self.config, '-this_is_not_a_flag_any_compiler_has'))
92+
93+
def test_multiple_flags(self):
94+
self.assertTrue(dsl.hasCompileFlag(self.config, '-O1 -Dhello'))
95+
96+
97+
class TestHasLocale(SetupConfigs):
98+
"""
99+
Tests for libcxx.test.dsl.hasLocale
100+
"""
101+
def test_doesnt_explode(self):
102+
# It's really hard to test that a system has a given locale, so at least
103+
# make sure we don't explode when we try to check it.
104+
try:
105+
dsl.hasLocale(self.config, 'en_US.UTF-8')
106+
except subprocess.CalledProcessError:
107+
self.fail("checking for hasLocale should not explode")
108+
109+
def test_nonexistent_locale(self):
110+
self.assertFalse(dsl.hasLocale(self.config, 'for_sure_this_is_not_an_existing_locale'))
111+
112+
113+
class TestCompilerMacros(SetupConfigs):
114+
"""
115+
Tests for libcxx.test.dsl.compilerMacros
116+
"""
117+
def test_basic(self):
118+
macros = dsl.compilerMacros(self.config)
119+
self.assertIsInstance(macros, dict)
120+
self.assertGreater(len(macros), 0)
121+
for (k, v) in macros.items():
122+
self.assertIsInstance(k, str)
123+
self.assertIsInstance(v, str)
124+
125+
def test_no_flag(self):
126+
macros = dsl.compilerMacros(self.config)
127+
self.assertIn('__cplusplus', macros.keys())
128+
129+
def test_empty_flag(self):
130+
macros = dsl.compilerMacros(self.config, '')
131+
self.assertIn('__cplusplus', macros.keys())
132+
133+
def test_with_flag(self):
134+
macros = dsl.compilerMacros(self.config, '-DFOO=3')
135+
self.assertIn('__cplusplus', macros.keys())
136+
self.assertEqual(macros['FOO'], '3')
137+
138+
def test_with_flags(self):
139+
macros = dsl.compilerMacros(self.config, '-DFOO=3 -DBAR=hello')
140+
self.assertIn('__cplusplus', macros.keys())
141+
self.assertEqual(macros['FOO'], '3')
142+
self.assertEqual(macros['BAR'], 'hello')
143+
144+
145+
class TestFeatureTestMacros(SetupConfigs):
146+
"""
147+
Tests for libcxx.test.dsl.featureTestMacros
148+
"""
149+
def test_basic(self):
150+
macros = dsl.featureTestMacros(self.config)
151+
self.assertIsInstance(macros, dict)
152+
self.assertGreater(len(macros), 0)
153+
for (k, v) in macros.items():
154+
self.assertIsInstance(k, str)
155+
self.assertIsInstance(v, int)
156+
157+
158+
class TestFeature(SetupConfigs):
159+
"""
160+
Tests for libcxx.test.dsl.Feature
161+
"""
162+
def test_trivial(self):
163+
feature = dsl.Feature(name='name')
164+
origSubstitutions = copy.deepcopy(self.config.substitutions)
165+
self.assertTrue(feature.isSupported(self.config))
166+
feature.enableIn(self.config)
167+
self.assertEqual(origSubstitutions, self.config.substitutions)
168+
self.assertIn('name', self.config.available_features)
169+
170+
def test_name_can_be_a_callable(self):
171+
feature = dsl.Feature(name=lambda cfg: (self.assertIs(self.config, cfg), 'name')[1])
172+
assert feature.isSupported(self.config)
173+
feature.enableIn(self.config)
174+
self.assertIn('name', self.config.available_features)
175+
176+
def test_adding_compile_flag(self):
177+
feature = dsl.Feature(name='name', compileFlag='-foo')
178+
origLinkFlags = copy.deepcopy(self.getSubstitution('%{link_flags}'))
179+
assert feature.isSupported(self.config)
180+
feature.enableIn(self.config)
181+
self.assertIn('name', self.config.available_features)
182+
self.assertIn('-foo', self.getSubstitution('%{compile_flags}'))
183+
self.assertEqual(origLinkFlags, self.getSubstitution('%{link_flags}'))
184+
185+
def test_adding_link_flag(self):
186+
feature = dsl.Feature(name='name', linkFlag='-foo')
187+
origCompileFlags = copy.deepcopy(self.getSubstitution('%{compile_flags}'))
188+
assert feature.isSupported(self.config)
189+
feature.enableIn(self.config)
190+
self.assertIn('name', self.config.available_features)
191+
self.assertIn('-foo', self.getSubstitution('%{link_flags}'))
192+
self.assertEqual(origCompileFlags, self.getSubstitution('%{compile_flags}'))
193+
194+
def test_adding_both_flags(self):
195+
feature = dsl.Feature(name='name', compileFlag='-hello', linkFlag='-world')
196+
assert feature.isSupported(self.config)
197+
feature.enableIn(self.config)
198+
self.assertIn('name', self.config.available_features)
199+
200+
self.assertIn('-hello', self.getSubstitution('%{compile_flags}'))
201+
self.assertNotIn('-world', self.getSubstitution('%{compile_flags}'))
202+
203+
self.assertIn('-world', self.getSubstitution('%{link_flags}'))
204+
self.assertNotIn('-hello', self.getSubstitution('%{link_flags}'))
205+
206+
def test_unsupported_feature(self):
207+
feature = dsl.Feature(name='name', when=lambda _: False)
208+
self.assertFalse(feature.isSupported(self.config))
209+
# Also make sure we assert if we ever try to add it to a config
210+
self.assertRaises(AssertionError, lambda: feature.enableIn(self.config))
211+
212+
def test_is_supported_gets_passed_the_config(self):
213+
feature = dsl.Feature(name='name', when=lambda cfg: (self.assertIs(self.config, cfg), True)[1])
214+
self.assertTrue(feature.isSupported(self.config))
215+
216+
217+
class TestParameter(SetupConfigs):
218+
"""
219+
Tests for libcxx.test.dsl.Parameter
220+
"""
221+
def test_empty_name_should_blow_up(self):
222+
self.assertRaises(ValueError, lambda: dsl.Parameter(name='', choices=['c++03'], type=str, help='', feature=lambda _: None))
223+
224+
def test_empty_choices_should_blow_up(self):
225+
self.assertRaises(ValueError, lambda: dsl.Parameter(name='std', choices=[], type=str, help='', feature=lambda _: None))
226+
227+
def test_name_is_set_correctly(self):
228+
param = dsl.Parameter(name='std', choices=['c++03'], type=str, help='', feature=lambda _: None)
229+
self.assertEqual(param.name, 'std')
230+
231+
def test_no_value_provided_on_command_line_and_no_default_value(self):
232+
param = dsl.Parameter(name='std', choices=['c++03'], type=str, help='', feature=lambda _: None)
233+
self.assertRaises(ValueError, lambda: param.getFeature(self.config, self.litConfig.params))
234+
235+
def test_no_value_provided_on_command_line_and_default_value(self):
236+
param = dsl.Parameter(name='std', choices=['c++03'], type=str, help='', default='c++03',
237+
feature=lambda std: dsl.Feature(name=std))
238+
param.getFeature(self.config, self.litConfig.params).enableIn(self.config)
239+
self.assertIn('c++03', self.config.available_features)
240+
241+
def test_value_provided_on_command_line_and_no_default_value(self):
242+
self.litConfig.params['std'] = 'c++03'
243+
param = dsl.Parameter(name='std', choices=['c++03'], type=str, help='',
244+
feature=lambda std: dsl.Feature(name=std))
245+
param.getFeature(self.config, self.litConfig.params).enableIn(self.config)
246+
self.assertIn('c++03', self.config.available_features)
247+
248+
def test_value_provided_on_command_line_and_default_value(self):
249+
self.litConfig.params['std'] = 'c++11'
250+
param = dsl.Parameter(name='std', choices=['c++03', 'c++11'], type=str, default='c++03', help='',
251+
feature=lambda std: dsl.Feature(name=std))
252+
param.getFeature(self.config, self.litConfig.params).enableIn(self.config)
253+
self.assertIn('c++11', self.config.available_features)
254+
self.assertNotIn('c++03', self.config.available_features)
255+
256+
def test_feature_is_None(self):
257+
self.litConfig.params['std'] = 'c++03'
258+
param = dsl.Parameter(name='std', choices=['c++03'], type=str, help='',
259+
feature=lambda _: None)
260+
feature = param.getFeature(self.config, self.litConfig.params)
261+
self.assertIsNone(feature)
262+
263+
def test_boolean_value_parsed_from_trueish_string_parameter(self):
264+
self.litConfig.params['enable_exceptions'] = "True"
265+
param = dsl.Parameter(name='enable_exceptions', choices=[True, False], type=bool, help='',
266+
feature=lambda exceptions: None if exceptions else ValueError())
267+
self.assertIsNone(param.getFeature(self.config, self.litConfig.params))
268+
269+
def test_boolean_value_from_true_boolean_parameter(self):
270+
self.litConfig.params['enable_exceptions'] = True
271+
param = dsl.Parameter(name='enable_exceptions', choices=[True, False], type=bool, help='',
272+
feature=lambda exceptions: None if exceptions else ValueError())
273+
self.assertIsNone(param.getFeature(self.config, self.litConfig.params))
274+
275+
def test_boolean_value_parsed_from_falseish_string_parameter(self):
276+
self.litConfig.params['enable_exceptions'] = "False"
277+
param = dsl.Parameter(name='enable_exceptions', choices=[True, False], type=bool, help='',
278+
feature=lambda exceptions: None if exceptions else dsl.Feature(name="-fno-exceptions"))
279+
param.getFeature(self.config, self.litConfig.params).enableIn(self.config)
280+
self.assertIn('-fno-exceptions', self.config.available_features)
281+
282+
def test_boolean_value_from_false_boolean_parameter(self):
283+
self.litConfig.params['enable_exceptions'] = False
284+
param = dsl.Parameter(name='enable_exceptions', choices=[True, False], type=bool, help='',
285+
feature=lambda exceptions: None if exceptions else dsl.Feature(name="-fno-exceptions"))
286+
param.getFeature(self.config, self.litConfig.params).enableIn(self.config)
287+
self.assertIn('-fno-exceptions', self.config.available_features)
288+
289+
290+
if __name__ == '__main__':
291+
unittest.main(verbosity=2)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Since we try to pass substitutions as-is to some tests, we must "escape"
2+
# them in case they contain other substitutions. Otherwise, the substitutions
3+
# will be fully expanded when passed to the tests. For example, we want an
4+
# %{exec} substitution that contains `--dependencies %{file_dependencies}`
5+
# to be passed as-is, without substituting the file dependencies. This way,
6+
# the test itself can use populate %{file_dependencies} as it sees fit, and
7+
# %{exec} will respect it.
8+
#
9+
# To solve this problem, we add base64 encoded versions of substitutions just
10+
# in this directory. We then base64-decode them from the tests when we need to.
11+
# Another option would be to have a way to prevent expansion in Lit itself.
12+
import base64
13+
escaped = [(k.replace('%{', '%{escaped_'), base64.b64encode(v)) for (k, v) in config.substitutions]
14+
config.substitutions.extend(escaped)
15+
16+
# The tests in this directory need to run Python
17+
import sys
18+
config.substitutions.append(('%{python}', sys.executable))

0 commit comments

Comments
 (0)