From f7dd26aa69017add3bc67a36603c9b40a41f6369 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Mon, 8 Feb 2021 22:42:48 +0000
Subject: [PATCH] Issue #2795567 by joachim, jungle, daffie, Sophie.SK,
 mondrake, ravi.shankar, jonathanshaw, dawehner, AaronBauman, alexpott: Use
 Symfony's VarDumper for easier test debugging with dump()

---
 .../src/Controller/TestPageTestController.php | 14 ++++++
 .../test_page_test/test_page_test.routing.yml |  8 ++++
 .../FunctionalTests/BrowserTestBaseTest.php   | 37 +++++++++++++++
 .../Drupal/KernelTests/KernelTestBase.php     |  6 +++
 .../Drupal/KernelTests/KernelTestBaseTest.php | 22 +++++++++
 core/tests/Drupal/TestTools/TestVarDumper.php | 46 +++++++++++++++++++
 core/tests/Drupal/Tests/BrowserTestBase.php   | 15 ++++++
 core/tests/Drupal/Tests/StreamCapturer.php    | 23 ++++++++++
 core/tests/Drupal/Tests/UnitTestCase.php      | 13 ++++++
 core/tests/Drupal/Tests/UnitTestCaseTest.php  | 44 ++++++++++++++++++
 10 files changed, 228 insertions(+)
 create mode 100644 core/tests/Drupal/TestTools/TestVarDumper.php
 create mode 100644 core/tests/Drupal/Tests/StreamCapturer.php

diff --git a/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php b/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php
index 02ea524b09ee..f23fc92cf59b 100644
--- a/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php
+++ b/core/modules/system/tests/modules/test_page_test/src/Controller/TestPageTestController.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\test_page_test\Controller;
 
+use Drupal\user\Entity\Role;
+
 /**
  * Controller routines for test_page_test routes.
  */
@@ -23,4 +25,16 @@ public function testPage() {
     ];
   }
 
+  /**
+   * Returns a test page and with the call to the dump() function.
+   */
+  public function testPageVarDump() {
+    $role = Role::create(['id' => 'test_role']);
+    dump($role);
+    return [
+      '#title' => t('Test page with var dump'),
+      '#markup' => t('Test page text.'),
+    ];
+  }
+
 }
diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml
index 43b6cf54c187..f3744df5ee61 100644
--- a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml
+++ b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml
@@ -144,3 +144,11 @@ test_page_test.deprecations:
     _controller: '\Drupal\test_page_test\Controller\Test::deprecations'
   requirements:
     _access: 'TRUE'
+
+test_page_test.test_page_var_dump:
+  path: '/test-page-var-dump'
+  defaults:
+    _title: 'Test front page with var dump'
+    _controller: '\Drupal\test_page_test\Controller\TestPageTestController::testPageVarDump'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
index affb55bfc787..10489ebb5f88 100644
--- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
+++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php
@@ -8,7 +8,9 @@
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\StreamCapturer;
 use Drupal\Tests\Traits\Core\CronRunTrait;
+use Drupal\user\Entity\Role;
 use PHPUnit\Framework\ExpectationFailedException;
 
 /**
@@ -949,4 +951,39 @@ public function testDrupalGetHeader() {
     $this->drupalGetHeader('Content-Type');
   }
 
+  /**
+   * Tests the dump() function provided by the var-dumper Symfony component.
+   */
+  public function testVarDump() {
+    // Append the stream capturer to the STDOUT stream, so that we can test the
+    // dump() output and also prevent it from actually outputting in this
+    // particular test.
+    stream_filter_register("capture", StreamCapturer::class);
+    stream_filter_append(STDOUT, "capture");
+
+    // Dump some variables to check that dump() in test code produces output
+    // on the command line that is running the test.
+    $role = Role::load('authenticated');
+    dump($role);
+    dump($role->id());
+
+    $this->assertStringContainsString('Drupal\user\Entity\Role', StreamCapturer::$cache);
+    $this->assertStringContainsString('authenticated', StreamCapturer::$cache);
+
+    // Visit a Drupal page with call to the dump() function to check that dump()
+    // in site code produces output in the requested web page's HTML.
+    $body = $this->drupalGet('test-page-var-dump');
+    $this->assertSession()->statusCodeEquals(200);
+
+    // It is too strict to assert all properties of the Role and it is easy to
+    // break if one of these properties gets removed or gets a new default
+    // value. It should be sufficient to test just a couple of properties.
+    $this->assertStringContainsString('<span class=sf-dump-note>', $body);
+    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">id</span>: "<span class=sf-dump-str title="9 characters">test_role</span>"', $body);
+    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">label</span>: <span class=sf-dump-const>null</span>', $body);
+    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">permissions</span>: []', $body);
+    $this->assertStringContainsString('  #<span class=sf-dump-protected title="Protected property">uuid</span>: "', $body);
+    $this->assertStringContainsString('</samp>}', $body);
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index e5afa6bba918..1c520289dee2 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -22,6 +22,7 @@
 use Drupal\Tests\TestRequirementsTrait;
 use Drupal\Tests\Traits\PhpUnitWarnings;
 use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
+use Drupal\TestTools\TestVarDumper;
 use PHPUnit\Framework\Exception;
 use PHPUnit\Framework\TestCase;
 use Symfony\Component\DependencyInjection\Reference;
@@ -30,6 +31,7 @@
 use org\bovigo\vfs\visitor\vfsStreamPrintVisitor;
 use Drupal\Core\Routing\RouteObjectInterface;
 use Symfony\Component\Routing\Route;
+use Symfony\Component\VarDumper\VarDumper;
 use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
 
 /**
@@ -64,6 +66,9 @@
  * KernelTestBase::installEntitySchema(). Alternately, tests which need modules
  * to be fully installed could inherit from \Drupal\Tests\BrowserTestBase.
  *
+ * Using Symfony's dump() function in Kernel tests will produce output on the
+ * command line, whether the call to dump() is in test code or site code.
+ *
  * @see \Drupal\Tests\KernelTestBase::$modules
  * @see \Drupal\Tests\KernelTestBase::enableModules()
  * @see \Drupal\Tests\KernelTestBase::installConfig()
@@ -223,6 +228,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
    */
   public static function setUpBeforeClass() {
     parent::setUpBeforeClass();
+    VarDumper::setHandler(TestVarDumper::class . '::cliHandler');
 
     // Change the current dir to DRUPAL_ROOT.
     chdir(static::getDrupalRoot());
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
index 4775bf53fa8e..025c5a490b54 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
@@ -5,6 +5,8 @@
 use Drupal\Component\FileCache\FileCacheFactory;
 use Drupal\Core\Database\Database;
 use GuzzleHttp\Exception\GuzzleException;
+use Drupal\Tests\StreamCapturer;
+use Drupal\user\Entity\Role;
 use org\bovigo\vfs\vfsStream;
 use org\bovigo\vfs\visitor\vfsStreamStructureVisitor;
 use PHPUnit\Framework\SkippedTestError;
@@ -396,4 +398,24 @@ public function testKernelTestBaseInstallSchema() {
     $this->assertFalse(Database::getConnection()->schema()->tableExists('key_value'));
   }
 
+  /**
+   * Tests the dump() function provided by the var-dumper Symfony component.
+   */
+  public function testVarDump() {
+    // Append the stream capturer to the STDOUT stream, so that we can test the
+    // dump() output and also prevent it from actually outputting in this
+    // particular test.
+    stream_filter_register("capture", StreamCapturer::class);
+    stream_filter_append(STDOUT, "capture");
+
+    // Dump some variables.
+    $this->enableModules(['system', 'user']);
+    $role = Role::create(['id' => 'test_role']);
+    dump($role);
+    dump($role->id());
+
+    $this->assertStringContainsString('Drupal\user\Entity\Role', StreamCapturer::$cache);
+    $this->assertStringContainsString('test_role', StreamCapturer::$cache);
+  }
+
 }
diff --git a/core/tests/Drupal/TestTools/TestVarDumper.php b/core/tests/Drupal/TestTools/TestVarDumper.php
new file mode 100644
index 000000000000..b47ffd704f8b
--- /dev/null
+++ b/core/tests/Drupal/TestTools/TestVarDumper.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\TestTools;
+
+use Symfony\Component\VarDumper\Cloner\VarCloner;
+use Symfony\Component\VarDumper\Dumper\CliDumper;
+use Symfony\Component\VarDumper\Dumper\HtmlDumper;
+
+/**
+ * Provides handlers for the Symfony VarDumper to work within tests.
+ *
+ * This allows the dump() function to produce output on the terminal without
+ * causing PHPUnit to complain.
+ */
+class TestVarDumper {
+
+  /**
+   * A CLI handler for \Symfony\Component\VarDumper\VarDumper.
+   */
+  public static function cliHandler($var) {
+    $cloner = new VarCloner();
+    $dumper = new CliDumper();
+    fwrite(STDOUT, "\n");
+    $dumper->setColors(TRUE);
+    $dumper->dump(
+      $cloner->cloneVar($var),
+      function ($line, $depth, $indent_pad) {
+        // A negative depth means "end of dump".
+        if ($depth >= 0) {
+          // Adds a two spaces indentation to the line.
+          fwrite(STDOUT, str_repeat($indent_pad, $depth) . $line . "\n");
+        }
+      }
+    );
+  }
+
+  /**
+   * A HTML handler for \Symfony\Component\VarDumper\VarDumper.
+   */
+  public static function htmlHandler($var) {
+    $cloner = new VarCloner();
+    $dumper = new HtmlDumper();
+    $dumper->dump($cloner->cloneVar($var));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 6dae38389f53..8da29400b4e4 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -20,10 +20,12 @@
 use Drupal\Tests\Traits\PhpUnitWarnings;
 use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\TestTools\Comparator\MarkupInterfaceComparator;
+use Drupal\TestTools\TestVarDumper;
 use GuzzleHttp\Cookie\CookieJar;
 use PHPUnit\Framework\TestCase;
 use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
 use Symfony\Component\CssSelector\CssSelectorConverter;
+use Symfony\Component\VarDumper\VarDumper;
 
 /**
  * Provides a test case for functional Drupal tests.
@@ -36,6 +38,11 @@
  * translation functionality. For example, avoid wrapping test text with t()
  * or TranslatableMarkup().
  *
+ * Using Symfony's dump() function in functional test test code will produce
+ * output on the command line; using dump() in site code will produce output in
+ * the requested web page, which can then be inspected in the HTML output from
+ * the test.
+ *
  * @ingroup testing
  */
 abstract class BrowserTestBase extends TestCase {
@@ -214,6 +221,14 @@ abstract class BrowserTestBase extends TestCase {
    */
   protected $originalContainer;
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function setUpBeforeClass() {
+    parent::setUpBeforeClass();
+    VarDumper::setHandler(TestVarDumper::class . '::cliHandler');
+  }
+
   /**
    * Initializes Mink sessions.
    */
diff --git a/core/tests/Drupal/Tests/StreamCapturer.php b/core/tests/Drupal/Tests/StreamCapturer.php
new file mode 100644
index 000000000000..9b8925723a41
--- /dev/null
+++ b/core/tests/Drupal/Tests/StreamCapturer.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\Tests;
+
+/**
+ * Captures output to a stream and stores it for retrieval.
+ */
+class StreamCapturer extends \php_user_filter {
+
+  public static $cache = '';
+
+  public function filter($in, $out, &$consumed, $closing) {
+    while ($bucket = stream_bucket_make_writeable($in)) {
+      self::$cache .= $bucket->data;
+      // cSpell:disable-next-line
+      $consumed += $bucket->datalen;
+      stream_bucket_append($out, $bucket);
+    }
+    // cSpell:disable-next-line
+    return PSFS_FEED_ME;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index c365afaf9ce0..7d81c84cd229 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -10,12 +10,17 @@
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
 use Drupal\Tests\Traits\PhpUnitWarnings;
+use Drupal\TestTools\TestVarDumper;
 use PHPUnit\Framework\TestCase;
+use Symfony\Component\VarDumper\VarDumper;
 use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
 
 /**
  * Provides a base class and helpers for Drupal unit tests.
  *
+ * Using Symfony's dump() function() in Unit tests will produce output on the
+ * command line.
+ *
  * @ingroup testing
  */
 abstract class UnitTestCase extends TestCase {
@@ -38,6 +43,14 @@ abstract class UnitTestCase extends TestCase {
    */
   protected $root;
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function setUpBeforeClass() {
+    parent::setUpBeforeClass();
+    VarDumper::setHandler(TestVarDumper::class . '::cliHandler');
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/tests/Drupal/Tests/UnitTestCaseTest.php b/core/tests/Drupal/Tests/UnitTestCaseTest.php
index bff0f13b6cc9..aac3b709c2f7 100644
--- a/core/tests/Drupal/Tests/UnitTestCaseTest.php
+++ b/core/tests/Drupal/Tests/UnitTestCaseTest.php
@@ -19,4 +19,48 @@ public function testAssertArrayEquals() {
     $this->assertArrayEquals([], []);
   }
 
+  /**
+   * Tests the dump() function in a test run in the same process.
+   */
+  public function testVarDumpSameProcess() {
+    // Append the stream capturer to the STDOUT stream, so that we can test the
+    // dump() output and also prevent it from actually outputting in this
+    // particular test.
+    stream_filter_register("capture", StreamCapturer::class);
+    stream_filter_append(STDOUT, "capture");
+
+    // Dump some variables.
+    $object = (object) [
+      'foo' => 'bar',
+    ];
+    dump($object);
+    dump('banana');
+
+    $this->assertStringContainsString('bar', StreamCapturer::$cache);
+    $this->assertStringContainsString('banana', StreamCapturer::$cache);
+  }
+
+  /**
+   * Tests the dump() function in a test run in a separate process.
+   *
+   * @runInSeparateProcess
+   */
+  public function testVarDumpSeparateProcess() {
+    // Append the stream capturer to the STDOUT stream, so that we can test the
+    // dump() output and also prevent it from actually outputting in this
+    // particular test.
+    stream_filter_register("capture", StreamCapturer::class);
+    stream_filter_append(STDOUT, "capture");
+
+    // Dump some variables.
+    $object = (object) [
+      'foo' => 'bar',
+    ];
+    dump($object);
+    dump('banana');
+
+    $this->assertStringContainsString('bar', StreamCapturer::$cache);
+    $this->assertStringContainsString('banana', StreamCapturer::$cache);
+  }
+
 }
-- 
GitLab