Nothing
# Tests for the streaming indexed-direct fixed-bin writer
# (Phase 4b of the large-contig fixes). The headline guarantee is
# bit-for-bit equivalence with the legacy per-chrom + pack flow.
# Build a small indexed DB from a 5-chromosome FASTA and create the
# named track via gtrack.create. Returns the path to the track dir.
restore_groot_on_exit()
.make_indexed_db_with_track <- function(track_name, bin_size = 4) {
test_fasta <- tempfile(fileext = ".fasta")
cat(">chr1\nACTGACTGACTGACTGACTGACTGACTGACTG\n",
">chr2\nGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCC\n",
">chr3\nTATATATATATATATATATATATATATATATA\n",
">chr4\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n",
">chr5\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n",
file = test_fasta, sep = ""
)
test_db <- tempfile(pattern = "misha_idx_direct_")
# Caller owns cleanup via withr::defer.
withr::with_options(list(gmulticontig.indexed_format = TRUE), {
gdb.create(groot = test_db, fasta = test_fasta, verbose = FALSE)
gdb.init(test_db)
})
# A trivial fixed-bin expression: position-derived counts.
gtrack.create(track_name, "indexed-direct test", "1", iterator = bin_size)
list(db = test_db, fasta = test_fasta)
}
test_that("indexed-direct fixed-bin matches convert-after-create bit-for-bit", {
handles_a <- .make_indexed_db_with_track("idx_direct", bin_size = 4)
withr::defer({
unlink(handles_a$db, recursive = TRUE)
unlink(handles_a$fasta)
})
direct_dir <- .track_dir("idx_direct")
direct_dat <- readBin(file.path(direct_dir, "track.dat"),
what = "raw",
n = file.info(file.path(direct_dir, "track.dat"))$size + 1
)
direct_idx <- readBin(file.path(direct_dir, "track.idx"),
what = "raw",
n = file.info(file.path(direct_dir, "track.idx"))$size + 1
)
# Comparison artifact: build a second indexed DB, hide genome.idx
# before calling gtrack.create, then convert. This forces the per-chrom
# write + pack flow that produced track.dat/track.idx prior to Phase 4b.
test_fasta <- tempfile(fileext = ".fasta")
cat(">chr1\nACTGACTGACTGACTGACTGACTGACTGACTG\n",
">chr2\nGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCC\n",
">chr3\nTATATATATATATATATATATATATATATATA\n",
">chr4\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n",
">chr5\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n",
file = test_fasta, sep = ""
)
test_db <- tempfile(pattern = "misha_idx_convert_")
withr::defer({
unlink(test_db, recursive = TRUE)
unlink(test_fasta)
})
withr::with_options(list(gmulticontig.indexed_format = TRUE), {
gdb.create(groot = test_db, fasta = test_fasta, verbose = FALSE)
gdb.init(test_db)
})
# Hide genome.idx so .gdb.is_indexed() returns FALSE and
# gtrack.create takes the per-chrom branch.
seq_dir <- file.path(test_db, "seq")
idx_path <- file.path(seq_dir, "genome.idx")
hidden_path <- file.path(seq_dir, "genome.idx.hidden")
expect_true(file.rename(idx_path, hidden_path))
withr::defer({
if (file.exists(hidden_path)) file.rename(hidden_path, idx_path)
})
gtrack.create("idx_via_convert", "via convert", "1", iterator = 4)
# Restore idx, then run convert (mirroring the legacy two-pass flow).
file.rename(hidden_path, idx_path)
gdb.reload()
gtrack.convert_to_indexed("idx_via_convert")
convert_dir <- .track_dir("idx_via_convert")
convert_dat <- readBin(file.path(convert_dir, "track.dat"),
what = "raw",
n = file.info(file.path(convert_dir, "track.dat"))$size + 1
)
convert_idx <- readBin(file.path(convert_dir, "track.idx"),
what = "raw",
n = file.info(file.path(convert_dir, "track.idx"))$size + 1
)
expect_identical(direct_dat, convert_dat)
expect_identical(direct_idx, convert_idx)
})
test_that("indexed-direct: no per-chrom files left over", {
handles <- .make_indexed_db_with_track("idx_clean", bin_size = 4)
withr::defer({
unlink(handles$db, recursive = TRUE)
unlink(handles$fasta)
})
dir_contents <- list.files(.track_dir("idx_clean"), all.files = TRUE, no.. = TRUE)
# Only track.dat + track.idx + .attributes should remain.
expect_setequal(dir_contents, c("track.dat", "track.idx", ".attributes"))
})
test_that("indexed-direct produces a working track (basic functional checks)", {
handles <- .make_indexed_db_with_track("idx_func", bin_size = 4)
withr::defer({
unlink(handles$db, recursive = TRUE)
unlink(handles$fasta)
})
info <- gtrack.info("idx_func")
expect_equal(info$type, "dense")
val <- gextract("idx_func", intervals = gintervals.all(), iterator = 4)
expect_gt(nrow(val), 0)
# Track expression "1" -> every bin should be 1.
expect_true(all(val$idx_func == 1))
# Coarser iterator should still produce data.
val_coarse <- gextract("idx_func", intervals = gintervals.all(), iterator = 8)
expect_gt(nrow(val_coarse), 0)
})
test_that("indexed-direct track.idx has one entry per chromosome", {
# Sanity: track.idx must contain N entries (N = number of chroms in
# the genome), matching what gtrack_pack_per_chrom_to_indexed
# produces. Header size + entry table size are deterministic.
handles <- .make_indexed_db_with_track("idx_full", bin_size = 4)
withr::defer({
unlink(handles$db, recursive = TRUE)
unlink(handles$fasta)
})
idx_path <- file.path(.track_dir("idx_full"), "track.idx")
idx_size <- file.info(idx_path)$size
# Header is 36 bytes (see TrackIndex.h). Each entry is 24 bytes.
n_chroms <- nrow(gintervals.all())
expect_equal(idx_size, 36 + 24 * n_chroms)
})
# ---------------------------------------------------------------------
# Sparse (INTERVALS1D iterator) - Phase 4c.
# Builds a small indexed DB, creates a sparse iterator source via
# gtrack.create_sparse, then exercises the iterator-based gtrack.create
# (INTERVALS1D path) on both the indexed-direct and convert-after-create
# flows; asserts bit-for-bit equivalence.
# Build a small indexed DB and create a sparse "source" track via
# gtrack.create_sparse (which writes the indexed format directly via
# its own C path, unrelated to GenomeTrackIndexedWriter). Returns
# handles + the source track name. Source intervals deliberately skip
# some chroms to exercise the missing-chrom offset logic.
.make_indexed_db_with_sparse_source <- function(src_name = "src_sparse") {
test_fasta <- tempfile(fileext = ".fasta")
cat(">chr1\nACTGACTGACTGACTGACTGACTGACTGACTG\n",
">chr2\nGGCCGGCCGGCCGGCCGGCCGGCCGGCCGGCC\n",
">chr3\nTATATATATATATATATATATATATATATATA\n",
">chr4\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n",
">chr5\nCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n",
file = test_fasta, sep = ""
)
test_db <- tempfile(pattern = "misha_idx_sparse_")
withr::with_options(list(gmulticontig.indexed_format = TRUE), {
gdb.create(groot = test_db, fasta = test_fasta, verbose = FALSE)
gdb.init(test_db)
})
# Sparse source intervals on chr1, chr3, chr5 only - chr2 and chr4
# are deliberately absent to exercise the missing-chrom offset path
# in finalize().
src_intervs <- data.frame(
chrom = c("chr1", "chr1", "chr3", "chr5", "chr5"),
start = c(0L, 16L, 8L, 0L, 20L),
end = c(8L, 24L, 24L, 12L, 28L),
stringsAsFactors = FALSE
)
gtrack.create_sparse(src_name, "sparse source", src_intervs,
values = seq_len(nrow(src_intervs))
)
list(db = test_db, fasta = test_fasta, src = src_name)
}
test_that("indexed-direct sparse matches convert-after-create bit-for-bit", {
handles_a <- .make_indexed_db_with_sparse_source("src_sparse")
withr::defer({
unlink(handles_a$db, recursive = TRUE)
unlink(handles_a$fasta)
})
# Direct path: gtrack.create on the indexed DB. Note: the source
# track's data is on chr1, chr3, chr5; the iterator scope is
# gintervals.all() so chr2 and chr4 must be backfilled with
# header-only entries.
gtrack.create("idx_sparse_direct", "via direct", "src_sparse",
iterator = "src_sparse"
)
direct_dir <- .track_dir("idx_sparse_direct")
direct_dat <- readBin(file.path(direct_dir, "track.dat"),
what = "raw",
n = file.info(file.path(direct_dir, "track.dat"))$size + 1
)
direct_idx <- readBin(file.path(direct_dir, "track.idx"),
what = "raw",
n = file.info(file.path(direct_dir, "track.idx"))$size + 1
)
# Comparison artifact in a SECOND indexed DB: re-create source +
# consumer by hiding genome.idx before the consumer create so the
# per-chrom + convert path runs.
handles_b <- .make_indexed_db_with_sparse_source("src_sparse")
withr::defer({
unlink(handles_b$db, recursive = TRUE)
unlink(handles_b$fasta)
})
seq_dir <- file.path(handles_b$db, "seq")
idx_path <- file.path(seq_dir, "genome.idx")
hidden_path <- file.path(seq_dir, "genome.idx.hidden")
expect_true(file.rename(idx_path, hidden_path))
withr::defer({
if (file.exists(hidden_path)) file.rename(hidden_path, idx_path)
})
gtrack.create("idx_sparse_via_convert", "via convert", "src_sparse",
iterator = "src_sparse"
)
# Restore idx, then convert (mirroring legacy two-pass flow).
file.rename(hidden_path, idx_path)
gdb.reload()
gtrack.convert_to_indexed("idx_sparse_via_convert")
convert_dir <- .track_dir("idx_sparse_via_convert")
convert_dat <- readBin(file.path(convert_dir, "track.dat"),
what = "raw",
n = file.info(file.path(convert_dir, "track.dat"))$size + 1
)
convert_idx <- readBin(file.path(convert_dir, "track.idx"),
what = "raw",
n = file.info(file.path(convert_dir, "track.idx"))$size + 1
)
expect_identical(direct_dat, convert_dat)
expect_identical(direct_idx, convert_idx)
# Functional equivalence (open in handles_b, which has both).
intv <- gintervals.all()
r_direct <- gextract("idx_sparse_via_convert", intervals = intv)
expect_true(nrow(r_direct) > 0)
})
test_that("indexed-direct sparse: no per-chrom files left over", {
handles <- .make_indexed_db_with_sparse_source("src_sparse_clean")
withr::defer({
unlink(handles$db, recursive = TRUE)
unlink(handles$fasta)
})
gtrack.create("idx_sparse_clean", "no leftover", "src_sparse_clean",
iterator = "src_sparse_clean"
)
dir_contents <- list.files(.track_dir("idx_sparse_clean"),
all.files = TRUE, no.. = TRUE
)
expect_setequal(dir_contents, c("track.dat", "track.idx", ".attributes"))
})
test_that("indexed-direct sparse produces a working track", {
handles <- .make_indexed_db_with_sparse_source("src_sparse_func")
withr::defer({
unlink(handles$db, recursive = TRUE)
unlink(handles$fasta)
})
gtrack.create("idx_sparse_func", "functional", "src_sparse_func",
iterator = "src_sparse_func"
)
info <- gtrack.info("idx_sparse_func")
expect_equal(info$type, "sparse")
val <- gextract("idx_sparse_func", intervals = gintervals.all())
expect_gt(nrow(val), 0)
expect_true(all(!is.na(val$idx_sparse_func)))
})
# ---------------------------------------------------------------------
# Liftover (gtrack.liftover) - Phase 6a.
# Builds a source DB with a sparse (or dense) track, an indexed target DB,
# and a chain file mapping a region of source -> target. Lifts the source
# track into the target DB twice: once with the target idx visible (direct
# indexed write path) and once with target idx hidden so the per-chrom +
# convert path runs. Asserts bit-for-bit equivalence of track.dat and
# track.idx.
# Build a small source DB with a sparse source track. Caller is
# responsible for the resulting db/fasta cleanup via withr::defer.
.make_liftover_source_sparse <- function(src_name) {
test_fasta <- tempfile(fileext = ".fasta")
cat(">chrSrc1\n", paste(rep("A", 64), collapse = ""), "\n",
">chrSrc2\n", paste(rep("C", 64), collapse = ""), "\n",
file = test_fasta, sep = ""
)
test_db <- tempfile(pattern = "misha_liftover_src_sparse_")
withr::with_options(list(gmulticontig.indexed_format = TRUE), {
gdb.create(groot = test_db, fasta = test_fasta, verbose = FALSE)
gdb.init(test_db)
})
src_intervs <- data.frame(
chrom = c("chrSrc1", "chrSrc1", "chrSrc1", "chrSrc2", "chrSrc2"),
start = c(0L, 16L, 40L, 0L, 32L),
end = c(8L, 24L, 56L, 16L, 48L),
stringsAsFactors = FALSE
)
gtrack.create_sparse(src_name, "sparse source", src_intervs,
values = c(1.5, 2.25, 3.0, 4.125, 5.5)
)
list(db = test_db, fasta = test_fasta, src = src_name)
}
# Build an empty indexed target DB on >2 chroms (one of which the chain
# will not cover, exercising the header-only empty-chrom path).
.make_liftover_target_db <- function() {
test_fasta <- tempfile(fileext = ".fasta")
cat(">chr1\n", paste(rep("T", 96), collapse = ""), "\n",
">chr2\n", paste(rep("G", 96), collapse = ""), "\n",
">chr3\n", paste(rep("A", 96), collapse = ""), "\n",
file = test_fasta, sep = ""
)
test_db <- tempfile(pattern = "misha_liftover_tgt_")
withr::with_options(list(gmulticontig.indexed_format = TRUE), {
gdb.create(groot = test_db, fasta = test_fasta, verbose = FALSE)
gdb.init(test_db)
})
list(db = test_db, fasta = test_fasta)
}
.write_liftover_chain <- function(chain_file) {
# chain 1: chrSrc1[0..56) + -> chr1[0..56) + (length 56)
cat("chain 1000 chrSrc1 64 + 0 56 chr1 96 + 0 56 1\n",
"56\n\n",
sep = "", file = chain_file
)
# chain 2: chrSrc2[0..48) + -> chr2[0..48) + (length 48)
# Note: chr3 is intentionally NOT covered by any chain - exercises the
# empty-chrom header-only path in the indexed writer.
cat("chain 1000 chrSrc2 64 + 0 48 chr2 96 + 0 48 2\n",
"48\n\n",
sep = "", file = chain_file, append = TRUE
)
}
test_that("indexed-direct sparse liftover matches convert-after-create bit-for-bit", {
# --- Source DB (shared by both runs) ---
src_handles <- .make_liftover_source_sparse("src_for_liftover")
withr::defer({
unlink(src_handles$db, recursive = TRUE)
unlink(src_handles$fasta)
})
src_track_dir <- file.path(src_handles$db, "tracks", "src_for_liftover.track")
# --- Chain file (shared) ---
chain_file <- tempfile(fileext = ".chain")
withr::defer(unlink(chain_file))
.write_liftover_chain(chain_file)
# --- Direct path: liftover into a fresh indexed target DB ---
tgt_a <- .make_liftover_target_db()
withr::defer({
unlink(tgt_a$db, recursive = TRUE)
unlink(tgt_a$fasta)
})
gtrack.liftover("lifted_direct", "via direct", src_track_dir, chain_file)
direct_dir <- .track_dir("lifted_direct")
expect_true(file.exists(file.path(direct_dir, "track.dat")))
expect_true(file.exists(file.path(direct_dir, "track.idx")))
direct_dat <- readBin(file.path(direct_dir, "track.dat"),
what = "raw",
n = file.info(file.path(direct_dir, "track.dat"))$size + 1
)
direct_idx <- readBin(file.path(direct_dir, "track.idx"),
what = "raw",
n = file.info(file.path(direct_dir, "track.idx"))$size + 1
)
# --- Convert path: build a second target DB, hide genome.idx, lift,
# restore idx, convert.
tgt_b <- .make_liftover_target_db()
withr::defer({
unlink(tgt_b$db, recursive = TRUE)
unlink(tgt_b$fasta)
})
seq_dir <- file.path(tgt_b$db, "seq")
idx_path <- file.path(seq_dir, "genome.idx")
hidden_path <- file.path(seq_dir, "genome.idx.hidden")
expect_true(file.rename(idx_path, hidden_path))
withr::defer({
if (file.exists(hidden_path)) file.rename(hidden_path, idx_path)
})
gdb.reload()
gtrack.liftover("lifted_via_convert", "via convert", src_track_dir, chain_file)
# Restore idx + reload + convert.
file.rename(hidden_path, idx_path)
gdb.reload()
gtrack.convert_to_indexed("lifted_via_convert")
convert_dir <- .track_dir("lifted_via_convert")
convert_dat <- readBin(file.path(convert_dir, "track.dat"),
what = "raw",
n = file.info(file.path(convert_dir, "track.dat"))$size + 1
)
convert_idx <- readBin(file.path(convert_dir, "track.idx"),
what = "raw",
n = file.info(file.path(convert_dir, "track.idx"))$size + 1
)
expect_identical(direct_dat, convert_dat)
expect_identical(direct_idx, convert_idx)
})
test_that("indexed-direct liftover: no per-chrom files left over", {
src_handles <- .make_liftover_source_sparse("src_clean_liftover")
withr::defer({
unlink(src_handles$db, recursive = TRUE)
unlink(src_handles$fasta)
})
src_track_dir <- file.path(src_handles$db, "tracks", "src_clean_liftover.track")
chain_file <- tempfile(fileext = ".chain")
withr::defer(unlink(chain_file))
.write_liftover_chain(chain_file)
tgt <- .make_liftover_target_db()
withr::defer({
unlink(tgt$db, recursive = TRUE)
unlink(tgt$fasta)
})
gtrack.liftover("lifted_clean", "no leftover", src_track_dir, chain_file)
dir_contents <- list.files(.track_dir("lifted_clean"),
all.files = TRUE, no.. = TRUE
)
expect_setequal(dir_contents, c("track.dat", "track.idx", ".attributes"))
})
test_that("indexed-direct liftover: gextract returns identical values both ways", {
src_handles <- .make_liftover_source_sparse("src_func_liftover")
withr::defer({
unlink(src_handles$db, recursive = TRUE)
unlink(src_handles$fasta)
})
src_track_dir <- file.path(src_handles$db, "tracks", "src_func_liftover.track")
chain_file <- tempfile(fileext = ".chain")
withr::defer(unlink(chain_file))
.write_liftover_chain(chain_file)
# Run 1: direct
tgt_a <- .make_liftover_target_db()
withr::defer({
unlink(tgt_a$db, recursive = TRUE)
unlink(tgt_a$fasta)
})
gtrack.liftover("lifted_func_direct", "direct", src_track_dir, chain_file)
res_direct <- gextract("lifted_func_direct", gintervals.all())
# Run 2: hidden idx + convert
tgt_b <- .make_liftover_target_db()
withr::defer({
unlink(tgt_b$db, recursive = TRUE)
unlink(tgt_b$fasta)
})
seq_dir <- file.path(tgt_b$db, "seq")
idx_path <- file.path(seq_dir, "genome.idx")
hidden_path <- file.path(seq_dir, "genome.idx.hidden")
expect_true(file.rename(idx_path, hidden_path))
withr::defer({
if (file.exists(hidden_path)) file.rename(hidden_path, idx_path)
})
gdb.reload()
gtrack.liftover("lifted_func_convert", "convert", src_track_dir, chain_file)
file.rename(hidden_path, idx_path)
gdb.reload()
gtrack.convert_to_indexed("lifted_func_convert")
res_convert <- gextract("lifted_func_convert", gintervals.all())
# Strip the (different) track-name column from each frame, compare the rest.
res_direct$lifted_func_direct <- NULL
res_convert$lifted_func_convert <- NULL
expect_equal(
res_direct[order(res_direct$chrom, res_direct$start), ],
res_convert[order(res_convert$chrom, res_convert$start), ],
ignore_attr = TRUE
)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.