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