-
-
Save thekid/1240769 to your computer and use it in GitHub Desktop.
diff --git a/core/src/main/php/lang/XPClass.class.php b/core/src/main/php/lang/XPClass.class.php | |
index c4e5ee9..33b29ed 100644 | |
--- a/core/src/main/php/lang/XPClass.class.php | |
+++ b/core/src/main/php/lang/XPClass.class.php | |
@@ -583,19 +583,125 @@ | |
* @throws lang.ClassFormatException | |
*/ | |
public static function parseAnnotations($input, $context) { | |
- ob_start(); | |
- $annotations= eval('return array('.($eval= preg_replace( | |
- array('/@([a-z_]+),/i', '/@([a-z_]+)\(\'([^\']+)\'\)/ie', '/@([a-z_]+)\(/i', '/(\(|, *)([a-z_]+) *= */i'), | |
- array('\'$1\' => NULL,', '"\'$1\' => urldecode(\'".urlencode(\'$2\')."\')"', '\'$1\' => array(', '$1\'$2\' => '), | |
- trim($input, "[]# \t\n\r").',' | |
- )).');'); | |
- $msg= ltrim(ob_get_contents(), ini_get('error_prepend_string')."\r\n\t "); | |
- if (FALSE === $annotations || $msg) { | |
- ob_end_clean(); | |
- xp::gc(); | |
- raise('lang.ClassFormatException', 'Parse error: '.$msg.' of "'.addcslashes($eval, "\0..\17").'" in '.$context); | |
+ $input= trim($input, "[]# \t\n\r").']'; | |
+ $offset= 0; | |
+ $annotations= array(); | |
+ $annotation= $value= NULL; | |
+ $length= strlen($input); | |
+ while ($offset < $length) { | |
+ $state= $input{$offset}; | |
+ if ('@' === $state) { | |
+ $s= strcspn($input, ',(]', $offset); | |
+ $annotation= substr($input, $offset+ 1, $s- 1); | |
+ $offset+= $s; | |
+ } else if (']' === $state) { | |
+ $annotations[$annotation]= $value; | |
+ break; | |
+ } else if ('(' === $state) { | |
+ $peek= substr($input, $offset+ 1, strcspn($input, '="\')', $offset)); | |
+ if ('\'' === $peek{0} || '"' === $peek{0}) { | |
+ $p= $offset+ 2; | |
+ $q= $peek{0}; | |
+ while (($s= strcspn($input, $q, $p)) !== 0) { | |
+ $p+= $s; | |
+ if ('\\' !== $input{$p- 1}) break; | |
+ $p++; | |
+ } | |
+ if (!is_string($value= @eval('return '.substr($input, $offset+ 1, $p - $offset).';'))) { | |
+ raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed string in '.$context); | |
+ } | |
+ $offset= $p+ 1; | |
+ } else if ('array' === substr($peek, 0, 5)) { | |
+ $b= 1; | |
+ $p= $offset+ 1+ 6; | |
+ while ($b > 0) { | |
+ $p+= strcspn($input, '()"\'', $p); | |
+ if ($p > $length) break; | |
+ if ('(' === $input{$p}) $b++; else if (')' === $input{$p}) $b--; else if ('\'' === $input{$p} || '"' === $input{$p}) { | |
+ $q= $input{$p}; | |
+ $p++; | |
+ while (($s= strcspn($input, $q, $p)) !== 0) { | |
+ $p+= $s; | |
+ if ('\\' !== $input{$p- 1}) break; | |
+ $p++; | |
+ } | |
+ } | |
+ $p++; | |
+ } | |
+ if (!is_array($value= @eval('return '.substr($input, $offset+ 1, $p- $offset- 1).';'))) { | |
+ raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed array in '.$context); | |
+ } | |
+ $offset= $p; | |
+ } else if ('=' !== $peek{strlen($peek)- 1}) { | |
+ $value= eval('return '.substr($peek, 0, -1).';'); | |
+ $offset+= strlen($peek); | |
+ } else { | |
+ $value= array(); | |
+ do { | |
+ $key= trim($peek, '= '); | |
+ $offset+= strlen($peek)+ 1; | |
+ $offset+= strspn($input, ' ', $offset); | |
+ if ($offset >= $length) { | |
+ break; | |
+ } else if ('array' === substr($input, $offset, 5)) { | |
+ $b= 1; | |
+ $p= $offset+ 6; | |
+ while ($b > 0) { | |
+ $p+= strcspn($input, '()"\'', $p); | |
+ if ($p > $length) break; | |
+ if ('(' === $input{$p}) $b++; else if (')' === $input{$p}) $b--; else if ('\'' === $input{$p} || '"' === $input{$p}) { | |
+ $q= $input{$p}; | |
+ $p++; | |
+ while (($s= strcspn($input, $q, $p)) !== 0) { | |
+ $p+= $s; | |
+ if ('\\' !== $input{$p- 1}) break; | |
+ $p++; | |
+ } | |
+ } | |
+ $p++; | |
+ } | |
+ if (!is_array($value[$key]= @eval('return '.substr($input, $offset, $p- $offset).';'))) { | |
+ raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed array in '.$context); | |
+ } | |
+ $offset= $p; | |
+ } else if ('\'' === $input{$offset} || '"' === $input{$offset}) { | |
+ $p= $offset+ 1; | |
+ $q= $input{$offset}; | |
+ while (($s= strcspn($input, $q, $p)) !== 0) { | |
+ $p+= $s; | |
+ if ('\\' !== $input{$p- 1}) break; | |
+ $p++; | |
+ } | |
+ if (!is_string($value[$key]= @eval('return '.substr($input, $offset, $p - $offset + 1).';'))) { | |
+ raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed string in '.$context); | |
+ } | |
+ $offset= $p+ 1; | |
+ } else { | |
+ $s= strcspn($input, ',)', $offset); | |
+ $value[$key]= eval('return '.substr($input, $offset, $s).';'); | |
+ $offset+= $s; | |
+ } | |
+ | |
+ // Find next key | |
+ $s= strcspn($input, '="\')', $offset); | |
+ $peek= substr($input, $offset+ 1, $s); | |
+ } while ($s); | |
+ } | |
+ $offset++; // ")" | |
+ if ($offset > $length) { | |
+ raise('lang.ClassFormatException', 'Parse error: Expecting ] in '.$context); | |
+ } | |
+ } else if (',' === $state) { | |
+ $annotations[$annotation]= $value; | |
+ $annotation= $value= NULL; | |
+ if (FALSE === ($offset= strpos($input, '@', $offset))) { | |
+ raise('lang.ClassFormatException', 'Parse error: Expecting @ in '.$context); | |
+ } | |
+ } else { | |
+ raise('lang.ClassFormatException', 'Parse error: Unknown state '.$state.' at position '.$offset.' in '.$context); | |
+ } | |
} | |
- ob_end_clean(); | |
+ | |
return $annotations; | |
} | |
diff --git a/core/src/resources/unittest/core.ini b/core/src/resources/unittest/core.ini | |
index 8d13028..02df721 100644 | |
--- a/core/src/resources/unittest/core.ini | |
+++ b/core/src/resources/unittest/core.ini | |
@@ -9,6 +9,12 @@ description="Core tests" | |
[annotations] | |
class="net.xp_framework.unittest.core.AnnotationTest" | |
+[annotation-parsing] | |
+class="net.xp_framework.unittest.annotations.AnnotationParsingTest" | |
+ | |
+[broken-annotations] | |
+class="net.xp_framework.unittest.annotations.BrokenAnnotationTest" | |
+ | |
[errors] | |
class="net.xp_framework.unittest.core.ErrorsTest" | |
@@ -123,9 +129,6 @@ class="net.xp_framework.unittest.core.BootstrapTest" | |
[newinstance] | |
class="net.xp_framework.unittest.core.NewInstanceTest" | |
-[annotation-parsing] | |
-class="net.xp_framework.unittest.annotations.BrokenAnnotationTest" | |
- | |
[commandline] | |
class="net.xp_framework.unittest.core.CommandLineTest" | |
diff --git a/core/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php b/core/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php | |
new file mode 100644 | |
index 0000000..964524a | |
--- /dev/null | |
+++ b/core/src/test/php/net/xp_framework/unittest/annotations/AnnotationParsingTest.class.php | |
@@ -0,0 +1,389 @@ | |
+<?php | |
+/* This class is part of the XP framework | |
+ * | |
+ * $Id$ | |
+ */ | |
+ | |
+ uses('unittest.TestCase'); | |
+ | |
+ /** | |
+ * Tests the XP Framework's annotation parsing implementation | |
+ * | |
+ * @see rfc://0016 | |
+ * @see xp://lang.XPClass#parseAnnotations | |
+ * @see http://bugs.xp-framework.net/show_bug.cgi?id=38 | |
+ * @see https://github.com/xp-framework/xp-framework/issues/14 | |
+ * @see https://github.com/xp-framework/xp-framework/pull/56 | |
+ * @see https://gist.github.com/1240769 | |
+ */ | |
+ class AnnotationParsingTest extends TestCase { | |
+ | |
+ /** | |
+ * Helper | |
+ * | |
+ * @param string input | |
+ * @return [:var] | |
+ */ | |
+ protected function parse($input) { | |
+ return XPClass::parseAnnotations($input, $this->getClassName()); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation without a value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function noValue() { | |
+ $this->assertEquals( | |
+ array('hello' => NULL), | |
+ $this->parse("#[@hello]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function sqStringValue() { | |
+ $this->assertEquals( | |
+ array('hello' => 'World'), | |
+ $this->parse("#[@hello('World')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function sqStringValueWithEqualsSign() { | |
+ $this->assertEquals( | |
+ array('hello' => 'World=Welt'), | |
+ $this->parse("#[@hello('World=Welt')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test string with at sign inside | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function sqStringValueWithAtSign() { | |
+ $this->assertEquals( | |
+ array('hello' => '@World'), | |
+ $this->parse("#[@hello('@World')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test string with an annotation inside a string | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function sqStringValueWithAnnotation() { | |
+ $this->assertEquals( | |
+ array('hello' => '@hello("World")'), | |
+ $this->parse("#[@hello('@hello(\"World\")')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function sqStringValueWithDoubleQuotes() { | |
+ $this->assertEquals( | |
+ array('hello' => 'said "he"'), | |
+ $this->parse("#[@hello('said \"he\"')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function sqStringValueWithEscapedSingleQuotes() { | |
+ $this->assertEquals( | |
+ array('hello' => "said 'he'"), | |
+ $this->parse("#[@hello('said \'he\'')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function dqStringValue() { | |
+ $this->assertEquals( | |
+ array('hello' => 'World'), | |
+ $this->parse('#[@hello("World")]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function dqStringValueWithSingleQuote() { | |
+ $this->assertEquals( | |
+ array('hello' => 'Beck\'s'), | |
+ $this->parse('#[@hello("Beck\'s")]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function dqStringValueWithEscapedDoubleQuotes() { | |
+ $this->assertEquals( | |
+ array('hello' => 'said "he"'), | |
+ $this->parse('#[@hello("said \"he\"")]') | |
+ ); | |
+ } | |
+ /** | |
+ * Tests simple annotation with string value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function dqStringValueWithEscapeSequence() { | |
+ $this->assertEquals( | |
+ array('hello' => "World\n"), | |
+ $this->parse('#[@hello("World\n")]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test string with at sign inside | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function dqStringValueWithAtSign() { | |
+ $this->assertEquals( | |
+ array('hello' => '@World'), | |
+ $this->parse('#[@hello("@World")]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test string with an annotation inside a string | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function dqStringValueWithAnnotation() { | |
+ $this->assertEquals( | |
+ array('hello' => '@hello(\'World\')'), | |
+ $this->parse('#[@hello("@hello(\'World\')")]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with an int value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function intValue() { | |
+ $this->assertEquals( | |
+ array('answer' => 42), | |
+ $this->parse('#[@answer(42)]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with a double value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function doubleValue() { | |
+ $this->assertEquals( | |
+ array('version' => 3.5), | |
+ $this->parse('#[@version(3.5)]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with an array value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function arrayValue() { | |
+ $this->assertEquals( | |
+ array('versions' => array(3.4, 3.5)), | |
+ $this->parse('#[@versions(array(3.4, 3.5))]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with an array value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function arrayValueWithNestedArray() { | |
+ $this->assertEquals( | |
+ array('versions' => array(array(3))), | |
+ $this->parse('#[@versions(array(array(3)))]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with an array value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function arrayValueWithNestedArrays() { | |
+ $this->assertEquals( | |
+ array('versions' => array(array(3), array(4))), | |
+ $this->parse('#[@versions(array(array(3), array(4)))]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with an array value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function arrayValueWithStringsContainingBraces() { | |
+ $this->assertEquals( | |
+ array('versions' => array('(3..4]')), | |
+ $this->parse('#[@versions(array("(3..4]"))]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple annotation with a bool value | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function boolValue() { | |
+ $this->assertEquals( | |
+ array('supported' => TRUE), | |
+ $this->parse('#[@supported(TRUE)]') | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests different value types | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function keyValuePairsAnnotationValue() { | |
+ $this->assertEquals( | |
+ array('config' => array('key' => 'value', 'times' => 5, 'disabled' => FALSE, 'null' => NULL, 'list' => array(1, 2))), | |
+ $this->parse("#[@config(key = 'value', times= 5, disabled= FALSE, null = NULL, list= array(1, 2))]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests multi-line annotations | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function multiLineAnnotation() { | |
+ $this->assertEquals( | |
+ array('interceptors' => array('classes' => array( | |
+ 'net.xp_framework.unittest.core.FirstInterceptor', | |
+ 'net.xp_framework.unittest.core.SecondInterceptor', | |
+ ))), | |
+ $this->parse(" | |
+ #[@interceptors(classes= array( | |
+ 'net.xp_framework.unittest.core.FirstInterceptor', | |
+ 'net.xp_framework.unittest.core.SecondInterceptor', | |
+ ))] | |
+ ") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests simple xpath annotations | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function simpleXPathAnnotation() { | |
+ $this->assertEquals( | |
+ array('fromXml' => array('xpath' => '/parent/child/@attribute')), | |
+ $this->parse("#[@fromXml(xpath= '/parent/child/@attribute')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests complex xpath annotations | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function complexXPathAnnotation() { | |
+ $this->assertEquals( | |
+ array('fromXml' => array('xpath' => '/parent[@attr="value"]/child[@attr1="val1" and @attr2="val2"]')), | |
+ $this->parse("#[@fromXml(xpath= '/parent[@attr=\"value\"]/child[@attr1=\"val1\" and @attr2=\"val2\"]')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Tests string default with "=" | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function stringWithEqualSigns() { | |
+ $this->assertEquals( | |
+ array('permission' => 'rn=login, rt=config'), | |
+ $this->parse("#[@permission('rn=login, rt=config')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test string assignment without whitespace is parsed correctly. | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function stringAssignedWithoutWhitespace() { | |
+ $this->assertEquals( | |
+ array('arg' => array('name' => 'verbose', 'short' => 'v')), | |
+ $this->parse("#[@arg(name= 'verbose', short='v')]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test annotation with mulitple values containing equal signs | |
+ * is parsed correctly. | |
+ * | |
+ */ | |
+ #[@test] | |
+ public function multipleValuesWithStringsAndEqualSigns() { | |
+ $this->assertEquals( | |
+ array('permission' => array('names' => array('rn=login, rt=config1', 'rn=login, rt=config2'))), | |
+ $this->parse("#[@permission(names= array('rn=login, rt=config1', 'rn=login, rt=config2'))]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test unittest annotations | |
+ * | |
+ * @see xp://unittest.TestCase | |
+ */ | |
+ #[@test] | |
+ public function unittestAnnotation() { | |
+ $this->assertEquals( | |
+ array('test' => NULL, 'ignore' => NULL, 'limit' => array('time' => 0.1, 'memory' => 100)), | |
+ $this->parse("#[@test, @ignore, @limit(time = 0.1, memory = 100)]") | |
+ ); | |
+ } | |
+ | |
+ /** | |
+ * Test overloaded annotations | |
+ * | |
+ * @see xp://lang.reflect.Proxy | |
+ */ | |
+ #[@test] | |
+ public function overloadedAnnotation() { | |
+ $this->assertEquals( | |
+ array('overloaded' => array('signatures' => array(array('string'), array('string', 'string')))), | |
+ $this->parse('#[@overloaded(signatures= array(array("string"), array("string", "string")))]') | |
+ ); | |
+ } | |
+ } | |
+?> | |
diff --git a/core/src/test/php/net/xp_framework/unittest/core/AnnotatedClass.class.php b/core/src/test/php/net/xp_framework/unittest/core/AnnotatedClass.class.php | |
index 50ae87d..5998ee1 100644 | |
--- a/core/src/test/php/net/xp_framework/unittest/core/AnnotatedClass.class.php | |
+++ b/core/src/test/php/net/xp_framework/unittest/core/AnnotatedClass.class.php | |
@@ -48,58 +48,5 @@ | |
#[@test, @ignore, @limit(time = 0.1, memory = 100)] | |
public function testMethod() { } | |
- /** | |
- * Method annotated with an annotation with a hash value containing | |
- * multiple key/value pairs | |
- * | |
- */ | |
- #[@config(key = 'value', times= 5, disabled= FALSE, null = NULL, list= array(1, 2))] | |
- public function keyValuePairs() { } | |
- | |
- /** | |
- * Method annotated with a multi-line annotation | |
- * | |
- */ | |
- #[@interceptors(classes= array( | |
- # 'net.xp_framework.unittest.core.FirstInterceptor', | |
- # 'net.xp_framework.unittest.core.SecondInterceptor', | |
- #))] | |
- public function multiLine() { } | |
- | |
- /** | |
- * Method annotated with a simple xpath expression | |
- * | |
- */ | |
- #[@fromXml(xpath= '/parent/child/@attribute')] | |
- public function simpleXPath() { } | |
- | |
- /** | |
- * Method annotated with a complex xpath expression | |
- * | |
- */ | |
- #[@fromXml(xpath= '/parent[@attr="value"]/child[@attr1="val1" and @attr2="val2"]')] | |
- public function complexXPath() { } | |
- | |
- /** | |
- * Method annotated with a string default containing "=" signs | |
- * | |
- * @see http://bugs.xp-framework.net/show_bug.cgi?id=38 | |
- */ | |
- #[@permission('rn=login, rt=config')] | |
- public function stringWithEqualSigns() { } | |
- | |
- /** | |
- * Method annotated with a string, w/o whitespace in assignment | |
- * | |
- */ | |
- #[@arg(name= 'verbose', short='v')] | |
- public function stringAssignedWithoutWhitespace() {} | |
- | |
- /** | |
- * Method annotated with multiple values which contains equal signs | |
- * | |
- * #[@permission(names= array('rn=login, rt=config1', 'rn=login, rt=config2'))] | |
- */ | |
- public function multipleValuesWithStringsAndEqualSigns() { } | |
} | |
?> | |
diff --git a/core/src/test/php/net/xp_framework/unittest/core/AnnotationTest.class.php b/core/src/test/php/net/xp_framework/unittest/core/AnnotationTest.class.php | |
index a04e480..1e970c5 100644 | |
--- a/core/src/test/php/net/xp_framework/unittest/core/AnnotationTest.class.php | |
+++ b/core/src/test/php/net/xp_framework/unittest/core/AnnotationTest.class.php | |
@@ -144,10 +144,9 @@ | |
*/ | |
#[@test] | |
public function multipleAnnotationsReturnedAsList() { | |
- $method= $this->class->getMethod('multiple'); | |
$this->assertEquals( | |
array('one' => NULL, 'two' => NULL, 'three' => NULL), | |
- $method->getAnnotations() | |
+ $this->class->getMethod('multiple')->getAnnotations() | |
); | |
} | |
@@ -183,103 +182,7 @@ | |
$m= $this->class->getMethod('testMethod'); | |
$this->assertTrue($m->hasAnnotation('test')); | |
$this->assertTrue($m->hasAnnotation('ignore')); | |
- $this->assertEquals(0.1, $m->getAnnotation('limit', 'time')); | |
- $this->assertEquals(100, $m->getAnnotation('limit', 'memory')); | |
- $this->assertEquals( | |
- array('time' => 0.1, 'memory' => 100), | |
- $m->getAnnotation('limit') | |
- ); | |
- } | |
- | |
- /** | |
- * Tests getAnnotation() returns the string associated with the | |
- * annotation. | |
- * | |
- * @see xp://net.xp_framework.unittest.core.AnnotatedClass#keyValuePairs | |
- */ | |
- #[@test] | |
- public function keyValuePairsAnnotationValue() { | |
- $this->assertEquals( | |
- array('key' => 'value', 'times' => 5, 'disabled' => FALSE, 'null' => NULL, 'list' => array(1, 2)), | |
- $this->methodAnnotation('keyValuePairs', 'config') | |
- ); | |
- } | |
- | |
- /** | |
- * Tests multi-line annotations | |
- * | |
- * @see xp://net.xp_framework.unittest.core.AnnotatedClass#multiLine | |
- */ | |
- #[@test] | |
- public function multiLineAnnotation() { | |
- $this->assertEquals(array('classes' => array( | |
- 'net.xp_framework.unittest.core.FirstInterceptor', | |
- 'net.xp_framework.unittest.core.SecondInterceptor', | |
- )), $this->methodAnnotation('multiLine', 'interceptors')); | |
- } | |
- | |
- /** | |
- * Tests simple xpath annotations | |
- * | |
- * @see xp://net.xp_framework.unittest.core.AnnotatedClass#simpleXPath | |
- */ | |
- #[@test] | |
- public function simpleXPathAnnotation() { | |
- $this->assertEquals(array( | |
- 'xpath' => '/parent/child/@attribute' | |
- ), $this->methodAnnotation('simpleXPath', 'fromXml')); | |
+ $this->assertEquals(array('time' => 0.1, 'memory' => 100), $m->getAnnotation('limit')); | |
} | |
- | |
- /** | |
- * Tests complex xpath annotations | |
- * | |
- * @see xp://net.xp_framework.unittest.core.AnnotatedClass#complexXPath | |
- */ | |
- #[@test] | |
- public function complexXPathAnnotation() { | |
- $this->assertEquals(array( | |
- 'xpath' => '/parent[@attr="value"]/child[@attr1="val1" and @attr2="val2"]' | |
- ), $this->methodAnnotation('complexXPath', 'fromXml')); | |
- } | |
- | |
- /** | |
- * Tests string default with "=" | |
- * | |
- * @see xp://net.xp_framework.unittest.core.AnnotatedClass#stringWithEqualSigns | |
- * @see http://bugs.xp-framework.net/show_bug.cgi?id=38 | |
- */ | |
- #[@test] | |
- public function stringWithEqualSigns() { | |
- $this->assertEquals( | |
- 'rn=login, rt=config', | |
- $this->methodAnnotation('stringWithEqualSigns', 'permission') | |
- ); | |
- } | |
- | |
- /** | |
- * Test string assignment without whitespace is parsed correctly. | |
- * | |
- */ | |
- #[@test] | |
- public function stringAssignedWithoutWhitespace() { | |
- $this->assertEquals( | |
- array('name' => 'verbose', 'short' => 'v'), | |
- $this->methodAnnotation('stringAssignedWithoutWhitespace', 'arg') | |
- ); | |
- } | |
- | |
- /** | |
- * Test annotation with mulitple values containing equal signs | |
- * is parsed correctly. | |
- * | |
- */ | |
- #[@test, @ignore('Test needs adjustment in XPClass and AnnotatedClass')] | |
- public function multipleValuesWithStringsAndEqualSigns() { | |
- $this->assertEquals( | |
- array('rn=login, rt=config1', 'rn=login, rt=config2'), | |
- $this->methodAnnotation('multipleValuesWithStringsAndEqualSigns') | |
- ); | |
- } | |
- | |
} | |
?> |
Performance shootout between this one here (see top) and the implementations in https://gist.github.com/1071362:
Timm Friebe@samson ~/devel/1071362
$ make shootout
make[1]: Entering directory `/home/Timm Friebe/devel/1071362'
@complete: lang.FileSystemClassLoader<D:\cygwin\home\Timm Friebe\devel\1071362\complete\>
==========================================
>> #[@test]: 100000 in 0.424 seconds
>> #[@test(expect = "lang.FormatException")]: 100000 in 3.654 seconds
>> #[@named('sybase')]: 100000 in 1.844 seconds
>> #[@permission('rn=login, rt=config')]: 100000 in 2.003 seconds
>> #[@arg(name= "verbose", short="v")]: 100000 in 4.886 seconds
>> #[@arg(position= 0)]: 100000 in 2.952 seconds
>> #[@fromxml(xpath= "/root/element[position() = 3]/@id")]: 100000 in 4.244 seconds
>> #[@webmethod, @restricted(role= "admin")]: 100000 in 3.413 seconds
>> #[@restricted(roles = array("admin", "root"))]: 100000 in 4.608 seconds
>> #[@require(permissions= array("rn=a", "rn=b"))]: 100000 in 4.429 seconds
==========================================
Total: 10 tests(s) in 32.456 seconds, avg. 3.246 seconds
make[1]: Leaving directory `/home/Timm Friebe/devel/1071362'
make[1]: Entering directory `/home/Timm Friebe/devel/1071362'
@regex: lang.FileSystemClassLoader<D:\cygwin\home\Timm Friebe\devel\1071362\regex\>
==========================================
>> #[@test]: 100000 in 1.888 seconds
>> #[@test(expect = "lang.FormatException")]: 100000 in 2.187 seconds
>> #[@named('sybase')]: 100000 in 2.822 seconds
>> #[@permission('rn=login, rt=config')]: 100000 in 3.029 seconds
>> #[@arg(name= "verbose", short="v")]: 100000 in 2.407 seconds
>> #[@arg(position= 0)]: 100000 in 2.124 seconds
>> #[@fromxml(xpath= "/root/element[position() = 3]/@id")]: 100000 in 2.635 seconds
>> #[@webmethod, @restricted(role= "admin")]: 100000 in 2.745 seconds
>> #[@restricted(roles = array("admin", "root"))]: 100000 in 2.381 seconds
>> #[@require(permissions= array("rn=a", "rn=b"))]: 100000 in 2.563 seconds
==========================================
Total: 10 tests(s) in 24.782 seconds, avg. 2.478 seconds
make[1]: Leaving directory `/home/Timm Friebe/devel/1071362'
make[1]: Entering directory `/home/Timm Friebe/devel/1071362'
@scanner: lang.FileSystemClassLoader<D:\cygwin\home\Timm Friebe\devel\1071362\scanner\>
==========================================
>> #[@test]: 100000 in 0.318 seconds
>> #[@test(expect = "lang.FormatException")]: 100000 in 1.577 seconds
>> #[@named('sybase')]: 100000 in 0.598 seconds
>> #[@permission('rn=login, rt=config')]: 100000 in 0.611 seconds
>> #[@arg(name= "verbose", short="v")]: 100000 in 1.829 seconds
>> #[@arg(position= 0)]: 100000 in 1.554 seconds
>> #[@fromxml(xpath= "/root/element[position() = 3]/@id")]: 100000 in 2.077 seconds
>> #[@webmethod, @restricted(role= "admin")]: 100000 in 1.849 seconds
>> #[@restricted(roles = array("admin", "root"))]: 100000 in 1.832 seconds
>> #[@require(permissions= array("rn=a", "rn=b"))]: 100000 in 2.088 seconds
==========================================
Total: 10 tests(s) in 14.334 seconds, avg. 1.433 seconds
make[1]: Leaving directory `/home/Timm Friebe/devel/1071362'
So we still have a little bit of work in front of us.
Found 1 small issue: use $content
instead of $context
in raise()
calls.
Also trying to squeeze the last bits of performance, I found the following will increase speed with about 15%:
-
Use
preg_match('/^([a-z_]+) *=/i', ...)
instead ofpreg_match('/^([a-z_]+) *= */i, ...)'
and$pos+= strlen($key[0][0]); $pos+= strspn($content, ' ', $pos);
instead of
$pos+= strlen($key[0][0]);
- Remove char-by-char traversing with
strcspn()
calls:
This:
$q= $content{$pos} ;
$p= $pos;
while (++$p < strlen($content)) {
if ($q === $content{$p} && '\\' !== $content{$p- 1}) break;
}
becomes:
$q= $content{$pos} ;
$p= $pos + 1;
while (($pp= strcspn($content, $q, $p)) != 0) {
$p+= $pp;
if ('\\' !== $content{$p- 1}) break;
}
And this (which I think it has an error in counting brackets, needing an extra closing bracket):
} else if ('array' === substr($content, $pos, 5)) { // Arrays
$b= 1;
$p= $pos;
while ($b > 0 && ++$p < strlen($content)) {
if ('(' === $content{$p}) $b++;
if (')' === $content{$p}) $b--;
}
if (!is_array($complex= @eval('return '.substr($content, $pos, $p- $pos).';'))) {
raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed array in '.$context);
}
$pos= $p+ 1;
} else { // Any other
becomes:
} else if ('array(' === substr($content, $pos, 6)) { // Arrays
$b= 1;
$p= $pos + 6;
while ($b > 0 && ($pp= strcspn($content, '()', $p)) != 0) {
$p+= $pp;
if ('(' === $content{$p}) $b++; else if (')' === $content{$p}) $b--;
}
if (!is_array($complex= @eval('return '.substr($content, $pos, $p- $pos + 1).';'))) {
raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed array in '.$content);
}
$pos= $p+ 1;
} else { // Any other
The scanner cannot handle annotation with key/val arrays:
@anno(allow= array(uid= 0, user= 'root', gid= 0, group= 'wheel'))
@mrosoiu, nice performance tweaks, I'll test them and see what it gives. For the array syntax, it should be array('uid' => 0, 'user' => ...)
because annotations keys' values are defined as being PHP literals.
With two of the improvements applied (the regex one didn't yield anything above or below the margin error), before: 33.201 seconds, after: 29.441 seconds
Curious about the regex change not yielding any improvements. It's true that I've used a different set of annotations to run the perf test against. I'll investigate more.
Improved version, after quite a bit of work:
$ make shootout
make[1]: Entering directory `/home/Timm Friebe/devel/1071362'
@complete: lang.FileSystemClassLoader<D:\cygwin\home\Timm Friebe\devel\1071362\complete\>
==========================================
>> #[@test]: 100000 in 0.423 seconds
>> #[@test(expect = "lang.FormatException")]: 100000 in 2.307 seconds
>> #[@named('sybase')]: 100000 in 1.669 seconds
>> #[@permission('rn=login, rt=config')]: 100000 in 1.637 seconds
>> #[@arg(name= "verbose", short="v")]: 100000 in 3.396 seconds
>> #[@arg(position= 0)]: 100000 in 2.027 seconds
>> #[@fromxml(xpath= "/root/element[position() = 3]/@id")]: 100000 in 2.334 seconds
>> #[@webmethod, @restricted(role= "admin")]: 100000 in 2.589 seconds
>> #[@restricted(roles = array("admin", "root"))]: 100000 in 2.464 seconds
>> #[@require(permissions= array("rn=a", "rn=b"))]: 100000 in 2.790 seconds
>> #[@overloaded(signatures= array(array("string"), array("string", "string")))]: 100000 in 3.050 seconds
==========================================
Total: 11 tests(s) in 24.686 seconds, avg. 2.244 seconds
make[1]: Leaving directory `/home/Timm Friebe/devel/1071362'
make[1]: Entering directory `/home/Timm Friebe/devel/1071362'
@regex: lang.FileSystemClassLoader<D:\cygwin\home\Timm Friebe\devel\1071362\regex\>
==========================================
>> #[@test]: 100000 in 1.916 seconds
>> #[@test(expect = "lang.FormatException")]: 100000 in 2.179 seconds
>> #[@named('sybase')]: 100000 in 2.817 seconds
>> #[@permission('rn=login, rt=config')]: 100000 in 3.044 seconds
>> #[@arg(name= "verbose", short="v")]: 100000 in 2.439 seconds
>> #[@arg(position= 0)]: 100000 in 2.144 seconds
>> #[@fromxml(xpath= "/root/element[position() = 3]/@id")]: 100000 in 2.662 seconds
>> #[@webmethod, @restricted(role= "admin")]: 100000 in 2.788 seconds
>> #[@restricted(roles = array("admin", "root"))]: 100000 in 2.398 seconds
>> #[@require(permissions= array("rn=a", "rn=b"))]: 100000 in 2.611 seconds
>> #[@overloaded(signatures= array(array("string"), array("string", "string")))]: 100000 in 2.611 seconds
==========================================
Total: 11 tests(s) in 27.611 seconds, avg. 2.510 seconds
make[1]: Leaving directory `/home/Timm Friebe/devel/1071362'
make[1]: Entering directory `/home/Timm Friebe/devel/1071362'
@scanner: lang.FileSystemClassLoader<D:\cygwin\home\Timm Friebe\devel\1071362\scanner\>
==========================================
>> #[@test]: 100000 in 0.323 seconds
>> #[@test(expect = "lang.FormatException")]: 100000 in 1.603 seconds
>> #[@named('sybase')]: 100000 in 0.610 seconds
>> #[@permission('rn=login, rt=config')]: 100000 in 0.618 seconds
>> #[@arg(name= "verbose", short="v")]: 100000 in 1.842 seconds
>> #[@arg(position= 0)]: 100000 in 1.573 seconds
>> #[@fromxml(xpath= "/root/element[position() = 3]/@id")]: 100000 in 2.081 seconds
>> #[@webmethod, @restricted(role= "admin")]: 100000 in 1.852 seconds
>> #[@restricted(roles = array("admin", "root"))]: 100000 in 1.848 seconds
>> #[@require(permissions= array("rn=a", "rn=b"))]: 100000 in 2.095 seconds
>> #[@overloaded(signatures= array(array("string"), array("string", "string")))]: 100000 in 2.327 seconds
==========================================
Total: 11 tests(s) in 16.772 seconds, avg. 1.525 seconds
make[1]: Leaving directory `/home/Timm Friebe/devel/1071362'
Now faster than the original regex on average!
Passes core test suite: