1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
|
defmodule Diff do
@moduledoc """
Utilities for comparing build artifacts.
"""
@known_chunks ~w(
abstract_code
debug_info
attributes
compile_info
exports
labeled_exports
imports
indexed_imports
locals
labeled_locals
atoms
)a
@doc """
Compares the build artifacts of two build directories.
"""
@spec compare_dirs(Path.t(), Path.t()) ::
{
only1_paths :: list(Path.t()),
only2_paths :: list(Path.t()),
diff :: list({Path.t(), diff :: String.t()})
}
def compare_dirs(dir1, dir2) do
dir1 = Path.expand(dir1)
dir2 = Path.expand(dir2)
assert_dir!(dir1)
assert_dir!(dir2)
dir1_paths = relative_paths(dir1)
dir2_paths = relative_paths(dir2)
only1_paths = dir1_paths -- dir2_paths
only2_paths = dir2_paths -- dir1_paths
common_paths = dir1_paths -- only1_paths
common_files = Enum.reject(common_paths, &File.dir?/1)
diff =
Enum.flat_map(common_files, fn path ->
file1 = Path.join(dir1, path)
file2 = Path.join(dir2, path)
case compare_files(file1, file2) do
:eq -> []
{:diff, diff} -> [{path, diff}]
end
end)
{only1_paths, only2_paths, diff}
end
@doc """
Compares the contents of two files.
If the files are BEAM files, it performs a more human-friendly
"BEAM-diff".
"""
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
def compare_files(file1, file2) do
content1 = File.read!(file1)
content2 = File.read!(file2)
if content1 == content2 do
:eq
else
diff =
if String.ends_with?(file1, ".beam") do
beam_diff(file1, content1, file2, content2)
else
file_diff(file1, file2)
end
{:diff, diff}
end
end
defp beam_diff(file1, content1, file2, content2) do
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks),
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks),
true <- chunks1 != chunks2 do
for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do
tmp_file1 =
chunk1
|> inspect(pretty: true, limit: :infinity)
|> write_tmp()
tmp_file2 =
chunk2
|> inspect(pretty: true, limit: :infinity)
|> write_tmp()
file_diff(tmp_file1, tmp_file2)
end
else
_ ->
file_diff(file1, file2)
end
end
defp file_diff(file1, file2) do
{diff, _} = System.cmd("diff", [file1, file2])
diff
end
defp relative_paths(dir) do
dir
|> Path.join("**")
|> Path.wildcard()
|> Enum.map(&Path.relative_to(&1, dir))
end
defp assert_dir!(dir) do
unless File.dir?(dir) do
raise ArgumentError, "#{inspect(dir)} is not a directory"
end
end
defp write_tmp(content) do
filename = generate_tmp_filename()
File.mkdir_p!("tmp")
File.write!(Path.join("tmp", filename), content)
Path.join("tmp", filename)
end
defp generate_tmp_filename do
sec = :os.system_time(:second)
rand = :rand.uniform(999_999_999)
scheduler_id = :erlang.system_info(:scheduler_id)
"tmp-#{sec}-#{rand}-#{scheduler_id}"
end
end
case System.argv() do
[dir1, dir2] ->
case Diff.compare_dirs(dir1, dir2) do
{[], [], []} ->
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
{only1, only2, diff} ->
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
System.halt(1)
end
_ ->
IO.puts("Please, provide two directories as arguments")
System.halt(1)
end
|