Skip to content
Commits on Source (18)
......@@ -18,5 +18,5 @@
bin_PROGRAMS += gnome-desktop-testing-runner
gnome_desktop_testing_runner_SOURCES = src/gnome-desktop-testing-runner.c
gnome_desktop_testing_runner_CPPFLAGS = $(AM_CPPFLAGS)
gnome_desktop_testing_runner_CFLAGS = $(BUILDDEP_GDT_CFLAGS)
gnome_desktop_testing_runner_LDADD = $(BUILDDEP_GDT_LIBS)
gnome_desktop_testing_runner_CFLAGS = $(BUILDDEP_GDT_CFLAGS) $(SYSTEMD_CFLAGS)
gnome_desktop_testing_runner_LDADD = $(BUILDDEP_GDT_LIBS) $(SYSTEMD_LIBS)
......@@ -35,12 +35,17 @@ privlib_LTLIBRARIES =
privlib_DATA =
INSTALL_DATA_HOOKS =
dist_man1_MANS = gnome-desktop-testing-runner.1
include Makefile-tests.am
install-data-hook: $(INSTALL_DATA_HOOKS)
install-exec-hook:
ln -s gnome-desktop-testing-runner $(DESTDIR)$(bindir)/ginsttest-runner
install -d $(DESTDIR)$(bindir)
install -d $(DESTDIR)$(man1dir)
ln -sf gnome-desktop-testing-runner $(DESTDIR)$(bindir)/ginsttest-runner
ln -sf gnome-desktop-testing-runner.1 $(DESTDIR)$(man1dir)/ginsttest-runner.1
release-tag:
git tag -m "Release $(VERSION)" v$(VERSION)
......@@ -27,7 +27,9 @@ AC_SUBST(WARN_CFLAGS)
LT_PREREQ([2.2.4])
LT_INIT([disable-static])
PKG_CHECK_MODULES(BUILDDEP_GDT, [gio-unix-2.0 >= 2.34.0 libsystemd])
PKG_CHECK_MODULES(BUILDDEP_GDT, [gio-unix-2.0 >= 2.34.0])
PKG_CHECK_MODULES([SYSTEMD], [libsystemd], [have_systemd=yes], [have_systemd=no])
AS_IF([test "x$have_systemd" = xyes], [AC_DEFINE([HAVE_SYSTEMD], [1], [Define if you have libsystemd])])
GIO_UNIX_CFLAGS="$GIO_UNIX_CFLAGS -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_36 -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_36"
AC_CONFIG_FILES([
......
.TH GNOME-DESKTOP-TESTING-RUNNER 1
.SH NAME
gnome-desktop-testing-runner, ginsttest-runner \- run "as-installed" tests
.SH SYNOPSIS
.B gnome-desktop-testing-runner
[\fB--dir=\fIDIR\fR]
[\fB--first-root\fR]
[\fB--list\fR]
[\fB--log-directory=\fIDIR\fR]
[\fB--parallel=\fIPROC\fR]
[\fB--quiet\fR]
[\fB--report-directory=\fIDIR\fR]
[\fB--status=yes\fR|\fBno\fR|\fBauto\fR]
[\fB--tap\fR]
[\fB--timeout=\fISECONDS\fR]
\fIPREFIX\fR [\fIPREFIX\fR...]
.B gnome-desktop-testing-runner
\fB--log-msgid\fR=\fIMSGID\fB=\fIMESSAGE\fR
.B ginsttest-runner
\fIOPTIONS\fR
.SH DESCRIPTION
.BR gnome-desktop-testing-runner ,
also known as
.BR ginsttest-runner ,
runs "as-installed" tests. These tests are discovered using metadata in
files named \fBinstalled-tests/**/*.test\fR, and are intended to check
that a library or program is functioning correctly and has been installed
correctly by a system integrator such as an OS distributor or system
administrator.
.PP
Tests in this format are typically provided by GNOME-related libraries,
but the concept, format and tools are not GNOME-specific and can be used
by any software.
.SH OPTIONS
.TP
\fB--dir=\fIDIR\fR, \fB-d\fR \fIDIR\fR
Look for test metadata in the \fBinstalled-tests\fR subdirectory of
\fIDIR\fR, instead of using \fB$XDG_DATA_DIRS\fR. If repeated, each \fIDIR\fR
is searched in order.
.TP
\fB--first-root\fR
Stop after a directory that contains \fBinstalled-tests\fR has been
encountered.
.TP
\fB--list\fR, \fB-l\fR
Don't run any tests. Instead, list what would have been run on standard
output, one per line, in the format \fINAME\fB (\fIPATH\fB)\fR.
.TP
\fB--log-directory=\fIDIR\fR, \fB-L\fR \fIDIR\fR
Write the output of each test to a file \fIDIR\fB/\fINAME\fB.txt\fR.
.TP
\fB--log-msgid=\fIMSGID\fB=\fIMESSAGE\fR
Don't run any tests. Instead, emit a log message to the systemd Journal
with the unique machine-readable message ID \fIMSGID\fR and the
human-readable message \fIMESSAGE\fR. This can be used to mark important
points in the test log from a test written in a language where direct
access to the Journal is awkward, such as shell script or JavaScript.
.TP
\fB--parallel=\fIPROC\fR, \fB-p\fR \fIPROC\fR
Run up to \fIPROC\fR tests in parallel. The default is 1, meaning do not
run tests in parallel. If \fIPROC\fR is 0, detect the number of CPUs and
run that many tests in parallel.
.TP
\fB--quiet\fR
Don't output test results, just log them to the systemd Journal
if supported.
.TP
\fB--report-directory=\fIDIR\fR
Run each test with \fIDIR\fB/\fINAME\fR as its current working directory,
and write its output to a file \fIDIR\fB/\fINAME\fB/output.txt\fR.
If the test succeeds, the directory is deleted. If the test fails, the
directory is kept for analysis, including any temporary files or logs
that the test itself might have written there.
.TP
\fB--status=yes\fR|\fBno\fR|\fBauto\fR
Output a status message every few seconds if a test takes a significant
time to run.
.TP
\fB--tap\fR
Output machine-readable test results on standard output, in the format
specified by TAP (the
.UR https://testanything.org/
Test Anything Protocol
.UE
originally used by Perl's test suite).
.TP
\fB--timeout=\fISECONDS\fR, \fB-t\fR \fISECONDS\fR
If a test takes longer than \fISECONDS\fR seconds to run, terminate it.
The default is 5 minutes (300 seconds).
.TP
\fIPREFIX\fR [\fIPREFIX\fR...]
Only list or run tests that match one of these prefixes, relative to the
.B installed-tests
directory.
.SH EXIT STATUS
.TP
0
All tests were successful, or \fB--list\fR or \fB--log-msgid\fR was
successful
.TP
1
An error occurred during options parsing, setup or testing
.TP
2
The tests were run, and least one failed
.SH ENVIRONMENT
.TP
XDG_DATA_DIRS
Used to discover tests if \fB--dir\fR, \fB-d\fR is not specified.
.SH FILES
.TP
/usr/local/share/installed-tests/**/*.test
Conventional location for metadata describing tests installed by
locally-installed software.
.TP
/usr/share/installed-tests/**/*.test
Conventional location for metadata describing tests installed by the
operating system packages.
.TP
/usr/local/libexec/installed-tests/**, /usr/libexec/installed-tests/**
Conventional location for test executables and the data files they
require (although this is not required, and they can be installed in any
convenient location).
.TP
.RB ./.testtmp
Each test will be invoked in a temporary directory containing only this
file. Tests can use this to avoid overwriting important files if run
without using
.BR gnome-desktop-testing-runner .
.SH EXAMPLE
To run the tests from the
.B json-glib
library:
.nf
.RS
ginsttest-runner json-glib-1.0/
.RE
.fi
.PP
To run the tests from the
.BR dbus ,
.B glib
and
.B json-glib
libraries, with up to one process per CPU, producing machine-readable
output and storing results of any failed tests in a directory:
.nf
.RS
ginsttest-runner \\
--parallel=0 \\
--report-directory=artifacts \\
--tap \\
dbus/ glib/ json-glib-1.0/
.RE
.fi
.SH SEE ALSO
.UR https://wiki.gnome.org/Initiatives/GnomeGoals/InstalledTests
GNOME Goal: Installed Tests
.UE ,
.UR https://testanything.org/
Test Anything Protocol
.UE
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright (C) 2011,2013 Colin Walters <walters@verbum.org>
* Copyright (C) 2009 Codethink Limited
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
......@@ -22,14 +23,21 @@
#include "config.h"
#include <gio/gio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/resource.h>
#ifdef HAVE_SYSTEMD
#include <systemd/sd-journal.h>
#endif
#define TEST_SKIP_ECODE 77
#define TEST_RUNNING_STATUS_MSGID "ed6199045dd38bb5321e551d9578f3d9"
#define TESTS_COMPLETE_MSGID "4d013788dd704743b826436c951e551d"
#define ONE_TEST_FAILED_MSGID "0eee66bf98514369bef9868327a43cf1"
......@@ -37,6 +45,174 @@
#define ONE_TEST_SUCCESS_MSGID "142bf5d40e9742e99d3ac8c1ace83b36"
#define ONE_TEST_TIMED_OUT_MSGID "db8f25eab14a4da68ef3ab3ce4b2c0bb"
/* Types of test_log() call */
typedef enum {
TEST_LOG_RUNNING_STATUS,
TEST_LOG_COMPLETE,
TEST_LOG_ONE_FAILED,
TEST_LOG_ONE_SKIPPED,
TEST_LOG_ONE_SUCCESS,
TEST_LOG_ONE_TIMED_OUT,
TEST_LOG_EXCEPTION,
TEST_LOG_ARBITRARY,
} TestLog;
/* Message IDs used for test_log() calls */
static const char * const test_log_message_ids[] = {
[TEST_LOG_RUNNING_STATUS] = TEST_RUNNING_STATUS_MSGID,
[TEST_LOG_COMPLETE] = TESTS_COMPLETE_MSGID,
[TEST_LOG_ONE_FAILED] = ONE_TEST_FAILED_MSGID,
[TEST_LOG_ONE_SKIPPED] = ONE_TEST_SKIPPED_MSGID,
[TEST_LOG_ONE_SUCCESS] = ONE_TEST_SUCCESS_MSGID,
[TEST_LOG_ONE_TIMED_OUT] = ONE_TEST_TIMED_OUT_MSGID,
/* Reusing ONE_TEST_FAILED_MSGID is not quite right, but whatever */
[TEST_LOG_EXCEPTION] = ONE_TEST_FAILED_MSGID,
/* Special-cased: the "test name" is really the message ID */
[TEST_LOG_ARBITRARY] = NULL,
};
static gboolean opt_quiet = FALSE;
static gboolean opt_tap = FALSE;
static void
test_log (TestLog what,
const char *test_name,
const char *format,
...)
{
const char *msgid = test_log_message_ids[what];
g_autofree char *message = NULL;
va_list ap;
if (what == TEST_LOG_ARBITRARY)
{
msgid = test_name;
test_name = NULL;
}
va_start (ap, format);
message = g_strdup_vprintf (format, ap);
va_end (ap);
#ifdef HAVE_SYSTEMD
if (test_name)
sd_journal_send ("MESSAGE_ID=%s", msgid,
"GDTR_TEST=%s", test_name,
"MESSAGE=%s", message,
NULL);
else
sd_journal_send ("MESSAGE_ID=%s", msgid,
"MESSAGE=%s", message,
NULL);
#else
/* we can't log this to the Journal, so do *something* with it */
if (what == TEST_LOG_ARBITRARY)
g_printerr ("%s: %s\n", msgid, message);
#endif
if (opt_tap)
{
switch (what)
{
default:
/* fall through */
case TEST_LOG_RUNNING_STATUS:
/* fall through */
case TEST_LOG_COMPLETE:
g_print ("# %s\n", message);
break;
case TEST_LOG_ONE_FAILED:
g_print ("# %s\n", message);
g_print ("not ok - %s\n", test_name);
break;
case TEST_LOG_ONE_SKIPPED:
g_print ("ok # SKIP - %s\n", test_name);
break;
case TEST_LOG_ONE_SUCCESS:
g_print ("ok - %s\n", test_name);
break;
case TEST_LOG_ONE_TIMED_OUT:
g_print ("not ok - %s\n", message);
break;
case TEST_LOG_EXCEPTION:
g_print ("Bail out! %s\n", message);
break;
case TEST_LOG_ARBITRARY:
/* do nothing, just print to the Journal */
break;
}
}
else if (!opt_quiet)
{
if (what != TEST_LOG_ARBITRARY)
g_print ("%s\n", message);
}
}
/* Taken from gio/gunixfdlist.c */
static int
dup_close_on_exec_fd (gint fd,
GError **error)
{
gint new_fd;
gint s;
#ifdef F_DUPFD_CLOEXEC
do
new_fd = fcntl (fd, F_DUPFD_CLOEXEC, 0l);
while (new_fd < 0 && (errno == EINTR));
if (new_fd >= 0)
return new_fd;
/* if that didn't work (new libc/old kernel?), try it the other way. */
#endif
do
new_fd = dup (fd);
while (new_fd < 0 && (errno == EINTR));
if (new_fd < 0)
{
int saved_errno = errno;
g_set_error (error, G_IO_ERROR,
g_io_error_from_errno (saved_errno),
"dup: %s", g_strerror (saved_errno));
return -1;
}
do
{
s = fcntl (new_fd, F_GETFD);
if (s >= 0)
s = fcntl (new_fd, F_SETFD, (long) (s | FD_CLOEXEC));
}
while (s < 0 && (errno == EINTR));
if (s < 0)
{
int saved_errno = errno;
g_set_error (error, G_IO_ERROR,
g_io_error_from_errno (saved_errno),
"fcntl: %s", g_strerror (saved_errno));
close (new_fd);
return -1;
}
return new_fd;
}
static gboolean
rm_rf (GFile *path, GError **error)
{
......@@ -218,15 +394,17 @@ static char *opt_status;
static char *opt_log_msgid;
static GOptionEntry options[] = {
{ "dir", 'd', 0, G_OPTION_ARG_STRING_ARRAY, &opt_dirs, "Only run tests from these dirs (default: all system data dirs)", NULL },
{ "dir", 'd', 0, G_OPTION_ARG_STRING_ARRAY, &opt_dirs, "Only run tests from these dirs (default: all system data dirs)", "DIR" },
{ "list", 'l', 0, G_OPTION_ARG_NONE, &opt_list, "List matching tests", NULL },
{ "parallel", 'p', 0, G_OPTION_ARG_INT, &opt_parallel, "Specify parallelization to PROC processors; 0 will be dynamic)", "PROC" },
{ "first-root", 0, 0, G_OPTION_ARG_NONE, &opt_firstroot, "Only use first entry in XDG_DATA_DIRS", "PROC" },
{ "first-root", 0, 0, G_OPTION_ARG_NONE, &opt_firstroot, "Only use first entry in XDG_DATA_DIRS", NULL },
{ "log-directory", 'L', 0, G_OPTION_ARG_FILENAME, &opt_log_directory, "Create a subdirectory with test logs", "DIR" },
{ "report-directory", 0, 0, G_OPTION_ARG_FILENAME, &opt_report_directory, "Create a subdirectory per failing test in DIR", "DIR" },
{ "status", 0, 0, G_OPTION_ARG_STRING, &opt_status, "Output status information", "yes/no/auto" },
{ "log-msgid", 0, 0, G_OPTION_ARG_STRING, &opt_log_msgid, "Log unique message with id MSGID=MESSAGE", "MSGID" },
{ "timeout", 't', 0, G_OPTION_ARG_INT, &opt_cancel_timeout, "Cancel test after timeout seconds; defaults to 5 minutes", "TIMEOUT" },
{ "quiet", 0, 0, G_OPTION_ARG_NONE, &opt_quiet, "Don't output test results", NULL },
{ "tap", 0, 0, G_OPTION_ARG_NONE, &opt_tap, "Output test results as TAP", NULL },
{ NULL }
};
......@@ -298,32 +476,24 @@ static void
log_test_completion (GdtrTest *test,
const char *reason)
{
const char *msgid_value;
g_autofree char *msg = NULL;
if (test->state == TEST_STATE_COMPLETE_SUCCESS)
{
msgid_value = ONE_TEST_SUCCESS_MSGID;
msg = g_strconcat ("PASS: ", test->name, NULL);
test_log (TEST_LOG_ONE_SUCCESS, test->name, "PASS: %s", test->name);
}
else if (test->state == TEST_STATE_COMPLETE_FAILED)
{
msgid_value = ONE_TEST_FAILED_MSGID;
msg = g_strconcat ("FAIL: ", test->name, " (", reason, ")", NULL);
g_autofree char *msg = g_strconcat ("FAIL: ", test->name, " (", reason,
")", NULL);
test_log (TEST_LOG_ONE_FAILED, test->name, "%s", msg);
g_ptr_array_add (app->failed_test_msgs, g_strdup (msg));
}
else if (test->state == TEST_STATE_COMPLETE_SKIPPED)
{
msgid_value = ONE_TEST_SKIPPED_MSGID;
msg = g_strconcat ("SKIP: ", test->name, NULL);
test_log (TEST_LOG_ONE_SKIPPED, test->name, "SKIP: %s", test->name);
}
else
g_assert_not_reached ();
sd_journal_send ("MESSAGE_ID=%s", msgid_value,
"GDTR_TEST=%s", test->name,
"MESSAGE=%s", msg,
NULL);
}
static void
......@@ -397,10 +567,8 @@ cancel_test (gpointer data)
{
GSubprocess*proc = data;
g_subprocess_force_exit (proc);
sd_journal_send ("MESSAGE_ID=%s", ONE_TEST_TIMED_OUT_MSGID,
"MESSAGE=Test timed out after %u seconds",
opt_cancel_timeout,
NULL);
test_log (TEST_LOG_ONE_TIMED_OUT, NULL, "Test timed out after %u seconds",
opt_cancel_timeout);
return FALSE;
}
......@@ -433,7 +601,7 @@ run_test_async (GdtrTest *test,
task = g_task_new (test, cancellable, callback, user_data);
g_print ("Running test: %s\n", test->name);
g_print ("%sRunning test: %s\n", opt_tap ? "# " : "", test->name);
test_squashed_name = g_regex_replace_literal (slash_regex, test->name, -1,
0, "_", 0, NULL);
......@@ -460,6 +628,9 @@ run_test_async (GdtrTest *test,
}
}
if (opt_log_directory)
g_mkdir_with_parents (opt_log_directory, 0755);
/* We create a .testtmp stamp file so that tests can *know* for sure
* they're in a temporary directory. This is used by at least the
* OSTree tests as protection against someone running a test script
......@@ -480,6 +651,20 @@ run_test_async (GdtrTest *test,
if (opt_report_directory || opt_log_directory)
flags |= G_SUBPROCESS_FLAGS_STDERR_MERGE;
proc_context = g_subprocess_launcher_new (flags);
if (opt_tap && !(opt_report_directory || opt_log_directory))
{
/* We can't put the test's output on our stdout, or it'd be
* misinterpreted as our structured TAP output. Put it on our
* stderr instead */
int copy_of_stderr;
copy_of_stderr = dup_close_on_exec_fd (STDERR_FILENO, error);
if (copy_of_stderr < 0)
goto out;
g_subprocess_launcher_take_stdout_fd (proc_context, copy_of_stderr);
}
g_subprocess_launcher_set_cwd (proc_context, test_tmpdir);
g_subprocess_launcher_set_environ (proc_context, test->envp);
if (opt_report_directory)
......@@ -592,10 +777,8 @@ idle_output_status (gpointer data)
first = FALSE;
g_string_append (status_str, test->name);
}
sd_journal_send ("MESSAGE_ID=%s", TEST_RUNNING_STATUS_MSGID,
"MESSAGE=%s", status_str->str,
NULL);
test_log (TEST_LOG_RUNNING_STATUS, NULL, "%s", status_str->str);
return TRUE;
}
......@@ -671,14 +854,24 @@ main (int argc, char **argv)
GOptionContext *context;
TestRunnerApp appstruct;
const char *const *datadirs_iter;
int n_passed, n_skipped, n_failed;
int n_passed = 0;
int n_skipped = 0;
int n_failed = 0;
memset (&appstruct, 0, sizeof (appstruct));
app = &appstruct;
app->pending_tests = g_hash_table_new (NULL, NULL);
app->tests = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
app->failed_test_msgs = g_ptr_array_new_with_free_func ((GDestroyNotify)g_free);
/* avoid gvfs (http://bugzilla.gnome.org/show_bug.cgi?id=526454) */
g_setenv ("GIO_USE_VFS", "local", TRUE);
/* There's no point in logging to the Journal every time we log to
* the Journal */
if (g_log_writer_is_journald (STDOUT_FILENO))
opt_quiet = TRUE;
context = g_option_context_new ("[PREFIX...] - Run installed tests");
g_option_context_add_main_entries (context, options, NULL);
......@@ -694,9 +887,8 @@ main (int argc, char **argv)
g_autofree char *msgid = NULL;
g_assert (eq);
msgid = g_strndup (opt_log_msgid, eq - opt_log_msgid);
sd_journal_send ("MESSAGE_ID=%s", msgid,
"MESSAGE=%s", eq + 1,
NULL);
test_log (TEST_LOG_ARBITRARY, msgid, "%s", eq + 1);
exit (0);
}
......@@ -705,10 +897,6 @@ main (int argc, char **argv)
else
app->parallel = opt_parallel;
app->pending_tests = g_hash_table_new (NULL, NULL);
app->tests = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
app->failed_test_msgs = g_ptr_array_new_with_free_func ((GDestroyNotify)g_free);
if (opt_dirs)
datadirs_iter = (const char *const*) opt_dirs;
else
......@@ -770,6 +958,14 @@ main (int argc, char **argv)
{
gboolean show_status;
if (opt_tap)
{
if (total_tests == 0)
g_print ("1..0 # SKIP - nothing to do\n");
else
g_print ("1..%d\n", total_tests);
}
fisher_yates_shuffle (app->tests);
reschedule_tests (app->cancellable);
......@@ -805,10 +1001,9 @@ main (int argc, char **argv)
if (!ret)
{
g_assert (local_error);
/* Reusing ONE_TEST_FAILED_MSGID is not quite right, but whatever */
sd_journal_send ("MESSAGE_ID=%s", ONE_TEST_FAILED_MSGID,
"MESSAGE=Caught exception during testing: %s", local_error->message,
NULL);
test_log (TEST_LOG_EXCEPTION, NULL,
"Caught exception during testing: %s",
local_error->message);
g_clear_error (&local_error);
}
if (!opt_list)
......@@ -816,7 +1011,6 @@ main (int argc, char **argv)
struct rusage child_rusage;
g_autofree char *rusage_str = NULL;
n_passed = n_skipped = n_failed = 0;
for (i = 0; i < app->tests->len; i++)
{
GdtrTest *test = app->tests->pdata[i];
......@@ -844,14 +1038,15 @@ main (int argc, char **argv)
child_rusage.ru_maxrss);
}
sd_journal_send ("MESSAGE_ID=%s", TESTS_COMPLETE_MSGID,
"MESSAGE=SUMMARY%s: total=%u; passed=%d; skipped=%d; failed=%d%s",
ret ? "" : " (incomplete)",
total_tests, n_passed, n_skipped, n_failed,
rusage_str != NULL ? rusage_str : "",
NULL);
test_log (TEST_LOG_COMPLETE, NULL,
"SUMMARY%s: total=%u; passed=%d; skipped=%d; failed=%d%s",
ret ? "" : " (incomplete)",
total_tests, n_passed, n_skipped, n_failed,
rusage_str != NULL ? rusage_str : "",
NULL);
for (i = 0; i < app->failed_test_msgs->len; i++)
g_print ("%s\n", (char *) app->failed_test_msgs->pdata[i]);
g_print ("%s%s\n", opt_tap ? "# " : "", (char *) app->failed_test_msgs->pdata[i]);
}
g_clear_pointer (&app->pending_tests, g_hash_table_unref);
g_clear_pointer (&app->tests, g_ptr_array_unref);
......