Add a script to generate benchmarking artifacts. (#3288)

Additional changes:
- Makes our integrations tests use Bazel's `TEST_UNDECLARED_OUTPUTS_DIR` to safely write to disk.
- Adds back the `saved_model` directory now that we can save it without artificially duplicating it.
- Adds `scripts/utils.py` to remove code duplication between `update_op_coverage.py`, `update_e2e_coveage.py` and `get_e2e_artifacts.py`.
diff --git a/scripts/utils.py b/scripts/utils.py
new file mode 100644
index 0000000..3b6f547
--- /dev/null
+++ b/scripts/utils.py
@@ -0,0 +1,75 @@
+# Lint as: python3
+# Copyright 2020 Google LLC
+#
+# 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
+#
+#      https://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.
+
+# pylint: disable=missing-docstring
+
+import argparse
+import os
+import re
+import subprocess
+from typing import Sequence
+
+BAZEL_FILTERS = [
+    r'Loading: [0-9]+ packages loaded',
+    r'.*Using python binary from PYTHON_BIN = .*'
+]
+
+
+def create_markdown_table(rows: Sequence[Sequence[str]]):
+  """Converts a 2D array to a Markdown table."""
+  return '\n'.join([' | '.join(row) for row in rows])
+
+
+def check_and_get_output(command: Sequence[str],
+                         dry_run: bool = False,
+                         log_stderr: bool = True,
+                         stderr_filters: Sequence[str] = ()):
+  print(f'Running: `{" ".join(command)}`')
+  if dry_run:
+    return None, None
+  process = subprocess.Popen(command,
+                             bufsize=1,
+                             stderr=subprocess.PIPE,
+                             stdout=subprocess.PIPE,
+                             universal_newlines=True)
+  process.wait()
+  stdout = [line.strip(os.linesep) for line in process.stdout]
+  stderr = [line.strip(os.linesep) for line in process.stderr]
+
+  if log_stderr:
+    for line in stderr:
+      if not any(re.match(pattern, line) for pattern in stderr_filters):
+        print(line)
+
+  if process.returncode != 0:
+    raise subprocess.CalledProcessError(process.returncode, ' '.join(command))
+
+  return stdout, stderr
+
+
+def get_test_targets(test_suite_path: str):
+  """Returns a list of test targets for the given test suite."""
+  # Check if the suite exists (which may not be true for failing suites).
+  # We use two queries here because the return code for a failed query is
+  # unfortunately the same as the return code for a bazel configuration error.
+  target_dir = test_suite_path.split(':')[0]
+  query = ['bazel', 'query', f'{target_dir}/...']
+  targets, _ = check_and_get_output(query, stderr_filters=BAZEL_FILTERS)
+  if test_suite_path not in targets:
+    return []
+
+  query = ['bazel', 'query', f'tests({test_suite_path})']
+  tests, _ = check_and_get_output(query, stderr_filters=BAZEL_FILTERS)
+  return tests