Skip to content

Commit

Permalink
WIP: unittest
Browse files Browse the repository at this point in the history
  • Loading branch information
joostvanzwieten committed May 14, 2024
1 parent 8dde875 commit 439cb23
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 69 deletions.
41 changes: 31 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,36 @@ 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_ID: ${{ matrix.name }}
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: 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
- name: Upload lcov artifact
uses: actions/upload-artifact@v4
with:
name: coverage
path: target/coverage/coverage.lcov
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.

69 changes: 69 additions & 0 deletions devtools/gha/report_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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()}

# Annotate lines with uncovered lines.
for file_name, file_coverage in sorted(coverage.items()):

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

# Generate summary.
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)

# Generate lcov.
with (cov_dir / 'coverage.lcov').open('w') as f:
print('TN:unittest', file=f)
for file_name, file_coverage in sorted(coverage.items()):
print(f'SF:{file_name}', file=f)
for i, status in enumerate(file_coverage[1:], 1):
if status:
print(f'DA:{i},{status - 1}', file=f)
52 changes: 52 additions & 0 deletions devtools/gha/unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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')
runner = unittest.TextTestRunner(buffer=True)
result = runner.run(suite)

coverage = {file_name[len(source) - 7:].replace('\\', '/'): 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)
4 changes: 4 additions & 0 deletions nutils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,13 @@ def __eq__(self, other):

@cached_property
def __nutils_hash__(self):
print(repr('{}.{}:{}\0'.format(type(self).__module__, type(self).__qualname__, type(self)._version).encode()))
h = hashlib.sha1('{}.{}:{}\0'.format(type(self).__module__, type(self).__qualname__, type(self)._version).encode())
print(h.hexdigest())
for arg in self._args:
h.update(nutils_hash(arg))
print(h.hexdigest())
print('---')
return h.digest()

def __getstate__(self):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,12 @@ class U(self.cls):
def __init__(self, x, y):
pass

self.assertEqual(nutils.types.nutils_hash(T(1, 2)).hex(), nutils.types.nutils_hash(T(1, 2)).hex())
self.assertNotEqual(nutils.types.nutils_hash(T(1, 2)).hex(), nutils.types.nutils_hash(T(2, 1)).hex())
self.assertNotEqual(nutils.types.nutils_hash(T(1, 2)).hex(), nutils.types.nutils_hash(U(1, 2)).hex())
#self.assertEqual(nutils.types.nutils_hash(T(1, 2)).hex(), nutils.types.nutils_hash(T(1, 2)).hex())
#self.assertNotEqual(nutils.types.nutils_hash(T(1, 2)).hex(), nutils.types.nutils_hash(T(2, 1)).hex())
#self.assertNotEqual(nutils.types.nutils_hash(T(1, 2)).hex(), nutils.types.nutils_hash(U(1, 2)).hex())
# Since the hash does not include base classes, the hashes of Immutable and Singleton are the same.
self.assertEqual(nutils.types.nutils_hash(T(1, 2)).hex(), '2f7fb825b73398a20ef5f95649429c75d7a9d615')
self.assertEqual(nutils.types.nutils_hash(T1(1, 2)).hex(), 'b907c718a9a7e8c28300e028cfbb578a608f7620')
#self.assertEqual(nutils.types.nutils_hash(T1(1, 2)).hex(), 'b907c718a9a7e8c28300e028cfbb578a608f7620')

@parametrize.enable_if(lambda cls: cls is nutils.types.Singleton)
def test_deduplication(self):
Expand Down

0 comments on commit 439cb23

Please sign in to comment.