run_python_test.py 8.1 KB

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