logging: use raw string literals in config tests
[folly.git] / folly / experimental / logging / test / ConfigParserTest.cpp
1 /*
2  * Copyright 2004-present Facebook, Inc.
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 <folly/String.h>
17 #include <folly/dynamic.h>
18 #include <folly/experimental/logging/LogCategory.h>
19 #include <folly/experimental/logging/LogConfig.h>
20 #include <folly/experimental/logging/LogConfigParser.h>
21 #include <folly/json.h>
22 #include <folly/portability/GMock.h>
23 #include <folly/portability/GTest.h>
24 #include <folly/test/TestUtils.h>
25
26 using namespace folly;
27
28 using ::testing::Pair;
29 using ::testing::UnorderedElementsAre;
30
31 namespace folly {
32 std::ostream& operator<<(std::ostream& os, const LogCategoryConfig& config) {
33   os << logLevelToString(config.level);
34   if (!config.inheritParentLevel) {
35     os << "!";
36   }
37   if (config.handlers.hasValue()) {
38     os << ":" << join(",", config.handlers.value());
39   }
40   return os;
41 }
42
43 std::ostream& operator<<(std::ostream& os, const LogHandlerConfig& config) {
44   os << (config.type ? config.type.value() : "[no type]");
45   bool first = true;
46   for (const auto& opt : config.options) {
47     if (!first) {
48       os << ",";
49     } else {
50       os << ":";
51       first = false;
52     }
53     os << opt.first << "=" << opt.second;
54   }
55   return os;
56 }
57 } // namespace folly
58
59 TEST(LogConfig, parseBasic) {
60   auto config = parseLogConfig("");
61   EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
62   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
63
64   config = parseLogConfig("   ");
65   EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
66   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
67
68   config = parseLogConfig(".=ERROR,folly=DBG2");
69   EXPECT_THAT(
70       config.getCategoryConfigs(),
71       UnorderedElementsAre(
72           Pair("", LogCategoryConfig{LogLevel::ERR, true}),
73           Pair("folly", LogCategoryConfig{LogLevel::DBG2, true})));
74   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
75
76   config = parseLogConfig(" INFO , folly  := FATAL   ");
77   EXPECT_THAT(
78       config.getCategoryConfigs(),
79       UnorderedElementsAre(
80           Pair("", LogCategoryConfig{LogLevel::INFO, true}),
81           Pair("folly", LogCategoryConfig{LogLevel::FATAL, false})));
82   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
83
84   config =
85       parseLogConfig("my.category:=INFO , my.other.stuff  := 19,foo.bar=DBG7");
86   EXPECT_THAT(
87       config.getCategoryConfigs(),
88       UnorderedElementsAre(
89           Pair("my.category", LogCategoryConfig{LogLevel::INFO, false}),
90           Pair(
91               "my.other.stuff",
92               LogCategoryConfig{static_cast<LogLevel>(19), false}),
93           Pair("foo.bar", LogCategoryConfig{LogLevel::DBG7, true})));
94   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
95
96   config = parseLogConfig(" ERR ");
97   EXPECT_THAT(
98       config.getCategoryConfigs(),
99       UnorderedElementsAre(Pair("", LogCategoryConfig{LogLevel::ERR, true})));
100   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
101
102   config = parseLogConfig(" ERR: ");
103   EXPECT_THAT(
104       config.getCategoryConfigs(),
105       UnorderedElementsAre(
106           Pair("", LogCategoryConfig{LogLevel::ERR, true, {}})));
107   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
108
109   config = parseLogConfig(" ERR:stderr; stderr=stream:stream=stderr ");
110   EXPECT_THAT(
111       config.getCategoryConfigs(),
112       UnorderedElementsAre(
113           Pair("", LogCategoryConfig{LogLevel::ERR, true, {"stderr"}})));
114   EXPECT_THAT(
115       config.getHandlerConfigs(),
116       UnorderedElementsAre(
117           Pair("stderr", LogHandlerConfig{"stream", {{"stream", "stderr"}}})));
118
119   config = parseLogConfig(
120       "ERR:myfile:custom, folly=DBG2, folly.io:=WARN:other;"
121       "myfile=file:path=/tmp/x.log; "
122       "custom=custom:foo=bar,hello=world,a = b = c; "
123       "other=custom2");
124   EXPECT_THAT(
125       config.getCategoryConfigs(),
126       UnorderedElementsAre(
127           Pair(
128               "", LogCategoryConfig{LogLevel::ERR, true, {"myfile", "custom"}}),
129           Pair("folly", LogCategoryConfig{LogLevel::DBG2, true}),
130           Pair(
131               "folly.io",
132               LogCategoryConfig{LogLevel::WARN, false, {"other"}})));
133   EXPECT_THAT(
134       config.getHandlerConfigs(),
135       UnorderedElementsAre(
136           Pair("myfile", LogHandlerConfig{"file", {{"path", "/tmp/x.log"}}}),
137           Pair(
138               "custom",
139               LogHandlerConfig{
140                   "custom",
141                   {{"foo", "bar"}, {"hello", "world"}, {"a", "b = c"}}}),
142           Pair("other", LogHandlerConfig{"custom2"})));
143
144   // Test updating existing handler configs, with no handler type
145   config = parseLogConfig("ERR;foo");
146   EXPECT_THAT(
147       config.getCategoryConfigs(),
148       UnorderedElementsAre(Pair("", LogCategoryConfig{LogLevel::ERR, true})));
149   EXPECT_THAT(
150       config.getHandlerConfigs(),
151       UnorderedElementsAre(Pair("foo", LogHandlerConfig{})));
152
153   config = parseLogConfig("ERR;foo:a=b,c=d");
154   EXPECT_THAT(
155       config.getCategoryConfigs(),
156       UnorderedElementsAre(Pair("", LogCategoryConfig{LogLevel::ERR, true})));
157   EXPECT_THAT(
158       config.getHandlerConfigs(),
159       UnorderedElementsAre(Pair(
160           "foo", LogHandlerConfig{folly::none, {{"a", "b"}, {"c", "d"}}})));
161
162   config = parseLogConfig("ERR;test=file:path=/tmp/test.log;foo:a=b,c=d");
163   EXPECT_THAT(
164       config.getCategoryConfigs(),
165       UnorderedElementsAre(Pair("", LogCategoryConfig{LogLevel::ERR, true})));
166   EXPECT_THAT(
167       config.getHandlerConfigs(),
168       UnorderedElementsAre(
169           Pair("foo", LogHandlerConfig{folly::none, {{"a", "b"}, {"c", "d"}}}),
170           Pair("test", LogHandlerConfig{"file", {{"path", "/tmp/test.log"}}})));
171
172   // Log handler changes with no category changes
173   config = parseLogConfig("; myhandler=custom:foo=bar");
174   EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
175   EXPECT_THAT(
176       config.getHandlerConfigs(),
177       UnorderedElementsAre(
178           Pair("myhandler", LogHandlerConfig{"custom", {{"foo", "bar"}}})));
179 }
180
181 TEST(LogConfig, parseBasicErrors) {
182   // Errors in the log category settings
183   EXPECT_THROW_RE(
184       parseLogConfig("=="),
185       LogConfigParseError,
186       R"(invalid log level "=" for category "")");
187   EXPECT_THROW_RE(
188       parseLogConfig("bogus_level"),
189       LogConfigParseError,
190       R"(invalid log level "bogus_level" for category ".")");
191   EXPECT_THROW_RE(
192       parseLogConfig("foo=bogus_level"),
193       LogConfigParseError,
194       R"(invalid log level "bogus_level" for category "foo")");
195   EXPECT_THROW_RE(
196       parseLogConfig("foo=WARN,bar=invalid"),
197       LogConfigParseError,
198       R"(invalid log level "invalid" for category "bar")");
199   EXPECT_THROW_RE(
200       parseLogConfig("foo=WARN,bar="),
201       LogConfigParseError,
202       R"(invalid log level "" for category "bar")");
203   EXPECT_THROW_RE(
204       parseLogConfig("foo=WARN,bar:="),
205       LogConfigParseError,
206       R"(invalid log level "" for category "bar")");
207   EXPECT_THROW_RE(
208       parseLogConfig("foo:=,bar:=WARN"),
209       LogConfigParseError,
210       R"(invalid log level "" for category "foo")");
211   EXPECT_THROW_RE(
212       parseLogConfig("x"),
213       LogConfigParseError,
214       R"(invalid log level "x" for category ".")");
215   EXPECT_THROW_RE(
216       parseLogConfig("x,y,z"),
217       LogConfigParseError,
218       R"(invalid log level "x" for category ".")");
219   EXPECT_THROW_RE(
220       parseLogConfig("foo=WARN,"),
221       LogConfigParseError,
222       R"(invalid log level "" for category ".")");
223   EXPECT_THROW_RE(
224       parseLogConfig("="),
225       LogConfigParseError,
226       R"(invalid log level "" for category "")");
227   EXPECT_THROW_RE(
228       parseLogConfig(":="),
229       LogConfigParseError,
230       R"(invalid log level "" for category "")");
231   EXPECT_THROW_RE(
232       parseLogConfig("foo=bar=ERR"),
233       LogConfigParseError,
234       R"(invalid log level "bar=ERR" for category "foo")");
235   EXPECT_THROW_RE(
236       parseLogConfig("foo.bar=ERR,foo..bar=INFO"),
237       LogConfigParseError,
238       R"(category "foo\.bar" listed multiple times under different names: )"
239       R"("foo\.+bar" and "foo\.+bar")");
240   EXPECT_THROW_RE(
241       parseLogConfig("=ERR,.=INFO"),
242       LogConfigParseError,
243       R"(category "" listed multiple times under different names: )"
244       R"("\.?" and "\.?")");
245
246   // Errors in the log handler settings
247   EXPECT_THROW_RE(
248       parseLogConfig("ERR;"),
249       LogConfigParseError,
250       "error parsing log handler configuration: empty log handler name");
251   EXPECT_THROW_RE(
252       parseLogConfig("ERR;foo="),
253       LogConfigParseError,
254       R"(error parsing configuration for log handler "foo": )"
255       "empty log handler type");
256   EXPECT_THROW_RE(
257       parseLogConfig("ERR;=file"),
258       LogConfigParseError,
259       "error parsing log handler configuration: empty log handler name");
260   EXPECT_THROW_RE(
261       parseLogConfig("ERR;handler1=file;"),
262       LogConfigParseError,
263       "error parsing log handler configuration: empty log handler name");
264   EXPECT_THROW_RE(
265       parseLogConfig("ERR;test=file,path=/tmp/test.log;foo:a=b,c=d"),
266       LogConfigParseError,
267       R"(error parsing configuration for log handler "test": )"
268       R"(invalid type "file,path=/tmp/test.log": type name cannot contain )"
269       "a comma when using the basic config format");
270   EXPECT_THROW_RE(
271       parseLogConfig("ERR;test,path=/tmp/test.log;foo:a=b,c=d"),
272       LogConfigParseError,
273       R"(error parsing configuration for log handler "test,path": )"
274       "name cannot contain a comma when using the basic config format");
275 }
276
277 TEST(LogConfig, parseJson) {
278   auto config = parseLogConfig("{}");
279   EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
280   config = parseLogConfig("  {}   ");
281   EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
282
283   config = parseLogConfig(R"JSON({
284     "categories": {
285       ".": "ERROR",
286       "folly": "DBG2",
287     }
288   })JSON");
289   EXPECT_THAT(
290       config.getCategoryConfigs(),
291       UnorderedElementsAre(
292           Pair("", LogCategoryConfig{LogLevel::ERR, true}),
293           Pair("folly", LogCategoryConfig{LogLevel::DBG2, true})));
294   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
295
296   config = parseLogConfig(R"JSON({
297     "categories": {
298       "": "ERROR",
299       "folly": "DBG2",
300     }
301   })JSON");
302   EXPECT_THAT(
303       config.getCategoryConfigs(),
304       UnorderedElementsAre(
305           Pair("", LogCategoryConfig{LogLevel::ERR, true}),
306           Pair("folly", LogCategoryConfig{LogLevel::DBG2, true})));
307   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
308
309   config = parseLogConfig(R"JSON({
310     "categories": {
311       ".": { "level": "INFO" },
312       "folly": { "level": "FATAL", "inherit": false },
313     }
314   })JSON");
315   EXPECT_THAT(
316       config.getCategoryConfigs(),
317       UnorderedElementsAre(
318           Pair("", LogCategoryConfig{LogLevel::INFO, true}),
319           Pair("folly", LogCategoryConfig{LogLevel::FATAL, false})));
320   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
321
322   config = parseLogConfig(R"JSON({
323     "categories": {
324       "my.category": { "level": "INFO", "inherit": true },
325       // comments are allowed
326       "my.other.stuff": { "level": 19, "inherit": false },
327       "foo.bar": { "level": "DBG7" },
328     },
329     "handlers": {
330       "h1": { "type": "custom", "options": {"foo": "bar", "a": "z"} }
331     }
332   })JSON");
333   EXPECT_THAT(
334       config.getCategoryConfigs(),
335       UnorderedElementsAre(
336           Pair("my.category", LogCategoryConfig{LogLevel::INFO, true}),
337           Pair(
338               "my.other.stuff",
339               LogCategoryConfig{static_cast<LogLevel>(19), false}),
340           Pair("foo.bar", LogCategoryConfig{LogLevel::DBG7, true})));
341   EXPECT_THAT(
342       config.getHandlerConfigs(),
343       UnorderedElementsAre(Pair(
344           "h1", LogHandlerConfig{"custom", {{"foo", "bar"}, {"a", "z"}}})));
345
346   // The JSON config parsing should allow unusual log category names
347   // containing whitespace, equal signs, and other characters not allowed in
348   // the basic config style.
349   config = parseLogConfig(R"JSON({
350     "categories": {
351       "  my.category  ": { "level": "INFO" },
352       " foo; bar=asdf, test": { "level": "DBG1" },
353     },
354     "handlers": {
355       "h1;h2,h3= ": { "type": " x;y " }
356     }
357   })JSON");
358   EXPECT_THAT(
359       config.getCategoryConfigs(),
360       UnorderedElementsAre(
361           Pair("  my.category  ", LogCategoryConfig{LogLevel::INFO, true}),
362           Pair(
363               " foo; bar=asdf, test",
364               LogCategoryConfig{LogLevel::DBG1, true})));
365   EXPECT_THAT(
366       config.getHandlerConfigs(),
367       UnorderedElementsAre(Pair("h1;h2,h3= ", LogHandlerConfig{" x;y "})));
368 }
369
370 TEST(LogConfig, parseJsonErrors) {
371   EXPECT_THROW_RE(
372       parseLogConfigJson("5"),
373       LogConfigParseError,
374       "JSON config input must be an object");
375   EXPECT_THROW_RE(
376       parseLogConfigJson("true"),
377       LogConfigParseError,
378       "JSON config input must be an object");
379   EXPECT_THROW_RE(
380       parseLogConfigJson(R"("hello")"),
381       LogConfigParseError,
382       "JSON config input must be an object");
383   EXPECT_THROW_RE(
384       parseLogConfigJson("[1, 2, 3]"),
385       LogConfigParseError,
386       "JSON config input must be an object");
387   EXPECT_THROW_RE(
388       parseLogConfigJson(""), std::runtime_error, "json parse error");
389   EXPECT_THROW_RE(
390       parseLogConfigJson("{"), std::runtime_error, "json parse error");
391   EXPECT_THROW_RE(parseLogConfig("{"), std::runtime_error, "json parse error");
392   EXPECT_THROW_RE(
393       parseLogConfig("{}}"), std::runtime_error, "json parse error");
394
395   StringPiece input = R"JSON({
396     "categories": 5
397   })JSON";
398   EXPECT_THROW_RE(
399       parseLogConfig(input),
400       LogConfigParseError,
401       "unexpected data type for log categories config: "
402       "got integer, expected an object");
403   input = R"JSON({
404     "categories": {
405       "foo": true,
406     }
407   })JSON";
408   EXPECT_THROW_RE(
409       parseLogConfig(input),
410       LogConfigParseError,
411       R"(unexpected data type for configuration of category "foo": )"
412       "got boolean, expected an object, string, or integer");
413
414   input = R"JSON({
415     "categories": {
416       "foo": [1, 2, 3],
417     }
418   })JSON";
419   EXPECT_THROW_RE(
420       parseLogConfig(input),
421       LogConfigParseError,
422       R"(unexpected data type for configuration of category "foo": )"
423       "got array, expected an object, string, or integer");
424
425   input = R"JSON({
426     "categories": {
427       ".": { "level": "INFO" },
428       "folly": { "level": "FATAL", "inherit": 19 },
429     }
430   })JSON";
431   EXPECT_THROW_RE(
432       parseLogConfig(input),
433       LogConfigParseError,
434       R"(unexpected data type for inherit field of category "folly": )"
435       "got integer, expected a boolean");
436   input = R"JSON({
437     "categories": {
438       "folly": { "level": [], },
439     }
440   })JSON";
441   EXPECT_THROW_RE(
442       parseLogConfig(input),
443       LogConfigParseError,
444       R"(unexpected data type for level field of category "folly": )"
445       "got array, expected a string or integer");
446   input = R"JSON({
447     "categories": {
448       5: {}
449     }
450   })JSON";
451   EXPECT_THROW_RE(
452       parseLogConfig(input), std::runtime_error, "json parse error");
453
454   input = R"JSON({
455     "categories": {
456       "foo...bar": { "level": "INFO", },
457       "foo..bar": { "level": "INFO", },
458     }
459   })JSON";
460   EXPECT_THROW_RE(
461       parseLogConfig(input),
462       LogConfigParseError,
463       R"(category "foo\.bar" listed multiple times under different names: )"
464       R"("foo\.\.+bar" and "foo\.+bar")");
465   input = R"JSON({
466     "categories": {
467       "...": { "level": "ERR", },
468       "": { "level": "INFO", },
469     }
470   })JSON";
471   EXPECT_THROW_RE(
472       parseLogConfig(input),
473       LogConfigParseError,
474       R"(category "" listed multiple times under different names: )"
475       R"X("(\.\.\.|)" and "(\.\.\.|)")X");
476
477   input = R"JSON({
478     "categories": { "folly": { "level": "ERR" } },
479     "handlers": 9.8
480   })JSON";
481   EXPECT_THROW_RE(
482       parseLogConfig(input),
483       LogConfigParseError,
484       "unexpected data type for log handlers config: "
485       "got double, expected an object");
486
487   input = R"JSON({
488     "categories": { "folly": { "level": "ERR" } },
489     "handlers": {
490       "foo": "test"
491     }
492   })JSON";
493   EXPECT_THROW_RE(
494       parseLogConfig(input),
495       LogConfigParseError,
496       R"(unexpected data type for configuration of handler "foo": )"
497       "got string, expected an object");
498
499   input = R"JSON({
500     "categories": { "folly": { "level": "ERR" } },
501     "handlers": {
502       "foo": {}
503     }
504   })JSON";
505   EXPECT_THROW_RE(
506       parseLogConfig(input),
507       LogConfigParseError,
508       R"(no handler type specified for log handler "foo")");
509
510   input = R"JSON({
511     "categories": { "folly": { "level": "ERR" } },
512     "handlers": {
513       "foo": {
514         "type": 19
515       }
516     }
517   })JSON";
518   EXPECT_THROW_RE(
519       parseLogConfig(input),
520       LogConfigParseError,
521       R"(unexpected data type for "type" field of handler "foo": )"
522       "got integer, expected a string");
523
524   input = R"JSON({
525     "categories": { "folly": { "level": "ERR" } },
526     "handlers": {
527       "foo": {
528         "type": "custom",
529         "options": true
530       }
531     }
532   })JSON";
533   EXPECT_THROW_RE(
534       parseLogConfig(input),
535       LogConfigParseError,
536       R"(unexpected data type for "options" field of handler "foo": )"
537       "got boolean, expected an object");
538
539   input = R"JSON({
540     "categories": { "folly": { "level": "ERR" } },
541     "handlers": {
542       "foo": {
543         "type": "custom",
544         "options": ["foo", "bar"]
545       }
546     }
547   })JSON";
548   EXPECT_THROW_RE(
549       parseLogConfig(input),
550       LogConfigParseError,
551       R"(unexpected data type for "options" field of handler "foo": )"
552       "got array, expected an object");
553
554   input = R"JSON({
555     "categories": { "folly": { "level": "ERR" } },
556     "handlers": {
557       "foo": {
558         "type": "custom",
559         "options": {"bar": 5}
560       }
561     }
562   })JSON";
563   EXPECT_THROW_RE(
564       parseLogConfig(input),
565       LogConfigParseError,
566       R"(unexpected data type for option "bar" of handler "foo": )"
567       "got integer, expected a string");
568 }
569
570 TEST(LogConfig, toJson) {
571   auto config = parseLogConfig("");
572   auto expectedJson = folly::parseJson(R"JSON({
573   "categories": {},
574   "handlers": {}
575 })JSON");
576   EXPECT_EQ(expectedJson, logConfigToDynamic(config));
577
578   config = parseLogConfig(
579       "ERROR:h1,foo.bar:=FATAL,folly=INFO:; "
580       "h1=custom:foo=bar");
581   expectedJson = folly::parseJson(R"JSON({
582   "categories" : {
583     "" : {
584       "inherit" : true,
585       "level" : "ERR",
586       "handlers" : ["h1"]
587     },
588     "folly" : {
589       "inherit" : true,
590       "level" : "INFO",
591       "handlers" : []
592     },
593     "foo.bar" : {
594       "inherit" : false,
595       "level" : "FATAL"
596     }
597   },
598   "handlers" : {
599     "h1": {
600       "type": "custom",
601       "options": { "foo": "bar" }
602     }
603   }
604 })JSON");
605   EXPECT_EQ(expectedJson, logConfigToDynamic(config));
606 }
607
608 TEST(LogConfig, mergeConfigs) {
609   auto config = parseLogConfig("bar=ERR:");
610   config.update(parseLogConfig("foo:=INFO"));
611   EXPECT_THAT(
612       config.getCategoryConfigs(),
613       UnorderedElementsAre(
614           Pair("foo", LogCategoryConfig{LogLevel::INFO, false}),
615           Pair("bar", LogCategoryConfig{LogLevel::ERR, true, {}})));
616   EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
617
618   config =
619       parseLogConfig("WARN:default; default=custom:opt1=value1,opt2=value2");
620   config.update(parseLogConfig("folly.io=DBG2,foo=INFO"));
621   EXPECT_THAT(
622       config.getCategoryConfigs(),
623       UnorderedElementsAre(
624           Pair("", LogCategoryConfig{LogLevel::WARN, true, {"default"}}),
625           Pair("foo", LogCategoryConfig{LogLevel::INFO, true}),
626           Pair("folly.io", LogCategoryConfig{LogLevel::DBG2, true})));
627   EXPECT_THAT(
628       config.getHandlerConfigs(),
629       UnorderedElementsAre(Pair(
630           "default",
631           LogHandlerConfig(
632               "custom", {{"opt1", "value1"}, {"opt2", "value2"}}))));
633
634   // Updating the root category's log level without specifying
635   // handlers should leave its current handler list intact
636   config =
637       parseLogConfig("WARN:default; default=custom:opt1=value1,opt2=value2");
638   config.update(parseLogConfig("ERR"));
639   EXPECT_THAT(
640       config.getCategoryConfigs(),
641       UnorderedElementsAre(
642           Pair("", LogCategoryConfig{LogLevel::ERR, true, {"default"}})));
643   EXPECT_THAT(
644       config.getHandlerConfigs(),
645       UnorderedElementsAre(Pair(
646           "default",
647           LogHandlerConfig(
648               "custom", {{"opt1", "value1"}, {"opt2", "value2"}}))));
649
650   config =
651       parseLogConfig("WARN:default; default=custom:opt1=value1,opt2=value2");
652   config.update(parseLogConfig(".:=ERR"));
653   EXPECT_THAT(
654       config.getCategoryConfigs(),
655       UnorderedElementsAre(
656           Pair("", LogCategoryConfig{LogLevel::ERR, false, {"default"}})));
657   EXPECT_THAT(
658       config.getHandlerConfigs(),
659       UnorderedElementsAre(Pair(
660           "default",
661           LogHandlerConfig(
662               "custom", {{"opt1", "value1"}, {"opt2", "value2"}}))));
663
664   // Test clearing the root category's log handlers
665   config =
666       parseLogConfig("WARN:default; default=custom:opt1=value1,opt2=value2");
667   config.update(parseLogConfig("FATAL:"));
668   EXPECT_THAT(
669       config.getCategoryConfigs(),
670       UnorderedElementsAre(
671           Pair("", LogCategoryConfig{LogLevel::FATAL, true, {}})));
672   EXPECT_THAT(
673       config.getHandlerConfigs(),
674       UnorderedElementsAre(Pair(
675           "default",
676           LogHandlerConfig(
677               "custom", {{"opt1", "value1"}, {"opt2", "value2"}}))));
678
679   // Test updating the settings on a log handler
680   config =
681       parseLogConfig("WARN:default; default=stream:stream=stderr,async=false");
682   config.update(parseLogConfig("INFO; default:async=true"));
683   EXPECT_THAT(
684       config.getCategoryConfigs(),
685       UnorderedElementsAre(
686           Pair("", LogCategoryConfig{LogLevel::INFO, true, {"default"}})));
687   EXPECT_THAT(
688       config.getHandlerConfigs(),
689       UnorderedElementsAre(Pair(
690           "default",
691           LogHandlerConfig(
692               "stream", {{"stream", "stderr"}, {"async", "true"}}))));
693
694   // Updating the settings for a non-existent log handler should fail
695   config =
696       parseLogConfig("WARN:default; default=stream:stream=stderr,async=false");
697   EXPECT_THROW_RE(
698       config.update(parseLogConfig("INFO; other:async=true")),
699       std::invalid_argument,
700       "cannot update configuration for "
701       R"(unknown log handler "other")");
702 }