# Copyright 2004-2018, Paul Johnson (paul@pjcj.net) # This software is free. It is licensed under the same terms as Perl itself. # The latest version of this software should be available from my homepage: # http://www.pjcj.net package Devel::Cover::DB::Structure; use strict; use warnings; use Carp; use Digest::MD5; use Devel::Cover::DB; use Devel::Cover::DB::IO; use Devel::Cover::Dumper; # For comprehensive debug logging. use constant DEBUG => 0; our $VERSION = '1.31'; # VERSION our $AUTOLOAD; sub new { my $class = shift; my $self = { @_ }; bless $self, $class } sub DESTROY {} sub AUTOLOAD { my $self = $_[0]; my $func = $AUTOLOAD; $func =~ s/.*:://; my ($function, $criterion) = $func =~ /^(add|get)_(.*)/; croak "Undefined subroutine $func called" unless $criterion && grep $_ eq $criterion, @Devel::Cover::DB::Criteria, qw( sub_name file line ); no strict "refs"; if ($function eq "get") { my $c = $criterion eq "time" ? "statement" : $criterion; if (grep $_ eq $c, qw( sub_name file line )) { *$func = sub { shift->{$c} }; } else { *$func = sub { my $self = shift; my $digest = shift; # print STDERR "file: $digest, condition: $c\n"; for my $fval (values %{$self->{f}}) { return $fval->{$c} if $fval->{digest} eq $digest; } return } }; } else { *$func = sub { my $self = shift; my $file = shift; push @{$self->{f}{$file}{$criterion}}, @_; }; } goto &$func } sub debuglog { my $self = shift; my $dir = "$self->{base}/debuglog"; unless (mkdir $dir) { confess "Can't mkdir $dir: $!" unless -d $dir; } local $\; # One log file per process, as we're potentially dumping out large amounts, # and might exceed the atomic write size of the OS open my $fh, '>>', "$dir/$$" or confess "Can't open $dir/$$: $!"; print $fh "----------------" . gmtime() . "----------------\n"; print $fh ref $_ ? Dumper($_) : $_; print $fh "\n"; close $fh or confess "Can't close $dir/$$: $!"; } sub add_criteria { my $self = shift; @{$self->{criteria}}{@_} = (); $self } sub criteria { my $self = shift; keys %{$self->{criteria}} } sub set_subroutine { my $self = shift; my ($sub_name, $file, $line, $scount) = @{$self}{qw( sub_name file line scount )} = @_; # When new code is added at runtime, via a string eval in some guise, we # need information about where structure information for the subroutine # is. This information is stored in $self->{f}{$file}{start} keyed on the # filename, line number, subroutine name and the count, the count being # for when there are multiple subroutines of the same name on the same # line (such subroutines generally being called BEGIN). # print STDERR "set_subroutine start $file:$line $sub_name($scount) ", # Dumper $self->{f}{$file}{start}; $self->{additional} = 0; if ($self->reuse($file)) { # reusing a structure if (exists $self->{f}{$file}{start}{$line}{$sub_name}[$scount]) { # sub already exists - normal case # print STDERR "reuse $file:$line:$sub_name\n"; $self->{count}{$_}{$file} = $self->{f}{$file}{start}{$line}{$sub_name}[$scount]{$_} for $self->criteria; } else { # sub doesn't exist, for example a conditional C $self->{additional} = 1; if (exists $self->{additional_count}{($self->criteria)[0]}{$file}) { # already had such a sub in module # print STDERR "reuse additional $file:$line:$sub_name\n"; $self->{count}{$_}{$file} = $self->{f}{$file}{start}{$line}{$sub_name}[$scount]{$_} = ($self->add_count($_))[0] for $self->criteria; } else { # first such a sub in module # print STDERR "reuse first $file:$line:$sub_name\n"; $self->{count}{$_}{$file} = $self->{additional_count}{$_}{$file} = $self->{f}{$file}{start}{$line}{$sub_name}[$scount]{$_} = $self->{f}{$file}{start}{-1}{"__COVER__"}[$scount]{$_} for $self->criteria; } } } else { # first time sub seen in new structure # print STDERR "new $file:$line:$sub_name\n"; $self->{count}{$_}{$file} = $self->{f}{$file}{start}{$line}{$sub_name}[$scount]{$_} = $self->get_count($file, $_) for $self->criteria; } # print STDERR "set_subroutine start $file:$line $sub_name($scount) ", # Dumper $self->{f}{$file}{start}; } sub store_counts { my $self = shift; my ($file) = @_; $self->{count}{$_}{$file} = $self->{f}{$file}{start}{-1}{__COVER__}[0]{$_} = $self->get_count($file, $_) for $self->criteria; # print STDERR "store_counts: ", Dumper $self->{f}{$file}{start}; } sub reuse { my $self = shift; my ($file) = @_; exists $self->{f}{$file}{start}{-1}{"__COVER__"} # TODO - exists $self->{f}{$file}{start}{-1} } sub set_file { my $self = shift; my ($file) = @_; $self->{file} = $file; my $digest = $self->digest($file); if ($digest) { # print STDERR "Adding $digest for $file\n"; $self->{f}{$file}{digest} = $digest; push @{$self->{digests}{$digest}}, $file; } $digest } sub digest { my $self = shift; my ($file) = @_; # print STDERR "Opening $file for MD5 digest\n"; my $digest; if (open my $fh, "<", $file) { binmode $fh; $digest = Digest::MD5->new->addfile($fh)->hexdigest; } else { print STDERR "Devel::Cover: Warning: can't open $file " . "for MD5 digest: $!\n" unless lc $file eq "-e" or $Devel::Cover::Silent or $file =~ $Devel::Cover::DB::Ignore_filenames; # require "Cwd"; print STDERR Carp::longmess("in " . Cwd::cwd()); } $digest } sub get_count { my $self = shift; my ($file, $criterion) = @_; $self->{count}{$criterion}{$file} } sub add_count { my $self = shift; # warn Carp::longmess("undefined file") unless defined $self->{file}; return unless defined $self->{file}; # can happen during self_cover my ($criterion) = @_; $self->{additional_count}{$criterion}{$self->{file}}++ if $self->{additional}; ($self->{count}{$criterion}{$self->{file}}++, !$self->reuse($self->{file}) || $self->{additional}) } sub delete_file { my $self = shift; my ($file) = @_; delete $self->{f}{$file}; } # TODO - concurrent runs updating structure? sub write { my $self = shift; my ($dir) = @_; # print STDERR Dumper $self; $dir .= "/structure"; unless (mkdir $dir) { confess "Can't mkdir $dir: $!" unless -d $dir; } chmod 0777, $dir if $self->{loose_perms}; for my $file (sort keys %{$self->{f}}) { $self->{f}{$file}{file} = $file; my $digest = $self->{f}{$file}{digest}; $digest = $1 if defined $digest && $digest =~ /(.*)/; # ie tainting unless ($digest) { print STDERR "Can't find digest for $file" unless $Devel::Cover::Silent || $file =~ $Devel::Cover::DB::Ignore_filenames || ($Devel::Cover::Self_cover && $file =~ q|/Devel/Cover[./]|); next; } my $df_final = "$dir/$digest"; my $df_temp = "$dir/.$digest.$$"; # TODO - determine if Structure has changed to save writing it # my $f = $df; my $n = 1; $df = $f . "." . $n++ while -e $df; my $io = Devel::Cover::DB::IO->new; $io->write($self->{f}{$file}, $df_temp); # unless -e $df; unless (rename $df_temp, $df_final) { unless ($Devel::Cover::Silent) { if(-e $df_final) { print STDERR "Can't rename $df_temp to $df_final " . "(which exists): $!"; $self->debuglog("Can't rename $df_temp to $df_final " . "(which exists): $!") if DEBUG; } else { print STDERR "Can't rename $df_temp to $df_final: $!"; $self->debuglog("Can't rename $df_temp to $df_final: $!") if DEBUG; } } unless (unlink $df_temp) { print STDERR "Can't remove $df_temp after failed rename: $!" unless $Devel::Cover::Silent; $self->debuglog("Can't remove $df_temp after failed rename: $!") if DEBUG; } } } } sub read { my $self = shift; my ($digest) = @_; my $file = "$self->{base}/structure/$digest"; my $io = Devel::Cover::DB::IO->new; my $s = eval { $io->read($file) }; if ($@ or !$s) { $self->debuglog("read retrieve $file failed: $@") if DEBUG; die $@; } if (DEBUG) { foreach my $key (qw(file digest)) { if (!defined $s->{$key}) { $self->debuglog("retrieve $file had no $key entry. Got:\n", $s); } } } my $d = $self->digest($s->{file}); # print STDERR "reading $digest from $file: ", Dumper $s; if (!$d) { # No digest implies that we can't read the file. Likely this is because # it's stored with a relative path. In which case, it's not valid to # assume that the file has been changed, and hence that we need to # "update" the structure database on disk. } elsif ($d eq $s->{digest}) { $self->{f}{$s->{file}} = $s; } else { print STDERR "Devel::Cover: Deleting old coverage ", "for changed file $s->{file}\n"; if (unlink $file) { $self->debuglog("Deleting old coverage $file for changed " . "$s->{file} $s->{digest} vs $d. Got:\n", $s, "Have:\n", $self->{f}{$file}) if DEBUG; } else { print STDERR "Devel::Cover: can't delete $file: $!\n"; $self->debuglog("Failed to delete coverage $file for changed " . "$s->{file} ($!) $s->{digest} vs $d. Got:\n", $s, "Have:\n", $self->{f}{$file}) if DEBUG; } } $self } sub read_all { my ($self) = @_; my $dir = $self->{base}; $dir .= "/structure"; opendir D, $dir or return; for my $d (sort grep $_ !~ /\./, readdir D) { $d = $1 if $d =~ /(.*)/; # Die tainting $self->read($d); } closedir D or die "Can't closedir $dir: $!"; $self } sub merge { my $self = shift; my ($from) = @_; Devel::Cover::DB::_merge_hash($self->{f}, $from->{f}, "noadd"); } 1 __END__ =head1 NAME Devel::Cover::DB::Structure - Internal: abstract structure of a source file =head1 VERSION version 1.31 =head1 SYNOPSIS use Devel::Cover::DB::Structure; =head1 DESCRIPTION =head1 SEE ALSO Devel::Cover Devel::Cover::DB =head1 METHODS =head1 BUGS Huh? =head1 LICENCE Copyright 2004-2018, Paul Johnson (paul@pjcj.net) This software is free. It is licensed under the same terms as Perl itself. The latest version of this software should be available from my homepage: http://www.pjcj.net =cut