Break Mix deps tests into groups

Break Mix deps tests into groups

Code.require_file("../test_helper.exs", __DIR__)

defmodule Mix.DepTest do
  use MixTest.Case

  defmodule DepsApp do
    def project do
      [
        deps: [
          {:ok, "0.1.0", path: "deps/ok"},
          {:invalidvsn, "0.2.0", path: "deps/invalidvsn"},
          {:invalidapp, "0.1.0", path: "deps/invalidapp"},
          {:noappfile, "0.1.0", path: "deps/noappfile"},
          {:uncloned, git: "https://github.com/elixir-lang/uncloned.git"},
          {:optional, git: "https://github.com/elixir-lang/optional.git", optional: true}
        ]
      ]
    end
  end

  defmodule ProcessDepsApp do
    def project do
      [app: :process_deps_app, deps: Process.get(:mix_deps)]
    end
  end

  defp with_deps(deps, fun) do
    Process.put(:mix_deps, deps)
    Mix.Project.push(ProcessDepsApp)
    fun.()
  after
    Mix.Project.pop()
  end

  defp assert_wrong_dependency(deps) do
    with_deps(deps, fn ->
      assert_raise Mix.Error, ~r"Dependency specified in the wrong format", fn ->
        Mix.Dep.load_on_environment([])
      end
    end)
  end

  test "clear deps cache" do
    Mix.Project.push(DepsApp)

    Mix.Dep.cached()
    key = {:cached_deps, DepsApp}

    {env, deps} = Mix.ProjectStack.read_cache(key)
    assert env == Mix.env()
    assert length(deps) == 6

    Mix.Dep.clear_cached()
    refute Mix.ProjectStack.read_cache(key)
  end

  test "extracts all dependencies from the given project" do
    Mix.Project.push(DepsApp)

    in_fixture("deps_status", fn ->
      deps = Mix.Dep.load_on_environment([])
      assert length(deps) == 6
      assert Enum.find(deps, &match?(%Mix.Dep{app: :ok, status: {:ok, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :invalidvsn, status: {:invalidvsn, :ok}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :invalidapp, status: {:invalidapp, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :noappfile, status: {:noappfile, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :uncloned, status: {:unavailable, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :optional, status: {:unavailable, _}}, &1))
    end)
  end

  test "extracts all dependencies paths from the given project" do
    Mix.Project.push(DepsApp)

    in_fixture("deps_status", fn ->
      paths = Mix.Project.deps_paths()
      assert map_size(paths) == 6
      assert paths[:ok] =~ "deps/ok"
      assert paths[:uncloned] =~ "deps/uncloned"
    end)
  end

  test "fails on invalid dependencies" do
    assert_wrong_dependency([{:ok}])
    assert_wrong_dependency([{:ok, nil}])
    assert_wrong_dependency([{:ok, nil, []}])
  end

  test "use requirements for dependencies" do
    deps = [{:ok, "~> 0.1", path: "deps/ok"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        deps = Mix.Dep.load_on_environment([])
        assert Enum.find(deps, &match?(%Mix.Dep{app: :ok, status: {:ok, _}}, &1))
      end)
    end)
  end

  test "raises when no SCM is specified" do
    deps = [{:ok, "~> 0.1", not_really: :ok}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        send(self(), {:mix_shell_input, :yes?, false})
        msg = "Could not find an SCM for dependency :ok from Mix.DepTest.ProcessDepsApp"
        assert_raise Mix.Error, msg, fn -> Mix.Dep.load_on_environment([]) end
      end)
    end)
  end

  test "does not set the manager before the dependency was loaded" do
    # It is important to not eagerly set the manager because the dependency
    # needs to be loaded (i.e. available in the file system) in order to get
    # the proper manager.
    Mix.Project.push(DepsApp)

    {_, true, _} =
      Mix.Dep.Converger.converge(false, [], nil, fn dep, acc, lock ->
        assert is_nil(dep.manager)
        {dep, acc or true, lock}
      end)
  end

  test "raises on invalid deps req" do
    deps = [{:ok, "+- 0.1.0", path: "deps/ok"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert_raise Mix.Error, ~r"Invalid requirement", fn ->
          Mix.Dep.load_on_environment([])
        end
      end)
    end)
  end

  test "nested deps come first" do
    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
      end)
    end)
  end

  test "nested optional deps are never added" do
    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.write!("custom/deps_repo/mix.exs", """
        defmodule DepsRepo do
          use Mix.Project

          def project do
            [app: :deps_repo,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
          end
        end
        """)

        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:deps_repo]
      end)
    end)
  end

  test "nested deps with convergence" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
      end)
    end)
  end

  test "nested deps with convergence and managers" do
    Process.put(:custom_deps_git_repo_opts, manager: :make)

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", manager: :rebar},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        [dep1, dep2] = Mix.Dep.load_on_environment([])
        assert dep1.manager == nil
        assert dep2.manager == :rebar
      end)
    end)
  end

  test "nested deps with convergence and optional dependencies" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.write!("custom/deps_repo/mix.exs", """
        defmodule DepsRepo do
          use Mix.Project

          def project do
            [app: :deps_repo,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
          end
        end
        """)

        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
      end)
    end)
  end

  test "nested deps with optional dependencies and cousin conflict" do
    deps = [
      {:deps_repo1, "0.1.0", path: "custom/deps_repo1"},
      {:deps_repo2, "0.1.0", path: "custom/deps_repo2"}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.mkdir_p!("custom/deps_repo1")

        File.write!("custom/deps_repo1/mix.exs", """
        defmodule DepsRepo1 do
          use Mix.Project

          def project do
            [app: :deps_repo1,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
          end
        end
        """)

        File.mkdir_p!("custom/deps_repo2")

        File.write!("custom/deps_repo2/mix.exs", """
        defmodule DepsRepo2 do
          use Mix.Project

          def project do
            [app: :deps_repo2,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", path: "somewhere"}]]
          end
        end
        """)

        Mix.Tasks.Deps.run([])
        assert_received {:mix_shell, :info, ["* git_repo" <> _]}
        assert_received {:mix_shell, :info, [msg]}
        assert msg =~ "different specs were given for the git_repo"
      end)
    end)
  end

  test "deps with system_env set" do
    file_path = tmp_path("load dependency with env vars/dep-test")
    dep_path = tmp_path("rebar_dep")

    system_env = [{"FILE_FROM_ENV", file_path}, {"CONTENTS_FROM_ENV", "contents dep test"}]
    deps = [{:rebar_dep, path: dep_path, app: false, manager: :rebar, system_env: system_env}]

    with_deps(deps, fn ->
      in_tmp("load dependency with env vars", fn ->
        Mix.Dep.load_on_environment([])
        assert {:ok, "contents dep test"} = File.read(file_path)
      end)
    end)
  end

  test "diverged with system_env set" do
    Process.put(:custom_deps_git_repo_opts, system_env: [{"FOO", "BAR"}])

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        [git_repo, _] = Mix.Dep.load_on_environment([])
        %{app: :git_repo, status: {:overridden, _}} = git_repo
      end)
    end)
  end

  ## Remote converger

  defmodule IdentityRemoteConverger do
    @behaviour Mix.RemoteConverger

    def remote?(%Mix.Dep{app: :deps_repo}), do: false
    def remote?(%Mix.Dep{}), do: true
    def deps(_dep, _lock), do: []
    def post_converge, do: :ok

    def converge(deps, lock) do
      Process.put(:remote_converger, deps)
      lock
    end
  end

  test "remote converger" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(IdentityRemoteConverger)

      in_fixture("deps_status", fn ->
        Mix.Tasks.Deps.Get.run([])

        message = "* Getting git_repo (#{fixture_path("git_repo")})"
        assert_received {:mix_shell, :info, [^message]}

        assert Process.get(:remote_converger)
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  defmodule DivergingRemoteConverger do
    @behaviour Mix.RemoteConverger

    def remote?(%Mix.Dep{app: :deps_repo, scm: Mix.SCM.Path}), do: true
    def remote?(%Mix.Dep{app: :git_repo, scm: Mix.SCM.Path}), do: true
    def remote?(%Mix.Dep{}), do: false
    def deps(%Mix.Dep{app: :deps_repo}, _lock), do: [{:git_repo, path: "custom/git_repo"}]
    def deps(%Mix.Dep{app: :git_repo}, _lock), do: []
    def post_converge, do: :ok

    def converge(_deps, lock) do
      lock
      |> Map.put(:deps_repo, :custom)
      |> Map.put(:git_repo, :custom)
    end
  end

  test "converger detects diverged deps from remote converger" do
    deps = [
      {:deps_on_git_repo, "0.2.0", git: MixTest.Case.fixture_path("deps_on_git_repo")},
      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(DivergingRemoteConverger)

      in_fixture("deps_status", fn ->
        assert_raise Mix.Error, fn ->
          Mix.Tasks.Deps.Get.run([])
        end

        assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  test "pass dependencies to remote converger in defined order" do
    deps = [
      {:ok, "0.1.0", path: "deps/ok"},
      {:invalidvsn, "0.2.0", path: "deps/invalidvsn"},
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:invalidapp, "0.1.0", path: "deps/invalidapp"},
      {:noappfile, "0.1.0", path: "deps/noappfile"}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(IdentityRemoteConverger)

      in_fixture("deps_status", fn ->
        Mix.Tasks.Deps.Get.run([])

        deps = Process.get(:remote_converger) |> Enum.map(& &1.app)
        assert deps == [:ok, :invalidvsn, :deps_repo, :invalidapp, :noappfile, :git_repo]
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  defmodule RaiseRemoteConverger do
    @behaviour Mix.RemoteConverger

    def remote?(_app), do: false
    def deps(_dep, _lock), do: :ok
    def post_converge, do: :ok

    def converge(_deps, lock) do
      Process.put(:remote_converger, true)
      lock
    end
  end

  test "remote converger is not invoked if deps diverge" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(RaiseRemoteConverger)

      in_fixture("deps_status", fn ->
        assert_raise Mix.Error, fn ->
          Mix.Tasks.Deps.Get.run([])
        end

        assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
        refute Process.get(:remote_converger)
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  test "remote converger is not invoked if deps graph has cycles" do
    deps = [{:app1, "0.1.0", path: "app1"}, {:app2, "0.1.0", path: "app2"}]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(RaiseRemoteConverger)

      in_fixture("deps_cycle", fn ->
        assert_raise Mix.Error, ~r/cycles in the dependency graph/, fn ->
          Mix.Tasks.Deps.Get.run([])
        end

        refute Process.get(:remote_converger)
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  test "deps_paths" do
    deps = [
      {:abc_repo, "0.1.0", path: "custom/abc_repo"},
      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) ==
                 [:git_repo, :abc_repo, :deps_repo]

        assert Map.keys(Mix.Project.deps_paths()) == [:abc_repo, :deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(depth: 1)) == [:abc_repo, :deps_repo]
        assert Map.keys(Mix.Project.deps_paths(depth: 2)) == [:abc_repo, :deps_repo, :git_repo]
        assert Map.keys(Mix.Project.deps_paths(depth: 3)) == [:abc_repo, :deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo])) == [:abc_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo])) == [:deps_repo, :git_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo])) == [:git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo], depth: 1)) == [:abc_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo], depth: 1)) == [:deps_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo], depth: 1)) == [:git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo], depth: 2)) == [:abc_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo], depth: 2)) == [:git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo], depth: 2)) ==
                 [:deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo])) ==
                 [:abc_repo, :deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo], depth: 1)) ==
                 [:abc_repo, :deps_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo], depth: 2)) ==
                 [:abc_repo, :deps_repo, :git_repo]
      end)
    end)
  end

  describe "only handling" do
    test "extracts deps matching environment" do
      deps = [
        {:foo, github: "elixir-lang/foo"},
        {:bar, github: "elixir-lang/bar", only: :other_env}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          deps = Mix.Dep.load_on_environment(env: :other_env)
          assert length(deps) == 2

          deps = Mix.Dep.load_on_environment([])
          assert length(deps) == 2

          assert [dep] = Mix.Dep.load_on_environment(env: :prod)
          assert dep.app == :foo
        end)
      end)
    end

    test "fetches parent deps matching specified env" do
      deps = [{:only, github: "elixir-lang/only", only: [:dev]}]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          Mix.Tasks.Deps.Get.run(["--only", "prod"])
          refute_received {:mix_shell, :info, ["* Getting" <> _]}

          assert_raise Mix.Error, "Can't continue due to errors on dependencies", fn ->
            Mix.Tasks.Deps.Loadpaths.run([])
          end

          Mix.ProjectStack.clear_cache()
          Mix.env(:prod)
          Mix.Tasks.Deps.Loadpaths.run([])
        end)
      end)
    end

    test "selects only prod dependencies on nested deps" do
      Process.put(:custom_deps_git_repo_opts, only: :test)
      deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          loaded = Mix.Dep.load_on_environment([])
          assert [:deps_repo] = Enum.map(loaded, & &1.app)

          loaded = Mix.Dep.load_on_environment(env: :test)
          assert [:deps_repo] = Enum.map(loaded, & &1.app)
        end)
      end)
    end

    test "conflicts on nested deps" do
      # deps_repo wants all git_repo, git_repo is restricted to only test
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          loaded = Mix.Dep.load_on_environment([])
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :dev)
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :test)
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

          Mix.Tasks.Deps.run([])
          assert_received {:mix_shell, :info, ["* git_repo" <> _]}
          assert_received {:mix_shell, :info, [msg]}
          assert msg =~ "Remove the :only restriction from your dep"
        end)
      end)
    end

    test "conflicts does not happen with optional deps on nested deps" do
      Process.put(:custom_deps_git_repo_opts, optional: true)

      # deps_repo wants all git_repo, git_repo is restricted to only test
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          loaded = Mix.Dep.load_on_environment([])
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :dev)
          assert [:deps_repo] = Enum.map(loaded, & &1.app)
          assert [noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :test)
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)
        end)
      end)
    end

    test "does not conflict on valid subsets on nested deps" do
      # deps_repo wants git_repo for prod, git_repo is restricted to only prod and test
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: [:prod, :test]}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          loaded = Mix.Dep.load_on_environment([])
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :dev)
          assert [] = Enum.map(loaded, & &1.app)

          loaded = Mix.Dep.load_on_environment(env: :test)
          assert [:git_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :prod)
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)
        end)
      end)
    end

    test "conflicts on invalid only subset on nested deps" do
      # deps_repo wants git_repo for dev, git_repo is restricted to only test
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :dev},
        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: [:test]}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          loaded = Mix.Dep.load_on_environment([])
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :dev)
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :test)
          assert [:git_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _] = Enum.map(loaded, & &1.status)

          Mix.Tasks.Deps.run([])
          assert_received {:mix_shell, :info, ["* git_repo" <> _]}
          assert_received {:mix_shell, :info, [msg]}
          assert msg =~ "Ensure you specify at least the same environments in :only in your dep"
        end)
      end)
    end

    test "does not conflict with valid only in both parent and child on nested deps" do
      Process.put(:custom_deps_git_repo_opts, only: :test)

      # deps_repo has environment set to test so it loads the deps_git_repo set to test too
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo", env: :test, only: [:dev, :test]},
        {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          loaded = Mix.Dep.load_on_environment([])
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :dev)
          assert [:deps_repo] = Enum.map(loaded, & &1.app)
          assert [noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :test)
          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
          assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

          loaded = Mix.Dep.load_on_environment(env: :prod)
          assert [] = Enum.map(loaded, & &1.app)
        end)
      end)
    end

    test "converges and diverges when only is not in_upper" do
      loaded = fn deps ->
        with_deps(deps, fn ->
          in_fixture("deps_status", fn ->
            File.mkdir_p!("custom/other_repo")

            File.write!("custom/other_repo/mix.exs", """
            defmodule OtherRepo do
              use Mix.Project

              def project do
                [app: :other_repo,
                 version: "0.1.0",
                 deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
              end
            end
            """)

            Mix.ProjectStack.clear_cache()
            loaded = Mix.Dep.load_on_environment([])
            Enum.map(loaded, &{&1.app, &1.opts[:only]})
          end)
        end)
      end

      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
        {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
      ]

      assert loaded.(deps) == [git_repo: [:test, :prod], other_repo: :test, deps_repo: :prod]

      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
        {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
      ]

      assert loaded.(deps) == [git_repo: nil, other_repo: :test, deps_repo: nil]

      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
        {:other_repo, "0.1.0", path: "custom/other_repo"}
      ]

      assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: :prod]

      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
        {:other_repo, "0.1.0", path: "custom/other_repo"}
      ]

      assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: nil]

      Process.put(:custom_deps_git_repo_opts, optional: true)

      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
        {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
      ]

      assert loaded.(deps) == [git_repo: :test, other_repo: :test, deps_repo: :prod]
    end

    test "converges and diverges when only is not specified" do
      Process.put(:custom_deps_git_repo_opts, only: :test)

      # Pass nil environment to mimic behaviour in umbrellas
      # where the children are loaded without an environment.
      deps = [
        {:abc_repo, "0.1.0", path: "custom/abc_repo", env: nil},
        {:deps_repo, "0.1.0", path: "custom/deps_repo", env: nil}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          File.mkdir_p!("custom/abc_repo")

          File.write!("custom/abc_repo/mix.exs", """
          defmodule OtherRepo do
            use Mix.Project

            def project do
              [app: :abc_repo,
               version: "0.1.0",
               deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
            end
          end
          """)

          Mix.Tasks.Deps.Get.run([])
          Mix.Tasks.Deps.Compile.run([])
          refute_receive {:mix_shell, :error, ["Could not compile :git_repo" <> _]}
        end)
      end)
    end
  end

  describe "overrides" do
    test "are not required when there are no conflicts" do
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          File.mkdir_p!("custom/deps_repo/lib")

          File.write!("custom/deps_repo/lib/a.ex", """
          # Check that the child dependency is top_level
          [%Mix.Dep{app: :git_repo, top_level: true}] = Mix.Dep.cached()
          """)

          Mix.Tasks.Deps.Get.run([])
          Mix.Tasks.Deps.Compile.run([])
        end)
      end)
    end

    test "are required when there are conflicts" do
      # deps_repo brings git_repo but it is overridden
      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
        {:git_repo, ">= 0.0.0", git: MixTest.Case.fixture_path("git_repo"), override: true}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          File.mkdir_p!("custom/deps_repo/lib")

          File.write!("custom/deps_repo/lib/a.ex", """
          # Check that the overridden requirement shows up in the child dependency
          [%Mix.Dep{app: :git_repo, requirement: ">= 0.0.0"}] = Mix.Dep.cached()
          """)

          Mix.Tasks.Deps.Get.run([])
          Mix.Tasks.Deps.Compile.run([])
        end)
      end)
    end
  end

  describe "app generation" do
    test "considers runtime from current app on nested deps" do
      Process.put(:custom_deps_git_repo_opts, runtime: false)

      deps = [
        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
        {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
      ]

      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          Mix.Tasks.Deps.Compile.run([])

          {:ok, [{:application, :deps_repo, opts}]} =
            :file.consult("_build/dev/lib/deps_repo/ebin/deps_repo.app")

          assert :git_repo not in Keyword.get(opts, :applications)
        end)
      end)
    end
  end
end

Code.require_file("../test_helper.exs", __DIR__)

defmodule Mix.DepTest do
  use MixTest.Case

  defmodule DepsApp do
    def project do
      [
        deps: [
          {:ok, "0.1.0", path: "deps/ok"},
          {:invalidvsn, "0.2.0", path: "deps/invalidvsn"},
          {:invalidapp, "0.1.0", path: "deps/invalidapp"},
          {:noappfile, "0.1.0", path: "deps/noappfile"},
          {:uncloned, git: "https://github.com/elixir-lang/uncloned.git"},
          {:optional, git: "https://github.com/elixir-lang/optional.git", optional: true}
        ]
      ]
    end
  end

  defmodule ProcessDepsApp do
    def project do
      [app: :process_deps_app, deps: Process.get(:mix_deps)]
    end
  end

  defp with_deps(deps, fun) do
    Process.put(:mix_deps, deps)
    Mix.Project.push(ProcessDepsApp)
    fun.()
  after
    Mix.Project.pop()
  end

  defp assert_wrong_dependency(deps) do
    with_deps(deps, fn ->
      assert_raise Mix.Error, ~r"Dependency specified in the wrong format", fn ->
        Mix.Dep.load_on_environment([])
      end
    end)
  end

  test "clear deps cache" do
    Mix.Project.push(DepsApp)

    Mix.Dep.cached()
    key = {:cached_deps, DepsApp}

    {env, deps} = Mix.ProjectStack.read_cache(key)
    assert env == Mix.env()
    assert length(deps) == 6

    Mix.Dep.clear_cached()
    refute Mix.ProjectStack.read_cache(key)
  end

  test "extracts all dependencies from the given project" do
    Mix.Project.push(DepsApp)

    in_fixture("deps_status", fn ->
      deps = Mix.Dep.load_on_environment([])
      assert length(deps) == 6
      assert Enum.find(deps, &match?(%Mix.Dep{app: :ok, status: {:ok, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :invalidvsn, status: {:invalidvsn, :ok}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :invalidapp, status: {:invalidapp, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :noappfile, status: {:noappfile, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :uncloned, status: {:unavailable, _}}, &1))
      assert Enum.find(deps, &match?(%Mix.Dep{app: :optional, status: {:unavailable, _}}, &1))
    end)
  end

  test "extracts all dependencies paths from the given project" do
    Mix.Project.push(DepsApp)

    in_fixture("deps_status", fn ->
      paths = Mix.Project.deps_paths()
      assert map_size(paths) == 6
      assert paths[:ok] =~ "deps/ok"
      assert paths[:uncloned] =~ "deps/uncloned"
    end)
  end

  test "fails on invalid dependencies" do
    assert_wrong_dependency([{:ok}])
    assert_wrong_dependency([{:ok, nil}])
    assert_wrong_dependency([{:ok, nil, []}])
  end

  test "use requirements for dependencies" do
    deps = [{:ok, "~> 0.1", path: "deps/ok"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        deps = Mix.Dep.load_on_environment([])
        assert Enum.find(deps, &match?(%Mix.Dep{app: :ok, status: {:ok, _}}, &1))
      end)
    end)
  end

  test "raises when no SCM is specified" do
    deps = [{:ok, "~> 0.1", not_really: :ok}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        send(self(), {:mix_shell_input, :yes?, false})
        msg = "Could not find an SCM for dependency :ok from Mix.DepTest.ProcessDepsApp"
        assert_raise Mix.Error, msg, fn -> Mix.Dep.load_on_environment([]) end
      end)
    end)
  end

  test "does not set the manager before the dependency was loaded" do
    # It is important to not eagerly set the manager because the dependency
    # needs to be loaded (i.e. available in the file system) in order to get
    # the proper manager.
    Mix.Project.push(DepsApp)

    {_, true, _} =
      Mix.Dep.Converger.converge(false, [], nil, fn dep, acc, lock ->
        assert is_nil(dep.manager)
        {dep, acc or true, lock}
      end)
  end

  test "raises on invalid deps req" do
    deps = [{:ok, "+- 0.1.0", path: "deps/ok"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert_raise Mix.Error, ~r"Invalid requirement", fn ->
          Mix.Dep.load_on_environment([])
        end
      end)
    end)
  end

  test "nested deps come first" do
    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
      end)
    end)
  end

  test "nested optional deps are never added" do
    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.write!("custom/deps_repo/mix.exs", """
        defmodule DepsRepo do
          use Mix.Project

          def project do
            [app: :deps_repo,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
          end
        end
        """)

        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:deps_repo]
      end)
    end)
  end

  test "nested deps with convergence" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
      end)
    end)
  end

  test "nested deps with convergence and managers" do
    Process.put(:custom_deps_git_repo_opts, manager: :make)

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", manager: :rebar},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        [dep1, dep2] = Mix.Dep.load_on_environment([])
        assert dep1.manager == nil
        assert dep2.manager == :rebar
      end)
    end)
  end

  test "nested deps with convergence and optional dependencies" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.write!("custom/deps_repo/mix.exs", """
        defmodule DepsRepo do
          use Mix.Project

          def project do
            [app: :deps_repo,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
          end
        end
        """)

        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
      end)
    end)
  end

  test "nested deps with optional dependencies and cousin conflict" do
    deps = [
      {:deps_repo1, "0.1.0", path: "custom/deps_repo1"},
      {:deps_repo2, "0.1.0", path: "custom/deps_repo2"}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.mkdir_p!("custom/deps_repo1")

        File.write!("custom/deps_repo1/mix.exs", """
        defmodule DepsRepo1 do
          use Mix.Project

          def project do
            [app: :deps_repo1,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
          end
        end
        """)

        File.mkdir_p!("custom/deps_repo2")

        File.write!("custom/deps_repo2/mix.exs", """
        defmodule DepsRepo2 do
          use Mix.Project

          def project do
            [app: :deps_repo2,
             version: "0.1.0",
             deps: [{:git_repo, "0.2.0", path: "somewhere"}]]
          end
        end
        """)

        Mix.Tasks.Deps.run([])
        assert_received {:mix_shell, :info, ["* git_repo" <> _]}
        assert_received {:mix_shell, :info, [msg]}
        assert msg =~ "different specs were given for the git_repo"
      end)
    end)
  end

  test "deps with system_env set" do
    file_path = tmp_path("load dependency with env vars/dep-test")
    dep_path = tmp_path("rebar_dep")

    system_env = [{"FILE_FROM_ENV", file_path}, {"CONTENTS_FROM_ENV", "contents dep test"}]
    deps = [{:rebar_dep, path: dep_path, app: false, manager: :rebar, system_env: system_env}]

    with_deps(deps, fn ->
      in_tmp("load dependency with env vars", fn ->
        Mix.Dep.load_on_environment([])
        assert {:ok, "contents dep test"} = File.read(file_path)
      end)
    end)
  end

  test "diverged with system_env set" do
    Process.put(:custom_deps_git_repo_opts, system_env: [{"FOO", "BAR"}])

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        [git_repo, _] = Mix.Dep.load_on_environment([])
        %{app: :git_repo, status: {:overridden, _}} = git_repo
      end)
    end)
  end

  ## Remote converger

  defmodule IdentityRemoteConverger do
    @behaviour Mix.RemoteConverger

    def remote?(%Mix.Dep{app: :deps_repo}), do: false
    def remote?(%Mix.Dep{}), do: true
    def deps(_dep, _lock), do: []
    def post_converge, do: :ok

    def converge(deps, lock) do
      Process.put(:remote_converger, deps)
      lock
    end
  end

  test "remote converger" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(IdentityRemoteConverger)

      in_fixture("deps_status", fn ->
        Mix.Tasks.Deps.Get.run([])

        message = "* Getting git_repo (#{fixture_path("git_repo")})"
        assert_received {:mix_shell, :info, [^message]}

        assert Process.get(:remote_converger)
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  defmodule DivergingRemoteConverger do
    @behaviour Mix.RemoteConverger

    def remote?(%Mix.Dep{app: :deps_repo, scm: Mix.SCM.Path}), do: true
    def remote?(%Mix.Dep{app: :git_repo, scm: Mix.SCM.Path}), do: true
    def remote?(%Mix.Dep{}), do: false
    def deps(%Mix.Dep{app: :deps_repo}, _lock), do: [{:git_repo, path: "custom/git_repo"}]
    def deps(%Mix.Dep{app: :git_repo}, _lock), do: []
    def post_converge, do: :ok

    def converge(_deps, lock) do
      lock
      |> Map.put(:deps_repo, :custom)
      |> Map.put(:git_repo, :custom)
    end
  end

  test "converger detects diverged deps from remote converger" do
    deps = [
      {:deps_on_git_repo, "0.2.0", git: MixTest.Case.fixture_path("deps_on_git_repo")},
      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(DivergingRemoteConverger)

      in_fixture("deps_status", fn ->
        assert_raise Mix.Error, fn ->
          Mix.Tasks.Deps.Get.run([])
        end

        assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  test "pass dependencies to remote converger in defined order" do
    deps = [
      {:ok, "0.1.0", path: "deps/ok"},
      {:invalidvsn, "0.2.0", path: "deps/invalidvsn"},
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:invalidapp, "0.1.0", path: "deps/invalidapp"},
      {:noappfile, "0.1.0", path: "deps/noappfile"}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(IdentityRemoteConverger)

      in_fixture("deps_status", fn ->
        Mix.Tasks.Deps.Get.run([])

        deps = Process.get(:remote_converger) |> Enum.map(& &1.app)
        assert deps == [:ok, :invalidvsn, :deps_repo, :invalidapp, :noappfile, :git_repo]
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  defmodule RaiseRemoteConverger do
    @behaviour Mix.RemoteConverger

    def remote?(_app), do: false
    def deps(_dep, _lock), do: :ok
    def post_converge, do: :ok

    def converge(_deps, lock) do
      Process.put(:remote_converger, true)
      lock
    end
  end

  test "remote converger is not invoked if deps diverge" do
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
    ]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(RaiseRemoteConverger)

      in_fixture("deps_status", fn ->
        assert_raise Mix.Error, fn ->
          Mix.Tasks.Deps.Get.run([])
        end

        assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
        refute Process.get(:remote_converger)
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  test "remote converger is not invoked if deps graph has cycles" do
    deps = [{:app1, "0.1.0", path: "app1"}, {:app2, "0.1.0", path: "app2"}]

    with_deps(deps, fn ->
      Mix.RemoteConverger.register(RaiseRemoteConverger)

      in_fixture("deps_cycle", fn ->
        assert_raise Mix.Error, ~r/cycles in the dependency graph/, fn ->
          Mix.Tasks.Deps.Get.run([])
        end

        refute Process.get(:remote_converger)
      end)
    end)
  after
    Mix.RemoteConverger.register(nil)
  end

  ## Only handling

  test "only extracts deps matching environment" do
    deps = [
      {:foo, github: "elixir-lang/foo"},
      {:bar, github: "elixir-lang/bar", only: :other_env}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        deps = Mix.Dep.load_on_environment(env: :other_env)
        assert length(deps) == 2

        deps = Mix.Dep.load_on_environment([])
        assert length(deps) == 2

        assert [dep] = Mix.Dep.load_on_environment(env: :prod)
        assert dep.app == :foo
      end)
    end)
  end

  test "only fetches parent deps matching specified env" do
    deps = [{:only, github: "elixir-lang/only", only: [:dev]}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        Mix.Tasks.Deps.Get.run(["--only", "prod"])
        refute_received {:mix_shell, :info, ["* Getting" <> _]}

        assert_raise Mix.Error, "Can't continue due to errors on dependencies", fn ->
          Mix.Tasks.Deps.Loadpaths.run([])
        end

        Mix.ProjectStack.clear_cache()
        Mix.env(:prod)
        Mix.Tasks.Deps.Loadpaths.run([])
      end)
    end)
  end

  test "nested deps selects only prod dependencies" do
    Process.put(:custom_deps_git_repo_opts, only: :test)
    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        loaded = Mix.Dep.load_on_environment([])
        assert [:deps_repo] = Enum.map(loaded, & &1.app)

        loaded = Mix.Dep.load_on_environment(env: :test)
        assert [:deps_repo] = Enum.map(loaded, & &1.app)
      end)
    end)
  end

  test "nested deps with no overrides" do
    # deps_repo brings git_repo but it is overridden
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.mkdir_p!("custom/deps_repo/lib")

        File.write!("custom/deps_repo/lib/a.ex", """
        # Check that the child dependency is top_level
        [%Mix.Dep{app: :git_repo, top_level: true}] = Mix.Dep.cached()
        """)

        Mix.Tasks.Deps.Get.run([])
        Mix.Tasks.Deps.Compile.run([])
      end)
    end)
  end

  test "nested deps considers runtime from current app" do
    Process.put(:custom_deps_git_repo_opts, runtime: false)

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        Mix.Tasks.Deps.Compile.run([])

        {:ok, [{:application, :deps_repo, opts}]} =
          :file.consult("_build/dev/lib/deps_repo/ebin/deps_repo.app")

        assert :git_repo not in Keyword.get(opts, :applications)
      end)
    end)
  end

  test "nested deps with overrides" do
    # deps_repo brings git_repo but it is overridden
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, ">= 0.0.0", git: MixTest.Case.fixture_path("git_repo"), override: true}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.mkdir_p!("custom/deps_repo/lib")

        File.write!("custom/deps_repo/lib/a.ex", """
        # Check that the overridden requirement shows up in the child dependency
        [%Mix.Dep{app: :git_repo, requirement: ">= 0.0.0"}] = Mix.Dep.cached()
        """)

        Mix.Tasks.Deps.Get.run([])
        Mix.Tasks.Deps.Compile.run([])
      end)
    end)
  end

  test "nested deps on only conflict" do
    # deps_repo wants all git_repo, git_repo is restricted to only test
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        loaded = Mix.Dep.load_on_environment([])
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :dev)
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :test)
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

        Mix.Tasks.Deps.run([])
        assert_received {:mix_shell, :info, ["* git_repo" <> _]}
        assert_received {:mix_shell, :info, [msg]}
        assert msg =~ "Remove the :only restriction from your dep"
      end)
    end)
  end

  test "nested deps on only conflict does not happen with optional deps" do
    Process.put(:custom_deps_git_repo_opts, optional: true)

    # deps_repo wants all git_repo, git_repo is restricted to only test
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        loaded = Mix.Dep.load_on_environment([])
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :dev)
        assert [:deps_repo] = Enum.map(loaded, & &1.app)
        assert [noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :test)
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)
      end)
    end)
  end

  test "nested deps with valid only subset" do
    # deps_repo wants git_repo for prod, git_repo is restricted to only prod and test
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: [:prod, :test]}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        loaded = Mix.Dep.load_on_environment([])
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :dev)
        assert [] = Enum.map(loaded, & &1.app)

        loaded = Mix.Dep.load_on_environment(env: :test)
        assert [:git_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :prod)
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)
      end)
    end)
  end

  test "nested deps with invalid only subset" do
    # deps_repo wants git_repo for dev, git_repo is restricted to only test
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :dev},
      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: [:test]}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        loaded = Mix.Dep.load_on_environment([])
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :dev)
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [divergedonly: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :test)
        assert [:git_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _] = Enum.map(loaded, & &1.status)

        Mix.Tasks.Deps.run([])
        assert_received {:mix_shell, :info, ["* git_repo" <> _]}
        assert_received {:mix_shell, :info, [msg]}
        assert msg =~ "Ensure you specify at least the same environments in :only in your dep"
      end)
    end)
  end

  test "nested deps with valid only in both parent and child" do
    Process.put(:custom_deps_git_repo_opts, only: :test)

    # deps_repo has environment set to test so it loads the deps_git_repo set to test too
    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", env: :test, only: [:dev, :test]},
      {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        loaded = Mix.Dep.load_on_environment([])
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :dev)
        assert [:deps_repo] = Enum.map(loaded, & &1.app)
        assert [noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :test)
        assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
        assert [unavailable: _, noappfile: _] = Enum.map(loaded, & &1.status)

        loaded = Mix.Dep.load_on_environment(env: :prod)
        assert [] = Enum.map(loaded, & &1.app)
      end)
    end)
  end

  test "nested deps converge and diverge when only is not in_upper" do
    loaded = fn deps ->
      with_deps(deps, fn ->
        in_fixture("deps_status", fn ->
          File.mkdir_p!("custom/other_repo")

          File.write!("custom/other_repo/mix.exs", """
          defmodule OtherRepo do
            use Mix.Project

            def project do
              [app: :other_repo,
               version: "0.1.0",
               deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
            end
          end
          """)

          Mix.ProjectStack.clear_cache()
          loaded = Mix.Dep.load_on_environment([])
          Enum.map(loaded, &{&1.app, &1.opts[:only]})
        end)
      end)
    end

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
      {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
    ]

    assert loaded.(deps) == [git_repo: [:test, :prod], other_repo: :test, deps_repo: :prod]

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
    ]

    assert loaded.(deps) == [git_repo: nil, other_repo: :test, deps_repo: nil]

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
      {:other_repo, "0.1.0", path: "custom/other_repo"}
    ]

    assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: :prod]

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
      {:other_repo, "0.1.0", path: "custom/other_repo"}
    ]

    assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: nil]

    Process.put(:custom_deps_git_repo_opts, optional: true)

    deps = [
      {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
      {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
    ]

    assert loaded.(deps) == [git_repo: :test, other_repo: :test, deps_repo: :prod]
  end

  test "nested deps converge and diverge when only is not specified" do
    Process.put(:custom_deps_git_repo_opts, only: :test)

    # Pass nil environment to mimic behaviour in umbrellas
    # where the children are loaded without an environment.
    deps = [
      {:abc_repo, "0.1.0", path: "custom/abc_repo", env: nil},
      {:deps_repo, "0.1.0", path: "custom/deps_repo", env: nil}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        File.mkdir_p!("custom/abc_repo")

        File.write!("custom/abc_repo/mix.exs", """
        defmodule OtherRepo do
          use Mix.Project

          def project do
            [app: :abc_repo,
             version: "0.1.0",
             deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
          end
        end
        """)

        Mix.Tasks.Deps.Get.run([])
        Mix.Tasks.Deps.Compile.run([])
        refute_receive {:mix_shell, :error, ["Could not compile :git_repo" <> _]}
      end)
    end)
  end

  test "deps_paths" do
    deps = [
      {:abc_repo, "0.1.0", path: "custom/abc_repo"},
      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
    ]

    with_deps(deps, fn ->
      in_fixture("deps_status", fn ->
        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) ==
                 [:git_repo, :abc_repo, :deps_repo]

        assert Map.keys(Mix.Project.deps_paths()) == [:abc_repo, :deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(depth: 1)) == [:abc_repo, :deps_repo]
        assert Map.keys(Mix.Project.deps_paths(depth: 2)) == [:abc_repo, :deps_repo, :git_repo]
        assert Map.keys(Mix.Project.deps_paths(depth: 3)) == [:abc_repo, :deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo])) == [:abc_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo])) == [:deps_repo, :git_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo])) == [:git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo], depth: 1)) == [:abc_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo], depth: 1)) == [:deps_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo], depth: 1)) == [:git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo], depth: 2)) == [:abc_repo]
        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo], depth: 2)) == [:git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo], depth: 2)) ==
                 [:deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo])) ==
                 [:abc_repo, :deps_repo, :git_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo], depth: 1)) ==
                 [:abc_repo, :deps_repo]

        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo], depth: 2)) ==
                 [:abc_repo, :deps_repo, :git_repo]
      end)
    end)
  end
end


even more error formatting

even more error formatting

defmodule Resemblixir.NoScenariosError do
  defexception [message: "No scenarios provided for Resemblixir to run! Assign scenarios to :scenarios in your :resemblixir config."]
  def exception(_) do
    %__MODULE__{}
  end
end

defmodule Resemblixir.ScenarioConfigError do
  defexception scenarios: []
  def message(_args) do

    """
    Resemblixir expects `Application.get_env(:resemblixir, :scenarios)` to return either a path to a json file, or a list of scenario maps.

    In your config file, you can either list out the scenarios directly in your :resemblixir config, like this:

          `config :resemblixir, scenarios: [%{name: "scenario_1", url: "http://localhost:4001", breakpoints: [...]}, ...`

    Or, alternatively, you can provide a path to a json file that will resolve to a list of maps:

          `config :resemblixir, scenarios: "/path/to/json_file.json"`

    Application.get_env(:resemblixir, :scenarios): #{:resemblixir |> Application.get_env(:scenarios) |> inspect()}
    """
  end
end

defmodule Resemblixir.MissingReferenceError do
  defexception [:message, :path, :breakpoint]
  def exception(args) do
    %__MODULE__{message: "Reference file not found at path: #{args[:path]}"}
  end
end

defmodule Resemblixir.NoBreakpointsError do
  defexception [:message, :scenario]
  def exception(args) do
    %__MODULE__{message: "Scenario #{args[:scenario]} has no breakpoints; please define a Keyword list of breakpoints for this scenario."}
  end
end

defmodule Resemblixir.TestFailure do
  defexception [:passed, :failed, :message]
  def message(args) do
    [
      "\n Some scenarios did not pass :(\n\n",
      "Failed scenarios:\n",
      failed_scenarios(args.failed),
      "\n\n",
      "Scenarios passing:\n",
      Enum.map(args.passed, & ["\s\s\s", &1.name, "\n" ])
    ]
    |> IO.iodata_to_binary()
  end

  defp failed_scenarios(failed) do
    for scenario <- failed do
      [
        "\s\s", scenario.name, ":\n",
        "\s\s\s\sFailing Breakpoints:\n",
        Enum.map(scenario.failed, &failed_breakpoint/1),
        "\n\n",
        "\s\s\s\sPassing Breakpoints:\n",
        Enum.map(scenario.passed, fn {name, _} -> ["\s\s\s\s", Atom.to_string(name), "\n"] end)
      ]
    end
  end

  defp failed_breakpoint({name, %Resemblixir.MissingReferenceError{}}) do
    ["\s\s\s\s", Atom.to_string(name), " -- reference image missing\n"]
  end
  defp failed_breakpoint({name, %Resemblixir.Compare{} = data}) do
    [
      "\s\s\s\s", Atom.to_string(name), " -- image mismatch:\n",
      data
      |> Map.from_struct()
      |> Enum.map(&format_breakpoint_value/1)
    ]
  end

  defp format_breakpoint_value({key, val}) do
    newline = if key == :test, do: "\n", else: ",\n"
    ["\s\s\s\s", Atom.to_string(key), ": ", do_format_breakpoint_value(key, val), newline]
  end

  defp do_format_breakpoint_value(:images, val) do
    [
      "%{\n",
      Enum.map(val, fn {name, path} -> ["\s\s\s\s\s\s", Atom.to_string(name), ": ", path, "\n"] end),
      "\s\s\s\s},\n"
    ]
  end
  defp do_format_breakpoint_value(_key, val), do: inspect(val)
end

defmodule Resemblixir.NoScenariosError do
  defexception [message: "No scenarios provided for Resemblixir to run! Assign scenarios to :scenarios in your :resemblixir config."]
  def exception(_) do
    %__MODULE__{}
  end
end

defmodule Resemblixir.ScenarioConfigError do
  defexception scenarios: []
  def message(_args) do

    """
    Resemblixir expects `Application.get_env(:resemblixir, :scenarios)` to return either a path to a json file, or a list of scenario maps.

    In your config file, you can either list out the scenarios directly in your :resemblixir config, like this:

          `config :resemblixir, scenarios: [%{name: "scenario_1", url: "http://localhost:4001", breakpoints: [...]}, ...`

    Or, alternatively, you can provide a path to a json file that will resolve to a list of maps:

          `config :resemblixir, scenarios: "/path/to/json_file.json"`

    Application.get_env(:resemblixir, :scenarios): #{:resemblixir |> Application.get_env(:scenarios) |> inspect()}
    """
  end
end

defmodule Resemblixir.MissingReferenceError do
  defexception [:message, :path, :breakpoint]
  def exception(args) do
    %__MODULE__{message: "Reference file not found at path: #{args[:path]}"}
  end
end

defmodule Resemblixir.NoBreakpointsError do
  defexception [:message, :scenario]
  def exception(args) do
    %__MODULE__{message: "Scenario #{args[:scenario]} has no breakpoints; please define a Keyword list of breakpoints for this scenario."}
  end
end

defmodule Resemblixir.TestFailure do
  defexception [:passed, :failed, :message]
  def message(args) do
    [
      "\n Some scenarios did not pass :(\n\n",
      "Failed scenarios:\n",
      failed_scenarios(args.failed),
      "\n\n",
      "Scenarios passing:\n",
      Enum.map(args.passed, & ["\s\s\s", &1.name, "\n" ])
    ]
    |> IO.iodata_to_binary()
  end

  defp failed_scenarios(failed) do
    for scenario <- failed do
      [
        "\s\s", scenario.name, ":\n",
        "\s\s\s\sFailing Breakpoints:\n",
        Enum.map(scenario.failed, &failed_breakpoint/1),
        "\n\n",
        "\s\s\s\sPassing Breakpoints:\n",
        Enum.map(scenario.passed, fn {name, _} -> ["\s\s\s\s", Atom.to_string(name), "\n"] end)
      ]
    end
  end

  defp failed_breakpoint({name, %Resemblixir.MissingReferenceError{}}) do
    ["\s\s\s\s", Atom.to_string(name), " -- reference image missing\n"]
  end
  defp failed_breakpoint({name, %Resemblixir.Compare{} = data}) do
    [
      "\s\s\s\s", Atom.to_string(name), " -- image mismatch:\n",
      data
      |> Map.from_struct()
      |> Enum.map(&format_breakpoint_value/1)
    ]
  end

  defp format_breakpoint_value({key, val}) do
    ["\s\s\s\s", Atom.to_string(key), " ", do_format_breakpoint_value(key, val), "\n"]
  end

  defp do_format_breakpoint_value(:images, val) do
    [
      "%{\n",
      Enum.map(val, fn {name, path} -> ["\s\s\s\s\s\s", Atom.to_string(name), ": ", path, "\n"] end),
      "}\n"
    ]
  end
  defp do_format_breakpoint_value(_key, val), do: inspect(val)
end


Update scheduler_test to be less strict.

When the scheduler was split up into two different processes
for executing cellery_script, checking times doesn't really make
sense. Asserting that they do infact execute in a certain
amount of time is fine

defmodule FarmbotCeleryScript.SchedulerTest do
  use ExUnit.Case, async: false
  alias FarmbotCeleryScript.{Scheduler, Compiler, AST}
  alias Farmbot.TestSupport.CeleryScript.TestSysCalls

  setup do
    {:ok, shim} = TestSysCalls.checkout()
    table = :"celery_scheduler_#{:rand.uniform(100)}"
    table = :ets.new(table, [:duplicate_bag, :named_table, :public])
    {:ok, sch} = Scheduler.start_link([table: table], [])
    [shim: shim, sch: sch, table: table]
  end

  test "uses default values when no parameter is found", %{table: sch} do
    sequence_ast =
      %{
        kind: :sequence,
        args: %{
          version: 1,
          locals: %{
            kind: :scope_declaration,
            args: %{},
            body: [
              %{
                kind: :parameter_declaration,
                args: %{
                  label: "foo",
                  default_value: %{
                    kind: :coordinate,
                    args: %{x: 129, y: 129, z: 129}
                  }
                }
              }
            ]
          }
        },
        body: [
          %{
            kind: :move_absolute,
            args: %{
              speed: 921,
              location: %{
                kind: :identifier,
                args: %{label: "foo"}
              },
              offset: %{
                kind: :coordinate,
                args: %{x: 0, y: 0, z: 0}
              }
            }
          }
        ]
      }
      |> AST.decode()

    executed = Compiler.compile(sequence_ast)
    me = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :move_absolute, args ->
          send(me, {:move_absolute, args})
          :ok

        :coordinate, [x, y, z] ->
          %{x: x, y: y, z: z}
      end)

    {:ok, execute_ref} = Scheduler.schedule(sch, executed)
    assert_receive {Scheduler, ^execute_ref, :ok}, 5000
    assert_receive {:move_absolute, [129, 129, 129, 921]}, 5000
  end

  test "syscall errors", %{table: sch} do
    execute_ast =
      %{
        kind: :rpc_request,
        args: %{label: "hello world"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    executed = Compiler.compile(execute_ast)

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :read_pin, _ -> {:error, "failed to read pin!"}
      end)

    {:ok, execute_ref} = Scheduler.schedule(sch, executed)
    assert_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}}, 5000
  end

  @tag :annoying
  test "regular exceptions still occur", %{table: table, sch: sch} do
    Process.flag(:trap_exit, true)

    execute_ast =
      %{
        kind: :rpc_request,
        args: %{label: "hello world"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    # {:ok, execute_ref} = Scheduler.schedule(sch, executed)
    # refute_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}}
    # assert_receive {:EXIT, ^sch, _}

    executed = Compiler.compile(execute_ast)

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :read_pin, _ -> raise("failed to read pin!")
      end)

    {:ok, execute_ref} = Scheduler.execute(table, executed)
    refute_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}}
    assert_receive {:EXIT, ^sch, _}, 1000
  end

  test "executing a sequence on top of a scheduled sequence", %{table: sch} do
    scheduled_ast =
      %{
        kind: :sequence,
        args: %{locals: %{kind: :variable_declaration, args: %{}}},
        body: [
          %{kind: :wait, args: %{milliseconds: 2000}},
          %{kind: :write_pin, args: %{pin_number: 1, pin_mode: 0, pin_value: 1}}
        ]
      }
      |> AST.decode()

    scheduled = Compiler.compile(scheduled_ast)

    execute_ast =
      %{
        kind: :rpc_request,
        args: %{label: "hello world"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    executed = Compiler.compile(execute_ast)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          send(pid, {:wait, :os.system_time()})
          Process.sleep(millis)

        :write_pin, _ ->
          send(pid, {:write_pin, :os.system_time()})
          :ok

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    {:ok, scheduled_ref} = Scheduler.schedule(sch, scheduled)
    {:ok, execute_ref} = Scheduler.schedule(sch, executed)

    assert_receive {Scheduler, ^scheduled_ref, :ok}, 5_000
    assert_receive {Scheduler, ^execute_ref, :ok}, 5_000

    assert_receive {:wait, time_1}
    assert_receive {:read_pin, time_2}
    assert_receive {:write_pin, time_3}

    assert [^time_1, ^time_3, ^time_2] = Enum.sort([time_1, time_2, time_3], &(&1 <= &2))
  end

  test "execute twice", %{table: sch} do
    execute_ast_1 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 1"},
        body: [
          %{kind: :wait, args: %{milliseconds: 1000}}
        ]
      }
      |> AST.decode()

    execute_ast_2 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 2"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    execute_1 = Compiler.compile(execute_ast_1)
    execute_2 = Compiler.compile(execute_ast_2)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          send(pid, {:wait, :os.system_time()})
          Process.sleep(millis)

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    task_1 =
      Task.async(fn ->
        {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1)
        assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000
      end)

    task_2 =
      Task.async(fn ->
        {:ok, execute_ref_2} = Scheduler.execute(sch, execute_2)
        assert_receive {Scheduler, ^execute_ref_2, :ok}, 3000
      end)

    _ = Task.await(task_1)
    _ = Task.await(task_2)

    assert_receive {:wait, _time_1}
    assert_receive {:read_pin, _time_2}
    # TODO(Connor) assert something about the time that these are executed
  end

  test "execute then schedule", %{table: sch} do
    execute_ast_1 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 1"},
        body: [
          %{kind: :wait, args: %{milliseconds: 1000}}
        ]
      }
      |> AST.decode()

    schedule_ast_1 =
      %{
        kind: :sequence,
        args: %{locals: %{kind: :variable_declaration, args: %{}}},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    execute_1 = Compiler.compile(execute_ast_1)
    schedule_1 = Compiler.compile(schedule_ast_1)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          send(pid, {:wait, :os.system_time()})
          Process.sleep(millis)

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    task_1 =
      Task.async(fn ->
        {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1)
        assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000
      end)

    task_2 =
      Task.async(fn ->
        {:ok, execute_ref_2} = Scheduler.execute(sch, schedule_1)
        assert_receive {Scheduler, ^execute_ref_2, :ok}, 3000
      end)

    _ = Task.await(task_1)
    _ = Task.await(task_2)

    assert_receive {:wait, _time_1}
    assert_receive {:read_pin, _time_2}
  end

  test "schedule and execute simultaneously", %{table: sch} do
    schedule_ast_1 =
      %{
        kind: :sequence,
        args: %{locals: %{kind: :variable_declaration, args: %{}}},
        body: [
          %{kind: :wait, args: %{milliseconds: 2500}}
        ]
      }
      |> AST.decode()

    execute_ast_1 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 1"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    schedule_1 = Compiler.compile(schedule_ast_1)
    execute_1 = Compiler.compile(execute_ast_1)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          Process.sleep(millis)
          send(pid, {:wait, :os.system_time()})

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    task_1 =
      Task.async(fn ->
        {:ok, schedule_ref_1} = Scheduler.schedule(sch, schedule_1)
        assert_receive {Scheduler, ^schedule_ref_1, :ok}, 5000
      end)

    task_2 =
      Task.async(fn ->
        {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1)
        assert_receive {Scheduler, ^execute_ref_1, :ok}, 5000
      end)

    _ = Task.await(task_1)
    _ = Task.await(task_2)

    assert_receive {:wait, _time_1}
    assert_receive {:read_pin, _time_2}
    # TODO(Connor) Assert something about these times.
  end
end

defmodule FarmbotCeleryScript.SchedulerTest do
  use ExUnit.Case, async: false
  alias FarmbotCeleryScript.{Scheduler, Compiler, AST}
  alias Farmbot.TestSupport.CeleryScript.TestSysCalls

  setup do
    {:ok, shim} = TestSysCalls.checkout()
    table = :"celery_scheduler_#{:rand.uniform(100)}"
    table = :ets.new(table, [:duplicate_bag, :named_table, :public])
    {:ok, sch} = Scheduler.start_link([table: table], [])
    [shim: shim, sch: sch, table: table]
  end

  test "uses default values when no parameter is found", %{table: sch} do
    sequence_ast =
      %{
        kind: :sequence,
        args: %{
          version: 1,
          locals: %{
            kind: :scope_declaration,
            args: %{},
            body: [
              %{
                kind: :parameter_declaration,
                args: %{
                  label: "foo",
                  default_value: %{
                    kind: :coordinate,
                    args: %{x: 129, y: 129, z: 129}
                  }
                }
              }
            ]
          }
        },
        body: [
          %{
            kind: :move_absolute,
            args: %{
              speed: 921,
              location: %{
                kind: :identifier,
                args: %{label: "foo"}
              },
              offset: %{
                kind: :coordinate,
                args: %{x: 0, y: 0, z: 0}
              }
            }
          }
        ]
      }
      |> AST.decode()

    executed = Compiler.compile(sequence_ast)
    me = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :move_absolute, args ->
          send(me, {:move_absolute, args})
          :ok

        :coordinate, [x, y, z] ->
          %{x: x, y: y, z: z}
      end)

    {:ok, execute_ref} = Scheduler.schedule(sch, executed)
    assert_receive {Scheduler, ^execute_ref, :ok}, 5000
    assert_receive {:move_absolute, [129, 129, 129, 921]}, 5000
  end

  test "syscall errors", %{table: sch} do
    execute_ast =
      %{
        kind: :rpc_request,
        args: %{label: "hello world"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    executed = Compiler.compile(execute_ast)

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :read_pin, _ -> {:error, "failed to read pin!"}
      end)

    {:ok, execute_ref} = Scheduler.schedule(sch, executed)
    assert_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}}, 5000
  end

  @tag :annoying
  test "regular exceptions still occur", %{table: table, sch: sch} do
    Process.flag(:trap_exit, true)

    execute_ast =
      %{
        kind: :rpc_request,
        args: %{label: "hello world"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    # {:ok, execute_ref} = Scheduler.schedule(sch, executed)
    # refute_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}}
    # assert_receive {:EXIT, ^sch, _}

    executed = Compiler.compile(execute_ast)

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :read_pin, _ -> raise("failed to read pin!")
      end)

    {:ok, execute_ref} = Scheduler.execute(table, executed)
    refute_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}}
    assert_receive {:EXIT, ^sch, _}, 1000
  end

  test "executing a sequence on top of a scheduled sequence", %{table: sch} do
    scheduled_ast =
      %{
        kind: :sequence,
        args: %{locals: %{kind: :variable_declaration, args: %{}}},
        body: [
          %{kind: :wait, args: %{milliseconds: 2000}},
          %{kind: :write_pin, args: %{pin_number: 1, pin_mode: 0, pin_value: 1}}
        ]
      }
      |> AST.decode()

    scheduled = Compiler.compile(scheduled_ast)

    execute_ast =
      %{
        kind: :rpc_request,
        args: %{label: "hello world"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    executed = Compiler.compile(execute_ast)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          send(pid, {:wait, :os.system_time()})
          Process.sleep(millis)

        :write_pin, _ ->
          send(pid, {:write_pin, :os.system_time()})
          :ok

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    {:ok, scheduled_ref} = Scheduler.schedule(sch, scheduled)
    {:ok, execute_ref} = Scheduler.schedule(sch, executed)

    assert_receive {Scheduler, ^scheduled_ref, :ok}, 5_000
    assert_receive {Scheduler, ^execute_ref, :ok}, 5_000

    assert_receive {:wait, time_1}
    assert_receive {:read_pin, time_2}
    assert_receive {:write_pin, time_3}

    assert [^time_1, ^time_3, ^time_2] = Enum.sort([time_1, time_2, time_3], &(&1 <= &2))
  end

  test "execute twice", %{table: sch} do
    execute_ast_1 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 1"},
        body: [
          %{kind: :wait, args: %{milliseconds: 1000}}
        ]
      }
      |> AST.decode()

    execute_ast_2 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 2"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    execute_1 = Compiler.compile(execute_ast_1)
    execute_2 = Compiler.compile(execute_ast_2)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          send(pid, {:wait, :os.system_time()})
          Process.sleep(millis)

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    task_1 =
      Task.async(fn ->
        {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1)
        assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000
      end)

    task_2 =
      Task.async(fn ->
        {:ok, execute_ref_2} = Scheduler.execute(sch, execute_2)
        assert_receive {Scheduler, ^execute_ref_2, :ok}, 3000
      end)

    _ = Task.await(task_1)
    _ = Task.await(task_2)

    assert_receive {:wait, time_1}
    assert_receive {:read_pin, time_2}

    assert time_2 - time_1 < 2000
  end

  test "execute then schedule", %{table: sch} do
    execute_ast_1 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 1"},
        body: [
          %{kind: :wait, args: %{milliseconds: 1000}}
        ]
      }
      |> AST.decode()

    schedule_ast_1 =
      %{
        kind: :sequence,
        args: %{locals: %{kind: :variable_declaration, args: %{}}},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    execute_1 = Compiler.compile(execute_ast_1)
    schedule_1 = Compiler.compile(schedule_ast_1)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          send(pid, {:wait, :os.system_time()})
          Process.sleep(millis)

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    task_1 =
      Task.async(fn ->
        {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1)
        assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000
      end)

    task_2 =
      Task.async(fn ->
        {:ok, execute_ref_2} = Scheduler.execute(sch, schedule_1)
        assert_receive {Scheduler, ^execute_ref_2, :ok}, 3000
      end)

    _ = Task.await(task_1)
    _ = Task.await(task_2)

    assert_receive {:wait, _time_1}
    assert_receive {:read_pin, _time_2}
  end

  test "schedule and execute simultaneously", %{table: sch} do
    schedule_ast_1 =
      %{
        kind: :sequence,
        args: %{locals: %{kind: :variable_declaration, args: %{}}},
        body: [
          %{kind: :wait, args: %{milliseconds: 2500}}
        ]
      }
      |> AST.decode()

    execute_ast_1 =
      %{
        kind: :rpc_request,
        args: %{label: "hello world 1"},
        body: [
          %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}}
        ]
      }
      |> AST.decode()

    schedule_1 = Compiler.compile(schedule_ast_1)
    execute_1 = Compiler.compile(execute_ast_1)

    pid = self()

    :ok =
      TestSysCalls.handle(TestSysCalls, fn
        :wait, [millis] ->
          Process.sleep(millis)
          send(pid, {:wait, :os.system_time()})

        :read_pin, _ ->
          send(pid, {:read_pin, :os.system_time()})
          1
      end)

    task_1 =
      Task.async(fn ->
        {:ok, schedule_ref_1} = Scheduler.schedule(sch, schedule_1)
        # TODO(Connor) Literally any function call will
        # make this not a race condition???
        IO.inspect(schedule_ref_1, label: "task_1")
        assert_receive {Scheduler, ^schedule_ref_1, :ok}, 5000
      end)

    task_2 =
      Task.async(fn ->
        {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1)
        IO.inspect(execute_ref_1, label: "task_2")
        assert_receive {Scheduler, ^execute_ref_1, :ok}, 5000
      end)

    _ = Task.await(task_1)
    _ = Task.await(task_2)

    assert_receive {:wait, time_1}
    assert_receive {:read_pin, time_2}

    # Assert that the read pin executed and finished before the wait.
    assert time_2 <= time_1
  end
end


No stop shuold not trigger compiler.

No stop shuold not trigger compiler.

defrecord Elixir::CLI::Config, commands: [], close: [],
  output: '.', compile: false, stop: true, compile_options: []

defmodule Elixir::CLI do
  import Exception, only: [format_stacktrace: 1]

  # Invoked directly from erlang boot process. It parses all argv
  # options and execute them in the order they are specified.
  def process_argv(options) do
    { config, argv } = process_options(options, Elixir::CLI::Config.new)
    Erlang.gen_server.call(:elixir_code_server, { :argv, argv })

    if config.compile do
      Erlang.file.make_dir(config.output)
    end

    all_commands = List.reverse(config.commands) ++ List.reverse(config.close)

    try do
      Enum.map all_commands, process_command(&1, config)
      if config.stop do
        at_exit(0)
        stop(0)
      end
    rescue: exception
      at_exit(1)
      IO.puts :standard_error, "** (#{exception.__record__(:name)}) #{exception.message}"
      print_stacktrace(Code.stacktrace)
      stop(1)
    catch: :exit, reason when is_integer(reason)
      at_exit(reason)
      stop(reason)
    catch: kind, reason
      at_exit(1)
      IO.puts :standard_error, "** (#{kind}) #{inspect(reason)}"
      print_stacktrace(Code.stacktrace)
      stop(1)
    end
  end

  ## Private

  defp stop(status) do
    Erlang.init.stop(status)
  end

  defp at_exit(status) do
    hooks = Erlang.gen_server.call(:elixir_code_server, :at_exit)
    lc hook in hooks do
      try do
        hook.(status)
      rescue: exception
        IO.puts :standard_error, "** (#{exception.__record__(:name)}) #{exception.message}"
        print_stacktrace(Code.stacktrace)
      catch: kind, reason
        IO.puts :standard_error, "** #{kind} #{inspect(reason)}"
        print_stacktrace(Code.stacktrace)
      end
    end
  end

  defp invalid_option(option) do
    IO.puts(:standard_error, "Unknown option #{list_to_binary(option)}")
    stop(1)
  end

  defp shared_option?(list, config, callback) do
    case process_shared(list, config) do
    match: { [h|t], _ } when h == hd(list)
      invalid_option h
    match: { new_list, new_config }
      callback.(new_list, new_config)
    end
  end

  defp print_stacktrace(stacktrace) do
    Enum.each stacktrace, fn(s, do: IO.puts :standard_error, "    #{format_stacktrace(s)}")
  end

  # Process shared options

  defp process_shared(['-v'|t], config) do
    IO.puts "Elixir #{Code.version}"
    process_shared t, config
  end

  defp process_shared(['-e',h|t], config) do
    process_shared t, config.prepend_commands [{:eval,h}]
  end

  defp process_shared(['-pa',h|t], config) do
    Code.prepend_path(h)
    process_shared t, config
  end

  defp process_shared(['-pz',h|t], config) do
    Code.append_path(h)
    process_shared t, config
  end

  defp process_shared(['-r',h|t], config) do
    process_shared t, config.prepend_commands [{:require,h}]
  end

  defp process_shared(list, config) do
    { list, config }
  end

  # Process init options

  defp process_options(['--'|t], config) do
    { config, t }
  end

  defp process_options(['--no-stop'|t], config) do
    process_options t, config.stop(false)
  end

  defp process_options(['+compile'|t], config) do
    process_compiler t, config.compile(true)
  end

  defp process_options([h|t] = list, config) do
    case h do
    match: '-' ++ _
      shared_option? list, config, process_options(&1, &2)
    else:
      { config.prepend_commands([{:require, h}]), t }
    end
  end

  defp process_options([], config) do
    { config, [] }
  end

  # Process compiler options

  defp process_compiler(['--'|t], config) do
    { config, t }
  end

  defp process_compiler(['-o',h|t], config) do
    process_compiler t, config.output(h)
  end

  defp process_compiler(['--docs'|t], config) do
    process_compiler t, config.merge_compile_options(docs: true)
  end

  defp process_compiler(['--debug-info'|t], config) do
    process_compiler t, config.merge_compile_options(debug_info: true)
  end

  # This option is used internally so we can compile
  # Elixir with Elixir without raising module conflicts
  defp process_compiler(['--ignore-module-conflict'|t], config) do
    process_compiler t, config.merge_compile_options(ignore_module_conflict: true)
  end

  defp process_compiler([h|t] = list, config) do
    case h do
    match: '-' ++ _
      shared_option? list, config, process_compiler(&1, &2)
    else:
      process_compiler t, config.prepend_commands[{:compile,h}]
    end
  end

  defp process_compiler([], config) do
    { config, [] }
  end

  # Process commands

  defp process_command({:eval, expr}, _config) do
    Erlang.elixir.eval(expr, [])
  end

  defp process_command({:require, file}, _config) do
    Enum.each File.wildcard(file), Code.require_file(&1)
  end

  defp process_command({:compile, pattern}, config) do
    compile_patterns [pattern], config
  end

  defp compile_patterns(lines, config) do
    lines  = Enum.map lines, File.wildcard(&1)
    concat = List.uniq(List.append(lines))

    Enum.map concat, fn(file) ->
      IO.puts "Compiling #{list_to_binary(file)}"
      Code.compile_file_to_dir(file, config.output, config.compile_options)
    end
  end
end

defrecord Elixir::CLI::Config, commands: [], close: [],
  output: '.', compile: false, stop: true, compile_options: []

defmodule Elixir::CLI do
  import Exception, only: [format_stacktrace: 1]

  # Invoked directly from erlang boot process. It parses all argv
  # options and execute them in the order they are specified.
  def process_argv(options) do
    { config, argv } = process_options(options, Elixir::CLI::Config.new)
    Erlang.gen_server.call(:elixir_code_server, { :argv, argv })

    if config.compile do
      Erlang.file.make_dir(config.output)
    end

    all_commands = List.reverse(config.commands) ++ List.reverse(config.close)

    try do
      Enum.map all_commands, process_command(&1, config)
      if config.stop do
        at_exit(0)
        stop(0)
      end
    rescue: exception
      at_exit(1)
      IO.puts :standard_error, "** (#{exception.__record__(:name)}) #{exception.message}"
      print_stacktrace(Code.stacktrace)
      stop(1)
    catch: :exit, reason when is_integer(reason)
      at_exit(reason)
      stop(reason)
    catch: kind, reason
      at_exit(1)
      IO.puts :standard_error, "** (#{kind}) #{inspect(reason)}"
      print_stacktrace(Code.stacktrace)
      stop(1)
    end
  end

  ## Private

  defp stop(status) do
    Erlang.init.stop(status)
  end

  defp at_exit(status) do
    hooks = Erlang.gen_server.call(:elixir_code_server, :at_exit)
    lc hook in hooks do
      try do
        hook.(status)
      rescue: exception
        IO.puts :standard_error, "** (#{exception.__record__(:name)}) #{exception.message}"
        print_stacktrace(Code.stacktrace)
      catch: kind, reason
        IO.puts :standard_error, "** #{kind} #{inspect(reason)}"
        print_stacktrace(Code.stacktrace)
      end
    end
  end

  defp invalid_option(option) do
    IO.puts(:standard_error, "Unknown option #{list_to_binary(option)}")
    stop(1)
  end

  defp shared_option?(list, config, callback) do
    case process_shared(list, config) do
    match: { [h|t], _ } when h == hd(list)
      invalid_option h
    match: { new_list, new_config }
      callback.(new_list, new_config)
    end
  end

  defp print_stacktrace(stacktrace) do
    Enum.each stacktrace, fn(s, do: IO.puts :standard_error, "    #{format_stacktrace(s)}")
  end

  # Process shared options

  defp process_shared(['-v'|t], config) do
    IO.puts "Elixir #{Code.version}"
    process_shared t, config
  end

  defp process_shared(['-e',h|t], config) do
    process_shared t, config.prepend_commands [{:eval,h}]
  end

  defp process_shared(['-pa',h|t], config) do
    Code.prepend_path(h)
    process_shared t, config
  end

  defp process_shared(['-pz',h|t], config) do
    Code.append_path(h)
    process_shared t, config
  end

  defp process_shared(['-r',h|t], config) do
    process_shared t, config.prepend_commands [{:require,h}]
  end

  defp process_shared(list, config) do
    { list, config }
  end

  # Process init options

  defp process_options(['--'|t], config) do
    { config, t }
  end

  defp process_options(['--no-stop'|t], config) do
    process_compiler t, config.stop(false)
  end

  defp process_options(['+compile'|t], config) do
    process_compiler t, config.compile(true)
  end

  defp process_options([h|t] = list, config) do
    case h do
    match: '-' ++ _
      shared_option? list, config, process_options(&1, &2)
    else:
      { config.prepend_commands([{:require, h}]), t }
    end
  end

  defp process_options([], config) do
    { config, [] }
  end

  # Process compiler options

  defp process_compiler(['--'|t], config) do
    { config, t }
  end

  defp process_compiler(['-o',h|t], config) do
    process_compiler t, config.output(h)
  end

  defp process_compiler(['--docs'|t], config) do
    process_compiler t, config.merge_compile_options(docs: true)
  end

  defp process_compiler(['--debug-info'|t], config) do
    process_compiler t, config.merge_compile_options(debug_info: true)
  end

  # This option is used internally so we can compile
  # Elixir with Elixir without raising module conflicts
  defp process_compiler(['--ignore-module-conflict'|t], config) do
    process_compiler t, config.merge_compile_options(ignore_module_conflict: true)
  end

  defp process_compiler([h|t] = list, config) do
    case h do
    match: '-' ++ _
      shared_option? list, config, process_compiler(&1, &2)
    else:
      process_compiler t, config.prepend_commands[{:compile,h}]
    end
  end

  defp process_compiler([], config) do
    { config, [] }
  end

  # Process commands

  defp process_command({:eval, expr}, _config) do
    Erlang.elixir.eval(expr, [])
  end

  defp process_command({:require, file}, _config) do
    Enum.each File.wildcard(file), Code.require_file(&1)
  end

  defp process_command({:compile, pattern}, config) do
    compile_patterns [pattern], config
  end

  defp compile_patterns(lines, config) do
    lines  = Enum.map lines, File.wildcard(&1)
    concat = List.uniq(List.append(lines))

    Enum.map concat, fn(file) ->
      IO.puts "Compiling #{list_to_binary(file)}"
      Code.compile_file_to_dir(file, config.output, config.compile_options)
    end
  end
end