<commit_before>package DCAPI::Sketch;

use strict;
use warnings;

use DCAPI::Terms;

use Util;
use Mo qw/default build builder is required option/;

use constant MEMBERS => qw/entry_point interface manifest metadata api namespace/;
use constant META_MEMBERS => qw/name version description license tags depends authors/;

has dcapi        => ( is => 'ro', required => 1 );
has repo         => ( is => 'ro', required => 1 );
has desc         => ( is => 'ro', required => 1 );
has describe     => ( is => 'rw' );
has location     => ( is => 'ro', required => 1 );
has rel_location => ( is => 'ro', required => 1 );
has verify_files => ( is => 'ro', required => 1 );

# sketch-specific properties
has api         => ( is => 'rw' );
has namespace   => ( is => 'rw' );
has entry_point => ( is => 'rw' );
has interface   => ( is => 'rw' );
has library     => ( is => 'rw', default => sub { 0 } );

has manifest    => ( is => 'rw', default => sub { {} } );
has metadata    => ( is => 'rw', default => sub { {} } );

sub BUILD
{
    my $self = shift;

    my ($config, $reason) = $self->dcapi()->load($self->desc());
    die sprintf("Sketch in location %s could not initialize ($reason) from data %s\n",
                $self->location(),
                $self->desc()||'')
    unless defined $config;

    $self->describe($config);

    my $metadata = Util::hashref_search($config, 'metadata');

    foreach my $m (MEMBERS())
    {
        my $v = Util::hashref_search($config, $m);
        unless (defined $v)
        {
            if ($m eq 'entry_point')
            {
                $self->library(1);
            }
            elsif ($m eq 'namespace')
            {
                # it's OK to have a null namespace, it becomes "default"
            }
            else
            {
                die sprintf("Sketch in location %s is missing required attribute: %s\n",
                            $self->location(),
                            $m);
            }
        }

        $self->$m($v);
    }

    $self->namespace('default') unless $self->namespace();

    if ($self->verify_files())
    {
        my $manifest = $self->manifest();
        my $name = $self->name();

        foreach my $f (sort keys %$manifest)
        {
            my $file = sprintf("%s/%s", $self->location(), $f);
            die "Sketch $name is missing manifest file $file" unless -f $file;
        }
    }
}

# yeah it's a hack, but better than AUTOLOAD
# define a metadata accessor for each keyword in META_MEMBERS
foreach my $mm ( META_MEMBERS() )
{
    my $method_name = __PACKAGE__ . '::' . $mm;
    no strict 'refs';
    *{$method_name} = sub
    {
        my $self = shift @_;
        if (scalar @_)
        {
            $self->metadata()->{$mm} = shift;
        }

        return $self->metadata()->{$mm};
    };
}

sub data_dump
{
    my $self = shift;
    my $flatten = shift;

    my %ret = (
               map { $_ => $self->$_ } MEMBERS()
              );
    if ($flatten)
    {
        foreach my $mkey (META_MEMBERS())
        {
            $ret{$mkey} = $ret{metadata}->{$mkey}
            if exists $ret{metadata}->{$mkey};
        }
    }

    return \%ret;
}

sub runfile_data_dump
{
    my $self = shift;

    my %ret;
    foreach my $m (sort qw/authors tags version name license location/)
    {
        $ret{$m} = $self->$m();
    }

    # only dependencies with :: are useful
    my %depends = defined $self->depends() ? %{$self->depends()} : ();
    $ret{depends} = [ grep { $_ =~ m/::/ } sort keys %depends ];

    my %manifest = defined $self->manifest() ? %{$self->manifest()} : ();
    my @manifest = sort keys %manifest;
    $ret{manifest} = \@manifest;
    $ret{manifest_cf} = [ grep { $_ =~ m/\.cf$/ } @manifest ];
    $ret{manifest_docs} = [ grep { $self->manifest()->{$_}->{documentation} } @manifest ];
    $ret{manifest_test} = [ grep { $self->manifest()->{$_}->{test} } @manifest ];
    $ret{manifest_exe} = [ grep {
        exists $self->manifest()->{$_}->{perm} &&
        (oct($self->manifest()->{$_}->{perm}) & 0111)
    } @manifest ];

    my %known = map { $_ => 1 } (@{$ret{manifest_cf}}, @{$ret{manifest_exe}}, @{$ret{manifest_docs}}, @{$ret{manifest_test}});
    $ret{manifest_extra} = [ grep { !exists $known{$_} } @manifest ];

    return \%ret;
}

sub test
{
    my $self = shift;
    my $options = shift;

    my $tests;
    eval
    {
        my @todo = @{$self->runfile_data_dump()->{manifest_test}};
        @todo = map { sprintf("%s/%s", $self->location(), $_) } @todo;

        $ENV{HARNESS_VERBOSE} = ! ! $options->{test};
        $ENV{CFAGENT} = $self->dcapi()->cfagent();
        $ENV{CFPROMISES} = $self->dcapi()->cfpromises();

        require Test::Harness;

        chdir $self->location();
        my $output = '';
        open(my $log_fh, ">", \$output);

        my ($total, $failed) = Test::Harness::execute_tests(
                                                            out => $log_fh,
                                                            tests => \@todo,
                                                           );

        foreach my $ref ($total, $failed)
        {
            next unless ref $ref eq 'HASH' && exists $ref->{bench};
            $ref->{bench} = $ref->{bench}->timestr();
        }

        $tests = {
                  total => $total,
                  failed => $failed,
                  log => $output,
                 };

        $tests->{success} = ! (ref $failed eq 'HASH' && scalar keys %{$failed});

        # print Util::dump_ref($tests);
    };

    return $tests || { total => undef, ok => undef, failed => undef, log => [$@] };
}

sub make_readme
{
    my $self = shift;

    # describe sketch metadata
    my $readme = <<EOHIPPUS;<commit_msg>cf-dc-api.pl: handle old-as-the-hills Test::Harness versions that do not have execute_tests<commit_after>package DCAPI::Sketch;

use strict;
use warnings;

use DCAPI::Terms;

use Util;
use Mo qw/default build builder is required option/;

use constant MEMBERS => qw/entry_point interface manifest metadata api namespace/;
use constant META_MEMBERS => qw/name version description license tags depends authors/;

has dcapi        => ( is => 'ro', required => 1 );
has repo         => ( is => 'ro', required => 1 );
has desc         => ( is => 'ro', required => 1 );
has describe     => ( is => 'rw' );
has location     => ( is => 'ro', required => 1 );
has rel_location => ( is => 'ro', required => 1 );
has verify_files => ( is => 'ro', required => 1 );

# sketch-specific properties
has api         => ( is => 'rw' );
has namespace   => ( is => 'rw' );
has entry_point => ( is => 'rw' );
has interface   => ( is => 'rw' );
has library     => ( is => 'rw', default => sub { 0 } );

has manifest    => ( is => 'rw', default => sub { {} } );
has metadata    => ( is => 'rw', default => sub { {} } );

sub BUILD
{
    my $self = shift;

    my ($config, $reason) = $self->dcapi()->load($self->desc());
    die sprintf("Sketch in location %s could not initialize ($reason) from data %s\n",
                $self->location(),
                $self->desc()||'')
    unless defined $config;

    $self->describe($config);

    my $metadata = Util::hashref_search($config, 'metadata');

    foreach my $m (MEMBERS())
    {
        my $v = Util::hashref_search($config, $m);
        unless (defined $v)
        {
            if ($m eq 'entry_point')
            {
                $self->library(1);
            }
            elsif ($m eq 'namespace')
            {
                # it's OK to have a null namespace, it becomes "default"
            }
            else
            {
                die sprintf("Sketch in location %s is missing required attribute: %s\n",
                            $self->location(),
                            $m);
            }
        }

        $self->$m($v);
    }

    $self->namespace('default') unless $self->namespace();

    if ($self->verify_files())
    {
        my $manifest = $self->manifest();
        my $name = $self->name();

        foreach my $f (sort keys %$manifest)
        {
            my $file = sprintf("%s/%s", $self->location(), $f);
            die "Sketch $name is missing manifest file $file" unless -f $file;
        }
    }
}

# yeah it's a hack, but better than AUTOLOAD
# define a metadata accessor for each keyword in META_MEMBERS
foreach my $mm ( META_MEMBERS() )
{
    my $method_name = __PACKAGE__ . '::' . $mm;
    no strict 'refs';
    *{$method_name} = sub
    {
        my $self = shift @_;
        if (scalar @_)
        {
            $self->metadata()->{$mm} = shift;
        }

        return $self->metadata()->{$mm};
    };
}

sub data_dump
{
    my $self = shift;
    my $flatten = shift;

    my %ret = (
               map { $_ => $self->$_ } MEMBERS()
              );
    if ($flatten)
    {
        foreach my $mkey (META_MEMBERS())
        {
            $ret{$mkey} = $ret{metadata}->{$mkey}
            if exists $ret{metadata}->{$mkey};
        }
    }

    return \%ret;
}

sub runfile_data_dump
{
    my $self = shift;

    my %ret;
    foreach my $m (sort qw/authors tags version name license location/)
    {
        $ret{$m} = $self->$m();
    }

    # only dependencies with :: are useful
    my %depends = defined $self->depends() ? %{$self->depends()} : ();
    $ret{depends} = [ grep { $_ =~ m/::/ } sort keys %depends ];

    my %manifest = defined $self->manifest() ? %{$self->manifest()} : ();
    my @manifest = sort keys %manifest;
    $ret{manifest} = \@manifest;
    $ret{manifest_cf} = [ grep { $_ =~ m/\.cf$/ } @manifest ];
    $ret{manifest_docs} = [ grep { $self->manifest()->{$_}->{documentation} } @manifest ];
    $ret{manifest_test} = [ grep { $self->manifest()->{$_}->{test} } @manifest ];
    $ret{manifest_exe} = [ grep {
        exists $self->manifest()->{$_}->{perm} &&
        (oct($self->manifest()->{$_}->{perm}) & 0111)
    } @manifest ];

    my %known = map { $_ => 1 } (@{$ret{manifest_cf}}, @{$ret{manifest_exe}}, @{$ret{manifest_docs}}, @{$ret{manifest_test}});
    $ret{manifest_extra} = [ grep { !exists $known{$_} } @manifest ];

    return \%ret;
}

sub test
{
    my $self = shift;
    my $options = shift;

    my $tests;
    eval
    {
        my @todo = @{$self->runfile_data_dump()->{manifest_test}};
        @todo = map { sprintf("%s/%s", $self->location(), $_) } @todo;

        $ENV{HARNESS_VERBOSE} = ! ! $options->{test};
        $ENV{CFAGENT} = $self->dcapi()->cfagent();
        $ENV{CFPROMISES} = $self->dcapi()->cfpromises();

        require Test::Harness;

        chdir $self->location();
        my $output = '';
        open(my $log_fh, ">", \$output);

        my $total = {};
        my $failed = {};

        if (defined &Test::Harness::execute_tests)
        {
            ($total, $failed) = Test::Harness::execute_tests(
                                                             out => $log_fh,
                                                             tests => \@todo,
                                                            );

            foreach my $ref ($total, $failed)
            {
                next unless ref $ref eq 'HASH' && exists $ref->{bench};
                $ref->{bench} = $ref->{bench}->timestr();
            }
        }
        else
        {
            $total = { };
            $failed->{$_} = 1 foreach @todo;
            eval
            {
                Test::Harness::runtests(@todo);
                $total = $failed;
                $failed = {};
            };
            $output = $@;
        }

        $tests = {
                  total => $total,
                  failed => $failed,
                  log => $output,
                 };

        $tests->{success} = ! (ref $failed eq 'HASH' && scalar keys %{$failed});

        # print Util::dump_ref($tests);
    };

    return $tests || { total => undef, ok => undef, failed => undef, log => [$@] };
}

sub make_readme
{
    my $self = shift;

    # describe sketch metadata
    my $readme = <<EOHIPPUS;
{'fraction_non_alphanumeric': 0.17884214910453977, 'fraction_numerical': 0.002249062890462307, 'mean_word_length': 1.819633630812588, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 212, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2221527', 'n_tokens_mistral': 3873, 'n_tokens_neox': 3680, 'n_words': 950}