diff --git a/src/slipcover/__main__.py b/src/slipcover/__main__.py index 500a945..b562b79 100644 --- a/src/slipcover/__main__.py +++ b/src/slipcover/__main__.py @@ -80,6 +80,33 @@ def wrapper(*pargs, **kwargs): return wrapper +def merge_files(args): + """Merges coverage files.""" + try: + with args.merge[0].open() as jf: + merged = json.load(jf) + except Exception as e: + print(f"Error reading in {args.merge[0]}: {e}") + return 1 + + try: + for f in args.merge[1:]: + with f.open() as jf: + sc.merge_coverage(merged, json.load(jf)) + except Exception as e: + print(f"Error merging in {f}: {e}") + return 1 + + try: + with args.out.open("w", encoding='utf-8') as jf: + json.dump(merged, jf) + except Exception as e: + print(e) + return 1 + + return 0 + + def main(): import argparse @@ -116,6 +143,7 @@ def main(): g = ap.add_mutually_exclusive_group(required=True) g.add_argument('-m', dest='module', nargs=1, help="run given module as __main__") + g.add_argument('--merge', nargs='+', type=Path, help="merge JSON coverage files, saving to --out") g.add_argument('script', nargs='?', type=Path, help="the script to run") ap.add_argument('script_or_module_args', nargs=argparse.REMAINDER) @@ -126,6 +154,12 @@ def main(): else: args = ap.parse_args(sys.argv[1:]) + + if args.merge: + if not args.out: ap.error("--out is required with --merge") + return merge_files(args) + + base_path = Path(args.script).resolve().parent if args.script \ else Path('.').resolve() diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 29d12b2..ba5b1cd 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -784,8 +784,8 @@ def test_merge_coverage(tmp_path, monkeypatch, do_branch): check_summaries(a) -@pytest.mark.parametrize("branch_in", ['a', 'b']) -def test_merge_coverage_branch_coverage_disagree(tmp_path, monkeypatch, branch_in): +@pytest.fixture +def cov_merge_fixture(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) (tmp_path / "t.py").write_text("""\ @@ -802,16 +802,22 @@ def test_merge_coverage_branch_coverage_disagree(tmp_path, monkeypatch, branch_i print("all done!") # 11 """) + yield tmp_path + + + +@pytest.mark.parametrize("branch_in", ['a', 'b']) +def test_merge_coverage_branch_coverage_disagree(cov_merge_fixture, branch_in): subprocess.run([sys.executable, '-m', 'slipcover'] +\ (['--branch'] if branch_in == 'a' else []) +\ - ['--json', '--out', tmp_path / "a.json", "t.py"], check=True) + ['--json', '--out', "a.json", "t.py"], check=True) subprocess.run([sys.executable, '-m', 'slipcover'] +\ (['--branch'] if branch_in == 'b' else []) +\ - ['--json', '--out', tmp_path / "b.json", "t.py", "X"], check=True) + ['--json', '--out', "b.json", "t.py", "X"], check=True) - with (tmp_path / "a.json").open() as f: + with Path("a.json").open() as f: a = json.load(f) - with (tmp_path / "b.json").open() as f: + with Path("b.json").open() as f: b = json.load(f) assert [1, 3, 4, 8, 11] == a['files']['t.py']['executed_lines'] @@ -852,3 +858,32 @@ def test_pytest_forked(tmp_path): cov = cov['files'][test_file] assert [1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 13, 14] == cov['executed_lines'] assert [] == cov['missing_lines'] + + +def test_merge_flag(cov_merge_fixture): + subprocess.run([sys.executable, '-m', 'slipcover', '--branch', + '--json', '--out', "a.json", "t.py"], check=True) + subprocess.run([sys.executable, '-m', 'slipcover', '--branch', + '--json', '--out', "b.json", "t.py", "X"], check=True) + + subprocess.run([sys.executable, '-m', 'slipcover', '--merge', + 'a.json', 'b.json', '--out', 'c.json'], check=True) + + with Path("c.json").open() as f: + c = json.load(f) + + assert [1, 3, 4, 6, 8, 11] == c['files']['t.py']['executed_lines'] + assert [9] == c['files']['t.py']['missing_lines'] + assert True == c['meta']['branch_coverage'] + + check_summaries(c) + + +def test_merge_flag_no_out(cov_merge_fixture): + subprocess.run([sys.executable, '-m', 'slipcover', '--branch', + '--json', '--out', "a.json", "t.py"], check=True) + subprocess.run([sys.executable, '-m', 'slipcover', '--branch', + '--json', '--out', "b.json", "t.py", "X"], check=True) + + with pytest.raises(subprocess.CalledProcessError): + subprocess.run([sys.executable, '-m', 'slipcover', '--merge', 'a.json', 'b.json'], check=True)