Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions ietf/meeting/tests_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose, generate_agenda_data
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.test_runner import TestBlobstoreManager
from ietf.utils.test_runner import TestBlobstoreManager, disable_coverage
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
from ietf.utils.timezone import date_today, time_now

Expand Down Expand Up @@ -1168,8 +1167,8 @@ def test_session_draft_tarfile(self):
os.unlink(filename)

@skipIf(skip_pdf_tests, skip_message)
@skip_coverage
def test_session_draft_pdf(self):
@disable_coverage()
def test_session_draft_pdf(self): # pragma: no cover
session, filenames = self.build_session_setup()
try:
url = urlreverse('ietf.meeting.views.session_draft_pdf', kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
Expand Down
13 changes: 4 additions & 9 deletions ietf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ def skip_unreadable_post(record):
"ietf/utils/patch.py",
"ietf/utils/test_data.py",
"ietf/utils/jstest.py",
"ietf/utils/coverage.py",
]

# These are code line regex patterns
Expand All @@ -736,15 +737,9 @@ def skip_unreadable_post(record):
TEST_COVERAGE_LATEST_FILE = os.path.join(BASE_DIR, "../latest-coverage.json")

TEST_CODE_COVERAGE_CHECKER = None
# TODO-PY312: figure out how to run coverage
# Context: the old version of coverage that we use (4.5.4, ca 2019) is falling back from its
# fast CTracer module to its very slow PyTracer when used on Python 3.12. It's not clear exactly
# why, but it's almost 3x slower. The situation may be better if we can update to a current
# version of coverage, but see https://github.com/nedbat/coveragepy/issues/1665 for more info.
# For now at least, disabling the checker completely.
# if SERVER_MODE != 'production':
# import coverage
# TEST_CODE_COVERAGE_CHECKER = coverage.Coverage(source=[ BASE_DIR ], cover_pylib=False, omit=TEST_CODE_COVERAGE_EXCLUDE_FILES)
if SERVER_MODE != 'production':
from ietf.utils.coverage import CoverageManager
TEST_CODE_COVERAGE_CHECKER = CoverageManager()

TEST_CODE_COVERAGE_REPORT_PATH = "coverage/"
TEST_CODE_COVERAGE_REPORT_URL = os.path.join(STATIC_URL, TEST_CODE_COVERAGE_REPORT_PATH, "index.html")
Expand Down
5 changes: 2 additions & 3 deletions ietf/settings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import shutil
import tempfile
from ietf.settings import * # pyflakes:ignore
from ietf.settings import TEST_CODE_COVERAGE_CHECKER, ORIG_AUTH_PASSWORD_VALIDATORS
from ietf.settings import ORIG_AUTH_PASSWORD_VALIDATORS
import debug # pyflakes:ignore
debug.debug = True

Expand Down Expand Up @@ -52,10 +52,9 @@ def __getitem__(self, item):
BLOBDB_DATABASE = "default"
DATABASE_ROUTERS = [] # type: ignore

if TEST_CODE_COVERAGE_CHECKER and not TEST_CODE_COVERAGE_CHECKER._started: # pyflakes:ignore
if TEST_CODE_COVERAGE_CHECKER: # pyflakes:ignore
TEST_CODE_COVERAGE_CHECKER.start() # pyflakes:ignore


def tempdir_with_cleanup(**kwargs):
"""Utility to create a temporary dir and arrange cleanup"""
_dir = tempfile.mkdtemp(**kwargs)
Expand Down
57 changes: 28 additions & 29 deletions ietf/submit/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from ietf.utils import tool_version
from ietf.utils.log import log, assertion
from ietf.utils.pipe import pipe
from ietf.utils.test_runner import set_coverage_checking
from ietf.utils.test_runner import disable_coverage

class DraftSubmissionChecker(object):
name = ""
Expand Down Expand Up @@ -247,34 +247,33 @@ def check_file_txt(self, path):
)

# yanglint
set_coverage_checking(False) # we can't count the following as it may or may not be run, depending on setup
if settings.SUBMIT_YANGLINT_COMMAND and os.path.exists(settings.YANGLINT_BINARY):
cmd_template = settings.SUBMIT_YANGLINT_COMMAND
command = [ w for w in cmd_template.split() if not '=' in w ][0]
cmd = cmd_template.format(model=path, rfclib=settings.SUBMIT_YANG_RFC_MODEL_DIR, tmplib=workdir,
draftlib=settings.SUBMIT_YANG_DRAFT_MODEL_DIR, ianalib=settings.SUBMIT_YANG_IANA_MODEL_DIR,
cataloglib=settings.SUBMIT_YANG_CATALOG_MODEL_DIR, )
code, out, err = pipe(cmd)
out = out.decode('utf-8')
err = err.decode('utf-8')
if code > 0 or len(err.strip()) > 0:
err_lines = err.splitlines()
for line in err_lines:
if line.strip():
try:
if 'err : ' in line:
errors += 1
if 'warn: ' in line:
warnings += 1
except ValueError:
pass
#passed = passed and code == 0 # For the submission tool. Yang checks always pass
message += "{version}: {template}:\n{output}\n".format(
version=tool_version[command],
template=cmd_template,
output=out + "No validation errors\n" if (code == 0 and len(err) == 0) else out + err,
)
set_coverage_checking(True)
with disable_coverage(): # pragma: no cover
if settings.SUBMIT_YANGLINT_COMMAND and os.path.exists(settings.YANGLINT_BINARY):
cmd_template = settings.SUBMIT_YANGLINT_COMMAND
command = [ w for w in cmd_template.split() if not '=' in w ][0]
cmd = cmd_template.format(model=path, rfclib=settings.SUBMIT_YANG_RFC_MODEL_DIR, tmplib=workdir,
draftlib=settings.SUBMIT_YANG_DRAFT_MODEL_DIR, ianalib=settings.SUBMIT_YANG_IANA_MODEL_DIR,
cataloglib=settings.SUBMIT_YANG_CATALOG_MODEL_DIR, )
code, out, err = pipe(cmd)
out = out.decode('utf-8')
err = err.decode('utf-8')
if code > 0 or len(err.strip()) > 0:
err_lines = err.splitlines()
for line in err_lines:
if line.strip():
try:
if 'err : ' in line:
errors += 1
if 'warn: ' in line:
warnings += 1
except ValueError:
pass
#passed = passed and code == 0 # For the submission tool. Yang checks always pass
message += "{version}: {template}:\n{output}\n".format(
version=tool_version[command],
template=cmd_template,
output=out + "No validation errors\n" if (code == 0 and len(err) == 0) else out + err,
)
else:
errors += 1
message += "No such file: %s\nPossible mismatch between extracted xym file name and returned module name?\n" % (path)
Expand Down
90 changes: 90 additions & 0 deletions ietf/utils/coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright The IETF Trust 2025, All Rights Reserved
from coverage import Coverage, CoverageData, FileReporter
from coverage.control import override_config as override_coverage_config
from coverage.results import Numbers
from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis
from django.conf import settings


class CoverageManager:
checker: Coverage | None = None
started = False

def start(self):
if settings.SERVER_MODE != "production" and not self.started:
self.checker = Coverage(
source=[settings.BASE_DIR],
cover_pylib=False,
omit=settings.TEST_CODE_COVERAGE_EXCLUDE_FILES,
)
for exclude_regex in getattr(
settings,
"TEST_CODE_COVERAGE_EXCLUDE_LINES",
[],
):
self.checker.exclude(exclude_regex)
self.checker.start()
self.started = True

def stop(self):
if self.checker is not None:
self.checker.stop()

def save(self):
if self.checker is not None:
self.checker.save()

def report(self, include: list[str] | None = None):
if self.checker is None:
return None
reporter = CustomDictReporter()
with override_coverage_config(
self.checker,
report_include=include,
):
return reporter.report(self.checker)


class CustomDictReporter: # pragma: no cover
total = Numbers()

def report(self, coverage):
coverage_data = coverage.get_data()
coverage_data.set_query_contexts(None)
measured_files = {}
for file_reporter, analysis in get_analysis_to_report(coverage, None):
measured_files[file_reporter.relative_filename()] = self.report_one_file(
coverage_data,
analysis,
file_reporter,
)
tot_numer, tot_denom = self.total.ratio_covered
return {
"coverage": 1 if tot_denom == 0 else tot_numer / tot_denom,
"covered": measured_files,
"format": 5,
}

def report_one_file(
self,
coverage_data: CoverageData,
analysis: Analysis,
file_reporter: FileReporter,
):
"""Extract the relevant report data for a single file."""
nums = analysis.numbers
self.total += nums
n_statements = nums.n_statements
numer, denom = nums.ratio_covered
fraction_covered = 1 if denom == 0 else numer / denom
missing_line_nums = sorted(analysis.missing)
# Extract missing lines from source files
source_lines = file_reporter.source().splitlines()
missing_lines = [source_lines[num - 1] for num in missing_line_nums]
return (
n_statements,
fraction_covered,
missing_line_nums,
missing_lines,
)
12 changes: 0 additions & 12 deletions ietf/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,9 @@

import debug # pyflakes:ignore

from ietf.utils.test_runner import set_coverage_checking
from ietf.person.models import Person, PersonalApiKey, PersonApiKeyEvent
from ietf.utils import log

def skip_coverage(f):
@wraps(f)
def _wrapper(*args, **kwargs):
if settings.TEST_CODE_COVERAGE_CHECKER:
set_coverage_checking(False)
result = f(*args, **kwargs)
set_coverage_checking(True)
return result
else:
return f(*args, **kwargs)
return _wrapper

def person_required(f):
@wraps(f)
Expand Down
41 changes: 37 additions & 4 deletions ietf/utils/jstest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import os

from django.conf import settings
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.urls import reverse as urlreverse
from unittest import skipIf

Expand All @@ -21,7 +23,11 @@


from ietf.utils.pipe import pipe
from ietf.utils.test_runner import IetfLiveServerTestCase
from ietf.utils.test_runner import (
set_template_coverage,
set_url_coverage,
load_and_run_fixtures,
)

executable_name = 'geckodriver'
code, out, err = pipe('{} --version'.format(executable_name))
Expand Down Expand Up @@ -49,17 +55,44 @@ def ifSeleniumEnabled(func):
return skipIf(skip_selenium, skip_message)(func)


class IetfSeleniumTestCase(IetfLiveServerTestCase):
class IetfSeleniumTestCase(StaticLiveServerTestCase): # pragma: no cover
login_view = 'ietf.ietfauth.views.login'

@classmethod
def setUpClass(cls):
set_template_coverage(False)
set_url_coverage(False)
super().setUpClass()

@classmethod
def tearDownClass(cls):
super().tearDownClass()
set_template_coverage(True)
set_url_coverage(True)

def setUp(self):
super(IetfSeleniumTestCase, self).setUp()
super().setUp()
# LiveServerTestCase uses TransactionTestCase which seems to
# somehow interfere with the fixture loading process in
# IetfTestRunner when running multiple tests (the first test
# is fine, in the next ones the fixtures have been wiped) -
# this is no doubt solvable somehow, but until then we simply
# recreate them here
from ietf.person.models import Person
if not Person.objects.exists():
load_and_run_fixtures(verbosity=0)
self.replaced_settings = dict()
if hasattr(settings, 'IDTRACKER_BASE_URL'):
self.replaced_settings['IDTRACKER_BASE_URL'] = settings.IDTRACKER_BASE_URL
settings.IDTRACKER_BASE_URL = self.live_server_url
self.driver = start_web_driver()
self.driver.set_window_size(1024,768)

def tearDown(self):
super(IetfSeleniumTestCase, self).tearDown()
self.driver.close()
for k, v in self.replaced_settings.items():
setattr(settings, k, v)
super().tearDown()

def absreverse(self,*args,**kwargs):
return '%s%s'%(self.live_server_url, urlreverse(*args, **kwargs))
Expand Down
Loading