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->getCheckStatusVout());
310         EXPECT_FALSE(rail->getCompareVoltageToLimits());
311         EXPECT_FALSE(rail->getGPIO().has_value());
312     }
313 
314     // Test where works: All properties specified
315     {
316         const json element = R"(
317             {
318                 "name": "VCS_CPU1",
319                 "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
320                 "page": 11,
321                 "check_status_vout": true,
322                 "compare_voltage_to_limits": true,
323                 "gpio": { "line": 60, "active_low": true }
324             }
325         )"_json;
326         std::unique_ptr<Rail> rail = parseRail(element);
327         EXPECT_EQ(rail->getName(), "VCS_CPU1");
328         EXPECT_TRUE(rail->getPresence().has_value());
329         EXPECT_EQ(
330             rail->getPresence().value(),
331             "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1");
332         EXPECT_TRUE(rail->getPage().has_value());
333         EXPECT_EQ(rail->getPage().value(), 11);
334         EXPECT_TRUE(rail->getCheckStatusVout());
335         EXPECT_TRUE(rail->getCompareVoltageToLimits());
336         EXPECT_TRUE(rail->getGPIO().has_value());
337         EXPECT_EQ(rail->getGPIO().value().line, 60);
338         EXPECT_TRUE(rail->getGPIO().value().activeLow);
339     }
340 
341     // Test where fails: Element is not an object
342     try
343     {
344         const json element = R"( [ "vdda", "vddb" ] )"_json;
345         parseRail(element);
346         ADD_FAILURE() << "Should not have reached this line.";
347     }
348     catch (const std::invalid_argument& e)
349     {
350         EXPECT_STREQ(e.what(), "Element is not an object");
351     }
352 
353     // Test where fails: Required name property not specified
354     try
355     {
356         const json element = R"(
357             {
358                 "page": 11
359             }
360         )"_json;
361         parseRail(element);
362         ADD_FAILURE() << "Should not have reached this line.";
363     }
364     catch (const std::invalid_argument& e)
365     {
366         EXPECT_STREQ(e.what(), "Required property missing: name");
367     }
368 
369     // Test where fails: name value is invalid
370     try
371     {
372         const json element = R"(
373             {
374                 "name": 31,
375                 "page": 11
376             }
377         )"_json;
378         parseRail(element);
379         ADD_FAILURE() << "Should not have reached this line.";
380     }
381     catch (const std::invalid_argument& e)
382     {
383         EXPECT_STREQ(e.what(), "Element is not a string");
384     }
385 
386     // Test where fails: presence value is invalid
387     try
388     {
389         const json element = R"(
390             {
391                 "name": "VCS_CPU1",
392                 "presence": false
393             }
394         )"_json;
395         parseRail(element);
396         ADD_FAILURE() << "Should not have reached this line.";
397     }
398     catch (const std::invalid_argument& e)
399     {
400         EXPECT_STREQ(e.what(), "Element is not a string");
401     }
402 
403     // Test where fails: page value is invalid
404     try
405     {
406         const json element = R"(
407             {
408                 "name": "VCS_CPU1",
409                 "page": 256
410             }
411         )"_json;
412         parseRail(element);
413         ADD_FAILURE() << "Should not have reached this line.";
414     }
415     catch (const std::invalid_argument& e)
416     {
417         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
418     }
419 
420     // Test where fails: check_status_vout value is invalid
421     try
422     {
423         const json element = R"(
424             {
425                 "name": "VCS_CPU1",
426                 "check_status_vout": "false"
427             }
428         )"_json;
429         parseRail(element);
430         ADD_FAILURE() << "Should not have reached this line.";
431     }
432     catch (const std::invalid_argument& e)
433     {
434         EXPECT_STREQ(e.what(), "Element is not a boolean");
435     }
436 
437     // Test where fails: compare_voltage_to_limits value is invalid
438     try
439     {
440         const json element = R"(
441             {
442                 "name": "VCS_CPU1",
443                 "compare_voltage_to_limits": 23
444             }
445         )"_json;
446         parseRail(element);
447         ADD_FAILURE() << "Should not have reached this line.";
448     }
449     catch (const std::invalid_argument& e)
450     {
451         EXPECT_STREQ(e.what(), "Element is not a boolean");
452     }
453 
454     // Test where fails: gpio value is invalid
455     try
456     {
457         const json element = R"(
458             {
459                 "name": "VCS_CPU1",
460                 "gpio": 131
461             }
462         )"_json;
463         parseRail(element);
464         ADD_FAILURE() << "Should not have reached this line.";
465     }
466     catch (const std::invalid_argument& e)
467     {
468         EXPECT_STREQ(e.what(), "Element is not an object");
469     }
470 
471     // Test where fails: check_status_vout is true and page not specified
472     try
473     {
474         const json element = R"(
475             {
476                 "name": "VCS_CPU1",
477                 "check_status_vout": true
478             }
479         )"_json;
480         parseRail(element);
481         ADD_FAILURE() << "Should not have reached this line.";
482     }
483     catch (const std::invalid_argument& e)
484     {
485         EXPECT_STREQ(e.what(), "Required property missing: page");
486     }
487 
488     // Test where fails: compare_voltage_to_limits is true and page not
489     // specified
490     try
491     {
492         const json element = R"(
493             {
494                 "name": "VCS_CPU1",
495                 "compare_voltage_to_limits": true
496             }
497         )"_json;
498         parseRail(element);
499         ADD_FAILURE() << "Should not have reached this line.";
500     }
501     catch (const std::invalid_argument& e)
502     {
503         EXPECT_STREQ(e.what(), "Required property missing: page");
504     }
505 
506     // Test where fails: Invalid property specified
507     try
508     {
509         const json element = R"(
510             {
511                 "name": "VCS_CPU1",
512                 "foo": "bar"
513             }
514         )"_json;
515         parseRail(element);
516         ADD_FAILURE() << "Should not have reached this line.";
517     }
518     catch (const std::invalid_argument& e)
519     {
520         EXPECT_STREQ(e.what(), "Element contains an invalid property");
521     }
522 }
523 
524 TEST(ConfigFileParserTests, ParseRailArray)
525 {
526     // Test where works: Array is empty
527     {
528         const json element = R"(
529             [
530             ]
531         )"_json;
532         std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
533         EXPECT_EQ(rails.size(), 0);
534     }
535 
536     // Test where works: Array is not empty
537     {
538         const json element = R"(
539             [
540                 { "name": "VDD_CPU0" },
541                 { "name": "VCS_CPU1" }
542             ]
543         )"_json;
544         std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
545         EXPECT_EQ(rails.size(), 2);
546         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
547         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
548     }
549 
550     // Test where fails: Element is not an array
551     try
552     {
553         const json element = R"(
554             {
555                 "foo": "bar"
556             }
557         )"_json;
558         parseRailArray(element);
559         ADD_FAILURE() << "Should not have reached this line.";
560     }
561     catch (const std::invalid_argument& e)
562     {
563         EXPECT_STREQ(e.what(), "Element is not an array");
564     }
565 
566     // Test where fails: Element within array is invalid
567     try
568     {
569         const json element = R"(
570             [
571                 { "name": "VDD_CPU0" },
572                 23
573             ]
574         )"_json;
575         parseRailArray(element);
576         ADD_FAILURE() << "Should not have reached this line.";
577     }
578     catch (const std::invalid_argument& e)
579     {
580         EXPECT_STREQ(e.what(), "Element is not an object");
581     }
582 }
583 
584 TEST(ConfigFileParserTests, ParseRoot)
585 {
586     // Test where works
587     {
588         const json element = R"(
589             {
590                 "rails": [
591                     {
592                         "name": "VDD_CPU0",
593                         "page": 11,
594                         "check_status_vout": true
595                     },
596                     {
597                         "name": "VCS_CPU1",
598                         "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
599                         "gpio": { "line": 60 }
600                     }
601                 ]
602             }
603         )"_json;
604         std::vector<std::unique_ptr<Rail>> rails = parseRoot(element);
605         EXPECT_EQ(rails.size(), 2);
606         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
607         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
608     }
609 
610     // Test where fails: Element is not an object
611     try
612     {
613         const json element = R"( [ "VDD_CPU0", "VCS_CPU1" ] )"_json;
614         parseRoot(element);
615         ADD_FAILURE() << "Should not have reached this line.";
616     }
617     catch (const std::invalid_argument& e)
618     {
619         EXPECT_STREQ(e.what(), "Element is not an object");
620     }
621 
622     // Test where fails: Required rails property not specified
623     try
624     {
625         const json element = R"(
626             {
627             }
628         )"_json;
629         parseRoot(element);
630         ADD_FAILURE() << "Should not have reached this line.";
631     }
632     catch (const std::invalid_argument& e)
633     {
634         EXPECT_STREQ(e.what(), "Required property missing: rails");
635     }
636 
637     // Test where fails: rails value is invalid
638     try
639     {
640         const json element = R"(
641             {
642                 "rails": 31
643             }
644         )"_json;
645         parseRoot(element);
646         ADD_FAILURE() << "Should not have reached this line.";
647     }
648     catch (const std::invalid_argument& e)
649     {
650         EXPECT_STREQ(e.what(), "Element is not an array");
651     }
652 
653     // Test where fails: Invalid property specified
654     try
655     {
656         const json element = R"(
657             {
658                 "rails": [
659                     {
660                         "name": "VDD_CPU0",
661                         "page": 11,
662                         "check_status_vout": true
663                     }
664                 ],
665                 "foo": true
666             }
667         )"_json;
668         parseRoot(element);
669         ADD_FAILURE() << "Should not have reached this line.";
670     }
671     catch (const std::invalid_argument& e)
672     {
673         EXPECT_STREQ(e.what(), "Element contains an invalid property");
674     }
675 }
676 
677 TEST(ConfigFileParserTests, ParseString)
678 {
679     // Test where works: Empty string
680     {
681         const json element = "";
682         std::string value = parseString(element, true);
683         EXPECT_EQ(value, "");
684     }
685 
686     // Test where works: Non-empty string
687     {
688         const json element = "vdd_cpu1";
689         std::string value = parseString(element, false);
690         EXPECT_EQ(value, "vdd_cpu1");
691     }
692 
693     // Test where fails: Element is not a string
694     try
695     {
696         const json element = R"( { "foo": "bar" } )"_json;
697         parseString(element);
698         ADD_FAILURE() << "Should not have reached this line.";
699     }
700     catch (const std::invalid_argument& e)
701     {
702         EXPECT_STREQ(e.what(), "Element is not a string");
703     }
704 
705     // Test where fails: Empty string
706     try
707     {
708         const json element = "";
709         parseString(element);
710         ADD_FAILURE() << "Should not have reached this line.";
711     }
712     catch (const std::invalid_argument& e)
713     {
714         EXPECT_STREQ(e.what(), "Element contains an empty string");
715     }
716 }
717 
718 TEST(ConfigFileParserTests, ParseUint8)
719 {
720     // Test where works: 0
721     {
722         const json element = R"( 0 )"_json;
723         uint8_t value = parseUint8(element);
724         EXPECT_EQ(value, 0);
725     }
726 
727     // Test where works: UINT8_MAX
728     {
729         const json element = R"( 255 )"_json;
730         uint8_t value = parseUint8(element);
731         EXPECT_EQ(value, 255);
732     }
733 
734     // Test where fails: Element is not an integer
735     try
736     {
737         const json element = R"( 1.03 )"_json;
738         parseUint8(element);
739         ADD_FAILURE() << "Should not have reached this line.";
740     }
741     catch (const std::invalid_argument& e)
742     {
743         EXPECT_STREQ(e.what(), "Element is not an integer");
744     }
745 
746     // Test where fails: Value < 0
747     try
748     {
749         const json element = R"( -1 )"_json;
750         parseUint8(element);
751         ADD_FAILURE() << "Should not have reached this line.";
752     }
753     catch (const std::invalid_argument& e)
754     {
755         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
756     }
757 
758     // Test where fails: Value > UINT8_MAX
759     try
760     {
761         const json element = R"( 256 )"_json;
762         parseUint8(element);
763         ADD_FAILURE() << "Should not have reached this line.";
764     }
765     catch (const std::invalid_argument& e)
766     {
767         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
768     }
769 }
770 
771 TEST(ConfigFileParserTests, ParseUnsignedInteger)
772 {
773     // Test where works: 1
774     {
775         const json element = R"( 1 )"_json;
776         unsigned int value = parseUnsignedInteger(element);
777         EXPECT_EQ(value, 1);
778     }
779 
780     // Test where fails: Element is not an integer
781     try
782     {
783         const json element = R"( 1.5 )"_json;
784         parseUnsignedInteger(element);
785         ADD_FAILURE() << "Should not have reached this line.";
786     }
787     catch (const std::invalid_argument& e)
788     {
789         EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
790     }
791 
792     // Test where fails: Value < 0
793     try
794     {
795         const json element = R"( -1 )"_json;
796         parseUnsignedInteger(element);
797         ADD_FAILURE() << "Should not have reached this line.";
798     }
799     catch (const std::invalid_argument& e)
800     {
801         EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
802     }
803 }
804 
805 TEST(ConfigFileParserTests, VerifyIsArray)
806 {
807     // Test where element is an array
808     {
809         const json element = R"( [ "foo", "bar" ] )"_json;
810         verifyIsArray(element);
811     }
812 
813     // Test where element is not an array
814     try
815     {
816         const json element = R"( { "foo": "bar" } )"_json;
817         verifyIsArray(element);
818         ADD_FAILURE() << "Should not have reached this line.";
819     }
820     catch (const std::invalid_argument& e)
821     {
822         EXPECT_STREQ(e.what(), "Element is not an array");
823     }
824 }
825 
826 TEST(ConfigFileParserTests, VerifyIsObject)
827 {
828     // Test where element is an object
829     {
830         const json element = R"( { "foo": "bar" } )"_json;
831         verifyIsObject(element);
832     }
833 
834     // Test where element is not an object
835     try
836     {
837         const json element = R"( [ "foo", "bar" ] )"_json;
838         verifyIsObject(element);
839         ADD_FAILURE() << "Should not have reached this line.";
840     }
841     catch (const std::invalid_argument& e)
842     {
843         EXPECT_STREQ(e.what(), "Element is not an object");
844     }
845 }
846 
847 TEST(ConfigFileParserTests, VerifyPropertyCount)
848 {
849     // Test where element has expected number of properties
850     {
851         const json element = R"(
852             {
853                 "line": 131,
854                 "active_low": true
855             }
856         )"_json;
857         verifyPropertyCount(element, 2);
858     }
859 
860     // Test where element has unexpected number of properties
861     try
862     {
863         const json element = R"(
864             {
865                 "line": 131,
866                 "active_low": true,
867                 "foo": 1.3
868             }
869         )"_json;
870         verifyPropertyCount(element, 2);
871         ADD_FAILURE() << "Should not have reached this line.";
872     }
873     catch (const std::invalid_argument& e)
874     {
875         EXPECT_STREQ(e.what(), "Element contains an invalid property");
876     }
877 }
878