common.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # Copyright 2022 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Common methods and variables used by Cr-Fuchsia testing infrastructure."""
  5. import json
  6. import os
  7. import platform
  8. import re
  9. import subprocess
  10. from argparse import ArgumentParser
  11. from typing import Iterable, List, Optional
  12. from compatible_utils import parse_host_port
  13. DIR_SRC_ROOT = os.path.abspath(
  14. os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
  15. REPO_ALIAS = 'fuchsia.com'
  16. SDK_ROOT = os.path.join(DIR_SRC_ROOT, 'third_party', 'fuchsia-sdk', 'sdk')
  17. def get_host_arch() -> str:
  18. """Retrieve CPU architecture of the host machine. """
  19. host_arch = platform.machine()
  20. # platform.machine() returns AMD64 on 64-bit Windows.
  21. if host_arch in ['x86_64', 'AMD64']:
  22. return 'x64'
  23. if host_arch == 'aarch64':
  24. return 'arm64'
  25. raise Exception('Unsupported host architecture: %s' % host_arch)
  26. SDK_TOOLS_DIR = os.path.join(SDK_ROOT, 'tools', get_host_arch())
  27. _FFX_TOOL = os.path.join(SDK_TOOLS_DIR, 'ffx')
  28. def _run_repair_command(output):
  29. """Scans |output| for a self-repair command to run and, if found, runs it.
  30. Returns:
  31. True if a repair command was found and ran successfully. False otherwise.
  32. """
  33. # Check for a string along the lines of:
  34. # "Run `ffx doctor --restart-daemon` for further diagnostics."
  35. match = re.search('`ffx ([^`]+)`', output)
  36. if not match or len(match.groups()) != 1:
  37. return False # No repair command found.
  38. args = match.groups()[0].split()
  39. try:
  40. run_ffx_command(args, suppress_repair=True)
  41. except subprocess.CalledProcessError:
  42. return False # Repair failed.
  43. return True # Repair succeeded.
  44. def run_ffx_command(cmd: Iterable[str],
  45. target_id: Optional[str] = None,
  46. check: bool = True,
  47. suppress_repair: bool = False,
  48. **kwargs) -> subprocess.CompletedProcess:
  49. """Runs `ffx` with the given arguments, waiting for it to exit.
  50. If `ffx` exits with a non-zero exit code, the output is scanned for a
  51. recommended repair command (e.g., "Run `ffx doctor --restart-daemon` for
  52. further diagnostics."). If such a command is found, it is run and then the
  53. original command is retried. This behavior can be suppressed via the
  54. `suppress_repair` argument.
  55. Args:
  56. cmd: A sequence of arguments to ffx.
  57. target_id: Whether to execute the command for a specific target. The
  58. target_id could be in the form of a nodename or an address.
  59. check: If True, CalledProcessError is raised if ffx returns a non-zero
  60. exit code.
  61. suppress_repair: If True, do not attempt to find and run a repair
  62. command.
  63. Returns:
  64. A CompletedProcess instance
  65. Raises:
  66. CalledProcessError if |check| is true.
  67. """
  68. ffx_cmd = [_FFX_TOOL]
  69. if target_id:
  70. ffx_cmd.extend(('--target', target_id))
  71. ffx_cmd.extend(cmd)
  72. try:
  73. return subprocess.run(ffx_cmd, check=check, encoding='utf-8', **kwargs)
  74. except subprocess.CalledProcessError as cpe:
  75. if suppress_repair or not _run_repair_command(cpe.output):
  76. raise
  77. # If the original command failed but a repair command was found and
  78. # succeeded, try one more time with the original command.
  79. return run_ffx_command(cmd, target_id, check, True, **kwargs)
  80. def run_continuous_ffx_command(cmd: Iterable[str],
  81. target_id: Optional[str] = None,
  82. **kwargs) -> subprocess.Popen:
  83. """Runs an ffx command asynchronously."""
  84. ffx_cmd = [_FFX_TOOL]
  85. if target_id:
  86. ffx_cmd.extend(('--target', target_id))
  87. ffx_cmd.extend(cmd)
  88. return subprocess.Popen(ffx_cmd, encoding='utf-8', **kwargs)
  89. def read_package_paths(out_dir: str, pkg_name: str) -> List[str]:
  90. """
  91. Returns:
  92. A list of the absolute path to all FAR files the package depends on.
  93. """
  94. with open(
  95. os.path.join(DIR_SRC_ROOT, out_dir, 'gen', 'package_metadata',
  96. f'{pkg_name}.meta')) as meta_file:
  97. data = json.load(meta_file)
  98. packages = []
  99. for package in data['packages']:
  100. packages.append(os.path.join(DIR_SRC_ROOT, out_dir, package))
  101. return packages
  102. def register_common_args(parser: ArgumentParser) -> None:
  103. """Register commonly used arguments."""
  104. common_args = parser.add_argument_group('common', 'common arguments')
  105. common_args.add_argument(
  106. '--out-dir',
  107. '-C',
  108. type=os.path.realpath,
  109. help='Path to the directory in which build files are located. ')
  110. def register_device_args(parser: ArgumentParser) -> None:
  111. """Register device arguments."""
  112. device_args = parser.add_argument_group('device', 'device arguments')
  113. device_args.add_argument('--target-id',
  114. default=os.environ.get('FUCHSIA_NODENAME'),
  115. help=('Specify the target device. This could be '
  116. 'a node-name (e.g. fuchsia-emulator) or an '
  117. 'an ip address along with an optional port '
  118. '(e.g. [fe80::e1c4:fd22:5ee5:878e]:22222, '
  119. '1.2.3.4, 1.2.3.4:33333). If unspecified, '
  120. 'the default target in ffx will be used.'))
  121. def register_log_args(parser: ArgumentParser) -> None:
  122. """Register commonly used arguments."""
  123. log_args = parser.add_argument_group('logging', 'logging arguments')
  124. log_args.add_argument('--logs-dir',
  125. type=os.path.realpath,
  126. help=('Directory to write logs to.'))
  127. def get_component_uri(package: str) -> str:
  128. """Retrieve the uri for a package."""
  129. return f'fuchsia-pkg://{REPO_ALIAS}/{package}#meta/{package}.cm'
  130. def resolve_packages(packages: List[str], target_id: Optional[str]) -> None:
  131. """Ensure that all |packages| are installed on a device."""
  132. for package in packages:
  133. # Try destroying the component to force an update.
  134. run_ffx_command(
  135. ['component', 'destroy', f'/core/ffx-laboratory:{package}'],
  136. target_id,
  137. check=False)
  138. run_ffx_command([
  139. 'component', 'create', f'/core/ffx-laboratory:{package}',
  140. f'fuchsia-pkg://{REPO_ALIAS}/{package}#meta/{package}.cm'
  141. ], target_id)
  142. run_ffx_command(
  143. ['component', 'resolve', f'/core/ffx-laboratory:{package}'],
  144. target_id)
  145. # TODO(crbug.com/1342460): Remove when Telemetry tests are using CFv2 packages.
  146. def resolve_v1_packages(packages: List[str], target_id: Optional[str]) -> None:
  147. """Ensure that all cfv1 packages are installed on a device."""
  148. ssh_address = run_ffx_command(('target', 'get-ssh-address'),
  149. target_id,
  150. capture_output=True).stdout.strip()
  151. address, port = parse_host_port(ssh_address)
  152. for package in packages:
  153. subprocess.run([
  154. 'ssh', '-F',
  155. os.path.expanduser('~/.fuchsia/sshconfig'), address, '-p',
  156. str(port), '--', 'pkgctl', 'resolve',
  157. 'fuchsia-pkg://%s/%s' % (REPO_ALIAS, package)
  158. ],
  159. check=True)