1919import slipcover .slipcover as sc
2020
2121
22- def _run (tmp_path , source , * , branch = False , exclude_lines = None ):
22+ def _run (tmp_path , source , * , branch = False , exclude_lines = None , exclude_also = None ):
2323 """Compiles, instruments, and runs a real on-disk module, returning its
2424 file coverage dict. A real file (not an in-memory filename) is needed
2525 since exclusion matching reads the source text from disk."""
@@ -30,7 +30,7 @@ def _run(tmp_path, source, *, branch=False, exclude_lines=None):
3030 if branch :
3131 t = br .preinstrument (t )
3232
33- sci = sc .Slipcover (branch = branch , exclude_lines = exclude_lines )
33+ sci = sc .Slipcover (branch = branch , exclude_lines = exclude_lines , exclude_also = exclude_also )
3434 code = compile (t , str (code_path ), "exec" )
3535 code = sci .instrument (code )
3636
@@ -112,9 +112,7 @@ def test_default_type_checking_block_excluded(tmp_path):
112112
113113def test_custom_pattern_replaces_defaults (tmp_path ):
114114 """Matches coverage.py's exclude_lines setting exactly: providing custom
115- patterns replaces the built-in defaults, it doesn't add to them. (A
116- future exclude_also, matching coverage.py's own separate additive
117- setting, could add the additive behavior back later if wanted.)"""
115+ patterns replaces the built-in defaults, it doesn't add to them."""
118116 cov = _run (tmp_path , """\
119117 def foo(x):
120118 if x < 0: # pragma: no cover
@@ -135,6 +133,53 @@ def foo(x):
135133 assert 6 in cov ['executed_lines' ]
136134
137135
136+ def test_exclude_also_adds_to_defaults (tmp_path ):
137+ """Matches coverage.py's exclude_also: unlike exclude_lines, it's always
138+ additive to whatever's currently active -- here, the untouched
139+ defaults."""
140+ cov = _run (tmp_path , """\
141+ def foo(x):
142+ if x < 0: # pragma: no cover
143+ return 1
144+ if x < 0: # custom-also
145+ return 2
146+ return 3
147+
148+ foo(1)
149+ """ , exclude_also = ["custom-also" ])
150+
151+ # both the default pragma and the exclude_also pattern apply
152+ for ln in (2 , 3 , 4 , 5 ):
153+ assert ln not in cov ['executed_lines' ] and ln not in cov ['missing_lines' ]
154+ assert 6 in cov ['executed_lines' ]
155+
156+
157+ def test_exclude_also_adds_to_custom_exclude_lines (tmp_path ):
158+ """exclude_also adds on top of exclude_lines too, once exclude_lines has
159+ already replaced the defaults -- so the (now inactive) default pragma
160+ still doesn't apply, but both custom patterns do."""
161+ cov = _run (tmp_path , """\
162+ def foo(x):
163+ if x < 0: # pragma: no cover
164+ return 1
165+ if x < 0: # custom-a
166+ return 2
167+ if x < 0: # custom-b
168+ return 3
169+ return 4
170+
171+ foo(1)
172+ """ , exclude_lines = ["custom-a" ], exclude_also = ["custom-b" ])
173+
174+ # the default pragma is still inactive (exclude_lines replaced it)
175+ assert 2 in cov ['executed_lines' ]
176+ assert 3 in cov ['missing_lines' ]
177+ # both custom-a and custom-b apply
178+ for ln in (4 , 5 , 6 , 7 ):
179+ assert ln not in cov ['executed_lines' ] and ln not in cov ['missing_lines' ]
180+ assert 8 in cov ['executed_lines' ]
181+
182+
138183def test_single_line_match_excludes_only_that_line (tmp_path ):
139184 cov = _run (tmp_path , """\
140185 x = 1
@@ -440,3 +485,29 @@ def test_cli_exclude_lines_empty_config_disables_defaults(tmp_path, monkeypatch)
440485 keys = [k for k in cov ['files' ] if 'script.py' in k ]
441486 assert keys , f"script.py not in coverage: { list (cov ['files' ].keys ())} "
442487 assert 3 in cov ['files' ][keys [0 ]]['executed_lines' ]
488+
489+
490+ def test_cli_exclude_also_config_applied (tmp_path , monkeypatch ):
491+ """[tool.slipcover] exclude-also adds a pattern on top of the untouched
492+ defaults, confirmed end-to-end, not just at the apply_config() level."""
493+ monkeypatch .chdir (tmp_path )
494+ (tmp_path / "pyproject.toml" ).write_text (
495+ '[tool.slipcover]\n '
496+ 'exclude-also = ["custom-nocov"]\n '
497+ )
498+ (tmp_path / "script.py" ).write_text (
499+ "def foo(x):\n "
500+ " if x < 0: # custom-nocov\n "
501+ " return 1\n "
502+ " return 2\n "
503+ "foo(1)\n "
504+ )
505+
506+ p = subprocess .run ([sys .executable , '-m' , 'slipcover' , '--json' , 'script.py' ],
507+ capture_output = True , text = True )
508+ assert p .returncode == 0 , f"stderr: { p .stderr } "
509+
510+ cov = json .loads (p .stdout )
511+ keys = [k for k in cov ['files' ] if 'script.py' in k ]
512+ assert keys , f"script.py not in coverage: { list (cov ['files' ].keys ())} "
513+ assert 3 not in cov ['files' ][keys [0 ]]['missing_lines' ]
0 commit comments