Browse Source

Initial Commit

master
Evgeniy Khramtsov 4 years ago
commit
0c515cc1dd
  1. 70
      Makefile
  2. 95
      README.md
  3. 532
      c_src/mqtree.c
  4. BIN
      rebar
  5. 50
      rebar.config
  6. 37
      src/mqtree.app.src
  7. 99
      src/mqtree.erl
  8. 244
      test/mqtree_test.erl

70
Makefile

@ -0,0 +1,70 @@
REBAR=./rebar
all: src
src:
$(REBAR) get-deps compile
clean:
$(REBAR) clean
distclean: clean
rm -f config.status
rm -f config.log
rm -rf autom4te.cache
rm -rf deps
rm -rf ebin
rm -rf priv
rm -f vars.config
rm -f compile_commands.json
rm -rf dialyzer
test: all
mkdir -p .eunit/priv/lib
cp priv/lib/mqtree.* .eunit/priv/lib/
$(REBAR) -v skip_deps=true eunit
xref: all
$(REBAR) skip_deps=true xref
deps := $(wildcard deps/*/ebin)
dialyzer/erlang.plt:
@mkdir -p dialyzer
@dialyzer --build_plt --output_plt dialyzer/erlang.plt \
-o dialyzer/erlang.log --apps kernel stdlib erts; \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
dialyzer/deps.plt:
@mkdir -p dialyzer
@dialyzer --build_plt --output_plt dialyzer/deps.plt \
-o dialyzer/deps.log $(deps); \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
dialyzer/mqtree.plt:
@mkdir -p dialyzer
@dialyzer --build_plt --output_plt dialyzer/mqtree.plt \
-o dialyzer/ejabberd.log ebin; \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
erlang_plt: dialyzer/erlang.plt
@dialyzer --plt dialyzer/erlang.plt --check_plt -o dialyzer/erlang.log; \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
deps_plt: dialyzer/deps.plt
@dialyzer --plt dialyzer/deps.plt --check_plt -o dialyzer/deps.log; \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
mqtree_plt: dialyzer/mqtree.plt
@dialyzer --plt dialyzer/mqtree.plt --check_plt -o dialyzer/ejabberd.log; \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
dialyzer: erlang_plt deps_plt mqtree_plt
@dialyzer --plts dialyzer/*.plt --no_check_plt \
--get_warnings -o dialyzer/error.log ebin; \
status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi
check-syntax:
gcc -o nul -S ${CHK_SOURCES}
.PHONY: clean src test all dialyzer erlang_plt deps_plt mqtree_plt

95
README.md

@ -0,0 +1,95 @@
mqtree: Index tree for MQTT topic filters
====================================================
mqtree is an Erlang NIF implementation of N-ary tree to keep MQTT
topic filters for efficient matching.
# System requirements
To compile mqtree you need:
- GNU Make.
- GCC.
- Erlang/OTP 17.5 or higher.
# Compiling
```
$ git clone https://github.com/processone/mqtree.git
$ cd mqtree
$ make
```
# API
## new/0
```erlang
-spec new() -> tree().
```
Creates new tree. Note that the tree is mutable just like ETS.
The created tree gets destroyed when it's garbage collected.
## insert/2
```erlang
-spec insert(Tree :: tree(), Filter :: iodata()) -> ok.
```
Inserts `Filter` into `Tree` and increases its reference counter.
The reference counter is increased every time when the same
filter is inserted into the tree. The reference counter is decreased
when the filter is deleted, see [delete/2](#delete2).
**NOTE**: no checks are performed on the filter being inserted:
it's up to the caller to check if the filter conforms to the MQTT
specification.
## delete/2
```erlang
-spec delete(Tree :: tree(), Filter :: iodata()) -> ok.
```
Deletes `Filter` from `Tree` and decreases its reference counter.
Nothing is done if the filter is not found in the tree.
**NOTE**: no checks are performed on the filter being deleted:
it's up to the caller to check if the filter conforms to the MQTT
specification.
## match/2
```erlang
-spec match(Tree :: tree(), Path :: iodata()) -> [binary()].
```
Finds filters in `Tree` matching `Path` according to the MQTT
specification.
**NOTE**: no checks are performed on the path being matched:
it's up to the caller to check if the path conforms to the MQTT
specification.
## refc/2
```erlang
-spec refc(Tree :: tree(), Filter :: iodata()) -> non_neg_intger().
```
Returns the reference counter of `Filter` in `Tree`. In particular,
zero (0) is returned if the filter is not found in the tree.
**NOTE**: no checks are performed on the filter being searched:
it's up to the caller to check if the filter conforms to the MQTT
specification.
## clear/1
```erlang
-spec clear(Tree :: tree()) -> ok.
```
Deletes all filters from `Tree`.
## size/1
```erlang
-spec size(Tree :: tree()) -> ok.
```
Returns the size of `Tree`. That is, the number of filters in the
tree (irrespective of their reference counters).
## is_empty/1
```erlang
-spec is_empty(Tree :: tree()) -> boolean().
```
Returns `true` if `Tree` holds no filters. Returns `false` otherwise.

532
c_src/mqtree.c

@ -0,0 +1,532 @@
/*
* ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne
*
* The ejabberd software is the exclusive property of the licensor
* ProcessOne. It is protected by the law on copyright and
* international conventions. As a result, the dealer
* recognizes that will make every effort to ensure the confidentiality
* on the software. It is recalled that a violation of the rights of
* authors of the software is an infringement and that any
* counterfeit is punishable in France by Article L339-2 of the Code of
* Intellectual property and punishable by three years imprisonment and
* 300000 euros.
*
* Any infringement liable to be so qualified and
* would be caused by third parties and whose dealer has knowledge
* should be terminated by the licensor that it will make its case
* personal conduct of the proceedings. Any allegation of infringement
* formed against the dealer because of the use of the Software will
* be brought to the knowledge of the licensor which will assist
* in defense of the dealer in the manner and form that
* see fit and fix alone.
*
*/
#include <erl_nif.h>
#include <stdio.h>
#include <errno.h>
#include "uthash.h"
/****************************************************************
* Structures/Globals definitions *
****************************************************************/
typedef struct __tree_t {
char *key;
char *val;
int refc;
struct __tree_t *sub;
UT_hash_handle hh;
} tree_t;
typedef struct {
tree_t *tree;
ErlNifRWLock *lock;
} state_t;
static ErlNifResourceType *tree_state_t = NULL;
/****************************************************************
* MQTT Tree Manipulation *
****************************************************************/
tree_t *tree_new(char *key, size_t len) {
tree_t *tree = malloc(sizeof(tree_t));
if (tree) {
memset(tree, 0, sizeof(tree_t));
if (key && len) {
tree->key = malloc(len);
if (tree->key) {
memcpy(tree->key, key, len);
} else {
free(tree);
tree = NULL;
}
}
}
return tree;
}
void tree_free(tree_t *t) {
tree_t *found, *iter;
if (t) {
free(t->key);
free(t->val);
HASH_ITER(hh, t->sub, found, iter) {
HASH_DEL(t->sub, found);
tree_free(found);
}
memset(t, 0, sizeof(tree_t));
free(t);
}
}
void tree_clear(tree_t *root) {
tree_t *found, *iter;
HASH_ITER(hh, root->sub, found, iter) {
HASH_DEL(root->sub, found);
tree_free(found);
}
}
int tree_add(tree_t *root, char *path, size_t size) {
int i = 0;
size_t len;
tree_t *t = root;
tree_t *found, *new;
while (i<=size) {
len = strlen(path+i) + 1;
HASH_FIND_STR(t->sub, path+i, found);
if (found) {
i += len;
t = found;
} else {
new = tree_new(path+i, len);
if (new) {
HASH_ADD_STR(t->sub, key, new);
i += len;
t = new;
} else
return errno;
}
}
if (t->val) {
free(path);
} else {
for (i=0; i<size; i++) {
if (!path[i])
path[i] = '/';
}
t->val = path;
}
t->refc++;
return 0;
}
int tree_del(tree_t *root, char *path, size_t i, size_t size) {
tree_t *found;
if (i<=size) {
HASH_FIND_STR(root->sub, path+i, found);
if (found) {
i += strlen(path+i) + 1;
int deleted = tree_del(found, path, i, size);
if (deleted) {
HASH_DEL(root->sub, found);
tree_free(found);
}
}
} else if (root->refc) {
root->refc--;
if (!root->refc) {
free(root->val);
root->val = NULL;
}
}
return !root->refc && !root->sub;
}
void tree_size(tree_t *tree, size_t *size) {
tree_t *found, *iter;
HASH_ITER(hh, tree->sub, found, iter) {
if (found->refc) (*size)++;
tree_size(found, size);
}
}
int tree_refc(tree_t *tree, char *path, size_t i, size_t size) {
tree_t *found;
if (i<=size) {
HASH_FIND_STR(tree->sub, path+i, found);
if (found) {
i += strlen(path+i) + 1;
return tree_refc(found, path, i, size);
} else {
return 0;
}
} else
return tree->refc;
}
/****************************************************************
* NIF helpers *
****************************************************************/
static ERL_NIF_TERM cons(ErlNifEnv *env, char *str, ERL_NIF_TERM tail)
{
if (str) {
size_t len = strlen(str);
ERL_NIF_TERM head;
unsigned char *buf = enif_make_new_binary(env, len, &head);
if (buf) {
memcpy(buf, str, len);
return enif_make_list_cell(env, head, tail);
}
}
return tail;
}
static void match(ErlNifEnv *env, tree_t *root,
char *path, size_t i, size_t size, ERL_NIF_TERM *acc)
{
tree_t *found;
size_t len = 0;
if (i<=size) {
HASH_FIND_STR(root->sub, path+i, found);
if (found) {
len = strlen(path+i) + 1;
match(env, found, path, i+len, size, acc);
};
HASH_FIND_STR(root->sub, "+", found);
if (found) {
len = strlen(path+i) + 1;
match(env, found, path, i+len, size, acc);
}
HASH_FIND_STR(root->sub, "#", found);
if (found) {
*acc = cons(env, found->val, *acc);
}
} else {
*acc = cons(env, root->val, *acc);
HASH_FIND_STR(root->sub, "#", found);
if (found)
*acc = cons(env, found->val, *acc);
}
}
static void to_list(ErlNifEnv *env, tree_t *root, ERL_NIF_TERM *acc)
{
tree_t *found, *iter;
HASH_ITER(hh, root->sub, found, iter) {
if (found->val) {
size_t len = strlen(found->val);
ERL_NIF_TERM refc = enif_make_int(env, found->refc);
ERL_NIF_TERM val;
unsigned char *buf = enif_make_new_binary(env, len, &val);
if (buf) {
memcpy(buf, found->val, len);
*acc = enif_make_list_cell(env, enif_make_tuple2(env, val, refc), *acc);
}
};
to_list(env, found, acc);
}
}
static ERL_NIF_TERM dump(ErlNifEnv *env, tree_t *tree)
{
tree_t *found, *iter;
ERL_NIF_TERM tail, head;
tail = enif_make_list(env, 0);
HASH_ITER(hh, tree->sub, found, iter) {
head = dump(env, found);
tail = enif_make_list_cell(env, head, tail);
}
if (tree->key) {
ERL_NIF_TERM part, path;
part = enif_make_string(env, tree->key, ERL_NIF_LATIN1);
if (tree->val)
path = enif_make_string(env, tree->val, ERL_NIF_LATIN1);
else
path = enif_make_atom(env, "none");
return enif_make_tuple4(env, part, path, enif_make_int(env, tree->refc), tail);
} else
return tail;
}
static ERL_NIF_TERM raise(ErlNifEnv *env, int err)
{
switch (err) {
case ENOMEM:
return enif_raise_exception(env, enif_make_atom(env, "enomem"));
default:
return enif_make_badarg(env);
}
}
static char *prep_path(ErlNifBinary *bin) {
int i;
unsigned char c;
char *buf = malloc(bin->size+1);
if (buf) {
buf[bin->size] = 0;
for (i=0; i<bin->size; i++) {
c = bin->data[i];
if (c == '/') buf[i] = 0;
else buf[i] = c;
}
}
return buf;
}
/****************************************************************
* Constructors/Destructors *
****************************************************************/
static state_t *init_tree_state(ErlNifEnv *env) {
state_t *state = enif_alloc_resource(tree_state_t, sizeof(state_t));
if (state) {
memset(state, 0, sizeof(state_t));
state->tree = tree_new(NULL, 0);
state->lock = enif_rwlock_create("mqtree_lock");
if (state->tree && state->lock)
return state;
else
enif_release_resource(state);
}
return NULL;
}
static void destroy_tree_state(ErlNifEnv *env, void *data) {
state_t *state = (state_t *) data;
if (state) {
tree_free(state->tree);
if (state->lock) enif_rwlock_destroy(state->lock);
}
memset(state, 0, sizeof(state_t));
}
/****************************************************************
* NIF definitions *
****************************************************************/
static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM max) {
ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
tree_state_t = enif_open_resource_type(env, NULL, "mqtree_state",
destroy_tree_state,
flags, NULL);
return 0;
}
static void unload(ErlNifEnv* env, void* priv) {}
static ERL_NIF_TERM new_0(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
ERL_NIF_TERM result;
state_t *state = init_tree_state(env);
if (state) {
result = enif_make_resource(env, state);
enif_release_resource(state);
} else
result = raise(env, ENOMEM);
return result;
}
static ERL_NIF_TERM insert_2(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
ErlNifBinary path_bin;
ERL_NIF_TERM result;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
return raise(env, EINVAL);
if (!path_bin.size)
return enif_make_atom(env, "ok");
char *path = prep_path(&path_bin);
if (path) {
enif_rwlock_rwlock(state->lock);
int ret = tree_add(state->tree, path, path_bin.size);
enif_rwlock_rwunlock(state->lock);
if (!ret)
result = enif_make_atom(env, "ok");
else
result = raise(env, ENOMEM);
} else
result = raise(env, ENOMEM);
return result;
}
static ERL_NIF_TERM delete_2(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
ErlNifBinary path_bin;
ERL_NIF_TERM result;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
return raise(env, EINVAL);
if (!path_bin.size)
return enif_make_atom(env, "ok");
char *path = prep_path(&path_bin);
if (path) {
enif_rwlock_rwlock(state->lock);
tree_del(state->tree, path, 0, path_bin.size);
enif_rwlock_rwunlock(state->lock);
free(path);
result = enif_make_atom(env, "ok");
} else
result = raise(env, ENOMEM);
return result;
}
static ERL_NIF_TERM match_2(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
ErlNifBinary path_bin;
ERL_NIF_TERM result = enif_make_list(env, 0);
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
return raise(env, EINVAL);
if (!path_bin.size)
return result;
char *path = prep_path(&path_bin);
if (path) {
enif_rwlock_rlock(state->lock);
match(env, state->tree, path, 0, path_bin.size, &result);
enif_rwlock_runlock(state->lock);
free(path);
} else
result = raise(env, ENOMEM);
return result;
}
static ERL_NIF_TERM refc_2(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
ErlNifBinary path_bin;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state) ||
!enif_inspect_iolist_as_binary(env, argv[1], &path_bin))
return raise(env, EINVAL);
if (!path_bin.size)
return enif_make_int(env, 0);
char *path = prep_path(&path_bin);
if (path) {
enif_rwlock_rlock(state->lock);
int refc = tree_refc(state->tree, path, 0, path_bin.size);
enif_rwlock_runlock(state->lock);
free(path);
return enif_make_int(env, refc);
} else
return raise(env, ENOMEM);
}
static ERL_NIF_TERM clear_1(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
return raise(env, EINVAL);
enif_rwlock_rwlock(state->lock);
tree_clear(state->tree);
enif_rwlock_rwunlock(state->lock);
return enif_make_atom(env, "ok");
}
static ERL_NIF_TERM size_1(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
size_t size = 0;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
return raise(env, EINVAL);
enif_rwlock_rlock(state->lock);
tree_size(state->tree, &size);
enif_rwlock_runlock(state->lock);
return enif_make_uint64(env, (ErlNifUInt64) size);
}
static ERL_NIF_TERM is_empty_1(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
return raise(env, EINVAL);
enif_rwlock_rlock(state->lock);
char *ret = state->tree->sub ? "false" : "true";
enif_rwlock_runlock(state->lock);
return enif_make_atom(env, ret);
}
static ERL_NIF_TERM to_list_1(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
ERL_NIF_TERM result = enif_make_list(env, 0);
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
return raise(env, EINVAL);
enif_rwlock_rlock(state->lock);
to_list(env, state->tree, &result);
enif_rwlock_runlock(state->lock);
return result;
}
static ERL_NIF_TERM dump_1(ErlNifEnv* env, int argc,
const ERL_NIF_TERM argv[])
{
state_t *state;
if (!enif_get_resource(env, argv[0], tree_state_t, (void *) &state))
return raise(env, EINVAL);
enif_rwlock_rlock(state->lock);
ERL_NIF_TERM result = dump(env, state->tree);
enif_rwlock_runlock(state->lock);
return result;
}
static ErlNifFunc nif_funcs[] =
{
{"new", 0, new_0},
{"insert", 2, insert_2},
{"delete", 2, delete_2},
{"match", 2, match_2},
{"refc", 2, refc_2},
{"clear", 1, clear_1},
{"size", 1, size_1},
{"is_empty", 1, is_empty_1},
{"to_list", 1, to_list_1},
{"dump", 1, dump_1}
};
ERL_NIF_INIT(mqtree, nif_funcs, load, NULL, NULL, unload)

BIN
rebar vendored

Binary file not shown.

50
rebar.config

@ -0,0 +1,50 @@
%%%-------------------------------------------------------------------
%%%
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne
%%%
%%% The ejabberd software is the exclusive property of the licensor
%%% ProcessOne. It is protected by the law on copyright and
%%% international conventions. As a result, the dealer
%%% recognizes that will make every effort to ensure the confidentiality
%%% on the software. It is recalled that a violation of the rights of
%%% authors of the software is an infringement and that any
%%% counterfeit is punishable in France by Article L339-2 of the Code of
%%% Intellectual property and punishable by three years imprisonment and
%%% 300000 euros.
%%%
%%% Any infringement liable to be so qualified and
%%% would be caused by third parties and whose dealer has knowledge
%%% should be terminated by the licensor that it will make its case
%%% personal conduct of the proceedings. Any allegation of infringement
%%% formed against the dealer because of the use of the Software will
%%% be brought to the knowledge of the licensor which will assist
%%% in defense of the dealer in the manner and form that
%%% see fit and fix alone.
%%%
%%%----------------------------------------------------------------------
{erl_opts, [debug_info, {src_dirs, ["src"]}]}.
{port_env, [{"CFLAGS", "$CFLAGS -g -O2 -Wall -I deps/uthash/src"},
{"LDFLAGS", "$LDFLAGS -lpthread"}]}.
{port_specs, [{"priv/lib/mqtree.so", ["c_src/mqtree.c"]}]}.
{deps, [{p1_utils, ".*", {git, "https://github.com/processone/p1_utils",
"6ff85e8"}},
{uthash, ".*", {git, "https://github.com/troydhanson/uthash.git",
{tag, "v2.0.2"}}, [raw]}]}.
{clean_files, ["c_src/mqtree.gcda", "c_src/mqtree.gcno"]}.
{cover_enabled, true}.
{cover_export_enabled, true}.
{xref_checks, [undefined_function_calls, undefined_functions,
deprecated_function_calls, deprecated_functions]}.
{profiles, [{test, [{erl_opts, [{src_dirs, ["test"]}]}]}]}.
%% Local Variables:
%% mode: erlang
%% End:
%% vim: set filetype=erlang tabstop=8:

37
src/mqtree.app.src

@ -0,0 +1,37 @@
%%%-------------------------------------------------------------------
%%%
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne
%%%
%%% The ejabberd software is the exclusive property of the licensor
%%% ProcessOne. It is protected by the law on copyright and
%%% international conventions. As a result, the dealer
%%% recognizes that will make every effort to ensure the confidentiality
%%% on the software. It is recalled that a violation of the rights of
%%% authors of the software is an infringement and that any
%%% counterfeit is punishable in France by Article L339-2 of the Code of
%%% Intellectual property and punishable by three years imprisonment and
%%% 300000 euros.
%%%
%%% Any infringement liable to be so qualified and
%%% would be caused by third parties and whose dealer has knowledge
%%% should be terminated by the licensor that it will make its case
%%% personal conduct of the proceedings. Any allegation of infringement
%%% formed against the dealer because of the use of the Software will
%%% be brought to the knowledge of the licensor which will assist
%%% in defense of the dealer in the manner and form that
%%% see fit and fix alone.
%%%
%%%----------------------------------------------------------------------
{application, mqtree,
[
{description, "Index tree for MQTT topic filters"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib]},
{env, []}
]}.
%% Local Variables:
%% mode: erlang
%% End:
%% vim: set filetype=erlang tabstop=8:

99
src/mqtree.erl

@ -0,0 +1,99 @@
%%%-------------------------------------------------------------------
%%%
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne
%%%
%%% The ejabberd software is the exclusive property of the licensor
%%% ProcessOne. It is protected by the law on copyright and
%%% international conventions. As a result, the dealer
%%% recognizes that will make every effort to ensure the confidentiality
%%% on the software. It is recalled that a violation of the rights of
%%% authors of the software is an infringement and that any
%%% counterfeit is punishable in France by Article L339-2 of the Code of
%%% Intellectual property and punishable by three years imprisonment and
%%% 300000 euros.
%%%
%%% Any infringement liable to be so qualified and
%%% would be caused by third parties and whose dealer has knowledge
%%% should be terminated by the licensor that it will make its case
%%% personal conduct of the proceedings. Any allegation of infringement
%%% formed against the dealer because of the use of the Software will
%%% be brought to the knowledge of the licensor which will assist
%%% in defense of the dealer in the manner and form that
%%% see fit and fix alone.
%%%
%%%----------------------------------------------------------------------
-module(mqtree).
-on_load(load_nif/0).
%% API
-export([new/0, insert/2, delete/2, match/2, refc/2,
clear/1, size/1, is_empty/1]).
%% For debugging
-export([dump/1, to_list/1]).
-type path() :: iodata().
-opaque tree() :: reference().
-export_type([tree/0, path/0]).
%%%===================================================================
%%% API
%%%===================================================================
-spec new() -> tree().
new() ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec insert(tree(), path()) -> ok.
insert(_Tree, _Path) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec delete(tree(), path()) -> ok.
delete(_Tree, _Path) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec match(tree(), path()) -> [binary()].
match(_Tree, _Path) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec refc(tree(), path()) -> non_neg_integer().
refc(_Tree, _Path) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec clear(tree()) -> ok.
clear(_Tree) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec size(tree()) -> non_neg_integer().
size(_Tree) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec is_empty(tree()) -> boolean().
is_empty(_Tree) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
%%%===================================================================
%%% For testing/debugging
%%%===================================================================
-type tree_node() :: {string(), string() | none,
non_neg_integer(), [tree_node()]}.
-spec dump(tree()) -> [tree_node()].
dump(_Tree) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
-spec to_list(tree()) -> [{binary(), non_neg_integer()}].
to_list(_Tree) ->
erlang:nif_error({nif_not_loaded, ?MODULE}).
%%%===================================================================
%%% Internal functions
%%%===================================================================
load_nif() ->
Path = p1_nif_utils:get_so_path(?MODULE, [?MODULE], atom_to_list(?MODULE)),
case erlang:load_nif(Path, 0) of
ok -> ok;
{error, {upgrade, _}} -> ok;
{error, {Reason, Text}} ->
error_logger:error_msg("Failed to load NIF ~s: ~s (~p)",
[Path, Text, Reason]),
erlang:nif_error(Reason)
end.

244
test/mqtree_test.erl

@ -0,0 +1,244 @@
%%%-------------------------------------------------------------------
%%%
%%% ejabberd Business Edition, Copyright (C) 2002-2018 ProcessOne
%%%
%%% The ejabberd software is the exclusive property of the licensor
%%% ProcessOne. It is protected by the law on copyright and
%%% international conventions. As a result, the dealer
%%% recognizes that will make every effort to ensure the confidentiality
%%% on the software. It is recalled that a violation of the rights of
%%% authors of the software is an infringement and that any
%%% counterfeit is punishable in France by Article L339-2 of the Code of
%%% Intellectual property and punishable by three years imprisonment and
%%% 300000 euros.
%%%
%%% Any infringement liable to be so qualified and
%%% would be caused by third parties and whose dealer has knowledge
%%% should be terminated by the licensor that it will make its case
%%% personal conduct of the proceedings. Any allegation of infringement
%%% formed against the dealer because of the use of the Software will
%%% be brought to the knowledge of the licensor which will assist
%%% in defense of the dealer in the manner and form that
%%% see fit and fix alone.
%%%
%%%----------------------------------------------------------------------
-module(mqtree_test).
-include_lib("eunit/include/eunit.hrl").
-define(assertTree(L),
case L of
[] ->
?assertEqual([], mqtree:dump(T)),
?assertEqual([], mqtree:to_list(T));
_ ->
?assertEqual(L, lists:sort(mqtree:to_list(T)))
end).
-define(assertInsert(E),
?assertEqual(ok, mqtree:insert(T, E))).
-define(assertDelete(E),
?assertEqual(ok, mqtree:delete(T, E))).
%%%===================================================================
%%% Tests
%%%===================================================================
new_test() ->
T = mqtree:new(),
?assertTree([]).
insert_test() ->
T = mqtree:new(),
Path = <<"/a/b/c">>,
?assertInsert(Path),
?assertTree([{Path, 1}]).
is_empty_test() ->
T = mqtree:new(),
?assert(mqtree:is_empty(T)),
?assertInsert(<<"/">>),
?assert(not mqtree:is_empty(T)).
insert_then_delete_test() ->
T = mqtree:new(),
Path = <<"a/b">>,
?assertInsert(Path),
?assertDelete(Path),
?assertTree([]).
insert_empty_then_delete_empty_test() ->
T = mqtree:new(),
?assertInsert(<<>>),
?assertTree([]),
?assertDelete(<<>>),
?assertTree([]).
insert_then_delete_empty_test() ->
T = mqtree:new(),
Path = <<"/a/b">>,
?assertInsert(Path),
?assertTree([{Path, 1}]),
?assertDelete(<<>>),
?assertTree([{Path, 1}]).
insert_then_delete_shuffle_test() ->
T = mqtree:new(),
Check = lists:sort(rand_paths()),
lists:foldl(
fun(insert, Refc) ->
lists:foreach(
fun(Path) -> ?assertInsert(Path) end,
rand_paths()),
Refc1 = Refc+1,
?assertTree([{P, Refc1} || P <- Check]),
Refc1;
(delete, Refc) ->
lists:foreach(
fun(Path) -> ?assertDelete(Path) end,
rand_paths()),
Refc1 = Refc-1,
case Refc1 of
0 ->
?assertTree([]);
_ ->
?assertTree([{P, Refc1} || P <- Check])
end,
Refc1
end, 0, rand_funs()).
refc_test() ->
T = mqtree:new(),
lists:foreach(
fun(Refc) ->
lists:foreach(
fun(P) ->
?assertEqual(Refc, mqtree:refc(T, P)),
?assertInsert(P)
end, rand_paths())
end, lists:seq(0, 5)),
lists:foreach(
fun(Refc) ->
lists:foreach(
fun(P) ->
?assertDelete(P),
?assertEqual(Refc, mqtree:refc(T, P))
end, rand_paths())
end, lists:seq(5, 0, -1)).
clear_test() ->
T = mqtree:new(),
lists:foreach(
fun(_) ->
lists:foreach(fun(P) -> ?assertInsert(P) end, rand_paths()),
?assertEqual(ok, mqtree:clear(T)),
?assertTree([])
end, lists:seq(1, 10)).
clear_empty_test() ->
T = mqtree:new(),
?assertEqual(ok, mqtree:clear(T)),
?assertTree([]).
size_test() ->
T = mqtree:new(),
?assertEqual(0, mqtree:size(T)),
Paths = rand_paths(),
lists:foreach(
fun(_) ->
lists:foreach(fun(P) -> ?assertInsert(P) end, rand_paths()),
?assert(mqtree:size(T) == length(Paths))
end, [1,2,3]),
?assertEqual(ok, mqtree:clear(T)),
?assertEqual(0, mqtree:size(T)).
delete_non_existent_test() ->
T = mqtree:new(),
lists:foreach(
fun(_) ->
lists:foreach(fun(P) -> ?assertDelete(P) end, rand_paths()),
?assertTree([])
end, lists:seq(1, 10)).
insert_then_delete_non_existent_test() ->
T = mqtree:new(),
Inserts = rand_paths("@$%&*"),
Check = [{P, 1} || P <- lists:sort(Inserts)],
lists:foreach(fun(P) -> ?assertInsert(P) end, Inserts),
lists:foreach(
fun(_) ->
lists:foreach(fun(P) -> ?assertDelete(P) end, rand_paths()),
?assertTree(Check)
end, lists:seq(1, 10)).
match_all_test() ->
T = mqtree:new(),
lists:foreach(
fun(_) ->
?assertInsert("#"),
lists:foreach(
fun(P) ->
?assertEqual([<<"#">>], mqtree:match(T, P))
end, rand_paths())
end, lists:seq(1, 10)).
match_none_test() ->
T = mqtree:new(),
lists:foreach(
fun(P) ->
?assertEqual([], mqtree:match(T, P))
end, rand_paths()).
match_exact_test() ->
T = mqtree:new(),
lists:foreach(fun(P) -> ?assertInsert(P) end, rand_paths()),
lists:foreach(
fun(P) ->
?assertEqual([P], mqtree:match(T, P))
end, rand_paths()).
match_tail_test() ->
T = mqtree:new(),
Filter = <<"a/b/#">>,
?assertInsert(Filter),
?assertEqual([], mqtree:match(T, "a/bc")),
?assertEqual([Filter], mqtree:match(T, "a/b")),
?assertEqual([Filter], mqtree:match(T, "a/b/")),
?assertEqual([Filter], mqtree:match(T, "a/b/c")),
?assertEqual([Filter], mqtree:match(T, "a/b/c/d")).
match_plus_test() ->
T = mqtree:new(),
Filter = lists:sort([<<A, $/, B>> || A<-"+a", B<-"+b"]),
lists:foreach(fun(P) -> ?assertInsert(P) end, Filter),
?assertEqual([<<"+/+">>], mqtree:match(T, "/")),
?assertEqual([<<"+/+">>], mqtree:match(T, "x/")),
?assertEqual([<<"+/+">>], mqtree:match(T, "/y")),
?assertEqual([<<"+/+">>], mqtree:match(T, "x/y")),
?assertEqual([<<"+/+">>, <<"a/+">>], mqtree:match(T, "a/")),
?assertEqual([<<"+/+">>, <<"a/+">>], mqtree:match(T, "a/y")),
?assertEqual([<<"+/+">>, <<"+/b">>], mqtree:match(T, "/b")),
?assertEqual([<<"+/+">>, <<"+/b">>], mqtree:match(T, "x/b")),
?assertEqual(Filter, lists:sort(mqtree:match(T, "a/b"))).
%%%===================================================================
%%% Internal functions
%%%===================================================================
rand_paths() ->
rand_paths("/abcd").
rand_paths(Set) ->
L1 = [{p1_rand:uniform(), <<A>>} || A<-Set],
L2 = [{p1_rand:uniform(), <<A,B>>} || A<-Set, B<-Set],
L3 = [{p1_rand:uniform(), <<A,B,C>>} || A<-Set, B<-Set, C<-Set],
L4 = [{p1_rand:uniform(), <<A,B,C,D>>} || A<-Set, B<-Set, C<-Set, D<-Set],
L5 = [{p1_rand:uniform(), <<A,B,C,D,E>>} || A<-Set, B<-Set, C<-Set, D<-Set, E<-Set],
[Path || {_, Path} <- lists:keysort(1, L1++L2++L3++L4++L5)].
rand_funs() ->
lists:flatmap(
fun(_) ->
I = p1_rand:uniform(5),
Inserts = lists:duplicate(I, insert),
Deletes = lists:duplicate(I, delete),
Inserts ++ Deletes
end, [1,2,3,4,5]).
Loading…
Cancel
Save