Only minor rearrangements necessary to make that possible with C++20
And while at this change (which requires a full rebuild of Lumiera)
- simplify the defined comparison operators, as C++20 can infer most variations
- also mark various usages of `const char*` either as Literal or CStr
Remark: regarding copyright, up to now this is entirely my work,
with two major creation steps in 2008 (conception) and
in 2017 (introduction of a symbol table)
536 lines
18 KiB
C++
536 lines
18 KiB
C++
/*
|
||
EVENT-LOG.hpp - test facility to verify the occurrence of expected events
|
||
|
||
Copyright (C)
|
||
2015, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** is free software; you can redistribute it and/or modify it
|
||
under the terms of the GNU General Public License as published by the
|
||
Free Software Foundation; either version 2 of the License, or (at your
|
||
option) any later version. See the file COPYING for further details.
|
||
|
||
*/
|
||
|
||
|
||
/** @file event-log.hpp
|
||
** Support for verifying the occurrence of events from unit tests.
|
||
** Typically used within special rigging and instrumentation for tests,
|
||
** the [EventLog] allows to record invocations and similar events. It is
|
||
** implemented as a "PImpl" to allow sharing of logs, which helps to trace
|
||
** events from transient UI elements and from destructor code. The front-end
|
||
** used for access offers a query DSL, so the test code may express some
|
||
** expected patterns of incidence and verify match or non-match.
|
||
**
|
||
** # Usage
|
||
**
|
||
** The EventLog just captures invocations in memory, as sequence of
|
||
** [string records](\ref lib::diff::Record). The _logging API_ offers some
|
||
** dedicated functions to record invocations, events, the creation and destruction
|
||
** of objects, warnings and error messages. Data passed to those logging functions
|
||
** is rendered into string, using Lumiera's generic string conversion helpers.
|
||
**
|
||
** To _verify and match_ on log contents, start an evaluation as a builder (DSL)
|
||
** expression, starting from one of the #verify functions, or starting with #ensureNot,
|
||
** to probe that a given set of matches does not occur in the log. These entrance
|
||
** functions set up the initial search condition, and further search conditions
|
||
** can be chained up by invoking suitable matching functions on the generated
|
||
** EventMatch builder. Most notably, it is possible to search for some further
|
||
** match condition _before_ or _after_ the position where the preceding condition
|
||
** produced a match -- allowing to specify sequences and patterns of events to
|
||
** expect in the log
|
||
** \code{.cpp}
|
||
** EventLog log("baked beans");
|
||
** log.event("spam");
|
||
** log.event("ham");
|
||
**
|
||
** CHECK (log.verify("ham")
|
||
** .after("beans")
|
||
** .before("spam")
|
||
** .before("ham"));
|
||
** \endcode
|
||
** In general, this kind of search with a switch of the search direction in the middle
|
||
** of a match requires some backtracking to try possible matches until exhaustion.
|
||
**
|
||
** Failure of match prints a detailed trace message to `STDERR`, in order
|
||
** to deliver a precise indication what part of the condition failed.
|
||
** @note this sequence prints the matches succeeding _at the point_ where each
|
||
** condition is added to the chain. Adding more conditions, especially when
|
||
** combined with changed search direction, might lead to backtracking, which
|
||
** happens silently within the search engine, without printing any further
|
||
** diagnostics. This means: the sequence of matches you see in this diagnostic
|
||
** output is not necessarily the last match path, which leads to final failure
|
||
** @warning as such EventLog is deliberately _not threadsafe_
|
||
**
|
||
** @see EventLog_test
|
||
** @see [usage example](\ref AbstractTangible_test)
|
||
**
|
||
*/
|
||
|
||
|
||
#ifndef LIB_TEST_EVENT_LOG_H
|
||
#define LIB_TEST_EVENT_LOG_H
|
||
|
||
|
||
#include "lib/error.hpp"
|
||
#include "lib/iter-cursor.hpp"
|
||
#include "lib/iter-chain-search.hpp"
|
||
#include "lib/format-util.hpp"
|
||
#include "lib/format-cout.hpp"
|
||
#include "lib/diff/record.hpp"
|
||
#include "lib/idi/genfunc.hpp"
|
||
#include "lib/symbol.hpp"
|
||
#include "lib/util.hpp"
|
||
|
||
#include <memory>
|
||
#include <vector>
|
||
#include <string>
|
||
#include <regex>
|
||
|
||
|
||
|
||
namespace lib {
|
||
namespace test{
|
||
|
||
using lib::Symbol;
|
||
using std::string;
|
||
using util::collectStr;
|
||
|
||
namespace {
|
||
using Entry = lib::diff::Record<string>;
|
||
using Log = std::vector<Entry>;
|
||
|
||
auto
|
||
buildSearchFilter(Log const& srcSeq)
|
||
{
|
||
using Cursor = lib::iter::CursorGear<Log::const_iterator>;
|
||
return iter::chainSearch (Cursor{srcSeq.begin(), srcSeq.end()});
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* @internal ongoing evaluation and match within an [EventLog].
|
||
* @remarks an EventMatch object is returned when building query expression
|
||
* to verify the occurrence of some events within the EventLog.
|
||
* This "matcher" object implements the query logic with backtracking.
|
||
* The query expressions are added as filter functors to a filtering
|
||
* iterator; when all of the log's contents are filtered away,
|
||
* the evaluation counts as failed.
|
||
*/
|
||
class EventMatch
|
||
{
|
||
using Filter = decltype( buildSearchFilter (std::declval<Log const&>()) );
|
||
|
||
using ArgSeq = lib::diff::RecordSetup<string>::Storage;
|
||
using RExSeq = std::vector<std::regex>;
|
||
|
||
|
||
/** match predicate evaluator */
|
||
Filter solution_;
|
||
|
||
/** record last match for diagnostics */
|
||
string lastMatch_;
|
||
|
||
/** support for positive and negative queries. */
|
||
bool look_for_match_;
|
||
|
||
/** record when the underlying query has failed */
|
||
string violation_;
|
||
|
||
|
||
|
||
/** @internal for creating EventLog matchers */
|
||
EventMatch(Log const& srcSeq);
|
||
|
||
friend class EventLog;
|
||
|
||
|
||
public:
|
||
/** final evaluation of the match query,
|
||
* usually triggered from the unit test `CHECK()`.
|
||
* @note failure cause is printed to STDERR.
|
||
*/
|
||
operator bool() const
|
||
{
|
||
if (not util::isnil (violation_))
|
||
{
|
||
cerr << "__Log_condition_violated__\n"+violation_ <<"\n";
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
|
||
/* query builders to continue search at current position */
|
||
|
||
EventMatch& locate (string match);
|
||
EventMatch& locateMatch (string regExp);
|
||
EventMatch& locateEvent (string match);
|
||
EventMatch& locateEvent (string classifier, string match);
|
||
EventMatch& locateCall (string match);
|
||
|
||
|
||
/* query builders to find a match stepping forwards */
|
||
|
||
EventMatch& before (string match);
|
||
EventMatch& beforeMatch (string regExp);
|
||
EventMatch& beforeEvent (string match);
|
||
EventMatch& beforeEvent (string classifier, string match);
|
||
EventMatch& beforeCall (string match);
|
||
|
||
|
||
/* query builders to find a match stepping backwards */
|
||
|
||
EventMatch& after (string match);
|
||
EventMatch& afterMatch (string regExp);
|
||
EventMatch& afterEvent (string match);
|
||
EventMatch& afterEvent (string classifier, string match);
|
||
EventMatch& afterCall (string match);
|
||
|
||
/** refine filter to additionally require specific arguments
|
||
* @remarks the refined filter works on each record qualified by the
|
||
* query expression established thus far; it additionally
|
||
* looks into the arguments (children list) of the log entry.
|
||
* @warning match is processed by comparison of string representation.
|
||
*/
|
||
template<typename...ARGS>
|
||
EventMatch&
|
||
arg (ARGS const& ...args)
|
||
{
|
||
refineSerach_matchArguments (collectStr<ArgSeq> (args...));
|
||
return *this;
|
||
}
|
||
|
||
|
||
/** refine filter to additionally cover all arguments
|
||
* with a series of regular expressions.
|
||
* @note For this match to succeed, any arguments of the log entry
|
||
* must be covered by applying the given regular expressions
|
||
* consecutively. Each regular expression is applied to further
|
||
* arguments, as long as it matches. It is possible to have
|
||
* just one RegExp to "rule them all", but as soon as there
|
||
* is one argument, that can not be covered by the next
|
||
* RegExp, the whole match counts as failed.
|
||
*/
|
||
template<typename...ARGS>
|
||
EventMatch&
|
||
argMatch (ARGS const& ...regExps)
|
||
{
|
||
refineSerach_matchArgsRegExp (collectStr<RExSeq> (regExps...),
|
||
util::join(collectStr<ArgSeq>(regExps...)));
|
||
return *this;
|
||
}
|
||
|
||
/** refine filter to additionally require match on a specific positional argument */
|
||
template<typename ARG>
|
||
EventMatch&
|
||
argPos (size_t idx, ARG const& arg)
|
||
{
|
||
refineSerach_matchArgument (idx, util::toString(arg));
|
||
return *this;
|
||
}
|
||
|
||
|
||
/* query builders to augment and refine the currently defined search condition*/
|
||
|
||
EventMatch& type (string typeID);
|
||
EventMatch& key (string key);
|
||
EventMatch& attrib (string key, string valueMatch);
|
||
EventMatch& id (string classifier);
|
||
EventMatch& on (string targetID);
|
||
EventMatch& on (CStr targetID);
|
||
|
||
template<typename X>
|
||
EventMatch& on (const X *const targetObj)
|
||
{
|
||
string targetID = idi::instanceTypeID (targetObj);
|
||
return this->on(targetID);
|
||
}
|
||
|
||
private:
|
||
bool foundSolution();
|
||
void evaluateQuery (string matchSpec, Literal rel = "after");
|
||
|
||
void refineSerach_matchArgument (size_t idx, string match);
|
||
void refineSerach_matchArguments (ArgSeq&& argSeq);
|
||
void refineSerach_matchArgsRegExp (RExSeq&& regExpSeq, string rendered_regExps);
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/****************************************************************//**
|
||
* Helper to log and verify the occurrence of events.
|
||
* The EventLog object is a front-end handle, logging flexible
|
||
* [information records](\ref lib::Record) into a possibly shared (vector)
|
||
* buffer in heap storage. An extended query DSL allows to write
|
||
* assertions to cover the occurrence of events in unit tests.
|
||
* @warning not threadsafe
|
||
* @see EventLog_test
|
||
*/
|
||
class EventLog
|
||
{
|
||
|
||
std::shared_ptr<Log> log_;
|
||
|
||
|
||
void
|
||
log (std::initializer_list<string> const& ili)
|
||
{
|
||
log_->emplace_back(ili);
|
||
}
|
||
|
||
template<typename ATTR, typename ARGS>
|
||
void
|
||
log (Symbol typeID, ATTR&& attribs, ARGS&& args)
|
||
{
|
||
log_->emplace_back(typeID, std::forward<ATTR>(attribs)
|
||
, std::forward<ARGS>(args));
|
||
}
|
||
|
||
|
||
public:
|
||
explicit
|
||
EventLog (string logID);
|
||
|
||
explicit
|
||
EventLog (CStr logID)
|
||
: EventLog(string(logID))
|
||
{ }
|
||
|
||
template<class X>
|
||
explicit
|
||
EventLog (const X *const obj)
|
||
: EventLog(idi::instanceTypeID (obj))
|
||
{ }
|
||
|
||
|
||
// standard copy operations acceptable
|
||
|
||
|
||
/// @return logID defined with the ctor to distinguish this log instance
|
||
string
|
||
getID() const
|
||
{
|
||
return log_->front().get("this");
|
||
}
|
||
|
||
|
||
/** Merge this log into another log, forming a combined log
|
||
* @param otherLog target to integrate this log's contents into.
|
||
* @return reference to the merged new log instance
|
||
* @see EventLog_test::verify_logJoining()
|
||
*/
|
||
EventLog& joinInto (EventLog& otherLog);
|
||
|
||
|
||
/** purge log contents while retaining just the original Header-ID */
|
||
EventLog& clear();
|
||
EventLog& clear (string alteredLogID);
|
||
EventLog& clear (CStr alteredLogID);
|
||
|
||
template<class X>
|
||
EventLog&
|
||
clear (const X *const obj)
|
||
{
|
||
return clear (idi::instanceTypeID (obj));
|
||
}
|
||
|
||
|
||
|
||
/* ==== Logging API ==== */
|
||
|
||
using ArgSeq = lib::diff::RecordSetup<string>::Storage;
|
||
|
||
/** log some text as event */
|
||
EventLog& event (string text);
|
||
|
||
/** log some event, with additional ID or classifier
|
||
* @param classifier info to be saved into the `ID` attribute
|
||
* @param text actual payload info, to be logged as argument
|
||
*/
|
||
EventLog& event (string classifier, string text);
|
||
|
||
template<typename...ELMS>
|
||
EventLog&
|
||
event (string classifier, ELMS const& ...initialiser)
|
||
{
|
||
log ("event", ArgSeq{"ID="+classifier}, collectStr<ArgSeq> (initialiser...));
|
||
return *this;
|
||
}
|
||
|
||
/** Log occurrence of a function call with no arguments.
|
||
* @param target the object or scope on which the function is invoked
|
||
* @param function name of the function being invoked
|
||
*/
|
||
EventLog& call (string target, string function);
|
||
|
||
/** Log a function call with a sequence of stringified arguments */
|
||
EventLog& call (string target, string function, ArgSeq&& args);
|
||
|
||
EventLog& call (CStr target, CStr function, ArgSeq&& args);
|
||
|
||
/** Log a function call with arbitrary arguments */
|
||
template<typename...ARGS>
|
||
EventLog&
|
||
call (string target, string function, ARGS const& ...args)
|
||
{
|
||
return call (target, function, collectStr<ArgSeq>(args...));
|
||
}
|
||
|
||
/** Log a function call on given object ("`this`")... */
|
||
template<class X, typename...ARGS>
|
||
EventLog&
|
||
call (const X *const targetObj, string function, ARGS const& ...args)
|
||
{
|
||
return call (idi::instanceTypeID (targetObj), function, args...);
|
||
}
|
||
|
||
template<typename...ARGS>
|
||
EventLog&
|
||
call (CStr target, string function, ARGS const& ...args)
|
||
{
|
||
return call (string(target), function, args...);
|
||
}
|
||
|
||
template<typename...ELMS>
|
||
EventLog&
|
||
note (ELMS const& ...initialiser)
|
||
{
|
||
log_->emplace_back (collectStr<ArgSeq> (initialiser...));
|
||
return *this;
|
||
}
|
||
|
||
|
||
/** Log a warning entry */
|
||
EventLog& warn (string text);
|
||
|
||
/** Log an error note */
|
||
EventLog& error (string text);
|
||
|
||
/** Log a fatal failure */
|
||
EventLog& fatal (string text);
|
||
|
||
/** Log the creation of an object.
|
||
* Such an entry can be [matched as event](\ref ::verifyEvent) */
|
||
EventLog& create (string text);
|
||
|
||
|
||
/** Log the destruction of an object.
|
||
* Can be matched as event. */
|
||
EventLog& destroy (string text);
|
||
|
||
|
||
/** Qualify the latest entry: set further attribute(s) */
|
||
template<typename X, typename...ARGS>
|
||
EventLog&
|
||
addAttrib (string const& key, X&& initialiser, ARGS&& ...args)
|
||
{
|
||
REQUIRE (not isnil (*log_));
|
||
mutateInPlace (log_->back())
|
||
.attrib(key,initialiser, std::forward<ARGS>(args)...);
|
||
return *this;
|
||
}
|
||
|
||
|
||
|
||
|
||
/* ==== Iteration ==== */
|
||
|
||
bool
|
||
empty() const
|
||
{
|
||
return 1 >= log_->size(); // do not count the log header
|
||
}
|
||
|
||
|
||
using Iter = lib::RangeIter<Log::const_iterator>;
|
||
|
||
typedef Iter const_iterator;
|
||
typedef const Entry value_type;
|
||
|
||
const_iterator begin() const { return Iter(log_->begin(), log_->end()); }
|
||
const_iterator end() const { return Iter(); }
|
||
|
||
friend const_iterator begin (EventLog const& log) { return log.begin(); }
|
||
friend const_iterator end (EventLog const& log) { return log.end(); }
|
||
|
||
|
||
|
||
|
||
/* ==== Query/Verification API ==== */
|
||
|
||
/** start a query to match for some substring.
|
||
* The resulting matcher object will qualify on any log entry
|
||
* containing the given string. By adding subsequent further
|
||
* query expressions on the returned [matcher object](\ref EventMatch),
|
||
* the query can be refined. Moreover it is possible to chain up further
|
||
* search queries, which will be executing starting from the position of the
|
||
* previous match. The final result can be retrieved by `bool` conversion
|
||
*/
|
||
EventMatch verify (string match) const;
|
||
|
||
/** start a query to match with a regular expression
|
||
* @param regExp definition
|
||
* @remarks the expression will work against the full
|
||
* `string` representation of the log entries.
|
||
* Meaning, it can also match type and attributes
|
||
*/
|
||
EventMatch verifyMatch (string regExp) const;
|
||
|
||
/** start a query to match for some event.
|
||
* @remarks only a subset of all log entries is treated as "event",
|
||
* any other log entry will not be considered for this query.
|
||
* Besides the regular [events](\ref ::event()), also errors,
|
||
* creation and destruction of objects count as "event".
|
||
* @param match text to (substring)match against the argument logged as event
|
||
*/
|
||
EventMatch verifyEvent (string match) const;
|
||
|
||
/** start a query to match for an specific kind of element
|
||
* @param classifier select kind of event by match on type or ID
|
||
*/
|
||
EventMatch verifyEvent (string classifier, string match) const;
|
||
|
||
template<typename X>
|
||
EventMatch
|
||
verifyEvent (string classifier, X const& something) const
|
||
{
|
||
return verifyEvent (classifier, util::toString (something));
|
||
}
|
||
|
||
/** start a query to match especially a function call
|
||
* @param match text to match against the function name
|
||
*/
|
||
EventMatch verifyCall (string match) const;
|
||
|
||
/** start a query to ensure the given expression does _not_ match. */
|
||
EventMatch
|
||
ensureNot (string match) const;
|
||
|
||
|
||
|
||
|
||
/** equality comparison is based on the actual log contents */
|
||
friend bool
|
||
operator== (EventLog const& l1, EventLog const& l2)
|
||
{
|
||
return l1.log_ == l2.log_
|
||
or (l1.log_ and l2.log_
|
||
and *l1.log_ == *l2.log_);
|
||
}
|
||
friend bool
|
||
operator!= (EventLog const& l1, EventLog const& l2)
|
||
{
|
||
return not (l1 == l2);
|
||
}
|
||
};
|
||
|
||
|
||
|
||
}} // namespace lib::test
|
||
#endif /*LIB_TEST_EVENT_LOG_H*/
|