6 import sys, os, subprocess, locale, re, platform, getopt, time
7 from abc
import ABC, abstractmethod
14 sys.path.append( os.path.dirname( sys.executable ))
15 sys.path.append( os.path.join( os.path.dirname( sys.executable ),
'DLLs' ))
16 sys.path.append( os.path.join( os.path.dirname( sys.executable ),
'lib' ))
18 current_dir = os.path.dirname( os.path.abspath( __file__ ))
19 sys.path.append( current_dir + os.sep +
"py" )
21 from rspy
import log, file, repo
24 ourname = os.path.basename(sys.argv[0])
25 print(
'Syntax: ' + ourname +
' [options] [dir]' )
26 print(
' dir: the directory holding the executable tests to run (default to the build directory')
28 print(
' --debug Turn on debugging information' )
29 print(
' -v, --verbose Errors will dump the log to stdout' )
30 print(
' -q, --quiet Suppress output; rely on exit status (0=no failures)' )
31 print(
' -r, --regex run all tests that fit the following regular expression' )
32 print(
' -s, --stdout do not redirect stdout to logs' )
33 print(
' -t, --tag run all tests with the following tag' )
34 print(
' --list-tags print out all available tags. This option will not run any tests')
35 print(
' --list-tests print out all available tests. This option will not run any tests')
40 system = platform.system()
41 if system ==
'Linux' and "microsoft" not in platform.uname()[3].lower():
48 opts,args = getopt.getopt( sys.argv[1:],
'hvqr:st:',
49 longopts = [
'help',
'verbose',
'debug',
'quiet',
'regex=',
'stdout',
'tag=',
'list-tags',
'list-tests' ])
50 except getopt.GetoptError
as err:
59 if opt
in (
'-h',
'--help'):
61 elif opt
in (
'-v',
'--verbose'):
63 elif opt
in (
'-q',
'--quiet'):
65 elif opt
in (
'-r',
'--regex'):
67 elif opt
in (
'-s',
'--stdout'):
69 elif opt
in (
'-t',
'--tag'):
71 elif opt ==
'--list-tags':
73 elif opt ==
'--list-tests':
80 if os.path.isdir( args[0] ):
86 build = repo.root + os.sep +
'build' 87 for executable
in file.find(build,
'(^|/)test-.*'):
88 if not file.is_executable(executable):
90 dir_with_test = build + os.sep + os.path.dirname(executable)
91 if target
and target != dir_with_test:
92 log.e(
"Found executable tests in 2 directories:", target,
"and", dir_with_test,
". Can't default to directory")
94 target = dir_with_test
97 logdir = target + os.sep +
'unit-tests' 99 logdir = os.path.join( repo.root,
'build',
'unit-tests' )
100 os.makedirs( logdir, exist_ok =
True )
108 for so
in file.find(repo.root,
'(^|/)pyrealsense2.*\.so$'):
111 for pyd
in file.find(repo.root,
'(^|/)pyrealsense2.*\.pyd$'):
117 pyrs_path = repo.root + os.sep + pyrs
119 pyrs_path = os.path.dirname(pyrs_path)
120 log.d(
'found pyrealsense pyd in:', pyrs_path )
123 log.d(
'assuming executable path same as pyd path' )
130 os.environ[
"PYTHONPATH"] = current_dir + os.sep +
"py" 133 os.environ[
"PYTHONPATH"] += os.pathsep + pyrs_path
137 Wrapper function for subprocess.run. 138 If the child process times out or ends with a non-zero exit status an exception is raised! 140 :param cmd: the command and argument for the child process, as a list 141 :param stdout: path of file to direct the output of the process to (None to disable) 142 :param timeout: number of seconds to give the process before forcefully ending it (None to disable) 143 :param append: if True and stdout is not None, the log of the test will be appended to the file instead of 145 :return: the output written by the child, if stdout is None -- otherwise N/A 147 log.d(
'running:', cmd )
149 start_time = time.time()
152 if stdout
and stdout != subprocess.PIPE:
154 handle = open(stdout,
"a" )
155 handle.write(
"\n---------------------------------------------------------------------------------\n\n")
158 handle = open( stdout,
"w" )
160 rv = subprocess.run( cmd,
162 stderr = subprocess.STDOUT,
163 universal_newlines =
True,
170 result = result.split(
'\n' )
176 run_time = time.time() - start_time
177 log.d(
"test took", run_time,
"seconds")
181 """ Return a string repr (with a prefix and/or suffix) of the configuration or '' if it's None """ 182 if configuration
is None:
184 return prefix +
'[' +
' '.
join( configuration ) +
']' + suffix
195 if path_to_log
is None:
198 for ctx
in file.grep(
r'^test cases:\s*(\d+) \|\s*(\d+) (passed|failed)|^-+$', path_to_log ):
200 if m.string ==
"---------------------------------------------------------------------------------":
208 total = int(results.group(1))
209 passed = int(results.group(2))
210 if results.group(3) ==
'failed':
212 passed = total - passed
214 if total == 1
or passed == 0:
217 desc =
str(total - passed) +
' of ' +
str(total) +
' failed' 219 if log.is_verbose_on():
220 log.e( log.red + testname + log.reset +
': ' +
configuration_str( configuration, suffix =
' ' ) + desc )
223 file.cat( path_to_log )
226 log.e( log.red + testname + log.reset +
': ' +
configuration_str( configuration, suffix =
' ' ) + desc +
'; see ' + path_to_log )
233 Configuration for a test, encompassing any metadata needed to control its run, like retries etc. 236 self._configurations = list()
237 self._priority = 1000
243 if self._priority != 1000:
244 log.d(
'priority:', self._priority )
245 if self._timeout != 200:
246 log.d(
'timeout:', self._timeout)
248 log.d(
'tags:', self._tags )
250 log.d(
'flags:', self._flags )
251 if len(self._configurations) > 1:
252 log.d( len( self._configurations ),
'configurations' )
257 return self._configurations
261 return self._priority
278 Configuration for a test -- from any text-based syntax with a given prefix, e.g. for python: 280 #test:device L500* D400* 283 And, for C++ the prefix could be: 288 :param source: The path to the text file 289 :param line_prefix: A regex to denote a directive (must be first thing in a line), which will 290 be immediately followed by the directive itself and optional arguments 292 TestConfig.__init__(self)
295 regex =
r'^' + line_prefix +
r'(\S+)((?:\s+\S+)*?)\s*(?:#\s*(.*))?$' 296 for context
in file.grep( regex, source ):
297 match = context[
'match']
298 directive = match.group(1)
299 text_params = match.group(2).strip()
300 params = [s
for s
in text_params.split()]
301 comment = match.group(3)
302 if directive ==
'device':
305 log.e( source +
'+' +
str(context[
'index']) +
': device directive with no devices listed' )
306 elif 'each' in text_params.lower()
and len(params) > 1:
307 log.e( source +
'+' +
str(context[
'index']) +
': each() cannot be used in combination with other specs', params )
308 elif 'each' in text_params.lower()
and not re.fullmatch(
r'each\(.+\)', text_params, re.IGNORECASE ):
309 log.e( source +
'+' +
str(context[
'index']) +
': invalid \'each\' syntax:', params )
311 self._configurations.append( params )
312 elif directive ==
'priority':
313 if len(params) == 1
and params[0].isdigit():
316 log.e( source +
'+' +
str(context[
'index']) +
': priority directive with invalid parameters:', params )
317 elif directive ==
'timeout':
318 if len(params) == 1
and params[0].isdigit():
321 log.e( source +
'+' +
str(context[
'index']) +
': timeout directive with invalid parameters:', params )
322 elif directive ==
'tag':
323 self._tags.update(params)
324 elif directive ==
'flag':
325 self._flags.update( params )
327 log.e( source +
'+' +
str(context[
'index']) +
': invalid directive "' + directive +
'"; ignoring' )
332 Abstract class for a test. Holds the name of the test 336 self._name = testname
341 def run_test( self, configuration = None, log_path = None ):
346 self._config.debug_dump()
365 path = logdir + os.sep + self.name +
".log" 370 Returns True if the test configurations specify devices (test has a 'device' directive) 372 return self._config
and len(self._config.configurations) > 0
377 Class for python tests. Hold the path to the script of the test 381 :param testname: name of the test 382 :param path_to_test: the relative path from the current directory to the path 385 Test.__init__(self, testname)
391 Test.debug_dump(self)
395 cmd = [sys.executable]
401 if sys.flags.verbose:
404 if 'custom-args' not in self.config.flags:
405 if log.is_debug_on():
407 if log.is_color_on():
411 def run_test( self, configuration = None, log_path = None ):
420 Class for c/cpp tests. Hold the path to the executable for the test 424 :param testname: name of the test 425 :param exe: full path to executable 428 Test.__init__(self, testname)
439 split_testname = testname.split(
'-' )
440 cpp_path = current_dir
441 found_test_dir =
False 443 while not found_test_dir:
445 found_test_dir =
True 446 for i
in range(2, len(split_testname) ):
447 sub_dir_path = cpp_path + os.sep +
'-'.
join(split_testname[1:i])
448 if os.path.isdir(sub_dir_path):
449 cpp_path = sub_dir_path
450 del split_testname[1:i]
451 found_test_dir =
False 454 cpp_path += os.sep +
'-'.
join( split_testname )
455 if os.path.isfile( cpp_path +
".cpp" ):
459 log.w( log.red + testname + log.reset +
':',
'No matching .cpp file was found; no configuration will be used!' )
464 if 'custom-args' not in self.config.flags:
468 if log.is_debug_on():
475 def run_test( self, configuration = None, log_path = None ):
483 global regex, target, pyrs, current_dir, linux
485 pattern = re.compile(regex)
491 manifestfile = target +
'/CMakeFiles/TargetDirectories.txt' 493 manifestfile = target +
'/../CMakeFiles/TargetDirectories.txt' 495 for manifest_ctx
in file.grep(
r'(?<=unit-tests/build/)\S+(?=/CMakeFiles/test-\S+.dir$)', manifestfile):
497 testdir = manifest_ctx[
'match'].group(0)
499 testparent = os.path.dirname(testdir)
501 testname =
'test-' + testparent.replace(
'/',
'-') +
'-' + os.path.basename(testdir)[5:]
505 if regex
and not pattern.search( testname ):
509 exe = target +
'/unit-tests/build/' + testdir +
'/' + testname
511 exe = target +
'/' + testname +
'.exe' 517 for py_test
in file.find(current_dir,
'(^|/)test-.*\.py'):
518 testparent = os.path.dirname(py_test)
520 testname =
'test-' + testparent.replace(
'/',
'-') +
'-' + os.path.basename(py_test)[5:-3]
522 testname = os.path.basename(py_test)[:-3]
524 if regex
and not pattern.search( testname ):
527 yield PyTest(testname, py_test)
531 return sorted(tests, key=
lambda t: t.config.priority)
535 Yield <configuration,serial-numbers> pairs for each valid configuration under which the 538 The <configuration> is a list of ('test:device') designations, e.g. ['L500*', 'D415']. 539 The <serial-numbers> is a set of device serial-numbers that fit this configuration. 541 :param test: The test (of class type Test) we're interested in 543 for configuration
in test.config.configurations:
545 for serial_numbers
in devices.by_configuration( configuration ):
546 yield configuration, serial_numbers
547 except RuntimeError
as e:
549 log.e( log.red + test.name + log.reset +
': ' +
str(e) )
551 log.w( log.yellow + test.name + log.reset +
': ' +
str(e) )
555 log.i(
'Logs in:', logdir )
560 if not log.is_debug_on()
or log.is_color_on():
561 log.progress(
configuration_str( configuration, suffix =
' ' ) + test.name,
'...' )
563 log_path = test.get_log()
565 test.run_test( configuration = configuration, log_path = log_path )
566 except FileNotFoundError
as e:
567 log.e( log.red + test.name + log.reset +
':',
str(e) +
configuration_str( configuration, prefix =
' ' ) )
568 except subprocess.TimeoutExpired:
569 log.e(log.red + test.name + log.reset +
':',
configuration_str(configuration, suffix=
' ') +
'timed out')
570 except subprocess.CalledProcessError
as cpe:
573 log.e( log.red + test.name + log.reset +
':',
configuration_str( configuration, suffix =
' ' ) +
'exited with non-zero value (' +
str(cpe.returncode) +
')' )
577 list_only = list_tags
or list_tests
580 sys.path.append( pyrs_path )
581 from rspy
import devices
585 skip_live_tests = len(devices.all()) == 0
and not devices.acroname
592 log.d(
'found', test.name,
'...' )
597 if tag
and tag
not in test.config.tags:
598 log.d(
'does not fit --tag:', test.config.tags )
601 tags.update( test.config.tags )
602 tests.append( test.name )
607 if not test.is_live():
612 log.w( test.name +
':',
'is live and there are no cameras; skipping' )
617 log.d(
'configuration:', configuration )
619 devices.enable_only( serial_numbers, recycle =
True )
620 except RuntimeError
as e:
621 log.w( log.red + test.name + log.reset +
': ' +
str(e) )
634 log.e(
'No unit-tests found!' )
639 print(
"Available tags:" )
640 for t
in sorted( list( tags )):
644 print(
"Available tests:" )
645 for t
in sorted( tests ):
649 n_errors = log.n_errors()
651 log.out( log.red +
str(n_errors) + log.reset,
'of', n_tests,
'test(s)', log.red +
'failed!' + log.reset + log.clear_eos )
654 log.out(
str(n_tests) +
' unit-test(s) completed successfully' + log.clear_eos )
def run_test(self, configuration=None, log_path=None)
std::string join(const std::string &base, const std::string &path)
def prioritize_tests(tests)
def __init__(self, testname, path_to_test)
def __init__(self, testname, exe)
def run_test(self, configuration=None, log_path=None)
def check_log_for_fails(path_to_log, testname, configuration=None)
def __init__(self, source, line_prefix)
def devices_by_test_config(test)
def run_test(self, configuration=None, log_path=None)
static std::string print(const transformation &tf)
def test_wrapper(test, configuration=None)
def configuration_str(configuration, prefix='', suffix='')
def subprocess_run(cmd, stdout=None, timeout=200, append=False)