HDFS-9117. Config file reader / options classes for libhdfs++. Contributed by Bob Hansen.

This commit is contained in:
James 2015-11-25 12:09:13 -05:00 committed by James Clampffer
parent a38703fdfc
commit 87362b1c17
7 changed files with 768 additions and 1 deletions

View File

@ -49,7 +49,9 @@ include_directories(
lib
${PROJECT_BINARY_DIR}/lib/proto
third_party/asio-1.10.2/include
third_party/rapidxml-1.13
third_party/gmock-1.7.0
third_party/tr2
${OPENSSL_INCLUDE_DIR}
../libhdfs/include
)

View File

@ -1 +1 @@
add_library(common base64.cc options.cc status.cc sasl_digest_md5.cc hdfs_public_api.cc)
add_library(common base64.cc status.cc sasl_digest_md5.cc hdfs_public_api.cc options.cc configuration.cc)

View File

@ -0,0 +1,231 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* The following features are not currently implemented
* - Deprecated values
* - Make filename and config file contents unicode-safe
* - Config redirection/environment substitution
*
* - getInts (comma separated))
* - getStrings (comma separated))
* - getIntegerRange
* - getSocketAddr
* - getTimeDuration
* - getBytes (e.g. 1M or 1G)
* - hex values
*/
#include "configuration.h"
#include <strings.h>
#include <sstream>
#include <map>
#include <rapidxml/rapidxml.hpp>
#include <rapidxml/rapidxml_utils.hpp>
namespace hdfs {
/*
* Configuration class
*/
Configuration::Configuration() {}
bool is_valid_bool(const std::string& raw) {
if (!strcasecmp(raw.c_str(), "true")) {
return true;
}
if (!strcasecmp(raw.c_str(), "false")) {
return true;
}
return false;
}
bool str_to_bool(const std::string& raw) {
if (!strcasecmp(raw.c_str(), "true")) {
return true;
}
return false;
}
optional<Configuration> Configuration::Load(const std::string& xmlData) {
Configuration result;
return result.OverlayResourceString(xmlData);
}
optional<Configuration> Configuration::OverlayResourceString(
const std::string& xmlData) const {
if (xmlData.size() == 0) {
return optional<Configuration>();
}
int length = xmlData.size();
std::vector<char> raw_bytes;
raw_bytes.reserve(length + 1);
std::copy(xmlData.begin(), xmlData.end(), std::back_inserter(raw_bytes));
raw_bytes.push_back('\0');
ConfigMap map(raw_values_);
bool success = UpdateMapWithResource(map, raw_bytes);
if (success) {
return optional<Configuration>(Configuration(map));
} else {
return optional<Configuration>();
}
}
bool Configuration::UpdateMapWithResource(ConfigMap& map,
std::vector<char>& raw_bytes) {
rapidxml::xml_document<> dom;
dom.parse<rapidxml::parse_trim_whitespace>(&raw_bytes[0]);
/* File must contain a single <configuration> stanza */
auto config_node = dom.first_node("configuration", 0, false);
if (!config_node) {
return false;
}
/* Walk all of the <property> nodes, ignoring the rest */
for (auto property_node = config_node->first_node("property", 0, false);
property_node;
property_node = property_node->next_sibling("property", 0, false)) {
auto name_node = property_node->first_node("name", 0, false);
auto value_node = property_node->first_node("value", 0, false);
if (name_node && value_node) {
auto mapValue = map.find(name_node->value());
if (mapValue != map.end() && mapValue->second.final) {
continue;
}
map[name_node->value()] = value_node->value();
auto final_node = property_node->first_node("final", 0, false);
if (final_node && is_valid_bool(final_node->value())) {
map[name_node->value()].final = str_to_bool(final_node->value());
}
}
auto name_attr = property_node->first_attribute("name", 0, false);
auto value_attr = property_node->first_attribute("value", 0, false);
if (name_attr && value_attr) {
auto mapValue = map.find(name_attr->value());
if (mapValue != map.end() && mapValue->second.final) {
continue;
}
map[name_attr->value()] = value_attr->value();
auto final_attr = property_node->first_attribute("final", 0, false);
if (final_attr && is_valid_bool(final_attr->value())) {
map[name_attr->value()].final = str_to_bool(final_attr->value());
}
}
}
return true;
}
optional<std::string> Configuration::Get(const std::string& key) const {
auto found = raw_values_.find(key);
if (found != raw_values_.end()) {
return std::experimental::make_optional(found->second.value);
} else {
return optional<std::string>();
}
}
std::string Configuration::GetWithDefault(
const std::string& key, const std::string& default_value) const {
return Get(key).value_or(default_value);
}
optional<int64_t> Configuration::GetInt(const std::string& key) const {
auto raw = Get(key);
if (raw) {
errno = 0;
char* end = nullptr;
auto result =
std::experimental::make_optional(strtol(raw->c_str(), &end, 10));
if (end == raw->c_str()) {
/* strtoll will set end to input if no conversion was done */
return optional<int64_t>();
}
if (errno == ERANGE) {
return optional<int64_t>();
}
return result;
} else {
return optional<int64_t>();
}
}
int64_t Configuration::GetIntWithDefault(const std::string& key,
int64_t default_value) const {
return GetInt(key).value_or(default_value);
}
optional<double> Configuration::GetDouble(const std::string& key) const {
auto raw = Get(key);
if (raw) {
errno = 0;
char* end = nullptr;
auto result = std::experimental::make_optional(strtod(raw->c_str(), &end));
if (end == raw->c_str()) {
/* strtod will set end to input if no conversion was done */
return optional<double>();
}
if (errno == ERANGE) {
return optional<double>();
}
return result;
} else {
return optional<double>();
}
}
double Configuration::GetDoubleWithDefault(const std::string& key,
double default_value) const {
return GetDouble(key).value_or(default_value);
}
optional<bool> Configuration::GetBool(const std::string& key) const {
auto raw = Get(key);
if (!raw) {
return optional<bool>();
}
if (!strcasecmp(raw->c_str(), "true")) {
return std::experimental::make_optional(true);
}
if (!strcasecmp(raw->c_str(), "false")) {
return std::experimental::make_optional(false);
}
return optional<bool>();
}
bool Configuration::GetBoolWithDefault(const std::string& key,
bool default_value) const {
return GetBool(key).value_or(default_value);
}
}

View File

@ -0,0 +1,94 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef COMMON_CONFIGURATION_H_
#define COMMON_CONFIGURATION_H_
#include <string>
#include <map>
#include <vector>
#include <set>
#include <istream>
#include <stdint.h>
#include <optional.hpp>
namespace hdfs {
template <class T>
using optional = std::experimental::optional<T>;
/**
* Configuration class that parses XML.
*
* Files should be an XML file of the form
* <configuration>
* <property>
* <name>Name</name>
* <value>Value</value>
* </property>
* <configuration>
*
* This class is not thread-safe.
*/
class Configuration {
public:
/* Creates a new Configuration from input xml */
static optional<Configuration> Load(const std::string &xml_data);
/* Constructs a configuration with no resources loaded */
Configuration();
/* Loads resources from a file or a stream */
optional<Configuration> OverlayResourceString(
const std::string &xml_data) const;
// Gets values
std::string GetWithDefault(const std::string &key,
const std::string &default_value) const;
optional<std::string> Get(const std::string &key) const;
int64_t GetIntWithDefault(const std::string &key, int64_t default_value) const;
optional<int64_t> GetInt(const std::string &key) const;
double GetDoubleWithDefault(const std::string &key,
double default_value) const;
optional<double> GetDouble(const std::string &key) const;
bool GetBoolWithDefault(const std::string &key, bool default_value) const;
optional<bool> GetBool(const std::string &key) const;
private:
/* Transparent data holder for property values */
struct ConfigData {
std::string value;
bool final;
ConfigData() : final(false){};
ConfigData(const std::string &value) : value(value), final(false) {}
void operator=(const std::string &new_value) {
value = new_value;
final = false;
}
};
typedef std::map<std::string, ConfigData> ConfigMap;
Configuration(ConfigMap &src_map) : raw_values_(src_map){};
static bool UpdateMapWithResource(ConfigMap &map,
std::vector<char> &raw_bytes);
const ConfigMap raw_values_;
};
}
#endif

View File

@ -68,6 +68,10 @@ add_executable(node_exclusion_test node_exclusion_test.cc)
target_link_libraries(node_exclusion_test fs gmock_main common ${PROTOBUF_LIBRARIES} ${OPENSSL_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
add_test(node_exclusion node_exclusion_test)
add_executable(configuration_test configuration_test.cc)
target_link_libraries(configuration_test common gmock_main ${CMAKE_THREAD_LIBS_INIT})
add_test(configuration configuration_test)
build_libhdfs_test(libhdfs_threaded hdfspp_test_shim_static expect.c test_libhdfs_threaded.c ${OS_DIR}/thread.c)
link_libhdfs_test(libhdfs_threaded hdfspp_test_shim_static fs reader rpc proto common ${PROTOBUF_LIBRARIES} ${OPENSSL_LIBRARIES} native_mini_dfs ${JAVA_JVM_LIBRARY})
add_libhdfs_test(libhdfs_threaded hdfspp_test_shim_static)

View File

@ -0,0 +1,363 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "configuration_test.h"
#include "common/configuration.h"
#include <gmock/gmock.h>
#include <cstdio>
#include <fstream>
using ::testing::_;
using namespace hdfs;
namespace hdfs {
TEST(ConfigurationTest, TestDegenerateInputs) {
/* Completely empty stream */
{
std::stringstream stream;
optional<Configuration> config = Configuration::Load("");
EXPECT_FALSE(config && "Empty stream");
}
/* No values */
{
std::string data = "<configuration></configuration>";
optional<Configuration> config = Configuration::Load(data);
EXPECT_TRUE(config && "Blank config");
}
/* Extraneous values */
{
std::string data = "<configuration><spam></spam></configuration>";
optional<Configuration> config = Configuration::Load(data);
EXPECT_TRUE(config && "Extraneous values");
}
}
TEST(ConfigurationTest, TestBasicOperations) {
/* Single value */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "value1");
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse single value");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
}
/* Multiple values */
{
optional<Configuration> config =
simpleConfig("key1", "value1", "key2", "value2");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
EXPECT_EQ("value2", config->GetWithDefault("key2", ""));
}
/* No defaults */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "value1");
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse single value");
optional<std::string> value = config->Get("key1");
EXPECT_TRUE((bool)value);
EXPECT_EQ("value1", *value);
EXPECT_FALSE(config->Get("key2"));
}
}
TEST(ConfigurationTest, TestCompactValues) {
{
std::stringstream stream;
stream << "<configuration><property name=\"key1\" "
"value=\"value1\"/></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Compact value parse");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
}
}
TEST(ConfigurationTest, TestMultipleResources) {
/* Single value */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "value1");
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key2", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value1", config2->GetWithDefault("key1", ""));
EXPECT_EQ("value2", config2->GetWithDefault("key2", ""));
}
}
TEST(ConfigurationTest, TestStringResource) {
/* Single value */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "value1");
std::string str = stream.str();
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse single value");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
}
}
TEST(ConfigurationTest, TestFinal) {
{
/* Explicitly non-final non-compact value */
std::stringstream stream;
stream << "<configuration><property><name>key1</name><value>value1</"
"value><final>false</final></property></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key1", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value2", config2->GetWithDefault("key1", ""));
}
{
/* Explicitly final non-compact value */
std::stringstream stream;
stream << "<configuration><property><name>key1</name><value>value1</"
"value><final>true</final></property></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key1", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value1", config2->GetWithDefault("key1", ""));
}
{
/* Explicitly non-final compact value */
std::stringstream stream;
stream << "<configuration><property name=\"key1\" value=\"value1\" "
"final=\"false\"/></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key1", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value2", config2->GetWithDefault("key1", ""));
}
{
/* Explicitly final compact value */
std::stringstream stream;
stream << "<configuration><property name=\"key1\" value=\"value1\" "
"final=\"true\"/></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key1", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value1", config2->GetWithDefault("key1", ""));
}
{
/* Bogus final value */
std::stringstream stream;
stream << "<configuration><property><name>key1</name><value>value1</"
"value><final>spam</final></property></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key1", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value2", config2->GetWithDefault("key1", ""));
}
{
/* Blank final value */
std::stringstream stream;
stream << "<configuration><property><name>key1</name><value>value1</"
"value><final></final></property></configuration>";
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse first stream");
EXPECT_EQ("value1", config->GetWithDefault("key1", ""));
std::stringstream stream2;
simpleConfigStream(stream2, "key1", "value2");
optional<Configuration> config2 =
config->OverlayResourceString(stream2.str());
EXPECT_TRUE(config2 && "Parse second stream");
EXPECT_EQ("value2", config2->GetWithDefault("key1", ""));
}
}
TEST(ConfigurationTest, TestIntConversions) {
/* No defaults */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "1");
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse single value");
optional<int64_t> value = config->GetInt("key1");
EXPECT_TRUE((bool)value);
EXPECT_EQ(1, *value);
EXPECT_FALSE(config->GetInt("key2"));
}
{
optional<Configuration> config = simpleConfig("key1", "1");
EXPECT_EQ(1, config->GetIntWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", "-100");
EXPECT_EQ(-100, config->GetIntWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", " 1 ");
EXPECT_EQ(1, config->GetIntWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", "");
EXPECT_EQ(-1, config->GetIntWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", "spam");
EXPECT_EQ(-1, config->GetIntWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key2", "");
EXPECT_EQ(-1, config->GetIntWithDefault("key1", -1));
}
}
TEST(ConfigurationTest, TestDoubleConversions) {
/* No defaults */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "1");
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse single value");
optional<double> value = config->GetDouble("key1");
EXPECT_TRUE((bool)value);
EXPECT_EQ(1, *value);
EXPECT_FALSE(config->GetDouble("key2"));
}
{
optional<Configuration> config = simpleConfig("key1", "1");
EXPECT_EQ(1, config->GetDoubleWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", "-100");
EXPECT_EQ(-100, config->GetDoubleWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", " 1 ");
EXPECT_EQ(1, config->GetDoubleWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", "");
EXPECT_EQ(-1, config->GetDoubleWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key1", "spam");
EXPECT_EQ(-1, config->GetDoubleWithDefault("key1", -1));
}
{
optional<Configuration> config = simpleConfig("key2", "");
EXPECT_EQ(-1, config->GetDoubleWithDefault("key1", -1));
}
{ /* Out of range */
optional<Configuration> config = simpleConfig("key2", "1e9999");
EXPECT_EQ(-1, config->GetDoubleWithDefault("key1", -1));
}
}
TEST(ConfigurationTest, TestBoolConversions) {
/* No defaults */
{
std::stringstream stream;
simpleConfigStream(stream, "key1", "true");
optional<Configuration> config = Configuration::Load(stream.str());
EXPECT_TRUE(config && "Parse single value");
optional<bool> value = config->GetBool("key1");
EXPECT_TRUE((bool)value);
EXPECT_EQ(true, *value);
EXPECT_FALSE(config->GetBool("key2"));
}
{
optional<Configuration> config = simpleConfig("key1", "true");
EXPECT_EQ(true, config->GetBoolWithDefault("key1", false));
}
{
optional<Configuration> config = simpleConfig("key1", "tRuE");
EXPECT_EQ(true, config->GetBoolWithDefault("key1", false));
}
{
optional<Configuration> config = simpleConfig("key1", "false");
EXPECT_FALSE(config->GetBoolWithDefault("key1", true));
}
{
optional<Configuration> config = simpleConfig("key1", "FaLsE");
EXPECT_FALSE(config->GetBoolWithDefault("key1", true));
}
{
optional<Configuration> config = simpleConfig("key1", " FaLsE ");
EXPECT_FALSE(config->GetBoolWithDefault("key1", true));
}
{
optional<Configuration> config = simpleConfig("key1", "");
EXPECT_EQ(true, config->GetBoolWithDefault("key1", true));
}
{
optional<Configuration> config = simpleConfig("key1", "spam");
EXPECT_EQ(true, config->GetBoolWithDefault("key1", true));
}
{
optional<Configuration> config = simpleConfig("key1", "");
EXPECT_EQ(true, config->GetBoolWithDefault("key2", true));
}
}
int main(int argc, char *argv[]) {
/*
* The following line must be executed to initialize Google Mock
* (and Google Test) before running the tests.
*/
::testing::InitGoogleMock(&argc, argv);
return RUN_ALL_TESTS();
}
}

View File

@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef TESTS_CONFIGURATION_H_
#define TESTS_CONFIGURATION_H_
#include "common/configuration.h"
#include <cstdio>
#include <fstream>
#include <istream>
#include <gmock/gmock.h>
namespace hdfs {
template <typename T, typename U>
void simpleConfigStreamProperty(std::stringstream& out, T key, U value) {
out << "<property>"
<< "<name>" << key << "</name>"
<< "<value>" << value << "</value>"
<< "</property>";
}
template <typename T, typename U, typename... Args>
void simpleConfigStreamProperty(std::stringstream& out, T key, U value,
Args... args) {
simpleConfigStreamProperty(out, key, value);
simpleConfigStreamProperty(out, args...);
}
template <typename... Args>
void simpleConfigStream(std::stringstream& out, Args... args) {
out << "<configuration>";
simpleConfigStreamProperty(out, args...);
out << "</configuration>";
}
template <typename... Args>
optional<Configuration> simpleConfig(Args... args) {
Configuration result;
std::stringstream stream;
simpleConfigStream(stream, args...);
optional<Configuration> parse = result.Load(stream.str());
EXPECT_TRUE((bool)parse);
return parse;
}
template <typename... Args>
void writeSimpleConfig(const std::string& filename, Args... args) {
std::stringstream stream;
simpleConfigStream(stream, args...);
std::ofstream out;
out.open(filename);
out << stream.rdbuf();
}
}
#endif