Repository: AcademySoftwareFoundation/rez (language: python)

Issue:
Track code coverage
[OpenSSF Best Practices](https://www.bestpractices.dev/en) badges requires us to have code coverage and track coverage over time.

It's a good thing to have anyway :)

Requirements:
* https://www.bestpractices.dev/en/projects/8389?criteria_level=1#test_statement_coverage80
* https://www.bestpractices.dev/en/projects/8389?criteria_level=2#test_branch_coverage80


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 33b623e81..a08a4ca56 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,6 +1,8 @@
 [run]
+# Relative is needed for coverage to be merged by SonarCloud
+relative_files = True
 branch = True
 omit =
     src/rez/tests/*,
     src/rez/completion/*,
-    src/rez/vendor/*
\ No newline at end of file
+    src/rez/vendor/*
diff --git a/.gitignore b/.gitignore
index 6e77c10b4..09eebc709 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ build/
 dist/
 htmlcov/
 .coverage
+coverage.xml
 *~
 docs/_build
 .DS_Store
diff --git a/.sonarcloud.properties b/.sonarcloud.properties
index baf07821e..9efdc32b4 100644
--- a/.sonarcloud.properties
+++ b/.sonarcloud.properties
@@ -25,5 +25,4 @@ sonar.projectName=rez
 
 # Source properties
 sonar.sources=src
-sonar.exclusions=src/build_utils/**,src/rez/backport/**,src/rez/data/**,src/rez/tests/**,src/rez/vendor/**
-
+sonar.exclusions=src/build_utils/**,src/rez/data/**,src/rez/tests/**,src/rez/vendor/**
diff --git a/README.md b/README.md
index cd8cfc16f..30bfda1fb 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@
 [![Docs](https://readthedocs.org/projects/rez/badge/?version=stable)](https://rez.readthedocs.io/en/stable)
 [![PyPI](https://github.com/AcademySoftwareFoundation/rez/workflows/pypi/badge.svg)](https://github.com/AcademySoftwareFoundation/rez/actions?query=workflow%3Apypi+event%3Arelease)
 [![Benchmark](https://github.com/AcademySoftwareFoundation/rez/workflows/benchmark/badge.svg)](https://github.com/AcademySoftwareFoundation/rez/actions?query=workflow%3Abenchmark+event%3Arelease)<br>
+[![Coverage](https://codecov.io/gh/AcademySoftwareFoundation/rez/graph/badge.svg?token=FLYggQOE7W)](https://codecov.io/gh/AcademySoftwareFoundation/rez)
 [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=AcademySoftwareFoundation_rez&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=AcademySoftwareFoundation_rez)
 [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=AcademySoftwareFoundation_rez&metric=bugs)](https://sonarcloud.io/summary/new_code?id=AcademySoftwareFoundation_rez)
 [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=AcademySoftwareFoundation_rez&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=AcademySoftwareFoundation_rez)
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 000000000..90ff2e0a5
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,11 @@
+coverage:
+  status:
+    project: off
+
+github_checks:
+  annotations: false
+
+ignore:
+  - "src/rez/tests"
+  - "src/rez/vendor"
+  - "src/rez/completion"


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 097c56be1..37b23583d 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -83,6 +83,14 @@ jobs:
       run: rez-python -m pip install pytest-cov parameterized
 
     - name: Run tests
-      run: rez-selftest -v
+      run: rez-selftest -v -- --cov=rez --cov-report=xml:coverage.xml
       env:
         _REZ_ENSURE_TEST_SHELLS: ${{ matrix.shells }}
+
+    - name: Upload coverage reports to Codecov
+      uses: codecov/codecov-action@v4
+      with:
+        slug: AcademySoftwareFoundation/rez
+        files: 'coverage.xml'
+      env:
+        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index d87389e84..000000000
--- a/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-addopts = --cov=src/rez --cov-report=html --cov-config=.coveragerc
\ No newline at end of file
diff --git a/src/rez/cli/selftest.py b/src/rez/cli/selftest.py
index d99cb8d71..ec4d0e39d 100644
--- a/src/rez/cli/selftest.py
+++ b/src/rez/cli/selftest.py
@@ -94,14 +94,7 @@ def command(opts, parser, extra_arg_groups=None):
 
     try:
         if use_pytest:
-            cwd = os.getcwd()
-            os.chdir(tests_dir)
-            try:
-                run_pytest(module_tests, opts.tests, opts.verbose,
-                           extra_arg_groups)
-            finally:
-                os.chdir(cwd)
-
+            run_pytest(module_tests, opts.tests, opts.verbose, extra_arg_groups)
         else:
             run_unittest(module_tests, opts.tests, opts.verbose)
     finally:
@@ -121,6 +114,8 @@ def run_unittest(module_tests, tests, verbosity):
 def run_pytest(module_tests, tests, verbosity, extra_arg_groups):
     from pytest import main
 
+    tests_dir = os.path.abspath(os.path.join(__file__, "..", "..", "tests"))
+
     # parse test name, e.g.
     #   "rez.tests.test_solver.TestSolver.test_01"
     # into
@@ -133,11 +128,11 @@ def run_pytest(module_tests, tests, verbosity, extra_arg_groups):
                 specifier += "::" + part
                 continue
             if os.path.isfile(part + ".py"):
-                specifier = part + ".py"
+                specifier = os.path.join(tests_dir, f"{part}.py")
         if specifier:
             test_specifications.append(specifier)
 
-    module_tests = [("test_%s.py" % x) for x in sorted(module_tests)]
+    module_tests = [os.path.join(tests_dir, f"test_{x}.py") for x in sorted(module_tests)]
     tests = module_tests + test_specifications
 
     argv = tests[:]