You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
16 KiB
406 lines
16 KiB
#!python3
|
|
|
|
# License: Apache 2.0. See LICENSE file in root directory.
|
|
# Copyright(c) 2020 Intel Corporation. All Rights Reserved.
|
|
|
|
#
|
|
# Syntax:
|
|
# unit-test-config.py <dir> <build-dir>
|
|
#
|
|
# Looks for possible single-file unit-testing targets (test-*) in $dir, and builds
|
|
# a CMakeLists.txt in $builddir to compile them.
|
|
#
|
|
# Each target is compiled in its own project, so that each file ends up in a different
|
|
# process and so individual tests cannot affect others except through hardware.
|
|
#
|
|
|
|
import sys, os, subprocess, locale, re, getopt
|
|
from glob import glob
|
|
|
|
current_dir = os.path.dirname( os.path.abspath( __file__ ) )
|
|
sys.path.append( current_dir + os.sep + "py" )
|
|
|
|
from rspy import file, repo, libci, log
|
|
|
|
def usage():
|
|
ourname = os.path.basename(sys.argv[0])
|
|
print( 'Syntax: ' + ourname + ' [options] <dir> <build-dir>' )
|
|
print( ' build unit-testing framework for the tree in $dir' )
|
|
print( ' -r, --regex configure all tests that fit the following regular expression' )
|
|
print( ' -t, --tag configure all tests with the following tag. If used multiple times runs all tests matching' )
|
|
print( ' all tags. e.g. -t tag1 -t tag2 will run tests who have both tag1 and tag2' )
|
|
print( ' tests automatically get tagged with \'exe\' or \'py\' and based on their location' )
|
|
print( ' inside unit-tests/, e.g. unit-tests/func/test-hdr.py gets [func, py]' )
|
|
print( ' --list-tags print out all available tags. This option will not run any tests' )
|
|
print( ' --list-tests print out all available tests. This option will not run any tests' )
|
|
print( ' if both list-tags and list-tests are specified each test will be printed along' )
|
|
print( ' with what tags it has' )
|
|
print( ' --context The context to use for test configuration' )
|
|
print( ' --live Only configure tests that are live (have test:device)' )
|
|
print( ' --not-live Only configure tests that are NOT live (do not have test:device)' )
|
|
sys.exit(2)
|
|
|
|
regex = None
|
|
required_tags = []
|
|
list_tags = False
|
|
list_tests = False
|
|
context = None
|
|
live_only = False
|
|
not_live_only = False
|
|
# parse command-line:
|
|
try:
|
|
opts, args = getopt.getopt( sys.argv[1:], 'hr:t:',
|
|
longopts=['help', 'regex=', 'tag=', 'list-tags', 'list-tests', 'context=', 'live', 'not-live'] )
|
|
except getopt.GetoptError as err:
|
|
log.e( err ) # something like "option -a not recognized"
|
|
usage()
|
|
for opt, arg in opts:
|
|
if opt in ('-h', '--help'):
|
|
usage()
|
|
elif opt in ('-r', '--regex'):
|
|
regex = arg
|
|
elif opt in ('-t', '--tag'):
|
|
required_tags.append( arg )
|
|
elif opt == '--list-tags':
|
|
list_tags = True
|
|
elif opt == '--list-tests':
|
|
list_tests = True
|
|
elif opt == '--context':
|
|
context = arg
|
|
elif opt == '--live':
|
|
if not_live_only:
|
|
raise RuntimeError( '--live and --not-live are mutually exclusive' )
|
|
live_only = True
|
|
elif opt == '--not-live':
|
|
if live_only:
|
|
raise RuntimeError( '--live and --not-live are mutually exclusive' )
|
|
not_live_only = True
|
|
|
|
if len( args ) != 2:
|
|
usage()
|
|
dir=args[0]
|
|
builddir=args[1]
|
|
if not os.path.isdir( dir ) or not os.path.isdir( builddir ):
|
|
usage()
|
|
|
|
# We have to stick to Unix conventions because CMake on Windows is fubar...
|
|
root = repo.root.replace( '\\' , '/' )
|
|
src = root + '/src'
|
|
|
|
|
|
def add_slash_before_spaces(links):
|
|
"""
|
|
This function adds '\' char before spaces in string or list of strings.
|
|
Because spaces in links can't been read properly from cmake files.
|
|
"""
|
|
if links and type(links) is str:
|
|
return links.replace(' ', '\ ')
|
|
if links and type(links) is list:
|
|
# Build list comprehension of strings with backslash before spaces in case the link.
|
|
# We can get a ${var} so, when we get this we return as is
|
|
return [link.replace(' ', '\ ') if link[0] != '$' else link for link in links]
|
|
else:
|
|
raise TypeError
|
|
|
|
|
|
def generate_cmake( builddir, testdir, testname, filelist, custom_main, dependencies ):
|
|
makefile = builddir + '/' + testdir + '/CMakeLists.txt'
|
|
log.d( ' creating:', makefile )
|
|
handle = open( makefile, 'w' )
|
|
|
|
#filelist = add_slash_before_spaces(filelist)
|
|
directory = add_slash_before_spaces(dir)
|
|
root_directory = add_slash_before_spaces(root)
|
|
|
|
filelist = '\n '.join( filelist )
|
|
handle.write( '''
|
|
# This file is automatically generated!!
|
|
# Do not modify or your changes will be lost!
|
|
|
|
cmake_minimum_required( VERSION 3.10.0 )
|
|
project( ''' + testname + ''' )
|
|
|
|
set( SRC_FILES ''' + filelist + '''
|
|
)
|
|
add_executable( ${PROJECT_NAME} ${SRC_FILES} )
|
|
add_definitions( ''' + ' '.join( f'-DLIBCI_DEPENDENCY_{d}' for d in dependencies.split() ) + ''' )
|
|
source_group( "Common Files" FILES ''' + directory + '''/test.cpp''')
|
|
if not custom_main:
|
|
handle.write(' ' + directory + '/unit-test-default-main.cpp')
|
|
handle.write( ''' )
|
|
target_link_libraries( ${PROJECT_NAME} ''' + dependencies + ''' Catch2 )
|
|
|
|
set_target_properties( ${PROJECT_NAME} PROPERTIES FOLDER "Unit-Tests/''' + os.path.dirname( testdir ) + '''" )
|
|
|
|
using_easyloggingpp( ${PROJECT_NAME} SHARED )
|
|
|
|
# Add the repo root directory (so includes into src/ will be specific: <src/...>)
|
|
target_include_directories( ${PROJECT_NAME} PRIVATE ''' + root + ''')
|
|
|
|
''' )
|
|
|
|
handle.close()
|
|
|
|
|
|
def find_include( include, relative_to ):
|
|
"""
|
|
Try to match the include to an existing file.
|
|
|
|
:param include: the text within "" or <> from the include directive
|
|
:param relative_to: the directory from which to start finding if include is non-absolute
|
|
:return: the normalized & absolute file path, if found -- otherwise, None
|
|
"""
|
|
if include:
|
|
if not os.path.isabs( include ):
|
|
include = os.path.normpath( relative_to + '/' + include )
|
|
include = include.replace( '\\', '/' )
|
|
if os.path.exists( include ):
|
|
return include
|
|
|
|
|
|
def find_include_in_dirs( include, dirs ):
|
|
"""
|
|
Search for the given include in all the specified directories
|
|
"""
|
|
for include_dir in dirs:
|
|
path = find_include( include, include_dir )
|
|
if path:
|
|
return path
|
|
|
|
|
|
def find_includes( filepath, filelist, dependencies ):
|
|
"""
|
|
Recursively searches a .cpp file for #include directives
|
|
:param filelist: any previous includes already processed (pass an empty dict() if none)
|
|
:param dependencies: set of dependencies
|
|
:return: a dictionary (include->source) of includes found
|
|
"""
|
|
include_dirs = list()
|
|
if 'realsense2' in dependencies:
|
|
include_dirs.append( os.path.join( root, 'include' ))
|
|
include_dirs.append( os.path.join( root, 'third-party', 'rsutils', 'include' ))
|
|
include_dirs.append( root )
|
|
|
|
filedir = os.path.dirname(filepath)
|
|
try:
|
|
log.debug_indent()
|
|
for include_line in file.grep( r'^\s*#\s*include\s+("(.*)"|<(.*)>)\s*$', filepath ):
|
|
m = include_line['match']
|
|
index = include_line['index']
|
|
include = find_include( m.group(2), filedir ) or find_include_in_dirs( m.group(2), include_dirs ) or find_include_in_dirs( m.group(3), include_dirs )
|
|
if include:
|
|
if include in filelist:
|
|
log.d( m.group(0), '->', include, '(already processed)' )
|
|
else:
|
|
log.d( m.group(0), '->', include )
|
|
filelist[include] = filepath
|
|
filelist = find_includes( include, filelist, dependencies )
|
|
else:
|
|
log.d( 'not found:', m.group(0) )
|
|
finally:
|
|
log.debug_unindent()
|
|
return filelist
|
|
|
|
def process_cpp( dir, builddir ):
|
|
global regex, required_tags, list_only, available_tags, tests_and_tags, live_only, not_live_only
|
|
found = []
|
|
shareds = []
|
|
statics = []
|
|
if regex:
|
|
pattern = re.compile( regex )
|
|
log.d( 'looking for C++ files in:', dir )
|
|
for f in file.find( dir, '(^|/)test-.*\.cpp$' ):
|
|
testdir = os.path.splitext( f )[0] # "log/internal/test-all" <- "log/internal/test-all.cpp"
|
|
testparent = os.path.dirname(testdir) # "log/internal"
|
|
# We need the project name unique: keep the path but make it nicer:
|
|
if testparent:
|
|
testname = 'test-' + testparent.replace( '/', '-' ) + '-' + os.path.basename( testdir )[
|
|
5:] # "test-log-internal-all"
|
|
else:
|
|
testname = testdir # no parent folder so we get "test-all"
|
|
|
|
if regex and not pattern.search( testname ):
|
|
continue
|
|
|
|
log.d( '... found:', f )
|
|
log.debug_indent()
|
|
try:
|
|
config = libci.TestConfigFromCpp( dir + os.sep + f, context )
|
|
if required_tags or list_tags:
|
|
if not all( tag in config.tags for tag in required_tags ):
|
|
continue
|
|
available_tags.update( config.tags )
|
|
if list_tests:
|
|
tests_and_tags[ testname ] = config.tags
|
|
|
|
if testname not in tests_and_tags:
|
|
tests_and_tags[testname] = None
|
|
|
|
if live_only:
|
|
if not config.configurations:
|
|
continue
|
|
elif not_live_only:
|
|
if config.configurations:
|
|
continue
|
|
|
|
if config.donotrun:
|
|
continue
|
|
|
|
# Build the list of files we want in the project:
|
|
# At a minimum, we have the original file, plus any common files
|
|
filelist = [ dir + '/' + f ]
|
|
includes = dict()
|
|
# Add any files explicitly listed in the .cpp itself, like this:
|
|
# //#cmake:add-file <filename>
|
|
# Any files listed are relative to $dir
|
|
shared = False
|
|
static = False
|
|
custom_main = False
|
|
dependencies = 'realsense2'
|
|
for cmake_directive in file.grep( '^//#cmake:\s*', dir + '/' + f ):
|
|
m = cmake_directive['match']
|
|
index = cmake_directive['index']
|
|
cmd, *rest = cmake_directive['line'][m.end():].split()
|
|
if cmd == 'add-file':
|
|
for additional_file in rest:
|
|
files = additional_file
|
|
if not os.path.isabs( additional_file ):
|
|
files = dir + '/' + testparent + '/' + additional_file
|
|
files = glob( files )
|
|
if not files:
|
|
log.e( f + '+' + str(index) + ': no files match "' + additional_file + '"' )
|
|
for abs_file in files:
|
|
abs_file = os.path.normpath( abs_file )
|
|
abs_file = abs_file.replace( '\\', '/' )
|
|
if not os.path.exists( abs_file ):
|
|
log.e( f + '+' + str(index) + ': file not found "' + additional_file + '"' )
|
|
log.d( 'add file:', abs_file )
|
|
filelist.append( abs_file )
|
|
if( os.path.splitext( abs_file )[0] == 'cpp' ):
|
|
# Add any "" includes specified in the .cpp that we can find
|
|
includes = find_includes( abs_file, includes, dependencies )
|
|
elif cmd == 'static!':
|
|
if len(rest):
|
|
log.e( f + '+' + str(index) + ': unexpected arguments past \'' + cmd + '\'' )
|
|
elif shared:
|
|
log.e( f + '+' + str(index) + ': \'' + cmd + '\' mutually exclusive with \'shared!\'' )
|
|
else:
|
|
log.d( 'static!' )
|
|
static = True
|
|
elif cmd == 'shared!':
|
|
if len(rest):
|
|
log.e( f + '+' + str(index) + ': unexpected arguments past \'' + cmd + '\'' )
|
|
elif static:
|
|
log.e( f + '+' + str(index) + ': \'' + cmd + '\' mutually exclusive with \'static!\'' )
|
|
else:
|
|
log.d( 'shared!' )
|
|
shared = True
|
|
elif cmd == 'custom-main':
|
|
custom_main = True
|
|
elif cmd == 'dependencies':
|
|
dependencies = ' '.join( rest )
|
|
else:
|
|
log.e( f + '+' + str(index) + ': unknown cmd \'' + cmd + '\' (should be \'add-file\', \'static!\', or \'shared!\')' )
|
|
|
|
# Add any includes specified in the .cpp that we can find
|
|
includes = find_includes( dir + '/' + f, includes, dependencies )
|
|
for include,source in includes.items():
|
|
filelist.append( f'"{include}" # {source}' )
|
|
|
|
# all tests use the common test.cpp file
|
|
filelist.append( root + "/unit-tests/test.cpp" )
|
|
|
|
# 'cmake:custom-main' indicates that the test is defining its own main() function.
|
|
# If not specified we use a default main() which lives in its own .cpp:
|
|
if not custom_main:
|
|
filelist.append( root + "/unit-tests/unit-test-default-main.cpp" )
|
|
|
|
if list_only:
|
|
continue
|
|
|
|
# Each CMakeLists.txt sits in its own directory
|
|
os.makedirs( builddir + '/' + testdir, exist_ok=True ) # "build/log/internal/test-all"
|
|
generate_cmake( builddir, testdir, testname, filelist, custom_main, dependencies )
|
|
if static:
|
|
statics.append( testdir )
|
|
elif shared:
|
|
shareds.append( testdir )
|
|
else:
|
|
found.append( testdir )
|
|
finally:
|
|
log.debug_unindent()
|
|
return found, shareds, statics
|
|
def process_py( dir, builddir ):
|
|
# TODO
|
|
return [],[],[]
|
|
|
|
list_only = list_tags or list_tests
|
|
available_tags = set()
|
|
tests_and_tags = dict()
|
|
normal_tests = []
|
|
shared_tests = []
|
|
static_tests = []
|
|
n,sh,st = process_cpp( dir, builddir )
|
|
|
|
if list_only:
|
|
if list_tags and list_tests:
|
|
for t in sorted( tests_and_tags.keys() ):
|
|
print( t, "has tags:", ' '.join( tests_and_tags[t] ) )
|
|
#
|
|
elif list_tags:
|
|
for t in sorted( list( available_tags ) ):
|
|
print( t )
|
|
#
|
|
elif list_tests:
|
|
for t in sorted( tests_and_tags.keys() ):
|
|
print( t )
|
|
sys.exit( 0 )
|
|
|
|
normal_tests.extend( n )
|
|
shared_tests.extend( sh )
|
|
static_tests.extend( st )
|
|
n,sh,st = process_py( dir, builddir )
|
|
normal_tests.extend( n )
|
|
shared_tests.extend( sh )
|
|
static_tests.extend( st )
|
|
|
|
cmakefile = builddir + '/CMakeLists.txt'
|
|
name = os.path.basename( os.path.realpath( dir ))
|
|
log.d( 'Creating "' + name + '" project in', cmakefile )
|
|
|
|
handle = open( cmakefile, 'w' )
|
|
handle.write( '''
|
|
|
|
''' )
|
|
|
|
n_tests = 0
|
|
for sdir in normal_tests:
|
|
handle.write( 'add_subdirectory( ' + sdir + ' )\n' )
|
|
log.d( '... including:', sdir )
|
|
n_tests += 1
|
|
if len(shared_tests):
|
|
handle.write( 'if(NOT ${BUILD_SHARED_LIBS})\n' )
|
|
handle.write( ' message( INFO " ' + str(len(shared_tests)) + ' shared lib unit-tests will be skipped. Check BUILD_SHARED_LIBS to run them..." )\n' )
|
|
handle.write( 'else()\n' )
|
|
for test in shared_tests:
|
|
handle.write( ' add_subdirectory( ' + test + ' )\n' )
|
|
log.d( '... including:', sdir )
|
|
n_tests += 1
|
|
handle.write( 'endif()\n' )
|
|
if len(static_tests):
|
|
handle.write( 'if(${BUILD_SHARED_LIBS})\n' )
|
|
handle.write( ' message( INFO " ' + str(len(static_tests)) + ' static lib unit-tests will be skipped. Uncheck BUILD_SHARED_LIBS to run them..." )\n' )
|
|
handle.write( 'else()\n' )
|
|
for test in static_tests:
|
|
handle.write( ' add_subdirectory( ' + test + ' )\n' )
|
|
log.d( '... including:', sdir )
|
|
n_tests += 1
|
|
handle.write( 'endif()\n' )
|
|
handle.close()
|
|
|
|
print( 'Generated ' + str(n_tests) + ' unit-tests' )
|
|
if log.n_errors():
|
|
sys.exit(1)
|
|
sys.exit(0)
|
|
|