package Ukigumo::Client; use strict; use warnings; use 5.008001; our $VERSION = '0.35'; use Carp (); use Capture::Tiny; use Encode::Locale; use Encode; use File::Spec; use File::Path qw(mkpath); use LWP::UserAgent; use English '-no_match_vars'; use HTTP::Request::Common qw(POST); use JSON qw(decode_json); use File::Temp; use File::HomeDir; use Cwd; use Scope::Guard; use Ukigumo::Constants; use Ukigumo::Client::CommandRunner; use Ukigumo::Client::Executor::Command; use Ukigumo::Client::Logger; use Ukigumo::Client::YamlConfig; use Ukigumo::Helper qw(normalize_path); use Ukigumo::Logger; use Mouse; has 'workdir' => ( is => 'ro', isa => 'Str', required => 1, lazy => 1, default => sub { my $self = shift; File::Spec->catdir( File::HomeDir->my_home, '.ukigumo', 'work') }, ); has 'project' => ( is => 'rw', isa => 'Str', default => sub { my $self = shift; my $proj = $self->repository; $proj =~ s/\.git$//; $proj =~ s!.+\/!!; $proj || '-'; }, lazy => 1, ); has 'logfh' => ( is => 'ro', default => sub { File::Temp->new(UNLINK => 1) } ); has 'server_url' => ( is => 'ro', isa => 'Str', required => 1, ); has 'user_agent' => ( is => 'ro', required => 1, lazy => 1, default => sub { my $ua = LWP::UserAgent->new( agent => "ukigumo-client/$Ukigumo::Client::VERSION" ); $ua->env_proxy; $ua; }, ); has quiet => ( is => 'ro', isa => 'Bool', default => 0, ); # components has 'vc' => ( is => 'ro', required => 1, handles => [qw(get_revision branch repository)], ); has 'executor' => ( is => 'ro', required => 1, ); has 'notifiers' => ( is => 'rw', default => sub { +[ ] }, ); has 'compare_url' => ( is => 'ro', isa => 'Str', default => '', ); has 'repository_owner' => ( is => 'ro', isa => 'Str', default => '', ); has 'repository_name' => ( is => 'ro', isa => 'Str', default => '', ); # for VC log has vc_log => ( is => 'ro', isa => 'Str', lazy => 1, default => sub { my $self = shift; chomp(my $orig_revision = $self->orig_revision); chomp(my $current_revision = $self->current_revision); join '', $self->vc->get_log($orig_revision, $current_revision); }, ); has current_revision => ( is => 'ro', isa => 'Str', lazy => 1, default => sub { my $self = shift; $self->vc->get_revision(); }, ); has orig_revision => ( is => 'ro', isa => 'Str', lazy => 1, default => sub { my $self = shift; $self->vc->get_revision(); }, ); has 'elapsed_time_sec' => ( is => 'rw', isa => 'Int', default => 0, ); has 'logger' => ( is => 'ro', isa => 'Ukigumo::Client::Logger', lazy => 1, default => sub { my $self = shift; Ukigumo::Client::Logger->new( logfh => $self->logfh, branch => $self->branch, quiet => $self->quiet, ); }, ); no Mouse; sub push_notifier { my $self = shift; push @{$self->notifiers}, @_; } sub run { my $self = shift; # Back to original directory, after work. my $orig_cwd = Cwd::getcwd(); my $guard = Scope::Guard->new( sub { chdir $orig_cwd } ); my $workdir = File::Spec->catdir( $self->workdir, normalize_path($self->project), normalize_path($self->branch) ); $self->logger->infof("ukigumo-client $VERSION"); $self->logger->infof("start testing : " . $self->vc->description()); $self->logger->infof("working directory : " . $workdir); { mkpath($workdir); unless (chdir($workdir)) { $self->reflect_result(STATUS_FAIL); die "Cannot chdir(@{[ $workdir ]}): $!"; } $self->logger->infof('run vc : ' . ref $self->vc); chomp(my $orig_revision = $self->orig_revision); $self->vc->update($self, $workdir); chomp(my $current_revision = $self->current_revision); if ($self->vc->skip_if_unmodified && $orig_revision eq $current_revision) { $self->logger->infof('skip testing'); return; } my $conf = Ukigumo::Client::YamlConfig->new(c => $self); local %ENV = %ENV; $conf->apply_environment_variables; $self->project($conf->project_name || $self->project); $self->push_notifier(@{$conf->notifiers}); my $repository_owner = $self->repository_owner; my $repository_name = $self->repository_name; for my $notify (grep { ref $_ eq NOTIFIER_GITHUBSTATUSES } @{$self->notifiers}) { $notify->send($self, STATUS_PENDING, '', '', $current_revision, $repository_owner, $repository_name); } my $command_runner = Ukigumo::Client::CommandRunner->new(c => $self, config => $conf); $command_runner->run('before_install'); $command_runner->run('install'); $command_runner->run('before_script'); my $executor = defined($conf->script) ? Ukigumo::Client::Executor::Command->new(command => $conf->script) : $self->executor; $self->logger->infof('run executor : ' . ref $executor); my $status = $executor->run($self); $self->logger->infof('finished testing : ' . $status); $command_runner->run('after_script'); $self->reflect_result($status); } $self->logger->infof("end testing"); } sub report_timeout { my ($self, $log_filename) = @_; $self->elapsed_time_sec(undef); $self->reflect_result(STATUS_TIMEOUT, $log_filename); } sub reflect_result { my ($self, $status, $log_filename) = @_; my ($report_url, $last_status) = $self->send_to_server($status, $log_filename); $self->logger->infof("sending notification: @{[ $self->branch ]}, $status"); my $repository_owner = $self->repository_owner; my $repository_name = $self->repository_name; for my $notify (@{$self->notifiers}) { $notify->send($self, $status, $last_status, $report_url, $self->current_revision, $repository_owner, $repository_name); } } sub send_to_server { my ($self, $status, $log_filename) = @_; my $ua = $self->user_agent(); # flush log file before send it $self->logfh->flush(); my $server_url = $self->server_url; $server_url =~ s!/$!!g; my $req = POST $server_url . '/api/v1/report/add', Content_Type => 'form-data', Content => [ project => $self->project, branch => $self->branch, repo => $self->repository, revision => substr($self->current_revision, 0, 10), status => $status, vc_log => $self->vc_log, body => [$log_filename || $self->logfh->filename], compare_url => $self->compare_url, elapsed_time_sec => $self->elapsed_time_sec, ]; my $res = $ua->request($req); $res->is_success or die "Cannot send a report to @{[ $self->server_url ]}/api/v1/report/add:\n" . $res->as_string; my $dat = eval { decode_json($res->decoded_content) } || $res->decoded_content . " : $@"; $self->logger->infof("report url: $dat->{report}->{url}"); my $report_url = $dat->{report}->{url} or die "Cannot get report url"; return ($report_url, $dat->{report}->{last_status}); } sub tee { my ($self, $command) = @_; $self->logger->infof("command: $command"); my ($out) = Capture::Tiny::tee_merged { ( $EUID, $EGID ) = ( $UID, $GID ); my $begin_time = time; system $command; $self->elapsed_time_sec($self->elapsed_time_sec + time - $begin_time); }; $out = Encode::encode("console_in", Encode::decode("console_out", $out)); print {$self->logfh} $out; return $?; } 1; __END__ =encoding utf8 =for stopwords infof warnf =head1 NAME Ukigumo::Client - Client library for Ukigumo =head1 SYNOPSIS use Ukigumo::Client; use Ukigumo::Client::VC::Git; use Ukigumo::Client::Executor::Auto; use Ukigumo::Client::Notify::Debug; use Ukigumo::Client::Notify::Ikachan; my $app = Ukigumo::Client->new( vc => Ukigumo::Client::VC::Git->new( branch => $branch, repository => $repo, ), executor => Ukigumo::Client::Executor::Perl->new(), server_url => $server_url, project => $project, ); $app->push_notifier( Ukigumo::Client::Notify::Ikachan->new( url => $ikachan_url, channel => $ikachan_channel, ) ); $app->run(); =head1 DESCRIPTION Ukigumo::Client is client library for Ukigumo. =head1 ATTRIBUTES =over 4 =item C Working directory for the code. It's C<$ENV{HOME}/.ukigumo/work/$project/$branch> by default. =item C Its' project name. This is a mandatory parameter. =item C Log file handle. It's read only parameter. =item C URL of the Ukigumo server. It's required. =item C instance of L. It's have a default value. =item C This is a version controller object. It's normally Ukigumo::Client::VC::*. But you can write your own class. VC::* objects should have a following methods: get_revision branch repository =item C This is a test executor object. It's normally Ukigumo::Client::Executor::*. But you can write your own class. =item C This is a arrayref of notifier object. It's normally Ukigumo::Client::Notify::*. But you can write your own class. =item C URL to compare differences between range of commitments. =item C Elapsed time as seconds about executing tests. =back =head1 METHODS =over 4 =item $client->push_notifier($notifier : Ukigumo::Client::Notify) push a notifier object to $client->notifiers. =item $client->run() Run a test context. =item $client->send_to_server($status: Int) Send a notification to the sever. =item reflect_result($status: Int) Send a notification to the server and notify via registered notifier. =item $client->tee($command: Str) This method runs C<< $command >> and tee the output of the STDOUT/STDERR to the C. I: exit code by the C<< $command >>. =item $client->logger->infof($message) Print C<< $message >> as INFO and write to the C. =item $client->logger->warnf($message) Print C<< $message >> as WARN and write to the C. =item $client->report_timeout() This method always sends FAIL report to server and notifies to each notifiers. =back =head1 CONFIGURATION If C<.ukigumo.yml> file is on the project root, L loads that file and applies the settings that are written in it. Please see also L. =head1 AUTHOR Tokuhiro Matsuno Etokuhirom AAJKLFJEF@ GMAIL COME =head1 SEE ALSO L, L =head1 LICENSE Copyright (C) Tokuhiro Matsuno This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut