From cd74c62406d5a938eb388214791ca3ba5a82c56e Mon Sep 17 00:00:00 2001
From: webchick <webchick@24967.no-reply.drupal.org>
Date: Thu, 3 Oct 2013 21:27:56 -0700
Subject: [PATCH] Issue #2091691 by tim.plunkett: Convert test non-form page
 callbacks to routes and controllers.

---
 core/includes/menu.inc                        | 376 ++++++++---------
 .../Core/Controller/HtmlPageController.php    |   4 +-
 .../block/custom_block/custom_block.module    |   8 +-
 .../custom_block_test.module                  |   5 +-
 .../custom_block_test.routing.yml             |   6 +
 .../comment/Tests/CommentNonNodeTest.php      |   2 +-
 .../content_translation.local_tasks.yml       |   4 +
 .../Plugin/ContentTranslationLocalTasks.php   |  41 ++
 .../ContentTranslationLocalTasks.php          |  92 +++++
 .../datetime/Tests/DatetimeFieldTest.php      |   6 +-
 .../lib/Drupal/email/Tests/EmailFieldTest.php |   2 +-
 .../field/lib/Drupal/field/Tests/FormTest.php |  28 +-
 .../Drupal/field/Tests/TranslationWebTest.php |   2 +-
 core/modules/forum/forum.module               |   4 +-
 .../lib/Drupal/link/Tests/LinkFieldTest.php   |  10 +-
 core/modules/node/node.module                 |   5 +-
 .../Drupal/number/Tests/NumberFieldTest.php   |   2 +-
 .../Tests/OptionsSelectDynamicValuesTest.php  |   2 +-
 .../options/Tests/OptionsWidgetsTest.php      |  52 +--
 .../system/Tests/Entity/EntityFormTest.php    |   4 +-
 .../system/Tests/Menu/MenuRouterTest.php      |  51 ---
 .../system/Tests/Module/ModuleApiTest.php     |  13 -
 core/modules/system/system.api.php            |  18 +-
 .../tests/modules/ajax_test/ajax_test.module  |  36 +-
 .../modules/ajax_test/ajax_test.routing.yml   |  37 +-
 .../Drupal/ajax_test/AjaxTestController.php   |  24 --
 .../Controller/AjaxTestController.php         |  59 +++
 .../database_test/database_test.module        |  30 +-
 .../database_test/database_test.routing.yml   |  34 ++
 .../Controller/DatabaseTestController.php     |  50 +++
 .../modules/entity_test/entity_test.module    |  23 +-
 .../entity_test/entity_test.services.yml      |   5 +
 .../Controller/EntityTestController.php       |  31 ++
 .../entity_test/EntityTestFormController.php  |   2 +-
 .../entity_test/Routing/RouteSubscriber.php   |  56 +++
 .../tests/modules/form_test/form_test.module  |  24 +-
 .../modules/form_test/form_test.routing.yml   |  17 +
 .../Controller/FormTestController.php         |  14 +
 .../Controller/MenuTestController.php         |  43 ++
 .../menu_test/menu_test.local_tasks.yml       |  19 +
 .../tests/modules/menu_test/menu_test.module  | 248 ++++--------
 .../modules/menu_test/menu_test.routing.yml   | 377 ++++++++++++++++++
 .../Controller/ModuleTestController.php       |  36 ++
 .../modules/module_test/module_test.module    |  31 +-
 .../module_test/module_test.routing.yml       |  23 ++
 .../Controller/SystemTestController.php       |  69 ++++
 .../modules/system_test/system_test.module    |  97 +----
 .../system_test/system_test.routing.yml       |  68 ++++
 .../Controller/TestPageTestController.php     |  22 +
 .../test_page_test/test_page_test.module      |  12 +-
 .../test_page_test/test_page_test.routing.yml |   8 +
 .../Tests/TermFieldMultipleVocabularyTest.php |   2 +-
 .../Drupal/taxonomy/Tests/TermFieldTest.php   |   2 +-
 .../lib/Drupal/text/Tests/TextFieldTest.php   |   6 +-
 .../Controller/UpdateTestController.php       |   7 +
 .../modules/update_test/update_test.module    |  18 +-
 .../update_test/update_test.routing.yml       |   9 +
 57 files changed, 1542 insertions(+), 734 deletions(-)
 create mode 100644 core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.routing.yml
 create mode 100644 core/modules/content_translation/content_translation.local_tasks.yml
 create mode 100644 core/modules/content_translation/lib/Drupal/content_translation/Plugin/ContentTranslationLocalTasks.php
 create mode 100644 core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationLocalTasks.php
 delete mode 100644 core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php
 create mode 100644 core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/Controller/AjaxTestController.php
 create mode 100644 core/modules/system/tests/modules/database_test/database_test.routing.yml
 create mode 100644 core/modules/system/tests/modules/database_test/lib/Drupal/database_test/Controller/DatabaseTestController.php
 create mode 100644 core/modules/system/tests/modules/entity_test/entity_test.services.yml
 create mode 100644 core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Controller/EntityTestController.php
 create mode 100644 core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Routing/RouteSubscriber.php
 create mode 100644 core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Controller/MenuTestController.php
 create mode 100644 core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Controller/ModuleTestController.php
 create mode 100644 core/modules/system/tests/modules/module_test/module_test.routing.yml
 create mode 100644 core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/SystemTestController.php
 create mode 100644 core/modules/system/tests/modules/test_page_test/lib/Drupal/test_page_test/Controller/TestPageTestController.php

diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index c8fddcd84f09..5eac9151a186 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -1942,10 +1942,10 @@ function menu_navigation_links($menu_name, $level = 0) {
 /**
  * Collects the local tasks (tabs), action links, and the root path.
  *
- * @param $level
+ * @param int $level
  *   The level of tasks you ask for. Primary tasks are 0, secondary are 1.
  *
- * @return
+ * @return array
  *   An array containing
  *   - tabs: Local tasks for the requested level.
  *   - actions: Action links for the requested level.
@@ -1965,11 +1965,6 @@ function menu_local_tasks($level = 0) {
   );
 
   if (!isset($data)) {
-    $data = array();
-    // Set defaults in case there are no actions or tabs.
-    $actions = $empty['actions'];
-    $tabs = array();
-
     // Look for route-based tabs.
     $data['tabs'] = array();
     $data['actions'] = array();
@@ -1983,202 +1978,30 @@ function menu_local_tasks($level = 0) {
       }
     }
 
-    // @todo Remove the code below once the old menu router system got removed.
+    // @todo Remove when all local tasks/actions are converted to plugins.
     $router_item = menu_get_item();
-
     // If this router item points to its parent, start from the parents to
     // compute tabs and actions.
     if ($router_item && ($router_item['type'] & MENU_LINKS_TO_PARENT)) {
       $router_item = menu_get_item($router_item['tab_parent_href']);
     }
-
     // If we failed to fetch a router item or the current user doesn't have
     // access to it, don't bother computing the tabs.
-    if (!$router_item || !$router_item['access']) {
+    if ((!$route_name && !$router_item) || ($router_item && !$router_item['access'])) {
       return $empty;
     }
-    // @todo remove all code using {menu_router} and anything using MENU_*
-    // constants when all local actions and local tasks are converted to
-    // plugins. The remaining code should just invoke those managers plus do the
-    // invocations of hook_menu_local_tasks() and hook_menu_local_tasks_alter().
-
-    // Get all tabs (also known as local tasks) and the root page.
-    $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC))
-      ->fields('menu_router')
-      ->condition('tab_root', $router_item['tab_root'])
-      ->condition('context', MENU_CONTEXT_INLINE, '<>')
-      ->orderBy('weight')
-      ->orderBy('title')
-      ->execute();
-    $map = $router_item['original_map'];
-    $children = array();
-    $tasks = array();
-    $root_path = $router_item['path'];
-
-    foreach ($result as $item) {
-      _menu_translate($item, $map, TRUE);
-      if ($item['tab_parent']) {
-        // All tabs, but not the root page.
-        $children[$item['tab_parent']][$item['path']] = $item;
-      }
-      // Store the translated item for later use.
-      $tasks[$item['path']] = $item;
-    }
-
-    // Find all tabs below the current path.
-    $path = $router_item['path'];
-    // Tab parenting may skip levels, so the number of parts in the path may not
-    // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
-    $depth = 1001;
-    $actions = array();
-    while (isset($children[$path])) {
-      $tabs_current = array();
-      $actions_current = array();
-      $next_path = '';
-      $tab_count = 0;
-      $action_count = 0;
-      foreach ($children[$path] as $item) {
-        // Local tasks can be normal items too, so bitmask with
-        // MENU_IS_LOCAL_TASK before checking.
-        if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
-          // This item is not a tab, skip it.
-          continue;
-        }
-        if ($item['access']) {
-          $link = $item;
-          // The default task is always active. As tabs can be normal items
-          // too, so bitmask with MENU_LINKS_TO_PARENT before checking.
-          if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
-            // Find the first parent which is not a default local task or action.
-            for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
-            // Use the path of the parent instead.
-            $link['href'] = $tasks[$p]['href'];
-            // Mark the link as active, if the current path happens to be the
-            // path of the default local task itself (i.e., instead of its
-            // tab_parent_href or tab_root_href). Normally, links for default
-            // local tasks link to their parent, but the path of default local
-            // tasks can still be accessed directly, in which case this link
-            // would not be marked as active, since l() only compares the href
-            // with current_path().
-            if ($link['href'] != current_path()) {
-              $link['localized_options']['attributes']['class'][] = 'active';
-            }
-            $tabs_current[$link['href']] = array(
-              '#theme' => 'menu_local_task',
-              '#link' => $link,
-              '#active' => TRUE,
-              '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
-            );
-            $next_path = $item['path'];
-            $tab_count++;
-          }
-          else {
-            // Actions can be normal items too, so bitmask with
-            // MENU_IS_LOCAL_ACTION before checking.
-            if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) {
-              // The item is an action, display it as such.
-              $actions_current[$link['href']] = array(
-                '#theme' => 'menu_local_action',
-                '#link' => $link,
-                '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
-              );
-              $action_count++;
-            }
-            else {
-              // Otherwise, it's a normal tab.
-              $tabs_current[$link['href']] = array(
-                '#theme' => 'menu_local_task',
-                '#link' => $link,
-                '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
-              );
-              $tab_count++;
-            }
-          }
-        }
-      }
-      $path = $next_path;
-      $tabs[$depth] = $tabs_current;
-      $actions = array_merge($actions, $actions_current);
-      $depth++;
-    }
-    $data['actions'] = $actions;
-    // Find all tabs at the same level or above the current one.
-    $parent = $router_item['tab_parent'];
-    $path = $router_item['path'];
-    $current = $router_item;
-    $depth = 1000;
-    while (isset($children[$parent])) {
-      $tabs_current = array();
-      $next_path = '';
-      $next_parent = '';
-      $count = 0;
-      foreach ($children[$parent] as $item) {
-        // Skip local actions.
-        if ($item['type'] & MENU_IS_LOCAL_ACTION) {
-          continue;
-        }
-        if ($item['access']) {
-          $count++;
-          $link = $item;
-          // Local tasks can be normal items too, so bitmask with
-          // MENU_LINKS_TO_PARENT before checking.
-          if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
-            // Find the first parent which is not a default local task.
-            for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
-            // Use the path of the parent instead.
-            $link['href'] = $tasks[$p]['href'];
-            if ($item['path'] == $router_item['path']) {
-              $root_path = $tasks[$p]['path'];
-            }
-          }
-          // We check for the active tab.
-          if ($item['path'] == $path) {
-            // Mark the link as active, if the current path is a (second-level)
-            // local task of a default local task. Since this default local task
-            // links to its parent, l() will not mark it as active, as it only
-            // compares the link's href to current_path().
-            if ($link['href'] != current_path()) {
-              $link['localized_options']['attributes']['class'][] = 'active';
-            }
-            $tabs_current[$link['href']] = array(
-              '#theme' => 'menu_local_task',
-              '#link' => $link,
-              '#active' => TRUE,
-              '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
-            );
-            $next_path = $item['tab_parent'];
-            if (isset($tasks[$next_path])) {
-              $next_parent = $tasks[$next_path]['tab_parent'];
-            }
-          }
-          else {
-            $tabs_current[$link['href']] = array(
-              '#theme' => 'menu_local_task',
-              '#link' => $link,
-              '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
-            );
-          }
-        }
-      }
-      $path = $next_path;
-      $parent = $next_parent;
-      $tabs[$depth] = $tabs_current;
-      $depth--;
+    if ($router_item) {
+      _menu_get_legacy_tasks($router_item, $data, $root_path);
     }
-    // Sort by depth.
-    ksort($tabs);
-    // Remove the depth, we are interested only in their relative placement.
-    $tabs = array_values($tabs);
-    $data['tabs'] += $tabs;
 
     // Allow modules to dynamically add further tasks.
     $module_handler = \Drupal::moduleHandler();
     foreach ($module_handler->getImplementations('menu_local_tasks') as $module) {
       $function = $module . '_menu_local_tasks';
-      $function($data, $router_item, $root_path);
+      $function($data, $route_name);
     }
     // Allow modules to alter local tasks.
-    $module_handler->alter('menu_local_tasks', $data, $router_item, $root_path);
+    $module_handler->alter('menu_local_tasks', $data, $route_name);
   }
 
   if (isset($data['tabs'][$level])) {
@@ -2194,6 +2017,189 @@ function menu_local_tasks($level = 0) {
   return $empty;
 }
 
+/**
+ * Finds legacy local tasks/actions.
+ *
+ * @param array $router_item
+ *   The current router item.
+ * @param array $data
+ *   An associative array of local tasks/actions.
+ * @param string $root_path
+ *
+ * @deprecated Remove once all local tasks/actions are converted to plugins.
+ */
+function _menu_get_legacy_tasks($router_item, &$data, &$root_path) {
+  // Get all tabs (also known as local tasks) and the root page.
+  $result = db_select('menu_router', NULL, array('fetch' => PDO::FETCH_ASSOC))
+    ->fields('menu_router')
+    ->condition('tab_root', $router_item['tab_root'])
+    ->condition('context', MENU_CONTEXT_INLINE, '<>')
+    ->orderBy('weight')
+    ->orderBy('title')
+    ->execute();
+  $map = $router_item['original_map'];
+  $children = array();
+  $tasks = array();
+  $root_path = $router_item['path'];
+
+  foreach ($result as $item) {
+    _menu_translate($item, $map, TRUE);
+    if ($item['tab_parent']) {
+      // All tabs, but not the root page.
+      $children[$item['tab_parent']][$item['path']] = $item;
+    }
+    // Store the translated item for later use.
+    $tasks[$item['path']] = $item;
+  }
+
+  // Find all tabs below the current path.
+  $path = $router_item['path'];
+  // Tab parenting may skip levels, so the number of parts in the path may not
+  // equal the depth. Thus we use the $depth counter (offset by 1000 for ksort).
+  $depth = 1001;
+  $tabs = array();
+  $actions = array();
+  while (isset($children[$path])) {
+    $tabs_current = array();
+    $actions_current = array();
+    $next_path = '';
+    $tab_count = 0;
+    $action_count = 0;
+    foreach ($children[$path] as $item) {
+      // Local tasks can be normal items too, so bitmask with
+      // MENU_IS_LOCAL_TASK before checking.
+      if (!($item['type'] & MENU_IS_LOCAL_TASK)) {
+        // This item is not a tab, skip it.
+        continue;
+      }
+      if ($item['access']) {
+        $link = $item;
+        // The default task is always active. As tabs can be normal items
+        // too, so bitmask with MENU_LINKS_TO_PARENT before checking.
+        if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
+          // Find the first parent which is not a default local task or action.
+          for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
+          // Use the path of the parent instead.
+          $link['href'] = $tasks[$p]['href'];
+          // Mark the link as active, if the current path happens to be the
+          // path of the default local task itself (i.e., instead of its
+          // tab_parent_href or tab_root_href). Normally, links for default
+          // local tasks link to their parent, but the path of default local
+          // tasks can still be accessed directly, in which case this link
+          // would not be marked as active, since l() only compares the href
+          // with current_path().
+          if ($link['href'] != current_path()) {
+            $link['localized_options']['attributes']['class'][] = 'active';
+          }
+          $tabs_current[$link['href']] = array(
+            '#theme' => 'menu_local_task',
+            '#link' => $link,
+            '#active' => TRUE,
+            '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
+          );
+          $next_path = $item['path'];
+          $tab_count++;
+        }
+        else {
+          // Actions can be normal items too, so bitmask with
+          // MENU_IS_LOCAL_ACTION before checking.
+          if (($item['type'] & MENU_IS_LOCAL_ACTION) == MENU_IS_LOCAL_ACTION) {
+            // The item is an action, display it as such.
+            $actions_current[$link['href']] = array(
+              '#theme' => 'menu_local_action',
+              '#link' => $link,
+              '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
+            );
+            $action_count++;
+          }
+          else {
+            // Otherwise, it's a normal tab.
+            $tabs_current[$link['href']] = array(
+              '#theme' => 'menu_local_task',
+              '#link' => $link,
+              '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
+            );
+            $tab_count++;
+          }
+        }
+      }
+    }
+    $path = $next_path;
+    $tabs[$depth] = $tabs_current;
+    $actions = array_merge($actions, $actions_current);
+    $depth++;
+  }
+  $data['actions'] = $actions;
+  // Find all tabs at the same level or above the current one.
+  $parent = $router_item['tab_parent'];
+  $path = $router_item['path'];
+  $current = $router_item;
+  $depth = 1000;
+  while (isset($children[$parent])) {
+    $tabs_current = array();
+    $next_path = '';
+    $next_parent = '';
+    $count = 0;
+    foreach ($children[$parent] as $item) {
+      // Skip local actions.
+      if ($item['type'] & MENU_IS_LOCAL_ACTION) {
+        continue;
+      }
+      if ($item['access']) {
+        $count++;
+        $link = $item;
+        // Local tasks can be normal items too, so bitmask with
+        // MENU_LINKS_TO_PARENT before checking.
+        if (($item['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT) {
+          // Find the first parent which is not a default local task.
+          for ($p = $item['tab_parent']; ($tasks[$p]['type'] & MENU_LINKS_TO_PARENT) == MENU_LINKS_TO_PARENT; $p = $tasks[$p]['tab_parent']);
+          // Use the path of the parent instead.
+          $link['href'] = $tasks[$p]['href'];
+          if ($item['path'] == $router_item['path']) {
+            $root_path = $tasks[$p]['path'];
+          }
+        }
+        // We check for the active tab.
+        if ($item['path'] == $path) {
+          // Mark the link as active, if the current path is a (second-level)
+          // local task of a default local task. Since this default local task
+          // links to its parent, l() will not mark it as active, as it only
+          // compares the link's href to current_path().
+          if ($link['href'] != current_path()) {
+            $link['localized_options']['attributes']['class'][] = 'active';
+          }
+          $tabs_current[$link['href']] = array(
+            '#theme' => 'menu_local_task',
+            '#link' => $link,
+            '#active' => TRUE,
+            '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
+          );
+          $next_path = $item['tab_parent'];
+          if (isset($tasks[$next_path])) {
+            $next_parent = $tasks[$next_path]['tab_parent'];
+          }
+        }
+        else {
+          $tabs_current[$link['href']] = array(
+            '#theme' => 'menu_local_task',
+            '#link' => $link,
+            '#weight' => isset($link['weight']) ? $link['weight'] : NULL,
+          );
+        }
+      }
+    }
+    $path = $next_path;
+    $parent = $next_parent;
+    $tabs[$depth] = $tabs_current;
+    $depth--;
+  }
+  // Sort by depth.
+  ksort($tabs);
+  // Remove the depth, we are interested only in their relative placement.
+  $tabs = array_values($tabs);
+  $data['tabs'] += $tabs;
+}
+
 /**
  * Retrieves contextual links for a path based on registered local tasks.
  *
diff --git a/core/lib/Drupal/Core/Controller/HtmlPageController.php b/core/lib/Drupal/Core/Controller/HtmlPageController.php
index 45253285208d..f3358adcd003 100644
--- a/core/lib/Drupal/Core/Controller/HtmlPageController.php
+++ b/core/lib/Drupal/Core/Controller/HtmlPageController.php
@@ -84,7 +84,9 @@ public function content(Request $request, $_content) {
     }
     if (!is_array($page_content)) {
       $page_content = array(
-        '#markup' => $page_content,
+        'main' => array(
+          '#markup' => $page_content,
+        ),
       );
     }
     if (!isset($page_content['#title'])) {
diff --git a/core/modules/block/custom_block/custom_block.module b/core/modules/block/custom_block/custom_block.module
index eed50a4c1073..8e8177859e95 100644
--- a/core/modules/block/custom_block/custom_block.module
+++ b/core/modules/block/custom_block/custom_block.module
@@ -30,8 +30,8 @@ function custom_block_help($path, $arg) {
 /**
  * Implements hook_menu_local_tasks().
  */
-function custom_block_menu_local_tasks(&$data, $router_item, $root_path) {
-  if ($router_item['route_name'] == 'custom_block.list') {
+function custom_block_menu_local_tasks(&$data, $route_name) {
+  if ($route_name == 'custom_block.list') {
     // @todo Move to a LocalAction plugin when https://drupal.org/node/2045267
     //   allows local actions to work with query strings.
     $item = menu_get_item('block/add');
@@ -48,13 +48,13 @@ function custom_block_menu_local_tasks(&$data, $router_item, $root_path) {
   $routes = array_map(function ($theme) {
     return "block.admin_display_$theme";
   }, array_keys(list_themes()));
-  if (in_array($router_item['route_name'], $routes)) {
+  if (in_array($route_name, $routes)) {
     // @todo Move to a LocalAction plugin when https://drupal.org/node/2045267
     //   allows local actions to work with query strings.
     $item = menu_get_item('block/add');
     if ($item['access']) {
       // Add a destination parameter.
-      $item['localized_options']['query']['theme'] = end($router_item['map']);
+      $item['localized_options']['query']['theme'] = \Drupal::request()->attributes->get('theme');
       $data['actions']['block/add'] = array(
         '#theme' => 'menu_local_action',
         '#link' => $item,
diff --git a/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.module b/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.module
index 277fae3779ea..b9e848c43912 100644
--- a/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.module
+++ b/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.module
@@ -92,10 +92,7 @@ function custom_block_test_menu() {
   $items['custom-block/%custom_block'] = array(
     'title callback' => 'entity_page_label',
     'title arguments' => array(1),
-    'page callback' => 'entity_view',
-    'page arguments' => array(1, 'default'),
-    'access callback' => 'entity_page_access',
-    'access arguments' => array(1, 'view'),
+    'route_name' => 'custom_block_test.custom_block_view',
   );
   return $items;
 }
diff --git a/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.routing.yml b/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.routing.yml
new file mode 100644
index 000000000000..a7ca068eb101
--- /dev/null
+++ b/core/modules/block/custom_block/tests/modules/custom_block_test/custom_block_test.routing.yml
@@ -0,0 +1,6 @@
+custom_block_test.custom_block_view:
+  path: '/custom-block/{custom_block}'
+  defaults:
+    _entity_view: 'custom_block'
+  requirements:
+    _entity_access: 'custom_block.view'
diff --git a/core/modules/comment/lib/Drupal/comment/Tests/CommentNonNodeTest.php b/core/modules/comment/lib/Drupal/comment/Tests/CommentNonNodeTest.php
index e269aa5961f7..8f999b6049ec 100644
--- a/core/modules/comment/lib/Drupal/comment/Tests/CommentNonNodeTest.php
+++ b/core/modules/comment/lib/Drupal/comment/Tests/CommentNonNodeTest.php
@@ -372,7 +372,7 @@ function testCommentFunctionality() {
     $data = array('bundle' => 'entity_test_render', 'name' => $random_label);
     $new_entity = entity_create('entity_test_render', $data);
     $new_entity->save();
-    $this->drupalGet('entity_test_render/manage/' . $new_entity->id() . '/edit');
+    $this->drupalGet('entity_test_render/manage/' . $new_entity->id());
     $this->assertNoFieldChecked('edit-field-foobar-0-status-1');
     $this->assertFieldChecked('edit-field-foobar-0-status-2');
     $this->assertNoField('edit-field-foobar-0-status-0');
diff --git a/core/modules/content_translation/content_translation.local_tasks.yml b/core/modules/content_translation/content_translation.local_tasks.yml
new file mode 100644
index 000000000000..6e4a6bb0a6e5
--- /dev/null
+++ b/core/modules/content_translation/content_translation.local_tasks.yml
@@ -0,0 +1,4 @@
+content_translation.local_tasks:
+  derivative: 'Drupal\content_translation\Plugin\Derivative\ContentTranslationLocalTasks'
+  class: 'Drupal\content_translation\Plugin\ContentTranslationLocalTasks'
+  weight: 100
diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Plugin/ContentTranslationLocalTasks.php b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/ContentTranslationLocalTasks.php
new file mode 100644
index 000000000000..50c4c60a4e2f
--- /dev/null
+++ b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/ContentTranslationLocalTasks.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\content_translation\Plugin\ContentTranslationLocalTasks.
+ */
+
+namespace Drupal\content_translation\Plugin;
+
+use Drupal\Core\Menu\LocalTaskDefault;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides route parameter manipulation for content translation local tasks.
+ */
+class ContentTranslationLocalTasks extends LocalTaskDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteParameters(Request $request) {
+    $parameters = parent::getRouteParameters($request);
+    $entity_type = $this->pluginDefinition['entity_type'];
+    if ($raw_variables = $request->attributes->get('_raw_variables')) {
+      // When the entity type is in the path, populate 'entity' for any dynamic
+      // local tasks.
+      if ($raw_variables->has($entity_type)) {
+        $entity = $raw_variables->get($entity_type);
+        $parameters['entity'] = $entity;
+      }
+      // When 'entity' is in the path, populate the parameters with the value
+      // for the actual entity type.
+      elseif ($raw_variables->has('entity')) {
+        $entity = $raw_variables->get('entity');
+        $parameters[$entity_type] = $entity;
+      }
+    }
+    return $parameters;
+  }
+
+}
diff --git a/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationLocalTasks.php b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationLocalTasks.php
new file mode 100644
index 000000000000..1cc9e66ab08b
--- /dev/null
+++ b/core/modules/content_translation/lib/Drupal/content_translation/Plugin/Derivative/ContentTranslationLocalTasks.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\content_translation\Plugin\Derivative\ContentTranslationLocalTasks.
+ */
+
+namespace Drupal\content_translation\Plugin\Derivative;
+
+use Drupal\Component\Plugin\Derivative\DerivativeBase;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides dynamic local tasks for content translation.
+ */
+class ContentTranslationLocalTasks extends DerivativeBase implements ContainerDerivativeInterface {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManager
+   */
+  protected $entityManager;
+
+  /**
+   * The route provider.
+   *
+   * @var \Drupal\Core\Routing\RouteProviderInterface
+   */
+  protected $routeProvider;
+
+  /**
+   * Constructs a new ContentTranslationLocalTasks.
+   *
+   * @param \Drupal\Core\Entity\EntityManager $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider.
+   */
+  public function __construct(EntityManager $entity_manager, RouteProviderInterface $route_provider) {
+    $this->entityManager = $entity_manager;
+    $this->routeProvider = $route_provider;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    return new static(
+      $container->get('entity.manager'),
+      $container->get('router.route_provider')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions(array $base_plugin_definition) {
+    // Create tabs for all possible entity types.
+    foreach ($this->entityManager->getDefinitions() as $entity_type => $entity_info) {
+      if ($entity_info['translatable'] && isset($entity_info['translation'])) {
+        $path = '/' . preg_replace('/%(.*)/', '{$1}', $entity_info['menu_base_path']);
+        if ($routes = $this->routeProvider->getRoutesByPattern($path)->all()) {
+          // Find the route name for the entity page.
+          $entity_route_name = key($routes);
+          $entity_tab = $entity_route_name . '_tab';
+          // Find the route name for the translation overview.
+          $translation_route_name = "content_translation.translation_overview_$entity_type";
+          $translation_tab = $translation_route_name . '_tab';
+
+          // Both tabs will have the same root and entity type.
+          $common_tab_settings = array(
+            'tab_root_id' => $entity_tab,
+            'entity_type' => $entity_type,
+          );
+          $this->derivatives[$entity_tab] = $base_plugin_definition + $common_tab_settings;
+          $this->derivatives[$entity_tab]['title'] = t('Edit');
+          $this->derivatives[$entity_tab]['route_name'] = $entity_route_name;
+
+          $this->derivatives[$translation_tab] = $base_plugin_definition + $common_tab_settings;
+          $this->derivatives[$translation_tab]['title'] = t('Translate');
+          $this->derivatives[$translation_tab]['route_name'] = $translation_route_name;
+        }
+      }
+    }
+    return parent::getDerivativeDefinitions($base_plugin_definition);
+  }
+
+}
diff --git a/core/modules/datetime/lib/Drupal/datetime/Tests/DatetimeFieldTest.php b/core/modules/datetime/lib/Drupal/datetime/Tests/DatetimeFieldTest.php
index 4fd86d94377f..357abd37a830 100644
--- a/core/modules/datetime/lib/Drupal/datetime/Tests/DatetimeFieldTest.php
+++ b/core/modules/datetime/lib/Drupal/datetime/Tests/DatetimeFieldTest.php
@@ -112,7 +112,7 @@ function testDateField() {
       "{$field_name}[0][value][date]" => $date->format($date_format),
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
     $this->assertRaw($date->format($date_format));
@@ -183,7 +183,7 @@ function testDatetimeField() {
       "{$field_name}[0][value][time]" => $date->format($time_format),
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
     $this->assertRaw($date->format($date_format));
@@ -276,7 +276,7 @@ function testDatelistWidget() {
     }
 
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
 
diff --git a/core/modules/email/lib/Drupal/email/Tests/EmailFieldTest.php b/core/modules/email/lib/Drupal/email/Tests/EmailFieldTest.php
index 4994f6270be7..60fd532ad7cf 100644
--- a/core/modules/email/lib/Drupal/email/Tests/EmailFieldTest.php
+++ b/core/modules/email/lib/Drupal/email/Tests/EmailFieldTest.php
@@ -103,7 +103,7 @@ function testEmailField() {
       "{$field_name}[0][value]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
     $this->assertRaw($value);
diff --git a/core/modules/field/lib/Drupal/field/Tests/FormTest.php b/core/modules/field/lib/Drupal/field/Tests/FormTest.php
index 4090764909fb..d6ed724d0c73 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FormTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FormTest.php
@@ -129,14 +129,14 @@ function testFieldFormSingle() {
       "{$field_name}[0][value]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
     $entity = entity_load('entity_test', $id);
     $this->assertEqual($entity->{$field_name}->value, $value, 'Field value was saved');
 
     // Display edit form.
-    $this->drupalGet('entity_test/manage/' . $id . '/edit');
+    $this->drupalGet('entity_test/manage/' . $id);
     $this->assertFieldByName("{$field_name}[0][value]", $value, 'Widget is displayed with the correct default value');
     $this->assertNoField("{$field_name}[1][value]", 'No extraneous widget is displayed');
 
@@ -160,7 +160,7 @@ function testFieldFormSingle() {
       'name' => $this->randomName(),
       "{$field_name}[0][value]" => $value
     );
-    $this->drupalPostForm('entity_test/manage/' . $id . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $id, $edit, t('Save'));
     $this->assertText(t('entity_test @id has been updated.', array('@id' => $id)), 'Entity was updated');
     $this->container->get('entity.manager')->getStorageController('entity_test')->resetCache(array($id));
     $entity = entity_load('entity_test', $id);
@@ -194,7 +194,7 @@ function testFieldFormDefaultValue() {
       "{$field_name}[0][value]" => '',
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created.');
     $entity = entity_load('entity_test', $id);
@@ -225,7 +225,7 @@ function testFieldFormSingleRequired() {
       "{$field_name}[0][value]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
     $entity = entity_load('entity_test', $id);
@@ -238,7 +238,7 @@ function testFieldFormSingleRequired() {
       'name' => $this->randomName(),
       "{$field_name}[0][value]" => $value,
     );
-    $this->drupalPostForm('entity_test/manage/' . $id . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $id, $edit, t('Save'));
     $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation');
   }
 
@@ -315,7 +315,7 @@ function testFieldFormUnlimited() {
 
     // Submit the form and create the entity.
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
     $entity = entity_load('entity_test', $id);
@@ -467,7 +467,7 @@ function testFieldFormMultipleWidget() {
       $field_name => '1, 2, 3',
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
 
     // Check that the values were saved.
@@ -475,7 +475,7 @@ function testFieldFormMultipleWidget() {
     $this->assertFieldValues($entity_init, $field_name, array(1, 2, 3));
 
     // Display the form, check that the values are correctly filled in.
-    $this->drupalGet('entity_test/manage/' . $id . '/edit');
+    $this->drupalGet('entity_test/manage/' . $id);
     $this->assertFieldByName($field_name, '1, 2, 3', 'Widget is displayed.');
 
     // Submit the form with more values than the field accepts.
@@ -548,7 +548,7 @@ function testFieldFormAccess() {
       "{$field_name}[0][value]" => 1,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match("|$entity_type/manage/(\d+)/edit|", $this->url, $match);
+    preg_match("|$entity_type/manage/(\d+)|", $this->url, $match);
     $id = $match[1];
 
     // Check that the default value was saved.
@@ -563,7 +563,7 @@ function testFieldFormAccess() {
       "{$field_name}[0][value]" => 2,
       'revision' => TRUE,
     );
-    $this->drupalPostForm($entity_type . '/manage/' . $id . '/edit', $edit, t('Save'));
+    $this->drupalPostForm($entity_type . '/manage/' . $id, $edit, t('Save'));
 
     // Check that the new revision has the expected values.
     $this->container->get('entity.manager')->getStorageController($entity_type)->resetCache(array($id));
@@ -605,7 +605,7 @@ function testFieldFormHiddenWidget() {
     // the field that uses the hidden widget.
     $this->assertNoField("{$field_name}[0][value]", 'The hidden widget is not displayed');
     $this->drupalPostForm(NULL, array('user_id' => 1, 'name' => $this->randomName()), t('Save'));
-    preg_match('|' . $entity_type . '/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|' . $entity_type . '/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test_rev @id has been created.', array('@id' => $id)), 'Entity was created');
     $entity = entity_load($entity_type, $id);
@@ -622,7 +622,7 @@ function testFieldFormHiddenWidget() {
       ->save();
 
     // Display edit form.
-    $this->drupalGet($entity_type . '/manage/' . $id . '/edit');
+    $this->drupalGet($entity_type . '/manage/' . $id);
     $this->assertFieldByName("{$field_name}[0][value]", 99, 'Widget is displayed with the correct default value');
 
     // Update the entity.
@@ -643,7 +643,7 @@ function testFieldFormHiddenWidget() {
 
     // Create a new revision.
     $edit = array('revision' => TRUE);
-    $this->drupalPostForm($entity_type . '/manage/' . $id . '/edit', $edit, t('Save'));
+    $this->drupalPostForm($entity_type . '/manage/' . $id, $edit, t('Save'));
 
     // Check that the expected value has been carried over to the new revision.
     entity_get_controller($entity_type)->resetCache(array($id));
diff --git a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php
index 6250a951a99d..639b9e280f30 100644
--- a/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/TranslationWebTest.php
@@ -122,7 +122,7 @@ function testFieldFormTranslationRevisions() {
       "{$field_name}[0][value]" => $entity->{$field_name}->value,
       'revision' => TRUE,
     );
-    $this->drupalPostForm($this->entity_type . '/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm($this->entity_type . '/manage/' . $entity->id(), $edit, t('Save'));
 
     // Check translation revisions.
     $this->checkTranslationRevisions($entity->id(), $entity->getRevisionId(), $available_langcodes);
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 67fa30a505fa..05cd432707af 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -148,11 +148,11 @@ function forum_menu() {
 /**
  * Implements hook_menu_local_tasks().
  */
-function forum_menu_local_tasks(&$data, $router_item, $root_path) {
+function forum_menu_local_tasks(&$data, $route_name) {
   global $user;
 
   // Add action link to 'node/add/forum' on 'forum' sub-pages.
-  if ($root_path == 'forum' || $root_path == 'forum/%') {
+  if (in_array($route_name, array('forum.index', 'forum.page'))) {
     $request = \Drupal::request();
     $forum_term = $request->attributes->get('taxonomy_term');
     $vid = \Drupal::config('forum.settings')->get('vocabulary');
diff --git a/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php b/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php
index 37de2be9bcc8..9075b46da2b5 100644
--- a/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php
+++ b/core/modules/link/lib/Drupal/link/Tests/LinkFieldTest.php
@@ -107,7 +107,7 @@ function testURLValidation() {
       "{$field_name}[0][url]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
     $this->assertRaw($value);
@@ -228,7 +228,7 @@ function testLinkTitle() {
       "{$field_name}[0][title]" => '',
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
 
@@ -243,7 +243,7 @@ function testLinkTitle() {
       'name' => $this->randomName(),
       "{$field_name}[0][title]" => $title,
     );
-    $this->drupalPostForm("entity_test/manage/$id/edit", $edit, t('Save'));
+    $this->drupalPostForm("entity_test/manage/$id", $edit, t('Save'));
     $this->assertText(t('entity_test @id has been updated.', array('@id' => $id)));
 
     $this->renderTestEntity($id);
@@ -309,7 +309,7 @@ function testLinkFormatter() {
     // Assert label is shown.
     $this->assertText('Read more about this entity');
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
 
@@ -445,7 +445,7 @@ function testLinkSeparateFormatter() {
       "{$field_name}[1][title]" => $title2,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
 
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 7b01e0eadfee..4e42006bc5c2 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -1062,9 +1062,10 @@ function node_menu() {
 /**
  * Implements hook_menu_local_tasks().
  */
-function node_menu_local_tasks(&$data, $router_item, $root_path) {
+function node_menu_local_tasks(&$data, $route_name) {
   // Add action link to 'node/add' on 'admin/content' page.
-  if ($root_path == 'admin/content') {
+  // @todo Switch to inspecting route name in https://drupal.org/node/2021161.
+  if (current_path() == 'admin/content') {
     $item = menu_get_item('node/add');
     if ($item['access']) {
       $data['actions'][] = array(
diff --git a/core/modules/number/lib/Drupal/number/Tests/NumberFieldTest.php b/core/modules/number/lib/Drupal/number/Tests/NumberFieldTest.php
index c5ff8f4fd5c8..1a1e2b405764 100644
--- a/core/modules/number/lib/Drupal/number/Tests/NumberFieldTest.php
+++ b/core/modules/number/lib/Drupal/number/Tests/NumberFieldTest.php
@@ -104,7 +104,7 @@ function testNumberDecimalField() {
       "{$this->field['field_name']}[0][value]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
     $this->assertRaw(round($value, 2), 'Value is displayed.');
diff --git a/core/modules/options/lib/Drupal/options/Tests/OptionsSelectDynamicValuesTest.php b/core/modules/options/lib/Drupal/options/Tests/OptionsSelectDynamicValuesTest.php
index f31fd9ba7b9f..5ddcf0f4efa2 100644
--- a/core/modules/options/lib/Drupal/options/Tests/OptionsSelectDynamicValuesTest.php
+++ b/core/modules/options/lib/Drupal/options/Tests/OptionsSelectDynamicValuesTest.php
@@ -31,7 +31,7 @@ function testSelectListDynamic() {
     $this->drupalLogin($web_user);
 
     // Display form.
-    $this->drupalGet('entity_test_rev/manage/' . $this->entity->id() . '/edit');
+    $this->drupalGet('entity_test_rev/manage/' . $this->entity->id());
     $options = $this->xpath('//select[@id="edit-test-options"]/option');
     $this->assertEqual(count($options), count($this->test) + 1);
     foreach ($options as $option) {
diff --git a/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php b/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php
index e2a004d5a177..206b8c82015c 100644
--- a/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php
+++ b/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php
@@ -130,7 +130,7 @@ function testRadioButtons() {
     $entity_init = clone $entity;
 
     // With no field data, no buttons are checked.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoFieldChecked('edit-card-1-0');
     $this->assertNoFieldChecked('edit-card-1-1');
     $this->assertNoFieldChecked('edit-card-1-2');
@@ -142,7 +142,7 @@ function testRadioButtons() {
     $this->assertFieldValues($entity_init, 'card_1', array(0));
 
     // Check that the selected button is checked.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFieldChecked('edit-card-1-0');
     $this->assertNoFieldChecked('edit-card-1-1');
     $this->assertNoFieldChecked('edit-card-1-2');
@@ -157,7 +157,7 @@ function testRadioButtons() {
     $this->card_1->save();
     $instance->required = TRUE;
     $instance->save();
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFieldChecked('edit-card-1-99');
   }
 
@@ -187,7 +187,7 @@ function testCheckBoxes() {
     $entity_init = clone $entity;
 
     // Display form: with no field data, nothing is checked.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoFieldChecked('edit-card-2-0');
     $this->assertNoFieldChecked('edit-card-2-1');
     $this->assertNoFieldChecked('edit-card-2-2');
@@ -203,7 +203,7 @@ function testCheckBoxes() {
     $this->assertFieldValues($entity_init, 'card_2', array(0, 2));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFieldChecked('edit-card-2-0');
     $this->assertNoFieldChecked('edit-card-2-1');
     $this->assertFieldChecked('edit-card-2-2');
@@ -218,7 +218,7 @@ function testCheckBoxes() {
     $this->assertFieldValues($entity_init, 'card_2', array(0));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFieldChecked('edit-card-2-0');
     $this->assertNoFieldChecked('edit-card-2-1');
     $this->assertNoFieldChecked('edit-card-2-2');
@@ -247,7 +247,7 @@ function testCheckBoxes() {
     $this->card_2->save();
     $instance->required = TRUE;
     $instance->save();
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFieldChecked('edit-card-2-99');
   }
 
@@ -278,7 +278,7 @@ function testSelectListSingle() {
     $entity_init = clone $entity;
 
     // Display form.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     // A required field without any value has a "none" option.
     $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1', ':label' => t('- Select a value -'))), 'A required select list has a "Select a value" choice.');
 
@@ -300,7 +300,7 @@ function testSelectListSingle() {
     $this->assertFieldValues($entity_init, 'card_1', array(0));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     // A required field with a value has no 'none' option.
     $this->assertFalse($this->xpath('//select[@id=:id]//option[@value="_none"]', array(':id' => 'edit-card-1')), 'A required select list with an actual value has no "none" choice.');
     $this->assertOptionSelected('edit-card-1', 0);
@@ -312,12 +312,12 @@ function testSelectListSingle() {
     $instance->save();
 
     // Display form.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     // A non-required field has a 'none' option.
     $this->assertTrue($this->xpath('//select[@id=:id]//option[@value="_none" and text()=:label]', array(':id' => 'edit-card-1', ':label' => t('- None -'))), 'A non-required select list has a "None" choice.');
     // Submit form: Unselect the option.
     $edit = array('card_1' => '_none');
-    $this->drupalPostForm('entity_test/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $entity->id(), $edit, t('Save'));
     $this->assertFieldValues($entity_init, 'card_1', array());
 
     // Test optgroups.
@@ -327,7 +327,7 @@ function testSelectListSingle() {
     $this->card_1->save();
 
     // Display form: with no field data, nothing is selected
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoOptionSelected('edit-card-1', 0);
     $this->assertNoOptionSelected('edit-card-1', 1);
     $this->assertNoOptionSelected('edit-card-1', 2);
@@ -340,14 +340,14 @@ function testSelectListSingle() {
     $this->assertFieldValues($entity_init, 'card_1', array(0));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertOptionSelected('edit-card-1', 0);
     $this->assertNoOptionSelected('edit-card-1', 1);
     $this->assertNoOptionSelected('edit-card-1', 2);
 
     // Submit form: Unselect the option.
     $edit = array('card_1' => '_none');
-    $this->drupalPostForm('entity_test/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $entity->id(), $edit, t('Save'));
     $this->assertFieldValues($entity_init, 'card_1', array());
   }
 
@@ -377,7 +377,7 @@ function testSelectListMultiple() {
     $entity_init = clone $entity;
 
     // Display form: with no field data, nothing is selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoOptionSelected('edit-card-2', 0);
     $this->assertNoOptionSelected('edit-card-2', 1);
     $this->assertNoOptionSelected('edit-card-2', 2);
@@ -389,7 +389,7 @@ function testSelectListMultiple() {
     $this->assertFieldValues($entity_init, 'card_2', array(0, 2));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertOptionSelected('edit-card-2', 0);
     $this->assertNoOptionSelected('edit-card-2', 1);
     $this->assertOptionSelected('edit-card-2', 2);
@@ -400,7 +400,7 @@ function testSelectListMultiple() {
     $this->assertFieldValues($entity_init, 'card_2', array(0));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertOptionSelected('edit-card-2', 0);
     $this->assertNoOptionSelected('edit-card-2', 1);
     $this->assertNoOptionSelected('edit-card-2', 2);
@@ -420,18 +420,18 @@ function testSelectListMultiple() {
     // Check that the 'none' option has no efect if actual options are selected
     // as well.
     $edit = array('card_2[]' => array('_none' => '_none', 0 => 0));
-    $this->drupalPostForm('entity_test/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $entity->id(), $edit, t('Save'));
     $this->assertFieldValues($entity_init, 'card_2', array(0));
 
     // Check that selecting the 'none' option empties the field.
     $edit = array('card_2[]' => array('_none' => '_none'));
-    $this->drupalPostForm('entity_test/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $entity->id(), $edit, t('Save'));
     $this->assertFieldValues($entity_init, 'card_2', array());
 
     // A required select list does not have an empty key.
     $instance->required = TRUE;
     $instance->save();
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFalse($this->xpath('//select[@id=:id]//option[@value=""]', array(':id' => 'edit-card-2')), 'A required select list does not have an empty key.');
 
     // We do not have to test that a required select list with one option is
@@ -447,7 +447,7 @@ function testSelectListMultiple() {
     $instance->save();
 
     // Display form: with no field data, nothing is selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoOptionSelected('edit-card-2', 0);
     $this->assertNoOptionSelected('edit-card-2', 1);
     $this->assertNoOptionSelected('edit-card-2', 2);
@@ -460,14 +460,14 @@ function testSelectListMultiple() {
     $this->assertFieldValues($entity_init, 'card_2', array(0));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertOptionSelected('edit-card-2', 0);
     $this->assertNoOptionSelected('edit-card-2', 1);
     $this->assertNoOptionSelected('edit-card-2', 2);
 
     // Submit form: Unselect the option.
     $edit = array('card_2[]' => array('_none' => '_none'));
-    $this->drupalPostForm('entity_test/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm('entity_test/manage/' . $entity->id(), $edit, t('Save'));
     $this->assertFieldValues($entity_init, 'card_2', array());
   }
 
@@ -496,7 +496,7 @@ function testOnOffCheckbox() {
     $entity_init = clone $entity;
 
     // Display form: with no field data, option is unchecked.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoFieldChecked('edit-bool');
     $this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', 'Option text was properly filtered.');
 
@@ -506,7 +506,7 @@ function testOnOffCheckbox() {
     $this->assertFieldValues($entity_init, 'bool', array(1));
 
     // Display form: check that the right options are selected.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertFieldChecked('edit-bool');
 
     // Submit form: uncheck the option.
@@ -515,7 +515,7 @@ function testOnOffCheckbox() {
     $this->assertFieldValues($entity_init, 'bool', array(0));
 
     // Display form: with 'off' value, option is unchecked.
-    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
+    $this->drupalGet('entity_test/manage/' . $entity->id());
     $this->assertNoFieldChecked('edit-bool');
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php
index 3ab3d44a7fef..3fa35337748d 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityFormTest.php
@@ -77,14 +77,14 @@ protected function assertFormCRUD($entity_type) {
     $this->assertTrue($entity, format_string('%entity_type: Entity found in the database.', array('%entity_type' => $entity_type)));
 
     $edit['name'] = $name2;
-    $this->drupalPostForm($entity_type . '/manage/' . $entity->id() . '/edit', $edit, t('Save'));
+    $this->drupalPostForm($entity_type . '/manage/' . $entity->id(), $edit, t('Save'));
     $entity = $this->loadEntityByName($entity_type, $name1);
     $this->assertFalse($entity, format_string('%entity_type: The entity has been modified.', array('%entity_type' => $entity_type)));
     $entity = $this->loadEntityByName($entity_type, $name2);
     $this->assertTrue($entity, format_string('%entity_type: Modified entity found in the database.', array('%entity_type' => $entity_type)));
     $this->assertNotEqual($entity->name->value, $name1, format_string('%entity_type: The entity name has been modified.', array('%entity_type' => $entity_type)));
 
-    $this->drupalPostForm($entity_type . '/manage/' . $entity->id() . '/edit', array(), t('Delete'));
+    $this->drupalPostForm($entity_type . '/manage/' . $entity->id(), array(), t('Delete'));
     $entity = $this->loadEntityByName($entity_type, $name2);
     $this->assertFalse($entity, format_string('%entity_type: Entity not found in the database.', array('%entity_type' => $entity_type)));
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
index 381e8913cd31..f628f6063fac 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
@@ -554,57 +554,6 @@ protected function menuLoadRouter($router_path) {
     return db_query('SELECT * FROM {menu_router} WHERE path = :path', array(':path' => $router_path))->fetchAssoc();
   }
 
-  /**
-   * Tests inheritance of 'load arguments'.
-   */
-  function testMenuLoadArgumentsInheritance() {
-    $expected = array(
-      'menu-test/arguments/%/%' => array(
-        2 => array('menu_test_argument_load' => array(3)),
-        3 => NULL,
-      ),
-      // Arguments are inherited to normal children.
-      'menu-test/arguments/%/%/default' => array(
-        2 => array('menu_test_argument_load' => array(3)),
-        3 => NULL,
-      ),
-      // Arguments are inherited to tab children.
-      'menu-test/arguments/%/%/task' => array(
-        2 => array('menu_test_argument_load' => array(3)),
-        3 => NULL,
-      ),
-      // Arguments are only inherited to the same loader functions.
-      'menu-test/arguments/%/%/common-loader' => array(
-        2 => array('menu_test_argument_load' => array(3)),
-        3 => 'menu_test_other_argument_load',
-      ),
-      // Arguments are not inherited to children not using the same loader
-      // function.
-      'menu-test/arguments/%/%/different-loaders-1' => array(
-        2 => NULL,
-        3 => 'menu_test_argument_load',
-      ),
-      'menu-test/arguments/%/%/different-loaders-2' => array(
-        2 => 'menu_test_other_argument_load',
-        3 => NULL,
-      ),
-      'menu-test/arguments/%/%/different-loaders-3' => array(
-        2 => NULL,
-        3 => NULL,
-      ),
-      // Explicit loader arguments should not be overriden by parent.
-      'menu-test/arguments/%/%/explicit-arguments' => array(
-        2 => array('menu_test_argument_load' => array()),
-        3 => NULL,
-      ),
-    );
-
-    foreach ($expected as $router_path => $load_functions) {
-      $router_item = $this->menuLoadRouter($router_path);
-      $this->assertIdentical(unserialize($router_item['load_functions']), $load_functions, format_string('Expected load functions for router %router_path' , array('%router_path' => $router_path)));
-    }
-  }
-
   /**
    * Test menu links that have optional placeholders.
    */
diff --git a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php
index 12321bf26acf..0d184b68f737 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Module/ModuleApiTest.php
@@ -136,19 +136,6 @@ function testModuleInvokeAll() {
     $this->assertText('success!', 'module_invoke_all() dynamically loads a hook defined in hook_hook_info().');
   }
 
-  /**
-   * Test that a menu item load function can invoke hooks defined in hook_hook_info().
-   *
-   * We test this separately from testModuleInvokeAll(), because menu item load
-   * functions execute early in the request handling.
-   */
-  function testModuleInvokeAllDuringLoadFunction() {
-    \Drupal::moduleHandler()->install(array('module_test'), FALSE);
-    $this->resetAll();
-    $this->drupalGet('module-test/hook-dynamic-loading-invoke-all-during-load/module_test');
-    $this->assertText('success!', 'Menu item load function invokes a hook defined in hook_hook_info().');
-  }
-
   /**
    * Test dependency resolution.
    */
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 076761711665..55daae44943c 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -858,12 +858,10 @@ function hook_menu_alter(&$items) {
  *     associative array as described above.
  *   - tabs: A list of (up to 2) tab levels that contain a list of of tabs keyed
  *     by their href, each one being an associative array as described above.
- * @param array $router_item
- *   The menu system router item of the page.
- * @param string $root_path
- *   The path to the root item for this set of tabs.
+ * @param string $route_name
+ *   The route name of the page.
  */
-function hook_menu_local_tasks(&$data, $router_item, $root_path) {
+function hook_menu_local_tasks(&$data, $route_name) {
   // Add an action linking to node/add to all pages.
   $data['actions']['node/add'] = array(
     '#theme' => 'menu_local_action',
@@ -890,8 +888,6 @@ function hook_menu_local_tasks(&$data, $router_item, $root_path) {
         ),
       ),
     ),
-    // Define whether this link is active. This can usually be omitted.
-    '#active' => ($router_item['path'] == $root_path),
   );
 }
 
@@ -904,14 +900,12 @@ function hook_menu_local_tasks(&$data, $router_item, $root_path) {
  * @param array $data
  *   An associative array containing tabs and actions. See
  *   hook_menu_local_tasks() for details.
- * @param array $router_item
- *   The menu system router item of the page.
- * @param string $root_path
- *   The path to the root item for this set of tabs.
+ * @param string $route_name
+ *   The route name of the page.
  *
  * @see hook_menu_local_tasks()
  */
-function hook_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+function hook_menu_local_tasks_alter(&$data, $route_name) {
 }
 
 /**
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index 962b3bb88cc8..802b34e757e6 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -16,34 +16,10 @@
  * Implements hook_menu().
  */
 function ajax_test_menu() {
-  $items['ajax-test/render'] = array(
-    'title' => 'ajax_render',
-    'page callback' => 'ajax_test_render',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
   $items['ajax-test/order'] = array(
     'title' => 'AJAX commands order',
-    'page callback' => 'ajax_test_order',
+    'route_name' => 'ajax_test.order',
     'theme callback' => 'ajax_base_page_theme',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-  $items['ajax-test/render-error'] = array(
-    'title' => 'ajax_render_error',
-    'page callback' => 'ajax_test_error',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-  $items['ajax-test/dialog'] = array(
-    'title' => 'AJAX Dialog',
-    'page callback' => 'ajax_test_dialog',
-    'access callback' => TRUE,
-  );
-  $items['ajax-test/dialog-close'] = array(
-    'title' => 'AJAX Dialog close',
-    'page callback' => 'ajax_test_dialog_close',
-    'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
   return $items;
@@ -63,6 +39,8 @@ function ajax_test_system_theme_info() {
  * Additionally ensures that ajax_render() incorporates JavaScript settings
  * generated during the page request by invoking drupal_add_js() with a dummy
  * setting.
+ *
+ * @deprecated \Drupal\ajax_test\Controller\AjaxTestController::render()
  */
 function ajax_test_render() {
   drupal_add_js(array('ajax' => 'test'), 'setting');
@@ -74,6 +52,8 @@ function ajax_test_render() {
  * Menu callback: Returns an AjaxResponse; settings command set last.
  *
  * Helps verifying AjaxResponse reorders commands to ensure correct execution.
+ *
+ * @deprecated \Drupal\ajax_test\Controller\AjaxTestController::order()
  */
 function ajax_test_order() {
   $response = new AjaxResponse();
@@ -93,6 +73,8 @@ function ajax_test_order() {
 
 /**
  * Menu callback: Returns AJAX element with #error property set.
+ *
+ * @deprecated \Drupal\ajax_test\Controller\AjaxTestController::renderError()
  */
 function ajax_test_error() {
   $message = '';
@@ -106,6 +88,8 @@ function ajax_test_error() {
 
 /**
  * Menu callback: Renders a form elements and links with #ajax['dialog'].
+ *
+ * @deprecated \Drupal\ajax_test\Controller\AjaxTestController::dialog()
  */
 function ajax_test_dialog() {
   // Add two wrapper elements for testing non-modal dialogs. Modal dialogs use
@@ -293,6 +277,8 @@ function ajax_test_dialog_contents() {
 
 /**
  * Menu callback: Close the ajax dialog.
+ *
+ * @deprecated \Drupal\ajax_test\Controller\AjaxTestController::dialogClose()
  */
 function ajax_test_dialog_close() {
   $response = new AjaxResponse();
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
index 5a87cb40595d..7cf293850505 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
@@ -1,7 +1,7 @@
 ajax_test.dialog_contents:
   path: ajax-test/dialog-contents
   defaults:
-    _content: '\Drupal\ajax_test\AjaxTestController::dialogContents'
+    _content: '\Drupal\ajax_test\Controller\AjaxTestController::dialogContents'
   requirements:
     _access: 'TRUE'
 
@@ -11,3 +11,38 @@ ajax_test.dialog_form:
     _form: '\Drupal\ajax_test\AjaxTestForm'
   requirements:
     _access: 'TRUE'
+
+ajax_test.dialog:
+  path: '/ajax-test/dialog'
+  defaults:
+    _content: '\Drupal\ajax_test\Controller\AjaxTestController::dialog'
+  requirements:
+    _access: 'TRUE'
+
+ajax_test.dialog_close:
+  path: '/ajax-test/dialog-close'
+  defaults:
+    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::dialogClose'
+  requirements:
+    _access: 'TRUE'
+
+ajax_test.render:
+  path: '/ajax-test/render'
+  defaults:
+    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::render'
+  requirements:
+    _access: 'TRUE'
+
+ajax_test.order:
+  path: '/ajax-test/order'
+  defaults:
+    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::order'
+  requirements:
+    _access: 'TRUE'
+
+ajax_test.render_error:
+  path: '/ajax-test/render-error'
+  defaults:
+    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderError'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php
deleted file mode 100644
index 53044c530068..000000000000
--- a/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/AjaxTestController.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\ajax_test\AjaxTestController.
- */
-
-namespace Drupal\ajax_test;
-
-/**
- * Provides content for dialog tests.
- */
-class AjaxTestController {
-
-  /**
-   * Returns example content for dialog testing.
-   */
-  public function dialogContents() {
-    // Re-use the utility method that returns the example content.
-    drupal_set_title(t('AJAX Dialog contents'));
-    return ajax_test_dialog_contents();
-  }
-
-}
diff --git a/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/Controller/AjaxTestController.php
new file mode 100644
index 000000000000..d0387ef33f08
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/lib/Drupal/ajax_test/Controller/AjaxTestController.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ajax_test\Controller\AjaxTestController.
+ */
+
+namespace Drupal\ajax_test\Controller;
+
+/**
+ * Provides content for dialog tests.
+ */
+class AjaxTestController {
+
+  /**
+   * Returns example content for dialog testing.
+   */
+  public function dialogContents() {
+    // Re-use the utility method that returns the example content.
+    drupal_set_title(t('AJAX Dialog contents'));
+    return ajax_test_dialog_contents();
+  }
+
+  /**
+   * @todo Remove ajax_test_render().
+   */
+  public function render() {
+    return ajax_test_render();
+  }
+
+  /**
+   * @todo Remove ajax_test_order().
+   */
+  public function order() {
+    return ajax_test_order();
+  }
+
+  /**
+   * @todo Remove ajax_test_error().
+   */
+  public function renderError() {
+    return ajax_test_error();
+  }
+
+  /**
+   * @todo Remove ajax_test_dialog().
+   */
+  public function dialog() {
+    return ajax_test_dialog();
+  }
+
+  /**
+   * @todo Remove ajax_test_dialog_close().
+   */
+  public function dialogClose() {
+    return ajax_test_dialog_close();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/database_test/database_test.module b/core/modules/system/tests/modules/database_test/database_test.module
index 518e3aaed025..19eb6ed23379 100644
--- a/core/modules/system/tests/modules/database_test/database_test.module
+++ b/core/modules/system/tests/modules/database_test/database_test.module
@@ -49,26 +49,6 @@ function database_test_query_database_test_alter_remove_range_alter(AlterableInt
  * Implements hook_menu().
  */
 function database_test_menu() {
-  $items['database_test/db_query_temporary'] = array(
-    'access callback' => TRUE,
-    'page callback' => 'database_test_db_query_temporary',
-  );
-  $items['database_test/pager_query_even'] = array(
-    'access callback' => TRUE,
-    'page callback' => 'database_test_even_pager_query',
-  );
-  $items['database_test/pager_query_odd'] = array(
-    'access callback' => TRUE,
-    'page callback' => 'database_test_odd_pager_query',
-  );
-  $items['database_test/tablesort'] = array(
-    'access callback' => TRUE,
-    'page callback' => 'database_test_tablesort',
-  );
-  $items['database_test/tablesort_first'] = array(
-    'access callback' => TRUE,
-    'page callback' => 'database_test_tablesort_first',
-  );
   $items['database_test/tablesort_default_sort'] = array(
     'access callback' => TRUE,
     'page callback' => 'drupal_get_form',
@@ -83,6 +63,8 @@ function database_test_menu() {
  * We need to test that the table created is temporary, so we run it here, in a
  * separate menu callback request; After this request is done, the temporary
  * table should automatically dropped.
+ *
+ * @deprecated \Drupal\database_test\Controller\DatabaseTestController::dbQueryTemporary()
  */
 function database_test_db_query_temporary() {
   $table_name = db_query_temporary('SELECT age FROM {test}', array());
@@ -97,6 +79,8 @@ function database_test_db_query_temporary() {
  *
  * This function does care about the page GET parameter, as set by the
  * simpletest HTTP call.
+ *
+ * @deprecated \Drupal\database_test\Controller\DatabaseTestController::pagerQueryEven()
  */
 function database_test_even_pager_query($limit) {
 
@@ -122,6 +106,8 @@ function database_test_even_pager_query($limit) {
  *
  * This function does care about the page GET parameter, as set by the
  * simpletest HTTP call.
+ *
+ * @deprecated \Drupal\database_test\Controller\DatabaseTestController::pagerQueryOdd()
  */
 function database_test_odd_pager_query($limit) {
 
@@ -147,6 +133,8 @@ function database_test_odd_pager_query($limit) {
  *
  * This function does care about the page GET parameter, as set by the
  * simpletest HTTP call.
+ *
+ * @deprecated \Drupal\database_test\Controller\DatabaseTestController::testTablesort()
  */
 function database_test_tablesort() {
   $header = array(
@@ -177,6 +165,8 @@ function database_test_tablesort() {
  *
  * This function does care about the page GET parameter, as set by the
  * simpletest HTTP call.
+ *
+ * @deprecated \Drupal\database_test\Controller\DatabaseTestController::testTablesortFirst()
  */
 function database_test_tablesort_first() {
   $header = array(
diff --git a/core/modules/system/tests/modules/database_test/database_test.routing.yml b/core/modules/system/tests/modules/database_test/database_test.routing.yml
new file mode 100644
index 000000000000..46fa58060992
--- /dev/null
+++ b/core/modules/system/tests/modules/database_test/database_test.routing.yml
@@ -0,0 +1,34 @@
+database_test.db_query_temporary:
+  path: '/database_test/db_query_temporary'
+  defaults:
+    _content: '\Drupal\database_test\Controller\DatabaseTestController::dbQueryTemporary'
+  requirements:
+    _access: 'TRUE'
+
+database_test.pager_query_even:
+  path: '/database_test/pager_query_even/{limit}'
+  defaults:
+    _content: '\Drupal\database_test\Controller\DatabaseTestController::pagerQueryEven'
+  requirements:
+    _access: 'TRUE'
+
+database_test.pager_query_odd:
+  path: '/database_test/pager_query_odd/{limit}'
+  defaults:
+    _content: '\Drupal\database_test\Controller\DatabaseTestController::pagerQueryOdd'
+  requirements:
+    _access: 'TRUE'
+
+database_test.tablesort:
+  path: '/database_test/tablesort'
+  defaults:
+    _content: '\Drupal\database_test\Controller\DatabaseTestController::testTablesort'
+  requirements:
+    _access: 'TRUE'
+
+database_test.tablesort_first:
+  path: '/database_test/tablesort_first'
+  defaults:
+    _content: '\Drupal\database_test\Controller\DatabaseTestController::testTablesortFirst'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/database_test/lib/Drupal/database_test/Controller/DatabaseTestController.php b/core/modules/system/tests/modules/database_test/lib/Drupal/database_test/Controller/DatabaseTestController.php
new file mode 100644
index 000000000000..23687d3cf60d
--- /dev/null
+++ b/core/modules/system/tests/modules/database_test/lib/Drupal/database_test/Controller/DatabaseTestController.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\database_test\Controller\DatabaseTestController.
+ */
+
+namespace Drupal\database_test\Controller;
+
+/**
+ * Controller routines for database_test routes.
+ */
+class DatabaseTestController {
+
+  /**
+   * @todo Remove database_test_db_query_temporary().
+   */
+  public function dbQueryTemporary() {
+    return database_test_db_query_temporary();
+  }
+
+  /**
+   * @todo Remove database_test_even_pager_query().
+   */
+  public function pagerQueryEven($limit) {
+    return database_test_even_pager_query($limit);
+  }
+
+  /**
+   * @todo Remove database_test_odd_pager_query().
+   */
+  public function pagerQueryOdd($limit) {
+    return database_test_odd_pager_query($limit);
+  }
+
+  /**
+   * @todo Remove database_test_tablesort().
+   */
+  public function testTablesort() {
+    return database_test_tablesort();
+  }
+
+  /**
+   * @todo Remove database_test_tablesort_first().
+   */
+  public function testTablesortFirst() {
+    return database_test_tablesort_first();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index fabd1d31c4c8..f490c6816785 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -61,9 +61,12 @@ function entity_test_entity_types($filter = NULL) {
  * Implements hook_entity_info_alter().
  */
 function entity_test_entity_info_alter(&$info) {
-  // Optionally specify a translation handler for testing translations.
-  if (\Drupal::state()->get('entity_test.translation')) {
-    foreach(entity_test_entity_types() as $entity_type) {
+  foreach (entity_test_entity_types() as $entity_type) {
+    // Ensure these test entity types use their base path as their edit path.
+    $info[$entity_type]['menu_edit_path'] = $info[$entity_type]['menu_base_path'];
+
+    // Optionally specify a translation handler for testing translations.
+    if (\Drupal::state()->get('entity_test.translation')) {
       $info[$entity_type]['translation'][$entity_type] = TRUE;
     }
   }
@@ -237,19 +240,13 @@ function entity_test_menu() {
     $items[$entity_type . '/add'] = array(
       'title' => 'Add an @type',
       'title arguments' => array('@type' => $entity_type),
-      'page callback' => 'entity_test_add',
-      'page arguments' => array($entity_type),
-      'access arguments' => array('administer entity_test content'),
-      'type' => MENU_NORMAL_ITEM,
+      'route_name' => "entity_test.add_$entity_type",
     );
 
     $items[$entity_type . '/manage/%' . $entity_type] = array(
       'title' => 'Edit @type',
       'title arguments' => array('@type' => $entity_type),
-      'page callback' => 'entity_test_edit',
-      'page arguments' => array(2),
-      'access arguments' => array('administer entity_test content'),
-      'type' => MENU_NORMAL_ITEM,
+      'route_name' => "entity_test.edit_$entity_type",
     );
 
     $items[$entity_type . '/manage/%' . $entity_type . '/edit'] = array(
@@ -279,6 +276,8 @@ function entity_test_form_node_form_alter(&$form, &$form_state, $form_id) {
  *   The processed form for a new entity_test.
  *
  * @see entity_test_menu()
+ *
+ * @deprecated \Drupal\entity_test\Controller\EntityTestController::testAdd()
  */
 function entity_test_add($entity_type) {
   drupal_set_title(t('Create an @type', array('@type' => $entity_type)));
@@ -296,6 +295,8 @@ function entity_test_add($entity_type) {
  *   The processed form for the edited entity.
  *
  * @see entity_test_menu()
+ *
+ * @deprecated \Drupal\entity_test\Controller\EntityTestController::testEdit()
  */
 function entity_test_edit(EntityInterface $entity) {
   drupal_set_title($entity->label(), PASS_THROUGH);
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.services.yml b/core/modules/system/tests/modules/entity_test/entity_test.services.yml
new file mode 100644
index 000000000000..77855b8efcff
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/entity_test.services.yml
@@ -0,0 +1,5 @@
+services:
+  entity_test.subscriber:
+    class: Drupal\entity_test\Routing\RouteSubscriber
+    tags:
+     - { name: event_subscriber }
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Controller/EntityTestController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Controller/EntityTestController.php
new file mode 100644
index 000000000000..053163bfaa59
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Controller/EntityTestController.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\Controller\EntityTestController.
+ */
+
+namespace Drupal\entity_test\Controller;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Controller routines for entity_test routes.
+ */
+class EntityTestController {
+
+  /**
+   * @todo Remove entity_test_add()
+   */
+  public function testAdd($entity_type) {
+    return entity_test_add($entity_type);
+  }
+
+  /**
+   * @todo Remove entity_test_edit()
+   */
+  public function testEdit(EntityInterface $entity) {
+    return entity_test_edit($entity);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
index e54520cd7597..7b0741d99ce1 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestFormController.php
@@ -93,7 +93,7 @@ public function save(array $form, array &$form_state) {
     drupal_set_message($message);
 
     if ($entity->id()) {
-      $form_state['redirect'] = $entity->entityType() . '/manage/' . $entity->id() . '/edit';
+      $form_state['redirect'] = $entity->entityType() . '/manage/' . $entity->id();
     }
     else {
       // Error on save.
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Routing/RouteSubscriber.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Routing/RouteSubscriber.php
new file mode 100644
index 000000000000..8834b4a7d978
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/Routing/RouteSubscriber.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\Routing\RouteSubscriber.
+ */
+
+namespace Drupal\entity_test\Routing;
+
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Subscriber for Entity Test routes.
+ */
+class RouteSubscriber implements EventSubscriberInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[RoutingEvents::DYNAMIC] = 'routes';
+    return $events;
+  }
+
+  /**
+   * Adds routes for the Entity Test.
+   */
+  public function routes(RouteBuildEvent $event) {
+    $collection = $event->getRouteCollection();
+    $types = entity_test_entity_types();
+    $types[] = 'entity_test_render';
+
+    foreach ($types as $entity_type) {
+      $route = new Route(
+        "$entity_type/add",
+        array('_content' => '\Drupal\entity_test\Controller\EntityTestController::testAdd', 'entity_type' => $entity_type),
+        array('_permission' => 'administer entity_test content')
+      );
+      $collection->add("entity_test.add_$entity_type", $route);
+
+      $route = new Route(
+        "$entity_type/manage/{entity}",
+        array('_content' => '\Drupal\entity_test\Controller\EntityTestController::testEdit'),
+        array('_permission' => 'administer entity_test content'),
+        array('parameters' => array(
+          'entity' => array('type' => 'entity:' . $entity_type),
+        ))
+      );
+      $collection->add("entity_test.edit_$entity_type", $route);
+    }
+  }
+
+}
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index 5b34e1bd88fb..7fe368de7658 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -32,11 +32,6 @@ function form_test_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
-  $items['form-test/system-config-form'] = array(
-    'title' => 'Form object builder test',
-    'page callback' => 'NOT_USED',
-    'type' => MENU_CALLBACK,
-  );
   $items['form-test/validate-required'] = array(
     'title' => 'Form #required validation',
     'page callback' => 'drupal_get_form',
@@ -117,14 +112,6 @@ function form_test_menu() {
     'type' => MENU_CALLBACK,
   );
 
-  $items['form_test/wrapper-callback'] = array(
-    'title' => 'Form wrapper callback test',
-    'page callback' => 'form_test_wrapper_callback',
-    'page arguments' => array('form_test_wrapper_callback_form'),
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
   $items['form_test/form-state-values-clean'] = array(
     'title' => 'Form state values clearance test',
     'page callback' => 'drupal_get_form',
@@ -277,13 +264,6 @@ function form_test_menu() {
     'type' => MENU_CALLBACK,
   );
 
-  $items['form-test/double-form'] = array(
-    'title' => 'Double form test',
-    'page callback' => 'form_test_double_form',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
   $items['form-test/load-include-menu'] = array(
     'title' => 'FAPI test loading includes',
     'page callback' => 'drupal_get_form',
@@ -1059,6 +1039,8 @@ function form_label_test_form() {
 
 /**
  * Menu callback; Invokes a form builder function with a wrapper callback.
+ *
+ * @deprecated \Drupal\form_test\Controller\FormTestController::wrapperCallback()
  */
 function form_test_wrapper_callback($form_id) {
   $form_state = array(
@@ -2338,6 +2320,8 @@ function form_test_required_attribute($form, &$form_state) {
 
 /**
  * Menu callback returns two instances of the same form.
+ *
+ * @deprecated \Drupal\form_test\Controller\FormTestController::doubleForm()
  */
 function form_test_double_form() {
   return array(
diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml
index 103b7806422d..e0997a5d4f6e 100644
--- a/core/modules/system/tests/modules/form_test/form_test.routing.yml
+++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml
@@ -67,3 +67,20 @@ form_test.autocomplete_2:
     controller: '\Drupal\form_test\AutocompleteController::autocomplete1'
   requirements:
     _permission: 'access autocomplete test'
+
+form_test.wrapper:
+  path: '/form_test/wrapper-callback'
+  defaults:
+    _title: 'Form wrapper callback test'
+    _content: '\Drupal\form_test\Controller\FormTestController::wrapperCallback'
+    form_id: 'form_test_wrapper_callback_form'
+  requirements:
+    _access: 'TRUE'
+
+form_test.double_form:
+  path: '/form-test/double-form'
+  defaults:
+    _title: 'Double form test'
+    _content: '\Drupal\form_test\Controller\FormTestController::doubleForm'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php b/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php
index 001967200e09..40dd178ebcb0 100644
--- a/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php
+++ b/core/modules/system/tests/modules/form_test/lib/Drupal/form_test/Controller/FormTestController.php
@@ -35,4 +35,18 @@ public function twoFormInstances() {
     return $return;
   }
 
+  /**
+   * @todo Remove form_test_wrapper_callback().
+   */
+  public function wrapperCallback($form_id) {
+    return form_test_wrapper_callback($form_id);
+  }
+
+  /**
+   * @todo Remove form_test_double_form().
+   */
+  public function doubleForm() {
+    return form_test_double_form();
+  }
+
 }
diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Controller/MenuTestController.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Controller/MenuTestController.php
new file mode 100644
index 000000000000..d315e6d42aa5
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Controller/MenuTestController.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\Controller\MenuTestController.
+ */
+
+namespace Drupal\menu_test\Controller;
+
+/**
+ * Controller routines for menu_test routes.
+ */
+class MenuTestController {
+
+  /**
+   * @todo Remove menu_test_callback().
+   */
+  public function menuTestCallback() {
+    return menu_test_callback();
+  }
+
+  /**
+   * @todo Remove menu_test_custom_403_404_callback().
+   */
+  public function custom403404() {
+    return menu_test_custom_403_404_callback();
+  }
+
+  /**
+   * @todo Remove menu_test_menu_trail_callback().
+   */
+  public function menuTrail() {
+    return menu_test_menu_trail_callback();
+  }
+
+  /**
+   * @todo Remove menu_test_theme_page_callback().
+   */
+  public function themePage($inherited) {
+    return menu_test_theme_page_callback($inherited);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.local_tasks.yml b/core/modules/system/tests/modules/menu_test/menu_test.local_tasks.yml
index 6e83ec62d545..2947bf7dfef3 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.local_tasks.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.local_tasks.yml
@@ -52,3 +52,22 @@ menu_test.local_task_test_upcasting_sub2:
   title: 'upcasting sub2'
   tab_root_id: menu_test.local_task_test_upcasting_sub1
   weight: 10
+
+menu_test.tasks_default_tab:
+  route_name: menu_test.tasks_default
+  title: 'View'
+  tab_root_id: menu_test.tasks_default_tab
+
+menu_test.tasks_tasks_tab:
+  route_name: menu_test.tasks_tasks
+  title: 'View'
+  tab_root_id: menu_test.tasks_tasks_tab
+menu_test.tasks_edit_tab:
+  route_name: menu_test.tasks_edit
+  title: 'Edit'
+  tab_root_id: menu_test.tasks_tasks_tab
+menu_test.tasks_settings_tab:
+  route_name: menu_test.tasks_settings
+  title: 'Settings'
+  weight: 100
+  tab_root_id: menu_test.tasks_tasks_tab
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index 833d4972371b..566a6202097e 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -14,16 +14,9 @@ function menu_test_menu() {
   // The name of the menu changes during the course of the test. Using a $_GET.
   $items['menu_name_test'] = array(
     'title' => 'Test menu_name router item',
-    'page callback' => 'menu_test_callback',
+    'route_name' => 'menu_test.menu_name_test',
     'menu_name' => menu_test_menu_name(),
   );
-  // This item is of type MENU_CALLBACK with no parents to test title.
-  $items['menu_callback_title'] = array(
-    'title' => 'Menu Callback Title',
-    'page callback' => 'menu_test_callback',
-    'type' => MENU_CALLBACK,
-    'access arguments' => array('access content'),
-  );
   // This item uses SystemController::systemAdminMenuBlockPage() to list child
   // items.
   $items['menu_callback_description'] = array(
@@ -34,64 +27,56 @@ function menu_test_menu() {
   // This item tests the description key.
   $items['menu_callback_description/description-plain'] = array(
     'title' => 'Menu item with a regular description',
-    'page callback' => 'menu_test_callback',
     'description' => 'Menu item description text',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.callback_description_plain',
   );
   // This item tests using a description callback.
   $items['menu_callback_description/description-callback'] = array(
     'title' => 'Menu item with a description set with a callback',
-    'page callback' => 'menu_test_callback',
     'description callback' => 'check_plain',
     'description arguments' => array('<strong>Menu item description arguments</strong>'),
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.callback_description_callback',
   );
   // Use FALSE as 'title callback' to bypass t().
   $items['menu_no_title_callback'] = array(
     'title' => 'A title with @placeholder',
     'title callback' => FALSE,
     'title arguments' => array('@placeholder' => 'some other text'),
-    'page callback' => 'menu_test_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.menu_no_title_callback',
   );
 
   // Hidden link for menu_link_maintain tests
   $items['menu_test_maintain/%'] = array(
     'title' => 'Menu maintain test',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.menu_test_maintain',
    );
   // Hierarchical tests.
   $items['menu-test/hierarchy/parent'] = array(
     'title' => 'Parent menu router',
-    'page callback' => 'test_page_test_page',
+    'route_name' => 'menu_test.hierarchy_parent',
   );
   $items['menu-test/hierarchy/parent/child'] = array(
     'title' => 'Child menu router',
-    'page callback' => 'test_page_test_page',
+    'route_name' => 'menu_test.hierarchy_parent_child',
   );
   $items['menu-test/hierarchy/parent/child2/child'] = array(
     'title' => 'Unattached subchild router',
-    'page callback' => 'test_page_test_page',
+    'route_name' => 'menu_test.hierarchy_parent_child2',
   );
   // Theme callback tests.
   $items['menu-test/theme-callback/%'] = array(
     'title' => 'Page that displays different themes',
-    'page callback' => 'menu_test_theme_page_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.theme_callback',
     'theme callback' => 'menu_test_theme_callback',
     'theme arguments' => array(2),
   );
   $items['menu-test/theme-callback/%/inheritance'] = array(
     'title' => 'Page that tests theme callback inheritance.',
-    'page callback' => 'menu_test_theme_page_callback',
-    'page arguments' => array(TRUE),
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.theme_callback_inheritance',
   );
   $items['menu-test/no-theme-callback'] = array(
     'title' => 'Page that displays different themes without using a theme callback.',
-    'page callback' => 'menu_test_theme_page_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.no_theme_callback',
   );
   // Path containing "exotic" characters.
   $path = "menu-test/ -._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters.
@@ -99,8 +84,7 @@ function menu_test_menu() {
     "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets.
   $items[$path] = array(
     'title' => '"Exotic" path',
-    'page callback' => 'menu_test_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.exotic_path',
   );
 
   // Hidden tests; base parents.
@@ -108,20 +92,17 @@ function menu_test_menu() {
   // change, we need to simulate our own in here.
   $items['menu-test'] = array(
     'title' => 'Menu test root',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.menu_test',
   );
   $items['menu-test/hidden'] = array(
     'title' => 'Hidden test root',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden',
   );
 
   // Hidden tests; one dynamic argument.
   $items['menu-test/hidden/menu'] = array(
     'title' => 'Menus',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_menu',
   );
   $items['menu-test/hidden/menu/list'] = array(
     'title' => 'List menus',
@@ -129,21 +110,18 @@ function menu_test_menu() {
   );
   $items['menu-test/hidden/menu/add'] = array(
     'title' => 'Add menu',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_menu_add',
     'type' => MENU_LOCAL_ACTION,
   );
   $items['menu-test/hidden/menu/settings'] = array(
     'title' => 'Settings',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_menu_settings',
     'type' => MENU_LOCAL_TASK,
     'weight' => 5,
   );
   $items['menu-test/hidden/menu/manage/%menu'] = array(
     'title' => 'Customize menu',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_manage',
   );
   $items['menu-test/hidden/menu/manage/%menu/list'] = array(
     'title' => 'List links',
@@ -152,28 +130,24 @@ function menu_test_menu() {
   );
   $items['menu-test/hidden/menu/manage/%menu/add'] = array(
     'title' => 'Add link',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_manage_add',
     'type' => MENU_LOCAL_ACTION,
   );
   $items['menu-test/hidden/menu/manage/%menu/edit'] = array(
     'title' => 'Edit menu',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_manage_edit',
     'type' => MENU_LOCAL_TASK,
     'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
   );
   $items['menu-test/hidden/menu/manage/%menu/delete'] = array(
     'title' => 'Delete menu',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_manage_delete',
   );
 
   // Hidden tests; two dynamic arguments.
   $items['menu-test/hidden/block'] = array(
     'title' => 'Blocks',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_block',
   );
   $items['menu-test/hidden/block/list'] = array(
     'title' => 'List',
@@ -181,14 +155,12 @@ function menu_test_menu() {
   );
   $items['menu-test/hidden/block/add'] = array(
     'title' => 'Add block',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_block_add',
     'type' => MENU_LOCAL_ACTION,
   );
   $items['menu-test/hidden/block/manage/%/%'] = array(
     'title' => 'Configure block',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_block_configure',
   );
   $items['menu-test/hidden/block/manage/%/%/configure'] = array(
     'title' => 'Configure block',
@@ -197,181 +169,111 @@ function menu_test_menu() {
   );
   $items['menu-test/hidden/block/manage/%/%/delete'] = array(
     'title' => 'Delete block',
-    'page callback' => 'test_page_test_page',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.hidden_block_delete',
     'type' => MENU_LOCAL_TASK,
     'context' => MENU_CONTEXT_NONE,
   );
 
   // Breadcrumbs tests.
   // @see \Drupal\system\Tests\Menu\BreadcrumbTest
-  $base = array(
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-  );
   // Local tasks: Second level below default local task.
   $items['menu-test/breadcrumb/tasks'] = array(
     'title' => 'Breadcrumbs test: Local tasks',
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs',
+  );
   $items['menu-test/breadcrumb/tasks/first'] = array(
     'title' => 'First',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs_first',
+  );
   $items['menu-test/breadcrumb/tasks/second'] = array(
     'title' => 'Second',
     'type' => MENU_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs_second',
+  );
   $items['menu-test/breadcrumb/tasks/first/first'] = array(
     'title' => 'First first',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs_first_first',
+  );
   $items['menu-test/breadcrumb/tasks/first/second'] = array(
     'title' => 'First second',
     'type' => MENU_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs_first_second',
+  );
   $items['menu-test/breadcrumb/tasks/second/first'] = array(
     'title' => 'Second first',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs_second_first',
+  );
   $items['menu-test/breadcrumb/tasks/second/second'] = array(
     'title' => 'Second second',
     'type' => MENU_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.breadcrumbs_second_second',
+  );
 
   // Menu local tasks tests.
   // @see \Drupal\system\Tests\Menu\TrailTest
   $items['menu-test/tasks'] = array(
     'title' => 'Local tasks',
-  ) + $base;
+    'route_name' => 'menu_test.tasks',
+  );
   $items['menu-test/tasks/empty'] = array(
     'title' => 'Empty',
-  ) + $base;
+    'route_name' => 'menu_test.tasks_empty',
+  );
   $items['menu-test/tasks/default'] = array(
     'title' => 'Default only',
-  ) + $base;
-  $items['menu-test/tasks/default/view'] = array(
-    'title' => 'View',
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-  ) + $base;
+    'route_name' => 'menu_test.tasks_default',
+  );
   $items['menu-test/tasks/tasks'] = array(
     'title' => 'With tasks',
-  ) + $base;
-  $items['menu-test/tasks/tasks/view'] = array(
-    'title' => 'View',
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-  ) + $base;
-  $items['menu-test/tasks/tasks/edit'] = array(
-    'title' => 'Edit',
-    'type' => MENU_LOCAL_TASK,
-  ) + $base;
-  $items['menu-test/tasks/tasks/settings'] = array(
-    'title' => 'Settings',
-    'type' => MENU_LOCAL_TASK,
-    'weight' => 100,
-  ) + $base;
+    'route_name' => 'menu_test.tasks_tasks',
+  );
 
   // Menu trail tests.
   // @see MenuTrailTestCase
   $items['menu-test/menu-trail'] = array(
     'title' => 'Menu trail - Case 1',
-    'page callback' => 'menu_test_menu_trail_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.menu_trail',
   );
   $items['admin/config/development/menu-trail'] = array(
     'title' => 'Menu trail - Case 2',
     'description' => 'Tests menu_tree_set_path()',
-    'page callback' => 'menu_test_menu_trail_callback',
-    'access arguments' => array('access administration pages'),
+    'route_name' => 'menu_test.menu_trail_admin',
   );
   $items['menu-test/custom-403-page'] = array(
     'title' => 'Custom 403 page',
-    'page callback' => 'menu_test_custom_403_404_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.custom_403',
   );
   $items['menu-test/custom-404-page'] = array(
     'title' => 'Custom 404 page',
-    'page callback' => 'menu_test_custom_403_404_callback',
-    'access arguments' => array('access content'),
+    'route_name' => 'menu_test.custom_404',
   );
 
   // Test the access key.
   $items['menu-title-test/case1'] = array(
-   'title' => 'Example title - Case 1',
-   'access callback' => TRUE,
-   'page callback' => 'menu_test_callback',
+    'title' => 'Example title - Case 1',
+    'route_name' => 'menu_test.title_test_case1',
   );
   $items['menu-title-test/case2'] = array(
-   'title' => 'Example @sub1 - Case @op2',
-   // If '2' is not in quotes, the argument becomes arg(2).
-   'title arguments' => array('@sub1' => 'title', '@op2' => '2'),
-   'access callback' => TRUE,
-   'page callback' => 'menu_test_callback',
+    'title' => 'Example @sub1 - Case @op2',
+    // If '2' is not in quotes, the argument becomes arg(2).
+    'title arguments' => array('@sub1' => 'title', '@op2' => '2'),
+    'route_name' => 'menu_test.title_test_case2',
   );
   $items['menu-title-test/case3'] = array(
-   'title' => 'Example title',
-   'title callback' => 'menu_test_title_callback',
-   'access callback' => TRUE,
-   'page callback' => 'menu_test_callback',
+    'title' => 'Example title',
+    'title callback' => 'menu_test_title_callback',
+    'route_name' => 'menu_test.title_test_case3',
   );
   $items['menu-title-test/case4'] = array(
-   // Title gets completely ignored. Good thing, too.
-   'title' => 'Bike sheds full of blue smurfs',
-   'title callback' => 'menu_test_title_callback',
-   // If '4' is not in quotes, the argument becomes arg(4).
-   'title arguments' => array('Example title', '4'),
-   'access callback' => TRUE,
-   'page callback' => 'menu_test_callback',
-  );
-
-  // Load arguments inheritance test.
-  $items['menu-test/arguments/%menu_test_argument/%'] = array(
-    'title' => 'Load arguments inheritance test',
-    'load arguments' => array(3),
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-  );
-  $items['menu-test/arguments/%menu_test_argument/%/default'] = array(
-    'title' => 'Default local task',
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-  );
-  $items['menu-test/arguments/%menu_test_argument/%/task'] = array(
-    'title' => 'Local task',
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-    'type' => MENU_LOCAL_TASK,
-  );
-  // For this path, load arguments should be inherited for the first loader only.
-  $items['menu-test/arguments/%menu_test_argument/%menu_test_other_argument/common-loader'] = array(
-    'title' => 'Local task',
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-    'type' => MENU_LOCAL_TASK,
-  );
-  // For these paths, no load arguments should be inherited.
-  // Not on the same position.
-  $items['menu-test/arguments/%/%menu_test_argument/different-loaders-1'] = array(
-    'title' => 'An item not sharing the same loader',
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-  );
-  // Not the same loader.
-  $items['menu-test/arguments/%menu_test_other_argument/%/different-loaders-2'] = array(
-    'title' => 'An item not sharing the same loader',
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-  );
-  // Not the same loader.
-  $items['menu-test/arguments/%/%/different-loaders-3'] = array(
-    'title' => 'An item not sharing the same loader',
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
-  );
-  // Explicit load arguments should not be overridden (even if empty).
-  $items['menu-test/arguments/%menu_test_argument/%/explicit-arguments'] = array(
-    'title' => 'An item defining explicit load arguments',
-    'load arguments' => array(),
-    'page callback' => 'menu_test_callback',
-    'access callback' => TRUE,
+    // Title gets completely ignored. Good thing, too.
+    'title' => 'Bike sheds full of blue smurfs',
+    'title callback' => 'menu_test_title_callback',
+    // If '4' is not in quotes, the argument becomes arg(4).
+    'title arguments' => array('Example title', '4'),
+    'route_name' => 'menu_test.title_test_case4',
   );
 
   // Parent page for controller-based local tasks.
@@ -443,11 +345,11 @@ function menu_test_local_action_dynamic_title($arg) {
  * If the menu_test.settings configuration 'tasks.add' has been set, adds
  * several local tasks to menu-test/tasks.
  */
-function menu_test_menu_local_tasks(&$data, $router_item, $root_path) {
+function menu_test_menu_local_tasks(&$data, $route_name) {
   if (!\Drupal::config('menu_test.settings')->get('tasks.add')) {
     return;
   }
-  if (strpos($router_item['tab_root'], 'menu-test/tasks/') === 0) {
+  if (in_array($route_name, array('menu_test.tasks_default', 'menu_test.tasks_empty', 'menu_test.tasks_tasks'))) {
     $data['tabs'][0]['foo'] = array(
       '#theme' => 'menu_local_task',
       '#link' => array(
@@ -473,17 +375,17 @@ function menu_test_menu_local_tasks(&$data, $router_item, $root_path) {
  * If the menu_test.settings configuration 'tasks.alter' has been set, adds
  * several local tasks to menu-test/tasks.
  */
-function menu_test_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+function menu_test_menu_local_tasks_alter(&$data, $route_name) {
   if (!\Drupal::config('menu_test.settings')->get('tasks.alter')) {
     return;
   }
-  if (strpos($router_item['tab_root'], 'menu-test/tasks/') === 0) {
+  if (in_array($route_name, array('menu_test.tasks_default', 'menu_test.tasks_empty', 'menu_test.tasks_tasks'))) {
     // Rename the default local task from 'View' to 'Show'.
     // $data['tabs'] is expected to be keyed by link hrefs.
     // The default local task always links to its parent path, which means that
     // if the tab root path appears as key in $data['tabs'], then that key is
     // the default local task.
-    $key = $router_item['tab_root'];
+    $key = $route_name . '_tab';
     if (isset($data['tabs'][0][$key])) {
       $data['tabs'][0][$key]['#link']['title'] = 'Show it';
     }
@@ -530,6 +432,8 @@ function menu_test_other_argument_load($arg1) {
  *   A string that can be used for comparison.
  *
  * @see menu_test_menu().
+ *
+ * @deprecated Use \Drupal\menu_test\Controller\MenuTestController::menuTestCallback()
  */
 function menu_test_callback() {
   return 'This is menu_test_callback().';
@@ -545,6 +449,8 @@ function menu_test_callback() {
  *   A string that can be used for comparison.
  *
  * @see menu_test_menu().
+ *
+ * @deprecated Use \Drupal\menu_test\Controller\MenuTestController::menuTrail()
  */
 function menu_test_menu_trail_callback() {
   $menu_path = \Drupal::state()->get('menu_test.menu_tree_set_path') ?: array();
@@ -561,6 +467,8 @@ function menu_test_menu_trail_callback() {
  *   A text string that can be used for comparison.
  *
  * @see menu_test_menu().
+ *
+ * @deprecated Use \Drupal\menu_test\Controller\MenuTestController::custom403404()
  */
 function menu_test_custom_403_404_callback() {
   // When requested by one of the TrailTest tests, record the final
@@ -585,6 +493,8 @@ function menu_test_custom_403_404_callback() {
  *   for the current page request.
  *
  * @see menu_test_menu().
+ *
+ * @deprecated Use \Drupal\menu_test\Controller\MenuTestController::themePage()
  */
 function menu_test_theme_page_callback($inherited = FALSE) {
   global $theme_key;
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index fd902531c890..c0155987e8f9 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -1,3 +1,19 @@
+menu_test.menu_name_test:
+  path: '/menu_name_test'
+  defaults:
+    _title: 'Test menu_name router item'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.menu_callback_title:
+  path: '/menu_callback_title'
+  defaults:
+    _title: 'Menu Callback Title'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _permission: 'access content'
+
 menu_test.login_callback:
   path: '/menu_login_callback'
   defaults:
@@ -12,6 +28,30 @@ menu_test.callback_description:
   requirements:
     _permission: 'access content'
 
+menu_test.callback_description_plain:
+  path: 'menu_callback_description/description-plain'
+  defaults:
+    _title: 'Menu item with a regular description'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _permission: 'access content'
+
+menu_test.callback_description_callback:
+  path: 'menu_callback_description/description-callback'
+  defaults:
+    _title: 'Menu item with a description set with a callback'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _permission: 'access content'
+
+menu_test.menu_no_title_callback:
+  path: 'menu_no_title_callback'
+  defaults:
+    _title: 'A title with @placeholder'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _permission: 'access content'
+
 menu_test.router_test1:
   path: '/foo/{bar}'
   defaults:
@@ -166,3 +206,340 @@ menu_test.optional_placeholder:
     placeholder: NULL
   requirements:
     _access: 'TRUE'
+
+menu_test.menu_test_maintain:
+  path: '/menu_test_maintain/{test}'
+  defaults:
+    _title: 'Menu maintain test'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hierarchy_parent:
+  path: '/menu-test/hierarchy/parent'
+  defaults:
+    _title: 'Parent menu router'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hierarchy_parent_child:
+  path: '/menu-test/hierarchy/parent/child'
+  defaults:
+    _title: 'Child menu router'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hierarchy_parent_child2:
+  path: '/menu-test/hierarchy/parent/child2/child'
+  defaults:
+    _title: 'Unattached subchild router'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.menu_test:
+  path: '/menu-test'
+  defaults:
+    _title: 'Menu test root'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden:
+  path: '/menu-test/hidden'
+  defaults:
+    _title: 'Hidden test root'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_menu:
+  path: '/menu-test/hidden/menu'
+  defaults:
+    _title: 'Menus'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_menu_add:
+  path: '/menu-test/hidden/menu/add'
+  defaults:
+    _title: 'Add menu'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_menu_settings:
+  path: '/menu-test/hidden/menu/settings'
+  defaults:
+    _title: 'Settings'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_manage:
+  path: '/menu-test/hidden/menu/manage/{menu}'
+  defaults:
+    _title: 'Customize menu'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_manage_add:
+  path: '/menu-test/hidden/menu/manage/{menu}/add'
+  defaults:
+    _title: 'Add link'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_manage_edit:
+  path: '/menu-test/hidden/menu/manage/{menu}/edit'
+  defaults:
+    _title: 'Edit menu'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_manage_delete:
+  path: '/menu-test/hidden/menu/manage/{menu}/delete'
+  defaults:
+    _title: 'Delete menu'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_block:
+  path: '/menu-test/hidden/block'
+  defaults:
+    _title: 'Blocks'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_block_add:
+  path: '/menu-test/hidden/block/add'
+  defaults:
+    _title: 'Add block'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_block_configure:
+  path: '/menu-test/hidden/block/manage/{foo}/{bar}'
+  defaults:
+    _title: 'Configure block'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.hidden_block_delete:
+  path: '/menu-test/hidden/block/manage/{foo}/{bar}/delete'
+  defaults:
+    _title: 'Configure block'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.breadcrumbs:
+  path: '/menu-test/breadcrumb/tasks'
+  defaults:
+    _title: 'Breadcrumbs test: Local tasks'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.breadcrumbs_first:
+  path: '/menu-test/breadcrumb/tasks/first'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.breadcrumbs_second:
+  path: '/menu-test/breadcrumb/tasks/second'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.breadcrumbs_first_first:
+  path: '/menu-test/breadcrumb/tasks/first/first'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.breadcrumbs_first_second:
+  path: '/menu-test/breadcrumb/tasks/first/second'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.breadcrumbs_second_first:
+  path: '/menu-test/breadcrumb/tasks/second/first'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.breadcrumbs_second_second:
+  path: '/menu-test/breadcrumb/tasks/second/second'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks:
+  path: '/menu-test/tasks'
+  defaults:
+    _title: 'Local tasks'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks_empty:
+  path: '/menu-test/tasks/empty'
+  defaults:
+    _title: 'Empty'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks_default:
+  path: '/menu-test/tasks/default'
+  defaults:
+    _title: 'Default only'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks_tasks:
+  path: '/menu-test/tasks/tasks'
+  defaults:
+    _title: 'With tasks'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks_tasks:
+  path: '/menu-test/tasks/tasks'
+  defaults:
+    _title: 'With tasks'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks_edit:
+  path: '/menu-test/tasks/tasks/edit'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.tasks_settings:
+  path: '/menu-test/tasks/tasks/settings'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.custom_403:
+  path: '/menu-test/custom-403-page'
+  defaults:
+    _title: 'Custom 403 page'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::custom403404'
+  requirements:
+    _permission: 'access content'
+
+menu_test.custom_404:
+  path: '/menu-test/custom-404-page'
+  defaults:
+    _title: 'Custom 404 page'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::custom403404'
+  requirements:
+    _permission: 'access content'
+
+menu_test.menu_trail:
+  path: '/menu-test/menu-trail'
+  defaults:
+    _title: 'Menu trail - Case 1'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTrail'
+  requirements:
+    _permission: 'access content'
+
+menu_test.menu_trail_admin:
+  path: '/admin/config/development/menu-trail'
+  defaults:
+    _title: 'Menu trail - Case 2'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTrail'
+  requirements:
+    _permission: 'access administration pages'
+
+menu_test.theme_callback:
+  path: '/menu-test/theme-callback/{inherited}'
+  defaults:
+    _title: 'Page that displays different themes'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.no_theme_callback:
+  path: '/menu-test/no-theme-callback'
+  defaults:
+    _title: 'Page that displays different themes without using a theme callback.'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
+    inherited: false
+  requirements:
+    _permission: 'access content'
+
+menu_test.exotic_path:
+  # "Special" ASCII characters, characters that look like a percent-escaped
+  # string, and characters from various non-ASCII alphabets.
+  # @todo Find a way to use the correct path.
+  #path: "/menu-test/ -._~!$'\"()*@[]?&+%#,;=:%23%25%26%2B%2F%3Féøïвβ中國書۞"
+  path: '/menu-test/{exotic}'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    exotic: '.+'
+    _permission: 'access content'
+
+menu_test.theme_callback_inheritance:
+  path: '/menu-test/theme-callback/{inherited}/inheritance'
+  defaults:
+    _title: 'Page that tests theme callback inheritance.'
+    _content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
+  requirements:
+    _permission: 'access content'
+
+menu_test.title_test_case1:
+  path: '/menu-title-test/case1'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.title_test_case2:
+  path: '/menu-title-test/case2'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.title_test_case3:
+  path: '/menu-title-test/case3'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
+
+menu_test.title_test_case4:
+  path: '/menu-title-test/case4'
+  defaults:
+    _content: '\Drupal\menu_test\Controller\MenuTestController::menuTestCallback'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Controller/ModuleTestController.php b/core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Controller/ModuleTestController.php
new file mode 100644
index 000000000000..df3b8f7f9589
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test/lib/Drupal/module_test/Controller/ModuleTestController.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\module_test\Controller\ModuleTestController.
+ */
+
+namespace Drupal\module_test\Controller;
+
+/**
+ * Controller routines for module_test routes.
+ */
+class ModuleTestController {
+
+  /**
+   * @todo Remove module_test_hook_dynamic_loading_invoke().
+   */
+  public function hookDynamicLoadingInvoke() {
+    return module_test_hook_dynamic_loading_invoke();
+  }
+
+  /**
+   * @todo Remove module_test_hook_dynamic_loading_invoke_all().
+   */
+  public function hookDynamicLoadingInvokeAll() {
+    return module_test_hook_dynamic_loading_invoke_all();
+  }
+
+  /**
+   * @todo Remove module_test_class_loading().
+   */
+  public function testClassLoading() {
+    return module_test_class_loading();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/module_test/module_test.module b/core/modules/system/tests/modules/module_test/module_test.module
index d859be95c30b..94278b77ec45 100644
--- a/core/modules/system/tests/modules/module_test/module_test.module
+++ b/core/modules/system/tests/modules/module_test/module_test.module
@@ -70,24 +70,15 @@ function module_test_hook_info() {
 function module_test_menu() {
   $items['module-test/hook-dynamic-loading-invoke'] = array(
     'title' => 'Test hook dynamic loading (invoke)',
-    'page callback' => 'module_test_hook_dynamic_loading_invoke',
-    'access arguments' => array('access content'),
+    'route_name' => 'module_test.dynamic_invoke',
   );
   $items['module-test/hook-dynamic-loading-invoke-all'] = array(
     'title' => 'Test hook dynamic loading (invoke_all)',
-    'page callback' => 'module_test_hook_dynamic_loading_invoke_all',
-    'access arguments' => array('access content'),
-  );
-  $items['module-test/hook-dynamic-loading-invoke-all-during-load/%module_test'] = array(
-    'title' => 'Test hook dynamic loading (menu item load)',
-    'page callback' => 'module_test_hook_dynamic_loading_invoke_all_during_load',
-    'page arguments' => array(2),
-    'access arguments' => array('access content'),
+    'route_name' => 'module_test.dynamic_invoke_all',
   );
   $items['module-test/class-loading'] = array(
     'title' => 'Test loading a class from another module',
-    'page callback' => 'module_test_class_loading',
-    'access callback' => TRUE,
+    'route_name' => 'module_test.class_loading',
   );
   return $items;
 }
@@ -97,6 +88,8 @@ function module_test_menu() {
  *
  * If the hook is dynamically loaded correctly, the menu callback should
  * return 'success!'.
+ *
+ * @deprecated \Drupal\module_test\Controller\ModuleTestController::hookDynamicLoadingInvoke()
  */
 function module_test_hook_dynamic_loading_invoke() {
   $result = module_invoke('module_test', 'test_hook');
@@ -108,22 +101,14 @@ function module_test_hook_dynamic_loading_invoke() {
  *
  * If the hook is dynamically loaded correctly, the menu callback should
  * return 'success!'.
+ *
+ * @deprecated \Drupal\module_test\Controller\ModuleTestController::hookDynamicLoadingInvokeAll()
  */
 function module_test_hook_dynamic_loading_invoke_all() {
   $result = \Drupal::moduleHandler()->invokeAll('test_hook');
   return $result['module_test'];
 }
 
-/**
- * Page callback for 'hook dynamic loading' test.
- *
- * If the hook is dynamically loaded correctly, the menu callback should
- * return 'success!'.
- */
-function module_test_hook_dynamic_loading_invoke_all_during_load($param) {
-  return $param;
-}
-
 /**
  * Load function used by module_test_hook_dynamic_loading_invoke_all_during_load().
  *
@@ -141,6 +126,8 @@ function module_test_load($param) {
  * that module is enabled, this function should return the string
  * 'Drupal\\module_autoload_test\\SomeClass::testMethod() was invoked.'. If
  * that module is not enabled, this function should return nothing.
+ *
+ * @deprecated \Drupal\module_test\Controller\ModuleTestController::testClassLoading()
  */
 function module_test_class_loading() {
   if (class_exists('Drupal\module_autoload_test\SomeClass')) {
diff --git a/core/modules/system/tests/modules/module_test/module_test.routing.yml b/core/modules/system/tests/modules/module_test/module_test.routing.yml
new file mode 100644
index 000000000000..450cf09ad8d0
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test/module_test.routing.yml
@@ -0,0 +1,23 @@
+module_test.dynamic_invoke:
+  path: '/module-test/hook-dynamic-loading-invoke'
+  defaults:
+    _title: 'Test hook dynamic loading (invoke)'
+    _content: '\Drupal\module_test\Controller\ModuleTestController::hookDynamicLoadingInvoke'
+  requirements:
+    _permission: 'access content'
+
+module_test.dynamic_invoke_all:
+  path: '/module-test/hook-dynamic-loading-invoke-all'
+  defaults:
+    _title: 'Test hook dynamic loading (invoke_all)'
+    _content: '\Drupal\module_test\Controller\ModuleTestController::hookDynamicLoadingInvokeAll'
+  requirements:
+    _permission: 'access content'
+
+module_test.class_loading:
+  path: '/module-test/class-loading'
+  defaults:
+    _title: 'Test loading a class from another module'
+    _content: '\Drupal\module_test\Controller\ModuleTestController::testClassLoading'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/SystemTestController.php
new file mode 100644
index 000000000000..db4302f15d5a
--- /dev/null
+++ b/core/modules/system/tests/modules/system_test/lib/Drupal/system_test/Controller/SystemTestController.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system_test\Controller\SystemTestController.
+ */
+
+namespace Drupal\system_test\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Controller routines for system_test routes.
+ */
+class SystemTestController extends ControllerBase {
+
+  /**
+   * Tests main content fallback.
+   *
+   * @return string
+   *   The text to display.
+   */
+  public function mainContentFallback() {
+    return $this->t('Content to test main content fallback');
+  }
+
+  /**
+   * @todo Remove system_test_lock_acquire().
+   */
+  public function lockAcquire() {
+    return system_test_lock_acquire();
+  }
+
+  /**
+   * @todo Remove system_test_lock_exit().
+   */
+  public function lockExit() {
+    return system_test_lock_exit();
+  }
+
+  /**
+   * @todo Remove system_test_authorize_init_page().
+   */
+  public function authorizeInit($page_title) {
+    return system_test_authorize_init_page($page_title);
+  }
+
+  /**
+   * @todo Remove as part of https://drupal.org/node/1775842.
+   */
+  public function variableGet() {
+    return variable_get('simpletest_bootstrap_variable_test');
+  }
+
+  /**
+   * @todo Remove system_test_set_header().
+   */
+  public function setHeader() {
+    return system_test_set_header();
+  }
+
+  /**
+   * @todo Remove system_test_page_shutdown_functions().
+   */
+  public function shutdownFunctions($arg1, $arg2) {
+    system_test_page_shutdown_functions($arg1, $arg2);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/system_test/system_test.module b/core/modules/system/tests/modules/system_test/system_test.module
index 56712b846a33..56ae2061c9c4 100644
--- a/core/modules/system/tests/modules/system_test/system_test.module
+++ b/core/modules/system/tests/modules/system_test/system_test.module
@@ -3,84 +3,10 @@
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
- * Implements hook_menu().
+ * Sets a header.
+ *
+ * @deprecated \Drupal\system_test\Controller\SystemTestController::setHeader()
  */
-function system_test_menu() {
-  $items['system-test/auth'] = array(
-    'page callback' => 'system_test_basic_auth_page',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-  $items['system-test/authorize-init/%'] = array(
-    'page callback' => 'system_test_authorize_init_page',
-    'page arguments' => array(2),
-    'access arguments' => array('administer software updates'),
-    'type' => MENU_CALLBACK,
-  );
-  $items['system-test/set-header'] = array(
-    'page callback' => 'system_test_set_header',
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-  );
-  $items['system-test/variable-get'] = array(
-    'title' => 'Variable Get',
-    'page callback' => 'variable_get',
-    'page arguments' => array('simpletest_bootstrap_variable_test', NULL),
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-  );
-
-  $items['system-test/lock-acquire'] = array(
-    'title' => 'Lock acquire',
-    'page callback' => 'system_test_lock_acquire',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  $items['system-test/lock-exit'] = array(
-    'title' => 'Lock acquire then exit',
-    'page callback' => 'system_test_lock_exit',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  $items['system-test/main-content-handling'] = array(
-    'title' => 'Test main content handling',
-    'page callback' => 'system_test_main_content_fallback',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  $items['system-test/main-content-fallback'] = array(
-    'title' => 'Test main content fallback',
-    'page callback' => 'system_test_main_content_fallback',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  $items['system-test/main-content-duplication'] = array(
-    'title' => 'Test main content duplication',
-    'page callback' => 'system_test_main_content_fallback',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  $items['system-test/shutdown-functions'] = array(
-    'title' => 'Test main content duplication',
-    'page callback' => 'system_test_page_shutdown_functions',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  return $items;
-}
-
-function system_test_basic_auth_page() {
-  $output = t('$_SERVER[\'PHP_AUTH_USER\'] is @username.', array('@username' => $_SERVER['PHP_AUTH_USER']));
-  $output .= t('$_SERVER[\'PHP_AUTH_PW\'] is @password.', array('@password' => $_SERVER['PHP_AUTH_PW']));
-  return $output;
-}
-
 function system_test_set_header() {
   drupal_add_http_header($_GET['name'], $_GET['value']);
   return t('The following header was set: %name: %value', array('%name' => $_GET['name'], '%value' => $_GET['value']));
@@ -146,6 +72,8 @@ function system_test_system_info_alter(&$info, $file, $type) {
 
 /**
  * Try to acquire a named lock and report the outcome.
+ *
+ * @deprecated \Drupal\system_test\Controller\SystemTestController::lockAcquire()
  */
 function system_test_lock_acquire() {
   if (lock()->acquire('system_test_lock_acquire')) {
@@ -159,6 +87,8 @@ function system_test_lock_acquire() {
 
 /**
  * Try to acquire a specific lock, and then exit.
+ *
+ * @deprecated \Drupal\system_test\Controller\SystemTestController::lockExit()
  */
 function system_test_lock_exit() {
   if (lock()->acquire('system_test_lock_exit', 900)) {
@@ -175,7 +105,7 @@ function system_test_lock_exit() {
  * Implements hook_page_build().
  */
 function system_test_page_build(&$page) {
-  $menu_item = menu_get_item();
+  $menu_item['path'] = \Drupal::request()->attributes->get('_system_path');
   $main_content_display = &drupal_static('system_main_content_added', FALSE);
 
   if ($menu_item['path'] == 'system-test/main-content-handling') {
@@ -196,15 +126,10 @@ function system_test_page_build(&$page) {
   }
 }
 
-/**
- * Menu callback to test main content fallback().
- */
-function system_test_main_content_fallback() {
-  return t('Content to test main content fallback');
-}
-
 /**
  * A simple page callback which adds a register shutdown function.
+ *
+ * @deprecated \Drupal\system_test\Controller\SystemTestController::shutdownFunctions()
  */
 function system_test_page_shutdown_functions($arg1, $arg2) {
   drupal_register_shutdown_function('_system_test_first_shutdown_function', $arg1, $arg2);
@@ -252,6 +177,8 @@ function system_test_filetransfer_info() {
  * Page callback to initialize authorize.php during testing.
  *
  * @see system_authorized_init().
+ *
+ * @deprecated \Drupal\system_test\Controller\SystemTestController::authorizeInit()
  */
 function system_test_authorize_init_page($page_title) {
   $authorize_url = $GLOBALS['base_url'] . '/core/authorize.php';
diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml
index 74640dbe957a..4c6b56546ecf 100644
--- a/core/modules/system/tests/modules/system_test/system_test.routing.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml
@@ -5,3 +5,71 @@ system_test.page_cache_accept_header:
   requirements:
     _access: 'TRUE'
 
+system_test.main_content_handling:
+  path: '/system-test/main-content-handling'
+  defaults:
+    _title: 'Test main content handling'
+    _content: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback'
+  requirements:
+    _access: 'TRUE'
+
+system_test.main_content_fallback:
+  path: '/system-test/main-content-fallback'
+  defaults:
+    _title: 'Test main content fallback'
+    _content: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback'
+  requirements:
+    _access: 'TRUE'
+
+system_test.main_content_duplication:
+  path: '/system-test/main-content-duplication'
+  defaults:
+    _title: 'Test main content duplication'
+    _content: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback'
+  requirements:
+    _access: 'TRUE'
+
+system_test.lock_acquire:
+  path: '/system-test/lock-acquire'
+  defaults:
+    _title: 'Lock acquire'
+    _content: '\Drupal\system_test\Controller\SystemTestController::lockAcquire'
+  requirements:
+    _access: 'TRUE'
+
+system_test.lock_exit:
+  path: '/system-test/lock-exit'
+  defaults:
+    _title: 'Lock acquire then exit'
+    _content: '\Drupal\system_test\Controller\SystemTestController::lockExit'
+  requirements:
+    _access: 'TRUE'
+
+system_test.authorize_init:
+  path: '/system-test/authorize-init/{page_title}'
+  defaults:
+    _controller: '\Drupal\system_test\Controller\SystemTestController::authorizeInit'
+  requirements:
+    _permission: 'administer software updates'
+
+system_test.variable_get:
+  path: '/system-test/variable-get'
+  defaults:
+    _title: 'Variable Get'
+    _content: '\Drupal\system_test\Controller\SystemTestController::variableGet'
+  requirements:
+    _permission: 'access content'
+
+system_test.set_header:
+  path: '/system-test/set-header'
+  defaults:
+    _content: '\Drupal\system_test\Controller\SystemTestController::setHeader'
+  requirements:
+    _permission: 'access content'
+
+system_test.shutdown_functions:
+  path: '/system-test/shutdown-functions/{arg1}/{arg2}'
+  defaults:
+    _content: '\Drupal\system_test\Controller\SystemTestController::shutdownFunctions'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/test_page_test/lib/Drupal/test_page_test/Controller/TestPageTestController.php b/core/modules/system/tests/modules/test_page_test/lib/Drupal/test_page_test/Controller/TestPageTestController.php
new file mode 100644
index 000000000000..76ac18946037
--- /dev/null
+++ b/core/modules/system/tests/modules/test_page_test/lib/Drupal/test_page_test/Controller/TestPageTestController.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\test_page_test\Controller\TestPageTestController.
+ */
+
+namespace Drupal\test_page_test\Controller;
+
+/**
+ * Controller routines for test_page_test routes.
+ */
+class TestPageTestController {
+
+  /**
+   * @todo Remove test_page_test_page().
+   */
+  public function testPage() {
+    return test_page_test_page();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.module b/core/modules/system/tests/modules/test_page_test/test_page_test.module
index 71a804c9d62b..5911afb1a893 100644
--- a/core/modules/system/tests/modules/test_page_test/test_page_test.module
+++ b/core/modules/system/tests/modules/test_page_test/test_page_test.module
@@ -6,9 +6,7 @@
 function test_page_test_menu() {
   $items['test-page'] = array(
     'title' => 'Test front page',
-    'page callback' => 'test_page_test_page',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
+    'route_name' => 'test_page_test.test_page',
   );
 
   return $items;
@@ -17,10 +15,12 @@ function test_page_test_menu() {
 /**
  * Page callback: Returns a test page and sets the title.
  *
- * @see test_page_test_menu()
+ * @deprecated Use \Drupal\test_page_test\Controller\TestPageTestController::testPage()
  */
 function test_page_test_page() {
   drupal_add_js(array('test-setting' => 'azAZ09();.,\\\/-_{}'), array('type' => 'setting'));
-  drupal_set_title(t('Test page'));
-  return t('Test page text.');
+  return array(
+    '#title' => t('Test page'),
+    '#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 13914811bb09..bf8e126eede2 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
@@ -1,3 +1,11 @@
+test_page_test.test_page:
+  path: '/test-page'
+  defaults:
+    _title: 'Test front page'
+    _content: '\Drupal\test_page_test\Controller\TestPageTestController::testPage'
+  requirements:
+    _access: 'TRUE'
+
 test_page_test.render_title:
   path: "/test-render-title"
   defaults:
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldMultipleVocabularyTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldMultipleVocabularyTest.php
index 5726333ba70b..a0a8a3b593d3 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldMultipleVocabularyTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldMultipleVocabularyTest.php
@@ -92,7 +92,7 @@ function testTaxonomyTermFieldMultipleVocabularies() {
       "{$this->field_name}[]" => array($term1->id(), $term2->id()),
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created.');
 
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php
index b480f0bf4bc3..a908f552f62b 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermFieldTest.php
@@ -111,7 +111,7 @@ function testTaxonomyTermFieldWidgets() {
       $this->field_name => array($term->id()),
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
 
diff --git a/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php b/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php
index 9b330d137435..f209a4491867 100644
--- a/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php
+++ b/core/modules/text/lib/Drupal/text/Tests/TextFieldTest.php
@@ -132,7 +132,7 @@ function _testTextfieldWidgets($field_type, $widget_type) {
       "{$this->field_name}[0][value]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
 
@@ -205,7 +205,7 @@ function _testTextfieldWidgetsFormatted($field_type, $widget_type) {
       "{$this->field_name}[0][value]" => $value,
     );
     $this->drupalPostForm(NULL, $edit, t('Save'));
-    preg_match('|entity_test/manage/(\d+)/edit|', $this->url, $match);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
     $id = $match[1];
     $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created');
 
@@ -237,7 +237,7 @@ function _testTextfieldWidgetsFormatted($field_type, $widget_type) {
 
     // Display edition form.
     // We should now have a 'text format' selector.
-    $this->drupalGet('entity_test/manage/' . $id . '/edit');
+    $this->drupalGet('entity_test/manage/' . $id);
     $this->assertFieldByName("{$this->field_name}[0][value]", NULL, 'Widget is displayed');
     $this->assertFieldByName("{$this->field_name}[0][format]", NULL, 'Format selector is displayed');
 
diff --git a/core/modules/update/tests/modules/update_test/lib/Drupal/update_test/Controller/UpdateTestController.php b/core/modules/update/tests/modules/update_test/lib/Drupal/update_test/Controller/UpdateTestController.php
index 613cfd20e97d..a2b459f7fd1c 100644
--- a/core/modules/update/tests/modules/update_test/lib/Drupal/update_test/Controller/UpdateTestController.php
+++ b/core/modules/update/tests/modules/update_test/lib/Drupal/update_test/Controller/UpdateTestController.php
@@ -28,4 +28,11 @@ public function updateError() {
     return $response;
   }
 
+  /**
+   * @todo Remove update_test_mock_page().
+   */
+  public function updateTest($project_name, $version) {
+    return update_test_mock_page($project_name, $version);
+  }
+
 }
diff --git a/core/modules/update/tests/modules/update_test/update_test.module b/core/modules/update/tests/modules/update_test/update_test.module
index 7898dba3d0bf..249ad83a9ea0 100644
--- a/core/modules/update/tests/modules/update_test/update_test.module
+++ b/core/modules/update/tests/modules/update_test/update_test.module
@@ -17,22 +17,6 @@ function update_test_system_theme_info() {
   return $themes;
 }
 
-/**
- * Implements hook_menu().
- */
-function update_test_menu() {
-  $items = array();
-
-  $items['update-test'] = array(
-    'title' => t('Update test'),
-    'page callback' => 'update_test_mock_page',
-    'access callback' => TRUE,
-    'type' => MENU_CALLBACK,
-  );
-
-  return $items;
-}
-
 /**
  * Implements hook_system_info_alter().
  *
@@ -101,6 +85,8 @@ function update_test_update_status_alter(&$projects) {
  *   content otherwise.
  *
  * @see update_test_menu()
+ *
+ * @deprecated \Drupal\update_test\Controller\UpdateTestController::updateTest()
  */
 function update_test_mock_page($project_name) {
   $xml_map = \Drupal::config('update_test.settings')->get('xml_map');
diff --git a/core/modules/update/tests/modules/update_test/update_test.routing.yml b/core/modules/update/tests/modules/update_test/update_test.routing.yml
index 2aca19506e67..708d42fceaa3 100644
--- a/core/modules/update/tests/modules/update_test/update_test.routing.yml
+++ b/core/modules/update/tests/modules/update_test/update_test.routing.yml
@@ -4,3 +4,12 @@ update_test.503:
     _controller: 'Drupal\update_test\Controller\UpdateTestController::updateError'
   requirements:
     _access: 'TRUE'
+
+update_test.update_test:
+  path: '/update-test/{project_name}/{version}'
+  defaults:
+    _title: 'Update test'
+    _controller: '\Drupal\update_test\Controller\UpdateTestController::updateTest'
+    version: NULL
+  requirements:
+    _access: 'TRUE'
-- 
GitLab