Skip to content
Snippets Groups Projects
Commit 0a7ddac9 authored by Timm Fitschen's avatar Timm Fitschen
Browse files

Merge branch 'f-full' into 'dev'

ENH: Full functionality

See merge request !3
parents 40e49173 3e02dd4d
No related branches found
No related tags found
1 merge request!3ENH: Full functionality
Pipeline #14785 failed
......@@ -5,7 +5,7 @@ services:
environment:
CAOSDB_SERVER_HOST: caosdb-server
CAOSDB_SERVER_CERT: /cert/caosdb.cert.pem
CAOSDB_CLIENT_CONFIGURATION: /caosdb-client.json
CAOSDB_CLIENT_CONFIGURATION: /caosdb_client.json
networks:
- docker_caosnet
volumes:
......
......
......@@ -23,4 +23,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ###
- Tests for error handling, Entity retrieval and query execution.
- Tests for everything in caosdb-octavelib.
......@@ -25,6 +25,8 @@
help:
@echo "Targets:"
@echo " test - run the test."
@echo " style - check code styling."
@echo " style_fix - fix code styling."
###############################################################################
# Style and linting #
......
......
......@@ -19,7 +19,9 @@
pkg load caosdb;
all_tests = true;
all_tests &= moxunit_runtests("-verbose", "test_caosdb.m");
all_tests &= moxunit_runtests("-verbose", "test_query_retrieve.m");
all_tests &= moxunit_runtests("-verbose", "test_transaction.m");
all_tests &= moxunit_runtests("-verbose", "test_files.m");
if not(all_tests)
exit(1);
......
......
% This file is a part of the CaosDB Project.
%
% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com>
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU Affero General Public License as
% published by the Free Software Foundation, either version 3 of the
% License, or (at your option) any later version.
%
% This program 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 Affero General Public License for more details.
%
% You should have received a copy of the GNU Affero General Public License
% along with this program. If not, see <https://www.gnu.org/licenses/>.
%% Clean up CaosDB (remove everything with ID > 99)
function cleanup()
c = Caosdb();
ids = caosdb_get_ids(c.query('FIND ENTITY WITH ID > 99'));
result = c.delete_by_id(ids);
end
% This file is a part of the CaosDB Project.
%
% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com>
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU Affero General Public License as
% published by the Free Software Foundation, either version 3 of the
% License, or (at your option) any later version.
%
% This program 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 Affero General Public License for more details.
%
% You should have received a copy of the GNU Affero General Public License
% along with this program. If not, see <https://www.gnu.org/licenses/>.
%% The main function which intitializes the tests.
function test_suite = test_files()
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions();
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
end
% Create files with nearly random content.
function filenames = create_testfiles(n)
seed = 20210831;
rand("state", seed);
filenames = {};
for i = [1:n]
[fid, filename, msg] = mkstemp ("testfile_XXXXXX", true);
if fid == -1
error(["Error while creating temporary file:\n ", msg]);
end
% TODO (dh) make bigger data (at least 100x100) again after the C++ library fix.
data = rand([10, 10]);
save("-hdf5", filename, "data");
filenames{i} = filename;
end
end
function test_insert_file()
cleanup();
filenames = create_testfiles(1);
c = Caosdb();
% Create file
F1 = Entity();
F1.role = "FILE";
F1.name = "file_1";
F1.filepath = "testfile1.xyz";
F1.localpath = filenames{1};
% upload file
insert = c.insert({F1}){1};
assertFalse(insert.has_errors(), print_messages(insert.get_errors(), "error"));
assertTrue(insert.has_warnings(), print_messages(insert.get_warnings(), "warning"));
assertFalse(insert.has_infos(), print_messages(insert.get_infos(), "info"));
assertEqual(numel(insert), 1);
F1_id = insert.id;
% query for file
q_result = c.query(["COUNT FILE WHICH IS STORED AT '", F1.filepath, "'"]);
assertEqual(q_result, int64(1));
% download file and compare
[fid, outfilename_1, msg] = mkstemp ("testfile_out_XXXXXX", true);
if fid == -1
error(["Error while creating temporary file:\n ", msg]);
end
download_result = c.download_file_by_single_id(F1_id, outfilename_1);
fid_orig = fopen(filenames{1}, "r");
fid_retr = fopen(outfilename_1, "r");
[data_orig, count_orig] = fread(fid_orig, Inf, "*uint8");
count_retr = count_orig + 1; % try to read up to one byte more
[data_retr, count_retr] = fread(fid_retr, count_retr, "*uint8");
fclose(fid_orig);
fclose(fid_retr);
assertEqual(count_retr, count_orig, "Downloaded and original file have different sizes.");
assertEqual(data_orig, data_retr, "Downloaded and original file have different content.");
% cleanup();
end
% Utility functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% print the messages into a cell
function result = print_messages(message_cell, log_name)
result = "";
if nargin > 1 && ~isempty(log_name)
result = ["[ ", log_name, " ] "];
end
for msg = message_cell
msg = msg{1};
result = [result, "\n ", msg.str()];
end
end
......@@ -17,7 +17,7 @@
% along with this program. If not, see <https://www.gnu.org/licenses/>.
%% The main function which intitializes the tests.
function test_suite = test_caosdb()
function test_suite = test_query_retrieve()
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions();
catch % no problem; early Matlab versions can use initTestSuite fine
......@@ -25,8 +25,15 @@ function test_suite = test_caosdb()
initTestSuite;
end
% local function, instead of lambda function
function info = non_existing_connection()
c = Caosdb("does-not-exist");
info = c.info();
end
%% Only test the connection
function test_connection()
% Default connection
c1 = Caosdb();
info = c1.info();
......@@ -37,6 +44,7 @@ function test_connection()
% This connection should fail
% c3 = Caosdb(connection = "does-not-exist");
% c3.info();
% Error message is:
% maox_info: The ConnectionManager does not know any connection of this name.
% No connection named 'does-not-exist' present.
......@@ -51,77 +59,92 @@ function test_connection()
assertEqual(lasterror().message, expected_msg);
end
function info = non_existing_connection()
c = Caosdb("does-not-exist");
info = c.info();
end
%% Test retrieval of a single Entity
function test_retrieve_single()
% Default connection configuration is sufficient.
cleanup();
c = Caosdb();
% Plain property must not fail.
plain_property = c.retrieve_by_id("101"){1};
P1 = Entity();
P1.role = "PROPERTY";
P1.name = "Prop 1";
P1.set_datatype("DOUBLE");
% Retrieve a single entity
violin = c.retrieve_by_id("120"){1};
P1_id = c.insert({P1}){1}.id;
assertTrue(numel(P1_id) > 0);
% Check content
assertEqual(violin.name, "Sherlock Holmes' violin");
props = violin.get_properties();
assertEqual(props{1}.name, "price");
assertEqual(props{1}.value, 814873.0);
assertEqual(props{2}.name, "Manufacturer");
assertEqual(props{2}.value, "119");
retrieved = c.retrieve_by_id(P1_id);
assertEqual(numel(retrieved), 1);
P1_retrieved = retrieved{1};
assertFalse(violin.has_errors());
assertFalse(violin.has_warnings());
assertFalse(violin.has_infos());
assertFalse(P1_retrieved.has_errors());
assertEqual(P1_retrieved.role, P1.role);
assertEqual(P1_retrieved.name, P1.name);
dtype_orig = P1.get_datatype();
dtype_retrieved = P1_retrieved.get_datatype();
assertEqual(dtype_retrieved.dtypeName, dtype_orig.dtypeName);
assertEqual(dtype_retrieved.isList, dtype_orig.isList);
assertEqual(dtype_retrieved.isReference, dtype_orig.isReference);
cleanup();
end
function test_retrieve_nonexisting()
cleanup();
c = Caosdb();
retrieved = c.retrieve_by_id("does_not_exist");
assertEqual(numel(retrieved), 1);
retrieved = retrieved{1};
assertTrue(retrieved.has_errors());
cleanup();
end
%% Test retrieval of multiple Entities
function test_retrieve_multiple()
% Retrieve two entities
cleanup();
ids = insert_data();
c = Caosdb();
violin = c.retrieve_by_id("120"){1};
collection = c.retrieve_by_id({"120", "119"});
retrieved = c.retrieve_by_id(ids.P1){1};
collection = c.retrieve_by_id({ids.P1, ids.P2});
assertEqual(length(collection), 2);
assertEqual(numel(collection), 2);
assertEqual(collection{1}.id, violin.id);
assertEqual(collection{1}.name, violin.name);
assertEqual(collection{1}.id, ids.P1);
assertEqual(collection{1}.name, "Prop_1");
assertEqual(collection{2}.name, "Prop_2");
assertEqual(collection{2}.name, "Antonio Stradivari");
props = collection{2}.get_properties();
assertEqual(props{1}.name, "latitude");
assertEqual(props{1}.value, 45.07353);
assertEqual(props{2}.name, "longitude");
assertEqual(props{2}.value, 7.68315);
cleanup();
end
%% Test query execution
function test_execute_query()
cleanup();
ids = insert_data();
c = Caosdb();
% FIND query
query = ['FIND Record Violin WITH name="Sherlock Holmes' "'" ' violin"'];
query = ['FIND Record Recordtype_1 WITH Prop_1 < ' ...
'0.0000000000000000000000000000000000000000000000000000000000000000000000001'];
results = c.query(query);
assertTrue(iscell(results));
assertFalse(isempty(results));
violin = results{1};
props = violin.get_properties();
assertEqual(props{1}.name, "price");
assertEqual(props{1}.value, 814873.0);
assertEqual(props{2}.name, "Manufacturer");
assertEqual(props{2}.value, "119");
% COUNT query
result = results{1};
props = result.get_properties();
assertEqual(props{1}.name, "Prop_1");
assertEqual(props{1}.value, 1e-300);
assertEqual(props{2}.name, "Prop_2");
assertEqual(props{2}.value, 1e-200);
% TODO still fails with current libcaosdb@dev %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% query = ['COUNT Record Violin'];
% count = c.query(query);
% assertTrue(isscalar(count));
% assertEqual(count, int64(4));
% COUNT query
query = ['COUNT ENTITY Recordtype_1'];
count = c.query(query);
assertTrue(isscalar(count));
assertEqual(count, int64(3));
end
% Error handling tests %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
......@@ -146,3 +169,76 @@ function test_execute_query_failure()
results = c.query("FIND Record I-Do-Not-Exist"); % Must not segfault.
assertTrue(isempty(results));
end
%% Utility functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Return a struct with the IDs
%
function ids = insert_data()
c = Caosdb();
ids = struct();
P1 = Entity(); % P1
P1.role = "PROPERTY";
P1.name = "Prop_1";
P1.set_datatype("DOUBLE");
ids.P1 = c.insert({P1}){1}.id;
P2 = Entity(); % P2
P2.role = "PROPERTY";
P2.name = "Prop_2";
P2.set_datatype("DOUBLE");
ids.P2 = c.insert({P2}){1}.id;
RT1 = Entity(); % RT1
RT1.role = "RECORD_TYPE";
RT1.name = "Recordtype_1";
p1 = Property();
p1.id = ids.P1;
p1.importance = "OBLIGATORY";
RT1.set_properties({p1}); % RT1 has P1 property
ids.RT1 = c.insert({RT1}){1}.id;
par = Parent();
par.id = ids.RT1;
RT1.set_parents({par}); % self-inheritance RT1 <|- RT1
RT1.id = ids.RT1;
c.update({RT1});
R1 = Entity(); % Record R1 -|> RT1
R1.role = "RECORD";
R1.set_parents({par});
% Properties for R1
p1 = Property();
p2 = Property();
p1.id = ids.P1;
p2.id = ids.P2;
p1.set_datatype("DOUBLE");
p2.set_datatype("DOUBLE");
p1.value = 1e-300;
p2.value = 1e-200;
R1.set_properties({p1, p2});
ids.R1 = c.insert({R1}){1}.id;
R2 = Entity(); % Record R2 -|> RT1
R2.role = "RECORD";
R2.set_parents({par});
% Properties for R2
p1 = Property();
p1.id = ids.P1;
p1.set_datatype("DOUBLE");
p1.value = 42.23;
R2.set_properties({p1});
ids.R2 = c.insert({R2}){1}.id;
end
% This file is a part of the CaosDB Project.
%
% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com>
%
% This program is free software: you can redistribute it and/or modify
% it under the terms of the GNU Affero General Public License as
% published by the Free Software Foundation, either version 3 of the
% License, or (at your option) any later version.
%
% This program 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 Affero General Public License for more details.
%
% You should have received a copy of the GNU Affero General Public License
% along with this program. If not, see <https://www.gnu.org/licenses/>.
%% The main function which intitializes the tests.
function test_suite = test_transaction()
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions();
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
end
function test_insert_simple()
cleanup();
c = Caosdb();
P1 = Entity();
P1.role = "PROPERTY";
P1.name = "Prop 1";
P1.set_datatype("DOUBLE");
P1_id = c.insert({P1}){1}.id;
assertTrue(numel(P1_id) > 0);
cleanup();
end
%% Test insertion of Entities
function test_insert_entity()
cleanup();
disp("================ test_insert_entity ==============");
% Default connection configuration is sufficient.
c = Caosdb();
% Create & insert Properties first (TODO Insert in a single step)
P1 = Entity();
P1.role = "PROPERTY";
P1.name = "Prop 1";
P1.set_datatype("DOUBLE");
P2 = Entity();
P2.role = "PROPERTY";
P2.name = "Prop 2";
P2.set_datatype("BOOLEAN");
P2.unit = "MegaTruths";
% assertExceptionThrown(@()c.insert({P1, P2}), 'maox:NotImplementedError');
inserted = c.insert({P1});
assertFalse(inserted{1}.has_errors(), print_messages(inserted{1}.get_errors(), "error"));
assertTrue(inserted{1}.has_warnings(), print_messages(inserted{1}.get_warnings(), "warning"));
assertEqual(numel(inserted{1}.get_warnings()), 1);
assertTrue(isequal(inserted{1}.get_warnings(){1}.description, "Entity has no unit."), ...
inserted{1}.get_warnings(){1}.description);
assertFalse(inserted{1}.has_infos(), print_messages(inserted{1}.get_infos(), "info"));
P1_id = inserted{1}.id;
inserted = c.insert({P2});
assertFalse(inserted{1}.has_errors(), print_messages(inserted{1}.get_errors(), "error"));
assertFalse(inserted{1}.has_warnings(), print_messages(inserted{1}.get_warnings(), "warning"));
assertFalse(inserted{1}.has_infos(), print_messages(inserted{1}.get_infos(), "info"));
P2_id = inserted{1}.id;
% Create RecordType
RT1 = Entity();
RT1.role = "RECORD_TYPE";
RT1.name = "Recordtype 1";
p1 = Property();
p1.id = P1_id;
p1.name = P1.name;
p1.importance = "OBLIGATORY";
RT1.set_properties({p1});
inserted = c.insert({RT1});
assertFalse(inserted{1}.has_errors(), print_messages(inserted{1}.get_errors(), "error"));
assertFalse(inserted{1}.has_warnings(), print_messages(inserted{1}.get_warnings(), "warning"));
assertFalse(inserted{1}.has_infos(), print_messages(inserted{1}.get_infos(), "info"));
RT1_id = inserted{1}.id;
% set plain Octave properties, reuse IDs
p1 = Property();
p1.id = P1_id;
p1.name = P1.name;
p1.set_datatype("DOUBLE");
p1.unit = "Mt";
p1.value = 98798747.85787;
p2 = Property();
p2.id = P2_id;
p2.name = P2.name;
p2.description = "No description";
p2.set_datatype("BOOLEAN");
p2.value = logical(1);
% Get ID of parent first
par = Parent();
par.id = RT1_id;
% TODO(dh) Test if empty Value remains empty after insert
e = Entity();
e.role = "RECORD";
e.name = "Test Record";
% merge objects
e.set_properties({p1, p2});
e.set_parents({par});
% There are 3 steps:
% 1. Insert
inserted = c.insert({e});
% 2. Look at insertion result
assertEqual(numel(inserted), 1);
inserted = inserted{1};
assertFalse(isempty(inserted.id));
assertTrue(isinteger(int64(str2num (inserted.id))));
assertTrue(int64(str2num (inserted.id)) > 99);
% 3. Retrieve the inserted entity again, by ID
retrieved = c.retrieve_by_id(inserted.id){1};
disp("======== Original ============");
disp(e);
disp("........ Retrieved ............");
disp(retrieved);
% Compare results
assertEqual(e.role, retrieved.role);
assertEqual(e.name, retrieved.name);
assertEqual(numel(e.get_properties()), numel(retrieved.get_properties()));
props = retrieved.get_properties();
prop_names = cellfun(@(x)(getfield(x.to_struct(), "name")), props, "UniformOutput", false);
assertTrue(all(ismember({"Prop 1", "Prop 2"}, prop_names)));
i_1 = find(cellfun(@(x)isequal(x, "Prop 1"), prop_names));
i_2 = find(cellfun(@(x)isequal(x, "Prop 2"), prop_names));
ret_p1 = props{i_1};
ret_p2 = props{i_2};
compare_properties(ret_p1, p1);
compare_properties(ret_p2, p2);
compare_parents(retrieved.get_parents{1}, par, {"name"});
% Check errors
assertFalse(inserted.has_errors(), print_messages(inserted.get_errors(), "error"));
assertFalse(inserted.has_warnings(), print_messages(inserted.get_warnings(), "warning"));
assertFalse(inserted.has_infos(), print_messages(inserted.get_infos(), "info"));
assertFalse(retrieved.has_errors(), print_messages(retrieved.get_errors(), "error"));
assertFalse(retrieved.has_warnings(), print_messages(retrieved.get_warnings(), "warning"));
assertFalse(retrieved.has_infos(), print_messages(retrieved.get_infos(), "info"));
cleanup();
end
%% Test updating of Entities
function test_update_entity()
% Default connection configuration is sufficient.
cleanup();
disp("================ test_update_entity ==============");
c = Caosdb();
% Create & insert Properties first (TODO Insert in a single step)
P1 = Entity();
P1.role = "PROPERTY";
P1.name = "Prop 1";
%% TODO(dh) units
% P1.unit = "Whales";
P1.set_datatype("INTEGER", false, true);
P1.value = int64([1, 2, 3, 4, 2^31 - 1, -2^31]);
P1_id = c.insert({P1}){1}.id;
% disp(P1)
P1_upd = c.retrieve_by_id(P1_id){1};
% disp(P1_upd);
P1_upd.name = "Prop 1a";
P1_upd.description = "This is a described Property.";
P1_upd.set_datatype("DOUBLE", false, false); % Double scalar
%% TODO(dh) units
% P1_upd.unit = "Wales"
P1_upd.value = 1e-300;
P1_upd_result = c.update({P1_upd}){1};
assertFalse(P1_upd_result.has_errors(), print_messages(P1_upd_result.get_errors(), "error"));
% TODO(dh) unit warning is currently existing
% assertFalse(P1_upd_result.has_warnings(),
% print_messages(P1_upd_result.get_warnings(), "warning"));
assertFalse(P1_upd_result.has_infos(), print_messages(P1_upd_result.get_infos(), "info"));
assertEqual(P1_upd_result.id, P1_id);
retrieved = c.retrieve_by_id(P1_id);
P1_upd_retr = retrieved{1};
% disp(P1_upd_retr);
assertFalse(P1_upd_retr.has_errors(), print_messages(P1_upd_retr.get_errors(), "error"));
assertFalse(P1_upd_retr.has_warnings(), print_messages(P1_upd_retr.get_warnings(), "warning"));
assertFalse(P1_upd_retr.has_infos(), print_messages(P1_upd_retr.get_infos(), "info"));
compare_properties(P1_upd_retr, P1_upd);
cleanup();
end
% Utility functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function diff_str = prints_diff(ent_actual, ent_expected)
diff_str = disp(["Differing entities!\nactual:\n", disp(ent_actual.to_struct()), ...
"----\nexpected:\n", disp(ent_expected.to_struct())]);
end
function compare_properties(prop_a, prop_b)
diff_str = prints_diff(prop_a, prop_b);
assertEqual(prop_a.id, prop_b.id, diff_str);
assertEqual(prop_a.name, prop_b.name, diff_str);
% If both are empty, we do not care about the exact type
assertTrue((isempty(prop_a.description) && isempty(prop_b.description)) || ...
isequal(prop_a.description, prop_b.description), diff_str);
% TODO (dh) Add unit comparison again when the server sends the unit.
% assertTrue(((isempty(prop_a.unit) && isempty(prop_b.unit)) ||
% isequal(prop_a.unit, prop_b.unit)), diff_str);
assertEqual(prop_a.value, prop_b.value, diff_str);
assertTrue(isequal(prop_a.get_datatype(), prop_b.get_datatype()), diff_str);
end
function compare_parents(prop_a, prop_b, exclude)
if nargin < 3
exclude = {};
end
diff_str = prints_diff(prop_a, prop_b);
if ~ismember({"id"}, exclude)
assertEqual(prop_a.id, prop_b.id, diff_str);
end
if ~ismember({"name"}, exclude)
assertEqual(prop_a.name, prop_b.name, diff_str);
end
% If both are empty, we do not care about the exact type
if ~ismember({"description"}, exclude)
assertTrue((isempty(prop_a.description) && isempty(prop_b.description)) || ...
isequal(prop_a.description, prop_b.description), diff_str);
end
end
%% print the messages into a cell
function result = print_messages(message_cell, log_name)
result = "";
if nargin > 1 && ~isempty(log_name)
result = ["[ ", log_name, " ] "];
end
for msg = message_cell
msg = msg{1};
result = [result, "\n ", msg.str()];
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment