ci: Fix coverage reporting (again) (#38849)
* chore: remove _decorate_all_methods_and_functions_with_type_checker No one understands this runtime magic anymore. * build: Bump coverage.py to latest * test: Skip github in coverage reporting * test: Print traceback from all threads when test is stuck * ci: Enable coverage in server side tests * ci: Always enable coverage It's cheap in recent python versions, our reasons for selectively disabling aren't valid anymore. * ci: Disable stderr capturing * ci: Use default buffer behaviour in unittest runner * ci(coverage): Set concurrency to multiprocessing We do use multiprocessing, perhaps the patches aren't concurrectly handled? * ci(coverage): Try parallel run * fix: Apply subprocess patch * ci: Don't start web server with coverage Causes deadlock for some reason. We don't actually report it either. * ci: only submit UI coverage if ran * test: remove aggresive stuck test checking * ci: disable UI coverage (for now)
This commit is contained in:
parent
d9d35fa4ad
commit
098a0851c6
11 changed files with 17 additions and 129 deletions
27
.github/workflows/_base-server-tests.yml
vendored
27
.github/workflows/_base-server-tests.yml
vendored
|
|
@ -29,7 +29,7 @@ on:
|
||||||
enable-coverage:
|
enable-coverage:
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -100,7 +100,6 @@ jobs:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
disable-socketio: true
|
disable-socketio: true
|
||||||
enable-coverage: ${{ inputs.enable-coverage }}
|
|
||||||
db-root-password: ${{ env.DB_ROOT_PASSWORD }}
|
db-root-password: ${{ env.DB_ROOT_PASSWORD }}
|
||||||
db: ${{ matrix.db }}
|
db: ${{ matrix.db }}
|
||||||
env:
|
env:
|
||||||
|
|
@ -110,29 +109,11 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
bench --site test_site \
|
bench --site test_site \
|
||||||
run-parallel-tests \
|
run-parallel-tests \
|
||||||
|
--with-coverage \
|
||||||
--app "${{ github.event.repository.name }}" \
|
--app "${{ github.event.repository.name }}" \
|
||||||
--total-builds ${{ inputs.parallel-runs }} \
|
--total-builds ${{ inputs.parallel-runs }} \
|
||||||
--build-number ${{ matrix.index }} 2> >(tee -a stderr.log >&2)
|
--build-number ${{ matrix.index }}
|
||||||
|
|
||||||
# Process warnings and create annotations
|
|
||||||
if [ -s stderr.log ] && [ "$DB" == "mariadb" ]; then
|
|
||||||
echo "Processing deprecation warnings..."
|
|
||||||
grep -E "DeprecationWarning" stderr.log | sort -u | while read -r warning; do
|
|
||||||
# Extract file path, line number, and warning type
|
|
||||||
file_info=$(echo "$warning" | grep -oP '^.*?:\d+:')
|
|
||||||
file_path=$(echo "$file_info" | cut -d':' -f1)
|
|
||||||
line_number=$(echo "$file_info" | cut -d':' -f2)
|
|
||||||
warning_type=$(echo "$warning" | grep -oP '\w+Warning')
|
|
||||||
|
|
||||||
# Extract the actual warning message
|
|
||||||
message=$(echo "$warning" | sed -E "s/^.*$warning_type: //")
|
|
||||||
|
|
||||||
# Create the annotation
|
|
||||||
echo "::warning file=${file_path},line=${line_number}::${warning_type}: ${message}"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "No deprecation warnings found."
|
|
||||||
fi
|
|
||||||
env:
|
env:
|
||||||
DB: ${{ matrix.db }}
|
DB: ${{ matrix.db }}
|
||||||
# consumed by bench run-parallel-tests
|
# consumed by bench run-parallel-tests
|
||||||
|
|
@ -144,7 +125,7 @@ jobs:
|
||||||
if: inputs.enable-coverage
|
if: inputs.enable-coverage
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.db }}-${{ matrix.index }}
|
name: coverage-${{ matrix.db }}-${{ matrix.index }}
|
||||||
path: ./sites/*-coverage*.xml
|
path: ./sites/*coverage*.xml
|
||||||
|
|
||||||
- name: Setup tmate session
|
- name: Setup tmate session
|
||||||
uses: mxschmitt/action-tmate@v3
|
uses: mxschmitt/action-tmate@v3
|
||||||
|
|
|
||||||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -48,7 +48,6 @@ jobs:
|
||||||
enable-postgres: ${{ needs.checkrun.outputs.run_postgres == 'true' }} # This enables PostgreSQL to run tests
|
enable-postgres: ${{ needs.checkrun.outputs.run_postgres == 'true' }} # This enables PostgreSQL to run tests
|
||||||
enable-sqlite: false # This will test against both MariaDB and SQLite if enabled
|
enable-sqlite: false # This will test against both MariaDB and SQLite if enabled
|
||||||
parallel-runs: 2
|
parallel-runs: 2
|
||||||
enable-coverage: ${{ github.event_name != 'pull_request' }}
|
|
||||||
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
||||||
needs: checkrun
|
needs: checkrun
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
@ -67,7 +66,6 @@ jobs:
|
||||||
name: Coverage Wrap Up
|
name: Coverage Wrap Up
|
||||||
needs: [test, checkrun]
|
needs: [test, checkrun]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
|
||||||
4
.github/workflows/ui-tests.yml
vendored
4
.github/workflows/ui-tests.yml
vendored
|
|
@ -44,14 +44,14 @@ jobs:
|
||||||
uses: ./.github/workflows/_base-ui-tests.yml
|
uses: ./.github/workflows/_base-ui-tests.yml
|
||||||
with:
|
with:
|
||||||
parallel-runs: 3
|
parallel-runs: 3
|
||||||
enable-coverage: ${{ github.event_name != 'pull_request' }}
|
enable-coverage: false
|
||||||
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
||||||
needs: checkrun
|
needs: checkrun
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
name: Coverage Wrap Up
|
name: Coverage Wrap Up
|
||||||
needs: [test, checkrun]
|
needs: [test, checkrun]
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ needs.checkrun.outputs.build == 'strawberry' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,6 @@ flags:
|
||||||
paths:
|
paths:
|
||||||
- "**/*.py"
|
- "**/*.py"
|
||||||
carryforward: true
|
carryforward: true
|
||||||
ui-tests:
|
|
||||||
paths:
|
|
||||||
- "**/*.js"
|
|
||||||
carryforward: true
|
|
||||||
server-ui:
|
server-ui:
|
||||||
paths:
|
paths:
|
||||||
- "**/*.py"
|
- "**/*.py"
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,6 @@ def main(
|
||||||
verbosity=2 if testing_module_logger.getEffectiveLevel() < logging.INFO else 1,
|
verbosity=2 if testing_module_logger.getEffectiveLevel() < logging.INFO else 1,
|
||||||
tb_locals=testing_module_logger.getEffectiveLevel() <= logging.INFO,
|
tb_locals=testing_module_logger.getEffectiveLevel() <= logging.INFO,
|
||||||
cfg=test_config,
|
cfg=test_config,
|
||||||
buffer=not debug, # unfortunate as it messes up stdout/stderr output order
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if doctype or doctype_list_path:
|
if doctype or doctype_list_path:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ STANDARD_EXCLUSIONS = [
|
||||||
"*/node_modules/*",
|
"*/node_modules/*",
|
||||||
"*/doctype/*/*_dashboard.py",
|
"*/doctype/*/*_dashboard.py",
|
||||||
"*/patches/*",
|
"*/patches/*",
|
||||||
|
"*/.github/*",
|
||||||
]
|
]
|
||||||
|
|
||||||
# tested via commands' test suite
|
# tested via commands' test suite
|
||||||
|
|
@ -78,7 +79,12 @@ class CodeCoverage:
|
||||||
if self.app == "frappe":
|
if self.app == "frappe":
|
||||||
omit.extend(FRAPPE_EXCLUSIONS)
|
omit.extend(FRAPPE_EXCLUSIONS)
|
||||||
|
|
||||||
self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
|
self.coverage = Coverage(
|
||||||
|
source=[source_path],
|
||||||
|
omit=omit,
|
||||||
|
include=STANDARD_INCLUSIONS,
|
||||||
|
data_suffix=True,
|
||||||
|
)
|
||||||
self.coverage.start()
|
self.coverage.start()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import requests
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import make_test_records, toggle_test_mode
|
from frappe.tests.utils import make_test_records, toggle_test_mode
|
||||||
|
|
||||||
from .testing.environment import _decorate_all_methods_and_functions_with_type_checker
|
|
||||||
from .testing.result import TestResult
|
from .testing.result import TestResult
|
||||||
|
|
||||||
click_ctx = click.get_current_context(True)
|
click_ctx = click.get_current_context(True)
|
||||||
|
|
@ -61,7 +60,6 @@ class ParallelTestRunner:
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
frappe.utils.scheduler.disable_scheduler()
|
frappe.utils.scheduler.disable_scheduler()
|
||||||
if not self.lightmode:
|
if not self.lightmode:
|
||||||
_decorate_all_methods_and_functions_with_type_checker()
|
|
||||||
self.before_test_setup()
|
self.before_test_setup()
|
||||||
|
|
||||||
def before_test_setup(self):
|
def before_test_setup(self):
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,6 @@ def _initialize_test_environment(site, config):
|
||||||
frappe.flags.print_messages = logger.getEffectiveLevel() < logging.INFO
|
frappe.flags.print_messages = logger.getEffectiveLevel() < logging.INFO
|
||||||
frappe.flags.tests_verbose = logger.getEffectiveLevel() < logging.INFO
|
frappe.flags.tests_verbose = logger.getEffectiveLevel() < logging.INFO
|
||||||
|
|
||||||
_decorate_all_methods_and_functions_with_type_checker()
|
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_after_tests():
|
def _cleanup_after_tests():
|
||||||
"""Perform cleanup operations after running tests"""
|
"""Perform cleanup operations after running tests"""
|
||||||
|
|
@ -83,95 +81,6 @@ def _disable_scheduler_if_needed():
|
||||||
frappe.utils.scheduler.disable_scheduler()
|
frappe.utils.scheduler.disable_scheduler()
|
||||||
|
|
||||||
|
|
||||||
@debug_timer
|
|
||||||
def _decorate_all_methods_and_functions_with_type_checker():
|
|
||||||
from frappe.utils.typing_validations import validate_argument_types
|
|
||||||
|
|
||||||
def _get_config_from_pyproject(app_path):
|
|
||||||
try:
|
|
||||||
with open(f"{app_path}/pyproject.toml", "rb") as f:
|
|
||||||
config = tomllib.load(f)
|
|
||||||
return (
|
|
||||||
config.get("tool", {})
|
|
||||||
.get("frappe", {})
|
|
||||||
.get("testing", {})
|
|
||||||
.get("function_type_validation", {})
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return {}
|
|
||||||
except tomllib.TOMLDecodeError:
|
|
||||||
logger.warning(f"Failed to parse pyproject.toml for app {app_path}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _decorate_callable(obj, parent_module):
|
|
||||||
# whitelisted methods are already checked, see frappe.whitelist
|
|
||||||
if getattr(obj, "__func__", obj) in frappe.whitelisted:
|
|
||||||
return obj
|
|
||||||
# Check if the function is already decorated
|
|
||||||
elif hasattr(obj, "_is_decorated_for_validate_argument_types"):
|
|
||||||
return obj
|
|
||||||
elif module := getattr(obj, "__module__", ""):
|
|
||||||
# ensure that the origin skip list is honored on imports; but not the origin
|
|
||||||
# max_depth because they are reimported thus attached to a different namespace
|
|
||||||
app = module.split(".", 1)[0]
|
|
||||||
config = _get_config_from_pyproject(frappe.get_app_source_path(app))
|
|
||||||
skip_namespaces = config.get("skip_namespaces", [])
|
|
||||||
if any(module.startswith(n) for n in skip_namespaces):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@functools.wraps(obj)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
return validate_argument_types(obj)(*args, **kwargs)
|
|
||||||
|
|
||||||
wrapper._is_decorated_for_validate_argument_types = True
|
|
||||||
|
|
||||||
if obj.__module__ != parent_module.__name__:
|
|
||||||
logger.debug(f"... patching {obj.__module__}.{obj.__name__} (inside {parent_module.__name__})")
|
|
||||||
else:
|
|
||||||
logger.debug(f"... patching {obj.__module__}.{obj.__name__}")
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def _decorate_module(app, module, apps, current_depth, max_depth):
|
|
||||||
if current_depth > max_depth:
|
|
||||||
return
|
|
||||||
for name in dir(module):
|
|
||||||
obj = getattr(module, name)
|
|
||||||
if inspect.isfunction(obj):
|
|
||||||
if not getattr(obj, "__annotations__", None):
|
|
||||||
continue
|
|
||||||
# never cross the apps (plural!) boundary for functions
|
|
||||||
if obj.__module__.split(".", 1)[0] not in apps:
|
|
||||||
continue
|
|
||||||
setattr(module, name, _decorate_callable(obj, module))
|
|
||||||
elif inspect.ismodule(obj):
|
|
||||||
# never cross the app (singular!) boundary for modules
|
|
||||||
if obj.__name__.split(".", 1)[0] != app:
|
|
||||||
continue
|
|
||||||
if hasattr(obj, "_is_decorated_for_validate_argument_types"):
|
|
||||||
continue
|
|
||||||
obj._is_decorated_for_validate_argument_types = True
|
|
||||||
_decorate_module(app, obj, apps, current_depth + 1, max_depth)
|
|
||||||
|
|
||||||
for app in (apps := frappe.get_installed_apps()):
|
|
||||||
config = _get_config_from_pyproject(frappe.get_app_source_path(app))
|
|
||||||
max_depth = config.get("max_module_depth", 0)
|
|
||||||
skip_namespaces = config.get("skip_namespaces", [])
|
|
||||||
logger.info(f"Adding type validator in {app!r} (up to level {max_depth})...")
|
|
||||||
pkg = frappe.get_module(app)
|
|
||||||
_decorate_module(app, pkg, apps, 1, max_depth)
|
|
||||||
|
|
||||||
for _, submodule_name, _ in pkgutil.walk_packages(path=pkg.__path__, prefix=pkg.__name__ + "."):
|
|
||||||
current_depth = len(submodule_name.split("."))
|
|
||||||
if current_depth > max_depth:
|
|
||||||
continue
|
|
||||||
if any(submodule_name.startswith(n) for n in skip_namespaces):
|
|
||||||
continue
|
|
||||||
|
|
||||||
submodule = frappe.get_module(submodule_name)
|
|
||||||
_decorate_module(app, submodule, apps, current_depth, max_depth)
|
|
||||||
|
|
||||||
|
|
||||||
class IntegrationTestPreparation:
|
class IntegrationTestPreparation:
|
||||||
def __init__(self, cfg):
|
def __init__(self, cfg):
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class TestRunner(unittest.TextTestRunner):
|
||||||
descriptions=True,
|
descriptions=True,
|
||||||
verbosity=1,
|
verbosity=1,
|
||||||
failfast=False,
|
failfast=False,
|
||||||
buffer=True,
|
buffer=False,
|
||||||
resultclass=None,
|
resultclass=None,
|
||||||
warnings="module",
|
warnings="module",
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import faulthandler
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ dev = [
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"unittest-xml-reporting~=3.2.0",
|
"unittest-xml-reporting~=3.2.0",
|
||||||
"coverage~=7.10.0",
|
"coverage~=7.13.5",
|
||||||
"hypothesis~=6.77.0",
|
"hypothesis~=6.77.0",
|
||||||
"freezegun~=1.5.1",
|
"freezegun~=1.5.1",
|
||||||
]
|
]
|
||||||
|
|
@ -146,7 +146,7 @@ skip_namespaces = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.bench.dev-dependencies]
|
[tool.bench.dev-dependencies]
|
||||||
coverage = "~=7.10.0"
|
coverage = "~=7.13.5"
|
||||||
pyngrok = "~=7.5.0"
|
pyngrok = "~=7.5.0"
|
||||||
unittest-xml-reporting = "~=3.2.0"
|
unittest-xml-reporting = "~=3.2.0"
|
||||||
watchdog = "~=6.0.0"
|
watchdog = "~=6.0.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue