Commit deff0677 authored by Philip Withnall's avatar Philip Withnall

Merge branch 'signal-logger' into 'master'

Signal logger, documentation, coverage and CI

See merge request !1
parents 7c6b0b84 32fd413c
Pipeline #11854 passed with stages
in 5 minutes and 35 seconds
image: debian:unstable
before_script:
- apt update -qq
- apt install -y -qq build-essential meson pkg-config gtk-doc-tools
libxml2-utils gobject-introspection dbus
libgirepository1.0-dev libglib2.0-dev
lcov
- export LANG=C.UTF-8
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- meson --prefix /usr --libdir /usr/lib64 --buildtype debug --werror _build .
- ninja -C _build
except:
- tags
artifacts:
when: on_failure
name: "libglib-testing-_${CI_COMMIT_REF_NAME}"
paths:
- "${CI_PROJECT_DIR}/_build/meson-logs"
test:
stage: test
script:
- meson _build . -Db_coverage=true
- ninja -C _build test
- ninja -C _build coverage
coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/'
# FIXME: Run gtkdoc-check when we can. See:
# https://github.com/mesonbuild/meson/issues/3580
dist-job:
stage: build
only:
- tags
script:
- meson --prefix /usr --libdir /usr/lib64 --buildtype release _build .
- ninja -C _build dist
artifacts:
paths:
- "${CI_PROJECT_DIR}/_build/meson-dist/libglib-testing-*.tar.xz"
pages:
stage: deploy
only:
- master
script:
- meson -Db_coverage=true -Ddocumentation=true _build .
- ninja -C _build test libglib-testing-doc
- ninja -C _build coverage
- mkdir -p public/
- mv _build/libglib-testing/docs/html/ public/docs/
- mv _build/meson-logs/coveragereport/ public/coverage/
artifacts:
paths:
- public
\ No newline at end of file
......@@ -19,6 +19,14 @@ Dependencies
• glib-2.0 ≥ 2.54
• gobject-2.0 ≥ 2.54
Development
===========
Development documentation and code coverage reports are available here:
https://pwithnall.pages.gitlab.gnome.org/libglib-testing/docs/
https://pwithnall.pages.gitlab.gnome.org/libglib-testing/coverage/
Licensing
=========
......
<?xml version="1.0"?>
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
<!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
<!ENTITY version SYSTEM "version.xml">
]>
<book id="index" xmlns:xi="http://www.w3.org/2003/XInclude">
<bookinfo>
<title>libglib-testing Reference Manual</title>
<releaseinfo>
This document is for the libglib-testing library, version &version;.
</releaseinfo>
</bookinfo>
<reference id="reference">
<title>API Reference</title>
<xi:include href="xml/signal-logger.xml" />
</reference>
<index id="api-index-full">
<title>Index of all symbols</title>
<xi:include href="xml/api-index-full.xml"><xi:fallback /></xi:include>
</index>
<index id="api-index-deprecated" role="deprecated">
<title>Index of deprecated symbols</title>
<xi:include href="xml/api-index-deprecated.xml"><xi:fallback /></xi:include>
</index>
<index role="0.1.0">
<title>Index of new symbols in 0.1.0</title>
<xi:include href="xml/api-index-0.1.0.xml"><xi:fallback/></xi:include>
</index>
<xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include>
</book>
\ No newline at end of file
<SECTION>
<TITLE>GtSignalLogger</TITLE>
<FILE>signal-logger</FILE>
<SUBSECTION>
GtSignalLogger
gt_signal_logger_new
gt_signal_logger_free
gt_signal_logger_connect
gt_signal_logger_get_n_emissions
gt_signal_logger_pop_emission
gt_signal_logger_format_emission
gt_signal_logger_format_emissions
gt_signal_logger_assert_no_emissions
gt_signal_logger_assert_emission_pop
gt_signal_logger_assert_notify_emission_pop
<SUBSECTION>
GtSignalLoggerEmission
gt_signal_logger_emission_get_params
gt_signal_logger_emission_free
</SECTION>
\ No newline at end of file
# FIXME: Would be good to eliminate version.xml generation if possible. See:
# https://github.com/mesonbuild/meson/issues/3581
version_conf = configuration_data()
version_conf.set('LIBGLIB_TESTING_VERSION', meson.project_version())
configure_file(
input: 'version.xml.in',
output: 'version.xml',
configuration: version_conf,
)
gnome.gtkdoc('libglib-testing',
mode: 'none',
main_xml: 'docs.xml',
src_dir: [
include_directories('..'),
],
dependencies: libglib_testing_dep,
scan_args: [
'--ignore-decorators=G_GNUC_WARN_UNUSED_RESULT',
'--ignore-headers=' + ' '.join(['tests']),
],
install: true,
)
\ No newline at end of file
@LIBGLIB_TESTING_VERSION@
\ No newline at end of file
libglib_testing_api_version = '0'
libglib_testing_api_name = 'gsystemservice-' + libglib_testing_api_version
libglib_testing_sources = [
'signal-logger.c',
]
libglib_testing_headers = [
'signal-logger.h',
]
libglib_testing_deps = [
dependency('gio-2.0', version: '>= 2.44'),
dependency('glib-2.0', version: '>= 2.44'),
dependency('gobject-2.0', version: '>= 2.44'),
]
libglib_testing_include_subdir = join_paths(libglib_testing_api_name, 'libglib-testing')
libglib_testing = library(libglib_testing_api_name,
libglib_testing_sources + libglib_testing_headers,
dependencies: libglib_testing_deps,
include_directories: root_inc,
install: true,
version: meson.project_version(),
soversion: libglib_testing_api_version,
)
libglib_testing_dep = declare_dependency(
link_with: libglib_testing,
include_directories: root_inc,
)
# Public library bits.
install_headers(libglib_testing_headers,
subdir: libglib_testing_include_subdir,
)
pkgconfig.generate(
libraries: [ libglib_testing ],
subdirs: libglib_testing_api_name,
version: meson.project_version(),
name: 'libglib-testing',
filebase: libglib_testing_api_name,
description: 'libglib-testing provides test harnesses and mock classes.',
# FIXME: This should be derived from libglib_testing_deps with Meson 0.45;
# see the `libraries` docs: http://mesonbuild.com/Pkgconfig-module.html#pkggenerate
requires: [ 'gio-2.0', 'glib-2.0', 'gobject-2.0' ],
)
subdir('docs')
subdir('tests')
\ No newline at end of file
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright © 2018 Endless Mobile, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Authors:
* - Philip Withnall <withnall@endlessm.com>
*/
#include "config.h"
#include <glib.h>
#include <glib-object.h>
#include <gobject/gvaluecollector.h>
#include <libglib-testing/signal-logger.h>
/**
* SECTION:signal-logger
* @short_description: GObject signal logging and checking
* @stability: Unstable
* @include: libglib-testing/signal-logger.h
*
* #GtSignalLogger is an object which allows logging of signals emitted from
* zero or more #GObjects, and later comparison of those signals against what
* was expected to be emitted.
*
* A single #GtSignalLogger instance can be used for multiple #GObjects, and can
* outlive the objects themselves. It can be connected to several different
* signals, emissions of which will all be added to the same queue (ordered by
* emission time).
*
* Testing of the emitted signals is performed by popping emissions off the
* queue and comparing them to what was expected. Macros are provided to assert
* that the next emission on the queue was for a specific signal — or callers
* may unconditionally pop the next emission and compare its properties
* themselves.
*
* By default, a #GtSignalLogger will not assert that its emission queue is
* empty on destruction: that is up to the caller, and it is highly recommended
* that gt_signal_logger_assert_no_emissions() is called before a signal logger
* is destroyed, or after a particular unit test is completed.
*
* Since: 0.1.0
*/
/**
* GtSignalLogger:
*
* An object which allows signal emissions from zero or more #GObjects to be
* logged easily, without needing to write specific callback functions for any
* of them.
*
* Since: 0.1.0
*/
struct _GtSignalLogger
{
/* Log of the signal emissions. Head emission was the first emitted. */
GPtrArray *log; /* (element-type GtSignalLoggerEmission) (owned) */
/* Set of currently connected signal handler closures. */
GPtrArray *closures; /* (element-type GtLoggedClosure) (owned) */
};
/**
* GtLoggedClosure:
*
* A closure representing a connection from @logger to the given @signal_name
* on @obj.
*
* The closure will be kept alive until the @logger is destroyed, though it will
* be invalidated and disconnected earlier if @obj is finalised.
*
* Since: 0.1.0
*/
typedef struct
{
GClosure closure;
GtSignalLogger *logger; /* (not owned) */
/* Pointer to the object instance this closure is connected to; no ref is
* held, and the object may be finalised before the closure, so this should
* only be used as an opaque pointer; add a #GWeakRef if the object needs to
* be accessed in future. */
gpointer obj; /* (not owned) */
/* A copy of `G_OBJECT_TYPE_NAME (obj)` for use when @obj may be invalid. */
gchar *obj_type_name; /* (owned) */
/* Name of the signal this closure is connected to, including detail
* (if applicable). */
gchar *signal_name; /* (owned) */
gulong signal_id; /* 0 when disconnected */
} GtLoggedClosure;
/**
* GtSignalLoggerEmission:
*
* The details of a particular signal emission, including its parameter values.
*
* @param_values does not include the object instance.
*
* Since: 0.1.0
*/
struct _GtSignalLoggerEmission
{
/* The closure this emission was captured by. */
GtLoggedClosure *closure; /* (owned) */
/* Array of parameter values, not including the object instance. */
GValue *param_values; /* (array length=n_param_values) */
gsize n_param_values;
};
/**
* gt_signal_logger_emission_free:
* @emission: (transfer full): a #GtSignalLoggerEmission
*
* Free a #GtSignalLoggerEmission.
*
* Since: 0.1.0
*/
void
gt_signal_logger_emission_free (GtSignalLoggerEmission *emission)
{
for (gsize i = 0; i < emission->n_param_values; i++)
g_value_unset (&emission->param_values[i]);
g_free (emission->param_values);
g_closure_unref ((GClosure *) emission->closure);
g_free (emission);
}
/**
* gt_signal_logger_emission_get_params:
* @self: a #GtSignalLoggerEmission
* @...: return locations for the signal parameters
*
* Get the parameters emitted in this signal emission. They are returned in the
* return locations provided as varargs. These locations must have the right
* type for the parameters of the signal which was emitted.
*
* To ignore a particular parameter, pass %NULL as the one (or more) return
* locations for that parameter.
*
* Since: 0.1.0
*/
void
gt_signal_logger_emission_get_params (GtSignalLoggerEmission *self,
...)
{
va_list ap;
va_start (ap, self);
for (gsize i = 0; i < self->n_param_values; i++)
{
g_autofree gchar *error_message = NULL;
G_VALUE_LCOPY (&self->param_values[i], ap, 0, &error_message);
/* Error messages are not fatal, as they typically indicate that the user
* has passed in %NULL rather than a valid return pointer. We can recover
* from that. */
if (error_message != NULL)
g_debug ("Error copying GValue %" G_GSIZE_FORMAT " from emission of %s::%s from %p: %s",
i, self->closure->obj_type_name, self->closure->signal_name,
self->closure->obj, error_message);
}
va_end (ap);
}
static void
gt_logged_closure_marshal (GClosure *closure,
GValue *return_value,
guint n_param_values,
const GValue *param_values,
gpointer invocation_hint,
gpointer marshal_data)
{
GtLoggedClosure *self = (GtLoggedClosure *) closure;
/* Log the @param_values. Ignore the @return_value, and the first of
* @param_values (which is the object instance). */
g_assert (n_param_values >= 1);
g_autoptr(GtSignalLoggerEmission) emission = g_new0 (GtSignalLoggerEmission, 1);
emission->closure = (GtLoggedClosure *) g_closure_ref ((GClosure *) self);
emission->n_param_values = n_param_values - 1;
emission->param_values = g_new0 (GValue, emission->n_param_values);
for (gsize i = 0; i < emission->n_param_values; i++)
{
g_value_init (&emission->param_values[i], G_VALUE_TYPE (&param_values[i + 1]));
g_value_copy (&param_values[i + 1], &emission->param_values[i]);
}
g_ptr_array_add (self->logger->log, g_steal_pointer (&emission));
}
static void
gt_logged_closure_invalidate (gpointer user_data,
GClosure *closure)
{
GtLoggedClosure *self = (GtLoggedClosure *) closure;
self->signal_id = 0;
}
static void
gt_logged_closure_finalize (gpointer user_data,
GClosure *closure)
{
GtLoggedClosure *self = (GtLoggedClosure *) closure;
/* Deliberately don’t g_ptr_array_remove() the closure from the
* self->logger->closures list, since finalize() can only be called when the
* final reference to the closure is dropped, and self->logger->closures holds
* a reference, so we must be being finalised from there (or that GPtrArray
* has already been finalised). */
g_free (self->obj_type_name);
g_free (self->signal_name);
g_assert (self->signal_id == 0);
}
/**
* gt_logged_closure_new:
* @logger: (transfer none): logger to connect the closure to
* @obj: (not nullable) (transfer none): #GObject to connect the closure to
* @signal_name: (not nullable): signal name to connect the closure to
*
* Create a new #GtLoggedClosure for @logger, @obj and @signal_name. @obj must
* be a valid object instance at this point (it may later be finalised before
* the closure).
*
* This does not connect the closure to @signal_name on @obj. Use
* gt_signal_logger_connect() for that.
*
* Returns: (transfer full): a new closure
* Since: 0.1.0
*/
static GClosure *
gt_logged_closure_new (GtSignalLogger *logger,
GObject *obj,
const gchar *signal_name)
{
g_autoptr(GClosure) closure = g_closure_new_simple (sizeof (GtLoggedClosure), NULL);
GtLoggedClosure *self = (GtLoggedClosure *) closure;
self->logger = logger;
self->obj = obj;
self->obj_type_name = g_strdup (G_OBJECT_TYPE_NAME (obj));
self->signal_name = g_strdup (signal_name);
self->signal_id = 0;
g_closure_add_invalidate_notifier (closure, NULL, (GClosureNotify) gt_logged_closure_invalidate);
g_closure_add_finalize_notifier (closure, NULL, (GClosureNotify) gt_logged_closure_finalize);
g_closure_set_marshal (closure, gt_logged_closure_marshal);
g_ptr_array_add (logger->closures, g_closure_ref (closure));
return g_steal_pointer (&closure);
}
/**
* gt_signal_logger_new:
*
* Create a new #GtSignalLogger. Add signals to it to log using
* gt_signal_logger_connect().
*
* Returns: (transfer full): a new #GtSignalLogger
* Since: 0.1.0
*/
GtSignalLogger *
gt_signal_logger_new (void)
{
g_autoptr(GtSignalLogger) logger = g_new0 (GtSignalLogger, 1);
logger->log = g_ptr_array_new_with_free_func ((GDestroyNotify) gt_signal_logger_emission_free);
logger->closures = g_ptr_array_new_with_free_func ((GDestroyNotify) g_closure_unref);
return g_steal_pointer (&logger);
}
/**
* gt_signal_logger_free:
* @self: (transfer full): a #GtSignalLogger
*
* Free a #GtSignalLogger. This will disconnect all its closures from the
* signals they are connected to.
*
* This function may be called when there are signal emissions left in the
* logged stack, but typically you will want to call
* gt_signal_logger_assert_no_emissions() first.
*
* Since: 0.1.0
*/
void
gt_signal_logger_free (GtSignalLogger *self)
{
g_return_if_fail (self != NULL);
/* Disconnect all the closures, since we don’t care about logging any more. */
for (gsize i = 0; i < self->closures->len; i++)
{
GClosure *closure = g_ptr_array_index (self->closures, i);
g_closure_invalidate (closure);
}
g_ptr_array_unref (self->closures);
g_ptr_array_unref (self->log);
g_free (self);
}
/**
* gt_signal_logger_connect:
* @self: a #GtSignalLogger
* @obj: (type GObject): a #GObject to connect to
* @signal_name: the signal on @obj to connect to
*
* A convenience wrapper around g_signal_connect() which connects the
* #GtSignalLogger to the given @signal_name on @obj so that emissions of it
* will be logged.
*
* The closure will be disconnected (and the returned signal connection ID
* invalidated) when:
*
* * @obj is finalised
* * The closure is freed or removed
* * The signal logger is freed
*
* This does not keep a strong reference to @obj.
*
* Returns: signal connection ID, as returned from g_signal_connect()
* Since: 0.1.0
*/
gulong
gt_signal_logger_connect (GtSignalLogger *self,
gpointer obj,
const gchar *signal_name)
{
g_return_val_if_fail (self != NULL, 0);
g_return_val_if_fail (G_IS_OBJECT (obj), 0);
g_return_val_if_fail (signal_name != NULL, 0);
g_autoptr(GClosure) closure = gt_logged_closure_new (self, obj, signal_name);
GtLoggedClosure *c = (GtLoggedClosure *) closure;
c->signal_id = g_signal_connect_closure (obj, signal_name, g_closure_ref (closure), FALSE);
return c->signal_id;
}
/**
* gt_signal_logger_get_n_emissions:
* @self: a #GtSignalLogger
*
* Get the number of signal emissions which have been logged (and not popped)
* since the logger was initialised.
*
* Returns: number of signal emissions
* Since: 0.1.0
*/
gsize
gt_signal_logger_get_n_emissions (GtSignalLogger *self)
{
g_return_val_if_fail (self != NULL, 0);
return self->log->len;
}
/**
* gt_signal_logger_pop_emission:
* @self: a #GtSignalLogger
* @out_obj: (out) (transfer none) (optional) (not nullable): return location
* for the object instance which emitted the signal
* @out_obj_type_name: (out) (transfer full) (optional) (not nullable): return
* location for the name of the type of @out_obj
* @out_signal_name: (out) (transfer full) (optional) (not nullable): return
* location for the name of the emitted signal
* @out_emission: (out) (transfer full) (optional) (not nullable): return
* location for the signal emission closure containing emission parameters
*
* Pop the oldest signal emission off the stack of logged emissions, and return
* its object, signal name and parameters in the given return locations. All
* return locations are optional: if they are all %NULL, this function just
* performs a pop.
*
* If there are no signal emissions on the logged stack, %FALSE is returned.
*
* @out_obj does not return a reference to the object instance, as it may have
* been finalised since the signal emission was logged. It should be treated as
* an opaque pointer. The type name of the object is given as
* @out_obj_type_name, which is guaranteed to be valid.
*
* Returns: %TRUE if an emission was popped and returned, %FALSE otherwise
* Since: 0.1.0
*/
gboolean
gt_signal_logger_pop_emission (GtSignalLogger *self,
gpointer *out_obj,
gchar **out_obj_type_name,
gchar **out_signal_name,
GtSignalLoggerEmission **out_emission)
{
g_return_val_if_fail (self != NULL, FALSE);
if (self->log->len == 0)
{
if (out_obj != NULL)
*out_obj = NULL;
if (out_obj_type_name != NULL)
*out_obj_type_name = NULL;
if (out_signal_name != NULL)
*out_signal_name = NULL;
if (out_emission != NULL)
*out_emission = NULL;
return FALSE;
}
/* FIXME: Could do with g_ptr_array_steal() here.
* https://bugzilla.gnome.org/show_bug.cgi?id=795376 */
g_ptr_array_set_free_func (self->log, NULL);
g_autoptr(GtSignalLoggerEmission) emission = g_steal_pointer (&self->log->pdata[0]);
g_ptr_array_remove_index (self->log, 0);
g_ptr_array_set_free_func (self->log, (GDestroyNotify) gt_signal_logger_emission_free);
if (out_obj != NULL)
*out_obj = emission->closure->obj;
if (out_obj_type_name != NULL)
*out_obj_type_name = g_strdup (emission->closure->obj_type_name);
if (out_signal_name != NULL)
*out_signal_name = g_strdup (emission->closure->signal_name);
if (out_emission != NULL)
*out_emission = g_steal_pointer (&emission);
return TRUE;
}
/**
* gt_signal_logger_format_emission:
* @obj: a #GObject instance which emitted a signal
* @obj_type_name: a copy of `G_OBJECT_TYPE_NAME (obj)` for use when @obj may
* be invalid
* @signal_name: name of the emitted signal
* @emission: details of the signal emission
*
* Format a signal emission in a human readable form, typically for logging it
* to some debug output.
*
* The returned string does not have a trailing newline character (`\n`).
*
* @obj may have been finalised, and is just treated as an opaque pointer.
*
* Returns: (transfer full): human readable string detailing the signal emission
* Since: 0.1.0
*/
gchar *
gt_signal_logger_format_emission (gpointer obj,
const gchar *obj_type_name,
const gchar *signal_name,
const GtSignalLoggerEmission *emission)
{
g_return_val_if_fail (obj != NULL, NULL); /* deliberately not a G_IS_OBJECT() check */
g_return_val_if_fail (signal_name != NULL, NULL);