1165 lines
48 KiB
Python
1165 lines
48 KiB
Python
#!/usr/bin/python3
|
|
# Generate testcase files and Makefile fragments for DSO sorting test
|
|
# Copyright (C) 2021-2022 Free Software Foundation, Inc.
|
|
# This file is part of the GNU C Library.
|
|
#
|
|
# The GNU C Library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# The GNU C Library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with the GNU C Library; if not, see
|
|
# <http://www.gnu.org/licenses/>.
|
|
|
|
"""Generate testcase files and Makefile fragments for DSO sorting test
|
|
|
|
This script takes a small description string language, and generates
|
|
testcases for displaying the ELF dynamic linker's dependency sorting
|
|
behavior, allowing verification.
|
|
|
|
Testcase descriptions are semicolon-separated description strings, and
|
|
this tool generates a testcase from the description, including main program,
|
|
associated modules, and Makefile fragments for including into elf/Makefile.
|
|
|
|
This allows automation of what otherwise would be very laborous manual
|
|
construction of complex dependency cases, however it must be noted that this
|
|
is only a tool to speed up testcase construction, and thus the generation
|
|
features are largely mechanical in nature; inconsistencies or errors may occur
|
|
if the input description was itself erroneous or have unforeseen interactions.
|
|
|
|
The format of the input test description files are:
|
|
|
|
# Each test description has a name, lines of description,
|
|
# and an expected output specification. Comments use '#'.
|
|
testname1: <test-description-line>
|
|
output: <expected-output-string>
|
|
|
|
# Tests can be marked to be XFAIL by using 'xfail_output' instead
|
|
testname2: <test-description-line>
|
|
xfail_output: <expected-output-string>
|
|
|
|
# A default set of GLIBC_TUNABLES tunables can be specified, for which
|
|
# all following tests will run multiple times, once for each of the
|
|
# GLIBC_TUNABLES=... strings set by the 'tunable_option' command.
|
|
tunable_option: <glibc-tunable-string1>
|
|
tunable_option: <glibc-tunable-string2>
|
|
|
|
# Test descriptions can use multiple lines, which will all be merged
|
|
# together, so order is not important.
|
|
testname3: <test-description-line>
|
|
<test-description-line>
|
|
<test-description-line>
|
|
...
|
|
output: <expected-output-string>
|
|
|
|
# 'testname3' will be run and compared two times, for both
|
|
# GLIBC_TUNABLES=<glibc-tunable-string1> and
|
|
# GLIBC_TUNABLES=<glibc-tunable-string2>. This can be cleared and reset by the
|
|
# 'clear_tunables' command:
|
|
clear_tunables
|
|
|
|
# Multiple expected outputs can also be specified, with an associated
|
|
# tunable option in (), which multiple tests will be run with each
|
|
# GLIBC_TUNABLES=... option tried.
|
|
testname4:
|
|
<test-description-line>
|
|
...
|
|
output(<glibc-tunable-string1>): <expected-output-string-1>
|
|
output(<glibc-tunable-string2>): <expected-output-string-2>
|
|
# Individual tunable output cases can be XFAILed, though note that
|
|
# this will have the effect of XFAILing the entire 'testname4' test
|
|
# in the final top-level tests.sum summary.
|
|
xfail_output(<glibc-tunable-string3>): <expected-output-string-3>
|
|
|
|
# When multiple outputs (with specific tunable strings) are specified,
|
|
# these take priority over any active 'tunable_option' settings.
|
|
|
|
# When a test is meant to be placed under 'xtests' (not run under
|
|
# "make check", but only when "make xtests" is used), the testcase name can be
|
|
# declared using 'xtest(<test-name>)':
|
|
...
|
|
xtest(test-too-big1): <test-description>
|
|
output: <expected-output-string>
|
|
...
|
|
|
|
# Do note that under current elf/Makefile organization, for such a xtest case,
|
|
# while the test execution is only run under 'make xtests', the associated
|
|
# DSOs are always built even under 'make check'.
|
|
|
|
On the description language used, an example description line string:
|
|
|
|
a->b!->[cdef];c=>g=>h;{+c;%c;-c}->a
|
|
|
|
Each identifier represents a shared object module, currently sequences of
|
|
letters/digits are allowed, case-sensitive.
|
|
|
|
All such shared objects have a constructor/destructor generated for them
|
|
that emits its name followed by a '>' for constructors, and '<' followed by
|
|
its name for destructors, e.g. if the name is 'obj1', then "obj1>" and "<obj1"
|
|
is printed by its constructor/destructor respectively.
|
|
|
|
The -> operator specifies a link time dependency, these can be chained for
|
|
convenience (e.g. a->b->c->d).
|
|
|
|
The => operator creates a call-reference, e.g. for a=>b, an fn_a() function
|
|
is created inside module 'a', which calls fn_b() in module 'b'.
|
|
These module functions emit 'name()' output in nested form,
|
|
e.g. a=>b emits 'a(b())'
|
|
|
|
For single character object names, square brackets [] in the description
|
|
allows specifying multiple objects; e.g. a->[bcd]->e is equivalent to
|
|
a->b->e;a->c->e;a->d->e
|
|
|
|
The () parenthesis construct with space separated names is also allowed for
|
|
specifying objects. For names with integer suffixes a range can also be used,
|
|
e.g. (foo1 bar2-5), specifies DSOs foo1, bar2, bar2, bar3, bar4, bar5.
|
|
|
|
A {} construct specifies the main test program, and its link dependencies
|
|
are also specified using ->. Inside {}, a few ;-separated constructs are
|
|
allowed:
|
|
+a Loads module a using dlopen(RTLD_LAZY|RTLD_GLOBAL)
|
|
^a Loads module a using dlopen(RTLD_LAZY)
|
|
%a Use dlsym() to load and call fn_a()
|
|
@a Calls fn_a() directly.
|
|
-a Unloads module a using dlclose()
|
|
|
|
The generated main program outputs '{' '}' with all output from above
|
|
constructs in between. The other output before/after {} are the ordered
|
|
constructor/destructor output.
|
|
|
|
If no {} construct is present, a default empty main program is linked
|
|
against all objects which have no dependency linked to it. e.g. for
|
|
'[ab]->c;d->e', the default main program is equivalent to '{}->[abd]'
|
|
|
|
Sometimes for very complex or large testcases, besides specifying a
|
|
few explicit dependencies from main{}, the above default dependency
|
|
behavior is still useful to automatically have, but is turned off
|
|
upon specifying a single explicit {}->dso_name.
|
|
In this case, add {}->* to explicitly add this generation behavior:
|
|
|
|
# Main program links to 'foo', and all other objects which have no
|
|
# dependency linked to it.
|
|
{}->foo,{}->*
|
|
|
|
Note that '*' works not only on main{}, but can be used as the
|
|
dependency target of any object. Note that it only works as a target,
|
|
not a dependency source.
|
|
|
|
The '!' operator after object names turns on permutation of its
|
|
dependencies, e.g. while a->[bcd] only generates one set of objects,
|
|
with 'a.so' built with a link line of "b.so c.so d.so", for a!->[bcd]
|
|
permutations of a's dependencies creates multiple testcases with
|
|
different link line orders: "b.so c.so d.so", "c.so b.so d.so",
|
|
"b.so d.so c.so", etc. Note that for a <test-name> specified on
|
|
the script command-line, multiple <test-name_1>, <test-name_2>, etc.
|
|
tests will be generated (e.g. for a!->[bc]!->[de], eight tests with
|
|
different link orders for a, b, and c will be generated)
|
|
|
|
It is possible to specify the ELF soname field for an object or the
|
|
main program:
|
|
# DSO 'a' will be linked with the appropriate -Wl,-soname=x setting
|
|
a->b->c;soname(a)=x
|
|
# The the main program can also have a soname specified
|
|
soname({})=y
|
|
|
|
This can be used to test how ld.so behaves when objects and/or the
|
|
main program have such a field set.
|
|
|
|
|
|
Strings Output by Generated Testcase Programs
|
|
|
|
The text output produced by a generated testcase consists of three main
|
|
parts:
|
|
1. The constructors' output
|
|
2. Output from the main program
|
|
3. Destructors' output
|
|
|
|
To see by example, a simple test description "a->b->c" generates a testcase
|
|
that when run, outputs: "c>b>a>{}<a<b<c"
|
|
|
|
Each generated DSO constructor prints its name followed by a '>' character,
|
|
and the "c>b>a" part above is the full constructor output by all DSOs, the
|
|
order indicating that DSO 'c', which does not depend on any other DSO, has
|
|
its constructor run first, followed by 'b' and then 'a'.
|
|
|
|
Destructor output for each DSO is a '<' character followed by its name,
|
|
reflecting its reverse nature of constructors. In the above example, the
|
|
destructor output part is "<a<b<c".
|
|
|
|
The middle "{}" part is the main program. In this simple example, nothing
|
|
was specified for the main program, so by default it is implicitly linked
|
|
to the DSO 'a' (with no other DSOs depending on it) and only prints the
|
|
brackets {} with no actions inside.
|
|
|
|
To see an example with actions inside the main program, lets see an example
|
|
description: c->g=>h;{+c;%c;-c}->a->h
|
|
|
|
This produces a testcase, that when executed outputs:
|
|
h>a>{+c[g>c>];%c();-c[<c<g];}<a<h
|
|
|
|
The constructor and destructor parts display the a->h dependency as expected.
|
|
Inside the main program, the "+c" action triggers a dlopen() of DSO 'c',
|
|
causing another chain of constructors "g>c>" to be triggered. Here it is
|
|
displayed inside [] brackets for each dlopen call. The same is done for "-c",
|
|
a dlclose() of 'c'.
|
|
|
|
The "%c" output is due to calling to fn_c() inside DSO 'c', this comprises
|
|
of two parts: the '%' character is printed by the caller, here it is the main
|
|
program. The 'c' character is printed from inside fn_c(). The '%' character
|
|
indicates that this is called by a dlsym() of "fn_c". A '@' character would
|
|
mean a direct call (with a symbol reference). These can all be controlled
|
|
by the main test program constructs documented earlier.
|
|
|
|
The output strings described here is the exact same form placed in
|
|
test description files' "output: <expected output>" line.
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
import os
|
|
import subprocess
|
|
import argparse
|
|
from collections import OrderedDict
|
|
import itertools
|
|
|
|
# BUILD_GCC is only used under the --build option,
|
|
# which builds the generated testcase, including DSOs using BUILD_GCC.
|
|
# Mainly for testing purposes, especially debugging of this script,
|
|
# and can be changed here to another toolchain path if needed.
|
|
build_gcc = "gcc"
|
|
|
|
def get_parser():
|
|
parser = argparse.ArgumentParser("")
|
|
parser.add_argument("description",
|
|
help="Description string of DSO dependency test to be "
|
|
"generated (see script source for documentation of "
|
|
"description language), either specified here as "
|
|
"command line argument, or by input file using "
|
|
"-f/--description-file option",
|
|
nargs="?", default="")
|
|
parser.add_argument("test_name",
|
|
help="Identifier for testcase being generated",
|
|
nargs="?", default="")
|
|
parser.add_argument("--objpfx",
|
|
help="Path to place generated files, defaults to "
|
|
"current directory if none specified",
|
|
nargs="?", default="./")
|
|
parser.add_argument("-m", "--output-makefile",
|
|
help="File to write Makefile fragment to, defaults to "
|
|
"stdout when option not present",
|
|
nargs="?", default="")
|
|
parser.add_argument("-f", "--description-file",
|
|
help="Input file containing testcase descriptions",
|
|
nargs="?", default="")
|
|
parser.add_argument("--build", help="After C testcase generated, build it "
|
|
"using gcc (for manual testing purposes)",
|
|
action="store_true")
|
|
parser.add_argument("--debug-output",
|
|
help="Prints some internal data "
|
|
"structures; used for debugging of this script",
|
|
action="store_true")
|
|
return parser
|
|
|
|
# Main script starts here.
|
|
cmdlineargs = get_parser().parse_args()
|
|
test_name = cmdlineargs.test_name
|
|
description = cmdlineargs.description
|
|
objpfx = cmdlineargs.objpfx
|
|
description_file = cmdlineargs.description_file
|
|
output_makefile = cmdlineargs.output_makefile
|
|
makefile = ""
|
|
default_tunable_options = []
|
|
|
|
current_input_lineno = 0
|
|
def error(msg):
|
|
global current_input_lineno
|
|
print("Error: %s%s" % ((("Line %d, " % current_input_lineno)
|
|
if current_input_lineno != 0 else ""),
|
|
msg))
|
|
exit(1)
|
|
|
|
if(test_name or description) and description_file:
|
|
error("both command-line testcase and input file specified")
|
|
if test_name and not description:
|
|
error("command-line testcase name without description string")
|
|
|
|
# Main class type describing a testcase.
|
|
class TestDescr:
|
|
def __init__(self):
|
|
self.objs = [] # list of all DSO objects
|
|
self.deps = OrderedDict() # map of DSO object -> list of dependencies
|
|
|
|
# map of DSO object -> list of call refs
|
|
self.callrefs = OrderedDict()
|
|
|
|
# map of DSO object -> list of permutations of dependencies
|
|
self.dep_permutations = OrderedDict()
|
|
|
|
# map of DSO object -> SONAME of object (if one is specified)
|
|
self.soname_map = OrderedDict()
|
|
|
|
# list of main program operations
|
|
self.main_program = []
|
|
# set if default dependencies added to main
|
|
self.main_program_default_deps = True
|
|
|
|
self.test_name = "" # name of testcase
|
|
self.expected_outputs = OrderedDict() # expected outputs of testcase
|
|
self.xfail = False # set if this is a XFAIL testcase
|
|
self.xtest = False # set if this is put under 'xtests'
|
|
|
|
# Add 'object -> [object, object, ...]' relations to CURR_MAP
|
|
def __add_deps_internal(self, src_objs, dst_objs, curr_map):
|
|
for src in src_objs:
|
|
for dst in dst_objs:
|
|
if not src in curr_map:
|
|
curr_map[src] = []
|
|
if not dst in curr_map[src]:
|
|
curr_map[src].append(dst)
|
|
def add_deps(self, src_objs, dst_objs):
|
|
self.__add_deps_internal(src_objs, dst_objs, self.deps)
|
|
def add_callrefs(self, src_objs, dst_objs):
|
|
self.__add_deps_internal(src_objs, dst_objs, self.callrefs)
|
|
|
|
# Process commands inside the {} construct.
|
|
# Note that throughout this script, the main program object is represented
|
|
# by the '#' string.
|
|
def process_main_program(test_descr, mainprog_str):
|
|
if mainprog_str:
|
|
test_descr.main_program = mainprog_str.split(';')
|
|
for s in test_descr.main_program:
|
|
m = re.match(r"^([+\-%^@])([0-9a-zA-Z]+)$", s)
|
|
if not m:
|
|
error("'%s' is not recognized main program operation" % (s))
|
|
opr = m.group(1)
|
|
obj = m.group(2)
|
|
if not obj in test_descr.objs:
|
|
test_descr.objs.append(obj)
|
|
if opr == '%' or opr == '@':
|
|
test_descr.add_callrefs(['#'], [obj])
|
|
# We have a main program specified, turn this off
|
|
test_descr.main_program_default_deps = False
|
|
|
|
# For(a1 a2 b1-12) object set descriptions, expand into an object list
|
|
def expand_object_set_string(descr_str):
|
|
obj_list = []
|
|
descr_list = descr_str.split()
|
|
for descr in descr_list:
|
|
m = re.match(r"^([a-zA-Z][0-9a-zA-Z]*)(-[0-9]+)?$", descr)
|
|
if not m:
|
|
error("'%s' is not a valid object set description" % (descr))
|
|
obj = m.group(1)
|
|
idx_end = m.group(2)
|
|
if not idx_end:
|
|
if not obj in obj_list:
|
|
obj_list.append(obj)
|
|
else:
|
|
idx_end = int(idx_end[1:])
|
|
m = re.match(r"^([0-9a-zA-Z][a-zA-Z]*)([0-9]+)$", obj)
|
|
if not m:
|
|
error("object description '%s' is malformed" % (obj))
|
|
obj_name = m.group(1)
|
|
idx_start = int(m.group (2))
|
|
if idx_start > idx_end:
|
|
error("index range %s-%s invalid" % (idx_start, idx_end))
|
|
for i in range(idx_start, idx_end + 1):
|
|
o = obj_name + str(i)
|
|
if not o in obj_list:
|
|
obj_list.append(o)
|
|
return obj_list
|
|
|
|
# Lexer for tokens
|
|
tokenspec = [ ("SONAME", r"soname\(([0-9a-zA-Z{}]+)\)=([0-9a-zA-Z]+)"),
|
|
("OBJ", r"([0-9a-zA-Z]+)"),
|
|
("DEP", r"->"),
|
|
("CALLREF", r"=>"),
|
|
("OBJSET", r"\[([0-9a-zA-Z]+)\]"),
|
|
("OBJSET2", r"\(([0-9a-zA-Z \-]+)\)"),
|
|
("OBJSET3", r"\*"),
|
|
("PROG", r"{([0-9a-zA-Z;+^\-%@]*)}"),
|
|
("PERMUTE", r"!"),
|
|
("SEMICOL", r";"),
|
|
("ERROR", r".") ]
|
|
tok_re = '|'.join('(?P<%s>%s)' % pair for pair in tokenspec)
|
|
|
|
# Main line parser of description language
|
|
def parse_description_string(t, descr_str):
|
|
# State used when parsing dependencies
|
|
curr_objs = []
|
|
in_dep = False
|
|
in_callref = False
|
|
def clear_dep_state():
|
|
nonlocal in_dep, in_callref
|
|
in_dep = in_callref = False
|
|
|
|
for m in re.finditer(tok_re, descr_str):
|
|
kind = m.lastgroup
|
|
value = m.group()
|
|
if kind == "SONAME":
|
|
s = re.match(r"soname\(([0-9a-zA-Z{}]+)\)=([0-9a-zA-Z]+)", value)
|
|
obj = s.group(1)
|
|
val = s.group(2)
|
|
if obj == "{}":
|
|
if '#' in t.soname_map:
|
|
error("soname of main program already set")
|
|
# Adjust to internal name
|
|
obj = '#'
|
|
else:
|
|
if re.match(r"[{}]", obj):
|
|
error("invalid object name '%s'" % (obj))
|
|
if not obj in t.objs:
|
|
error("'%s' is not name of already defined object" % (obj))
|
|
if obj in t.soname_map:
|
|
error("'%s' already has soname of '%s' set"
|
|
% (obj, t.soname_map[obj]))
|
|
t.soname_map[obj] = val
|
|
|
|
elif kind == "OBJ":
|
|
if in_dep:
|
|
t.add_deps(curr_objs, [value])
|
|
elif in_callref:
|
|
t.add_callrefs(curr_objs, [value])
|
|
clear_dep_state()
|
|
curr_objs = [value]
|
|
if not value in t.objs:
|
|
t.objs.append(value)
|
|
|
|
elif kind == "OBJSET":
|
|
objset = value[1:len(value)-1]
|
|
if in_dep:
|
|
t.add_deps(curr_objs, list (objset))
|
|
elif in_callref:
|
|
t.add_callrefs(curr_objs, list (objset))
|
|
clear_dep_state()
|
|
curr_objs = list(objset)
|
|
for o in list(objset):
|
|
if not o in t.objs:
|
|
t.objs.append(o)
|
|
|
|
elif kind == "OBJSET2":
|
|
descr_str = value[1:len(value)-1]
|
|
descr_str.strip()
|
|
objs = expand_object_set_string(descr_str)
|
|
if not objs:
|
|
error("empty object set '%s'" % (value))
|
|
if in_dep:
|
|
t.add_deps(curr_objs, objs)
|
|
elif in_callref:
|
|
t.add_callrefs(curr_objs, objs)
|
|
clear_dep_state()
|
|
curr_objs = objs
|
|
for o in objs:
|
|
if not o in t.objs:
|
|
t.objs.append(o)
|
|
|
|
elif kind == "OBJSET3":
|
|
if in_dep:
|
|
t.add_deps(curr_objs, ['*'])
|
|
elif in_callref:
|
|
t.add_callrefs(curr_objs, ['*'])
|
|
else:
|
|
error("non-dependence target set '*' can only be used "
|
|
"as target of ->/=> operations")
|
|
clear_dep_state()
|
|
curr_objs = ['*']
|
|
|
|
elif kind == "PERMUTE":
|
|
if in_dep or in_callref:
|
|
error("syntax error, permute operation invalid here")
|
|
if not curr_objs:
|
|
error("syntax error, no objects to permute here")
|
|
|
|
for obj in curr_objs:
|
|
if not obj in t.dep_permutations:
|
|
# Signal this object has permuted dependencies
|
|
t.dep_permutations[obj] = []
|
|
|
|
elif kind == "PROG":
|
|
if t.main_program:
|
|
error("cannot have more than one main program")
|
|
if in_dep:
|
|
error("objects cannot have dependency on main program")
|
|
if in_callref:
|
|
# TODO: A DSO can resolve to a symbol in the main binary,
|
|
# which we syntactically allow here, but haven't yet
|
|
# implemented.
|
|
t.add_callrefs(curr_objs, ["#"])
|
|
process_main_program(t, value[1:len(value)-1])
|
|
clear_dep_state()
|
|
curr_objs = ["#"]
|
|
|
|
elif kind == "DEP":
|
|
if in_dep or in_callref:
|
|
error("syntax error, multiple contiguous ->,=> operations")
|
|
if '*' in curr_objs:
|
|
error("non-dependence target set '*' can only be used "
|
|
"as target of ->/=> operations")
|
|
in_dep = True
|
|
|
|
elif kind == "CALLREF":
|
|
if in_dep or in_callref:
|
|
error("syntax error, multiple contiguous ->,=> operations")
|
|
if '*' in curr_objs:
|
|
error("non-dependence target set '*' can only be used "
|
|
"as target of ->/=> operations")
|
|
in_callref = True
|
|
|
|
elif kind == "SEMICOL":
|
|
curr_objs = []
|
|
clear_dep_state()
|
|
|
|
else:
|
|
error("unknown token '%s'" % (value))
|
|
return t
|
|
|
|
# Main routine to process each testcase description
|
|
def process_testcase(t):
|
|
global objpfx
|
|
assert t.test_name
|
|
|
|
base_test_name = t.test_name
|
|
test_subdir = base_test_name + "-dir"
|
|
testpfx = objpfx + test_subdir + "/"
|
|
test_srcdir = "dso-sort-tests-src/"
|
|
testpfx_src = objpfx + test_srcdir
|
|
|
|
if not os.path.exists(testpfx):
|
|
os.mkdir(testpfx)
|
|
if not os.path.exists(testpfx_src):
|
|
os.mkdir(testpfx_src)
|
|
|
|
def find_objs_not_depended_on(t):
|
|
objs_not_depended_on = []
|
|
for obj in t.objs:
|
|
skip = False
|
|
for r in t.deps.items():
|
|
if obj in r[1]:
|
|
skip = True
|
|
break
|
|
if not skip:
|
|
objs_not_depended_on.append(obj)
|
|
return objs_not_depended_on
|
|
|
|
non_dep_tgt_objs = find_objs_not_depended_on(t)
|
|
for obj in t.objs:
|
|
if obj in t.deps:
|
|
deps = t.deps[obj]
|
|
if '*' in deps:
|
|
deps.remove('*')
|
|
t.add_deps([obj], non_dep_tgt_objs)
|
|
if obj in t.callrefs:
|
|
deps = t.callrefs[obj]
|
|
if '*' in deps:
|
|
deps.remove('*')
|
|
t.add_callrefs([obj], non_dep_tgt_objs)
|
|
if "#" in t.deps:
|
|
deps = t.deps["#"]
|
|
if '*' in deps:
|
|
deps.remove('*')
|
|
t.add_deps(["#"], non_dep_tgt_objs)
|
|
|
|
# If no main program was specified in dependency description, make a
|
|
# default main program with deps pointing to all DSOs which are not
|
|
# depended by another DSO.
|
|
if t.main_program_default_deps:
|
|
main_deps = non_dep_tgt_objs
|
|
if not main_deps:
|
|
error("no objects for default main program to point "
|
|
"dependency to(all objects strongly connected?)")
|
|
t.add_deps(["#"], main_deps)
|
|
|
|
# Some debug output
|
|
if cmdlineargs.debug_output:
|
|
print("Testcase: %s" % (t.test_name))
|
|
print("All objects: %s" % (t.objs))
|
|
print("--- Static link dependencies ---")
|
|
for r in t.deps.items():
|
|
print("%s -> %s" % (r[0], r[1]))
|
|
print("--- Objects whose dependencies are to be permuted ---")
|
|
for r in t.dep_permutations.items():
|
|
print("%s" % (r[0]))
|
|
print("--- Call reference dependencies ---")
|
|
for r in t.callrefs.items():
|
|
print("%s => %s" % (r[0], r[1]))
|
|
print("--- main program ---")
|
|
print(t.main_program)
|
|
|
|
# Main testcase generation routine, does Makefile fragment generation,
|
|
# testcase source generation, and if --build specified builds testcase.
|
|
def generate_testcase(test_descr, test_suffix):
|
|
|
|
test_name = test_descr.test_name + test_suffix
|
|
|
|
# Print out needed Makefile fragments for use in glibc/elf/Makefile.
|
|
module_names = ""
|
|
for o in test_descr.objs:
|
|
rule = ("$(objpfx)" + test_subdir + "/" + test_name
|
|
+ "-" + o + ".os: $(objpfx)" + test_srcdir
|
|
+ test_name + "-" + o + ".c\n"
|
|
"\t$(compile.c) $(OUTPUT_OPTION)\n")
|
|
makefile.write (rule)
|
|
module_names += " " + test_subdir + "/" + test_name + "-" + o
|
|
makefile.write("modules-names +=%s\n" % (module_names))
|
|
|
|
# Depth-first traversal, executing FN(OBJ) in post-order
|
|
def dfs(t, fn):
|
|
def dfs_rec(obj, fn, obj_visited):
|
|
if obj in obj_visited:
|
|
return
|
|
obj_visited[obj] = True
|
|
if obj in t.deps:
|
|
for dep in t.deps[obj]:
|
|
dfs_rec(dep, fn, obj_visited)
|
|
fn(obj)
|
|
|
|
obj_visited = {}
|
|
for obj in t.objs:
|
|
dfs_rec(obj, fn, obj_visited)
|
|
|
|
# Generate link dependencies for all DSOs, done in a DFS fashion.
|
|
# Usually this doesn't need to be this complex, just listing the direct
|
|
# dependencies is enough. However to support creating circular
|
|
# dependency situations, traversing it by DFS and tracking processing
|
|
# status is the natural way to do it.
|
|
obj_processed = {}
|
|
fake_created = {}
|
|
def gen_link_deps(obj):
|
|
if obj in test_descr.deps:
|
|
dso = test_subdir + "/" + test_name + "-" + obj + ".so"
|
|
dependencies = ""
|
|
for dep in test_descr.deps[obj]:
|
|
if dep in obj_processed:
|
|
depstr = (" $(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-" + dep + ".so")
|
|
else:
|
|
# A circular dependency is satisfied by making a
|
|
# fake DSO tagged with the correct SONAME
|
|
depstr = (" $(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-" + dep + ".FAKE.so")
|
|
# Create empty C file and Makefile fragments for fake
|
|
# object. This only needs to be done at most once for
|
|
# an object name.
|
|
if not dep in fake_created:
|
|
f = open(testpfx_src + test_name + "-" + dep
|
|
+ ".FAKE.c", "w")
|
|
f.write(" \n")
|
|
f.close()
|
|
# Generate rule to create fake object
|
|
makefile.write \
|
|
("LDFLAGS-%s = -Wl,--no-as-needed "
|
|
"-Wl,-soname=%s\n"
|
|
% (test_name + "-" + dep + ".FAKE.so",
|
|
("$(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-" + dep + ".so")))
|
|
rule = ("$(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-" + dep + ".FAKE.os: "
|
|
"$(objpfx)" + test_srcdir
|
|
+ test_name + "-" + dep + ".FAKE.c\n"
|
|
"\t$(compile.c) $(OUTPUT_OPTION)\n")
|
|
makefile.write (rule)
|
|
makefile.write \
|
|
("modules-names += %s\n"
|
|
% (test_subdir + "/"
|
|
+ test_name + "-" + dep + ".FAKE"))
|
|
fake_created[dep] = True
|
|
dependencies += depstr
|
|
makefile.write("$(objpfx)%s:%s\n" % (dso, dependencies))
|
|
# Mark obj as processed
|
|
obj_processed[obj] = True
|
|
|
|
dfs(test_descr, gen_link_deps)
|
|
|
|
# Print LDFLAGS-* and *-no-z-defs
|
|
for o in test_descr.objs:
|
|
dso = test_name + "-" + o + ".so"
|
|
ldflags = "-Wl,--no-as-needed"
|
|
if o in test_descr.soname_map:
|
|
soname = ("$(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-"
|
|
+ test_descr.soname_map[o] + ".so")
|
|
ldflags += (" -Wl,-soname=" + soname)
|
|
makefile.write("LDFLAGS-%s = %s\n" % (dso, ldflags))
|
|
if o in test_descr.callrefs:
|
|
makefile.write("%s-no-z-defs = yes\n" % (dso))
|
|
|
|
# Print dependencies for main test program.
|
|
depstr = ""
|
|
if '#' in test_descr.deps:
|
|
for o in test_descr.deps['#']:
|
|
depstr += (" $(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-" + o + ".so")
|
|
makefile.write("$(objpfx)%s/%s:%s\n" % (test_subdir, test_name, depstr))
|
|
ldflags = "-Wl,--no-as-needed"
|
|
if '#' in test_descr.soname_map:
|
|
soname = ("$(objpfx)" + test_subdir + "/"
|
|
+ test_name + "-"
|
|
+ test_descr.soname_map['#'] + ".so")
|
|
ldflags += (" -Wl,-soname=" + soname)
|
|
makefile.write("LDFLAGS-%s = %s\n" % (test_name, ldflags))
|
|
rule = ("$(objpfx)" + test_subdir + "/" + test_name + ".o: "
|
|
"$(objpfx)" + test_srcdir + test_name + ".c\n"
|
|
"\t$(compile.c) $(OUTPUT_OPTION)\n")
|
|
makefile.write (rule)
|
|
|
|
# Ensure that all shared objects are built before running the
|
|
# test, whether there link-time dependencies or not.
|
|
depobjs = ["$(objpfx){}/{}-{}.so".format(test_subdir, test_name, dep)
|
|
for dep in test_descr.objs]
|
|
makefile.write("$(objpfx){}.out: {}\n".format(
|
|
base_test_name, " ".join(depobjs)))
|
|
|
|
# Add main executable to test-srcs
|
|
makefile.write("test-srcs += %s/%s\n" % (test_subdir, test_name))
|
|
# Add dependency on main executable of test
|
|
makefile.write("$(objpfx)%s.out: $(objpfx)%s/%s\n"
|
|
% (base_test_name, test_subdir, test_name))
|
|
|
|
for r in test_descr.expected_outputs.items():
|
|
tunable_options = []
|
|
specific_tunable = r[0]
|
|
xfail = r[1][1]
|
|
if specific_tunable != "":
|
|
tunable_options = [specific_tunable]
|
|
else:
|
|
tunable_options = default_tunable_options
|
|
if not tunable_options:
|
|
tunable_options = [""]
|
|
|
|
for tunable in tunable_options:
|
|
tunable_env = ""
|
|
tunable_sfx = ""
|
|
exp_tunable_sfx = ""
|
|
if tunable:
|
|
tunable_env = "GLIBC_TUNABLES=%s " % tunable
|
|
tunable_sfx = "-" + tunable.replace("=","_")
|
|
if specific_tunable:
|
|
tunable_sfx = "-" + specific_tunable.replace("=","_")
|
|
exp_tunable_sfx = tunable_sfx
|
|
tunable_descr = ("(%s)" % tunable_env.strip()
|
|
if tunable_env else "")
|
|
# Write out fragment of shell script for this single test.
|
|
test_descr.sh.write \
|
|
("%s${test_wrapper_env} ${run_program_env} \\\n"
|
|
"${common_objpfx}support/test-run-command \\\n"
|
|
"${common_objpfx}elf/ld.so \\\n"
|
|
"--library-path ${common_objpfx}elf/%s:"
|
|
"${common_objpfx}elf:${common_objpfx}.:"
|
|
"${common_objpfx}dlfcn \\\n"
|
|
"${common_objpfx}elf/%s/%s > \\\n"
|
|
" ${common_objpfx}elf/%s/%s%s.output\n"
|
|
% (tunable_env ,test_subdir,
|
|
test_subdir, test_name, test_subdir, test_name,
|
|
tunable_sfx))
|
|
# Generate a run of each test and compare with expected out
|
|
test_descr.sh.write \
|
|
("if [ $? -ne 0 ]; then\n"
|
|
" echo '%sFAIL: %s%s execution test'\n"
|
|
" something_failed=true\n"
|
|
"else\n"
|
|
" diff -wu ${common_objpfx}elf/%s/%s%s.output \\\n"
|
|
" ${common_objpfx}elf/%s%s%s.exp\n"
|
|
" if [ $? -ne 0 ]; then\n"
|
|
" echo '%sFAIL: %s%s expected output comparison'\n"
|
|
" something_failed=true\n"
|
|
" fi\n"
|
|
"fi\n"
|
|
% (("X" if xfail else ""), test_name, tunable_descr,
|
|
test_subdir, test_name, tunable_sfx,
|
|
test_srcdir, base_test_name, exp_tunable_sfx,
|
|
("X" if xfail else ""), test_name, tunable_descr))
|
|
|
|
# Generate C files according to dependency and calling relations from
|
|
# description string.
|
|
for obj in test_descr.objs:
|
|
src_name = test_name + "-" + obj + ".c"
|
|
f = open(testpfx_src + src_name, "w")
|
|
if obj in test_descr.callrefs:
|
|
called_objs = test_descr.callrefs[obj]
|
|
for callee in called_objs:
|
|
f.write("extern void fn_%s (void);\n" % (callee))
|
|
if len(obj) == 1:
|
|
f.write("extern int putchar(int);\n")
|
|
f.write("static void __attribute__((constructor)) " +
|
|
"init(void){putchar('%s');putchar('>');}\n" % (obj))
|
|
f.write("static void __attribute__((destructor)) " +
|
|
"fini(void){putchar('<');putchar('%s');}\n" % (obj))
|
|
else:
|
|
f.write('extern int printf(const char *, ...);\n')
|
|
f.write('static void __attribute__((constructor)) ' +
|
|
'init(void){printf("%s>");}\n' % (obj))
|
|
f.write('static void __attribute__((destructor)) ' +
|
|
'fini(void){printf("<%s");}\n' % (obj))
|
|
if obj in test_descr.callrefs:
|
|
called_objs = test_descr.callrefs[obj]
|
|
if len(obj) != 1:
|
|
f.write("extern int putchar(int);\n")
|
|
f.write("void fn_%s (void) {\n" % (obj))
|
|
if len(obj) == 1:
|
|
f.write(" putchar ('%s');\n" % (obj));
|
|
f.write(" putchar ('(');\n");
|
|
else:
|
|
f.write(' printf ("%s(");\n' % (obj));
|
|
for callee in called_objs:
|
|
f.write(" fn_%s ();\n" % (callee))
|
|
f.write(" putchar (')');\n");
|
|
f.write("}\n")
|
|
else:
|
|
for callref in test_descr.callrefs.items():
|
|
if obj in callref[1]:
|
|
if len(obj) == 1:
|
|
# We need to declare printf here in this case.
|
|
f.write('extern int printf(const char *, ...);\n')
|
|
f.write("void fn_%s (void) {\n" % (obj))
|
|
f.write(' printf ("%s()");\n' % (obj))
|
|
f.write("}\n")
|
|
break
|
|
f.close()
|
|
|
|
# Open C file for writing main program
|
|
f = open(testpfx_src + test_name + ".c", "w")
|
|
|
|
# if there are some operations in main(), it means we need -ldl
|
|
f.write("#include <stdio.h>\n")
|
|
f.write("#include <stdlib.h>\n")
|
|
f.write("#include <dlfcn.h>\n")
|
|
for s in test_descr.main_program:
|
|
if s[0] == '@':
|
|
f.write("extern void fn_%s (void);\n" % (s[1:]));
|
|
f.write("int main (void) {\n")
|
|
f.write(" putchar('{');\n")
|
|
|
|
# Helper routine for generating sanity checking code.
|
|
def put_fail_check(fail_cond, action_desc):
|
|
f.write(' if (%s) { printf ("\\n%s failed: %%s\\n", '
|
|
'dlerror()); exit (1);}\n' % (fail_cond, action_desc))
|
|
i = 0
|
|
while i < len(test_descr.main_program):
|
|
s = test_descr.main_program[i]
|
|
obj = s[1:]
|
|
dso = test_name + "-" + obj
|
|
if s[0] == '+' or s[0] == '^':
|
|
if s[0] == '+':
|
|
dlopen_flags = "RTLD_LAZY|RTLD_GLOBAL"
|
|
f.write(" putchar('+');\n");
|
|
else:
|
|
dlopen_flags = "RTLD_LAZY"
|
|
f.write(" putchar(':');\n");
|
|
if len(obj) == 1:
|
|
f.write(" putchar('%s');\n" % (obj));
|
|
else:
|
|
f.write(' printf("%s");\n' % (obj));
|
|
f.write(" putchar('[');\n");
|
|
f.write(' void *%s = dlopen ("%s.so", %s);\n'
|
|
% (obj, dso, dlopen_flags))
|
|
put_fail_check("!%s" % (obj),
|
|
"%s.so dlopen" % (dso))
|
|
f.write(" putchar(']');\n");
|
|
elif s[0] == '-':
|
|
f.write(" putchar('-');\n");
|
|
if len(obj) == 1:
|
|
f.write(" putchar('%s');\n" % (obj));
|
|
else:
|
|
f.write(' printf("%s");\n' % (obj));
|
|
f.write(" putchar('[');\n");
|
|
put_fail_check("dlclose (%s) != 0" % (obj),
|
|
"%s.so dlclose" % (dso))
|
|
f.write(" putchar(']');\n");
|
|
elif s[0] == '%':
|
|
f.write(" putchar('%');\n");
|
|
f.write(' void (*fn_%s)(void) = dlsym (%s, "fn_%s");\n'
|
|
% (obj, obj, obj))
|
|
put_fail_check("!fn_%s" % (obj),
|
|
"dlsym(fn_%s) from %s.so" % (obj, dso))
|
|
f.write(" fn_%s ();\n" % (obj))
|
|
elif s[0] == '@':
|
|
f.write(" putchar('@');\n");
|
|
f.write(" fn_%s ();\n" % (obj))
|
|
f.write(" putchar(';');\n");
|
|
i += 1
|
|
f.write(" putchar('}');\n")
|
|
f.write(" return 0;\n")
|
|
f.write("}\n")
|
|
f.close()
|
|
|
|
# --build option processing: build generated sources using 'build_gcc'
|
|
if cmdlineargs.build:
|
|
# Helper routine to run a shell command, for running GCC below
|
|
def run_cmd(args):
|
|
cmd = str.join(' ', args)
|
|
if cmdlineargs.debug_output:
|
|
print(cmd)
|
|
p = subprocess.Popen(args)
|
|
p.wait()
|
|
if p.returncode != 0:
|
|
error("error running command: %s" % (cmd))
|
|
|
|
# Compile individual .os files
|
|
for obj in test_descr.objs:
|
|
src_name = test_name + "-" + obj + ".c"
|
|
obj_name = test_name + "-" + obj + ".os"
|
|
run_cmd([build_gcc, "-c", "-fPIC", testpfx_src + src_name,
|
|
"-o", testpfx + obj_name])
|
|
|
|
obj_processed = {}
|
|
fake_created = {}
|
|
# Function to create <test_name>-<obj>.so
|
|
def build_dso(obj):
|
|
obj_name = test_name + "-" + obj + ".os"
|
|
dso_name = test_name + "-" + obj + ".so"
|
|
deps = []
|
|
if obj in test_descr.deps:
|
|
for dep in test_descr.deps[obj]:
|
|
if dep in obj_processed:
|
|
deps.append(dep)
|
|
else:
|
|
deps.append(dep + ".FAKE")
|
|
if not dep in fake_created:
|
|
base_name = testpfx + test_name + "-" + dep
|
|
src_base_name = (testpfx_src + test_name
|
|
+ "-" + dep)
|
|
cmd = [build_gcc, "-Wl,--no-as-needed",
|
|
("-Wl,-soname=" + base_name + ".so"),
|
|
"-shared", base_name + ".FAKE.c",
|
|
"-o", src_base_name + ".FAKE.so"]
|
|
run_cmd(cmd)
|
|
fake_created[dep] = True
|
|
dso_deps = map(lambda d: testpfx + test_name + "-" + d + ".so",
|
|
deps)
|
|
cmd = [build_gcc, "-shared", "-o", testpfx + dso_name,
|
|
testpfx + obj_name, "-Wl,--no-as-needed"]
|
|
if obj in test_descr.soname_map:
|
|
soname = ("-Wl,-soname=" + testpfx + test_name + "-"
|
|
+ test_descr.soname_map[obj] + ".so")
|
|
cmd += [soname]
|
|
cmd += list(dso_deps)
|
|
run_cmd(cmd)
|
|
obj_processed[obj] = True
|
|
|
|
# Build all DSOs, this needs to be in topological dependency order,
|
|
# or link will fail
|
|
dfs(test_descr, build_dso)
|
|
|
|
# Build main program
|
|
deps = []
|
|
if '#' in test_descr.deps:
|
|
deps = test_descr.deps['#']
|
|
main_deps = map(lambda d: testpfx + test_name + "-" + d + ".so",
|
|
deps)
|
|
cmd = [build_gcc, "-Wl,--no-as-needed", "-o", testpfx + test_name,
|
|
testpfx_src + test_name + ".c", "-L%s" % (os.getcwd()),
|
|
"-Wl,-rpath-link=%s" % (os.getcwd())]
|
|
if '#' in test_descr.soname_map:
|
|
soname = ("-Wl,-soname=" + testpfx + test_name + "-"
|
|
+ test_descr.soname_map['#'] + ".so")
|
|
cmd += [soname]
|
|
cmd += list(main_deps)
|
|
run_cmd(cmd)
|
|
|
|
# Check if we need to enumerate permutations of dependencies
|
|
need_permutation_processing = False
|
|
if t.dep_permutations:
|
|
# Adjust dep_permutations into map of object -> dependency permutations
|
|
for r in t.dep_permutations.items():
|
|
obj = r[0]
|
|
if obj in t.deps and len(t.deps[obj]) > 1:
|
|
deps = t.deps[obj]
|
|
t.dep_permutations[obj] = list(itertools.permutations (deps))
|
|
need_permutation_processing = True
|
|
|
|
def enum_permutations(t, perm_list):
|
|
test_subindex = 1
|
|
curr_perms = []
|
|
def enum_permutations_rec(t, perm_list):
|
|
nonlocal test_subindex, curr_perms
|
|
if len(perm_list) >= 1:
|
|
curr = perm_list[0]
|
|
obj = curr[0]
|
|
perms = curr[1]
|
|
if not perms:
|
|
# This may be an empty list if no multiple dependencies to
|
|
# permute were found, skip to next in this case
|
|
enum_permutations_rec(t, perm_list[1:])
|
|
else:
|
|
for deps in perms:
|
|
t.deps[obj] = deps
|
|
permstr = "" if obj == "#" else obj + "_"
|
|
permstr += str.join('', deps)
|
|
curr_perms.append(permstr)
|
|
enum_permutations_rec(t, perm_list[1:])
|
|
curr_perms = curr_perms[0:len(curr_perms)-1]
|
|
else:
|
|
# t.deps is now instantiated with one dependency order
|
|
# permutation(across all objects that have multiple
|
|
# permutations), now process a testcase
|
|
generate_testcase(t, ("_" + str (test_subindex)
|
|
+ "-" + str.join('-', curr_perms)))
|
|
test_subindex += 1
|
|
enum_permutations_rec(t, perm_list)
|
|
|
|
# Create *.exp files with expected outputs
|
|
for r in t.expected_outputs.items():
|
|
sfx = ""
|
|
if r[0] != "":
|
|
sfx = "-" + r[0].replace("=","_")
|
|
f = open(testpfx_src + t.test_name + sfx + ".exp", "w")
|
|
(output, xfail) = r[1]
|
|
f.write('%s' % output)
|
|
f.close()
|
|
|
|
# Create header part of top-level testcase shell script, to wrap execution
|
|
# and output comparison together.
|
|
t.sh = open(testpfx_src + t.test_name + ".sh", "w")
|
|
t.sh.write("#!/bin/sh\n")
|
|
t.sh.write("# Test driver for %s, generated by "
|
|
"dso-ordering-test.py\n" % (t.test_name))
|
|
t.sh.write("common_objpfx=$1\n")
|
|
t.sh.write("test_wrapper_env=$2\n")
|
|
t.sh.write("run_program_env=$3\n")
|
|
t.sh.write("something_failed=false\n")
|
|
|
|
# Starting part of Makefile fragment
|
|
makefile.write("ifeq (yes,$(build-shared))\n")
|
|
|
|
if need_permutation_processing:
|
|
enum_permutations(t, list (t.dep_permutations.items()))
|
|
else:
|
|
# We have no permutations to enumerate, just process testcase normally
|
|
generate_testcase(t, "")
|
|
|
|
# If testcase is XFAIL, indicate so
|
|
if t.xfail:
|
|
makefile.write("test-xfail-%s = yes\n" % t.test_name)
|
|
|
|
# Output end part of Makefile fragment
|
|
expected_output_files = ""
|
|
for r in t.expected_outputs.items():
|
|
sfx = ""
|
|
if r[0] != "":
|
|
sfx = "-" + r[0].replace("=","_")
|
|
expected_output_files += " $(objpfx)%s%s%s.exp" % (test_srcdir,
|
|
t.test_name, sfx)
|
|
makefile.write \
|
|
("$(objpfx)%s.out: $(objpfx)%s%s.sh%s "
|
|
"$(common-objpfx)support/test-run-command\n"
|
|
% (t.test_name, test_srcdir, t.test_name,
|
|
expected_output_files))
|
|
makefile.write("\t$(SHELL) $< $(common-objpfx) '$(test-wrapper-env)' "
|
|
"'$(run-program-env)' > $@; $(evaluate-test)\n")
|
|
makefile.write("ifeq ($(run-built-tests),yes)\n")
|
|
if t.xtest:
|
|
makefile.write("xtests-special += $(objpfx)%s.out\n" % (t.test_name))
|
|
else:
|
|
makefile.write("tests-special += $(objpfx)%s.out\n" % (t.test_name))
|
|
makefile.write("endif\n")
|
|
makefile.write("endif\n")
|
|
|
|
# Write ending part of shell script generation
|
|
t.sh.write("if $something_failed; then\n"
|
|
" exit 1\n"
|
|
"else\n"
|
|
" echo '%sPASS: all tests for %s succeeded'\n"
|
|
" exit 0\n"
|
|
"fi\n" % (("X" if t.xfail else ""),
|
|
t.test_name))
|
|
t.sh.close()
|
|
|
|
# Decription file parsing
|
|
def parse_description_file(filename):
|
|
global default_tunable_options
|
|
global current_input_lineno
|
|
f = open(filename)
|
|
if not f:
|
|
error("cannot open description file %s" % (filename))
|
|
descrfile_lines = f.readlines()
|
|
t = None
|
|
for line in descrfile_lines:
|
|
p = re.compile(r"#.*$")
|
|
line = p.sub("", line) # Filter out comments
|
|
line = line.strip() # Remove excess whitespace
|
|
current_input_lineno += 1
|
|
|
|
m = re.match(r"^tunable_option:\s*(.*)$", line)
|
|
if m:
|
|
if m.group(1) == "":
|
|
error("tunable option cannot be empty")
|
|
default_tunable_options.append(m.group (1))
|
|
continue
|
|
|
|
m = re.match(r"^clear_tunables$", line)
|
|
if m:
|
|
default_tunable_options = []
|
|
continue
|
|
|
|
m = re.match(r"^([^:]+):\s*(.*)$", line)
|
|
if m:
|
|
lhs = m.group(1)
|
|
o = re.match(r"^output(.*)$", lhs)
|
|
xfail = False
|
|
if not o:
|
|
o = re.match(r"^xfail_output(.*)$", lhs)
|
|
if o:
|
|
xfail = True;
|
|
if o:
|
|
if not t:
|
|
error("output specification without testcase description")
|
|
tsstr = ""
|
|
if o.group(1):
|
|
ts = re.match(r"^\(([a-zA-Z0-9_.=]*)\)$", o.group (1))
|
|
if not ts:
|
|
error("tunable option malformed '%s'" % o.group(1))
|
|
tsstr = ts.group(1)
|
|
t.expected_outputs[tsstr] = (m.group(2), xfail)
|
|
# Any tunable option XFAILed means entire testcase
|
|
# is XFAIL/XPASS
|
|
t.xfail |= xfail
|
|
else:
|
|
if t:
|
|
# Starting a new test description, end and process
|
|
# current one.
|
|
process_testcase(t)
|
|
t = TestDescr()
|
|
x = re.match(r"^xtest\((.*)\)$", lhs)
|
|
if x:
|
|
t.xtest = True
|
|
t.test_name = x.group(1)
|
|
else:
|
|
t.test_name = lhs
|
|
descr_string = m.group(2)
|
|
parse_description_string(t, descr_string)
|
|
continue
|
|
else:
|
|
if line:
|
|
if not t:
|
|
error("no active testcase description")
|
|
parse_description_string(t, line)
|
|
# Process last completed test description
|
|
if t:
|
|
process_testcase(t)
|
|
|
|
# Setup Makefile output to file or stdout as selected
|
|
if output_makefile:
|
|
output_makefile_dir = os.path.dirname(output_makefile)
|
|
if output_makefile_dir:
|
|
os.makedirs(output_makefile_dir, exist_ok = True)
|
|
makefile = open(output_makefile, "w")
|
|
else:
|
|
makefile = open(sys.stdout.fileno (), "w")
|
|
|
|
# Finally, the main top-level calling of above parsing routines.
|
|
if description_file:
|
|
parse_description_file(description_file)
|
|
else:
|
|
t = TestDescr()
|
|
t.test_name = test_name
|
|
parse_description_string(t, description)
|
|
process_testcase(t)
|
|
|
|
# Close Makefile fragment output
|
|
makefile.close()
|