Add automated test of basic functions

- Added test helpers in t/TestHelpers.pm
- Added basic test of update.py and query.py in t/100-basic.t
- Updated README:
    - Added automated-testing section
    - Formatted identifiers (i.e., added backticks)
This commit is contained in:
Christopher White 2020-01-21 09:36:10 -05:00
parent b22a562b5b
commit fa7243638d
4 changed files with 413 additions and 18 deletions

View file

@ -1,6 +1,5 @@
# The Elixir Cross Referencer
Elixir is a source code cross-referencer inspired by
[LXR](https://en.wikipedia.org/wiki/LXR_Cross_Referencer). It's written
in Python and its main purpose is to index every release of a C or C++
@ -13,14 +12,16 @@ duplicating work and data. It has a straightforward data structure
You can see it in action on https://elixir.bootlin.com/
Note: this documentation applies to version 1.0 of Elixir.
# Requirements
* Python >= 3.5
* The Jinja2 and Pygments (>= 2.2) Python libraries
* Berkeley DB (and its Python binding)
* Exuberant Ctags
* Perl (for non-greedy regexes)
* Falcon and mod_wsgi (for the REST api)
* Perl (for non-greedy regexes and automated testing)
* Falcon and `mod_wsgi` (for the REST api)
# Installation
@ -63,8 +64,8 @@ yum install python36-jinja2 python36-pygments python36-bsddb3 python3-falcon glo
sudo apt install python3 python3-jinja2 python3-pygments python3-bsddb3 python3-falcon exuberant-ctags perl git apache2 libapache2-mod-wsgi-py3
```
To enable the REST api, follow the installation instructions on [mod_wsgi](https://github.com/GrahamDumpleton/mod_wsgi)
and connect it to the apache installation as detailed in https://github.com/GrahamDumpleton/mod_wsgi#connecting-into-apache-installation
To enable the REST api, follow the installation instructions on [`mod_wsgi`](https://github.com/GrahamDumpleton/mod_wsgi)
and connect it to the apache installation as detailed in <https://github.com/GrahamDumpleton/mod_wsgi#connecting-into-apache-installation>.
To know which packages to install, you can also read the Docker files in the `docker/` directory
to know what packages Elixir needs in your favorite distribution.
@ -87,8 +88,8 @@ mkdir -p /path/elixir-data/linux/data
Two environment variables are used to tell Elixir where to find the project's
local git repository and its databases:
* LXR_REPO_DIR (the git repository directory for your project)
* LXR_DATA_DIR (the database directory for your project)
* `LXR_REPO_DIR` (the git repository directory for your project)
* `LXR_DATA_DIR` (the database directory for your project)
Now open `/etc/profile` and append the following content.
@ -150,16 +151,16 @@ server. Since it includes support for indexing multiple projects,
it expects a different variable (`LXR_PROJ_DIR`) which points to a
directory with a specific structure:
* <LXR_PROJ_DIR>
* <project 1>
* data
* repo
* <project 2>
* data
* repo
* <project 3>
* data
* repo
* `<LXR_PROJ_DIR>`
* `<project 1>`
* `data`
* `repo`
* `<project 2>`
* `data`
* `repo`
* `<project 3>`
* `data`
* `repo`
It will then generate the other two variables upon calling the query
command.
@ -415,4 +416,27 @@ The response body is of the following structure:
}
```
Note: this documentation applies to version 1.0 of Elixir.
# Automated testing
Elixir includes a simple test suite in `t/`. To run it,
from the top-level Elixir directory, run:
prove
The test suite uses code extracted from Linux v5.4 in `t/tree`.
## Licensing of code in `t/tree`
The copied code is licensed as described in the [COPYING] file included with
Linux. All the files copied carry SPDX license identifiers of `GPL-2.0+` or
`GPL-2.0-or-later`. Per [GNU's compatibility table], GPL 2.0+ code can be used
under GPLv3 provided the combination is under GPLv3. Moreover, [GNU's overview
of AGPLv3] indicates that its terms "effectively consist of the terms of GPLv3"
plus the network-use paragraph. Therefore, the developers have a good-faith
belief that licensing these files under AGPLv3 is authorized. (See also [this
issue comment] for another example of a similar situation.)
[COPYING]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/COPYING
[GNU's compatibility table]: https://www.gnu.org/licenses/gpl-faq.en.html#AllCompatibility
[GNU's overview of AGPLv3]: https://www.gnu.org/licenses/license-list.en.html#AGPLv3.0
[this issue comment]: https://github.com/Freemius/wordpress-sdk/issues/166#issuecomment-310561976

2
t/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Where we put the test database used by 100-basic.t
/db/

145
t/100-basic.t Normal file
View file

@ -0,0 +1,145 @@
#!/usr/bin/env perl
# 100-basic.t: Test basic elixir functions against the files in tree/ .
#
# Copyright (c) 2020 D3 Engineering, LLC.
# By Christopher White, <cwhite@d3engineering.com>.
#
# 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 <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# This file uses core Perl modules only.
use strict;
use warnings;
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 TestHelpers;
# ===========================================================================
# Main
# Set up
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});
}
# Check programs
my $script_sh = find_program('script.sh');
my $update_py = find_program('update.py');
my $query_py = find_program('query.py');
ok_or_die( (-f $script_sh && -r _ && -x _), 'script.sh executable',
"Could not find executable script.sh at $script_sh");
ok_or_die( (-f $update_py && -r _ && -x _), 'update.py executable',
"Could not find executable update.py at $update_py");
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;
# Check for tags in `script.sh list-tags`, as a sanity check before
# building the test DB
my @tags = `$script_sh list-tags`;
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');
ok_or_die( -d $db_dir, 'database dir exists',
"Database dir $db_dir not present");
# Make sure the database has the files we expect
ok( (-r File::Spec->catfile($db_dir, $_)), "$_ exists" )
foreach qw(blobs.db definitions.db filenames.db hashes.db references.db
variables.db versions.db);
# Spot-check some identifiers
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);
run_produces_ok('ident query (existent)',
[$query_py, qw(v5.4 ident i2c_acpi_notify)],
[qr{^Symbol Definitions:}, qr{^Symbol References:},
qr{drivers/i2c/i2c-core-acpi\.c.+\b402\b.+\bfunction\b}, # def
qr{drivers/i2c/i2c-core-acpi\.c.+\b402,439} # refs
],
1);
# Spot-check some files
run_produces_ok('file query (nonexistent)',
[$query_py, qw(v5.4 file /SOME_NONEXISTENT_FILENAME_XYZZY_PLUGH)],
[{not => qr{\S}}]);
run_produces_ok('file query (existent), .h',
[$query_py, qw(v5.4 file /drivers/i2c/i2c-dev.c)],
[qr{\S}],
1);
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);
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);
#system('bash'); # Uncomment this if you want to interact with the test repo
done_testing;

224
t/TestHelpers.pm Normal file
View file

@ -0,0 +1,224 @@
#!/usr/bin/env perl
# TestHelpers.pm: Common routines for use in tests.
# See license information at end of file.
#
# For a cleaner view of the documentation, run
# perldoc TestHelpers.pm
# (on Ubuntu, you may need to install the perl-doc package first.)
#
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# This file uses core Perl modules only.
=head1 NAME
TestHelpers - Common routines for use in tests
=head1 SYNOPSIS
C<use TestHelpers;>, and all the functions below will be exported.
=head1 FUNCTIONS
=cut
package TestHelpers;
use strict;
use warnings;
use autodie; # note: still need to check system() calls manually
use File::Spec;
use FindBin;
use IO::Select;
use IPC::Open3;
use Symbol;
use Test::More;
# Automatically export all the functions below
use parent 'Exporter';
our @EXPORT;
BEGIN { @EXPORT = qw(sibling_abs_path find_program run_program ok_or_die
run_produces_ok); }
# ===========================================================================
=head2 sibling_abs_path
Return the absolute path of a file or directory in the same directory as
this file. Usage:
$path = sibling_abs_path('name');
=cut
sub sibling_abs_path {
return File::Spec->rel2abs(File::Spec->catfile($FindBin::Bin, @_));
}
=head2 find_program
Looks for a program in the parent directory of this script.
Usage:
$path = find_program('program name')
=cut
sub find_program {
my $program = shift;
my ($vol, $directories, $file) = 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);
return File::Spec->catpath($vol, $directories, $program);
} #find_program()
=head2 run_program
Print a command, then run it. Returns true if system() and the command
succeed, false otherwise. Usage:
$ok = run_program('program', 'arg1', ...)
=cut
sub run_program {
diag "Running @_";
my $status = system(@_);
if ($status == -1) {
diag "failed to execute $_[0]: $!";
}
elsif ($status & 127) {
diag sprintf "$_[0] died with signal %d, %s coredump\n",
($status & 127), ($status & 128) ? 'with' : 'without';
}
else {
diag sprintf "$_[0] exited with value %d\n", $status >> 8;
}
return($status == 0);
} #run_program()
=head2 ok_or_die
Run a test, but die if it fails. Usage:
ok_or_die( <some condition>, 'description', 'what to print if it dies' )
=cut
sub ok_or_die {
my ($cond, $msg, $err_msg) = @_;
my $line = (caller)[2];
my $retval = eval <<EOT; # Make the error message report the caller's line number
#line $line
ok(\$cond, \$msg);
EOT
die($err_msg) unless $retval;
return $retval;
} #ok_or_die()
=head2 run_produces_ok
Run a program and check whether it produces expected output.
Usage:
run_produces_ok($desc, \@program_and_args, \@expected_regexes,
<optional> $mustSucceed)
The test passes if each regex in C<@expected_regexes> matches at least one
line in the output of C<@program_and_args>, and if each C<< { not => regex } >>
in C<@expected_regexes> is NOT found in that output.
If C<$mustSucceed> is true, also tests for exit status 0 and empty stderr.
=cut
sub run_produces_ok {
my ($desc, $lrProgram, $lrRegexes, $mustSucceed) = @_;
# Run program and capture stdout and stderr
my ($in , $out, $err); # Filehandles
$err = Symbol::gensym;
diag "Running @$lrProgram";
my $pid = open3($in, $out, $err, @$lrProgram);
my (@outlines, @errlines); # Captured output
my $s = IO::Select->new;
$s->add($out);
$s->add($err);
while(my @ready = $s->can_read) {
for my $fh (@ready) {
if(eof($fh)) {
$s->remove($fh);
next;
}
if($fh == $out) {
push @outlines, scalar readline $fh;
} else {
push @errlines, scalar readline $fh;
}
}
}
waitpid $pid, 0;
my $exit_status = $? >> 8;
# Basic checks
if($mustSucceed) {
cmp_ok($exit_status, '==', 0, "$desc: exit status 0");
cmp_ok(@errlines, '==', 0, "$desc: stderr empty");
}
# Check regexes
for my $entry (@$lrRegexes) {
if(ref $entry eq 'Regexp') {
ok( (grep { m{$entry} } @outlines), "$desc: output includes $entry" );
} elsif(ref $entry eq 'HASH' && ref $entry->{not} eq 'Regexp') {
my $re = $entry->{not};
ok( !(grep { m{$re} } @outlines), "$desc: output excludes $re" );
} else {
die "Invalid entry $entry";
}
} #foreach $entry
} #run_produces_ok()
1;
__END__
=head1 AUTHOR
Christopher White, C<< <cwhite@d3engineering.com> >>
=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 <http://www.gnu.org/licenses/>.
=cut