nedbat / coveragepy

The code coverage tool for Python
https://coverage.readthedocs.io
Apache License 2.0
3.03k stars 435 forks source link

False positive partial branch when capturing the match-case wildcard statement #1860

Closed moritz89 closed 1 month ago

moritz89 commented 1 month ago

Describe the bug Two equivalent function using the match/case construct with two different ways to use wildcard pattern are reported differently, with the "capture in default / wildcard" case marked as missing branch.

To Reproduce Here is a minimal reproducible example, note testing is done pairwise to verify the functions as equivalent.

import os

import pytest

def wildcard_alone() -> str:

    match os.getenv("SOME_VAR", "default"):
        case "dev":
            return "development value"
        case "test" | "prod":
            return "production value"
        case _:
            return "default value"

def wildcard_or() -> str:

    match os.getenv("SOME_VAR", "default"):
        case "dev":
            return "development value"
        case "test" | "prod":
            return "production value"
        case _ as value:  # marked as partial branch
            return "default value"

@pytest.mark.parametrize("f", [wildcard_or, wildcard_alone])
def test_match_env_dev(f, monkeypatch):
    monkeypatch.setenv("SOME_VAR", "dev")
    assert f() == "development value"

@pytest.mark.parametrize("f", [wildcard_or, wildcard_alone])
def test_match_env_test(f, monkeypatch):
    monkeypatch.setenv("SOME_VAR", "test")
    assert f() == "production value"

@pytest.mark.parametrize("f", [wildcard_or, wildcard_alone])
def test_match_env_prod(f, monkeypatch):
    monkeypatch.setenv("SOME_VAR", "prod")
    assert f() == "production value"

@pytest.mark.parametrize("f", [wildcard_or, wildcard_alone])
def test_match_env_unset(f, monkeypatch):
    monkeypatch.delenv("SOME_VAR", raising=False)
    assert f() == "default value"

@pytest.mark.parametrize("f", [wildcard_or, wildcard_alone])
def test_match_env_other(f, monkeypatch):
    monkeypatch.setenv("SOME_VAR", "unmatched")
    assert f() == "default value"

Here is the coverage.

============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-8.3.3, pluggy-1.5.0
rootdir: /home/moritz/Sandbox/testing
collected 10 items                                                             

coverage_test.py ..........                                              [100%]

============================== 10 passed in 0.03s ==============================
Name               Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------------
coverage_test.py      38      0     10      1    98%   24->exit
--------------------------------------------------------------
TOTAL                 38      0     10      1    98%

And finally, the output of coverage debug sys.

-- sys -------------------------------------------------------
               coverage_version: 7.6.1
                coverage_module: /home/moritz/.virtualenvs/testing-qQMgLcoQ/lib/python3.10/site-packages/coverage/__init__.py
                           core: -none-
                        CTracer: available
           plugins.file_tracers: -none-
            plugins.configurers: -none-
      plugins.context_switchers: -none-
              configs_attempted: /home/moritz/Sandbox/testing/.coveragerc
                                 /home/moritz/Sandbox/testing/setup.cfg
                                 /home/moritz/Sandbox/testing/tox.ini
                                 /home/moritz/Sandbox/testing/pyproject.toml
                   configs_read: -none-
                    config_file: None
                config_contents: -none-
                      data_file: -none-
                         python: 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0]
                       platform: Linux-6.8.0-40-generic-x86_64-with-glibc2.35
                 implementation: CPython
                    gil_enabled: True
                     executable: /home/moritz/.virtualenvs/testing-qQMgLcoQ/bin/python
                   def_encoding: utf-8
                    fs_encoding: utf-8
                            pid: 2149091
                            cwd: /home/moritz/Sandbox/testing
                           path: /home/moritz/.virtualenvs/testing-qQMgLcoQ/bin
                                 /usr/lib/python310.zip
                                 /usr/lib/python3.10
                                 /usr/lib/python3.10/lib-dynload
                                 /home/moritz/.virtualenvs/testing-qQMgLcoQ/lib/python3.10/site-packages
                    environment: HOME = /home/moritz
                                 PIP_PYTHON_PATH = /home/moritz/.virtualenvs/testing-qQMgLcoQ/bin/python
                                 PYTHONDONTWRITEBYTECODE = 1
                   command_line: /home/moritz/.virtualenvs/testing-qQMgLcoQ/bin/coverage debug sys
         sqlite3_sqlite_version: 3.37.2
             sqlite3_temp_store: 0
        sqlite3_compile_options: ATOMIC_INTRINSICS=1, COMPILER=gcc-11.4.0, DEFAULT_AUTOVACUUM,
                                 DEFAULT_CACHE_SIZE=-2000, DEFAULT_FILE_FORMAT=4,
                                 DEFAULT_JOURNAL_SIZE_LIMIT=-1, DEFAULT_MMAP_SIZE=0, DEFAULT_PAGE_SIZE=4096,
                                 DEFAULT_PCACHE_INITSZ=20, DEFAULT_RECURSIVE_TRIGGERS,
                                 DEFAULT_SECTOR_SIZE=4096, DEFAULT_SYNCHRONOUS=2,
                                 DEFAULT_WAL_AUTOCHECKPOINT=1000, DEFAULT_WAL_SYNCHRONOUS=2,
                                 DEFAULT_WORKER_THREADS=0, ENABLE_COLUMN_METADATA, ENABLE_DBSTAT_VTAB,
                                 ENABLE_FTS3, ENABLE_FTS3_PARENTHESIS, ENABLE_FTS3_TOKENIZER, ENABLE_FTS4,
                                 ENABLE_FTS5, ENABLE_JSON1, ENABLE_LOAD_EXTENSION, ENABLE_MATH_FUNCTIONS,
                                 ENABLE_PREUPDATE_HOOK, ENABLE_RTREE, ENABLE_SESSION, ENABLE_STMTVTAB,
                                 ENABLE_UNLOCK_NOTIFY, ENABLE_UPDATE_DELETE_LIMIT, HAVE_ISNAN,
                                 LIKE_DOESNT_MATCH_BLOBS, MALLOC_SOFT_LIMIT=1024, MAX_ATTACHED=10,
                                 MAX_COLUMN=2000, MAX_COMPOUND_SELECT=500, MAX_DEFAULT_PAGE_SIZE=32768,
                                 MAX_EXPR_DEPTH=1000, MAX_FUNCTION_ARG=127, MAX_LENGTH=1000000000,
                                 MAX_LIKE_PATTERN_LENGTH=50000, MAX_MMAP_SIZE=0x7fff0000,
                                 MAX_PAGE_COUNT=1073741823, MAX_PAGE_SIZE=65536, MAX_SCHEMA_RETRY=25,
                                 MAX_SQL_LENGTH=1000000000, MAX_TRIGGER_DEPTH=1000,
                                 MAX_VARIABLE_NUMBER=250000, MAX_VDBE_OP=250000000, MAX_WORKER_THREADS=8,
                                 MUTEX_PTHREADS, OMIT_LOOKASIDE, SECURE_DELETE, SOUNDEX, SYSTEM_MALLOC,
                                 TEMP_STORE=1, THREADSAFE=1, USE_URI

Expected behavior All branches are covered and both function should be at 100% coverage.

Additional context This bug is very similar to #1421 from which the example was taken and simply updated to highlight the bug.

nedbat commented 1 month ago

Thanks. Fixed in commit 71f0f4c78b925758768b9a4ab10859c4c76c521e

nedbat commented 1 month ago

This is now released as part of coverage 7.6.2.