xref: /illumos-gate/usr/src/test/header-tests/tests/common/symbol_test.py (revision e1e6b944360d951edf36ef4198261818ed2d9b2f)
1861094cfSGordon Ross#!@PYTHON@
2861094cfSGordon Ross#
3861094cfSGordon Ross# This file and its contents are supplied under the terms of the
4861094cfSGordon Ross# Common Development and Distribution License ("CDDL"), version 1.0.
5861094cfSGordon Ross# You may only use this file in accordance with the terms of version
6861094cfSGordon Ross# 1.0 of the CDDL.
7861094cfSGordon Ross#
8861094cfSGordon Ross# A full copy of the text of the CDDL should have accompanied this
9861094cfSGordon Ross# source.  A copy of the CDDL is also available via the Internet at
10861094cfSGordon Ross# http://www.illumos.org/license/CDDL.
11861094cfSGordon Ross#
12861094cfSGordon Ross
13861094cfSGordon Ross#
14861094cfSGordon Ross# Copyright 2026 Gordon W. Ross
15861094cfSGordon Ross#
16861094cfSGordon Ross
17861094cfSGordon Ross"""
18861094cfSGordon Rosssymbol_test.py - C and C++ symbol visibility test driver.
19861094cfSGordon Ross
20861094cfSGordon RossReads an environment config file and one or more symbols config files,
21861094cfSGordon Rossgenerates probe programs for each (symbol, environment) pair, compiles
22861094cfSGordon Rossthem, and reports pass/fail.  Supports parallel compilation jobs.
23861094cfSGordon Ross
24861094cfSGordon RossThis program is run separately for 64-bit compiles and (where supported)
25861094cfSGordon Ross32-bit compiles by the test driver scripts.  See the setup.ksh scripts in
26861094cfSGordon Rossthe ../c-symbols/ and ../cxx-symbols/ directories for examples.
27861094cfSGordon Ross
28861094cfSGordon RossSee also tests/common/README.md.
29861094cfSGordon Ross"""
30861094cfSGordon Ross
31861094cfSGordon Rossimport argparse
32861094cfSGordon Rossimport io
33861094cfSGordon Rossimport os
34861094cfSGordon Rossimport signal
35861094cfSGordon Rossimport subprocess
36861094cfSGordon Rossimport sys
37861094cfSGordon Rossimport tempfile
38861094cfSGordon Rossimport threading
39861094cfSGordon Rossfrom concurrent.futures import ThreadPoolExecutor
40861094cfSGordon Ross
41861094cfSGordon Ross
42861094cfSGordon Ross# ---------------------------------------------------------------------------
43861094cfSGordon Ross# parse_cfg - shared cfg file parser
44861094cfSGordon Ross# ---------------------------------------------------------------------------
45861094cfSGordon Ross
46861094cfSGordon Rossdef parse_cfg(fileobj, filename, handlers):
47861094cfSGordon Ross    """
48861094cfSGordon Ross    Parse a cfg file-like object, dispatching each directive line to handlers.
49861094cfSGordon Ross
50861094cfSGordon Ross    Handles continuation lines (trailing backslash), blank lines, and comments
51861094cfSGordon Ross    (lines starting with '#').  Each non-blank, non-comment line is split on
52861094cfSGordon Ross    '|' with each field stripped, then dispatched as:
53861094cfSGordon Ross
54861094cfSGordon Ross        handlers[keyword](fields, filename, lineno)
55861094cfSGordon Ross
56861094cfSGordon Ross    Unknown keywords cause an error via sys.exit().
57861094cfSGordon Ross    """
58861094cfSGordon Ross    lineno = 0
59861094cfSGordon Ross    accum = ''
60861094cfSGordon Ross    for raw in fileobj:
61861094cfSGordon Ross        lineno += 1
62861094cfSGordon Ross        text = raw.rstrip('\n').rstrip('\r')
63861094cfSGordon Ross        if text.endswith('\\'):
64861094cfSGordon Ross            accum += text[:-1]
65861094cfSGordon Ross            continue
66861094cfSGordon Ross        accum += text
67861094cfSGordon Ross        line = accum.strip()
68861094cfSGordon Ross        accum = ''
69861094cfSGordon Ross        if not line or line.startswith('#'):
70861094cfSGordon Ross            continue
71861094cfSGordon Ross        parts = [p.strip() for p in line.split('|')]
72861094cfSGordon Ross        keyword, fields = parts[0], parts[1:]
73861094cfSGordon Ross        if keyword not in handlers:
74861094cfSGordon Ross            sys.exit(
75861094cfSGordon Ross                f'error: {filename}:{lineno}: unknown keyword {keyword!r}')
76861094cfSGordon Ross        handlers[keyword](fields, filename, lineno)
77861094cfSGordon Ross
78861094cfSGordon Ross
79861094cfSGordon Ross# ---------------------------------------------------------------------------
80861094cfSGordon Ross# CompileEnv - one compilation environment (name, lang standard, defines)
81861094cfSGordon Ross# ---------------------------------------------------------------------------
82861094cfSGordon Ross
83861094cfSGordon Rossclass CompileEnv:
84861094cfSGordon Ross    """One compilation environment as defined by an 'env' line in the cfg."""
85861094cfSGordon Ross
86861094cfSGordon Ross    def __init__(self, name, lang, defs):
87861094cfSGordon Ross        self.name = name
88861094cfSGordon Ross        self.lang = lang    # e.g. 'c++98', 'c99'
89861094cfSGordon Ross        self.defs = defs    # preprocessor defines string, may be empty
90861094cfSGordon Ross
91861094cfSGordon Ross
92861094cfSGordon Ross# ---------------------------------------------------------------------------
93861094cfSGordon Ross# EnvConfig - loads and represents the env cfg file
94861094cfSGordon Ross# See test_parse_env.py for examples of method calls and results.
95861094cfSGordon Ross# ---------------------------------------------------------------------------
96861094cfSGordon Ross
97861094cfSGordon Rossclass EnvConfig:
98861094cfSGordon Ross    """
99861094cfSGordon Ross    Loads a compilation environment config file (c-symbols-env.cfg or
100861094cfSGordon Ross    cxx-symbols-env.cfg) and provides environment and group lookup.
101861094cfSGordon Ross
102861094cfSGordon Ross    Public attributes:
103861094cfSGordon Ross      envs:   dict  name -> CompileEnv
104861094cfSGordon Ross      groups: dict  name -> frozenset of env names
105861094cfSGordon Ross    """
106861094cfSGordon Ross
107861094cfSGordon Ross    def __init__(self, lang):
108861094cfSGordon Ross        self.lang = lang
109861094cfSGordon Ross        self.envs = {}
110861094cfSGordon Ross        self.groups = {}
111861094cfSGordon Ross
112861094cfSGordon Ross    def load(self, path):
113861094cfSGordon Ross        """Load env cfg from a file path."""
114861094cfSGordon Ross        # Locations searched for the file:
115861094cfSGordon Ross        #   1. path as given
116861094cfSGordon Ross        #   2. $STF_SUITE/cfg/<path>
117861094cfSGordon Ross        #   3. cfg/<path>
118861094cfSGordon Ross        candidates = [path]
119861094cfSGordon Ross        if not os.path.isabs(path):
120861094cfSGordon Ross            stf = os.environ.get('STF_SUITE', '../..')
121861094cfSGordon Ross            candidates.append(os.path.join(stf, 'cfg', path))
122861094cfSGordon Ross            candidates.append(os.path.join('cfg', path))
123861094cfSGordon Ross
124861094cfSGordon Ross        for candidate in candidates:
125861094cfSGordon Ross            if os.path.exists(candidate):
126861094cfSGordon Ross                with open(candidate) as f:
127861094cfSGordon Ross                    self._parse(f, filename=candidate)
128861094cfSGordon Ross                return
129861094cfSGordon Ross
130861094cfSGordon Ross        sys.exit(f'error: env cfg file not found: {path}')
131861094cfSGordon Ross
132861094cfSGordon Ross    def _parse(self, fileobj, filename='<input>'):
133861094cfSGordon Ross        """
134861094cfSGordon Ross        Parse a file-like object as an environment config file.
135861094cfSGordon Ross
136861094cfSGordon Ross        self.lang must be 'c' or 'c++'.  Each env line's lang field is
137861094cfSGordon Ross        validated against this value; a mismatch is an error.
138861094cfSGordon Ross        """
139861094cfSGordon Ross        handlers = {
140861094cfSGordon Ross            'env':       lambda f, fn, ln: self._do_env(f, fn, ln),
141861094cfSGordon Ross            'env_group': self._do_env_group,
142861094cfSGordon Ross        }
143861094cfSGordon Ross        parse_cfg(fileobj, filename, handlers)
144861094cfSGordon Ross
145861094cfSGordon Ross    def _do_env(self, fields, filename, lineno):
146861094cfSGordon Ross        if len(fields) != 3:
147861094cfSGordon Ross            sys.exit(
148861094cfSGordon Ross                f'error: {filename}:{lineno}: env: expected 3 fields, '
149861094cfSGordon Ross                f'got {len(fields)}')
150861094cfSGordon Ross
151861094cfSGordon Ross        name, env_lang, defs = fields
152861094cfSGordon Ross
153861094cfSGordon Ross        # Validate lang consistency
154861094cfSGordon Ross        is_cxx = env_lang.startswith('c++')
155861094cfSGordon Ross        if self.lang == 'c++' and not is_cxx:
156861094cfSGordon Ross            sys.exit(
157861094cfSGordon Ross                f'error: --lang=c++ but {filename} line {lineno} '
158861094cfSGordon Ross                f'has lang {env_lang!r}')
159861094cfSGordon Ross        if self.lang == 'c' and is_cxx:
160861094cfSGordon Ross            sys.exit(
161861094cfSGordon Ross                f'error: --lang=c but {filename} line {lineno} '
162861094cfSGordon Ross                f'has lang {env_lang!r}')
163861094cfSGordon Ross
164861094cfSGordon Ross        self.envs[name] = CompileEnv(name=name, lang=env_lang, defs=defs)
165861094cfSGordon Ross
166861094cfSGordon Ross    def _do_env_group(self, fields, filename, lineno):
167861094cfSGordon Ross        if len(fields) != 2:
168861094cfSGordon Ross            sys.exit(
169861094cfSGordon Ross                f'error: {filename}:{lineno}: env_group: expected 2 fields, '
170861094cfSGordon Ross                f'got {len(fields)}')
171861094cfSGordon Ross
172861094cfSGordon Ross        name, members_str = fields
173861094cfSGordon Ross        members = set()
174861094cfSGordon Ross        for member in members_str.split():
175861094cfSGordon Ross            if member in self.envs:
176861094cfSGordon Ross                members.add(member)
177861094cfSGordon Ross            elif member in self.groups:
178861094cfSGordon Ross                members |= self.groups[member]
179861094cfSGordon Ross            else:
180861094cfSGordon Ross                sys.exit(
181861094cfSGordon Ross                    f'error: {filename}:{lineno}: '
182861094cfSGordon Ross                    f'reference to undefined env {member!r}')
183861094cfSGordon Ross
184861094cfSGordon Ross        self.groups[name] = frozenset(members)
185861094cfSGordon Ross
186861094cfSGordon Ross    def resolve(self, spec):
187861094cfSGordon Ross        """
188861094cfSGordon Ross        Parse an env spec string like 'CXX98+ -CXX11 +CXX98'.
189861094cfSGordon Ross
190861094cfSGordon Ross        Returns (test_set, need_set) where both are sets of env names:
191861094cfSGordon Ross          test_set  - all envs to compile
192861094cfSGordon Ross          need_set  - envs where compilation must succeed (pass)
193861094cfSGordon Ross
194861094cfSGordon Ross        Bare name or +name -> added to both test_set and need_set.
195861094cfSGordon Ross        -name -> added to test_set only (must fail).
196861094cfSGordon Ross        """
197861094cfSGordon Ross        test_set = set()
198861094cfSGordon Ross        need_set = set()
199861094cfSGordon Ross
200861094cfSGordon Ross        for token in spec.split():
201861094cfSGordon Ross            if token.startswith('+'):
202861094cfSGordon Ross                act = True
203861094cfSGordon Ross                token = token[1:]
204861094cfSGordon Ross            elif token.startswith('-'):
205861094cfSGordon Ross                act = False
206861094cfSGordon Ross                token = token[1:]
207861094cfSGordon Ross            else:
208861094cfSGordon Ross                act = True
209861094cfSGordon Ross
210861094cfSGordon Ross            # Expand token - may be a single env or a group
211861094cfSGordon Ross            expanded = self._expand(token)
212861094cfSGordon Ross            test_set |= expanded
213861094cfSGordon Ross            if act:
214861094cfSGordon Ross                need_set |= expanded
215861094cfSGordon Ross            else:
216861094cfSGordon Ross                need_set -= expanded
217861094cfSGordon Ross
218861094cfSGordon Ross        return (test_set, need_set)
219861094cfSGordon Ross
220861094cfSGordon Ross    def _expand(self, name):
221861094cfSGordon Ross        """Return frozenset of env names for a single env name or group name."""
222861094cfSGordon Ross        if name in self.envs:
223861094cfSGordon Ross            return frozenset({name})
224861094cfSGordon Ross        if name in self.groups:
225861094cfSGordon Ross            return self.groups[name]
226861094cfSGordon Ross        sys.exit(f'error: reference to undefined env or group {name!r}')
227861094cfSGordon Ross
228*e1e6b944SGordon Ross    def expand(self, name):
229*e1e6b944SGordon Ross        """Return the set of environments named by one env or group."""
230*e1e6b944SGordon Ross        return set(self._expand(name))
231*e1e6b944SGordon Ross
232861094cfSGordon Ross
233861094cfSGordon Ross# ---------------------------------------------------------------------------
234861094cfSGordon Ross# SymEntry - one symbol entry from a symbols config file
235861094cfSGordon Ross# ---------------------------------------------------------------------------
236861094cfSGordon Ross
237861094cfSGordon Rossclass SymEntry:
238861094cfSGordon Ross    """One test entry as parsed from a symbols config file."""
239861094cfSGordon Ross
240861094cfSGordon Ross    def __init__(self, directive, symbol, env_spec, headers,
241861094cfSGordon Ross                 rtype=None, atypes=None, defval=None):
242861094cfSGordon Ross        self.directive = directive  # 'func' | 'type' | 'value' | 'define'
243861094cfSGordon Ross        self.symbol    = symbol     # name to test
244861094cfSGordon Ross        self.env_spec  = env_spec   # raw env spec string e.g. 'CXX98+ -CXX11'
245861094cfSGordon Ross        self.headers   = headers    # list of header filenames
246861094cfSGordon Ross        self.rtype     = rtype      # return/declaration type (not for define)
247861094cfSGordon Ross        self.atypes    = atypes if atypes is not None else []
248861094cfSGordon Ross        self.defval    = defval     # expected value for 'define', else None
249861094cfSGordon Ross
250861094cfSGordon Ross
251861094cfSGordon Ross# ---------------------------------------------------------------------------
252861094cfSGordon Ross# SymConfig - loads symbols config files
253861094cfSGordon Ross# See test_parse_sym.py for examples of method calls and results.
254861094cfSGordon Ross# ---------------------------------------------------------------------------
255861094cfSGordon Ross
256861094cfSGordon Rossclass SymConfig:
257861094cfSGordon Ross    """
258861094cfSGordon Ross    Loads one or more symbols config files.
259861094cfSGordon Ross
260861094cfSGordon Ross    Public attributes:
261861094cfSGordon Ross      entries: list of SymEntry, in file order
262*e1e6b944SGordon Ross      primary_header: first header named by the loaded configuration files
263861094cfSGordon Ross    """
264861094cfSGordon Ross
265861094cfSGordon Ross    def __init__(self):
266861094cfSGordon Ross        self.entries = []
267*e1e6b944SGordon Ross        self.primary_header = None
268861094cfSGordon Ross
269861094cfSGordon Ross    def load(self, path):
270861094cfSGordon Ross        """Load symbols config from a file path, with STF_SUITE search."""
271861094cfSGordon Ross        candidates = [path]
272861094cfSGordon Ross        if not os.path.isabs(path):
273861094cfSGordon Ross            stf = os.environ.get('STF_SUITE', '../..')
274861094cfSGordon Ross            candidates.append(os.path.join(stf, 'cfg', path))
275861094cfSGordon Ross            candidates.append(os.path.join('cfg', path))
276861094cfSGordon Ross
277861094cfSGordon Ross        for candidate in candidates:
278861094cfSGordon Ross            if os.path.exists(candidate):
279861094cfSGordon Ross                with open(candidate) as f:
280861094cfSGordon Ross                    self._parse(f, filename=candidate)
281861094cfSGordon Ross                return
282861094cfSGordon Ross
283861094cfSGordon Ross        sys.exit(f'error: symbols config file not found: {path}')
284861094cfSGordon Ross
285861094cfSGordon Ross    def _parse(self, fileobj, filename='<input>'):
286861094cfSGordon Ross        """Parse a file-like object as a symbols config file."""
287*e1e6b944SGordon Ross        self._file_primary_header = None
288*e1e6b944SGordon Ross        self._warned_primary_headers = set()
289861094cfSGordon Ross        handlers = {
290861094cfSGordon Ross            'type':   self._do_type,
291861094cfSGordon Ross            'value':  self._do_value,
292861094cfSGordon Ross            'define': self._do_define,
293861094cfSGordon Ross            'func':   self._do_func,
294861094cfSGordon Ross        }
295861094cfSGordon Ross        parse_cfg(fileobj, filename, handlers)
296*e1e6b944SGordon Ross        if self._file_primary_header is None:
297*e1e6b944SGordon Ross            sys.exit(f'error: {filename}: symbols configuration is empty')
298861094cfSGordon Ross
299861094cfSGordon Ross    @staticmethod
300861094cfSGordon Ross    def _split_list(s):
301861094cfSGordon Ross        """Split a semicolon-separated field, stripping each item."""
302861094cfSGordon Ross        return [item.strip() for item in s.split(';') if item.strip()]
303861094cfSGordon Ross
304*e1e6b944SGordon Ross    def _add_entry(self, entry, filename, lineno):
305*e1e6b944SGordon Ross        if not entry.headers:
306*e1e6b944SGordon Ross            sys.exit(f'error: {filename}:{lineno}: no header specified')
307*e1e6b944SGordon Ross
308*e1e6b944SGordon Ross        # A configuration describes one primary header.  Later headers in an
309*e1e6b944SGordon Ross        # entry are there to support its probe, but only the first identifies
310*e1e6b944SGordon Ross        # the header whose positive coverage we track.
311*e1e6b944SGordon Ross        header = entry.headers[0]
312*e1e6b944SGordon Ross        if self._file_primary_header is None:
313*e1e6b944SGordon Ross            self._file_primary_header = header
314*e1e6b944SGordon Ross            if self.primary_header is None:
315*e1e6b944SGordon Ross                self.primary_header = header
316*e1e6b944SGordon Ross            elif header != self.primary_header:
317*e1e6b944SGordon Ross                sys.exit(
318*e1e6b944SGordon Ross                    f'error: {filename}:{lineno}: primary header {header!r} '
319*e1e6b944SGordon Ross                    f'differs from {self.primary_header!r}\n'
320*e1e6b944SGordon Ross                    'Only one primary header per invocation is supported')
321*e1e6b944SGordon Ross        elif header != self._file_primary_header:
322*e1e6b944SGordon Ross            # Keep the entry for its ordinary symbol test, but it does not
323*e1e6b944SGordon Ross            # establish positive coverage for the primary header.
324*e1e6b944SGordon Ross            if header not in self._warned_primary_headers:
325*e1e6b944SGordon Ross                print(
326*e1e6b944SGordon Ross                    f'warning: {filename}:{lineno}: primary header {header!r} '
327*e1e6b944SGordon Ross                    f'differs from {self._file_primary_header!r}\n'
328*e1e6b944SGordon Ross                    'Only one primary header per configuration file is '
329*e1e6b944SGordon Ross                    'expected',
330*e1e6b944SGordon Ross                    file=sys.stderr)
331*e1e6b944SGordon Ross                self._warned_primary_headers.add(header)
332*e1e6b944SGordon Ross
333*e1e6b944SGordon Ross        self.entries.append(entry)
334*e1e6b944SGordon Ross
335861094cfSGordon Ross    def _do_type(self, fields, filename, lineno):
336861094cfSGordon Ross        # type | decl | headers | envs
337861094cfSGordon Ross        if len(fields) != 3:
338861094cfSGordon Ross            sys.exit(
339861094cfSGordon Ross                f'error: {filename}:{lineno}: type: expected 3 fields, '
340861094cfSGordon Ross                f'got {len(fields)}')
341861094cfSGordon Ross        decl, hdrs, envs = fields
342*e1e6b944SGordon Ross        self._add_entry(SymEntry(
343861094cfSGordon Ross            directive='type',
344861094cfSGordon Ross            symbol=decl,
345861094cfSGordon Ross            rtype=decl,
346861094cfSGordon Ross            headers=self._split_list(hdrs),
347861094cfSGordon Ross            env_spec=envs,
348*e1e6b944SGordon Ross        ), filename, lineno)
349861094cfSGordon Ross
350861094cfSGordon Ross    def _do_value(self, fields, filename, lineno):
351861094cfSGordon Ross        # value | name | type | headers | envs
352861094cfSGordon Ross        if len(fields) != 4:
353861094cfSGordon Ross            sys.exit(
354861094cfSGordon Ross                f'error: {filename}:{lineno}: value: expected 4 fields, '
355861094cfSGordon Ross                f'got {len(fields)}')
356861094cfSGordon Ross        name, rtype, hdrs, envs = fields
357*e1e6b944SGordon Ross        self._add_entry(SymEntry(
358861094cfSGordon Ross            directive='value',
359861094cfSGordon Ross            symbol=name,
360861094cfSGordon Ross            rtype=rtype,
361861094cfSGordon Ross            headers=self._split_list(hdrs),
362861094cfSGordon Ross            env_spec=envs,
363*e1e6b944SGordon Ross        ), filename, lineno)
364861094cfSGordon Ross
365861094cfSGordon Ross    def _do_define(self, fields, filename, lineno):
366861094cfSGordon Ross        # define | name | value | headers | envs  (value may be empty)
367861094cfSGordon Ross        if len(fields) != 4:
368861094cfSGordon Ross            sys.exit(
369861094cfSGordon Ross                f'error: {filename}:{lineno}: define: expected 4 fields, '
370861094cfSGordon Ross                f'got {len(fields)}')
371861094cfSGordon Ross        name, defval, hdrs, envs = fields
372*e1e6b944SGordon Ross        self._add_entry(SymEntry(
373861094cfSGordon Ross            directive='define',
374861094cfSGordon Ross            symbol=name,
375861094cfSGordon Ross            defval=defval if defval else None,
376861094cfSGordon Ross            headers=self._split_list(hdrs),
377861094cfSGordon Ross            env_spec=envs,
378*e1e6b944SGordon Ross        ), filename, lineno)
379861094cfSGordon Ross
380861094cfSGordon Ross    def _do_func(self, fields, filename, lineno):
381861094cfSGordon Ross        # func | name | rtype | atypes | headers | envs
382861094cfSGordon Ross        if len(fields) != 5:
383861094cfSGordon Ross            sys.exit(
384861094cfSGordon Ross                f'error: {filename}:{lineno}: func: expected 5 fields, '
385861094cfSGordon Ross                f'got {len(fields)}')
386861094cfSGordon Ross        name, rtype, atypes, hdrs, envs = fields
387*e1e6b944SGordon Ross        self._add_entry(SymEntry(
388861094cfSGordon Ross            directive='func',
389861094cfSGordon Ross            symbol=name,
390861094cfSGordon Ross            rtype=rtype,
391861094cfSGordon Ross            atypes=self._split_list(atypes),
392861094cfSGordon Ross            headers=self._split_list(hdrs),
393861094cfSGordon Ross            env_spec=envs,
394*e1e6b944SGordon Ross        ), filename, lineno)
395861094cfSGordon Ross
396861094cfSGordon Ross
397861094cfSGordon Ross# ---------------------------------------------------------------------------
398861094cfSGordon Ross# ProbeGen - generates probe program source for a (SymEntry, lang) pair
399861094cfSGordon Ross# See test_gen_probe.py for examples of method calls and results.
400861094cfSGordon Ross# ---------------------------------------------------------------------------
401861094cfSGordon Ross
402861094cfSGordon Rossclass ProbeGen:
403861094cfSGordon Ross
404861094cfSGordon Ross    RESULT_MACRO = (
405861094cfSGordon Ross        '#if __cplusplus >= 201103L\n'
406861094cfSGordon Ross        '#define RESULT(v) result{v}\n'
407861094cfSGordon Ross        '#else\n'
408861094cfSGordon Ross        '#define RESULT(v) result = (v)\n'
409861094cfSGordon Ross        '#endif\n'
410861094cfSGordon Ross    )
411861094cfSGordon Ross
412861094cfSGordon Ross    @staticmethod
413861094cfSGordon Ross    def gen_probe(entry, lang):
414861094cfSGordon Ross        """
415861094cfSGordon Ross        Generate probe program source text for a SymEntry and language.
416861094cfSGordon Ross
417861094cfSGordon Ross        """
418861094cfSGordon Ross        out = []
419861094cfSGordon Ross
420861094cfSGordon Ross        for h in entry.headers:
421861094cfSGordon Ross            out.append(f'#include <{h}>\n')
422861094cfSGordon Ross
423861094cfSGordon Ross        rtype = entry.rtype or ''
424861094cfSGordon Ross        prefix, suffix = ProbeGen._split_rtype(rtype)
425861094cfSGordon Ross        is_fnptr = suffix != ''
426861094cfSGordon Ross
427861094cfSGordon Ross        # Emit the RESULT() macro for C++ func probes with a non-void,
428861094cfSGordon Ross        # non-fnptr return value (brace-init catches narrowing errors in
429861094cfSGordon Ross        # C++11+; plain assignment used for C++98).
430861094cfSGordon Ross        has_rtype = (entry.directive == 'func' and
431861094cfSGordon Ross                     rtype != '' and rtype != 'void')
432861094cfSGordon Ross        use_result = has_rtype and not is_fnptr and lang == 'c++'
433861094cfSGordon Ross
434861094cfSGordon Ross        if use_result:
435861094cfSGordon Ross            out.append(ProbeGen.RESULT_MACRO)
436861094cfSGordon Ross
437861094cfSGordon Ross        # Emit return type prefix + trailing space (matches C programs'
438861094cfSGordon Ross        # unconditional addprogch(' ') after the rtype loop).
439861094cfSGordon Ross        if rtype:
440861094cfSGordon Ross            out.append(prefix + ' ')
441861094cfSGordon Ross
442*e1e6b944SGordon Ross        # Special "include-only" tests are used to ensure that a header
443*e1e6b944SGordon Ross        # (at least) compiles in a trivial C program.
444*e1e6b944SGordon Ross        if entry.directive == 'include-only':
445*e1e6b944SGordon Ross            out.append('int header_compile_test;\n')
446*e1e6b944SGordon Ross
447*e1e6b944SGordon Ross        elif entry.directive == 'type':
448861094cfSGordon Ross            out.append('test_type;\n')
449861094cfSGordon Ross
450861094cfSGordon Ross        elif entry.directive == 'value':
451861094cfSGordon Ross            out.append(f'test_value{suffix};\n')
452861094cfSGordon Ross            out.append('void\ntest_func(void)\n{\n')
453861094cfSGordon Ross            out.append(f'\ttest_value = {entry.symbol};\n}}\n')
454861094cfSGordon Ross
455861094cfSGordon Ross        elif entry.directive == 'define':
456861094cfSGordon Ross            out.append(f'#if !defined({entry.symbol})')
457861094cfSGordon Ross            if entry.defval:
458861094cfSGordon Ross                out.append(f' || {entry.symbol} != {entry.defval}')
459861094cfSGordon Ross            out.append(f'\n#error {entry.symbol} is not defined or has the wrong value')
460861094cfSGordon Ross            out.append('\n#endif\n')
461861094cfSGordon Ross            out.append('\n')
462861094cfSGordon Ross
463861094cfSGordon Ross        elif entry.directive == 'func':
464861094cfSGordon Ross            arglist = ProbeGen._build_arglist(entry.atypes)
465861094cfSGordon Ross            out.append(f'\ntest_func({arglist}){suffix}\n{{\n\t')
466861094cfSGordon Ross
467861094cfSGordon Ross            call = f'{entry.symbol}({ProbeGen._build_callargs(entry.atypes)})'
468861094cfSGordon Ross
469861094cfSGordon Ross            if use_result:
470861094cfSGordon Ross                out.append(f'{rtype} RESULT({call});\n\treturn result;\n}}')
471861094cfSGordon Ross            elif has_rtype or is_fnptr:
472861094cfSGordon Ross                out.append(f'return {call};\n}}')
473861094cfSGordon Ross            else:
474861094cfSGordon Ross                out.append(f'{call};\n}}')
475861094cfSGordon Ross            out.append('\n')
476861094cfSGordon Ross
477861094cfSGordon Ross        return ''.join(out)
478861094cfSGordon Ross
479861094cfSGordon Ross    @staticmethod
480861094cfSGordon Ross    def _split_rtype(rtype):
481861094cfSGordon Ross        """
482861094cfSGordon Ross        Split rtype for function pointer support.
483861094cfSGordon Ross
484861094cfSGordon Ross        For 'void (*)(int)': returns ('void (*', ')(int)')
485861094cfSGordon Ross        For 'double':        returns ('double', '')
486861094cfSGordon Ross        """
487861094cfSGordon Ross        idx = rtype.find('(*')
488861094cfSGordon Ross        if idx >= 0:
489861094cfSGordon Ross            return rtype[:idx + 2], rtype[idx + 2:]
490861094cfSGordon Ross        return rtype, ''
491861094cfSGordon Ross
492861094cfSGordon Ross    @staticmethod
493861094cfSGordon Ross    def _build_arglist(atypes):
494861094cfSGordon Ross        """Build the formal parameter list string, matching C arg-name insertion."""
495861094cfSGordon Ross        if not atypes:
496861094cfSGordon Ross            return 'void'
497861094cfSGordon Ross        parts = []
498861094cfSGordon Ross        for i, atype in enumerate(atypes):
499861094cfSGordon Ross            if atype == '':
500861094cfSGordon Ross                parts.append('void')
501861094cfSGordon Ross            elif atype == 'void':
502861094cfSGordon Ross                parts.append('void')
503861094cfSGordon Ross            elif '(*' in atype:
504861094cfSGordon Ross                idx = atype.index('(*')
505861094cfSGordon Ross                parts.append(f'{atype[:idx + 2]}a{i}{atype[idx + 2:]}')
506861094cfSGordon Ross            elif '[' in atype:
507861094cfSGordon Ross                idx = atype.index('[')
508861094cfSGordon Ross                parts.append(f'{atype[:idx]}a{i}{atype[idx:]}')
509861094cfSGordon Ross            else:
510861094cfSGordon Ross                parts.append(f'{atype} a{i}')
511861094cfSGordon Ross        return ', '.join(parts)
512861094cfSGordon Ross
513861094cfSGordon Ross    @staticmethod
514861094cfSGordon Ross    def _build_callargs(atypes):
515861094cfSGordon Ross        """Build actual argument names for the function call, skipping void/empty."""
516861094cfSGordon Ross        return ', '.join(
517861094cfSGordon Ross            f'a{i}' for i, atype in enumerate(atypes)
518861094cfSGordon Ross            if atype not in ('', 'void')
519861094cfSGordon Ross        )
520861094cfSGordon Ross
521861094cfSGordon Ross
522861094cfSGordon Ross# ---------------------------------------------------------------------------
523861094cfSGordon Ross# Job - inputs to a single (symbol, env) compilation
524861094cfSGordon Ross# ---------------------------------------------------------------------------
525861094cfSGordon Ross
526861094cfSGordon Rossclass Job:
527861094cfSGordon Ross    """All inputs needed to run one compilation job in a worker thread."""
528861094cfSGordon Ross
529861094cfSGordon Ross    __slots__ = ('index', 'entry', 'env', 'expect_pass', 'lang',
530861094cfSGordon Ross                 'compiler', 'mflag', 'arch', 'std_flag', 'base_flags',
531181d1aa3SGordon Ross                 'tmpdir', 'debug', 'extra_debug', 'force',
532181d1aa3SGordon Ross                 'output', 'done')
533861094cfSGordon Ross
534861094cfSGordon Ross    def __init__(self, index, entry, env, expect_pass, lang,
535861094cfSGordon Ross                 compiler, mflag, arch, std_flag, base_flags,
536861094cfSGordon Ross                 tmpdir, debug, extra_debug, force):
537861094cfSGordon Ross        self.index       = index
538861094cfSGordon Ross        self.entry       = entry
539861094cfSGordon Ross        self.env         = env
540861094cfSGordon Ross        self.expect_pass = expect_pass
541861094cfSGordon Ross        self.lang        = lang
542861094cfSGordon Ross        self.compiler    = compiler
543861094cfSGordon Ross        self.mflag       = mflag
544861094cfSGordon Ross        self.arch        = arch          # '64-bit' or '32-bit'
545861094cfSGordon Ross        self.std_flag    = std_flag      # e.g. '-std=c99'
546861094cfSGordon Ross        self.base_flags  = base_flags    # list of flags common to all jobs
547861094cfSGordon Ross        self.tmpdir      = tmpdir
548861094cfSGordon Ross        self.debug       = debug         # -d: show probe + compiler output on failure
549f9db9ff7SGordon Ross        self.extra_debug = extra_debug   # -D: also show command + probe on pass
550861094cfSGordon Ross        self.force       = force         # -f: continue after failures
551181d1aa3SGordon Ross        self.output      = None         # buffered text, set once the job finishes
552181d1aa3SGordon Ross        self.done        = False        # True once this job has finished
553861094cfSGordon Ross
554861094cfSGordon Ross
555861094cfSGordon Ross# ---------------------------------------------------------------------------
556861094cfSGordon Ross# TestDriver - builds job list, drives thread pool, reports results
557861094cfSGordon Ross# ---------------------------------------------------------------------------
558861094cfSGordon Ross
559861094cfSGordon Rossclass TestDriver:
560861094cfSGordon Ross    """
561861094cfSGordon Ross    Expands symbol entries × environments into jobs, runs them in a thread
562861094cfSGordon Ross    pool, and reports results.
563861094cfSGordon Ross    """
564861094cfSGordon Ross
565*e1e6b944SGordon Ross    def run(self, sym_config, env_config, positive_coverage,
566*e1e6b944SGordon Ross                compiler, mflag, arch, base_flags, tmpdir, opts):
567861094cfSGordon Ross        """
568861094cfSGordon Ross        Run all (symbol, env) compilations.
569861094cfSGordon Ross
570861094cfSGordon Ross        Returns True if all tests passed (and not interrupted), else False.
571861094cfSGordon Ross        """
572861094cfSGordon Ross        lock = threading.Lock()
573861094cfSGordon Ross        stop = threading.Event()
574861094cfSGordon Ross        counters = {'pass': 0, 'fail': 0}
575181d1aa3SGordon Ross        next_to_print = 0
576861094cfSGordon Ross
577861094cfSGordon Ross        orig_sigint  = signal.getsignal(signal.SIGINT)
578861094cfSGordon Ross        orig_sigterm = signal.getsignal(signal.SIGTERM)
579861094cfSGordon Ross
580861094cfSGordon Ross        def handle_signal(signum, frame):
581861094cfSGordon Ross            stop.set()
582861094cfSGordon Ross
583861094cfSGordon Ross        signal.signal(signal.SIGINT,  handle_signal)
584861094cfSGordon Ross        signal.signal(signal.SIGTERM, handle_signal)
585861094cfSGordon Ross
586181d1aa3SGordon Ross        # drain() prints a run of now-finished jobs starting at
587181d1aa3SGordon Ross        # next_to_print, stopping at the first unfinished job.
588181d1aa3SGordon Ross        # This makes output appears in job (array) order even
589181d1aa3SGordon Ross        # though jobs complete in a non-deterministic order.
590181d1aa3SGordon Ross        # Must be called with lock held.
591181d1aa3SGordon Ross        def drain():
592181d1aa3SGordon Ross            nonlocal next_to_print
593181d1aa3SGordon Ross            while next_to_print < len(jobs) and jobs[next_to_print].done:
594181d1aa3SGordon Ross                if jobs[next_to_print].output:
595181d1aa3SGordon Ross                    print(jobs[next_to_print].output, flush=True)
596181d1aa3SGordon Ross                    jobs[next_to_print].output = None
597181d1aa3SGordon Ross                next_to_print += 1
598181d1aa3SGordon Ross
599861094cfSGordon Ross        # run_job() is the worker function called by each thread in the pool.
600861094cfSGordon Ross        # It captures all output for one job in a local buffer, then acquires
601181d1aa3SGordon Ross        # the lock to record it and drain any now-printable jobs in order.
602861094cfSGordon Ross        def run_job(job):
603861094cfSGordon Ross            if stop.is_set():
604861094cfSGordon Ross                return
605861094cfSGordon Ross
606861094cfSGordon Ross            # Generate probe source and write to temp file.
607861094cfSGordon Ross            src  = ProbeGen.gen_probe(job.entry, job.lang)
608861094cfSGordon Ross            ext  = 'cc' if job.lang == 'c++' else 'c'
609861094cfSGordon Ross            base = os.path.join(job.tmpdir, f'job-{job.index}')
610861094cfSGordon Ross            srcfile = f'{base}.{ext}'
611861094cfSGordon Ross            objfile = f'{base}.o'
612861094cfSGordon Ross            logfile = f'{base}.log'
613861094cfSGordon Ross
614861094cfSGordon Ross            with open(srcfile, 'w') as f:
615861094cfSGordon Ross                f.write(src)
616861094cfSGordon Ross
617861094cfSGordon Ross            # Build compiler command.
618861094cfSGordon Ross            env_defs = job.env.defs.split() if job.env.defs else []
619861094cfSGordon Ross            cmd = ([job.compiler, job.mflag, job.std_flag] +
620861094cfSGordon Ross                   job.base_flags + env_defs +
621861094cfSGordon Ross                   ['-c', srcfile, '-o', objfile])
622861094cfSGordon Ross
623861094cfSGordon Ross            with open(logfile, 'w') as lf:
624861094cfSGordon Ross                proc = subprocess.run(cmd, stdout=lf, stderr=lf)
625861094cfSGordon Ross
626861094cfSGordon Ross            compile_ok = (proc.returncode == 0)
627861094cfSGordon Ross            passed     = (compile_ok == job.expect_pass)
628861094cfSGordon Ross
629861094cfSGordon Ross            # Output buffer (flushed at end of job)
630861094cfSGordon Ross            sign  = '+' if job.expect_pass else '-'
631861094cfSGordon Ross            label = f'{job.entry.symbol} : {sign}{job.env.name} ({job.arch})'
632861094cfSGordon Ross            out   = [f'TEST STARTING {label}:']
633861094cfSGordon Ross
634861094cfSGordon Ross            if job.extra_debug:
635861094cfSGordon Ross                out.append(f'TEST DEBUG {label}: command: {" ".join(cmd)}')
636861094cfSGordon Ross
637f9db9ff7SGordon Ross            if job.extra_debug or (job.debug and not passed):
638861094cfSGordon Ross                out.append(f'TEST DEBUG {label}: probe program:')
639861094cfSGordon Ross                for line in src.splitlines():
640861094cfSGordon Ross                    out.append(f'TEST DEBUG {label}:   {line}')
641f9db9ff7SGordon Ross
642f9db9ff7SGordon Ross            if job.debug and not passed:
643861094cfSGordon Ross                with open(logfile) as lf:
644861094cfSGordon Ross                    cc_out = lf.read().strip()
645861094cfSGordon Ross                if cc_out:
646861094cfSGordon Ross                    out.append(f'TEST DEBUG {label}: compiler output:')
647861094cfSGordon Ross                    for line in cc_out.splitlines():
648861094cfSGordon Ross                        out.append(f'TEST DEBUG {label}:   {line}')
649861094cfSGordon Ross
650861094cfSGordon Ross            if passed:
651861094cfSGordon Ross                out.append(f'TEST PASS: {label}')
652861094cfSGordon Ross            else:
653861094cfSGordon Ross                verb   = 'FAILING' if job.force else 'FAILED'
654861094cfSGordon Ross                reason = (f'error compiling in {job.env.name}'
655861094cfSGordon Ross                          if job.expect_pass
656861094cfSGordon Ross                          else f'symbol visible in {job.env.name}')
657861094cfSGordon Ross                out.append(f'TEST {verb} {label}: {reason}')
658861094cfSGordon Ross
659861094cfSGordon Ross            with lock:
660181d1aa3SGordon Ross                job.output = '\n'.join(out)
661181d1aa3SGordon Ross                job.done   = True
662861094cfSGordon Ross                if passed:
663861094cfSGordon Ross                    counters['pass'] += 1
664861094cfSGordon Ross                else:
665861094cfSGordon Ross                    counters['fail'] += 1
666861094cfSGordon Ross                    if not job.force:
667861094cfSGordon Ross                        stop.set()
668181d1aa3SGordon Ross                drain()
669861094cfSGordon Ross
670861094cfSGordon Ross        # Build the list of jobs to run.
671*e1e6b944SGordon Ross        # Order jobs by configuration entry and then environment name
672861094cfSGordon Ross        jobs = []
673*e1e6b944SGordon Ross
674*e1e6b944SGordon Ross        # First add the synthetic include-only test configurations.
675*e1e6b944SGordon Ross        # These are added when there is no positive test case for some
676*e1e6b944SGordon Ross        # compilation environment.  That ensures that negative cases
677*e1e6b944SGordon Ross        # don't pass by accident if a header does not compile at all.
678*e1e6b944SGordon Ross        #
679*e1e6b944SGordon Ross        # Note that with symbol filtering (opts.sym) we skip this,
680*e1e6b944SGordon Ross        # and with (opts.env) filter the envoronments the same as
681*e1e6b944SGordon Ross        # how normal jobs from the test config would do.
682*e1e6b944SGordon Ross
683*e1e6b944SGordon Ross        if not opts.sym:
684*e1e6b944SGordon Ross            covered = set()
685*e1e6b944SGordon Ross            for entry in sym_config.entries:
686*e1e6b944SGordon Ross                if entry.headers[0] == sym_config.primary_header:
687*e1e6b944SGordon Ross                    _, need_set = env_config.resolve(entry.env_spec)
688*e1e6b944SGordon Ross                    covered |= need_set
689*e1e6b944SGordon Ross            missing = positive_coverage - covered
690*e1e6b944SGordon Ross            if opts.env:
691*e1e6b944SGordon Ross                narrow, _ = env_config.resolve(opts.env)
692*e1e6b944SGordon Ross                missing &= narrow
693*e1e6b944SGordon Ross            entry = SymEntry(
694*e1e6b944SGordon Ross                directive='include-only',
695*e1e6b944SGordon Ross                symbol=f'include-only <{sym_config.primary_header}>',
696*e1e6b944SGordon Ross                env_spec='',
697*e1e6b944SGordon Ross                headers=[sym_config.primary_header],
698*e1e6b944SGordon Ross            )
699*e1e6b944SGordon Ross            for env_name in sorted(missing):
700*e1e6b944SGordon Ross                env = env_config.envs[env_name]
701*e1e6b944SGordon Ross                jobs.append(Job(
702*e1e6b944SGordon Ross                    index=len(jobs),
703*e1e6b944SGordon Ross                    entry=entry,
704*e1e6b944SGordon Ross                    env=env,
705*e1e6b944SGordon Ross                    expect_pass=True,
706*e1e6b944SGordon Ross                    lang=opts.lang,
707*e1e6b944SGordon Ross                    compiler=compiler,
708*e1e6b944SGordon Ross                    mflag=mflag,
709*e1e6b944SGordon Ross                    arch=arch,
710*e1e6b944SGordon Ross                    std_flag=f'-std={env.lang}',
711*e1e6b944SGordon Ross                    base_flags=base_flags,
712*e1e6b944SGordon Ross                    tmpdir=tmpdir,
713*e1e6b944SGordon Ross                    debug=opts.debug,
714*e1e6b944SGordon Ross                    extra_debug=opts.extra_debug,
715*e1e6b944SGordon Ross                    force=opts.force,
716*e1e6b944SGordon Ross                ))
717*e1e6b944SGordon Ross
718*e1e6b944SGordon Ross        #
719*e1e6b944SGordon Ross        # Now add jobs from the normal config file rows.
720*e1e6b944SGordon Ross        # These may be filtered by symbol and/or environment
721*e1e6b944SGordon Ross        # using opts.sym or opts.env
722*e1e6b944SGordon Ross        #
723*e1e6b944SGordon Ross
724*e1e6b944SGordon Ross        for entry in sym_config.entries:
725861094cfSGordon Ross            if opts.sym and entry.symbol != opts.sym:
726861094cfSGordon Ross                continue
727861094cfSGordon Ross            test_set, need_set = env_config.resolve(entry.env_spec)
728861094cfSGordon Ross            if opts.env:
729861094cfSGordon Ross                narrow, _ = env_config.resolve(opts.env)
730861094cfSGordon Ross                test_set  &= narrow
731861094cfSGordon Ross                need_set  &= narrow
732861094cfSGordon Ross            for env_name in sorted(test_set):
733861094cfSGordon Ross                env = env_config.envs[env_name]
734861094cfSGordon Ross                jobs.append(Job(
735861094cfSGordon Ross                    index=len(jobs),
736861094cfSGordon Ross                    entry=entry,
737861094cfSGordon Ross                    env=env,
738861094cfSGordon Ross                    expect_pass=(env_name in need_set),
739861094cfSGordon Ross                    lang=opts.lang,
740861094cfSGordon Ross                    compiler=compiler,
741861094cfSGordon Ross                    mflag=mflag,
742861094cfSGordon Ross                    arch=arch,
743861094cfSGordon Ross                    std_flag=f'-std={env.lang}',
744861094cfSGordon Ross                    base_flags=base_flags,
745861094cfSGordon Ross                    tmpdir=tmpdir,
746861094cfSGordon Ross                    debug=opts.debug,
747861094cfSGordon Ross                    extra_debug=opts.extra_debug,
748861094cfSGordon Ross                    force=opts.force,
749861094cfSGordon Ross                ))
750861094cfSGordon Ross
751861094cfSGordon Ross        # The ThreadPoolExecutor runs up to opts.j worker threads concurrently.
752861094cfSGordon Ross        # executor.submit() queues each job; the pool calls run_job(job) in a
753181d1aa3SGordon Ross        # worker thread.  Exiting the "with" block waits for all thread pool
754181d1aa3SGordon Ross        # executors to finish before proceeding.
755861094cfSGordon Ross        with ThreadPoolExecutor(max_workers=opts.j) as executor:
756861094cfSGordon Ross            for job in jobs:
757861094cfSGordon Ross                executor.submit(run_job, job)
758861094cfSGordon Ross
759181d1aa3SGordon Ross        # All thread pool executors have finished.  If we were interrupted,
760181d1aa3SGordon Ross        # there may be unfinished jobs and there may also be finished jobs
761181d1aa3SGordon Ross        # scattered among those with pending output.  Scan the remainder of
762181d1aa3SGordon Ross        # the jobs list and flush (drain) any jobs with pending output.
763181d1aa3SGordon Ross        with lock:
764181d1aa3SGordon Ross            while next_to_print < len(jobs):
765181d1aa3SGordon Ross                if jobs[next_to_print].done and jobs[next_to_print].output:
766181d1aa3SGordon Ross                    print(jobs[next_to_print].output, flush=True)
767181d1aa3SGordon Ross                    # Could free jobs[].output here but we're
768181d1aa3SGordon Ross                    # about to exit so just skip that work.
769181d1aa3SGordon Ross                next_to_print += 1
770181d1aa3SGordon Ross
771861094cfSGordon Ross        signal.signal(signal.SIGINT,  orig_sigint)
772861094cfSGordon Ross        signal.signal(signal.SIGTERM, orig_sigterm)
773861094cfSGordon Ross
774861094cfSGordon Ross        passes = counters['pass']
775861094cfSGordon Ross        total  = passes + counters['fail']
776861094cfSGordon Ross        if passes == total:
777861094cfSGordon Ross            print(f'TEST SUMMARY: {passes} / {total} (ok)')
778861094cfSGordon Ross        else:
779861094cfSGordon Ross            print(f'TEST SUMMARY: {passes} / {total} ({total - passes} failing)')
780861094cfSGordon Ross
781861094cfSGordon Ross        return counters['fail'] == 0 and not stop.is_set()
782861094cfSGordon Ross
783861094cfSGordon Ross
784861094cfSGordon Ross# ---------------------------------------------------------------------------
785861094cfSGordon Ross# Compiler detection
786861094cfSGordon Ross# ---------------------------------------------------------------------------
787861094cfSGordon Ross
788861094cfSGordon Ross# Exit codes emitted by the compiler-detection probe.
789861094cfSGordon Ross_COMP_STUDIO  = 51
790861094cfSGordon Ross_COMP_CLANG   = 52
791861094cfSGordon Ross_COMP_GCC     = 53
792861094cfSGordon Ross_COMP_UNKNOWN = 99
793861094cfSGordon Ross
794861094cfSGordon Ross# clang defines both __GNUC__ and __clang__, therefore test for
795861094cfSGordon Ross# __clang__ ahead of __GNUC__.
796861094cfSGordon Ross
797861094cfSGordon Ross_C_PROBE_SRC = """\
798861094cfSGordon Ross#include <stdlib.h>
799861094cfSGordon Rossint main(int argc, char **argv) {
800861094cfSGordon Ross#if defined(__SUNPRO_C)
801861094cfSGordon Rossexit(51);
802861094cfSGordon Ross#elif defined(__clang__)
803861094cfSGordon Rossexit(52);
804861094cfSGordon Ross#elif defined(__GNUC__)
805861094cfSGordon Rossexit(53);
806861094cfSGordon Ross#else
807861094cfSGordon Rossexit(99);
808861094cfSGordon Ross#endif
809861094cfSGordon Ross}
810861094cfSGordon Ross"""
811861094cfSGordon Ross
812861094cfSGordon Ross_CXX_PROBE_SRC = """\
813861094cfSGordon Ross#include <cstdlib>
814861094cfSGordon Rossint main(int argc, char **argv) {
815861094cfSGordon Ross#if defined(__SUNPRO_CC)
816861094cfSGordon Rossexit(51);
817861094cfSGordon Ross#elif defined(__clang__)
818861094cfSGordon Rossexit(52);
819861094cfSGordon Ross#elif defined(__GNUC__)
820861094cfSGordon Rossexit(53);
821861094cfSGordon Ross#else
822861094cfSGordon Rossexit(99);
823861094cfSGordon Ross#endif
824861094cfSGordon Ross}
825861094cfSGordon Ross"""
826861094cfSGordon Ross
827f9db9ff7SGordon Rossdef sys_include_dir(root=None):
828f9db9ff7SGordon Ross    """
829f9db9ff7SGordon Ross    Return the system include directory to use for -isystem/-nostdinc
830f9db9ff7SGordon Ross    compiles: '<root>/usr/include' if root is given, else '/usr/include'.
831f9db9ff7SGordon Ross
832f9db9ff7SGordon Ross    root is resolved by the caller from, in order of preference: the -R
833f9db9ff7SGordon Ross    command-line option, the HEADER_TEST_ROOT environment variable, or
834f9db9ff7SGordon Ross    None (meaning the true system root).
835f9db9ff7SGordon Ross    """
836f9db9ff7SGordon Ross    if root:
837f9db9ff7SGordon Ross        return os.path.join(root, 'usr/include')
838f9db9ff7SGordon Ross    return '/usr/include'
839f9db9ff7SGordon Ross
840f9db9ff7SGordon Ross
841f9db9ff7SGordon Rossdef c_base_flags(root=None):
842f9db9ff7SGordon Ross    """
843f9db9ff7SGordon Ross    Base flags used for all C compilations.  We turn off -Wformat-security
844f9db9ff7SGordon Ross    because the auto-generated tests don't pass string literals to printf
845f9db9ff7SGordon Ross    family functions, which will trigger warnings in some compilers (e.g.
846f9db9ff7SGordon Ross    clang-16).
847f9db9ff7SGordon Ross    """
848f9db9ff7SGordon Ross    return [
849861094cfSGordon Ross        '-Wall', '-Werror', '-nostdinc',
850f9db9ff7SGordon Ross        '-isystem', sys_include_dir(root),
851861094cfSGordon Ross        '-Wno-format-security',
852861094cfSGordon Ross    ]
853861094cfSGordon Ross
854861094cfSGordon Ross
855861094cfSGordon Rossdef _run_compiler_probe(compiler, src, ext, mflag, tmpdir):
856861094cfSGordon Ross    """
857861094cfSGordon Ross    Write src to detect.ext, compile with compiler+mflag, run the result.
858861094cfSGordon Ross    Returns the probe exit code, or None on compile/exec failure.
859861094cfSGordon Ross    """
860861094cfSGordon Ross    srcfile   = os.path.join(tmpdir, f'detect.{ext}')
861861094cfSGordon Ross    exec_name = os.path.join(tmpdir, 'detect')
862861094cfSGordon Ross    with open(srcfile, 'w') as f:
863861094cfSGordon Ross        f.write(src)
864861094cfSGordon Ross    try:
865861094cfSGordon Ross        r = subprocess.run(
866861094cfSGordon Ross            [compiler, mflag, srcfile, '-o', exec_name],
867861094cfSGordon Ross            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
868861094cfSGordon Ross        if r.returncode != 0:
869861094cfSGordon Ross            return None
870861094cfSGordon Ross        r = subprocess.run(
871861094cfSGordon Ross            [exec_name],
872861094cfSGordon Ross            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
873861094cfSGordon Ross        return r.returncode
874861094cfSGordon Ross    except OSError:
875861094cfSGordon Ross        return None
876861094cfSGordon Ross
877861094cfSGordon Ross
878861094cfSGordon Rossdef _validate_c_compiler(cc, mflag, tmpdir):
879861094cfSGordon Ross    """Return True if cc is a usable C compiler (gcc or clang); else False."""
880861094cfSGordon Ross    code = _run_compiler_probe(cc, _C_PROBE_SRC, 'c', mflag, tmpdir)
881861094cfSGordon Ross    return code in (_COMP_GCC, _COMP_CLANG)
882861094cfSGordon Ross
883861094cfSGordon Ross
884861094cfSGordon Rossdef _validate_cxx_compiler(cc, mflag, tmpdir):
885861094cfSGordon Ross    """Return True if cc is a usable C++ compiler (g++ or clang++); else False."""
886861094cfSGordon Ross    code = _run_compiler_probe(cc, _CXX_PROBE_SRC, 'cc', mflag, tmpdir)
887861094cfSGordon Ross    return code in (_COMP_GCC, _COMP_CLANG)
888861094cfSGordon Ross
889861094cfSGordon Ross
890861094cfSGordon Rossdef _compiler_kind(compiler):
891861094cfSGordon Ross    """
892861094cfSGordon Ross    Return 'clang' or 'gcc' by inspecting the compiler's --version output.
893861094cfSGordon Ross    Used when an explicit compiler path is given and the kind cannot be
894861094cfSGordon Ross    inferred from the name.
895861094cfSGordon Ross    """
896861094cfSGordon Ross    result = subprocess.run(
897861094cfSGordon Ross        [compiler, '--version'],
898861094cfSGordon Ross        capture_output=True, text=True)
899861094cfSGordon Ross    if 'clang' in result.stdout.lower():
900861094cfSGordon Ross        return 'clang'
901861094cfSGordon Ross    return 'gcc'
902861094cfSGordon Ross
903861094cfSGordon Ross
904861094cfSGordon Rossdef find_c_compiler(mflag, tmpdir, explicit=None):
905861094cfSGordon Ross    """
906861094cfSGordon Ross    Find a usable C compiler.  Returns the compiler path.
907861094cfSGordon Ross    Raises SystemExit if none is found.
908861094cfSGordon Ross    """
909861094cfSGordon Ross    candidates = [explicit] if explicit else ['gcc', 'clang']
910861094cfSGordon Ross    for cc in candidates:
911861094cfSGordon Ross        if _validate_c_compiler(cc, mflag, tmpdir):
912861094cfSGordon Ross            return cc
913861094cfSGordon Ross    if explicit:
914861094cfSGordon Ross        sys.exit(f'error: C compiler {explicit!r} is not usable')
915861094cfSGordon Ross    sys.exit('error: no usable C compiler found (tried gcc, clang)')
916861094cfSGordon Ross
917861094cfSGordon Ross
918861094cfSGordon Rossdef find_cxx_compiler(mflag, tmpdir, explicit=None):
919861094cfSGordon Ross    """
920861094cfSGordon Ross    Find a usable C++ compiler.  Returns (compiler, kind) where kind is
921861094cfSGordon Ross    'gcc' or 'clang'.  Raises SystemExit if none is found.
922861094cfSGordon Ross    """
923861094cfSGordon Ross    if explicit:
924861094cfSGordon Ross        if not _validate_cxx_compiler(explicit, mflag, tmpdir):
925861094cfSGordon Ross            sys.exit(f'error: C++ compiler {explicit!r} is not usable')
926861094cfSGordon Ross        return explicit, _compiler_kind(explicit)
927861094cfSGordon Ross    for cc, kind in [('g++', 'gcc'), ('clang++', 'clang')]:
928861094cfSGordon Ross        if _validate_cxx_compiler(cc, mflag, tmpdir):
929861094cfSGordon Ross            return cc, kind
930861094cfSGordon Ross    sys.exit('error: no usable C++ compiler found (tried g++, clang++)')
931861094cfSGordon Ross
932861094cfSGordon Ross
933f9db9ff7SGordon Rossdef find_gcc_cxx_includes(compiler, root=None):
934861094cfSGordon Ross    """
935861094cfSGordon Ross    Query a GCC C++ compiler for its internal include directory and return a
936861094cfSGordon Ross    base_flags list with all necessary -isystem paths.
937861094cfSGordon Ross
938861094cfSGordon Ross    The compiler reports its internal include directory as, e.g.:
939861094cfSGordon Ross      /opt/gcc-14/lib/gcc/x86_64-pc-solaris2.11/14.2.0/include
940861094cfSGordon Ross
941861094cfSGordon Ross    We parse out prefix, target triple, and version, then build:
942861094cfSGordon Ross      -isystem prefix/include/c++/version
943861094cfSGordon Ross      -isystem prefix/include/c++/version/target
944861094cfSGordon Ross      -isystem prefix/lib/gcc/target/version/include
945f9db9ff7SGordon Ross      -isystem <sys_include_dir>
946f9db9ff7SGordon Ross
947f9db9ff7SGordon Ross    The compiler's own internal C++ headers always come from the real
948f9db9ff7SGordon Ross    toolchain install; only the final system headers entry is redirected
949f9db9ff7SGordon Ross    under root (see sys_include_dir()).
950861094cfSGordon Ross    """
951861094cfSGordon Ross    result = subprocess.run(
952861094cfSGordon Ross        [compiler, '-print-file-name=include'],
953861094cfSGordon Ross        capture_output=True, text=True)
954861094cfSGordon Ross    buf = result.stdout.strip()
955861094cfSGordon Ross
956861094cfSGordon Ross    sep = '/lib/gcc/'
957861094cfSGordon Ross    idx = buf.find(sep)
958861094cfSGordon Ross    if idx < 0:
959861094cfSGordon Ross        sys.exit(f'error: unexpected -print-file-name=include output: {buf!r}')
960861094cfSGordon Ross
961861094cfSGordon Ross    prefix = buf[:idx]
962861094cfSGordon Ross    rest   = buf[idx + len(sep):]   # "target/version/include"
963861094cfSGordon Ross    parts  = rest.split('/')
964861094cfSGordon Ross    if len(parts) < 3:
965861094cfSGordon Ross        sys.exit(f'error: cannot parse target/version from: {buf!r}')
966861094cfSGordon Ross    target  = parts[0]
967861094cfSGordon Ross    version = parts[1]
968861094cfSGordon Ross
969861094cfSGordon Ross    return [
970861094cfSGordon Ross        '-Wall', '-Werror', '-nostdinc',
971861094cfSGordon Ross        '-isystem', f'{prefix}/include/c++/{version}',
972861094cfSGordon Ross        '-isystem', f'{prefix}/include/c++/{version}/{target}',
973861094cfSGordon Ross        '-isystem', f'{prefix}/lib/gcc/{target}/{version}/include',
974f9db9ff7SGordon Ross        '-isystem', sys_include_dir(root),
975861094cfSGordon Ross        '-Wno-format-security',
976861094cfSGordon Ross    ]
977861094cfSGordon Ross
978861094cfSGordon Ross
979f9db9ff7SGordon Rossdef find_clang_cxx_includes(compiler, root=None):
980861094cfSGordon Ross    """
981861094cfSGordon Ross    Query a clang++ compiler for its C++ include search paths by running
982861094cfSGordon Ross    it in preprocessing mode with -v, then parse the include list from
983f9db9ff7SGordon Ross    stderr.  Returns a base_flags list with -isystem for each path found.
984f9db9ff7SGordon Ross
985f9db9ff7SGordon Ross    clang always reports the real /usr/include in this list (it has no
986f9db9ff7SGordon Ross    notion of an alternate root); if root is given, that entry is
987f9db9ff7SGordon Ross    replaced with sys_include_dir(root) rather than appended alongside it,
988f9db9ff7SGordon Ross    so proto headers take precedence instead of conflicting with the
989f9db9ff7SGordon Ross    real ones.
990861094cfSGordon Ross    """
991861094cfSGordon Ross    result = subprocess.run(
992861094cfSGordon Ross        [compiler, '-xc++', '-E', '-v', '-'],
993861094cfSGordon Ross        input='', capture_output=True, text=True)
994861094cfSGordon Ross
995861094cfSGordon Ross    paths = []
996861094cfSGordon Ross    in_list = False
997861094cfSGordon Ross    for line in result.stderr.splitlines():
998861094cfSGordon Ross        if line == '#include <...> search starts here:':
999861094cfSGordon Ross            in_list = True
1000861094cfSGordon Ross        elif line == 'End of search list.':
1001861094cfSGordon Ross            break
1002861094cfSGordon Ross        elif in_list:
1003861094cfSGordon Ross            paths.append(line.strip())
1004861094cfSGordon Ross
1005861094cfSGordon Ross    if not paths:
1006861094cfSGordon Ross        sys.exit(f'error: could not determine C++ include paths from {compiler}')
1007861094cfSGordon Ross
1008f9db9ff7SGordon Ross    sys_dir = sys_include_dir(root)
1009f9db9ff7SGordon Ross    if '/usr/include' in paths:
1010f9db9ff7SGordon Ross        paths = [sys_dir if p == '/usr/include' else p for p in paths]
1011f9db9ff7SGordon Ross    else:
1012f9db9ff7SGordon Ross        paths.append(sys_dir)
1013f9db9ff7SGordon Ross
1014861094cfSGordon Ross    flags = ['-Wall', '-Werror', '-nostdinc']
1015861094cfSGordon Ross    for p in paths:
1016861094cfSGordon Ross        flags += ['-isystem', p]
1017861094cfSGordon Ross    flags.append('-Wno-format-security')
1018861094cfSGordon Ross    return flags
1019861094cfSGordon Ross
1020861094cfSGordon Ross
1021861094cfSGordon Ross# ---------------------------------------------------------------------------
1022861094cfSGordon Ross# Argument parsing and main
1023861094cfSGordon Ross# ---------------------------------------------------------------------------
1024861094cfSGordon Ross
1025861094cfSGordon Rossdef _parse_args():
1026861094cfSGordon Ross    p = argparse.ArgumentParser(
1027861094cfSGordon Ross        description='Test C/C++ symbol visibility in system headers.')
1028861094cfSGordon Ross
1029861094cfSGordon Ross    p.add_argument('--lang', required=True, choices=('c', 'c++'),
1030861094cfSGordon Ross                   help='Language to test')
1031861094cfSGordon Ross
1032861094cfSGordon Ross    bits = p.add_mutually_exclusive_group(required=True)
1033861094cfSGordon Ross    bits.add_argument('-m64', dest='mflag', action='store_const', const='-m64',
1034861094cfSGordon Ross                      help='Compile for 64-bit')
1035861094cfSGordon Ross    bits.add_argument('-m32', dest='mflag', action='store_const', const='-m32',
1036861094cfSGordon Ross                      help='Compile for 32-bit')
1037861094cfSGordon Ross
1038861094cfSGordon Ross    p.add_argument('-c', dest='compiler', metavar='COMPILER', default=None,
1039861094cfSGordon Ross                   help='Explicit compiler path')
1040861094cfSGordon Ross    p.add_argument('-d', dest='debug', action='store_true',
1041861094cfSGordon Ross                   help='Show probe and compiler output on failure')
1042861094cfSGordon Ross    p.add_argument('-D', dest='extra_debug', action='store_true',
1043f9db9ff7SGordon Ross                   help='Also show compiler command and probe program for '
1044f9db9ff7SGordon Ross                        'every test, not just failures (implies -d)')
1045861094cfSGordon Ross    p.add_argument('-e', dest='env', metavar='ENV', default=None,
1046861094cfSGordon Ross                   help='Narrow to one environment name')
1047861094cfSGordon Ross    p.add_argument('-f', dest='force', action='store_true',
1048861094cfSGordon Ross                   help='Continue after failures')
1049861094cfSGordon Ross    p.add_argument('-j', dest='j', metavar='N', type=int, default=None,
1050861094cfSGordon Ross                   help='Number of parallel jobs (default: SYMBOL_TEST_JOBS or 4)')
1051*e1e6b944SGordon Ross    p.add_argument('--positive-coverage', metavar='NAME', default=None,
1052*e1e6b944SGordon Ross                   help='Ensure an expected-success test compiles the primary '
1053*e1e6b944SGordon Ross                        'header in every environment named by NAME; by default '
1054*e1e6b944SGordon Ross                        'all declared environments require positive coverage')
1055861094cfSGordon Ross    p.add_argument('-s', dest='sym', metavar='SYM', default=None,
1056861094cfSGordon Ross                   help='Narrow to one symbol name')
1057861094cfSGordon Ross
1058861094cfSGordon Ross    p.add_argument('-C', dest='compiler_check', action='store_true',
1059861094cfSGordon Ross                   help='Check compiler only, do not run tests')
1060861094cfSGordon Ross
1061f9db9ff7SGordon Ross    p.add_argument('-R', dest='root', metavar='ROOT', default=None,
1062f9db9ff7SGordon Ross                   help='Alternate root directory (e.g. a proto area) whose '
1063f9db9ff7SGordon Ross                        'ROOT/usr/include is tested instead of the default '
1064f9db9ff7SGordon Ross                        '(default: $HEADER_TEST_ROOT/usr/include if that '
1065f9db9ff7SGordon Ross                        'environment variable is set, else /usr/include)')
1066f9db9ff7SGordon Ross
1067861094cfSGordon Ross    p.add_argument('env_cfg', nargs='?', help='Environment config file')
1068861094cfSGordon Ross    p.add_argument('sym_cfgs', nargs='*', metavar='sym_cfg',
1069861094cfSGordon Ross                   help='One or more symbols config files')
1070861094cfSGordon Ross
1071861094cfSGordon Ross    args = p.parse_args()
1072861094cfSGordon Ross    if args.extra_debug:
1073861094cfSGordon Ross        args.debug = True
1074861094cfSGordon Ross    jobs = 4
1075861094cfSGordon Ross    env_jobs = os.environ.get('SYMBOL_TEST_JOBS')
1076861094cfSGordon Ross    if env_jobs is not None:
1077861094cfSGordon Ross        jobs = int(env_jobs)
1078861094cfSGordon Ross    if args.j is not None:
1079861094cfSGordon Ross        jobs = args.j
1080861094cfSGordon Ross    args.j = jobs
1081f9db9ff7SGordon Ross    if args.root is None:
1082f9db9ff7SGordon Ross        args.root = os.environ.get('HEADER_TEST_ROOT')
1083861094cfSGordon Ross    if not args.compiler_check and not args.env_cfg:
1084861094cfSGordon Ross        p.error('env_cfg is required unless -C is specified')
1085861094cfSGordon Ross    if not args.compiler_check and not args.sym_cfgs:
1086861094cfSGordon Ross        p.error('at least one sym_cfg is required unless -C is specified')
1087861094cfSGordon Ross    return args
1088861094cfSGordon Ross
1089861094cfSGordon Ross
1090861094cfSGordon Rossdef main():
1091861094cfSGordon Ross    args = _parse_args()
1092861094cfSGordon Ross    mflag = args.mflag
1093861094cfSGordon Ross    arch  = '64-bit' if mflag == '-m64' else '32-bit'
1094861094cfSGordon Ross
1095861094cfSGordon Ross    with tempfile.TemporaryDirectory() as tmpdir:
1096861094cfSGordon Ross        if args.lang == 'c':
1097861094cfSGordon Ross            compiler   = find_c_compiler(mflag, tmpdir, args.compiler)
1098f9db9ff7SGordon Ross            base_flags = c_base_flags(args.root)
1099861094cfSGordon Ross        else:
1100861094cfSGordon Ross            # kind is 'gcc' or 'clang', used to select the right
1101861094cfSGordon Ross            # include path discovery method.
1102861094cfSGordon Ross            compiler, kind = find_cxx_compiler(mflag, tmpdir, args.compiler)
1103861094cfSGordon Ross            if kind == 'gcc':
1104f9db9ff7SGordon Ross                base_flags = find_gcc_cxx_includes(compiler, args.root)
1105861094cfSGordon Ross            else:
1106f9db9ff7SGordon Ross                base_flags = find_clang_cxx_includes(compiler, args.root)
1107861094cfSGordon Ross
1108861094cfSGordon Ross        if args.compiler_check:
1109861094cfSGordon Ross            sys.exit(0)
1110861094cfSGordon Ross
1111861094cfSGordon Ross        env_cfg = EnvConfig(args.lang)
1112861094cfSGordon Ross        env_cfg.load(args.env_cfg)
1113861094cfSGordon Ross
1114861094cfSGordon Ross        sym_cfg = SymConfig()
1115861094cfSGordon Ross        for path in args.sym_cfgs:
1116861094cfSGordon Ross            sym_cfg.load(path)
1117861094cfSGordon Ross
1118*e1e6b944SGordon Ross        # Named groups may intentionally omit declared environments, so the
1119*e1e6b944SGordon Ross        # default is the complete environment configuration.  One could
1120*e1e6b944SGordon Ross        # also make "ALL" the default but that would encode expectations
1121*e1e6b944SGordon Ross        # on that being defined in the environment config file.
1122*e1e6b944SGordon Ross        if args.positive_coverage is None:
1123*e1e6b944SGordon Ross            positive_coverage = set(env_cfg.envs)
1124*e1e6b944SGordon Ross        else:
1125*e1e6b944SGordon Ross            positive_coverage = env_cfg.expand(args.positive_coverage)
1126*e1e6b944SGordon Ross
1127861094cfSGordon Ross        ok = TestDriver().run(
1128*e1e6b944SGordon Ross            sym_cfg, env_cfg, positive_coverage, compiler,
1129861094cfSGordon Ross            mflag, arch, base_flags, tmpdir, args)
1130861094cfSGordon Ross
1131861094cfSGordon Ross    sys.exit(0 if ok else 1)
1132861094cfSGordon Ross
1133861094cfSGordon Ross
1134861094cfSGordon Rossif __name__ == '__main__':
1135861094cfSGordon Ross    main()
1136