Current section
Files
Jump to
Current section
Files
lib/git_snapshot.ex
defmodule GitSnapshot do
@moduledoc """
`GitSnapshot` is a library for snapshot testing using `git`.
This library takes string snaphots (see `m:GitSnapshot.Snapshot`) and stores
them on disk. When a file exists the current test value will be compared to
the staged content of that file.
If the compared values are equal everything is fine.
If the compared values are not equal the file is overwritten with the new value
and an `ExUnit.AssertionError` is raised for the test to fail. The changed
value can become accepted by staging the updated file contents.
"""
import ExUnit.Assertions
alias ExUnit.AssertionError
@doc """
Assert on a snapshot.
## Example
test "name", context do
assert_snapshot(context, "file_name", "value")
end
"""
def assert_snapshot(context, key, value) do
string = GitSnapshot.Snapshot.to_string(value)
if not is_binary(string) or not String.printable?(string) do
raise "uncomparable value: #{inspect(string)}"
end
dir = create_tmp_dir!(context)
path = Path.join(dir, "#{key}.txt")
case System.cmd(ensure_git!(), ["show", ":#{path}"], stderr_to_stdout: true) do
{expected, 0} ->
try do
assert string == expected
rescue
e in [AssertionError] ->
File.write!(path, string)
reraise(e, __STACKTRACE__)
end
{_, 128} ->
File.write!(path, string)
:ok
end
end
@doc """
Assert on json data.
## Options
See `Jason.decode/2`.
## Example
test "name", context do
assert_json(context, "file_name", "value")
end
"""
if Code.ensure_loaded?(Jason) do
def assert_json(context, key, value, opts \\ []) do
# Expecting plain binaries to already be json data
string =
if is_binary(value) do
value
else
Jason.encode!(value, pretty: true)
end
if not is_binary(string) or not String.printable?(string) do
raise "uncomparable value: #{inspect(string)}"
end
dir = create_tmp_dir!(context)
path = Path.join(dir, "#{key}.json")
case System.cmd(ensure_git!(), ["show", ":#{path}"], stderr_to_stdout: true) do
{expected, 0} ->
try do
data = Jason.decode!(string, opts)
expected_data = Jason.decode!(expected, opts)
assert data == expected_data
rescue
e in [AssertionError] ->
File.write!(path, string)
reraise(e, __STACKTRACE__)
end
{_, 128} ->
File.write!(path, string)
:ok
end
end
else
def assert_json(_context, _key, _value, _opts \\ []) do
raise "Install optional dependency :jason"
end
end
@doc """
Assert on a image.
## Example
test "name", context do
assert_image(context, "file_name.png", image)
end
"""
if Code.ensure_loaded?(Image) do
def assert_image(context, key, image, opts \\ []) do
if not is_binary(image) do
raise "uncomparable value: #{inspect(image)}"
end
{compare_opts, opts} =
Keyword.split(opts, [
:metric,
:saturation,
:brightness,
:difference_color,
:difference_boost
])
dir = create_tmp_dir!(context)
path = Path.join(dir, key)
extname = Path.extname(key)
basename = Path.basename(key, extname)
diff_path = Path.join(dir, "#{basename}-diff#{extname}")
case System.cmd(ensure_git!(), ["show", ":#{path}"], stderr_to_stdout: true) do
{expected, 0} ->
{:ok, expected_img} = Image.open(expected)
{:ok, given_img} = Image.open(image)
{:ok, number, diff} = Image.compare(expected_img, given_img, compare_opts)
try do
assert number <= Keyword.get(opts, :threshold, 0.0)
rm_f!(diff_path)
{_, 0} = System.cmd(ensure_git!(), ["restore", path], stderr_to_stdout: true)
rescue
e in [AssertionError] ->
File.write!(path, image)
Image.write!(diff, diff_path)
reraise(e, __STACKTRACE__)
end
{_, 128} ->
File.write!(path, image)
:ok
end
end
defp rm_f!(path) do
case File.rm(path) do
:ok ->
:ok
{:error, :enoent} ->
:ok
{:error, reason} ->
raise File.Error,
reason: reason,
path: IO.chardata_to_string(path),
action: "force remove file"
end
end
else
def assert_image(_context, _key, _image) do
raise "Install optional dependency :image"
end
end
defp ensure_git! do
case System.find_executable("git") do
nil -> raise "git not found"
path -> path
end
end
defp create_tmp_dir!(context) do
module_string = inspect(context.module)
name_string = to_string(context.test)
module = escape_path(module_string)
name = escape_path(name_string)
short_hash = short_hash(module_string, name_string)
path = ["snapshots", module, "#{name}-#{short_hash}"] |> Path.join()
File.mkdir_p!(Path.expand(path))
path
end
@escape Enum.map(~c" [~#%&*{}\\:<>?/+|\"]", &<<&1::utf8>>)
defp escape_path(path) do
String.replace(path, @escape, "-")
end
defp short_hash(module, test_name) do
(module <> "/" <> test_name)
|> :erlang.md5()
|> Base.encode16(case: :lower)
|> binary_slice(0..7)
end
end