run_python_test.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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 click
  16. import coloredlogs
  17. import datetime
  18. import logging
  19. import os
  20. import pathlib
  21. import pty
  22. import queue
  23. import shlex
  24. import signal
  25. import subprocess
  26. import sys
  27. import threading
  28. import time
  29. import typing
  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 as ex:
  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, help='Path to local application to use, omit to use external apps.')
  59. @click.option("--factoryreset", is_flag=True, help='Remove app config and repl configs (/tmp/chip* and /tmp/repl*) before running the tests.')
  60. @click.option("--app-args", type=str, default='', help='The extra arguments passed to the device.')
  61. @click.option("--script", type=click.Path(exists=True), default=os.path.join(DEFAULT_CHIP_ROOT, 'src', 'controller', 'python', 'test', 'test_scripts', 'mobile-device-test.py'), help='Test script to use.')
  62. @click.option("--script-args", type=str, default='', help='Path to the test script to use, omit to use the default test script (mobile-device-test.py).')
  63. @click.option("--script-gdb", is_flag=True, help='Run script through gdb')
  64. def main(app: str, factoryreset: bool, app_args: str, script: str, script_args: str, script_gdb: bool):
  65. if factoryreset:
  66. retcode = subprocess.call("rm -rf /tmp/chip* /tmp/repl*", shell=True)
  67. if retcode != 0:
  68. raise Exception("Failed to remove /tmp/chip* for factory reset.")
  69. coloredlogs.install(level='INFO')
  70. log_queue = queue.Queue()
  71. log_cooking_threads = []
  72. app_process = None
  73. if app:
  74. if not os.path.exists(app):
  75. if app is None:
  76. raise FileNotFoundError(f"{app} not found")
  77. app_args = [app] + shlex.split(app_args)
  78. logging.info(f"Execute: {app_args}")
  79. app_process = subprocess.Popen(
  80. app_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
  81. DumpProgramOutputToQueue(
  82. log_cooking_threads, Fore.GREEN + "APP " + Style.RESET_ALL, app_process, log_queue)
  83. script_command = [script, "--paa-trust-store-path", os.path.join(DEFAULT_CHIP_ROOT, MATTER_DEVELOPMENT_PAA_ROOT_CERTS),
  84. '--log-format', '%(message)s'] + shlex.split(script_args)
  85. if script_gdb:
  86. #
  87. # When running through Popen, we need to preserve some space-delimited args to GDB as a single logical argument. To do that, let's use '|' as a placeholder
  88. # for the space character so that the initial split will not tokenize them, and then replace that with the space char there-after.
  89. #
  90. script_command = "gdb -batch -return-child-result -q -ex run -ex thread|apply|all|bt --args python3".split() + script_command
  91. else:
  92. script_command = "/usr/bin/env python3".split() + script_command
  93. final_script_command = [i.replace('|', ' ') for i in script_command]
  94. logging.info(f"Execute: {final_script_command}")
  95. test_script_process = subprocess.Popen(
  96. final_script_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  97. DumpProgramOutputToQueue(log_cooking_threads, Fore.GREEN + "TEST" + Style.RESET_ALL,
  98. test_script_process, log_queue)
  99. test_script_exit_code = test_script_process.wait()
  100. if test_script_exit_code != 0:
  101. logging.error("Test script exited with error %r" % test_script_exit_code)
  102. test_app_exit_code = 0
  103. if app_process:
  104. logging.warning("Stopping app with SIGINT")
  105. app_process.send_signal(signal.SIGINT.value)
  106. test_app_exit_code = app_process.wait()
  107. # There are some logs not cooked, so we wait until we have processed all logs.
  108. # This procedure should be very fast since the related processes are finished.
  109. for thread in log_cooking_threads:
  110. thread.join()
  111. if test_script_exit_code != 0:
  112. sys.exit(test_script_exit_code)
  113. else:
  114. # We expect both app and test script should exit with 0
  115. sys.exit(test_app_exit_code)
  116. if __name__ == '__main__':
  117. main()