Commit 68292c2b authored by Dominik Charousset's avatar Dominik Charousset

Implement and test parse_config function

parent 7dc0fd2e
......@@ -22,6 +22,7 @@
#include <istream>
#include "caf/optional.hpp"
#include "caf/parse_config.hpp"
namespace caf {
......@@ -32,8 +33,9 @@ namespace detail {
/// @param raw_data Input stream of INI formatted text.
/// @param errors Output stream for parser errors.
/// @param consumer Callback consuming generated key-value pairs.
void parse_ini(std::istream& raw_data, std::ostream& errors,
config_consumer consumer);
void parse_ini(std::istream& raw_data,
config_consumer consumer,
optional<std::ostream&> errors = none);
} // namespace detail
} // namespace caf
......
......@@ -21,15 +21,16 @@
#define CAF_PARSER_CONFIG_HPP
#include <string>
#include <istream>
#include <algorithm>
#include "caf/variant.hpp"
#include "caf/optional.hpp"
namespace caf {
/// Denotes the format of a configuration file.
enum class config_format {
auto_detect,
ini
};
......@@ -39,9 +40,21 @@ using config_value = variant<std::string, double, int64_t, bool>;
/// Denotes a callback for config parser implementations.
using config_consumer = std::function<void (std::string, config_value)>;
/// Parse `file_name` using given file format `cf`.
/// Read configuration from `input_stream` using given `format`.
/// @param input_stream ASCII-formatted configuration.
/// @param format Configuration format such as INI.
/// @param errors Output streams for error messages.
void parse_config(std::istream& input_stream, config_format format,
optional<std::ostream&> errors = none);
/// Read configuration from `file_name` using given `format` or try to
/// deduce file format automatically if `cf == none`.
/// @param file_name Path to configuration file.
/// @param cf Forces the parser to use a specific file format unless `none`.
/// @param errors Output streams for error messages.
void parse_config(const std::string& file_name,
config_format cf = config_format::auto_detect);
optional<config_format> cf = none,
optional<std::ostream&> errors = none);
} // namespace caf
......
......@@ -19,59 +19,65 @@
#include "caf/parse_config.hpp"
#include <vector>
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <iostream>
#include "caf/actor.hpp"
#include "caf/atom.hpp"
#include "caf/detail/optional_message_visitor.hpp"
#include "caf/detail/parse_ini.hpp"
#include "caf/send.hpp"
#include "caf/actor.hpp"
#include "caf/experimental/whereis.hpp"
#include "caf/detail/parse_ini.hpp"
#include "caf/detail/optional_message_visitor.hpp"
namespace caf {
class message_visitor : public static_visitor<message> {
public:
template <class T>
message operator()(T& value) const {
return make_message(std::move(value));
}
};
void parse_config(std::istream& input, config_format format,
optional<std::ostream&> errors) {
if (! input)
return;
auto cs = experimental::whereis(atom("ConfigServ"));
auto consume = [&](std::string key, config_value value) {
message_visitor mv;
anon_send(cs, put_atom::value, std::move(key), apply_visitor(mv, value));
};
switch (format) {
case config_format::ini:
detail::parse_ini(input, consume, errors);
}
}
void parse_config(const std::string& file_name,
config_format cf) {
if (cf == config_format::auto_detect) {
optional<config_format> format,
optional<std::ostream&> errors) {
if (! format) {
// try to detect file format according to file extension
if (file_name.size() < 5) {
if (file_name.size() < 5)
std::cerr << "filename is to short" << std::endl;
} else if (file_name.compare(file_name.size() - 4, 4, ".ini")) {
parse_config(file_name, config_format::ini);
} else {
std::cerr << "unknown config file format" << std::endl;
}
else if (file_name.compare(file_name.size() - 4, 4, ".ini"))
parse_config(file_name, config_format::ini, errors);
else if (errors)
*errors << "error: unknown config file format" << std::endl;
return;
}
std::ifstream raw_data(file_name);
std::stringstream error_stream;
struct consumer {
actor config_server_;
consumer() {
config_server_ = experimental::whereis(atom("ConfigServ"));
}
consumer(const consumer&) = default;
consumer& operator=(const consumer&) = default;
void operator()(const std::string&, config_value) const {
// send message to config server
}
};
consumer cons;
switch (cf) {
case config_format::ini:
detail::parse_ini(raw_data, error_stream, cons);
break;
default:
std::cerr << "no other format is supported" << std::endl;
break;
std::ifstream input{file_name};
if (! input) {
if (errors)
*errors << "error: unable to open " << file_name << std::endl;
return;
}
raw_data.close();
parse_config(input, *format, errors);
}
} // namespace caf
......@@ -26,13 +26,15 @@
namespace caf {
void detail::parse_ini(std::istream& input, std::ostream& errors,
config_consumer consumer) {
void detail::parse_ini(std::istream& input,
config_consumer consumer,
optional<std::ostream&> errors) {
std::string group;
std::string line;
size_t ln = 0; // line number
auto print = [&](const char* category, const char* str) {
errors << category << " in line " << ln << ": " << str << std::endl;
if (errors)
*errors << category << " in line " << ln << ": " << str << std::endl;
};
auto print_error = [&](const char* str) {
print("error", str);
......
......@@ -27,6 +27,8 @@
#include "caf/all.hpp"
#include "caf/experimental/whereis.hpp"
#include "caf/detail/parse_ini.hpp"
#include "caf/detail/safe_equal.hpp"
......@@ -79,24 +81,72 @@ hop=--"hiho"
)__";
struct fixture {
void load(const char* str) {
auto f = [&](std::string key, config_value value) {
values.emplace(std::move(key), std::move(value));
};
~fixture() {
shutdown();
}
template <class F>
void load_impl(F loader, const char* str) {
std::stringstream ss;
std::stringstream err;
ss << str;
detail::parse_ini(ss, err, f);
loader(ss, err);
split(errors, err.str(), is_any_of("\n"), token_compress_on);
}
void load_to_config_server(const char* str) {
config_server = experimental::whereis(atom("ConfigServ"));;
auto f = [&](std::istream& in, std::ostream& out) {
parse_config(in, config_format::ini, out);
};
load_impl(f, str);
}
void load(const char* str) {
auto consume = [&](std::string key, config_value value) {
values.emplace(std::move(key), std::move(value));
};
auto f = [&](std::istream& in, std::ostream& out) {
detail::parse_ini(in, consume, out);
};
load_impl(f, str);
}
bool has_error(const char* err) {
return std::any_of(errors.begin(), errors.end(),
[=](const std::string& str) { return str == err; });
}
template <class T>
bool config_server_has(const char* key, const T& what) {
using type =
typename std::conditional<
std::is_convertible<T, std::string>::value,
std::string,
typename std::conditional<
std::is_integral<T>::value && ! std::is_same<T, bool>::value,
int64_t,
T
>::type
>::type;
bool result = false;
scoped_actor self;
self->sync_send(config_server, get_atom::value, key).await(
[&](ok_atom, std::string&, message& msg) {
msg.apply(
[&](type& val) {
result = detail::safe_equal(what, val);
}
);
}
);
return result;
}
template <class T>
bool value_is(const char* key, const T& what) {
if (config_server != invalid_actor)
return config_server_has(key, what);
auto& cv = values[key];
using type =
typename std::conditional<
......@@ -112,7 +162,63 @@ struct fixture {
return ptr != nullptr && detail::safe_equal(*ptr, what);
}
size_t num_values() {
if (config_server != invalid_actor) {
size_t result = 0;
scoped_actor self;
self->sync_send(config_server, get_atom::value, "*").await(
[&](ok_atom, std::vector<std::pair<std::string, message>>& msgs) {
result = msgs.size();
}
);
return result;
}
return values.size();
}
void check_case1() {
CAF_CHECK(errors.empty());
CAF_CHECK(num_values() == 6);
CAF_CHECK(value_is("nexus.port", 4242));
CAF_CHECK(value_is("nexus.host", "127.0.0.1"));
CAF_CHECK(value_is("scheduler.policy", "work-sharing"));
CAF_CHECK(value_is("scheduler.max-threads", 2));
CAF_CHECK(value_is("middleman.automatic-connections", true));
CAF_CHECK(value_is("cash.greeting",
"Hi there, this is \"CASH!\"\n ~\\~ use at your own risk ~\\~"));
}
void check_case2() {
CAF_CHECK(errors.empty());
CAF_CHECK(num_values() == 5);
CAF_CHECK(value_is("test.foo", -0xff));
CAF_CHECK(value_is("test.bar", 034));
CAF_CHECK(value_is("test.baz", -0.23));
CAF_CHECK(value_is("test.buzz", 1E-34));
CAF_CHECK(value_is("test.bazz", 10931));
}
void check_case3() {
CAF_CHECK(has_error("error in line 2: missing ] at end of line"));
CAF_CHECK(has_error("error in line 3: value outside of a group"));
CAF_CHECK(has_error("error in line 6: no '=' found"));
CAF_CHECK(has_error("error in line 7: line starting with '='"));
CAF_CHECK(has_error("error in line 8: line ends with '='"));
CAF_CHECK(has_error("error in line 9: stray '\"'"));
CAF_CHECK(has_error("error in line 10: string not terminated by '\"'"));
CAF_CHECK(has_error("warning in line 12: trailing quotation mark escaped"));
CAF_CHECK(has_error("error in line 13: '-' is not a number"));
CAF_CHECK(has_error("error in line 14: invalid hex value"));
CAF_CHECK(has_error("error in line 15: invalid binary value"));
CAF_CHECK(has_error("error in line 16: invalid oct value"));
CAF_CHECK(has_error("error in line 17: invalid value"));
CAF_CHECK(num_values() == 2);
CAF_CHECK(value_is("test.some-int", 42));
CAF_CHECK(value_is("test.some-string", "hi there!"));
}
std::map<std::string, config_value> values;
actor config_server;
std::vector<std::string> errors;
};
......@@ -120,48 +226,40 @@ struct fixture {
CAF_TEST_FIXTURE_SCOPE(parse_ini_tests, fixture)
CAF_TEST(simple_ini) {
load(case1);
CAF_CHECK(errors.empty());
CAF_CHECK(values.count("nexus.port") > 0);
CAF_CHECK(value_is("nexus.port", 4242));
CAF_CHECK(value_is("nexus.host", "127.0.0.1"));
CAF_CHECK(value_is("scheduler.policy", "work-sharing"));
CAF_CHECK(value_is("scheduler.max-threads", 2));
CAF_CHECK(value_is("middleman.automatic-connections", true));
CAF_CHECK(values.count("cash.greeting") > 0);
CAF_CHECK(value_is("cash.greeting",
"Hi there, this is \"CASH!\"\n ~\\~ use at your own risk ~\\~"));
check_case1();
}
CAF_TEST(numbers) {
load(case2);
CAF_CHECK(join(errors, "\n") == "");
CAF_CHECK(value_is("test.foo", -0xff));
CAF_CHECK(value_is("test.bar", 034));
CAF_CHECK(value_is("test.baz", -0.23));
CAF_CHECK(value_is("test.buzz", 1E-34));
CAF_CHECK(value_is("test.bazz", 10931));
check_case2();
}
CAF_TEST(errors) {
load(case3);
CAF_CHECK(has_error("error in line 2: missing ] at end of line"));
CAF_CHECK(has_error("error in line 3: value outside of a group"));
CAF_CHECK(has_error("error in line 6: no '=' found"));
CAF_CHECK(has_error("error in line 7: line starting with '='"));
CAF_CHECK(has_error("error in line 8: line ends with '='"));
CAF_CHECK(has_error("error in line 9: stray '\"'"));
CAF_CHECK(has_error("error in line 10: string not terminated by '\"'"));
CAF_CHECK(has_error("warning in line 12: trailing quotation mark escaped"));
CAF_CHECK(has_error("error in line 13: '-' is not a number"));
CAF_CHECK(has_error("error in line 14: invalid hex value"));
CAF_CHECK(has_error("error in line 15: invalid binary value"));
CAF_CHECK(has_error("error in line 16: invalid oct value"));
CAF_CHECK(has_error("error in line 17: invalid value"));
CAF_CHECK(values.size() == 2);
CAF_CHECK(value_is("test.some-int", 42));
CAF_CHECK(value_is("test.some-string", "hi there!"));
load_to_config_server(case3);
check_case3();
}
CAF_TEST(simple_ini_via_config_server) {
load_to_config_server(case1);
CAF_CHECK(values.empty());
check_case1();
}
CAF_TEST(numbers_via_config_server) {
load_to_config_server(case2);
CAF_CHECK(values.empty());
check_case2();
}
CAF_TEST(errors_via_config_server) {
load_to_config_server(case3);
CAF_CHECK(values.empty());
check_case3();
}
CAF_TEST_FIXTURE_SCOPE_END()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment