run_python_test.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. #!/usr/bin/env -S python3 -B
  2. # Copyright (c) 2022 Project CHIP Authors
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import datetime
  16. import logging
  17. import os
  18. import queue
  19. import re
  20. import shlex
  21. import signal
  22. import subprocess
  23. import sys
  24. import threading
  25. import time
  26. import typing
  27. import click
  28. import coloredlogs
  29. from colorama import Fore, Style
  30. DEFAULT_CHIP_ROOT = os.path.abspath(
  31. os.path.join(os.path.dirname(__file__), '..', '..'))
  32. MATTER_DEVELOPMENT_PAA_ROOT_CERTS = "credentials/development/paa-root-certs"
  33. def EnqueueLogOutput(fp, tag, q):
  34. for line in iter(fp.readline, b''):
  35. timestamp = time.time()
  36. if len(line) > len('[1646290606.901990]') and line[0:1] == b'[':
  37. try:
  38. timestamp = float(line[1:18].decode())
  39. line = line[19:]
  40. except Exception:
  41. pass
  42. sys.stdout.buffer.write(
  43. (f"[{datetime.datetime.fromtimestamp(timestamp).isoformat(sep=' ')}]").encode() + tag + line)
  44. sys.stdout.flush()
  45. fp.close()
  46. def RedirectQueueThread(fp, tag, queue) -> threading.Thread:
  47. log_queue_thread = threading.Thread(target=EnqueueLogOutput, args=(
  48. fp, tag, queue))
  49. log_queue_thread.start()
  50. return log_queue_thread
  51. def DumpProgramOutputToQueue(thread_list: typing.List[threading.Thread], tag: str, process: subprocess.Popen, queue: queue.Queue):
  52. thread_list.append(RedirectQueueThread(process.stdout,
  53. (f"[{tag}][{Fore.YELLOW}STDOUT{Style.RESET_ALL}]").encode(), queue))
  54. thread_list.append(RedirectQueueThread(process.stderr,
  55. (f"[{tag}][{Fore.RED}STDERR{Style.RESET_ALL}]").encode(), queue))
  56. @click.command()
  57. @click.option("--app", type=click.Path(exists=True), default=None,
  58. help='Path to local application to use, omit to use external apps.')
  59. @click.option("--factoryreset", is_flag=True,
  60. help='Remove app config and repl configs (/tmp/chip* and /tmp/repl*) before running the tests.')
  61. @click.option("--app-args", type=str, default='',
  62. help='The extra arguments passed to the device.')
  63. @click.option("--script", type=click.Path(exists=True), default=os.path.join(DEFAULT_CHIP_ROOT,
  64. 'src',
  65. 'controller',
  66. 'python',
  67. 'test',
  68. 'test_scripts',
  69. 'mobile-device-test.py'), help='Test script to use.')
  70. @click.option("--script-args", type=str, default='',
  71. help='Path to the test script to use, omit to use the default test script (mobile-device-test.py).')
  72. @click.option("--script-gdb", is_flag=True,
  73. help='Run script through gdb')
  74. def main(app: str, factoryreset: bool, app_args: str, script: str, script_args: str, script_gdb: bool):
  75. if factoryreset:
  76. # Remove native app config
  77. retcode = subprocess.call("rm -rf /tmp/chip* /tmp/repl*", shell=True)
  78. if retcode != 0:
  79. raise Exception("Failed to remove /tmp/chip* for factory reset.")
  80. print("Contents of test directory: %s" % os.getcwd())
  81. print(subprocess.check_output(["ls -l"], shell=True).decode('utf-8'))
  82. # Remove native app KVS if that was used
  83. kvs_match = re.search(r"--KVS (?P<kvs_path>[^ ]+)", app_args)
  84. if kvs_match:
  85. kvs_path_to_remove = kvs_match.group("kvs_path")
  86. retcode = subprocess.call("rm -f %s" % kvs_path_to_remove, shell=True)
  87. print("Trying to remove KVS path %s" % kvs_path_to_remove)
  88. if retcode != 0:
  89. raise Exception("Failed to remove %s for factory reset." % kvs_path_to_remove)
  90. # Remove Python test admin storage if provided
  91. storage_match = re.search(r"--storage-path (?P<storage_path>[^ ]+)", script_args)
  92. if storage_match:
  93. storage_path_to_remove = storage_match.group("storage_path")
  94. retcode = subprocess.call("rm -f %s" % storage_path_to_remove, shell=True)
  95. print("Trying to remove storage path %s" % storage_path_to_remove)
  96. if retcode != 0:
  97. raise Exception("Failed to remove %s for factory reset." % storage_path_to_remove)
  98. coloredlogs.install(level='INFO')
  99. log_queue = queue.Queue()
  100. log_cooking_threads = []
  101. app_process = None
  102. if app:
  103. if not os.path.exists(app):
  104. if app is None:
  105. raise FileNotFoundError(f"{app} not found")
  106. app_args = [app] + shlex.split(app_args)
  107. logging.info(f"Execute: {app_args}")
  108. app_process = subprocess.Popen(
  109. app_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
  110. DumpProgramOutputToQueue(
  111. log_cooking_threads, Fore.GREEN + "APP " + Style.RESET_ALL, app_process, log_queue)
  112. script_command = [script, "--paa-trust-store-path", os.path.join(DEFAULT_CHIP_ROOT, MATTER_DEVELOPMENT_PAA_ROOT_CERTS),
  113. '--log-format', '%(message)s'] + shlex.split(script_args)
  114. if script_gdb:
  115. #
  116. # When running through Popen, we need to preserve some space-delimited args to GDB as a single logical argument.
  117. # To do that, let's use '|' as a placeholder for the space character so that the initial split will not tokenize them,
  118. # and then replace that with the space char there-after.
  119. #
  120. script_command = ("gdb -batch -return-child-result -q -ex run -ex "
  121. "thread|apply|all|bt --args python3".split() + script_command)
  122. else:
  123. script_command = "/usr/bin/env python3".split() + script_command
  124. final_script_command = [i.replace('|', ' ') for i in script_command]
  125. logging.info(f"Execute: {final_script_command}")
  126. test_script_process = subprocess.Popen(
  127. final_script_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  128. DumpProgramOutputToQueue(log_cooking_threads, Fore.GREEN + "TEST" + Style.RESET_ALL,
  129. test_script_process, log_queue)
  130. test_script_exit_code = test_script_process.wait()
  131. if test_script_exit_code != 0:
  132. logging.error("Test script exited with error %r" % test_script_exit_code)
  133. test_app_exit_code = 0
  134. if app_process:
  135. logging.warning("Stopping app with SIGINT")
  136. app_process.send_signal(signal.SIGINT.value)
  137. test_app_exit_code = app_process.wait()
  138. # There are some logs not cooked, so we wait until we have processed all logs.
  139. # This procedure should be very fast since the related processes are finished.
  140. for thread in log_cooking_threads:
  141. thread.join()
  142. if test_script_exit_code != 0:
  143. sys.exit(test_script_exit_code)
  144. else:
  145. # We expect both app and test script should exit with 0
  146. sys.exit(test_app_exit_code)
  147. if __name__ == '__main__':
  148. main(auto_envvar_prefix='CHIP')