Skip to content

Commit 16a2e0c

Browse files
Add Python API tests to improve coverage (94% → 95%)
- matching.py: 98% → 100% (add_vestigial_root validation, AncestorMatcher optional kwargs) - config.py: 95% → 99% (field spec resolution, parsing edge cases, format optional fields) - pipeline.py: 90% → 91% (_erase_flanks no-intervals, augment_sites missing metadata)
1 parent b1f2cd0 commit 16a2e0c

3 files changed

Lines changed: 233 additions & 0 deletions

File tree

tests/test_config.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,3 +1165,129 @@ def test_groups_rejected(self, tmp_path):
11651165
)
11661166
with pytest.raises(ValueError, match="Unrecognised.*match.*groups"):
11671167
config.Config.from_toml(_write_toml(tmp_path, toml))
1168+
1169+
1170+
class TestConfigCoverageEdgeCases:
1171+
def test_resolve_field_spec_dict_with_path(self):
1172+
spec = {"path": "some/path", "field": "x"}
1173+
result = config._resolve_field_spec(spec)
1174+
assert result["path"] == "some/path"
1175+
assert result["field"] == "x"
1176+
1177+
def test_resolve_field_spec_string(self):
1178+
result = config._resolve_field_spec("simple_field")
1179+
assert result == "simple_field"
1180+
1181+
def test_ancestral_state_missing_path(self, tmp_path):
1182+
toml = """\
1183+
[ancestral_state]
1184+
field = "variant_ancestral_allele"
1185+
1186+
[[source]]
1187+
name = "cohort"
1188+
path = "samples.vcz"
1189+
1190+
[ancestors]
1191+
path = "ancestors.vcz"
1192+
sources = ["cohort"]
1193+
1194+
[match]
1195+
output = "out.trees"
1196+
1197+
[match.sources.ancestors]
1198+
[match.sources.cohort]
1199+
"""
1200+
with pytest.raises(ValueError, match="missing required key"):
1201+
config.Config.from_toml(_write_toml(tmp_path, toml))
1202+
1203+
def test_ancestors_invalid_format(self):
1204+
raw = {
1205+
"ancestral_state": {"path": "x", "field": "y"},
1206+
"ancestors": "not_a_table",
1207+
"match": {"sources": {"cohort": {}}, "output": "out.trees"},
1208+
"source": [{"name": "cohort", "path": "s.vcz"}],
1209+
}
1210+
with pytest.raises(ValueError, match="must be a table"):
1211+
config._parse_ancestors(raw)
1212+
1213+
def test_augment_sites_missing_sources(self, tmp_path):
1214+
raw = {"augment_sites": {}}
1215+
with pytest.raises(ValueError, match="missing required key.*sources"):
1216+
config._parse_augment_sites(raw)
1217+
1218+
def test_augment_sites_sources_not_list(self, tmp_path):
1219+
raw = {"augment_sites": {"sources": "not_a_list"}}
1220+
with pytest.raises(ValueError, match="sources must be a list"):
1221+
config._parse_augment_sites(raw)
1222+
1223+
def test_ancestor_not_in_match_sources(self):
1224+
with pytest.raises(ValueError, match="must appear in"):
1225+
config.Config(
1226+
sources={"cohort": config.Source(name="cohort", path="s.vcz")},
1227+
ancestors=[
1228+
config.AncestorsConfig(
1229+
name="ancestors",
1230+
path="a.vcz",
1231+
sources=["cohort"],
1232+
)
1233+
],
1234+
match=config.MatchConfig(
1235+
sources={"cohort": config.MatchSourceConfig()},
1236+
output="out.trees",
1237+
),
1238+
ancestral_state=config.AncestralState(path="ann.vcz", field="x"),
1239+
)
1240+
1241+
def test_match_source_simple_value(self, tmp_path):
1242+
"""A match source with a bare value (not a table) gets default config."""
1243+
toml = """\
1244+
[ancestral_state]
1245+
path = "annotations.vcz"
1246+
field = "variant_ancestral_allele"
1247+
1248+
[[source]]
1249+
name = "cohort"
1250+
path = "samples.vcz"
1251+
1252+
[ancestors]
1253+
path = "ancestors.vcz"
1254+
sources = ["cohort"]
1255+
1256+
[match]
1257+
output = "out.trees"
1258+
1259+
[match.sources]
1260+
ancestors = true
1261+
cohort = true
1262+
"""
1263+
cfg = config.Config.from_toml(_write_toml(tmp_path, toml))
1264+
assert isinstance(cfg.match.sources["cohort"], config.MatchSourceConfig)
1265+
1266+
def test_config_format_optional_fields(self):
1267+
"""Config.format() includes optional source fields when set."""
1268+
cfg = config.Config(
1269+
sources={
1270+
"cohort": config.Source(
1271+
name="cohort",
1272+
path="s.vcz",
1273+
regions="chr1:1-100",
1274+
targets="targets.bed",
1275+
)
1276+
},
1277+
ancestors=[
1278+
config.AncestorsConfig(
1279+
name="ancestors", path="a.vcz", sources=["cohort"]
1280+
)
1281+
],
1282+
match=config.MatchConfig(
1283+
sources={
1284+
"ancestors": config.MatchSourceConfig(),
1285+
"cohort": config.MatchSourceConfig(),
1286+
},
1287+
output="out.trees",
1288+
),
1289+
ancestral_state=config.AncestralState(path="ann.vcz", field="x"),
1290+
)
1291+
text = cfg.format()
1292+
assert "regions" in text
1293+
assert "targets" in text

tests/test_matching.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from __future__ import annotations
2424

2525
import numpy as np
26+
import pytest
2627
import tskit
2728

2829
from tsinfer import grouping, matching, vcz
@@ -758,3 +759,46 @@ def test_metadata_survives_multiple_cycles(self):
758759
)
759760
assert "sequence_intervals" in ts2.metadata
760761
assert ts2.metadata["sequence_intervals"] == [[10, 51]]
762+
763+
764+
# ---------------------------------------------------------------------------
765+
# TestAddVestigialRoot
766+
# ---------------------------------------------------------------------------
767+
768+
769+
class TestAddVestigialRoot:
770+
def test_non_discrete_genome(self):
771+
tables = tskit.TableCollection(sequence_length=1.5)
772+
tables.nodes.add_row(flags=tskit.NODE_IS_SAMPLE, time=0)
773+
ts = tables.tree_sequence()
774+
with pytest.raises(ValueError, match="discrete genome"):
775+
matching.add_vestigial_root(ts)
776+
777+
def test_empty_tree_sequence(self):
778+
tables = tskit.TableCollection(sequence_length=1)
779+
ts = tables.tree_sequence()
780+
with pytest.raises(ValueError, match="Emtpy trees"):
781+
matching.add_vestigial_root(ts)
782+
783+
784+
# ---------------------------------------------------------------------------
785+
# TestAncestorMatcherWrapper
786+
# ---------------------------------------------------------------------------
787+
788+
789+
class TestAncestorMatcherWrapper:
790+
def test_optional_kwargs(self):
791+
ts = tskit.Tree.generate_balanced(4).tree_sequence
792+
tables = ts.dump_tables()
793+
tables.sequence_length = 2
794+
tables.edges.right = np.full(len(tables.edges), 2, dtype=np.float64)
795+
tables.sites.add_row(position=1, ancestral_state="A")
796+
tables.mutations.add_row(site=0, node=1, derived_state="T")
797+
ts = tables.tree_sequence()
798+
mi = matching.MatcherIndexes(ts)
799+
r = np.full(ts.num_sites, 1e-9)
800+
m = np.full(ts.num_sites, 0.0)
801+
am = matching.AncestorMatcher(
802+
mi, r, m, likelihood_threshold=1e-10, weight_by_n=False
803+
)
804+
assert am is not None

tests/test_pipeline.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,3 +1358,66 @@ def test_run_integration(self):
13581358
# Should have the augmented site
13591359
positions = set(out_ts.sites_position)
13601360
assert 200.0 in positions
1361+
1362+
1363+
class TestEraseFlanks:
1364+
def _make_ts_with_json_metadata(self, metadata=None):
1365+
"""Build a ts with JSON metadata schema so ts.metadata returns a dict."""
1366+
tables = tskit.Tree.generate_balanced(4).tree_sequence.dump_tables()
1367+
tables.metadata_schema = tskit.MetadataSchema.permissive_json()
1368+
tables.metadata = metadata if metadata is not None else {}
1369+
return tables.tree_sequence()
1370+
1371+
def test_no_intervals_returns_unchanged(self):
1372+
"""_erase_flanks returns ts unchanged when metadata has no sequence_intervals."""
1373+
ts = self._make_ts_with_json_metadata()
1374+
result = pipeline._erase_flanks(ts)
1375+
assert result.num_edges == ts.num_edges
1376+
assert result.num_nodes == ts.num_nodes
1377+
1378+
def test_empty_metadata(self):
1379+
"""_erase_flanks handles ts.metadata being empty dict."""
1380+
ts = self._make_ts_with_json_metadata({})
1381+
result = pipeline._erase_flanks(ts)
1382+
assert result.num_edges == ts.num_edges
1383+
1384+
1385+
class TestAugmentSitesEdgeCases:
1386+
def test_missing_node_metadata(self):
1387+
"""augment_sites raises when sample nodes lack required metadata."""
1388+
# Build a ts with JSON node metadata schema but nodes missing keys
1389+
tables = tskit.Tree.generate_balanced(4).tree_sequence.dump_tables()
1390+
tables.nodes.metadata_schema = tskit.MetadataSchema.permissive_json()
1391+
md = [{}] * len(tables.nodes)
1392+
tables.nodes.packset_metadata(
1393+
[tables.nodes.metadata_schema.encode_row(m) for m in md]
1394+
)
1395+
tables.metadata_schema = tskit.MetadataSchema.permissive_json()
1396+
tables.metadata = {}
1397+
ts = tables.tree_sequence()
1398+
cfg = config.Config(
1399+
sources={"s": config.Source(name="s", path="s.vcz")},
1400+
ancestors=[],
1401+
match=config.MatchConfig(
1402+
sources={}, output="out.trees", reference_ts="ref.trees"
1403+
),
1404+
ancestral_state=config.AncestralState(path="ann.vcz", field="x"),
1405+
augment_sites=config.AugmentSitesConfig(sources=["s"]),
1406+
)
1407+
with pytest.raises(ValueError, match="missing required metadata key"):
1408+
pipeline.augment_sites(ts, cfg)
1409+
1410+
def test_no_augment_config(self):
1411+
"""augment_sites returns ts unchanged when no augment config."""
1412+
ts = tskit.Tree.generate_balanced(4).tree_sequence
1413+
cfg = config.Config(
1414+
sources={},
1415+
ancestors=[],
1416+
match=config.MatchConfig(
1417+
sources={}, output="out.trees", reference_ts="ref.trees"
1418+
),
1419+
ancestral_state=config.AncestralState(path="ann.vcz", field="x"),
1420+
augment_sites=None,
1421+
)
1422+
result = pipeline.augment_sites(ts, cfg)
1423+
assert result.equals(ts)

0 commit comments

Comments
 (0)