/** * Copyright © 2024 IBM Corporation * * Licensed 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 "config_file_parser.hpp" #include "config_file_parser_error.hpp" #include "rail.hpp" #include "temporary_file.hpp" #include "temporary_subdirectory.hpp" #include // for chmod() #include #include #include #include #include #include #include #include #include #include #include using namespace phosphor::power::sequencer; using namespace phosphor::power::sequencer::config_file_parser; using namespace phosphor::power::sequencer::config_file_parser::internal; using namespace phosphor::power::util; using json = nlohmann::json; namespace fs = std::filesystem; void writeConfigFile(const fs::path& pathName, const std::string& contents) { std::ofstream file{pathName}; file << contents; } void writeConfigFile(const fs::path& pathName, const json& contents) { std::ofstream file{pathName}; file << contents; } TEST(ConfigFileParserTests, Find) { std::vector compatibleSystemTypes{ "com.acme.Hardware.Chassis.Model.MegaServer4CPU", "com.acme.Hardware.Chassis.Model.MegaServer", "com.acme.Hardware.Chassis.Model.Server"}; // Test where works: Fully qualified system type: First in list { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer4CPU.json"; writeConfigFile(configFilePath, std::string{""}); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_EQ(pathFound, configFilePath); } // Test where works: Fully qualified system type: Second in list { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer.json"; writeConfigFile(configFilePath, std::string{""}); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_EQ(pathFound, configFilePath); } // Test where works: Last node in system type: Second in list { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "MegaServer.json"; writeConfigFile(configFilePath, std::string{""}); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_EQ(pathFound, configFilePath); } // Test where works: Last node in system type: Last in list { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "Server.json"; writeConfigFile(configFilePath, std::string{""}); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_EQ(pathFound, configFilePath); } // Test where works: System type has no '.' { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "Server.json"; writeConfigFile(configFilePath, std::string{""}); std::vector noDotSystemTypes{"MegaServer4CPU", "MegaServer", "Server"}; fs::path pathFound = find(noDotSystemTypes, configFileDirPath); EXPECT_EQ(pathFound, configFilePath); } // Test where fails: System type list is empty { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "Server.json"; writeConfigFile(configFilePath, std::string{""}); std::vector emptySystemTypes{}; fs::path pathFound = find(emptySystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: Configuration file directory is empty { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: Configuration file directory does not exist { fs::path configFileDirPath{"/tmp/does_not_exist_XYZ"}; fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: Configuration file directory is not readable { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::permissions(configFileDirPath, fs::perms::none); EXPECT_THROW(find(compatibleSystemTypes, configFileDirPath), std::exception); fs::permissions(configFileDirPath, fs::perms::owner_all); } // Test where fails: No matching file name found { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer"; writeConfigFile(configFilePath, std::string{""}); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: Matching file name is a directory: Fully qualified { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "com.acme.Hardware.Chassis.Model.MegaServer4CPU.json"; fs::create_directory(configFilePath); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: Matching file name is a directory: Last node { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "MegaServer.json"; fs::create_directory(configFilePath); fs::path pathFound = find(compatibleSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: System type has no '.' { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "MegaServer2CPU.json"; writeConfigFile(configFilePath, std::string{""}); std::vector noDotSystemTypes{"MegaServer4CPU", "MegaServer", "Server", ""}; fs::path pathFound = find(noDotSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } // Test where fails: System type ends with '.' { TemporarySubDirectory configFileDir; fs::path configFileDirPath = configFileDir.getPath(); fs::path configFilePath = configFileDirPath; configFilePath /= "MegaServer4CPU.json"; writeConfigFile(configFilePath, std::string{""}); std::vector dotAtEndSystemTypes{ "com.acme.Hardware.Chassis.Model.MegaServer4CPU.", "a.", "."}; fs::path pathFound = find(dotAtEndSystemTypes, configFileDirPath); EXPECT_TRUE(pathFound.empty()); } } TEST(ConfigFileParserTests, Parse) { // Test where works { const json configFileContents = R"( { "rails": [ { "name": "VDD_CPU0", "page": 11, "check_status_vout": true }, { "name": "VCS_CPU1", "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1", "gpio": { "line": 60 } } ] } )"_json; TemporaryFile configFile; fs::path pathName{configFile.getPath()}; writeConfigFile(pathName, configFileContents); std::vector> rails = parse(pathName); EXPECT_EQ(rails.size(), 2); EXPECT_EQ(rails[0]->getName(), "VDD_CPU0"); EXPECT_EQ(rails[1]->getName(), "VCS_CPU1"); } // Test where fails: File does not exist { fs::path pathName{"/tmp/non_existent_file"}; EXPECT_THROW(parse(pathName), ConfigFileParserError); } // Test where fails: File is not readable { const json configFileContents = R"( { "rails": [ { "name": "VDD_CPU0" } ] } )"_json; TemporaryFile configFile; fs::path pathName{configFile.getPath()}; writeConfigFile(pathName, configFileContents); chmod(pathName.c_str(), 0222); EXPECT_THROW(parse(pathName), ConfigFileParserError); } // Test where fails: File is not valid JSON { const std::string configFileContents = "] foo ["; TemporaryFile configFile; fs::path pathName{configFile.getPath()}; writeConfigFile(pathName, configFileContents); EXPECT_THROW(parse(pathName), ConfigFileParserError); } // Test where fails: JSON does not conform to config file format { const json configFileContents = R"( [ "foo", "bar" ] )"_json; TemporaryFile configFile; fs::path pathName{configFile.getPath()}; writeConfigFile(pathName, configFileContents); EXPECT_THROW(parse(pathName), ConfigFileParserError); } } TEST(ConfigFileParserTests, GetRequiredProperty) { // Test where property exists { const json element = R"( { "name": "VDD_CPU0" } )"_json; const json& propertyElement = getRequiredProperty(element, "name"); EXPECT_EQ(propertyElement.get(), "VDD_CPU0"); } // Test where property does not exist try { const json element = R"( { "foo": 23 } )"_json; getRequiredProperty(element, "name"); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Required property missing: name"); } } TEST(ConfigFileParserTests, ParseBoolean) { // Test where works: true { const json element = R"( true )"_json; bool value = parseBoolean(element); EXPECT_EQ(value, true); } // Test where works: false { const json element = R"( false )"_json; bool value = parseBoolean(element); EXPECT_EQ(value, false); } // Test where fails: Element is not a boolean try { const json element = R"( 1 )"_json; parseBoolean(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a boolean"); } } TEST(ConfigFileParserTests, ParseGPIO) { // Test where works: Only required properties specified { const json element = R"( { "line": 60 } )"_json; GPIO gpio = parseGPIO(element); EXPECT_EQ(gpio.line, 60); EXPECT_FALSE(gpio.activeLow); } // Test where works: All properties specified { const json element = R"( { "line": 131, "active_low": true } )"_json; GPIO gpio = parseGPIO(element); EXPECT_EQ(gpio.line, 131); EXPECT_TRUE(gpio.activeLow); } // Test where fails: Element is not an object try { const json element = R"( [ "vdda", "vddb" ] )"_json; parseGPIO(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an object"); } // Test where fails: Required line property not specified try { const json element = R"( { "active_low": true } )"_json; parseGPIO(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Required property missing: line"); } // Test where fails: line value is invalid try { const json element = R"( { "line": -131, "active_low": true } )"_json; parseGPIO(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an unsigned integer"); } // Test where fails: active_low value is invalid try { const json element = R"( { "line": 131, "active_low": "true" } )"_json; parseGPIO(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a boolean"); } // Test where fails: Invalid property specified try { const json element = R"( { "line": 131, "foo": "bar" } )"_json; parseGPIO(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element contains an invalid property"); } } TEST(ConfigFileParserTests, ParseRail) { // Test where works: Only required properties specified { const json element = R"( { "name": "VDD_CPU0" } )"_json; std::unique_ptr rail = parseRail(element); EXPECT_EQ(rail->getName(), "VDD_CPU0"); EXPECT_FALSE(rail->getPresence().has_value()); EXPECT_FALSE(rail->getPage().has_value()); EXPECT_FALSE(rail->isPowerSupplyRail()); EXPECT_FALSE(rail->getCheckStatusVout()); EXPECT_FALSE(rail->getCompareVoltageToLimit()); EXPECT_FALSE(rail->getGPIO().has_value()); } // Test where works: All properties specified { const json element = R"( { "name": "12.0VB", "presence": "/xyz/openbmc_project/inventory/system/chassis/powersupply1", "page": 11, "is_power_supply_rail": true, "check_status_vout": true, "compare_voltage_to_limit": true, "gpio": { "line": 60, "active_low": true } } )"_json; std::unique_ptr rail = parseRail(element); EXPECT_EQ(rail->getName(), "12.0VB"); EXPECT_TRUE(rail->getPresence().has_value()); EXPECT_EQ(rail->getPresence().value(), "/xyz/openbmc_project/inventory/system/chassis/powersupply1"); EXPECT_TRUE(rail->getPage().has_value()); EXPECT_EQ(rail->getPage().value(), 11); EXPECT_TRUE(rail->isPowerSupplyRail()); EXPECT_TRUE(rail->getCheckStatusVout()); EXPECT_TRUE(rail->getCompareVoltageToLimit()); EXPECT_TRUE(rail->getGPIO().has_value()); EXPECT_EQ(rail->getGPIO().value().line, 60); EXPECT_TRUE(rail->getGPIO().value().activeLow); } // Test where fails: Element is not an object try { const json element = R"( [ "vdda", "vddb" ] )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an object"); } // Test where fails: Required name property not specified try { const json element = R"( { "page": 11 } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Required property missing: name"); } // Test where fails: name value is invalid try { const json element = R"( { "name": 31, "page": 11 } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a string"); } // Test where fails: presence value is invalid try { const json element = R"( { "name": "VCS_CPU1", "presence": false } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a string"); } // Test where fails: page value is invalid try { const json element = R"( { "name": "VCS_CPU1", "page": 256 } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer"); } // Test where fails: is_power_supply_rail value is invalid try { const json element = R"( { "name": "12.0VA", "is_power_supply_rail": "true" } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a boolean"); } // Test where fails: check_status_vout value is invalid try { const json element = R"( { "name": "VCS_CPU1", "check_status_vout": "false" } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a boolean"); } // Test where fails: compare_voltage_to_limit value is invalid try { const json element = R"( { "name": "VCS_CPU1", "compare_voltage_to_limit": 23 } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a boolean"); } // Test where fails: gpio value is invalid try { const json element = R"( { "name": "VCS_CPU1", "gpio": 131 } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an object"); } // Test where fails: check_status_vout is true and page not specified try { const json element = R"( { "name": "VCS_CPU1", "check_status_vout": true } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Required property missing: page"); } // Test where fails: compare_voltage_to_limit is true and page not // specified try { const json element = R"( { "name": "VCS_CPU1", "compare_voltage_to_limit": true } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Required property missing: page"); } // Test where fails: Invalid property specified try { const json element = R"( { "name": "VCS_CPU1", "foo": "bar" } )"_json; parseRail(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element contains an invalid property"); } } TEST(ConfigFileParserTests, ParseRailArray) { // Test where works: Array is empty { const json element = R"( [ ] )"_json; std::vector> rails = parseRailArray(element); EXPECT_EQ(rails.size(), 0); } // Test where works: Array is not empty { const json element = R"( [ { "name": "VDD_CPU0" }, { "name": "VCS_CPU1" } ] )"_json; std::vector> rails = parseRailArray(element); EXPECT_EQ(rails.size(), 2); EXPECT_EQ(rails[0]->getName(), "VDD_CPU0"); EXPECT_EQ(rails[1]->getName(), "VCS_CPU1"); } // Test where fails: Element is not an array try { const json element = R"( { "foo": "bar" } )"_json; parseRailArray(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an array"); } // Test where fails: Element within array is invalid try { const json element = R"( [ { "name": "VDD_CPU0" }, 23 ] )"_json; parseRailArray(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an object"); } } TEST(ConfigFileParserTests, ParseRoot) { // Test where works { const json element = R"( { "rails": [ { "name": "VDD_CPU0", "page": 11, "check_status_vout": true }, { "name": "VCS_CPU1", "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1", "gpio": { "line": 60 } } ] } )"_json; std::vector> rails = parseRoot(element); EXPECT_EQ(rails.size(), 2); EXPECT_EQ(rails[0]->getName(), "VDD_CPU0"); EXPECT_EQ(rails[1]->getName(), "VCS_CPU1"); } // Test where fails: Element is not an object try { const json element = R"( [ "VDD_CPU0", "VCS_CPU1" ] )"_json; parseRoot(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an object"); } // Test where fails: Required rails property not specified try { const json element = R"( { } )"_json; parseRoot(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Required property missing: rails"); } // Test where fails: rails value is invalid try { const json element = R"( { "rails": 31 } )"_json; parseRoot(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an array"); } // Test where fails: Invalid property specified try { const json element = R"( { "rails": [ { "name": "VDD_CPU0", "page": 11, "check_status_vout": true } ], "foo": true } )"_json; parseRoot(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element contains an invalid property"); } } TEST(ConfigFileParserTests, ParseString) { // Test where works: Empty string { const json element = ""; std::string value = parseString(element, true); EXPECT_EQ(value, ""); } // Test where works: Non-empty string { const json element = "vdd_cpu1"; std::string value = parseString(element, false); EXPECT_EQ(value, "vdd_cpu1"); } // Test where fails: Element is not a string try { const json element = R"( { "foo": "bar" } )"_json; parseString(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not a string"); } // Test where fails: Empty string try { const json element = ""; parseString(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element contains an empty string"); } } TEST(ConfigFileParserTests, ParseUint8) { // Test where works: 0 { const json element = R"( 0 )"_json; uint8_t value = parseUint8(element); EXPECT_EQ(value, 0); } // Test where works: UINT8_MAX { const json element = R"( 255 )"_json; uint8_t value = parseUint8(element); EXPECT_EQ(value, 255); } // Test where fails: Element is not an integer try { const json element = R"( 1.03 )"_json; parseUint8(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an integer"); } // Test where fails: Value < 0 try { const json element = R"( -1 )"_json; parseUint8(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer"); } // Test where fails: Value > UINT8_MAX try { const json element = R"( 256 )"_json; parseUint8(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer"); } } TEST(ConfigFileParserTests, ParseUnsignedInteger) { // Test where works: 1 { const json element = R"( 1 )"_json; unsigned int value = parseUnsignedInteger(element); EXPECT_EQ(value, 1); } // Test where fails: Element is not an integer try { const json element = R"( 1.5 )"_json; parseUnsignedInteger(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an unsigned integer"); } // Test where fails: Value < 0 try { const json element = R"( -1 )"_json; parseUnsignedInteger(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an unsigned integer"); } } TEST(ConfigFileParserTests, VerifyIsArray) { // Test where element is an array { const json element = R"( [ "foo", "bar" ] )"_json; verifyIsArray(element); } // Test where element is not an array try { const json element = R"( { "foo": "bar" } )"_json; verifyIsArray(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an array"); } } TEST(ConfigFileParserTests, VerifyIsObject) { // Test where element is an object { const json element = R"( { "foo": "bar" } )"_json; verifyIsObject(element); } // Test where element is not an object try { const json element = R"( [ "foo", "bar" ] )"_json; verifyIsObject(element); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element is not an object"); } } TEST(ConfigFileParserTests, VerifyPropertyCount) { // Test where element has expected number of properties { const json element = R"( { "line": 131, "active_low": true } )"_json; verifyPropertyCount(element, 2); } // Test where element has unexpected number of properties try { const json element = R"( { "line": 131, "active_low": true, "foo": 1.3 } )"_json; verifyPropertyCount(element, 2); ADD_FAILURE() << "Should not have reached this line."; } catch (const std::invalid_argument& e) { EXPECT_STREQ(e.what(), "Element contains an invalid property"); } }