Repository: pytest-dev/pytest-cov (language: python)

Issue:
Removing message formatting rounding
When defining a coverage threshold (88.45 for example) and the actual coverage is very close (88.449  for example) we will get a failure message. However, the message will be: "FAIL Required test coverage of 88.45% not reached. Total coverage: 88.45%" where it's wrong and confusing since we failed because it's under while it the message it presented as equal. The removing of the rounding will fix it.
fail_under setting with precision is not working
# Summary

I have `report: precision` set to 2 and `fail_under` set to 97.47, and my test coverage total is reading as 97.47, but I'm getting a failure message and failure code (exit code 2).

## Expected vs actual result

Expected: test coverage passes
Actual: `FAIL Required test coverage of 97.47% not reached. Total coverage: 97.47%`

I even tried modifying `fail_under` to 97.469, in which case I got this even more nonsensical message:

`FAIL Required test coverage of 97.469% not reached. Total coverage: 97.47%`

# Reproducer

## Versions

Output of relevant packages `pip list`, `python --version`, `pytest --version` etc.

Make sure you include complete output of `tox` if you use it (it will show versions of various things).

```
Python 3.7.5
pipenv, version 2018.11.26
pytest version 5.4.1
pytest-cov 2.8.1
```

## Config

Include your `tox.ini`, `pytest.ini`, `.coveragerc`, `setup.cfg` or any relevant configuration.

```
# .coveragerc
[report]
fail_under = 97.47
precision = 2
skip_covered = true
show_missing = true
```

## Code

Link to your repository, gist, pastebin or just paste raw code that illustrates the issue.

If you paste raw code make sure you quote it, eg:

https://github.com/votingworks/arlo/pull/447/commits/89c50e43216963f06af6e4c5104b67fd33e4ff36



Discussion / hints:
Hello @AdiKrasin, can this fix get merged and released?
@ionelmc Can you have a look at this? Thanks again
If you are not ok with this change, I'll gladly come up with a PR fixing it as well matching your expectations. This is a long standing issue that is quite painful with long running test suites (because it's a pain to relaunch it to know the coverage) that are distributed (so cannot run with coverage only). 

I'd argue a more in-line change would be to read the coverage "precision" setting and report it the same way coverage is. Is that something you would be willing to merge and release?
Thanks. Can you provide details about how you configure your virtualenv and run your tests?
Thanks for the quick response!

Sure, I think most of what you're looking for is here: https://github.com/votingworks/arlo/blob/89c50e43216963f06af6e4c5104b67fd33e4ff36/Makefile.

Here are the relevant bits for running tests/coverage:

```make
PIPENV=python3.7 -m pipenv

test-server:
	FLASK_ENV=test ${PIPENV} run python -m pytest ${FILE} \
		-k '${TEST}' --ignore=arlo-client -vv ${FLAGS}

test-server-coverage:
	FLAGS='--cov=. ${FLAGS}' make test-server
```

I don't know exactly what to tell you about the virutalenv. I didn't set up the repo and don't quite understand how it all works to be honest.
Unrelated to this bug, but I also realized as I've been working with the test coverage more that it would be more useful to me to be able to set a threshold for the actual number of missed lines, instead of a percentage.

I am introducing test coverage to a repo that didn't have it before, so I'm trying to lock in the coverage at it's current state so I don't regress (until I have time to invest in covering all the remaining bits). The problem with using a percentage is that whenever I write new code, it changes the percentage. Even if all the new code is covered, the percentage increases... So I'll have to update the fail_under threshold with each PR.

If I could lock in the actual number of uncovered lines, then it would be a much more useful baseline to compare to when I add new code.

Wondering if you have thoughts on this. If useful, I could open up a new issue to discuss.
I can reproduce this issue:
<img width="686" alt="Screenshot 2020-06-02 07 33 04" src="https://user-images.githubusercontent.com/9391/83520469-5a4f6a80-a4a3-11ea-9a28-31938cb8723a.png">

I also experience this issue: 
[requirements.txt](https://gist.github.com/dogmatic69/4723e2fd41cc2f72e075a780580db4e3)
```
	docker run --rm \
		-v ${TEST_PATH}:/tests \
		-v ${SRC_PATH}:/src \
		--entrypoint pytest \
		-e PYTHONPATH=/src:/tests \
		${DOCKER_IMAGE}  -W ignore::DeprecationWarning -v -x \
			--cov-report html:/tests/coverage \
			--cov=/src \
			--cov-branch \
			--cov-fail-under=35.7 \
			/tests

----------- coverage: platform linux, python 3.8.2-final-0 -----------
Coverage HTML written to dir /tests/coverage

FAIL Required test coverage of 35.7% not reached. Total coverage: 35.70%

============================== 63 passed in 1.37s ==============================
```
Playing with the numbers:

```
FAIL Required test coverage of 35.699% not reached. Total coverage: 35.70%
Required test coverage of 35.69% reached. Total coverage: 35.70%
```
If you are seeing this issue, can you increase the reporting precision to see what the actual coverage value is?  For example, if the total coverage is 93.18757, it will be reported to two decimal places as 93.19, but the actual value is less than 93.189.
This [PR](https://github.com/pytest-dev/pytest-cov/pull/611) fixes this reporting issue.

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fe74b356..6bc8d8cd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,13 +6,13 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)'
 # Note the order is intentional to avoid multiple passes of the hooks
 repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.4.5
+    rev: v0.6.5
     hooks:
       - id: ruff
         args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes]
       - id: ruff-format
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0
+    rev: v4.6.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 44e92874..81e0b8e6 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -2,7 +2,7 @@
 Authors
 =======
 
-* Marc Schlaich - \http://www.schlamar.org
+* Marc Schlaich - https://github.com/schlamar (\http://www.schlamar.org)
 * Rick van Hattem - http://wol.ph
 * Buck Evan - https://github.com/bukzor
 * Eric Larson - http://larsoner.com
@@ -19,7 +19,7 @@ Authors
 * Michael Elovskikh - https://github.com/wronglink
 * Saurabh Kumar - https://github.com/theskumar
 * Michael Elovskikh - https://github.com/wronglink
-* Daniel Hahler - https://daniel.hahler.de
+* Daniel Hahler - https://github.com/blueyed (\https://daniel.hahler.de)
 * Florian Bruhin - http://www.the-compiler.org
 * Zoltan Kozma - https://github.com/kozmaz87
 * Francis Niu - https://flniu.github.io
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5dd46aa3..9adf1341 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,13 @@
 Changelog
 =========
 
+5.1.0 (2024-09-??)
+------------------
+
+* Updated various documentation inaccuracies, especially on subprocess handling.
+* Changed fail under checks to use the precision set in the coverage configuration.
+  Now it will perform the check just like ``coverage report`` would.
+
 5.0.0 (2024-03-24)
 ------------------
 
diff --git a/MANIFEST.in b/MANIFEST.in
index 22042a59..529ba8f4 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -25,5 +25,6 @@ include CHANGELOG.rst
 include CONTRIBUTING.rst
 include LICENSE
 include README.rst
+include SECURITY.md
 
 global-exclude *.py[cod] __pycache__/* *.so *.dylib
diff --git a/pyproject.toml b/pyproject.toml
index fe8d1f8b..bdcc05fd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,6 @@ select = [
     "PLE", # pylint errors
     "PT", # flake8-pytest-style
     "PTH", # flake8-use-pathlib
-    "Q", # flake8-quotes
     "RSE", # flake8-raise
     "RUF", # ruff-specific rules
     "S", # flake8-bandit
@@ -45,12 +44,9 @@ select = [
 fixture-parentheses = false
 mark-parentheses = false
 
-[tool.ruff.lint.flake8-quotes]
-inline-quotes = "single"
-
 [tool.ruff.lint.isort]
 forced-separate = ["conftest"]
 force-single-line = true
 
 [tool.ruff.format]
-quote-style = "preserve"
+quote-style = "single"
diff --git a/setup.py b/setup.py
index 221111e8..080ac348 100755
--- a/setup.py
+++ b/setup.py
@@ -127,7 +127,7 @@ def run(self):
     python_requires='>=3.8',
     install_requires=[
         'pytest>=4.6',
-        'coverage[toml]>=5.2.1',
+        'coverage[toml]>=7.5',
     ],
     extras_require={
         'testing': [
diff --git a/tox.ini b/tox.ini
index 4cbe6c8a..5c300fc7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -14,7 +14,7 @@ envlist =
     clean,
     check,
     docs,
-    {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}-pytest{81}-xdist{350}-coverage{74},
+    {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}-pytest{81}-xdist{350}-coverage{75,76},
     report
 ignore_basepython_conflict = true
 
@@ -81,6 +81,8 @@ setenv =
     coverage72: _DEP_COVERAGE=coverage==7.2.7
     coverage73: _DEP_COVERAGE=coverage==7.3.4
     coverage74: _DEP_COVERAGE=coverage==7.4.4
+    coverage75: _DEP_COVERAGE=coverage==7.5.4
+    coverage76: _DEP_COVERAGE=coverage==7.6.1
     # For testing against a coverage.py working tree.
     coveragedev: _DEP_COVERAGE=-e{env:COVERAGE_HOME}
 passenv =


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 84753edb..f701965a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -60,149 +60,293 @@ jobs:
             toxpython: 'python3.11'
             tox_env: 'docs'
             os: 'ubuntu-latest'
-          - name: 'py38-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'py38-pytest81-xdist350-coverage75 (ubuntu)'
             python: '3.8'
             toxpython: 'python3.8'
             python_arch: 'x64'
-            tox_env: 'py38-pytest81-xdist350-coverage74'
+            tox_env: 'py38-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'py38-pytest81-xdist350-coverage74 (windows)'
+          - name: 'py38-pytest81-xdist350-coverage75 (windows)'
             python: '3.8'
             toxpython: 'python3.8'
             python_arch: 'x64'
-            tox_env: 'py38-pytest81-xdist350-coverage74'
+            tox_env: 'py38-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'py38-pytest81-xdist350-coverage74 (macos)'
+          - name: 'py38-pytest81-xdist350-coverage75 (macos)'
+            python: '3.8'
+            toxpython: 'python3.8'
+            python_arch: 'arm64'
+            tox_env: 'py38-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'py38-pytest81-xdist350-coverage76 (ubuntu)'
+            python: '3.8'
+            toxpython: 'python3.8'
+            python_arch: 'x64'
+            tox_env: 'py38-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'py38-pytest81-xdist350-coverage76 (windows)'
             python: '3.8'
             toxpython: 'python3.8'
             python_arch: 'x64'
-            tox_env: 'py38-pytest81-xdist350-coverage74'
+            tox_env: 'py38-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'py38-pytest81-xdist350-coverage76 (macos)'
+            python: '3.8'
+            toxpython: 'python3.8'
+            python_arch: 'arm64'
+            tox_env: 'py38-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'py39-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'py39-pytest81-xdist350-coverage75 (ubuntu)'
             python: '3.9'
             toxpython: 'python3.9'
             python_arch: 'x64'
-            tox_env: 'py39-pytest81-xdist350-coverage74'
+            tox_env: 'py39-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'py39-pytest81-xdist350-coverage74 (windows)'
+          - name: 'py39-pytest81-xdist350-coverage75 (windows)'
             python: '3.9'
             toxpython: 'python3.9'
             python_arch: 'x64'
-            tox_env: 'py39-pytest81-xdist350-coverage74'
+            tox_env: 'py39-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'py39-pytest81-xdist350-coverage74 (macos)'
+          - name: 'py39-pytest81-xdist350-coverage75 (macos)'
+            python: '3.9'
+            toxpython: 'python3.9'
+            python_arch: 'arm64'
+            tox_env: 'py39-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'py39-pytest81-xdist350-coverage76 (ubuntu)'
+            python: '3.9'
+            toxpython: 'python3.9'
+            python_arch: 'x64'
+            tox_env: 'py39-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'py39-pytest81-xdist350-coverage76 (windows)'
             python: '3.9'
             toxpython: 'python3.9'
             python_arch: 'x64'
-            tox_env: 'py39-pytest81-xdist350-coverage74'
+            tox_env: 'py39-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'py39-pytest81-xdist350-coverage76 (macos)'
+            python: '3.9'
+            toxpython: 'python3.9'
+            python_arch: 'arm64'
+            tox_env: 'py39-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'py310-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'py310-pytest81-xdist350-coverage75 (ubuntu)'
             python: '3.10'
             toxpython: 'python3.10'
             python_arch: 'x64'
-            tox_env: 'py310-pytest81-xdist350-coverage74'
+            tox_env: 'py310-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'py310-pytest81-xdist350-coverage74 (windows)'
+          - name: 'py310-pytest81-xdist350-coverage75 (windows)'
             python: '3.10'
             toxpython: 'python3.10'
             python_arch: 'x64'
-            tox_env: 'py310-pytest81-xdist350-coverage74'
+            tox_env: 'py310-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'py310-pytest81-xdist350-coverage74 (macos)'
+          - name: 'py310-pytest81-xdist350-coverage75 (macos)'
+            python: '3.10'
+            toxpython: 'python3.10'
+            python_arch: 'arm64'
+            tox_env: 'py310-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'py310-pytest81-xdist350-coverage76 (ubuntu)'
+            python: '3.10'
+            toxpython: 'python3.10'
+            python_arch: 'x64'
+            tox_env: 'py310-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'py310-pytest81-xdist350-coverage76 (windows)'
             python: '3.10'
             toxpython: 'python3.10'
             python_arch: 'x64'
-            tox_env: 'py310-pytest81-xdist350-coverage74'
+            tox_env: 'py310-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'py310-pytest81-xdist350-coverage76 (macos)'
+            python: '3.10'
+            toxpython: 'python3.10'
+            python_arch: 'arm64'
+            tox_env: 'py310-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'py311-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'py311-pytest81-xdist350-coverage75 (ubuntu)'
             python: '3.11'
             toxpython: 'python3.11'
             python_arch: 'x64'
-            tox_env: 'py311-pytest81-xdist350-coverage74'
+            tox_env: 'py311-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'py311-pytest81-xdist350-coverage74 (windows)'
+          - name: 'py311-pytest81-xdist350-coverage75 (windows)'
             python: '3.11'
             toxpython: 'python3.11'
             python_arch: 'x64'
-            tox_env: 'py311-pytest81-xdist350-coverage74'
+            tox_env: 'py311-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'py311-pytest81-xdist350-coverage74 (macos)'
+          - name: 'py311-pytest81-xdist350-coverage75 (macos)'
+            python: '3.11'
+            toxpython: 'python3.11'
+            python_arch: 'arm64'
+            tox_env: 'py311-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'py311-pytest81-xdist350-coverage76 (ubuntu)'
+            python: '3.11'
+            toxpython: 'python3.11'
+            python_arch: 'x64'
+            tox_env: 'py311-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'py311-pytest81-xdist350-coverage76 (windows)'
             python: '3.11'
             toxpython: 'python3.11'
             python_arch: 'x64'
-            tox_env: 'py311-pytest81-xdist350-coverage74'
+            tox_env: 'py311-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'py311-pytest81-xdist350-coverage76 (macos)'
+            python: '3.11'
+            toxpython: 'python3.11'
+            python_arch: 'arm64'
+            tox_env: 'py311-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'py312-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'py312-pytest81-xdist350-coverage75 (ubuntu)'
             python: '3.12'
             toxpython: 'python3.12'
             python_arch: 'x64'
-            tox_env: 'py312-pytest81-xdist350-coverage74'
+            tox_env: 'py312-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'py312-pytest81-xdist350-coverage74 (windows)'
+          - name: 'py312-pytest81-xdist350-coverage75 (windows)'
             python: '3.12'
             toxpython: 'python3.12'
             python_arch: 'x64'
-            tox_env: 'py312-pytest81-xdist350-coverage74'
+            tox_env: 'py312-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'py312-pytest81-xdist350-coverage74 (macos)'
+          - name: 'py312-pytest81-xdist350-coverage75 (macos)'
+            python: '3.12'
+            toxpython: 'python3.12'
+            python_arch: 'arm64'
+            tox_env: 'py312-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'py312-pytest81-xdist350-coverage76 (ubuntu)'
+            python: '3.12'
+            toxpython: 'python3.12'
+            python_arch: 'x64'
+            tox_env: 'py312-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'py312-pytest81-xdist350-coverage76 (windows)'
             python: '3.12'
             toxpython: 'python3.12'
             python_arch: 'x64'
-            tox_env: 'py312-pytest81-xdist350-coverage74'
+            tox_env: 'py312-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'py312-pytest81-xdist350-coverage76 (macos)'
+            python: '3.12'
+            toxpython: 'python3.12'
+            python_arch: 'arm64'
+            tox_env: 'py312-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'pypy38-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'pypy38-pytest81-xdist350-coverage75 (ubuntu)'
             python: 'pypy-3.8'
             toxpython: 'pypy3.8'
             python_arch: 'x64'
-            tox_env: 'pypy38-pytest81-xdist350-coverage74'
+            tox_env: 'pypy38-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'pypy38-pytest81-xdist350-coverage74 (windows)'
+          - name: 'pypy38-pytest81-xdist350-coverage75 (windows)'
             python: 'pypy-3.8'
             toxpython: 'pypy3.8'
             python_arch: 'x64'
-            tox_env: 'pypy38-pytest81-xdist350-coverage74'
+            tox_env: 'pypy38-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'pypy38-pytest81-xdist350-coverage74 (macos)'
+          - name: 'pypy38-pytest81-xdist350-coverage75 (macos)'
+            python: 'pypy-3.8'
+            toxpython: 'pypy3.8'
+            python_arch: 'arm64'
+            tox_env: 'pypy38-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'pypy38-pytest81-xdist350-coverage76 (ubuntu)'
+            python: 'pypy-3.8'
+            toxpython: 'pypy3.8'
+            python_arch: 'x64'
+            tox_env: 'pypy38-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'pypy38-pytest81-xdist350-coverage76 (windows)'
             python: 'pypy-3.8'
             toxpython: 'pypy3.8'
             python_arch: 'x64'
-            tox_env: 'pypy38-pytest81-xdist350-coverage74'
+            tox_env: 'pypy38-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'pypy38-pytest81-xdist350-coverage76 (macos)'
+            python: 'pypy-3.8'
+            toxpython: 'pypy3.8'
+            python_arch: 'arm64'
+            tox_env: 'pypy38-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'pypy39-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'pypy39-pytest81-xdist350-coverage75 (ubuntu)'
             python: 'pypy-3.9'
             toxpython: 'pypy3.9'
             python_arch: 'x64'
-            tox_env: 'pypy39-pytest81-xdist350-coverage74'
+            tox_env: 'pypy39-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'pypy39-pytest81-xdist350-coverage74 (windows)'
+          - name: 'pypy39-pytest81-xdist350-coverage75 (windows)'
             python: 'pypy-3.9'
             toxpython: 'pypy3.9'
             python_arch: 'x64'
-            tox_env: 'pypy39-pytest81-xdist350-coverage74'
+            tox_env: 'pypy39-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'pypy39-pytest81-xdist350-coverage74 (macos)'
+          - name: 'pypy39-pytest81-xdist350-coverage75 (macos)'
+            python: 'pypy-3.9'
+            toxpython: 'pypy3.9'
+            python_arch: 'arm64'
+            tox_env: 'pypy39-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'pypy39-pytest81-xdist350-coverage76 (ubuntu)'
+            python: 'pypy-3.9'
+            toxpython: 'pypy3.9'
+            python_arch: 'x64'
+            tox_env: 'pypy39-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'pypy39-pytest81-xdist350-coverage76 (windows)'
             python: 'pypy-3.9'
             toxpython: 'pypy3.9'
             python_arch: 'x64'
-            tox_env: 'pypy39-pytest81-xdist350-coverage74'
+            tox_env: 'pypy39-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'pypy39-pytest81-xdist350-coverage76 (macos)'
+            python: 'pypy-3.9'
+            toxpython: 'pypy3.9'
+            python_arch: 'arm64'
+            tox_env: 'pypy39-pytest81-xdist350-coverage76'
             os: 'macos-latest'
-          - name: 'pypy310-pytest81-xdist350-coverage74 (ubuntu)'
+          - name: 'pypy310-pytest81-xdist350-coverage75 (ubuntu)'
             python: 'pypy-3.10'
             toxpython: 'pypy3.10'
             python_arch: 'x64'
-            tox_env: 'pypy310-pytest81-xdist350-coverage74'
+            tox_env: 'pypy310-pytest81-xdist350-coverage75'
             os: 'ubuntu-latest'
-          - name: 'pypy310-pytest81-xdist350-coverage74 (windows)'
+          - name: 'pypy310-pytest81-xdist350-coverage75 (windows)'
             python: 'pypy-3.10'
             toxpython: 'pypy3.10'
             python_arch: 'x64'
-            tox_env: 'pypy310-pytest81-xdist350-coverage74'
+            tox_env: 'pypy310-pytest81-xdist350-coverage75'
             os: 'windows-latest'
-          - name: 'pypy310-pytest81-xdist350-coverage74 (macos)'
+          - name: 'pypy310-pytest81-xdist350-coverage75 (macos)'
+            python: 'pypy-3.10'
+            toxpython: 'pypy3.10'
+            python_arch: 'arm64'
+            tox_env: 'pypy310-pytest81-xdist350-coverage75'
+            os: 'macos-latest'
+          - name: 'pypy310-pytest81-xdist350-coverage76 (ubuntu)'
+            python: 'pypy-3.10'
+            toxpython: 'pypy3.10'
+            python_arch: 'x64'
+            tox_env: 'pypy310-pytest81-xdist350-coverage76'
+            os: 'ubuntu-latest'
+          - name: 'pypy310-pytest81-xdist350-coverage76 (windows)'
             python: 'pypy-3.10'
             toxpython: 'pypy3.10'
             python_arch: 'x64'
-            tox_env: 'pypy310-pytest81-xdist350-coverage74'
+            tox_env: 'pypy310-pytest81-xdist350-coverage76'
+            os: 'windows-latest'
+          - name: 'pypy310-pytest81-xdist350-coverage76 (macos)'
+            python: 'pypy-3.10'
+            toxpython: 'pypy3.10'
+            python_arch: 'arm64'
+            tox_env: 'pypy310-pytest81-xdist350-coverage76'
             os: 'macos-latest'
     steps:
     - uses: actions/checkout@v4
diff --git a/ci/templates/.github/workflows/test.yml b/ci/templates/.github/workflows/test.yml
index 39e64078..22fec036 100644
--- a/ci/templates/.github/workflows/test.yml
+++ b/ci/templates/.github/workflows/test.yml
@@ -17,15 +17,15 @@ jobs:
           - {python-version: "pypy-3.9", tox-python-version: "pypy3"}
           - {python-version: "3.11", tox-python-version: "py311"}
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
       - name: Cache
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: ~/.cache/pip
           key:
@@ -77,7 +77,7 @@ jobs:
 {% for os, python_arch in [
     ['ubuntu', 'x64'],
     ['windows', 'x64'],
-    ['macos', 'x64'],
+    ['macos', 'arm64'],
 ] %}
           - name: '{{ env }} ({{ os }})'
             python: '{{ python }}'
diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py
index 40ab15f3..f0835af6 100644
--- a/src/pytest_cov/plugin.py
+++ b/src/pytest_cov/plugin.py
@@ -8,6 +8,8 @@
 
 import coverage
 import pytest
+from coverage.results import display_covered
+from coverage.results import should_fail_under
 
 from . import compat
 from . import embed
@@ -31,6 +33,10 @@ class CovReportWarning(PytestCovWarning):
     """Indicates that we failed to generate a report"""
 
 
+class CovFailUnderWarning(PytestCovWarning):
+    """Indicates that we failed to generate a report"""
+
+
 def validate_report(arg):
     file_choices = ['annotate', 'html', 'xml', 'json', 'lcov']
     term_choices = ['term', 'term-missing']
@@ -270,6 +276,7 @@ class Config:
         cov_config = self.cov_controller.cov.config
         if self.options.cov_fail_under is None and hasattr(cov_config, 'fail_under'):
             self.options.cov_fail_under = cov_config.fail_under
+        self.options.cov_precision = getattr(cov_config, 'precision', 0)
 
     def _is_worker(self, session):
         return getattr(session.config, 'workerinput', None) is not None
@@ -318,10 +325,6 @@ def _should_report(self):
         needed = self.options.cov_report or self.options.cov_fail_under
         return needed and not (self.failed and self.options.no_cov_on_fail)
 
-    def _failed_cov_total(self):
-        cov_fail_under = self.options.cov_fail_under
-        return cov_fail_under is not None and self.cov_total < cov_fail_under
-
     # we need to wrap pytest_runtestloop. by the time pytest_sessionfinish
     # runs, it's too late to set testsfailed
     @pytest.hookimpl(hookwrapper=True)
@@ -346,11 +349,22 @@ def pytest_runtestloop(self, session):
                 self.cov_total = self.cov_controller.summary(self.cov_report)
             except CoverageException as exc:
                 message = f'Failed to generate report: {exc}\n'
-                session.config.pluginmanager.getplugin('terminalreporter').write(f'WARNING: {message}\n', red=True, bold=True)
+                session.config.pluginmanager.getplugin('terminalreporter').write(f'\nWARNING: {message}\n', red=True, bold=True)
                 warnings.warn(CovReportWarning(message), stacklevel=1)
                 self.cov_total = 0
             assert self.cov_total is not None, 'Test coverage should never be `None`'
-            if self._failed_cov_total() and not self.options.collectonly:
+            cov_fail_under = self.options.cov_fail_under
+            cov_precision = self.options.cov_precision
+            if cov_fail_under is None or self.options.collectonly:
+                return
+            if should_fail_under(self.cov_total, cov_fail_under, cov_precision):
+                message = 'Coverage failure: total of {total} is less than fail-under={fail_under:.{p}f}'.format(
+                    total=display_covered(self.cov_total, cov_precision),
+                    fail_under=cov_fail_under,
+                    p=cov_precision,
+                )
+                session.config.pluginmanager.getplugin('terminalreporter').write(f'\nERROR: {message}\n', red=True, bold=True)
+                warnings.warn(CovFailUnderWarning(message), stacklevel=1)
                 # make sure we get the EXIT_TESTSFAILED exit code
                 compat_session.testsfailed += 1
 
diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py
index 114d34bb..0969267a 100644
--- a/tests/test_pytest_cov.py
+++ b/tests/test_pytest_cov.py
@@ -454,7 +454,10 @@ def test_cov_min_float_value(testdir):
 
 def test_cov_min_float_value_not_reached(testdir):
     script = testdir.makepyfile(SCRIPT)
-
+    testdir.tmpdir.join('.coveragerc').write("""
+[report]
+precision = 3
+""")
     result = testdir.runpytest('-v', f'--cov={script.dirpath()}', '--cov-report=term-missing', '--cov-fail-under=88.89', script)
     assert result.ret == 1
     result.stdout.fnmatch_lines(['FAIL Required test coverage of 88.89% not reached. Total coverage: 88.89%'])
@@ -1692,7 +1695,7 @@ def bad_init():
     monkeypatch.setattr(sys, 'stderr', buff)
     monkeypatch.setitem(os.environ, 'COV_CORE_SOURCE', 'foobar')
     exec(payload)
-    expected = "pytest-cov: Failed to setup subprocess coverage. " "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n"
+    expected = 'pytest-cov: Failed to setup subprocess coverage. ' "Environ: {'COV_CORE_SOURCE': 'foobar'} Exception: SpecificError()\n"
     assert buff.getvalue() == expected