From 48a5ebf3be46f9cd2ddaef2c5c7790d6773734d7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 25 Aug 2024 17:48:12 -0400 Subject: [PATCH] wip: everything --- CHANGES.rst | 5 +- coverage/core.py | 17 +- coverage/env.py | 13 - coverage/execfile.py | 4 +- coverage/parser.py | 348 +++++------------ coverage/python.py | 2 +- coverage/results.py | 40 +- lab/notes/arcs-to-branches.txt | 24 ++ lab/parser.py | 2 - lab/run_sysmon.py | 72 ++-- tests/coveragetest.py | 96 ++--- tests/helpers.py | 34 +- tests/test_api.py | 26 +- tests/test_arcs.py | 659 ++++++++++++++------------------- tests/test_coverage.py | 49 +-- tests/test_json.py | 8 +- tests/test_parser.py | 135 +------ tests/test_process.py | 3 + tests/test_testing.py | 57 +-- tests/test_xml.py | 2 +- 20 files changed, 595 insertions(+), 1001 deletions(-) create mode 100644 lab/notes/arcs-to-branches.txt diff --git a/CHANGES.rst b/CHANGES.rst index 2b6d53bce..83c17c041 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,7 +23,10 @@ upgrading your version of coverage.py. Unreleased ---------- -Nothing yet. +- Fewer things are considered branches now: + + - Lambdas, comprehensions, and generator expressions are no longer marked as + missing branches if they don't complete execution. .. scriv-start-here diff --git a/coverage/core.py b/coverage/core.py index 89e49c49b..b8916b683 100644 --- a/coverage/core.py +++ b/coverage/core.py @@ -14,7 +14,11 @@ from coverage.exceptions import ConfigError from coverage.pytracer import PyTracer from coverage.sysmon import SysMonitor -from coverage.types import TFileDisposition, Tracer, TWarnFn +from coverage.types import ( + TFileDisposition, + Tracer, + TWarnFn, +) try: @@ -45,7 +49,14 @@ class Core: packed_arcs: bool systrace: bool - def __init__(self, warn: TWarnFn, timid: bool, metacov: bool) -> None: + def __init__(self, + warn: TWarnFn, + timid: bool, + metacov: bool, + ) -> None: + # Defaults + self.tracer_kwargs = {} + core_name: str | None if timid: core_name = "pytrace" @@ -74,14 +85,12 @@ def __init__(self, warn: TWarnFn, timid: bool, metacov: bool) -> None: self.systrace = False elif core_name == "ctrace": self.tracer_class = CTracer - self.tracer_kwargs = {} self.file_disposition_class = CFileDisposition self.supports_plugins = True self.packed_arcs = True self.systrace = True elif core_name == "pytrace": self.tracer_class = PyTracer - self.tracer_kwargs = {} self.file_disposition_class = FileDisposition self.supports_plugins = False self.packed_arcs = False diff --git a/coverage/env.py b/coverage/env.py index f51a6e330..3bae7e51a 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -64,15 +64,6 @@ class PYBEHAVIOR: # 3.7 changed how functions with only docstrings are numbered. docstring_only_function = (not PYPY) and (PYVERSION <= (3, 10)) - # When a break/continue/return statement in a try block jumps to a finally - # block, does the finally jump back to the break/continue/return (pre-3.10) - # to do the work? - finally_jumps_back = (PYVERSION < (3, 10)) - - # CPython 3.11 now jumps to the decorator line again while executing - # the decorator. - trace_decorator_line_again = (CPYTHON and PYVERSION > (3, 11, 0, "alpha", 3, 0)) - # CPython 3.9a1 made sys.argv[0] and other reported files absolute paths. report_absolute_files = ( (CPYTHON or (PYPY and PYPYVERSION >= (7, 3, 10))) @@ -112,10 +103,6 @@ class PYBEHAVIOR: # only a 0-number line, which is ignored, giving a truly empty module. empty_is_empty = (PYVERSION >= (3, 11, 0, "beta", 4)) - # Are comprehensions inlined (new) or compiled as called functions (old)? - # Changed in https://github.com/python/cpython/pull/101441 - comprehensions_are_functions = (PYVERSION <= (3, 12, 0, "alpha", 7, 0)) - # PEP669 Low Impact Monitoring: https://peps.python.org/pep-0669/ pep669 = bool(getattr(sys, "monitoring", None)) diff --git a/coverage/execfile.py b/coverage/execfile.py index 7011c70f9..c6b667253 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -288,13 +288,13 @@ def run_python_file(args: list[str]) -> None: def make_code_from_py(filename: str) -> CodeType: """Get source from `filename` and make a code object of it.""" - # Open the source file. try: source = get_python_source(filename) except (OSError, NoSource) as exc: raise NoSource(f"No file to run: '{filename}'") from exc - return compile(source, filename, "exec", dont_inherit=True) + code = compile(source, filename, mode="exec", dont_inherit=True) + return code def make_code_from_pyc(filename: str) -> CodeType: diff --git a/coverage/parser.py b/coverage/parser.py index c291c4437..89bc89dfc 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -84,9 +84,6 @@ def __init__( # The raw line numbers of excluded lines of code, as marked by pragmas. self.raw_excluded: set[TLineNo] = set() - # The line numbers of class definitions. - self.raw_classdefs: set[TLineNo] = set() - # The line numbers of docstring lines. self.raw_docstrings: set[TLineNo] = set() @@ -100,6 +97,7 @@ def __init__( # Lazily-created arc data, and missing arc descriptions. self._all_arcs: set[TArc] | None = None self._missing_arc_fragments: TArcFragments | None = None + self._with_jump_fixers: dict[TArc, tuple[TArc, TArc]] = {} def lines_matching(self, regex: str) -> set[TLineNo]: """Find the lines matching a regex. @@ -212,9 +210,6 @@ def _raw_parse(self) -> None: # functions and classes. assert self._ast_root is not None for node in ast.walk(self._ast_root): - # Find class definitions. - if isinstance(node, ast.ClassDef): - self.raw_classdefs.add(node.lineno) # Find docstrings. if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef, ast.Module)): if node.body: @@ -258,7 +253,7 @@ def translate_lines(self, lines: Iterable[TLineNo]) -> set[TLineNo]: def translate_arcs(self, arcs: Iterable[TArc]) -> set[TArc]: """Implement `FileReporter.translate_arcs`.""" - return {(self.first_line(a), self.first_line(b)) for (a, b) in arcs} + return {(self.first_line(a), self.first_line(b)) for (a, b) in self.fix_with_jumps(arcs)} def parse_source(self) -> None: """Parse source text to find executable lines, excluded lines, etc. @@ -305,9 +300,10 @@ def _analyze_ast(self) -> None: assert self._ast_root is not None aaa = AstArcAnalyzer(self._ast_root, self.raw_statements, self._multiline) aaa.analyze() + self._with_jump_fixers = aaa.with_jump_fixers() self._all_arcs = set() - for l1, l2 in aaa.arcs: + for l1, l2 in self.fix_with_jumps(aaa.arcs): fl1 = self.first_line(l1) fl2 = self.first_line(l2) if fl1 != fl2: @@ -315,6 +311,19 @@ def _analyze_ast(self) -> None: self._missing_arc_fragments = aaa.missing_arc_fragments + def fix_with_jumps(self, arcs: Iterable[TArc]) -> set[TArc]: + """Adjust arcs to fix jumps leaving `with` statements.""" + to_remove = set() + to_add = set() + for arc in arcs: + if arc in self._with_jump_fixers: + start_next, prev_next = self._with_jump_fixers[arc] + if start_next in arcs: + to_add.add(prev_next) + to_remove.add(arc) + to_remove.add(start_next) + return (set(arcs) | to_add) - to_remove + @functools.lru_cache() def exit_counts(self) -> dict[TLineNo, int]: """Get a count of exits from that each line. @@ -335,46 +344,21 @@ def exit_counts(self) -> dict[TLineNo, int]: continue exit_counts[l1] += 1 - # Class definitions have one extra exit, so remove one for each: - for l in self.raw_classdefs: - # Ensure key is there: class definitions can include excluded lines. - if l in exit_counts: - exit_counts[l] -= 1 - return exit_counts - def missing_arc_description( - self, - start: TLineNo, - end: TLineNo, - executed_arcs: Iterable[TArc] | None = None, - ) -> str: + def missing_arc_description(self, start: TLineNo, end: TLineNo) -> str: """Provide an English sentence describing a missing arc.""" if self._missing_arc_fragments is None: self._analyze_ast() assert self._missing_arc_fragments is not None actual_start = start - - if ( - executed_arcs and - end < 0 and end == -start and - (end, start) not in executed_arcs and - (end, start) in self._missing_arc_fragments - ): - # It's a one-line callable, and we never even started it, - # and we have a message about not starting it. - start, end = end, start - fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)]) msgs = [] for smsg, emsg in fragment_pairs: if emsg is None: if end < 0: - # Hmm, maybe we have a one-line callable, let's check. - if (-end, end) in self._missing_arc_fragments: - return self.missing_arc_description(-end, end) emsg = "didn't jump to the function exit" else: emsg = "didn't jump to line {lineno}" @@ -521,15 +505,11 @@ class Block: # pylint: disable=unused-argument def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: """Process break exits.""" - # Because break can only appear in loops, and most subclasses - # implement process_break_exits, this function is never reached. - raise AssertionError + return False def process_continue_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: """Process continue exits.""" - # Because continue can only appear in loops, and most subclasses - # implement process_continue_exits, this function is never reached. - raise AssertionError + return False def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: """Process raise exits.""" @@ -591,82 +571,12 @@ def __init__(self, handler_start: TLineNo | None, final_start: TLineNo | None) - # The line number of the "finally:" clause, if any. self.final_start = final_start - # The ArcStarts for breaks/continues/returns/raises inside the "try:" - # that need to route through the "finally:" clause. - self.break_from: set[ArcStart] = set() - self.continue_from: set[ArcStart] = set() - self.raise_from: set[ArcStart] = set() - self.return_from: set[ArcStart] = set() - - def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - if self.final_start is not None: - self.break_from.update(exits) - return True - return False - - def process_continue_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - if self.final_start is not None: - self.continue_from.update(exits) - return True - return False - def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: if self.handler_start is not None: for xit in exits: add_arc(xit.lineno, self.handler_start, xit.cause) - else: - assert self.final_start is not None - self.raise_from.update(exits) return True - def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - if self.final_start is not None: - self.return_from.update(exits) - return True - return False - - -class WithBlock(Block): - """A block on the block stack representing a `with` block.""" - def __init__(self, start: TLineNo) -> None: - # We only ever use this block if it is needed, so that we don't have to - # check this setting in all the methods. - assert env.PYBEHAVIOR.exit_through_with - - # The line number of the with statement. - self.start = start - - # The ArcStarts for breaks/continues/returns/raises inside the "with:" - # that need to go through the with-statement while exiting. - self.break_from: set[ArcStart] = set() - self.continue_from: set[ArcStart] = set() - self.return_from: set[ArcStart] = set() - - def _process_exits( - self, - exits: set[ArcStart], - add_arc: TAddArcFn, - from_set: set[ArcStart] | None = None, - ) -> bool: - """Helper to process the four kinds of exits.""" - for xit in exits: - add_arc(xit.lineno, self.start, xit.cause) - if from_set is not None: - from_set.update(exits) - return True - - def process_break_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - return self._process_exits(exits, add_arc, self.break_from) - - def process_continue_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - return self._process_exits(exits, add_arc, self.continue_from) - - def process_raise_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - return self._process_exits(exits, add_arc) - - def process_return_exits(self, exits: set[ArcStart], add_arc: TAddArcFn) -> bool: - return self._process_exits(exits, add_arc, self.return_from) - class NodeList(ast.AST): """A synthetic fictitious node, containing a sequence of nodes. @@ -681,14 +591,6 @@ def __init__(self, body: Sequence[ast.AST]) -> None: # TODO: Shouldn't the cause messages join with "and" instead of "or"? -def _make_expression_code_method(noun: str) -> Callable[[AstArcAnalyzer, ast.AST], None]: - """A function to make methods for expression-based callable _code_object__ methods.""" - def _code_object__expression_callable(self: AstArcAnalyzer, node: ast.AST) -> None: - start = self.line_for_node(node) - self.add_arc(-start, start, None, f"didn't run the {noun} on line {start}") - self.add_arc(start, -start, None, f"didn't finish the {noun} on line {start}") - return _code_object__expression_callable - class AstArcAnalyzer: """Analyze source text with an AST to find executable code paths. @@ -705,6 +607,10 @@ class AstArcAnalyzer: For an arc starting from line 17, they should be usable to form complete sentences like: "Line 17 {endmsg} because {startmsg}". + NOTE: Starting in July 2024, I've been whittling this down to only report + arc that are part of true branches. It's not clear how far this work will + go. + """ def __init__( @@ -735,6 +641,15 @@ def __init__( self.missing_arc_fragments: TArcFragments = collections.defaultdict(list) self.block_stack: list[Block] = [] + # If `with` clauses jump to their start on the way out, we need + # information to be able to skip over that jump. We record the arcs + # from `with` into the clause (with_entries), and the arcs from the + # clause to the `with` (with_exits). + self.current_with_starts: set[TLineNo] = set() + self.all_with_starts: set[TLineNo] = set() + self.with_entries: set[TArc] = set() + self.with_exits: set[TArc] = set() + # $set_env.py: COVERAGE_TRACK_ARCS - Trace possible arcs added while parsing code. self.debug = bool(int(os.getenv("COVERAGE_TRACK_ARCS", "0"))) @@ -746,6 +661,35 @@ def analyze(self) -> None: if code_object_handler is not None: code_object_handler(node) + def with_jump_fixers(self) -> dict[TArc, tuple[TArc, TArc]]: + """Get a dict with data for fixing jumps out of with statements. + + Returns a dict. The keys are arcs leaving a with statement by jumping + back to its start. The values are pairs: first, the arc from the start + to the next statement, then the arc that exits the with without going + to the start. + + """ + if not env.PYBEHAVIOR.exit_through_with: + return {} + + fixers = {} + with_nexts = { + arc + for arc in self.arcs + if arc[0] in self.all_with_starts and arc not in self.with_entries + } + for start in self.all_with_starts: + nexts = {arc[1] for arc in with_nexts if arc[0] == start} + if not nexts: + continue + assert len(nexts) == 1, f"Expected one arc, got {nexts} with {start = }" + nxt = nexts.pop() + prvs = {arc[0] for arc in self.with_exits if arc[1] == start} + for prv in prvs: + fixers[(prv, start)] = ((start, nxt), (prv, nxt)) + return fixers + # Code object dispatchers: _code_object__* # # These methods are used by analyze() as the start of the analysis. @@ -754,18 +698,17 @@ def analyze(self) -> None: def _code_object__Module(self, node: ast.Module) -> None: start = self.line_for_node(node) if node.body: - exits = self.body_exits(node.body, from_start=ArcStart(-start)) + exits = self.process_body(node.body) for xit in exits: self.add_arc(xit.lineno, -start, xit.cause, "didn't exit the module") else: # Empty module. - self.add_arc(-start, start) self.add_arc(start, -start) def _code_object__FunctionDef(self, node: ast.FunctionDef) -> None: start = self.line_for_node(node) self.block_stack.append(FunctionBlock(start=start, name=node.name)) - exits = self.body_exits(node.body, from_start=ArcStart(-start)) + exits = self.process_body(node.body) self.process_return_exits(exits) self.block_stack.pop() @@ -773,22 +716,13 @@ def _code_object__FunctionDef(self, node: ast.FunctionDef) -> None: def _code_object__ClassDef(self, node: ast.ClassDef) -> None: start = self.line_for_node(node) - self.add_arc(-start, start) - exits = self.body_exits(node.body, from_start=ArcStart(start)) + exits = self.process_body(node.body)#, from_start=ArcStart(start)) for xit in exits: self.add_arc( xit.lineno, -start, xit.cause, f"didn't exit the body of class {node.name!r}", ) - _code_object__Lambda = _make_expression_code_method("lambda") - _code_object__GeneratorExp = _make_expression_code_method("generator expression") - if env.PYBEHAVIOR.comprehensions_are_functions: - _code_object__DictComp = _make_expression_code_method("dictionary comprehension") - _code_object__SetComp = _make_expression_code_method("set comprehension") - _code_object__ListComp = _make_expression_code_method("list comprehension") - - def add_arc( self, start: TLineNo, @@ -798,9 +732,11 @@ def add_arc( ) -> None: """Add an arc, including message fragments to use if it is missing.""" if self.debug: # pragma: debugging - print(f"\nAdding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}") - print(short_stack()) + print(f"Adding possible arc: ({start}, {end}): {smsg!r}, {emsg!r}") + print(short_stack(), end="\n\n") self.arcs.add((start, end)) + if start in self.current_with_starts: + self.with_entries.add((start, end)) if smsg is not None or emsg is not None: self.missing_arc_fragments[(start, end)].append((smsg, emsg)) @@ -913,27 +849,31 @@ def node_exits(self, node: ast.AST) -> set[ArcStart]: arc_starts = {ArcStart(self.line_for_node(node))} return arc_starts - def body_exits( + def process_body( self, body: Sequence[ast.AST], from_start: ArcStart | None = None, prev_starts: set[ArcStart] | None = None, ) -> set[ArcStart]: - """Find arc starts that exit the body of a compound statement. + """Process the body of a compound statement. + + `body` is the body node to process. - `body` is the body node. `from_start` is a single `ArcStart` that can - be the previous line in flow before this body. `prev_starts` is a set - of ArcStarts that can be the previous line. Only one of them should be + `from_start` is a single `ArcStart` that starts an arc into this body. + `prev_starts` is a set of ArcStarts that can all be the start of arcs + into this body. Only one of `from_start` and `prev_starts` should be given. - Also records arcs (using `add_arc`) within the body. + Records arcs within the body by calling `self.add_arc`. Returns a set of ArcStarts, the exits from this body. """ if prev_starts is None: - assert from_start is not None - prev_starts = {from_start} + if from_start is None: + prev_starts = set() + else: + prev_starts = {from_start} else: assert from_start is None @@ -962,7 +902,7 @@ def find_non_missing_node(self, node: ast.AST) -> ast.AST | None: Returns a node, or None if none of the node remains. """ - # This repeats work just done in body_exits, but this duplication + # This repeats work just done in process_body, but this duplication # means we can avoid a function call in the 99.9999% case of not # optimizing away statements. lineno = self.line_for_node(node) @@ -1110,11 +1050,6 @@ def _handle_decorated(self, node: ast.FunctionDef) -> set[ArcStart]: assert last is not None self.add_arc(last, main_line) last = main_line - if env.PYBEHAVIOR.trace_decorator_line_again: - for top, bot in zip(decs, decs[1:]): - self.add_arc(self.line_for_node(bot), self.line_for_node(top)) - self.add_arc(self.line_for_node(decs[0]), main_line) - self.add_arc(main_line, self.line_for_node(decs[-1])) # The definition line may have been missed, but we should have it # in `self.statements`. For some constructs, `line_for_node` is # not what we'd think of as the first line in the statement, so map @@ -1138,7 +1073,7 @@ def _handle__For(self, node: ast.For) -> set[ArcStart]: start = self.line_for_node(node.iter) self.block_stack.append(LoopBlock(start=start)) from_start = ArcStart(start, cause="the loop on line {lineno} never started") - exits = self.body_exits(node.body, from_start=from_start) + exits = self.process_body(node.body, from_start=from_start) # Any exit from the body will go back to the top of the loop. for xit in exits: self.add_arc(xit.lineno, start, xit.cause) @@ -1147,7 +1082,7 @@ def _handle__For(self, node: ast.For) -> set[ArcStart]: exits = my_block.break_exits from_start = ArcStart(start, cause="the loop on line {lineno} didn't complete") if node.orelse: - else_exits = self.body_exits(node.orelse, from_start=from_start) + else_exits = self.process_body(node.orelse, from_start=from_start) exits |= else_exits else: # No else clause: exit from the for line. @@ -1162,9 +1097,9 @@ def _handle__For(self, node: ast.For) -> set[ArcStart]: def _handle__If(self, node: ast.If) -> set[ArcStart]: start = self.line_for_node(node.test) from_start = ArcStart(start, cause="the condition on line {lineno} was never true") - exits = self.body_exits(node.body, from_start=from_start) + exits = self.process_body(node.body, from_start=from_start) from_start = ArcStart(start, cause="the condition on line {lineno} was always true") - exits |= self.body_exits(node.orelse, from_start=from_start) + exits |= self.process_body(node.orelse, from_start=from_start) return exits if sys.version_info >= (3, 10): @@ -1179,7 +1114,7 @@ def _handle__Match(self, node: ast.Match) -> set[ArcStart]: case_start, cause="the pattern on line {lineno} never matched", ) - exits |= self.body_exits(case.body, from_start=from_start) + exits |= self.process_body(case.body, from_start=from_start) last_start = case_start # case is now the last case, check for wildcard match. @@ -1200,7 +1135,7 @@ def _handle__Match(self, node: ast.Match) -> set[ArcStart]: def _handle__NodeList(self, node: NodeList) -> set[ArcStart]: start = self.line_for_node(node) - exits = self.body_exits(node.body, from_start=ArcStart(start)) + exits = self.process_body(node.body, from_start=ArcStart(start)) return exits def _handle__Raise(self, node: ast.Raise) -> set[ArcStart]: @@ -1235,91 +1170,35 @@ def _handle__Try(self, node: ast.Try) -> set[ArcStart]: self.block_stack.append(try_block) start = self.line_for_node(node) - exits = self.body_exits(node.body, from_start=ArcStart(start)) + exits = self.process_body(node.body, from_start=ArcStart(start)) # We're done with the `try` body, so this block no longer handles # exceptions. We keep the block so the `finally` clause can pick up # flows from the handlers and `else` clause. if node.finalbody: try_block.handler_start = None - if node.handlers: - # If there are `except` clauses, then raises in the try body - # will already jump to them. Start this set over for raises in - # `except` and `else`. - try_block.raise_from = set() else: self.block_stack.pop() handler_exits: set[ArcStart] = set() if node.handlers: - last_handler_start: TLineNo | None = None for handler_node in node.handlers: handler_start = self.line_for_node(handler_node) - if last_handler_start is not None: - self.add_arc(last_handler_start, handler_start) - last_handler_start = handler_start from_cause = "the exception caught by line {lineno} didn't happen" from_start = ArcStart(handler_start, cause=from_cause) - handler_exits |= self.body_exits(handler_node.body, from_start=from_start) + handler_exits |= self.process_body(handler_node.body, from_start=from_start) if node.orelse: - exits = self.body_exits(node.orelse, prev_starts=exits) + exits = self.process_body(node.orelse, prev_starts=exits) exits |= handler_exits if node.finalbody: self.block_stack.pop() - final_from = ( # You can get to the `finally` clause from: - exits | # the exits of the body or `else` clause, - try_block.break_from | # or a `break`, - try_block.continue_from | # or a `continue`, - try_block.raise_from | # or a `raise`, - try_block.return_from # or a `return`. - ) - - final_exits = self.body_exits(node.finalbody, prev_starts=final_from) + final_from = exits - if try_block.break_from: - if env.PYBEHAVIOR.finally_jumps_back: - for break_line in try_block.break_from: - lineno = break_line.lineno - cause = break_line.cause.format(lineno=lineno) - for final_exit in final_exits: - self.add_arc(final_exit.lineno, lineno, cause) - breaks = try_block.break_from - else: - breaks = self._combine_finally_starts(try_block.break_from, final_exits) - self.process_break_exits(breaks) - - if try_block.continue_from: - if env.PYBEHAVIOR.finally_jumps_back: - for continue_line in try_block.continue_from: - lineno = continue_line.lineno - cause = continue_line.cause.format(lineno=lineno) - for final_exit in final_exits: - self.add_arc(final_exit.lineno, lineno, cause) - continues = try_block.continue_from - else: - continues = self._combine_finally_starts(try_block.continue_from, final_exits) - self.process_continue_exits(continues) - - if try_block.raise_from: - self.process_raise_exits( - self._combine_finally_starts(try_block.raise_from, final_exits), - ) - - if try_block.return_from: - if env.PYBEHAVIOR.finally_jumps_back: - for return_line in try_block.return_from: - lineno = return_line.lineno - cause = return_line.cause.format(lineno=lineno) - for final_exit in final_exits: - self.add_arc(final_exit.lineno, lineno, cause) - returns = try_block.return_from - else: - returns = self._combine_finally_starts(try_block.return_from, final_exits) - self.process_return_exits(returns) + final_exits = self.process_body(node.finalbody, prev_starts=final_from) if exits: # The finally clause's exits are only exits for the try block @@ -1328,21 +1207,6 @@ def _handle__Try(self, node: ast.Try) -> set[ArcStart]: return exits - def _combine_finally_starts(self, starts: set[ArcStart], exits: set[ArcStart]) -> set[ArcStart]: - """Helper for building the cause of `finally` branches. - - "finally" clauses might not execute their exits, and the causes could - be due to a failure to execute any of the exits in the try block. So - we use the causes from `starts` as the causes for `exits`. - """ - causes = [] - for start in sorted(starts): - if start.cause: - causes.append(start.cause.format(lineno=start.lineno)) - cause = " or ".join(causes) - exits = {ArcStart(xit.lineno, cause) for xit in exits} - return exits - def _handle__While(self, node: ast.While) -> set[ArcStart]: start = to_top = self.line_for_node(node.test) constant_test = self.is_constant_expr(node.test) @@ -1355,7 +1219,7 @@ def _handle__While(self, node: ast.While) -> set[ArcStart]: to_top = self.line_for_node(node.body[0]) self.block_stack.append(LoopBlock(start=to_top)) from_start = ArcStart(start, cause="the condition on line {lineno} was never true") - exits = self.body_exits(node.body, from_start=from_start) + exits = self.process_body(node.body, from_start=from_start) for xit in exits: self.add_arc(xit.lineno, to_top, xit.cause) exits = set() @@ -1364,7 +1228,7 @@ def _handle__While(self, node: ast.While) -> set[ArcStart]: exits.update(my_block.break_exits) from_start = ArcStart(start, cause="the condition on line {lineno} was always true") if node.orelse: - else_exits = self.body_exits(node.orelse, from_start=from_start) + else_exits = self.process_body(node.orelse, from_start=from_start) exits |= else_exits else: # No `else` clause: you can exit from the start. @@ -1375,28 +1239,18 @@ def _handle__While(self, node: ast.While) -> set[ArcStart]: def _handle__With(self, node: ast.With) -> set[ArcStart]: start = self.line_for_node(node) if env.PYBEHAVIOR.exit_through_with: - self.block_stack.append(WithBlock(start=start)) - exits = self.body_exits(node.body, from_start=ArcStart(start)) + self.current_with_starts.add(start) + self.all_with_starts.add(start) + exits = self.process_body(node.body, from_start=ArcStart(start)) if env.PYBEHAVIOR.exit_through_with: - with_block = self.block_stack.pop() - assert isinstance(with_block, WithBlock) + self.current_with_starts.remove(start) with_exit = {ArcStart(start)} if exits: for xit in exits: self.add_arc(xit.lineno, start) + self.with_exits.add((xit.lineno, start)) exits = with_exit - if with_block.break_from: - self.process_break_exits( - self._combine_finally_starts(with_block.break_from, with_exit), - ) - if with_block.continue_from: - self.process_continue_exits( - self._combine_finally_starts(with_block.continue_from, with_exit), - ) - if with_block.return_from: - self.process_return_exits( - self._combine_finally_starts(with_block.return_from, with_exit), - ) + return exits _handle__AsyncWith = _handle__With diff --git a/coverage/python.py b/coverage/python.py index 4ac241257..3ef0e5ff4 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -225,7 +225,7 @@ def missing_arc_description( end: TLineNo, executed_arcs: Iterable[TArc] | None = None, ) -> str: - return self.parser.missing_arc_description(start, end, executed_arcs) + return self.parser.missing_arc_description(start, end) def source(self) -> str: if self._source is None: diff --git a/coverage/results.py b/coverage/results.py index 755c42e3c..d5842d944 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -34,7 +34,26 @@ def analysis_from_file_reporter( if has_arcs: _arc_possibilities_set = file_reporter.arcs() - _arcs_executed_set = file_reporter.translate_arcs(data.arcs(filename) or []) + arcs = data.arcs(filename) or [] + + # Reduce the set of arcs to the ones that could be branches. + dests = collections.defaultdict(set) + for fromno, tono in _arc_possibilities_set: + dests[fromno].add(tono) + single_dests = { + fromno: list(tonos)[0] + for fromno, tonos in dests.items() + if len(tonos) == 1 + } + new_arcs = set() + for fromno, tono in arcs: + if fromno != tono: + new_arcs.add((fromno, tono)) + else: + if fromno in single_dests: + new_arcs.add((fromno, single_dests[fromno])) + + _arcs_executed_set = file_reporter.translate_arcs(new_arcs) exit_counts = file_reporter.exit_counts() no_branch = file_reporter.no_branch_lines() else: @@ -165,21 +184,6 @@ def arcs_missing(self) -> list[TArc]: ) return sorted(missing) - def arcs_unpredicted(self) -> list[TArc]: - """Returns a sorted list of the executed arcs missing from the code.""" - # Exclude arcs here which connect a line to itself. They can occur - # in executed data in some cases. This is where they can cause - # trouble, and here is where it's the least burden to remove them. - # Also, generators can somehow cause arcs from "enter" to "exit", so - # make sure we have at least one positive value. - unpredicted = ( - e for e in self.arcs_executed - if e not in self.arc_possibilities - and e[0] != e[1] - and (e[0] > 0 or e[1] > 0) - ) - return sorted(unpredicted) - def _branch_lines(self) -> list[TLineNo]: """Returns a list of line numbers that have more than one exit.""" return [l1 for l1,count in self.exit_counts.items() if count > 1] @@ -198,6 +202,8 @@ def missing_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]: branch_lines = set(self._branch_lines()) mba = collections.defaultdict(list) for l1, l2 in missing: + if l1 == l2: + continue if l1 in branch_lines: mba[l1].append(l2) return mba @@ -211,6 +217,8 @@ def executed_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]: branch_lines = set(self._branch_lines()) eba = collections.defaultdict(list) for l1, l2 in self.arcs_executed: + if l1 == l2: + continue if l1 in branch_lines: eba[l1].append(l2) return eba diff --git a/lab/notes/arcs-to-branches.txt b/lab/notes/arcs-to-branches.txt new file mode 100644 index 000000000..ca2b2bedd --- /dev/null +++ b/lab/notes/arcs-to-branches.txt @@ -0,0 +1,24 @@ +August 2024 + +Until now, "arcs" means a complete set of predicted and measured (from, to) +pairs of line numbers. Branches were determined by finding "from" lines that +appeared in more than one predicted arc. + +That scheme found branches that were not true branches, such as the lines in +finally clauses that could jump to more than one place based on how the finally +clause was reached. + +Now we are shifting to true branches. To do this, we are removing code that +predicted arcs that aren't part of true branches. The ideal goal would be to +only predict arcs that are part of branches, but a minimal goal is to stop +predicting arcs that led to false branches. ie, it's ok to predict an arc if +the arc is the only arc for a given "from" line. Those arcs will be discarded +and won't lead to false branches. + +There are many tests that look odd now, because they were testing arc +determination, but they have no branches. Or the interesting part of the tests +were non-branch arcs, so they aren't visible in the tests anymore. + +parser.py likely is working harder than it needs to, since we don't need to find +all arcs. The new code.co_branches() function might be good enough to replace +it. diff --git a/lab/parser.py b/lab/parser.py index b9edc4f5c..3c040c1e0 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -107,8 +107,6 @@ def one_file(self, options, filename): marks[2] = str(exits) if lineno in pyparser.raw_docstrings: marks[3] = '"' - if lineno in pyparser.raw_classdefs: - marks[3] = 'C' if lineno in pyparser.raw_excluded: marks[4] = 'X' elif lineno in pyparser.excluded: diff --git a/lab/run_sysmon.py b/lab/run_sysmon.py index a52453cc1..fbbd6a315 100644 --- a/lab/run_sysmon.py +++ b/lab/run_sysmon.py @@ -9,7 +9,7 @@ print(sys.version) the_program = sys.argv[1] -code = open(the_program).read() +code = compile(open(the_program).read(), filename=the_program, mode="exec") my_id = sys.monitoring.COVERAGE_ID sys.monitoring.use_tool_id(my_id, "run_sysmon.py") @@ -27,50 +27,70 @@ def bytes_to_lines(code): return b2l +def show_off(label, code, instruction_offset): + if code.co_filename == the_program: + b2l = bytes_to_lines(code) + print(f"{label}: {code.co_filename}@{instruction_offset} #{b2l[instruction_offset]}") + +def show_line(label, code, line_number): + if code.co_filename == the_program: + print(f"{label}: {code.co_filename} #{line_number}") + +def show_off_off(label, code, instruction_offset, destination_offset): + if code.co_filename == the_program: + b2l = bytes_to_lines(code) + print( + f"{label}: {code.co_filename}@{instruction_offset}->{destination_offset} " + + f"#{b2l[instruction_offset]}->{b2l[destination_offset]}" + ) + def sysmon_py_start(code, instruction_offset): - print(f"PY_START: {code.co_filename}@{instruction_offset}") + show_off("PY_START", code, instruction_offset) sys.monitoring.set_local_events( my_id, code, - events.PY_RETURN | events.PY_RESUME | events.LINE | events.BRANCH | events.JUMP, + events.PY_RETURN + | events.PY_RESUME + | events.LINE + | events.BRANCH_TAKEN + | events.BRANCH_NOT_TAKEN + | events.JUMP, ) def sysmon_py_resume(code, instruction_offset): - b2l = bytes_to_lines(code) - print( - f"PY_RESUME: {code.co_filename}@{instruction_offset}, " - + f"{b2l[instruction_offset]}" - ) + show_off("PY_RESUME", code, instruction_offset) + return sys.monitoring.DISABLE def sysmon_py_return(code, instruction_offset, retval): - b2l = bytes_to_lines(code) - print( - f"PY_RETURN: {code.co_filename}@{instruction_offset}, " - + f"{b2l[instruction_offset]}" - ) + show_off("PY_RETURN", code, instruction_offset) + return sys.monitoring.DISABLE def sysmon_line(code, line_number): - print(f"LINE: {code.co_filename}@{line_number}") + show_line("LINE", code, line_number) return sys.monitoring.DISABLE def sysmon_branch(code, instruction_offset, destination_offset): - b2l = bytes_to_lines(code) - print( - f"BRANCH: {code.co_filename}@{instruction_offset}->{destination_offset}, " - + f"{b2l[instruction_offset]}->{b2l[destination_offset]}" - ) + show_off_off("BRANCH", code, instruction_offset, destination_offset) + return sys.monitoring.DISABLE + + +def sysmon_branch_taken(code, instruction_offset, destination_offset): + show_off_off("BRANCH_TAKEN", code, instruction_offset, destination_offset) + return sys.monitoring.DISABLE + + +def sysmon_branch_not_taken(code, instruction_offset, destination_offset): + show_off_off("BRANCH_NOT_TAKEN", code, instruction_offset, destination_offset) + return sys.monitoring.DISABLE def sysmon_jump(code, instruction_offset, destination_offset): - b2l = bytes_to_lines(code) - print( - f"JUMP: {code.co_filename}@{instruction_offset}->{destination_offset}, " - + f"{b2l[instruction_offset]}->{b2l[destination_offset]}" - ) + show_off_off("JUMP", code, instruction_offset, destination_offset) + return sys.monitoring.DISABLE sys.monitoring.set_events( @@ -82,7 +102,9 @@ def sysmon_jump(code, instruction_offset, destination_offset): register(events.PY_RETURN, sysmon_py_return) # register(events.PY_UNWIND, sysmon_py_unwind_arcs) register(events.LINE, sysmon_line) -register(events.BRANCH, sysmon_branch) +#register(events.BRANCH, sysmon_branch) +register(events.BRANCH_TAKEN, sysmon_branch_taken) +register(events.BRANCH_NOT_TAKEN, sysmon_branch_not_taken) register(events.JUMP, sysmon_jump) exec(code) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 38803cf00..86f25a955 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -5,9 +5,9 @@ from __future__ import annotations +import collections import contextlib import datetime -import difflib import glob import io import os @@ -29,7 +29,7 @@ from coverage.misc import import_local_file from coverage.types import TArc, TLineNo -from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal +from tests.helpers import arcz_to_arcs, assert_count_equal from tests.helpers import nice_file, run_command from tests.mixins import PytestBase, StdStreamCapturingMixin, RestoreModulesMixin, TempDirMixin @@ -46,6 +46,23 @@ COVERAGE_INSTALL_ARGS = os.getenv("COVERAGE_INSTALL_ARGS", nice_file(TESTS_DIR, "..")) +def arcs_to_branches(arcs: Iterable[TArc]) -> dict[TLineNo, list[TLineNo]]: + """Convert a list of arcs into a dict showing branches.""" + arcs_combined = collections.defaultdict(set) + for fromno, tono in arcs: + arcs_combined[fromno].add(tono) + branches = collections.defaultdict(list) + for fromno, tono in arcs: + if len(arcs_combined[fromno]) > 1: + branches[fromno].append(tono) + return branches + + +def branches_to_arcs(branches: dict[TLineNo, list[TLineNo]]) -> list[TArc]: + """Convert a dict od branches into a list of arcs.""" + return [(fromno, tono) for fromno, tonos in branches.items() for tono in tonos] + + class CoverageTest( StdStreamCapturingMixin, RestoreModulesMixin, @@ -125,28 +142,6 @@ def get_module_name(self) -> str: self.last_module_name = 'coverage_test_' + str(random.random())[2:] return self.last_module_name - def _check_arcs( - self, - a1: Iterable[TArc] | None, - a2: Iterable[TArc] | None, - arc_type: str, - ) -> str: - """Check that the arc lists `a1` and `a2` are equal. - - If they are equal, return empty string. If they are unequal, return - a string explaining what is different. - """ - # Make them into multi-line strings so we can see what's going wrong. - s1 = arcs_to_arcz_repr(a1) - s2 = arcs_to_arcz_repr(a2) - if s1 != s2: - lines1 = s1.splitlines(True) - lines2 = s2.splitlines(True) - diff = "".join(difflib.ndiff(lines1, lines2)) - return "\n" + arc_type + " arcs differ: minus is expected, plus is actual\n" + diff - else: - return "" - def check_coverage( self, text: str, @@ -155,12 +150,8 @@ def check_coverage( report: str = "", excludes: Iterable[str] | None = None, partials: Iterable[str] = (), - arcz: str | None = None, - arcz_missing: str | None = None, - arcz_unpredicted: str | None = None, - arcs: Iterable[TArc] | None = None, - arcs_missing: Iterable[TArc] | None = None, - arcs_unpredicted: Iterable[TArc] | None = None, + branchz: str | None = None, + branchz_missing: str | None = None, ) -> Coverage: """Check the coverage measurement of `text`. @@ -170,12 +161,9 @@ def check_coverage( regexes to match against for excluding lines, and `report` is the text of the measurement report. - For arc measurement, `arcz` is a string that can be decoded into arcs - in the code (see `arcz_to_arcs` for the encoding scheme). - `arcz_missing` are the arcs that are not executed, and - `arcz_unpredicted` are the arcs executed in the code, but not deducible - from the code. These last two default to "", meaning we explicitly - check that there are no missing or unpredicted arcs. + For branch measurement, `branchz` is a string that can be decoded into + arcs in the code (see `arcz_to_arcs` for the encoding scheme). + `branchz_missing` are the arcs that are not executed. Returns the Coverage object, in case you want to poke at it some more. @@ -188,12 +176,11 @@ def check_coverage( self.make_file(modname + ".py", text) - if arcs is None and arcz is not None: - arcs = arcz_to_arcs(arcz) - if arcs_missing is None and arcz_missing is not None: - arcs_missing = arcz_to_arcs(arcz_missing) - if arcs_unpredicted is None and arcz_unpredicted is not None: - arcs_unpredicted = arcz_to_arcs(arcz_unpredicted) + branches = branches_missing = None + if branchz is not None: + branches = arcz_to_arcs(branchz) + if branchz_missing is not None: + branches_missing = arcz_to_arcs(branchz_missing) # Start up coverage.py. cov = coverage.Coverage(branch=True) @@ -229,20 +216,19 @@ def check_coverage( msg = f"missing: {missing_formatted!r} != {missing!r}" assert missing_formatted == missing, msg - if arcs is not None: - # print("Possible arcs:") - # print(" expected:", arcs) - # print(" actual:", analysis.arc_possibilities) - # print("Executed:") - # print(" actual:", sorted(set(analysis.arcs_executed))) - # TODO: this would be nicer with pytest-check, once we can run that. - msg = ( - self._check_arcs(arcs, analysis.arc_possibilities, "Possible") + - self._check_arcs(arcs_missing, analysis.arcs_missing(), "Missing") + - self._check_arcs(arcs_unpredicted, analysis.arcs_unpredicted(), "Unpredicted") + if branches is not None: + trimmed_arcs = branches_to_arcs(arcs_to_branches(analysis.arc_possibilities)) + assert branches == trimmed_arcs, ( + f"Wrong possible branches: {branches} != {trimmed_arcs}" ) - if msg: - assert False, msg + if branches_missing is not None: + assert set(branches_missing) <= set(branches), ( + f"{branches_missing = }, has non-branches in it." + ) + analysis_missing = branches_to_arcs(analysis.missing_branch_arcs()) + assert branches_missing == analysis_missing, ( + f"Wrong missing branches: {branches_missing} != {analysis_missing}" + ) if report: frep = io.StringIO() diff --git a/tests/helpers.py b/tests/helpers.py index aa55085a1..823bbfd41 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -29,7 +29,7 @@ from coverage import env from coverage.debug import DebugControl from coverage.exceptions import CoverageWarning -from coverage.types import TArc, TLineNo +from coverage.types import TArc def run_command(cmd: str) -> tuple[int, str]: @@ -259,38 +259,6 @@ def arcz_to_arcs(arcz: str) -> list[TArc]: return sorted(arcs) -_arcz_unmap = {val: ch for ch, val in _arcz_map.items()} - - -def _arcs_to_arcz_repr_one(num: TLineNo) -> str: - """Return an arcz form of the number `num`, or "?" if there is none.""" - if num == -1: - return "." - z = "" - if num < 0: - z += "-" - num *= -1 - z += _arcz_unmap.get(num, "?") - return z - - -def arcs_to_arcz_repr(arcs: Iterable[TArc] | None) -> str: - """Convert a list of arcs to a readable multi-line form for asserting. - - Each pair is on its own line, with a comment showing the arcz form, - to make it easier to decode when debugging test failures. - - """ - repr_list = [] - for a, b in (arcs or ()): - line = repr((a, b)) - line += " # " - line += _arcs_to_arcz_repr_one(a) - line += _arcs_to_arcz_repr_one(b) - repr_list.append(line) - return "\n".join(repr_list) + "\n" - - @contextlib.contextmanager def change_dir(new_dir: str | Path) -> Iterator[None]: """Change directory, and then change back. diff --git a/tests/test_api.py b/tests/test_api.py index 9f65166b9..58f046367 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -115,7 +115,8 @@ def test_filenames(self) -> None: filename, _, _, _ = cov.analysis(sys.modules["mymod"]) assert os.path.basename(filename) == "mymod.py" - def test_ignore_stdlib(self) -> None: + @pytest.mark.parametrize("cover_pylib", [False, True]) + def test_stdlib(self, cover_pylib: bool) -> None: self.make_file("mymain.py", """\ import colorsys a = 1 @@ -123,27 +124,18 @@ def test_ignore_stdlib(self) -> None: """) # Measure without the stdlib. - cov1 = coverage.Coverage() - assert cov1.config.cover_pylib is False + cov1 = coverage.Coverage(cover_pylib=cover_pylib) self.start_import_stop(cov1, "mymain") - # some statements were marked executed in mymain.py _, statements, missing, _ = cov1.analysis("mymain.py") - assert statements != missing + assert statements == [1, 2, 3] + assert missing == [] # but none were in colorsys.py _, statements, missing, _ = cov1.analysis("colorsys.py") - assert statements == missing - - # Measure with the stdlib. - cov2 = coverage.Coverage(cover_pylib=True) - self.start_import_stop(cov2, "mymain") - - # some statements were marked executed in mymain.py - _, statements, missing, _ = cov2.analysis("mymain.py") - assert statements != missing - # and some were marked executed in colorsys.py - _, statements, missing, _ = cov2.analysis("colorsys.py") - assert statements != missing + if cover_pylib: + assert statements != missing + else: + assert statements == missing def test_include_can_measure_stdlib(self) -> None: self.make_file("mymain.py", """\ diff --git a/tests/test_arcs.py b/tests/test_arcs.py index d4719db5f..14352e016 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -16,14 +16,6 @@ from coverage.files import abs_file -# When a try block ends, does the finally block (incorrectly) jump to the -# last statement, or does it go the line outside the try block that it -# should? -xfail_pypy_3882 = pytest.mark.xfail( - env.PYPY and env.PYVERSION[:2] == (3, 8) and env.PYPYVERSION >= (7, 3, 11), - reason="https://foss.heptapod.net/pypy/pypy/-/issues/3882", -) - class SimpleArcTest(CoverageTest): """Tests for coverage.py's arc measurement.""" @@ -32,16 +24,15 @@ def test_simple_sequence(self) -> None: a = 1 b = 2 """, - arcz=".1 12 2.", + branchz="", ) self.check_coverage("""\ a = 1 b = 3 """, - arcz=".1 13 3.", + branchz="", ) - line1 = 1 if env.PYBEHAVIOR.module_firstline_1 else 2 self.check_coverage("""\ a = 2 @@ -49,7 +40,7 @@ def test_simple_sequence(self) -> None: c = 5 """, - arcz=f"-{line1}2 23 35 5-{line1}", + branchz="", ) def test_function_def(self) -> None: @@ -59,7 +50,7 @@ def foo(): foo() """, - arcz=".1 .2 14 2. 4.", + branchz="", ) def test_if(self) -> None: @@ -69,7 +60,8 @@ def test_if(self) -> None: a = 3 assert a == 3 """, - arcz=".1 12 23 24 34 4.", arcz_missing="24", + branchz="23 24", + branchz_missing="24", ) self.check_coverage("""\ a = 1 @@ -77,7 +69,8 @@ def test_if(self) -> None: a = 3 assert a == 1 """, - arcz=".1 12 23 24 34 4.", arcz_missing="23 34", + branchz="23 24", + branchz_missing="23", ) def test_if_else(self) -> None: @@ -88,7 +81,8 @@ def test_if_else(self) -> None: a = 4 assert a == 2 """, - arcz=".1 12 25 14 45 5.", arcz_missing="14 45", + branchz="12 14", + branchz_missing="14", ) self.check_coverage("""\ if len([]) == 1: @@ -97,7 +91,8 @@ def test_if_else(self) -> None: a = 4 assert a == 4 """, - arcz=".1 12 25 14 45 5.", arcz_missing="12 25", + branchz="12 14", + branchz_missing="12", ) def test_compact_if(self) -> None: @@ -106,7 +101,7 @@ def test_compact_if(self) -> None: if len([]) == 0: a = 2 assert a == 2 """, - arcz=".1 12 23 3.", + branchz="", branchz_missing="", ) self.check_coverage("""\ def fn(x): @@ -115,7 +110,7 @@ def fn(x): a = fn(1) assert a is True """, - arcz=".1 14 45 5. .2 2. 23 3.", arcz_missing="23 3.", + branchz="2. 23", branchz_missing="23", ) def test_multiline(self) -> None: @@ -127,7 +122,7 @@ def test_multiline(self) -> None: b = \\ 6 """, - arcz=".1 15 5.", + branchz="", ) def test_if_return(self) -> None: @@ -140,7 +135,8 @@ def if_ret(a): x = if_ret(0) + if_ret(1) assert x == 8 """, - arcz=".1 16 67 7. .2 23 24 3. 45 5.", + branchz="23 24", + branchz_missing="", ) def test_dont_confuse_exit_and_else(self) -> None: @@ -153,7 +149,8 @@ def foo(): return a assert foo() == 3 # 7 """, - arcz=".1 17 7. .2 23 36 25 56 6.", arcz_missing="25 56", + branchz="23 25", + branchz_missing="25", ) self.check_coverage("""\ def foo(): @@ -163,19 +160,8 @@ def foo(): a = 5 foo() # 6 """, - arcz=".1 16 6. .2 23 3. 25 5.", arcz_missing="25 5.", - ) - - def test_what_is_the_sound_of_no_lines_clapping(self) -> None: - if env.PYBEHAVIOR.empty_is_empty: - arcz_missing=".1 1." - else: - arcz_missing="" - self.check_coverage("""\ - # __init__.py - """, - arcz=".1 1.", - arcz_missing=arcz_missing, + branchz="23 25", + branchz_missing="25", ) def test_bug_1184(self) -> None: @@ -191,8 +177,8 @@ def foo(x): for i in range(3): # 9 foo(i) """, - arcz=".1 19 9-1 .2 23 27 34 47 56 67 7-1 9A A9", - arcz_unpredicted="45", + branchz="23 27 9A 9.", + branchz_missing="", ) @@ -200,9 +186,6 @@ class WithTest(CoverageTest): """Arc-measuring tests involving context managers.""" def test_with(self) -> None: - arcz = ".1 .2 23 34 4. 16 6." - if env.PYBEHAVIOR.exit_through_with: - arcz = arcz.replace("4.", "42 2.") self.check_coverage("""\ def example(): with open("test", "w") as f: @@ -211,13 +194,11 @@ def example(): example() """, - arcz=arcz, + branchz="", + branchz_missing="", ) def test_with_return(self) -> None: - arcz = ".1 .2 23 34 4. 16 6." - if env.PYBEHAVIOR.exit_through_with: - arcz = arcz.replace("4.", "42 2.") self.check_coverage("""\ def example(): with open("test", "w") as f: @@ -226,14 +207,12 @@ def example(): example() """, - arcz=arcz, + branchz="", + branchz_missing="", ) def test_bug_146(self) -> None: # https://github.com/nedbat/coveragepy/issues/146 - arcz = ".1 12 23 34 41 15 5." - if env.PYBEHAVIOR.exit_through_with: - arcz = arcz.replace("34", "32 24") self.check_coverage("""\ for i in range(2): with open("test", "w") as f: @@ -241,13 +220,12 @@ def test_bug_146(self) -> None: print(4) print(5) """, - arcz=arcz, + branchz="12 15", + branchz_missing="", ) + assert self.stdout() == "3\n4\n3\n4\n5\n" def test_nested_with_return(self) -> None: - arcz = ".1 .2 23 34 45 56 6. 18 8." - if env.PYBEHAVIOR.exit_through_with: - arcz = arcz.replace("6.", "64 42 2.") self.check_coverage("""\ def example(x): with open("test", "w") as f2: @@ -258,13 +236,11 @@ def example(x): example(8) """, - arcz=arcz, + branchz="", + branchz_missing="", ) def test_break_through_with(self) -> None: - arcz = ".1 12 23 34 45 15 5." - if env.PYBEHAVIOR.exit_through_with: - arcz = arcz.replace("45", "42 25") self.check_coverage("""\ for i in range(1+1): with open("test", "w") as f: @@ -272,14 +248,11 @@ def test_break_through_with(self) -> None: break print(5) """, - arcz=arcz, - arcz_missing="15", + branchz="12 15", + branchz_missing="15", ) def test_continue_through_with(self) -> None: - arcz = ".1 12 23 34 41 15 5." - if env.PYBEHAVIOR.exit_through_with: - arcz = arcz.replace("41", "42 21") self.check_coverage("""\ for i in range(1+1): with open("test", "w") as f: @@ -287,23 +260,16 @@ def test_continue_through_with(self) -> None: continue print(5) """, - arcz=arcz, + branchz="12 15", + branchz_missing="", ) # https://github.com/nedbat/coveragepy/issues/1270 def test_raise_through_with(self) -> None: - if env.PYBEHAVIOR.exit_through_with: - arcz = ".1 12 27 78 8. 9A A. -23 34 45 53 6-2" - arcz_missing = "6-2 8." - arcz_unpredicted = "3-2 89" - else: - arcz = ".1 12 27 78 8. 9A A. -23 34 45 5-2 6-2" - arcz_missing = "6-2 8." - arcz_unpredicted = "89" cov = self.check_coverage("""\ - from contextlib import suppress + from contextlib import nullcontext def f(x): - with suppress(): # used as a null context manager + with nullcontext(): print(4) raise Exception("Boo6") print(6) @@ -312,24 +278,35 @@ def f(x): except Exception: print("oops 10") """, - arcz=arcz, - arcz_missing=arcz_missing, - arcz_unpredicted=arcz_unpredicted, + branchz="", + branchz_missing="", ) expected = "line 3 didn't jump to the function exit" assert self.get_missing_arc_description(cov, 3, -2) == expected + def test_untaken_if_through_with(self) -> None: + cov = self.check_coverage("""\ + from contextlib import nullcontext + def f(x): + with nullcontext(): + print(4) + if x == 5: + print(6) + print(7) + f(8) + """, + branchz="56 57", + branchz_missing="56", + ) + assert self.stdout() == "4\n7\n" + expected = "line 3 didn't jump to the function exit" + assert self.get_missing_arc_description(cov, 3, -2) == expected + def test_untaken_raise_through_with(self) -> None: - if env.PYBEHAVIOR.exit_through_with: - arcz = ".1 12 28 89 9. AB B. -23 34 45 56 53 63 37 7-2" - arcz_missing = "56 63 AB B." - else: - arcz = ".1 12 28 89 9. AB B. -23 34 45 56 6-2 57 7-2" - arcz_missing = "56 6-2 AB B." cov = self.check_coverage("""\ - from contextlib import suppress + from contextlib import nullcontext def f(x): - with suppress(): # used as a null context manager + with nullcontext(): print(4) if x == 5: raise Exception("Boo6") @@ -339,12 +316,24 @@ def f(x): except Exception: print("oops 11") """, - arcz=arcz, - arcz_missing=arcz_missing, + branchz="56 57", + branchz_missing="56", ) + assert self.stdout() == "4\n7\n" expected = "line 3 didn't jump to the function exit" assert self.get_missing_arc_description(cov, 3, -2) == expected + def test_with_with_lambda(self) -> None: + self.check_coverage("""\ + from contextlib import nullcontext + with nullcontext(lambda x: 2): + print(3) + print(4) + """, + branchz="", + branchz_missing="", + ) + class LoopArcTest(CoverageTest): """Arc-measuring tests involving loops.""" @@ -355,7 +344,8 @@ def test_loop(self) -> None: a = i assert a == 9 """, - arcz=".1 12 21 13 3.", + branchz="12 13", + branchz_missing="", ) self.check_coverage("""\ a = -1 @@ -363,7 +353,8 @@ def test_loop(self) -> None: a = i assert a == -1 """, - arcz=".1 12 23 32 24 4.", arcz_missing="23 32", + branchz="23 24", + branchz_missing="23", ) def test_nested_loop(self) -> None: @@ -373,17 +364,11 @@ def test_nested_loop(self) -> None: a = i + j assert a == 4 """, - arcz=".1 12 23 32 21 14 4.", + branchz="12 14 23 21", + branchz_missing="", ) def test_break(self) -> None: - if env.PYBEHAVIOR.omit_after_jump: - arcz = ".1 12 23 35 15 5." - arcz_missing = "15" - else: - arcz = ".1 12 23 35 15 41 5." - arcz_missing = "15 41" - self.check_coverage("""\ for i in range(10): a = i @@ -391,17 +376,11 @@ def test_break(self) -> None: a = 99 assert a == 0 # 5 """, - arcz=arcz, arcz_missing=arcz_missing, + branchz="12 15", + branchz_missing="15", ) def test_continue(self) -> None: - if env.PYBEHAVIOR.omit_after_jump: - arcz = ".1 12 23 31 15 5." - arcz_missing = "" - else: - arcz = ".1 12 23 31 15 41 5." - arcz_missing = "41" - self.check_coverage("""\ for i in range(10): a = i @@ -409,7 +388,8 @@ def test_continue(self) -> None: a = 99 assert a == 9 # 5 """, - arcz=arcz, arcz_missing=arcz_missing, + branchz="12 15", + branchz_missing="", ) def test_nested_breaks(self) -> None: @@ -422,15 +402,12 @@ def test_nested_breaks(self) -> None: break assert a == 2 and i == 2 # 7 """, - arcz=".1 12 23 34 45 25 56 51 67 17 7.", arcz_missing="17 25", + branchz="12 17 23 25 51 56", + branchz_missing="17 25", ) def test_while_1(self) -> None: # With "while 1", the loop knows it's constant. - if env.PYBEHAVIOR.keep_constant_test: - arcz = ".1 12 23 34 45 36 62 57 7." - else: - arcz = ".1 13 34 45 36 63 57 7." self.check_coverage("""\ a, i = 1, 0 while 1: @@ -440,16 +417,13 @@ def test_while_1(self) -> None: i += 1 assert a == 4 and i == 3 """, - arcz=arcz, + branchz="34 36", + branchz_missing="", ) def test_while_true(self) -> None: # With "while True", 2.x thinks it's computation, # 3.x thinks it's constant. - if env.PYBEHAVIOR.keep_constant_test: - arcz = ".1 12 23 34 45 36 62 57 7." - else: - arcz = ".1 13 34 45 36 63 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -459,7 +433,8 @@ def test_while_true(self) -> None: i += 1 assert a == 4 and i == 3 """, - arcz=arcz, + branchz="34 36", + branchz_missing="", ) def test_zero_coverage_while_loop(self) -> None: @@ -484,11 +459,6 @@ def method(self): def test_bug_496_continue_in_constant_while(self) -> None: # https://github.com/nedbat/coveragepy/issues/496 - # A continue in a while-true needs to jump to the right place. - if env.PYBEHAVIOR.keep_constant_test: - arcz = ".1 12 23 34 45 52 46 67 7." - else: - arcz = ".1 13 34 45 53 46 67 7." self.check_coverage("""\ up = iter('ta') while True: @@ -498,7 +468,8 @@ def test_bug_496_continue_in_constant_while(self) -> None: i = "line 6" break """, - arcz=arcz, + branchz="45 46", + branchz_missing="", ) def test_for_if_else_for(self) -> None: @@ -521,11 +492,8 @@ def branches_3(l): branches_2([0,1]) branches_3([0,1]) """, - arcz= - ".1 18 8G GH H. " + - ".2 23 34 43 26 3. 6. " + - "-89 9A 9-8 AB BC CB B9 AE E9", - arcz_missing="26 6.", + branchz="23 26 34 3. 9A 9-8 AB AE BC B9", + branchz_missing="26", ) def test_for_else(self) -> None: @@ -538,9 +506,22 @@ def forelse(seq): print('None of the values were greater than 5') print('Done') forelse([1,2]) + """, + branchz="23 26 34 32", + branchz_missing="34", + ) + self.check_coverage("""\ + def forelse(seq): + for n in seq: + if n > 5: + break + else: + print('None of the values were greater than 5') + print('Done') forelse([1,6]) """, - arcz=".1 .2 23 32 34 47 26 67 7. 18 89 9.", + branchz="23 26 34 32", + branchz_missing="26", ) def test_while_else(self) -> None: @@ -554,16 +535,26 @@ def whileelse(seq): n = 99 return n assert whileelse([1, 2]) == 99 + """, + branchz="23 27 45 42", + branchz_missing="45", + ) + self.check_coverage("""\ + def whileelse(seq): + while seq: + n = seq.pop() + if n > 4: + break + else: + n = 99 + return n assert whileelse([1, 5]) == 5 """, - arcz=".1 19 9A A. .2 23 34 45 58 42 27 78 8.", + branchz="23 27 45 42", + branchz_missing="27 42", ) def test_confusing_for_loop_bug_175(self) -> None: - if env.PYBEHAVIOR.comprehensions_are_functions: - extra_arcz = " -22 2-2" - else: - extra_arcz = "" self.check_coverage("""\ o = [(1,2), (3,4)] o = [a for a in o] @@ -571,7 +562,8 @@ def test_confusing_for_loop_bug_175(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 12 23 34 45 53 3." + extra_arcz, + branchz="34 3.", + branchz_missing="", ) self.check_coverage("""\ o = [(1,2), (3,4)] @@ -579,7 +571,8 @@ def test_confusing_for_loop_bug_175(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 12 23 34 42 2." + extra_arcz, + branchz="23 2.", + branchz_missing="", ) # https://bugs.python.org/issue44672 @@ -595,12 +588,11 @@ def wrong_loop(x): wrong_loop(8) """, - arcz=".1 .2 23 26 34 43 3. 6. 18 8.", - arcz_missing="26 6.", + branchz="23 26 34 3.", + branchz_missing="26", ) # https://bugs.python.org/issue44672 - @pytest.mark.xfail(env.PYVERSION < (3, 10), reason="<3.10 traced final pass incorrectly") def test_incorrect_if_bug_1175(self) -> None: self.check_coverage("""\ def wrong_loop(x): @@ -612,8 +604,8 @@ def wrong_loop(x): wrong_loop(8) """, - arcz=".1 .2 23 26 34 4. 3. 6. 18 8.", - arcz_missing="26 3. 6.", + branchz="23 26 34 3.", + branchz_missing="26 3.", ) def test_generator_expression(self) -> None: @@ -625,7 +617,8 @@ def test_generator_expression(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + branchz="34 3.", + branchz_missing="", ) def test_generator_expression_another_way(self) -> None: @@ -640,14 +633,11 @@ def test_generator_expression_another_way(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 25 56 67 75 5.", + branchz="56 5.", + branchz_missing="", ) def test_other_comprehensions(self) -> None: - if env.PYBEHAVIOR.comprehensions_are_functions: - extra_arcz = " -22 2-2" - else: - extra_arcz = "" # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -656,7 +646,8 @@ def test_other_comprehensions(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 12 23 34 45 53 3." + extra_arcz, + branchz="34 3.", + branchz_missing="", ) # Dict comprehension: self.check_coverage("""\ @@ -666,14 +657,11 @@ def test_other_comprehensions(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 12 23 34 45 53 3." + extra_arcz, + branchz="34 3.", + branchz_missing="", ) def test_multiline_dict_comp(self) -> None: - if env.PYBEHAVIOR.comprehensions_are_functions: - extra_arcz = " 2-2" - else: - extra_arcz = "" # Multiline dict comp: self.check_coverage("""\ # comment @@ -688,7 +676,7 @@ def test_multiline_dict_comp(self) -> None: } x = 11 """, - arcz="-22 2B B-2" + extra_arcz, + branchz="", branchz_missing="", ) # Multi dict comp: self.check_coverage("""\ @@ -708,7 +696,7 @@ def test_multiline_dict_comp(self) -> None: } x = 15 """, - arcz="-22 2F F-2" + extra_arcz, + branchz="", branchz_missing="", ) @@ -724,16 +712,10 @@ def test_try_except(self) -> None: b = 5 assert a == 3 and b == 1 """, - arcz=".1 12 23 36 45 56 6.", arcz_missing="45 56", + branchz="", branchz_missing="", ) def test_raise_followed_by_statement(self) -> None: - if env.PYBEHAVIOR.omit_after_jump: - arcz = ".1 12 23 34 46 67 78 8." - arcz_missing = "" - else: - arcz = ".1 12 23 34 46 58 67 78 8." - arcz_missing = "58" self.check_coverage("""\ a, b = 1, 1 try: @@ -744,7 +726,7 @@ def test_raise_followed_by_statement(self) -> None: b = 7 assert a == 3 and b == 7 """, - arcz=arcz, arcz_missing=arcz_missing, + branchz="", branchz_missing="", ) def test_hidden_raise(self) -> None: @@ -761,8 +743,8 @@ def oops(x): b = 10 assert a == 6 and b == 10 """, - arcz=".1 12 -23 34 3-2 4-2 25 56 67 78 8B 9A AB B.", - arcz_missing="3-2 78 8B", arcz_unpredicted="79", + branchz="34 3-2", + branchz_missing="3-2", ) def test_except_with_type(self) -> None: @@ -782,11 +764,10 @@ def try_it(x): assert try_it(0) == 9 # C assert try_it(1) == 7 # D """, - arcz=".1 12 -23 34 3-2 4-2 25 5D DE E. -56 67 78 89 9C AB BC C-5", - arcz_unpredicted="8A", + branchz="34 3-2", + branchz_missing="", ) - @xfail_pypy_3882 def test_try_finally(self) -> None: self.check_coverage("""\ a, c = 1, 1 @@ -796,7 +777,7 @@ def test_try_finally(self) -> None: c = 5 assert a == 3 and c == 5 """, - arcz=".1 12 23 35 56 6.", + branchz="", ) self.check_coverage("""\ a, c, d = 1, 1, 1 @@ -809,8 +790,7 @@ def test_try_finally(self) -> None: d = 8 assert a == 4 and c == 6 and d == 1 # 9 """, - arcz=".1 12 23 34 46 78 89 69 9.", - arcz_missing="78 89", + branchz="", ) self.check_coverage("""\ a, c, d = 1, 1, 1 @@ -825,11 +805,9 @@ def test_try_finally(self) -> None: d = 10 # A assert a == 4 and c == 8 and d == 10 # B """, - arcz=".1 12 23 34 45 58 89 9A AB B.", - arcz_missing="", + branchz="", ) - @xfail_pypy_3882 def test_finally_in_loop(self) -> None: self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 @@ -846,8 +824,8 @@ def test_finally_in_loop(self) -> None: d = 12 # C assert a == 5 and c == 10 and d == 12 # D """, - arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB BC CD D.", - arcz_missing="3D", + branchz="34 3D 67 68", + branchz_missing="3D", ) self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 @@ -864,16 +842,12 @@ def test_finally_in_loop(self) -> None: d = 12 # C assert a == 8 and c == 10 and d == 1 # D """, - arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB BC CD D.", - arcz_missing="67 7A AB BC CD", + branchz="34 3D 67 68", + branchz_missing="67", ) - @xfail_pypy_3882 def test_break_through_finally(self) -> None: - arcz = ".1 12 23 34 3D 45 56 67 68 7A AD 8A A3 BC CD D." - if env.PYBEHAVIOR.finally_jumps_back: - arcz = arcz.replace("AD", "A7 7D") self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -889,8 +863,8 @@ def test_break_through_finally(self) -> None: d = 12 # C assert a == 5 and c == 10 and d == 1 # D """, - arcz=arcz, - arcz_missing="3D BC CD", + branchz="34 3D 67 68", + branchz_missing="3D", ) def test_break_continue_without_finally(self) -> None: @@ -909,15 +883,11 @@ def test_break_continue_without_finally(self) -> None: d = 12 # C assert a == 5 and c == 1 and d == 1 # D """, - arcz=".1 12 23 34 3D 45 56 67 68 7D 83 9A A3 BC CD D.", - arcz_missing="3D 9A A3 BC CD", + branchz="34 3D 67 68", + branchz_missing="3D", ) - @xfail_pypy_3882 def test_continue_through_finally(self) -> None: - arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D." - if env.PYBEHAVIOR.finally_jumps_back: - arcz += " 73 A7" self.check_coverage("""\ a, b, c, d, i = 1, 1, 1, 1, 99 try: @@ -933,8 +903,8 @@ def test_continue_through_finally(self) -> None: d = 12 # C assert (a, b, c, d) == (5, 8, 10, 1) # D """, - arcz=arcz, - arcz_missing="BC CD", + branchz="34 3D 67 68", + branchz_missing="", ) def test_finally_in_loop_bug_92(self) -> None: @@ -947,7 +917,8 @@ def test_finally_in_loop_bug_92(self) -> None: g = 6 h = 7 """, - arcz=".1 12 23 35 56 61 17 7.", + branchz="12 17", + branchz_missing="", ) def test_bug_212(self) -> None: @@ -970,9 +941,8 @@ def b(exc): except: pass """, - arcz=".1 .2 1A 23 34 3. 45 56 67 68 7. 8. AB BC C. DE E.", - arcz_missing="3. C.", - arcz_unpredicted="CD", + branchz="34 3-1 67 68", + branchz_missing="3-1", ) def test_except_finally(self) -> None: @@ -986,7 +956,7 @@ def test_except_finally(self) -> None: c = 7 assert a == 3 and b == 1 and c == 7 """, - arcz=".1 12 23 45 37 57 78 8.", arcz_missing="45 57", + branchz="", ) self.check_coverage("""\ a, b, c = 1, 1, 1 @@ -1002,8 +972,7 @@ def oops(x): c = 11 assert a == 5 and b == 9 and c == 11 """, - arcz=".1 12 -23 3-2 24 45 56 67 7B 89 9B BC C.", - arcz_missing="67 7B", arcz_unpredicted="68", + branchz="", ) def test_multiple_except_clauses(self) -> None: @@ -1019,8 +988,7 @@ def test_multiple_except_clauses(self) -> None: c = 9 assert a == 3 and b == 1 and c == 9 """, - arcz=".1 12 23 45 46 39 59 67 79 9A A.", - arcz_missing="45 59 46 67 79", + branchz="", ) self.check_coverage("""\ a, b, c = 1, 1, 1 @@ -1034,9 +1002,7 @@ def test_multiple_except_clauses(self) -> None: c = 9 assert a == 1 and b == 5 and c == 9 """, - arcz=".1 12 23 45 46 39 59 67 79 9A A.", - arcz_missing="39 46 67 79", - arcz_unpredicted="34", + branchz="", ) self.check_coverage("""\ a, b, c = 1, 1, 1 @@ -1050,9 +1016,7 @@ def test_multiple_except_clauses(self) -> None: c = 9 assert a == 7 and b == 1 and c == 9 """, - arcz=".1 12 23 45 46 39 59 67 79 9A A.", - arcz_missing="39 45 59", - arcz_unpredicted="34", + branchz="", ) self.check_coverage("""\ a, b, c = 1, 1, 1 @@ -1069,15 +1033,10 @@ def test_multiple_except_clauses(self) -> None: pass assert a == 1 and b == 1 and c == 10 """, - arcz=".1 12 23 34 4A 56 6A 57 78 8A AD BC CD D.", - arcz_missing="4A 56 6A 78 8A AD", - arcz_unpredicted="45 7A AB", + branchz="", ) def test_return_finally(self) -> None: - arcz = ".1 12 29 9A AB BC C-1 -23 34 45 7-2 57 38 8-2" - if env.PYBEHAVIOR.finally_jumps_back: - arcz = arcz.replace("7-2", "75 5-2") self.check_coverage("""\ a = [1] def check_token(data): @@ -1092,20 +1051,11 @@ def check_token(data): assert check_token(True) == 5 assert a == [1, 7] """, - arcz=arcz, + branchz="34 38", + branchz_missing="", ) - @xfail_pypy_3882 def test_except_jump_finally(self) -> None: - arcz = ( - ".1 1Q QR RS ST TU U. " + - ".2 23 34 45 56 4O 6L " + - "78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL " + - "LO L4 L. LM " + - "MN NO O." - ) - if env.PYBEHAVIOR.finally_jumps_back: - arcz = arcz.replace("LO", "LA AO").replace("L4", "L4 LD D4").replace("L.", "LG G.") self.check_coverage("""\ def func(x): a = f = g = 2 @@ -1138,22 +1088,11 @@ def func(x): assert func('raise') == (18, 21, 23, 0) # T assert func('other') == (2, 21, 2, 3) # U 30 """, - arcz=arcz, - arcz_missing="6L", - arcz_unpredicted="67", + branchz="45 4O 89 8B BC BE EF EH HI HL", + branchz_missing="", ) - @xfail_pypy_3882 def test_else_jump_finally(self) -> None: - arcz = ( - ".1 1S ST TU UV VW W. " + - ".2 23 34 45 56 6A 78 8N 4Q " + - "AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN " + - "N4 NQ N. NO " + - "OP PQ Q." - ) - if env.PYBEHAVIOR.finally_jumps_back: - arcz = arcz.replace("NQ", "NC CQ").replace("N4", "N4 NF F4").replace("N.", "NI I.") self.check_coverage("""\ def func(x): a = f = g = 2 @@ -1188,9 +1127,8 @@ def func(x): assert func('raise') == (20, 23, 25, 0) # V assert func('other') == (2, 23, 2, 3) # W 32 """, - arcz=arcz, - arcz_missing="78 8N", - arcz_unpredicted="", + branchz="45 4Q AB AD DE DG GH GJ JK JN", + branchz_missing="" ) @@ -1205,7 +1143,8 @@ def gen(inp): list(gen([1,2,3])) """, - arcz=".1 .2 23 2. 32 15 5.", + branchz="23 2-1", + branchz_missing="", ) def test_padded_yield_in_loop(self) -> None: @@ -1220,7 +1159,8 @@ def gen(inp): list(gen([1,2,3])) """, - arcz=".1 19 9. .2 23 34 45 56 63 37 7.", + branchz="34 37", + branchz_missing="", ) def test_bug_308(self) -> None: @@ -1232,7 +1172,8 @@ def run(): for f in run(): print(f()) """, - arcz=".1 15 56 65 5. .2 23 32 2. -33 3-3", + branchz="23 2. 56 5.", + branchz_missing="", ) self.check_coverage("""\ def run(): @@ -1243,7 +1184,8 @@ def run(): for f in run(): print(f()) """, - arcz=".1 16 67 76 6. .2 23 34 43 3. -22 2-2 -44 4-4", + branchz="34 3. 67 6.", + branchz_missing="", ) self.check_coverage("""\ def run(): @@ -1252,7 +1194,8 @@ def run(): for f in run(): print(f()) """, - arcz=".1 14 45 54 4. .2 2. -22 2-2", + branchz="45 4.", + branchz_missing="", ) def test_bug_324(self) -> None: @@ -1266,11 +1209,8 @@ def gen(inp): list(gen([1,2,3])) """, - arcz= - ".1 15 5. " # The module level - ".2 23 32 2. " # The gen() function - "-33 3-3", # The generator expression - arcz_missing="-33 3-3", + branchz="23 2.", + branchz_missing="", ) def test_coroutines(self) -> None: @@ -1287,8 +1227,8 @@ def double_inputs(): next(gen) print(gen.send(6)) """, - arcz=".1 17 78 89 9A AB B. .2 23 34 45 52 2.", - arcz_missing="2.", + branchz="23 2-1", + branchz_missing="2-1", ) assert self.stdout() == "20\n12\n" @@ -1304,7 +1244,8 @@ def gen(inp): list(gen([1,2,3])) """, - arcz=".1 19 9. .2 23 34 45 56 63 37 7.", + branchz="34 37", + branchz_missing="", ) def test_abandoned_yield(self) -> None: @@ -1319,8 +1260,8 @@ def gen(): """, lines=[1, 2, 3, 4, 6], missing="4", - arcz=".1 16 6. .2 23 34 4.", - arcz_missing="34 4.", + branchz="", + branchz_missing="", ) assert self.stdout() == "2\n3\n" @@ -1340,7 +1281,8 @@ def test_match_case_with_default(self) -> None: match = "default" print(match) """, - arcz=".1 12 23 34 49 35 56 69 57 78 89 91 1.", + branchz="12 1-1 34 35 56 57", + branchz_missing="", ) assert self.stdout() == "default\nno go\ngo: n\n" @@ -1356,7 +1298,8 @@ def test_match_case_with_wildcard(self) -> None: match = f"default: {x}" print(match) """, - arcz=".1 12 23 34 49 35 56 69 57 78 89 91 1.", + branchz="12 1-1 34 35 56 57", + branchz_missing="", ) assert self.stdout() == "default: ['huh']\nno go\ngo: n\n" @@ -1371,7 +1314,8 @@ def test_match_case_without_wildcard(self) -> None: match = "no go" print(match) """, - arcz=".1 12 23 34 45 58 46 78 67 68 82 2.", + branchz="23 2-1 45 46 67 68", + branchz_missing="", ) assert self.stdout() == "None\nno go\ngo: n\n" @@ -1384,8 +1328,9 @@ def absurd(x): print("default") absurd(5) """, - # No arc from 3 to 5 because 3 always matches. - arcz=".1 15 5. .2 23 34 4.", + # No branches because 3 always matches. + branchz="", + branchz_missing="", ) assert self.stdout() == "default\n" self.check_coverage("""\ @@ -1395,8 +1340,8 @@ def absurd(x): print("not default") absurd(5) """, - arcz=".1 15 5. .2 23 34 3. 4.", - arcz_missing="34 4.", + branchz="34 3-1", + branchz_missing="34", ) assert self.stdout() == "" self.check_coverage("""\ @@ -1408,8 +1353,8 @@ def absurd(x): print("also not default") absurd(7) """, - arcz=".1 17 7. .2 23 34 4. 35 56 5. 6.", - arcz_missing="34 4. 5.", + branchz="34 35 56 5-1", + branchz_missing="34 5-1", ) assert self.stdout() == "also not default\n" self.check_coverage("""\ @@ -1421,8 +1366,8 @@ def absurd(x): print("also not default") absurd(7) """, - arcz=".1 17 7. .2 23 34 4. 35 56 5. 6.", - arcz_missing="34 4. 5.", + branchz="34 35 56 5-1", + branchz_missing="34 5-1", ) assert self.stdout() == "also not default\n" @@ -1433,15 +1378,15 @@ class OptimizedIfTest(CoverageTest): def test_optimized_away_if_0(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3, 4, 8, 9] - arcz = ".1 12 23 24 34 48 49 89 9." - arcz_missing = "24" # 49 isn't missing because line 4 is matched by the default partial # exclusion regex, and no branches are considered missing if they # start from an excluded line. + branchz = "23 24 48 49" + branchz_missing = "24" else: lines = [1, 2, 3, 8, 9] - arcz = ".1 12 23 28 38 89 9." - arcz_missing = "28" + branchz = "23 28" + branchz_missing = "28" self.check_coverage("""\ a = 1 @@ -1455,22 +1400,22 @@ def test_optimized_away_if_0(self) -> None: f = 9 """, lines=lines, - arcz=arcz, - arcz_missing=arcz_missing, + branchz=branchz, + branchz_missing=branchz_missing, ) def test_optimized_away_if_1(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3, 4, 5, 6, 9] - arcz = ".1 12 23 24 34 45 49 56 69 59 9." - arcz_missing = "24 59" # 49 isn't missing because line 4 is matched by the default partial # exclusion regex, and no branches are considered missing if they # start from an excluded line. + branchz = "23 24 45 49 56 59" + branchz_missing = "24 59" else: lines = [1, 2, 3, 5, 6, 9] - arcz = ".1 12 23 25 35 56 69 59 9." - arcz_missing = "25 59" + branchz = "23 25 56 59" + branchz_missing = "25 59" self.check_coverage("""\ a = 1 @@ -1484,22 +1429,22 @@ def test_optimized_away_if_1(self) -> None: f = 9 """, lines=lines, - arcz=arcz, - arcz_missing=arcz_missing, + branchz=branchz, + branchz_missing=branchz_missing, ) def test_optimized_away_if_1_no_else(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 3, 4, 5] - arcz = ".1 12 23 25 34 45 5." - arcz_missing = "" # 25 isn't missing because line 2 is matched by the default partial # exclusion regex, and no branches are considered missing if they # start from an excluded line. + branchz = "23 25" + branchz_missing = "" else: lines = [1, 3, 4, 5] - arcz = ".1 13 34 45 5." - arcz_missing = "" + branchz = "" + branchz_missing = "" self.check_coverage("""\ a = 1 if 1: @@ -1508,22 +1453,22 @@ def test_optimized_away_if_1_no_else(self) -> None: d = 5 """, lines=lines, - arcz=arcz, - arcz_missing=arcz_missing, + branchz=branchz, + branchz_missing=branchz_missing, ) def test_optimized_if_nested(self) -> None: if env.PYBEHAVIOR.keep_constant_test: lines = [1, 2, 8, 11, 12, 13, 14, 15] - arcz = ".1 12 28 2F 8B 8F BC CD DE EF F." - arcz_missing = "" + branchz = "28 2F 8B 8F" + branchz_missing = "" # 2F and 8F aren't missing because they're matched by the default # partial exclusion regex, and no branches are considered missing # if they start from an excluded line. else: lines = [1, 12, 14, 15] - arcz = ".1 1C CE EF F." - arcz_missing = "" + branchz = "" + branchz_missing = "" self.check_coverage("""\ a = 1 @@ -1543,8 +1488,8 @@ def test_optimized_if_nested(self) -> None: i = 15 """, lines=lines, - arcz=arcz, - arcz_missing=arcz_missing, + branchz=branchz, + branchz_missing=branchz_missing, ) def test_dunder_debug(self) -> None: @@ -1565,11 +1510,11 @@ def test_dunder_debug(self) -> None: def test_if_debug(self) -> None: if env.PYBEHAVIOR.optimize_if_debug: - arcz = ".1 12 24 41 26 61 1." - arcz_missing = "" + branchz = "12 1. 24 26" + branchz_missing = "" else: - arcz = ".1 12 23 31 34 41 26 61 1." - arcz_missing = "31" + branchz = "12 23 31 34 26 1." + branchz_missing = "31" self.check_coverage("""\ for value in [True, False]: if value: @@ -1578,19 +1523,22 @@ def test_if_debug(self) -> None: else: x = 6 """, - arcz=arcz, - arcz_missing=arcz_missing, + branchz=branchz, + branchz_missing=branchz_missing, ) - @xfail_pypy_3882 + @pytest.mark.xfail( + env.PYPY and env.PYVERSION[:2] == (3, 8) and env.PYPYVERSION >= (7, 3, 11), + reason="https://foss.heptapod.net/pypy/pypy/-/issues/3882", + ) def test_if_not_debug(self) -> None: if env.PYBEHAVIOR.optimize_if_not_debug == 1: - arcz = ".1 12 23 34 42 37 72 28 8." + branchz = "23 28 34 37" elif env.PYBEHAVIOR.optimize_if_not_debug == 2: - arcz = ".1 12 23 35 52 37 72 28 8." + branchz = "23 28 35 37" else: assert env.PYBEHAVIOR.optimize_if_not_debug == 3 - arcz = ".1 12 23 32 37 72 28 8." + branchz = "23 28 32 37" self.check_coverage("""\ lines = set() @@ -1602,7 +1550,7 @@ def test_if_not_debug(self) -> None: lines.add(7) assert lines == set([7]) """, - arcz=arcz, + branchz=branchz, ) @@ -1621,7 +1569,7 @@ def test_dict_literal(self) -> None: } assert d """, - arcz=".1 19 9.", + branchz="", branchz_missing="", ) self.check_coverage("""\ d = \\ @@ -1634,7 +1582,7 @@ def test_dict_literal(self) -> None: } assert d """, - arcz=".1 19 9.", + branchz="", branchz_missing="", ) def test_unpacked_literals(self) -> None: @@ -1650,7 +1598,7 @@ def test_unpacked_literals(self) -> None: } assert weird['b'] == 3 """, - arcz=".1 15 5A A.", + branchz="", branchz_missing="", ) self.check_coverage("""\ l = [ @@ -1664,7 +1612,7 @@ def test_unpacked_literals(self) -> None: ] assert weird[1] == 3 """, - arcz=".1 15 5A A.", + branchz="", branchz_missing="", ) @pytest.mark.parametrize("n", [10, 50, 100, 500, 1000, 2000, 10000]) @@ -1684,7 +1632,7 @@ def test_pathologically_long_code_object(self, n: int) -> None: print(len(data)) """ - self.check_coverage(code, arcs=[(-1, 1), (1, 2*n+4), (2*n+4, -1)]) + self.check_coverage(code, branchz="") assert self.stdout() == f"{n}\n" def test_partial_generators(self) -> None: @@ -1692,7 +1640,7 @@ def test_partial_generators(self) -> None: # Line 2 is executed completely. # Line 3 is started but not finished, because zip ends before it finishes. # Line 4 is never started. - cov = self.check_coverage("""\ + self.check_coverage("""\ def f(a, b): c = (i for i in a) # 2 d = (j for j in b) # 3 @@ -1701,26 +1649,15 @@ def f(a, b): f(['a', 'b'], [1, 2, 3]) """, - arcz=".1 17 7. .2 23 34 45 5. -22 2-2 -33 3-3 -44 4-4", - arcz_missing="3-3 -44 4-4", + branchz="", + branchz_missing="", ) - expected = "line 3 didn't finish the generator expression on line 3" - assert self.get_missing_arc_description(cov, 3, -3) == expected - expected = "line 4 didn't run the generator expression on line 4" - assert self.get_missing_arc_description(cov, 4, -4) == expected class DecoratorArcTest(CoverageTest): """Tests of arcs with decorators.""" def test_function_decorator(self) -> None: - arcz = ( - ".1 16 67 7A AE EF F. " # main line - ".2 24 4. -23 3-2 " # decorators - "-6D D-6 " # my_function - ) - if env.PYBEHAVIOR.trace_decorator_line_again: - arcz += "A7 76 6A " self.check_coverage("""\ def decorator(arg): def _dec(f): @@ -1738,18 +1675,11 @@ def my_function( a = 14 my_function() """, - arcz=arcz, + branchz="", branchz_missing="", ) @xfail_pypy38 def test_class_decorator(self) -> None: - arcz = ( - ".1 16 67 6D 7A AE E. " # main line - ".2 24 4. -23 3-2 " # decorators - "-66 D-6 " # MyObject - ) - if env.PYBEHAVIOR.trace_decorator_line_again: - arcz += "A7 76 6A " self.check_coverage("""\ def decorator(arg): def _dec(c): @@ -1766,15 +1696,12 @@ class MyObject( X = 13 a = 14 """, - arcz=arcz, + branchz="", branchz_missing="", ) def test_bug_466a(self) -> None: # A bad interaction between decorators and multi-line list assignments, # believe it or not...! - arcz = ".1 1A A. 13 34 4. -35 58 8-3 " - if env.PYBEHAVIOR.trace_decorator_line_again: - arcz += "43 " # This example makes more sense when considered in tandem with 466b below. self.check_coverage("""\ class Parser(object): @@ -1788,15 +1715,12 @@ def parse(cls): Parser.parse() """, - arcz=arcz, + branchz="", branchz_missing="", ) def test_bug_466b(self) -> None: # A bad interaction between decorators and multi-line list assignments, # believe it or not...! - arcz = ".1 1A A. 13 34 4. -35 58 8-3 " - if env.PYBEHAVIOR.trace_decorator_line_again: - arcz += "43 " self.check_coverage("""\ class Parser(object): @@ -1809,7 +1733,7 @@ def parse(cls): Parser.parse() """, - arcz=arcz, + branchz="", branchz_missing="", ) @@ -1823,7 +1747,8 @@ def test_multiline_lambda(self) -> None: ) assert fn(4) == 6 """, - arcz=".1 14 4-1 1-1", + branchz="", + branchz_missing="", ) self.check_coverage("""\ @@ -1837,7 +1762,8 @@ def test_multiline_lambda(self) -> None: ) assert fn(10) == 18 """, - arcz="-22 2A A-2 2-2", + branchz="", + branchz_missing="", ) def test_unused_lambdas_are_confusing_bug_90(self) -> None: @@ -1846,7 +1772,7 @@ def test_unused_lambdas_are_confusing_bug_90(self) -> None: fn = lambda x: x b = 3 """, - arcz=".1 12 -22 2-2 23 3.", arcz_missing="-22 2-2", + branchz="", branchz_missing="", ) def test_raise_with_lambda_looks_like_partial_branch(self) -> None: @@ -1865,9 +1791,8 @@ def ouch(fn): """, lines=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], missing="6-7", - arcz=".1 13 34 45 56 67 6A 7A 89 9A AB B. .2 2. -55 5-5", - arcz_missing="56 67 6A 7A -55 5-5", - arcz_unpredicted="58", + branchz="67 6A", + branchz_missing="67 6A", ) def test_lambda_in_dict(self) -> None: @@ -1885,7 +1810,8 @@ def test_lambda_in_dict(self) -> None: if k & 1: v() """, - arcz=".1 12 23 3A AB BC BA CA A. -33 3-3", + branchz="AB A. BA BC", + branchz_missing="", ) @@ -1923,10 +1849,7 @@ async def print_sum(x, y): # 8 loop.run_until_complete(print_sum(1, 2)) loop.close() # G """, - arcz= - ".1 13 38 8E EF FG G. " + - "-34 45 56 6-3 " + - "-89 9C C-8", + branchz="", branchz_missing="", ) assert self.stdout() == "Compute 1 + 2 ...\n1 + 2 = 3\n" @@ -1957,37 +1880,22 @@ async def doit(): # G loop.run_until_complete(doit()) loop.close() """, - arcz= - ".1 13 3G GL LM MN N. " # module main line - "-33 34 47 7A A-3 " # class definition - "-GH HI IH HJ J-G " # doit - "-45 5-4 " # __init__ - "-78 8-7 " # __aiter__ - "-AB BC C-A DE E-A ", # __anext__ - arcz_unpredicted="CD", + branchz="HI HJ", + branchz_missing="", ) assert self.stdout() == "a\nb\nc\n.\n" def test_async_with(self) -> None: - if env.PYBEHAVIOR.exit_through_with: - arcz = ".1 1. .2 23 32 2." - arcz_missing = ".2 23 32 2." - else: - arcz = ".1 1. .2 23 3." - arcz_missing = ".2 23 3." self.check_coverage("""\ async def go(): async with x: pass """, - arcz=arcz, - arcz_missing=arcz_missing, + branchz="", + branchz_missing="", ) def test_async_decorator(self) -> None: - arcz = ".1 14 45 5. .2 2. -46 6-4 " - if env.PYBEHAVIOR.trace_decorator_line_again: - arcz += "54 " self.check_coverage("""\ def wrap(f): # 1 return f @@ -1996,8 +1904,8 @@ def wrap(f): # 1 async def go(): return """, - arcz=arcz, - arcz_missing='-46 6-4', + branchz="", + branchz_missing="", ) # https://github.com/nedbat/coveragepy/issues/1158 @@ -2021,7 +1929,8 @@ async def async_test(): asyncio.run(async_test()) assert a == 12 """, - arcz=".1 13 36 6E EF F. -34 4-3 -68 89 9A 9C A9 C-6", + branchz="9A 9C", + branchz_missing="", ) assert self.stdout() == "14\n" @@ -2041,7 +1950,8 @@ async def async_test(): asyncio.run(async_test()) """, - arcz=".1 13 36 6A A. -34 4-3 -67 78 87 7-6", + branchz="78 7-6", + branchz_missing="", ) assert self.stdout() == "12\n" @@ -2062,8 +1972,8 @@ def func(): T, F = (lambda _: True), (lambda _: False) func() """, - arcz=".1 1C CD D. .2 23 29 34 38 45 4. 56 5. 6. 8. 9. 9A A. -CC C-C", - arcz_missing="29 38 45 56 5. 6. 8. 9. 9A A.", + branchz="23 29 34 38 45 4. 56 5. 9A 9.", + branchz_missing="29 38 45 56 5. 9A 9.", ) @@ -2077,7 +1987,7 @@ def f(x:str, y:int) -> str: return f"{x}, {y}, {a}, 3" print(f("x", 4)) """, - arcz=".1 .2 23 3. 14 4.", + branchz="", branchz_missing="", ) assert self.stdout() == "x, 4, 2, 3\n" @@ -2099,7 +2009,8 @@ def test_default(self) -> None: f = 9 """, [1,2,3,4,5,6,7,8,9], - arcz=".1 12 23 24 34 45 56 57 67 78 89 9. 8.", + branchz="23 24 56 57 89 8.", + branchz_missing="", ) def test_custom_pragmas(self) -> None: @@ -2112,7 +2023,7 @@ def test_custom_pragmas(self) -> None: """, [1,2,3,4,5], partials=["only some"], - arcz=".1 12 23 34 45 25 5.", + branchz="23 25", branchz_missing="", ) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index d50971120..16c5ebfc9 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -1170,8 +1170,7 @@ def test_try_except(self) -> None: assert a == 123 """, [1,2,3,4,5,7,8], "4-5", - arcz=".1 12 23 45 58 37 78 8.", - arcz_missing="45 58", + branchz="", branchz_missing="", ) def test_try_except_stranded_else(self) -> None: @@ -1179,13 +1178,9 @@ def test_try_except_stranded_else(self) -> None: # The else can't be reached because the try ends with a raise. lines = [1,2,3,4,5,6,9] missing = "" - arcz = ".1 12 23 34 45 56 69 9." - arcz_missing = "" else: lines = [1,2,3,4,5,6,8,9] missing = "8" - arcz = ".1 12 23 34 45 56 69 89 9." - arcz_missing = "89" self.check_coverage("""\ a = 0 try: @@ -1199,8 +1194,7 @@ def test_try_except_stranded_else(self) -> None: """, lines=lines, missing=missing, - arcz=arcz, - arcz_missing=arcz_missing, + branchz="", branchz_missing="", ) def test_try_finally(self) -> None: @@ -1275,7 +1269,6 @@ def foo( ) def test_class_def(self) -> None: - arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A" self.check_coverage("""\ # A comment. class theClass: @@ -1293,7 +1286,7 @@ def foo(self): assert x == 1 """, [2, 6, 8, 10, 11, 13, 14], "", - arcz=arcz, + branchz="", branchz_missing="", ) @@ -1358,18 +1351,10 @@ def test_excluding_try_except(self) -> None: assert a == 123 """, [1,2,3,7,8], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 37 45 58 78 8.", - arcz_missing="58", + branchz="", branchz_missing="", ) def test_excluding_try_except_stranded_else(self) -> None: - if env.PYBEHAVIOR.optimize_unreachable_try_else: - # The else can't be reached because the try ends with a raise. - arcz = ".1 12 23 34 45 56 69 9." - arcz_missing = "" - else: - arcz = ".1 12 23 34 45 56 69 89 9." - arcz_missing = "89" self.check_coverage("""\ a = 0 try: @@ -1382,8 +1367,7 @@ def test_excluding_try_except_stranded_else(self) -> None: assert a == 99 """, [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], - arcz=arcz, - arcz_missing=arcz_missing, + branchz="", branchz_missing="", ) def test_excluded_comprehension_branches(self) -> None: @@ -1396,8 +1380,8 @@ def test_excluded_comprehension_branches(self) -> None: raise NotImplementedError # pragma: NO COVER """, [1,2,4], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 24 45 4. -44 4-4", - arcz_missing="4-4", + branchz="23 24 45 4.", + branchz_missing="", ) @@ -1515,7 +1499,7 @@ def test_try_except_finally(self) -> None: assert a == 1 and b == 2 """, [1,2,3,4,5,7,8], "4-5", - arcz=".1 12 23 37 45 57 78 8.", arcz_missing="45 57", + branchz="", branchz_missing="", ) self.check_coverage("""\ a = 0; b = 0 @@ -1529,7 +1513,7 @@ def test_try_except_finally(self) -> None: assert a == 99 and b == 2 """, [1,2,3,4,5,6,8,9], "", - arcz=".1 12 23 34 45 56 68 89 9.", + branchz="", branchz_missing="", ) self.check_coverage("""\ a = 0; b = 0 @@ -1545,7 +1529,7 @@ def test_try_except_finally(self) -> None: assert a == 123 and b == 2 """, [1,2,3,4,5,6,7,8,10,11], "6", - arcz=".1 12 23 34 45 56 57 78 6A 8A AB B.", arcz_missing="56 6A", + branchz="", branchz_missing="", ) self.check_coverage("""\ a = 0; b = 0 @@ -1563,8 +1547,7 @@ def test_try_except_finally(self) -> None: assert a == 17 and b == 2 """, [1,2,3,4,5,6,7,8,9,10,12,13], "6, 9-10", - arcz=".1 12 23 34 45 56 6C 57 78 8C 79 9A AC CD D.", - arcz_missing="56 6C 79 9A AC", + branchz="", branchz_missing="", ) self.check_coverage("""\ a = 0; b = 0 @@ -1579,8 +1562,7 @@ def test_try_except_finally(self) -> None: assert a == 123 and b == 2 """, [1,2,3,4,5,7,9,10], "4-5", - arcz=".1 12 23 37 45 59 79 9A A.", - arcz_missing="45 59", + branchz="", branchz_missing="", ) def test_try_except_finally_stranded_else(self) -> None: @@ -1588,13 +1570,9 @@ def test_try_except_finally_stranded_else(self) -> None: # The else can't be reached because the try ends with a raise. lines = [1,2,3,4,5,6,10,11] missing = "" - arcz = ".1 12 23 34 45 56 6A AB B." - arcz_missing = "" else: lines = [1,2,3,4,5,6,8,10,11] missing = "8" - arcz = ".1 12 23 34 45 56 6A 8A AB B." - arcz_missing = "8A" self.check_coverage("""\ a = 0; b = 0 try: @@ -1610,8 +1588,7 @@ def test_try_except_finally_stranded_else(self) -> None: """, lines=lines, missing=missing, - arcz=arcz, - arcz_missing=arcz_missing, + branchz="", branchz_missing="", ) diff --git a/tests/test_json.py b/tests/test_json.py index 0b6f32483..0ccfd84c0 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -32,13 +32,13 @@ def _assert_expected_json_report( self.make_file("a.py", """\ a = {'b': 1} if a.get('a'): - b = 1 + b = 3 elif a.get('b'): - b = 2 + b = 5 else: - b = 3 + b = 7 if not a: - b = 4 + b = 9 """) self._compare_json_reports(cov, expected_result, "a") diff --git a/tests/test_parser.py b/tests/test_parser.py index c19e378cb..4eeea5870 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -50,22 +50,6 @@ class Bar: 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1, } - def test_generator_exit_counts(self) -> None: - # https://github.com/nedbat/coveragepy/issues/324 - parser = self.parse_text("""\ - def gen(input): - for n in inp: - yield (i * 2 for i in range(n)) - - list(gen([1,2,3])) - """) - assert parser.exit_counts() == { - 1:1, # def -> list - 2:2, # for -> yield; for -> exit - 3:2, # yield -> for; genexp exit - 5:1, # list -> exit - } - def test_try_except(self) -> None: parser = self.parse_text("""\ try: @@ -79,7 +63,7 @@ def test_try_except(self) -> None: b = 9 """) assert parser.exit_counts() == { - 1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1, + 1: 1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1, } def test_excluded_classes(self) -> None: @@ -92,9 +76,7 @@ def __init__(self): class Bar: pass """) - assert parser.exit_counts() == { - 1:0, 2:1, 3:1, - } + assert parser.exit_counts() == { 2:1, 3:1 } def test_missing_branch_to_excluded_code(self) -> None: parser = self.parse_text("""\ @@ -157,18 +139,14 @@ def bar(self): """) expected_statements = {1, 2, 4, 5, 8, 9, 10} - expected_arcs = set(arcz_to_arcs(".1 14 45 58 89 9. .2 2. -8A A-8")) + expected_arcs = set(arcz_to_arcs("14 45 58 89 9. 2. A-8")) expected_exits = {1: 1, 2: 1, 4: 1, 5: 1, 8: 1, 9: 1, 10: 1} if env.PYBEHAVIOR.docstring_only_function: # 3.7 changed how functions with only docstrings are numbered. - expected_arcs.update(set(arcz_to_arcs("-46 6-4"))) + expected_arcs.update(set(arcz_to_arcs("6-4"))) expected_exits.update({6: 1}) - if env.PYBEHAVIOR.trace_decorator_line_again: - expected_arcs.update(set(arcz_to_arcs("54 98"))) - expected_exits.update({9: 2, 5: 2}) - assert expected_statements == parser.statements assert expected_arcs == parser.arcs() assert expected_exits == parser.exit_counts() @@ -1016,26 +994,6 @@ def func10(): ) assert expected == parser.missing_arc_description(11, 13) - def test_missing_arc_descriptions_for_small_callables(self) -> None: - parser = self.parse_text("""\ - callables = [ - lambda: 2, - (x for x in range(3)), - {x:1 for x in range(4)}, - {x for x in range(5)}, - ] - x = 7 - """) - expected = "line 2 didn't finish the lambda on line 2" - assert expected == parser.missing_arc_description(2, -2) - expected = "line 3 didn't finish the generator expression on line 3" - assert expected == parser.missing_arc_description(3, -3) - if env.PYBEHAVIOR.comprehensions_are_functions: - expected = "line 4 didn't finish the dictionary comprehension on line 4" - assert expected == parser.missing_arc_description(4, -4) - expected = "line 5 didn't finish the set comprehension on line 5" - assert expected == parser.missing_arc_description(5, -5) - def test_missing_arc_descriptions_for_exceptions(self) -> None: parser = self.parse_text("""\ try: @@ -1056,91 +1014,6 @@ def test_missing_arc_descriptions_for_exceptions(self) -> None: ) assert expected == parser.missing_arc_description(5, 6) - def test_missing_arc_descriptions_for_finally(self) -> None: - parser = self.parse_text("""\ - def function(): - for i in range(2): - try: - if something(4): - break - elif something(6): - x = 7 - else: - if something(9): - continue - else: - continue - if also_this(13): - return 14 - else: - raise Exception(16) - finally: - this_thing(18) - that_thing(19) - """) - if env.PYBEHAVIOR.finally_jumps_back: - expected = "line 18 didn't jump to line 5 because the break on line 5 wasn't executed" - assert expected == parser.missing_arc_description(18, 5) - expected = "line 5 didn't jump to line 19 because the break on line 5 wasn't executed" - assert expected == parser.missing_arc_description(5, 19) - expected = ( - "line 18 didn't jump to line 10 " + - "because the continue on line 10 wasn't executed" - ) - assert expected == parser.missing_arc_description(18, 10) - expected = ( - "line 10 didn't jump to line 2 " + - "because the continue on line 10 wasn't executed" - ) - assert expected == parser.missing_arc_description(10, 2) - expected = ( - "line 18 didn't jump to line 14 " + - "because the return on line 14 wasn't executed" - ) - assert expected == parser.missing_arc_description(18, 14) - expected = ( - "line 14 didn't return from function 'function' " + - "because the return on line 14 wasn't executed" - ) - assert expected == parser.missing_arc_description(14, -1) - expected = ( - "line 18 didn't except from function 'function' " + - "because the raise on line 16 wasn't executed" - ) - assert expected == parser.missing_arc_description(18, -1) - else: - expected = ( - "line 18 didn't jump to line 19 " + - "because the break on line 5 wasn't executed" - ) - assert expected == parser.missing_arc_description(18, 19) - expected = ( - "line 18 didn't jump to line 2 " + - "because the continue on line 10 wasn't executed" + - " or " + - "the continue on line 12 wasn't executed" - ) - assert expected == parser.missing_arc_description(18, 2) - expected = ( - "line 18 didn't except from function 'function' " + - "because the raise on line 16 wasn't executed" + - " or " + - "line 18 didn't return from function 'function' " + - "because the return on line 14 wasn't executed" - ) - assert expected == parser.missing_arc_description(18, -1) - - def test_missing_arc_descriptions_bug460(self) -> None: - parser = self.parse_text("""\ - x = 1 - d = { - 3: lambda: [], - 4: lambda: [], - } - x = 6 - """) - assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3" - @pytest.mark.skipif(not env.PYBEHAVIOR.match_case, reason="Match-case is new in 3.10") class MatchCaseMissingArcDescriptionTest(PythonParserTestBase): diff --git a/tests/test_process.py b/tests/test_process.py index 27d01c0f0..891bc3750 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -924,6 +924,9 @@ def excepthook(*args): # executed. data = coverage.CoverageData() data.read() + print(f"{line_counts(data) = }") + print(f"{data = }") + print("data.lines excepthook.py:", data.lines(os.path.abspath('excepthook.py'))) assert line_counts(data)['excepthook.py'] == 7 @pytest.mark.skipif(not env.CPYTHON, diff --git a/tests/test_testing.py b/tests/test_testing.py index 933384b4d..c673a6410 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -22,7 +22,7 @@ from tests.coveragetest import CoverageTest from tests.helpers import ( CheckUniqueFilenames, FailingProxy, - arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings, + arcz_to_arcs, assert_count_equal, assert_coverage_warnings, re_lines, re_lines_text, re_line, ) @@ -265,38 +265,34 @@ def oops(x): b = 10 assert a == 6 and b == 10 """ - ARCZ = ".1 12 -23 34 3-2 4-2 25 56 67 78 8B 9A AB B." - ARCZ_MISSING = "3-2 78 8B" - ARCZ_UNPREDICTED = "79" + BRANCHZ = "34 3-2" + BRANCHZ_MISSING = "3-2" - def test_check_coverage_possible(self) -> None: - msg = r"(?s)Possible arcs differ: .*- \(6, 3\).*\+ \(6, 7\)" - with pytest.raises(AssertionError, match=msg): + def test_check_coverage_possible_branches(self) -> None: + msg = "Wrong possible branches: [(7, -2), (7, 4)] != [(3, -2), (3, 4)]" + with pytest.raises(AssertionError, match=re.escape(msg)): self.check_coverage( self.CODE, - arcz=self.ARCZ.replace("7", "3"), - arcz_missing=self.ARCZ_MISSING, - arcz_unpredicted=self.ARCZ_UNPREDICTED, + branchz=self.BRANCHZ.replace("3", "7"), + branchz_missing=self.BRANCHZ_MISSING, ) - def test_check_coverage_missing(self) -> None: - msg = r"(?s)Missing arcs differ: .*- \(3, 8\).*\+ \(7, 8\)" - with pytest.raises(AssertionError, match=msg): + def test_check_coverage_missing_branches(self) -> None: + msg = "Wrong missing branches: [(3, 4)] != [(3, -2)]" + with pytest.raises(AssertionError, match=re.escape(msg)): self.check_coverage( self.CODE, - arcz=self.ARCZ, - arcz_missing=self.ARCZ_MISSING.replace("7", "3"), - arcz_unpredicted=self.ARCZ_UNPREDICTED, + branchz=self.BRANCHZ, + branchz_missing="34", ) - def test_check_coverage_unpredicted(self) -> None: - msg = r"(?s)Unpredicted arcs differ: .*- \(3, 9\).*\+ \(7, 9\)" - with pytest.raises(AssertionError, match=msg): + def test_check_coverage_mismatched_missing_branches(self) -> None: + msg = "branches_missing = [(1, 2)], has non-branches in it." + with pytest.raises(AssertionError, match=re.escape(msg)): self.check_coverage( self.CODE, - arcz=self.ARCZ, - arcz_missing=self.ARCZ_MISSING, - arcz_unpredicted=self.ARCZ_UNPREDICTED.replace("7", "3"), + branchz=self.BRANCHZ, + branchz_missing="12", ) @@ -378,23 +374,6 @@ class ArczTest(CoverageTest): def test_arcz_to_arcs(self, arcz: str, arcs: list[TArc]) -> None: assert arcz_to_arcs(arcz) == arcs - @pytest.mark.parametrize("arcs, arcz_repr", [ - ([(-1, 1), (1, 2), (2, -1)], "(-1, 1) # .1\n(1, 2) # 12\n(2, -1) # 2.\n"), - ([(-1, 1), (1, 2), (2, -5)], "(-1, 1) # .1\n(1, 2) # 12\n(2, -5) # 2-5\n"), - ([(-26, 10), (12, 11), (18, 29), (35, -10), (1, 33), (100, 7)], - ( - "(-26, 10) # -QA\n" + - "(12, 11) # CB\n" + - "(18, 29) # IT\n" + - "(35, -10) # Z-A\n" + - "(1, 33) # 1X\n" + - "(100, 7) # ?7\n" - ), - ), - ]) - def test_arcs_to_arcz_repr(self, arcs: list[TArc], arcz_repr: str) -> None: - assert arcs_to_arcz_repr(arcs) == arcz_repr - class AssertCoverageWarningsTest(CoverageTest): """Tests of assert_coverage_warnings""" diff --git a/tests/test_xml.py b/tests/test_xml.py index ad915380a..f105035d6 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -332,7 +332,7 @@ def test_no_duplicate_packages(self) -> None: # problem occurs when they are dynamically generated during xml report cov = coverage.Coverage() with cov.collect(): - import_local_file("foo", "namespace/package/__init__.py") + import_local_file("namespace.package", "namespace/package/__init__.py") cov.xml_report()