#!/usr/bin/env python3
# Copyright 2016, VIXL authors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of ARM Limited nor the names of its contributors may be
# used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Generating tests
================
From the VIXL toplevel directory run:
$ ./tools/generate_tests.py
The script assumes that `clang-format-3.6` is in the current path. If it isn't,
you can provide your own:
$ ./tools/generate_tests.py --clang-format /patch/to/clang-format
Once the script has finished, it will have generated test files, as many as
present in the `default_config_files` list. For example:
- test/aarch32/test-assembler-cond-rd-rn-immediate-a32.cc
- test/aarch32/test-assembler-cond-rd-rn-rm-a32.cc
- test/aarch32/test-assembler-cond-rd-rn-rm-q-a32.cc
- test/aarch32/test-assembler-cond-rd-rn-rm-ge-a32.cc
Because these test cases need traces in order to build, the script will have
generated dummy trace files in `test/aarch32/traces/`. If you look at them
you'll see they are basically empty:
$ cat test/aarch32/traces/sim-cond-rd-rn-immediate-adc-a32.h
static const TestResult *kReferenceAdc = NULL;
So of course, we can now build the test cases but running them will crash. We
need to re-generate traces with real hardware; the test cases do not support
running in the simulator just yet.
Generating traces
=================
You need to have either compiled natively for ARM, or cross-compiled
`test-runner`. The traces can then be generated in the same way as with VIXL64.
Note that it takes a few minutes to generate everything.
./tools/generate_simulator_traces.py --runner /path/to/test-runner \
--aarch32-only
You can now rebuild everything. If it all goes well, running the new tests
should pass.
Test configuration format
=========================
TODO: Write a simple and well documented complete example configuration file and
mention it here.
The underlying `test_generator` framework reads JSON description files and
generates tests according to them. These files live in `test/aarch32/config` by
default, but you may provide your own files with the `--config-files FILE ...`
flag. The JSON format was extended to support C++ like one-line comments.
Each configuration file will serve to generate one or more test files,
we even use its file name to choose the name of the test:
test/aarch32/config/cond-rd-rn-immediate-a32.json
`-> test/aarch32/test-simulator-cond-rd-rn-immediate-a32.cc
`-> test/aarch32/test-assembler-cond-rd-rn-immediate-a32.cc
In addition to these test configuration files, we also provide a JSON
description with shared information. This information represents data types that
instructions use and lives in `test/aarch32/config/data-types.json`.
Data types description
----------------------
We refer to two kinds of data types: `operand` and `input`.
An `operand` represents an argument passed to the macro-assembler to generate an
instruction. For example, a register or an immediate are operands. We can think
of it as "assemble-time" data.
As opposed to `operands`, an `input` represents data passed to an instruction at
runtime. For example, it will be the value you write to a register before
executing the instruction under test.
The `data-types.json` file has the following structure:
~~~
{
"operands": [
// List of operand types.
],
"inputs": [
// List of input types.
]
}
~~~
Each operand is described with the following structure:
~~~
{
// Unique name for this operand type.
"name": "AllRegistersButPC",
// C++ type used by VIXL to represent this operand.
"type": "Register",
// List of possible variants.
"variants": [
"r0",
"r1",
"r2",
"r3",
"r4",
"r5",
"r6",
"r7",
"r8",
"r9",
"r10",
"r11",
"r12",
"r13",
"r14"
],
// Default variant to use.
"default": "r0"
}
~~~
The "name" field of the operand will be used by test configuration files in
order to specify what kind of operands an instruction takes. The "type" field
simply tells the generator what C++ type should be generated, e.g. "Condition",
"Register", "uint32_t", "ShiftType", ...etc.
Inputs are described in a very similar way:
~~~
{
// Unique name for this input type.
"name": "Register",
// Python type from `test_generator.data_types` to use to generate C++ code
// for this input.
"type": "Register",
// List of possible values.
"values": [
"0x00000000",
"0xffffffff",
"0xabababab",
"0x5a5a5a5a"
],
// Default value.
"default": "0xabababab"
}
~~~
The "name" field has the same purpose as for operands. The "type" field however,
is the name of a Python class in `test_generator.data_types`. The type will
specify what C++ code to generate in order to load and record the input value,
e.g. how to load a value into a register, how to read and record it.
When adding more tests, one may have to create new data types in this file. For
example, when we want to test an instruction with a different set of registers.
If adding new input types which need different C++ code to load and record them,
one will have to add it to `test_generator.data_types` and override the
`Epilogue` and `Prologue` methods.
Test configuration
------------------
Once we have all the data types we need described, we need test configuration
files to describe what instructions to test and with what `inputs` and
`operands` they take.
These files have the following structure:
~~~
{
"mnemonics": [
// List of instruction mnemonics to use. These must correspond to
// `MacroAssembler` methods.
],
"description": {
"operands": [
// List of operands the instruction takes.
],
"inputs: [
// List of inputs the instruction can be affected by.
]
},
// List of files to generate.
"test-files": [
{
"type": "assembler",
"mnemonics": [
// Optional list of instruction mnemonics to use, overriding the
// top-level list.
],
"test-cases": [
// List of test cases for "assembler" tests, see below for
// details.
]
},
{
"type": "simulator",
"test-cases": [
// List of test cases for "simulator" tests, see below for
// details.
]
}
]
}
~~~
- List of operands:
The operand list describes the actual argument to the `MacroAssembler` method.
For example, if we take instruction in the form
"XXX.cond rd rn rm shift #amount":
We want to generate C++ code as such:
~~~
Condition cond = ...;
Register rd = ...;
Register rn = ...;
Register rm = ...;
ShiftType type = ...;
uint32_t amount = ...;
Operand op(rm, type, amount);
__ Xxx(cond, rd, rn, op);
~~~
We will have the following operand list:
~~~
"operands": [
{
"name": "cond",
"type": "Condition"
},
{
"name": "rd",
"type": "AllRegistersButPC"
},
{
"name": "rn",
"type": "AllRegistersButPC"
},
{
"name": "op",
"wrapper": "Operand",
"operands": [
{
"name": "rm",
"operand": "AllRegistersButPC"
},
{
"name": "type",
"operand": "Shift"
},
{
"name": "amount",
"operand": "ImmediateShiftAmount"
}
]
}
]
~~~
The "name" field represents the identifier of the operand and will be used as a
variable name in the generated code. The "type" field corresponds to an operand
type described in the `data-types.json` file as described above.
We can see that we've wrapped the last three operands into an "op"
wrapper object. This allows us to tell the generator to wrap these
operands into a `Operand` C++ object.
- List of inputs:
This structure is similar to the operand list, but this time it describes what
input data the instructions may be affected by at runtime. If we take the same
example as above, we will have the following list:
~~~
"inputs": [
{
"name": "apsr",
"type": "NZCV"
},
{
"name": "rd",
"type": "Register"
},
{
"name": "rn",
"type": "Register"
},
{
"name": "rm",
"type": "Register"
}
]
~~~
This will specify what C++ code to generate before and after emitting the
instruction under test. The C++ code will set and record register values for
example. See `test_generator.data_types` for more details.
- Test files and test cases:
Up until now, we've only just described the environment in which instructions
can operate. We need to express what files we want generating, what instructions
we want to test and what we want them to do.
As previously mentioned, a configuration file can control the generation of
several test files. We will generate one file per element in the "test-files"
array:
~~~
"test-files": [
{
"type": "assembler",
"test-cases": [
// List of test cases for "assembler" tests, see below for
// details.
]
},
{
"type": "assembler",
"name": "special-case",
"mnemonics": [
// Override the top-level list with a subset of instructions concerned
// with this special case.
],
"test-cases": [
// List of test cases for "assembler" tests, see below for
// details.
]
},
{
"type": "simulator",
"test-cases": [
// List of test cases for "simulator" tests, see below for
// details.
]
}
]
~~~
Above, we've decided to generate three tests: a "simulator" test and two
"assembler" tests. The resulting files will have names with the following
pattern.
- "test/aarch32/test-assembler-{configuration name}-a32.cc"
- "test/aarch32/test-assembler-{configuration name}-special-case-a32.cc"
- "test/aarch32/test-simulator-{configuration name}-a32.cc"
The "type" field describes the kind of testing we want to do, these types are
recognized by the generator and, at the moment, can be one of "simulator",
"assembler" and "macro-assembler". Simulator tests will run each instruction and
record the changes while assembler tests will only record the code buffer and
never execute anything. MacroAssembler tests currently only generate code to
check that the MacroAssembler does not crash; the output itself is not yet
tested. Because you may want to generate more than one test of the same type, as
we are doing in the example, we need a way to differentiate them. You may use
the optional "name" field for this.
Finally, we describe how to test the instruction by declaring a list of test
cases with the "test-cases" field.
Here is an example of what we can express:
~~~
[
// Generate all combinations of instructions where "rd" an "rn" are the same
// register and "cond" and "rm" are just the default.
// For example:
// __ Xxx(al, r0, r0, r0);
// __ Xxx(al, r1, r1, r0);
// __ Xxx(al, r2, r2, r0);
// ...
// __ Xxx(al, r12, r12, r0);
// __ Xxx(al, r13, r13, r0);
// __ Xxx(al, r14, r14, r0);
//
// For each of the instructions above, run them with a different value in "rd"
// and "rn".
{
"name": "RdIsRn",
"operands": [
"rd", "rn"
],
"operand-filter": "rd == rn",
"inputs": [
"rd", "rn"
],
"input-filter": "rd == rn"
},
// Generate all combinations of instructions with different condition codes.
// For example:
// __ Xxx(eq, r0, r0, r0);
// __ Xxx(ne, r0, r0, r0);
// __ Xxx(cs, r0, r0, r0);
// ...
// __ Xxx(gt, r0, r0, r0);
// __ Xxx(le, r0, r0, r0);
// __ Xxx(al, r0, r0, r0);
//
// For each of the instructions above, run them against all combinations of
// NZCV bits.
{
"name": "ConditionVersusNZCV",
"operands": [
"cond"
],
"inputs": [
"apsr"
]
},
// We are interested in testing that the Q bit gets set and cleared, so we've
// limited the instruction generation to a single instruction and instead have
// stressed the values put in "rn" and "rm".
//
// So for this instruction, we choose to run it will all combinations of
// values in "rn" and "rm". Additionally, we include "qbit" in the inputs,
// which will make the test set or clear it before executing the instruction.
// Note that "qbit" needs to be declared as an input in the instruction
// description (see "List of inputs" section).
{
"name": "Qbit",
"operands": [
"rn", "rm"
],
"inputs": [
"qbit", "rn", "rm"
],
"operand-filter": "rn != rm'",
"operand-limit": 1
},
// Generate 10 random instructions with all different registers but use the
// default condition.
// For example:
// __ Xxx(al, r5, r1, r0);
// __ Xxx(al, r8, r9, r7);
// __ Xxx(al, r9, r1, r2);
// __ Xxx(al, r0, r6, r2);
// __ Xxx(al, r11, r9, r11);
// __ Xxx(al, r14, r2, r11);
// __ Xxx(al, r8, r2, r5);
// __ Xxx(al, r10, r0, r1);
// __ Xxx(al, r11, r2, r7);
// __ Xxx(al, r2, r6, r1);
//
// For each instruction, feed it 200 different combination of values in the
// three registers.
{
"name": "RegisterSimulatorTest",
"operands": [
"rd", "rn", "rm"
],
"inputs": [
"rd", "rn", "rm"
],
"operand-limit": 10,
"input-limit": 200
}
]
~~~
Assembler test cases are much simpler, here are some examples:
~~~
// Generate 2000 random instructions out of all possible operand combinations.
{
"name": "LotsOfRandomInstructions",
"operands": [
"cond", "rd", "rn", "rm"
],
"operand-limit": 2000
},
// Same as above but limit the test to 200 instructions where rd == rn.
{
"name": "RdIsRn",
"operands": [
"cond", "rd", "rn", "rm"
],
"operand-filter": "rd == rn",
"operand-limit": 200
}
~~~
As can be expected, assembler test do not have the notion of "inputs".
Here are details about each field. Note that all of them except for "name" are
optional.
* "name":
A unique name should be given to the test case, it will be used to give the
generated C++ `const Input[]` array a name.
* "operands":
List of operand names that we are interested in testing. The generator will
lookup the list of variants for each operand and build the product of all of
them. It will then choose the default variant for the operands not specified
here.
* "operand-filter":
As you would expect, the product of all operand variants may be huge. To
prevent this, you may specify a Python expression to filter the list.
* "operand-limit":
We can potentially obtain a *massive* set of variants of instructions, as we
are computing a product of operand variants in "operands". This field allows
us to limit this by choosing a random sample from the computed variants.
Note that this is a seeded pseudo-random sample, and the seed corresponds to
the test case description. The same test case description will always
generate the same code.
* "inputs":
This is exactly the same as "operands" but for inputs.
* "input-filter":
Ditto.
* "input-limit":
Ditto.
Here is an example of the C++ code that will be generated for a given test case.
For simplicity, let's generate tests for an instruction with only `NZCV` and two
registers as inputs.
For the following test case, which will target encodings where `rd` and `rn` are
the same registers:
~~~
{
"name": "RdIsRn",
"operands": [
"rd", "rn"
],
"operand-filter": "rd == rn",
"inputs": [
"rd", "rn"
],
"input-filter": "rd == rn"
},
~~~
It will generate the following input array.
~~~
// apsr, rd, rn
static const Inputs kRdIsRn[] = {{NoFlag, 0x00000000, 0x00000000},
{NoFlag, 0xffffffff, 0xffffffff},
{NoFlag, 0xabababab, 0xabababab},
{NoFlag, 0x5a5a5a5a, 0x5a5a5a5a}};
~~~
We can see that the default apsr value was chosen (NoFlag), as apsr is not in
the list of "inputs".
It will also generate a list of instructions to test:
~~~
static const TestLoopData kTests[] = {
{{al, r1, r1, 0x000000ab}, ARRAY_SIZE(kRdIsRn), kRdIsRn, "RdIsRn"},
{{al, r2, r2, 0x000000ab}, ARRAY_SIZE(kRdIsRn), kRdIsRn, "RdIsRn"},
{{al, r8, r8, 0x000000ab}, ARRAY_SIZE(kRdIsRn), kRdIsRn, "RdIsRn"},
{{al, r9, r9, 0x000000ab}, ARRAY_SIZE(kRdIsRn), kRdIsRn, "RdIsRn"},
};
~~~
As a result, the new test we will assemble each instructions in "mnemonics" with
all of the operands described in `kTests` above. And each instruction will be
executed and passed all inputs in `kRdIsRn`.
"""
import subprocess
import argparse
import string
import re
import multiprocessing
import functools
import test_generator.parser
default_config_files = [
'test/aarch32/config/rd-rn-rm-a32.json',
'test/aarch32/config/cond-rd-rn-operand-const-a32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-a32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-shift-amount-1to31-a32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-shift-amount-1to32-a32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-shift-rs-a32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-ror-amount-a32.json',
'test/aarch32/config/cond-rd-rn-a32.json',
'test/aarch32/config/cond-rd-rn-pc-a32.json',
'test/aarch32/config/cond-rd-rn-rm-a32.json',
'test/aarch32/config/cond-rd-operand-const-a32.json',
'test/aarch32/config/cond-rd-operand-rn-a32.json',
'test/aarch32/config/cond-rd-operand-rn-shift-amount-1to31-a32.json',
'test/aarch32/config/cond-rd-operand-rn-shift-amount-1to32-a32.json',
'test/aarch32/config/cond-rd-operand-rn-shift-rs-a32.json',
'test/aarch32/config/cond-rd-operand-rn-ror-amount-a32.json',
'test/aarch32/config/cond-rd-memop-immediate-512-a32.json',
'test/aarch32/config/cond-rd-memop-immediate-8192-a32.json',
'test/aarch32/config/cond-rd-memop-rs-a32.json',
'test/aarch32/config/cond-rd-memop-rs-shift-amount-1to31-a32.json',
'test/aarch32/config/cond-rd-memop-rs-shift-amount-1to32-a32.json',
'test/aarch32/config/cond-rd-rn-t32.json',
'test/aarch32/config/cond-rd-rn-rm-t32.json',
'test/aarch32/config/cond-rdlow-rnlow-rmlow-t32.json',
'test/aarch32/config/cond-rd-rn-operand-const-t32.json',
'test/aarch32/config/cond-rd-pc-operand-imm12-t32.json',
'test/aarch32/config/cond-rd-rn-operand-imm12-t32.json',
'test/aarch32/config/cond-rd-pc-operand-imm8-t32.json',
'test/aarch32/config/cond-rd-sp-operand-imm8-t32.json',
'test/aarch32/config/cond-rdlow-rnlow-operand-immediate-t32.json',
'test/aarch32/config/cond-sp-sp-operand-imm7-t32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-t32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-shift-amount-1to31-t32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-shift-amount-1to32-t32.json',
'test/aarch32/config/cond-rd-rn-operand-rm-ror-amount-t32.json',
'test/aarch32/config/cond-rd-operand-const-t32.json',
'test/aarch32/config/cond-rd-operand-imm16-t32.json',
'test/aarch32/config/cond-rdlow-operand-imm8-t32.json',
'test/aarch32/config/cond-rd-operand-rn-shift-amount-1to31-t32.json',
'test/aarch32/config/cond-rd-operand-rn-shift-amount-1to32-t32.json',
'test/aarch32/config/cond-rd-operand-rn-shift-rs-t32.json',
'test/aarch32/config/cond-rd-operand-rn-ror-amount-t32.json',
'test/aarch32/config/cond-rd-operand-rn-t32.json',
'test/aarch32/config/rd-rn-rm-t32.json',
]
# Link a test type with a template file.
template_files = {
'simulator': "test/aarch32/config/template-simulator-aarch32.cc.in",
'assembler': "test/aarch32/config/template-assembler-aarch32.cc.in",
'macro-assembler': "test/aarch32/config/template-macro-assembler-aarch32.cc.in",
}
def BuildOptions():
result = argparse.ArgumentParser(
description = 'Test generator for AArch32.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
result.add_argument('--config-files', nargs='+',
default=default_config_files,
metavar='FILE',
help='Configuration files, each will generate a test file.')
result.add_argument('--clang-format',
default='clang-format-3.6', help='Path to clang-format.')
result.add_argument('--jobs', '-j', type=int, metavar='N',
default=multiprocessing.cpu_count(),
help='Allow N jobs at once')
result.add_argument('--skip-traces', action='store_true',
help='Skip generation of dummy traces.')
return result.parse_args()
def DoNotEditComment(template_file):
# We rely on `clang-format` to wrap this comment to 80 characters.
return """
// -----------------------------------------------------------------------------
// This file is auto generated from the {} template file using tools/generate_tests.py.
//
// PLEASE DO NOT EDIT.
// -----------------------------------------------------------------------------
""".format(template_file)
def GenerateTest(generator, clang_format, skip_traces):
template_file = template_files[generator.test_type]
generated_file = ""
with open(template_file, "r") as f:
# Strip out comments starting with three forward slashes before creating the
# string.Template object.
template = string.Template(re.sub("\/\/\/.*", "", f.read()))
# The `generator` object has methods generating strings to fill the template.
generated_file = template.substitute({
# Add a top comment stating this file is auto-generated.
'do_not_edit_comment': DoNotEditComment(template_file),
# List of mnemonics.
'instruction_list_declaration': generator.InstructionListDeclaration(),
# Declarations.
'operand_list_declaration': generator.OperandDeclarations(),
'input_declarations': generator.InputDeclarations(),
# Definitions.
'input_definitions': generator.InputDefinitions(),
'test_case_definitions': generator.TestCaseDefinitions(),
# Include traces.
'include_trace_files': generator.IncludeTraceFiles(),
# Define a typedef for the MacroAssembler method.
'macroassembler_method_args': generator.MacroAssemblerMethodArgs(),
# Generate code to switch instruction set.
'macroassembler_set_isa': generator.MacroAssemblerSetISA(),
# Generate code to emit instructions.
'code_instantiate_operands': generator.CodeInstantiateOperands(),
'code_prologue': generator.CodePrologue(),
'code_epilogue': generator.CodeEpilogue(),
'code_parameter_list': generator.CodeParameterList(),
# Generate code to trace the execution and print C++.
'trace_print_outputs': generator.TracePrintOutputs(),
# Generate code to compare the results against a trace.
'check_instantiate_results': generator.CheckInstantiateResults(),
'check_instantiate_inputs': generator.CheckInstantiateInputs(),
'check_instantiate_references': generator.CheckInstantiateReferences(),
'check_results_against_references':
generator.CheckResultsAgainstReferences(),
'check_print_input': generator.CheckPrintInput(),
'check_print_expected': generator.CheckPrintExpected(),
'check_print_found': generator.CheckPrintFound(),
'test_name': generator.TestName(),
'isa_guard': generator.GetIsaGuard()
})
# Create the test case and pipe it through `clang-format` before writing it.
with open(
"test/aarch32/test-{}-{}.cc".format(generator.test_type, generator.test_name),
"w") as f:
proc = subprocess.Popen([clang_format], stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
out, _ = proc.communicate(generated_file.encode())
f.write(out.decode())
if not skip_traces:
# Write dummy trace files into 'test/aarch32/traces/'.
generator.WriteEmptyTraces("test/aarch32/traces/")
print("Generated {} test for \"{}\".".format(generator.test_type, generator.test_name))
if __name__ == '__main__':
args = BuildOptions()
# Each file in `args.config_files` populates a `Generator` object.
generators = test_generator.parser.Parse('test/aarch32/config/data-types.json',
args.config_files)
# Call the `GenerateTest` function for each generator object in parallel. This
# will use as many processes as defined by `-jN`, which defaults to 1.
with multiprocessing.Pool(processes=args.jobs) as pool:
pool.map(functools.partial(GenerateTest, clang_format=args.clang_format,
skip_traces=args.skip_traces),
generators)