diff --git a/src/main/php/util/log/LogConfiguration.class.php b/src/main/php/util/log/LogConfiguration.class.php new file mode 100755 index 0000000..1b024b8 --- /dev/null +++ b/src/main/php/util/log/LogConfiguration.class.php @@ -0,0 +1,130 @@ +sections() as $section) { + $cat= new LogCategory($section); + foreach ($this->appendersFor($properties, $section) as $level => $appender) { + $cat->addAppender($appender, $level); + } + $this->categories[$section]= $cat; + } + } + + /** + * Returns log appenders for a given property file section + * + * @param util.PropertyAccess $properties + * @param string $section + * @return iterable + * @throws lang.FormatException + */ + private function appendersFor($properties, $section) { + static $names= [ + 'INFO' => LogLevel::INFO, + 'WARN' => LogLevel::WARN, + 'ERROR' => LogLevel::ERROR, + 'DEBUG' => LogLevel::DEBUG, + 'ALL' => LogLevel::ALL, + 'NONE' => LogLevel::NONE, + ]; + + // Class + if ($class= $properties->readString($section, 'class', null)) { + try { + $appender= XPClass::forName($class)->newInstance(...$properties->readArray($section, 'args', [])); + } catch (Throwable $e) { + throw new FormatException('Class '.$class.' in section "'.$section.'" cannot be instantiated', $e); + } + + if ($levels= $properties->readArray($section, 'level', null)) { + $level= LogLevel::NONE; + foreach ($levels as $name) { + if (!isset($names[$name])) { + throw new FormatException('Level '.$name.' in section "'.$section.'" not recognized'); + } + $level |= $names[$name]; + } + yield $level => $appender; + } else { + yield LogLevel::ALL => $appender; + } + } + + // Uses, referencing other section + if ($uses= $properties->readArray($section, 'uses', null)) { + foreach ($uses as $use) { + if (!$properties->hasSection($use)) { + throw new FormatException('Uses in section "'.$section.'" references non-existant section "'.$use.'"'); + } + foreach ($this->appendersFor($properties, $use) as $level => $appender) { + yield $level => $appender; + } + } + } + } + + /** @return [:util.log.LogCategory] */ + public function categories() { return $this->categories; } + + /** + * Test whether this configuration provides a log category by its name + * + * @param string $name + * @return bool + */ + public function provides($name) { + return isset($this->categories[$name]); + } + + /** + * Return a log category by its name + * + * @param string $name + * @return util.log.LogCategory + * @throws lang.IllegalArgumentException + */ + public function category($name) { + if (isset($this->categories[$name])) return $this->categories[$name]; + + throw new IllegalArgumentException('No log category "'.$name.'"'); + } +} \ No newline at end of file diff --git a/src/test/php/util/log/unittest/LogConfigurationTest.class.php b/src/test/php/util/log/unittest/LogConfigurationTest.class.php new file mode 100755 index 0000000..14be898 --- /dev/null +++ b/src/test/php/util/log/unittest/LogConfigurationTest.class.php @@ -0,0 +1,202 @@ +load(new MemoryInputStream(trim($properties))); + return $p; + } + + #[@test] + public function can_create() { + new LogConfiguration($this->properties('')); + } + + #[@test] + public function categories_for_empty_file() { + $config= new LogConfiguration($this->properties('')); + $this->assertEquals([], $config->categories()); + } + + #[@test] + public function categories() { + $config= new LogConfiguration($this->properties(' + [default] + class=util.log.ConsoleAppender + ')); + + $this->assertInstanceOf('[:util.log.LogCategory]', $config->categories()); + } + + #[@test, @values([ + # ['default', true], + # ['files', false], + #])] + public function provides($name, $expected) { + $config= new LogConfiguration($this->properties(' + [default] + class=util.log.ConsoleAppender + ')); + $this->assertEquals($expected, $config->provides($name)); + } + + #[@test] + public function appender_configured_via_class() { + $config= new LogConfiguration($this->properties(' + [default] + class=util.log.ConsoleAppender + ')); + + $appenders= $config->category('default')->getAppenders(); + $this->assertEquals(1, sizeof($appenders), Objects::stringOf($appenders)); + } + + #[@test] + public function appenders_referenced_via_uses() { + $config= new LogConfiguration($this->properties(' + [default] + uses=console|files + + [console] + class=util.log.ConsoleAppender + + [files] + class=util.log.FileAppender + args=test.log + ')); + + $appenders= $config->category('default')->getAppenders(); + $this->assertEquals(2, sizeof($appenders), Objects::stringOf($appenders)); + } + + #[@test] + public function uses_can_be_nested() { + $config= new LogConfiguration($this->properties(' + [default] + uses=tee + + [tee] + class=util.log.ConsoleAppender + uses=syslog|files + + [syslog] + class=util.log.SyslogAppender + + [files] + class=util.log.FileAppender + args=test.log + ')); + + $appenders= $config->category('default')->getAppenders(); + $this->assertEquals(3, sizeof($appenders), Objects::stringOf($appenders)); + } + + #[@test, @expect( + # class= FormatException::class, + # withMessage= 'Uses in section "default" references non-existant section "missing"' + #)] + public function uses_referencing_non_existant_section() { + new LogConfiguration($this->properties(' + [default] + uses=console|missing + + [console] + class=util.log.ConsoleAppender + ')); + } + + #[@test, @expect( + # class= FormatException::class, + # withMessage= 'Class util.log.NonExistantAppender in section "default" cannot be instantiated' + #)] + public function non_existant_appender() { + new LogConfiguration($this->properties(' + [default] + class=util.log.NonExistantAppender + ')); + } + + #[@test, @expect( + # class= FormatException::class, + # withMessage= 'Class util.log.ConsoleAppender in section "default" cannot be instantiated' + #)] + public function exceptions_when_instantiating_appenders() { + new LogConfiguration($this->properties(' + [default] + class=util.log.ConsoleAppender + args=STDIN + ')); + } + + #[@test, @expect( + # class= FormatException::class, + # withMessage= 'Level TEST in section "default" not recognized' + #)] + public function non_existant_level() { + new LogConfiguration($this->properties(' + [default] + class=util.log.ConsoleAppender + level=TEST + ')); + } + + #[@test, @expect( + # class= IllegalArgumentException::class, + # withMessage= 'No log category "default"' + #)] + public function missing_category() { + $config= new LogConfiguration($this->properties('')); + $config->category('default'); + } + + #[@test] + public function category_with_class_and_argument() { + $config= new LogConfiguration($this->properties(' + [default] + class=util.log.FileAppender + args=test.log + ')); + + $appenders= $config->category('default')->getAppenders(); + $this->assertEquals('test.log', $appenders[0]->filename); + } + + #[@test] + public function categories_with_loglevels() { + $config= new LogConfiguration($this->properties(' + [default] + uses=console|files + + [console] + class=util.log.ConsoleAppender + level=INFO + + [files] + class=util.log.FileAppender + args=test.log + level=ERROR + ')); + + $cat= $config->category('default'); + $this->assertInstanceOf(ConsoleAppender::class, $cat->getAppenders(LogLevel::INFO)[0]); + $this->assertInstanceOf(FileAppender::class, $cat->getAppenders(LogLevel::ERROR)[0]); + } +} \ No newline at end of file