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

#!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)