Implement bedtools correctness test suite#75
Merged
conradbzura merged 6 commits intomainfrom Mar 25, 2026
Merged
Conversation
Data models, pybedtools wrapper, result comparison logic, DuckDB table loader, and pytest fixtures for validating GIQL operator correctness against bedtools. Tests skip gracefully when bedtools binary or Python dependencies are not installed. References #74
Integration tests covering INTERSECTS, MERGE, NEAREST, CLUSTER, and DISTANCE operators. Each test generates controlled genomic intervals, executes the equivalent operation via GIQL (transpiled to SQL, run on DuckDB) and bedtools (via pybedtools), then compares results. Includes strand-aware tests for INTERSECTS and NEAREST with same-strand, opposite-strand, and ignore-strand modes. Closes #74
Point containment, range containment, column-to-column, cross- chromosome, and CONTAINS ALL set predicate tests. WITHIN tests cover basic, narrow range, column-to-column, and exact boundary. References #74
DISTANCE: signed downstream/upstream, stranded with unstranded input, stranded same-strand, signed+stranded minus-strand sign flip. NEAREST: k>1, k exceeding available count, max_distance filter, standalone mode with literal reference. MERGE: distance parameter bridging gaps, stranded GIQL execution. CLUSTER: distance parameter grouping with gap tolerance. INTERSECTS: literal range, literal cross-chromosome, ANY and ALL set predicates. References #74
- Fix test_nearest_opposite_strand: remove unused duckdb_connection param, document as bedtools-only reference validation - Fix test_merge_strand_specific: now executes GIQL MERGE with stranded := true and compares against bedtools, assert == 2 - Remove unused SimulatedDataset and IntervalGeneratorConfig - Align docstrings to GIVEN/WHEN/THEN convention (all caps, no colons) - Modernize type hints: list[tuple] instead of typing.List[Tuple] - Rename bed_export.py to duckdb_loader.py - Rename format param to bed_format to avoid shadowing builtin - Add pytest.mark.integration marker via pytestmark - Add giql_query fixture to reduce load/transpile/execute boilerplate - Fix _sort_key to use sentinel tuple for None values - Remove pass from BedtoolsError (docstring suffices) - Add identifier safety comment to load_intervals - Add field layout comment to bedtool_to_tuples closest format
Ruff auto-formatted these files when they were touched by pre-commit hooks during the integration test work. Changes are purely cosmetic: trailing commas, line-length wrapping, import sorting, and a missing newline at end of file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add an integration test suite that validates GIQL operator correctness against bedtools (the de facto standard for genomic interval operations) and known expected results. Each test generates controlled genomic interval datasets, executes the equivalent operation in both GIQL (transpiled to SQL, executed via DuckDB) and the reference implementation, then compares results. Tests skip gracefully when bedtools or Python dependencies are not installed.
Closes #74
Proposed changes
Test infrastructure (
tests/integration/bedtools/utils/)data_models.py—GenomicIntervaldataclass with validation andto_tuple(),ComparisonResultwithfailure_message()for readable assertion outputbedtools_wrapper.py— Thin wrappers around pybedtools forintersect,merge, andclosestwith strand mode support andbedtool_to_tuples()convertercomparison.py— Order-independent row comparison with epsilon tolerance for floats and deterministic None handlingduckdb_loader.py—load_intervals()helper to create DuckDB tables with GIQL default column namesconftest.py—duckdb_connectionfixture (function-scoped),giql_queryfixture (load + transpile + execute in one call),pytest.importorskipguards for duckdb/pybedtools,shutil.whichguard for bedtools binary,pytestmark = pytest.mark.integrationOperator test files
test_intersect.py— 9 tests: basic overlap, partial, no overlap, adjacent (half-open boundary), multi-chromosome, literal range, literal cross-chromosome, INTERSECTS ANY, INTERSECTS ALLtest_contains.py— 5 tests: point containment, range containment, column-to-column, cross-chromosome, CONTAINS ALLtest_within.py— 4 tests: basic, narrow range, column-to-column, exact boundarytest_merge.py— 6 tests: adjacent, overlapping, separated, multi-chromosome, distance parameter, strandedtest_cluster.py— 5 tests: basic (cross-validated against bedtools merge count), separated, multi-chromosome, stranded, distance parametertest_nearest.py— 8 tests: non-overlapping, equidistant candidates, cross-chromosome, boundary (adjacent), k > 1, k > available, max_distance filter, standalone literal referencetest_distance.py— 9 tests: non-overlapping, overlapping, adjacent, cross-chromosome (NULL), signed downstream/upstream, stranded with unstranded input (NULL), stranded same-strand, signed + stranded minus-strand (sign flip)test_strand_aware.py— 8 tests: INTERSECTS same/opposite/ignore/mixed strand, NEAREST same-strand, NEAREST opposite-strand (bedtools reference), NEAREST ignore-strand, MERGE strand-specificTest cases
test_intersecta.interval INTERSECTS b.intervalbedtools intersect -utest_intersecttest_intersecttest_intersecttest_intersecttest_intersectinterval INTERSECTS 'chr1:150-220'test_intersectinterval INTERSECTS 'chr2:150-250'test_intersectinterval INTERSECTS ANY(...)test_intersectinterval INTERSECTS ALL(...)test_containsinterval CONTAINS 'chr1:150'test_containsinterval CONTAINS 'chr1:150-250'test_containsa.interval CONTAINS b.intervaltest_containsinterval CONTAINS 'chr1:150'test_containsinterval CONTAINS ALL(...)test_withininterval WITHIN 'chr1:100-300'test_withininterval WITHIN 'chr1:150-160'test_withina.interval WITHIN b.intervaltest_withininterval WITHIN 'chr1:100-200'test_mergeMERGE(interval)test_mergeMERGE(interval)test_mergeMERGE(interval)test_mergeMERGE(interval)test_mergeMERGE(interval, 100)test_mergeMERGE(interval, stranded := true)test_clusterCLUSTER(interval)test_clusterCLUSTER(interval)test_clusterCLUSTER(interval)test_clusterCLUSTER(interval, stranded := true)test_clusterCLUSTER(interval, 100)test_nearestNEAREST(b, reference := a.interval, k := 1)test_nearestNEAREST(b, reference := a.interval, k := 1)test_nearestNEAREST(b, reference := a.interval, k := 1)test_nearestNEAREST(b, reference := a.interval, k := 1)test_nearestNEAREST(b, reference := a.interval, k := 3)test_nearestNEAREST(b, reference := a.interval, k := 5)test_nearestNEAREST(b, ..., max_distance := 50)test_nearestNEAREST(t, reference := 'chr1:350-360', k := 2)test_distanceDISTANCE(a.interval, b.interval)test_distanceDISTANCE(a.interval, b.interval)test_distanceDISTANCE(a.interval, b.interval)test_distanceDISTANCE(a.interval, b.interval)test_distanceDISTANCE(..., signed := true)test_distanceDISTANCE(..., signed := true)test_distanceDISTANCE(..., stranded := true)test_distanceDISTANCE(..., stranded := true)test_distanceDISTANCE(..., signed+stranded := true)test_strand_awarea.strand = b.strandtest_strand_awarea.strand != b.strandtest_strand_awaretest_strand_awaretest_strand_awareNEAREST(..., stranded := true)test_strand_awarebedtools closest -Stest_strand_awareNEAREST(...)unstrandedtest_strand_awareMERGE(interval, stranded := true)Implementation plan
tests/integration/bedtools/withutils/subpackage)GenomicInterval,ComparisonResultinutils/data_models.pyintersect,merge,closestinutils/bedtools_wrapper.pyutils/comparison.pyutils/duckdb_loader.pyduckdb_connection,giql_queryfixtures, skip guards, integration marker