1 /** 2 * Copyright c 2020 IBM Corporation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 #include <errno.h> 17 #include <stdio.h> 18 #include <stdlib.h> 19 #include <sys/wait.h> 20 21 #include <nlohmann/json.hpp> 22 23 #include <fstream> 24 25 #include <gtest/gtest.h> 26 27 #define EXPECT_FILE_VALID(configFile) expectFileValid(configFile) 28 #define EXPECT_FILE_INVALID(configFile, expectedErrorMessage, \ 29 expectedOutputMessage) \ 30 expectFileInvalid(configFile, expectedErrorMessage, expectedOutputMessage) 31 #define EXPECT_JSON_VALID(configFileJson) expectJsonValid(configFileJson) 32 #define EXPECT_JSON_INVALID(configFileJson, expectedErrorMessage, \ 33 expectedOutputMessage) \ 34 expectJsonInvalid(configFileJson, expectedErrorMessage, \ 35 expectedOutputMessage) 36 37 using json = nlohmann::json; 38 39 const json validConfigFile = R"( 40 { 41 "comments": [ "Config file for a FooBar one-chassis system" ], 42 43 "rules": [ 44 { 45 "comments": [ "Sets output voltage for a PMBus regulator rail" ], 46 "id": "set_voltage_rule", 47 "actions": [ 48 { 49 "pmbus_write_vout_command": { 50 "format": "linear" 51 } 52 } 53 ] 54 } 55 ], 56 57 "chassis": [ 58 { 59 "comments": [ "Chassis number 1 containing CPUs and memory" ], 60 "number": 1, 61 "devices": [ 62 { 63 "comments": [ "IR35221 regulator producing the Vdd rail" ], 64 "id": "vdd_regulator", 65 "is_regulator": true, 66 "fru": "/system/chassis/motherboard/regulator1", 67 "i2c_interface": { 68 "bus": 1, 69 "address": "0x70" 70 }, 71 "rails": [ 72 { 73 "comments": [ "Vdd rail" ], 74 "id": "vdd", 75 "configuration": { 76 "volts": 1.03, 77 "rule_id": "set_voltage_rule" 78 }, 79 "sensor_monitoring": { 80 "rule_id": "read_sensors_rule" 81 } 82 } 83 ] 84 } 85 ] 86 } 87 ] 88 } 89 )"_json; 90 91 std::string createTmpFile() 92 { 93 // create temporary file using mkstemp under /tmp/. random name for XXXXXX 94 char fileName[] = "/tmp/temp-XXXXXX"; 95 int fd = mkstemp(fileName); 96 if (fd == -1) 97 { 98 perror("Can't create temporary file"); 99 } 100 close(fd); 101 return fileName; 102 } 103 104 std::string getValidationToolCommand(const std::string& configFileName) 105 { 106 std::string command = "python ../tools/validate-regulators-config.py -s \ 107 ../schema/config_schema.json -c "; 108 command += configFileName; 109 return command; 110 } 111 112 int runToolForOutput(const std::string& configFileName, std::string& output, 113 bool isReadingStderr = false) 114 { 115 // run the validation tool with the temporary file and return the output 116 // of the validation tool. 117 std::string command = getValidationToolCommand(configFileName); 118 // reading the stderr while isReadingStderr is true. 119 if (isReadingStderr == true) 120 { 121 command += " 2>&1 >/dev/null"; 122 } 123 // get the jsonschema print from validation tool. 124 char buffer[256]; 125 std::string result = ""; 126 // to get the stdout from the validation tool. 127 FILE* pipe = popen(command.c_str(), "r"); 128 if (!pipe) 129 { 130 throw std::runtime_error("popen() failed!"); 131 } 132 while (!std::feof(pipe)) 133 { 134 if (fgets(buffer, sizeof buffer, pipe) != NULL) 135 { 136 result += buffer; 137 } 138 } 139 int returnValue = pclose(pipe); 140 // Check if pclose() failed 141 if (returnValue == -1) 142 { 143 // unable to close pipe. Print error and exit function. 144 throw std::runtime_error("pclose() failed!"); 145 } 146 std::string firstLine = result.substr(0, result.find('\n')); 147 output = firstLine; 148 // Get command exit status from return value 149 int exitStatus = WEXITSTATUS(returnValue); 150 return exitStatus; 151 } 152 153 void expectFileValid(const std::string& configFileName) 154 { 155 std::string errorMessage = ""; 156 std::string outputMessage = ""; 157 EXPECT_EQ(runToolForOutput(configFileName, errorMessage, true), 0); 158 EXPECT_EQ(runToolForOutput(configFileName, outputMessage), 0); 159 EXPECT_EQ(errorMessage, ""); 160 EXPECT_EQ(outputMessage, ""); 161 } 162 163 void expectFileInvalid(const std::string& configFileName, 164 const std::string& expectedErrorMessage, 165 const std::string& expectedOutputMessage) 166 { 167 std::string errorMessage = ""; 168 std::string outputMessage = ""; 169 EXPECT_EQ(runToolForOutput(configFileName, errorMessage, true), 1); 170 EXPECT_EQ(runToolForOutput(configFileName, outputMessage), 1); 171 EXPECT_EQ(errorMessage, expectedErrorMessage); 172 EXPECT_EQ(outputMessage, expectedOutputMessage); 173 } 174 175 void expectJsonValid(const json configFileJson) 176 { 177 std::string fileName; 178 fileName = createTmpFile(); 179 std::string jsonData = configFileJson.dump(); 180 std::ofstream out(fileName); 181 out << jsonData; 182 out.close(); 183 184 EXPECT_FILE_VALID(fileName); 185 unlink(fileName.c_str()); 186 } 187 188 void expectJsonInvalid(const json configFileJson, 189 const std::string& expectedErrorMessage, 190 const std::string& expectedOutputMessage) 191 { 192 std::string fileName; 193 fileName = createTmpFile(); 194 std::string jsonData = configFileJson.dump(); 195 std::ofstream out(fileName); 196 out << jsonData; 197 out.close(); 198 199 EXPECT_FILE_INVALID(fileName, expectedErrorMessage, expectedOutputMessage); 200 unlink(fileName.c_str()); 201 } 202 203 TEST(ValidateRegulatorsConfigTest, Rule) 204 { 205 // valid test comments property, id property, 206 // action property specified. 207 { 208 json configFile = validConfigFile; 209 EXPECT_JSON_VALID(configFile); 210 } 211 212 // valid test rule with no comments 213 { 214 json configFile = validConfigFile; 215 configFile["rules"][0].erase("comments"); 216 EXPECT_JSON_VALID(configFile); 217 } 218 219 // invalid test comments property has invalid value type 220 { 221 json configFile = validConfigFile; 222 configFile["rules"][0]["comments"] = {1}; 223 EXPECT_JSON_INVALID(configFile, "Validation failed.", 224 "1 is not of type u'string'"); 225 } 226 227 // invalid test rule with no ID 228 { 229 json configFile = validConfigFile; 230 configFile["rules"][0].erase("id"); 231 EXPECT_JSON_INVALID(configFile, "Validation failed.", 232 "u'id' is a required property"); 233 } 234 235 // invalid test id property has invalid value type (not string) 236 { 237 json configFile = validConfigFile; 238 configFile["rules"][0]["id"] = true; 239 EXPECT_JSON_INVALID(configFile, "Validation failed.", 240 "True is not of type u'string'"); 241 } 242 243 // invalid test id property has invalid value 244 { 245 json configFile = validConfigFile; 246 configFile["rules"][0]["id"] = "foo%"; 247 EXPECT_JSON_INVALID(configFile, "Validation failed.", 248 "u'foo%' does not match u'^[A-Za-z0-9_]+$'"); 249 } 250 251 // invalid test rule with no actions property 252 { 253 json configFile = validConfigFile; 254 configFile["rules"][0].erase("actions"); 255 EXPECT_JSON_INVALID(configFile, "Validation failed.", 256 "u'actions' is a required property"); 257 } 258 259 // valid test rule with multiple actions 260 { 261 json configFile = validConfigFile; 262 configFile["rules"][0]["actions"][1]["run_rule"] = 263 "set_page0_voltage_rule"; 264 EXPECT_JSON_VALID(configFile); 265 } 266 267 // invalid test actions property has invalid value type (not an array) 268 { 269 json configFile = validConfigFile; 270 configFile["rules"][0]["actions"] = 1; 271 EXPECT_JSON_INVALID(configFile, "Validation failed.", 272 "1 is not of type u'array'"); 273 } 274 275 // invalid test actions property has invalid value of action 276 { 277 json configFile = validConfigFile; 278 configFile["rules"][0]["actions"][0] = "foo"; 279 EXPECT_JSON_INVALID(configFile, "Validation failed.", 280 "u'foo' is not of type u'object'"); 281 } 282 283 // invalid test actions property has empty array 284 { 285 json configFile = validConfigFile; 286 configFile["rules"][0]["actions"] = json::array(); 287 EXPECT_JSON_INVALID(configFile, "Validation failed.", 288 "[] is too short"); 289 } 290 } 291 TEST(ValidateRegulatorsConfigTest, And) 292 { 293 // Valid. 294 { 295 json configFile = validConfigFile; 296 json andAction = 297 R"( 298 { 299 "and": [ 300 { "i2c_compare_byte": { "register": "0xA0", "value": "0x00" } }, 301 { "i2c_compare_byte": { "register": "0xA1", "value": "0x00" } } 302 ] 303 } 304 )"_json; 305 configFile["rules"][0]["actions"].push_back(andAction); 306 EXPECT_JSON_VALID(configFile); 307 } 308 309 // Invalid: actions property value is an empty array. 310 { 311 json configFile = validConfigFile; 312 json andAction = 313 R"( 314 { 315 "and": [] 316 } 317 )"_json; 318 configFile["rules"][0]["actions"].push_back(andAction); 319 EXPECT_JSON_INVALID(configFile, "Validation failed.", 320 "[] is too short"); 321 } 322 323 // Invalid: actions property has incorrect value data type. 324 { 325 json configFile = validConfigFile; 326 json andAction = 327 R"( 328 { 329 "and": true 330 } 331 )"_json; 332 configFile["rules"][0]["actions"].push_back(andAction); 333 EXPECT_JSON_INVALID(configFile, "Validation failed.", 334 "True is not of type u'array'"); 335 } 336 337 // Invalid: actions property value contains wrong element type 338 { 339 json configFile = validConfigFile; 340 json andAction = 341 R"( 342 { 343 "and": ["foo"] 344 } 345 )"_json; 346 configFile["rules"][0]["actions"].push_back(andAction); 347 EXPECT_JSON_INVALID(configFile, "Validation failed.", 348 "u'foo' is not of type u'object'"); 349 } 350 } 351 TEST(ValidateRegulatorsConfigTest, ComparePresence) 352 { 353 json comparePresenceFile = validConfigFile; 354 comparePresenceFile["rules"][0]["actions"][1]["compare_presence"]["fru"] = 355 "/system/chassis/motherboard/regulator2"; 356 comparePresenceFile["rules"][0]["actions"][1]["compare_presence"]["value"] = 357 true; 358 // Valid. 359 { 360 json configFile = comparePresenceFile; 361 EXPECT_JSON_VALID(configFile); 362 } 363 364 // Invalid: no FRU property. 365 { 366 json configFile = comparePresenceFile; 367 configFile["rules"][0]["actions"][1]["compare_presence"].erase("fru"); 368 EXPECT_JSON_INVALID(configFile, "Validation failed.", 369 "u'fru' is a required property"); 370 } 371 372 // Invalid: FRU property length is string less than 1. 373 { 374 json configFile = comparePresenceFile; 375 configFile["rules"][0]["actions"][1]["compare_presence"]["fru"] = ""; 376 EXPECT_JSON_INVALID(configFile, "Validation failed.", 377 "u'' is too short"); 378 } 379 380 // Invalid: no value property. 381 { 382 json configFile = comparePresenceFile; 383 configFile["rules"][0]["actions"][1]["compare_presence"].erase("value"); 384 EXPECT_JSON_INVALID(configFile, "Validation failed.", 385 "u'value' is a required property"); 386 } 387 388 // Invalid: value property type is not boolean. 389 { 390 json configFile = comparePresenceFile; 391 configFile["rules"][0]["actions"][1]["compare_presence"]["value"] = "1"; 392 EXPECT_JSON_INVALID(configFile, "Validation failed.", 393 "u'1' is not of type u'boolean'"); 394 } 395 396 // Invalid: FRU property type is not string. 397 { 398 json configFile = comparePresenceFile; 399 configFile["rules"][0]["actions"][1]["compare_presence"]["fru"] = 1; 400 EXPECT_JSON_INVALID(configFile, "Validation failed.", 401 "1 is not of type u'string'"); 402 } 403 } 404 TEST(ValidateRegulatorsConfigTest, CompareVpd) 405 { 406 json compareVpdFile = validConfigFile; 407 compareVpdFile["rules"][0]["actions"][1]["compare_vpd"]["fru"] = 408 "/system/chassis/motherboard/regulator2"; 409 compareVpdFile["rules"][0]["actions"][1]["compare_vpd"]["keyword"] = "CCIN"; 410 compareVpdFile["rules"][0]["actions"][1]["compare_vpd"]["value"] = "2D35"; 411 412 // Valid. 413 { 414 json configFile = compareVpdFile; 415 EXPECT_JSON_VALID(configFile); 416 } 417 418 // Invalid: no FRU property. 419 { 420 json configFile = compareVpdFile; 421 configFile["rules"][0]["actions"][1]["compare_vpd"].erase("fru"); 422 EXPECT_JSON_INVALID(configFile, "Validation failed.", 423 "u'fru' is a required property"); 424 } 425 426 // Invalid: no keyword property. 427 { 428 json configFile = compareVpdFile; 429 configFile["rules"][0]["actions"][1]["compare_vpd"].erase("keyword"); 430 EXPECT_JSON_INVALID(configFile, "Validation failed.", 431 "u'keyword' is a required property"); 432 } 433 434 // Invalid: no value property. 435 { 436 json configFile = compareVpdFile; 437 configFile["rules"][0]["actions"][1]["compare_vpd"].erase("value"); 438 EXPECT_JSON_INVALID(configFile, "Validation failed.", 439 "u'value' is a required property"); 440 } 441 442 // Invalid: property FRU wrong type. 443 { 444 json configFile = compareVpdFile; 445 configFile["rules"][0]["actions"][1]["compare_vpd"]["fru"] = 1; 446 EXPECT_JSON_INVALID(configFile, "Validation failed.", 447 "1 is not of type u'string'"); 448 } 449 450 // Invalid: property FRU is string less than 1. 451 { 452 json configFile = compareVpdFile; 453 configFile["rules"][0]["actions"][1]["compare_vpd"]["fru"] = ""; 454 EXPECT_JSON_INVALID(configFile, "Validation failed.", 455 "u'' is too short"); 456 } 457 458 // Invalid: property keyword is not "CCIN", "Manufacturer", "Model", 459 // "PartNumber" 460 { 461 json configFile = compareVpdFile; 462 configFile["rules"][0]["actions"][1]["compare_vpd"]["keyword"] = 463 "Number"; 464 EXPECT_JSON_INVALID(configFile, "Validation failed.", 465 "u'Number' is not one of [u'CCIN', " 466 "u'Manufacturer', u'Model', u'PartNumber']"); 467 } 468 469 // Invalid: property value wrong type. 470 { 471 json configFile = compareVpdFile; 472 configFile["rules"][0]["actions"][1]["compare_vpd"]["value"] = 1; 473 EXPECT_JSON_INVALID(configFile, "Validation failed.", 474 "1 is not of type u'string'"); 475 } 476 } 477