Merge "[results_uploader] Use pathlib for all file-related operations" into main
diff --git a/tools/results_uploader/src/mobly_result_converter.py b/tools/results_uploader/src/mobly_result_converter.py
index 9348add..fab584b 100644
--- a/tools/results_uploader/src/mobly_result_converter.py
+++ b/tools/results_uploader/src/mobly_result_converter.py
@@ -36,9 +36,7 @@
 import dataclasses
 import datetime
 import enum
-import glob
 import logging
-import os
 import pathlib
 import re
 from typing import Any, Dict, Iterator, List, Mapping, Optional
@@ -60,11 +58,6 @@
 _ILLEGAL_YAML_CHARS = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]')
 
 
-def as_posix_path(path):
-    """Returns a given path as a string in POSIX format."""
-    return str(pathlib.Path(path).as_posix())
-
-
 class MoblyResultstoreProperties(enum.Enum):
     """Resultstore properties defined specifically for all Mobly tests.
 
@@ -204,54 +197,45 @@
 def _add_file_annotations(
         entry: Mapping[str, Any],
         properties_element: ElementTree.Element,
-        mobly_base_directory: Optional[str],
-        resultstore_root_directory: Optional[str],
+        mobly_base_directory: Optional[pathlib.Path],
 ) -> None:
     """Adds file annotations for a Mobly test case files.
 
     The mobly_base_directory is used to find the files belonging to a test case.
     The files under "mobly_base_directory/test_class/test_method" belong to the
-    test_class#test_method Resultstore node.
-
-    The resultstore_root_directory is used to determine the
-    relative path of the files to the Resultstore root directory for undeclared
-    outputs. The file annotation must be written for the relative path.
+    test_class#test_method Resultstore node. Additionally, it is used to
+    determine the relative path of the files for Resultstore undeclared outputs.
+    The file annotation must be written for the relative path.
 
     Args:
       entry: Mobly summary entry for the test case.
       properties_element: Test case properties element.
       mobly_base_directory: Base directory of the Mobly test.
-      resultstore_root_directory: Root directory for Resultstore undeclared
-        outputs.
     """
-    # If these directories are not provided, the converter will not add the
+    # If mobly_base_directory is not provided, the converter will not add the
     # annotations to associate the files with the test cases.
     if (
             mobly_base_directory is None
-            or resultstore_root_directory is None
             or entry.get(records.TestResultEnums.RECORD_SIGNATURE, None) is None
     ):
         return
 
     test_class = entry[records.TestResultEnums.RECORD_CLASS]
-    test_case_directory = os.path.join(
-        mobly_base_directory,
+    test_case_directory = mobly_base_directory.joinpath(
         test_class,
-        entry[records.TestResultEnums.RECORD_SIGNATURE],
+        entry[records.TestResultEnums.RECORD_SIGNATURE]
     )
 
-    test_case_files = glob.glob(
-        os.path.join(test_case_directory, '**'), recursive=True
-    )
+    test_case_files = test_case_directory.rglob('*')
     file_counter = 0
     for file_path in test_case_files:
-        if not os.path.isfile(file_path):
+        if not file_path.is_file():
             continue
-        relative_path = os.path.relpath(file_path, resultstore_root_directory)
+        relative_path = file_path.relative_to(mobly_base_directory)
         _add_or_update_property_element(
             properties_element,
             f'test_output{file_counter}',
-            as_posix_path(relative_path),
+            str(relative_path.as_posix()),
         )
         file_counter += 1
 
@@ -447,8 +431,7 @@
 def _process_record(
         entry: Mapping[str, Any],
         reran_node: Optional[ReranNode],
-        mobly_base_directory: Optional[str],
-        resultstore_root_directory: Optional[str],
+        mobly_base_directory: Optional[pathlib.Path],
 ) -> ElementTree.Element:
     """Processes a single Mobly test record entry to a Resultstore test case
     node.
@@ -459,8 +442,6 @@
         if this test is part of a rerun chain.
       mobly_base_directory: Base directory for the Mobly test. Artifacts from
         the Mobly test will be saved here.
-      resultstore_root_directory: Root directory for Resultstore undeclared
-        outputs.
 
     Returns:
       A Resultstore XML node representing a single test case.
@@ -577,7 +558,6 @@
         entry,
         properties_element,
         mobly_base_directory,
-        resultstore_root_directory,
     )
 
     if entry[records.TestResultEnums.RECORD_UID] is not None:
@@ -655,28 +635,25 @@
 
 
 def convert(
-        mobly_results_path: str,
-        mobly_base_directory: Optional[str] = None,
-        resultstore_root_directory: Optional[str] = None,
+        mobly_results_path: pathlib.Path,
+        mobly_base_directory: Optional[pathlib.Path] = None,
 ) -> ElementTree.ElementTree:
     """Converts a Mobly results summary file to Resultstore XML schema.
 
-    The mobly_base_directory and resultstore_root_directory will be used to
-    compute the file links for each Resultstore tree element. If these are
-    absent then the file links will be omitted.
+    The mobly_base_directory will be used to compute the file links for each
+    Resultstore tree element. If it is absent then the file links will be
+    omitted.
 
     Args:
       mobly_results_path: Path to the Mobly summary YAML file.
       mobly_base_directory: Base directory of the Mobly test.
-      resultstore_root_directory: Root directory for Resultstore undeclared
-        outputs.
 
     Returns:
       A Resultstore XML tree for the Mobly test.
     """
     logging.info('Generating Resultstore tree...')
 
-    with open(mobly_results_path, 'r', encoding='utf-8') as f:
+    with mobly_results_path.open('r', encoding='utf-8') as f:
         summary_entries = list(
             yaml.safe_load_all(_ILLEGAL_YAML_CHARS.sub('', f.read()))
         )
@@ -694,21 +671,16 @@
     mobly_root_properties = _create_or_return_properties_element(
         mobly_test_root)
     # Add files under the Mobly root directory to the Mobly test suite node.
-    if (
-            mobly_base_directory is not None
-            and resultstore_root_directory is not None
-    ):
+    if mobly_base_directory is not None:
         file_counter = 0
-        for filename in os.listdir(mobly_base_directory):
-            file_path = os.path.join(mobly_base_directory, filename)
-            if not os.path.isfile(file_path):
+        for file_path in mobly_base_directory.iterdir():
+            if not file_path.is_file():
                 continue
-            relative_path = os.path.relpath(file_path,
-                                            resultstore_root_directory)
+            relative_path = file_path.relative_to(mobly_base_directory)
             _add_or_update_property_element(
                 mobly_root_properties,
                 f'test_output{file_counter}',
-                as_posix_path(relative_path),
+                str(relative_path.as_posix()),
             )
             file_counter += 1
 
@@ -760,10 +732,7 @@
         else:
             reran_node = None
         class_elements[class_name].append(
-            _process_record(
-                entry, reran_node, mobly_base_directory,
-                resultstore_root_directory
-            )
+            _process_record(entry, reran_node, mobly_base_directory)
         )
 
     user_data_entries = [
diff --git a/tools/results_uploader/src/results_uploader.py b/tools/results_uploader/src/results_uploader.py
index 08ada2a..6a949c9 100644
--- a/tools/results_uploader/src/results_uploader.py
+++ b/tools/results_uploader/src/results_uploader.py
@@ -22,7 +22,6 @@
 from importlib import resources
 import logging
 import mimetypes
-import os
 import pathlib
 import platform
 import shutil
@@ -84,33 +83,32 @@
     target_id: str | None = None
 
 
-def _convert_results(mobly_dir: str, dest_dir: str) -> _TestResultInfo:
-    """Converts Mobly test results into a Resultstore artifacts."""
+def _convert_results(
+        mobly_dir: pathlib.Path, dest_dir: pathlib.Path) -> _TestResultInfo:
+    """Converts Mobly test results into Resultstore artifacts."""
     test_result_info = _TestResultInfo()
     logging.info('Converting raw Mobly logs into Resultstore artifacts...')
     # Generate the test.xml
-    mobly_yaml_path = os.path.join(mobly_dir, _TEST_SUMMARY_YAML)
-    if os.path.isfile(mobly_yaml_path):
-        test_xml = mobly_result_converter.convert(
-            mobly_yaml_path, mobly_dir, mobly_dir
-        )
+    mobly_yaml_path = mobly_dir.joinpath(_TEST_SUMMARY_YAML)
+    if mobly_yaml_path.is_file():
+        test_xml = mobly_result_converter.convert(mobly_yaml_path, mobly_dir)
         ElementTree.indent(test_xml)
         test_xml.write(
-            os.path.join(dest_dir, _TEST_XML),
+            str(dest_dir.joinpath(_TEST_XML)),
             encoding='utf-8',
             xml_declaration=True,
         )
         test_result_info = _get_test_result_info_from_test_xml(test_xml)
 
     # Copy test_log.INFO to test.log
-    test_log_info = os.path.join(mobly_dir, _TEST_LOG_INFO)
-    if os.path.isfile(test_log_info):
-        shutil.copyfile(test_log_info, os.path.join(dest_dir, _TEST_LOGS))
+    test_log_info = mobly_dir.joinpath(_TEST_LOG_INFO)
+    if test_log_info.is_file():
+        shutil.copyfile(test_log_info, dest_dir.joinpath(_TEST_LOGS))
 
     # Copy directory to undeclared_outputs/
     shutil.copytree(
         mobly_dir,
-        os.path.join(dest_dir, _UNDECLARED_OUTPUTS),
+        dest_dir.joinpath(_UNDECLARED_OUTPUTS),
         dirs_exist_ok=True,
     )
     return test_result_info
@@ -186,7 +184,7 @@
 
 
 def _upload_dir_to_gcs(
-        src_dir: str, gcs_bucket: str, gcs_dir: str
+        src_dir: pathlib.Path, gcs_bucket: str, gcs_dir: str
 ) -> list[str]:
     """Uploads the given directory to a GCS bucket."""
     # Set correct MIME types for certain text-format files.
@@ -196,7 +194,7 @@
 
     bucket_obj = storage.Client().bucket(gcs_bucket)
 
-    glob = pathlib.Path(src_dir).expanduser().rglob('*')
+    glob = src_dir.rglob('*')
     file_paths = [
         str(path.relative_to(src_dir).as_posix())
         for path in glob
@@ -204,9 +202,9 @@
     ]
 
     logging.info(
-        'Uploading %s files from %s to Cloud Storage bucket %s/%s...',
+        'Uploading %s files from %s to Cloud Storage directory %s/%s...',
         len(file_paths),
-        src_dir,
+        str(src_dir),
         gcs_bucket,
         gcs_dir,
     )
@@ -224,7 +222,7 @@
     results = transfer_manager.upload_many_from_filenames(
         bucket_obj,
         file_paths,
-        source_directory=src_dir,
+        source_directory=str(src_dir),
         blob_name_prefix=blob_name_prefix,
         worker_type=worker_type,
     )
@@ -246,7 +244,7 @@
     return success_paths
 
 
-def _prompt_user_upload(src_dir: str, gcs_bucket: str) -> None:
+def _prompt_user_upload(src_dir: pathlib.Path, gcs_bucket: str) -> None:
     """Prompts the user to manually upload files to GCS."""
     print(_GCS_UPLOAD_INSTRUCTIONS % (gcs_bucket, src_dir))
     while True:
@@ -327,9 +325,10 @@
         else args.gcs_dir
     )
     with tempfile.TemporaryDirectory() as tmp:
-        converted_dir = os.path.join(tmp, gcs_dir)
-        os.mkdir(converted_dir)
-        test_result_info = _convert_results(args.mobly_dir, converted_dir)
+        converted_dir = pathlib.Path(tmp).joinpath(gcs_dir)
+        converted_dir.mkdir()
+        mobly_dir = pathlib.Path(args.mobly_dir).absolute().expanduser()
+        test_result_info = _convert_results(mobly_dir, converted_dir)
         gcs_files = _upload_dir_to_gcs(
             converted_dir, gcs_bucket, gcs_dir)
     _upload_to_resultstore(