pw_build: Run python tests with coverage

- Generate coverage data files for each python test when
  pw_build_PYTHON_TEST_COVERAGE=true

- Presubmit check that generates a coverage report

Test:
pw presubmit --step gn_python_test_coverage
open ./.presubmit/gn_python_test_coverage/htmlcov/index.html

Change-Id: I604767452874da01597c42c493892ab7d81f92b2
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/80781
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_build/python.gni b/pw_build/python.gni
index eac4917..0f8de8f 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -28,6 +28,11 @@
   # See pip help install.
   pw_build_PIP_CONSTRAINTS =
       [ "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list" ]
+
+  # If true, GN will run each Python test using the coverage command. A separate
+  # coverage data file for each test will be saved. To generate reports from
+  # this information run: pw presubmit --step gn_python_test_coverage
+  pw_build_PYTHON_TEST_COVERAGE = false
 }
 
 # Python packages provide the following targets as $target_name.$subtarget.
@@ -659,7 +664,33 @@
 
     if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
       pw_python_action(_test_target) {
-        script = test
+        if (pw_build_PYTHON_TEST_COVERAGE) {
+          module = "coverage"
+          working_directory =
+              rebase_path(get_path_info(test, "dir"), root_build_dir)
+          args = [
+            "run",
+            "--branch",
+
+            # Include all source files in the working_directory when calculating coverage.
+            "--source=.",
+
+            # Test file to run.
+            get_path_info(test, "file"),
+          ]
+
+          # Set the coverage file to a location in out/python/gen/
+          _coverage_data_file = "$target_gen_dir/$target_name.coverage"
+          outputs = [ _coverage_data_file ]
+
+          # The coverage tool only allows setting the output with an environment variable.
+          environment =
+              [ "COVERAGE_FILE=" +
+                rebase_path(_coverage_data_file, get_path_info(test, "dir")) ]
+        } else {
+          script = test
+        }
+
         stamp = true
 
         deps = _test_install_deps