Devel-Cover-Report-Kritika/lib/Devel/Cover/Report/Kritika.pm
package Devel::Cover::Report::Kritika;
use strict;
use warnings;
our $VERSION = '0.05';
use List::Util qw(sum);
use HTTP::Tiny;
use JSON ();
use Devel::Cover::DB;
our $API_ENDPOINT =
($ENV{KRITIKA_HOST} || 'https://kritika.io') . '/upload/coverage';
sub report {
my ($class, $db, $options) = @_;
my $token = $ENV{KRITIKA_TOKEN};
die 'KRITIKA_TOKEN is not defined' unless $token;
my $coverage = $class->_parse_db($db);
$class->_post($token, $coverage);
print "Coverage submitted to `$API_ENDPOINT`\n";
}
sub _post {
my $class = shift;
my ( $token, $coverage ) = @_;
$coverage = JSON::encode_json($coverage);
my $ua = $class->_build_ua;
my $response;
for my $i ( 1 .. 3 ) {
$response = $ua->post_form(
$API_ENDPOINT,
{
revision => $class->_detect_revision,
coverage => $coverage
},
{ headers => { Authorization => 'Token ' . $token } }
);
last if $response->{success};
last unless $response->{status} eq '599';
warn "Retrying in ${i}s because of $response->{reason}: $response->{content}...\n";
$class->_sleep($i);
}
if (!$response->{success}) {
my $error = $response->{reason};
if ($response->{status} eq '599') {
$error .= ': ' . $response->{content};
}
die "Error: $error\n" unless $response->{success};
}
}
sub _sleep { shift; sleep(@_) }
sub _detect_revision {
my $class = shift;
for (
qw/
TRAVIS_COMMIT
CI_BUILD_REF
/
)
{
return $ENV{$_} if $ENV{$_};
}
die 'Cannot detect revision';
}
sub _parse_db {
my $class = shift;
my ($db) = @_;
my $coverage = [];
my $cover = $db->cover;
my @files = $cover->items;
foreach my $file (sort @files) {
my $lines = {};
my $summary = {};
my $f = $cover->file($file);
for my $criterion ($f->items) {
next if $criterion eq 'time' || $criterion eq 'pod';
my $c = $f->criterion($criterion);
for my $location ($c->items) {
my @calls = @{$c->location($location)};
if ($criterion eq 'subroutine' || $criterion eq 'statement') {
my $realcriterion = $criterion;
$realcriterion = 'function'
if $realcriterion eq 'subroutine';
$summary->{$realcriterion}->{total} += @calls;
$summary->{$realcriterion}->{covered} ||= 0;
$lines->{$location}->{$realcriterion}->{total} += @calls;
$lines->{$location}->{$realcriterion}->{covered} ||= 0;
if (my @covered = grep { $_->covered } @calls) {
$lines->{$location}->{$realcriterion}->{covered} +=
@calls;
$summary->{$realcriterion}->{covered} += @calls;
}
}
elsif ($criterion eq 'branch' || $criterion eq 'condition') {
my $total = sum map { $_->total } @calls;
my $covered =
sum map { $_ ? 1 : 0 } map { $_->values } @calls;
$lines->{$location}->{$criterion}->{total} += $total;
$lines->{$location}->{$criterion}->{covered} += $covered;
foreach my $call (@calls) {
push @{$lines->{$location}->{$criterion}->{hits}},
[map { $_ ? 1 : 0 } $call->values];
}
$summary->{$criterion}->{total} += $total;
$summary->{$criterion}->{covered} += $covered;
}
}
}
my $realfile = $file;
$realfile =~ s{^blib/}{};
push @$coverage,
{
file => $realfile,
summary => $summary,
lines => [
map { {line => $_, coverage => $lines->{$_}} }
sort { $a <=> $b } keys %$lines
]
};
}
return $coverage;
}
sub _build_ua {
my $class = shift;
return HTTP::Tiny->new(agent => "$class/$VERSION ");
}
1;
__END__
=head1 NAME
Devel::Cover::Report::Kritika - Cover reporting to Kritika
=head1 SYNOPSIS
export KRITIKA_TOKEN=yourtoken
cover -test -report kritika
=head1 DESCRIPTION
L<Devel::Cover::Report::Kritika> reports coverage to L<Kritika|https://kritika.io>.
In order to submit the report, you have to set KRITIKA_TOKEN environmental variable to the appropriate token, which can
be obtained from Kritika web interface.
=head1 INTEGRATION
L<Devel::Cover::Report::Kritika> was written having in mind the integration possibility with many public/private CI/CD
services.
It will detect the following services:
=over 4
=item * L<Travis CI|https://travis-ci.org/>
=item * L<GitLab|https://about.gitlab.com/gitlab-ci/>
=back
=head1 DEVELOPMENT
=head2 Repository
http://github.com/kritikaio/devel-cover-report-kritika-perl
=head1 CREDITS
=head1 AUTHOR
Viacheslav Tykhanovskyi, C<vti@cpan.org>.
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2017, Viacheslav Tykhanovskyi
This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.
=cut