use noodles_sam::alignment::{
record::{
cigar::{op::Kind, Op},
data::field::Tag,
},
record_buf::{data::field::Value, Cigar, Data, QualityScores, Sequence},
RecordBuf,
};
use crate::core::types::MutationType;
#[derive(Debug, Clone, PartialEq)]
pub enum ModifyResult {
Modified,
Unchanged,
Skipped,
}
pub fn apply_snv(record: &mut RecordBuf, mutation: &MutationType) -> ModifyResult {
let (pos, ref_base, alt_base) = match mutation {
MutationType::Snv {
pos,
ref_base,
alt_base,
} => (*pos, *ref_base, *alt_base),
_ => return ModifyResult::Unchanged,
};
let align_start = match record.alignment_start() {
Some(p) => usize::from(p) as u64 - 1, None => return ModifyResult::Unchanged, };
let seq_len = record.sequence().len();
if seq_len == 0 {
return ModifyResult::Unchanged;
}
let offset = match cigar_ref_to_read_offset(record.cigar().as_ref(), align_start, pos) {
Some(o) => o,
None => return ModifyResult::Unchanged,
};
if offset >= seq_len {
return ModifyResult::Unchanged;
}
let bq = record
.quality_scores()
.as_ref()
.get(offset)
.copied()
.unwrap_or(0);
if bq < 20 {
return ModifyResult::Skipped;
}
let current_base = sequence_base(record.sequence(), offset);
if current_base != ref_base {
return ModifyResult::Unchanged;
}
let mut new_seq: Vec<u8> = record.sequence().as_ref().to_vec();
new_seq[offset] = alt_base;
let new_quals: Vec<u8> = record.quality_scores().as_ref().to_vec();
*record.sequence_mut() = Sequence::from(new_seq.as_slice());
*record.quality_scores_mut() = QualityScores::from(new_quals);
update_nm_tag(record, 1);
update_md_for_snv(record, offset, ref_base, align_start);
ModifyResult::Modified
}
pub fn apply_insertion(record: &mut RecordBuf, mutation: &MutationType) -> ModifyResult {
let (pos, ref_seq, alt_seq) = match mutation {
MutationType::Indel {
pos,
ref_seq,
alt_seq,
} if alt_seq.len() > ref_seq.len() => (*pos, ref_seq.clone(), alt_seq.clone()),
_ => return ModifyResult::Unchanged,
};
let align_start = match record.alignment_start() {
Some(p) => usize::from(p) as u64 - 1,
None => return ModifyResult::Unchanged,
};
let seq_len = record.sequence().len();
if seq_len == 0 {
return ModifyResult::Unchanged;
}
let offset = match cigar_ref_to_read_offset(record.cigar().as_ref(), align_start, pos) {
Some(o) => o,
None => return ModifyResult::Unchanged,
};
if offset >= seq_len {
return ModifyResult::Unchanged;
}
if offset < ref_seq.len() || sequence_base(record.sequence(), offset) != ref_seq[0] {
if !ref_seq.is_empty() && sequence_base(record.sequence(), offset) != ref_seq[0] {
return ModifyResult::Unchanged;
}
}
let insert_len = alt_seq.len() - ref_seq.len();
let orig_seq: Vec<u8> = record.sequence().as_ref().to_vec();
let orig_qual: Vec<u8> = record.quality_scores().as_ref().to_vec();
let mut new_seq = Vec::with_capacity(seq_len + insert_len);
new_seq.extend_from_slice(&orig_seq[..offset]);
new_seq.extend_from_slice(&alt_seq);
if offset + ref_seq.len() < orig_seq.len() {
new_seq.extend_from_slice(&orig_seq[offset + ref_seq.len()..]);
}
let avg_qual = if offset > 0 && offset < orig_qual.len() {
((orig_qual[offset - 1] as u16 + orig_qual[offset] as u16) / 2) as u8
} else {
orig_qual.get(offset).copied().unwrap_or(30)
};
let mut new_qual = Vec::with_capacity(seq_len + insert_len);
new_qual.extend_from_slice(&orig_qual[..offset]);
for _ in 0..alt_seq.len() {
new_qual.push(avg_qual);
}
if offset + ref_seq.len() < orig_qual.len() {
new_qual.extend_from_slice(&orig_qual[offset + ref_seq.len()..]);
}
new_seq.truncate(seq_len);
new_qual.truncate(seq_len);
*record.sequence_mut() = Sequence::from(new_seq.as_slice());
*record.quality_scores_mut() = QualityScores::from(new_qual);
update_cigar_for_insertion(record, offset, insert_len, seq_len);
update_nm_tag(record, insert_len as i32);
clear_md_tag(record);
ModifyResult::Modified
}
pub fn apply_deletion(record: &mut RecordBuf, mutation: &MutationType) -> ModifyResult {
let (pos, ref_seq, alt_seq) = match mutation {
MutationType::Indel {
pos,
ref_seq,
alt_seq,
} if ref_seq.len() > alt_seq.len() => (*pos, ref_seq.clone(), alt_seq.clone()),
_ => return ModifyResult::Unchanged,
};
let align_start = match record.alignment_start() {
Some(p) => usize::from(p) as u64 - 1,
None => return ModifyResult::Unchanged,
};
let seq_len = record.sequence().len();
if seq_len == 0 {
return ModifyResult::Unchanged;
}
let offset = match cigar_ref_to_read_offset(record.cigar().as_ref(), align_start, pos) {
Some(o) => o,
None => return ModifyResult::Unchanged,
};
if offset >= seq_len {
return ModifyResult::Unchanged;
}
let del_len = ref_seq.len() - alt_seq.len();
let orig_seq: Vec<u8> = record.sequence().as_ref().to_vec();
let orig_qual: Vec<u8> = record.quality_scores().as_ref().to_vec();
let anchor_end = offset + alt_seq.len();
let deleted_end = offset + ref_seq.len();
let mut new_seq = Vec::with_capacity(seq_len);
new_seq.extend_from_slice(&orig_seq[..anchor_end]);
if deleted_end < orig_seq.len() {
new_seq.extend_from_slice(&orig_seq[deleted_end..]);
}
let mut new_qual = Vec::with_capacity(seq_len);
new_qual.extend_from_slice(&orig_qual[..anchor_end]);
if deleted_end < orig_qual.len() {
new_qual.extend_from_slice(&orig_qual[deleted_end..]);
}
let fill_qual = orig_qual.last().copied().unwrap_or(30);
while new_seq.len() < seq_len {
new_seq.push(b'N');
new_qual.push(fill_qual);
}
new_seq.truncate(seq_len);
new_qual.truncate(seq_len);
*record.sequence_mut() = Sequence::from(new_seq.as_slice());
*record.quality_scores_mut() = QualityScores::from(new_qual);
update_cigar_for_deletion(record, offset, del_len, seq_len);
update_nm_tag(record, del_len as i32);
clear_md_tag(record);
ModifyResult::Modified
}
pub fn cigar_ref_to_read_offset(cigar: &[Op], align_start: u64, ref_pos: u64) -> Option<usize> {
if ref_pos < align_start {
return None;
}
let mut ref_cursor = align_start;
let mut read_cursor: usize = 0;
for op in cigar {
let len = op.len();
match op.kind() {
Kind::Match | Kind::SequenceMatch | Kind::SequenceMismatch => {
if ref_pos >= ref_cursor && ref_pos < ref_cursor + len as u64 {
let delta = (ref_pos - ref_cursor) as usize;
return Some(read_cursor + delta);
}
ref_cursor += len as u64;
read_cursor += len;
}
Kind::Insertion | Kind::SoftClip => {
read_cursor += len;
}
Kind::Deletion | Kind::Skip => {
if ref_pos >= ref_cursor && ref_pos < ref_cursor + len as u64 {
return None; }
ref_cursor += len as u64;
}
Kind::HardClip | Kind::Pad => {
}
}
}
None
}
#[cfg(test)]
pub fn cigar_to_string(ops: &[Op]) -> String {
ops.iter()
.map(|op| {
let ch = match op.kind() {
Kind::Match => 'M',
Kind::Insertion => 'I',
Kind::Deletion => 'D',
Kind::Skip => 'N',
Kind::SoftClip => 'S',
Kind::HardClip => 'H',
Kind::Pad => 'P',
Kind::SequenceMatch => '=',
Kind::SequenceMismatch => 'X',
};
format!("{}{}", op.len(), ch)
})
.collect::<Vec<_>>()
.join("")
}
fn update_cigar_for_insertion(
record: &mut RecordBuf,
read_offset: usize,
insert_len: usize,
read_len: usize,
) {
let old_ops: Vec<Op> = record.cigar().as_ref().to_vec();
let mut new_ops: Vec<Op> = Vec::new();
let mut read_cursor: usize = 0;
let mut inserted = false;
for op in &old_ops {
if inserted {
new_ops.push(*op);
continue;
}
let op_read_len = if op.kind().consumes_read() {
op.len()
} else {
0
};
if read_cursor + op_read_len > read_offset && !inserted {
let before = read_offset - read_cursor;
let after = op_read_len - before;
if before > 0 {
new_ops.push(Op::new(op.kind(), before));
}
new_ops.push(Op::new(Kind::Insertion, insert_len));
inserted = true;
let remaining_read = read_len.saturating_sub(read_offset + insert_len);
if op.kind().consumes_read() {
let after_clamped = after.min(remaining_read);
if after_clamped > 0 {
new_ops.push(Op::new(op.kind(), after_clamped));
}
} else if op.kind().consumes_reference() {
new_ops.push(*op);
}
read_cursor += op_read_len;
} else {
new_ops.push(*op);
read_cursor += op_read_len;
}
}
if !inserted {
new_ops.push(Op::new(Kind::Insertion, insert_len));
}
let merged = merge_cigar_ops(new_ops);
*record.cigar_mut() = merged.into_iter().collect::<Cigar>();
}
fn update_cigar_for_deletion(
record: &mut RecordBuf,
read_offset: usize,
del_len: usize,
_read_len: usize,
) {
let old_ops: Vec<Op> = record.cigar().as_ref().to_vec();
let mut new_ops: Vec<Op> = Vec::new();
let mut read_cursor: usize = 0;
let mut deleted = false;
for op in &old_ops {
if deleted {
new_ops.push(*op);
continue;
}
let op_read_len = if op.kind().consumes_read() {
op.len()
} else {
0
};
if op.kind().consumes_read() && read_cursor + op_read_len > read_offset && !deleted {
let before = read_offset - read_cursor;
let after = op_read_len - before;
if before > 0 {
new_ops.push(Op::new(op.kind(), before));
}
new_ops.push(Op::new(Kind::Deletion, del_len));
deleted = true;
if after > 0 {
new_ops.push(Op::new(op.kind(), after));
}
read_cursor += op_read_len;
} else {
new_ops.push(*op);
read_cursor += op_read_len;
}
}
if !deleted {
new_ops.push(Op::new(Kind::Deletion, del_len));
}
let merged = merge_cigar_ops(new_ops);
*record.cigar_mut() = merged.into_iter().collect::<Cigar>();
}
fn merge_cigar_ops(ops: Vec<Op>) -> Vec<Op> {
let mut result: Vec<Op> = Vec::new();
for op in ops {
match result.last_mut() {
Some(last) if last.kind() == op.kind() => {
*last = Op::new(last.kind(), last.len() + op.len());
}
_ => result.push(op),
}
}
result
}
fn update_nm_tag(record: &mut RecordBuf, delta: i32) {
let current = record
.data()
.get(&Tag::EDIT_DISTANCE)
.and_then(|v| v.as_int())
.unwrap_or(0);
let new_nm = (current + delta as i64).max(0) as i32;
let mut data: Data = record
.data()
.iter()
.filter(|(t, _)| *t != Tag::EDIT_DISTANCE)
.map(|(t, v)| (t, v.clone()))
.collect();
data.insert(Tag::EDIT_DISTANCE, Value::from(new_nm));
*record.data_mut() = data;
}
fn update_md_for_snv(record: &mut RecordBuf, read_offset: usize, ref_base: u8, _align_start: u64) {
let md_str = match record.data().get(&Tag::MISMATCHED_POSITIONS) {
Some(Value::String(s)) => String::from_utf8(s.iter().copied().collect()).ok(),
_ => None,
};
if let Some(md) = md_str {
let new_md = inject_snv_into_md(&md, read_offset, ref_base);
let mut data: Data = record
.data()
.iter()
.filter(|(t, _)| *t != Tag::MISMATCHED_POSITIONS)
.map(|(t, v)| (t, v.clone()))
.collect();
data.insert(
Tag::MISMATCHED_POSITIONS,
Value::String(new_md.into_bytes().into()),
);
*record.data_mut() = data;
}
}
#[derive(Debug, PartialEq)]
enum MdToken {
Run(usize),
Mismatch(u8),
Deletion(Vec<u8>),
}
fn parse_md(md: &str) -> Vec<MdToken> {
let bytes = md.as_bytes();
let mut tokens = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let n: usize = md[start..i].parse().unwrap_or(0);
tokens.push(MdToken::Run(n));
} else if bytes[i] == b'^' {
i += 1;
let start = i;
while i < bytes.len() && bytes[i].is_ascii_uppercase() {
i += 1;
}
tokens.push(MdToken::Deletion(bytes[start..i].to_vec()));
} else if bytes[i].is_ascii_uppercase() {
tokens.push(MdToken::Mismatch(bytes[i]));
i += 1;
} else {
i += 1;
}
}
tokens
}
fn serialise_md(tokens: &[MdToken]) -> String {
let mut out = String::new();
let mut last_was_nonrun = false;
for token in tokens {
match token {
MdToken::Run(n) => {
out.push_str(&n.to_string());
last_was_nonrun = false;
}
MdToken::Mismatch(b) => {
if last_was_nonrun {
out.push('0');
}
out.push(*b as char);
last_was_nonrun = true;
}
MdToken::Deletion(bases) => {
if last_was_nonrun {
out.push('0');
}
out.push('^');
for &b in bases {
out.push(b as char);
}
last_was_nonrun = true;
}
}
}
out
}
fn inject_snv_into_md(md: &str, read_offset: usize, ref_base: u8) -> String {
let tokens = parse_md(md);
let mut new_tokens: Vec<MdToken> = Vec::new();
let mut cursor: usize = 0;
let mut injected = false;
for token in tokens {
if injected {
new_tokens.push(token);
continue;
}
match token {
MdToken::Run(n) => {
if cursor + n > read_offset {
let before = read_offset - cursor;
let after = n - before - 1; new_tokens.push(MdToken::Run(before));
new_tokens.push(MdToken::Mismatch(ref_base));
new_tokens.push(MdToken::Run(after));
injected = true;
cursor += n;
} else {
cursor += n;
new_tokens.push(MdToken::Run(n));
}
}
MdToken::Mismatch(b) => {
if cursor == read_offset {
new_tokens.push(MdToken::Mismatch(ref_base));
injected = true;
} else {
new_tokens.push(MdToken::Mismatch(b));
}
cursor += 1;
}
MdToken::Deletion(bases) => {
new_tokens.push(MdToken::Deletion(bases));
}
}
}
serialise_md(&new_tokens)
}
fn clear_md_tag(record: &mut RecordBuf) {
let data: Data = record
.data()
.iter()
.filter(|(t, _)| *t != Tag::MISMATCHED_POSITIONS)
.map(|(t, v)| (t, v.clone()))
.collect();
*record.data_mut() = data;
}
fn sequence_base(seq: &Sequence, offset: usize) -> u8 {
seq.as_ref().get(offset).copied().unwrap_or(b'N')
}
#[cfg(test)]
mod tests {
use super::*;
use noodles_core::Position;
use noodles_sam::alignment::{
record::Flags,
record_buf::{Cigar, Sequence},
RecordBuf,
};
fn make_record(seq: &[u8], align_start: u64, cigar_str: &str) -> RecordBuf {
let cigar_ops = parse_cigar_str(cigar_str);
RecordBuf::builder()
.set_flags(Flags::empty())
.set_alignment_start(Position::new(align_start as usize + 1).unwrap())
.set_cigar(cigar_ops.into_iter().collect::<Cigar>())
.set_sequence(Sequence::from(seq))
.set_quality_scores(QualityScores::from(vec![30u8; seq.len()]))
.build()
}
fn make_record_with_nm(seq: &[u8], align_start: u64, cigar_str: &str, nm: i32) -> RecordBuf {
let cigar_ops = parse_cigar_str(cigar_str);
let mut data = Data::default();
data.insert(Tag::EDIT_DISTANCE, Value::from(nm));
RecordBuf::builder()
.set_flags(Flags::empty())
.set_alignment_start(Position::new(align_start as usize + 1).unwrap())
.set_cigar(cigar_ops.into_iter().collect::<Cigar>())
.set_sequence(Sequence::from(seq))
.set_quality_scores(QualityScores::from(vec![30u8; seq.len()]))
.set_data(data)
.build()
}
fn parse_cigar_str(s: &str) -> Vec<Op> {
crate::io::bam::parse_cigar(s).expect("valid cigar")
}
fn get_seq(record: &RecordBuf) -> Vec<u8> {
record.sequence().as_ref().to_vec()
}
fn get_cigar_str(record: &RecordBuf) -> String {
let ops: Vec<Op> = record.cigar().as_ref().to_vec();
cigar_to_string(&ops)
}
#[test]
fn test_apply_snv_basic() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Snv {
pos: 102,
ref_base: b'G',
alt_base: b'T',
};
let result = apply_snv(&mut record, &mutation);
assert_eq!(result, ModifyResult::Modified);
let seq = get_seq(&record);
assert_eq!(&seq, b"ACTTACGT", "base at offset 2 should change G->T");
}
#[test]
fn test_apply_snv_preserves_quality() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let orig_quals: Vec<u8> = record.quality_scores().as_ref().to_vec();
let mutation = MutationType::Snv {
pos: 102,
ref_base: b'G',
alt_base: b'T',
};
apply_snv(&mut record, &mutation);
let new_quals: Vec<u8> = record.quality_scores().as_ref().to_vec();
assert_eq!(
orig_quals, new_quals,
"quality scores must be preserved for SNV"
);
}
#[test]
fn test_apply_snv_wrong_ref_base_unchanged() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Snv {
pos: 102,
ref_base: b'T',
alt_base: b'A',
};
let result = apply_snv(&mut record, &mutation);
assert_eq!(result, ModifyResult::Unchanged);
let seq = get_seq(&record);
assert_eq!(&seq, b"ACGTACGT", "unchanged when ref base doesn't match");
}
#[test]
fn test_apply_snv_out_of_range() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Snv {
pos: 200,
ref_base: b'A',
alt_base: b'T',
};
let result = apply_snv(&mut record, &mutation);
assert_eq!(result, ModifyResult::Unchanged);
}
#[test]
fn test_apply_snv_updates_nm_tag() {
let mut record = make_record_with_nm(b"ACGTACGT", 100, "8M", 0);
let mutation = MutationType::Snv {
pos: 100,
ref_base: b'A',
alt_base: b'C',
};
apply_snv(&mut record, &mutation);
let nm = record
.data()
.get(&Tag::EDIT_DISTANCE)
.and_then(|v| v.as_int())
.unwrap_or(-1);
assert_eq!(nm, 1, "NM should be incremented to 1 after SNV");
}
#[test]
fn test_apply_snv_nm_increments_from_existing() {
let mut record = make_record_with_nm(b"ACGTACGT", 100, "8M", 2);
let mutation = MutationType::Snv {
pos: 100,
ref_base: b'A',
alt_base: b'C',
};
apply_snv(&mut record, &mutation);
let nm = record
.data()
.get(&Tag::EDIT_DISTANCE)
.and_then(|v| v.as_int())
.unwrap_or(-1);
assert_eq!(nm, 3, "NM should increment from existing value");
}
#[test]
fn test_apply_insertion_basic() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Indel {
pos: 102,
ref_seq: vec![b'G'],
alt_seq: vec![b'G', b'T', b'T'],
};
let result = apply_insertion(&mut record, &mutation);
assert_eq!(result, ModifyResult::Modified);
let seq = get_seq(&record);
assert_eq!(seq.len(), 8, "read length preserved after insertion");
assert_eq!(&seq[..5], b"ACGTT", "insertion visible at offset 2");
}
#[test]
fn test_apply_insertion_updates_cigar() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Indel {
pos: 102,
ref_seq: vec![b'G'],
alt_seq: vec![b'G', b'T', b'T'],
};
apply_insertion(&mut record, &mutation);
let cigar = get_cigar_str(&record);
assert!(
cigar.contains('I'),
"CIGAR should contain an insertion op: {cigar}"
);
}
#[test]
fn test_apply_deletion_basic() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Indel {
pos: 102,
ref_seq: vec![b'G', b'T'],
alt_seq: vec![b'G'],
};
let result = apply_deletion(&mut record, &mutation);
assert_eq!(result, ModifyResult::Modified);
let seq = get_seq(&record);
assert_eq!(seq.len(), 8, "read length preserved after deletion");
assert_eq!(&seq[..3], b"ACG", "bases before deletion unchanged");
}
#[test]
fn test_apply_deletion_updates_cigar() {
let mut record = make_record(b"ACGTACGT", 100, "8M");
let mutation = MutationType::Indel {
pos: 102,
ref_seq: vec![b'G', b'T'],
alt_seq: vec![b'G'],
};
apply_deletion(&mut record, &mutation);
let cigar = get_cigar_str(&record);
assert!(
cigar.contains('D'),
"CIGAR should contain a deletion op: {cigar}"
);
}
#[test]
fn test_cigar_ref_to_read_offset_simple_match() {
let ops = parse_cigar_str("10M");
let offset = cigar_ref_to_read_offset(&ops, 100, 105);
assert_eq!(offset, Some(5));
}
#[test]
fn test_cigar_ref_to_read_offset_with_soft_clip() {
let ops = parse_cigar_str("2S8M");
let offset = cigar_ref_to_read_offset(&ops, 100, 102);
assert_eq!(offset, Some(4));
}
#[test]
fn test_cigar_ref_to_read_offset_deletion_returns_none() {
let ops = parse_cigar_str("5M2D5M");
let offset = cigar_ref_to_read_offset(&ops, 100, 106); assert_eq!(offset, None);
}
#[test]
fn test_cigar_ref_to_read_offset_before_start() {
let ops = parse_cigar_str("10M");
let offset = cigar_ref_to_read_offset(&ops, 100, 50);
assert_eq!(offset, None);
}
#[test]
fn test_cigar_to_string() {
let ops = parse_cigar_str("5M2I143M");
let s = cigar_to_string(&ops);
assert_eq!(s, "5M2I143M");
}
#[test]
fn test_merge_cigar_ops() {
let ops = vec![
Op::new(Kind::Match, 5),
Op::new(Kind::Match, 3),
Op::new(Kind::Insertion, 2),
Op::new(Kind::Match, 5),
];
let merged = merge_cigar_ops(ops);
assert_eq!(merged.len(), 3);
assert_eq!(merged[0], Op::new(Kind::Match, 8));
assert_eq!(merged[1], Op::new(Kind::Insertion, 2));
assert_eq!(merged[2], Op::new(Kind::Match, 5));
}
#[test]
fn test_insertion_quality_avg_no_overflow() {
let cigar_ops = parse_cigar_str("8M");
let mut record = RecordBuf::builder()
.set_flags(noodles_sam::alignment::record::Flags::empty())
.set_alignment_start(Position::new(101).unwrap())
.set_cigar(cigar_ops.into_iter().collect::<Cigar>())
.set_sequence(Sequence::from(b"ACGTACGT".as_ref()))
.set_quality_scores(QualityScores::from(vec![200u8; 8]))
.build();
let mutation = MutationType::Indel {
pos: 102,
ref_seq: vec![b'G'],
alt_seq: vec![b'G', b'T'],
};
let result = apply_insertion(&mut record, &mutation);
assert_eq!(result, ModifyResult::Modified);
let quals: Vec<u8> = record.quality_scores().as_ref().to_vec();
assert!(
quals.iter().all(|&q| q == 200),
"all qualities must be 200 after insertion; got {quals:?}"
);
}
#[test]
fn test_inject_snv_at_position_zero() {
let result = inject_snv_into_md("150", 0, b'T');
assert_eq!(result, "0T149");
}
#[test]
fn test_inject_snv_in_middle_of_all_match() {
let result = inject_snv_into_md("150", 50, b'A');
assert_eq!(result, "50A99");
}
#[test]
fn test_inject_snv_at_last_base() {
let result = inject_snv_into_md("150", 149, b'C');
assert_eq!(result, "149C0");
}
#[test]
fn test_inject_snv_into_read_with_existing_mismatch() {
let result = inject_snv_into_md("75A74", 100, b'G');
assert_eq!(result, "75A24G49");
}
#[test]
fn test_inject_snv_into_read_with_deletion() {
let result = inject_snv_into_md("10^AC20", 25, b'G');
assert_eq!(result, "10^AC15G4");
}
#[test]
fn test_inject_snv_into_read_with_deletion_before_offset() {
let result = inject_snv_into_md("5^GT10", 12, b'C');
assert_eq!(result, "5^GT7C2");
}
}