Skip to content

Commit

Permalink
WIP: unittest
Browse files Browse the repository at this point in the history
  • Loading branch information
joostvanzwieten committed May 13, 2024
1 parent 8dde875 commit e23629e
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 65 deletions.
38 changes: 28 additions & 10 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ jobs:
MKL_NUM_THREADS: 1
PYTHONHASHSEED: 0
NUTILS_TENSORIAL: ${{ matrix.tensorial }}
# Use PEP669's sys.monitoring for faster coverage analysis, if available.
# This also fixes a significant regression in coverage analysis on Python
# 3.12. Related issue: https://github.com/python/cpython/issues/107674 .
COVERAGE_CORE: sysmon
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -95,7 +91,7 @@ jobs:
_numpy_version: ${{ matrix.numpy-version }}
run: |
python -um pip install --upgrade --upgrade-strategy eager wheel
python -um pip install --upgrade --upgrade-strategy eager coverage numpy$_numpy_version
python -um pip install --upgrade --upgrade-strategy eager numpy$_numpy_version
# Install Nutils from `dist` dir created in job `build-python-package`.
python -um pip install "$_wheel[import_gmsh,export_mpl]"
- name: Install Scipy
Expand All @@ -107,11 +103,33 @@ jobs:
python -um pip install --upgrade --upgrade-strategy eager mkl
python -um devtools.gha.configure_mkl
- name: Test
run: python -um coverage run -m unittest discover -b -q -t . -s tests
- name: Post-process coverage
run: python -um devtools.gha.coverage_report_xml
- name: Upload coverage
uses: codecov/codecov-action@v3
env:
COVERAGE_FILE: target/coverage/${{ matrix.name }}.json
run: python -um devtools.gha.unittest
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: _coverage_${{ matrix.name }}
path: target/coverage/
if-no-files-found: error
process-coverage:
if: ${{ always() }}
needs: test
name: 'Test coverage'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: debug
run: env
- name: Download coverage artifacts
uses: actions/download-artifact@v4
with:
pattern: _coverage_*
path: target/coverage
merge-multiple: true
- name: Generate summary
run: python -um devtools.gha.report_coverage
test-examples:
needs: build-python-package
name: 'Test examples ${{ matrix.os }}'
Expand Down
55 changes: 0 additions & 55 deletions devtools/gha/coverage_report_xml.py

This file was deleted.

44 changes: 44 additions & 0 deletions devtools/gha/report_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import itertools
import json
import os
from pathlib import Path

cov_dir = Path() / 'target' / 'coverage'

# Load and merge coverage data.
coverage = {}
for part in cov_dir.glob('*.json'):
with part.open('r') as f:
part = json.load(f)
for file_name, part_file_coverage in part.items():
coverage.setdefault(file_name, []).append(part_file_coverage)
coverage = {file_name: list(map(max, *file_coverage)) if len(file_coverage) > 1 else file_coverage[0] for file_name, file_coverage in coverage.items()}

with open(os.environ.get('GITHUB_STEP_SUMMARY', None) or cov_dir / 'summary.md', 'w') as f:
print('| Name | Stmts | Miss | Cover |', file=f)
print('| :--- | ----: | ---: | ----: |', file=f)
total_covered = 0
total_executable = 0
for file_name, file_coverage in sorted(coverage.items()):

# Annotate lines with uncovered lines.
i = 0
while i < len(file_coverage):
j = i
if file_coverage[i] == 1:
while j + 1 < len(file_coverage) and file_coverage[j + 1] == 1:
j += 1
if i == j:
print(f'::warning file={file_name},line={i},endLine={j},title=Uncovered lines,Line {i} is not covered by tests')
else:
print(f'::warning file={file_name},line={i},endLine={j},title=Uncovered lines,Lines {i} - {j} are not covered by tests')
i = j + 1

file_covered = sum(status == 2 for status in file_coverage)
file_executable = sum(status != 0 for status in file_coverage)
file_percentage = 100 * file_covered / file_executable if file_executable else 0
print(f'| `{file_name}` | {file_executable} | {file_covered} | {file_percentage:.1f}% |', file=f)
total_covered += file_covered
total_executable += file_executable
total_percentage = 100 * total_covered / total_executable if total_executable else 0
print(f'| TOTAL | {total_executable} | {total_covered} | {total_percentage:.1f}% |', file=f)
53 changes: 53 additions & 0 deletions devtools/gha/unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import importlib
import inspect
import json
import os
from pathlib import Path
import sys
import unittest

source = importlib.util.find_spec('nutils').origin
assert source.endswith(os.sep + '__init__.py')
source = source[:-11]
coverage = {}

if hasattr(sys, 'monitoring'):

def start(code, _):
if isinstance(code.co_filename, str) and code.co_filename.startswith(source) and not sys.monitoring.get_local_events(sys.monitoring.COVERAGE_ID, code):
if (file_coverage := coverage.get(code.co_filename)) is None:
with open(code.co_filename, 'rb') as f:
nlines = sum(1 for _ in f)
coverage[code.co_filename] = file_coverage = [0] * (nlines + 1)
for _, _, l in code.co_lines():
if l:
file_coverage[l] = 1
sys.monitoring.set_local_events(sys.monitoring.COVERAGE_ID, code, sys.monitoring.events.LINE)
for obj in code.co_consts:
if inspect.iscode(obj):
start(obj, None)
return sys.monitoring.DISABLE

def line(code, line_number):
coverage[code.co_filename][line_number] = 2
return sys.monitoring.DISABLE

sys.monitoring.register_callback(sys.monitoring.COVERAGE_ID, sys.monitoring.events.PY_START, start)
sys.monitoring.register_callback(sys.monitoring.COVERAGE_ID, sys.monitoring.events.LINE, line)
sys.monitoring.use_tool_id(sys.monitoring.COVERAGE_ID, 'test')
sys.monitoring.set_events(sys.monitoring.COVERAGE_ID, sys.monitoring.events.PY_START)

loader = unittest.TestLoader()
#suite = loader.discover('tests')
suite = loader.loadTestsFromName('tests.test_unit')
runner = unittest.TextTestRunner(buffer=True)
result = runner.run(suite)

coverage = {file_name[len(source) - 7:]: file_coverage for file_name, file_coverage in coverage.items()}
cov_dir = (Path() / 'target' / 'coverage')
cov_dir.mkdir(parents=True, exist_ok=True)
cov_file = cov_dir / (os.environ.get('COVERAGE_ID', 'coverage') + '.json')
with cov_file.open('w') as f:
json.dump(coverage, f)

sys.exit(0 if result.wasSuccessful() else 1)

0 comments on commit e23629e

Please sign in to comment.