1
0
mirror of https://github.com/rwengine/openrw.git synced 2024-11-07 03:12:36 +01:00

config: log parsing Errors to a ParseResult object (which is queryable)

This commit is contained in:
Anonymous Maarten 2017-02-18 07:33:53 +01:00 committed by Daniel Evans
parent e0813e4378
commit 76f2665acf
3 changed files with 264 additions and 40 deletions

View File

@ -13,7 +13,7 @@ GameConfig::GameConfig(const std::string& configName,
const std::string& configPath) const std::string& configPath)
: m_configName(configName) : m_configName(configName)
, m_configPath(configPath) , m_configPath(configPath)
, m_valid(false) , m_parseResult()
, m_inputInvertY(false) { , m_inputInvertY(false) {
if (m_configPath.empty()) { if (m_configPath.empty()) {
m_configPath = getDefaultConfigPath(); m_configPath = getDefaultConfigPath();
@ -23,7 +23,7 @@ GameConfig::GameConfig(const std::string& configName,
auto configFile = getConfigFile(); auto configFile = getConfigFile();
std::string dummy; std::string dummy;
m_valid = parseConfig(ParseType::FILE, configFile, ParseType::CONFIG, dummy); m_parseResult = parseConfig(ParseType::FILE, configFile, ParseType::CONFIG, dummy);
} }
std::string GameConfig::getConfigFile() const { std::string GameConfig::getConfigFile() const {
@ -31,7 +31,11 @@ std::string GameConfig::getConfigFile() const {
} }
bool GameConfig::isValid() const { bool GameConfig::isValid() const {
return m_valid; return m_parseResult.isValid();
}
const GameConfig::ParseResult &GameConfig::getParseResult() const {
return m_parseResult;
} }
std::string GameConfig::getDefaultConfigPath() { std::string GameConfig::getDefaultConfigPath() {
@ -109,7 +113,7 @@ struct IntTranslator {
} }
}; };
bool GameConfig::saveConfig() { GameConfig::ParseResult GameConfig::saveConfig() {
auto filename = getConfigFile(); auto filename = getConfigFile();
return parseConfig(ParseType::CONFIG, "", return parseConfig(ParseType::CONFIG, "",
ParseType::FILE, filename); ParseType::FILE, filename);
@ -121,32 +125,32 @@ std::string GameConfig::getDefaultINIString() {
return result; return result;
} }
bool GameConfig::parseConfig( GameConfig::ParseResult GameConfig::parseConfig(
GameConfig::ParseType srcType, const std::string &source, GameConfig::ParseType srcType, const std::string &source,
ParseType destType, std::string &destination) ParseType destType, std::string &destination)
{ {
pt::ptree srcTree; pt::ptree srcTree;
ParseResult parseResult;
try { try {
if (srcType == ParseType::STRING) { if (srcType == ParseType::STRING) {
pt::read_ini(source, srcTree); pt::read_ini(source, srcTree);
} else if (srcType == ParseType::FILE) { } else if (srcType == ParseType::FILE) {
std::ifstream ifs(source); pt::read_ini(source, srcTree);
pt::read_ini(ifs, srcTree);
} }
} catch (pt::ini_parser_error &e) { } catch (pt::ini_parser_error &e) {
// Catches illegal input files (nonsensical input, duplicate keys) // Catches illegal input files (nonsensical input, duplicate keys)
parseResult.failInputFile(e.filename(), e.line(), e.message());
RW_MESSAGE(e.what()); RW_MESSAGE(e.what());
return false; return parseResult;
} }
if (destType == ParseType::DEFAULT) { if (destType == ParseType::DEFAULT) {
parseResult.failArgument();
RW_ERROR("Target cannot be DEFAULT."); RW_ERROR("Target cannot be DEFAULT.");
return false; return parseResult;
} }
bool success = true;
auto read_config = [&](const std::string &key, auto &target, auto read_config = [&](const std::string &key, auto &target,
const auto &defaultValue, auto &translator, const auto &defaultValue, auto &translator,
bool optional=true) { bool optional=true) {
@ -168,15 +172,14 @@ bool GameConfig::parseConfig(
} catch (pt::ptree_bad_path &e) { } catch (pt::ptree_bad_path &e) {
// Catches missing key-value pairs: fail when required // Catches missing key-value pairs: fail when required
if (!optional) { if (!optional) {
success = false; parseResult.failRequiredMissing(key);
RW_MESSAGE(e.what()); RW_MESSAGE(e.what());
return; return;
} }
sourceValue = defaultValue; sourceValue = defaultValue;
} catch (pt::ptree_bad_data &e) { } catch (pt::ptree_bad_data &e) {
// Catches illegal value data: always fail // Catches illegal value data: always fail
success = false; parseResult.failInvalidData(key);
RW_MESSAGE("invalid data");
RW_MESSAGE(e.what()); RW_MESSAGE(e.what());
return; return;
} }
@ -187,7 +190,7 @@ bool GameConfig::parseConfig(
switch (destType) { switch (destType) {
case ParseType::DEFAULT: case ParseType::DEFAULT:
// Target cannot be DEFAULT (case already handled) // Target cannot be DEFAULT (case already handled)
success = false; parseResult.failArgument();
break; break;
case ParseType::CONFIG: case ParseType::CONFIG:
// Don't care if success == false // Don't care if success == false
@ -211,8 +214,8 @@ bool GameConfig::parseConfig(
read_config("input.invert_y", this->m_inputInvertY, false, boolt); read_config("input.invert_y", this->m_inputInvertY, false, boolt);
if (!success) if (!parseResult.isValid())
return success; return parseResult;
try { try {
if (destType == ParseType::STRING) { if (destType == ParseType::STRING) {
@ -223,9 +226,64 @@ bool GameConfig::parseConfig(
pt::write_ini(destination, srcTree); pt::write_ini(destination, srcTree);
} }
} catch (pt::ini_parser_error &e) { } catch (pt::ini_parser_error &e) {
success = false; parseResult.failOutputFile(e.filename(), e.line(), e.message());
RW_MESSAGE(e.what()); RW_MESSAGE(e.what());
} }
return success; return parseResult;
} }
GameConfig::ParseResult::ParseResult()
: m_result(ErrorType::GOOD)
, m_filename()
, m_line(0)
, m_message()
, m_keys_requiredMissing()
, m_keys_invalidData() {
}
GameConfig::ParseResult::ErrorType GameConfig::ParseResult::type() const {
return this->m_result;
}
bool GameConfig::ParseResult::isValid() const {
return this->type() == ErrorType::GOOD;
}
void GameConfig::ParseResult::failInputFile(const std::string &filename, size_t line,
const std::string &message) {
this->m_result = ParseResult::ErrorType::INVALIDINPUTFILE;
this->m_filename = filename;
this->m_line = line;
this->m_message = message;
}
void GameConfig::ParseResult::failArgument() {
this->m_result = ParseResult::ErrorType::INVALIDARGUMENT;
}
void GameConfig::ParseResult::failRequiredMissing(const std::string &key) {
this->m_result = ParseResult::ErrorType::INVALIDCONTENT;
this->m_keys_requiredMissing.push_back(key);
}
void GameConfig::ParseResult::failInvalidData(const std::string &key) {
this->m_result = ParseResult::ErrorType::INVALIDCONTENT;
this->m_keys_invalidData.push_back(key);
}
void GameConfig::ParseResult::failOutputFile(const std::string &filename, size_t line, const std::string &message) {
this->m_result = ParseResult::ErrorType::INVALIDOUTPUTFILE;
this->m_filename = filename;
this->m_line = line;
this->m_message = message;
}
const std::vector<std::string> &GameConfig::ParseResult::getKeysRequiredMissing() const {
return this->m_keys_requiredMissing;
}
const std::vector<std::string> &GameConfig::ParseResult::getKeysInvalidData() const {
return this->m_keys_invalidData;
}

View File

@ -1,9 +1,104 @@
#ifndef RWGAME_GAMECONFIG_HPP #ifndef RWGAME_GAMECONFIG_HPP
#define RWGAME_GAMECONFIG_HPP #define RWGAME_GAMECONFIG_HPP
#include <string> #include <string>
#include <vector>
class GameConfig { class GameConfig {
public: public:
class ParseResult {
public:
enum ErrorType {
/// GOOD: Input file/string was good
GOOD,
/// INVALIDINPUTFILE: There was some error while reading from a file or string or the input was ambiguous (e.g. duplicate keys)
INVALIDINPUTFILE,
/// INVALIDARGUMENT: The parser received impossible arguments
INVALIDARGUMENT,
/// INVALIDCONTENT: Some required keys were missing or some values were of incorrect type
INVALIDCONTENT,
/// INVALIDOUTPUTFILE: There was some error while writing to a file or string
INVALIDOUTPUTFILE
};
/**
* @brief ParseResult Holds the issues occurred while parsing of a config file.
*/
ParseResult();
/**
* @brief type Get the type of error
* @return Type of error or GOOD if there was no error
*/
ErrorType type() const;
/**
* @brief getKeysRequiredMissing Get the keys that were missing
* @return A vector with all the keys
*/
const std::vector<std::string> &getKeysRequiredMissing() const;
/**
* @brief getKeysInvalidData Get the keys that contained invalid data
* @return A vector with all the keys
*/
const std::vector<std::string> &getKeysInvalidData() const;
/**
* @brief failInputFile Fail because the input file was invalid
* @param filename Filename of the invalid file
* @param line Line number where the error is located
* @param message Description of the error
*/
void failInputFile(const std::string &filename, size_t line, const std::string &message);
/**
* @brief failArgument Fail because an argument was invalid
*/
void failArgument();
/**
* @brief failRequiredMissing Fail because a required key is missing
* @param key The key that is missing
*/
void failRequiredMissing(const std::string &key);
/**
* @brief failInvalidData Fail because a key contains invalid data
* @param key The key that contains invalid data
*/
void failInvalidData(const std::string &key);
/**
* @brief failOutputFile Fail because an error occurred while while writing to the output
* @param filename Filename of the invalid file
* @param line Line number where the error is located
* @param message Description of the error
*/
void failOutputFile(const std::string &filename, size_t line, const std::string &message);
/**
* @brief isValid
* @return True if the loaded configuration is valid
*/
bool isValid() const;
private:
/// Type of the failure
ErrorType m_result;
/// Filename of the invalid input or output file
std::string m_filename;
/// Line number where the failure occurred (on invalid input or output file)
size_t m_line;
/// Description of the failure (on invalid input or output file)
std::string m_message;
/// All required keys that are missing
std::vector<std::string> m_keys_requiredMissing;
/// All keys that contain invalid data
std::vector<std::string> m_keys_invalidData;
};
/** /**
* @brief GameConfig Loads a game configuration * @brief GameConfig Loads a game configuration
* @param configName The configuration filename to load * @param configName The configuration filename to load
@ -20,7 +115,7 @@ public:
/** /**
* @brief writeConfig Save the game configuration * @brief writeConfig Save the game configuration
*/ */
bool saveConfig(); ParseResult saveConfig();
/** /**
* @brief isValid * @brief isValid
@ -28,6 +123,12 @@ public:
*/ */
bool isValid() const; bool isValid() const;
/**
* @brief getParseResult Get more information on parsing failures
* @return A ParseResult object containing more information
*/
const ParseResult &getParseResult() const;
/** /**
* @brief getConfigString Returns the content of the default INI configuration. * @brief getConfigString Returns the content of the default INI configuration.
* @return INI string * @return INI string
@ -66,13 +167,13 @@ private:
* INI string if srcType == STRING * INI string if srcType == STRING
* @return True if the parsing succeeded * @return True if the parsing succeeded
*/ */
bool parseConfig(ParseType srcType, const std::string &source, ParseResult parseConfig(ParseType srcType, const std::string &source,
ParseType destType, std::string &destination); ParseType destType, std::string &destination);
/* Config State */ /* Config State */
std::string m_configName; std::string m_configName;
std::string m_configPath; std::string m_configPath;
bool m_valid; ParseResult m_parseResult;
/* Actual Configuration */ /* Actual Configuration */

View File

@ -58,13 +58,27 @@ public:
std::string dirname() { std::string dirname() {
return this->m_path.parent_path().string(); return this->m_path.parent_path().string();
} }
void change_perms_readonly() {
fs::permissions(this->m_path, fs::perms::owner_read
| fs::perms::group_read | fs::perms::others_read);
}
template<typename T> template<typename T>
void write(T t) { bool append(T t) {
// Append argument at the end of the file. // Append argument at the end of the file.
// File is open/closes repeatedly. Not optimal. // File is open/closes repeatedly. Not optimal.
std::ofstream ofs(this->path(), std::ios::out | std::ios::app); std::ofstream ofs(this->path(), std::ios::out | std::ios::app);
ofs << t; ofs << t;
ofs.close(); ofs.close();
return ofs.good();
}
template<typename T>
bool write(T t) {
// Write the argument to the file, discarding all contents.
// File is open/closes repeatedly. Not optimal.
std::ofstream ofs(this->path(), std::ios::out | std::ios::trunc);
ofs << t;
ofs.close();
return ofs.good();
} }
private: private:
static fs::path getRandomFilePath() { static fs::path getRandomFilePath() {
@ -88,14 +102,18 @@ BOOST_AUTO_TEST_CASE(test_TempFile) {
BOOST_CHECK_EQUAL(tempFile.exists(), true); BOOST_CHECK_EQUAL(tempFile.exists(), true);
tempFile.remove(); tempFile.remove();
tempFile.write("abc"); BOOST_CHECK_EQUAL(tempFile.append("abc"), true);
tempFile.write("def"); BOOST_CHECK_EQUAL(tempFile.append("def"), true);
BOOST_CHECK_EQUAL(tempFile.exists(), true); BOOST_CHECK_EQUAL(tempFile.exists(), true);
tempFile.touch(); tempFile.touch();
std::ifstream ifs(tempFile.path()); std::ifstream ifs(tempFile.path());
std::string line; std::string line;
std::getline(ifs, line); std::getline(ifs, line);
BOOST_CHECK_EQUAL(line, "abcdef"); BOOST_CHECK_EQUAL(line, "abcdef");
tempFile.change_perms_readonly();
BOOST_CHECK_EQUAL(tempFile.write("abc"), false);
BOOST_CHECK_EQUAL(tempFile.append("def"), false);
} }
BOOST_AUTO_TEST_CASE(test_config_valid) { BOOST_AUTO_TEST_CASE(test_config_valid) {
@ -103,11 +121,14 @@ BOOST_AUTO_TEST_CASE(test_config_valid) {
auto cfg = getValidConfig(); auto cfg = getValidConfig();
TempFile tempFile; TempFile tempFile;
tempFile.write(cfg); tempFile.append(cfg);
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(config.isValid()); BOOST_CHECK(config.isValid());
BOOST_CHECK_EQUAL(config.getParseResult().type(), GameConfig::ParseResult::GOOD);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysRequiredMissing().size(), 0);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysInvalidData().size(), 0);
BOOST_CHECK_EQUAL(config.getGameDataPath(), "/dev/test"); BOOST_CHECK_EQUAL(config.getGameDataPath(), "/dev/test");
BOOST_CHECK_EQUAL(config.getGameLanguage(), "american"); BOOST_CHECK_EQUAL(config.getGameLanguage(), "american");
@ -121,11 +142,14 @@ BOOST_AUTO_TEST_CASE(test_config_valid_modified) {
cfg["input"]["invert_y"] = "0"; cfg["input"]["invert_y"] = "0";
TempFile tempFile; TempFile tempFile;
tempFile.write(cfg); tempFile.append(cfg);
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(config.isValid()); BOOST_CHECK(config.isValid());
BOOST_CHECK_EQUAL(config.getParseResult().type(), GameConfig::ParseResult::GOOD);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysRequiredMissing().size(), 0);
BOOST_CHECK_EQUAL(config.getParseResult().getKeysInvalidData().size(), 0);
BOOST_CHECK_EQUAL(config.getInputInvertY(), false); BOOST_CHECK_EQUAL(config.getInputInvertY(), false);
BOOST_CHECK_EQUAL(config.getGameDataPath(), "Liberty City"); BOOST_CHECK_EQUAL(config.getGameDataPath(), "Liberty City");
@ -137,7 +161,7 @@ BOOST_AUTO_TEST_CASE(test_config_save) {
cfg["game"]["path"] = "Liberty City"; cfg["game"]["path"] = "Liberty City";
TempFile tempFile; TempFile tempFile;
tempFile.write(cfg); tempFile.append(cfg);
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
@ -146,14 +170,30 @@ BOOST_AUTO_TEST_CASE(test_config_save) {
tempFile.remove(); tempFile.remove();
BOOST_CHECK(!tempFile.exists()); BOOST_CHECK(!tempFile.exists());
BOOST_CHECK(config.saveConfig()); auto writeResult = config.saveConfig();
BOOST_CHECK(writeResult.isValid());
BOOST_CHECK(tempFile.exists()); BOOST_CHECK(tempFile.exists());
GameConfig config2(tempFile.filename(), tempFile.dirname()); GameConfig config2(tempFile.filename(), tempFile.dirname());
BOOST_CHECK_EQUAL(config2.getGameDataPath(), "Liberty City"); BOOST_CHECK_EQUAL(config2.getGameDataPath(), "Liberty City");
} }
BOOST_AUTO_TEST_CASE(test_config_save_readonly) {
// Test whether saving to a readonly INI file fails
auto cfg = getValidConfig();
TempFile tempFile;
tempFile.append(cfg);
tempFile.change_perms_readonly();
GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK_EQUAL(config.isValid(), true);
auto writeResult = config.saveConfig();
BOOST_CHECK(!writeResult.isValid());
BOOST_CHECK_EQUAL(writeResult.type(), GameConfig::ParseResult::INVALIDOUTPUTFILE);
}
BOOST_AUTO_TEST_CASE(test_config_valid_default) { BOOST_AUTO_TEST_CASE(test_config_valid_default) {
// Test whether the default INI string is valid // Test whether the default INI string is valid
TempFile tempFile; TempFile tempFile;
@ -163,7 +203,7 @@ BOOST_AUTO_TEST_CASE(test_config_valid_default) {
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!config.isValid());
auto defaultINI = config.getDefaultINIString(); auto defaultINI = config.getDefaultINIString();
tempFile.write(defaultINI); tempFile.append(defaultINI);
config = GameConfig(tempFile.filename(), tempFile.dirname()); config = GameConfig(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(config.isValid()); BOOST_CHECK(config.isValid());
@ -175,11 +215,13 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_duplicate) {
cfg["input"]["invert_y "] = "0"; cfg["input"]["invert_y "] = "0";
TempFile tempFile; TempFile tempFile;
tempFile.write(cfg); tempFile.append(cfg);
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), GameConfig::ParseResult::INVALIDINPUTFILE);
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_required_missing) { BOOST_AUTO_TEST_CASE(test_config_invalid_required_missing) {
@ -188,11 +230,19 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_required_missing) {
cfg["game"].erase("path"); cfg["game"].erase("path");
TempFile tempFile; TempFile tempFile;
tempFile.write(cfg); tempFile.append(cfg);
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), GameConfig::ParseResult::INVALIDCONTENT);
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing().size(), 1);
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 0);
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing()[0], "game.path");
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_wrong_type) { BOOST_AUTO_TEST_CASE(test_config_invalid_wrong_type) {
@ -201,16 +251,24 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_wrong_type) {
cfg["input"]["invert_y"]="d"; cfg["input"]["invert_y"]="d";
TempFile tempFile; TempFile tempFile;
tempFile.write(cfg); tempFile.append(cfg);
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), GameConfig::ParseResult::INVALIDCONTENT);
BOOST_CHECK_EQUAL(parseResult.getKeysRequiredMissing().size(), 0);
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData().size(), 1);
BOOST_CHECK_EQUAL(parseResult.getKeysInvalidData()[0], "input.invert_y");
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_empty) { BOOST_AUTO_TEST_CASE(test_config_invalid_empty) {
// Test reading empty configuration file // Test reading empty configuration file
// An empty file has a valid data structure, but has missing keys and is thus invalid.
TempFile tempFile; TempFile tempFile;
tempFile.touch(); tempFile.touch();
BOOST_CHECK(tempFile.exists()); BOOST_CHECK(tempFile.exists());
@ -218,6 +276,10 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_empty) {
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), GameConfig::ParseResult::INVALIDCONTENT);
BOOST_CHECK_GE(parseResult.getKeysRequiredMissing().size(), 1);
} }
BOOST_AUTO_TEST_CASE(test_config_invalid_nonexisting) { BOOST_AUTO_TEST_CASE(test_config_invalid_nonexisting) {
@ -228,6 +290,9 @@ BOOST_AUTO_TEST_CASE(test_config_invalid_nonexisting) {
GameConfig config(tempFile.filename(), tempFile.dirname()); GameConfig config(tempFile.filename(), tempFile.dirname());
BOOST_CHECK(!config.isValid()); BOOST_CHECK(!config.isValid());
const auto &parseResult = config.getParseResult();
BOOST_CHECK_EQUAL(parseResult.type(), GameConfig::ParseResult::INVALIDINPUTFILE);
} }
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()