Repository: phpbench/phpbench (php)

Issue:
Unable to use DateTime value in provider
If I use a `DateTime` instance in a provider, phpbench errors with:

>In Reflector.php line 109:
>Overloaded object of type DateTime is not compatible with RecursiveArrayIterator

```
    public function provideTypes()
    {
        yield 'first' => ['time' => new DateTime()];
    }
```

Pull request:
Serialize objects
Fixes #615 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd2fb302..f52de011 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ dev-master
 Improvements:
 
 - Support for binary data in param providers #532
+- Support serializable objects in param providers #823
 
 Bug fix:
 
diff --git a/lib/Serializer/XmlDecoder.php b/lib/Serializer/XmlDecoder.php
index ed3ba0da..aabae6e3 100644
--- a/lib/Serializer/XmlDecoder.php
+++ b/lib/Serializer/XmlDecoder.php
@@ -13,6 +13,7 @@
 namespace PhpBench\Serializer;
 
 use function base64_decode;
+use DOMElement;
 use PhpBench\Assertion\AssertionResult;
 use PhpBench\Dom\Document;
 use PhpBench\Dom\Element;
@@ -110,6 +111,7 @@ class XmlDecoder
         $resultClasses = [];
 
         foreach ($suiteEl->query('//result') as $resultEl) {
+            assert($resultEl instanceof DOMElement);
             $class = $resultEl->getAttribute('class');
 
             if (!class_exists($class)) {
@@ -125,6 +127,7 @@ class XmlDecoder
         $suite->setEnvInformations($informations);
 
         foreach ($suiteEl->query('./benchmark') as $benchmarkEl) {
+            assert($benchmarkEl instanceof Element);
             $benchmark = $suite->createBenchmark(
                 $benchmarkEl->getAttribute('class')
             );
@@ -218,6 +221,12 @@ class XmlDecoder
                 continue;
             }
 
+            if ($parameterEl->getAttribute('type') === XmlEncoder::PARAM_TYPE_SERIALIZED) {
+                $parameters[$name] = unserialize(base64_decode($parameterEl->nodeValue));
+
+                continue;
+            }
+
             if ($parameterEl->getAttribute('xsi:nil') === 'true') {
                 $parameters[$name] = null;
 
diff --git a/lib/Serializer/XmlEncoder.php b/lib/Serializer/XmlEncoder.php
index 726c5c29..e8a80e10 100644
--- a/lib/Serializer/XmlEncoder.php
+++ b/lib/Serializer/XmlEncoder.php
@@ -14,6 +14,7 @@ namespace PhpBench\Serializer;
 
 use function base64_encode;
 use DOMElement;
+use Exception;
 use PhpBench\Dom\Document;
 use PhpBench\Dom\Element;
 use PhpBench\Model\Benchmark;
@@ -24,6 +25,7 @@ use PhpBench\Model\SuiteCollection;
 use PhpBench\Model\Variant;
 use PhpBench\PhpBench;
 use PhpBench\Util\TimeUnit;
+use RuntimeException;
 
 /**
  * Encodes the Suite object graph into an XML document.
@@ -32,6 +34,7 @@ class XmlEncoder
 {
     public const PARAM_TYPE_BINARY = 'binary';
     public const PARAM_TYPE_COLLECTION = 'collection';
+    const PARAM_TYPE_SERIALIZED = 'serialized';
 
     /**
      * Encode a Suite object into a XML document.
@@ -232,10 +235,17 @@ class XmlEncoder
             return $parameterEl;
         }
 
-        throw new \InvalidArgumentException(sprintf(
-            'Parameters must be either scalars or arrays, got: %s',
-            is_object($value) ? get_class($value) : gettype($value)
-        ));
+        try {
+            $serialized = @serialize($value);
+        } catch (Exception $e) {
+            throw new RuntimeException(sprintf(
+                'Cannot serialize object of type "%s" for parameter "%s"', gettype($value), $name
+            ));
+        }
+        $parameterEl->setAttribute('type', self::PARAM_TYPE_SERIALIZED);
+        $parameterEl->appendChild(
+            $parameterEl->ownerDocument->createCDATASection(base64_encode($serialized))
+        );
     }
 
     private function appendExecutor(Element $subjectEl, ResolvedExecutor $executor = null): void
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 99430d07..4c3cfa5e 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -7,12 +7,7 @@ parameters:
 
 		-
 			message: "#^Call to an undefined method DOMNode\\:\\:getAttribute\\(\\)\\.$#"
-			count: 22
-			path: lib/Serializer/XmlDecoder.php
-
-		-
-			message: "#^Parameter \\#2 \\$benchmarkEl of method PhpBench\\\\Serializer\\\\XmlDecoder\\:\\:processBenchmark\\(\\) expects PhpBench\\\\Dom\\\\Element, DOMNode given\\.$#"
-			count: 1
+			count: 20
 			path: lib/Serializer/XmlDecoder.php
 
 		-


Test patch:
diff --git a/tests/Unit/Serializer/XmlDecoderTest.php b/tests/Unit/Serializer/XmlDecoderTest.php
index a8abd9d2..c0fbd458 100644
--- a/tests/Unit/Serializer/XmlDecoderTest.php
+++ b/tests/Unit/Serializer/XmlDecoderTest.php
@@ -188,6 +188,21 @@ EOT
         );
     }
 
+    public function doTestDate(SuiteCollection $collection): void
+    {
+        $dom = $this->encode($collection);
+
+        $decoder = new XmlDecoder();
+        $collection = $decoder->decode($dom);
+
+        $decodedDom = $this->encode($collection);
+
+        $this->assertEquals(
+            $dom->dump(),
+            $decodedDom->dump()
+        );
+    }
+
     private function encode(SuiteCollection $collection)
     {
         $xmlEncoder = new XmlEncoder();
diff --git a/tests/Unit/Serializer/XmlEncoderTest.php b/tests/Unit/Serializer/XmlEncoderTest.php
index bd7c53ef..3a2a25b4 100644
--- a/tests/Unit/Serializer/XmlEncoderTest.php
+++ b/tests/Unit/Serializer/XmlEncoderTest.php
@@ -12,6 +12,7 @@
 
 namespace PhpBench\Tests\Unit\Serializer;
 
+use PhpBench\Dom\Document;
 use PhpBench\Environment\Information;
 use PhpBench\Model\Benchmark;
 use PhpBench\Model\Iteration;
@@ -22,6 +23,7 @@ use PhpBench\Model\Variant;
 use PhpBench\PhpBench;
 use PhpBench\Serializer\XmlEncoder;
 use PhpBench\Tests\Util\Approval;
+use RuntimeException;
 
 class XmlEncoderTest extends XmlTestCase
 {
@@ -47,16 +49,46 @@ class XmlEncoderTest extends XmlTestCase
         $params = $approval->getConfig(0);
 
         $collection = $this->getSuiteCollection($params);
-        $xmlEncoder = new XmlEncoder();
-        $dom = $xmlEncoder->encode($collection);
-        $approval->approve(str_replace(PhpBench::version(), 'PHPBENCH_VERSION', $dom->dump()));
+        $dom = $this->encode($collection);
+        $approval->approve($this->dumpNormalized($dom));
     }
 
     public function doTestBinary(SuiteCollection $collection): void
     {
         $approval = Approval::create(__DIR__ . '/examples/binary1.example', 0);
+        $dom = $this->encode($collection);
+        $approval->approve($this->dumpNormalized($dom));
+    }
+
+    public function doTestDate(SuiteCollection $collection): void
+    {
+        $approval = Approval::create(__DIR__ . '/examples/date1.example', 0);
+        $dom = $this->encode($collection);
+        $approval->approve($this->dumpNormalized($dom));
+    }
+
+    public function testUnserizableParameter(): void
+    {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessage('Cannot serialize');
+        $collection = $this->getSuiteCollection([
+            'params' => [
+                'invalid' => function (): void {
+                }
+            ],
+        ]);
+        $this->encode($collection);
+    }
+
+    private function dumpNormalized(Document $dom)
+    {
+        return str_replace(PhpBench::version(), 'PHPBENCH_VERSION', $dom->dump());
+    }
+
+    private function encode(SuiteCollection $collection): Document
+    {
         $xmlEncoder = new XmlEncoder();
-        $dom = $xmlEncoder->encode($collection);
-        $approval->approve(str_replace(PhpBench::version(), 'PHPBENCH_VERSION', $dom->dump()));
+
+        return $xmlEncoder->encode($collection);
     }
 }
diff --git a/tests/Unit/Serializer/XmlTestCase.php b/tests/Unit/Serializer/XmlTestCase.php
index c464b968..eb3ef3e9 100644
--- a/tests/Unit/Serializer/XmlTestCase.php
+++ b/tests/Unit/Serializer/XmlTestCase.php
@@ -12,6 +12,7 @@
 
 namespace PhpBench\Tests\Unit\Serializer;
 
+use DateTime;
 use PhpBench\Assertion\AssertionResult;
 use PhpBench\Assertion\VariantAssertionResults;
 use PhpBench\Environment\Information;
@@ -155,5 +156,16 @@ abstract class XmlTestCase extends TestCase
         $this->doTestBinary($collection);
     }
 
+    public function testDate(): void
+    {
+        $collection = $this->getSuiteCollection([
+            'params' => [
+                'foo' => new DateTime('2021-01-01 00:00:00+00:00'),
+            ]
+        ]);
+        $this->doTestDate($collection);
+    }
+
     abstract public function doTestBinary(SuiteCollection $collection): void;
+    abstract public function doTestDate(SuiteCollection $collection): void;
 }
diff --git a/tests/Unit/Serializer/examples/date1.example b/tests/Unit/Serializer/examples/date1.example
new file mode 100644
index 00000000..39bb7bf9
--- /dev/null
+++ b/tests/Unit/Serializer/examples/date1.example
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<phpbench xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="PHPBENCH_VERSION">
+  <suite tag="test" context="test" date="2015-01-01T00:00:00+00:00" config-path="/path/to/config.json" uuid="1234">
+    <env>
+      <info1>
+        <value name="foo" type="string">fooo &amp; bar</value>
+      </info1>
+    </env>
+    <benchmark class="Bench1">
+      <subject name="subjectName">
+        <executor name="foo"/>
+        <variant sleep="5" output-time-unit="milliseconds" output-time-precision="7" output-mode="throughput" revs="100" warmup="50" retry-threshold="10">
+          <parameter-set name="some params">
+            <parameter name="foo" type="serialized"><![CDATA[Tzo4OiJEYXRlVGltZSI6Mzp7czo0OiJkYXRlIjtzOjI2OiIyMDIxLTAxLTAxIDAwOjAwOjAwLjAwMDAwMCI7czoxMzoidGltZXpvbmVfdHlwZSI7aToxO3M6ODoidGltZXpvbmUiO3M6NjoiKzAwOjAwIjt9]]></parameter>
+          </parameter-set>
+          <iteration time-net="10" time-revs="1" time-avg="10" mem-peak="100" mem-real="110" mem-final="109" comp-z-value="0" comp-deviation="0"/>
+          <stats max="0.1" mean="0.1" min="0.1" mode="0.1" rstdev="0" stdev="0" sum="0.1" variance="0"/>
+        </variant>
+      </subject>
+    </benchmark>
+    <result key="time" class="PhpBench\Model\Result\TimeResult"/>
+    <result key="mem" class="PhpBench\Model\Result\MemoryResult"/>
+    <result key="comp" class="PhpBench\Model\Result\ComputedResult"/>
+  </suite>
+</phpbench>