// Copyright (c) 2015, Google Inc.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//go:build ignore
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"boringssl.googlesource.com/boringssl/util/testconfig"
"boringssl.googlesource.com/boringssl/util/testresult"
)
// TODO(davidben): Link tests with the malloc shim and port -malloc-test to this runner.
var (
useValgrind = flag . Bool ( "valgrind" , false , "If true, run code under valgrind" )
useCallgrind = flag . Bool ( "callgrind" , false , "If true, run code under valgrind to generate callgrind traces." )
useGDB = flag . Bool ( "gdb" , false , "If true, run BoringSSL code under gdb" )
useSDE = flag . Bool ( "sde" , false , "If true, run BoringSSL code under Intel's SDE for each supported chip" )
sdePath = flag . String ( "sde-path" , "sde" , "The path to find the sde binary." )
buildDir = flag . String ( "build-dir" , "build" , "The build directory to run the tests from." )
numWorkers = flag . Int ( "num-workers" , runtime . NumCPU ( ) , "Runs the given number of workers when testing." )
jsonOutput = flag . String ( "json-output" , "" , "The file to output JSON results to." )
mallocTest = flag . Int64 ( "malloc-test" , - 1 , "If non-negative, run each test with each malloc in turn failing from the given number onwards." )
mallocTestDebug = flag . Bool ( "malloc-test-debug" , false , "If true, ask each test to abort rather than fail a malloc. This can be used with a specific value for --malloc-test to identity the malloc failing that is causing problems." )
simulateARMCPUs = flag . Bool ( "simulate-arm-cpus" , simulateARMCPUsDefault ( ) , "If true, runs tests simulating different ARM CPUs." )
)
func simulateARMCPUsDefault ( ) bool {
return ( runtime . GOOS == "linux" || runtime . GOOS == "android" ) && ( runtime . GOARCH == "arm" || runtime . GOARCH == "arm64" )
}
type test struct {
testconfig . Test
shard , numShards int
// cpu, if not empty, contains a code to simulate. For SDE, run `sde64
// -help` to get a list of these codes. For ARM, see gtest_main.cc for
// the supported values.
cpu string
}
type result struct {
Test test
Passed bool
Error error
}
// sdeCPUs contains a list of CPU code that we run all tests under when *useSDE
// is true.
var sdeCPUs = [ ] string {
"p4p" , // Pentium4 Prescott
"mrm" , // Merom
"pnr" , // Penryn
"nhm" , // Nehalem
"wsm" , // Westmere
"snb" , // Sandy Bridge
"ivb" , // Ivy Bridge
"hsw" , // Haswell
"bdw" , // Broadwell
"slt" , // Saltwell
"slm" , // Silvermont
"glm" , // Goldmont
"glp" , // Goldmont Plus
"tnt" , // Tremont
"skl" , // Skylake
"cnl" , // Cannon Lake
"icl" , // Ice Lake
"skx" , // Skylake server
"clx" , // Cascade Lake
"cpx" , // Cooper Lake
"icx" , // Ice Lake server
"knl" , // Knights landing
"knm" , // Knights mill
"tgl" , // Tiger Lake
}
var armCPUs = [ ] string {
"none" , // No support for any ARM extensions.
"neon" , // Support for NEON.
"crypto" , // Support for NEON and crypto extensions.
}
func valgrindOf ( dbAttach bool , path string , args ... string ) * exec . Cmd {
valgrindArgs := [ ] string { "--error-exitcode=99" , "--track-origins=yes" , "--leak-check=full" , "--quiet" }
if dbAttach {
valgrindArgs = append ( valgrindArgs , "--db-attach=yes" , "--db-command=xterm -e gdb -nw %f %p" )
}
valgrindArgs = append ( valgrindArgs , path )
valgrindArgs = append ( valgrindArgs , args ... )
return exec . Command ( "valgrind" , valgrindArgs ... )
}
func callgrindOf ( path string , args ... string ) * exec . Cmd {
valgrindArgs := [ ] string { "-q" , "--tool=callgrind" , "--dump-instr=yes" , "--collect-jumps=yes" , "--callgrind-out-file=" + * buildDir + "/callgrind/callgrind.out.%p" }
valgrindArgs = append ( valgrindArgs , path )
valgrindArgs = append ( valgrindArgs , args ... )
return exec . Command ( "valgrind" , valgrindArgs ... )
}
func gdbOf ( path string , args ... string ) * exec . Cmd {
xtermArgs := [ ] string { "-e" , "gdb" , "--args" }
xtermArgs = append ( xtermArgs , path )
xtermArgs = append ( xtermArgs , args ... )
return exec . Command ( "xterm" , xtermArgs ... )
}
func sdeOf ( cpu , path string , args ... string ) * exec . Cmd {
sdeArgs := [ ] string { "-" + cpu }
// The kernel's vdso code for gettimeofday sometimes uses the RDTSCP
// instruction. Although SDE has a -chip_check_vsyscall flag that
// excludes such code by default, it does not seem to work. Instead,
// pass the -chip_check_exe_only flag which retains test coverage when
// statically linked and excludes the vdso.
if cpu == "p4p" || cpu == "pnr" || cpu == "mrm" || cpu == "slt" {
sdeArgs = append ( sdeArgs , "-chip_check_exe_only" )
}
sdeArgs = append ( sdeArgs , "--" , path )
sdeArgs = append ( sdeArgs , args ... )
return exec . Command ( * sdePath , sdeArgs ... )
}
var (
errMoreMallocs = errors . New ( "child process did not exhaust all allocation calls" )
errTestSkipped = errors . New ( "test was skipped" )
)
func runTestOnce ( test test , mallocNumToFail int64 ) ( passed bool , err error ) {
prog := path . Join ( * buildDir , test . Cmd [ 0 ] )
args := append ( [ ] string { } , test . Cmd [ 1 : ] ... )
if * simulateARMCPUs && test . cpu != "" {
args = append ( args , "--cpu=" + test . cpu )
}
if * useSDE {
// SDE is neither compatible with the unwind tester nor automatically
// detected.
args = append ( args , "--no_unwind_tests" )
}
var cmd * exec . Cmd
if * useValgrind {
cmd = valgrindOf ( false , prog , args ... )
} else if * useCallgrind {
cmd = callgrindOf ( prog , args ... )
} else if * useGDB {
cmd = gdbOf ( prog , args ... )
} else if * useSDE {
cmd = sdeOf ( test . cpu , prog , args ... )
} else {
cmd = exec . Command ( prog , args ... )
}
if test . Env != nil || test . numShards != 0 {
cmd . Env = make ( [ ] string , len ( os . Environ ( ) ) )
copy ( cmd . Env , os . Environ ( ) )
}
if test . Env != nil {
cmd . Env = append ( cmd . Env , test . Env ... )
}
if test . numShards != 0 {
cmd . Env = append ( cmd . Env , fmt . Sprintf ( "GTEST_SHARD_INDEX=%d" , test . shard ) )
cmd . Env = append ( cmd . Env , fmt . Sprintf ( "GTEST_TOTAL_SHARDS=%d" , test . numShards ) )
}
var outBuf bytes . Buffer
cmd . Stdout = & outBuf
cmd . Stderr = & outBuf
if mallocNumToFail >= 0 {
cmd . Env = os . Environ ( )
cmd . Env = append ( cmd . Env , "MALLOC_NUMBER_TO_FAIL=" + strconv . FormatInt ( mallocNumToFail , 10 ) )
if * mallocTestDebug {
cmd . Env = append ( cmd . Env , "MALLOC_ABORT_ON_FAIL=1" )
}
cmd . Env = append ( cmd . Env , "_MALLOC_CHECK=1" )
}
if err := cmd . Start ( ) ; err != nil {
return false , err
}
if err := cmd . Wait ( ) ; err != nil {
if exitError , ok := err . ( * exec . ExitError ) ; ok {
switch exitError . Sys ( ) . ( syscall . WaitStatus ) . ExitStatus ( ) {
case 88 :
return false , errMoreMallocs
case 89 :
fmt . Print ( string ( outBuf . Bytes ( ) ) )
return false , errTestSkipped
}
}
fmt . Print ( string ( outBuf . Bytes ( ) ) )
return false , err
}
// Account for Windows line-endings.
stdout := bytes . Replace ( outBuf . Bytes ( ) , [ ] byte ( "\r\n" ) , [ ] byte ( "\n" ) , - 1 )
if bytes . HasSuffix ( stdout , [ ] byte ( "PASS\n" ) ) &&
( len ( stdout ) == 5 || stdout [ len ( stdout ) - 6 ] == '\n' ) {
return true , nil
}
// Also accept a googletest-style pass line. This is left here in
// transition until the tests are all converted and this script made
// unnecessary.
if bytes . Contains ( stdout , [ ] byte ( "\n[ PASSED ]" ) ) {
return true , nil
}
fmt . Print ( string ( outBuf . Bytes ( ) ) )
return false , nil
}
func runTest ( test test ) ( bool , error ) {
if * mallocTest < 0 {
return runTestOnce ( test , - 1 )
}
for mallocNumToFail := int64 ( * mallocTest ) ; ; mallocNumToFail ++ {
if passed , err := runTestOnce ( test , mallocNumToFail ) ; err != errMoreMallocs {
if err != nil {
err = fmt . Errorf ( "at malloc %d: %s" , mallocNumToFail , err )
}
return passed , err
}
}
}
// setWorkingDirectory walks up directories as needed until the current working
// directory is the top of a BoringSSL checkout.
func setWorkingDirectory ( ) {
for i := 0 ; i < 64 ; i ++ {
if _ , err := os . Stat ( "BUILDING.md" ) ; err == nil {
return
}
os . Chdir ( ".." )
}
panic ( "Couldn't find BUILDING.md in a parent directory!" )
}
func worker ( tests <- chan test , results chan <- result , done * sync . WaitGroup ) {
defer done . Done ( )
for test := range tests {
passed , err := runTest ( test )
results <- result { test , passed , err }
}
}
func ( t test ) shortName ( ) string {
return strings . Join ( t . Cmd , " " ) + t . shardMsg ( ) + t . cpuMsg ( ) + t . envMsg ( )
}
func SpaceIf ( returnSpace bool ) string {
if ! returnSpace {
return ""
}
return " "
}
func ( t test ) longName ( ) string {
return strings . Join ( t . Env , " " ) + SpaceIf ( len ( t . Env ) != 0 ) + strings . Join ( t . Cmd , " " ) + t . shardMsg ( ) + t . cpuMsg ( )
}
func ( t test ) shardMsg ( ) string {
if t . numShards == 0 {
return ""
}
return fmt . Sprintf ( " [shard %d/%d]" , t . shard + 1 , t . numShards )
}
func ( t test ) cpuMsg ( ) string {
if len ( t . cpu ) == 0 {
return ""
}
return fmt . Sprintf ( " (for CPU %q)" , t . cpu )
}
func ( t test ) envMsg ( ) string {
if len ( t . Env ) == 0 {
return ""
}
return " (custom environment)"
}
func ( t test ) getGTestShards ( ) ( [ ] test , error ) {
if * numWorkers == 1 || ! t . Shard {
return [ ] test { t } , nil
}
shards := make ( [ ] test , * numWorkers )
for i := range shards {
shards [ i ] = t
shards [ i ] . shard = i
shards [ i ] . numShards = * numWorkers
}
return shards , nil
}
func main ( ) {
flag . Parse ( )
setWorkingDirectory ( )
testCases , err := testconfig . ParseTestConfig ( "util/all_tests.json" )
if err != nil {
fmt . Printf ( "Failed to parse input: %s\n" , err )
os . Exit ( 1 )
}
var wg sync . WaitGroup
tests := make ( chan test , * numWorkers )
results := make ( chan result , * numWorkers )
for i := 0 ; i < * numWorkers ; i ++ {
wg . Add ( 1 )
go worker ( tests , results , & wg )
}
go func ( ) {
for _ , baseTest := range testCases {
test := test { Test : baseTest }
if * useSDE {
if test . SkipSDE {
continue
}
// SDE generates plenty of tasks and gets slower
// with additional sharding.
for _ , cpu := range sdeCPUs {
testForCPU := test
testForCPU . cpu = cpu
tests <- testForCPU
}
} else if * simulateARMCPUs {
// This mode is run instead of the default path,
// so also include the native flow.
tests <- test
for _ , cpu := range armCPUs {
testForCPU := test
testForCPU . cpu = cpu
tests <- testForCPU
}
} else {
shards , err := test . getGTestShards ( )
if err != nil {
fmt . Printf ( "Error listing tests: %s\n" , err )
os . Exit ( 1 )
}
for _ , shard := range shards {
tests <- shard
}
}
}
close ( tests )
wg . Wait ( )
close ( results )
} ( )
testOutput := testresult . NewResults ( )
var failed , skipped [ ] test
for testResult := range results {
test := testResult . Test
args := test . Cmd
if testResult . Error == errTestSkipped {
fmt . Printf ( "%s\n" , test . longName ( ) )
fmt . Printf ( "%s was skipped\n" , args [ 0 ] )
skipped = append ( skipped , test )
testOutput . AddSkip ( test . longName ( ) )
} else if testResult . Error != nil {
fmt . Printf ( "%s\n" , test . longName ( ) )
fmt . Printf ( "%s failed to complete: %s\n" , args [ 0 ] , testResult . Error )
failed = append ( failed , test )
testOutput . AddResult ( test . longName ( ) , "CRASH" )
} else if ! testResult . Passed {
fmt . Printf ( "%s\n" , test . longName ( ) )
fmt . Printf ( "%s failed to print PASS on the last line.\n" , args [ 0 ] )
failed = append ( failed , test )
testOutput . AddResult ( test . longName ( ) , "FAIL" )
} else {
fmt . Printf ( "%s\n" , test . shortName ( ) )
testOutput . AddResult ( test . longName ( ) , "PASS" )
}
}
if * jsonOutput != "" {
if err := testOutput . WriteToFile ( * jsonOutput ) ; err != nil {
fmt . Fprintf ( os . Stderr , "Error: %s\n" , err )
}
}
if len ( skipped ) > 0 {
fmt . Printf ( "\n%d of %d tests were skipped:\n" , len ( skipped ) , len ( testCases ) )
for _ , test := range skipped {
fmt . Printf ( "\t%s\n" , test . shortName ( ) )
}
}
if len ( failed ) > 0 {
fmt . Printf ( "\n%d of %d tests failed:\n" , len ( failed ) , len ( testCases ) )
for _ , test := range failed {
fmt . Printf ( "\t%s\n" , test . shortName ( ) )
}
os . Exit ( 1 )
}
fmt . Printf ( "\nAll tests passed!\n" )
}