package Test2::Harness::Job::Runner::Fork;
use strict;
use warnings;

our $VERSION = '0.001047';

use POSIX;
use Scalar::Util qw/openhandle/;
use Test2::Util qw/clone_io CAN_REALLY_FORK pkg_to_file/;
use Test2::Harness::Util qw/write_file/;
use Test2::Harness::Util::IPC qw/swap_io/;

sub viable {
    my $class = shift;
    my ($test) = @_;

    return 0 unless CAN_REALLY_FORK();

    return 0 if $ENV{HARNESS_PERL_SWITCHES};

    my $job = $test->job;

    return 0 if !$job->use_fork;

    # -w switch is ok, otherwise it is a no-go
    return 0 if grep { !m/\s*-w\s*/ } @{$job->switches};

    return 1;
}

sub run {
    my $class = shift;
    my ($test) = @_;

    my $job = $test->job;
    my $preloads = $job->preload || [];

    $_->pre_fork($job) for @$preloads;

    my $pid = fork();
    die "Failed to fork: $!" unless defined $pid;

    # In parent
    return ($pid, undef) if $pid;

    # In Child
    my $file = $job->file;

    # toggle -w switch late
    $^W = 1 if grep { m/\s*-w\s*/ } @{$job->switches};

    $SIG{TERM} = 'DEFAULT';
    $SIG{INT} = 'DEFAULT';
    $SIG{HUP} = 'DEFAULT';

    my $env = $job->env_vars;
    {
        no warnings 'uninitialized';
        $ENV{$_} = $env->{$_} for keys %$env;
    }

    $ENV{T2_HARNESS_FORKED}  = 1;
    $ENV{T2_HARNESS_PRELOAD} = 1;

    my ($in_file, $out_file, $err_file, $event_file) = $test->output_filenames;

    $0 = File::Spec->abs2rel($file);
    $class->_reset_DATA($file);
    @ARGV = ();

    $_->post_fork($job) for @$preloads;

    my $importer = eval <<'    EOT' or die $@;
package main;
#line 0 "-"
sub { shift->import(@_) }
    EOT

    for my $mod (@{$job->load_import || []}) {
        my @args;
        if ($mod =~ s/=(.*)$//) {
            @args = split /,/, $1;
        }
        my $file = pkg_to_file($mod);
        local $0 = '-';
        require $file;
        $importer->($mod, @args);
    }

    for my $mod (@{$job->load || []}) {
        my $file = pkg_to_file($mod);
        require $file;
    }

    # if FindBin is preloaded, reset it with the new $0
    FindBin::init() if defined &FindBin::init;

    # restore defaults
    Getopt::Long::ConfigDefaults() if defined &Getopt::Long::ConfigDefaults;

    # reset the state of empty pattern matches, so that they have the same
    # behavior as running in a clean process.
    # see "The empty pattern //" in perlop.
    # note that this has to be dynamically scoped and can't go to other subs
    "" =~ /^/;

    # Keep a copy of the old STDERR for a while so we can still report errors
    my $stderr = clone_io(\*STDERR);

    write_file($in_file, $job->input);

    my $die = sub {
        my @caller = caller;
        my @caller2 = caller(1);
        my $msg = "$_[0] at $caller[1] line $caller[2] ($caller2[1] line $caller2[2]).\n";
        print $stderr $msg;
        print STDERR $msg;
        POSIX::_exit(127);
    };

    swap_io(\*STDIN,  $in_file,  $die);
    swap_io(\*STDOUT, $out_file, $die);
    swap_io(\*STDERR, $err_file, $die);

    # avoid child processes sharing the same seed value as the parent
    srand();

    if ($INC{'Test2/API.pm'}) {
        Test2::API::test2_stop_preload();
        Test2::API::test2_post_preload_reset();
    }

    my %seen;
    @INC = grep { !$seen{$_}++ } (@{$job->libs}, @INC);

    if ($job->use_stream) {
        $ENV{T2_FORMATTER} = 'Stream';
        require Test2::Formatter::Stream;
        Test2::Formatter::Stream->import(file => $event_file);
    }

    if ($job->times) {
        require Test2::Plugin::Times;
        Test2::Plugin::Times->import();
    }

    @ARGV = @{$job->args};

    $_->pre_launch($job) for @$preloads;

    return (undef, $file);
}

# Heavily modified from forkprove
sub _reset_DATA {
    my $class = shift;
    my ($file) = @_;

    # open DATA from test script
    if (openhandle(\*main::DATA)) {
        close ::DATA;
        if (open my $fh, $file) {
            my $code = do { local $/; <$fh> };
            if (my ($data) = $code =~ /^__(?:END|DATA)__$(.*)/ms) {
                open ::DATA, '<', \$data
                    or die "Can't open string as DATA. $!";
            }
        }
    }

    for my $set (@{$class->preload_list}) {
        my ($mod, $file, $pos) = @$set;

        my $fh = do {
            no strict 'refs';
            *{$mod . '::DATA'};
        };

        # note that we need to ensure that each forked copy is using a
        # different file handle, or else concurrent processes will interfere
        # with each other

        close $fh if openhandle($fh);

        if (open $fh, '<', $file) {
            seek($fh, $pos, 0);
        }
        else {
            warn "Couldn't reopen DATA for $mod ($file): $!";
        }
    }
}

# Heavily modified from forkprove
sub preload_list {
    my $class = shift;

    my $list = [];

    for my $loaded (keys %INC) {
        next unless $loaded =~ /\.pm$/;

        my $mod = $loaded;
        $mod =~ s{/}{::}g;
        $mod =~ s{\.pm$}{};

        my $fh = do {
            no strict 'refs';
            no warnings 'once';
            *{$mod . '::DATA'};
        };

        next unless openhandle($fh);
        push @$list => [$mod, $INC{$loaded}, tell($fh)];
    }

    return $list;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Test2::Harness::Job::Runner::Fork - Logic for running a test job by forking.

=head1 DESCRIPTION

=head1 SOURCE

The source code repository for Test2-Harness can be found at
F<http://github.com/Test-More/Test2-Harness/>.

=head1 MAINTAINERS

=over 4

=item Chad Granum E<lt>exodist@cpan.orgE<gt>

=back

=head1 AUTHORS

=over 4

=item Chad Granum E<lt>exodist@cpan.orgE<gt>

=back

=head1 COPYRIGHT

Copyright 2017 Chad Granum E<lt>exodist7@gmail.comE<gt>.

This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

See F<http://dev.perl.org/licenses/>

=cut