#include "infrastructure/application_settings_loader.h" #include "support/test_support.h" #include #include #include #include #include #include #include #include namespace { using TestSupport::require; void writeUtf8(const QString &path, const QByteArray &contents) { QFile file(path); require(file.open(QIODevice::WriteOnly), "test configuration file must open"); require(file.write(contents) == contents.size(), "test configuration file must be written completely"); } bool containsMessage( const ApplicationSettingsLoadResult &result, const std::string &fragment) { return std::any_of( result.messages.cbegin(), result.messages.cend(), [&fragment](const std::string &message) { return message.find(fragment) != std::string::npos; }); } QByteArray readAll(const QString &path) { QFile file(path); require(file.open(QIODevice::ReadOnly), "test configuration file must open for reading"); return file.readAll(); } QByteArray completeConfiguration() { return QByteArrayLiteral( "[Config]\n" "Version=1\n" "[ProjectLimits]\n" "MaxHmiPages=8\n" "MaxHmiControlsPerPage=24\n" "MaxAlarmDefinitions=40\n" "MaxControlLogics=6\n" "MaxRungsPerLogic=30\n" "MaxOutputMessages=120\n" "[HmiDefaults]\n" "DefaultPageWidth=1024\n" "DefaultPageHeight=600\n" "[PlcDefaults]\n" "PortName=COM7\n" "ServerAddress=2\n" "BaudRate=19200\n" "DataBits=7\n" "Parity=3\n" "StopBits=2\n"); } void testMissingFileCreatesDefaults() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); const QString path = directory.filePath("config/application.ini"); const ApplicationSettingsLoadResult result = ApplicationSettingsLoader::load(path); require(QFile::exists(path), "a missing application.ini must be created"); require(!result.warningRequired, "successful first-run configuration creation must not warn"); require(result.settings.projectLimits.maximumHmiPages == 32U && result.settings.hmiDefaults.pageWidth == 800 && result.settings.plcDefaults.portName == "COM3", "first run must use code defaults"); const QByteArray created = readAll(path); require(created.contains("[HmiDefaults]\n") && created.contains("DefaultPageWidth=800\n") && created.contains("DefaultPageHeight=400\n") && created.contains("[PlcDefaults]\n") && created.contains("PortName=COM3\n"), "the generated file must expose HMI dimensions and PLC defaults"); require(containsMessage(result, "已创建默认配置文件"), "first run must report the created file"); } void testCompleteConfigurationLoadsEveryPublicField() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); const QString path = directory.filePath("application.ini"); writeUtf8(path, completeConfiguration()); const ApplicationSettingsLoadResult result = ApplicationSettingsLoader::load(path); const ApplicationSettings &settings = result.settings; require(!result.warningRequired, "a valid configuration must be accepted"); require(settings.projectLimits.maximumHmiPages == 8U && settings.projectLimits.maximumHmiControlsPerPage == 24U && settings.projectLimits.maximumAlarmDefinitions == 40U && settings.projectLimits.maximumControlLogics == 6U && settings.projectLimits.maximumRungsPerLogic == 30U && settings.projectLimits.maximumOutputMessages == 120, "all configurable project limits must be loaded"); require(settings.hmiDefaults.pageWidth == 1024 && settings.hmiDefaults.pageHeight == 600, "HMI default dimensions must be loaded"); require(settings.plcDefaults.portName == "COM7" && settings.plcDefaults.serverAddress == 2 && settings.plcDefaults.baudRate == 19200 && settings.plcDefaults.dataBits == 7 && settings.plcDefaults.parity == 3 && settings.plcDefaults.stopBits == 2, "PLC defaults must be loaded"); } void testMissingAndUnknownFieldsDoNotInvalidateConfiguration() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); const QString path = directory.filePath("application.ini"); writeUtf8( path, QByteArrayLiteral( "[Config]\nVersion=1\n" "[ProjectLimits]\nMaxHmiPages=5\nFutureLimit=99\n")); const ApplicationSettingsLoadResult result = ApplicationSettingsLoader::load(path); require(!result.warningRequired && result.settings.projectLimits.maximumHmiPages == 5U && result.settings.projectLimits.maximumAlarmDefinitions == 256U, "missing fields must default without discarding valid values"); require(containsMessage(result, "未知字段") && containsMessage(result, "缺失"), "missing and unknown fields must be reported"); } void testDuplicateOrInvalidFieldsRejectTheWholeFile() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); const QString duplicate_path = directory.filePath("duplicate.ini"); QByteArray duplicate = completeConfiguration(); duplicate.append("[ProjectLimits]\nMaxHmiPages=4\n"); writeUtf8(duplicate_path, duplicate); const ApplicationSettingsLoadResult duplicate_result = ApplicationSettingsLoader::load(duplicate_path); require(duplicate_result.warningRequired && duplicate_result.settings.projectLimits.maximumHmiPages == 32U && containsMessage(duplicate_result, "重复"), "a duplicate field must reject the entire configuration"); const QString invalid_path = directory.filePath("invalid.ini"); QByteArray invalid = completeConfiguration(); invalid.replace("DefaultPageWidth=1024", "DefaultPageWidth=2000"); writeUtf8(invalid_path, invalid); const ApplicationSettingsLoadResult invalid_result = ApplicationSettingsLoader::load(invalid_path); require(invalid_result.warningRequired && invalid_result.settings.hmiDefaults.pageWidth == 800 && invalid_result.settings.plcDefaults.portName == "COM3", "one invalid value must roll every section back to code defaults"); const QString non_integer_path = directory.filePath("non-integer.ini"); QByteArray non_integer = completeConfiguration(); non_integer.replace("MaxHmiPages=8", "MaxHmiPages=8pages"); writeUtf8(non_integer_path, non_integer); const ApplicationSettingsLoadResult non_integer_result = ApplicationSettingsLoader::load(non_integer_path); require(non_integer_result.warningRequired && non_integer_result.settings.projectLimits.maximumHmiPages == 32U && containsMessage(non_integer_result, "8pages"), "quantity settings must reject values that are not pure integers"); } void testVersionAndEncodingErrorsRejectTheWholeFile() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); const QString version_path = directory.filePath("future-version.ini"); QByteArray future_version = completeConfiguration(); future_version.replace("Version=1", "Version=2"); writeUtf8(version_path, future_version); const ApplicationSettingsLoadResult version_result = ApplicationSettingsLoader::load(version_path); require(version_result.warningRequired && version_result.settings.projectLimits.maximumHmiPages == 32U && containsMessage(version_result, "Config.Version") && containsMessage(version_result, "允许范围为 1~1"), "an incompatible configuration version must reject the whole file"); const QString encoding_path = directory.filePath("invalid-utf8.ini"); QByteArray invalid_utf8("[Config]\nVersion=1\n# "); invalid_utf8.append(static_cast(0xFF)); writeUtf8(encoding_path, invalid_utf8); const ApplicationSettingsLoadResult encoding_result = ApplicationSettingsLoader::load(encoding_path); require(encoding_result.warningRequired && containsMessage(encoding_result, "UTF-8") && encoding_result.settings.plcDefaults.portName == "COM3", "invalid UTF-8 must fall back to all code defaults"); } void testCreationFailureKeepsTheApplicationStartable() { QTemporaryDir directory; require(directory.isValid(), "temporary directory must be valid"); const QString blocker = directory.filePath("not-a-directory"); writeUtf8(blocker, QByteArrayLiteral("block")); const ApplicationSettingsLoadResult result = ApplicationSettingsLoader::load(blocker + "/application.ini"); require(result.warningRequired && result.settings.projectLimits.maximumOutputMessages == 1000 && containsMessage(result, "使用代码默认值"), "configuration creation failure must fall back without aborting startup"); } } // namespace int main(int argc, char *argv[]) { QCoreApplication application(argc, argv); try { testMissingFileCreatesDefaults(); testCompleteConfigurationLoadsEveryPublicField(); testMissingAndUnknownFieldsDoNotInvalidateConfiguration(); testDuplicateOrInvalidFieldsRejectTheWholeFile(); testVersionAndEncodingErrorsRejectTheWholeFile(); testCreationFailureKeepsTheApplicationStartable(); } catch (const std::exception &error) { std::cerr << "application settings tests failed: " << error.what() << '\n'; return 1; } std::cout << "application settings tests passed\n"; return 0; }