4242import os
4343import random
4444import string
45+ import shutil
4546import subprocess
4647import threading
4748
8687 " to find available values. If none, will use FTL's default." )
8788
8889
90+ # Full paths to the gCloud SDK tools. On Windows, subprocess.run does not check
91+ # the PATH, so we need to find and supply the full paths.
92+ # shutil.which returns None if it doesn't find a tool.
93+ _GCLOUD = shutil .which ("gcloud" )
94+ _GSUTIL = shutil .which ("gsutil" )
95+
96+
8997def main (argv ):
9098 if len (argv ) > 1 :
9199 raise app .UsageError ("Too many command-line arguments." )
92100
93- testapp_dir = FLAGS .testapp_dir
101+ _verify_gcloud_sdk_command_line_tools ()
102+
103+ testapp_dir = _fix_path (FLAGS .testapp_dir )
104+ key_file_path = _fix_path (FLAGS .key_file )
94105 code_platform = FLAGS .code_platform
95106
107+ if not os .path .exists (key_file_path ):
108+ raise ValueError ("Key file path does not exist: %s" % key_file_path )
109+
96110 android_device = Device (model = FLAGS .android_model , version = FLAGS .android_api )
97111 ios_device = Device (model = FLAGS .ios_model , version = FLAGS .ios_version )
98112
@@ -111,7 +125,7 @@ def main(argv):
111125
112126 logging .info ("Testapps found: %s" , "\n " .join (path for _ , _ , path in testapps ))
113127
114- _authorize_gcs (FLAGS . key_file )
128+ _authorize_gcs (key_file_path )
115129
116130 gcs_base_dir = _get_base_results_dir ()
117131 logging .info ("Storing results in %s" , _relative_path_to_gs_uri (gcs_base_dir ))
@@ -148,6 +162,19 @@ def main(argv):
148162 return 0 if all_success else 1
149163
150164
165+ def _verify_gcloud_sdk_command_line_tools ():
166+ """Verifies the presence of the gCloud SDK's command line tools."""
167+ logging .info ("Looking for gcloud and gsutil tools..." )
168+ if not _GCLOUD :
169+ logging .error ("gcloud not on path" )
170+ if not _GSUTIL :
171+ logging .error ("gsutil not on path" )
172+ if not _GCLOUD or not _GSUTIL :
173+ raise RuntimeError ("Could not find required gCloud SDK tool(s)" )
174+ subprocess .run ([_GCLOUD , "version" ], check = True )
175+ subprocess .run ([_GSUTIL , "version" ], check = True )
176+
177+
151178def _get_base_results_dir ():
152179 """Defines the object used on GCS for all tests in this run."""
153180 # We generate a unique directory to store the results by appending 4
@@ -162,12 +189,12 @@ def _authorize_gcs(key_file):
162189 """Activates the service account on GCS and specifies the project."""
163190 subprocess .run (
164191 args = [
165- "gcloud" , "auth" , "activate-service-account" , "--key-file" , key_file
192+ _GCLOUD , "auth" , "activate-service-account" , "--key-file" , key_file
166193 ],
167194 check = True )
168195 # Keep using this project for subsequent gcloud commands.
169196 subprocess .run (
170- args = ["gcloud" , "config" , "set" , "project" , _PROJECT_ID ],
197+ args = [_GCLOUD , "config" , "set" , "project" , _PROJECT_ID ],
171198 check = True )
172199
173200
@@ -250,20 +277,25 @@ def _relative_path_to_gs_uri(path):
250277
251278def _gcs_list_dir (gcs_path ):
252279 """Recursively returns a list of contents for a directory on GCS."""
253- args = ["gsutil" , "ls" , "-r" , gcs_path ]
280+ args = [_GSUTIL , "ls" , "-r" , gcs_path ]
254281 logging .info ("Listing GCS contents: %s" , " " .join (args ))
255282 result = subprocess .run (args = args , capture_output = True , text = True , check = True )
256283 return result .stdout .splitlines ()
257284
258285
259286def _gcs_read_file (gcs_path ):
260287 """Extracts the contents of a file on GCS."""
261- args = ["gsutil" , "cat" , gcs_path ]
288+ args = [_GSUTIL , "cat" , gcs_path ]
262289 logging .info ("Reading GCS file: %s" , " " .join (args ))
263290 result = subprocess .run (args = args , capture_output = True , text = True , check = True )
264291 return result .stdout
265292
266293
294+ def _fix_path (path ):
295+ """Expands ~, normalizes slashes, and converts relative paths to absolute."""
296+ return os .path .abspath (os .path .expanduser (path ))
297+
298+
267299@attr .s (frozen = False , eq = False )
268300class Test (object ):
269301 """Holds data related to the testing of one testapp."""
@@ -293,9 +325,9 @@ def run(self):
293325 def _gcloud_command (self ):
294326 """Returns the args to send this testapp to FTL on the command line."""
295327 if self .platform == _ANDROID :
296- cmd = ["gcloud" , "firebase" , "test" , "android" , "run" ]
328+ cmd = [_GCLOUD , "firebase" , "test" , "android" , "run" ]
297329 elif self .platform == _IOS :
298- cmd = ["gcloud" , "beta" , "firebase" , "test" , "ios" , "run" ]
330+ cmd = [_GCLOUD , "beta" , "firebase" , "test" , "ios" , "run" ]
299331 else :
300332 raise ValueError ("Invalid platform, must be 'Android' or 'iOS'" )
301333 return cmd + self .device .get_gcloud_flags () + [
0 commit comments