Current section

Files

Jump to
zigler lib zig unit.ex
Raw

lib/zig/unit.ex

defmodule Zig.Unit do
@moduledoc """
Hooks your zig code into ExUnit, by converting zig tests into ExUnit tests.
### Usage
#### Example
Inside your zig code (`dependent.zig`):
```
fn one() i64 {
return 1;
}
test "the one function returns one" {
std.testing.expect(one() == 1);
}
```
Inside your elixir code:
```
defmodule MyZigModule do
use Zig
~Z\"""
const dependent = @import("dependent.zig");
/// nif: some_nif_fn/1
...
\"""
end
```
Inside your test module:
```
defmodule MyZigTest do
use ExUnit.Case, async: true
use Zig.Unit
zigtest MyZigModule
end
```
### Scope
zigtest will run tests from the following sources:
- any tests inside of a sigil Z or sigil z construction
- any tests inside `pub` `@import` zig sources.
- any tests inside `pub usingnamespace` zig sources.
- recursively discovered `pub` structs.
"""
@doc false
def name_to_hash(str) do
Base.encode16(<<:erlang.phash2(str)::32>>)
end
@transfer_params [
:otp_app,
:file,
:libs,
:resources,
:zig_version,
:imports,
:c_includes,
:include_dirs,
:version,
:system_include_dirs,
:link_libc
]
alias Zig.Assembler
alias Zig.Parser.Unit
@doc """
loads a module that wraps a zigler NIF, consults the corresponding zig code,
generating the corresponding zig tests.
Must be called from within a module that has `use ExUnit.Case` and `use Zig`
"""
defmacro zigtest(mod, options \\ []) do
module = Macro.expand(mod, __CALLER__)
Code.ensure_loaded(module)
info =
__CALLER__
|> Map.take([:file, :line])
|> Map.to_list()
unless function_exported?(module, :__info__, 1) do
raise CompileError, info ++ [description: "zigtest called on a nonexistent module"]
end
ref_zigler =
case module.__info__(:attributes)[:zigler] do
[zigler] ->
zigler
_ ->
raise CompileError,
info ++ [description: "zigtest called on a module that doesn't bind a zig nif"]
end
assembly_dir =
Path.join(
System.tmp_dir!(),
".zigler_compiler/#{Mix.env()}/#{__CALLER__.module}"
)
# convert the code to substitute functions for tests
{nifs, code} =
ref_zigler.code
|> IO.iodata_to_binary()
|> Unit.parse(info)
File.mkdir_p!(assembly_dir)
# gather all code dependencies. We'll want to look for all things
# which are labeled as "pub" and modify those code bits accordingly.
transitive_nifs =
ref_zigler.code
|> Assembler.parse_code(
parent_dir: Path.dirname(ref_zigler.file),
target_dir: assembly_dir,
pub: true,
context: []
)
|> Enum.flat_map(fn
assembly = %{pub: true} ->
# if it's public, we have to rewrite the code on its
# way in, and retarget it to the transferred file.
{new_nifs, rewritten_source} =
assembly.source
|> File.read!()
|> Unit.parse(context: assembly.context)
# write out the file
File.write!(assembly.target, rewritten_source)
new_nifs
_ ->
[]
end)
zigler =
struct(
Zig.Module,
ref_zigler
|> Map.take(@transfer_params)
|> Map.merge(%{
code: code,
nifs: nifs ++ transitive_nifs,
module: __CALLER__.module,
link_libc: true
})
)
|> Macro.escape()
in_ex_unit =
Enum.any?(Application.started_applications(), fn
{app, _, _} -> app == :ex_unit
end)
tests =
make_code(
__CALLER__.module,
__CALLER__.file,
nifs ++ transitive_nifs,
!options[:dry_run] && in_ex_unit
)
# trigger the zigler compiler on this module.
quote do
Module.put_attribute(unquote(__CALLER__.module), :zigler, unquote(zigler))
unquote(tests)
end
end
####################################################################
## ExUnit Boilerplate
defp make_code(module, file, nifs, true) do
quote bind_quoted: [module: module, file: file, nifs: Macro.escape(nifs)] do
# register our tests.
env = __ENV__
for {name, test} <- Zig.Unit.__zigtests__(module, nifs) do
@file file
test_name = ExUnit.Case.register_test(env, :zigtest, name, [])
def unquote(test_name)(_), do: unquote(test)
end
defp line_lookup(code_cache, dest_line) do
code_cache
|> Enum.with_index(1)
|> Enum.reduce({"", 0}, fn
{_, ^dest_line}, fileline ->
throw fileline
{"// ref: " <> spec, _}, _ ->
[file, "line:", line] = String.split(spec)
{file, String.to_integer(line) + 1}
_, {file, line} ->
{file, line + 1}
end)
catch
fileline -> fileline
end
end
end
# for testing purposes only:
defp make_code(module, _, nifs, false) do
quote bind_quoted: [module: module, nifs: Macro.escape(nifs)] do
# register our tests.
for {name, test} <- Zig.Unit.__zigtests__(module, nifs) do
test_name = name |> Zig.Unit.name_to_hash() |> String.to_atom()
def unquote(test_name)(_), do: unquote(test)
end
end
end
def __zigtests__(module, test_nifs) do
Enum.map(test_nifs, &{&1.test, test_content(module, &1)})
end
defp test_content(module, nif) do
quote do
try do
apply(unquote(module), unquote(nif.test), [])
:ok
rescue
exception ->
zig_error_return_trace =
Enum.map(exception.error_return_trace, fn
{_, fun, error_file, error_line} ->
code_map = unquote(module).__info__(:attributes)[:nif_code_map]
[zig_root] = unquote(module).__info__(:attributes)[:zig_root_dir]
cond do
String.starts_with?(error_file, zig_root) ->
file = String.replace_leading(error_file, zig_root, "[zig]")
{:@, fun, [:...], [file: file, line: error_line]}
String.starts_with?(Path.basename(error_file), "#{unquote(module)}") ->
[%{code: code, nifs: nifs}] = unquote(module).__info__(:attributes)[:zigler]
{src_file, src_line} =
code
|> IO.iodata_to_binary()
|> String.split("\n")
|> line_lookup(error_line)
test =
Enum.find_value(nifs, fn nif ->
if nif.name == fun, do: :"test #{nif.test}"
end)
{:.., test, [], [file: src_file, line: src_line]}
lookup = List.keyfind(code_map, error_file, 0) ->
{_, src_file} = lookup
{:.., fun, [:...], [file: src_file, line: error_line]}
true ->
{:.., fun, [:...], [file: error_file, line: error_line]}
end
end)
new_stacktrace = Enum.reverse(zig_error_return_trace, tl(__STACKTRACE__))
case exception.message do
message = "Test" <> _ ->
# create correct content
error = [
message: "Zig test `#{unquote(nif.test)}` failed with error.#{message}",
doctest: ExUnit.AssertionError.no_value(),
expr: ExUnit.AssertionError.no_value(),
left: ExUnit.AssertionError.no_value(),
right: ExUnit.AssertionError.no_value()
]
reraise ExUnit.AssertionError, error, new_stacktrace
_ ->
reraise exception, new_stacktrace
end
end
end
end
end