defmodule OpenAgents.Release.AppupTest do
@moduledoc """
Proves the packaged appup describes the two revisions it was built from.
`:systools.make_relup/4` copies the appup into the relup verbatim, so an
instruction list that names a fixed set of modules installs only those
modules. A node would then run an interleaved mixture of two revisions while
`verify` passed and `BuildInfo.revision/0` reported the new SHA. These tests
drive `rel/openagents.appup.exs`, the file the release actually ships.
"""
use ExUnit.Case, async: false
alias OpenAgents.Release.Appup
@appup_file Path.expand("../../../rel/openagents.appup.exs", __DIR__)
@dockerfile Path.expand("../../../Dockerfile", __DIR__)
@version_file Path.expand("../../../VERSION", __DIR__)
@environment [
"OPENAGENTS_RELEASE_VSN",
"RELUP_FROM",
"RELUP_TO",
"RELUP_FROM_EBIN",
"RELUP_TO_EBIN",
"RELUP_FROM_STATE",
"RELUP_TO_STATE"
]
setup do
previous = Map.new(@environment, &{&1, System.get_env(&1)})
on_exit(fn ->
Enum.each(previous, fn
{name, nil} -> System.delete_env(name)
{name, value} -> System.put_env(name, value)
end)
end)
:ok
end
test "plain releases keep the appup and project versions aligned" do
Enum.each(@environment, &System.delete_env/1)
assert String.trim(File.read!(@version_file)) == Mix.Project.config()[:version]
assert {String.to_charlist(Mix.Project.config()[:version]), [], []} ==
@appup_file |> Code.eval_file() |> elem(0)
assert File.read!(@dockerfile) =~ "ARG OPENAGENTS_RELEASE_VSN\n"
refute File.read!(@dockerfile) =~ "ARG OPENAGENTS_RELEASE_VSN="
assert File.read!(@dockerfile) =~ "COPY VERSION mix.exs mix.lock ./"
end
test "covers a changed module the fixed proof set never named" do
from = ebin([widget(1), only_in_from()])
to = ebin([widget(2), only_in_to()])
{~c"0.3.0", [{~c"0.2.0", up}], [{~c"0.2.0", down}]} =
generate(from, to, "0.2.0", "0.3.0", 2, 2)
assert {:load_module, Relup.Fixture.Widget} in up
assert {:load_module, Relup.Fixture.Widget} in down
assert {:add_module, Relup.Fixture.OnlyInTo} in up
assert {:delete_module, Relup.Fixture.OnlyInFrom} in up
assert {:add_module, Relup.Fixture.OnlyInFrom} in down
assert {:delete_module, Relup.Fixture.OnlyInTo} in down
assert {:apply, {OpenAgents.ReleaseState, :install_barrier, []}} in up
assert {:apply, {OpenAgents.ReleaseState, :install_barrier, []}} in down
end
test "leaves untouched modules alone" do
from = ebin([widget(1), stable()])
to = ebin([widget(2), stable()])
{_version, [{_from, up}], _down} = generate(from, to, "0.2.0", "0.3.0", 2, 2)
assert {:load_module, Relup.Fixture.Widget} in up
refute {:load_module, Relup.Fixture.Stable} in up
end
test "carries each direction's target state schema in the advanced update" do
from = ebin([widget(1)])
to = ebin([widget(2)])
{_version, [{_from, up}], [{_to, down}]} = generate(from, to, "0.1.0", "0.2.0", 1, 2)
assert {:update, OpenAgents.ReleaseState, {:advanced, [schema_version: 2]}} in up
assert {:update, OpenAgents.ReleaseState, {:advanced, [schema_version: 1]}} in down
end
test "keeps a same-schema pair on its schema in both directions" do
from = ebin([widget(1)])
to = ebin([widget(2)])
{_version, [{_from, up}], [{_to, down}]} = generate(from, to, "0.2.0", "0.3.0", 2, 2)
assert {:update, OpenAgents.ReleaseState, {:advanced, [schema_version: 2]}} in up
assert {:update, OpenAgents.ReleaseState, {:advanced, [schema_version: 2]}} in down
end
test "updates a supervisor rather than loading it" do
from = ebin([supervisor(1)])
to = ebin([supervisor(2)])
{_version, [{_from, up}], _down} = generate(from, to, "0.2.0", "0.3.0", 2, 2)
assert {:update, Relup.Fixture.Tree, :supervisor} in up
end
test "refuses to emit an instruction list without the from build" do
to = ebin([widget(2)])
System.put_env("RELUP_FROM", "0.2.0")
System.put_env("RELUP_TO", "0.3.0")
System.put_env("RELUP_TO_EBIN", to)
System.put_env("RELUP_FROM_STATE", "2")
System.put_env("RELUP_TO_STATE", "2")
System.delete_env("RELUP_FROM_EBIN")
assert_raise RuntimeError, ~r/RELUP_FROM_EBIN/, fn -> Code.eval_file(@appup_file) end
end
test "refuses one version without the other" do
System.put_env("RELUP_TO", "0.3.0")
System.delete_env("RELUP_FROM")
assert_raise RuntimeError, ~r/must be set together/, fn -> Code.eval_file(@appup_file) end
end
test "verify_relup! rejects a relup that omits a changed module" do
from = ebin([widget(1)])
to = ebin([widget(2)])
path = Path.join(directory(), "relup")
File.write!(path, ~s|{"0.3.0",[{"0.2.0","",[]}],[{"0.2.0","",[]}]}.\n|)
assert_raise RuntimeError, ~r/Relup.Fixture.Widget/, fn ->
Appup.verify_relup!(path,
from_ebin: from,
to_ebin: to,
from_state_version: 2,
to_state_version: 2
)
end
end
test "verify_relup! accepts a relup that names every covered module" do
from = ebin([widget(1)])
to = ebin([widget(2)])
{_version, [{_from, up}], [{_to, down}]} = generate(from, to, "0.2.0", "0.3.0", 2, 2)
path = Path.join(directory(), "relup")
File.write!(
path,
:io_lib.format(~c"~tp.~n", [{~c"0.3.0", [{~c"0.2.0", ~c"", up}], [{~c"0.2.0", ~c"", down}]}])
)
assert OpenAgents.ReleaseState in Appup.verify_relup!(path,
from_ebin: from,
to_ebin: to,
from_state_version: 2,
to_state_version: 2
)
end
defp generate(from_ebin, to_ebin, from_version, to_version, from_state, to_state) do
System.put_env("RELUP_FROM", from_version)
System.put_env("RELUP_TO", to_version)
System.put_env("RELUP_FROM_EBIN", from_ebin)
System.put_env("RELUP_TO_EBIN", to_ebin)
System.put_env("RELUP_FROM_STATE", to_string(from_state))
System.put_env("RELUP_TO_STATE", to_string(to_state))
{appup, []} = Code.eval_file(@appup_file)
appup
end
defp ebin(sources) do
directory = directory()
state_beam = OpenAgents.ReleaseState |> :code.which() |> to_string()
File.cp!(state_beam, Path.join(directory, Path.basename(state_beam)))
Enum.each(sources, &compile_into(directory, &1))
directory
end
defp directory do
path =
Path.join(
System.tmp_dir!(),
"openagents-appup-#{System.unique_integer([:positive])}"
)
File.mkdir_p!(path)
on_exit(fn -> File.rm_rf!(path) end)
path
end
defp compile_into(directory, source) do
previous = Code.compiler_options(ignore_module_conflict: true)
try do
for {module, binary} <- Code.compile_string(source) do
File.write!(Path.join(directory, "#{module}.beam"), binary)
:code.purge(module)
:code.delete(module)
end
after
Code.compiler_options(previous)
end
end
defp widget(value) do
"defmodule Relup.Fixture.Widget do def value, do: #{value} end"
end
defp stable do
"defmodule Relup.Fixture.Stable do def value, do: :stable end"
end
defp only_in_from do
"defmodule Relup.Fixture.OnlyInFrom do def value, do: :gone end"
end
defp only_in_to do
"defmodule Relup.Fixture.OnlyInTo do def value, do: :new end"
end
defp supervisor(value) do
"""
defmodule Relup.Fixture.Tree do
use Supervisor
@impl true
def init(_arguments), do: Supervisor.init([], strategy: :one_for_one, max_restarts: #{value})
end
"""
end
end