@@ -1,0 +1,201 @@
# Copyright 2026 Cole Christensen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule ExGitObjectstore.Protocol.TelemetryTest do
@moduledoc """
Operational coverage: every new code path added in the recent
protocol-v2 work must emit a telemetry event with useful payload.
"""
use ExUnit.Case, async: false
alias ExGitObjectstore.Object
alias ExGitObjectstore.Object.{Blob, Commit, Tree}
alias ExGitObjectstore.Protocol.{PktLine, ReceivePack, UploadPackV2}
alias ExGitObjectstore.Ref
alias ExGitObjectstore.Test.RepoHelper
@zero_sha String.duplicate("0", 40)
setup do
test_pid = self()
handler_id = "telemetry-test-#{:erlang.unique_integer([:positive])}"
events = [
[:ex_git_objectstore, :protocol, :fetch, :start],
[:ex_git_objectstore, :protocol, :fetch, :stop],
[:ex_git_objectstore, :protocol, :receive_pack, :atomic, :start],
[:ex_git_objectstore, :protocol, :receive_pack, :atomic, :stop],
[:ex_git_objectstore, :pack, :filter]
]
:telemetry.attach_many(
handler_id,
events,
fn event, measurements, metadata, _cfg ->
send(test_pid, {:telemetry, event, measurements, metadata})
end,
nil
)
on_exit(fn -> :telemetry.detach(handler_id) end)
:ok
end
describe "fetch telemetry" do
test "emits :start and :stop with mode + pack stats for a regular clone" do
repo = RepoHelper.memory_repo("tel-fetch")
ExGitObjectstore.init(repo)
sha = make_commit(repo, "hello\n")
:ok = Ref.put(repo, "refs/heads/main", sha, nil)
{_advert, state} = UploadPackV2.init(repo)
request =
PktLine.encode("command=fetch") <>
PktLine.delim() <>
PktLine.encode("want #{sha}") <>
PktLine.encode("done") <>
PktLine.flush()
{_response, _state} = UploadPackV2.feed(state, request)
assert_receive {:telemetry, [:ex_git_objectstore, :protocol, :fetch, :start], _measurements,
%{mode: :full, wants: 1, haves: 0}}
assert_receive {:telemetry, [:ex_git_objectstore, :protocol, :fetch, :stop],
%{duration: duration}, stop_meta}
assert duration > 0
assert stop_meta.objects > 0
assert stop_meta.pack_bytes > 0
assert stop_meta.mode == :full
end
test "reports :shallow mode when deepen args are present" do
repo = RepoHelper.memory_repo("tel-shallow")
ExGitObjectstore.init(repo)
sha = make_commit(repo, "hi\n")
:ok = Ref.put(repo, "refs/heads/main", sha, nil)
{_advert, state} = UploadPackV2.init(repo)
request =
PktLine.encode("command=fetch") <>
PktLine.delim() <>
PktLine.encode("want #{sha}") <>
PktLine.encode("deepen 1") <>
PktLine.encode("done") <>
PktLine.flush()
{_response, _state} = UploadPackV2.feed(state, request)
assert_receive {:telemetry, [:ex_git_objectstore, :protocol, :fetch, :stop], _measurements,
%{mode: :shallow}}
end
end
describe "filter telemetry" do
test "emits :objects_in / :objects_out with spec kind" do
repo = RepoHelper.memory_repo("tel-filter")
ExGitObjectstore.init(repo)
sha = make_commit(repo, "filterable\n")
:ok = Ref.put(repo, "refs/heads/main", sha, nil)
{_advert, state} = UploadPackV2.init(repo)
request =
PktLine.encode("command=fetch") <>
PktLine.delim() <>
PktLine.encode("want #{sha}") <>
PktLine.encode("filter blob:none") <>
PktLine.encode("done") <>
PktLine.flush()
{_response, _state} = UploadPackV2.feed(state, request)
assert_receive {:telemetry, [:ex_git_objectstore, :pack, :filter], measurements, meta}
assert meta.spec_kind == :blob_none
assert measurements.objects_in >= 0
# blob:none always excludes blobs, so there's at least a
# non-zero drop — we don't assert an exact delta but we do
# assert objects_out <= objects_in.
assert measurements.objects_out <= measurements.objects_in
end
end
describe "atomic receive-pack telemetry" do
test "emits :start and :stop with outcome=:committed on success" do
repo = RepoHelper.memory_repo("tel-atomic-ok")
ExGitObjectstore.init(repo)
commit_sha = make_commit(repo, "content\n")
{_advert, state} = ReceivePack.init(repo)
state = %{
state
| client_caps: MapSet.new(["report-status", "atomic"]),
commands: [
%{ref: "refs/heads/a", old_sha: @zero_sha, new_sha: commit_sha},
%{ref: "refs/heads/b", old_sha: @zero_sha, new_sha: commit_sha}
],
phase: :pack
}
# Drive through the pack-stage to trigger ref-update processing.
# With no pack bytes and all-creates, ReceivePack treats it as
# the "all-creates, pack optional" path and runs ref updates.
{_report, _final} = ReceivePack.feed(state, empty_pack())
assert_receive {:telemetry, [:ex_git_objectstore, :protocol, :receive_pack, :atomic, :stop],
%{duration: duration},
%{outcome: :committed, commands: 2, validation_failures: 0}}
assert duration > 0
end
end
# --- helpers ---
defp make_commit(repo, content) do
blob = Blob.from_content(content)
{:ok, blob_sha} = Object.write(repo, blob)
tree = Tree.new([%{mode: "100644", name: "f.txt", sha: blob_sha}])
{:ok, tree_sha} = Object.write(repo, tree)
commit = %Commit{
tree: tree_sha,
parents: [],
author: "T <t@t.com> 1000000000 +0000",
committer: "T <t@t.com> 1000000000 +0000",
message: "c\n"
}
{:ok, sha} = Object.write(repo, commit)
sha
end
# Empty pack (header + checksum) — enough for ReceivePack's
# `check_pack_complete/1` to declare the pack phase done so the
# state machine runs ref updates.
defp empty_pack do
header = <<"PACK", 2::unsigned-big-32, 0::unsigned-big-32>>
checksum = :crypto.hash(:sha, header)
<<header::binary, checksum::binary>>
end
end