def test_feature_selector_exact_interval(self):
        iv_rule = "exact"
        chrom, strand, start, stop = "n/a", ".", 5, 9
        feat, fs = self.make_feature_for_interval_test(iv_rule,
                                                       "Exact Overlap", chrom,
                                                       strand, start, stop)

        aln_match = {'seq': 'ATGC', 'chrom': chrom, 'strand': strand}
        aln_short = {'seq': 'NNN', 'chrom': chrom, 'strand': strand}
        aln_exact = make_parsed_sam_record(
            **dict(aln_match, start=start, name="exact"))
        aln_short_lo = make_parsed_sam_record(
            **dict(aln_short, start=start, name="match lo"))
        aln_short_hi = make_parsed_sam_record(
            **dict(aln_short, start=start + 1, name="match hi"))
        aln_spill_lo = make_parsed_sam_record(
            **dict(aln_match, start=start - 1, name="spill lo"))
        aln_spill_hi = make_parsed_sam_record(
            **dict(aln_match, start=start + 1, name="spill hi"))
        """
        aln_match:            |ATGC|
        aln_short:            |NNN|
        feat:               5 |----| 9
        aln_exact:          5 |ATGC| 9
        aln_short_lo:       5 |NNN| 8
        aln_short_hi:        6 |NNN| 9
        aln_spill_lo:      4 |ATGC| 8
        aln_spill_hi:        6 |ATGC| 10
        """

        self.assertEqual(fs.choose(feat, aln_exact), {"Exact Overlap"})
        self.assertEqual(fs.choose(feat, aln_short_lo), set())
        self.assertEqual(fs.choose(feat, aln_short_hi), set())
        self.assertEqual(fs.choose(feat, aln_spill_lo), set())
        self.assertEqual(fs.choose(feat, aln_spill_hi), set())
    def test_feature_selector_partial_interval(self):
        iv_rule = "partial"
        chrom, strand, start, stop = "n/a", ".", 5, 10
        feat, fs = self.make_feature_for_interval_test(iv_rule,
                                                       "Partial Overlap",
                                                       chrom, strand, start,
                                                       stop)

        aln_base = {'seq': 'ATGC', 'chrom': chrom, 'strand': strand}
        aln_spill_lo = make_parsed_sam_record(
            **dict(aln_base, start=start - 1, name="spill"))
        aln_spill_hi = make_parsed_sam_record(
            **dict(aln_base, start=start + 2, name="spill"))
        aln_contained_lo = make_parsed_sam_record(
            **dict(aln_base, start=start, name="contained"))
        aln_contained_hi = make_parsed_sam_record(
            **dict(aln_base, start=start + 1, name="contained"))
        """
        aln:                  |ATGC|
        feat:               5 |-----| 10
        aln_spill_lo:      4 |ATGC| 8
        aln_spill_hi:         7 |ATGC| 11
        aln_contained_lo:   5 |ATGC| 9      Shared start position
        aln_contained_hi:    6 |ATGC| 10    Shared end position
        """

        self.assertEqual(fs.choose(feat, aln_spill_lo), {"Partial Overlap"})
        self.assertEqual(fs.choose(feat, aln_spill_hi), {"Partial Overlap"})
        self.assertEqual(fs.choose(feat, aln_contained_lo),
                         {"Partial Overlap"})
        self.assertEqual(fs.choose(feat, aln_contained_hi),
                         {"Partial Overlap"})
    def test_feature_selector_3p_interval(self):
        iv_rule = "3' anchored"
        chrom, start, end = "n/a", 5, 10
        """
                No match  5 |--------> 9 |     aln_none
                  Match 4 --|----------->| 10  aln_long
                  Match   5 |----------->| 10  aln_exact
                  Match     | 6 -------->| 10  aln_short
        (+) 5' -------------|==feat_A===>|-----------> 3'
                  start = 5                end = 10
        (-) 3' <------------|<===feat_B==|------------ 5'
                          5 |<-------- 9 |       Match
                          5 |<-----------| 10    Match
                          5 |<-----------|-- 11  Match
                            | 6 <--------| 10    No match
        """

        # Test feat_A on (+) strand
        feat_A, fs = self.make_feature_for_interval_test(
            iv_rule, "3' Anchored Overlap (+)", chrom, '+', start, end)
        aln_base = {'start': start, 'chrom': chrom, 'strand': '+'}
        aln = {
            'aln_none':
            make_parsed_sam_record(**dict(aln_base, seq="ATGC")),
            'aln_long':
            make_parsed_sam_record(
                **dict(aln_base, start=start - 1, seq="ATGCNN")),
            'aln_exact':
            make_parsed_sam_record(**dict(aln_base, seq="ATGCN")),
            'aln_short':
            make_parsed_sam_record(
                **dict(aln_base, start=start + 1, seq="ATGC")),
        }

        self.assertEqual(fs.choose(feat_A, aln['aln_none']), set())
        self.assertEqual(fs.choose(feat_A, aln['aln_long']),
                         {"3' Anchored Overlap (+)"})
        self.assertEqual(fs.choose(feat_A, aln['aln_exact']),
                         {"3' Anchored Overlap (+)"})
        self.assertEqual(fs.choose(feat_A, aln['aln_short']),
                         {"3' Anchored Overlap (+)"})

        # Test feat_B on (-) strand
        feat_B, fs = self.make_feature_for_interval_test(
            iv_rule, "3' Anchored Overlap (-)", chrom, '-', start, end)
        aln['aln_short'].update({'start': 5, 'end': 9, 'strand': '-'})
        aln['aln_exact'].update({'start': 5, 'end': 10, 'strand': '-'})
        aln['aln_long'].update({'start': 5, 'end': 11, 'strand': '-'})
        aln['aln_none'].update({'start': 6, 'end': 10, 'strand': '-'})

        self.assertEqual(fs.choose(feat_B, aln['aln_none']), set())
        self.assertEqual(fs.choose(feat_B, aln['aln_long']),
                         {"3' Anchored Overlap (-)"})
        self.assertEqual(fs.choose(feat_B, aln['aln_exact']),
                         {"3' Anchored Overlap (-)"})
        self.assertEqual(fs.choose(feat_B, aln['aln_short']),
                         {"3' Anchored Overlap (-)"})
    def test_assign_features_single_base_overlap(self):
        htsgas = HTSeq.GenomicArrayOfSets("auto", stranded=False)
        Features.chrom_vectors = htsgas.chrom_vectors
        chrom, strand = "I", "+"

        # Add test features and intervals to the Genomic Array
        iv_olap = HTSeq.GenomicInterval(chrom, 1, 2, strand)
        iv_none = HTSeq.GenomicInterval(chrom, 2, 3, strand)
        htsgas[iv_olap] += "Overlaps alignment by one base"
        htsgas[iv_none] += "Non-overlapping feature"

        # Create mock SAM alignment which overlaps iv_olap by one base
        sam_aln = make_parsed_sam_record(**{
            'chrom': chrom,
            'strand': strand,
            'start': 0,
            'seq': 'AT'
        })
        """
        iv_none:    2 |-| 3
        iv_olap:  1 |-| 2
        sam_aln: 0 |--| 2
        """

        with patch("tiny.rna.counter.features.FeatureCounter") as mock:
            instance = mock.return_value
            FeatureCounter.assign_features(instance, sam_aln)

        expected_match_list = {'Overlaps alignment by one base'}
        instance.selector.choose.assert_called_once_with(
            expected_match_list, sam_aln)
        instance.stats.chrom_misses.assert_not_called()
    def test_assign_features_no_match(self):
        htsgas = HTSeq.GenomicArrayOfSets("auto", stranded=False)
        Features.chrom_vectors = htsgas.chrom_vectors
        chrom, strand = "I", "+"

        # Add test feature and interval to the GenomicArray
        iv_none = HTSeq.GenomicInterval(chrom, 0, 2, strand)
        htsgas[iv_none] += "Should not match"

        # Create mock SAM alignment with non-overlapping interval
        sam_aln = make_parsed_sam_record(**{
            'start': 2,
            'chrom': chrom,
            'strand': strand
        })
        """
        iv_none: 0 |--| 2
        sam_aln:    2 |-- ... --|
        """

        with patch("tiny.rna.counter.features.FeatureCounter") as mock:
            instance = mock.return_value
            FeatureCounter.assign_features(instance, sam_aln)

        instance.choose.assert_not_called()
        instance.stats.chrom_misses.assert_not_called()