Skip to content

Commit a11a99b

Browse files
committed
Finalize how we will handle no coverage for new projects in the PTB
* Switch COVERAGE_FILE to COVERAGE_DB
1 parent 02155ce commit a11a99b

File tree

3 files changed

+118
-38
lines changed

3 files changed

+118
-38
lines changed

doc/user_guide/getting_started.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,14 @@ We also need to configure settings for github-pages environment:
204204
8. Set up for Sonar
205205
+++++++++++++++++++
206206
PTB supports using SonarQube Cloud to analyze, visualize, & track linting, security, &
207-
coverage. In order to set it up, you'll need to do the following instructions.
207+
coverage. All of our Python projects are evaluated against the
208+
`Exasol Way <https://sonarcloud.io/organizations/exasol/quality_gates/show/AXxvLH-3BdtLlpiYmZhh>`__
209+
and subscribe to the
210+
`Clean as You Code <https://docs.sonarsource.com/sonarqube-server/9.8/user-guide/clean-as-you-code/>`__
211+
methodology, which means that SonarQube analysis will fail and, if its included in the branch protections, block a PR
212+
if code modified in that PR does not meet the standards of the Exasol Way.
213+
214+
In order to set up Sonar, you will need to perform the following instructions.
208215

209216
For a **public** project
210217
^^^^^^^^^^^^^^^^^^^^^^^^
@@ -223,6 +230,11 @@ For a **public** project
223230
hostUrl = "https://sonarcloud.io"
224231
organization = "exasol"
225232
6. Post-merge, update the branch protections to include SonarQube analysis
233+
* This should only be done when tests exist for the project, & that the project is
234+
at a state in which enforced code coverage would not be a burden. For new projects,
235+
we recommend creating an issue to add the SonarQube analysis to the branch protections
236+
at a later point. In such scenarios, SonarQube analysis will still report its analysis
237+
results to the PR, but it will not prevent the PR from being merged.
226238

227239
For a **private** project
228240
^^^^^^^^^^^^^^^^^^^^^^^^^

exasol/toolbox/nox/_artifacts.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
Config,
1919
)
2020

21-
COVERAGE_FILE = ".coverage"
21+
COVERAGE_DB = ".coverage"
2222
COVERAGE_XML = "ci-coverage.xml"
2323
LINT_JSON = ".lint.json"
2424
LINT_TXT = ".lint.txt"
2525
SECURITY_JSON = ".security.json"
2626

27-
ALL_LINT_FILES = {COVERAGE_FILE, LINT_JSON, LINT_TXT, SECURITY_JSON}
27+
ALL_LINT_FILES = {COVERAGE_DB, LINT_JSON, LINT_TXT, SECURITY_JSON}
2828
COVERAGE_TABLES = {"coverage_schema", "meta", "file", "line_bits"}
2929
LINT_JSON_ATTRIBUTES = {
3030
"type",
@@ -55,7 +55,7 @@ def check_artifacts(session: Session) -> None:
5555
_is_valid_lint_txt(Path(PROJECT_CONFIG.root, LINT_TXT)),
5656
_is_valid_lint_json(Path(PROJECT_CONFIG.root, LINT_JSON)),
5757
_is_valid_security_json(Path(PROJECT_CONFIG.root, SECURITY_JSON)),
58-
_is_valid_coverage(Path(PROJECT_CONFIG.root, COVERAGE_FILE)),
58+
_is_valid_coverage(Path(PROJECT_CONFIG.root, COVERAGE_DB)),
5959
]
6060
if not all(all_is_valid_checks):
6161
sys.exit(1)
@@ -146,7 +146,7 @@ def copy_artifacts(session: Session) -> None:
146146

147147
artifact_dir = Path(session.posargs[0])
148148
suffix = _python_version_suffix()
149-
_combine_coverage(session, artifact_dir, f"coverage{suffix}*/{COVERAGE_FILE}")
149+
_combine_coverage(session, artifact_dir, f"coverage{suffix}*/{COVERAGE_DB}")
150150
_copy_artifacts(
151151
artifact_dir,
152152
artifact_dir.parent,
@@ -189,14 +189,15 @@ def _prepare_coverage_xml(session: Session, source: Path) -> None:
189189
Prepare the coverage XML for input into Sonar
190190
191191
The coverage XML is used within Sonar to determine the overall coverage in our
192-
project, as well as, the coverage associated with changes made in a PR. If the
193-
coverage in our PR does not meet the set Sonar guidelines for that project,
194-
then when the sonarqubecloud bot writes in the PR, it will indicate that the
195-
coverage constraint was not met & block the PR from being merged. Thus, in the
196-
preparation of the coverage XML, we set `--fail-under=0` so that this sub-step
197-
will not fail prior to sending the data to Sonar. Otherwise, this sub-step would
198-
fail whenever the `fail_under` in `[tool.coverage.report]` of the `pyproject.toml`
199-
was not met.
192+
project and the coverage associated with source code changes made in a PR. If the
193+
code coverage associated with changes made in our PR does not meet the set Sonar
194+
guidelines for that project, then when the sonarqubecloud bot writes in the PR, it
195+
will indicate that the coverage constraint was not met, &, if the bot is required in
196+
the branch protections for that project, then it will block the PR from being
197+
merged. Thus, in the preparation of the coverage XML, we set `--fail-under=0` so
198+
that this sub-step will not fail prior to sending the data to Sonar. Otherwise, this
199+
sub-step would fail whenever the `fail_under` in `[tool.coverage.report]` of the
200+
`pyproject.toml` was not met.
200201
"""
201202
command = [
202203
"coverage",
@@ -207,7 +208,19 @@ def _prepare_coverage_xml(session: Session, source: Path) -> None:
207208
f"{source}/*",
208209
"--fail-under=0",
209210
]
210-
session.run(*command)
211+
output = subprocess.run(command, capture_output=True, text=True, cwd=source)
212+
if output.returncode != 0:
213+
if output.stdout.strip() == "No data to report.":
214+
# Assuming that previous steps passed in the CI, this indicates — as
215+
# is in the case for newly created projects — that no coverage over the
216+
# `source` files was found. In this scenario, we do not need the CI to
217+
# break from a non-zero exit code. This is because without a coverage file
218+
# the sonarqubecloud bot will still report that the coverage is 0%, but as
219+
# long as the bot is not required in the project's branch protections, then
220+
# the PR can still be merged, provided that the other required constraints
221+
# have been met.
222+
return
223+
session.error(output.returncode, output.stdout, output.stderr)
211224

212225

213226
def _upload_to_sonar(
@@ -226,9 +239,8 @@ def _upload_to_sonar(
226239
"--sonar-sources",
227240
config.source,
228241
]
229-
if Path("dummy").exists():
242+
if Path(COVERAGE_XML).exists():
230243
command.extend(["--sonar-python-coverage-report-paths", COVERAGE_XML])
231-
232244
session.run(*command) # type: ignore
233245

234246

test/unit/nox/_artifacts_test.py

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dataclasses import dataclass
66
from inspect import cleandoc
77
from pathlib import Path
8+
from typing import Any
89
from unittest import mock
910
from unittest.mock import (
1011
Mock,
@@ -13,10 +14,21 @@
1314
)
1415

1516
import pytest
17+
from nox import (
18+
Session,
19+
_options,
20+
manifest,
21+
virtualenv,
22+
)
23+
from nox.sessions import (
24+
SessionRunner,
25+
_SessionQuit,
26+
)
1627

28+
from exasol.toolbox.nox import _artifacts
1729
from exasol.toolbox.nox._artifacts import (
1830
ALL_LINT_FILES,
19-
COVERAGE_FILE,
31+
COVERAGE_DB,
2032
COVERAGE_TABLES,
2133
COVERAGE_XML,
2234
LINT_JSON,
@@ -32,7 +44,6 @@
3244
check_artifacts,
3345
copy_artifacts,
3446
)
35-
from noxconfig import PROJECT_CONFIG
3647

3748

3849
@contextlib.contextmanager
@@ -229,7 +240,7 @@ def test_missing_attributes(self, tmp_path, capsys, missing_attributes):
229240

230241
class TestIsValidCoverage:
231242
def test_passes_when_as_expected(self, tmp_path):
232-
path = Path(tmp_path, COVERAGE_FILE)
243+
path = Path(tmp_path, COVERAGE_DB)
233244
_create_coverage_file(path, COVERAGE_TABLES)
234245

235246
result = _is_valid_coverage(path)
@@ -244,7 +255,7 @@ def test_passes_when_as_expected(self, tmp_path):
244255
)
245256
def test_database_missing_tables(self, tmp_path, capsys, missing_table):
246257
tables = COVERAGE_TABLES - missing_table
247-
path = Path(tmp_path, COVERAGE_FILE)
258+
path = Path(tmp_path, COVERAGE_DB)
248259
_create_coverage_file(path, tables)
249260

250261
result = _is_valid_coverage(path)
@@ -308,21 +319,66 @@ def test_all_files(tmp_path, capsys):
308319
assert (tmp_path / f).exists()
309320

310321

311-
# class TestPrepareCoverageXml:
312-
# @staticmethod
313-
# def test_no_coverage_file_creates_dummy():
314-
# _prepare_coverage_xml(Mock(), PROJECT_CONFIG.source)
315-
#
316-
# assert Path(COVERAGE_XML).exists()
317-
# assert Path(COVERAGE_XML).read_text() == ""
318-
# assert not Path(COVERAGE_FILE).exists()
319-
#
320-
# @staticmethod
321-
# def test_that_bad_coverage_file_still_raises_error(capsys):
322-
# _create_coverage_file(Path(COVERAGE_FILE), COVERAGE_TABLES)
323-
# session_mock = Mock()
324-
#
325-
# _prepare_coverage_xml(session_mock, PROJECT_CONFIG.source)
326-
#
327-
# assert Path(COVERAGE_FILE).exists()
328-
# assert not Path(COVERAGE_XML).exists()
322+
class FakeEnv(mock.MagicMock):
323+
# Extracted from nox testing
324+
_get_env = virtualenv.VirtualEnv._get_env
325+
326+
327+
def make_fake_env(venv_backend: str = "venv", **kwargs: Any) -> FakeEnv:
328+
# Extracted from nox testing
329+
return FakeEnv(
330+
spec=virtualenv.VirtualEnv,
331+
env={},
332+
venv_backend=venv_backend,
333+
**kwargs,
334+
)
335+
336+
337+
@pytest.fixture
338+
def nox_session(tmp_path):
339+
# Extracted from nox testing
340+
session_runner = SessionRunner(
341+
name="test",
342+
signatures=["test"],
343+
func=mock.Mock(spec=["python"], python="3.10"),
344+
global_config=_options.options.namespace(
345+
posargs=[],
346+
error_on_external_run=False,
347+
install_only=False,
348+
invoked_from=tmp_path,
349+
),
350+
manifest=mock.create_autospec(manifest.Manifest),
351+
)
352+
session_runner.venv = make_fake_env(bin_paths=["/no/bin/for/you"])
353+
session = Session(session_runner)
354+
yield session
355+
356+
357+
class TestPrepareCoverageXml:
358+
@staticmethod
359+
def test_no_coverage_file_silently_passes(monkeypatch, tmp_path, nox_session):
360+
coverage_xml = tmp_path / COVERAGE_XML
361+
coverage_db = tmp_path / COVERAGE_DB
362+
monkeypatch.setattr(_artifacts, "COVERAGE_XML", coverage_xml)
363+
364+
_prepare_coverage_xml(nox_session, tmp_path)
365+
366+
assert not Path(coverage_db).exists()
367+
assert not Path(coverage_xml).exists()
368+
369+
@staticmethod
370+
def test_that_bad_coverage_file_still_raises_error(
371+
monkeypatch, tmp_path, nox_session
372+
):
373+
coverage_xml = tmp_path / COVERAGE_XML
374+
coverage_db = tmp_path / COVERAGE_DB
375+
monkeypatch.setattr(_artifacts, "COVERAGE_XML", coverage_xml)
376+
_create_coverage_file(coverage_db, COVERAGE_TABLES)
377+
378+
with pytest.raises(
379+
_SessionQuit, match="doesn't seem to be a coverage data file"
380+
):
381+
_prepare_coverage_xml(nox_session, tmp_path)
382+
383+
assert coverage_db.exists()
384+
assert not coverage_xml.exists()

0 commit comments

Comments
 (0)