Repository: phoenixframework/phoenix
Pull request #4225 (2021-03-01 03:28:30)

Remove conditionals based on opt_release
Since minimum supported version is Elixir v1.9 / OTP 20


Patch:
diff --git a/README.md b/README.md
index a26c22e2a..a059d387e 100644
--- a/README.md
+++ b/README.md
@@ -25,10 +25,10 @@ You can create a new project using the latest Phoenix source installer (the `phx
 
 1. Remove any previously installed `phx_new` archives so that Mix will pick up the local source code. This can be done with `mix archive.uninstall phx_new` or by simply deleting the file, which is usually in `~/.mix/archives/`.
 2. Copy this repo via `git clone https://github.com/phoenixframework/phoenix` or by downloading it
-3. Run the `phx.new` mix task from within the `installer` directory, for example:
+3. Run the `phx.new` Mix task from within the `installer` directory, for example:
 
 ```bash
-$ cd installer
+$ cd phoenix/installer
 $ mix phx.new dev_app --dev
 ```
 
diff --git a/RELEASE.md b/RELEASE.md
index babdd4ffe..1dc48d1ab 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -15,7 +15,5 @@
   * `CHANGELOG`
   * `mix.exs`
   * `installer/mix.exs`
-  * `installer/README.md`
   * `package.json`
   * `assets/package.json`
-  * `guides/introduction/installation.md`
diff --git a/guides/ecto.md b/guides/ecto.md
index 71846c306..0346f7fc4 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -97,7 +97,7 @@ hello_dev=# \d users
 Table "public.users"
 Column         |            Type             | Modifiers
 ---------------+-----------------------------+----------------------------------------------------
-id             | integer                     | not null default nextval('users_id_seq'::regclass)
+id             | bigint                      | not null default nextval('users_id_seq'::regclass)
 name           | character varying(255)      |
 email          | character varying(255)      |
 bio            | character varying(255)      |
@@ -368,38 +368,63 @@ iex> alias Hello.{Repo, User}
 [Hello.Repo, Hello.User]
 
 iex> Repo.insert(%User{email: "user1@example.com"})
-[debug] QUERY OK db=4.6ms
-INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", {{2017, 5, 23}, {19, 6, 4, 822044}}, {{2017, 5, 23}, {19, 6, 4, 822055}}]
+[debug] QUERY OK db=6.5ms decode=2.0ms queue=0.5ms idle=1358.3ms
+INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", ~N[2021-02-25 01:58:55], ~N[2021-02-25 01:58:55]]
 {:ok,
- %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user1@example.com", id: 3,
-  inserted_at: ~N[2017-05-23 19:06:04.822044], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:04.822055]}}
-
+ %Hello.User{
+   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+   bio: nil,
+   email: "user1@example.com",
+   id: 1,
+   inserted_at: ~N[2021-02-25 01:58:55],
+   name: nil,
+   number_of_pets: nil,
+   updated_at: ~N[2021-02-25 01:58:55]
+ }}
 iex> Repo.insert(%User{email: "user2@example.com"})
-[debug] QUERY OK db=5.1ms
-INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", {{2017, 5, 23}, {19, 6, 8, 452545}}, {{2017, 5, 23}, {19, 6, 8, 452556}}]
+[debug] QUERY OK db=1.3ms idle=1402.7ms
+INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", ~N[2021-02-25 02:03:28], ~N[2021-02-25 02:03:28]]
 {:ok,
- %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user2@example.com", id: 4,
-  inserted_at: ~N[2017-05-23 19:06:08.452545], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:08.452556]}}
+ %Hello.User{
+   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+   bio: nil,
+   email: "user2@example.com",
+   id: 2,
+   inserted_at: ~N[2021-02-25 02:03:28],
+   name: nil,
+   number_of_pets: nil,
+   updated_at: ~N[2021-02-25 02:03:28]
+ }}
 ```
 
 We started by aliasing our `User` and `Repo` modules for easy access. Next, we called `Repo.insert/1` and passed a user struct. Since we're in the `dev` environment, we can see the debug logs for the query our Repo performed when inserting the underlying `%User{}` data. We received a 2-tuple back with `{:ok, %User{}}`, which lets us know the insertion was successful. With a couple of users inserted, let's fetch them back out of the repo.
 
 ```elixir
 iex> Repo.all(User)
-[debug] QUERY OK source="users" db=2.7ms
+[debug] QUERY OK source="users" db=5.8ms queue=1.4ms idle=1672.0ms
 SELECT u0."id", u0."bio", u0."email", u0."name", u0."number_of_pets", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
-[%Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user1@example.com", id: 3,
-  inserted_at: ~N[2017-05-23 19:06:04.822044], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:04.822055]},
- %Hello.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
-  bio: nil, email: "user2@example.com", id: 4,
-  inserted_at: ~N[2017-05-23 19:06:08.452545], name: nil, number_of_pets: nil,
-  updated_at: ~N[2017-05-23 19:06:08.452556]}]
+[
+  %Hello.User{
+    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+    bio: nil,
+    email: "user1@example.com",
+    id: 1,
+    inserted_at: ~N[2021-02-25 01:58:55],
+    name: nil,
+    number_of_pets: nil,
+    updated_at: ~N[2021-02-25 01:58:55]
+  },
+  %Hello.User{
+    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
+    bio: nil,
+    email: "user2@example.com",
+    id: 2,
+    inserted_at: ~N[2021-02-25 02:03:28],
+    name: nil,
+    number_of_pets: nil,
+    updated_at: ~N[2021-02-25 02:03:28]
+  }
+]
 ```
 
 That was easy! `Repo.all/1` takes a data source, our `User` schema in this case, and translates that to an underlying SQL query against our database. After it fetches the data, the Repo then uses our Ecto schema to map the database values back into Elixir data-structures according to our `User` schema. We're not just limited to basic querying – Ecto includes a full-fledged query DSL for advanced SQL generation. In addition to a natural Elixir DSL, Ecto's query engine gives us multiple great features, such as SQL injection protection and compile-time optimization of queries. Let's try it out.
@@ -409,7 +434,7 @@ iex> import Ecto.Query
 Ecto.Query
 
 iex> Repo.all(from u in User, select: u.email)
-[debug] QUERY OK source="users" db=2.4ms
+[debug] QUERY OK source="users" db=0.8ms queue=0.9ms idle=1634.0ms
 SELECT u0."email" FROM "users" AS u0 []
 ["user1@example.com", "user2@example.com"]
 ```
diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md
index 1853b0072..5333ccefe 100644
--- a/guides/introduction/installation.md
+++ b/guides/introduction/installation.md
@@ -43,7 +43,7 @@ Elixir 1.6.3
 Once we have Elixir and Erlang, we are ready to install the Phoenix application generator:
 
 ```console
-$ mix archive.install hex phx_new 1.5.0
+$ mix archive.install hex phx_new
 ```
 
 The `phx.new` generator is now available to generate new applications in the next guide, called [Up and Running](up_and_running.html). The flags mentioned below are command line options to the generator; see all available options by calling `mix help phx.new`.
diff --git a/guides/plug.md b/guides/plug.md
index abcab2003..0403c2360 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -113,7 +113,7 @@ defmodule HelloWeb.Router do
   ...
 ```
 
-In the [`init/1`] callback, we pass a default locale to use if none is present in the params. We also use pattern matching to define multiple [`call/2`] function heads to validate the locale in the params, and fall back to `"en"` if there is no match.
+In the [`init/1`] callback, we pass a default locale to use if none is present in the params. We also use pattern matching to define multiple [`call/2`] function heads to validate the locale in the params, and fall back to `"en"` if there is no match. The [`assign/3`] is a part of the `Plug.Conn` module and it's how we store values in the `conn` data structure.
 
 To see the assign in action, go to the layout in `lib/hello_web/templates/layout/app.html.eex` and add the following close to the main container:
 
diff --git a/installer/README.md b/installer/README.md
index f155fda50..0f3f53819 100644
--- a/installer/README.md
+++ b/installer/README.md
@@ -2,9 +2,9 @@
 
 Provides `phx.new` installer as an archive.
 
-To install from hex, run:
+To install from Hex, run:
 
-    $ mix archive.install hex phx_new 1.5.0
+    $ mix archive.install hex phx_new
 
 To build and install it locally,
 ensure any previous archive versions are removed:
diff --git a/installer/mix.exs b/installer/mix.exs
index 2a0d1e7b3..ce748dcb4 100644
--- a/installer/mix.exs
+++ b/installer/mix.exs
@@ -1,4 +1,4 @@
-for path <- :code.get_path,
+for path <- :code.get_path(),
     Regex.match?(~r/phx_new\-\d+\.\d+\.\d\/ebin$/, List.to_string(path)) do
   Code.delete_path(path)
 end
@@ -6,7 +6,7 @@ end
 defmodule Phx.New.MixProject do
   use Mix.Project
 
-  @version "1.5.0"
+  @version "1.6.0-dev"
   @github_path "phoenixframework/phoenix"
   @url "https://github.com/#{@github_path}"
 
diff --git a/installer/templates/phx_single/gitignore b/installer/templates/phx_single/gitignore
index 6c634d280..8eae1ec4d 100644
--- a/installer/templates/phx_single/gitignore
+++ b/installer/templates/phx_single/gitignore
@@ -32,3 +32,6 @@ npm-debug.log
 # we ignore priv/static. You may want to comment
 # this depending on your deployment strategy.
 /priv/static/
+
+# Temporary files, for example, from tests.
+/tmp
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index d2c2bbdcf..4baecafa7 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -42,7 +42,7 @@ defmodule <%= @app_module %>.MixProject do
       {:ecto_sql, "~> 3.5"},
       {<%= inspect @adapter_app %>, ">= 0.0.0"},<% end %><%= if @html do %><%= if @live do %>
       {:phoenix_live_view, "~> 0.15.0"},
-      {:floki, ">= 0.27.0", only: :test},<% end %>
+      {:floki, ">= 0.30.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if @dashboard do %>
       {:phoenix_live_dashboard, "~> 0.4"},<% end %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
index ee8416b15..b709f43ed 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -40,7 +40,7 @@ defmodule <%= @web_namespace %>.MixProject do
       <%= @phoenix_dep %>,<%= if @ecto do %>
       {:phoenix_ecto, "~> 4.1"},<% end %><%= if @html do %><%= if @live do %>
       {:phoenix_live_view, "~> 0.15.0"},
-      {:floki, ">= 0.27.0", only: :test},<% end %>
+      {:floki, ">= 0.30.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if @dashboard do %>
       {:phoenix_live_dashboard, "~> 0.4"},<% end %>
diff --git a/installer/templates/phx_umbrella/gitignore b/installer/templates/phx_umbrella/gitignore
index 12179ea3c..ffa1e1923 100644
--- a/installer/templates/phx_umbrella/gitignore
+++ b/installer/templates/phx_umbrella/gitignore
@@ -18,3 +18,6 @@ erl_crash.dump
 
 # Also ignore archive artifacts (built via "mix archive.build").
 *.ez
+
+# Temporary files, for example, from tests.
+/tmp
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index a77c775bb..cadc130e6 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -245,7 +245,7 @@ defmodule Mix.Phoenix.Schema do
     if schema.params.create |> Map.values() |> Enum.any?(&date_value?/1) do
       "is invalid"
     else
-      "can&apos;t be blank"
+      "can&#39;t be blank"
     end
   end
 
diff --git a/lib/phoenix/config.ex b/lib/phoenix/config.ex
index 685755b82..6927f91bb 100644
--- a/lib/phoenix/config.ex
+++ b/lib/phoenix/config.ex
@@ -46,7 +46,18 @@ defmodule Phoenix.Config do
   """
   @spec cache(module, term, (module -> {:cache | :nocache, term})) :: term
   def cache(module, key, fun) do
-    case :ets.lookup(module, key) do
+    try do
+      :ets.lookup(module, key)
+    rescue
+      e ->
+        case :ets.info(module) do
+          :undefined ->
+            raise "could not find ets table for endpoint #{inspect(module)}. Make sure your endpoint is started and note you cannot access endpoint functions at compile-time."
+
+          _ ->
+            reraise e, __STACKTRACE__
+        end
+    else
       [{^key, :cache, val}] ->
         val
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index ec1158f63..49ee33ffc 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -218,7 +218,7 @@ defmodule Phoenix.Endpoint do
       `c:broadcast_from/4`, `c:broadcast_from!/4`, `c:local_broadcast/3`,
       and `c:local_broadcast_from/4`
 
-    * for configuration: `c:start_link/0`, `c:config/2`, and `c:config_change/2`
+    * for configuration: `c:start_link/1`, `c:config/2`, and `c:config_change/2`
 
     * as required by the `Plug` behaviour: `c:Plug.init/1` and `c:Plug.call/2`
 
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 2ec096cfb..2402c49b5 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -346,6 +346,9 @@ defmodule Phoenix.Router do
 
     case pipeline.(conn) do
       %Plug.Conn{halted: true} = halted_conn ->
+        measurements = %{duration: System.monotonic_time() - start}
+        metadata = %{metadata | conn: halted_conn}
+        :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
         halted_conn
       %Plug.Conn{} = piped_conn ->
         try do
@@ -408,12 +411,6 @@ defmodule Phoenix.Router do
     end
   end
 
-  @anno (if :erlang.system_info(:otp_release) >= '19' do
-    [generated: true]
-  else
-    [line: -1]
-  end)
-
   @doc false
   defmacro __before_compile__(env) do
     routes = env.module |> Module.get_attribute(:phoenix_routes) |> Enum.reverse
@@ -431,9 +428,8 @@ defmodule Phoenix.Router do
         end
       end
 
-    # @anno is used here to avoid warnings if forwarding to root path
     match_404 =
-      quote @anno do
+      quote [generated: true] do
         def __match_route__(_method, _path_info, _host) do
           :error
         end
@@ -628,6 +624,14 @@ defmodule Phoenix.Router do
   are appended to the ones previously given.
   """
   defmacro pipeline(plug, do: block) do
+    with true <- is_atom(plug),
+         imports = __CALLER__.macros ++ __CALLER__.functions,
+         {mod, _} <- Enum.find(imports, fn {_, imports} -> {plug, 2} in imports end) do
+      raise ArgumentError,
+            "cannot define pipeline named #{inspect(plug)} " <>
+              "because there is an import from #{inspect(mod)} with the same name"
+    end
+
     block =
       quote do
         plug = unquote(plug)
diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex
index d0cc374a0..1fdd30d02 100644
--- a/lib/phoenix/router/helpers.ex
+++ b/lib/phoenix/router/helpers.ex
@@ -5,12 +5,6 @@ defmodule Phoenix.Router.Helpers do
   alias Phoenix.Router.Route
   alias Plug.Conn
 
-  @anno (if :erlang.system_info(:otp_release) >= '19' do
-    [generated: true, unquote: false]
-  else
-    [line: -1, unquote: false]
-  end)
-
   @doc """
   Callback invoked by the url generated in each helper module.
   """
@@ -119,7 +113,7 @@ defmodule Phoenix.Router.Helpers do
 
     catch_all = Enum.map(groups, &defhelper_catch_all/1)
 
-    defhelper = quote @anno do
+    defhelper = quote [generated: true, unquote: false] do
       defhelper = fn helper, vars, opts, bins, segs, trailing_slash? ->
         def unquote(:"#{helper}_path")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars)) do
           unquote(:"#{helper}_path")(conn_or_endpoint, unquote(Macro.escape(opts)), unquote_splicing(vars), [])
@@ -142,7 +136,7 @@ defmodule Phoenix.Router.Helpers do
       end
     end
 
-    defcatch_all = quote @anno do
+    defcatch_all = quote [generated: true, unquote: false] do
       defcatch_all = fn helper, lengths, routes ->
 	      for length <- lengths do
 	        binding = List.duplicate({:_, [], nil}, length)
diff --git a/priv/templates/phx.gen.html/show.html.eex b/priv/templates/phx.gen.html/show.html.eex
index c19a2c955..469fc4a13 100644
--- a/priv/templates/phx.gen.html/show.html.eex
+++ b/priv/templates/phx.gen.html/show.html.eex
@@ -9,5 +9,5 @@
 <% end %>
 </ul>
 
-<span><%%= link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>) %></span>
+<span><%%= link "Edit", to: Routes.<%= schema.route_helper %>_path(@conn, :edit, @<%= schema.singular %>) %></span> |
 <span><%%= link "Back", to: Routes.<%= schema.route_helper %>_path(@conn, :index) %></span>
diff --git a/priv/templates/phx.gen.live/show.html.leex b/priv/templates/phx.gen.live/show.html.leex
index 236f80e45..9de080b77 100644
--- a/priv/templates/phx.gen.live/show.html.leex
+++ b/priv/templates/phx.gen.live/show.html.leex
@@ -18,5 +18,5 @@
 <% end %>
 </ul>
 
-<span><%%= live_patch "Edit", to: Routes.<%= schema.route_helper %>_show_path(@socket, :edit, @<%= schema.singular %>), class: "button" %></span>
+<span><%%= live_patch "Edit", to: Routes.<%= schema.route_helper %>_show_path(@socket, :edit, @<%= schema.singular %>), class: "button" %></span> |
 <span><%%= live_redirect "Back", to: Routes.<%= schema.route_helper %>_index_path(@socket, :index) %></span>


Test patch:
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 655a14e5c..09134b8d4 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -34,7 +34,7 @@ defmodule Phoenix.Integration.MixProject do
       {:myxql, ">= 0.0.0"},
       {:tds, ">= 0.0.0"},
       {:phoenix_live_view, "~> 0.15.0"},
-      {:floki, ">= 0.27.0"},
+      {:floki, ">= 0.30.0"},
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2"},
       {:phoenix_live_dashboard, "~> 0.4"},
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index bc6f6e153..c06488281 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -12,9 +12,9 @@
   "ecto_sql": {:hex, :ecto_sql, "3.5.4", "a9e292c40bd79fff88885f95f1ecd7b2516e09aa99c7dd0201aa84c54d2358e4", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1fff1a28a898d7bbef263f1f3ea425b04ba9f33816d843238c84eff883347343"},
   "elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
   "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
-  "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"},
+  "floki": {:hex, :floki, "0.30.0", "22ebbe681a5d3777cdd830ca091b1b806d33c3449c26312eadca7f7be685c0c8", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "a9e128a4ca9bb71f11affa315b6768a9ad326d5996ff1e92acf1d7a01a10076a"},
   "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
-  "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
+  "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
   "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
   "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
   "myxql": {:hex, :myxql, "0.4.5", "49784e6a3e4fc33088cc9004948ef255ee698b0d7b533fb1fa453cc99a3f9972", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.3", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "40a6166ab0a54f44a6e2c437aed6360ce51ce7f779557ae30d1cc4c4b4e7ad13"},
diff --git a/test/mix/tasks/phx.gen.cert_test.exs b/test/mix/tasks/phx.gen.cert_test.exs
index 188b2aa63..7e8c66090 100644
--- a/test/mix/tasks/phx.gen.cert_test.exs
+++ b/test/mix/tasks/phx.gen.cert_test.exs
@@ -3,67 +3,62 @@ Code.require_file("../../../installer/test/mix_helper.exs", __DIR__)
 defmodule Mix.Tasks.Phx.CertTest do
   use ExUnit.Case
 
-  @otp_release :erlang.system_info(:otp_release) |> List.to_integer()
+  import MixHelper
+  alias Mix.Tasks.Phx.Gen
 
-  if @otp_release >= 20 do
-    import MixHelper
-    alias Mix.Tasks.Phx.Gen
+  @timeout 5_000
 
-    @timeout 5_000
+  test "write certificate and key files" do
+    in_tmp("mix_phx_gen_cert", fn ->
+      Gen.Cert.run([])
 
-    # RSA key generation requires OTP 20 or later
-    test "write certificate and key files" do
-      in_tmp("mix_phx_gen_cert", fn ->
-        Gen.Cert.run([])
+      assert_received {:mix_shell, :info, ["* creating priv/cert/selfsigned_key.pem"]}
+      assert_received {:mix_shell, :info, ["* creating priv/cert/selfsigned.pem"]}
 
-        assert_received {:mix_shell, :info, ["* creating priv/cert/selfsigned_key.pem"]}
-        assert_received {:mix_shell, :info, ["* creating priv/cert/selfsigned.pem"]}
-
-        assert_file("priv/cert/selfsigned_key.pem", "-----BEGIN RSA PRIVATE KEY-----")
-        assert_file("priv/cert/selfsigned.pem", "-----BEGIN CERTIFICATE-----")
-      end)
-    end
+      assert_file("priv/cert/selfsigned_key.pem", "-----BEGIN RSA PRIVATE KEY-----")
+      assert_file("priv/cert/selfsigned.pem", "-----BEGIN CERTIFICATE-----")
+    end)
+  end
 
-    test "write certificate and key with custom filename" do
-      in_tmp("mix_phx_gen_cert", fn ->
-        Gen.Cert.run(["-o", "priv/cert/localhost"])
+  test "write certificate and key with custom filename" do
+    in_tmp("mix_phx_gen_cert", fn ->
+      Gen.Cert.run(["-o", "priv/cert/localhost"])
 
-        assert_received {:mix_shell, :info, ["* creating priv/cert/localhost_key.pem"]}
-        assert_received {:mix_shell, :info, ["* creating priv/cert/localhost.pem"]}
+      assert_received {:mix_shell, :info, ["* creating priv/cert/localhost_key.pem"]}
+      assert_received {:mix_shell, :info, ["* creating priv/cert/localhost.pem"]}
 
-        assert_file("priv/cert/localhost_key.pem", "-----BEGIN RSA PRIVATE KEY-----")
-        assert_file("priv/cert/localhost.pem", "-----BEGIN CERTIFICATE-----")
-      end)
-    end
-
-    test "TLS connection with generated certificate and key" do
-      Application.ensure_all_started(:ssl)
+      assert_file("priv/cert/localhost_key.pem", "-----BEGIN RSA PRIVATE KEY-----")
+      assert_file("priv/cert/localhost.pem", "-----BEGIN CERTIFICATE-----")
+    end)
+  end
 
-      in_tmp("mix_phx_gen_cert", fn ->
-        Gen.Cert.run([])
+  test "TLS connection with generated certificate and key" do
+    Application.ensure_all_started(:ssl)
 
-        assert {:ok, server} =
-                 :ssl.listen(
-                   0,
-                   certfile: "priv/cert/selfsigned.pem",
-                   keyfile: "priv/cert/selfsigned_key.pem"
-                 )
+    in_tmp("mix_phx_gen_cert", fn ->
+      Gen.Cert.run([])
 
-        {:ok, {_, port}} = :ssl.sockname(server)
+      assert {:ok, server} =
+               :ssl.listen(
+                 0,
+                 certfile: "priv/cert/selfsigned.pem",
+                 keyfile: "priv/cert/selfsigned_key.pem"
+               )
 
-        spawn_link(fn ->
-          with {:ok, conn} <- :ssl.transport_accept(server, @timeout),
-               :ok <- :ssl.handshake(conn, @timeout) do
-            :ssl.close(conn)
-          end
-        end)
+      {:ok, {_, port}} = :ssl.sockname(server)
 
-        # We don't actually verify the server cert contents, we just check that
-        # the client and server are able to complete the TLS handshake
-        assert {:ok, client} = :ssl.connect('localhost', port, [], @timeout)
-        :ssl.close(client)
-        :ssl.close(server)
+      spawn_link(fn ->
+        with {:ok, conn} <- :ssl.transport_accept(server, @timeout),
+             :ok <- :ssl.handshake(conn, @timeout) do
+          :ssl.close(conn)
+        end
       end)
-    end
+
+      # We don't actually verify the server cert contents, we just check that
+      # the client and server are able to complete the TLS handshake
+      assert {:ok, client} = :ssl.connect('localhost', port, [], @timeout)
+      :ssl.close(client)
+      :ssl.close(server)
+    end)
   end
 end
diff --git a/test/phoenix/config_test.exs b/test/phoenix/config_test.exs
index ad69c5d5a..6c0156d49 100644
--- a/test/phoenix/config_test.exs
+++ b/test/phoenix/config_test.exs
@@ -22,6 +22,12 @@ defmodule Phoenix.ConfigTest do
     assert :ets.lookup(meta.test, :custom)  == [custom: true]
   end
 
+  test "raises with warning about compile time when table not started" do
+    assert_raise RuntimeError,
+                 "could not find ets table for endpoint Fooz. Make sure your endpoint is started and note you cannot access endpoint functions at compile-time.",
+                 fn -> cache(Fooz, :foo, fn _ -> {:nocache, :bar} end) end
+  end
+
   test "can change configuration", meta do
     {:ok, pid} = start_link({meta.test, @all, @defaults, []})
     ref = Process.monitor(pid)
diff --git a/test/phoenix/router/pipeline_test.exs b/test/phoenix/router/pipeline_test.exs
index 053838686..2838c2b2c 100644
--- a/test/phoenix/router/pipeline_test.exs
+++ b/test/phoenix/router/pipeline_test.exs
@@ -158,4 +158,19 @@ defmodule Phoenix.Router.PipelineTest do
       end
     end
   end
+
+  test "pipeline raises on conflict" do
+    assert_raise ArgumentError, ~r{there is an import from Kernel with the same nam}, fn ->
+      defmodule ConflictingPipeline do
+        use Phoenix.Router, otp_app: :phoenix
+        pipeline :raise do
+          plug Plug.Head
+        end
+        scope "/" do
+          pipe_through [:raise]
+          get "/", UnknownController, :index
+        end
+      end
+    end
+  end
 end
diff --git a/test/phoenix/router/routing_test.exs b/test/phoenix/router/routing_test.exs
index c5d1b5933..8dc01c095 100644
--- a/test/phoenix/router/routing_test.exs
+++ b/test/phoenix/router/routing_test.exs
@@ -23,6 +23,11 @@ defmodule Phoenix.Router.RoutingTest do
     def any(conn, _params), do: text(conn, "users any")
     def raise(_conn, _params), do: raise("boom")
     def exit(_conn, _params), do: exit(:boom)
+    def halt(conn, _params) do
+      conn
+      |> send_resp(401, "Unauthorized")
+      |> halt()
+    end
   end
 
   defmodule LogLevel do
@@ -44,6 +49,7 @@ defmodule Phoenix.Router.RoutingTest do
     get "/backups/*path", UserController, :image
     get "/static/images/icons/*image", UserController, :image
     get "/exit", UserController, :exit
+    get "/halt-controller", UserController, :halt
 
     trace("/trace", UserController, :trace)
     options "/options", UserController, :options
@@ -60,9 +66,18 @@ defmodule Phoenix.Router.RoutingTest do
     get "/no_log", SomePlug, [], log: false
     get "/fun_log", SomePlug, [], log: {LogLevel, :log_level, []}
     get "/users/:user_id/files/:id", UserController, :image
+
+    scope "/halt-plug" do
+      pipe_through :halt
+      get "/", UserController, :raise
+    end
+
     get "/*path", UserController, :not_found
 
     defp noop(conn, _), do: conn
+    defp halt(conn, _) do
+      conn |> Plug.Conn.send_resp(401, "Unauthorized") |> halt()
+    end
   end
 
   setup do
@@ -274,9 +289,47 @@ defmodule Phoenix.Router.RoutingTest do
 
     test "phoenix.router_dispatch.start and .stop are emitted on success" do
       call(Router, :get, "users/123")
-      assert_received {:telemetry_event, @router_start_event, _event}
-      assert_received {:telemetry_event, @router_stop_event, _event}
-      refute_received {:telemetry_event, @router_exception_event, _event}
+
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/users/:id"}, _}}
+
+      assert_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/users/:id"}, _}}
+
+      refute_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/users/:id"}, _}}
+    end
+
+    test "phoenix.router_dispatch.start and .stop are emitted when conn halted in router" do
+      conn = call(Router, :get, "/halt-plug")
+
+      assert conn.halted
+      assert conn.status == 401
+
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/halt-plug"}, _}}
+
+      assert_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/halt-plug"}, _}}
+
+      refute_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/halt-plug"}, _}}
+    end
+
+    test "phoenix.router_dispatch.start and .stop are emitted when conn is halted in controller" do
+      conn = call(Router, :get, "/halt-controller")
+
+      assert conn.halted
+      assert conn.status == 401
+
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/halt-controller"}, _}}
+
+      assert_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/halt-controller"}, _}}
+
+      refute_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/halt-controller"}, _}}
     end
 
     test "phoenix.router_dispatch.start and .exception are emitted on crash" do
@@ -284,23 +337,35 @@ defmodule Phoenix.Router.RoutingTest do
         call(Router, :get, "route_that_crashes")
       end
 
-      assert_received {:telemetry_event, @router_start_event, _event}
-      assert_received {:telemetry_event, @router_exception_event, _event}
-      refute_received {:telemetry_event, @router_stop_event, _event}
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/route_that_crashes"}, _}}
+
+      assert_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/route_that_crashes"}, _}}
+
+      refute_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/route_that_crashes"}, _}}
     end
 
     test "phoenix.router_dispatch.start and .exception are emitted on exit" do
       catch_exit(call(Router, :get, "exit"))
 
-      assert_received {:telemetry_event, @router_start_event, _event}
-      assert_received {:telemetry_event, @router_exception_event, _event}
-      refute_received {:telemetry_event, @router_stop_event, _event}
+      assert_received {:telemetry_event, @router_start_event,
+                       {_, %{route: "/exit"}, _}}
+
+      assert_received {:telemetry_event, @router_exception_event,
+                       {_, %{route: "/exit"}, _}}
+
+      refute_received {:telemetry_event, @router_stop_event,
+                       {_, %{route: "/exit"}, _}}
     end
 
     test "phoenix.router_dispatch.start has supported measurements and metadata" do
       call(Router, :get, "users/123")
 
-      assert_received {:telemetry_event, @router_start_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_start_event,
+                       {measures, %{route: "/users/:id"} = meta, _config}}
+
       assert is_integer(measures.system_time)
 
       assert %{
@@ -318,7 +383,9 @@ defmodule Phoenix.Router.RoutingTest do
     test "phoenix.router_dispatch.stop has supported measurements and metadata" do
       call(Router, :get, "users/123")
 
-      assert_received {:telemetry_event, @router_stop_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_stop_event,
+                       {measures, %{route: "/users/:id"} = meta, _config}}
+
       assert is_integer(measures.duration)
 
       assert %{
@@ -338,7 +405,9 @@ defmodule Phoenix.Router.RoutingTest do
         call(Router, :get, "users/123/raise")
       end
 
-      assert_received {:telemetry_event, @router_exception_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_exception_event,
+                       {measures, %{route: "/users/:id/raise"} = meta, _config}}
+
       assert is_integer(measures.duration)
 
       assert %{
@@ -366,7 +435,9 @@ defmodule Phoenix.Router.RoutingTest do
     test "phoenix.router_dispatch.exception has supported measurements and metadata on exit" do
       catch_exit(call(Router, :get, "exit"))
 
-      assert_received {:telemetry_event, @router_exception_event, {measures, meta, _config}}
+      assert_received {:telemetry_event, @router_exception_event,
+                       {measures, %{route: "/exit"} = meta, _config}}
+
       assert is_integer(measures.duration)
 
       assert %{