Commit 3d6e9fab authored by Philip Chimento's avatar Philip Chimento 🚮

tests: Use embedded copy of Jasmine to run tests

This replaces the old JSUnit test harness with an embedded copy of
Jasmine [1], which makes writing tests less of a pain, includes more
handy test facilities, and produces better output.

jasmine.js is a copy of upstream Jasmine 2.5.2. minijasmine.js contains
code for starting up Jasmine, adapting it to the GJS environment, and
producing TAP output. minijasmine.cpp makes an executable which loads the
preceding two files from the unit test GResource.

All the tests in installed-tests/js are converted to use Jasmine's
describe()/it() style. Quite often this allows simplifying them since
Jasmine has features like array and object equality, spies, and clock
ticks.

[1] https://jasmine.github.io/2.5/introduction.html

https://bugzilla.gnome.org/show_bug.cgi?id=775444
parent bf114964
EXTRA_DIST += \
installed-tests/jsunit.test.in \
installed-tests/minijasmine.test.in \
installed-tests/script.test.in \
installed-tests/js/jsunit.gresources.xml \
$(NULL)
MAINTAINERCLEANFILES += jsunit.test
gjsinsttestdir = $(pkglibexecdir)/installed-tests
installedtestmetadir = $(datadir)/installed-tests/gjs
jstestsdir = $(gjsinsttestdir)/js
......@@ -20,27 +18,18 @@ pkglib_LTLIBRARIES =
if BUILDOPT_INSTALL_TESTS
gjsinsttest_PROGRAMS += jsunit
gjsinsttest_PROGRAMS += minijasmine
gjsinsttest_DATA += $(TEST_INTROSPECTION_TYPELIBS)
installedtestmeta_DATA += jsunit.test
jstests_DATA += $(common_jstests_files)
installedtestmeta_DATA += $(jasmine_tests:.js=.test)
jstests_DATA += $(jasmine_tests)
pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la
if ENABLE_CAIRO
jstests_DATA += installed-tests/js/testCairo.js
endif
if ENABLE_GTK
jstests_DATA += installed-tests/js/testGtk.js
endif
if DBUS_TESTS
jstests_DATA += installed-tests/js/testGDBus.js
endif
jsunit.test: installed-tests/jsunit.test.in Makefile
%.test: %.js installed-tests/minijasmine.test.in Makefile
$(AM_V_GEN)$(MKDIR_P) $(@D) && \
$(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir), < $< > $@.tmp && mv $@.tmp $@
$(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir),g \
-e s,@name\@,$(notdir $<), \
< $(srcdir)/installed-tests/minijasmine.test.in > $@.tmp && \
mv $@.tmp $@
%.test: installed-tests/scripts/%.js installed-tests/script.test.in Makefile
$(AM_V_GEN)$(MKDIR_P) $(@D) && \
......
......@@ -3,8 +3,6 @@ EXTRA_DIST += \
test/run-test \
$(NULL)
SKIPPED_TESTS =
if XVFB_TESTS
XVFB_INVOCATION = $(XVFB) -ac -noreset -screen 0 1024x768x16
XIDS = 101 102 103 104 105 106 107 197 199 211 223 227 293 307 308 309 310 311 \
......@@ -24,18 +22,6 @@ else
XVFB_START =
endif
if !DBUS_TESTS
SKIPPED_TESTS += /js/GDBus
endif
if !ENABLE_GTK
SKIPPED_TESTS += /js/Gtk
endif
if !ENABLE_CAIRO
SKIPPED_TESTS += /js/Cairo
endif
### TEST RESOURCES #####################################################
mock_js_resources_files := $(shell glib-compile-resources --sourcedir=$(srcdir) --generate-dependencies $(srcdir)/test/mock-js-resources.gresource.xml)
......@@ -95,7 +81,7 @@ CLEANFILES += \
# as well as installed if --enable-installed-tests is given at configure time.
# See Makefile-insttest.am for the build rules installing the tests.
check_PROGRAMS += gjs-tests jsunit
check_PROGRAMS += gjs-tests minijasmine
gjs_tests_CPPFLAGS = \
$(AM_CPPFLAGS) \
......@@ -122,23 +108,20 @@ gjs_tests_DEPENDENCIES = \
mock-cache-invalidation-after.gresource \
$(NULL)
jsunit_CPPFLAGS = \
minijasmine_SOURCES = \
installed-tests/minijasmine.cpp \
jsunit-resources.c \
jsunit-resources.h \
$(NULL)
minijasmine_CPPFLAGS = \
$(AM_CPPFLAGS) \
$(GJS_CFLAGS) \
-DPKGLIBDIR=\"$(pkglibdir)\" \
-DINSTTESTDIR=\"$(gjsinsttestdir)\" \
-I$(top_srcdir) \
-DINSTTESTDIR=\"$(gjsinsttestdir)\" \
$(NULL)
jsunit_LDADD = $(GJS_LIBS) libgjs.la
jsunit_LDFLAGS = -rpath $(pkglibdir)
jsunit_SOURCES = \
installed-tests/gjs-unit.cpp \
jsunit-resources.c \
jsunit-resources.h \
$(NULL)
minijasmine_LDADD = $(GJS_LIBS) libgjs.la
### TEST GIRS ##########################################################
......@@ -258,6 +241,20 @@ common_jstests_files = \
installed-tests/js/testTweener.js \
$(NULL)
jasmine_tests = $(common_jstests_files)
if DBUS_TESTS
jasmine_tests += installed-tests/js/testGDBus.js
endif
if ENABLE_GTK
jasmine_tests += installed-tests/js/testGtk.js
endif
if ENABLE_CAIRO
jasmine_tests += installed-tests/js/testCairo.js
endif
EXTRA_DIST += \
$(common_jstests_files) \
installed-tests/js/testCairo.js \
......@@ -270,10 +267,8 @@ EXTRA_DIST += \
# GJS_PATH is empty here since we want to force the use of our own
# resources. G_FILENAME_ENCODING ensures filenames are not UTF-8.
AM_TESTS_ENVIRONMENT = \
export TOP_SRCDIR="$(abs_top_srcdir)"; \
export TOP_BUILDDIR="$(abs_top_builddir)"; \
export GJS_USE_UNINSTALLED_FILES=1; \
export GJS_TEST_SKIP="$(SKIPPED_TESTS)"; \
export GJS_PATH=; \
export GI_TYPELIB_PATH="$(builddir):$${GI_TYPELIB_PATH:+:$$GI_TYPELIB_PATH}"; \
export LD_LIBRARY_PATH="$(builddir)/.libs:$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH}"; \
......@@ -283,16 +278,28 @@ AM_TESTS_ENVIRONMENT = \
simple_tests = test/testCommandLine.sh
EXTRA_DIST += $(simple_tests)
TESTS += $(simple_tests)
TESTS = \
gjs-tests \
$(simple_tests) \
$(jasmine_tests) \
$(NULL)
TEST_EXTENSIONS = .js
LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
JS_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
if DBUS_TESTS
LOG_COMPILER = $(DBUS_RUN_SESSION)
AM_LOG_FLAGS = --config-file=$(srcdir)/test/test-bus.conf -- $(top_srcdir)/test/run-test
JS_LOG_COMPILER = $(DBUS_RUN_SESSION)
AM_JS_LOG_FLAGS = --config-file=$(srcdir)/test/test-bus.conf -- $(top_builddir)/minijasmine
else
LOG_COMPILER = $(top_srcdir)/test/run-test
AM_LOG_FLAGS =
JS_LOG_COMPILER = $(top_builddir)/minijasmine
AM_JS_LOG_FLAGS =
endif !DBUS_TESTS
if CODE_COVERAGE_ENABLED
......
......@@ -14,7 +14,6 @@ CLEANFILES =
EXTRA_DIST =
check_PROGRAMS =
check_LTLIBRARIES =
TESTS = $(check_PROGRAMS)
INTROSPECTION_GIRS =
## ACLOCAL_AMFLAGS can be removed for Automake 1.13
ACLOCAL_AMFLAGS = -I m4
......
/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
/*
* Copyright (c) 2008 litl, LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include <config.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <girepository.h>
#include <locale.h>
#include <string.h>
#include <gio/gio.h>
#include <gjs/gjs.h>
#include "gjs/coverage.h"
#include "gjs/mem.h"
static const char *skip;
typedef struct {
const char *coverage_prefix;
const char *coverage_output_path;
char *filename;
char *test_name;
} GjsTestData;
static GjsTestData *
gjs_unit_test_data_new(const char *coverage_prefix,
const char *coverage_output_path,
char *filename,
char *test_name)
{
GjsTestData *data = (GjsTestData *) g_new0(GjsTestData, 1);
data->coverage_prefix = coverage_prefix;
data->coverage_output_path = coverage_output_path;
data->filename = filename;
data->test_name = test_name;
return data;
}
static void
gjs_unit_test_data_free(gpointer test_data, gpointer user_data)
{
GjsTestData *data = (GjsTestData *) test_data;
g_free(data->filename);
g_free(data->test_name);
g_free(data);
}
typedef struct {
GjsContext *context;
GjsCoverage *coverage;
} GjsTestJSFixture;
static void
setup(GjsTestJSFixture *fix,
gconstpointer test_data)
{
GjsTestData *data = (GjsTestData *) test_data;
fix->context = gjs_context_new ();
if (data->coverage_prefix) {
const char *coverage_prefixes[2] = { data->coverage_prefix, NULL };
if (!data->coverage_output_path) {
g_error("GJS_UNIT_COVERAGE_OUTPUT is required when using GJS_UNIT_COVERAGE_PREFIX");
}
GFile *output = g_file_new_for_commandline_arg(data->coverage_output_path);
fix->coverage = gjs_coverage_new(coverage_prefixes, fix->context, output);
g_object_unref(output);
}
}
static void
teardown(GjsTestJSFixture *fix,
gconstpointer unused)
{
if (fix->coverage) {
gjs_coverage_write_statistics(fix->coverage);
g_clear_object(&fix->coverage);
}
gjs_memory_report("before destroying context", false);
g_object_unref(fix->context);
gjs_memory_report("after destroying context", true);
}
static void
test(GjsTestJSFixture *fix,
gconstpointer test_data)
{
GError *error = NULL;
bool success;
int code;
GjsTestData *data = (GjsTestData *) test_data;
if (skip && strstr(skip, data->test_name)) {
g_test_message("Skipping %s", data->test_name);
g_test_skip("due to GJS_TEST_SKIP environment variable");
return;
}
success = gjs_context_eval_file(fix->context, data->filename, &code, &error);
if (!success)
g_error("%s", error->message);
g_assert(error == NULL);
if (code != 0)
g_error("Test script returned code %d; assertions will be in gjs.log", code);
}
static GSList *
read_all_dir_sorted (const char *dirpath)
{
GSList *result = NULL;
GDir *dir;
const char *name;
dir = g_dir_open(dirpath, 0, NULL);
g_assert(dir != NULL);
while ((name = g_dir_read_name(dir)) != NULL)
result = g_slist_prepend (result, g_strdup (name));
result = g_slist_sort(result, (GCompareFunc) strcmp);
g_dir_close(dir);
return result;
}
int
main(int argc, char **argv)
{
char *js_test_dir;
GSList *all_tests, *iter;
GSList *all_registered_test_data = NULL;
gpointer context_class;
int retval;
/* The tests are known to fail in the presence of the JIT;
* we leak objects.
* https://bugzilla.gnome.org/show_bug.cgi?id=616193
*/
g_setenv("GJS_DISABLE_JIT", "1", false);
/* The fact that this isn't the default is kind of lame... */
g_setenv("GJS_DEBUG_OUTPUT", "stderr", false);
setlocale(LC_ALL, "");
g_test_init(&argc, &argv, NULL);
/* Make sure to create the GjsContext class first, so we
* can override the GjsPrivate lookup path.
*/
context_class = g_type_class_ref (gjs_context_get_type ());
if (g_getenv ("GJS_USE_UNINSTALLED_FILES") != NULL) {
g_irepository_prepend_search_path(g_getenv ("TOP_BUILDDIR"));
js_test_dir = g_build_filename(g_getenv ("TOP_SRCDIR"), "installed-tests", "js", NULL);
} else {
g_irepository_prepend_search_path(INSTTESTDIR);
js_test_dir = g_build_filename(INSTTESTDIR, "js", NULL);
}
const char *coverage_prefix = g_getenv("GJS_UNIT_COVERAGE_PREFIX");
const char *coverage_output_directory = g_getenv("GJS_UNIT_COVERAGE_OUTPUT");
skip = g_getenv("GJS_TEST_SKIP");
all_tests = read_all_dir_sorted(js_test_dir);
for (iter = all_tests; iter; iter = iter->next) {
char *name = (char*) iter->data;
char *test_name;
char *file_name;
GjsTestData *test_data;
if (!(g_str_has_prefix(name, "test") &&
g_str_has_suffix(name, ".js"))) {
g_free(name);
continue;
}
/* pretty print, drop 'test' prefix and '.js' suffix from test name */
test_name = g_strconcat("/js/", name + 4, NULL);
test_name[strlen(test_name)-3] = '\0';
file_name = g_build_filename(js_test_dir, name, NULL);
test_data = gjs_unit_test_data_new(coverage_prefix,
coverage_output_directory, file_name,
test_name);
g_test_add(test_name, GjsTestJSFixture, test_data, setup, test, teardown);
g_free(name);
all_registered_test_data = g_slist_prepend(all_registered_test_data, test_data);
/* not freeing file_name, test_name, or test_data yet as it's needed
* while running the test */
}
g_free(js_test_dir);
g_slist_free(all_tests);
retval = g_test_run ();
g_slist_foreach(all_registered_test_data,
(GFunc)gjs_unit_test_data_free,
all_registered_test_data);
g_slist_free(all_registered_test_data);
g_type_class_unref (context_class);
return retval;
}
This diff is collapsed.
......@@ -2,6 +2,8 @@
<gresources>
<gresource prefix="/org/gjs/jsunit">
<file preprocess="xml-stripblanks">complex.ui</file>
<file>jasmine.js</file>
<file>minijasmine.js</file>
<file>modules/alwaysThrows.js</file>
<file>modules/foobar.js</file>
<file>modules/modunicode.js</file>
......
#!/usr/bin/env gjs
const GLib = imports.gi.GLib;
const Lang = imports.lang;
function _removeNewlines(str) {
let allNewlines = /\n/g;
return str.replace(allNewlines, '\\n');
}
function _filterStack(stack) {
return stack.split('\n')
.filter(stackLine => stackLine.indexOf('resource:///org/gjs/jsunit') === -1)
.filter(stackLine => stackLine.indexOf('<jasmine-start>') === -1)
.join('\n');
}
function _setTimeoutInternal(continueTimeout, func, time) {
return GLib.timeout_add(GLib.PRIORITY_DEFAULT, time, function () {
func();
return continueTimeout;
});
}
function _clearTimeoutInternal(id) {
if (id > 0)
GLib.source_remove(id);
}
// Install the browser setTimeout/setInterval API on the global object
window.setTimeout = _setTimeoutInternal.bind(undefined, GLib.SOURCE_REMOVE);
window.setInterval = _setTimeoutInternal.bind(undefined, GLib.SOURCE_CONTINUE);
window.clearTimeout = window.clearInterval = _clearTimeoutInternal;
let jasmineRequire = imports.jasmine.getJasmineRequireObj();
let jasmineCore = jasmineRequire.core(jasmineRequire);
window._jasmineEnv = jasmineCore.getEnv();
window._jasmineMain = GLib.MainLoop.new(null, false);
window._jasmineRetval = 0;
// Install Jasmine API on the global object
let jasmineInterface = jasmineRequire.interface(jasmineCore, window._jasmineEnv);
Lang.copyProperties(jasmineInterface, window);
// Reporter that outputs according to the Test Anything Protocol
// See http://testanything.org/tap-specification.html
const TapReporter = new Lang.Class({
Name: 'TapReporter',
_init: function () {
this._failedSuites = [];
this._specCount = 0;
},
jasmineStarted: function (info) {
print('1..' + info.totalSpecsDefined);
},
jasmineDone: function () {
this._failedSuites.forEach(failure => {
failure.failedExpectations.forEach(result => {
print('not ok - An error was thrown outside a test');
print('# ' + result.message);
});
});
window._jasmineMain.quit();
},
suiteDone: function (result) {
if (result.failedExpectations && result.failedExpectations.length > 0) {
window._jasmineRetval = 1;
this._failedSuites.push(result);
}
if (result.status === 'disabled') {
print('# Suite was disabled:', result.fullName);
}
},
specStarted: function () {
this._specCount++;
},
specDone: function (result) {
let tap_report;
if (result.status === 'failed') {
window._jasmineRetval = 1;
tap_report = 'not ok';
} else {
tap_report = 'ok';
}
tap_report += ' ' + this._specCount + ' ' + result.fullName;
if (result.status === 'pending' || result.status === 'disabled') {
let reason = result.pendingReason || result.status;
tap_report += ' # SKIP ' + reason;
}
print(tap_report);
// Print additional diagnostic info on failure
if (result.status === 'failed' && result.failedExpectations) {
result.failedExpectations.forEach((failedExpectation) => {
print('# Message:', _removeNewlines(failedExpectation.message));
print('# Stack:');
let stackTrace = _filterStack(failedExpectation.stack).trim();
print(stackTrace.split('\n').map((str) => '# ' + str).join('\n'));
});
}
},
});
window._jasmineEnv.addReporter(new TapReporter());
// tests for imports.lang module
const JSUnit = imports.jsUnit;
const ByteArray = imports.byteArray;
const Gio = imports.gi.Gio;
function testEmptyByteArray() {
let a = new ByteArray.ByteArray();
JSUnit.assertEquals("length is 0 for empty array", 0, a.length);
}
function testInitialSizeByteArray() {
let a = new ByteArray.ByteArray(10);
JSUnit.assertEquals("length is 10 for initially-sized-10 array", 10, a.length);
let i;
for (i = 0; i < a.length; ++i) {
JSUnit.assertEquals("new array initialized to zeroes", 0, a[i]);
}
JSUnit.assertEquals("array had proper number of elements post-construct (counting for)",
10, i);
}
function testAssignment() {
let a = new ByteArray.ByteArray(256);
JSUnit.assertEquals("length is 256 for initially-sized-256 array", 256, a.length);
let i;
let count;
count = 0;
for (i = 0; i < a.length; ++i) {
JSUnit.assertEquals("new array initialized to zeroes", 0, a[i]);
a[i] = 255 - i;
count += 1;
}
JSUnit.assertEquals("set proper number of values", 256, count);
count = 0;
for (i = 0; i < a.length; ++i) {
JSUnit.assertEquals("assignment set expected value", 255 - i, a[i]);
count += 1;
}
JSUnit.assertEquals("checked proper number of values", 256, count);
}
function testAssignmentPastEnd() {
let a = new ByteArray.ByteArray();
JSUnit.assertEquals("length is 0 for empty array", 0, a.length);
a[2] = 5;
JSUnit.assertEquals("implicitly made length 3", 3, a.length);
JSUnit.assertEquals("implicitly-created zero byte", 0, a[0]);
JSUnit.assertEquals("implicitly-created zero byte", 0, a[1]);
JSUnit.assertEquals("stored 5 in autocreated position", 5, a[2]);
}
function testAssignmentToLength() {
let a = new ByteArray.ByteArray(20);
JSUnit.assertEquals("length is 20 for new array", 20, a.length);
a.length = 5;
JSUnit.assertEquals("length is 5 after setting it to 5", 5, a.length);
}
function testNonIntegerAssignment() {
let a = new ByteArray.ByteArray();
a[0] = 5;
JSUnit.assertEquals("assigning 5 gives a byte 5", 5, a[0]);
a[0] = null;
JSUnit.assertEquals("assigning null gives a zero byte", 0, a[0]);
a[0] = 5;
JSUnit.assertEquals("assigning 5 gives a byte 5", 5, a[0]);
a[0] = undefined;
JSUnit.assertEquals("assigning undefined gives a zero byte", 0, a[0]);
a[0] = 3.14;
JSUnit.assertEquals("assigning a double rounds off", 3, a[0]);
}
function testFromString() {
let a = ByteArray.fromString('abcd');
JSUnit.assertEquals("from string 'abcd' gives length 4", 4, a.length);
JSUnit.assertEquals("'a' results in 97", 97, a[0]);
JSUnit.assertEquals("'b' results in 98", 98, a[1]);
JSUnit.assertEquals("'c' results in 99", 99, a[2]);
JSUnit.assertEquals("'d' results in 100", 100, a[3]);
}
function testFromArray() {
let a = ByteArray.fromArray([ 1, 2, 3, 4 ]);
JSUnit.assertEquals("from array [1,2,3,4] gives length 4", 4, a.length);
JSUnit.assertEquals("a[0] == 1", 1, a[0]);
JSUnit.assertEquals("a[1] == 2", 2, a[1]);
JSUnit.assertEquals("a[2] == 3", 3, a[2]);
JSUnit.assertEquals("a[3] == 4", 4, a[3]);
}
function testToString() {
let a = new ByteArray.ByteArray();
a[0] = 97;
a[1] = 98;
a[2] = 99;
a[3] = 100;
let s = a.toString();
JSUnit.assertEquals("toString() on 4 ascii bytes gives length 4", 4, s.length);
JSUnit.assertEquals("toString() gives 'abcd'", "abcd", s);
}
JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
describe('Byte array', function () {
it('has length 0 for empty array', function () {
let a = new ByteArray.ByteArray();
expect(a.length).toEqual(0);
});
describe('initially sized to 10', function () {
let a;
beforeEach(function () {
a = new ByteArray.ByteArray(10);
});
it('has length 10', function () {
expect(a.length).toEqual(10);
});
it('is initialized to zeroes', function () {
for (let i = 0; i < a.length; ++i) {
expect(a[i]).toEqual(0);
}
});
});
it('assigns values correctly', function () {
let a = new ByteArray.ByteArray(256);
for (let i = 0; i < a.length; ++i) {
a[i] = 255 - i;
}
for (let i = 0; i < a.length; ++i) {
expect(a[i]).toEqual(255 - i);
}
});
describe('assignment past end', function () {
let a;
beforeEach(function () {
a = new ByteArray.ByteArray();
a[2] = 5;
});
it('implicitly lengthens the array', function () {
expect(a.length).toEqual(3);
expect(a[2]).toEqual(5);
});
it('implicitly creates zero bytes', function () {
expect(a[0]).toEqual(0);
expect(a[1]).toEqual(0);
});
});
it('changes the length when assigning to length property', function () {
let a = new ByteArray.ByteArray(20);
expect(a.length).toEqual(20);
a.length = 5;