diff --git a/t/100-basic.t b/t/100-basic.t index ef1a66b..5d837fe 100644 --- a/t/100-basic.t +++ b/t/100-basic.t @@ -28,13 +28,11 @@ use autodie; # note: still need to check system() calls manually use FindBin '$Bin'; use lib $Bin; -use Cwd qw(abs_path); -use File::Path qw(remove_tree); use File::Spec; -use File::Temp 0.14 qw(tempdir); use Test::More; +use TestEnvironment; use TestHelpers; # =========================================================================== @@ -44,15 +42,12 @@ use TestHelpers; my $tree_src_dir = sibling_abs_path('tree'); my $db_dir = sibling_abs_path('db'); # the db dir is .gitignored -{ # Remove any existing DB dir - my $ignore; - remove_tree($db_dir, {error => \$ignore}); -} +my $tenv = TestEnvironment->new; # Check programs -my $script_sh = find_program('script.sh'); -my $update_py = find_program('update.py'); -my $query_py = find_program('query.py'); +my $script_sh = $tenv->script_sh; +my $update_py = $tenv->update_py; +my $query_py = $tenv->query_py; ok_or_die( (-f $script_sh && -r _ && -x _), 'script.sh executable', "Could not find executable script.sh at $script_sh"); @@ -61,26 +56,8 @@ ok_or_die( (-f $update_py && -r _ && -x _), 'update.py executable', ok_or_die( (-f $query_py && -r _ && -x _), 'query.py executable', "Could not find executable query.py at $query_py"); -# Copy tree/ into a temporary Git repository, since script.sh requires -# it be run in a Git repo. - -my $tempdir = tempdir(CLEANUP => 1); -my $tempdir_path = abs_path($tempdir); -diag "Using temporary directory $tempdir_path"; -run_program('bash', '-c', "cd \"$tempdir\" && git init") or die("git init failed"); - -run_program('bash', '-c', "tar cf - -C \"$tree_src_dir\" . | tar xf - -C \"$tempdir\"") - or die("Could not copy files into $tempdir"); - -my @gitdir = ('-C', $tempdir_path); - -run_program('git', @gitdir, 'add', '.') or die("git add failed"); -run_program('git', @gitdir, 'commit', '-am', 'Initial commit') - or die("git commit failed"); -run_program('git', @gitdir, 'tag', 'v5.4') or die("git tag failed"); - -$ENV{LXR_REPO_DIR} = $tempdir; -$ENV{LXR_DATA_DIR} = $db_dir; +$tenv->build_repo($tree_src_dir); +$tenv->update_env; # Set LXR_REPO_DIR # Check for tags in `script.sh list-tags`, as a sanity check before # building the test DB @@ -89,12 +66,8 @@ die("Could not list tags: $! ($?)") if $?; ok_or_die( @tags == 1, 'One tag present', "Not one tag (@{[scalar @tags]})"); ok_or_die( $tags[0] =~ /^v5.4$/, 'Found the correct tag', 'Not the tag we expected'); -# Make the new database -ok_or_die( mkdir($db_dir), "Created $db_dir", - "Could not create $db_dir"); - -ok_or_die( run_program($update_py), 'update.py succeeded', - 'Could not create database'); +$tenv->build_db($db_dir); +$tenv->update_env; # Set LXR_DATA_DIR ok_or_die( -d $db_dir, 'database dir exists', "Database dir $db_dir not present"); @@ -109,7 +82,7 @@ ok( (-r File::Spec->catfile($db_dir, $_)), "$_ exists" ) run_produces_ok('ident query (nonexistent)', [$query_py, qw(v5.4 ident SOME_NONEXISTENT_IDENTIFIER_XYZZY_PLUGH)], [qr{^Symbol Definitions:}, qr{^Symbol References:}, qr{^\s*$}], - 1); + MUST_SUCCEED); run_produces_ok('ident query (existent)', [$query_py, qw(v5.4 ident i2c_acpi_notify)], @@ -117,7 +90,7 @@ run_produces_ok('ident query (existent)', qr{drivers/i2c/i2c-core-acpi\.c.+\b402\b.+\bfunction\b}, # def qr{drivers/i2c/i2c-core-acpi\.c.+\b402,439} # refs ], - 1); + MUST_SUCCEED); # Spot-check some files @@ -128,17 +101,17 @@ run_produces_ok('file query (nonexistent)', run_produces_ok('file query (existent), .h', [$query_py, qw(v5.4 file /drivers/i2c/i2c-dev.c)], [qr{\S}], - 1); + MUST_SUCCEED); run_produces_ok('file query (existent), .c', [$query_py, qw(v5.4 file /drivers/i2c/i2c-dev.c)], [qr{i2c-dev\.c}, qr{\bVogl\b}], - 1); + MUST_SUCCEED); run_produces_ok('file query (existent), .h', [$query_py, qw(v5.4 file /drivers/i2c/i2c-core.h)], [qr{i2c-core\.h}, qr{\bWe\b}], - 1); + MUST_SUCCEED); #system('bash'); # Uncomment this if you want to interact with the test repo diff --git a/t/TestClass.pm b/t/TestClass.pm new file mode 100644 index 0000000..006e948 --- /dev/null +++ b/t/TestClass.pm @@ -0,0 +1,8 @@ +# A minified OOP library - https://metacpan.org/pod/distribution/Mo/ReadMe.pod + +package TestClass; +# use Mo qw(build default is required import); +# The following line of code was produced from the previous line by +# Mo::Inline version 0.40 +no warnings;my$M=__PACKAGE__.'::';*{$M.Object::new}=sub{my$c=shift;my$s=bless{@_},$c;my%n=%{$c.'::'.':E'};map{$s->{$_}=$n{$_}->()if!exists$s->{$_}}keys%n;$s};*{$M.import}=sub{import warnings;$^H|=1538;my($P,%e,%o)=caller.'::';shift;eval"no Mo::$_",&{$M.$_.::e}($P,\%e,\%o,\@_)for@_;return if$e{M};%e=(extends,sub{eval"no $_[0]()";@{$P.ISA}=$_[0]},has,sub{my$n=shift;my$m=sub{$#_?$_[0]{$n}=$_[1]:$_[0]{$n}};@_=(default,@_)if!($#_%2);$m=$o{$_}->($m,$n,@_)for sort keys%o;*{$P.$n}=$m},%e,);*{$P.$_}=$e{$_}for keys%e;@{$P.ISA}=$M.Object};*{$M.'build::e'}=sub{my($P,$e)=@_;$e->{new}=sub{$c=shift;my$s=&{$M.Object::new}($c,@_);my@B;do{@B=($c.::BUILD,@B)}while($c)=@{$c.::ISA};exists&$_&&&$_($s)for@B;$s}};*{$M.'default::e'}=sub{my($P,$e,$o)=@_;$o->{default}=sub{my($m,$n,%a)=@_;exists$a{default}or return$m;my($d,$r)=$a{default};my$g='HASH'eq($r=ref$d)?sub{+{%$d}}:'ARRAY'eq$r?sub{[@$d]}:'CODE'eq$r?$d:sub{$d};my$i=exists$a{lazy}?$a{lazy}:!${$P.':N'};$i or ${$P.':E'}{$n}=$g and return$m;sub{$#_?$m->(@_):!exists$_[0]{$n}?$_[0]{$n}=$g->(@_):$m->(@_)}}};*{$M.'is::e'}=sub{my($P,$e,$o)=@_;$o->{is}=sub{my($m,$n,%a)=@_;$a{is}or return$m;sub{$#_&&$a{is}eq'ro'&&caller ne'Mo::coerce'?die$n.' is ro':$m->(@_)}}};*{$M.'required::e'}=sub{my($P,$e,$o)=@_;$o->{required}=sub{my($m,$n,%a)=@_;if($a{required}){my$C=*{$P."new"}{CODE}||*{$M.Object::new}{CODE};no warnings 'redefine';*{$P."new"}=sub{my$s=$C->(@_);my%a=@_[1..$#_];die$n." required"if!exists$a{$n};$s}}$m}};my$i=\&import;*{$M.import}=sub{(@_==2 and not$_[1])?pop@_:@_==1?push@_,grep!/import/,@f:();goto&$i};@f=qw[build default is required import];use strict;use warnings; +1; diff --git a/t/TestEnvironment.pm b/t/TestEnvironment.pm new file mode 100644 index 0000000..3a9d8e7 --- /dev/null +++ b/t/TestEnvironment.pm @@ -0,0 +1,238 @@ +#!/usr/bin/env perl +# TestEnvironment.pm: A class representing an Elixir test environment. +# See license information at end of file. +# +# For a cleaner view of the documentation, run +# perldoc TestEnvironment.pm +# (on Ubuntu, you may need to install the perl-doc package first.) +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# This file uses only core Perl modules, and modules bundled with +# the Elixir distribution. + +=head1 NAME + +TestEnvironment - Class representing an Elixir test environment + +=head1 SYNOPSIS + + use TestEnvironment; + my $tenv = TestEnvironment->new; + $tenv->build_repo($source_path); # Make a git repo + $tenv->build_db($db_path); # Run update.py + $tenv->export_env; # Set $LXR_* environment vars + # Now run tests against the database in $db_path + +=cut + +package TestEnvironment; + +use TestClass; # Now we are a class +use autodie; # note: still need to check system() calls manually + +use Cwd qw(abs_path); +use File::Path qw(remove_tree); +use File::Spec; +use File::Temp 0.14 qw(tempdir); +use FindBin; +use IO::Select; +use IPC::Open3; +use Symbol; + +use Test::More; + +use TestHelpers; + +=head1 ATTRIBUTES + +=head2 lxr_data_dir + +C<$lxr_data_dir> is the value to use in the C environment +variable. + +=head2 lxr_repo_dir + +C<$lxr_repo_dir> is the value to use in the C environment +variable. + +=head2 script_sh + +The path to C. Assigned by default using +C if not specified. + +=head2 query_py + +As L, but for C. + +=head2 update_py + +As L, but for C. + +=cut + +has lxr_data_dir => (); +has lxr_repo_dir => (); +has script_sh => ( + default => sub { find_program('script.sh') } +); +has query_py => ( + default => sub { find_program('query.py') } +); +has update_py => ( + default => sub { find_program('update.py') } +); + +# Internal attributes + +# a variable representing the temporary repository directory. +# When this goes out of scope, the directory will be removed. +has _repo_dir_token => (); + +# a variable representing the temporary DB directory, if any. +# When this goes out of scope, the directory will be removed. +has _data_dir_token => (); + +=head1 MEMBER FUNCTIONS + +=head2 build_repo + +Create a Git repo and tag it. Usage: + + $tenv->build_repo($source_tree_dir); + +C<$source_tree_dir> is the directory holding the tree of source files +you want to index. + +Dies on error. On success, returns the instance, for chaining. + +=cut + +sub build_repo { + my ($self, $tree_src_dir) = @_; + die "Need a source dir" unless $tree_src_dir; + + my $tempdir = tempdir(CLEANUP => 1); + my $tempdir_path = abs_path($tempdir); + my @gitdir = ('-C', $tempdir_path); + + diag "Using temporary directory $tempdir_path"; + run_program('git', 'init', $tempdir_path) or die("git init failed"); + + run_program('bash', '-c', "tar cf - -C \"$tree_src_dir\" . | tar xf - -C \"$tempdir_path\"") + or die("Could not copy files into $tempdir_path"); + + run_program('git', @gitdir, 'add', '.') or die("git add failed"); + run_program('git', @gitdir, 'commit', '-am', 'Initial commit') + or die("git commit failed"); + run_program('git', @gitdir, 'tag', 'v5.4') or die("git tag failed"); + + # Save the results in the instance + $self->_repo_dir_token($tempdir); + $self->lxr_repo_dir($tempdir_path); + + return $self; +} #build_repo() + +=head2 build_db + +Build a test database for the repository. L must be set +before calling this. Usage: + + $tenv->build_db([$db_dir]) + +C<$db_dir> is the directory where you want to put the database. If you do +not provide one, a temporary directory will be created. + +Dies on error. On success, returns the instance, for chaining. + +B: This function will remove the contents of C<$db_dir> +unconditionally. + +=cut + +sub build_db { + my ($self, $db_dir) = @_; + die "No repo dir" unless $self->lxr_repo_dir; + + if($db_dir) { # Remove any existing DB dir + remove_tree($db_dir); + mkdir($db_dir) or die "Could not create fresh $db_dir"; + } + + # Create a temp DB dir if necessary + my $temp_db_dir; + unless($db_dir) { + $temp_db_dir = tempdir(CLEANUP => 1); + $db_dir = abs_path($temp_db_dir); + } + + local $ENV{LXR_REPO_DIR} = $self->lxr_repo_dir; + local $ENV{LXR_DATA_DIR} = $db_dir; + + run_program($self->update_py) + or die "Could not create database from $ENV{LXR_REPO_DIR} in $ENV{LXR_DATA_DIR}"; + + $self->_data_dir_token($temp_db_dir); + $self->lxr_data_dir($db_dir); + + return $self; +} #build_db() + +=head2 update_env + +Set the C and C environment variables. +Will not set a variable if the corresponding member does not have a value. + +Returns the instance, for chaining. + +=cut + +sub update_env { + my $self = shift; + $ENV{LXR_REPO_DIR} = $self->lxr_repo_dir if $self->lxr_repo_dir; + $ENV{LXR_DATA_DIR} = $self->lxr_data_dir if $self->lxr_data_dir; + return $self; +} #update_env() + +=head2 DESTROY + +Destructor. Called automatically. + +=cut + +sub DESTROY { + local($., $@, $!, $^E, $?); + my $self = shift; + + # Release the temporary directories + $self->_data_dir_token(undef); + $self->_repo_dir_token(undef); +} + +1; +__END__ + + +=head1 AUTHOR + +Christopher White, C<< >> + +=head1 COPYRIGHT + +Copyright (c) 2020 D3 Engineering, LLC. + +Elixir is free software; you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Elixir is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with Elixir. If not, see . + +=cut diff --git a/t/TestHelpers.pm b/t/TestHelpers.pm index 4cd06f1..b920da1 100644 --- a/t/TestHelpers.pm +++ b/t/TestHelpers.pm @@ -18,8 +18,6 @@ TestHelpers - Common routines for use in tests C, and all the functions below will be exported. -=head1 FUNCTIONS - =cut package TestHelpers; @@ -28,7 +26,9 @@ use strict; use warnings; use autodie; # note: still need to check system() calls manually +use Cwd qw(abs_path); use File::Spec; +use File::Temp 0.14 qw(tempdir); use FindBin; use IO::Select; use IPC::Open3; @@ -40,10 +40,24 @@ use Test::More; use parent 'Exporter'; our @EXPORT; BEGIN { @EXPORT = qw(sibling_abs_path find_program run_program ok_or_die - run_produces_ok); } + run_produces_ok MUST_SUCCEED); } # =========================================================================== +=head1 CONSTANTS + +=head2 MUST_SUCCEED + +True. So that calls to L will be more self-explanatory. + +=cut + +use constant MUST_SUCCEED => !!1; + +=head1 FUNCTIONS + +These are helper routines that generally perform specific tasks. + =head2 sibling_abs_path Return the absolute path of a file or directory in the same directory as