fix(coverage): missing files in the coverage report if they have no tests (#2607)

This ensures that un-executed files _(i.e. files that aren't tested)_
are included in the coverage report. The current behavior is that
coverage.py excludes them by default.

This PR configures source files via the auto-generated `.coveragerc`
file.

See https://coverage.readthedocs.io/en/7.6.10/source.html#execution:

> If the source option is specified, only code in those locations will
be measured. Specifying the source option also enables coverage.py to
report on un-executed files, since it can search the source tree for
files that haven’t been measured at all.

Closes #2599
Closes #2597
Fixes #2575

---------

Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 413442e..403dbaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,7 @@
   ([#1169](https://github.com/bazelbuild/rules_python/issues/1169)).
 * (gazelle) Don't collapse depsets to a list or into args when generating the modules mapping file.
   Support spilling modules mapping args into a params file.
+* (coverage) Fix missing files in the coverage report if they have no tests.
 * (pypi) From now on `python` invocations in repository and module extension
   evaluation contexts will invoke Python interpreter with `-B` to avoid
   creating `.pyc` files.
diff --git a/examples/bzlmod/.python_version b/examples/bzlmod/.python_version
new file mode 100644
index 0000000..bd28b9c
--- /dev/null
+++ b/examples/bzlmod/.python_version
@@ -0,0 +1 @@
+3.9
diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt
index e3b39e3..9f671dd 100644
--- a/python/private/python_bootstrap_template.txt
+++ b/python/private/python_bootstrap_template.txt
@@ -425,12 +425,21 @@
         directory under the runfiles tree, and will recursively delete the
         runfiles directory if set.
   """
+  instrumented_files = [abs_path for abs_path, _ in InstrumentedFilePaths()]
+  unique_dirs = {os.path.dirname(file) for file in instrumented_files}
+  source = "\n\t".join(unique_dirs)
+
+  PrintVerboseCoverage("[coveragepy] Instrumented Files:\n" + "\n".join(instrumented_files))
+  PrintVerboseCoverage("[coveragepy] Sources:\n" + "\n".join(unique_dirs))
+
   # We need for coveragepy to use relative paths.  This can only be configured
   unique_id = uuid.uuid4()
   rcfile_name = os.path.join(os.environ['COVERAGE_DIR'], ".coveragerc_{}".format(unique_id))
   with open(rcfile_name, "w") as rcfile:
-    rcfile.write('''[run]
+    rcfile.write(f'''[run]
 relative_files = True
+source =
+\t{source}
 ''')
   PrintVerboseCoverage('Coverage entrypoint:', coverage_entrypoint)
   # First run the target Python file via coveragepy to create a .coverage
diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py
index b1f6b03..4687bc0 100644
--- a/python/private/stage2_bootstrap_template.py
+++ b/python/private/stage2_bootstrap_template.py
@@ -276,6 +276,13 @@
         yield
         return
 
+    instrumented_files = [abs_path for abs_path, _ in instrumented_file_paths()]
+    unique_dirs = {os.path.dirname(file) for file in instrumented_files}
+    source = "\n\t".join(unique_dirs)
+
+    print_verbose_coverage("Instrumented Files:\n" + "\n".join(instrumented_files))
+    print_verbose_coverage("Sources:\n" + "\n".join(unique_dirs))
+
     import uuid
 
     import coverage
@@ -289,8 +296,10 @@
     print_verbose_coverage("coveragerc file:", rcfile_name)
     with open(rcfile_name, "w") as rcfile:
         rcfile.write(
-            """[run]
+            f"""[run]
 relative_files = True
+source =
+\t{source}
 """
         )
     try: