3.5 KiB · text History b868907
defmodule GitGudWeb.Protos.RunnerWireTest do
  @moduledoc """
  The runner messages, checked against bytes laid out the way
  forgejo-runner lays them out (code.forgejo.org/forgejo/actions-proto-def,
  runner/v1/messages.proto) rather than round-tripped through our own
  module — which is how mismatched field numbers went unnoticed: a runner
  read our `version` as its labels and our `token` as its name.
  """
  use ExUnit.Case, async: true

  import Bitwise

  alias Runner.V1.DeclareResponse
  alias Runner.V1.RegisterRequest
  alias Runner.V1.Runner, as: RunnerMsg
  alias Runner.V1.TaskNeed

  # One length-delimited field: key = field number << 3 | wire type 2.
  defp str(field, value), do: <<field <<< 3 ||| 2>> <> varint(byte_size(value)) <> value

  defp varint(n) when n < 128, do: <<n>>
  defp varint(n), do: <<(n &&& 0x7F) ||| 0x80>> <> varint(n >>> 7)

  test "RegisterRequest decodes what a runner sends" do
    # name=1 token=2 agent_labels=3 custom_labels=4 version=5 labels=6
    bytes =
      str(1, "win-ltsc") <>
        str(2, "reg-token") <>
        str(3, "old-agent") <>
        str(4, "old-custom") <>
        str(5, "v13.2.0") <> str(6, "windows:host") <> str(6, "windows-latest:host")

    req = RegisterRequest.decode(bytes)
    assert req.name == "win-ltsc"
    assert req.token == "reg-token"
    assert req.version == "v13.2.0"
    assert req.labels == ["windows:host", "windows-latest:host"]
    assert req.agent_labels == ["old-agent"]
    assert req.custom_labels == ["old-custom"]
  end

  test "Runner encodes where a runner looks" do
    bytes =
      RunnerMsg.encode(%RunnerMsg{
        id: 7,
        uuid: "u",
        token: "secret",
        name: "win-ltsc",
        version: "v13.2.0",
        labels: ["windows:host"]
      })

    # token=3 name=4 version=8 labels=9
    assert :binary.match(bytes, str(3, "secret")) != :nomatch
    assert :binary.match(bytes, str(4, "win-ltsc")) != :nomatch
    assert :binary.match(bytes, str(8, "v13.2.0")) != :nomatch
    assert :binary.match(bytes, str(9, "windows:host")) != :nomatch
  end

  test "a DeclareResponse reads back with the runner's own numbering" do
    bytes =
      DeclareResponse.encode(%DeclareResponse{
        runner: %RunnerMsg{name: "win-ltsc", version: "v13.2.0", labels: ["windows:host"]}
      })

    # Field 1 is the embedded Runner; decode it by upstream numbers.
    <<0x0A, rest::binary>> = bytes
    {len, runner_bytes} = decode_varint(rest)
    runner = binary_part(runner_bytes, 0, len)

    assert :binary.match(runner, str(4, "win-ltsc")) != :nomatch
    assert :binary.match(runner, str(8, "v13.2.0")) != :nomatch
    assert :binary.match(runner, str(9, "windows:host")) != :nomatch
  end

  test "TaskNeed: outputs=1, result=2" do
    bytes = TaskNeed.encode(%TaskNeed{result: :RESULT_SUCCESS, outputs: %{}})
    # result is a varint (wire type 0) on field 2: key 0x10, value 1.
    assert bytes == <<0x10, 0x01>>
  end

  test "TaskNeed: outputs ride on field 1 as map entries, result on field 2" do
    bytes =
      %TaskNeed{result: :RESULT_FAILURE, outputs: %{"x" => "1"}}
      |> TaskNeed.encode()
      |> IO.iodata_to_binary()

    entry = str(1, "x") <> str(2, "1")
    assert bytes == str(1, entry) <> <<0x10, 0x02>>

    assert %TaskNeed{result: :RESULT_FAILURE, outputs: %{"x" => "1"}} = TaskNeed.decode(bytes)
  end

  defp decode_varint(<<0::1, n::7, rest::binary>>), do: {n, rest}

  defp decode_varint(<<1::1, n::7, rest::binary>>) do
    {high, rest} = decode_varint(rest)
    {n + (high <<< 7), rest}
  end
end