mirror of
https://github.com/KosinskiLab/AlphaPulldown.git
synced 2026-06-04 14:14:24 +08:00
Expand unit coverage for helper and backend manager utilities
This commit is contained in:
@@ -139,6 +139,19 @@ def test_get_best_plddt_falls_back_to_ranked_pdb_bfactors(monkeypatch, tmp_path)
|
||||
np.testing.assert_array_equal(plddt, expected)
|
||||
|
||||
|
||||
def test_get_best_plddt_returns_none_when_no_sources_exist(tmp_path, capsys):
|
||||
(tmp_path / "ranking_debug.json").write_text(
|
||||
json.dumps({"order": ["model_4"]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
plddt = mpdockq.get_best_plddt(str(tmp_path))
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert plddt is None
|
||||
assert "ranked_0.pdb not found" in captured.out
|
||||
|
||||
|
||||
def test_read_plddt_slices_values_per_chain():
|
||||
best_plddt = np.asarray([90.0, 91.0, 50.0])
|
||||
chain_ca_inds = {"A": [0, 1], "B": [0]}
|
||||
@@ -201,6 +214,26 @@ def test_read_pdb_pdockq_uses_cb_and_gly_ca_coordinates(tmp_path):
|
||||
np.testing.assert_array_equal(chain_plddt["B"], np.asarray([60.0]))
|
||||
|
||||
|
||||
def test_read_pdb_pdockq_appends_multiple_qualifying_atoms_per_chain(tmp_path):
|
||||
pdb_path = _write_pdb(
|
||||
tmp_path / "pdockq_multi.pdb",
|
||||
[
|
||||
_atom_line(1, "CB", "ALA", "A", 1, 1.0, 1.0, 0.0, bfactor=40.0),
|
||||
_atom_line(2, "CB", "SER", "A", 2, 2.0, 1.0, 0.0, bfactor=45.0),
|
||||
_atom_line(3, "CA", "GLY", "B", 1, 6.0, 0.0, 0.0, bfactor=60.0),
|
||||
],
|
||||
)
|
||||
|
||||
chain_coords, chain_plddt = mpdockq.read_pdb_pdockq(str(pdb_path))
|
||||
|
||||
np.testing.assert_array_equal(
|
||||
chain_coords["A"],
|
||||
np.asarray([[1.0, 1.0, 0.0], [2.0, 1.0, 0.0]]),
|
||||
)
|
||||
np.testing.assert_array_equal(chain_plddt["A"], np.asarray([40.0, 45.0]))
|
||||
np.testing.assert_array_equal(chain_coords["B"], np.asarray([[6.0, 0.0, 0.0]]))
|
||||
|
||||
|
||||
def test_calc_pdockq_returns_zero_without_contacts():
|
||||
score = mpdockq.calc_pdockq(
|
||||
{
|
||||
|
||||
@@ -196,3 +196,33 @@ def test_calculate_rmsd_and_superpose_returns_none_without_matching_chains(
|
||||
assert rmsd is None
|
||||
assert "No suitable atoms found for RMSD calculation." in caplog.text
|
||||
|
||||
|
||||
def test_calculate_rmsd_and_superpose_writes_to_cwd_when_temp_dir_is_none(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
):
|
||||
ref_path = _write_pdb(
|
||||
tmp_path / "cwd_ref.pdb",
|
||||
[
|
||||
_atom_line(1, "CA", "ALA", "A", 1, 0.0, 0.0, 0.0),
|
||||
_atom_line(2, "CB", "ALA", "A", 1, 1.0, 0.0, 0.0),
|
||||
],
|
||||
)
|
||||
target_path = _write_pdb(
|
||||
tmp_path / "cwd_target.pdb",
|
||||
[
|
||||
_atom_line(1, "CA", "ALA", "A", 1, 5.0, 0.0, 0.0),
|
||||
_atom_line(2, "CB", "ALA", "A", 1, 6.0, 0.0, 0.0),
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
rmsd = calculate_rmsd.calculate_rmsd_and_superpose(
|
||||
str(ref_path),
|
||||
str(target_path),
|
||||
)
|
||||
|
||||
assert rmsd == pytest.approx(0.0)
|
||||
assert (tmp_path / "superposed_cwd_ref.pdb").is_file()
|
||||
assert (tmp_path / "superposed_cwd_target.pdb").is_file()
|
||||
|
||||
104
test/unit/test_folding_backend_manager.py
Normal file
104
test/unit/test_folding_backend_manager.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
import alphapulldown.folding_backend as folding_backend
|
||||
|
||||
|
||||
def test_try_import_returns_attribute_on_success():
|
||||
imported = folding_backend._try_import("pathlib", "Path")
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
assert imported is Path
|
||||
|
||||
|
||||
def test_try_import_logs_warning_and_returns_none_on_failure(monkeypatch):
|
||||
warnings = []
|
||||
|
||||
def fake_import_module(path):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(folding_backend, "import_module", fake_import_module)
|
||||
monkeypatch.setattr(folding_backend.logging, "warning", warnings.append)
|
||||
|
||||
imported = folding_backend._try_import("missing.mod", "Thing")
|
||||
|
||||
assert imported is None
|
||||
assert len(warnings) == 1
|
||||
assert "missing.mod:Thing" in warnings[0]
|
||||
|
||||
|
||||
def test_backend_manager_repr_and_getattr_without_backend():
|
||||
manager = folding_backend.FoldingBackendManager()
|
||||
|
||||
assert repr(manager) == "<BackendManager: no backend selected>"
|
||||
with pytest.raises(AttributeError):
|
||||
_ = manager.predict
|
||||
|
||||
|
||||
def test_backend_manager_dir_includes_backend_attributes():
|
||||
manager = folding_backend.FoldingBackendManager()
|
||||
manager._backend = types.SimpleNamespace(custom_attr=1)
|
||||
|
||||
names = manager.__dir__()
|
||||
|
||||
assert "custom_attr" in names
|
||||
assert "available_backends" in names
|
||||
|
||||
|
||||
def test_available_backends_filters_by_successful_import(monkeypatch):
|
||||
manager = folding_backend.FoldingBackendManager()
|
||||
|
||||
def fake_try_import(module_path, class_name):
|
||||
return object if class_name in {"AlphaFold2Backend", "AlphaLinkBackend"} else None
|
||||
|
||||
monkeypatch.setattr(folding_backend, "_try_import", fake_try_import)
|
||||
|
||||
assert manager.available_backends() == ["alphafold2", "alphalink"]
|
||||
|
||||
|
||||
def test_load_backend_class_validates_names_and_imports(monkeypatch):
|
||||
manager = folding_backend.FoldingBackendManager()
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
manager._load_backend_class("missing")
|
||||
|
||||
monkeypatch.setattr(folding_backend, "_try_import", lambda *_args: None)
|
||||
with pytest.raises(ImportError):
|
||||
manager._load_backend_class("alphafold2")
|
||||
|
||||
class FakeBackend:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(folding_backend, "_try_import", lambda *_args: FakeBackend)
|
||||
assert manager._load_backend_class("alphafold2") is FakeBackend
|
||||
|
||||
|
||||
def test_change_backend_uses_default_and_stores_kwargs(monkeypatch):
|
||||
manager = folding_backend.FoldingBackendManager()
|
||||
|
||||
class FakeBackend:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
monkeypatch.setattr(manager, "_load_backend_class", lambda name: FakeBackend)
|
||||
|
||||
manager.change_backend(param=3)
|
||||
|
||||
assert manager._backend_name == "alphafold2"
|
||||
assert isinstance(manager._backend, FakeBackend)
|
||||
assert manager._backend.kwargs == {"param": 3}
|
||||
assert manager._backend_args == {"param": 3}
|
||||
assert repr(manager) == "<BackendManager: using alphafold2>"
|
||||
|
||||
|
||||
def test_module_change_backend_delegates_to_manager(monkeypatch):
|
||||
calls = []
|
||||
fake_manager = types.SimpleNamespace(change_backend=lambda name, **kwargs: calls.append((name, kwargs)))
|
||||
|
||||
monkeypatch.setattr(folding_backend, "_get_manager", lambda: fake_manager)
|
||||
|
||||
folding_backend.change_backend("alphafold3", debug=True)
|
||||
|
||||
assert calls == [("alphafold3", {"debug": True})]
|
||||
@@ -56,6 +56,22 @@ def test_a3m_to_ids_returns_empty_matrix_for_empty_input():
|
||||
assert rows.dtype == np.int32
|
||||
|
||||
|
||||
def test_a3m_to_ids_ignores_blank_lines_between_headers_and_sequences():
|
||||
char_to_id = msa_encoding.get_char_to_id_map()
|
||||
a3m = "\n>sequence_0\n\nAC-\n\n>sequence_1\nAZ-\n\n"
|
||||
|
||||
rows = msa_encoding.a3m_to_ids(a3m)
|
||||
|
||||
expected = np.asarray(
|
||||
[
|
||||
[char_to_id["A"], char_to_id["C"], char_to_id["-"]],
|
||||
[char_to_id["A"], 20, char_to_id["-"]],
|
||||
],
|
||||
dtype=np.int32,
|
||||
)
|
||||
np.testing.assert_array_equal(rows, expected)
|
||||
|
||||
|
||||
def test_ids_to_a3m_af3_uses_af3_alphabet_and_unknown_fallback():
|
||||
rows = np.asarray([[0, 21, 22, 29, 30, 99]], dtype=np.int32)
|
||||
|
||||
|
||||
123
test/unit/test_objects_helpers.py
Normal file
123
test/unit/test_objects_helpers.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from alphapulldown.objects import MonomericObject
|
||||
|
||||
|
||||
def test_monomeric_object_initializes_and_exposes_uniprot_runner():
|
||||
monomer = MonomericObject("desc", "ACDE")
|
||||
|
||||
assert monomer.description == "desc"
|
||||
assert monomer.sequence == "ACDE"
|
||||
assert monomer.feature_dict == {}
|
||||
assert monomer.uniprot_runner is None
|
||||
|
||||
monomer.uniprot_runner = "runner"
|
||||
|
||||
assert monomer.uniprot_runner == "runner"
|
||||
|
||||
|
||||
def test_zip_msa_files_invokes_gzip_for_supported_suffixes(monkeypatch, tmp_path):
|
||||
for name in ("hits.a3m", "query.fasta", "align.sto", "profile.hmm", "ignore.txt"):
|
||||
(tmp_path / name).write_text("x", encoding="utf-8")
|
||||
|
||||
commands = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
commands.append((cmd, kwargs))
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("alphapulldown.objects.subprocess.run", fake_run)
|
||||
|
||||
MonomericObject.zip_msa_files(str(tmp_path))
|
||||
|
||||
assert sorted(cmd for cmd, _ in commands) == sorted([
|
||||
f"gzip {tmp_path / 'hits.a3m'}",
|
||||
f"gzip {tmp_path / 'query.fasta'}",
|
||||
f"gzip {tmp_path / 'align.sto'}",
|
||||
f"gzip {tmp_path / 'profile.hmm'}",
|
||||
])
|
||||
|
||||
|
||||
def test_unzip_msa_files_returns_true_and_unzips_gz_files(monkeypatch, tmp_path):
|
||||
(tmp_path / "hits.a3m.gz").write_text("x", encoding="utf-8")
|
||||
(tmp_path / "other.txt").write_text("x", encoding="utf-8")
|
||||
commands = []
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
commands.append((cmd, kwargs))
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("alphapulldown.objects.subprocess.run", fake_run)
|
||||
|
||||
used_zipped = MonomericObject.unzip_msa_files(str(tmp_path))
|
||||
|
||||
assert used_zipped is True
|
||||
assert [cmd for cmd, _ in commands] == [f"gunzip {tmp_path / 'hits.a3m.gz'}"]
|
||||
|
||||
|
||||
def test_unzip_msa_files_returns_false_when_no_gz_files(tmp_path):
|
||||
(tmp_path / "hits.a3m").write_text("x", encoding="utf-8")
|
||||
|
||||
used_zipped = MonomericObject.unzip_msa_files(str(tmp_path))
|
||||
|
||||
assert used_zipped is False
|
||||
|
||||
|
||||
def test_remove_msa_files_removes_only_msa_files_and_empty_directory(tmp_path):
|
||||
for name in ("hits.a3m", "query.fasta", "align.sto", "profile.hmm"):
|
||||
(tmp_path / name).write_text("x", encoding="utf-8")
|
||||
|
||||
MonomericObject.remove_msa_files(str(tmp_path))
|
||||
|
||||
assert not tmp_path.exists()
|
||||
|
||||
|
||||
def test_remove_msa_files_keeps_directory_if_non_msa_files_remain(tmp_path):
|
||||
(tmp_path / "hits.a3m").write_text("x", encoding="utf-8")
|
||||
keep_file = tmp_path / "keep.txt"
|
||||
keep_file.write_text("x", encoding="utf-8")
|
||||
|
||||
MonomericObject.remove_msa_files(str(tmp_path))
|
||||
|
||||
assert tmp_path.is_dir()
|
||||
assert keep_file.is_file()
|
||||
|
||||
|
||||
def test_all_seq_msa_features_keeps_only_pairing_related_keys(monkeypatch, tmp_path):
|
||||
monomer = MonomericObject("desc", "ACDE")
|
||||
input_fasta_path = str(tmp_path / "input.fasta")
|
||||
Path(input_fasta_path).write_text(">x\nACDE\n", encoding="utf-8")
|
||||
|
||||
class FakeMsa:
|
||||
def truncate(self, max_seqs):
|
||||
assert max_seqs == 50000
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("alphapulldown.objects.pipeline.run_msa_tool", lambda *args, **kwargs: {"sto": "fake"})
|
||||
monkeypatch.setattr("alphapulldown.objects.parsers.parse_stockholm", lambda sto: FakeMsa())
|
||||
monkeypatch.setattr(
|
||||
"alphapulldown.objects.pipeline.make_msa_features",
|
||||
lambda _msas: {
|
||||
"msa": np.asarray([[1, 2]], dtype=np.int32),
|
||||
"msa_species_identifiers": np.asarray([b"9606"]),
|
||||
"msa_uniprot_accession_identifiers": np.asarray([b"P12345"]),
|
||||
"deletion_matrix_int": np.asarray([[0, 0]], dtype=np.int32),
|
||||
"extra_feature": np.asarray([1], dtype=np.int32),
|
||||
},
|
||||
)
|
||||
|
||||
features = monomer.all_seq_msa_features(
|
||||
input_fasta_path=input_fasta_path,
|
||||
uniprot_msa_runner="runner",
|
||||
output_dir=str(tmp_path),
|
||||
use_precomputed_msa=True,
|
||||
)
|
||||
|
||||
assert set(features) == {
|
||||
"msa_all_seq",
|
||||
"msa_species_identifiers_all_seq",
|
||||
"msa_uniprot_accession_identifiers_all_seq",
|
||||
"deletion_matrix_int_all_seq",
|
||||
}
|
||||
Reference in New Issue
Block a user