Merge branch '4.4' into 5.1

* 4.4:
  fix merge
  drop logger mock in favor of using the BufferingLogger
  catch ValueError thrown on PHP 8
  [Yaml Parser] Fix edge cases when parsing multiple documents
  fix parsing comments not prefixed by a space
  [Translator] Make sure a null locale is handled properly
  deal with errors being thrown on PHP 8
  [Cache] Allow cache tags to be objects implementing __toString()
  [HttpKernel] Do not override max_redirects option in HttpClientKernel
  remove superfluous cast
  [HttpClient] Support for CURLOPT_LOCALPORT.
  Upgrade PHPUnit to 8.5 (php 7.2) and 9.3 (php >= 7.3).
  Fixed exception message formatting
  [FrameworkBundle] Fix error in xsd which prevent to register more than one metadata
  [Console] work around disabled putenv()
  [PhpUnitBridge] Fix error with ReflectionClass
  [HttpClient][HttpClientTrait] don't calculate alternatives if option is auth_ntlm
  Change 'cache_key' to AbstractRendererEngine::CACHE_KEY_VAR
  Upgrade PHPUnit to 8.5 (php 7.2) and 9.3 (php >= 7.3).
This commit is contained in:
Christian Flothmann 2020-09-18 16:27:32 +02:00
commit 4ee591bec6
29 changed files with 313 additions and 42 deletions

View File

@ -12,10 +12,10 @@ if (!getenv('SYMFONY_PHPUNIT_VERSION')) {
if (false === getenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT') && false !== strpos(@file_get_contents(__DIR__.'/src/Symfony/Component/HttpKernel/Kernel.php'), 'const MAJOR_VERSION = 3;')) {
putenv('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1');
}
if (\PHP_VERSION_ID >= 80000) {
putenv('SYMFONY_PHPUNIT_VERSION=9.3');
if (\PHP_VERSION_ID < 70300) {
putenv('SYMFONY_PHPUNIT_VERSION=8.5');
} else {
putenv('SYMFONY_PHPUNIT_VERSION=8.3');
putenv('SYMFONY_PHPUNIT_VERSION=9.3');
}
} elseif (\PHP_VERSION_ID >= 70000) {
putenv('SYMFONY_PHPUNIT_VERSION=6.5');

View File

@ -109,7 +109,7 @@ class CoverageListenerTrait
// Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException
$covers = array_filter($covers, function ($class) {
$reflector = new ReflectionClass($class);
$reflector = new \ReflectionClass($class);
return $reflector->isUserDefined();
});

View File

@ -1197,7 +1197,7 @@ class Configuration implements ConfigurationInterface
return $middleware;
}
if (1 < \count($middleware)) {
throw new \InvalidArgumentException(sprintf('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, %s given.', json_encode($middleware)));
throw new \InvalidArgumentException('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, '.json_encode($middleware).' given.');
}
return [

View File

@ -331,7 +331,7 @@
<xsd:complexType name="metadata">
<xsd:sequence>
<xsd:any minOccurs="0" processContents="lax"/>
<xsd:any minOccurs="0" maxOccurs="unbounded" processContents="lax"/>
</xsd:sequence>
</xsd:complexType>

View File

@ -10,6 +10,10 @@ $container->loadFromExtension('framework', [
FrameworkExtensionTest::class,
],
'initial_marking' => ['draft'],
'metadata' => [
'title' => 'article workflow',
'description' => 'workflow for articles'
],
'places' => [
'draft',
'wait_for_journalist',

View File

@ -35,6 +35,10 @@
<framework:from>approved_by_spellchecker</framework:from>
<framework:to>published</framework:to>
</framework:transition>
<framework:metadata>
<framework:title>article workflow</framework:title>
<framework:description>workflow for articles</framework:description>
</framework:metadata>
</framework:workflow>
<framework:workflow name="pull_request">

View File

@ -5,6 +5,9 @@ framework:
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_marking: [draft]
metadata:
title: article workflow
description: workflow for articles
places:
# simple format
- draft

View File

@ -54,6 +54,7 @@ use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Workflow;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
abstract class FrameworkExtensionTest extends TestCase
{
@ -231,6 +232,12 @@ abstract class FrameworkExtensionTest extends TestCase
);
$this->assertCount(4, $workflowDefinition->getArgument(1));
$this->assertSame(['draft'], $workflowDefinition->getArgument(2));
$metadataStoreDefinition = $workflowDefinition->getArgument(3);
$this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass());
$this->assertSame([
'title' => 'article workflow',
'description' => 'workflow for articles',
], $metadataStoreDefinition->getArgument(0));
$this->assertTrue($container->hasDefinition('state_machine.pull_request'), 'State machine is registered as a service');
$this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.pull_request')->getParent());
@ -255,7 +262,7 @@ abstract class FrameworkExtensionTest extends TestCase
$metadataStoreDefinition = $stateMachineDefinition->getArgument(3);
$this->assertInstanceOf(Definition::class, $metadataStoreDefinition);
$this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass());
$this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass());
$workflowMetadata = $metadataStoreDefinition->getArgument(0);
$this->assertSame(['title' => 'workflow title'], $workflowMetadata);

View File

@ -119,9 +119,10 @@ final class CacheItem implements ItemInterface
$tags = [$tags];
}
foreach ($tags as $tag) {
if (!\is_string($tag)) {
throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given.', get_debug_type($tag)));
if (!\is_string($tag) && !(\is_object($tag) && method_exists($tag, '__toString'))) {
throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag)));
}
$tag = (string) $tag;
if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
continue;
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Tests\Fixtures\StringableTag;
class CacheItemTest extends TestCase
{
@ -61,9 +62,11 @@ class CacheItemTest extends TestCase
$this->assertSame($item, $item->tag('foo'));
$this->assertSame($item, $item->tag(['bar', 'baz']));
$this->assertSame($item, $item->tag(new StringableTag('qux')));
$this->assertSame($item, $item->tag([new StringableTag('quux'), new StringableTag('quuux')]));
(\Closure::bind(function () use ($item) {
$this->assertSame(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'], $item->newMetadata[CacheItem::METADATA_TAGS]);
$this->assertSame(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz', 'qux' => 'qux', 'quux' => 'quux', 'quuux' => 'quuux'], $item->newMetadata[CacheItem::METADATA_TAGS]);
}, $this, CacheItem::class))();
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Tests\Fixtures;
class StringableTag
{
/**
* @var string
*/
private $tag;
public function __construct(string $tag)
{
$this->tag = $tag;
}
public function __toString(): string
{
return $this->tag;
}
}

View File

@ -107,8 +107,10 @@ class Application implements ResetInterface
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
putenv('LINES='.$this->terminal->getHeight());
putenv('COLUMNS='.$this->terminal->getWidth());
if (\function_exists('putenv')) {
@putenv('LINES='.$this->terminal->getHeight());
@putenv('COLUMNS='.$this->terminal->getWidth());
}
if (null === $input) {
$input = new ArgvInput();
@ -891,7 +893,9 @@ class Application implements ResetInterface
$input->setInteractive(false);
}
putenv('SHELL_VERBOSITY='.$shellVerbosity);
if (\function_exists('putenv')) {
@putenv('SHELL_VERBOSITY='.$shellVerbosity);
}
$_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
$_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\EventDispatcher\Tests\Debug;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\BufferingLogger;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -192,41 +193,57 @@ class TraceableEventDispatcherTest extends TestCase
public function testLogger()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$logger = new BufferingLogger();
$dispatcher = new EventDispatcher();
$tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger);
$tdispatcher->addListener('foo', $listener1 = function () {});
$tdispatcher->addListener('foo', $listener2 = function () {});
$logger->expects($this->exactly(2))
->method('debug')
->withConsecutive(
['Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure']],
['Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure']]
);
$tdispatcher->dispatch(new Event(), 'foo');
$this->assertSame([
[
'debug',
'Notified event "{event}" to listener "{listener}".',
['event' => 'foo', 'listener' => 'closure'],
],
[
'debug',
'Notified event "{event}" to listener "{listener}".',
['event' => 'foo', 'listener' => 'closure'],
],
], $logger->cleanLogs());
}
public function testLoggerWithStoppedEvent()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$logger = new BufferingLogger();
$dispatcher = new EventDispatcher();
$tdispatcher = new TraceableEventDispatcher($dispatcher, new Stopwatch(), $logger);
$tdispatcher->addListener('foo', $listener1 = function (Event $event) { $event->stopPropagation(); });
$tdispatcher->addListener('foo', $listener2 = function () {});
$logger->expects($this->exactly(3))
->method('debug')
->withConsecutive(
['Notified event "{event}" to listener "{listener}".', ['event' => 'foo', 'listener' => 'closure']],
['Listener "{listener}" stopped propagation of the event "{event}".', ['event' => 'foo', 'listener' => 'closure']],
['Listener "{listener}" was not called for event "{event}".', ['event' => 'foo', 'listener' => 'closure']]
);
$tdispatcher->dispatch(new Event(), 'foo');
$this->assertSame([
[
'debug',
'Notified event "{event}" to listener "{listener}".',
['event' => 'foo', 'listener' => 'closure'],
],
[
'debug',
'Listener "{listener}" stopped propagation of the event "{event}".',
['event' => 'foo', 'listener' => 'closure'],
],
[
'debug',
'Listener "{listener}" was not called for event "{event}".',
['event' => 'foo', 'listener' => 'closure'],
],
], $logger->cleanLogs());
}
public function testDispatchCallListeners()

View File

@ -25,6 +25,7 @@
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/expression-language": "^4.4|^5.0",
"symfony/config": "^4.4|^5.0",
"symfony/error-handler": "^4.4|^5.0",
"symfony/http-foundation": "^4.4|^5.0",
"symfony/service-contracts": "^1.1|^2",
"symfony/stopwatch": "^4.4|^5.0",

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
@ -112,7 +113,7 @@ abstract class BaseType extends AbstractType
// collection form have different types (dynamically), they should
// be rendered differently.
// https://github.com/symfony/symfony/issues/5038
'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
AbstractRendererEngine::CACHE_KEY_VAR => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getBlockPrefix(),
]);
}

View File

@ -267,7 +267,14 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
}
if ($options['bindto']) {
$curlopts[file_exists($options['bindto']) ? \CURLOPT_UNIX_SOCKET_PATH : \CURLOPT_INTERFACE] = $options['bindto'];
if (file_exists($options['bindto'])) {
$curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
} elseif (preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
$curlopts[\CURLOPT_INTERFACE] = $matches[1];
$curlopts[\CURLOPT_LOCALPORT] = $matches[2];
} else {
$curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
}
}
if (0 < $options['max_duration']) {

View File

@ -198,6 +198,16 @@ trait HttpClientTrait
continue;
}
if ('auth_ntlm' === $name) {
if (!\extension_loaded('curl')) {
$msg = 'try installing the "curl" extension to use "%s" instead.';
} else {
$msg = 'try using "%s" instead.';
}
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
}
$alternatives = [];
foreach ($defaultOptions as $key => $v) {
@ -206,10 +216,6 @@ trait HttpClientTrait
}
}
if ('auth_ntlm' === $name) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", try using CurlHttpClient instead.', __CLASS__));
}
throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
}

View File

@ -34,6 +34,74 @@ class CurlHttpClientTest extends HttpClientTestCase
return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]);
}
public function testBindToPort()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://localhost:8057', ['bindto' => '127.0.0.1:9876']);
$response->getStatusCode();
$r = new \ReflectionProperty($response, 'handle');
$r->setAccessible(true);
$curlInfo = curl_getinfo($r->getValue($response));
self::assertSame('127.0.0.1', $curlInfo['local_ip']);
self::assertSame(9876, $curlInfo['local_port']);
}
/**
* @requires PHP 7.2.17
*/
public function testHttp2PushVulcain()
{
$client = $this->getVulcainClient();
$logger = new TestLogger();
$client->setLogger($logger);
$responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [
'headers' => [
'Preload' => '/documents/*/id',
],
])->toArray();
foreach ($responseAsArray['documents'] as $document) {
$client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray();
}
$client->reset();
$expected = [
'Request: "GET https://127.0.0.1:3000/json"',
'Queueing pushed response: "https://127.0.0.1:3000/json/1"',
'Queueing pushed response: "https://127.0.0.1:3000/json/2"',
'Queueing pushed response: "https://127.0.0.1:3000/json/3"',
'Response: "200 https://127.0.0.1:3000/json"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"',
'Response: "200 https://127.0.0.1:3000/json/1"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"',
'Response: "200 https://127.0.0.1:3000/json/2"',
'Accepting pushed response: "GET https://127.0.0.1:3000/json/3"',
'Response: "200 https://127.0.0.1:3000/json/3"',
];
$this->assertSame($expected, $logger->logs);
}
/**
* @requires PHP 7.2.17
*/
public function testHttp2PushVulcainWithUnusedResponse()
{
$client = $this->getVulcainClient();
$logger = new TestLogger();
$client->setLogger($logger);
$responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [
'headers' => [
'Preload' => '/documents/*/id',
],
])->toArray();
}
public function testTimeoutIsNotAFatalError()
{
if ('\\' === \DIRECTORY_SEPARATOR) {

View File

@ -35,7 +35,7 @@ final class HttpClientKernel implements HttpKernelInterface
public function __construct(HttpClientInterface $client = null)
{
if (!class_exists(HttpClient::class)) {
if (null === $client && !class_exists(HttpClient::class)) {
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
}
@ -53,7 +53,6 @@ final class HttpClientKernel implements HttpKernelInterface
$response = $this->client->request($request->getMethod(), $request->getUri(), [
'headers' => $headers,
'body' => $body,
'max_redirects' => 0,
] + $request->attributes->get('http_client_options', []));
$response = new Response($response->getContent(!$catch), $response->getStatusCode(), $response->getHeaders(!$catch));

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpClientKernel;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class HttpClientKernelTest extends TestCase
{
public function testHandlePassesMaxRedirectsHttpClientOption()
{
$request = new Request();
$request->attributes->set('http_client_options', ['max_redirects' => 50]);
$response = $this->getMockBuilder(ResponseInterface::class)->getMock();
$response->expects($this->once())->method('getStatusCode')->willReturn(200);
$client = $this->getMockBuilder(HttpClientInterface::class)->getMock();
$client
->expects($this->once())
->method('request')
->willReturnCallback(function (string $method, string $uri, array $options) use ($request, $response) {
$this->assertSame($request->getMethod(), $method);
$this->assertSame($request->getUri(), $uri);
$this->assertArrayHasKey('max_redirects', $options);
$this->assertSame(50, $options['max_redirects']);
return $response;
});
$kernel = new HttpClientKernel($client);
$kernel->handle($request);
}
}

View File

@ -20,6 +20,7 @@
"symfony/deprecation-contracts": "^2.1",
"symfony/error-handler": "^4.4|^5.0",
"symfony/event-dispatcher": "^5.0",
"symfony/http-client-contracts": "^1.1|^2",
"symfony/http-foundation": "^4.4|^5.0",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php73": "^1.9",

View File

@ -19,6 +19,19 @@ use Symfony\Component\Translation\Translator;
class TranslatorTest extends TestCase
{
private $defaultLocale;
protected function setUp(): void
{
$this->defaultLocale = \Locale::getDefault();
\Locale::setDefault('en');
}
protected function tearDown(): void
{
\Locale::setDefault($this->defaultLocale);
}
/**
* @dataProvider getInvalidLocalesTests
*/

View File

@ -158,7 +158,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
*/
public function getLocale()
{
return $this->locale;
return $this->locale ?? \Locale::getDefault();
}
/**

View File

@ -78,7 +78,11 @@ class TimezoneValidator extends ConstraintValidator
private static function getPhpTimezones(int $zone, string $countryCode = null): array
{
if (null !== $countryCode) {
return @\DateTimeZone::listIdentifiers($zone, $countryCode) ?: [];
try {
return @\DateTimeZone::listIdentifiers($zone, $countryCode) ?: [];
} catch (\ValueError $e) {
return [];
}
}
return \DateTimeZone::listIdentifiers($zone);

View File

@ -110,6 +110,14 @@ class SplCaster
$a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state';
return $a;
} catch (\Error $e) {
if ('Object not initialized' !== $e->getMessage()) {
throw $e;
}
$a[$prefix.'⚠'] = 'The parent constructor was not called: the object is in an invalid state';
return $a;
}
}

View File

@ -90,7 +90,7 @@ class Inline
}
// some comments are allowed at the end
if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
if (preg_replace('/\s*#.*$/A', '', substr($value, $i))) {
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)), self::$parsedLineNumber + 1, $value, self::$parsedFilename);
}

View File

@ -104,6 +104,7 @@ class Parser
$this->refs = [];
$this->skippedLineNumbers = [];
$this->locallySkippedLineNumbers = [];
$this->totalNumberOfLines = null;
}
return $data;
@ -728,7 +729,7 @@ class Parser
if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
$modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
$data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs((int) $modifiers));
$data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers));
if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
if ('!!binary' === $matches['tag']) {

View File

@ -837,6 +837,16 @@ class InlineTest extends TestCase
self::assertSame('-0123456789', Inline::parse('-0123456789'));
}
public function testParseCommentNotPrefixedBySpaces()
{
self::assertSame('foo', Inline::parse('"foo"#comment'));
}
public function testParseUnquotedStringContainingHashTagNotPrefixedBySpace()
{
self::assertSame('foo#nocomment', Inline::parse('foo#nocomment'));
}
/**
* @dataProvider unquotedExclamationMarkThrowsProvider
*/

View File

@ -2437,6 +2437,39 @@ YAML;
$this->parser->parse($yaml)
);
}
/**
* This is a regression test for a bug where a YAML block with a nested multiline string using | was parsed without
* a trailing \n when a shorter YAML document was parsed before.
*
* When a shorter document was parsed before, the nested string did not have a \n at the end of the string, because
* the Parser thought it was the end of the file, even though it is not.
*/
public function testParsingMultipleDocuments()
{
$shortDocument = 'foo: bar';
$longDocument = <<<YAML
a:
b: |
row
row2
c: d
YAML;
$expected = ['a' => ['b' => "row\nrow2\n"], 'c' => 'd'];
// The parser was not used before, so there is a new line after row2
$this->assertSame($expected, $this->parser->parse($longDocument));
$parser = new Parser();
// The first parsing set and fixed the totalNumberOfLines in the Parser before, so parsing the short document here
// to reproduce the issue. If the issue would not have been fixed, the next assertion will fail
$parser->parse($shortDocument);
// After the total number of lines has been rset the result will be the same as if a new parser was used
// (before, there was no \n after row2)
$this->assertSame($expected, $parser->parse($longDocument));
}
}
class B