1 /**
2  * Copyright © 2024 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 "config_file_parser.hpp"
17 #include "config_file_parser_error.hpp"
18 #include "rail.hpp"
19 #include "temporary_file.hpp"
20 
21 #include <sys/stat.h> // for chmod()
22 
23 #include <nlohmann/json.hpp>
24 
25 #include <cstdint>
26 #include <exception>
27 #include <filesystem>
28 #include <fstream>
29 #include <memory>
30 #include <optional>
31 #include <stdexcept>
32 #include <string>
33 #include <vector>
34 
35 #include <gtest/gtest.h>
36 
37 using namespace phosphor::power::sequencer;
38 using namespace phosphor::power::sequencer::config_file_parser;
39 using namespace phosphor::power::sequencer::config_file_parser::internal;
40 using json = nlohmann::json;
41 using TemporaryFile = phosphor::power::util::TemporaryFile;
42 
43 void writeConfigFile(const std::filesystem::path& pathName,
44                      const std::string& contents)
45 {
46     std::ofstream file{pathName};
47     file << contents;
48 }
49 
50 void writeConfigFile(const std::filesystem::path& pathName,
51                      const json& contents)
52 {
53     std::ofstream file{pathName};
54     file << contents;
55 }
56 
57 TEST(ConfigFileParserTests, Parse)
58 {
59     // Test where works
60     {
61         const json configFileContents = R"(
62             {
63                 "rails": [
64                     {
65                         "name": "VDD_CPU0",
66                         "page": 11,
67                         "check_status_vout": true
68                     },
69                     {
70                         "name": "VCS_CPU1",
71                         "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
72                         "gpio": { "line": 60 }
73                     }
74                 ]
75             }
76         )"_json;
77 
78         TemporaryFile configFile;
79         std::filesystem::path pathName{configFile.getPath()};
80         writeConfigFile(pathName, configFileContents);
81 
82         std::vector<std::unique_ptr<Rail>> rails = parse(pathName);
83 
84         EXPECT_EQ(rails.size(), 2);
85         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
86         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
87     }
88 
89     // Test where fails: File does not exist
90     {
91         std::filesystem::path pathName{"/tmp/non_existent_file"};
92         EXPECT_THROW(parse(pathName), ConfigFileParserError);
93     }
94 
95     // Test where fails: File is not readable
96     {
97         const json configFileContents = R"(
98             {
99                 "rails": [
100                     {
101                         "name": "VDD_CPU0"
102                     }
103                 ]
104             }
105         )"_json;
106 
107         TemporaryFile configFile;
108         std::filesystem::path pathName{configFile.getPath()};
109         writeConfigFile(pathName, configFileContents);
110 
111         chmod(pathName.c_str(), 0222);
112         EXPECT_THROW(parse(pathName), ConfigFileParserError);
113     }
114 
115     // Test where fails: File is not valid JSON
116     {
117         const std::string configFileContents = "] foo [";
118 
119         TemporaryFile configFile;
120         std::filesystem::path pathName{configFile.getPath()};
121         writeConfigFile(pathName, configFileContents);
122 
123         EXPECT_THROW(parse(pathName), ConfigFileParserError);
124     }
125 
126     // Test where fails: JSON does not conform to config file format
127     {
128         const json configFileContents = R"( [ "foo", "bar" ] )"_json;
129 
130         TemporaryFile configFile;
131         std::filesystem::path pathName{configFile.getPath()};
132         writeConfigFile(pathName, configFileContents);
133 
134         EXPECT_THROW(parse(pathName), ConfigFileParserError);
135     }
136 }
137 
138 TEST(ConfigFileParserTests, GetRequiredProperty)
139 {
140     // Test where property exists
141     {
142         const json element = R"( { "name": "VDD_CPU0" } )"_json;
143         const json& propertyElement = getRequiredProperty(element, "name");
144         EXPECT_EQ(propertyElement.get<std::string>(), "VDD_CPU0");
145     }
146 
147     // Test where property does not exist
148     try
149     {
150         const json element = R"( { "foo": 23 } )"_json;
151         getRequiredProperty(element, "name");
152         ADD_FAILURE() << "Should not have reached this line.";
153     }
154     catch (const std::invalid_argument& e)
155     {
156         EXPECT_STREQ(e.what(), "Required property missing: name");
157     }
158 }
159 
160 TEST(ConfigFileParserTests, ParseBoolean)
161 {
162     // Test where works: true
163     {
164         const json element = R"( true )"_json;
165         bool value = parseBoolean(element);
166         EXPECT_EQ(value, true);
167     }
168 
169     // Test where works: false
170     {
171         const json element = R"( false )"_json;
172         bool value = parseBoolean(element);
173         EXPECT_EQ(value, false);
174     }
175 
176     // Test where fails: Element is not a boolean
177     try
178     {
179         const json element = R"( 1 )"_json;
180         parseBoolean(element);
181         ADD_FAILURE() << "Should not have reached this line.";
182     }
183     catch (const std::invalid_argument& e)
184     {
185         EXPECT_STREQ(e.what(), "Element is not a boolean");
186     }
187 }
188 
189 TEST(ConfigFileParserTests, ParseGPIO)
190 {
191     // Test where works: Only required properties specified
192     {
193         const json element = R"(
194             {
195                 "line": 60
196             }
197         )"_json;
198         GPIO gpio = parseGPIO(element);
199         EXPECT_EQ(gpio.line, 60);
200         EXPECT_FALSE(gpio.activeLow);
201     }
202 
203     // Test where works: All properties specified
204     {
205         const json element = R"(
206             {
207                 "line": 131,
208                 "active_low": true
209             }
210         )"_json;
211         GPIO gpio = parseGPIO(element);
212         EXPECT_EQ(gpio.line, 131);
213         EXPECT_TRUE(gpio.activeLow);
214     }
215 
216     // Test where fails: Element is not an object
217     try
218     {
219         const json element = R"( [ "vdda", "vddb" ] )"_json;
220         parseGPIO(element);
221         ADD_FAILURE() << "Should not have reached this line.";
222     }
223     catch (const std::invalid_argument& e)
224     {
225         EXPECT_STREQ(e.what(), "Element is not an object");
226     }
227 
228     // Test where fails: Required line property not specified
229     try
230     {
231         const json element = R"(
232             {
233                 "active_low": true
234             }
235         )"_json;
236         parseGPIO(element);
237         ADD_FAILURE() << "Should not have reached this line.";
238     }
239     catch (const std::invalid_argument& e)
240     {
241         EXPECT_STREQ(e.what(), "Required property missing: line");
242     }
243 
244     // Test where fails: line value is invalid
245     try
246     {
247         const json element = R"(
248             {
249                 "line": -131,
250                 "active_low": true
251             }
252         )"_json;
253         parseGPIO(element);
254         ADD_FAILURE() << "Should not have reached this line.";
255     }
256     catch (const std::invalid_argument& e)
257     {
258         EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
259     }
260 
261     // Test where fails: active_low value is invalid
262     try
263     {
264         const json element = R"(
265             {
266                 "line": 131,
267                 "active_low": "true"
268             }
269         )"_json;
270         parseGPIO(element);
271         ADD_FAILURE() << "Should not have reached this line.";
272     }
273     catch (const std::invalid_argument& e)
274     {
275         EXPECT_STREQ(e.what(), "Element is not a boolean");
276     }
277 
278     // Test where fails: Invalid property specified
279     try
280     {
281         const json element = R"(
282             {
283                 "line": 131,
284                 "foo": "bar"
285             }
286         )"_json;
287         parseGPIO(element);
288         ADD_FAILURE() << "Should not have reached this line.";
289     }
290     catch (const std::invalid_argument& e)
291     {
292         EXPECT_STREQ(e.what(), "Element contains an invalid property");
293     }
294 }
295 
296 TEST(ConfigFileParserTests, ParseRail)
297 {
298     // Test where works: Only required properties specified
299     {
300         const json element = R"(
301             {
302                 "name": "VDD_CPU0"
303             }
304         )"_json;
305         std::unique_ptr<Rail> rail = parseRail(element);
306         EXPECT_EQ(rail->getName(), "VDD_CPU0");
307         EXPECT_FALSE(rail->getPresence().has_value());
308         EXPECT_FALSE(rail->getPage().has_value());
309         EXPECT_FALSE(rail->isPowerSupplyRail());
310         EXPECT_FALSE(rail->getCheckStatusVout());
311         EXPECT_FALSE(rail->getCompareVoltageToLimit());
312         EXPECT_FALSE(rail->getGPIO().has_value());
313     }
314 
315     // Test where works: All properties specified
316     {
317         const json element = R"(
318             {
319                 "name": "12.0VB",
320                 "presence": "/xyz/openbmc_project/inventory/system/chassis/powersupply1",
321                 "page": 11,
322                 "is_power_supply_rail": true,
323                 "check_status_vout": true,
324                 "compare_voltage_to_limit": true,
325                 "gpio": { "line": 60, "active_low": true }
326             }
327         )"_json;
328         std::unique_ptr<Rail> rail = parseRail(element);
329         EXPECT_EQ(rail->getName(), "12.0VB");
330         EXPECT_TRUE(rail->getPresence().has_value());
331         EXPECT_EQ(rail->getPresence().value(),
332                   "/xyz/openbmc_project/inventory/system/chassis/powersupply1");
333         EXPECT_TRUE(rail->getPage().has_value());
334         EXPECT_EQ(rail->getPage().value(), 11);
335         EXPECT_TRUE(rail->isPowerSupplyRail());
336         EXPECT_TRUE(rail->getCheckStatusVout());
337         EXPECT_TRUE(rail->getCompareVoltageToLimit());
338         EXPECT_TRUE(rail->getGPIO().has_value());
339         EXPECT_EQ(rail->getGPIO().value().line, 60);
340         EXPECT_TRUE(rail->getGPIO().value().activeLow);
341     }
342 
343     // Test where fails: Element is not an object
344     try
345     {
346         const json element = R"( [ "vdda", "vddb" ] )"_json;
347         parseRail(element);
348         ADD_FAILURE() << "Should not have reached this line.";
349     }
350     catch (const std::invalid_argument& e)
351     {
352         EXPECT_STREQ(e.what(), "Element is not an object");
353     }
354 
355     // Test where fails: Required name property not specified
356     try
357     {
358         const json element = R"(
359             {
360                 "page": 11
361             }
362         )"_json;
363         parseRail(element);
364         ADD_FAILURE() << "Should not have reached this line.";
365     }
366     catch (const std::invalid_argument& e)
367     {
368         EXPECT_STREQ(e.what(), "Required property missing: name");
369     }
370 
371     // Test where fails: name value is invalid
372     try
373     {
374         const json element = R"(
375             {
376                 "name": 31,
377                 "page": 11
378             }
379         )"_json;
380         parseRail(element);
381         ADD_FAILURE() << "Should not have reached this line.";
382     }
383     catch (const std::invalid_argument& e)
384     {
385         EXPECT_STREQ(e.what(), "Element is not a string");
386     }
387 
388     // Test where fails: presence value is invalid
389     try
390     {
391         const json element = R"(
392             {
393                 "name": "VCS_CPU1",
394                 "presence": false
395             }
396         )"_json;
397         parseRail(element);
398         ADD_FAILURE() << "Should not have reached this line.";
399     }
400     catch (const std::invalid_argument& e)
401     {
402         EXPECT_STREQ(e.what(), "Element is not a string");
403     }
404 
405     // Test where fails: page value is invalid
406     try
407     {
408         const json element = R"(
409             {
410                 "name": "VCS_CPU1",
411                 "page": 256
412             }
413         )"_json;
414         parseRail(element);
415         ADD_FAILURE() << "Should not have reached this line.";
416     }
417     catch (const std::invalid_argument& e)
418     {
419         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
420     }
421 
422     // Test where fails: is_power_supply_rail value is invalid
423     try
424     {
425         const json element = R"(
426             {
427                 "name": "12.0VA",
428                 "is_power_supply_rail": "true"
429             }
430         )"_json;
431         parseRail(element);
432         ADD_FAILURE() << "Should not have reached this line.";
433     }
434     catch (const std::invalid_argument& e)
435     {
436         EXPECT_STREQ(e.what(), "Element is not a boolean");
437     }
438 
439     // Test where fails: check_status_vout value is invalid
440     try
441     {
442         const json element = R"(
443             {
444                 "name": "VCS_CPU1",
445                 "check_status_vout": "false"
446             }
447         )"_json;
448         parseRail(element);
449         ADD_FAILURE() << "Should not have reached this line.";
450     }
451     catch (const std::invalid_argument& e)
452     {
453         EXPECT_STREQ(e.what(), "Element is not a boolean");
454     }
455 
456     // Test where fails: compare_voltage_to_limit value is invalid
457     try
458     {
459         const json element = R"(
460             {
461                 "name": "VCS_CPU1",
462                 "compare_voltage_to_limit": 23
463             }
464         )"_json;
465         parseRail(element);
466         ADD_FAILURE() << "Should not have reached this line.";
467     }
468     catch (const std::invalid_argument& e)
469     {
470         EXPECT_STREQ(e.what(), "Element is not a boolean");
471     }
472 
473     // Test where fails: gpio value is invalid
474     try
475     {
476         const json element = R"(
477             {
478                 "name": "VCS_CPU1",
479                 "gpio": 131
480             }
481         )"_json;
482         parseRail(element);
483         ADD_FAILURE() << "Should not have reached this line.";
484     }
485     catch (const std::invalid_argument& e)
486     {
487         EXPECT_STREQ(e.what(), "Element is not an object");
488     }
489 
490     // Test where fails: check_status_vout is true and page not specified
491     try
492     {
493         const json element = R"(
494             {
495                 "name": "VCS_CPU1",
496                 "check_status_vout": true
497             }
498         )"_json;
499         parseRail(element);
500         ADD_FAILURE() << "Should not have reached this line.";
501     }
502     catch (const std::invalid_argument& e)
503     {
504         EXPECT_STREQ(e.what(), "Required property missing: page");
505     }
506 
507     // Test where fails: compare_voltage_to_limit is true and page not
508     // specified
509     try
510     {
511         const json element = R"(
512             {
513                 "name": "VCS_CPU1",
514                 "compare_voltage_to_limit": true
515             }
516         )"_json;
517         parseRail(element);
518         ADD_FAILURE() << "Should not have reached this line.";
519     }
520     catch (const std::invalid_argument& e)
521     {
522         EXPECT_STREQ(e.what(), "Required property missing: page");
523     }
524 
525     // Test where fails: Invalid property specified
526     try
527     {
528         const json element = R"(
529             {
530                 "name": "VCS_CPU1",
531                 "foo": "bar"
532             }
533         )"_json;
534         parseRail(element);
535         ADD_FAILURE() << "Should not have reached this line.";
536     }
537     catch (const std::invalid_argument& e)
538     {
539         EXPECT_STREQ(e.what(), "Element contains an invalid property");
540     }
541 }
542 
543 TEST(ConfigFileParserTests, ParseRailArray)
544 {
545     // Test where works: Array is empty
546     {
547         const json element = R"(
548             [
549             ]
550         )"_json;
551         std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
552         EXPECT_EQ(rails.size(), 0);
553     }
554 
555     // Test where works: Array is not empty
556     {
557         const json element = R"(
558             [
559                 { "name": "VDD_CPU0" },
560                 { "name": "VCS_CPU1" }
561             ]
562         )"_json;
563         std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
564         EXPECT_EQ(rails.size(), 2);
565         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
566         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
567     }
568 
569     // Test where fails: Element is not an array
570     try
571     {
572         const json element = R"(
573             {
574                 "foo": "bar"
575             }
576         )"_json;
577         parseRailArray(element);
578         ADD_FAILURE() << "Should not have reached this line.";
579     }
580     catch (const std::invalid_argument& e)
581     {
582         EXPECT_STREQ(e.what(), "Element is not an array");
583     }
584 
585     // Test where fails: Element within array is invalid
586     try
587     {
588         const json element = R"(
589             [
590                 { "name": "VDD_CPU0" },
591                 23
592             ]
593         )"_json;
594         parseRailArray(element);
595         ADD_FAILURE() << "Should not have reached this line.";
596     }
597     catch (const std::invalid_argument& e)
598     {
599         EXPECT_STREQ(e.what(), "Element is not an object");
600     }
601 }
602 
603 TEST(ConfigFileParserTests, ParseRoot)
604 {
605     // Test where works
606     {
607         const json element = R"(
608             {
609                 "rails": [
610                     {
611                         "name": "VDD_CPU0",
612                         "page": 11,
613                         "check_status_vout": true
614                     },
615                     {
616                         "name": "VCS_CPU1",
617                         "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
618                         "gpio": { "line": 60 }
619                     }
620                 ]
621             }
622         )"_json;
623         std::vector<std::unique_ptr<Rail>> rails = parseRoot(element);
624         EXPECT_EQ(rails.size(), 2);
625         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
626         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
627     }
628 
629     // Test where fails: Element is not an object
630     try
631     {
632         const json element = R"( [ "VDD_CPU0", "VCS_CPU1" ] )"_json;
633         parseRoot(element);
634         ADD_FAILURE() << "Should not have reached this line.";
635     }
636     catch (const std::invalid_argument& e)
637     {
638         EXPECT_STREQ(e.what(), "Element is not an object");
639     }
640 
641     // Test where fails: Required rails property not specified
642     try
643     {
644         const json element = R"(
645             {
646             }
647         )"_json;
648         parseRoot(element);
649         ADD_FAILURE() << "Should not have reached this line.";
650     }
651     catch (const std::invalid_argument& e)
652     {
653         EXPECT_STREQ(e.what(), "Required property missing: rails");
654     }
655 
656     // Test where fails: rails value is invalid
657     try
658     {
659         const json element = R"(
660             {
661                 "rails": 31
662             }
663         )"_json;
664         parseRoot(element);
665         ADD_FAILURE() << "Should not have reached this line.";
666     }
667     catch (const std::invalid_argument& e)
668     {
669         EXPECT_STREQ(e.what(), "Element is not an array");
670     }
671 
672     // Test where fails: Invalid property specified
673     try
674     {
675         const json element = R"(
676             {
677                 "rails": [
678                     {
679                         "name": "VDD_CPU0",
680                         "page": 11,
681                         "check_status_vout": true
682                     }
683                 ],
684                 "foo": true
685             }
686         )"_json;
687         parseRoot(element);
688         ADD_FAILURE() << "Should not have reached this line.";
689     }
690     catch (const std::invalid_argument& e)
691     {
692         EXPECT_STREQ(e.what(), "Element contains an invalid property");
693     }
694 }
695 
696 TEST(ConfigFileParserTests, ParseString)
697 {
698     // Test where works: Empty string
699     {
700         const json element = "";
701         std::string value = parseString(element, true);
702         EXPECT_EQ(value, "");
703     }
704 
705     // Test where works: Non-empty string
706     {
707         const json element = "vdd_cpu1";
708         std::string value = parseString(element, false);
709         EXPECT_EQ(value, "vdd_cpu1");
710     }
711 
712     // Test where fails: Element is not a string
713     try
714     {
715         const json element = R"( { "foo": "bar" } )"_json;
716         parseString(element);
717         ADD_FAILURE() << "Should not have reached this line.";
718     }
719     catch (const std::invalid_argument& e)
720     {
721         EXPECT_STREQ(e.what(), "Element is not a string");
722     }
723 
724     // Test where fails: Empty string
725     try
726     {
727         const json element = "";
728         parseString(element);
729         ADD_FAILURE() << "Should not have reached this line.";
730     }
731     catch (const std::invalid_argument& e)
732     {
733         EXPECT_STREQ(e.what(), "Element contains an empty string");
734     }
735 }
736 
737 TEST(ConfigFileParserTests, ParseUint8)
738 {
739     // Test where works: 0
740     {
741         const json element = R"( 0 )"_json;
742         uint8_t value = parseUint8(element);
743         EXPECT_EQ(value, 0);
744     }
745 
746     // Test where works: UINT8_MAX
747     {
748         const json element = R"( 255 )"_json;
749         uint8_t value = parseUint8(element);
750         EXPECT_EQ(value, 255);
751     }
752 
753     // Test where fails: Element is not an integer
754     try
755     {
756         const json element = R"( 1.03 )"_json;
757         parseUint8(element);
758         ADD_FAILURE() << "Should not have reached this line.";
759     }
760     catch (const std::invalid_argument& e)
761     {
762         EXPECT_STREQ(e.what(), "Element is not an integer");
763     }
764 
765     // Test where fails: Value < 0
766     try
767     {
768         const json element = R"( -1 )"_json;
769         parseUint8(element);
770         ADD_FAILURE() << "Should not have reached this line.";
771     }
772     catch (const std::invalid_argument& e)
773     {
774         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
775     }
776 
777     // Test where fails: Value > UINT8_MAX
778     try
779     {
780         const json element = R"( 256 )"_json;
781         parseUint8(element);
782         ADD_FAILURE() << "Should not have reached this line.";
783     }
784     catch (const std::invalid_argument& e)
785     {
786         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
787     }
788 }
789 
790 TEST(ConfigFileParserTests, ParseUnsignedInteger)
791 {
792     // Test where works: 1
793     {
794         const json element = R"( 1 )"_json;
795         unsigned int value = parseUnsignedInteger(element);
796         EXPECT_EQ(value, 1);
797     }
798 
799     // Test where fails: Element is not an integer
800     try
801     {
802         const json element = R"( 1.5 )"_json;
803         parseUnsignedInteger(element);
804         ADD_FAILURE() << "Should not have reached this line.";
805     }
806     catch (const std::invalid_argument& e)
807     {
808         EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
809     }
810 
811     // Test where fails: Value < 0
812     try
813     {
814         const json element = R"( -1 )"_json;
815         parseUnsignedInteger(element);
816         ADD_FAILURE() << "Should not have reached this line.";
817     }
818     catch (const std::invalid_argument& e)
819     {
820         EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
821     }
822 }
823 
824 TEST(ConfigFileParserTests, VerifyIsArray)
825 {
826     // Test where element is an array
827     {
828         const json element = R"( [ "foo", "bar" ] )"_json;
829         verifyIsArray(element);
830     }
831 
832     // Test where element is not an array
833     try
834     {
835         const json element = R"( { "foo": "bar" } )"_json;
836         verifyIsArray(element);
837         ADD_FAILURE() << "Should not have reached this line.";
838     }
839     catch (const std::invalid_argument& e)
840     {
841         EXPECT_STREQ(e.what(), "Element is not an array");
842     }
843 }
844 
845 TEST(ConfigFileParserTests, VerifyIsObject)
846 {
847     // Test where element is an object
848     {
849         const json element = R"( { "foo": "bar" } )"_json;
850         verifyIsObject(element);
851     }
852 
853     // Test where element is not an object
854     try
855     {
856         const json element = R"( [ "foo", "bar" ] )"_json;
857         verifyIsObject(element);
858         ADD_FAILURE() << "Should not have reached this line.";
859     }
860     catch (const std::invalid_argument& e)
861     {
862         EXPECT_STREQ(e.what(), "Element is not an object");
863     }
864 }
865 
866 TEST(ConfigFileParserTests, VerifyPropertyCount)
867 {
868     // Test where element has expected number of properties
869     {
870         const json element = R"(
871             {
872                 "line": 131,
873                 "active_low": true
874             }
875         )"_json;
876         verifyPropertyCount(element, 2);
877     }
878 
879     // Test where element has unexpected number of properties
880     try
881     {
882         const json element = R"(
883             {
884                 "line": 131,
885                 "active_low": true,
886                 "foo": 1.3
887             }
888         )"_json;
889         verifyPropertyCount(element, 2);
890         ADD_FAILURE() << "Should not have reached this line.";
891     }
892     catch (const std::invalid_argument& e)
893     {
894         EXPECT_STREQ(e.what(), "Element contains an invalid property");
895     }
896 }
897