| %% Copyright (c) 2012, Magnus Klaar <klaar@ninenines.eu> |
| %% |
| %% Permission to use, copy, modify, and/or distribute this software for any |
| %% purpose with or without fee is hereby granted, provided that the above |
| %% copyright notice and this permission notice appear in all copies. |
| %% |
| %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| |
| |
| %% @doc Event filter implementation. |
| %% |
| %% An event query is constructed using the built in operators exported from |
| %% this module. The filtering operators are used to specify which events |
| %% should be included in the output of the query. The default output action |
| %% is to copy all events matching the input filters associated with a query |
| %% to the output. This makes it possible to construct and compose multiple |
| %% queries at runtime. |
| %% |
| %% === Examples of built in filters === |
| %% ``` |
| %% %% Select all events where 'a' exists and is greater than 0. |
| %% glc:gt(a, 0). |
| %% %% Select all events where 'a' exists and is equal to 0. |
| %% glc:eq(a, 0). |
| %% %% Select all events where 'a' exists and is less than 0. |
| %% glc:lt(a, 0). |
| %% |
| %% %% Select no input events. Used as black hole query. |
| %% glc:null(false). |
| %% %% Select all input events. Used as passthrough query. |
| %% glc:null(true). |
| %% ''' |
| %% |
| %% === Examples of combining filters === |
| %% ``` |
| %% %% Select all events where both 'a' and 'b' exists and are greater than 0. |
| %% glc:all([glc:gt(a, 0), glc:gt(b, 0)]). |
| %% %% Select all events where 'a' or 'b' exists and are greater than 0. |
| %% glc:any([glc:get(a, 0), glc:gt(b, 0)]). |
| %% ''' |
| %% |
| %% === Handling output events === |
| %% |
| %% Once a query has been composed it is possible to override the output action |
| %% with an erlang function. The function will be applied to each output event |
| %% from the query. The return value from the function will be ignored. |
| %% |
| %% ``` |
| %% %% Write all input events as info reports to the error logger. |
| %% glc:with(glc:null(true), fun(E) -> |
| %% error_logger:info_report(gre:pairs(E)) end). |
| %% ''' |
| %% |
| -module(glc). |
| |
| -export([ |
| compile/2, |
| handle/2, |
| delete/1 |
| ]). |
| |
| -export([ |
| lt/2, |
| eq/2, |
| gt/2 |
| ]). |
| |
| -export([ |
| all/1, |
| any/1, |
| null/1, |
| with/2 |
| ]). |
| |
| -export([ |
| union/1 |
| ]). |
| |
| -record(module, { |
| 'query' :: term(), |
| tables :: [{atom(), ets:tid()}], |
| qtree :: term() |
| }). |
| |
| -type syntaxTree() :: erl_syntax:syntaxTree(). |
| |
| -record(state, { |
| event = undefined :: syntaxTree(), |
| fields = [] :: [{atom(), syntaxTree()}], |
| fieldc = 0 :: non_neg_integer(), |
| paramvars = [] :: [{term(), syntaxTree()}], |
| paramstab = undefined :: ets:tid() |
| }). |
| |
| -type nextFun() :: fun((#state{}) -> [syntaxTree()]). |
| -type q() :: tuple(). |
| |
| -spec lt(atom(), term()) -> q(). |
| lt(Key, Term) when is_atom(Key) -> {Key, '<', Term}. |
| |
| -spec eq(atom(), term()) -> q(). |
| eq(Key, Term) when is_atom(Key) -> {Key, '=', Term}. |
| |
| -spec gt(atom(), term()) -> q(). |
| gt(Key, Term) when is_atom(Key) -> {Key, '>', Term}. |
| |
| |
| %% @doc Filter the input using multiple filters. |
| %% |
| %% For an input to be considered valid output the all filters specified |
| %% in the list must hold for the input event. The list is expected to |
| %% be a non-empty list. If the list of filters is an empty list a `badarg' |
| %% error will be thrown. |
| -spec all([q()]) -> q(). |
| all([_|_]=Conds) -> |
| {all, Conds}; |
| all(Other) -> |
| erlang:error(badarg, [Other]). |
| |
| %% @doc Filter the input using one of multiple filters. |
| %% |
| %% For an input to be considered valid output on of the filters specified |
| %% in the list must hold for the input event. The list is expected to be |
| %% a non-empty list. If the list of filters is an empty list a `badarg' |
| %% error will be thrown. |
| -spec any([q()]) -> q(). |
| any([_|_]=Conds) -> |
| {any, Conds}; |
| any(Other) -> |
| erlang:error(badarg, [Other]). |
| |
| |
| %% @doc Always return `true' or `false'. |
| -spec null(boolean()) -> q(). |
| null(Result) when is_boolean(Result) -> |
| {null, Result}. |
| |
| %% @doc Apply a function to each output of a query. |
| %% |
| %% Updating the output action of a query finalizes it. Attempting |
| %% to use a finalized query to construct a new query will result |
| %% in a `badarg' error. |
| -spec with(q(), fun((gre:event()) -> term())) -> q(). |
| with(Query, Fun) when is_function(Fun, 1) -> |
| {with, Query, Fun}. |
| |
| %% @doc Return a union of multiple queries. |
| -spec union([q()]) -> q(). |
| union(Queries) -> |
| {union, Queries}. |
| |
| |
| %% @doc Compile a query to a module. |
| %% |
| %% On success the module representing the query is returned. The module and |
| %% data associated with the query must be released using the {@link delete/1} |
| %% function. The name of the query module is expected to be unique. |
| -spec compile(atom(), list()) -> {ok, atom()}. |
| compile(Module, Query) -> |
| {ok, ModuleData} = module_data(Query), |
| {ok, forms, Forms} = abstract_module(Module, ModuleData), |
| {ok, Module, Binary} = compile_forms(Forms, []), |
| {ok, loaded, Module} = load_binary(Module, Binary), |
| {ok, Module}. |
| |
| %% @doc Handle an event using a compiled query. |
| %% |
| %% The input event is expected to have been returned from {@link gre:make/2}. |
| -spec handle(atom(), gre:event()) -> ok. |
| handle(Module, Event) -> |
| Module:handle(Event). |
| |
| %% @doc Release a compiled query. |
| %% |
| %% This releases all resources allocated by a compiled query. The query name |
| %% is expected to be associated with an existing query module. Calling this |
| %% function will result in a runtime error. |
| -spec delete(atom()) -> ok. |
| delete(_Module) -> |
| ok. |
| |
| |
| %% @private Map a query to a module data term. |
| -spec module_data(term()) -> {ok, #module{}}. |
| module_data(Query) -> |
| %% terms in the query which are not valid arguments to the |
| %% erl_syntax:abstract/1 functions are stored in ETS. |
| %% the terms are only looked up once they are necessary to |
| %% continue evaluation of the query. |
| Params = ets:new(params, [set,protected]), |
| %% query counters are stored in a shared ETS table. this should |
| %% be an optional feature. enable by defaults to simplify tests. |
| Counters = ets:new(counters, [set,public]), |
| ets:insert(Counters, [{input,0}, {filter,0}, {output,0}]), |
| %% the abstract_tables/1 function expects a list of name-tid pairs. |
| %% tables are referred to by name in the generated code. the table/1 |
| %% function maps names to tids. |
| Tables = [{params,Params}, {counters,Counters}], |
| Query2 = glc_lib:reduce(Query), |
| {ok, #module{'query'=Query, tables=Tables, qtree=Query2}}. |
| |
| |
| %% @private Map a query to a simplified query tree term. |
| %% |
| %% The simplified query tree is used to combine multiple queries into one |
| %% query module. The goal of this is to reduce the filtering and dispatch |
| %% overhead when multiple concurrent queries are executed. |
| %% |
| %% A fixed selection condition may be used to specify a property that an event |
| %% must have in order to be considered part of the input stream for a query. |
| %% |
| %% For the sake of simplicity it is only possible to define selection |
| %% conditions using the fields present in the context and identifiers |
| %% of an event. The fields in the context are bound to the reserved |
| %% names: |
| %% |
| %% - '$n': node name |
| %% - '$a': application name |
| %% - '$p': process identifier |
| %% - '$t': timestamp |
| %% |
| %% |
| %% If an event must be selected based on the runtime state of an event handler |
| %% this must be done in the body of the handler. |
| -type qcond() :: |
| {atom(), '<', term()} | |
| {atom(), '=', term()} | |
| {atom(), '>', term()} | |
| {any, [qcond()]} | |
| {all, [qcond()]}. |
| |
| |
| %% abstract code geneation functions |
| |
| %% @private Generate an abstract dispatch module. |
| -spec abstract_module(atom(), #module{}) -> {ok, forms, list()}. |
| abstract_module(Module, Data) -> |
| Forms = [erl_syntax:revert(E) || E <- abstract_module_(Module, Data)], |
| case lists:keyfind(errors, 1, erl_syntax_lib:analyze_forms(Forms)) of |
| false -> {ok, forms, Forms}; |
| {_, []} -> {ok, forms, Forms}; |
| {_, [_|_]}=Errors -> Errors |
| end. |
| |
| %% @private Generate an abstract dispatch module. |
| -spec abstract_module_(atom(), #module{}) -> [erl_syntax:syntaxTree()]. |
| abstract_module_(Module, #module{tables=Tables, qtree=Tree}=Data) -> |
| {_, ParamsTable} = lists:keyfind(params, 1, Tables), |
| AbstractMod = [ |
| %% -module(Module) |
| erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]), |
| %% -export([ |
| erl_syntax:attribute( |
| erl_syntax:atom(export), |
| [erl_syntax:list([ |
| %% info/1 |
| erl_syntax:arity_qualifier( |
| erl_syntax:atom(info), |
| erl_syntax:integer(1)), |
| %% table/1 |
| erl_syntax:arity_qualifier( |
| erl_syntax:atom(table), |
| erl_syntax:integer(1)), |
| %% handle/1 |
| erl_syntax:arity_qualifier( |
| erl_syntax:atom(handle), |
| erl_syntax:integer(1))])]), |
| %% ]). |
| %% info(Name) -> Term. |
| erl_syntax:function( |
| erl_syntax:atom(info), |
| abstract_info(Data) ++ |
| [erl_syntax:clause( |
| [erl_syntax:underscore()], none, |
| [abstract_apply(erlang, error, [erl_syntax:atom(badarg)])])]), |
| %% table(Name) -> ets:tid(). |
| erl_syntax:function( |
| erl_syntax:atom(table), |
| abstract_tables(Tables) ++ |
| [erl_syntax:clause( |
| [erl_syntax:underscore()], none, |
| [abstract_apply(erlang, error, [erl_syntax:atom(badarg)])])]), |
| %% handle(Event) - entry function |
| erl_syntax:function( |
| erl_syntax:atom(handle), |
| [erl_syntax:clause([erl_syntax:variable("Event")], none, |
| [abstract_count(input), |
| erl_syntax:application(none, |
| erl_syntax:atom(handle_), [erl_syntax:variable("Event")])])]), |
| %% input_(Node, App, Pid, Tags, Values) - filter roots |
| erl_syntax:function( |
| erl_syntax:atom(handle_), |
| [erl_syntax:clause([erl_syntax:variable("Event")], none, |
| abstract_filter(Tree, #state{ |
| event=erl_syntax:variable("Event"), |
| paramstab=ParamsTable}))]) |
| ], |
| %% Transform Term -> Key to Key -> Term |
| ParamsList = [{K, V} || {V, K} <- ets:tab2list(ParamsTable)], |
| ets:delete_all_objects(ParamsTable), |
| ets:insert(ParamsTable, ParamsList), |
| AbstractMod. |
| |
| %% @private Return the clauses of the table/1 function. |
| abstract_tables(Tables) -> |
| [erl_syntax:clause( |
| [erl_syntax:abstract(K)], none, |
| [erl_syntax:abstract(V)]) |
| || {K, V} <- Tables]. |
| |
| %% @private Return the clauses of the info/1 function. |
| abstract_info(#module{'query'=Query}) -> |
| [erl_syntax:clause([erl_syntax:abstract(K)], none, V) |
| || {K, V} <- [ |
| {'query', abstract_query(Query)}, |
| {input, abstract_getcount(input)}, |
| {filter, abstract_getcount(filter)}, |
| {output, abstract_getcount(output)} |
| ]]. |
| |
| %% @private Return the original query as an expression. |
| abstract_query({with, _, _}) -> |
| [erl_syntax:abstract([])]; |
| abstract_query(Query) -> |
| [erl_syntax:abstract(Query)]. |
| |
| |
| %% @private Return a list of expressions to apply a filter. |
| %% @todo Allow mulitple functions to be specified using `with/2'. |
| -spec abstract_filter(q(), #state{}) -> [syntaxTree()]. |
| abstract_filter({with, Cond, Fun}, State) -> |
| abstract_filter_(Cond, |
| _OnMatch=fun(State2) -> |
| [abstract_count(output)] ++ abstract_with(Fun, State2) end, |
| _OnNomatch=fun(_State2) -> [abstract_count(filter)] end, State); |
| abstract_filter(Cond, State) -> |
| abstract_filter_(Cond, |
| _OnMatch=fun(_State2) -> [abstract_count(output)] end, |
| _OnNomatch=fun(_State2) -> [abstract_count(filter)] end, State). |
| |
| %% @private Return a list of expressions to apply a filter. |
| %% A filter expects two continuation functions which generates the expressions |
| %% to apply when the filter matches or fails to match. The state passed to the |
| %% functions will be contain all variable bindings to previously accessed |
| %% fields and parameters. |
| -spec abstract_filter_(qcond(), nextFun(), nextFun(), #state{}) -> |
| syntaxTree(). |
| abstract_filter_({null, true}, OnMatch, _OnNomatch, State) -> |
| OnMatch(State); |
| abstract_filter_({null, false}, _OnMatch, OnNomatch, State) -> |
| OnNomatch(State); |
| abstract_filter_({Key, Op, Value}, OnMatch, OnNomatch, State) |
| when Op =:= '>'; Op =:= '='; Op =:= '<' -> |
| Op2 = case Op of '=' -> '=:='; Op -> Op end, |
| abstract_opfilter(Key, Op2, Value, OnMatch, OnNomatch, State); |
| abstract_filter_({'any', Conds}, OnMatch, OnNomatch, State) -> |
| abstract_any(Conds, OnMatch, OnNomatch, State); |
| abstract_filter_({'all', Conds}, OnMatch, OnNomatch, State) -> |
| abstract_all(Conds, OnMatch, OnNomatch, State). |
| |
| %% @private Return a branch based on a built in operator. |
| -spec abstract_opfilter(atom(), atom(), term(), nextFun(), |
| nextFun(), #state{}) -> [syntaxTree()]. |
| abstract_opfilter(Key, Opname, Value, OnMatch, OnNomatch, State) -> |
| abstract_getkey(Key, |
| _OnMatch=fun(#state{fields=Fields}=State2) -> |
| {_, Field} = lists:keyfind(Key, 1, Fields), |
| [erl_syntax:case_expr( |
| erl_syntax:application( |
| erl_syntax:atom(erlang), erl_syntax:atom(Opname), |
| [Field, erl_syntax:abstract(Value)]), |
| [erl_syntax:clause([erl_syntax:atom(true)], none, |
| OnMatch(State2)), |
| erl_syntax:clause([erl_syntax:atom(false)], none, |
| OnNomatch(State2))])] end, |
| _OnNomatch=fun(State2) -> OnNomatch(State2) end, State). |
| |
| |
| %% @private Generate an `all' filter. |
| %% An `all' filter is evaluated by testing all conditions that must hold. If |
| %% any of the conditions does not hold the evaluation is short circuted at that |
| %% point. This means that the `OnNomatch' branch is executed once for each |
| %% condition. The `OnMatch' branch is only executed once. |
| -spec abstract_all([qcond()], nextFun(), nextFun(), #state{}) -> |
| [syntaxTree()]. |
| abstract_all([H|T], OnMatch, OnNomatch, State) -> |
| abstract_filter_(H, |
| _OnMatch=fun(State2) -> abstract_all(T, OnMatch, OnNomatch, State2) |
| end, OnNomatch, State); |
| abstract_all([], OnMatch, _OnNomatch, State) -> |
| OnMatch(State). |
| |
| %% @private |
| -spec abstract_any([qcond()], nextFun(), nextFun(), #state{}) -> |
| [syntaxTree()]. |
| abstract_any([H|T], OnMatch, OnNomatch, State) -> |
| abstract_filter_(H, OnMatch, |
| _OnNomatch=fun(State2) -> abstract_any(T, OnMatch, OnNomatch, State2) |
| end, State); |
| abstract_any([], _OnMatch, OnNomatch, State) -> |
| OnNomatch(State). |
| |
| %% @private |
| -spec abstract_with(fun((gre:event()) -> term()), #state{}) -> [syntaxTree()]. |
| abstract_with(Fun, State) when is_function(Fun, 1) -> |
| abstract_getparam(Fun, fun(#state{event=Event, paramvars=Params}) -> |
| {_, Fun2} = lists:keyfind(Fun, 1, Params), |
| [erl_syntax:application(none, Fun2, [Event])] |
| end, State). |
| |
| %% @private Bind the value of a field to a variable. |
| %% If the value of a field has already been bound to a variable the previous |
| %% binding is reused over re-accessing the value. The `OnMatch' function is |
| %% expected to access the variable stored in the state record. The `OnNomatch' |
| %% function must not attempt to access the variable. |
| -spec abstract_getkey(atom(), nextFun(), nextFun(), #state{}) -> |
| [syntaxTree()]. |
| abstract_getkey(Key, OnMatch, OnNomatch, #state{fields=Fields}=State) -> |
| case lists:keyfind(Key, 1, Fields) of |
| {Key, _Variable} -> OnMatch(State); |
| false -> abstract_getkey_(Key, OnMatch, OnNomatch, State) |
| end. |
| |
| |
| -spec abstract_getkey_(atom(), nextFun(), nextFun(), #state{}) -> |
| [syntaxTree()]. |
| abstract_getkey_(Key, OnMatch, OnNomatch, #state{ |
| event=Event, fields=Fields}=State) -> |
| [erl_syntax:case_expr( |
| abstract_apply(gre, find, [erl_syntax:atom(Key), Event]), |
| [erl_syntax:clause([ |
| erl_syntax:tuple([ |
| erl_syntax:atom(true), |
| field_variable(Key)])], none, |
| OnMatch(State#state{ |
| fields=[{Key, field_variable(Key)}|Fields]})), |
| erl_syntax:clause([ |
| erl_syntax:atom(false)], none, |
| OnNomatch(State)) |
| ] |
| )]. |
| |
| %% @private Bind the value of a parameter to a variable. |
| %% During code generation the parameter value is used as the identity of the |
| %% parameter. At runtime a unique integer is used as the identity. |
| -spec abstract_getparam(term(), nextFun(), #state{}) -> [syntaxTree()]. |
| abstract_getparam(Term, OnBound, #state{paramvars=Params}=State) -> |
| case lists:keyfind(Term, 1, Params) of |
| {_, _Variable} -> OnBound(State); |
| %% parameter not bound to variable in this scope. |
| false -> abstract_getparam_(Term, OnBound, State) |
| end. |
| |
| |
| -spec abstract_getparam_(term(), nextFun(), #state{}) -> [syntaxTree()]. |
| abstract_getparam_(Term, OnBound, #state{paramstab=Table, |
| paramvars=Params}=State) -> |
| Key = case ets:lookup(Table, Term) of |
| [{_, Key2}] -> |
| Key2; |
| [] -> |
| Key2 = ets:info(Table, size), |
| ets:insert(Table, {Term, Key2}), |
| Key2 |
| end, |
| [erl_syntax:match_expr( |
| param_variable(Key), |
| abstract_apply(ets, lookup_element, |
| [abstract_apply(table, [erl_syntax:atom(params)]), |
| erl_syntax:abstract(Key), |
| erl_syntax:abstract(2)])) |
| ] ++ OnBound(State#state{paramvars=[{Term, param_variable(Key)}|Params]}). |
| |
| %% @private Generate a variable name for the value of a field. |
| %% @todo Encode non-alphanumeric characters as integer values. |
| -spec field_variable(atom()) -> syntaxTree(). |
| field_variable(Key) -> |
| erl_syntax:variable("Field_" ++ atom_to_list(Key)). |
| |
| %% @private Generate a variable name for the value of a parameter. |
| -spec param_variable(integer()) -> syntaxTree(). |
| param_variable(Key) -> |
| erl_syntax:variable("Param_" ++ integer_to_list(Key)). |
| |
| |
| %% @private Return an expression to increment a counter. |
| %% @todo Pass state record. Only Generate code if `statistics' is enabled. |
| -spec abstract_count(atom()) -> syntaxTree(). |
| abstract_count(Counter) -> |
| abstract_apply(ets, update_counter, |
| [abstract_apply(table, [erl_syntax:atom(counters)]), |
| erl_syntax:abstract(Counter), |
| erl_syntax:abstract({2,1})]). |
| |
| |
| %% @private Return an expression to get the value of a counter. |
| %% @todo Pass state record. Only Generate code if `statistics' is enabled. |
| -spec abstract_getcount(atom()) -> [syntaxTree()]. |
| abstract_getcount(Counter) -> |
| [abstract_apply(ets, lookup_element, |
| [abstract_apply(table, [erl_syntax:atom(counters)]), |
| erl_syntax:abstract(Counter), |
| erl_syntax:abstract(2)])]. |
| |
| |
| %% abstract code util functions |
| |
| |
| %% @private Compile an abstract module. |
| -spec compile_forms(term(), [term()]) -> {ok, atom(), binary()}. |
| compile_forms(Forms, _Opts) -> |
| case compile:forms(Forms) of |
| {ok, Module, Binary} -> |
| {ok, Module, Binary}; |
| {ok, Module, Binary, _Warnings} -> |
| {ok, Module, Binary}; |
| Error -> |
| erlang:error({compile_forms, Error}) |
| end. |
| |
| %% @private Load a module binary. |
| -spec load_binary(atom(), binary()) -> {ok, loaded, atom()}. |
| load_binary(Module, Binary) -> |
| case code:load_binary(Module, "", Binary) of |
| {module, Module} -> {ok, loaded, Module}; |
| {error, Reason} -> exit({error_loading_module, Module, Reason}) |
| end. |
| |
| %% @private Apply an exported function. |
| -spec abstract_apply(atom(), atom(), [syntaxTree()]) -> syntaxTree(). |
| abstract_apply(Module, Function, Arguments) -> |
| erl_syntax:application( |
| erl_syntax:atom(Module), |
| erl_syntax:atom(Function), |
| Arguments). |
| |
| %% @private Apply a module local function. |
| -spec abstract_apply(atom(), [syntaxTree()]) -> syntaxTree(). |
| abstract_apply(Function, Arguments) -> |
| erl_syntax:application( |
| erl_syntax:atom(Function), |
| Arguments). |
| |
| |
| -ifdef(TEST). |
| -include_lib("eunit/include/eunit.hrl"). |
| |
| setup_query(Module, Query) -> |
| ?assertNot(erlang:module_loaded(Module)), |
| ?assertEqual({ok, Module}, case (catch compile(Module, Query)) of |
| {'EXIT',_}=Error -> ?debugFmt("~p", [Error]), Error; Else -> Else end), |
| ?assert(erlang:function_exported(Module, table, 1)), |
| ?assert(erlang:function_exported(Module, handle, 1)), |
| {compiled, Module}. |
| |
| nullquery_compiles_test() -> |
| {compiled, Mod} = setup_query(testmod1, glc:null(false)), |
| ?assertError(badarg, Mod:table(noexists)). |
| |
| params_table_exists_test() -> |
| {compiled, Mod} = setup_query(testmod2, glc:null(false)), |
| ?assert(is_integer(Mod:table(params))), |
| ?assertMatch([_|_], ets:info(Mod:table(params))). |
| |
| nullquery_exists_test() -> |
| {compiled, Mod} = setup_query(testmod3, glc:null(false)), |
| ?assert(erlang:function_exported(Mod, info, 1)), |
| ?assertError(badarg, Mod:info(invalid)), |
| ?assertEqual({null, false}, Mod:info('query')). |
| |
| init_counters_test() -> |
| {compiled, Mod} = setup_query(testmod4, glc:null(false)), |
| ?assertEqual(0, Mod:info(input)), |
| ?assertEqual(0, Mod:info(filter)), |
| ?assertEqual(0, Mod:info(output)). |
| |
| filtered_event_test() -> |
| %% If no selection condition is specified no inputs can match. |
| {compiled, Mod} = setup_query(testmod5, glc:null(false)), |
| glc:handle(Mod, gre:make([], [list])), |
| ?assertEqual(1, Mod:info(input)), |
| ?assertEqual(1, Mod:info(filter)), |
| ?assertEqual(0, Mod:info(output)). |
| |
| nomatch_event_test() -> |
| %% If a selection condition but no body is specified the event |
| %% is expected to count as filtered out if the condition does |
| %% not hold. |
| {compiled, Mod} = setup_query(testmod6, glc:eq('$n', 'noexists@nohost')), |
| glc:handle(Mod, gre:make([{'$n', 'noexists2@nohost'}], [list])), |
| ?assertEqual(1, Mod:info(input)), |
| ?assertEqual(1, Mod:info(filter)), |
| ?assertEqual(0, Mod:info(output)). |
| |
| opfilter_eq_test() -> |
| %% If a selection condition but no body is specified the event |
| %% counts as input to the query, but not as filtered out. |
| {compiled, Mod} = setup_query(testmod7, glc:eq('$n', 'noexists@nohost')), |
| glc:handle(Mod, gre:make([{'$n', 'noexists@nohost'}], [list])), |
| ?assertEqual(1, Mod:info(input)), |
| ?assertEqual(0, Mod:info(filter)), |
| ?assertEqual(1, Mod:info(output)), |
| done. |
| |
| |
| opfilter_gt_test() -> |
| {compiled, Mod} = setup_query(testmod8, glc:gt(a, 1)), |
| glc:handle(Mod, gre:make([{'a', 2}], [list])), |
| ?assertEqual(1, Mod:info(input)), |
| ?assertEqual(0, Mod:info(filter)), |
| glc:handle(Mod, gre:make([{'a', 0}], [list])), |
| ?assertEqual(2, Mod:info(input)), |
| ?assertEqual(1, Mod:info(filter)), |
| ?assertEqual(1, Mod:info(output)), |
| done. |
| |
| opfilter_lt_test() -> |
| {compiled, Mod} = setup_query(testmod9, glc:lt(a, 1)), |
| glc:handle(Mod, gre:make([{'a', 0}], [list])), |
| ?assertEqual(1, Mod:info(input)), |
| ?assertEqual(0, Mod:info(filter)), |
| ?assertEqual(1, Mod:info(output)), |
| glc:handle(Mod, gre:make([{'a', 2}], [list])), |
| ?assertEqual(2, Mod:info(input)), |
| ?assertEqual(1, Mod:info(filter)), |
| ?assertEqual(1, Mod:info(output)), |
| done. |
| |
| allholds_op_test() -> |
| {compiled, Mod} = setup_query(testmod10, |
| glc:all([glc:eq(a, 1), glc:eq(b, 2)])), |
| glc:handle(Mod, gre:make([{'a', 1}], [list])), |
| glc:handle(Mod, gre:make([{'a', 2}], [list])), |
| ?assertEqual(2, Mod:info(input)), |
| ?assertEqual(2, Mod:info(filter)), |
| glc:handle(Mod, gre:make([{'b', 1}], [list])), |
| glc:handle(Mod, gre:make([{'b', 2}], [list])), |
| ?assertEqual(4, Mod:info(input)), |
| ?assertEqual(4, Mod:info(filter)), |
| glc:handle(Mod, gre:make([{'a', 1},{'b', 2}], [list])), |
| ?assertEqual(5, Mod:info(input)), |
| ?assertEqual(4, Mod:info(filter)), |
| ?assertEqual(1, Mod:info(output)), |
| done. |
| |
| anyholds_op_test() -> |
| {compiled, Mod} = setup_query(testmod11, |
| glc:any([glc:eq(a, 1), glc:eq(b, 2)])), |
| glc:handle(Mod, gre:make([{'a', 2}], [list])), |
| glc:handle(Mod, gre:make([{'b', 1}], [list])), |
| ?assertEqual(2, Mod:info(input)), |
| ?assertEqual(2, Mod:info(filter)), |
| glc:handle(Mod, gre:make([{'a', 1}], [list])), |
| glc:handle(Mod, gre:make([{'b', 2}], [list])), |
| ?assertEqual(4, Mod:info(input)), |
| ?assertEqual(2, Mod:info(filter)), |
| done. |
| |
| with_function_test() -> |
| Self = self(), |
| {compiled, Mod} = setup_query(testmod12, |
| glc:with(glc:eq(a, 1), fun(Event) -> Self ! gre:fetch(a, Event) end)), |
| glc:handle(Mod, gre:make([{a,1}], [list])), |
| ?assertEqual(1, Mod:info(output)), |
| ?assertEqual(1, receive Msg -> Msg after 0 -> notcalled end), |
| done. |
| |
| union_single_test() -> |
| {compiled, _Mod} = setup_query(testmod13, |
| glc:union([glc:eq(a, 1)])), |
| done. |
| |
| -endif. |