Compare commits

...
26 Commits
Author SHA1 Message Date
José Valim 794441a255 Release v1.19.4 2025-11-27 16:50:26 +01:00
Eksperimental 727106beb4 Fix typos (#14951) 2025-11-27 16:49:21 +01:00
Wojtek Mach 5e45871b5e mix help app:APP: compile current Mix project (if any) (#14958)
Prior to this patch, it was impossible to get help for the current app:

    nimble_csv$ mix help app:nimble_csv
    Application nimble_csv does not exist or is not loaded

It also helps with projects with depenendencies when they haven't been
compiled yet:

    req$ mix deps.get && mix help app:finch # mix deps.compile haven't been run yet
    Application finch does not exist or is not loaded

Now it works:

    nimble_csv$ mix help app:nimble_csv
    Compiling 1 file (.ex)
    Generated nimble_csv app
    # NimbleCSV

    NimbleCSV is a small and fast parsing and dumping library.

    # NimbleCSV.RFC4180

    A CSV parser that uses comma as separator and double-quotes as escape according
    to RFC4180.

    # NimbleCSV.Spreadsheet

    A parser with spreadsheet friendly settings.
2025-11-27 15:36:13 +01:00
José Valim 0db308cbff Clarify @on_load will be called early on releases, closes #14960 2025-11-27 15:26:18 +01:00
José Valim 35a7e3aa1f Fix typo 2025-11-26 19:53:12 +01:00
José Valim 7340597020 Clarify struct and record fields, closes #14963 2025-11-26 19:52:43 +01:00
José Valim 3b6acc33d2 Fix task supervisor name 2025-11-24 18:27:17 +01:00
José Valim 2ed048acc6 Include hex in the list of escript.build special cases (#14953) 2025-11-24 15:28:33 +01:00
José Valim bf0c203d52 Skip warning on :re.import/1 2025-11-21 14:04:19 -08:00
Łukasz Samson b074e6b159 Fix ex_unit formatter crash when find_diff returns nil (#14939) 2025-11-21 05:34:16 -08:00
Jean Klingler bfe7315fbb File.cp_r reports non-existing dest dir properly (#14929) 2025-11-20 21:43:53 +09:00
Jean Klingler 28f4fdc21b Fix dialyzer warning in Code.format_string!/2 (#14928)
* Fix dialyzer warning in Code.format_string!/2

Tentative fix for https://github.com/elixir-lang/elixir/issues/14927

* Add regression test for IO.inspect/2 opts
2025-11-20 19:41:01 +09:00
José Valim ee1dc0729f Ensure parallel matches in assert propagate type information, closes #14921 2025-11-18 15:20:17 -08:00
José Valim 32e724c87c Ensure logger formatter works with chardata, closes #14921 2025-11-18 14:50:50 -08:00
José Valim 7758084211 Add --min-cycle-label to mix xref graph 2025-11-15 06:36:31 -08:00
Jean Klingler 57d3098c2a Fix since in doc for E regex modifier (#14909) 2025-11-14 07:48:38 +09:00
José Valim 427d21e410 Release v1.19.3 2025-11-13 17:33:43 +01:00
José Valim 24a188a016 Share consulting logic between compile.app and release 2025-11-13 16:11:15 +01:00
Jean Klingler 457d52c89b Add 'E' modifier to Regex for :export option (#14907) 2025-11-13 16:11:15 +01:00
José Valim 6684dbd3f2 Simplify format stacktrace handling 2025-11-13 13:10:59 +01:00
Łukasz Samson ccb2a742ad Fix crash when format_kind_reason is executed with TestModule (#14901)
Closes #14900.
2025-11-13 13:10:58 +01:00
Jean Klingler 92094c9ef1 Write ascii printable charlists as such in .app file (#14906) 2025-11-13 18:42:54 +09:00
Wojtek Mach d4d5da2ccf mix help app:APP: Always try loading apps and support Erlang apps (#14894) 2025-11-08 12:23:04 +01:00
José Valim 666c082105 &Mod.fun/arity are valid app terms, closes #14891 2025-11-08 12:23:03 +01:00
José Valim d2c9d39efe Allow forcing specific compilers 2025-11-08 11:47:24 +01:00
Jean Klingler 97d16038fe Fix how UTF8 env is stored in .app file (#14890) 2025-11-04 16:21:08 +09:00
39 changed files with 625 additions and 184 deletions
+56
View File
@@ -234,6 +234,62 @@ These additions offer greater transparency into the components and licenses of e
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.4 (2025-11-27)
### 1. Enhancements
#### Mix
* [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter
### 2. Bug fixes
#### Elixir
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
#### ExUnit
* [ExUnit] Fix formatter crash when diffing takes too long
* [ExUnit] Ensure parallel matches in `assert` propagate type information
#### Logger
* [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters)
#### Mix
* [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled
* [mix escript.build] Ensure the `hex` application can be included in escripts
## v1.19.3 (2025-11-13)
### 1. Enhancements
#### Elixir
* [Kernel] Support /E modifier for regular expressions in config files
#### Mix
* [mix compile] Allow forcing specific compilers, such as `--force-elixir`, `--force-app`, etc
* [mix help app:APP] Support showing helps for apps in Elixir and Erlang standard libraries
### 2. Bug fixes
#### Elixir
* [IO] Fix dialyzer warning on `IO.inspect :label`
#### ExUnit
* [ExUnit.Case] Fix crash when formatting errors caused by a linked/trapped exit during `setup_all`
#### Mix
* [mix compile.app] Ensure functions in the format `&Mod.fun/arity` can be written to .app files
* [mix compile.app] Ensure strings with Unicode characters can be written to .app files
## v1.19.2 (2025-11-02)
### 1. Enhancements
+1 -1
View File
@@ -1 +1 @@
1.19.2
1.19.4
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.19.2
ELIXIR_VERSION=1.19.4
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.2
set ELIXIR_VERSION=1.19.4
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+1 -1
View File
@@ -1090,7 +1090,7 @@ defmodule Code do
@doc since: "1.6.0"
@spec format_string!(binary, [format_opt]) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
line_length = Keyword.get(opts, :line_length, 98)
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
to_quoted_opts =
[
+7 -1
View File
@@ -1224,7 +1224,13 @@ defmodule File do
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
case :elixir_utils.read_link_type(src) do
{:ok, :regular} ->
do_cp_file(src, dest, on_conflict, acc)
case do_cp_file(src, dest, on_conflict, acc) do
# we don't have a way to make a distinction between a non-existing src
# or dest being a non-existing dir in the case of :enoent,
# but we already know that src exists here.
{:error, :enoent, _} -> {:error, :enoent, dest}
other -> other
end
{:ok, :symlink} ->
case :file.read_link(src) do
+1
View File
@@ -569,6 +569,7 @@ defimpl Inspect, for: Regex do
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
+3 -2
View File
@@ -959,8 +959,9 @@ defmodule Macro do
backwards compatibility purposes.
In future releases, Elixir may introduce truly required struct
fields, and therefore only one of required or default will be
present. Your code should prepare for such scenario accordingly.
fields, the required field will be removed and default will be
present only if the field is optional. Your code should prepare
for such scenario accordingly.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
+8 -17
View File
@@ -402,30 +402,21 @@ defmodule Module do
Accepts the function name (as an atom) of a function in the current module.
The function must have an arity of 0 (no arguments). If the function does
not return `:ok`, the loading of the module will be aborted.
For example:
not return `:ok`, the loading of the module will be aborted. Its primary
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
defmodule MyModule do
@on_load :load_check
@on_load :load_external_code
def load_check do
if some_condition() do
:ok
else
:abort
end
end
def some_condition do
false
def load_external_code do
:erlang.load_nif(~c"path/to/extension.so_or_dll")
end
end
The function given to `on_load` should avoid calling functions from
other modules. If you must call functions in other modules and those
modules are defined within the same project, the called modules must
have the `@compile {:autoload, true}` annotation, so they are loaded
upfront (and not from within the `@on_load` callback).
other modules. This is because, when running a `mix release`,
`on_load` runs extremely early, before any application starts running,
and therefore even systems like the `Logger` and `IO` are not yet available.
### `@vsn`
+3 -2
View File
@@ -27,8 +27,9 @@ defmodule Record do
## Types
Types can be defined for tuples with the `record/2` macro (only available in
typespecs). This macro will expand to a tuple as seen in the example below:
Types can be defined for tuples with the `record/2` construct (which is only
available in typespecs), with the record name as an atom and a keyword list
of fields and their types as argument:
defmodule MyModule do
require Record
+32 -5
View File
@@ -101,6 +101,13 @@ defmodule Regex do
* `:ungreedy` (U) - inverts the "greediness" of the regexp
(the previous `r` option is deprecated in favor of `U`)
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
which can be shared across nodes or through config, at the cost of a runtime
overhead every time to re-import it every time it is executed.
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
and do not need to be exported in order to be shared in these versions.
## Captures
Many functions in this module handle what to capture in a regex
@@ -251,7 +258,7 @@ defmodule Regex do
This checks the version stored in the regular expression
and recompiles the regex in case of version mismatch.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile(%Regex{} = regex) do
@@ -261,7 +268,7 @@ defmodule Regex do
@doc """
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "It can be removed and it has no effect"
@doc since: "1.4.0"
def recompile!(regex) do
@@ -271,7 +278,7 @@ defmodule Regex do
@doc """
Returns the version of the underlying Regex engine.
"""
# Remove me on Elixir v1.22
# TODO: Deprecate on Elixir v1.22
@doc deprecated: "Use :re.version() instead"
@doc since: "1.4.0"
def version do
@@ -515,7 +522,7 @@ defmodule Regex do
"""
@spec names(t) :: [String.t()]
def names(%Regex{re_pattern: re_pattern}) do
{:namelist, names} = :re.inspect(re_pattern, :namelist)
{:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist)
names
end
@@ -585,10 +592,17 @@ defmodule Regex do
%Regex{source: source, opts: compile_opts} = regex
:re.run(string, source, compile_opts ++ options)
else
_ -> :re.run(string, re_pattern, options)
_ -> :re.run(string, maybe_import_pattern(re_pattern), options)
end
end
@compile {:inline, maybe_import_pattern: 1}
@compile {:no_warn_undefined, {:re, :import, 1}}
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
do: :re.import(exported)
defp maybe_import_pattern(pattern), do: pattern
@typedoc """
Options for regex functions that capture matches.
"""
@@ -1007,6 +1021,16 @@ defmodule Regex do
translate_options(t, [:ungreedy | acc])
end
defp translate_options(<<?E, t::binary>>, acc) do
# on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed
# (regexes aren't using references and can be shared across nodes or stored in config)
# TODO: remove this check on Erlang/OTP 28+ and update docs
case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
true -> translate_options(t, [:export | acc])
false -> translate_options(t, acc)
end
end
defp translate_options(<<>>, acc), do: acc
defp translate_options(t, _acc), do: {:error, t}
@@ -1022,6 +1046,9 @@ defmodule Regex do
:erlang.system_info(:otp_release) < [?2, ?8] ->
Macro.escape(regex.re_pattern)
:lists.member(:export, regex.opts) ->
Macro.escape(regex.re_pattern)
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
@@ -228,7 +228,7 @@ Now we need to change `loop_acceptor/1` to use `Task.Supervisor` to serve each r
```elixir
defp loop_acceptor(socket) do
{:ok, client} = :gen_tcp.accept(socket)
{:ok, pid} = Task.Supervisor.start_child(KV.BucketSupervisor, fn -> serve(client) end)
{:ok, pid} = Task.Supervisor.start_child(KV.ServerSupervisor, fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)
loop_acceptor(socket)
end
+12
View File
@@ -787,6 +787,18 @@ defmodule FileTest do
end
end
test "cp_r! with file src and dest unknown" do
src = fixture_path("cp_r/a/1.txt")
dest = tmp_path("tmp/unknown/")
message =
"could not copy recursively from #{inspect(src)} to #{inspect(dest)}. #{dest}: no such file or directory"
assert_raise File.CopyError, message, fn ->
File.cp_r!(src, dest)
end
end
test "cp preserves mode" do
File.mkdir_p!(tmp_path("tmp"))
src = fixture_path("cp_mode")
@@ -0,0 +1,14 @@
defmodule Dialyzer.Regressions do
def io_inspect_opts do
IO.inspect(123, label: "foo", limit: :infinity)
end
def format_opts do
Code.format_string!("",
line_length: 120,
force_do_end_blocks: true,
locals_without_parens: true,
migrate: true
)
end
end
+5
View File
@@ -906,6 +906,11 @@ defmodule Inspect.OthersTest do
assert inspect(Regex.compile!("foo", [:ucp])) == ~S'Regex.compile!("foo", [:ucp])'
end
@tag :re_import
test "exported regex" do
assert inspect(~r/foo/E) == "~r/foo/E"
end
test "inspect_fun" do
fun = fn
integer, _opts when is_integer(integer) ->
@@ -185,6 +185,11 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context)
end
test "no warning in various non-regression cases", context do
copy_beam!(context, Dialyzer.Regressions)
assert_dialyze_no_warnings!(context)
end
defp copy_beam!(context, module) do
name = "#{module}.beam"
File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
+14
View File
@@ -52,6 +52,20 @@ defmodule RegexTest do
assert Regex.match?(~r/^b$/m, "a\nb\nc")
end
@tag :re_import
test "export" do
# exported patterns have no structs, so these are structurally equal
assert ~r/foo/E == Regex.compile!("foo", [:export])
assert Regex.match?(~r/foo/E, "foo")
refute Regex.match?(~r/foo/E, "Foo")
assert Regex.run(~r/c(d)/E, "abcd") == ["cd", "d"]
assert Regex.run(~r/e/E, "abcd") == nil
assert Regex.names(~r/(?<FOO>foo)/E) == ["FOO"]
end
test "precedence" do
assert {"aa", :unknown} |> elem(0) =~ ~r/(a)\1/
end
+9 -2
View File
@@ -160,6 +160,7 @@ defmodule ExUnit.Assertions do
end
end
{left, right} = move_match(left, right)
__match__(left, right, code, check, __CALLER__)
end
@@ -380,6 +381,12 @@ defmodule ExUnit.Assertions do
{ExUnit.AssertionError.no_value(), expr}
end
defp move_match(left, {:=, meta, [middle, right]}),
do: move_match({:=, meta, [left, middle]}, right)
defp move_match(left, right),
do: {left, right}
@doc false
def __match__({:when, _, _} = left, right, _, _, _) do
suggestion =
@@ -409,7 +416,7 @@ defmodule ExUnit.Assertions do
case right do
unquote(left) ->
unquote(check)
unquote(mark_as_generated(vars))
{unquote_splicing(mark_as_generated(vars))}
_ ->
left = unquote(Macro.escape(left))
@@ -427,7 +434,7 @@ defmodule ExUnit.Assertions do
quote do
right = unquote(right)
expr = unquote(code)
unquote(vars) = unquote(match_expr)
{unquote_splicing(vars)} = unquote(match_expr)
right
end
end
+7
View File
@@ -964,7 +964,14 @@ defmodule ExUnit.DocTest do
@doc false
defmacro __assert__({:=, _, [left, right]} = assertion) do
{left, right} = move_match(left, right)
code = Macro.escape(assertion, prune_metadata: true)
ExUnit.Assertions.__match__(left, right, code, :ok, __CALLER__)
end
defp move_match(left, {:=, meta, [middle, right]}),
do: move_match({:=, meta, [left, middle]}, right)
defp move_match(left, right),
do: {left, right}
end
+22 -10
View File
@@ -338,7 +338,8 @@ defmodule ExUnit.Formatter do
{formatted_reason, wrapped_stack} =
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)
formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
formatted_stack = format_stacktrace(wrapped_stack, test, formatter)
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}
:error ->
@@ -512,10 +513,15 @@ defmodule ExUnit.Formatter do
defp format_diff(left, right, context, formatter) do
if has_value?(left) and has_value?(right) do
{result, env} = find_diff(left, right, context)
result = if formatter.(:diff_enabled?, false), do: result
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
{result, hints}
case find_diff(left, right, context) do
{result, env} ->
result = if formatter.(:diff_enabled?, false), do: result
hints = Enum.map(env.hints, &{:hint, format_hint(&1)})
{result, hints}
nil ->
{nil, []}
end
else
{nil, []}
end
@@ -672,22 +678,28 @@ defmodule ExUnit.Formatter do
|> Algebra.format(width)
end
defp format_stacktrace([], _case, _test, _color) do
defp format_stacktrace(stack, %{module: module, name: name}, color),
do: format_stacktrace(stack, module, name, color)
defp format_stacktrace(stack, %{name: name}, color),
do: format_stacktrace(stack, name, nil, color)
defp format_stacktrace([], _module, _test, _color) do
""
end
defp format_stacktrace(stacktrace, test_case, test, color) do
defp format_stacktrace(stacktrace, module, test, color) do
extra_info("stacktrace:", color) <>
Enum.map_join(stacktrace, fn entry ->
stacktrace_info(format_stacktrace_entry(entry, test_case, test), color)
stacktrace_info(format_stacktrace_entry(entry, module, test), color)
end)
end
defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
defp format_stacktrace_entry({module, test, _, location}, module, test) do
format_file_line(location[:file], location[:line], " (test)")
end
defp format_stacktrace_entry(entry, _test_case, _test) do
defp format_stacktrace_entry(entry, _module, _test) do
format_stacktrace_entry(entry)
end
@@ -246,6 +246,16 @@ defmodule ExUnit.AssertionsTest do
end
end
test "assert parallel match" do
assert %URI{} = uri = apply(URI, :parse, ["/foo"])
# This should not warn
assert %URI{uri | path: "/bar"}.path == "/bar"
assert uri = %URI{} = apply(URI, :parse, ["/foo"])
# This should not warn
assert %URI{uri | path: "/bar"}.path == "/bar"
end
test "assert match with pinned variable" do
a = 1
{2, 1} = assert {2, ^a} = Value.tuple()
@@ -465,6 +465,21 @@ defmodule ExUnit.FormatterTest do
"""
end
test "formats setup_all exit stacktraces using module context" do
stacktrace = [
{Hello, :setup_all, [], [file: __ENV__.file, line: 123]},
{Hello, :some_fun, [], [file: __ENV__.file, line: 456]}
]
failure = [{{:EXIT, self()}, {:function_clause, stacktrace}, []}]
formatted =
format_test_all_failure(test_module(), failure, 1, 80, &formatter/2)
assert formatted =~ "Hello.setup_all/0"
assert formatted =~ "Hello.some_fun()"
end
test "formats assertions with operators with no limit" do
failure = [{:error, catch_assertion(assert [1, 2, 3] == [4, 5, 6]), []}]
+26 -14
View File
@@ -244,26 +244,38 @@ defmodule Logger.Formatter do
case if(is_function(enabled, 0), do: enabled.(), else: enabled) do
true ->
color = md[:ansi_color] || Map.fetch!(colors, level)
fragment = IO.ANSI.format_fragment(color, true)
data = IO.iodata_to_binary(data)
size = byte_size(data)
cond do
:binary.at(data, size - 2) == ?\r and :binary.at(data, size - 1) == ?\n ->
[fragment, binary_part(data, 0, size - 2), IO.ANSI.reset() | "\r\n"]
:binary.at(data, size - 1) == ?\n ->
[fragment, binary_part(data, 0, size - 1), IO.ANSI.reset(), ?\n]
true ->
[fragment, data | IO.ANSI.reset()]
end
[IO.ANSI.format_fragment(color, true), add_reset(data)]
false ->
data
end
end
defp add_reset(binary) when is_binary(binary) do
size = byte_size(binary)
cond do
binary == "" ->
IO.ANSI.reset()
:binary.at(binary, size - 1) == ?\n ->
if size > 1 and :binary.at(binary, size - 2) == ?\r do
[binary_part(binary, 0, size - 2), IO.ANSI.reset() | "\r\n"]
else
[binary_part(binary, 0, size - 1), IO.ANSI.reset(), ?\n]
end
true ->
[binary | IO.ANSI.reset()]
end
end
defp add_reset([?\r, ?\n]), do: [IO.ANSI.reset(), ?\r, ?\n]
defp add_reset([?\n]), do: [IO.ANSI.reset(), ?\n]
defp add_reset([last]), do: add_reset(last)
defp add_reset([h | t]), do: [h | add_reset(t)]
defp add_reset(rest), do: [rest | IO.ANSI.reset()]
@doc """
Formats the message of a log event.
"""
+20
View File
@@ -105,6 +105,14 @@ defmodule Logger.FormatterTest do
colors: [enabled: true]
)
assert %{level: :warning, msg: {:string, ""}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33m\e[0m"
assert %{level: :warning, msg: {:string, "s"}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33ms\e[0m"
assert %{level: :warning, msg: {:string, "message"}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mmessage\e[0m"
@@ -116,6 +124,18 @@ defmodule Logger.FormatterTest do
assert %{level: :warning, msg: {:string, "message\r\n"}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mmessage\e[0m\r\n"
assert %{level: :warning, msg: {:string, [?é, ["message", [?\n]]]}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\n"
assert %{level: :warning, msg: {:string, [?é, "message", ?\r, ?\n]}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n"
assert %{level: :warning, msg: {:string, [?é, "message" | "\r\n"]}, meta: %{}}
|> format(formatter)
|> IO.chardata_to_string() == "\e[33mémessage\e[0m\r\n"
end
end
+35 -38
View File
@@ -468,51 +468,48 @@ defmodule Mix.Release do
{sys_config, runtime_config?} =
merge_provider_config(release, sys_config, config_provider_path)
path = Path.join(release.version_path, "sys.config")
args = [runtime_config?, sys_config]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
case :file.consult(path) do
{:ok, _} ->
case consultable_sys_config(sys_config) do
{:ok, contents} ->
path = Path.join(release.version_path, "sys.config")
args = [runtime_config?, contents]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~ts.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
:ok
{:error, reason} ->
invalid =
for {app, kv} <- sys_config,
{key, value} <- kv,
not valid_config?(value),
do: """
Application: #{inspect(app)}
Key: #{inspect(key)}
Value: #{inspect(value)}
"""
message =
case invalid do
[] ->
"Could not read configuration file. Reason: #{inspect(reason)}"
_ ->
"Could not read configuration file. It has invalid configuration terms " <>
"such as functions, references, and pids. Please make sure your configuration " <>
"is made of numbers, atoms, strings, maps, tuples and lists. The following entries " <>
"are wrong:\n#{Enum.join(invalid)}"
end
{:error, message} ->
{:error, message}
end
end
defp valid_config?(m) when is_map(m),
do: Enum.all?(Map.delete(m, :__struct__), &valid_config?/1)
defp consultable_sys_config(sys_config) do
contents =
Enum.map_intersperse(sys_config, ?,, fn {app, kv} ->
kv =
Enum.map_intersperse(kv, ?,, fn {key, value} ->
case Mix.Utils.consultable(value) do
{:ok, value} -> [?{, :io_lib.print(key), ?,, value, ?}]
{:error, term, reason} -> throw({:error, app, key, term, reason})
end
end)
defp valid_config?(l) when is_list(l), do: Enum.all?(l, &valid_config?/1)
defp valid_config?(t) when is_tuple(t), do: Enum.all?(Tuple.to_list(t), &valid_config?/1)
defp valid_config?(o), do: is_number(o) or is_atom(o) or is_binary(o)
[?{, :io_lib.print(app), ?,, ?[, kv, ?], ?}]
end)
{:ok, [?[, contents, ?]]}
catch
{:error, app, key, value, reason} ->
message = """
Could not write configuration file because it has invalid terms
Application: #{inspect(app)}
Key: #{inspect(key)}
Invalid value: #{inspect(value)}
Reason: #{reason}
"""
{:error, message}
end
defp merge_provider_config(%{config_providers: []}, sys_config, _), do: {sys_config, false}
+15 -1
View File
@@ -295,10 +295,24 @@ defmodule Mix.Task.Compiler do
end
defp run_compiler(compiler, args) do
result = normalize(Mix.Task.run("compile.#{compiler}", args), compiler)
args_maybe_force = maybe_prepend_force(args, compiler)
result = normalize(Mix.Task.run("compile.#{compiler}", args_maybe_force), compiler)
Enum.reduce(Mix.ProjectStack.pop_after_compiler(compiler), result, & &1.(&2))
end
defp maybe_prepend_force(args, compiler) do
args
|> Enum.any?(fn
"--force-" <> rest -> rest == compiler |> to_string() |> String.replace("_", "-")
_ -> false
end)
|> if do
["--force" | args]
else
args
end
end
# Normalize the compiler result to a diagnostic tuple
defp normalize(result, name) do
case result do
+1 -1
View File
@@ -26,7 +26,7 @@ defmodule Mix.Tasks.Cmd do
Your shell will expand "lib/*" and then pass multiple arguments to
`mix cmd`, which in turn passes them to `echo`. Note that, `mix cmd`
by itself, does not perform any shell expansion. This means that,
if you invoke `mix cmd` programatically, as in:
if you invoke `mix cmd` programmatically, as in:
Mix.Task.run("cmd", ["echo", "lib/*"])
+12 -49
View File
@@ -187,12 +187,22 @@ defmodule Mix.Tasks.Compile.App do
|> add_compile_env(current_properties)
|> add_modules(modules, compile_path)
contents = to_erl_term({:application, app, properties})
contents =
case Mix.Utils.consultable({:application, app, properties}) do
{:ok, contents} ->
contents
{:error, term, reason} ->
Mix.raise(
"\"def application\" has a term which cannot be written to .app files: #{inspect(term)} (#{reason})"
)
end
:application.unload(app)
:application.load({:application, app, properties})
Mix.Project.ensure_structure()
File.write!(target, [contents, ?.])
File.write!(target, IO.chardata_to_string([contents, ?.]))
File.touch!(target, new_mtime)
# If we just created the .app file, it will have touched
@@ -209,53 +219,6 @@ defmodule Mix.Tasks.Compile.App do
end
end
defp to_erl_term(tuple) when is_tuple(tuple) do
[?{, tuple |> Tuple.to_list() |> to_erl_head(), ?}]
end
defp to_erl_term(list) when is_list(list) do
[?[, to_erl_head(list), ?]]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(term) when is_function(term) or is_reference(term) or is_pid(term) do
Mix.raise(
"\"def application\" has a term which cannot be written to .app files: #{inspect(term)}"
)
end
defp to_erl_term(term) do
:io_lib.print(term)
end
defp to_erl_head([]), do: []
defp to_erl_head([h | t]), do: [to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([h | t]), do: [?,, to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([]), do: []
defp to_erl_tail(other), do: [?|, to_erl_term(other)]
defp current_app_properties(target) do
case :file.consult(target) do
{:ok, [{:application, _app, properties}]} -> properties
+4 -2
View File
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Compile do
@shortdoc "Compiles source files"
@moduledoc """
@moduledoc ~S"""
The main entry point to compile source files.
It simply runs the compilers registered in your project and returns
@@ -59,7 +59,9 @@ defmodule Mix.Tasks.Compile do
* `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations
(default is true except on errors)
* `--erl-config` - path to an Erlang term file that will be loaded as Mix config
* `--force` - forces compilation
* `--force` - forces compilation. You can also specify `--force-#{compiler}` for each compiler
invoked to force specifically that compiler to run (it requires the compiler to respect the
`--force` option, which is advised)
* `--list` - lists all enabled compilers
* `--listeners` - starts Mix listeners (they are started by default,
unless `--no-listeners` or `--no-deps-check` are given)
+2 -1
View File
@@ -271,7 +271,8 @@ defmodule Mix.Tasks.Escript.Build do
[]
end
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do
# We include hex in here as it is always an archive and it cannot be depended on
defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix, :hex] do
[app]
end
+15 -1
View File
@@ -112,12 +112,26 @@ defmodule Mix.Tasks.Help do
def run(["app:" <> app]) do
loadpaths!()
app = String.to_atom(app)
# If the application is not available, attempt to load it from Erlang/Elixir
if is_nil(Application.spec(app, :vsn)) do
try do
Mix.ensure_application!(app)
rescue
_ ->
# Otherwise, it may be a dep or the current project not yet compiled
if Mix.Project.get() do
Mix.Task.run("compile")
end
end
end
if modules = Application.spec(app, :modules) do
for module <- modules,
not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")),
{:docs_v1, _, :elixir, "text/markdown", %{"en" => <<doc::binary>>}, _, _} <-
{:docs_v1, _, _, "text/markdown", %{"en" => <<doc::binary>>}, _, _} <-
[Code.fetch_docs(module)] do
leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd()
"# #{inspect(module)}\n#{leading}\n"
+64 -15
View File
@@ -44,12 +44,22 @@ defmodule Mix.Tasks.Xref do
└── lib/a.ex
Because you have a compile-time dependency, any of the files `lib/a.ex`,
`lib/b.ex`, and `lib/c.ex` depend on will cause the whole cycle to
recompile. Therefore, your first priority to reduce compile times is
to remove such cycles. You can spot them by running:
`lib/b.ex`, and `lib/c.ex` depend on will cause `lib/a.ex` to recompile.
In other words, whenever you have a cycle, **a change to any file in the
cycle will cause all compile-time deps to recompile**. Therefore, your
first priority to reduce constant recompilations is to remove them.
You can spot them by running:
$ mix xref graph --format cycles --label compile-connected
> #### Use the --label option
>
> The job of `mix xref` is to explore relationships between files
> and it is expected that most of your files are either directly
> or indirectly connected. For this reason, it is strongly advised
> to pass the `--label` option to filter the amount of data,
> specifically with `compile-connected` (or `compile`) as values.
Whenever you find a compile-time dependency, such as `lib/a.ex` pointing
to `lib/b.ex`, there are two ways to remove them:
@@ -208,13 +218,11 @@ defmodule Mix.Tasks.Xref do
* `--exclude` - path to exclude. Can be repeated to exclude multiple paths.
* `--label` - only shows relationships with the given label.
The labels are "compile", "export" and "runtime". By default, the `--label`
option does not change how the graph is computed, it simply filters the
printed graph to show only relationships with the given label. However,
you can pass `--only-direct` to trim the graph to only the nodes that
have the direct relationship given by label. There is also a special
label called "compile-connected" that keeps only compile-time files with
at least one transitive dependency. See "Dependency types" section below.
The labels are "compile-connected", "compile", "export" and "runtime".
By default, the `--label` option does not change how the graph is computed,
it simply filters the printed graph to show only relationships with the given
label. However, you can pass `--only-direct` to trim the graph to only the
nodes that have the direct relationship given by label.
* `--group` - provide comma-separated paths to consider as a group. Dependencies
from and into multiple files of the group are considered a single dependency.
@@ -239,6 +247,9 @@ defmodule Mix.Tasks.Xref do
* `--min-cycle-size` - controls the minimum cycle size on formats
like `stats` and `cycles`
* `--min-cycle-label` - controls the minimum number of dependencies
with the given `--label` on a cycle
* `--format` - can be set to one of:
* `pretty` - prints the graph to the terminal using Unicode characters.
@@ -435,6 +446,7 @@ defmodule Mix.Tasks.Xref do
sink: :keep,
source: :keep,
min_cycle_size: :integer,
min_cycle_label: :integer,
output: :string
]
@@ -1218,9 +1230,22 @@ defmodule Mix.Tasks.Xref do
cycles
end
# :compile_connected is the same
min_cycle_label =
if integer = opts[:min_cycle_label] do
if filter == :all do
Mix.raise("--min-cycle-label requires the --label option to be given")
end
integer
else
1
end
# :compile_connected is the same as :compile
if cycle_fn = cycle_filter_fn(filter) do
Enum.filter(cycles, fn {_length, cycle} -> Enum.any?(cycle, cycle_fn) end)
Enum.filter(cycles, fn {_length, cycle} ->
Enum.count_until(cycle, cycle_fn, min_cycle_label) == min_cycle_label
end)
else
cycles
end
@@ -1269,11 +1294,35 @@ defmodule Mix.Tasks.Xref do
shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")
for {length, cycle} <- cycles do
shell.info("Cycle of length #{length}:\n")
meta =
cycle
|> Enum.reduce({0, 0}, fn
{_, :compile}, {compile, export} -> {compile + 1, export}
{_, :export}, {compile, export} -> {compile, export + 1}
{_, _}, {compile, export} -> {compile, export}
end)
|> case do
{0, 0} ->
""
{compile, export} ->
info =
if(compile > 0, do: ["#{compile} compile"], else: []) ++
if(export > 0, do: ["#{export} export"], else: [])
" (" <> Enum.join(info, ", ") <> ")"
end
shell.info("Cycle of length #{length}#{meta}:\n")
for {node, type} <- cycle do
type = if type, do: " (#{type})", else: ""
shell.info(" " <> node <> type)
shell.info(
case type do
:compile -> [:red, " #{node} (compile)"]
:export -> " #{node} (export)"
_ -> " #{node}"
end
)
end
shell.info("")
+73
View File
@@ -907,4 +907,77 @@ defmodule Mix.Utils do
uid
end
end
@doc """
Convert the given terms to a consultable Erlang term.
"""
def consultable(term) do
{:ok, to_erl_term(term)}
catch
{:error, term, reason} -> {:error, term, reason}
end
defp to_erl_term(tuple) when is_tuple(tuple) do
[?{, tuple |> Tuple.to_list() |> to_erl_head(), ?}]
end
defp to_erl_term(list) when is_list(list) do
if List.ascii_printable?(list) do
:io_lib.print(list)
else
[?[, to_erl_head(list), ?]]
end
end
defp to_erl_term(%Regex{re_pattern: {:re_pattern, _, _, _, ref}} = regex)
when is_reference(ref) do
throw({:error, regex, "you must use the /E modifier to store regexes"})
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(map) when is_map(map) do
inner =
Enum.map_intersperse(
:maps.to_list(:maps.iterator(map, :reversed)),
?,,
fn {key, value} -> [to_erl_term(key), "=>", to_erl_term(value)] end
)
[?#, ?{, inner, ?}]
end
defp to_erl_term(function) when is_function(function) do
fun_info = Function.info(function)
if fun_info[:type] == :external and fun_info[:env] == [] do
:io_lib.print(function)
else
throw({:error, function, "only functions in the form &Mod.fun/arity are allowed"})
end
end
defp to_erl_term(term) when is_reference(term) or is_pid(term) do
throw({:error, term, "PIDs and References are not supported"})
end
defp to_erl_term(term) do
:io_lib.print(term)
end
defp to_erl_head([]), do: []
defp to_erl_head([h | t]), do: [to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([h | t]), do: [?,, to_erl_term(h) | to_erl_tail(t)]
defp to_erl_tail([]), do: []
defp to_erl_tail(other), do: [?|, to_erl_term(other)]
end
+6 -5
View File
@@ -592,19 +592,20 @@ defmodule Mix.ReleaseTest do
assert make_sys_config(release([]), [foo: [bar: :baz]], "unused/runtime/path") == :ok
contents = File.read!(@sys_config)
assert contents =~ "%% RUNTIME_CONFIG=false"
assert contents =~ "[{foo,[{bar,baz}]}]."
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[foo: [bar: :baz]]]
end
test "writes sys_config with encoding" do
assert make_sys_config(
release([]),
[encoding: {:_μ, :"£", "£", ~c"£"}],
[encoding: [key: {:_μ, :"£", "£", ~c"£"}]],
"unused/runtime/path"
) ==
:ok
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[encoding: {:_μ, :"£", "£", ~c"£"}]]
assert contents == [[encoding: [key: {:_μ, :"£", "£", ~c"£"}]]]
end
test "writes the given sys_config with config providers" do
@@ -649,12 +650,12 @@ defmodule Mix.ReleaseTest do
end
test "errors on bad config" do
assert {:error, "Could not read configuration file." <> _} =
assert {:error, "Could not write configuration file " <> _} =
make_sys_config(release([]), [foo: [bar: self()]], "unused/runtime/path")
env = %{__ENV__ | lexical_tracker: self()}
assert {:error, "Could not read configuration file." <> _} =
assert {:error, "Could not write configuration file " <> _} =
make_sys_config(release([]), [foo: [bar: env]], "unused/runtime/path")
end
end
+24 -2
View File
@@ -110,7 +110,7 @@ defmodule Mix.Tasks.Compile.AppTest do
test "uses custom application settings" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(CustomProject)
env = [foo: [:one, "two", 3, 4], bar: [{} | %{foo: :bar}]]
env = [foo: [:one, "two", 3, 4, "฿", &List.flatten/1], bar: [{} | %{foo: :bar}]]
Process.put(:application,
maxT: :infinity,
@@ -132,7 +132,10 @@ defmodule Mix.Tasks.Compile.AppTest do
assert properties[:applications] ==
[:kernel, :stdlib, :elixir, :logger, :ex_unit, :example_app, :mix]
assert properties[:env] == [foo: [:one, "two", 3, 4], bar: [{} | %{foo: :bar}]]
assert properties[:env] == [
foo: [:one, "two", 3, 4, "฿", &List.flatten/1],
bar: [{} | %{foo: :bar}]
]
refute Keyword.has_key?(properties, :extra_applications)
end)
@@ -260,6 +263,25 @@ defmodule Mix.Tasks.Compile.AppTest do
end)
end
@tag :re_import
test "accepts only regexes without a reference" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(CustomProject)
Process.put(:application, env: [regex: ~r/foo/])
assert_raise Mix.Error, ~r/you must use the \/E modifier to store regexes/, fn ->
Mix.Tasks.Compile.App.run([])
end
Process.put(:application, env: [exported: ~r/foo/E])
Mix.Tasks.Compile.Elixir.run([])
Mix.Tasks.Compile.App.run([])
properties = parse_resource_file(:custom_project)
assert properties[:env] == [exported: ~r/foo/E]
end)
end
test ".app contains description and registered (as required by systools)" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
@@ -1253,6 +1253,23 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end)
end
test "recompiles with --force-elixir" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
assert Mix.Task.run("compile") == {:ok, []}
purge([A, B])
# Now we have a noop
Mix.Task.clear()
assert Mix.Task.run("compile") == {:noop, []}
# --force-elixir
Mix.Task.clear()
assert Mix.Task.run("compile", ["--verbose", "--force-elixir"]) == {:ok, []}
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
end)
end
test "compiles files with autoload disabled" do
in_fixture("no_mixfile", fn ->
Mix.Project.push(MixTest.Case.Sample)
+47 -3
View File
@@ -199,20 +199,64 @@ defmodule Mix.Tasks.HelpTest do
in_tmp(context.test, fn ->
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:mix"])
Mix.Tasks.Help.run(["app:iex"])
end)
assert output =~ "# Mix\n\nMix is a build tool"
assert output =~ "# IEx\n\nElixir's interactive shell."
if System.otp_release() >= "27" do
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:parsetools"])
end)
assert output =~ "# :leex"
end
end)
end
test "help unknown app:APP", context do
test "help app:UNKNOWN", context do
in_tmp(context.test, fn ->
Mix.Tasks.Help.run(["app:foobar"])
assert_received {:mix_shell, :error, ["Application foobar does not exist or is not loaded"]}
end)
end
defmodule ExampleProject do
def project do
[
app: :sample,
version: "0.1.0"
]
end
end
test "help app:APP for current project", context do
in_tmp(context.test, fn ->
Mix.Project.push(ExampleProject)
File.mkdir_p!("lib")
File.write!("lib/example.ex", ~s'''
defmodule Example do
@moduledoc """
This is an example module.
"""
end
''')
output =
capture_io(fn ->
Mix.Tasks.Help.run(["app:sample"])
end)
assert output =~ """
# Example
This is an example module.
"""
end)
end
test "help Elixir MODULE", context do
in_tmp(context.test, fn ->
output =
+25 -7
View File
@@ -500,7 +500,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -512,7 +512,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--label", "compile"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -524,7 +524,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--label", "compile-connected"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -539,7 +539,7 @@ defmodule Mix.Tasks.XrefTest do
assert_graph(["--format", "cycles", "--fail-above", "0"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -548,11 +548,11 @@ defmodule Mix.Tasks.XrefTest do
end
end
test "cycles with min_cycle_size matching actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
test "cycles with min_cycle_label matching actual length" do
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "1"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
@@ -560,6 +560,24 @@ defmodule Mix.Tasks.XrefTest do
""")
end
test "cycles with min_cycle_size matching actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "2"], """
1 cycles found. Showing them in decreasing size:
Cycle of length 2 (1 compile):
lib/a.ex (compile)
lib/b.ex
""")
end
test "cycles with min_cycle_label greater than actual length" do
assert_graph(["--format", "cycles", "--label", "compile", "--min-cycle-label", "2"], """
No cycles found
""")
end
test "cycles with min_cycle_size greater than actual length" do
assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """
No cycles found
+11 -1
View File
@@ -43,12 +43,22 @@ cover_exclude =
[]
end
# OTP 28.1+
re_import_exclude =
if Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
[]
else
[:re_import]
end
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("mix")
ExUnit.start(
trace: !!System.get_env("TRACE"),
exclude: epmd_exclude ++ os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude,
exclude:
epmd_exclude ++
os_exclude ++ git_exclude ++ line_exclude ++ cover_exclude ++ re_import_exclude,
include: line_include,
assert_receive_timeout: String.to_integer(System.get_env("ELIXIR_ASSERT_TIMEOUT", "300"))
)