TestEnvironment: create PROJ_DIR structure

- Instead of creating standalone temporary data and repo dirs, create a
  temporary LXR_PROJ_DIR and put the data and repo dirs under it.
- Change API test project name to match that now used in TestEnvironment

This is the beginning of the test infrastructure for web.py.
This commit is contained in:
Christopher White 2020-05-13 10:02:56 -04:00 committed by Chris White
parent d3ba0cea3b
commit 52d52cb2d6
4 changed files with 192 additions and 66 deletions

View file

@ -24,6 +24,9 @@ TestEnvironment - Class representing an Elixir test environment
$tenv->export_env; # Set $LXR_* environment vars
# Now run tests against the database in $db_path
This module creates a temporary project dir and populates it with repo and
data subdirs in a single project, named "testproj".
=cut
package TestEnvironment;
@ -44,8 +47,15 @@ use Test::More;
use TestHelpers;
use constant PROJECT => 'testproj';
=head1 ATTRIBUTES
=head2 lxr_proj_dir
C<$lxr_proj_dir> is the value to use in the C<LXR_DATA_DIR> environment
variable.
=head2 lxr_data_dir
C<$lxr_data_dir> is the value to use in the C<LXR_DATA_DIR> environment
@ -69,12 +79,17 @@ As L</script_sh>, but for C<query.py>.
As L</script_sh>, but for C<update.py>.
=head2 web_py
As L</script_sh>, but for C<web.py>.
=head2 find_doc
As L</script_sh>, but for C<find-file-doc-comments.pl>.
=cut
has lxr_proj_dir => ();
has lxr_data_dir => ();
has lxr_repo_dir => ();
has script_sh => (
@ -86,19 +101,18 @@ has query_py => (
has update_py => (
default => sub { find_program('update.py') }
);
has web_py => (
default => sub { find_program(qw(http web.py)) }
);
has find_doc => (
default => sub { find_program('find-file-doc-comments.pl') }
);
# Internal attributes
# a variable representing the temporary repository directory.
# a variable representing the temporary project 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 => ();
has _proj_dir_token => ();
=head1 MEMBER FUNCTIONS
@ -118,15 +132,15 @@ Dies on error. On success, returns the instance, for chaining.
sub build_repo {
my ($self, $tree_src_dir) = @_;
die "Need a source dir" unless $tree_src_dir;
die "No repo dir" unless $self->lxr_repo_dir;
my $tempdir = tempdir(CLEANUP => 1);
my $tempdir_path = abs_path($tempdir);
my $tempdir_path = $self->lxr_repo_dir;
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\"")
run_program('sh', '-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");
@ -134,10 +148,6 @@ sub build_repo {
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()
@ -146,50 +156,40 @@ sub build_repo {
Build a test database for the repository. L</lxr_repo_dir> 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.
$tenv->build_db()
Dies on error. On success, returns the instance, for chaining.
B<CAUTION>: This function will remove the contents of C<$db_dir>
B<CAUTION>: This function will remove the contents of C<< $tenv->lxr_data_dir >>
unconditionally.
=cut
sub build_db {
my ($self, $db_dir) = @_;
my $self = shift;
die "No repo dir" unless $self->lxr_repo_dir;
die "No data dir" unless $self->lxr_data_dir;
my $db_dir = $self->lxr_data_dir;
if($db_dir) { # Remove any existing DB dir
if(-e $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<LXR_REPO_DIR> and C<LXR_DATA_DIR> environment variables.
Will not set a variable if the corresponding member does not have a value.
Set the C<LXR_PROJ_DIR>, C<LXR_REPO_DIR>, and C<LXR_DATA_DIR> environment
variables. Will not set a variable if the corresponding member does not have
a value.
Returns the instance, for chaining.
@ -197,6 +197,7 @@ Returns the instance, for chaining.
sub update_env {
my $self = shift;
$ENV{LXR_PROJ_DIR} = $self->lxr_proj_dir if $self->lxr_proj_dir;
$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;
@ -211,15 +212,73 @@ Returns a human-readable report of the current environment's state.
sub report {
my $self = shift;
return <<EOT;
Project: @{[$self->lxr_proj_dir || '<unknown>']}
Repository: @{[$self->lxr_repo_dir || '<unknown>']}
Database: @{[$self->lxr_data_dir || '<unknown>']}
script.sh: @{[$self->script_sh || '<unknown>']}
update.py: @{[$self->update_py || '<unknown>']}
query.py: @{[$self->query_py || '<unknown>']}
web.py: @{[$self->web_py || '<unknown>']}
find-file-doc-comments.pl: @{[$self->find_doc || '<unknown>']}
EOT
} #report()
=head2 make_web_request
Request a URL from L</web_py>. Usage:
my $html = $tenv->make_web_request($url);
# Returns the HTML from stdout, or dies
my ($exit_status, $lrStdout, $lrStderr) = $tenv->make_web_request($url);
# Returns the shell exit status, stdout text, and stderr text.
See L<TestEnvironment/run_program> for the details of the return values
in the second case.
=cut
sub make_web_request {
my ($self, $url) = @_;
$self->update_env; # just in case
local $ENV{REQUEST_URI} = $url;
my ($exit_status, $lrStdout, $lrStderr) = run_program($self->web_py);
if(!wantarray) {
return $lrStdout;
} else {
return ($exit_status, $lrStdout, $lrStderr);
}
} #make_web_request()
=head2 BUILD
Constructor. Creates the temporary project dir.
=cut
sub BUILD {
my $self = shift;
my $temp_proj_dir = tempdir(CLEANUP => 1);
my $proj_dir = abs_path($temp_proj_dir);
# Make the directory structure
mkdir File::Spec->catdir($proj_dir, PROJECT);
my $data_dir = File::Spec->catdir($proj_dir, PROJECT, 'data');
my $repo_dir = File::Spec->catdir($proj_dir, PROJECT, 'repo');
mkdir $data_dir;
mkdir $repo_dir;
# Save the paths
$self->_proj_dir_token($temp_proj_dir);
$self->lxr_proj_dir($proj_dir);
$self->lxr_data_dir($data_dir);
$self->lxr_repo_dir($repo_dir);
} #BUILD()
=head2 DESTROY
Destructor. Called automatically.
@ -231,9 +290,8 @@ sub DESTROY {
my $self = shift;
# Release the temporary directories
$self->_data_dir_token(undef);
$self->_repo_dir_token(undef);
}
$self->_proj_dir_token(undef);
} #DESTROY()
1;
__END__

View file

@ -87,45 +87,90 @@ sub sibling_abs_path {
Looks for a program in the parent directory of this script.
Usage:
$path = find_program('program name')
$path = find_program(['subdir', ]'program name')
=cut
sub find_program {
my $program = shift;
my $pgm_file = pop; # Last arg
my @pgm_dirs = @_; # Any args before the last are additional dirs.
my ($vol, $directories, $file) = File::Spec->splitpath($FindBin::Bin, 1); # 1 => is a dir
my ($my_vol, $my_dirs, undef) = File::Spec->splitpath($FindBin::Bin, 1); # 1 => is a dir
# Go up to the parent of the directory holding this file
my @dirs = File::Spec->splitdir($directories);
die "Cannot run from the root directory" unless @dirs >= 2;
pop @dirs;
$directories = File::Spec->catdir(@dirs);
my @my_dirs = File::Spec->splitdir($my_dirs);
die "Cannot run from the root directory" unless @my_dirs >= 2;
pop @my_dirs;
my $dest_dirs = File::Spec->catdir(@my_dirs, @pgm_dirs);
return File::Spec->catpath($vol, $directories, $program);
return File::Spec->catpath($my_vol, $dest_dirs, $pgm_file);
} #find_program()
=head2 run_program
Print a command, then run it. Returns true if system() and the command
succeed, false otherwise. Usage:
Print a command, then run it. Can be used three ways:
$ok = run_program('program', 'arg1', ...)
=over
=item In void context
Returns if system() and the command succeed, dies otherwise. Usage:
run_program('program', 'arg1', ...);
=item In scalar context
Returns true if system() and the command succeed, false otherwise. Usage:
my $ok = run_program('program', 'arg1', ...);
=item In list context
Returns the exit status, stdout, and stderr. Usage:
my ($exit_status, $lrStdout, $lrStderr) = run_program('program', 'arg1', ...);
# Returns the shell exit status, stdout text, and stderr text.
C<$lrStdout> and C<$lrStderr> are references to the lists of output lines
on the respective handles.
C<$exit_status> is C<128+signal> if the process was killed by C<signal>,
for consistency with bash (L<https://tldp.org/LDP/abs/html/exitcodes.html>).
=back
=cut
sub _run_and_capture; # forward
sub run_program {
diag "Running @_";
if(wantarray) {
goto &_run_and_capture;
}
my $errmsg;
my $status = system(@_);
if ($status == -1) {
diag "failed to execute $_[0]: $!";
$errmsg = "failed to execute $_[0]: $!";
}
elsif ($status & 127) {
diag sprintf "$_[0] died with signal %d, %s coredump\n",
$errmsg = sprintf "$_[0] died with signal %d, %s coredump\n",
($status & 127), ($status & 128) ? 'with' : 'without';
}
elsif($status != 0) {
$errmsg = sprintf "$_[0] exited with value %d\n", $status >> 8;
}
else {
diag sprintf "$_[0] exited with value %d\n", $status >> 8;
diag "$_[0] reported success";
}
if($errmsg) {
die $errmsg unless defined wantarray;
diag $errmsg;
}
return($status == 0);
@ -200,14 +245,16 @@ the "Documented in" section of the output of C<@program_and_args>.
=cut
sub run_produces_ok {
my ($desc, $lrProgram, $lrRegexes, $mustSucceed, $printOutput) = @_;
# _run_and_capture: run a program and return its exit status and output.
# Usage:
# my ($exit_status, \@stdout, \@stderr) = run_program('program', 'arg1', ...);
# Run program and capture stdout and stderr
sub _run_and_capture {
my ($in , $out, $err); # Filehandles
$err = Symbol::gensym;
diag "Running @$lrProgram";
my $pid = open3($in, $out, $err, @$lrProgram);
diag "Running @_";
my $pid = open3($in, $out, $err, @_);
my (@outlines, @errlines); # Captured output
my $s = IO::Select->new;
@ -231,7 +278,19 @@ sub run_produces_ok {
}
waitpid $pid, 0;
my $exit_status = $? >> 8;
my $exit_status = $?;
$exit_status = ($exit_status & 127) + 128 if $exit_status & 127; # Killed by signal
return ($exit_status, \@outlines, \@errlines);
} #_run_and_capture()
sub run_produces_ok {
my ($desc, $lrProgram, $lrRegexes, $mustSucceed, $printOutput) = @_;
my ($exit_status, $outlines, $errlines) = _run_and_capture(@$lrProgram);
my @outlines = @$outlines;
my @errlines = @$errlines;
if ($printOutput) {
diag "@outlines";

View file

@ -5,7 +5,7 @@ import sys
import falcon
from falcon import testing
api_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..','api'))
api_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..','api'))
sys.path.insert(0, api_dir)
from api import create_ident_getter
@ -17,14 +17,14 @@ class APITest(testing.TestCase):
self.app = create_ident_getter()
def test_identifier_not_found(self):
result = self.simulate_get('/ident/tree/SOME_NONEXISTENT_IDENTIFIER', query_string="version=latest&family=C")
result = self.simulate_get('/ident/testproj/SOME_NONEXISTENT_IDENTIFIER', query_string="version=latest&family=C")
self.assertEqual(result.status_code, 200)
self.assertEqual(result.json, {'definitions': [], 'references':[]})
def test_missing_version(self):
# A get request without a version query string
result = self.simulate_get('/ident/tree/of_i2c_get_board_info')
result = self.simulate_get('/ident/testproj/of_i2c_get_board_info', query_string="")
self.assertEqual(result.status_code, 400)
@ -33,8 +33,8 @@ class APITest(testing.TestCase):
self.assertEqual(result.json["description"], required_response.description)
def test_existing_identifier(self):
result_for_specific_version = self.simulate_get('/ident/tree/of_i2c_get_board_info', query_string="version=v5.4&family=C")
result_for_latest_version = self.simulate_get('/ident/tree/of_i2c_get_board_info', query_string="version=latest&family=C")
result_for_specific_version = self.simulate_get('/ident/testproj/of_i2c_get_board_info', query_string="version=v5.4&family=C")
result_for_latest_version = self.simulate_get('/ident/testproj/of_i2c_get_board_info', query_string="version=latest&family=C")
expected_json = {
'definitions':

View file

@ -22,13 +22,16 @@
use FindBin '$Bin';
use lib $Bin;
use Cwd;
use TestEnvironment;
use TestHelpers;
# ===========================================================================
# Main
# These two lines are all that's required to set up for a test.
my $pwd = getcwd;
# This block is all that's required to set up for a test.
my $tenv = TestEnvironment->new;
$tenv->build_repo(sibling_abs_path('tree')); # dies on error
eval { $tenv->build_db; };
@ -45,9 +48,15 @@ system(qw(ln -s), $tenv->update_py, 'update.py') == 0
or warn "error creating ./update.py: $? $!";
system(qw(ln -s), $tenv->query_py, 'query.py') == 0
or warn "error creating ./query.py: $? $!";
system(qw(ln -s), $tenv->web_py, 'web.py') == 0
or warn "error creating ./web.py: $? $!";
system(qw(ln -s), $tenv->find_doc, 'find-file-doc-comments.pl') == 0
or warn "error creating ./find-file-doc-comments.pl: $? $!";
print("Exit when done, and the repository and database will be removed.\n");
my $retval = system($ENV{SHELL} || 'sh');
# Don't stay in the temp dir --- the dir can't be removed if we are there.
chdir $pwd;
exit $retval>>8;