Commit 51a5943f authored by Dominik Charousset's avatar Dominik Charousset

Fix regression in --dump-config output

- Make sure the output of `--dump-config` generates valid input for
  `--config-file`.
- Add a regression test with Robot that ensures this property by calling
  a CAF application with `--dump-config` and then have the same
  application parse the output via `--config-file`.
- Omit the options from CAF modules that aren't loaded.
parent 4883df85
......@@ -11,6 +11,8 @@ is based on [Keep a Changelog](https://keepachangelog.com).
- The class `caf::telemetry::label` now has a new `compare` overload that
accepts a `caf::telemetry::label_view` to make the interface of the both
classes symmetrical.
- The template class `caf::dictionary` now has new member functions for erasing
elements.
### Changed
......@@ -18,10 +20,15 @@ is based on [Keep a Changelog](https://keepachangelog.com).
user-defined options. Previously, only options in the global category or
options with a short name were included. Only CAF options are now excluded
from the output. They will still be included in the output of `--long-help`.
- The output of `--dump-config` now only contains CAF options from loaded
modules. Previously, it also included options from modules that were not
loaded.
### Fixed
- Fix build errors with exceptions disabled.
- Fix a regression in `--dump-config` that caused CAF applications to emit
malformed output.
## [0.19.2] - 2023-06-13
......
......@@ -281,6 +281,24 @@ public:
return std::upper_bound(begin(), end(), key, cmp);
}
// -- removal ----------------------------------------------------------------
iterator erase(const_iterator i) {
return xs_.erase(i);
}
iterator erase(const_iterator first, const_iterator last) {
return xs_.erase(first, last);
}
size_type erase(std::string_view key) {
if (auto i = lower_bound(key); i != end() && i->first == key) {
erase(i);
return 1;
}
return 0;
}
// -- element access ---------------------------------------------------------
mapped_type& operator[](std::string_view key) {
......
......@@ -49,7 +49,7 @@ actor_system_config::actor_system_config()
.add<bool>("help,h?", "print help text to STDERR and exit")
.add<bool>("long-help",
"same as --help but list options that are omitted by default")
.add<bool>("dump-config", "print configuration to STDERR and exit")
.add<bool>("dump-config", "print configuration and exit")
.add<string>("config-file", "sets a path to a configuration file");
opt_group{custom_options_, "caf.scheduler"}
.add<string>("policy", "'stealing' (default) or 'sharing'")
......@@ -90,6 +90,9 @@ actor_system_config::actor_system_config()
settings actor_system_config::dump_content() const {
settings result = content;
// Hide options that make no sense in a config file.
result.erase("dump-config");
result.erase("config-file");
auto& caf_group = result["caf"].as_dictionary();
// -- scheduler parameters
auto& scheduler_group = caf_group["scheduler"].as_dictionary();
......@@ -118,7 +121,7 @@ settings actor_system_config::dump_content() const {
defaults::work_stealing::relaxed_sleep_duration);
// -- logger parameters
auto& logger_group = caf_group["logger"].as_dictionary();
put_missing(logger_group, "inline-output", false);
// Note: omit "inline-output" option since it should only be used for testing.
auto& file_group = logger_group["file"].as_dictionary();
put_missing(file_group, "path", defaults::logger::file::path);
put_missing(file_group, "format", defaults::logger::file::format);
......@@ -127,23 +130,6 @@ settings actor_system_config::dump_content() const {
put_missing(console_group, "colored", defaults::logger::console::colored);
put_missing(console_group, "format", defaults::logger::console::format);
put_missing(console_group, "excluded-components", std::vector<std::string>{});
// -- middleman parameters
auto& middleman_group = caf_group["middleman"].as_dictionary();
auto default_id = std::string{defaults::middleman::app_identifier};
put_missing(middleman_group, "app-identifiers",
std::vector<std::string>{std::move(default_id)});
put_missing(middleman_group, "enable-automatic-connections", false);
put_missing(middleman_group, "max-consecutive-reads",
defaults::middleman::max_consecutive_reads);
put_missing(middleman_group, "heartbeat-interval",
defaults::middleman::heartbeat_interval);
// -- openssl parameters
auto& openssl_group = caf_group["openssl"].as_dictionary();
put_missing(openssl_group, "certificate", std::string{});
put_missing(openssl_group, "key", std::string{});
put_missing(openssl_group, "passphrase", std::string{});
put_missing(openssl_group, "capath", std::string{});
put_missing(openssl_group, "cafile", std::string{});
return result;
}
......@@ -225,7 +211,7 @@ bool operator!=(config_iter iter, config_sentinel) {
}
struct indentation {
size_t size;
size_t size = 0;
};
indentation operator+(indentation x, size_t y) noexcept {
......@@ -238,30 +224,101 @@ std::ostream& operator<<(std::ostream& out, indentation indent) {
return out;
}
void print(const config_value::dictionary& xs, indentation indent) {
using std::cout;
for (const auto& kvp : xs) {
if (kvp.first == "dump-config")
continue;
if (auto submap = get_if<config_value::dictionary>(&kvp.second)) {
cout << indent << kvp.first << " {\n";
print(*submap, indent + 2);
cout << indent << "}\n";
} else if (auto lst = get_if<config_value::list>(&kvp.second)) {
if (lst->empty()) {
cout << indent << kvp.first << " = []\n";
} else {
cout << indent << kvp.first << " = [\n";
auto list_indent = indent + 2;
for (auto& x : *lst)
cout << list_indent << to_string(x) << ",\n";
cout << indent << "]\n";
}
struct cout_pseudo_buf {
void push_back(char ch) {
std::cout.put(ch);
}
unit_t end() {
return unit;
}
template <class Iterator, class Sentinel>
void insert(unit_t, Iterator first, Sentinel last) {
while (first != last)
std::cout.put(*first++);
}
};
class config_printer {
public:
config_printer() = default;
explicit config_printer(indentation indent) : indent_(indent) {
// nop
}
void operator()(const config_value& val) {
std::visit(*this, val.get_data());
}
void operator()(none_t) {
detail::print(out_, none);
}
template <class T>
std::enable_if_t<std::is_arithmetic_v<T>> operator()(T val) {
detail::print(out_, val);
}
void operator()(timespan val) {
detail::print(out_, val);
}
void operator()(const uri& val) {
std::cout << '<' << val.str() << '>';
}
void operator()(const std::string& str) {
detail::print_escaped(out_, str);
}
void operator()(const config_value::list& xs) {
if (xs.empty()) {
std::cout << "[]";
return;
}
std::cout << "[\n";
auto nestd_indent = indent_ + 2;
for (auto& x : xs) {
std::cout << nestd_indent;
std::visit(config_printer{nestd_indent}, x.get_data());
std::cout << ",\n";
}
std::cout << indent_ << "]";
}
void operator()(const config_value::dictionary& dict) {
if (dict.empty()) {
std::cout << "{}";
return;
}
auto pos = dict.begin();
for (;;) {
print_kvp(*pos++);
if (pos == dict.end())
return;
std::cout << '\n';
}
}
private:
void print_kvp(const config_value::dictionary::value_type& kvp) {
const auto& [key, val] = kvp;
if (auto* submap = get_if<config_value::dictionary>(&val)) {
std::cout << indent_ << key << " {\n";
auto sub_printer = config_printer{indent_ + 2};
sub_printer(*submap);
std::cout << "\n" << indent_ << "}";
} else {
cout << indent << kvp.first << " = " << to_string(kvp.second) << '\n';
std::cout << indent_ << key << " = ";
std::visit(*this, val.get_data());
}
}
}
indentation indent_;
cout_pseudo_buf out_;
};
} // namespace
......@@ -297,8 +354,9 @@ error actor_system_config::parse(string_list args, std::istream& config) {
}
// Generate config dump if needed.
if (!cli_helptext_printed && get_or(content, "dump-config", false)) {
print(dump_content(), indentation{0});
std::cout << std::flush;
config_printer printer;
printer(dump_content());
std::cout << std::endl;
cli_helptext_printed = true;
}
return none;
......
......@@ -190,6 +190,7 @@ void middleman::init_global_meta_objects() {
}
void middleman::add_module_options(actor_system_config& cfg) {
// Add options to the CLI parser.
config_option_adder{cfg.custom_options(), "caf.middleman"}
.add<std::string>("network-backend",
"either 'default' or 'asio' (if available)")
......@@ -211,6 +212,18 @@ void middleman::add_module_options(actor_system_config& cfg) {
config_option_adder{cfg.custom_options(), "caf.middleman.prometheus-http"}
.add<uint16_t>("port", "listening port for incoming scrapes")
.add<std::string>("address", "bind address for the HTTP server socket");
// Add the defaults to the config so they show up in --dump-config.
auto& grp = put_dictionary(cfg.content, "caf.middleman");
auto default_id = std::string{defaults::middleman::app_identifier};
put_missing(grp, "app-identifiers",
std::vector<std::string>{std::move(default_id)});
put_missing(grp, "enable-automatic-connections", false);
put_missing(grp, "max-consecutive-reads",
defaults::middleman::max_consecutive_reads);
put_missing(grp, "heartbeat-interval",
defaults::middleman::heartbeat_interval);
put_missing(grp, "connection-timeout",
defaults::middleman::connection_timeout);
}
actor_system::module* middleman::make(actor_system& sys, detail::type_list<>) {
......
......@@ -136,6 +136,7 @@ bool manager::authentication_enabled() {
}
void manager::add_module_options(actor_system_config& cfg) {
// Add options to the CLI parser.
config_option_adder(cfg.custom_options(), "caf.openssl")
.add<std::string>(cfg.openssl_certificate, "certificate",
"path to the PEM-formatted certificate file")
......@@ -151,6 +152,13 @@ void manager::add_module_options(actor_system_config& cfg) {
"path to a file of concatenated PEM-formatted certificates")
.add<std::string>("cipher-list",
"colon-separated list of OpenSSL cipher strings to use");
// Add the defaults to the config so they show up in --dump-config.
auto& grp = put_dictionary(cfg.content, "caf.openssl");
put_missing(grp, "certificate", cfg.openssl_certificate);
put_missing(grp, "key", cfg.openssl_key);
put_missing(grp, "passphrase", cfg.openssl_passphrase);
put_missing(grp, "capath", cfg.openssl_capath);
put_missing(grp, "cafile", cfg.openssl_cafile);
}
actor_system::module* manager::make(actor_system& sys, detail::type_list<>) {
......
find_package(Python COMPONENTS Interpreter)
add_test(
NAME "robot-config-dump-config"
COMMAND
${Python_EXECUTABLE}
-m robot
--variable BINARY_PATH:$<TARGET_FILE:hello_world>
"${CMAKE_CURRENT_SOURCE_DIR}/config/dump-config.robot")
add_test(
NAME "robot-http-rest"
COMMAND
......
*** Settings ***
Documentation A test suite for --dump-config.
Library OperatingSystem
Library Process
*** Variables ***
${BINARY_PATH} /path/to/the/test/binary
*** Test Cases ***
The output of --dump-config should generate valid input for --config-file
[Tags] Config
Run Process ${BINARY_PATH} --dump-config stdout=out1.cfg
Run Process ${BINARY_PATH} --config-file\=out1.cfg --dump-config stdout=out2.cfg
${out1}= Get File out1.cfg
${out2}= Get File out2.cfg
Should Be Equal As Strings ${out1} ${out2}
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