The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Tapper::Notification;
# git description: v4.1.0-1-g60d5ae9

BEGIN {
  $Tapper::Notification::AUTHORITY = 'cpan:TAPPER';
}
{
  $Tapper::Notification::VERSION = '4.1.1';
}
# ABSTRACT: Tapper - Daemon and plugins to handle MCP notifications

use 5.010;
use warnings;
use strict;

use Moose;
use Tapper::Model 'model';
use Tapper::Config;
use Try::Tiny;
use Hash::Merge::Simple 'merge';
use Language::Expr;

use Data::DPath qw/dpath/;
use Tapper::Reports::DPath qw/reportdata /;


extends 'Tapper::Base';

has cfg => (is => 'rw', default => sub { Tapper::Config->subconfig} );



# We have these variables global to have them available in condition
# match functions in notification subscriptions. If they were not global
# the functions used in these conditions would need to have them in the API
our ($testrun, $report);


sub get_events
{
        my ($self) = @_;

        my $events;
        $events = model('ReportsDB')->resultset('NotificationEvent')->search();

        return $events;
}


sub get_subscriptions
{
        my ($self, $type) = @_;
        my $subscriptions = model('ReportsDB')->resultset('Notification')->search({event => $type});
        return $subscriptions;
}



sub get_testrun_data
{
        my ($self, $testrun_id) = @_;
        my $testrun      = model('TestrunDB')->resultset('Testrun')->find({id => $testrun_id},{ result_class => 'DBIx::Class::ResultClass::HashRefInflator' });
        my $job          = model('TestrunDB')->resultset('TestrunScheduling')->find({testrun_id => $testrun_id},{ result_class => 'DBIx::Class::ResultClass::HashRefInflator' });
        my $owner        = model('TestrunDB')->resultset('Owner')->find($testrun->{owner_id});
        $testrun         = merge($job, $testrun);
        $testrun->{owner} = $owner ? $owner->login : 'unknown';
        return $testrun;
}


sub get_report_data
{
        my ($self, $report_id) = @_;
        my $report = model('ReportsDB')->resultset('Report')->find($report_id);
        my $report_hash = Tapper::Reports::DPath::_as_data($report);
        return $report_hash;
}


sub get_testrun_success
{
        my ($testrun_id) = @_;
        my $stats = model('ReportsDB')->resultset('ReportgroupTestrunStats')->search({testrun_id => $testrun_id});
        return if not $stats->count;
        return ($stats->search({}, {rows => 1})->first->success_ratio == 100) ? 'pass' : 'fail';
}


sub testrun_success_change
{
        my ($search, $lookback) = @_;
        my $testruns = model('TestrunDB')->resultset('Testrun')->search($search, {order_by => {-desc => 'created_at'}});
        my $success;
        while ($lookback-- and my $this_testrun = $testruns->next) {
                # the testrun that triggered this check is not part of the backlog since we want
                # to check its success against the success of the testruns before this one
                do {$lookback++, next} if $this_testrun->id == $testrun->{id};
                my $this_success = get_testrun_success($this_testrun->id);
                # this testrun has no reports, ignore it and don't count it against lookback
                do {$lookback++, next} if not defined $this_success;

                $success = $this_success if not defined($success);
                # last $lookback tests did not have the same success state
                return 0 if not $this_success ~~ $success;
        }
        my $testrun_success = get_testrun_success($testrun->{id});

        return 1 if defined($testrun_success) and defined($success) and $testrun_success ne $success;
        return 0;
}


sub topic_success_change
{
        my ($lookback) = @_;
        return unless ref($testrun) eq 'HASH' and exists $testrun->{topic_name};
        return testrun_success_change({topic_name => $testrun->{topic_name}}, $lookback);;
}


# =head2 matches
#
# Check whether the given notification condition matches on the given
# event, i.e. whether we need to notify the owner.
#
# @param string - condition
# @param Result::NotificationEvent - event
#
# @return true/false based on condition
#
# =cut
#
# sub matches
# {
#         my ($self, $condition, $event) = @_;
#         given($event->type){
#                 when ('testrun_finished') {
#                         $testrun             = $self->get_testrun_data($event->message->{testrun_id});
#                 }
#                 when ('report_received')  {
#                         $report       = $self->get_report_data($event->message->{report_id});
#                 };
#                 default { return };
#         }
#
#         my $le = Language::Expr->new;
#         $le->interpreted(1);
#
#         $le->var('report' => $report, 'testrun' => $testrun);
#         $le->func(testrun     => sub { return ( @_ ? $testrun->{$_[0]} : $testrun ) if $testrun });
#         $le->func(report      => sub { return ( @_ ? $report->{$_[0]}  : $report  ) if $report  });
#         $le->func(deep_search => sub { return dpath($_[1])->match($_[0]) } );
#         $le->func(reportdata  => sub { return reportdata @_ } );
#         $le->func(testrun_success_change  => sub { testrun_success_change @_ });
#         $le->func(topic_success_change    => sub { topic_success_change @_ });
#
#         my $success;
#         $success = $le->eval($condition);
#         return  $success;
# }
#



sub matches
{
        my ($self, $condition, $event) = @_;
        given($event->type){
                when ('testrun_finished') {
                        $testrun             = $self->get_testrun_data($event->message->{testrun_id});
                }
                when ('report_received')  {
                        $report       = $self->get_report_data($event->message->{report_id});
                };
                default { return };
        }

        ## no critic ProhibitNestedSubs
        sub testrun { return unless $testrun; return ( @_ ? $testrun->{$_[0]} : $testrun ) }
        sub report { return unless $report; return ( @_ ? $report->{$_[0]}  : $report ) }
        sub deep_search { return dpath($_[1])->match($_[0]) }

        my $success;
        $success = eval($condition); ## no critic
        return  $success;
}


sub notify_owner
{
        my ($self, $subscription) = @_;
        my $text = $subscription->comment;
        if (not $text) {
                $text = "Your notification subscription matched a Tapper event.\n".
                  "The following subscription was triggered:\n\n".
                    "Event type: ".$subscription->event.
                      "\nFilter: ".$subscription->filter."\n";
        }


        my $contact      = $subscription->owner->contacts->search({}, {rows => 1})->first;
        my $plugin       = ucfirst($contact->protocol);
        my $plugin_class = "Tapper::Notification::Plugin::${plugin}";
        eval "use $plugin_class"; ## no critic

        if ($@) {
                $self->log->error( "Could not load $plugin_class: $@" );
        } else {
                try{
                no strict 'refs'; ## no critic
                $self->log->info("Call ${plugin_class}::notify()");
                my $cfg = $self->cfg->{notification}{sender}{plugins}{$plugin};
                my $obj = ${plugin_class}->new({cfg => $cfg});
                $obj->notify($contact->address, $text);
                } catch {
                        $self->log->error("Error occured: $_");
                }
        }

        return;
}


sub run
{
        my ($self) = @_;

        my $events = $self->get_events;
        while (my $event = $events->next) {
                my $subscriptions = $self->get_subscriptions($event->type);
                while (my $subscription = $subscriptions->next) {
                        try {
                                if ($self->matches($subscription->filter, $event)) {


                                        $self->notify_owner($subscription);
                                        $subscription->delete unless $subscription->persist;
                                }
                        } catch {
                                my $errormsg = "An error occured while trying to match your subscription for event type '";
                                $errormsg   .= $subscription->event;
                                $errormsg   .= "'.\n";
                                $errormsg   .= "Comment was:'";
                                $errormsg   .= $subscription->comment;
                                $errormsg   .= "'.\n";
                                $errormsg   .= "Condition was:\n";
                                $errormsg   .= $subscription->filter;
                                $errormsg   .= "\n\nThe following error occured:\n$_";
                                $subscription->comment($errormsg);
                                $subscription->update;
                                $self->notify_owner($subscription);
                                $subscription->delete; #always delete broken subscriptions

                        }
                }
                $event->delete;
        }
        return;
}


sub loop
{
        my ($self) = @_;
        while () {
                $self->run();
                sleep $self->cfg->{times}{notification_poll_intervall} || 1;
        }
}

1; # End of Tapper::Notification

__END__
=pod

=encoding utf-8

=head1 NAME

Tapper::Notification - Tapper - Daemon and plugins to handle MCP notifications

=head1 SYNOPSIS

The notification system is responsible for telling people that a certain
condition they subscribed to has occured.

    use Tapper::Notification;

    my $daemon = Tapper::Notification->new();
    $daemon->run();

=head1 FUNCTIONS

=head2 get_events

Read all pending events from database. Try no more than timeout seconds

@return success - Resultset class containing all available events

=head2 get_subscriptions

Get all subscriptions for a given event type.

@param string - type of the event

@return Tapper::Schema::ReportsDB::ResultSet::Notification

=head2 get_testrun_data

Get all neccessary testrun data related to given testrun id.

@param int - testrun id

@return hash ref - testrun data

=head2 get_report_data

Get all neccessary report data related to given report id.

@param int - report id

@return hash ref - report data

=head2 get_testrun_success

Get the overall success of the testrun with given id. If an error
occurs, the function returns undef. This is most probably because the
given testrun has no reports and thus no testrun stats can be found.

@param int - testrun id

@return success - success rate in percent
@return error   - undef

=head2 testrun_success_change

Return whether the last given testruns of a given condition have a
different success state as the current one.

@param string - search string to define matching testruns
@param int    - number of testruns to look back

@return boolean - success change (yes/no)?

=head2 topic_success_change

Return whether the last given testruns of same topic name have a
different success state then the current one. This is pretty much a
testrun_success_change with the topic_name of the current testrun.

@param int    - number of testruns to look back

@return boolean - success change (yes/no)?

=head2 matches

Check whether the given notification condition matches on the given
event, i.e. whether we need to notify the owner. This version uses eval
and should be replaced as soon as perl5.14.2 is available.

@param string - condition
@param Result::NotificationEvent - event

@return true/false based on condition

=head2 testrun

=head2 report

=head2 deep_search

=head2 notify_owner

Send notification to owner.

@param Result::Notification - subscription that triggered the notification

=head2 run

Run the Notification daemon once.

=head2 loop

Run the Notification daemon in an endless loop.

=head1 AUTHOR

AMD OSRC Tapper Team <tapper@amd64.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2012 by Advanced Micro Devices, Inc..

This is free software, licensed under:

  The (two-clause) FreeBSD License

=cut