1package App::Yath::Plugin::YathUI; 2use strict; 3use warnings; 4 5our $VERSION = '1.000082'; 6 7use File::Spec; 8use Test2::Harness::Util qw/read_file mod2file/; 9use Test2::Harness::Util::JSON qw/decode_json/; 10 11use App::Yath::Options; 12use parent 'App::Yath::Plugin'; 13 14sub can_log { 15 my ($option, $options) = @_; 16 17 return 1 if $options->included->{'App::Yath::Options::Logging'}; 18 return 0; 19} 20 21sub can_finder { 22 my ($option, $options) = @_; 23 24 return 1 if $options->included->{'App::Yath::Options::Finder'}; 25 return 0; 26} 27 28option_group {prefix => 'yathui', category => "YathUI Options"} => sub { 29 option url => ( 30 type => 's', 31 alt => ['uri'], 32 description => "Yath-UI url", 33 long_examples => [" http://my-yath-ui.com/..."], 34 ); 35 36 option api_key => ( 37 type => 's', 38 description => "Yath-UI API key. This is not necessary if your Yath-UI instance is set to single-user" 39 ); 40 41 option project => ( 42 type => 's', 43 description => "The Yath-UI project for your test results", 44 ); 45 46 option mode => ( 47 type => 's', 48 default => 'qvfd', 49 description => "Set the upload mode (default 'qvfd')", 50 long_examples => [ 51 ' summary', 52 ' qvf', 53 ' qvfd', 54 ' complete', 55 ], 56 ); 57 58 option retry => ( 59 type => 'c', 60 description => "How many times to try an operation before giving up", 61 default => 0, 62 ); 63 64 option grace => ( 65 description => "If yath cannot connect to yath-ui it normally throws an error, use this to make it fail gracefully. You get a warning, but things keep going.", 66 default => 0, 67 ); 68 69 option durations => ( 70 description => "Poll duration data from Yath-UI to help order tests efficiently", 71 default => 0, 72 applicable => \&can_finder, 73 ); 74 75 option coverage => ( 76 description => "Poll coverage data from Yath-UI to determine what tests should be run for changed files", 77 default => 0, 78 applicable => \&can_finder, 79 ); 80 81# TODO 82# option median_durations => ( 83# type => 'b', 84# description => "Get median duration data", 85# default => 0, 86# ); 87 88 option medium_duration => ( 89 type => 's', 90 description => "Minimum duration length (seconds) before a test goes from SHORT to MEDIUM", 91 long_examples => [' 5'], 92 default => 5, 93 ); 94 95 option long_duration => ( 96 type => 's', 97 description => "Minimum duration length (seconds) before a test goes from MEDIUM to LONG", 98 long_examples => [' 10'], 99 default => 10, 100 ); 101 102 option upload => ( 103 description => "Upload the log to Yath-UI", 104 default => 0, 105 applicable => \&can_log, 106 ); 107 108 post -1 => sub { 109 my %params = @_; 110 111 my $settings = $params{settings}; 112 my $options = $params{options}; 113 114 my $has_finder = $options->included->{'App::Yath::Options::Finder'}; 115 my $has_logger = $options->included->{'App::Yath::Options::Logging'}; 116 117 my $has_durations = $has_finder && $settings->yathui->durations; 118 my $has_upload = $has_logger && $settings->yathui->upload; 119 my $has_coverage = $has_finder && $settings->yathui->coverage; 120 121 return unless $has_durations || $has_upload || $has_coverage; 122 123 my $url = $settings->yathui->url or die "'--yathui-url URL' is required to use durations, coverage, or upload a log"; 124 my $project = $settings->yathui->project or die "'--yathui-project NAME' is required to use durations, coverage, or upload a log"; 125 my $grace = $settings->yathui->grace; 126 127 $url =~ s{/+$}{}g; 128 129 if ($has_upload) { 130 $settings->logging->field(log => 1); 131 $settings->logging->field(bzip2 => 1); 132 } 133 134 if ($has_coverage) { 135 my $curl = join '/' => ($url, 'coverage', $project); 136 $settings->cover->field(($grace ? 'maybe_from' : 'from'), $curl); 137 } 138 139 if ($has_durations) { 140 my $med = $settings->yathui->medium_duration; 141 my $long = $settings->yathui->long_duration; 142 143 my $durl = join '/' => ($url, 'durations', $project, $med, $long); 144 $settings->finder->field(($grace ? 'maybe_durations' : 'durations'), $durl); 145 } 146 147 return; 148 }; 149}; 150 151sub finish { 152 my $this = shift; 153 my %params = @_; 154 155 my $settings = $params{settings}; 156 157 return unless $settings->yathui->upload; 158 159 my $log_file = $settings->logging->log_file; 160 my ($filename) = reverse File::Spec->splitpath($log_file); 161 162 my $url = $settings->yathui->url; 163 $url =~ s{/+$}{}g; 164 $url = join "/" => ($url, 'upload'); 165 166 my %fields; 167 168 for my $field (qw/project api_key mode/) { 169 my $val = $settings->yathui->field($field) or next; 170 $fields{$field} = $val; 171 } 172 173 require HTTP::Tiny; 174 eval { require HTTP::Tiny::Multipart; 1 } or die "To use --yathui-upload you must install HTTP::Tiny::Multipart.\n"; 175 176 my $res; 177 for (0 .. $settings->yathui->retry) { 178 my $http = HTTP::Tiny->new; 179 $res = $http->post_multipart( 180 $url => { 181 headers => {'Content-Type' => 'application/json'}, 182 183 log_file => { 184 filename => $filename, 185 content => read_file($log_file, no_decompress => 1), 186 content_type => 'application/x-bzip2', 187 }, 188 189 action => 'Upload Log', 190 json => 1, 191 192 %fields, 193 }, 194 ); 195 196 next unless $res; 197 last if $res->{status} eq '200'; 198 } 199 200 my ($ok, $msg); 201 if ($res && $res->{status} eq '200') { 202 my $data; 203 $ok = eval { $data = decode_json($res->{content}); 1 }; 204 if ($ok) { 205 if ($data->{errors} && @{$data->{errors}}) { 206 $ok = 0; 207 $msg = join "\n" => (@{$data->{errors}}); 208 } 209 elsif ($data->{messages}) { 210 $ok = 1; 211 212 my $url = $settings->yathui->url; 213 $url =~ s{/+$}{}g; 214 215 $msg = join "\n" => ( 216 @{$data->{messages}}, 217 $data->{run_id} ? ("YathUI run url: " . join '/' => ($url, 'run', $data->{run_id})) : (), 218 ); 219 } 220 else { 221 $ok = 0; 222 $msg = "No messages recieved"; 223 } 224 } 225 else { 226 $msg = $@; 227 } 228 } 229 else { 230 if ($res) { 231 $msg = "Server responded with " . $res->{status} . ":\n" . ($res->{content} // 'NO CONTENT'); 232 } 233 else { 234 $msg = "Failed to upload yathui log, no response object"; 235 } 236 } 237 238 chomp($msg); 239 $msg = "YathUI Upload: $msg"; 240 if ($ok) { 241 print "\n$msg\n"; 242 } 243 else { 244 if ($settings->yathui->grace) { 245 warn $msg; 246 } 247 else { 248 die $msg; 249 } 250 } 251 252 return; 253} 254 2551; 256 257__END__ 258 259=pod 260 261=encoding UTF-8 262 263=head1 NAME 264 265App::Yath::Plugin::YathUI - Plugin to interact with a YathUI server 266 267=head1 DESCRIPTION 268 269If you have a Yath-UI L<Test2::Harness::UI> server, you can use this module to 270have yath automatically upload logs or retrieve durations data 271 272=head1 PROVIDED OPTIONS 273 274=head2 COMMAND OPTIONS 275 276=head3 YathUI Options 277 278=over 4 279 280=item --yathui-api-key ARG 281 282=item --yathui-api-key=ARG 283 284=item --no-yathui-api-key 285 286Yath-UI API key. This is not necessary if your Yath-UI instance is set to single-user 287 288 289=item --yathui-coverage 290 291=item --no-yathui-coverage 292 293Poll coverage data from Yath-UI to determine what tests should be run for changed files 294 295 296=item --yathui-durations 297 298=item --no-yathui-durations 299 300Poll duration data from Yath-UI to help order tests efficiently 301 302 303=item --yathui-grace 304 305=item --no-yathui-grace 306 307If yath cannot connect to yath-ui it normally throws an error, use this to make it fail gracefully. You get a warning, but things keep going. 308 309 310=item --yathui-long-duration 10 311 312=item --no-yathui-long-duration 313 314Minimum duration length (seconds) before a test goes from MEDIUM to LONG 315 316 317=item --yathui-medium-duration 5 318 319=item --no-yathui-medium-duration 320 321Minimum duration length (seconds) before a test goes from SHORT to MEDIUM 322 323 324=item --yathui-mode summary 325 326=item --yathui-mode qvf 327 328=item --yathui-mode qvfd 329 330=item --yathui-mode complete 331 332=item --no-yathui-mode 333 334Set the upload mode (default 'qvfd') 335 336 337=item --yathui-project ARG 338 339=item --yathui-project=ARG 340 341=item --no-yathui-project 342 343The Yath-UI project for your test results 344 345 346=item --yathui-retry 347 348=item --no-yathui-retry 349 350How many times to try an operation before giving up 351 352Can be specified multiple times 353 354 355=item --yathui-upload 356 357=item --no-yathui-upload 358 359Upload the log to Yath-UI 360 361 362=item --yathui-url http://my-yath-ui.com/... 363 364=item --uri http://my-yath-ui.com/... 365 366=item --no-yathui-url 367 368Yath-UI url 369 370 371=back 372 373=head1 SOURCE 374 375The source code repository for Test2-Harness can be found at 376F<http://github.com/Test-More/Test2-Harness/>. 377 378=head1 MAINTAINERS 379 380=over 4 381 382=item Chad Granum E<lt>exodist@cpan.orgE<gt> 383 384=back 385 386=head1 AUTHORS 387 388=over 4 389 390=item Chad Granum E<lt>exodist@cpan.orgE<gt> 391 392=back 393 394=head1 COPYRIGHT 395 396Copyright 2020 Chad Granum E<lt>exodist7@gmail.comE<gt>. 397 398This program is free software; you can redistribute it and/or 399modify it under the same terms as Perl itself. 400 401See F<http://dev.perl.org/licenses/> 402 403=cut 404