diff --git a/core/includes/common.inc b/core/includes/common.inc
index 409fa67..e60b049 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -22,6 +22,7 @@
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\Renderer;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
 use Symfony\Component\HttpFoundation\Response;
@@ -1505,15 +1506,12 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
  *
  * @return array
  *   The merged #attached array.
+ *
+ * @deprecated as of 8.0.x
+ *   Use
  */
 function drupal_merge_attached(array $a, array $b) {
-  // If both #attached arrays contain drupalSettings, then merge them correctly;
-  // adding the same settings multiple times needs to behave idempotently.
-  if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
-    $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
-    unset($b['drupalSettings']);
-  }
-  return NestedArray::mergeDeep($a, $b);
+  return Renderer::mergeAttached($a, $b);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 91e4bd9..09d9a76 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -109,7 +109,7 @@ public function render(&$elements, $is_root_call = FALSE) {
    */
   protected function doRender(&$elements, $is_root_call = FALSE) {
     if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
-      if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
+      if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') !== FALSE) {
         $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
       }
       $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
@@ -311,7 +311,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $this->updateStack($elements);
 
     // Cache the processed element if #cache is set.
-    if (isset($elements['#cache'])) {
+    if (isset($elements['#cache']) && $elements['#cache'] !== ['tags' => []]) {
       drupal_render_cache_set($elements['#markup'], $elements);
     }
 
@@ -376,7 +376,7 @@ protected function updateStack(&$element) {
     // Update the frame, but also update the current element, to ensure it
     // contains up-to-date information in case it gets render cached.
     $frame->tags = $element['#cache']['tags'] = Cache::mergeTags($element['#cache']['tags'], $frame->tags);
-    $frame->attached = $element['#attached'] = drupal_merge_attached($element['#attached'], $frame->attached);
+    $frame->attached = $element['#attached'] = static::mergeAttached($element['#attached'], $frame->attached);
     $frame->postRenderCache = $element['#post_render_cache'] = NestedArray::mergeDeep($element['#post_render_cache'], $frame->postRenderCache);
   }
 
@@ -399,7 +399,7 @@ protected function bubbleStack() {
     $current = static::$stack->pop();
     $parent = static::$stack->pop();
     $current->tags = Cache::mergeTags($current->tags, $parent->tags);
-    $current->attached = drupal_merge_attached($current->attached, $parent->attached);
+    $current->attached = static::mergeAttached($current->attached, $parent->attached);
     $current->postRenderCache = NestedArray::mergeDeep($current->postRenderCache, $parent->postRenderCache);
     static::$stack->push($current);
   }
@@ -437,4 +437,56 @@ protected function processPostRenderCache(array &$elements) {
     }
   }
 
+  /**
+   * Merges two #attached arrays.
+   *
+   * The values under the 'drupalSettings' key are merged in a special way, to
+   * match the behavior of
+   *
+   * @code
+   *   jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
+   * @endcode
+   *
+   * This means integer indices are preserved just like string indices are,
+   * rather than re-indexed as is common in PHP array merging.
+   *
+   * Example:
+   * @code
+   * function module1_page_attachments(&$page) {
+   *   $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
+   * }
+   * function module2_page_attachments(&$page) {
+   *   $page['#attached']['drupalSettings']['foo'] = ['d'];
+   * }
+   * // When the page is rendered after the above code, and the browser runs the
+   * // resulting <SCRIPT> tags, the value of drupalSettings.foo is
+   * // ['d', 'b', 'c'], not ['a', 'b', 'c', 'd'].
+   * @endcode
+   *
+   * By following jQuery.extend() merge logic rather than common PHP array merge
+   * logic, the following are ensured:
+   * - Attaching JavaScript settings is idempotent: attaching the same settings
+   *   twice does not change the output sent to the browser.
+   * - If pieces of the page are rendered in separate PHP requests and the
+   *   returned settings are merged by JavaScript, the resulting settings are the
+   *   same as if rendered in one PHP request and merged by PHP.
+   *
+   * @param array $a
+   *   An #attached array.
+   * @param array $b
+   *   Another #attached array.
+   *
+   * @return array
+   *   The merged #attached array.
+   */
+  public static function mergeAttached(array $a, array $b) {
+    // If both #attached arrays contain drupalSettings, then merge them correctly;
+    // adding the same settings multiple times needs to behave idempotently.
+    if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
+      $a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
+      unset($b['drupalSettings']);
+    }
+    return NestedArray::mergeDeep($a, $b);
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index 6848dc9..bc9369e 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -28,324 +28,6 @@ class RenderTest extends KernelTestBase {
   public static $modules = array('system', 'common_test');
 
   /**
-   * Tests the output drupal_render() for some elementary input values.
-   */
-  function testDrupalRenderBasics() {
-    $types = array(
-      array(
-        'name' => 'null',
-        'value' => NULL,
-        'expected' => '',
-      ),
-      array(
-        'name' => 'no value',
-        'expected' => '',
-      ),
-      array(
-        'name' => 'empty string',
-        'value' => '',
-        'expected' => '',
-      ),
-      array(
-        'name' => 'no access',
-        'value' => array(
-          '#markup' => 'foo',
-          '#access' => FALSE,
-        ),
-        'expected' => '',
-      ),
-      array(
-        'name' => 'access denied via callback',
-        'value' => array(
-          '#markup' => 'foo',
-          '#access_callback' => 'is_bool',
-        ),
-        'expected' => '',
-      ),
-      array(
-        'name' => 'access granted via callback',
-        'value' => array(
-          '#markup' => 'foo',
-          '#access_callback' => 'is_array',
-        ),
-        'expected' => 'foo',
-      ),
-      array(
-        'name' => 'access FALSE is honored',
-        'value' => array(
-          '#markup' => 'foo',
-          '#access' => FALSE,
-          '#access_callback' => 'is_array',
-        ),
-        'expected' => '',
-      ),
-      array(
-        'name' => 'previously printed',
-        'value' => array(
-          '#markup' => 'foo',
-          '#printed' => TRUE,
-        ),
-        'expected' => '',
-      ),
-      array(
-        'name' => 'printed in prerender',
-        'value' => array(
-          '#markup' => 'foo',
-          '#pre_render' => array('common_test_drupal_render_printing_pre_render'),
-        ),
-        'expected' => '',
-      ),
-
-      // Test that #theme and #theme_wrappers can co-exist on an element.
-      array(
-        'name' => '#theme and #theme_wrappers basic',
-        'value' => array(
-          '#theme' => 'common_test_foo',
-          '#foo' => 'foo',
-          '#bar' => 'bar',
-          '#theme_wrappers' => array('container'),
-          '#attributes' => array('class' => array('baz')),
-        ),
-        'expected' => '<div class="baz">foobar</div>' . "\n",
-      ),
-      // Test that #theme_wrappers can disambiguate element attributes shared
-      // with rendering methods that build #children by using the alternate
-      // #theme_wrappers attribute override syntax.
-      array(
-        'name' => '#theme and #theme_wrappers attribute disambiguation',
-        'value' => array(
-          '#type' => 'link',
-          '#theme_wrappers' => array(
-            'container' => array(
-              '#attributes' => array('class' => array('baz')),
-            ),
-          ),
-          '#attributes' => array('id' => 'foo'),
-          '#url' => Url::fromUri('http://drupal.org'),
-          '#title' => 'bar',
-        ),
-        'expected' => '<div class="baz"><a href="http://drupal.org" id="foo">bar</a></div>' . "\n",
-      ),
-      // Test that #theme_wrappers can disambiguate element attributes when the
-      // "base" attribute is not set for #theme.
-      array(
-        'name' => '#theme_wrappers attribute disambiguation with undefined #theme attribute',
-        'value' => array(
-          '#type' => 'link',
-          '#url' => Url::fromUri('http://drupal.org'),
-          '#title' => 'foo',
-          '#theme_wrappers' => array(
-            'container' => array(
-              '#attributes' => array('class' => array('baz')),
-            ),
-          ),
-        ),
-        'expected' => '<div class="baz"><a href="http://drupal.org">foo</a></div>' . "\n",
-      ),
-      // Two 'container' #theme_wrappers, one using the "base" attributes and
-      // one using an override.
-      array(
-        'name' => 'Two #theme_wrappers container hooks with different attributes',
-        'value' => array(
-          '#attributes' => array('class' => array('foo')),
-          '#theme_wrappers' => array(
-            'container' => array(
-              '#attributes' => array('class' => array('bar')),
-            ),
-            'container',
-          ),
-        ),
-        'expected' => '<div class="foo"><div class="bar"></div>' . "\n" . '</div>' . "\n",
-      ),
-      // Array syntax theme hook suggestion in #theme_wrappers.
-      array(
-        'name' => '#theme_wrappers implements an array style theme hook suggestion',
-        'value' => array(
-          '#theme_wrappers' => array(array('container')),
-          '#attributes' => array('class' => array('foo')),
-        ),
-        'expected' => '<div class="foo"></div>' . "\n",
-      ),
-
-      // Test handling of #markup as a fallback for #theme hooks.
-      // Simple #markup with no theme.
-      array(
-        'name' => 'basic #markup based renderable array',
-        'value' => array('#markup' => 'foo'),
-        'expected' => 'foo',
-      ),
-      // Theme suggestion is not implemented, #markup should be rendered.
-      array(
-        'name' => '#markup fallback for #theme suggestion not implemented',
-        'value' => array(
-          '#theme' => array('suggestionnotimplemented'),
-          '#markup' => 'foo',
-        ),
-        'expected' => 'foo',
-      ),
-      // Theme suggestion is not implemented, child #markup should be rendered.
-      array(
-        'name' => '#markup fallback for child elements, #theme suggestion not implemented',
-        'value' => array(
-          '#theme' => array('suggestionnotimplemented'),
-          'child' => array(
-            '#markup' => 'foo',
-          ),
-        ),
-        'expected' => 'foo',
-      ),
-      // Theme suggestion is implemented but returns empty string, #markup
-      // should not be rendered.
-      array(
-        'name' => 'Avoid #markup if #theme is implemented but returns an empty string',
-        'value' => array(
-          '#theme' => array('common_test_empty'),
-          '#markup' => 'foo',
-        ),
-        'expected' => '',
-      ),
-      // Theme suggestion is implemented but returns empty string, children
-      // should not be rendered.
-      array(
-        'name' => 'Avoid rendering child elements if #theme is implemented but returns an empty string',
-        'value' => array(
-          '#theme' => array('common_test_empty'),
-          'child' => array(
-            '#markup' => 'foo',
-          ),
-        ),
-        'expected' => '',
-      ),
-
-      // Test handling of #children and child renderable elements.
-      // #theme is not set, #children is not set and the array has children.
-      array(
-        'name' => '#theme is not set, #children is not set and array has children',
-        'value' => array(
-          'child' => array('#markup' => 'bar'),
-        ),
-        'expected' => 'bar',
-      ),
-      // #theme is not set, #children is set but empty and the array has
-      // children.
-      array(
-        'name' => '#theme is not set, #children is an empty string and array has children',
-        'value' => array(
-          '#children' => '',
-          'child' => array('#markup' => 'bar'),
-        ),
-        'expected' => 'bar',
-      ),
-      // #theme is not set, #children is not empty and will be assumed to be the
-      // rendered child elements even though the #markup for 'child' differs.
-      array(
-        'name' => '#theme is not set, #children is set and array has children',
-        'value' => array(
-          '#children' => 'foo',
-          'child' => array('#markup' => 'bar'),
-        ),
-        'expected' => 'foo',
-      ),
-      // #theme is implemented so the values of both #children and 'child' will
-      // be ignored - it is the responsibility of the theme hook to render these
-      // if appropriate.
-      array(
-        'name' => '#theme is implemented, #children is set and array has children',
-        'value' => array(
-          '#theme' => 'common_test_foo',
-          '#children' => 'baz',
-          'child' => array('#markup' => 'boo'),
-        ),
-        'expected' => 'foobar',
-      ),
-      // #theme is implemented but #render_children is TRUE. As in the case
-      // where #theme is not set, empty #children means child elements are
-      // rendered recursively.
-      array(
-        'name' => '#theme is implemented, #render_children is TRUE, #children is empty and array has children',
-        'value' => array(
-          '#theme' => 'common_test_foo',
-          '#children' => '',
-          '#render_children' => TRUE,
-          'child' => array(
-            '#markup' => 'boo',
-          ),
-        ),
-        'expected' => 'boo',
-      ),
-      // #theme is implemented but #render_children is TRUE. As in the case
-      // where #theme is not set, #children will take precedence over 'child'.
-      array(
-        'name' => '#theme is implemented, #render_children is TRUE, #children is set and array has children',
-        'value' => array(
-          '#theme' => 'common_test_foo',
-          '#children' => 'baz',
-          '#render_children' => TRUE,
-          'child' => array(
-            '#markup' => 'boo',
-          ),
-        ),
-        'expected' => 'baz',
-      ),
-    );
-
-    foreach($types as $type) {
-      $this->assertIdentical(drupal_render($type['value']), $type['expected'], '"' . $type['name'] . '" input rendered correctly by drupal_render().');
-    }
-  }
-
-  /**
-   * Tests sorting by weight.
-   */
-  function testDrupalRenderSorting() {
-    $first = $this->randomMachineName();
-    $second = $this->randomMachineName();
-    // Build an array with '#weight' set for each element.
-    $elements = array(
-      'second' => array(
-        '#weight' => 10,
-        '#markup' => $second,
-      ),
-      'first' => array(
-        '#weight' => 0,
-        '#markup' => $first,
-      ),
-    );
-    $output = drupal_render($elements);
-
-    // The lowest weight element should appear last in $output.
-    $this->assertTrue(strpos($output, $second) > strpos($output, $first), 'Elements were sorted correctly by weight.');
-
-    // Confirm that the $elements array has '#sorted' set to TRUE.
-    $this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array");
-
-    // Pass $elements through \Drupal\Core\Render\Element::children() and
-    // ensure it remains sorted in the correct order. drupal_render() will
-    // return an empty string if used on the same array in the same request.
-    $children = Element::children($elements);
-    $this->assertTrue(array_shift($children) == 'first', 'Child found in the correct order.');
-    $this->assertTrue(array_shift($children) == 'second', 'Child found in the correct order.');
-
-
-    // The same array structure again, but with #sorted set to TRUE.
-    $elements = array(
-      'second' => array(
-        '#weight' => 10,
-        '#markup' => $second,
-      ),
-      'first' => array(
-        '#weight' => 0,
-        '#markup' => $first,
-      ),
-      '#sorted' => TRUE,
-    );
-    $output = drupal_render($elements);
-
-    // The elements should appear in output in the same order as the array.
-    $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.');
-  }
-
-  /**
    * Tests #attached functionality in children elements.
    */
   function testDrupalRenderChildrenAttached() {
@@ -389,24 +71,6 @@ function testDrupalRenderChildrenAttached() {
   }
 
   /**
-   * Tests passing arguments to the theme function.
-   */
-  function testDrupalRenderThemeArguments() {
-    $element = array(
-      '#theme' => 'common_test_foo',
-    );
-    // Test that defaults work.
-    $this->assertEqual(drupal_render($element), 'foobar', 'Defaults work');
-    $element = array(
-      '#theme' => 'common_test_foo',
-      '#foo' => $this->randomMachineName(),
-      '#bar' => $this->randomMachineName(),
-    );
-    // Tests that passing arguments to the theme function works.
-    $this->assertEqual(drupal_render($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
-  }
-
-  /**
    * Tests theme preprocess functions being able to attach assets.
    */
   function testDrupalRenderThemePreprocessAttached() {
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
new file mode 100644
index 0000000..0698e20
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -0,0 +1,584 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Render\RendererTest.
+ */
+
+namespace Drupal\Tests\Core\Render;
+
+
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Renderer;
+use Drupal\Core\Template\Attribute;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Renderer
+ * @group Render
+ */
+class RendererTest extends UnitTestCase {
+
+  /**
+   * The tested renderer.
+   *
+   * @var \Drupal\Core\Render\Renderer
+   */
+  protected $renderer;
+
+  /**
+   * The mocked controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $controllerResolver;
+
+  /**
+   * The mocked theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $themeManager;
+
+  /**
+   * The mocked element info.
+   *
+   * @var \Drupal\Core\Render\ElementInfoManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $elementInfo;
+
+  protected $defaultThemeVars = [
+    '#cache' => ['tags' => []],
+    '#attached' => [],
+    '#post_render_cache' => [],
+    '#children' => '',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
+    $this->elementInfo = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface');
+    $this->renderer = new Renderer($this->controllerResolver, $this->themeManager, $this->elementInfo);
+  }
+
+  /**
+   * @dataProvider providerTestRenderBasic
+   */
+  public function testRenderBasic($build, $expected, callable $setup_code = NULL) {
+
+    if (isset($setup_code)) {
+      $setup_code = $setup_code->bindTo($this);
+      $setup_code();
+    }
+
+    $this->assertSame($expected, $this->renderer->render($build));
+  }
+
+  public function providerTestRenderBasic() {
+    $data = [];
+
+    // Pass a NULL.
+    $data[] = [NULL, ''];
+    // Pass an empty string.
+    $data[] = ['', ''];
+    // Previously printed, see ::renderTwice for a more integration like test.
+    $data[] = [[
+      '#markup' => 'foo',
+      '#printed' => TRUE,
+      ], ''];
+    // Printed in pre_render.
+    $data[] = [[
+      '#markup' => 'foo',
+      '#pre_render' => [[new TestCallables(), 'preRenderPrinted']]
+    ], ''];
+
+    // Test that #theme and #theme_wrappers can co-exist on an element.
+    // #theme and #theme_wrappers basic
+    $build = [
+      '#theme' => 'common_test_foo',
+      '#foo' => 'foo',
+      '#bar' => 'bar',
+      '#theme_wrappers' => ['container'],
+      '#attributes' => ['class' => ['baz']],
+    ];
+    $setup_code_type_link = function() {
+      $this->setupThemeContainer();
+      $this->themeManager->expects($this->at(0))
+        ->method('render')
+        ->with('common_test_foo', $this->anything())
+        ->willReturnCallback(function($theme, $vars) {
+          return $vars['#foo'] . $vars['#bar'];
+        });
+    };
+    $data[] = [$build, '<div class="baz">foobar</div>' . "\n", $setup_code_type_link];
+
+    // Test that #theme_wrappers can disambiguate element attributes shared
+    // with rendering methods that build #children by using the alternate
+    // #theme_wrappers attribute override syntax.
+    // '#theme and #theme_wrappers attribute disambiguation'.
+
+    $build = [
+      '#type' => 'link',
+      '#theme_wrappers' => [
+        'container' => [
+          '#attributes' => ['class' => ['baz']],
+        ],
+      ],
+      '#attributes' => ['id' => 'foo'],
+      '#url' => 'http://drupal.org',
+      '#title' => 'bar',
+    ];
+    $setup_code_type_link = function() {
+      $this->setupThemeContainer();
+      $this->themeManager->expects($this->at(0))
+        ->method('render')
+        ->with('link', $this->anything())
+        ->willReturnCallback(function($theme, $vars) {
+          $attributes = new Attribute(['href' => $vars['#url']] + (isset($vars['#attributes']) ? $vars['#attributes'] : []));
+          return '<a' . (string) $attributes . '>' . $vars['#title'] . '</a>';
+        });
+      $this->elementInfo->expects($this->atLeastOnce())
+        ->method('getInfo')
+        ->with('link')
+        ->willReturn(['#theme' => 'link']);
+    };
+    $data[] = [$build, '<div class="baz"><a href="http://drupal.org" id="foo">bar</a></div>' . "\n", $setup_code_type_link];
+
+    // Test that #theme_wrappers can disambiguate element attributes when the
+    // "base" attribute is not set for #theme.
+    // #theme_wrappers attribute disambiguation with undefined #theme attribute.
+    $build = [
+      '#type' => 'link',
+      '#url' => 'http://drupal.org',
+      '#title' => 'foo',
+      '#theme_wrappers' => [
+        'container' => [
+          '#attributes' => ['class' => ['baz']],
+        ],
+      ],
+    ];
+    $data[] = [$build, '<div class="baz"><a href="http://drupal.org">foo</a></div>' . "\n", $setup_code_type_link];
+
+    // Two 'container' #theme_wrappers, one using the "base" attributes and
+    // one using an override.
+    $build = [
+      '#attributes' => ['class' => ['foo']],
+      '#theme_wrappers' => [
+        'container' => [
+          '#attributes' => ['class' => ['bar']],
+        ],
+        'container',
+      ],
+    ];
+    $setup_code = function() {
+      $this->setupThemeContainer($this->any());
+    };
+    $data[] = [$build, '<div class="foo"><div class="bar"></div>' . "\n" . '</div>' . "\n", $setup_code];
+
+    // Array syntax theme hook suggestion in #theme_wrappers.
+    $build = [
+      '#theme_wrappers' => [['container']],
+      '#attributes' => ['class' => ['foo']],
+    ];
+    $setup_code = function() {
+      $this->setupThemeContainerMultiSuggestion($this->any());
+    };
+    $data[] = [$build, '<div class="foo"></div>' . "\n", $setup_code];
+
+    // Test handling of #markup as a fallback for #theme hooks.
+    // Theme suggestion is not implemented, #markup should be rendered.
+    $build = [
+      '#theme' => ['suggestionnotimplemented'],
+      '#markup' => 'foo',
+    ];
+    $setup_code = function() {
+      $this->themeManager->expects($this->once())
+        ->method('render')
+        ->with(['suggestionnotimplemented'], $this->anything())
+        ->willReturn(FALSE);
+    };
+    $data[] = [$build, 'foo', $setup_code];
+
+    // Theme suggestion is not implemented, child #markup should be rendered.
+    $build = [
+      '#theme' => ['suggestionnotimplemented'],
+      'child' => [
+        '#markup' => 'foo',
+      ],
+    ];
+
+    $setup_code = function() {
+      $this->themeManager->expects($this->once())
+        ->method('render')
+        ->with(['suggestionnotimplemented'], $this->anything())
+        ->willReturn(FALSE);
+    };
+
+    $data[] = [$build, 'foo', $setup_code];
+
+    // Theme suggestion is implemented but returns empty string, #markup
+    // should not be rendered.
+    $build = [
+      '#theme' => ['common_test_empty'],
+      '#markup' => 'foo',
+    ];
+
+    $setup_code = function() {
+      $this->themeManager->expects($this->once())
+        ->method('render')
+        ->with(['common_test_empty'], $this->anything())
+        ->willReturn('');
+    };
+
+    $data[] = [$build, '', $setup_code];
+
+    // Theme suggestion is implemented but returns empty string, children
+    // should not be rendered.
+
+    $build = [
+      '#theme' => ['common_test_empty'],
+      'child' => [
+        '#markup' => 'foo',
+      ],
+    ];
+
+    $setup_code = function() {
+      $this->themeManager->expects($this->once())
+        ->method('render')
+        ->with(['common_test_empty'], $this->anything())
+        ->willReturn('');
+    };
+
+    $data[] = [$build, '', $setup_code];
+
+    // Test handling of #children and child renderable elements.
+    // #theme is implemented so the values of both #children and 'child' will
+    // be ignored - it is the responsibility of the theme hook to render these
+    // if appropriate.
+
+    $build = [
+      '#theme' => 'common_test_foo',
+      '#children' => 'baz',
+      'child' => ['#markup' => 'boo'],
+    ];
+
+    $setup_code = function() {
+      $this->themeManager->expects($this->once())
+        ->method('render')
+        ->with('common_test_foo', $this->anything())
+        ->willReturn('foobar');
+    };
+
+    $data[] = [$build, 'foobar', $setup_code];
+
+    // #theme is implemented but #render_children is TRUE. As in the case
+    // where #theme is not set, empty #children means child elements are
+    // rendered recursively.
+    $build = [
+      '#theme' => 'common_test_foo',
+      '#children' => '',
+      '#render_children' => TRUE,
+      'child' => [
+        '#markup' => 'boo',
+      ],
+    ];
+
+    $setup_code = function() {
+      $this->themeManager->expects($this->never())
+        ->method('render');
+    };
+
+    $data[] = [$build, 'boo', $setup_code];
+
+    // #theme is implemented but #render_children is TRUE. As in the case
+    // where #theme is not set, #children will take precedence over 'child'.
+
+    $build = [
+      '#theme' => 'common_test_foo',
+      '#children' => 'baz',
+      '#render_children' => TRUE,
+      'child' => [
+        '#markup' => 'boo',
+      ],
+    ];
+
+    $setup_code = function() {
+      $this->themeManager->expects($this->never())
+        ->method('render');
+    };
+
+    $data[] = [$build, 'baz', $setup_code];
+
+    // Test handling of #markup as a fallback for #theme hooks.
+    // Simple #markup with no theme.
+
+    // Basic #markup based renderable array.
+    $data[] = [
+      ['#markup' => 'foo']
+    , 'foo'];
+
+    // Test handling of #children and child renderable elements.
+    // #theme is not set, #children is not set and the array has children.
+    $data[] = [
+      [
+        'child' => ['#markup' => 'bar'],
+      ], 'bar'];
+    // #theme is not set, #children is set but empty and the array has
+    // children.
+    $data[] = [
+      [
+        '#children' => '',
+        'child' => ['#markup' => 'bar'],
+      ], 'bar'
+    ];
+    // #theme is not set, #children is not empty and will be assumed to be the
+    // rendered child elements even though the #markup for 'child' differs.
+    $data[] = [
+      [
+        '#children' => 'foo',
+        'child' => ['#markup' => 'bar'],
+      ], 'foo'
+    ];
+
+    return $data;
+  }
+
+  /**
+   * @covers ::render
+   */
+  public function testRenderSorting() {
+    $first = $this->randomMachineName();
+    $second = $this->randomMachineName();
+    // Build an array with '#weight' set for each element.
+    $elements = [
+      'second' => [
+        '#weight' => 10,
+        '#markup' => $second,
+      ],
+      'first' => [
+        '#weight' => 0,
+        '#markup' => $first,
+      ],
+    ];
+    $output = $this->renderer->render($elements);
+
+    // The lowest weight element should appear last in $output.
+    $this->assertTrue(strpos($output, $second) > strpos($output, $first), 'Elements were sorted correctly by weight.');
+
+    // Confirm that the $elements array has '#sorted' set to TRUE.
+    $this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array");
+
+    // Pass $elements through \Drupal\Core\Render\Element::children() and
+    // ensure it remains sorted in the correct order. drupal_render() will
+    // return an empty string if used on the same array in the same request.
+    $children = Element::children($elements);
+    $this->assertTrue(array_shift($children) == 'first', 'Child found in the correct order.');
+    $this->assertTrue(array_shift($children) == 'second', 'Child found in the correct order.');
+  }
+
+  /**
+   * @covers ::render
+   */
+  public function testRenderSortingWithSetHashSorted() {
+    $first = $this->randomMachineName();
+    $second = $this->randomMachineName();
+    // The same array structure again, but with #sorted set to TRUE.
+    $elements = array(
+      'second' => array(
+        '#weight' => 10,
+        '#markup' => $second,
+      ),
+      'first' => array(
+        '#weight' => 0,
+        '#markup' => $first,
+      ),
+      '#sorted' => TRUE,
+    );
+    $output = $this->renderer->render($elements);
+
+    // The elements should appear in output in the same order as the array.
+    $this->assertTrue(strpos($output, $second) < strpos($output, $first), 'Elements were not sorted.');
+  }
+
+  /**
+   * @dataProvider providerBoolean
+   */
+  public function testRenderWithPresetAccess($access) {
+    $build = [
+      '#markup' => 'test',
+      '#access' => $access,
+    ];
+
+    $this->assertAccess($build, $access);
+  }
+
+  /**
+   * @dataProvider providerBoolean
+   * @covers ::render
+   */
+  public function testRenderWithAccessCallbackCallable($access) {
+    $build = [
+      '#markup' => 'test',
+      '#access_callback' => function() use ($access) {
+        return $access;
+      }
+    ];
+
+    $this->assertAccess($build, $access);
+  }
+
+  /**
+   * @dataProvider providerBoolean
+   *
+   * Ensure that the #access property wins over the callable.
+   *
+   * @covers ::render
+   */
+  public function testRenderWithAccessPropertyAndCallback($access) {
+    $build = [
+      '#markup' => 'test',
+      '#access' => $access,
+      '#access_callback' => function() {
+        return TRUE;
+      }
+    ];
+
+    $this->assertAccess($build, $access);
+  }
+
+  /**
+   * @dataProvider providerBoolean
+   *
+   * @covers ::render
+   */
+  public function testRenderWithAccessControllerResolved($access) {
+    $build = [
+      '#markup' => 'test',
+      '#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . ($access ? 'accessTrue' : 'accessFalse'),
+    ];
+
+    $this->controllerResolver->expects($this->atLeastOnce())
+      ->method('getControllerFromDefinition')
+      ->willReturnMap([
+        ['Drupal\Tests\Core\Render\TestAccessClass::accessTrue', [new TestAccessClass(), 'accessTrue']],
+        ['Drupal\Tests\Core\Render\TestAccessClass::accessFalse', [new TestAccessClass(), 'accessFalse']],
+      ]);
+
+    $this->assertAccess($build, $access);
+  }
+
+  /**
+   * @covers ::render
+   */
+  public function testRenderTwice() {
+    $build = [
+      '#markup' => 'test',
+    ];
+
+    $this->assertEquals('test', $this->renderer->render($build));
+    // @todo This behaviour is odd ...
+    $this->assertEquals('', $this->renderer->render($build));
+  }
+
+
+
+  public function providerBoolean() {
+    return [
+      [FALSE],
+      [TRUE]
+    ];
+  }
+
+  protected function assertAccess($build, $access) {
+    if ($access) {
+      $this->assertSame('test', $this->renderer->render($build));
+    }
+    else {
+      $this->assertSame('', $this->renderer->render($build));
+    }
+  }
+
+  protected function setupThemeContainer($matcher = NULL) {
+    $this->themeManager->expects($matcher ?: $this->at(1))
+      ->method('render')
+      ->with('container', $this->anything())
+      ->willReturnCallback(function($theme, $vars) {
+        return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n";
+      });
+  }
+
+  protected function setupThemeContainerMultiSuggestion($matcher = NULL) {
+    $this->themeManager->expects($matcher ?: $this->at(1))
+      ->method('render')
+      ->with(['container'], $this->anything())
+      ->willReturnCallback(function($theme, $vars) {
+        return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n";
+      });
+  }
+
+  /**
+   * @covers ::render
+   */
+  public function testRenderWithoutThemeArguments() {
+    $element = array(
+      '#theme' => 'common_test_foo',
+    );
+
+    $this->themeManager->expects($this->once())
+      ->method('render')
+      ->with('common_test_foo', $this->defaultThemeVars + $element)
+      ->willReturn('foobar');
+
+    // Test that defaults work.
+    $this->assertEquals($this->renderer->render($element), 'foobar', 'Defaults work');
+  }
+
+  /**
+   * @covers ::render
+   */
+  public function testRenderWithThemeArguments() {
+    $element = array(
+      '#theme' => 'common_test_foo',
+      '#foo' => $this->randomMachineName(),
+      '#bar' => $this->randomMachineName(),
+    );
+
+    $this->themeManager->expects($this->once())
+      ->method('render')
+      ->with('common_test_foo', $this->defaultThemeVars + $element)
+      ->willReturnCallback(function ($hook, $vars) {
+        return $vars['#foo'] . $vars['#bar'];
+      });
+
+    // Tests that passing arguments to the theme function works.
+    $this->assertEquals($this->renderer->render($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
+  }
+
+}
+
+class TestAccessClass {
+
+  public function accessTrue() {
+    return TRUE;
+  }
+
+  public function accessFalse() {
+    return FALSE;
+  }
+
+}
+
+class TestCallables {
+
+  public function preRenderPrinted($elements) {
+    $elements['#printed'] = TRUE;
+    return $elements;
+  }
+
+}
\ No newline at end of file
