defmodule GitGud.BadgesTest do
use GitGud.DataCase, async: false
import GitGud.ForgeFixtures
alias GitGud.Badges
alias GitGud.Git
alias GitGud.WorkflowGitHelpers
alias GitGud.Workflows
alias GitGud.Workflows.WorkflowJob
alias GitGud.Workflows.WorkflowRun
alias GitGud.Workflows.Yaml
setup do
{_user, repo} = repository_fixture()
{:ok, repo: repo}
end
defp run!(repo, attrs) do
%WorkflowRun{repository_id: repo.id}
|> WorkflowRun.create_changeset(
Map.merge(
%{
"number" => System.unique_integer([:positive]),
"workflow_path" => ".forgejo/workflows/ci.yml",
"trigger" => "push",
"head_ref" => "refs/heads/#{repo.default_branch}",
"head_sha" => :crypto.strong_rand_bytes(20)
},
attrs
)
)
|> Ecto.Changeset.change(status: attrs["status"] || "success")
|> Repo.insert!()
end
describe "ci" do
test "none before any run", %{repo: repo} do
assert Badges.content(repo, "ci") == %{label: "ci", value: "none", slot: "muted"}
end
test "the newest push to the default branch decides", %{repo: repo} do
run!(repo, %{"status" => "failure"})
run!(repo, %{"status" => "success"})
assert %{value: "passing", slot: "success"} = Badges.content(repo, "ci")
end
test "scheduled and hand-run runs don't count", %{repo: repo} do
run!(repo, %{"status" => "success"})
run!(repo, %{"status" => "failure", "trigger" => "schedule"})
run!(repo, %{"status" => "failure", "trigger" => "workflow_dispatch"})
assert %{value: "passing"} = Badges.content(repo, "ci")
end
test "pull request runs and other branches don't count", %{repo: repo} do
run!(repo, %{"status" => "success"})
run!(repo, %{"status" => "failure", "head_ref" => "refs/heads/feature"})
run!(repo, %{
"status" => "failure",
"head_ref" => "refs/heads/#{repo.default_branch}",
"pull_request_number" => 7
})
assert %{value: "passing"} = Badges.content(repo, "ci")
end
test "running is info and queued is warning" do
assert %{slot: "info"} = Badges.ci("running")
assert %{slot: "warning"} = Badges.ci("queued")
assert %{slot: "muted"} = Badges.ci("cancelled")
end
end
describe "last-commit" do
test "none on an empty repository", %{repo: repo} do
assert %{value: "none", slot: "muted"} = Badges.content(repo, "last-commit")
end
test "today for a fresh commit", %{repo: repo} do
sha = WorkflowGitHelpers.commit!(repo, %{"README.md" => "hi"}, [])
{:ok, bin} = Git.from_hex(sha)
:ok = Git.update_ref(repo.disk_path, "refs/heads/#{repo.default_branch}", bin, nil)
assert %{value: "today", slot: "success"} = Badges.content(repo, "last-commit")
end
test "ages turn yellow at 30 days and red at 180" do
assert %{value: "1 day", slot: "success"} = Badges.age(1)
assert %{slot: "warning"} = Badges.age(30)
assert %{value: "213 days", slot: "error"} = Badges.age(213)
end
end
describe "coverage" do
test "a job's gitgud-coverage output becomes the badge", %{repo: repo} do
sha = :crypto.strong_rand_bytes(20)
{:ok, doc} =
Yaml.parse("""
name: ci
on: push
jobs:
test:
runs-on: ubuntu-latest
outputs:
gitgud-coverage: ${{ steps.cov.outputs.percent }}
steps:
- id: cov
run: ./cover
""")
seed =
run!(repo, %{
"head_sha" => sha,
"workflow_name" => doc.name,
"workflow_doc" => doc.raw,
"status" => "queued"
})
{:ok, run} = Workflows.rerun(seed)
job = Repo.get_by!(WorkflowJob, workflow_run_id: run.id)
assert %{value: "none"} = Badges.content(repo, "coverage")
{:ok, job} = Workflows.set_outputs(job, %{"gitgud-coverage" => "67.25%"})
{:ok, _} = Workflows.update_job_status(job, "success")
assert %{value: "67.3%", slot: "warning"} = Badges.content(repo, "coverage")
assert Enum.any?(
Workflows.list_commit_statuses(repo, sha),
&(&1.context == "coverage" and &1.description == "67.3%")
)
end
test "a failed job reports no coverage", %{repo: repo} do
{:ok, doc} =
Yaml.parse("""
name: ci
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: ./cover
""")
seed = run!(repo, %{"workflow_doc" => doc.raw, "status" => "queued"})
{:ok, run} = Workflows.rerun(seed)
job = Repo.get_by!(WorkflowJob, workflow_run_id: run.id)
{:ok, job} = Workflows.set_outputs(job, %{"gitgud-coverage" => "99"})
{:ok, _} = Workflows.update_job_status(job, "failure")
assert %{value: "none"} = Badges.content(repo, "coverage")
end
test "parsing the reported figure" do
assert Badges.parse_percent("87.3") == 87.3
assert Badges.parse_percent(" 87.3% ") == 87.3
assert Badges.parse_percent("100") == 100.0
assert Badges.parse_percent("101") == nil
assert Badges.parse_percent("lots") == nil
assert Badges.parse_percent(nil) == nil
end
test "thresholds" do
assert %{slot: "success"} = Badges.coverage(80.0)
assert %{slot: "warning"} = Badges.coverage(60.0)
assert %{slot: "error", value: "41.0%"} = Badges.coverage(41)
end
end
describe "workflow" do
@audit """
name: audit
on:
schedule:
- cron: "17 6 * * *"
jobs:
osv:
runs-on: ubuntu-latest
outputs:
gitgud-conclusion: ${{ steps.scan.outputs.conclusion }}
gitgud-summary: ${{ steps.scan.outputs.summary }}
steps:
- id: scan
run: ./scan
image:
runs-on: ubuntu-latest
steps:
- run: ./image
"""
defp audit_run!(repo, yaml \\ @audit, attrs \\ %{}) do
{:ok, doc} = Yaml.parse(yaml)
seed =
run!(
repo,
Map.merge(
%{
"trigger" => "schedule",
"workflow_path" => ".forgejo/workflows/audit.yml",
"workflow_name" => doc.name,
"workflow_doc" => doc.raw,
"status" => "queued"
},
attrs
)
)
{:ok, run} = Workflows.rerun(seed)
Map.new(
Repo.all(from j in WorkflowJob, where: j.workflow_run_id == ^run.id),
&{&1.job_id, &1}
)
end
defp finish!(job, status, outputs \\ %{}) do
{:ok, job} = Workflows.set_outputs(job, outputs)
{:ok, job} = Workflows.update_job_status(job, status)
job
end
test "none before it has run", %{repo: repo} do
assert Badges.workflow(repo, "audit") == %{label: "audit", value: "none", slot: "muted"}
end
test "a warning says what the job found", %{repo: repo} do
jobs = audit_run!(repo)
finish!(jobs["osv"], "success", %{
"gitgud-conclusion" => "warning",
"gitgud-summary" => "1 no fix"
})
finish!(jobs["image"], "success")
assert Badges.workflow(repo, "audit") == %{
label: "audit",
value: "1 no fix",
slot: "warning"
}
assert "audit" in Badges.workflow_names(repo)
end
test "the worst job decides, and only its summary shows", %{repo: repo} do
jobs = audit_run!(repo)
finish!(jobs["osv"], "failure", %{"gitgud-summary" => "2 to fix, 1 no fix"})
finish!(jobs["image"], "success", %{
"gitgud-conclusion" => "warning",
"gitgud-summary" => "4 image updates"
})
assert %{value: "2 to fix, 1 no fix", slot: "error"} = Badges.workflow(repo, "audit")
end
test "all passing says passing", %{repo: repo} do
jobs = audit_run!(repo)
finish!(jobs["osv"], "success", %{"gitgud-conclusion" => "success"})
finish!(jobs["image"], "success")
assert %{value: "passing", slot: "success"} = Badges.workflow(repo, "audit")
end
test "a warning without a summary says warning", %{repo: repo} do
jobs = audit_run!(repo)
finish!(jobs["osv"], "success", %{"gitgud-conclusion" => "warning"})
finish!(jobs["image"], "running")
assert %{value: "warning", slot: "warning"} = Badges.workflow(repo, "audit")
end
test "the newest run counts, and pull request runs don't", %{repo: repo} do
jobs = audit_run!(repo)
finish!(jobs["osv"], "failure")
finish!(jobs["image"], "success")
jobs = audit_run!(repo)
finish!(jobs["osv"], "success")
finish!(jobs["image"], "success")
jobs = audit_run!(repo, @audit, %{"trigger" => "pull_request", "pull_request_number" => 3})
finish!(jobs["osv"], "failure")
assert %{value: "passing"} = Badges.workflow(repo, "audit")
end
test "a nameless workflow goes by its file", %{repo: repo} do
jobs =
audit_run!(repo, String.replace(@audit, "name: audit\n", ""), %{
"workflow_path" => ".forgejo/workflows/nightly.yml"
})
finish!(jobs["osv"], "success")
finish!(jobs["image"], "success")
assert %{label: "nightly", value: "passing"} = Badges.workflow(repo, "nightly")
assert "nightly" in Badges.workflow_names(repo)
end
test "the summary also lands on the job's check", %{repo: repo} do
jobs = audit_run!(repo)
osv =
finish!(jobs["osv"], "success", %{
"gitgud-conclusion" => "warning",
"gitgud-summary" => "1 no fix"
})
run = Repo.get!(WorkflowRun, osv.workflow_run_id)
assert [status] =
repo
|> Workflows.list_commit_statuses(run.head_sha)
|> Enum.filter(&(&1.context =~ "osv"))
assert status.state == "warning"
assert status.description =~ ~r/^Warning.*: 1 no fix$/
end
test "summaries are one trimmed line" do
assert Badges.job_summary(%WorkflowJob{outputs: %{"gitgud-summary" => " 1 no fix\n"}}) ==
"1 no fix"
assert Badges.job_summary(%WorkflowJob{outputs: %{"gitgud-summary" => " "}}) == nil
assert Badges.job_summary(%WorkflowJob{outputs: %{}}) == nil
long = String.duplicate("x", 80)
assert String.length(Badges.job_summary(%WorkflowJob{outputs: %{"gitgud-summary" => long}})) ==
40
end
end
describe "counts" do
test "open issues and pull requests", %{repo: repo} do
{user, _} = repository_fixture()
issue_fixture(repo, user)
issue_fixture(repo, user)
assert Badges.content(repo, "issues") == %{label: "issues", value: "2 open", slot: "info"}
assert %{label: "pulls", value: "0 open"} = Badges.content(repo, "pulls")
end
test "commits on the default branch", %{repo: repo} do
assert %{value: "none", slot: "muted"} = Badges.content(repo, "commits")
first = WorkflowGitHelpers.commit!(repo, %{"a" => "1"}, [])
second = WorkflowGitHelpers.commit!(repo, %{"a" => "2"}, [first])
{:ok, bin} = Git.from_hex(second)
:ok = Git.update_ref(repo.disk_path, "refs/heads/#{repo.default_branch}", bin, nil)
assert %{label: "commits", value: "2", slot: "info"} = Badges.content(repo, "commits")
end
test "digits are grouped" do
assert Badges.group_digits(7) == "7"
assert Badges.group_digits(1234) == "1,234"
assert Badges.group_digits(1_234_567) == "1,234,567"
end
end
describe "package" do
test "install with the format's tool, at the latest version", %{repo: repo} do
{user, _} = repository_fixture()
for version <- ~w(0.4.0 0.4.1) do
{:ok, _, _, _} =
GitGud.Packages.publish_file(repo, "pypi", "Widget", version, %{
filename: "widget-#{version}.tar.gz",
body: "sdist #{version}",
content_type: "application/x-tar",
uploader: user
})
end
assert Badges.package(repo, "pypi", "widget") ==
%{label: "install with pip", value: "Widget 0.4.1", slot: "info"}
assert {"pypi", "Widget"} in Badges.packages(repo)
end
test "none for a package that isn't there", %{repo: repo} do
assert %{label: "install with cargo", value: "none", slot: "muted"} =
Badges.package(repo, "cargo", "nope")
end
end
describe "more of the repository" do
defp commit_row(repo, email) do
%{
repository_id: repo.id,
sha: :crypto.strong_rand_bytes(20),
tree_sha: :crypto.strong_rand_bytes(20),
parent_shas: [],
author_name: "t",
author_email: email,
authored_at: DateTime.utc_now(:second),
committer_name: "t",
committer_email: email,
committed_at: DateTime.utc_now(:second),
message: "m"
}
end
test "contributors are distinct authors", %{repo: repo} do
Repo.insert_all(GitGud.Repositories.GitCommit, [
commit_row(repo, "a@e"),
commit_row(repo, "a@e"),
commit_row(repo, "b@e")
])
assert %{label: "contributors", value: "2"} = Badges.content(repo, "contributors")
end
test "the newest tag", %{repo: repo} do
assert %{value: "none"} = Badges.content(repo, "release")
sha = WorkflowGitHelpers.commit!(repo, %{"a" => "1"}, [])
{:ok, bin} = Git.from_hex(sha)
for tag <- ~w(v1.9.0 v1.10.0 v2.0.0-rc.1 nightly) do
:ok = Git.update_ref(repo.disk_path, "refs/tags/#{tag}", bin, nil)
end
assert %{label: "release", value: "v1.10.0", slot: "info"} = Badges.content(repo, "release")
end
test "picking a tag" do
assert Badges.pick_tag(~w(v1.2.0 v1.10.0 v1.9.9)) == "v1.10.0"
assert Badges.pick_tag(~w(1.0.0-rc.1 1.0.0-rc.2)) == "1.0.0-rc.2"
assert Badges.pick_tag(~w(build-9 build-10 build-2)) == "build-10"
assert Badges.pick_tag([]) == nil
end
test "license", %{repo: repo} do
assert %{value: "none", slot: "muted"} = Badges.content(repo, "license")
assert %{value: "MIT", slot: "info"} = Badges.content(%{repo | license: "MIT"}, "license")
end
test "mirror", %{repo: repo} do
now = ~U[2026-11-09 12:00:00Z]
mirror = %{repo | upstream_url: "https://example.com/x.git"}
assert %{value: "not a mirror", slot: "muted"} = Badges.mirror(repo, now)
# Imported once and never synced since: developed here, not a mirror.
refute Badges.mirror?(mirror)
assert %{value: "not a mirror"} = Badges.mirror(mirror, now)
# Set to sync, but hasn't yet.
assert %{value: "never synced", slot: "warning"} =
Badges.mirror(%{mirror | upstream_auto_sync: true}, now)
assert %{value: "synced 3h ago", slot: "success"} =
Badges.mirror(%{mirror | upstream_synced_at: ~U[2026-11-09 09:00:00Z]}, now)
assert %{value: "synced 9d ago", slot: "warning"} =
Badges.mirror(%{mirror | upstream_synced_at: ~U[2026-10-31 12:00:00Z]}, now)
assert %{slot: "error"} =
Badges.mirror(%{mirror | upstream_synced_at: ~U[2026-09-01 12:00:00Z]}, now)
assert %{value: "sync failing", slot: "error"} =
Badges.mirror(
%{mirror | upstream_synced_at: now, upstream_sync_error: "boom"},
now
)
end
test "the top language and its share", %{repo: repo} do
assert %{value: "none"} = Badges.content(repo, "language")
Repo.insert!(%GitGud.Repositories.GitCommitLanguageStats{
repository_id: repo.id,
sha: :crypto.strong_rand_bytes(20),
languages: %{
"items" => [
%{"language" => "Elixir", "bytes" => 780, "files" => 3},
%{"language" => "Rust", "bytes" => 220, "files" => 1}
]
},
total_bytes: 1000,
file_count: 4,
computed_at: DateTime.utc_now(:second)
})
assert %{label: "language", value: "Elixir 78%"} = Badges.content(repo, "language")
end
test "syms, in the configured order", %{repo: repo} do
assert %{value: "none"} = Badges.content(repo, "syms")
[first, second | _] = GitGud.Syms.symbols()
for _ <- 1..2 do
{user, _} = repository_fixture()
{:ok, _} = GitGud.Syms.toggle(user, repo, second)
end
{user, _} = repository_fixture()
{:ok, _} = GitGud.Syms.toggle(user, repo, first)
assert %{label: "syms", value: value} = Badges.content(repo, "syms")
assert value == "#{first} 1 #{second} 2"
end
test "ci duration skips a push still running", %{repo: repo} do
assert %{value: "none"} = Badges.content(repo, "ci-duration")
t0 = ~U[2026-11-09 10:00:00Z]
event = Ecto.UUID.generate()
for {start, finish} <- [{0, 120}, {30, 252}] do
run!(repo, %{"status" => "success"})
|> Ecto.Changeset.change(
event_id: event,
started_at: DateTime.add(t0, start),
completed_at: DateTime.add(t0, finish)
)
|> Repo.update!()
end
run!(repo, %{"status" => "running"})
|> Ecto.Changeset.change(event_id: Ecto.UUID.generate(), started_at: DateTime.add(t0, 600))
|> Repo.update!()
assert %{label: "ci duration", value: "4m 12s"} = Badges.content(repo, "ci-duration")
end
test "durations" do
assert Badges.duration(45) == "45s"
assert Badges.duration(252) == "4m 12s"
assert Badges.duration(3780) == "1h 3m"
end
end
test "samples are a fixed set" do
assert {:ok, %{value: "failing"}} = Badges.sample("ci", "failing")
assert {:ok, :mark} = Badges.sample("mark", "gg")
assert Badges.sample("ci", "exploded") == :error
assert Badges.sample("coverage", "12") == :error
assert {:ok, %{label: "install with podman"}} = Badges.sample("package", "oci")
assert {:ok, %{value: "1,234"}} = Badges.sample("commits", "1234")
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
17.7 KiB · text
History
b868907