diff --git a/src/ndi/util/__init__.py b/src/ndi/util/__init__.py index 96fca72..3d3b07c 100644 --- a/src/ndi/util/__init__.py +++ b/src/ndi/util/__init__.py @@ -14,7 +14,9 @@ """ from .classname import ndi_matlab_classname, ndi_python_classname +from .compare_dataset_summary import compareDatasetSummary from .compare_session_summary import compareSessionSummary +from .dataset_summary import datasetSummary from .datestamp2datetime import datestamp2datetime from .downsampleTimeseries import downsampleTimeseries from .getHexDiffFromFileObj import getHexDiffFromFileObj @@ -28,7 +30,9 @@ __all__ = [ "ndi_matlab_classname", "ndi_python_classname", + "compareDatasetSummary", "compareSessionSummary", + "datasetSummary", "datestamp2datetime", "downsampleTimeseries", "getHexDiffFromFileObj", diff --git a/src/ndi/util/compare_dataset_summary.py b/src/ndi/util/compare_dataset_summary.py new file mode 100644 index 0000000..d649d3c --- /dev/null +++ b/src/ndi/util/compare_dataset_summary.py @@ -0,0 +1,109 @@ +"""Compare two dataset summaries and return a report of differences. + +MATLAB equivalent: ``ndi.util.compareDatasetSummary`` + +Compares two summary dicts (as produced by :func:`datasetSummary`) and +returns a list of human-readable difference strings. +""" + +from __future__ import annotations + +from typing import Any + +from .compare_session_summary import compareSessionSummary + + +def compareDatasetSummary( + summary1: dict[str, Any], + summary2: dict[str, Any], + *, + excludeFiles: list[str] | None = None, + excludeFields: list[str] | None = None, +) -> list[str]: + """Compare two dataset summaries and return a report. + + MATLAB equivalent: ``ndi.util.compareDatasetSummary(s1, s2, ...)`` + + Args: + summary1: First dataset summary dict. + summary2: Second dataset summary dict. + excludeFiles: Filenames to ignore when comparing file lists + within session summaries. + excludeFields: Field names to skip entirely during comparison. + + Returns: + List of difference strings. Empty list means summaries match. + """ + if excludeFiles is None: + excludeFiles = [] + if excludeFields is None: + excludeFields = [] + + report: list[str] = [] + + # 1. Compare numSessions + if "numSessions" not in excludeFields: + n1 = summary1.get("numSessions", 0) + n2 = summary2.get("numSessions", 0) + if n1 != n2: + report.append(f"numSessions differs: {n1} vs {n2}") + + # 2. Compare references + if "references" not in excludeFields: + refs1 = sorted(summary1.get("references", [])) + refs2 = sorted(summary2.get("references", [])) + if refs1 != refs2: + report.append(f"references differ: {refs1} vs {refs2}") + + # 3. Compare sessionIds + if "sessionIds" not in excludeFields: + ids1 = sorted(summary1.get("sessionIds", [])) + ids2 = sorted(summary2.get("sessionIds", [])) + if ids1 != ids2: + report.append(f"sessionIds differ: {ids1} vs {ids2}") + + # 4. Compare sessionSummaries + if "sessionSummaries" not in excludeFields: + ss1 = summary1.get("sessionSummaries", []) + ss2 = summary2.get("sessionSummaries", []) + + if len(ss1) != len(ss2): + report.append(f"sessionSummaries count differs: {len(ss1)} vs {len(ss2)}") + else: + # Match session summaries by sessionId when available, + # otherwise compare by index order. + ids1 = summary1.get("sessionIds", []) + ids2 = summary2.get("sessionIds", []) + + if len(ids1) == len(ss1) and len(ids2) == len(ss2): + # Build lookup by sessionId for summary2 + lookup2: dict[str, dict] = {} + for sid, ss in zip(ids2, ss2): + lookup2[sid] = ss + + for i, sid in enumerate(ids1): + match = lookup2.get(sid) + if match is None: + report.append(f"sessionSummaries: session {sid} not found in summary2") + continue + sub = compareSessionSummary( + ss1[i], + match, + excludeFiles=excludeFiles, + excludeFields=excludeFields, + ) + for s in sub: + report.append(f"sessionSummaries[{sid}]: {s}") + else: + # Fallback: compare by index + for i, (s1, s2) in enumerate(zip(ss1, ss2)): + sub = compareSessionSummary( + s1, + s2, + excludeFiles=excludeFiles, + excludeFields=excludeFields, + ) + for s in sub: + report.append(f"sessionSummaries[{i}]: {s}") + + return report diff --git a/src/ndi/util/dataset_summary.py b/src/ndi/util/dataset_summary.py new file mode 100644 index 0000000..c9a9552 --- /dev/null +++ b/src/ndi/util/dataset_summary.py @@ -0,0 +1,42 @@ +"""ndi_dataset summary utility for symmetry testing. + +MATLAB equivalent: ``ndi.util.datasetSummary`` + +Creates a summary dict of an ``ndi.dataset.Dataset`` object containing key +fields and properties, intended for symmetry testing between NDI language +implementations. +""" + +from __future__ import annotations + +from typing import Any + +from .session_summary import sessionSummary + + +def datasetSummary(dataset_obj: Any) -> dict[str, Any]: + """Create a summary structure of an ndi.dataset.Dataset object. + + MATLAB equivalent: ``ndi.util.datasetSummary(dataset_obj)`` + + Args: + dataset_obj: An NDI Dataset object. + + Returns: + Dict with keys: numSessions, references, sessionIds, + sessionSummaries. + """ + refs, session_ids, *_ = dataset_obj.session_list() + + # Build a session summary for each session in the dataset + session_summaries = [] + for sid in session_ids: + sess = dataset_obj.open_session(sid) + session_summaries.append(sessionSummary(sess)) + + return { + "numSessions": len(refs), + "references": refs, + "sessionIds": session_ids, + "sessionSummaries": session_summaries, + } diff --git a/src/ndi/util/ndi_matlab_python_bridge.yaml b/src/ndi/util/ndi_matlab_python_bridge.yaml index f257165..3f2b3b2 100644 --- a/src/ndi/util/ndi_matlab_python_bridge.yaml +++ b/src/ndi/util/ndi_matlab_python_bridge.yaml @@ -174,6 +174,89 @@ functions: Exact match. Replaces NDI sentinel strings for NaN, Infinity, and -Infinity in JSON text with Python-compatible representations. + - name: sessionSummary + type: function + matlab_path: "+ndi/+util/sessionSummary.m" + python_path: "ndi/util/session_summary.py" + input_arguments: + - name: session_obj + type_matlab: "ndi.session" + type_python: "Any" + output_arguments: + - name: summary + type_python: "dict[str, Any]" + decision_log: > + Exact match. Creates a summary dict of an ndi.session object + for symmetry testing. + + - name: compareSessionSummary + type: function + matlab_path: "+ndi/+util/compareSessionSummary.m" + python_path: "ndi/util/compare_session_summary.py" + input_arguments: + - name: summary1 + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: summary2 + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: excludeFiles + type_matlab: "cell array of char" + type_python: "list[str] | None" + default: "None" + - name: excludeFields + type_matlab: "cell array of char" + type_python: "list[str] | None" + default: "None" + output_arguments: + - name: report + type_python: "list[str]" + decision_log: > + Exact match. Compares two session summaries and returns a list + of difference strings. Empty list means summaries match. + + - name: datasetSummary + type: function + matlab_path: "+ndi/+util/datasetSummary.m" + python_path: "ndi/util/dataset_summary.py" + input_arguments: + - name: dataset_obj + type_matlab: "ndi.dataset.Dataset" + type_python: "Any" + output_arguments: + - name: summary + type_python: "dict[str, Any]" + decision_log: > + Exact match. Creates a summary dict of an ndi.dataset.Dataset + object for symmetry testing. + + - name: compareDatasetSummary + type: function + matlab_path: "+ndi/+util/compareDatasetSummary.m" + python_path: "ndi/util/compare_dataset_summary.py" + input_arguments: + - name: summary1 + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: summary2 + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: excludeFiles + type_matlab: "cell array of char" + type_python: "list[str] | None" + default: "None" + - name: excludeFields + type_matlab: "cell array of char" + type_python: "list[str] | None" + default: "None" + output_arguments: + - name: report + type_python: "list[str]" + decision_log: > + Exact match. Compares two dataset summaries and returns a list + of difference strings. Delegates session-level comparison to + compareSessionSummary. + - name: unwrapTableCellContent type: function matlab_path: "+ndi/+util/unwrapTableCellContent.m" diff --git a/tests/symmetry/make_artifacts/dataset/test_build_dataset.py b/tests/symmetry/make_artifacts/dataset/test_build_dataset.py index 327a76f..c31c77a 100644 --- a/tests/symmetry/make_artifacts/dataset/test_build_dataset.py +++ b/tests/symmetry/make_artifacts/dataset/test_build_dataset.py @@ -24,7 +24,7 @@ from ndi.document import Document from ndi.query import Query from ndi.session.dir import DirSession -from ndi.util import sessionSummary +from ndi.util import datasetSummary from tests.symmetry.conftest import PYTHON_ARTIFACTS ARTIFACT_DIR = PYTHON_ARTIFACTS / "dataset" / "buildDataset" / "testBuildDatasetArtifacts" @@ -46,29 +46,6 @@ def _add_doc_with_file(session: DirSession, doc_number: int) -> None: session.database_add(doc) -def _dataset_summary(dataset: Dataset) -> dict: - """Create a summary structure for a dataset. - - Mirrors MATLAB's ``ndi.symmetry.makeArtifacts.dataset.buildDataset`` - which writes: numSessions, references, sessionIds, sessionSummaries. - """ - refs, session_ids, *_ = dataset.session_list() - num_sessions = len(refs) - - # Build a session summary for each session in the dataset - session_summaries = [] - for sid in session_ids: - sess = dataset.open_session(sid) - session_summaries.append(sessionSummary(sess)) - - return { - "numSessions": num_sessions, - "references": refs, - "sessionIds": session_ids, - "sessionSummaries": session_summaries, - } - - class TestBuildDataset: """Mirror of ndi.symmetry.makeArtifacts.dataset.buildDataset.""" @@ -132,7 +109,7 @@ def test_build_dataset_artifacts(self): # Write datasetSummary.json – open from artifact_dir so the session # path lists files that are actually present (including jsonDocuments). - summary = _dataset_summary(artifact_dataset) + summary = datasetSummary(artifact_dataset) summary_json = json.dumps(summary, indent=2, allow_nan=True) summary_path = artifact_dir / "datasetSummary.json" summary_path.write_text(summary_json, encoding="utf-8") diff --git a/tests/symmetry/make_artifacts/dataset/test_download_ingested.py b/tests/symmetry/make_artifacts/dataset/test_download_ingested.py index 39c5ae6..1604a0f 100644 --- a/tests/symmetry/make_artifacts/dataset/test_download_ingested.py +++ b/tests/symmetry/make_artifacts/dataset/test_download_ingested.py @@ -24,7 +24,7 @@ from ndi.dataset import Dataset from ndi.query import Query -from ndi.util import sessionSummary +from ndi.util import datasetSummary from tests.symmetry.conftest import PYTHON_ARTIFACTS ARTIFACT_DIR = PYTHON_ARTIFACTS / "dataset" / "downloadIngested" / "testDownloadIngestedArtifacts" @@ -99,31 +99,17 @@ def test_download_ingested_artifacts(self): # Open the dataset dataset = Dataset(dataset_path) - # Get session list - ref_list, id_list, *_ = dataset.session_list() - num_sessions = len(ref_list) + # Build the dataset summary using the public utility + dataset_summary = datasetSummary(dataset) - # Build session summaries for each session - session_summaries = [] - for sid in id_list: - sess = dataset.open_session(sid) - session_summaries.append(sessionSummary(sess)) - - # Record document counts per session + # Record document counts per session (extra field for this test) + _ref_list, id_list, *_ = dataset.session_list() document_counts = [] for sid in id_list: sess = dataset.open_session(sid) docs = sess.database_search(Query("base.id").match("(.*)")) document_counts.append({"sessionId": sid, "count": len(docs)}) - - # Build the dataset summary - dataset_summary = { - "numSessions": num_sessions, - "references": ref_list, - "sessionIds": id_list, - "sessionSummaries": session_summaries, - "documentCounts": document_counts, - } + dataset_summary["documentCounts"] = document_counts # Write datasetSummary.json summary_json = json.dumps(dataset_summary, indent=2, allow_nan=True) diff --git a/tests/symmetry/read_artifacts/dataset/test_build_dataset.py b/tests/symmetry/read_artifacts/dataset/test_build_dataset.py index d1212e4..2d43607 100644 --- a/tests/symmetry/read_artifacts/dataset/test_build_dataset.py +++ b/tests/symmetry/read_artifacts/dataset/test_build_dataset.py @@ -24,7 +24,7 @@ from ndi.dataset import Dataset from ndi.query import Query -from ndi.util import compareSessionSummary, sessionSummary +from ndi.util import compareDatasetSummary, datasetSummary from tests.symmetry.conftest import SOURCE_TYPES, SYMMETRY_BASE @@ -54,7 +54,7 @@ def _open_dataset(self, source_type): # -- tests ---------------------------------------------------------------- def test_build_dataset_summary(self, source_type): - """Verify session counts, references, and IDs match datasetSummary.json.""" + """Verify dataset summary matches datasetSummary.json.""" artifact_dir, dataset = self._open_dataset(source_type) summary_path = artifact_dir / "datasetSummary.json" @@ -62,65 +62,15 @@ def test_build_dataset_summary(self, source_type): pytest.skip(f"datasetSummary.json not found in {source_type} artifact directory.") expected = json.loads(summary_path.read_text(encoding="utf-8")) + actual = datasetSummary(dataset) - # Verify session list - refs, session_ids, *_ = dataset.session_list() - num_sessions = len(refs) - - expected_num = expected.get("numSessions", 0) - expected_ids = expected.get("sessionIds", []) - expected_refs = expected.get("references", []) - - assert ( - num_sessions == expected_num - ), f"Session count mismatch in {source_type}: got {num_sessions}, expected {expected_num}" - - for exp_id in expected_ids: - assert ( - exp_id in session_ids - ), f"Expected session ID {exp_id!r} not found in dataset from {source_type}" - - for exp_ref in expected_refs: - assert ( - exp_ref in refs - ), f"Expected session reference {exp_ref!r} not found in dataset from {source_type}" - - def test_build_dataset_session_summaries(self, source_type): - """Compare per-session summaries against those stored in datasetSummary.json.""" - artifact_dir, dataset = self._open_dataset(source_type) - - summary_path = artifact_dir / "datasetSummary.json" - if not summary_path.exists(): - pytest.skip(f"datasetSummary.json not found in {source_type} artifact directory.") - - expected = json.loads(summary_path.read_text(encoding="utf-8")) - expected_summaries = expected.get("sessionSummaries", []) - expected_ids = expected.get("sessionIds", []) - - if not expected_summaries: - pytest.skip(f"No sessionSummaries in {source_type} datasetSummary.json.") - - for i, sid in enumerate(expected_ids): - if i >= len(expected_summaries): - break - - sess = dataset.open_session(sid) - if sess is None: - pytest.fail(f"Could not open session {sid} from {source_type} dataset.") - - actual_summary = sessionSummary(sess) - expected_summary = expected_summaries[i] - - report = compareSessionSummary( - actual_summary, - expected_summary, - excludeFiles=["datasetSummary.json", "jsonDocuments"], - ) + report = compareDatasetSummary( + actual, + expected, + excludeFiles=["datasetSummary.json", "jsonDocuments"], + ) - assert len(report) == 0, ( - f"Session summary mismatch for session {sid} " - f"in {source_type} dataset:\n" + "\n".join(report) - ) + assert len(report) == 0, f"Dataset summary mismatch in {source_type}:\n" + "\n".join(report) def test_build_dataset_documents(self, source_type): """Verify that every exported JSON document can be loaded from session DBs. diff --git a/tests/symmetry/read_artifacts/dataset/test_download_ingested.py b/tests/symmetry/read_artifacts/dataset/test_download_ingested.py index eee7e1d..c3cc5b4 100644 --- a/tests/symmetry/read_artifacts/dataset/test_download_ingested.py +++ b/tests/symmetry/read_artifacts/dataset/test_download_ingested.py @@ -21,7 +21,7 @@ from ndi.dataset import Dataset from ndi.query import Query -from ndi.util import compareSessionSummary, sessionSummary +from ndi.util import compareDatasetSummary, datasetSummary from tests.symmetry.conftest import SOURCE_TYPES, SYMMETRY_BASE @@ -71,84 +71,34 @@ def _open_dataset(self, source_type): # -- tests ---------------------------------------------------------------- def test_download_ingested_summary(self, source_type): - """Verify session counts, references, and IDs match datasetSummary.json.""" + """Verify dataset summary matches datasetSummary.json.""" _artifact_dir, dataset, expected = self._open_dataset(source_type) - # Get session list from the dataset - ref_list, id_list, *_ = dataset.session_list() - num_sessions = len(ref_list) + actual = datasetSummary(dataset) - # Verify number of sessions - expected_num = expected.get("numSessions", 0) - assert num_sessions == expected_num, ( - f"Session count mismatch in {source_type}: " - f"got {num_sessions}, expected {expected_num}" - ) - - # Verify references - expected_refs = expected.get("references", []) - assert sorted(ref_list) == sorted( - expected_refs - ), f"Session references mismatch against {source_type} generated artifacts." - - # Verify session IDs - expected_ids = expected.get("sessionIds", []) - assert sorted(id_list) == sorted( - expected_ids - ), f"Session IDs mismatch against {source_type} generated artifacts." - - def test_download_ingested_session_summaries(self, source_type): - """Compare per-session summaries against datasetSummary.json.""" - _artifact_dir, dataset, expected = self._open_dataset(source_type) - - expected_summaries = expected.get("sessionSummaries", []) - _ref_list, id_list, *_ = dataset.session_list() - - if not expected_summaries: - pytest.skip(f"No sessionSummaries in {source_type} datasetSummary.json.") - - for sid in id_list: - sess = dataset.open_session(sid) - if sess is None: - pytest.fail(f"Could not open session {sid} from {source_type} dataset.") - - actual_summary = sessionSummary(sess) - - # Filter macOS resource fork files (._*) from file lists; - # these may be present in archives but MATLAB does not list them. - def _filter_dot_underscore(files: list[str]) -> list[str]: - return [f for f in files if not f.split("/")[-1].startswith("._")] + # Filter macOS resource fork files (._*) from file lists; + # these may be present in archives but MATLAB does not list them. + def _filter_dot_underscore(files: list[str]) -> list[str]: + return [f for f in files if not f.split("/")[-1].startswith("._")] + for ss in actual.get("sessionSummaries", []): for key in ("files", "filesInDotNDI"): - if key in actual_summary: - actual_summary[key] = _filter_dot_underscore(actual_summary[key]) - - # Find the expected summary with the matching sessionId - match = None - for es in expected_summaries: - if es.get("sessionId") == sid: - match = es - break - - assert match is not None, ( - f"No expected session summary found for session ID " f"{sid} in {source_type}" - ) + if key in ss: + ss[key] = _filter_dot_underscore(ss[key]) - # Also filter macOS resource fork files from the expected summary + for ss in expected.get("sessionSummaries", []): for key in ("files", "filesInDotNDI"): - if key in match: - match[key] = _filter_dot_underscore(match[key]) - - report = compareSessionSummary( - actual_summary, - match, - excludeFiles=["datasetSummary.json", "jsonDocuments"], - ) + if key in ss: + ss[key] = _filter_dot_underscore(ss[key]) + + report = compareDatasetSummary( + actual, + expected, + excludeFiles=["datasetSummary.json", "jsonDocuments"], + excludeFields=["documentCounts"], + ) - assert len(report) == 0, ( - f"Session summary mismatch for session {sid} " - f"in {source_type} dataset:\n" + "\n".join(report) - ) + assert len(report) == 0, f"Dataset summary mismatch in {source_type}:\n" + "\n".join(report) def test_download_ingested_document_counts(self, source_type): """Verify document counts per session match expected values.""" diff --git a/tests/test_dataset_summary.py b/tests/test_dataset_summary.py new file mode 100644 index 0000000..5b5fe09 --- /dev/null +++ b/tests/test_dataset_summary.py @@ -0,0 +1,290 @@ +"""Unit tests for ndi.util.datasetSummary and ndi.util.compareDatasetSummary.""" + +from unittest.mock import MagicMock + +from ndi.util import compareDatasetSummary, datasetSummary + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _mock_session(reference="ref1", session_id="sid1", path="/tmp/sess"): + """Create a mock session object with the fields sessionSummary expects.""" + sess = MagicMock() + sess.reference = reference + sess.id.return_value = session_id + sess.path = path + sess.daqsystem_load.return_value = [] + sess.getprobes.return_value = [] + return sess + + +def _mock_dataset(sessions): + """Create a mock Dataset whose session_list/open_session work with the given sessions.""" + ds = MagicMock() + refs = [s.reference for s in sessions] + ids = [s.id() for s in sessions] + ds.session_list.return_value = (refs, ids) + + def _open(sid): + for s in sessions: + if s.id() == sid: + return s + return None + + ds.open_session.side_effect = _open + return ds + + +# =========================================================================== +# datasetSummary +# =========================================================================== + + +class TestDatasetSummary: + def test_empty_dataset(self): + ds = MagicMock() + ds.session_list.return_value = ([], []) + result = datasetSummary(ds) + assert result["numSessions"] == 0 + assert result["references"] == [] + assert result["sessionIds"] == [] + assert result["sessionSummaries"] == [] + + def test_single_session(self): + sess = _mock_session("myref", "mysid", "/tmp/nonexistent") + ds = _mock_dataset([sess]) + result = datasetSummary(ds) + assert result["numSessions"] == 1 + assert result["references"] == ["myref"] + assert result["sessionIds"] == ["mysid"] + assert len(result["sessionSummaries"]) == 1 + assert result["sessionSummaries"][0]["reference"] == "myref" + assert result["sessionSummaries"][0]["sessionId"] == "mysid" + + def test_multiple_sessions(self): + s1 = _mock_session("ref_a", "id_a", "/tmp/a") + s2 = _mock_session("ref_b", "id_b", "/tmp/b") + ds = _mock_dataset([s1, s2]) + result = datasetSummary(ds) + assert result["numSessions"] == 2 + assert result["references"] == ["ref_a", "ref_b"] + assert result["sessionIds"] == ["id_a", "id_b"] + assert len(result["sessionSummaries"]) == 2 + + def test_returns_expected_keys(self): + sess = _mock_session() + ds = _mock_dataset([sess]) + result = datasetSummary(ds) + assert set(result.keys()) == { + "numSessions", + "references", + "sessionIds", + "sessionSummaries", + } + + +# =========================================================================== +# compareDatasetSummary +# =========================================================================== + + +class TestCompareDatasetSummary: + def test_identical_summaries(self): + summary = { + "numSessions": 1, + "references": ["ref1"], + "sessionIds": ["sid1"], + "sessionSummaries": [ + { + "reference": "ref1", + "sessionId": "sid1", + "files": [], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + ], + } + report = compareDatasetSummary(summary, summary) + assert report == [] + + def test_different_num_sessions(self): + s1 = {"numSessions": 1, "references": ["r"], "sessionIds": ["s"], "sessionSummaries": []} + s2 = {"numSessions": 2, "references": ["r"], "sessionIds": ["s"], "sessionSummaries": []} + report = compareDatasetSummary(s1, s2) + assert any("numSessions" in r for r in report) + + def test_different_references(self): + s1 = { + "numSessions": 1, + "references": ["ref_a"], + "sessionIds": ["s"], + "sessionSummaries": [], + } + s2 = { + "numSessions": 1, + "references": ["ref_b"], + "sessionIds": ["s"], + "sessionSummaries": [], + } + report = compareDatasetSummary(s1, s2) + assert any("references" in r for r in report) + + def test_different_session_ids(self): + s1 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["id_a"], + "sessionSummaries": [], + } + s2 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["id_b"], + "sessionSummaries": [], + } + report = compareDatasetSummary(s1, s2) + assert any("sessionIds" in r for r in report) + + def test_different_session_summary_count(self): + ss = { + "reference": "r", + "sessionId": "s", + "files": [], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + s1 = {"numSessions": 1, "references": ["r"], "sessionIds": ["s"], "sessionSummaries": [ss]} + s2 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["s"], + "sessionSummaries": [ss, ss], + } + report = compareDatasetSummary(s1, s2) + assert any("sessionSummaries count" in r for r in report) + + def test_session_summary_field_diff(self): + ss1 = { + "reference": "ref1", + "sessionId": "sid1", + "files": ["a.txt"], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + ss2 = { + "reference": "ref1", + "sessionId": "sid1", + "files": ["b.txt"], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + s1 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["sid1"], + "sessionSummaries": [ss1], + } + s2 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["sid1"], + "sessionSummaries": [ss2], + } + report = compareDatasetSummary(s1, s2) + assert any("sessionSummaries" in r for r in report) + + def test_exclude_fields(self): + s1 = {"numSessions": 1, "references": ["r"], "sessionIds": ["s"], "sessionSummaries": []} + s2 = {"numSessions": 2, "references": ["x"], "sessionIds": ["y"], "sessionSummaries": []} + report = compareDatasetSummary( + s1, + s2, + excludeFields=["numSessions", "references", "sessionIds", "sessionSummaries"], + ) + assert report == [] + + def test_exclude_files_passed_to_session_compare(self): + ss1 = { + "reference": "r", + "sessionId": "s", + "files": ["keep.txt", "exclude.txt"], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + ss2 = { + "reference": "r", + "sessionId": "s", + "files": ["keep.txt"], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + s1 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["s"], + "sessionSummaries": [ss1], + } + s2 = { + "numSessions": 1, + "references": ["r"], + "sessionIds": ["s"], + "sessionSummaries": [ss2], + } + report = compareDatasetSummary(s1, s2, excludeFiles=["exclude.txt"]) + assert report == [] + + def test_empty_summaries_match(self): + s1 = {"numSessions": 0, "references": [], "sessionIds": [], "sessionSummaries": []} + s2 = {"numSessions": 0, "references": [], "sessionIds": [], "sessionSummaries": []} + report = compareDatasetSummary(s1, s2) + assert report == [] + + def test_session_id_matching(self): + """Session summaries are matched by sessionId, not index order.""" + ss_a = { + "reference": "ra", + "sessionId": "id_a", + "files": ["a.txt"], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + ss_b = { + "reference": "rb", + "sessionId": "id_b", + "files": ["b.txt"], + "filesInDotNDI": [], + "daqSystemNames": [], + "daqSystemDetails": [], + "probes": [], + } + # summary1 has [a, b] order, summary2 has [b, a] order + s1 = { + "numSessions": 2, + "references": ["ra", "rb"], + "sessionIds": ["id_a", "id_b"], + "sessionSummaries": [ss_a, ss_b], + } + s2 = { + "numSessions": 2, + "references": ["ra", "rb"], + "sessionIds": ["id_b", "id_a"], + "sessionSummaries": [ss_b, ss_a], + } + report = compareDatasetSummary(s1, s2) + assert report == []