text: #!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
  from glob import glob
  
  if len(sys.argv) != 3:
      ourname = os.path.basename(sys.argv[0])
      print( 'Syntax: ' + ourname + ' <dir> <build-dir>' )
      print( '        build unit-testing framework for the tree in $dir' )
      exit(1)
  dir=sys.argv[1]
  builddir=sys.argv[2]
  if not os.path.isdir( dir ):
      print( 'FATAL  Directory not found:', dir )
      exit(1)
  if not os.path.isdir( builddir ):
      print( 'FATAL  Directory not found:', builddir )
      exit(1)
  
  have_errors = False
  
  def debug(*args):
      #print( '-D-', *args )
      pass
  def error(*args):
      print( '-E-', *args )
      global have_errors
      have_errors = True
  def filesin( root ):
      # Yield all files found in root, using relative names ('root/a' would be yielded as 'a')
      for (path,subdirs,leafs) in os.walk( root ):
          for leaf in leafs:
              # We have to stick to Unix conventions because CMake on Windows is fubar...
              yield os.path.relpath( path + '/' + leaf, root ).replace( '\\', '/' )
  def find( dir, mask ):
      pattern = re.compile( mask )
      for leaf in filesin( dir ):
          if pattern.search( leaf ):
              debug(leaf)
              yield leaf
  
  
  def remove_newlines (lines):
      for line in lines:
          if line[-1] == '\n':
              line = line[:-1]    # excluding the endline
          yield line
  
  def grep_( pattern, lines, context ):
      index = 0
      matches = 0
      for line in lines:
          index = index + 1
          match = pattern.search( line )
          if match:
              context['index'] = index
              context['line']  = line
              context['match'] = match
              yield context
              matches = matches + 1
      if matches:
          del context['index']
          del context['line']
          del context['match']
      # UnicodeDecodeError can be thrown in binary files
  
  def grep( expr, *args ):
      #debug( f"grep {expr} {args}" )
      pattern = re.compile( expr )
      context = dict()
      for filename in args:
          context['filename'] = filename
          with open( filename, errors = 'ignore' ) as file:
              for line in grep_( pattern, remove_newlines( file ), context ):
                  yield context
  
  def generate_cmake( builddir, testdir, testname, filelist ):
      makefile = builddir + '/' + testdir + '/CMakeLists.txt'
      debug( '   creating:', makefile )
      handle = open( makefile, 'w' );
      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.1.0 )
  project( ''' + testname + ''' )
  
  set( SRC_FILES ''' + filelist + '''
  )
  add_executable( ''' + testname + ''' ${SRC_FILES} )
  source_group( "Common Files" FILES ${ELPP_FILES} ${CATCH_FILES} )
  set_property(TARGET ''' + testname + ''' PROPERTY CXX_STANDARD 11)
  target_link_libraries( ''' + testname + ''' ${DEPENDENCIES})
  
  set_target_properties( ''' + testname + ''' PROPERTIES FOLDER "Unit-Tests/''' + os.path.dirname( testdir ) + '''" )
  
  ''' )
      handle.close()
  
  # Recursively searches a .cpp file for #include directives and returns
  # a set of all of them.
  #
  # Only directives that are relative to the current path (#include "<path>")
  # are looked for!
  #
  def find_includes( filepath ):
      filelist = set()
      filedir = os.path.dirname(filepath)
      for context in grep( '^\s*#\s*include\s+"(.*)"\s*$', filepath ):
          m = context['match']
          index = context['index']
          include = m.group(1)
          if not os.path.isabs( include ):
              include = os.path.normpath( filedir + '/' + include )
          include = include.replace( '\\', '/' )
          if os.path.exists( include ):
              filelist.add( include )
              filelist |= find_includes( include )
      return filelist
  
  def process_cpp( dir, builddir ):
      found = []
      shareds = []
      statics = []
      for f in 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"
          # Each CMakeLists.txt sits in its own directory
          os.makedirs( builddir + '/' + testdir, exist_ok = True );   # "build/log/internal/test-all"
          # We need the project name unique: keep the path but make it nicer:
          testname = 'test-' + testparent.replace( '/', '-' ) + '-' + os.path.basename(testdir)[5:]   # "test-log-internal-all"
          # 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, '${ELPP_FILES}', '${CATCH_FILES}' ]
          # Add any "" includes specified in the .cpp that we can find
          includes = find_includes( dir + '/' + f )
          # 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
          for context in grep( '^//#cmake:\s*', dir + '/' + f ):
              m = context['match']
              index = context['index']
              cmd, *rest = context['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:
                          error( 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 ):
                              error( f + '+' + str(index) + ': file not found "' + additional_file + '"' )
                          debug( '   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 )
              elif cmd == 'static!':
                  if len(rest):
                      error( f + '+' + str(index) + ': unexpected arguments past \'' + cmd + '\'' )
                  elif shared:
                      error( f + '+' + str(index) + ': \'' + cmd + '\' mutually exclusive with \'shared!\'' )
                  else:
                      static = True
              elif cmd == 'shared!':
                  if len(rest):
                      error( f + '+' + str(index) + ': unexpected arguments past \'' + cmd + '\'' )
                  elif static:
                      error( f + '+' + str(index) + ': \'' + cmd + '\' mutually exclusive with \'static!\'' )
                  else:
                      shared = True
              else:
                  error( f + '+' + str(index) + ': unknown cmd \'' + cmd + '\' (should be \'add-file\', \'static!\', or \'shared!\')' )
          for include in includes:
              filelist.append( include )
          generate_cmake( builddir, testdir, testname, filelist )
          if static:
              statics.append( testdir )
          elif shared:
              shareds.append( testdir )
          else:
              found.append( testdir )
      return found, shareds, statics
  def process_py( dir, builddir ):
      # TODO
      return [],[],[]
  
  normal_tests = []
  shared_tests = []
  static_tests = []
  n,sh,st = process_cpp( dir, builddir )
  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 ))
  debug( 'Creating "' + name + '" project in', cmakefile )
  
  handle = open( cmakefile, 'w' );
  handle.write( '''
  
  # We make use of ELPP (EasyLogging++):
  include_directories( ''' + dir +  '''/../third-party/easyloggingpp/src )
  set( ELPP_FILES
      ''' + dir + '''/../third-party/easyloggingpp/src/easylogging++.cc
      ''' + dir + '''/../third-party/easyloggingpp/src/easylogging++.h
  )
  set( CATCH_FILES
      ''' + dir + '''/catch/catch.hpp
  )
  
  ''' )
  
  n_tests = 0
  for sdir in normal_tests:
      handle.write( 'add_subdirectory( ' + sdir + ' )\n' )
      debug( '... 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' )
          debug( '... 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' )
          debug( '... including:', sdir )
          n_tests += 1
      handle.write( 'endif()\n' )
  handle.close()
  
  print( 'Generated ' + str(n_tests) + ' unit-tests' )
  if have_errors:
      exit(1)
  exit(0)
  
  