Per module testing state with shared index

This moved the creation of the shared state index to test.py such
that it is created once and ahead of test runs. Each test module
emits to the shared index and appends a status block on completion.
Test state is moved to per-module sub-directories and filters are
computed based on the state of the tests in each modules sub-dir.

Bug: b/297316723
Change-Id: Ie20754600125c3d53194bd684f7c976ca96d4926
diff --git a/tools/test.py b/tools/test.py
index 5820590..a6c34f0 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -19,6 +19,7 @@
 import download_kotlin_dev
 import gradle
 import notify
+import test_state
 import utils
 
 if utils.is_python3():
@@ -376,11 +377,7 @@
 
   if testing_state:
     if options.new_gradle:
-      # In the new build the test state directory must be passed explictitly.
-      # TODO(b/297316723): Simplify this and just support a single flag: --testing-state <path>
-      if not testing_state_path:
-        testing_state_path = "%s/test-state/%s" % (utils.BUILD, utils.get_HEAD_branch())
-      gradle_args.append('-Ptesting-state=%s' % testing_state_path)
+      test_state.set_up_test_state(gradle_args, testing_state_path)
     else:
       gradle_args.append('-Ptesting-state')
 
@@ -503,7 +500,6 @@
 
   return 0
 
-
 def archive_and_return(return_code, options):
   if return_code != 0:
     if options.archive_failures:
diff --git a/tools/test_state.py b/tools/test_state.py
new file mode 100644
index 0000000..4541533
--- /dev/null
+++ b/tools/test_state.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import utils
+
+import datetime
+import os
+
+def set_up_test_state(gradle_args, testing_state_path):
+  # In the new build the test state directory must be passed explictitly.
+  # TODO(b/297316723): Simplify this and just support a single flag: --testing-state <path>
+  if not testing_state_path:
+    testing_state_path = "%s/test-state/%s" % (utils.BUILD, utils.get_HEAD_branch())
+  gradle_args.append('-Ptesting-state=%s' % testing_state_path)
+  prepare_testing_index(testing_state_path)
+
+def fresh_testing_index(testing_state_dir):
+  number = 0
+  while True:
+    freshIndex = os.path.join(testing_state_dir, "index.%d.html" % number)
+    number += 1
+    if not os.path.exists(freshIndex):
+      return freshIndex
+
+def prepare_testing_index(testing_state_dir):
+  if not os.path.exists(testing_state_dir):
+    os.makedirs(testing_state_dir)
+  index_path = os.path.join(testing_state_dir, "index.html")
+  parent_report = None
+  resuming = os.path.exists(index_path)
+  if (resuming):
+    parent_report = fresh_testing_index(testing_state_dir)
+    os.rename(index_path, parent_report)
+  index = open(index_path, "a")
+  run_prefix = "Resuming" if resuming else "Starting"
+  relative_state_dir = os.path.relpath(testing_state_dir)
+  title = f"{run_prefix} @ {relative_state_dir}"
+  # Print a console link to the test report for easy access.
+  print("=" * 70)
+  print(f"{run_prefix} test, report written to:")
+  print(f"  file://{index_path}")
+  print("=" * 70)
+  # Print the new index content.
+  index.write(f"<html><head><title>{title}</title>")
+  index.write("<style> * { font-family: monospace; }</style>")
+  index.write("<meta http-equiv='refresh' content='10' />")
+  index.write(f"</head><body><h1>{title}</h1>")
+  # write index links first to avoid jumping when browsing.
+  if parent_report:
+    index.write(f"<p><a href=\"file://{parent_report}\">Previous result index</a></p>")
+  index.write(f"<p><a href=\"file://{index_path}\">Most recent result index</a></p>")
+  index.write(f"<p><a href=\"file://{testing_state_dir}\">Test directories</a></p>")
+  # git branch/hash and diff for future reference
+  index.write(f"<p>Run on: {datetime.datetime.now()}</p>")
+  index.write(f"<p>Git branch: {utils.get_HEAD_branch()}")
+  index.write(f"</br>Git SHA: {utils.get_HEAD_sha1()}")
+  index.write(f'</br>Git diff summary:\n')
+  index.write(f'<pre style="background-color: lightgray">{utils.get_HEAD_diff_stat()}</pre></p>')
+  # header for the failing tests
+  index.write("<h2>Failing tests (refreshing automatically every 10 seconds)</h2><ul>")
diff --git a/tools/utils.py b/tools/utils.py
index 279e1d0..fe8b539 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -362,6 +362,9 @@
 def get_HEAD_sha1():
   return get_HEAD_sha1_for_checkout(REPO_ROOT)
 
+def get_HEAD_diff_stat():
+  return subprocess.check_output(['git', 'diff', '--stat']).decode('utf-8')
+
 def get_HEAD_sha1_for_checkout(checkout):
   cmd = ['git', 'rev-parse', 'HEAD']
   PrintCmd(cmd)