4.4 KiB · text History b55f15e
defmodule GitGud.WorkflowsDurationsTest do
  @moduledoc """
  The run-duration history behind the CI charts.

  What the charts need and the run list doesn't: only finished runs,
  oldest first, with the duration computed once in the database rather
  than re-derived per view.
  """
  use GitGud.DataCase, async: false

  import GitGud.ForgeFixtures

  alias GitGud.Workflows
  alias GitGud.Workflows.WorkflowRun

  setup do
    {user, repo} = repository_fixture()
    %{user: user, repo: repo}
  end

  defp run(repo, number, opts) do
    # `:utc_datetime` columns reject microseconds, and `utc_now/0` has
    # them.
    default_start = DateTime.add(DateTime.utc_now(:second), -number * 60, :second)
    started = Keyword.get(opts, :started, default_start)
    seconds = Keyword.get(opts, :seconds)

    %WorkflowRun{repository_id: repo.id}
    |> WorkflowRun.create_changeset(%{
      number: number,
      workflow_path: ".forgejo/workflows/build.yml",
      trigger: "push",
      head_sha: :crypto.strong_rand_bytes(20)
    })
    |> Ecto.Changeset.change(
      status: Keyword.get(opts, :status, "success"),
      conclusion: Keyword.get(opts, :conclusion, "success"),
      started_at: started,
      completed_at: seconds && DateTime.add(started, seconds, :second)
    )
    |> GitGud.Repo.insert!()
  end

  describe "run_durations/2" do
    test "returns finished runs oldest first, with their duration", %{repo: repo} do
      run(repo, 1, seconds: 30, started: ~U[2026-09-01 10:00:00Z])
      run(repo, 2, seconds: 90, started: ~U[2026-09-02 10:00:00Z])

      assert [first, second] = Workflows.run_durations(repo)

      assert first.number == 1
      assert first.seconds == 30
      assert second.number == 2
      assert second.seconds == 90
    end

    test "skips runs that haven't finished", %{repo: repo} do
      run(repo, 1, seconds: 30)
      run(repo, 2, seconds: nil, status: "running", conclusion: nil)

      assert [only] = Workflows.run_durations(repo)
      assert only.number == 1
    end

    test "keeps failures — a slow failing run is the interesting one", %{repo: repo} do
      run(repo, 1, seconds: 10, status: "failure", conclusion: "failure")

      assert [point] = Workflows.run_durations(repo)
      assert point.conclusion == "failure"
    end

    test "honours the limit, keeping the most recent", %{repo: repo} do
      for n <- 1..5,
          do:
            run(repo, n,
              seconds: n,
              started: DateTime.add(~U[2026-09-01 00:00:00Z], n * 3600, :second)
            )

      points = Workflows.run_durations(repo, limit: 2)

      assert Enum.map(points, & &1.number) == [4, 5]
    end

    test "a clock that went backwards doesn't make a negative bar", %{repo: repo} do
      run(repo, 1, seconds: -30)

      assert [point] = Workflows.run_durations(repo)
      assert point.seconds == 0
    end
  end

  describe "run_durations_for_repos/2" do
    test "groups by repository in one query", %{repo: repo, user: user} do
      {_user, other} = repository_fixture(%{owner: user})
      run(repo, 1, seconds: 10)
      run(other, 1, seconds: 20)

      histories = Workflows.run_durations_for_repos([repo.id, other.id])

      assert [%{seconds: 10}] = histories[repo.id]
      assert [%{seconds: 20}] = histories[other.id]
    end

    test "caps each repository's series independently", %{repo: repo} do
      for n <- 1..5,
          do:
            run(repo, n,
              seconds: n,
              started: DateTime.add(~U[2026-09-01 00:00:00Z], n * 3600, :second)
            )

      assert [%{number: 4}, %{number: 5}] =
               Workflows.run_durations_for_repos([repo.id], per_repo: 2)[repo.id]
    end

    test "is empty for no repositories, rather than querying for none" do
      assert Workflows.run_durations_for_repos([]) == %{}
    end
  end

  describe "median_duration/1" do
    test "is the middle of an odd series" do
      assert Workflows.median_duration([%{seconds: 10}, %{seconds: 90}, %{seconds: 20}]) == 20
    end

    test "averages the two middles of an even one" do
      assert Workflows.median_duration([%{seconds: 10}, %{seconds: 20}]) == 15
    end

    test "resists one pathological run, which a mean would not" do
      points = Enum.map([10, 10, 10, 10, 6000], &%{seconds: &1})

      assert Workflows.median_duration(points) == 10
    end

    test "is nil for nothing" do
      assert Workflows.median_duration([]) == nil
    end
  end
end