From d79368b2f6bae872002e36e5df26e450d329b279 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 24 Aug 2018 14:57:55 +0200 Subject: [PATCH 1/5] Add util.log.LogConfiguration See https://github.com/xp-framework/logging/issues/11#issuecomment-415737649 --- .../php/util/log/LogConfiguration.class.php | 103 ++++++++++++++++++ .../unittest/LogConfigurationTest.class.php | 98 +++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100755 src/main/php/util/log/LogConfiguration.class.php create mode 100755 src/test/php/util/log/unittest/LogConfigurationTest.class.php 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..1906701 --- /dev/null +++ b/src/main/php/util/log/LogConfiguration.class.php @@ -0,0 +1,103 @@ +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 + */ + private function appendersFor($properties, $section) { + + // Class + if ($class= $properties->readString($section, 'class', null)) { + $appender= XPClass::forName($class)->newInstance(...$properties->readArray($section, 'args', [])); + if ($levels= $properties->readArray($section, 'levels', null)) { + $level= LogLevel::NONE; + foreach ($levels as $name) { + $level |= LogLevel::named($name); + } + yield $level => $appender; + } else { + yield LogLevel::ALL => $appender; + } + } + + // Uses, referencing other section + if ($uses= $properties->readArray($section, 'uses', null)) { + foreach ($uses as $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..94c8d76 --- /dev/null +++ b/src/test/php/util/log/unittest/LogConfigurationTest.class.php @@ -0,0 +1,98 @@ +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 category_returns_appender_from_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 category_returns_appenders_from_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 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); + } +} \ No newline at end of file From c3c15b7757b2d9b89a0e6b9d1d26299d39f2b407 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 24 Aug 2018 16:51:45 +0200 Subject: [PATCH 2/5] Add test for using different levels --- .../php/util/log/LogConfiguration.class.php | 2 +- .../unittest/LogConfigurationTest.class.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/php/util/log/LogConfiguration.class.php b/src/main/php/util/log/LogConfiguration.class.php index 1906701..3aa9b42 100755 --- a/src/main/php/util/log/LogConfiguration.class.php +++ b/src/main/php/util/log/LogConfiguration.class.php @@ -54,7 +54,7 @@ private function appendersFor($properties, $section) { // Class if ($class= $properties->readString($section, 'class', null)) { $appender= XPClass::forName($class)->newInstance(...$properties->readArray($section, 'args', [])); - if ($levels= $properties->readArray($section, 'levels', null)) { + if ($levels= $properties->readArray($section, 'level', null)) { $level= LogLevel::NONE; foreach ($levels as $name) { $level |= LogLevel::named($name); diff --git a/src/test/php/util/log/unittest/LogConfigurationTest.class.php b/src/test/php/util/log/unittest/LogConfigurationTest.class.php index 94c8d76..9ec332e 100755 --- a/src/test/php/util/log/unittest/LogConfigurationTest.class.php +++ b/src/test/php/util/log/unittest/LogConfigurationTest.class.php @@ -5,6 +5,7 @@ use util\Objects; use util\Properties; use util\log\ConsoleAppender; +use util\log\FileAppender; use util\log\LogConfiguration; use util\log\LogLevel; @@ -95,4 +96,25 @@ class=util.log.FileAppender $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 From 4c8a61a86de34e7ce685cf163db7d1ae9b27a834 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 24 Aug 2018 17:00:29 +0200 Subject: [PATCH 3/5] Throw exceptions when uses references non-existant sections --- .../php/util/log/LogConfiguration.class.php | 5 ++ .../unittest/LogConfigurationTest.class.php | 51 ++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/main/php/util/log/LogConfiguration.class.php b/src/main/php/util/log/LogConfiguration.class.php index 3aa9b42..859f6be 100755 --- a/src/main/php/util/log/LogConfiguration.class.php +++ b/src/main/php/util/log/LogConfiguration.class.php @@ -1,5 +1,6 @@ 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; } diff --git a/src/test/php/util/log/unittest/LogConfigurationTest.class.php b/src/test/php/util/log/unittest/LogConfigurationTest.class.php index 9ec332e..6caebce 100755 --- a/src/test/php/util/log/unittest/LogConfigurationTest.class.php +++ b/src/test/php/util/log/unittest/LogConfigurationTest.class.php @@ -1,6 +1,8 @@ properties(' [default] class=util.log.ConsoleAppender @@ -68,7 +70,7 @@ class=util.log.ConsoleAppender } #[@test] - public function category_returns_appenders_from_uses() { + public function appenders_referenced_via_uses() { $config= new LogConfiguration($this->properties(' [default] uses=console|files @@ -85,6 +87,51 @@ class=util.log.FileAppender $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= 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(' From 0fd13133de95bb69a27563113dee3fe655c921c1 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 24 Aug 2018 17:09:12 +0200 Subject: [PATCH 4/5] Throw exceptions if level contains unknown log levels --- .../php/util/log/LogConfiguration.class.php | 28 +++++++++++++++++-- .../unittest/LogConfigurationTest.class.php | 23 +++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/main/php/util/log/LogConfiguration.class.php b/src/main/php/util/log/LogConfiguration.class.php index 859f6be..1b024b8 100755 --- a/src/main/php/util/log/LogConfiguration.class.php +++ b/src/main/php/util/log/LogConfiguration.class.php @@ -2,6 +2,7 @@ use lang\FormatException; use lang\IllegalArgumentException; +use lang\Throwable; use lang\XPClass; use util\PropertyAccess; @@ -32,7 +33,12 @@ class LogConfiguration { private $categories= []; - /** Creates a new log configuration from a properties file */ + /** + * Creates a new log configuration from a properties file + * + * @param util.PropertyAccess $properties + * @throws lang.FormatException if the property file contains errors + */ public function __construct(PropertyAccess $properties) { foreach ($properties->sections() as $section) { $cat= new LogCategory($section); @@ -52,14 +58,30 @@ public function __construct(PropertyAccess $properties) { * @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)) { - $appender= XPClass::forName($class)->newInstance(...$properties->readArray($section, 'args', [])); + 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) { - $level |= LogLevel::named($name); + if (!isset($names[$name])) { + throw new FormatException('Level '.$name.' in section "'.$section.'" not recognized'); + } + $level |= $names[$name]; } yield $level => $appender; } else { diff --git a/src/test/php/util/log/unittest/LogConfigurationTest.class.php b/src/test/php/util/log/unittest/LogConfigurationTest.class.php index 6caebce..a132b64 100755 --- a/src/test/php/util/log/unittest/LogConfigurationTest.class.php +++ b/src/test/php/util/log/unittest/LogConfigurationTest.class.php @@ -123,6 +123,29 @@ 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= '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"' From 3476ba9b29f3363889118084d66da8a58e06f913 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Fri, 24 Aug 2018 17:11:03 +0200 Subject: [PATCH 5/5] Verify exceptions while instantiating classes are wrapped in FormatException --- .../util/log/unittest/LogConfigurationTest.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/php/util/log/unittest/LogConfigurationTest.class.php b/src/test/php/util/log/unittest/LogConfigurationTest.class.php index a132b64..14be898 100755 --- a/src/test/php/util/log/unittest/LogConfigurationTest.class.php +++ b/src/test/php/util/log/unittest/LogConfigurationTest.class.php @@ -134,6 +134,18 @@ 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'