diff --git a/bin/flatfile-to-json.pl b/bin/flatfile-to-json.pl index 55042afbd4..0ce637f346 100755 --- a/bin/flatfile-to-json.pl +++ b/bin/flatfile-to-json.pl @@ -1,5 +1,13 @@ #!/usr/bin/env perl +use FindBin qw($RealBin); +use lib "$RealBin/../lib"; +use Script::FlatFileToJson; + +exit Script::FlatFileToJson->new(@ARGV)->run; + +__END__ + =head1 NAME flatfile-to-json.pl - format data into JBrowse JSON format from an annotation file @@ -146,283 +154,3 @@ =head2 BED-SPECIFIC =back =cut - -use strict; -use warnings; - -use FindBin qw($Bin); - -use Getopt::Long; -use Pod::Usage; - -use Bio::FeatureIO; - -use lib "$Bin/../lib"; -use ArrayRepr; -use GenomeDB; -use BioperlFlattener; -use ExternalSorter; -use JSON 2; - -my ($gff, $bed, $bam, $trackLabel, $key, - $urlTemplate, $subfeatureClasses, $arrowheadClass, - $clientConfig, $thinType, $thickType, - $types, $nclChunk); -$types = []; -my $autocomplete = "none"; -my $outdir = "data"; -my $cssClass = "feature"; -my ($getType, $getPhase, $getSubs, $getLabel, $compress) = (0, 0, 0, 0, 0); -my $sortMem = 1024 * 1024 * 512; -my $help; - GetOptions("gff=s" => \$gff, - "bed=s" => \$bed, - "bam=s" => \$bam, - "out=s" => \$outdir, - "tracklabel|trackLabel=s" => \$trackLabel, - "key=s" => \$key, - "cssClass=s" => \$cssClass, - "autocomplete=s" => \$autocomplete, - "getType" => \$getType, - "getPhase" => \$getPhase, - "getSubs|getSubfeatures" => \$getSubs, - "getLabel" => \$getLabel, - "urltemplate=s" => \$urlTemplate, - "arrowheadClass=s" => \$arrowheadClass, - "subfeatureClasses=s" => \$subfeatureClasses, - "clientConfig=s" => \$clientConfig, - "thinType=s" => \$thinType, - "thickType=s" => \$thickType, - "type=s@" => \$types, - "nclChunk=i" => \$nclChunk, - "compress" => \$compress, - "sortMem=i" =>\$sortMem, - "help|h|?" => \$help, - ); -@$types = split /,/, join( ',', @$types); - -pod2usage( -verbose => 2 ) if $help; - -my $gdb = GenomeDB->new( $outdir ); - -pod2usage( "Must provide a --tracklabel parameter." ) unless defined $trackLabel; -pod2usage( "You must supply either a --gff or --bed parameter." ) - unless defined $gff || defined $bed || defined $bam; - -$bam and die "BAM support has been moved to a separate program: bam-to-json.pl\n"; - -if (!defined($nclChunk)) { - # default chunk size is 50KiB - $nclChunk = 50000; - # $nclChunk is the uncompressed size, so we can make it bigger if - # we're compressing - $nclChunk *= 4 if $compress; -} - -my $idSub = sub { - return $_[0]->load_id if $_[0]->can('load_id') && defined $_[0]->load_id; - return $_[0]->can('primary_id') ? $_[0]->primary_id : $_[0]->id; -}; - -my $stream; -my $labelStyle = 1; -if ($gff) { - my $io = Bio::FeatureIO->new( - -format => 'gff', - -version => '3', - -file => $gff, - ); - $stream = sub { $io->next_feature_group }; -} elsif ($bed) { - my $io = Bio::FeatureIO->new( - -format => 'bed', - -file => $bed, - ($thinType ? ("-thin_type" => $thinType) : ()), - ($thickType ? ("-thick_type" => $thickType) : ()), - ); - $labelStyle = sub { - #label sub for features returned by Bio::FeatureIO::bed - return $_[0]->name; - }; - $stream = sub { $io->next_feature }; -} else { - die "Please specify --gff or --bed.\n"; -} - -$clientConfig = JSON::from_json( $clientConfig ) - if defined $clientConfig; - -$subfeatureClasses = JSON::from_json($subfeatureClasses) - if defined $subfeatureClasses; - -my %config = ( - autocomplete => $autocomplete, - type => $getType || @$types ? 1 : 0, - phase => $getPhase, - subfeatures => $getSubs, - style => { - %{ $clientConfig || {} }, - className => $cssClass, - ( $arrowheadClass ? ( arrowheadClass => $arrowheadClass ) : () ), - ( $subfeatureClasses ? ( subfeatureClasses => $subfeatureClasses ) : () ), - }, - key => defined($key) ? $key : $trackLabel, - compress => $compress, - urlTemplate => $urlTemplate, - ); - - -my $flattener = BioperlFlattener->new( - $trackLabel, - { - "idSub" => $idSub, - "label" => ($getLabel || ($autocomplete ne "none")) - ? $labelStyle : 0, - %config, - }, - [], [] ); - -# The ExternalSorter will get [chrom, [start, end, ...]] arrays from -# the flattener -my $sorter = ExternalSorter->new( - do { - my $startIndex = BioperlFlattener->startIndex; - my $endIndex = BioperlFlattener->endIndex; - sub ($$) { - $_[0]->[0] cmp $_[1]->[0] - || - $_[0]->[1]->[$startIndex] <=> $_[1]->[1]->[$startIndex] - || - $_[1]->[1]->[$endIndex] <=> $_[0]->[1]->[$endIndex]; - } - }, - $sortMem -); - -my @arrayrepr_classes = ( - { - attributes => $flattener->featureHeaders, - isArrayAttr => { Subfeatures => 1 }, - }, - { - attributes => $flattener->subfeatureHeaders, - isArrayAttr => {}, - }, - ); - -# build a filtering subroutine for the features -my $filter = make_feature_filter( $types ); - -my %featureCounts; -while ( my @feats = $filter->( $stream->() ) ) { - for my $feat ( @feats ) { - my $chrom = ref $feat->seq_id ? $feat->seq_id->value : $feat->seq_id; - $featureCounts{$chrom} += 1; - - my $row = [ $chrom, - $flattener->flatten_to_feature( $feat ), - $flattener->flatten_to_name( $feat, $chrom ), - ]; - $sorter->add( $row ); - } -} -$sorter->finish(); - -################################ - -my $track = $gdb->getTrack( $trackLabel ) - || $gdb->createFeatureTrack( $trackLabel, - \%config, - $config{key}, - ); - -my $curChrom = 'NONE YET'; -my $totalMatches = 0; -while( my $feat = $sorter->get ) { - - unless( $curChrom eq $feat->[0] ) { - $curChrom = $feat->[0]; - $track->finishLoad; #< does nothing if no load happening - $track->startLoad( $curChrom, - $nclChunk, - \@arrayrepr_classes, - ); - } - $totalMatches++; - $track->addSorted( $feat->[1] ); - - # load the feature's name record into the track if necessary - if( my $namerec = $feat->[2] ) { - $track->nameHandler->addName( $namerec ); - } -} - -$gdb->writeTrackEntry( $track ); - -# If no features are found, check for mistakes in user input -if( !$totalMatches && defined $types ) { - warn "WARNING: No matching features found for @$types\n"; -} - - -################ - -sub make_feature_filter { - - my @filters; - - # add a filter for type:source if --type was specified - if( $types && @$types ) { - my @type_regexes = map { - my $t = $_; - $t .= ":.*" unless $t =~ /:/; - qr/^$t$/ - } @$types; - - push @filters, sub { - my ($f) = @_; - my $type = $f->primary_tag - or return 0; - my $source = $f->source_tag; - my $t_s = "$type:$source"; - for( @type_regexes ) { - return 1 if $t_s =~ $_; - } - return 0; - }; - } - - # if no filtering, just return a pass-through now. - return sub { @_ } unless @filters; - - # make a sub that tells whether a single feature passes - my $pass_feature = sub { - my ($f) = @_; - $_->($f) || return 0 for @filters; - return 1; - }; - - # Apply this filtering rule through the whole feature hierarchy, - # returning features that pass. If a given feature passes, return - # it *and* all of its subfeatures, with no further filtering - # applied to the subfeatures. If a given feature does NOT pass, - # search its subfeatures to see if they do. - return sub { - _find_passing_features( $pass_feature, @_ ); - } -}; - -# given a subref that says whether an individual feature passes, -# return the LIST of features among the whole feature hierarchy that -# pass the filtering rule -sub _find_passing_features { - my $pass_feature = shift; - return map { - my $feature = $_; - $pass_feature->( $feature ) - # if this feature passes, we're done, just return it - ? ( $feature ) - # otherwise, look for passing features in its subfeatures - : _find_passing_features( $pass_feature, $feature->get_SeqFeatures ); - } @_; -} diff --git a/lib/Script.pm b/lib/Script.pm new file mode 100644 index 0000000000..21766d72a7 --- /dev/null +++ b/lib/Script.pm @@ -0,0 +1,56 @@ +package Script; +use strict; +use warnings; + +use Getopt::Long (); +use Pod::Usage (); + +=head1 NAME + +Script - base class for a JBrowse command-line script + +=head1 DESCRIPTION + +This wheel is smaller than the ones on CPAN, but not really rounder. + +=cut + +sub new { + my $class = shift; + my $opts = $class->getopts(@_); + return bless { opt => $opts }, $class; +} + +sub getopts { + my $class = shift; + my $opts = { + $class->option_defaults, + }; + local @ARGV = @_; + Getopt::Long::GetOptions( $opts, $class->option_definitions ); + Pod::Usage::pod2usage( -verbose => 2 ) if $opts->{help}; + return $opts; +} + +sub opt { + if( @_ > 2 ) { + return $_[0]->{opt}{$_[1]} = $_[2]; + } else { + return $_[0]->{opt}{$_[1]} + } +} + +#override me +sub option_defaults { + ( ) +} + +#override me +sub option_definitions { + ( "help|h|?" ) +} + +sub run { +} + +1; diff --git a/lib/Script/FlatFileToJson.pm b/lib/Script/FlatFileToJson.pm new file mode 100644 index 0000000000..87f3fa8ed6 --- /dev/null +++ b/lib/Script/FlatFileToJson.pm @@ -0,0 +1,291 @@ +#!/usr/bin/env perl + +=head1 NAME + +Script::FlatfileToJson - implementation of bin/flatfile-to-json.pl + +=head1 DESCRIPTION + +Do C for most of the documentation. + +=cut + +package Script::FlatFileToJson; + +use strict; +use warnings; + +use base 'Script'; + +use ArrayRepr; +use GenomeDB; +use ExternalSorter; +use JSON 2; + +sub option_defaults { + ( type => [], + autocomplete => 'none', + out => 'data', + cssClass => 'feature', + sortMem => 1024 * 1024 * 512, + ) +} + +sub option_definitions { + ( + "gff=s", + "bed=s", + "bam=s", + "out=s", + "trackLabel|tracklabel=s", + "key=s", + "cssClass=s", + "autocomplete=s", + "getType", + "getPhase", + "getSubs|getSubfeatures", + "getLabel", + "urltemplate=s", + "arrowheadClass=s", + "subfeatureClasses=s", + "clientConfig=s", + "thinType=s", + "thickType=s", + "type=s@", + "nclChunk=i", + "compress", + "sortMem=i", + "help|h|?", + ) +} + +sub run { + my ( $self ) = @_; + + my $types = $self->opt('type'); + @$types = split /,/, join ',', @$types; + + my $gdb = GenomeDB->new( $self->opt('out') ); + + Pod::Usage::pod2usage( "Must provide a --tracklabel parameter." ) unless defined $self->opt('trackLabel'); + unless( defined $self->opt('gff') || defined $self->opt('bed') || defined $self->opt('bam') ) { + Pod::Usage::pod2usage( "You must supply either a --gff or --bed parameter." ) + } + + $self->opt('bam') and die "BAM support has been moved to a separate program: bam-to-json.pl\n"; + + if( ! $self->opt('nclChunk') ) { + # default chunk size is 50KiB + my $nclChunk = 50000; + # $nclChunk is the uncompressed size, so we can make it bigger if + # we're compressing + $nclChunk *= 4 if $self->opt('compress'); + $self->opt( nclChunk => $nclChunk ); + } + + for my $optname ( qw( clientConfig subfeatureClasses ) ) { + if( my $o = $self->opt($optname) ) { + $self->opt( $optname => JSON::from_json( $o )); + } + } + + + my %config = ( + autocomplete => $self->opt('autocomplete'), + type => $self->opt('getType') || @$types ? 1 : 0, + phase => $self->opt('getPhase'), + subfeatures => $self->opt('getSubs'), + style => { + %{ $self->opt('clientConfig') || {} }, + className => $self->opt('cssClass'), + ( $self->opt('arrowheadClass') ? ( arrowheadClass => $self->opt('arrowheadClass') ) : () ), + ( $self->opt('subfeatureClasses') ? ( subfeatureClasses => $self->opt('subfeatureClasses') ) : () ), + }, + key => defined( $self->opt('key') ) ? $self->opt('key') : $self->opt('trackLabel'), + compress => $self->opt('compress'), + urlTemplate => $self->opt('urlTemplate'), + ); + + my $feature_stream = $self->opt('gff') ? $self->make_gff_stream : + $self->opt('bed') ? $self->make_bed_stream( \%config ) : + die "Please specify --gff or --bed.\n"; + + # The ExternalSorter will get flattened [chrom, [start, end, ...]] + # arrays from the feature_stream + my $sorter = ExternalSorter->new( + do { + my $startIndex = $feature_stream->startIndex; + my $endIndex = $feature_stream->endIndex; + sub ($$) { + $_[0]->[0] cmp $_[1]->[0] + || + $_[0]->[1]->[$startIndex] <=> $_[1]->[1]->[$startIndex] + || + $_[1]->[1]->[$endIndex] <=> $_[0]->[1]->[$endIndex]; + } + }, + $self->opt('sortMem'), + ); + + my @arrayrepr_classes = ( + { + attributes => $feature_stream->featureHeaders, + isArrayAttr => { Subfeatures => 1 }, + }, + { + attributes => $feature_stream->subfeatureHeaders, + isArrayAttr => {}, + }, + ); + + # build a filtering subroutine for the features + my $filter = $self->make_feature_filter( $types ); + + my %featureCounts; + while ( my @feats = $feature_stream->next_items ) { + + for my $feat ( $filter->( @feats ) ) { + my $chrom = $feat->{seq_id}; + $featureCounts{$chrom} += 1; + + my $row = [ $chrom, + $feature_stream->flatten_to_feature( $feat ), + $feature_stream->flatten_to_name( $feat ), + ]; + $sorter->add( $row ); + } + } + $sorter->finish(); + + ################################ + + my $track = $gdb->getTrack( $self->opt('trackLabel') ) + || $gdb->createFeatureTrack( $self->opt('trackLabel'), + \%config, + $config{key}, + ); + + my $curChrom = 'NONE YET'; + my $totalMatches = 0; + while( my $feat = $sorter->get ) { + + unless( $curChrom eq $feat->[0] ) { + $curChrom = $feat->[0]; + $track->finishLoad; #< does nothing if no load happening + $track->startLoad( $curChrom, + $self->opt('nclChunk'), + \@arrayrepr_classes, + ); + } + $totalMatches++; + $track->addSorted( $feat->[1] ); + + # load the feature's name record into the track if necessary + if( my $namerec = $feat->[2] ) { + $track->nameHandler->addName( $namerec ); + } + } + + $gdb->writeTrackEntry( $track ); + + # If no features are found, check for mistakes in user input + if( !$totalMatches && defined $types ) { + warn "WARNING: No matching features found for @$types\n"; + } +} + +sub make_gff_stream { + my $self = shift; + + require Bio::GFF3::LowLevel::Parser; + require Script::FlatFileToJson::FeatureStream::GFF3_LowLevel; + + my $p = Bio::GFF3::LowLevel::Parser->new( $self->opt('gff') ); + + return Script::FlatFileToJson::FeatureStream::GFF3_LowLevel->new( + parser => $p, + track_label => $self->opt('trackLabel') + ); +} + +sub make_bed_stream { + my ( $self, $config_hash ) = @_; + + require Bio::FeatureIO; + require Script::FlatFileToJson::FeatureStream::BioPerl; + + my $io = Bio::FeatureIO->new( + -format => 'bed', + -file => $self->opt('bed'), + ($self->opt('thinType') ? ("-thin_type" => $self->opt('thinType') ) : ()), + ($self->opt('thickType') ? ("-thick_type" => $self->opt('thickType')) : ()), + ); + + return Script::FlatFileToJson::FeatureStream::BioPerl->new( + stream => sub { $io->next_feature }, + track_label => $self->opt('trackLabel'), + ); +} + +sub make_feature_filter { + my ( $self, $types ) = @_; + + my @filters; + + # add a filter for type:source if --type was specified + if( $types && @$types ) { + my @type_regexes = map { + my $t = $_; + $t .= ":.*" unless $t =~ /:/; + qr/^$t$/ + } @$types; + + push @filters, sub { + my ($f) = @_; + my $type = $f->{type} + or return 0; + my $source = $f->{source}; + my $t_s = "$type:$source"; + for( @type_regexes ) { + return 1 if $t_s =~ $_; + } + return 0; + }; + } + + # if no filtering, just return a pass-through now. + return sub { @_ } unless @filters; + + # make a sub that tells whether a single feature passes + my $pass_feature = sub { + my ($f) = @_; + $_->($f) || return 0 for @filters; + return 1; + }; + + # Apply this filtering rule through the whole feature hierarchy, + # returning features that pass. If a given feature passes, return + # it *and* all of its subfeatures, with no further filtering + # applied to the subfeatures. If a given feature does NOT pass, + # search its subfeatures to see if they do. + return sub { + _find_passing_features( $pass_feature, @_ ); + } +}; + +# given a subref that says whether an individual feature passes, +# return the LIST of features among the whole feature hierarchy that +# pass the filtering rule +sub _find_passing_features { + my $pass_feature = shift; + return map { + my $feature = $_; + $pass_feature->( $feature ) + # if this feature passes, we're done, just return it + ? ( $feature ) + # otherwise, look for passing features in its subfeatures + : _find_passing_features( $pass_feature, @{$feature->{child_features}} ); + } @_; +} + +1; diff --git a/lib/Script/FlatFileToJson/FeatureStream.pm b/lib/Script/FlatFileToJson/FeatureStream.pm new file mode 100644 index 0000000000..eeb2b8eeff --- /dev/null +++ b/lib/Script/FlatFileToJson/FeatureStream.pm @@ -0,0 +1,42 @@ +package Script::FlatFileToJson::FeatureStream; +use strict; +use warnings; + +sub new { + my $class = shift; + bless { @_ }, $class; +} + +sub flatten_to_feature { + my ( $self, $f, $class_index ) = @_; + my @f = ( $class_index || 0, + @{$f}{qw{ start end strand source phase type }}, + (map $f->{attributes}{$_}[0], qw(ID Name)), + [ map $self->flatten_to_feature($_,1), @{$f->{child_features}} ], + ); + # convert start to interbase + $f[1] -= 1; + # convert strand to 1/0/-1/undef if necessary + $f[3] = { '+' => 1, '-' => -1 }->{$f[3]} || $f[3] || undef; + return \@f; +} + +sub flatten_to_name { + my ( $self, $f ) = @_; + my @namerec = ( + [ grep defined, @{$f->{attributes}{Name}}, @{$f->{attributes}{Alias}} ], + $self->{track_label}, + $f->{attributes}{Name}[0], + @{$f}{'seq_id','start','end'}, + $f->{attributes}{ID}[0], + ); + $namerec[4]--; #< to one-based + return \@namerec; +} + +sub featureHeaders { [qw[ Start End Strand Source Phase Type Id Name Subfeatures ]] } +*subfeatureHeaders = \&featureHeaders; +sub startIndex { 1 } +sub endIndex { 2 } + +1; diff --git a/lib/Script/FlatFileToJson/FeatureStream/BioPerl.pm b/lib/Script/FlatFileToJson/FeatureStream/BioPerl.pm new file mode 100644 index 0000000000..43ecf81dd3 --- /dev/null +++ b/lib/Script/FlatFileToJson/FeatureStream/BioPerl.pm @@ -0,0 +1,39 @@ +package Script::FlatFileToJson::FeatureStream::BioPerl; +use strict; +use warnings; +use base 'Script::FlatFileToJson::FeatureStream'; + +sub next_items { + my ( $self ) = @_; + return map $self->_bp_to_hashref( $_ ), + $self->{stream}->(); +} + +# downconvert a bioperl feature object back to bare-hashref-format +sub _bp_to_hashref { + my ( $self, $f ) = @_; + my %h; + @h{qw{ seq_id start end strand source phase type child_features }} = + ( $f->seq_id, + $f->start, + $f->end, + $f->strand, + $f->source_tag, + {0=>0,1=>1,2=>2}->{$f->phase}, + $f->primary_tag || undef, + [ map $self->_bp_to_hashref($_), $f->get_SeqFeatures ], + ); + for(qw( seq_id start end strand source type )) { + no warnings 'uninitialized'; + $h{$_} = undef if $h{$_} eq '.'; + } + $h{attributes} = { + map { + my $t = $_; + $t => [ grep $_ ne '.', $f->get_tag_values($t) ] + } $f->get_all_tags + }; + return \%h; +}; + +1; diff --git a/lib/Script/FlatFileToJson/FeatureStream/GFF3_LowLevel.pm b/lib/Script/FlatFileToJson/FeatureStream/GFF3_LowLevel.pm new file mode 100644 index 0000000000..84bdb69b9d --- /dev/null +++ b/lib/Script/FlatFileToJson/FeatureStream/GFF3_LowLevel.pm @@ -0,0 +1,15 @@ +## featurestream that wraps a Bio::GFF3::LowLevel::Parser +package Script::FlatFileToJson::FeatureStream::GFF3_LowLevel; +use strict; +use warnings; + +use base 'Script::FlatFileToJson::FeatureStream'; + +sub next_items { + while ( my $i = $_[0]->{parser}->next_item ) { + return $i if $i->{child_features}; + } + return; +} + +1; diff --git a/tests/perl_tests/flatfile-to-json.pl.t b/tests/perl_tests/flatfile-to-json.pl.t index c6cffc6851..80e640ed28 100644 --- a/tests/perl_tests/flatfile-to-json.pl.t +++ b/tests/perl_tests/flatfile-to-json.pl.t @@ -1,8 +1,11 @@ -use strict; +use Carp::Always;use strict; use warnings; use Test::More; +use lib 'lib'; +use Script::FlatFileToJson; + use File::Spec::Functions 'catfile'; use File::Temp (); use File::Copy::Recursive 'dircopy'; @@ -10,8 +13,9 @@ use File::Copy::Recursive 'dircopy'; use JsonGenerator; sub run_with(@) { - system $^X, 'bin/flatfile-to-json.pl', @_; - ok( ! $?, 'flatfile-to-json.pl ran ok' ); + #system $^X, 'bin/flatfile-to-json.pl', @_; + #ok( ! $?, 'flatfile-to-json.pl ran ok' ); + Script::FlatFileToJson->new( @_ )->run; } sub tempdir { @@ -36,6 +40,9 @@ sub tempdir { '--clientConfig' => '{"featureCss": "height: 8px;", "histScale": 2}', ); + #system "find $tempdir -type f"; + #die 'break'; + run_with ( '--out' => $tempdir, '--gff' => 'sample_data/raw/volvox/volvox.gff3', @@ -49,7 +56,7 @@ sub tempdir { ); my $hist_output = $read_json->(qw( tracks ExampleFeatures ctgA hist-10000-0.json )); - is_deeply( $hist_output, [4,3,4,3,4,1], 'got right histogram output' ) or diag explain( $hist_output ); + is_deeply( $hist_output, [4,3,4,3,4,1], 'got right histogram output for ExampleFeatures' ) or diag explain( $hist_output ); my $names_output = $read_json->(qw( tracks ExampleFeatures ctgA names.json )); is_deeply( $names_output->[3], @@ -68,7 +75,7 @@ sub tempdir { ) or diag explain $names_output; my $cds_trackdata = $read_json->(qw( tracks CDS ctgA trackData.json )); - is( $cds_trackdata->{featureCount}, 3, 'got right feature count' ) or diag explain $cds_trackdata; + is( $cds_trackdata->{featureCount}, 3, 'got right feature count for CDS track' ) or diag explain $cds_trackdata; is( ref $cds_trackdata->{intervals}{nclist}[2][9], 'ARRAY', 'exonerate mRNA has its subfeatures' ) or diag explain $cds_trackdata; is( scalar @{$cds_trackdata->{intervals}{nclist}[2][9]}, 5, 'exonerate mRNA has 5 subfeatures' ); @@ -136,16 +143,14 @@ sub tempdir { is( ref $cds_trackdata->{intervals}{nclist}[0][9], 'ARRAY', 'gene has its subfeatures' ) or diag explain $cds_trackdata; is( scalar @{$cds_trackdata->{intervals}{nclist}[0][9]}, 1, 'gene has 1 subfeature' ); - is( ref $cds_trackdata->{intervals}{nclist}[0][9][0][6], 'ARRAY', 'mRNA has its subfeatures' ) + is( ref $cds_trackdata->{intervals}{nclist}[0][9][0][9], 'ARRAY', 'mRNA has its subfeatures' ) or diag explain $cds_trackdata; - is( scalar @{$cds_trackdata->{intervals}{nclist}[0][9][0][6]}, 7, 'mRNA has 7 subfeatures' ); + is( scalar @{$cds_trackdata->{intervals}{nclist}[0][9][0][9]}, 7, 'mRNA has 7 subfeatures' ); } for my $testfile ( "tests/data/au9_scaffold_subset.gff3", "tests/data/au9_scaffold_subset_sync.gff3" ) { # add a test for duplicate lazyclasses bug found by Gregg - my $start_time = time; - my $tempdir = tempdir(); dircopy( 'tests/data/AU9', $tempdir ); run_with ( @@ -165,8 +170,6 @@ for my $testfile ( "tests/data/au9_scaffold_subset.gff3", "tests/data/au9_scaffo is( scalar @{$cds_trackdata->{intervals}{classes}}, 3, 'got the right number of classes' ) or diag explain $cds_trackdata->{intervals}{classes}; - diag "formatting $testfile took ".(time-$start_time)." seconds"; - #system "find $tempdir"; }