Repository: phoenixframework/phoenix
Pull request #3995 (2020-09-18 16:31:15)

Correct a grammar mistake ("Their" -> "The")

adamjcooper: Hello,

I'm new to Phoenix and to contributing to this repository. I see one of the CI test runs failed. I suspect it is unrelated to my documentation correction, but please let me know if there is something else I should do.

Thanks for Phoenix!

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b48cfd2e4..a22513428 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,6 +32,37 @@ jobs:
         working-directory: installer
       # - run: MIX_ENV=docs mix inch.report
 
+  integration-test-elixir:
+    runs-on: ubuntu-latest
+    container:
+      image: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-alpine-3.12.0
+      env:
+        ELIXIR_ASSERT_TIMEOUT: 2000
+
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          # Can't run integration tests before OTP 20.2 due to incompatiblity with cowlib.
+          #
+          # See https://github.com/phoenixframework/phoenix/issues/3903
+          - elixir: 1.9.4
+            otp: 21.2.7
+          - elixir: 1.10.4
+            otp: 21.3.8.17
+          - elixir: 1.10.4
+            otp: 23.0.3
+    steps:
+      - uses: actions/checkout@v2.3.1
+      - name: Install Dependencies
+        run: |
+          mix local.rebar --force
+          mix local.hex --force
+          apk add --no-progress --update git
+          mix deps.get
+      - name: Run integration tests
+        run: mix test.integration
+
   npm_test:
     runs-on: ubuntu-latest
     steps:
diff --git a/guides/routing.md b/guides/routing.md
index bea13811e..d4d16e82a 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -142,7 +142,7 @@ The `Phoenix.Router.resources/4` macro describes additional options for customiz
 
 ## Path Helpers
 
-Path helpers are functions which are dynamically defined on the `Router.Helpers` module for an individual application. For us, that is `HelloWeb.Router.Helpers`. Their name of each path helper is derived from the name of the controller used in the route definition. Our controller is `HelloWeb.PageController`, and `page_path` is the function which will return the path to the root of our application.
+Path helpers are functions which are dynamically defined on the `Router.Helpers` module for an individual application. For us, that is `HelloWeb.Router.Helpers`. The name of each path helper is derived from the name of the controller used in the route definition. Our controller is `HelloWeb.PageController`, and `page_path` is the function which will return the path to the root of our application.
 
 That's a mouthful. Let's see it in action. Run `iex -S mix` at the root of the project. When we call the `page_path` function on our router helpers with the `Endpoint` or connection and action as arguments, it returns the path to us.
 
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index 6d7c5f7b1..191d30481 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -62,6 +62,12 @@ defmodule Mix.Tasks.Phx.New do
   `phx.gen.html` will no longer work, as important HTML components
   will be missing.
 
+  ## Installation
+
+  `mix phx.new` by default prompts you to fetch and install your
+  dependencies. You can enable this behaviour by passing the
+  `--install` flag or disable it with the `--no-install` flag.
+
   ## Examples
 
       mix phx.new hello_world
@@ -102,7 +108,7 @@ defmodule Mix.Tasks.Phx.New do
              app: :string, module: :string, web_module: :string,
              database: :string, binary_id: :boolean, html: :boolean,
              gettext: :boolean, umbrella: :boolean, verbose: :boolean,
-             live: :boolean, dashboard: :boolean]
+             live: :boolean, dashboard: :boolean, install: :boolean]
 
   def run([version]) when version in ~w(-v --version) do
     Mix.shell().info("Phoenix v#{@version}")
@@ -149,7 +155,12 @@ defmodule Mix.Tasks.Phx.New do
 
   defp prompt_to_install_deps(%Project{} = project, generator, path_key) do
     path = Map.fetch!(project, path_key)
-    install? = Mix.shell().yes?("\nFetch and install dependencies?")
+
+    install? =
+      Keyword.get_lazy(project.opts, :install, fn ->
+        Mix.shell().yes?("\nFetch and install dependencies?")
+      end)
+
     cd_step = ["$ cd #{relative_app_path(path)}"]
 
     maybe_cd(path, fn ->
diff --git a/installer/mix.lock b/installer/mix.lock
index 5ffe12033..e245db2ec 100644
--- a/installer/mix.lock
+++ b/installer/mix.lock
@@ -1,7 +1,8 @@
 %{
-  "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm", "5e8806285d8a3a8999bd38e4a73c58d28534c856bc38c44818e5ba85bbda16fb"},
+  "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"},
+  "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
   "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0e11d67e662142fc3945b0ee410c73c8c956717fbeae4ad954b418747c734973"},
-  "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
-  "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
-  "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm", "51aa192e0941313c394956718bdb1e59325874f88f45871cff90345b97f60bba"},
+  "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
+  "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
+  "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
 }
diff --git a/installer/templates/phx_web/telemetry.ex b/installer/templates/phx_web/telemetry.ex
index bdeac0af0..21385f520 100644
--- a/installer/templates/phx_web/telemetry.ex
+++ b/installer/templates/phx_web/telemetry.ex
@@ -38,7 +38,7 @@ defmodule <%= web_namespace %>.Telemetry do
       summary("<%= app_name %>.repo.query.decode_time",
         unit: {:native, :millisecond},
         description: "The time spent decoding the data received from the database"
-      ), 
+      ),
       summary("<%= app_name %>.repo.query.query_time",
         unit: {:native, :millisecond},
         description: "The time spent executing the query"
@@ -49,7 +49,8 @@ defmodule <%= web_namespace %>.Telemetry do
       ),
       summary("<%= app_name %>.repo.query.idle_time",
         unit: {:native, :millisecond},
-        description: "The time the connection spent waiting before being checked out for the query"
+        description:
+          "The time the connection spent waiting before being checked out for the query"
       ),<% end %>
 
       # VM Metrics
diff --git a/mix.exs b/mix.exs
index d3ed848b7..655a2cc81 100644
--- a/mix.exs
+++ b/mix.exs
@@ -28,6 +28,7 @@ defmodule Phoenix.MixProject do
         ]
       ],
       elixirc_paths: elixirc_paths(Mix.env()),
+      test_paths: test_paths(System.get_env("TEST_SUITE")),
       name: "Phoenix",
       docs: docs(),
       aliases: aliases(),
@@ -194,7 +195,8 @@ defmodule Phoenix.MixProject do
 
   defp aliases do
     [
-      docs: ["docs", &generate_js_docs/1]
+      docs: ["docs", &generate_js_docs/1],
+      "test.integration": [&start_integration_test/1]
     ]
   end
 
@@ -202,4 +204,26 @@ defmodule Phoenix.MixProject do
     Mix.Task.run("app.start")
     System.cmd("npm", ["run", "docs"], cd: "assets")
   end
+
+  defp test_paths("integration"), do: ["integration_test"]
+  defp test_paths(_), do: ["test"]
+
+  defp start_integration_test(args) do
+    mix_cmd_with_status_check(
+      ["test", ansi_option() | args],
+      env: [{"TEST_SUITE", "integration"}]
+    )
+  end
+
+  defp ansi_option do
+    if IO.ANSI.enabled?(), do: "--color", else: "--no-color"
+  end
+
+  defp mix_cmd_with_status_check(args, opts) when is_list(opts) do
+    {_, res} = System.cmd("mix", args, [into: IO.binstream(:stdio, :line)] ++ opts)
+
+    if res > 0 do
+      System.at_exit(fn _ -> exit({:shutdown, 1}) end)
+    end
+  end
 end
diff --git a/mix.lock b/mix.lock
index f5387c77c..f880364a8 100644
--- a/mix.lock
+++ b/mix.lock
@@ -3,14 +3,15 @@
   "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
   "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
   "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"},
-  "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
+  "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
+  "ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"},
   "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm", "d17ac3aa40835f7feb92913fcdf655edea59938fa8b25f5ce5d423e76a695c3b"},
   "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"},
   "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b96c400e04b7b765c0854c05a4966323e90c0d11fee0483b1567cda079abb205"},
-  "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"},
-  "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
+  "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
+  "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
   "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
-  "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
+  "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
   "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
   "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},


Test patch:
diff --git a/installer/test/mix_helper.exs b/installer/test/mix_helper.exs
index d38a3ade2..72d9527ce 100644
--- a/installer/test/mix_helper.exs
+++ b/installer/test/mix_helper.exs
@@ -2,12 +2,6 @@
 # process to avoid polluting tests.
 Mix.shell(Mix.Shell.Process)
 
-# Mock live reloading for testing the generated application.
-defmodule Phoenix.LiveReloader do
-  def init(opts), do: opts
-  def call(conn, _), do: conn
-end
-
 defmodule MixHelper do
   import ExUnit.Assertions
   import ExUnit.CaptureIO
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index cfa5b0e75..2364a7950 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -491,6 +491,23 @@ defmodule Mix.Tasks.Phx.NewTest do
     end
   end
 
+  test "new with --no-install" do
+    in_tmp "new with no install", fn ->
+      Mix.Tasks.Phx.New.run([@app_name, "--no-install"])
+
+      # Does not prompt to install dependencies
+      refute_received {:mix_shell, :yes?, ["\nFetch and install dependencies?"]}
+
+      # Instructions
+      assert_received {:mix_shell, :info, ["\nWe are almost there" <> _ = msg]}
+      assert msg =~ "$ cd phx_blog"
+      assert msg =~ "$ mix deps.get"
+
+      assert_received {:mix_shell, :info, ["Then configure your database in config/dev.exs" <> _]}
+      assert_received {:mix_shell, :info, ["Start your Phoenix app" <> _]}
+    end
+  end
+
   test "new defaults to pg adapter" do
     in_tmp "new defaults to pg adapter", fn ->
       project_path = Path.join(File.cwd!(), "custom_path")
diff --git a/integration_test/code_generation/app_with_defaults_test.exs b/integration_test/code_generation/app_with_defaults_test.exs
new file mode 100644
index 000000000..3fcb64be4
--- /dev/null
+++ b/integration_test/code_generation/app_with_defaults_test.exs
@@ -0,0 +1,14 @@
+Code.require_file("../support/code_generator_case.exs", __DIR__)
+
+defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
+  use Phoenix.Integration.CodeGeneratorCase, async: true
+
+  test "newly generated app has no warnings or errors" do
+    with_installer_tmp("new with defaults", fn tmp_dir ->
+      app_root_path = generate_phoenix_app(tmp_dir, "phx_blog")
+
+      assert_no_compilation_warnings(app_root_path)
+      assert_passes_formatter_check(app_root_path)
+    end)
+  end
+end
diff --git a/integration_test/code_generation/app_with_no_options_test.exs b/integration_test/code_generation/app_with_no_options_test.exs
new file mode 100644
index 000000000..65858f505
--- /dev/null
+++ b/integration_test/code_generation/app_with_no_options_test.exs
@@ -0,0 +1,20 @@
+Code.require_file("../support/code_generator_case.exs", __DIR__)
+
+defmodule Phoenix.Integration.CodeGeneration.AppWithNoOptionsTest do
+  use Phoenix.Integration.CodeGeneratorCase, async: true
+
+  test "newly generated app has no warnings or errors" do
+    with_installer_tmp("app_with_no_options", fn tmp_dir ->
+      app_root_path = generate_phoenix_app(tmp_dir, "phx_blog", [
+            "--no-html",
+            "--no-webpack",
+            "--no-ecto",
+            "--no-gettext",
+            "--no-dashboard"
+          ])
+
+      assert_no_compilation_warnings(app_root_path)
+      assert_passes_formatter_check(app_root_path)
+    end)
+  end
+end
diff --git a/integration_test/code_generation/umbrella_app_with_defaults_test.exs b/integration_test/code_generation/umbrella_app_with_defaults_test.exs
new file mode 100644
index 000000000..6fcb20f21
--- /dev/null
+++ b/integration_test/code_generation/umbrella_app_with_defaults_test.exs
@@ -0,0 +1,14 @@
+Code.require_file("../support/code_generator_case.exs", __DIR__)
+
+defmodule Phoenix.Integration.CodeGeneration.UmbrellaAppWithDefaultsTest do
+  use Phoenix.Integration.CodeGeneratorCase, async: true
+
+  test "new umbrella app" do
+    with_installer_tmp("umbrella_app_with_defaults", fn tmp_dir ->
+      app_root_path = generate_phoenix_app(tmp_dir, "phx_blog", ["--umbrella"])
+
+      assert_no_compilation_warnings(app_root_path)
+      assert_passes_formatter_check(app_root_path)
+    end)
+  end
+end
diff --git a/integration_test/support/code_generator_case.exs b/integration_test/support/code_generator_case.exs
new file mode 100644
index 000000000..0580e1fae
--- /dev/null
+++ b/integration_test/support/code_generator_case.exs
@@ -0,0 +1,89 @@
+defmodule Phoenix.Integration.CodeGeneratorCase do
+  use ExUnit.CaseTemplate
+
+  using do
+    quote do
+      import unquote(__MODULE__)
+    end
+  end
+
+  def generate_phoenix_app(tmp_dir, app_name, opts \\ [])
+      when is_binary(app_name) and is_list(opts) do
+    app_path = Path.expand(app_name, tmp_dir)
+    installer_root = Path.expand("../../installer", __DIR__)
+    app_root_path = get_app_root_path(tmp_dir, app_name, opts)
+
+    mix_run!(["phx.new", app_path, "--dev", "--no-install"] ++ opts, installer_root)
+
+    mix_run!(~w(deps.get), app_root_path)
+
+    app_root_path
+  end
+
+  def mix_run!(args, app_path, opts \\ [])
+      when is_list(args) and is_binary(app_path) and is_list(opts) do
+    case mix_run(args, app_path, opts) do
+      {output, 0} ->
+        output
+
+      {output, exit_code} ->
+        raise """
+        mix command failed with exit code: #{inspect(exit_code)}
+
+        mix #{Enum.join(args, " ")}
+
+        #{output}
+
+        Options
+        cd: #{Path.expand(app_path)}
+        env: #{opts |> Keyword.get(:env, []) |> inspect()}
+        """
+    end
+  end
+
+  def mix_run(args, app_path, opts \\ [])
+      when is_list(args) and is_binary(app_path) and is_list(opts) do
+    System.cmd("mix", args, [stderr_to_stdout: true, cd: Path.expand(app_path)] ++ opts)
+  end
+
+  def assert_passes_formatter_check(app_path) do
+    mix_run!(~w(format --check-formatted), app_path)
+  end
+
+  def assert_no_compilation_warnings(app_path) do
+    mix_run!(["do", "clean,", "compile", "--warnings-as-errors"], app_path)
+  end
+
+  def with_installer_tmp(name, opts \\ [], function)
+      when is_list(opts) and is_function(function, 1) do
+    autoremove? = Keyword.get(opts, :autoremove?, true)
+    path = Path.join([installer_tmp_path(), random_string(10), to_string(name)])
+
+    try do
+      File.rm_rf!(path)
+      File.mkdir_p!(path)
+      function.(path)
+    after
+      if autoremove?, do: File.rm_rf!(path)
+    end
+  end
+
+  defp installer_tmp_path do
+    Path.expand("../../installer/tmp", __DIR__)
+  end
+
+  defp get_app_root_path(tmp_dir, app_name, opts) do
+    app_root_dir =
+      if "--umbrella" in opts do
+        app_name <> "_umbrella"
+      else
+        app_name
+      end
+
+    Path.expand(app_root_dir, tmp_dir)
+  end
+
+  defp random_string(len) do
+    len |> :crypto.strong_rand_bytes() |> Base.encode64() |> binary_part(0, len)
+  end
+end
diff --git a/integration_test/test_helper.exs b/integration_test/test_helper.exs
new file mode 100644
index 000000000..141158fc2
--- /dev/null
+++ b/integration_test/test_helper.exs
@@ -0,0 +1,8 @@
+Code.require_file("./support/code_generator_case.exs", __DIR__)
+
+# Compile installer app up front so multiple test cases
+# don't try to compile it at the same time.
+Phoenix.Integration.CodeGeneratorCase.mix_run!(["do", "deps.get,", "compile"], "./installer")
+
+ExUnit.configure(max_cases: 2, timeout: 180_000)
+ExUnit.start()
diff --git a/test/mix/tasks/phx.digest_test.exs b/test/mix/tasks/phx.digest_test.exs
index ecceee699..ddeba6ac0 100644
--- a/test/mix/tasks/phx.digest_test.exs
+++ b/test/mix/tasks/phx.digest_test.exs
@@ -13,7 +13,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
   test "digests and compress files" do
     in_tmp @output_path, fn ->
       File.mkdir_p!("priv/static")
-      Mix.Tasks.Phx.Digest.run(["priv/static", "-o", @output_path, "--no-deps-check"])
+      Mix.Tasks.Phx.Digest.run(["priv/static", "-o", @output_path, "--no-deps-check", "--no-compile"])
       assert_received {:mix_shell, :info, ["Check your digested files at \"mix_phoenix_digest\""]}
     end
   end
@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
   test "digests and compress files without the input path" do
     in_tmp @output_path, fn ->
       File.mkdir_p!("priv/static")
-      Mix.Tasks.Phx.Digest.run(["-o", @output_path, "--no-deps-check"])
+      Mix.Tasks.Phx.Digest.run(["-o", @output_path, "--no-deps-check", "--no-compile"])
       assert_received {:mix_shell, :info, ["Check your digested files at \"mix_phoenix_digest_no_input\""]}
     end
   end
@@ -31,7 +31,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
   test "uses the input path as output path when no output path is given" do
     in_tmp @input_path, fn ->
       File.mkdir_p!(@input_path)
-      Mix.Tasks.Phx.Digest.run([@input_path, "--no-deps-check"])
+      Mix.Tasks.Phx.Digest.run([@input_path, "--no-deps-check", "--no-compile"])
       assert_received {:mix_shell, :info, ["Check your digested files at \"input_path\""]}
     end
   end
diff --git a/test/mix/tasks/phx.new_test.exs b/test/mix/tasks/phx.new_test.exs
index d979f23b9..7f270ceaf 100644
--- a/test/mix/tasks/phx.new_test.exs
+++ b/test/mix/tasks/phx.new_test.exs
@@ -3,6 +3,12 @@ with list when is_list(list) <- :code.lib_dir(:phx_new) do
   :code.del_path(:filename.join(list, 'ebin'))
 end
 
+# Mock live reloading for testing the generated application.
+defmodule Phoenix.LiveReloader do
+  def init(opts), do: opts
+  def call(conn, _), do: conn
+end
+
 _ = Kernel.ParallelCompiler.compile Path.wildcard("installer/lib/**/*.ex")
 Code.require_file("installer/test/mix_helper.exs")
 
@@ -56,14 +62,6 @@ defmodule Mix.Tasks.Phx.NewTest do
           refute_received {:mix_shell, :info, ["Generated phoenix app"]}
           Mix.shell().flush()
 
-          # Adding a new template touches file (through mix)
-          File.touch! "lib/phx_blog_web/views/layout_view.ex", @epoch
-          File.write! "lib/phx_blog_web/templates/layout/another.html.eex", "oops"
-
-          Mix.Task.clear()
-          Mix.Task.run "compile", ["--no-deps-check"]
-          assert File.stat!("lib/phx_blog_web/views/layout_view.ex").mtime > @epoch
-
           # Adding a new template triggers recompilation (through request)
           File.touch! "lib/phx_blog_web/views/page_view.ex", @epoch
           File.write! "lib/phx_blog_web/templates/page/another.html.eex", "oops"
diff --git a/test/phoenix/integration/endpoint_test.exs b/test/phoenix/integration/endpoint_test.exs
index 0613f944d..2fe130960 100644
--- a/test/phoenix/integration/endpoint_test.exs
+++ b/test/phoenix/integration/endpoint_test.exs
@@ -1,21 +1,30 @@
 Code.require_file "../../support/http_client.exs", __DIR__
+Code.require_file "../../support/endpoint_helper.exs", __DIR__
 
 defmodule Phoenix.Integration.EndpointTest do
   use ExUnit.Case
   import ExUnit.CaptureLog
 
+  import Phoenix.Integration.EndpointHelper
+
   alias Phoenix.Integration.AdapterTest.ProdEndpoint
   alias Phoenix.Integration.AdapterTest.DevEndpoint
   alias Phoenix.Integration.AdapterTest.ProdInet6Endpoint
 
+  # Find available ports to use for this test
+  [dev, prod, prod_inet6] = get_unused_port_numbers(3)
+  @dev dev
+  @prod prod
+  @prod_inet6 prod_inet6
+
   Application.put_env(:endpoint_int, ProdEndpoint,
-    http: [port: "4807"], url: [host: "example.com"], server: true, drainer: false,
+    http: [port: @prod], url: [host: "example.com"], server: true, drainer: false,
     render_errors: [accepts: ~w(html json)])
   Application.put_env(:endpoint_int, DevEndpoint,
-    http: [port: "4808"], debug_errors: true, drainer: false)
+    http: [port: @dev], debug_errors: true, drainer: false)
 
   Application.put_env(:endpoint_int, ProdInet6Endpoint,
-    http: [port: "4806", transport_options: [socket_opts: [:inet6]]],
+    http: [port: @prod_inet6, transport_options: [socket_opts: [:inet6]]],
     url: [host: "example.com"],
     server: true)
 
@@ -100,9 +109,6 @@ defmodule Phoenix.Integration.EndpointTest do
     end
   end
 
-  @prod 4807
-  @dev  4808
-
   alias Phoenix.Integration.HTTPClient
 
   test "starts drainer in supervision tree if configured" do
diff --git a/test/support/endpoint_helper.exs b/test/support/endpoint_helper.exs
new file mode 100644
index 000000000..ae1bd9a28
--- /dev/null
+++ b/test/support/endpoint_helper.exs
@@ -0,0 +1,27 @@
+defmodule Phoenix.Integration.EndpointHelper do
+  @moduledoc """
+  Utility functions for integration testing endpoints.
+  """
+
+  @doc """
+  Finds `n` unused network port numbers.
+  """
+  def get_unused_port_numbers(n) when is_integer(n) and n > 1 do
+    (1..n)
+    # Open up `n` sockets at the same time, so we don't get
+    # duplicate port numbers
+    |> Enum.map(&listen_on_os_assigned_port/1)
+    |> Enum.map(&get_port_number_and_close/1)
+  end
+
+  defp listen_on_os_assigned_port(_) do
+    {:ok, socket} = :gen_tcp.listen(0, [])
+    socket
+  end
+
+  defp get_port_number_and_close(socket) do
+    {:ok, port_number} = :inet.port(socket)
+    :gen_tcp.close(socket)
+    port_number
+  end
+end