Implement torq simpleperf symbolization validation

Add logic to check for and download device symbols, and to check for
simpleperf scripts. This logic will be used in a subsequent CL to
process the simpleperf traces. Add tests for this logic.

Test: atest validate_simpleperf_unit_test
Test: atest torq_unit_test
Bug: 365985190
Change-Id: Ie4eeb655acf457e1028a3bef256cd68a15e6b027
diff --git a/torq/Android.bp b/torq/Android.bp
index 6bc39d4..e3245df 100644
--- a/torq/Android.bp
+++ b/torq/Android.bp
@@ -28,6 +28,7 @@
         "config_builder.py",
         "open_ui.py",
         "utils.py",
+        "validate_simpleperf.py",
     ],
 }
 
@@ -100,3 +101,19 @@
         unit_test: true,
     },
 }
+
+python_test_host {
+    name: "validate_simpleperf_unit_test",
+    main: "tests/validate_simpleperf_unit_test.py",
+    srcs: ["tests/validate_simpleperf_unit_test.py"],
+    defaults: ["torq_defaults"],
+    version: {
+        py3: {
+            enabled: true,
+            embedded_launcher: false,
+        },
+    },
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/torq/tests/torq_unit_test.py b/torq/tests/torq_unit_test.py
index 112bf58..5a0e4a9 100644
--- a/torq/tests/torq_unit_test.py
+++ b/torq/tests/torq_unit_test.py
@@ -24,6 +24,7 @@
 TEST_USER_ID = 10
 TEST_PACKAGE = "com.android.contacts"
 TEST_FILE = "file.pbtxt"
+SYMBOLS_PATH = "/folder/symbols"
 
 
 class TorqUnitTest(unittest.TestCase):
@@ -90,7 +91,11 @@
     with self.assertRaises(SystemExit):
       parser.parse_args()
 
-  def test_create_parser_valid_profiler_names(self):
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  def test_create_parser_valid_profiler_names(self, mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_exists.return_value = True
     parser = self.set_up_parser("torq.py -p perfetto")
 
     args = parser.parse_args()
@@ -99,7 +104,8 @@
     self.assertEqual(error, None)
     self.assertEqual(args.profiler, "perfetto")
 
-    parser = self.set_up_parser("torq.py -p simpleperf")
+    parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+                                % SYMBOLS_PATH)
 
     args = parser.parse_args()
     args, error = verify_args(args)
@@ -482,8 +488,14 @@
                                         " torq --event app-startup --app"
                                         " <package-name>"))
 
-  def test_verify_args_profiler_and_simpleperf_event_valid_dependencies(self):
-    parser = self.set_up_parser("torq.py -p simpleperf")
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  def test_verify_args_profiler_and_simpleperf_event_valid_dependencies(self,
+      mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_exists.return_value = True
+    parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+                                % SYMBOLS_PATH)
 
     args = parser.parse_args()
     args, error = verify_args(args)
@@ -492,7 +504,8 @@
     self.assertEqual(len(args.simpleperf_event), 1)
     self.assertEqual(args.simpleperf_event[0], "cpu-cycles")
 
-    parser = self.set_up_parser("torq.py -p simpleperf -s cpu-cycles")
+    parser = self.set_up_parser("torq.py -p simpleperf -s cpu-cycles "
+                                "--symbols %s" % SYMBOLS_PATH)
 
     args = parser.parse_args()
     args, error = verify_args(args)
@@ -707,9 +720,15 @@
                                         " include power/cpu_idle in the"
                                         " config."))
 
-  def test_verify_args_multiple_valid_simpleperf_events(self):
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  def test_verify_args_multiple_valid_simpleperf_events(self, mock_isdir,
+      mock_exists):
+    mock_isdir.return_value = True
+    mock_exists.return_value = True
     parser = self.set_up_parser(("torq.py -p simpleperf -s cpu-cycles"
-                                 " -s instructions"))
+                                 " -s instructions --symbols %s"
+                                 % SYMBOLS_PATH))
 
     args = parser.parse_args()
     args, error = verify_args(args)
diff --git a/torq/tests/validate_simpleperf_unit_test.py b/torq/tests/validate_simpleperf_unit_test.py
new file mode 100644
index 0000000..3eec988
--- /dev/null
+++ b/torq/tests/validate_simpleperf_unit_test.py
@@ -0,0 +1,229 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import builtins
+import unittest
+import sys
+import os
+import subprocess
+from unittest import mock
+from torq import create_parser, verify_args
+
+TORQ_TEMP_DIR = "/tmp/.torq"
+ANDROID_BUILD_TOP = "/folder"
+ANDROID_PRODUCT_OUT = "/folder/out/product/seahawk"
+SYMBOLS_PATH = "/folder/symbols"
+
+
+class ValidateSimpleperfUnitTest(unittest.TestCase):
+
+  def set_up_parser(self, command_string):
+    parser = create_parser()
+    sys.argv = command_string.split()
+    return parser
+
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP,
+                                "ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+                   clear=True)
+  def test_create_parser_valid_symbols(self, mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_exists.return_value = True
+    parser = self.set_up_parser("torq.py -p simpleperf "
+                                "--symbols %s" % SYMBOLS_PATH)
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error, None)
+    self.assertEqual(args.symbols, SYMBOLS_PATH)
+    self.assertEqual(args.scripts_path, "%s/system/extras/simpleperf/scripts"
+                     % ANDROID_BUILD_TOP)
+
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP,
+                                "ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+                   clear=True)
+  def test_create_parser_valid_android_product_out_no_symbols(self,
+      mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_exists.return_value = True
+    parser = self.set_up_parser("torq.py -p simpleperf")
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error, None)
+    self.assertEqual(args.symbols, ANDROID_PRODUCT_OUT)
+    self.assertEqual(args.scripts_path, "%s/system/extras/simpleperf/scripts"
+                     % ANDROID_BUILD_TOP)
+
+  @mock.patch.dict(os.environ, {"ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+                   clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  def test_create_parser_invalid_android_product_no_symbols(self,
+      mock_isdir, mock_exists):
+    mock_isdir.return_value = False
+    mock_exists.return_value = False
+    parser = self.set_up_parser("torq.py -p simpleperf")
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error.message, ("%s is not a valid $ANDROID_PRODUCT_OUT."
+                                     % ANDROID_PRODUCT_OUT))
+    self.assertEqual(error.suggestion, "Set --symbols to a valid symbols lib "
+                                       "path or set $ANDROID_PRODUCT_OUT to "
+                                       "your android product out directory "
+                                       "(<ANDROID_BUILD_TOP>/out/target/product"
+                                       "/<TARGET>).")
+
+  @mock.patch.dict(os.environ, {},
+                   clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  def test_create_parser_invalid_symbols_no_android_product_out(self,
+      mock_isdir, mock_exists):
+    mock_isdir.return_value = False
+    mock_exists.return_value = False
+    parser = self.set_up_parser("torq.py -p simpleperf "
+                                "--symbols %s" % SYMBOLS_PATH)
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error.message, ("%s is not a valid path." % SYMBOLS_PATH))
+    self.assertEqual(error.suggestion, "Set --symbols to a valid symbols lib "
+                                       "path or set $ANDROID_PRODUCT_OUT to "
+                                       "your android product out directory "
+                                       "(<ANDROID_BUILD_TOP>/out/target/product"
+                                       "/<TARGET>).")
+
+  @mock.patch.dict(os.environ, {}, clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  def test_create_parser_no_android_product_out_no_symbols(self, mock_isdir,
+      mock_exists):
+    mock_isdir.return_value = False
+    mock_exists.return_value = False
+    parser = self.set_up_parser("torq.py -p simpleperf")
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error.message, "ANDROID_PRODUCT_OUT is not set.")
+    self.assertEqual(error.suggestion, "Set --symbols to a valid symbols lib "
+                                       "path or set $ANDROID_PRODUCT_OUT to "
+                                       "your android product out directory "
+                                       "(<ANDROID_BUILD_TOP>/out/target/"
+                                       "product/<TARGET>).")
+
+  @mock.patch.dict(os.environ, {"ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+                   clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  @mock.patch.object(subprocess, "run", autospec=True)
+  @mock.patch.object(builtins, "input")
+  def test_create_parser_successfully_download_scripts(self, mock_input,
+      mock_subprocess_run, mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_input.return_value = "y"
+    mock_exists.side_effect = [False, True]
+    mock_subprocess_run.return_value = None
+    parser = self.set_up_parser("torq.py -p simpleperf")
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error, None)
+    self.assertEqual(args.symbols, ANDROID_PRODUCT_OUT)
+    self.assertEqual(args.scripts_path, TORQ_TEMP_DIR)
+
+  @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP},
+                   clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  @mock.patch.object(subprocess, "run", autospec=True)
+  @mock.patch.object(builtins, "input")
+  def test_create_parser_failed_to_download_scripts(self, mock_input,
+      mock_subprocess_run, mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_input.return_value = "y"
+    mock_exists.side_effect = [False, False, False]
+    mock_subprocess_run.return_value = None
+    parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+                                % SYMBOLS_PATH)
+
+    args = parser.parse_args()
+    with self.assertRaises(Exception) as e:
+      args, error = verify_args(args)
+
+    self.assertEqual(str(e.exception),
+                     "Error while downloading simpleperf scripts. Try "
+                     "again or set $ANDROID_BUILD_TOP to your android root "
+                     "path and make sure you have $ANDROID_BUILD_TOP/system"
+                     "/extras/simpleperf/scripts downloaded.")
+
+  @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP},
+                   clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  @mock.patch.object(builtins, "input")
+  def test_create_parser_download_scripts_wrong_input(self, mock_input,
+      mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_input.return_value = "bad-input"
+    mock_exists.side_effect = [False, False]
+    parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+                                % SYMBOLS_PATH)
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error.message, "Invalid inputs.")
+    self.assertEqual(error.suggestion, "Set $ANDROID_BUILD_TOP to your android "
+                                       "root path and make sure you have "
+                                       "$ANDROID_BUILD_TOP/system/extras/"
+                                       "simpleperf/scripts downloaded.")
+
+  @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP},
+                   clear=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  @mock.patch.object(os.path, "isdir", autospec=True)
+  @mock.patch.object(builtins, "input")
+  def test_create_parser_download_scripts_refuse_download(self, mock_input,
+      mock_isdir, mock_exists):
+    mock_isdir.return_value = True
+    mock_input.return_value = "n"
+    mock_exists.side_effect = [False, False]
+    parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+                                % SYMBOLS_PATH)
+
+    args = parser.parse_args()
+    args, error = verify_args(args)
+
+    self.assertEqual(error.message, "Did not download simpleperf scripts.")
+    self.assertEqual(error.suggestion, "Set $ANDROID_BUILD_TOP to your android "
+                                       "root path and make sure you have "
+                                       "$ANDROID_BUILD_TOP/system/extras/"
+                                       "simpleperf/scripts downloaded.")
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/torq/torq.py b/torq/torq.py
index 0137326..a01dadb 100644
--- a/torq/torq.py
+++ b/torq/torq.py
@@ -20,7 +20,8 @@
 from device import AdbDevice
 from validation_error import ValidationError
 from config_builder import PREDEFINED_PERFETTO_CONFIGS
-from utils import does_path_exist
+from utils import path_exists
+from validate_simpleperf import verify_simpleperf_args
 
 DEFAULT_DUR_MS = 10000
 MIN_DURATION_MS = 3000
@@ -72,6 +73,8 @@
   parser.add_argument('--serial',
                       help=(('Specifies serial of the device that will be'
                              ' used.')))
+  parser.add_argument('--symbols',
+                      help='Specifies path to symbols library.')
   subparsers = parser.add_subparsers(dest='subcommands', help='Subcommands')
   config_parser = subparsers.add_parser('config',
                                         help=('The config subcommand used'
@@ -324,11 +327,18 @@
            "\t torq pull lightweight to copy to ./lightweight.pbtxt\n"
            "\t torq pull memory to copy to ./memory.pbtxt"))
 
-  if args.subcommands == "open" and not does_path_exist(args.file_path):
+  if args.subcommands == "open" and not path_exists(args.file_path):
     return None, ValidationError(
         "Command is invalid because %s is an invalid file path."
         % args.file_path, "Make sure your file exists.")
 
+  if args.profiler == "simpleperf":
+    args, error = verify_simpleperf_args(args)
+    if error is not None:
+      return None, error
+  else:
+    args.scripts_path = None
+
   return args, None
 
 
diff --git a/torq/utils.py b/torq/utils.py
index 8ef0662..8716d9f 100644
--- a/torq/utils.py
+++ b/torq/utils.py
@@ -16,7 +16,12 @@
 
 import os
 
-def does_path_exist(path: str):
+def path_exists(path: str):
   if path is None:
     return False
   return os.path.exists(os.path.expanduser(path))
+
+def dir_exists(path: str):
+  if path is None:
+    return False
+  return os.path.isdir(os.path.expanduser(path))
diff --git a/torq/validate_simpleperf.py b/torq/validate_simpleperf.py
new file mode 100644
index 0000000..4b8c41c
--- /dev/null
+++ b/torq/validate_simpleperf.py
@@ -0,0 +1,106 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import os
+import subprocess
+from utils import path_exists, dir_exists
+from validation_error import ValidationError
+
+TORQ_TEMP_DIR = "/tmp/.torq"
+TEMP_CACHE_BUILDER_SCRIPT = TORQ_TEMP_DIR + "/binary_cache_builder.py"
+SIMPLEPERF_SCRIPTS_DIR = "/system/extras/simpleperf/scripts"
+BUILDER_SCRIPT = SIMPLEPERF_SCRIPTS_DIR + "/binary_cache_builder.py"
+
+def verify_simpleperf_args(args):
+  args.scripts_path = TORQ_TEMP_DIR
+  if ("ANDROID_BUILD_TOP" in os.environ
+      and path_exists(os.environ["ANDROID_BUILD_TOP"] + BUILDER_SCRIPT)):
+    args.scripts_path = (os.environ["ANDROID_BUILD_TOP"]
+                         + SIMPLEPERF_SCRIPTS_DIR)
+
+  if args.symbols is None or not dir_exists(args.symbols):
+    if args.symbols is not None:
+      return None, ValidationError(
+          ("%s is not a valid path." % args.symbols),
+          "Set --symbols to a valid symbols lib path or set "
+          "$ANDROID_PRODUCT_OUT to your android product out directory "
+          "(<ANDROID_BUILD_TOP>/out/target/product/<TARGET>).")
+    if "ANDROID_PRODUCT_OUT" not in os.environ:
+      return None, ValidationError(
+          "ANDROID_PRODUCT_OUT is not set.",
+          "Set --symbols to a valid symbols lib path or set "
+          "$ANDROID_PRODUCT_OUT to your android product out directory "
+          "(<ANDROID_BUILD_TOP>/out/target/product/<TARGET>).")
+    if not dir_exists(os.environ["ANDROID_PRODUCT_OUT"]):
+      return None, ValidationError(
+          ("%s is not a valid $ANDROID_PRODUCT_OUT."
+           % (os.environ["ANDROID_PRODUCT_OUT"])),
+          "Set --symbols to a valid symbols lib path or set "
+          "$ANDROID_PRODUCT_OUT to your android product out directory "
+          "(<ANDROID_BUILD_TOP>/out/target/product/<TARGET>).")
+    args.symbols = os.environ["ANDROID_PRODUCT_OUT"]
+
+  if (args.scripts_path != TORQ_TEMP_DIR or
+      path_exists(TEMP_CACHE_BUILDER_SCRIPT)):
+    return args, None
+
+  error = download_simpleperf_scripts()
+
+  if error is not None:
+    return None, error
+
+  return args, None
+
+def download_simpleperf_scripts():
+  i = 0
+  while i <= 3:
+    i += 1
+    confirmation = input("You do not have an Android Root configured with "
+                         "the simpleperf directory. To use simpleperf, torq "
+                         "will download simpleperf scripts to '%s'. "
+                         "Are you ok with this download? [Y/N]: "
+                         % TORQ_TEMP_DIR)
+
+    if confirmation.lower() == "y":
+      break
+    elif confirmation.lower() == "n":
+      return ValidationError("Did not download simpleperf scripts.",
+                             "Set $ANDROID_BUILD_TOP to your android root "
+                             "path and make sure you have $ANDROID_BUILD_TOP"
+                             "/system/extras/simpleperf/scripts "
+                             "downloaded.")
+    if i == 3:
+      return ValidationError("Invalid inputs.",
+                             "Set $ANDROID_BUILD_TOP to your android root "
+                             "path and make sure you have $ANDROID_BUILD_TOP"
+                             "/system/extras/simpleperf/scripts "
+                             "downloaded.")
+
+  subprocess.run(("mkdir -p %s && wget -P %s "
+                 "https://android.googlesource.com/platform/system/extras"
+                 "/+archive/refs/heads/main/simpleperf/scripts.tar.gz "
+                 "&& tar -xvzf %s/scripts.tar.gz -C %s"
+                  % (TORQ_TEMP_DIR, TORQ_TEMP_DIR, TORQ_TEMP_DIR,
+                     TORQ_TEMP_DIR)),
+                 shell=True)
+
+  if not path_exists(TEMP_CACHE_BUILDER_SCRIPT):
+    raise Exception("Error while downloading simpleperf scripts. Try again "
+                    "or set $ANDROID_BUILD_TOP to your android root path and "
+                    "make sure you have $ANDROID_BUILD_TOP/system/extras/"
+                    "simpleperf/scripts downloaded.")
+
+  return None