Skip to content

Commit

Permalink
feat: Add support for -x/--exclude-lines.
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin committed Oct 23, 2024
1 parent a9f5869 commit a447abe
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 22 deletions.
4 changes: 3 additions & 1 deletion src/slipcover/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def main():
ap.add_argument('--pretty-print', action='store_true', help="pretty-print JSON output")
ap.add_argument('--out', type=Path, help="specify output file name")
ap.add_argument('--source', help="specify directories to cover")
ap.add_argument('-x', '--exclude-lines', action='append', type=str, help="Regex line patterns to ignore coverage for")
ap.add_argument('--omit', help="specify file(s) to omit")
ap.add_argument('--immediate', action='store_true',
help=(argparse.SUPPRESS if platform.python_implementation() == "PyPy" else "request immediate de-instrumentation"))
Expand Down Expand Up @@ -185,10 +186,11 @@ def main():
for o in args.omit.split(','):
file_matcher.addOmit(o)

exclude_lines = set(args.exclude_lines) if args.exclude_lines else None

sci = sc.Slipcover(immediate=args.immediate,
d_miss_threshold=args.threshold, branch=args.branch,
disassemble=args.dis, source=args.source)
disassemble=args.dis, source=args.source, exclude_lines=exclude_lines)


if not args.dont_wrap_pytest:
Expand Down
77 changes: 56 additions & 21 deletions src/slipcover/slipcover.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations
import functools
import sys
import dis
import types
from typing import Dict, Set, List, Tuple, Optional, Iterator, cast
from typing import Dict, Set, List, Tuple, TYPE_CHECKING, Iterator, Optional
from collections import defaultdict, Counter
import threading

Expand All @@ -14,6 +15,9 @@
from . import branch as br
from .version import __version__

if TYPE_CHECKING:
from re import Pattern

# FIXME provide __all__

# Counter.total() is new in 3.10
Expand Down Expand Up @@ -231,13 +235,19 @@ def both(f, field):
class Slipcover:
def __init__(self, immediate: bool = False,
d_miss_threshold: int = 50, branch: bool = False,
disassemble: bool = False, source: Optional[List[str]] = None):
disassemble: bool = False, source: Optional[List[str]] = None,
exclude_lines: Optional[Set[str]] = None):
self.immediate = immediate
self.d_miss_threshold = d_miss_threshold
self.branch = branch
self.disassemble = disassemble
self.source = source

self.exclude_lines = None
if exclude_lines:
import re
self.exclude_lines = {re.compile(exclude_line) for exclude_line in exclude_lines}

# mutex protecting this state
self.lock = threading.RLock()

Expand Down Expand Up @@ -291,46 +301,46 @@ def _get_newly_seen(self):

if sys.version_info >= (3,12):
@staticmethod
def lines_from_code(co: types.CodeType) -> Iterator[int]:
def lines_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[int]:
for c in co.co_consts:
if isinstance(c, types.CodeType):
yield from Slipcover.lines_from_code(c)
yield from Slipcover.lines_from_code(c, exclude_lines)

yield from (line for _, line in findlinestarts(co) if not br.is_branch(line))
yield from (line for _, line in findlinestarts(co) if not br.is_branch(line) and Slipcover.consider_line(co, exclude_lines))


@staticmethod
def branches_from_code(co: types.CodeType) -> Iterator[Tuple[int, int]]:
def branches_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[Tuple[int, int]]:
for c in co.co_consts:
if isinstance(c, types.CodeType):
yield from Slipcover.branches_from_code(c)
yield from Slipcover.branches_from_code(c, exclude_lines)

yield from (br.decode_branch(line) for _, line in findlinestarts(co) if br.is_branch(line))
yield from (br.decode_branch(line) for _, line in findlinestarts(co) if br.is_branch(line) and Slipcover.consider_line(co, exclude_lines))

else:
@staticmethod
def lines_from_code(co: types.CodeType) -> Iterator[int]:
def lines_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[int]:
for c in co.co_consts:
if isinstance(c, types.CodeType):
yield from Slipcover.lines_from_code(c)
yield from Slipcover.lines_from_code(c, exclude_lines)

# Python 3.11 generates a 0th line; 3.11+ generates a line just for RESUME
yield from (line for _, line in findlinestarts(co))
yield from (line for _, line in findlinestarts(co) if Slipcover.consider_line(co, exclude_lines))


@staticmethod
def branches_from_code(co: types.CodeType) -> Iterator[Tuple[int, int]]:
def branches_from_code(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None) -> Iterator[Tuple[int, int]]:
for c in co.co_consts:
if isinstance(c, types.CodeType):
yield from Slipcover.branches_from_code(c)
yield from Slipcover.branches_from_code(c, exclude_lines)

ed = bc.Editor(co)
for _, _, br_index in ed.find_const_assignments(br.BRANCH_NAME):
yield co.co_consts[br_index]


if sys.version_info >= (3,12):
def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None) -> types.CodeType:
def instrument(self, co: types.CodeType, parent: types.CodeType = 0) -> types.CodeType:
"""Instruments a code object for coverage detection.
If invoked on a function, instruments its code.
Expand All @@ -351,13 +361,13 @@ def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None

if not parent:
with self.lock:
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co))
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co))
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co, self.exclude_lines))
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co, self.exclude_lines))

return co

else:
def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None) -> types.CodeType:
def instrument(self, co: types.CodeType, parent: types.CodeType = 0) -> types.CodeType:
"""Instruments a code object for coverage detection.
If invoked on a function, instruments its code.
Expand Down Expand Up @@ -439,8 +449,8 @@ def instrument(self, co: types.CodeType, parent: Optional[types.CodeType] = None

with self.lock:
if not parent:
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co))
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co))
self.code_lines[co.co_filename].update(Slipcover.lines_from_code(co, self.exclude_lines))
self.code_branches[co.co_filename].update(Slipcover.branches_from_code(co, self.exclude_lines))

self.instrumented[co.co_filename].add(new_code)

Expand Down Expand Up @@ -500,6 +510,21 @@ def deinstrument(self, co, lines: set) -> types.CodeType:

return new_code

@staticmethod
def consider_line(co: types.CodeType, exclude_lines: Optional[Set[Pattern]] = None):
if not exclude_lines:
return True

line = get_source(co)
if not line:
return True

for exclusion in exclude_lines:
if exclusion.search(line):
return False

return True


def _add_unseen_source_files(self, source: List[str]):
import ast
Expand All @@ -521,9 +546,9 @@ def _add_unseen_source_files(self, source: List[str]):
if self.branch:
t = br.preinstrument(t)
code = compile(t, filename, "exec")
self.code_lines[filename] = set(Slipcover.lines_from_code(code))
self.code_lines[filename] = set(Slipcover.lines_from_code(code, self.exclude_lines))
if self.branch:
self.code_branches[filename] = set(Slipcover.branches_from_code(code))
self.code_branches[filename] = set(Slipcover.branches_from_code(code, self.exclude_lines))

except Exception as e: # for SyntaxError and such... FIXME curate list and catch only those
print(f"Warning: unable to include {filename}: {e}")
Expand Down Expand Up @@ -684,3 +709,13 @@ def deinstrument_seen(self) -> None:

# all references should have been replaced now... right?
self.replace_map.clear()


@functools.lru_cache(None)
def get_source(co) -> Optional[str]:
import inspect

try:
return '\n'.join(inspect.getsourcelines(co)[0])
except Exception:
return None

0 comments on commit a447abe

Please sign in to comment.