Kazoo FixtureDB
What can you say about a data application? That it loves to be distributed and fault tolerant? That it listens to data streams and not understand a bit?
Overview
General speaking, FixtureDB removes dependency of Kazoo Data Manager to CouchDB and RabbitMQ for running EUnit tests and removes the burden of isolating source codes with -ifdef(TEST)
when it comes to reading from database, which in part result in a tiny little bit more coverage (if any) by running through all Kazoo Data Manager code.
FixtureDB acts as a central repository for all fixtures which we simply use during tests.
When writing your EUnit test you can use fixture test representation to setup a your test environment by starting a dummy connection to FixtureDB, and cleanup the connection later.
Below is an example with setup
and cleanup
method. In setup()
function we’re starting kazoo_config
applications to read the default config-test.ini
and then starts the Kazoo data link supervisor. After these steps most of the calls to kz_datamgr
functions will use FixtureDB (few operations, like view maintenance cleanup, are not implemented).
Example EUnit Test Using Setup/Cleanup Method
-spec render_test_() -> any().
render_test_() ->
{setup
,fun setup/0
,fun cleanup/1
,fun(_ReturnOfSetup) ->
[{"your awesome test"
,?_assertEqual(good, evil)
}
]
end
}.
setup() ->
?LOG_DEBUG(":: Setting up Kazoo FixtureDB"),
{ok, _} = application:ensure_all_started(kazoo_config),
{ok, LinkPid} = kazoo_data_link_sup:start_link(),
LinkPid.
cleanup(LinkPid) ->
_ = erlang:exit(LinkPid, normal),
_ = application:stop(kazoo_config),
?LOG_DEBUG(":: Stopped Kazoo FixtureDB").
Database File Structure
All of the fixtures are reside in core/kazoo_fixturedb/priv/dbs. In dbs
directory, each sub-directory is represented a database. For example dbs/accounts
is represented accounts
database. Inside these database directories, there are sub-directories to store the JSON document for this database (in docs
directory) and CouchDB view result (in views
directory):
$ tree core/kazoo_fixturedb/priv/dbs/
core/kazoo_fixturedb/priv/dbs/
...
├── accounts
│ └── docs
│ ├── account0000000000000000000000001.json
│ ├── account0000000000000000000000002.json
│ └── account0000000000000000000000003.json
└── services
├── docs
│ ├── account0000000000000000000000001.json
│ ├── account0000000000000000000000002.json
│ └── account0000000000000000000000003.json
├── view-index.csv
└── views
└── services+cascade_quantities-dffd4eaab58007e68e839f620b80d77c.json
...
Attachments reside in docs
directory
Document attachments are stored in docs
directory. Their file names are encoded by the document ID and attachment name, (see Document Attachments section). Attachments are usually audio files, PDFs or pictures. FixtureDB has some default files in priv/media_files
. You can simply use them by creating a symbolic link to them.
View Results Directory
View results are exactly the same result sets which kz_datamgr:get_results/2,3
returns. It is your responsibility to write the correct view result structure and in the proper sort order. Complex queries have their own unique filename which is explained in the View Results section.
Using FixtureDB
FixtureDB acts as database driver for kazoo_data
, so you need to start kazoo_config
first to read the database configuration, then start kazoo_data
connection link by running kazoo_data_link_sup:start_link()
to bring up Kazoo data connection ETS table. kazoo_data_link_sup
is a lite version of kazoo_data_sup
which does not depend on kazoo_amqp
and tracing capability to be available; its just making a new connection to a database (which in this case is FixtureDB).
OS environment variable KAZOO_CONFIG
is necessary for kazoo_config
to read the correct FixtureDB database configuration and by default is set to $(ROOT)/rel/config-test.ini
in kz.mk
file for test
, eunit
, proper
and fixture_db
targets.
In tests which access the database, you have to bring up kazoo_config
and kazoo_data_link_sup
:
{'ok', _} = application:ensure_all_started('kazoo_config').
{'ok', _Pid} = kazoo_data_link_sup:start_link().
NOTE: Don’t forget to stop the
kazoo_config
andkazoo_data_link_sup
after your test is finished (see EUnit test example above), otherwise your test will fail because thekazoo_config
andkazoo_data_link_sup
are not shut down normally!
Database
Each database has a directory of their own inside core/kazoo_fixturedb/priv/dbs/
.
NOTE: Database name in URL encoded, e.g.
account/xxxx
becomesaccount%2Fac%2Fco%2Funtxxxx
.
In each database there are two directories: docs
for storing regular JSON documents inside the database and views
for storing view results.
To make it easy to map each attachment or complex view to their corresponding files in the docs
and views
directories, there are two CSV index files at the root of the database directory.
Fixture JSON Documents
Documents are the exact copy of a real CouchDB documents. Document’s filename is the _id
of the document with .json
extension in the end.
NOTE: The
_id
in the filename is URL encoded, e.g._design/services
becomes_design%2Fservices.json
.
You can use kz_fixturedb_util:get_doc_path/2,3
to get document’s file path.
Saving a document is not really saving the JObj in a file, instead it just returned an updated JObj with bumped _rev
and updated pvt_document_hash
. If database (the database folder) does not exists it returns error {'error', 'not_found'}
. If you try to save a document which exists in the docs
without correct _rev
then the result is {'error', 'conflict'}
as expected.
Delete document(s) is result in a bulk save in Couchbeam, so it returns a bulk save result:
(fixturedb@hes.2600hz.com)12> kz_datamgr:del_docs(<<"accounts">>, [<<"file_not_exists">>, <<"account0000000000000000000000002">>]).
{ok,[{[{<<"ok">>,true},
{<<"id">>,<<"file_not_exists">>},
{<<"rev">>,<<"1-a91148dc2361d2ebba6278fc1352bedd">>},
{<<"accepted">>,true}]},
{[{<<"ok">>,true},
{<<"id">>,<<"account0000000000000000000000002">>},
{<<"rev">>,<<"744-81cd18e2bd386c5b3fcd6df4e731d596">>},
{<<"accepted">>,true}]}]}
Utility Functions
If you change an existing JSON file manually and need to update pvt_document_hash
you can use kz_fixturedb_util:update_pvt_doc_hash(Path)
. This is necessary for example in Teletype notification templates in system_config
database since during initializing a template, teletype checks if document hash is changed or not so it does not update template which is customized directly in database.
To update all JSON files document hash in FixtureDB use kz_fixturedb_util:update_pvt_doc_hash/0
.
Document Attachments
Each document’s attachment is saved in database docs
directory in a separate file. Attachment’s filename is the MD5 hash of {DOC_ID}
and {ATTACHMENT_NAME}
ending in .att
. Use kz_fixturedb_util:get_att_path/3,4
to get the file path:
(fixturedb@hes.2600hz.com)13> kz_fixturedb_util:get_att_path(<<"account%2Fac%2Fco%2Funt0000000000000000000000003-201710">>, <<"201710-vm_message0000000000000000000001">>, <<"vm-new_message.mp3">>).
"/opt/kazoo/core/kazoo_fixturedb/priv/dbs/account%2Fac%2Fco%2Funt0000000000000000000000003-201710/docs/5cef38cd0260632cf119c6fcf0c44f58.att"
For easily understand which file is referring which doc_id+attachment_name
hash, there is an index file (attachment-index.csv
) in the root of each database directory:
$ cat priv/dbs/account%2Fac%2Fco%2Funt0000000000000000000000003-201710/attachment-index.csv
doc_id, attachment_name, attachment_file_name
201710-vm_message0000000000000000000001, vm-new_message.wav, 5cef38cd0260632cf119c6fcf0c44f58.att
Putting an attachment is just returning the updated document JObj back. Deleting an attachment is returning tuple {'ok', JObj}
which the JObj just have the id
and rev
. Both put and delete will return error tuple if the JSON document file does not exists in database.
For large attachments like voicemail or a fax, use the files from core/kazoo_fixturedb/priv/media_files
(or if it does not exists add one). And then soft link it in the destination database. For example in Unix shell if you’re already at code/kazoo_fixturedb
directory:
kazoo_fixturedb $ ls -l priv/dbs/account%2Fac%2Fco%2Funt0000000000000000000000003-201710/docs/
lrwxrwxrwx 1 hehe hehe 39 Oct 28 17:45 5cef38cd0260632cf119c6fcf0c44f58.att -> ../../../media_files/vm-new_message.mp3
Utility Functions
kz_fixturedb_util:get_att_path(DbName, DocId, AName)
: you can use this to get the attachment file path and inspect the hashed filenamekz_fixturedb_util:add_att_to_index(DbName, DocId, AName)
: will createdoc_id+attachment_name
hash and add it to theattachment-index.csv
file and make a symbolic link to a file incore/kazoo_fixturedb/priv/media_files
if theattachment_name
matches want of the file in that directory.
Advanced Usage: To use a customized data plan you can use arity 4 of above function (as first argument).
View Results
FixtureDB uses a new cutting edge advanced high performance technique called Human® to compute the CouchDB view’s result. Same like attachments, there is an index file for views at view-index.csv
in each database root directory to help you keep track of filename and query.
All view result are in the views
database’s directory. Since views are tricky when it comes to reduce
, group_level
, startkey
and etc…, every time FixtureDB sees these view options, it hashes the view options and will add it to the file name. For example:
(fixturedb@hes.2600hz.com)17> kz_fixturedb_util:get_view_path(<<"account%2Fac%2Fco%2Funt0000000000000000000000001">>, <<"users/crossbar_listing">>, []).
"/opt/kazoo/core/kazoo_fixturedb/priv/dbs/account%2Fac%2Fco%2Funt0000000000000000000000001/views/users+crossbar_listing.json"
As you can see above when there is no complex view options (in this example an empty options []
) the filename is simply reflecting the view name. Here is an example with a complex view options:
(fixturedb@hes.2600hz.com)18> kz_fixturedb_util:get_view_path(<<"account%2Fac%2Fco%2Funt0000000000000000000000001">>, <<"users/crossbar_listing">>, [{key,<<"user">>},include_docs]).
"/opt/kazoo/core/kazoo_fixturedb/priv/dbs/account%2Fac%2Fco%2Funt0000000000000000000000001/views/users+crossbar_listing-ba9ee0020a95e519332f16eacf0a3324.json"
The list of view options which will result in creating a hashed file name are defined in kz_fixturedb.hrl
. Current options are:
-define(DANGEROUS_VIEW_OPTS, [startkey, endkey, key
,keys, group, group_level
,reduce, list, skip
]).
Utility Functions
kz_fixturedb_util:get_view_path(DbName, Design, Options)
: get file path for your view query. After adding a view result toviews
directory usekz_fixturedb_util:add_view_to_index(DbName, Design, Options)
: adds the view with options to the index file
Advanced Usage: To use a customized data plan you can use arity 4 of above function (as first argument).
Note about include_docs
If include_docs
is set, you don’t need to include docs in your view result, kz_fixturedb_view
will add each document to the result set based on each result keys. So you can instead add the document in database doc
directory.
Note about limit
and descending
As a rule write your view result in the ascending
order. Again kz_fixturedb_view
is reversing the the result if descending
is set.
This only works if your query does not have reduce
, group
or group_level
view options.
This includes limit
too, if your test case is not have a complex view options (it doesn’t have any of ?DANGEROUS_VIEW_OPTS
) you have to include all view results in the file kz_fixturedb_view
keep taking care of splitting the result set.
Note about ?DANGEROUS_VIEW_OPTS
You as a test designer must have a complete scenario of your queries that your test needs. If your query is using startkey
, endkey
, keys
, key
reduce
, group*
or etc… make a rough plan of what each query is and kz_fixturedb_util:get_view_path/3,4
then create the view result file for them.
How to Get the Data Plan
It’s simple:
SystemPlan = kzs_plan:plan().
PlanForThisDb = kzs_plan:plan(SomeDb).
Have Database in Other Application Directory (Experimental)
Note: currently there is no way to have update the
kz_dataconnection
to use this feature.
FixtureDB has an experimental way to read database from other path than the default core/kazoo_fixturedb/priv/dbs
path. It could be useful if you want to test an applications which maybe requires a lot of document and you want to put them in the default path.
For this to work you have to setup a new connection with these options set
#{test_app => atom() %% required, is the name app of which you're writing test
,test_db => ne_binary() %% required, the name of your database which you want fixturedb read from
,test_db_subdir => atom() %% optional, the path to where is the root dbs directory, default is priv/ folder of your app
}
The exact way how to make a new connection this is remain un-discovered :) But it works for every database (including system_data, system_config and services).
Better option is use data plan (although you may loose the ability to use those three databases in this way). This way is un-discovered too.
FixtureDB Shell Target
There is a target in the root Makefile
and kz.mk
for start a shell with all core, applications and deps in path and has KAZOO_CONFIG
set, so you can easily run kz_fixturedb_util
functions where ever you’re in Kazoo project path.
kazoo $ make fixture_shell
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V8.3 (abort with ^G)
(fixturedb@hes.2600hz.com)1> application:ensure_all_started(kazoo_config)
16:36:34.542 [info] Application lager started on node 'fixturedb@hes.2600hz.com'
16:36:34.542 [info] Application kazoo started on node 'fixturedb@hes.2600hz.com'
16:36:34.544 [info] Application zucchini started on node 'fixturedb@hes.2600hz.com'
16:36:34.587 [info] loaded configs from file /home/hesaam/work/2600hz/kazoo/rel/config-test.ini
16:36:34.588 [notice] loaded settings : [{amqp,[{uri,"amqp://guest:guest@127.0.0.1:5672"}]},{data,[{config,kazoo_fixturedb}]},{kazoo_fixturedb,[{driver,kazoo_fixturedb},{tag,local}]},{kazoo_apps,[{cookie,change_me}]},{ecallmgr,[{cookie,change_me}]},{log,[{syslog,none},{console,none},{file,none}]}]
16:36:34.615 [notice] setting zone to local
16:36:34.616 [info] Application kazoo_config started on node 'fixturedb@hes.2600hz.com'
{ok,[crypto,inet_cidr,observer,syntax_tools,compiler,
goldrush,lager,kazoo,zucchini,kazoo_config]}
(fixturedb@hes.2600hz.com)2>
(fixturedb@hes.2600hz.com)2> kazoo_data_link_sup:start_link().
16:36:46.073 [info] Application asn1 started on node 'fixturedb@hes.2600hz.com'
16:36:46.073 [info] Application public_key started on node 'fixturedb@hes.2600hz.com'
16:36:46.089 [info] Application kazoo_fixturedb started on node 'fixturedb@hes.2600hz.com'
16:36:46.126 [info] waiting for first connection...
16:36:46.126 [info] adding connection
16:36:46.142 [info] start connection
16:36:46.142 [info] trying to connect kazoo_fixturedb
16:36:46.365 [info] connected to local: {"kazoo":"Willkommen","version":"0.0.0.0.0.0.0.1","features":["cool"],"vendor":{"name":"the Great FixtureDB Committee"}}
{ok,<0.117.0>}
(fixturedb@hes.2600hz.com)3>