From fa7243638d8814c3a075b180a600f84c871844b4 Mon Sep 17 00:00:00 2001 From: Christopher White Date: Tue, 21 Jan 2020 09:36:10 -0500 Subject: [PATCH] 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) --- README.md | 60 +++++++++---- t/.gitignore | 2 + t/100-basic.t | 145 ++++++++++++++++++++++++++++++ t/TestHelpers.pm | 224 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 413 insertions(+), 18 deletions(-) create mode 100644 t/.gitignore create mode 100644 t/100-basic.t create mode 100644 t/TestHelpers.pm diff --git a/README.md b/README.md index da7e567..40dc1e6 100644 --- a/README.md +++ b/README.md @@ -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 . 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: -* - * - * data - * repo - * - * data - * repo - * - * data - * repo +* `` + * `` + * `data` + * `repo` + * `` + * `data` + * `repo` + * `` + * `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 diff --git a/t/.gitignore b/t/.gitignore new file mode 100644 index 0000000..439ae2f --- /dev/null +++ b/t/.gitignore @@ -0,0 +1,2 @@ +# Where we put the test database used by 100-basic.t +/db/ diff --git a/t/100-basic.t b/t/100-basic.t new file mode 100644 index 0000000..ef1a66b --- /dev/null +++ b/t/100-basic.t @@ -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, . +# +# 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 . +# +# 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; diff --git a/t/TestHelpers.pm b/t/TestHelpers.pm new file mode 100644 index 0000000..4cd06f1 --- /dev/null +++ b/t/TestHelpers.pm @@ -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, 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( , 'description', 'what to print if it dies' ) + +=cut + +sub ok_or_die { + my ($cond, $msg, $err_msg) = @_; + my $line = (caller)[2]; + my $retval = eval < $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<< >> + +=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