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