lib/mix/tasks/k6.ex

defmodule Mix.Tasks.K6 do
  @moduledoc """
  Runs k6 commands

  The command runs using the binary installed locally in the `_build` folder of the project.
  If k6 is not installed for the local project, it will automatically install it before running the command.

  k6 is executed in the folder where k6 tests reside, which defaults to `priv/k6`.

  ## Examples

      $ mix k6 run my_test.js
      $ mix k6 run --vus 10 --duration 30s my_test.js
  """
  use Mix.Task

  @shortdoc "Runs k6"
  def run(args) when is_list(args) do
    unless correct_version_installed?(), do: Mix.Task.run("k6.install")

    test_dir = tests_directory()
    unless File.exists?(test_dir), do: File.mkdir_p(test_dir)

    wrapper_args = [binary_path() | args]

    options = [
      :nouse_stdio,
      :exit_status,
      args: wrapper_args,
      cd: String.to_charlist(test_dir),
      env: k6_env()
    ]

    port = Port.open({:spawn_executable, wrapper_path()}, options)

    receive do
      {^port, {:exit_status, exit_status}} -> exit({:shutdown, exit_status})
    end
  end

  defp k6_env do
    stringify = fn s -> String.to_charlist(to_string(s)) end

    :k6
    |> Application.get_env(:env, [])
    |> Enum.into([], fn {k, v} -> {stringify.(k), stringify.(v)} end)
  end

  defp binary_path, do: Path.join(Path.dirname(Mix.Project.build_path()), "k6")
  defp wrapper_path, do: Path.join(Application.app_dir(:k6), "priv/wrapper.sh")
  defp tests_directory, do: Application.get_env(:k6, :workdir, "priv/k6")

  defp correct_version_installed? do
    path = binary_path()

    if File.exists?(path) do
      expected_version = Application.get_env(:k6, :version, "v0.39.0")
      installed_version() == expected_version
    else
      false
    end
  end

  defp installed_version do
    {res, 0 = _exit_code} = System.cmd(binary_path(), ["version"])
    ["k6", version | _] = String.split(res, " ", trim: true)
    version
  end
end