Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
327063cc84 | ||
|
|
795d0a9583 | ||
|
|
1150ebad03 | ||
|
|
18feec4cd8 | ||
|
|
7d5920206b | ||
|
|
a694447601 | ||
|
|
19ae388dda | ||
|
|
6b5edea5f1 | ||
|
|
b736c23af9 | ||
|
|
63f0000ace | ||
|
|
5e8b187d08 | ||
|
|
aea1a47b8d | ||
|
|
eeba8e992d | ||
|
|
dd42dd8620 | ||
|
|
ed1ddfd338 | ||
|
|
d17df054e2 | ||
|
|
41f6ee3f2e | ||
|
|
9d31768f42 | ||
|
|
558e0ba6a6 | ||
|
|
ab1e3448f0 | ||
|
|
17cd103a28 | ||
|
|
60e6ceae10 | ||
|
|
f64ccd0871 | ||
|
|
6c463b2310 | ||
|
|
e310dd91d6 | ||
|
|
29ef147a22 | ||
|
|
74ab38439e | ||
|
|
5654752026 | ||
|
|
e78538a42b | ||
|
|
079b92e98e | ||
|
|
325877a224 | ||
|
|
097c4cf672 | ||
|
|
fe16fd3442 | ||
|
|
e520e8ce96 | ||
|
|
dd004539c6 | ||
|
|
2512cbaff0 | ||
|
|
a99a9cf47a | ||
|
|
677ad618e9 | ||
|
|
ff309747b7 | ||
|
|
e5ec5ce7f6 | ||
|
|
f3e669c471 | ||
|
|
bf915aa2e7 | ||
|
|
ef3e2796ef | ||
|
|
d8cc841ab0 | ||
|
|
7098ac8a16 | ||
|
|
e0658bb55a | ||
|
|
cf8c28c34b | ||
|
|
60dcd14390 | ||
|
|
8344e218a6 | ||
|
|
740b2d74df | ||
|
|
da0189e641 | ||
|
|
ad778a6f29 | ||
|
|
c35edd1ee8 | ||
|
|
bb8ad4406b | ||
|
|
111b48dcf4 | ||
|
|
6dccefe687 | ||
|
|
9ea950c9af | ||
|
|
7d100fff9f | ||
|
|
6b69c7f5ac | ||
|
|
691d402341 | ||
|
|
b96211a325 | ||
|
|
17b9ba61d9 | ||
|
|
f83b4e4e84 | ||
|
|
4de331597a | ||
|
|
6e295998ef | ||
|
|
f2dae095f8 | ||
|
|
6f5715fc30 | ||
|
|
1ece71a831 | ||
|
|
197351dd73 | ||
|
|
0d671dafea | ||
|
|
9ae7c39125 | ||
|
|
ba8fb4dff1 | ||
|
|
194661197e | ||
|
|
298acd1cdb | ||
|
|
97c608c346 | ||
|
|
f84bc19bf2 | ||
|
|
91375778cb |
+4
-1
@@ -13,7 +13,10 @@
|
||||
assert_same: 2,
|
||||
|
||||
# Errors tests
|
||||
assert_eval_raise: 3
|
||||
assert_eval_raise: 3,
|
||||
|
||||
# Float tests
|
||||
float_assert: 1
|
||||
],
|
||||
normalize_bitstring_modifiers: false
|
||||
]
|
||||
|
||||
@@ -78,6 +78,78 @@ Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorpora
|
||||
|
||||
Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs.
|
||||
|
||||
## v1.16.3 (2024-05-21)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
|
||||
* [Code.Formatter] Add brackets around keyword lists when formatting the left-hand side of `when`
|
||||
* [Kernel] Only infer size in pinned variable in binary strings when needed
|
||||
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
|
||||
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
|
||||
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Ensure translators are persisted across logger restarts
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure compile paths are accessible during compilation
|
||||
|
||||
## v1.16.2 (2024-03-10)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Emit `:defmodule` tracing event on module definition
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add `Mix.install_project_dir/0`
|
||||
* [Mix] Add environment variable for reusing `Mix.install/2` installation
|
||||
* [Mix.SCM] Add `Mix.SCM.delete/1`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Fix charlist formatting issue when a single-quoted charlist escapes a double-quote character
|
||||
* [Path] Fix regression on how `Path.relative_to/2` dealt with "." as input
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Discard mermaid fenced blocks from ansi docs
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Properly compared pinned values when building diffs
|
||||
|
||||
## v1.16.1 (2024-01-31)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand
|
||||
* [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation
|
||||
* [Kernel.ParallelCompiler] Always log errors at the end of compilation
|
||||
* [String] Fix `String.capitalize/1` with a single codepoint
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Fix autocompletion of function signatures on Erlang/OTP 26
|
||||
* [IEx] Do not assume `$HOME` is set
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix
|
||||
* [mix test] Properly resolve relative paths when running tests from individual files
|
||||
* [mix test] Properly resolve Windows paths when running tests from individual files
|
||||
|
||||
## v1.16.0 (2023-12-22)
|
||||
|
||||
### 1. Enhancements
|
||||
@@ -118,6 +190,7 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m
|
||||
* [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
|
||||
* [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
|
||||
* [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
|
||||
* [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata
|
||||
* [mix escript.install] Support `--sparse` option
|
||||
* [mix release] Include `include/` directory in releases
|
||||
* [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`
|
||||
|
||||
@@ -189,7 +189,6 @@ docs_elixir: compile ../ex_doc/bin/ex_doc
|
||||
$(Q) rm -rf doc/elixir
|
||||
$(call DOCS_COMPILE,Elixir,elixir,Kernel,--config "lib/elixir/scripts/elixir_docs.exs")
|
||||
$(call DOCS_CONFIG,elixir)
|
||||
cp -R lib/elixir/pages/images doc/elixir
|
||||
|
||||
docs_eex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (eex)"
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.16.0
|
||||
ELIXIR_VERSION=1.16.3
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
@@ -112,10 +112,10 @@ while [ $I -le $LENGTH ]; do
|
||||
C=1
|
||||
MODE="iex"
|
||||
;;
|
||||
-v|--no-halt|--dbg)
|
||||
-v|--no-halt)
|
||||
C=1
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex)
|
||||
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
|
||||
C=2
|
||||
;;
|
||||
--rpc-eval)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
|
||||
set ELIXIR_VERSION=1.16.0
|
||||
set ELIXIR_VERSION=1.16.3
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -158,6 +158,10 @@ defmodule Code do
|
||||
of keys to traverse in the application environment and `return` is either
|
||||
`{:ok, value}` or `:error`.
|
||||
|
||||
* `:defmodule` - (since v1.16.2) traced as soon as the definition of a module
|
||||
starts. This is invoked early on in the module life-cycle, `Module.open?/1`
|
||||
still returns `false` for such traces
|
||||
|
||||
* `{:on_module, bytecode, _ignore}` - (since v1.13.0) traced whenever a module
|
||||
is defined. This is equivalent to the `@after_compile` callback and invoked
|
||||
after any `@after_compile` in the given module. The third element is currently
|
||||
|
||||
@@ -6,7 +6,8 @@ defmodule Code.Formatter do
|
||||
@double_heredoc "\"\"\""
|
||||
@single_quote "'"
|
||||
@single_heredoc "'''"
|
||||
@sigil_c "~c\""
|
||||
@sigil_c_double "~c\""
|
||||
@sigil_c_single "~c'"
|
||||
@sigil_c_heredoc "~c\"\"\""
|
||||
@newlines 2
|
||||
@min_line 0
|
||||
@@ -299,7 +300,7 @@ defmodule Code.Formatter do
|
||||
remote_to_algebra(quoted, context, state)
|
||||
|
||||
meta[:delimiter] == ~s['''] ->
|
||||
{opener, quotes} = get_charlist_quotes(true, state)
|
||||
{opener, quotes} = get_charlist_quotes(:heredoc, state)
|
||||
|
||||
{doc, state} =
|
||||
entries
|
||||
@@ -309,7 +310,7 @@ defmodule Code.Formatter do
|
||||
{force_unfit(doc), state}
|
||||
|
||||
true ->
|
||||
{opener, quotes} = get_charlist_quotes(false, state)
|
||||
{opener, quotes} = get_charlist_quotes({:regular, entries}, state)
|
||||
list_interpolation_to_algebra(entries, quotes, state, opener, quotes)
|
||||
end
|
||||
end
|
||||
@@ -368,13 +369,14 @@ defmodule Code.Formatter do
|
||||
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
|
||||
case meta[:delimiter] do
|
||||
~s['''] ->
|
||||
{opener, quotes} = get_charlist_quotes(true, state)
|
||||
{opener, quotes} = get_charlist_quotes(:heredoc, state)
|
||||
string = list |> List.to_string() |> escape_heredoc(quotes)
|
||||
{opener |> concat(string) |> concat(quotes) |> force_unfit(), state}
|
||||
|
||||
~s['] ->
|
||||
{opener, quotes} = get_charlist_quotes(false, state)
|
||||
string = list |> List.to_string() |> escape_string(quotes)
|
||||
string = list |> List.to_string()
|
||||
{opener, quotes} = get_charlist_quotes({:regular, [string]}, state)
|
||||
string = escape_string(string, quotes)
|
||||
{opener |> concat(string) |> concat(quotes), state}
|
||||
|
||||
_other ->
|
||||
@@ -1957,6 +1959,14 @@ defmodule Code.Formatter do
|
||||
# fn a, b, c when d -> e end
|
||||
defp clause_args_to_algebra([{:when, meta, args}], state) do
|
||||
{args, right} = split_last(args)
|
||||
|
||||
# If there are any keywords, wrap them in lists
|
||||
args =
|
||||
Enum.map(args, fn
|
||||
[_ | _] = keyword -> {:__block__, [], [keyword]}
|
||||
other -> other
|
||||
end)
|
||||
|
||||
left = {{:special, :clause_args}, meta, [args]}
|
||||
binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state)
|
||||
end
|
||||
@@ -2410,19 +2420,23 @@ defmodule Code.Formatter do
|
||||
{left, right}
|
||||
end
|
||||
|
||||
defp get_charlist_quotes(_heredoc = false, state) do
|
||||
if state.normalize_charlists_as_sigils do
|
||||
{@sigil_c, @double_quote}
|
||||
else
|
||||
{@single_quote, @single_quote}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_charlist_quotes(_heredoc = true, state) do
|
||||
defp get_charlist_quotes(:heredoc, state) do
|
||||
if state.normalize_charlists_as_sigils do
|
||||
{@sigil_c_heredoc, @double_heredoc}
|
||||
else
|
||||
{@single_heredoc, @single_heredoc}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_charlist_quotes({:regular, chunks}, state) do
|
||||
cond do
|
||||
!state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
|
||||
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
|
||||
true -> {@sigil_c_double, @double_quote}
|
||||
end
|
||||
end
|
||||
|
||||
defp has_double_quote?(chunk) do
|
||||
is_binary(chunk) and chunk =~ @double_quote
|
||||
end
|
||||
end
|
||||
|
||||
@@ -352,6 +352,10 @@ defmodule Code.Normalizer do
|
||||
last = List.last(args)
|
||||
|
||||
cond do
|
||||
not allow_keyword?(form, arity) ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
|
||||
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
@@ -363,7 +367,7 @@ defmodule Code.Normalizer do
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
allow_keyword?(form, arity) ->
|
||||
true ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||
|
||||
@@ -384,10 +388,6 @@ defmodule Code.Normalizer do
|
||||
end
|
||||
|
||||
{form, meta, leading_args ++ last_args}
|
||||
|
||||
true ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ defmodule Enum do
|
||||
traversed as if it was an enumerable.
|
||||
|
||||
For a general overview of all functions in the `Enum` module, see
|
||||
[the `Enum` cheatsheet](enum-cheat.html).
|
||||
[the `Enum` cheatsheet](enum-cheat.cheatmd).
|
||||
|
||||
The functions in this module work in linear time. This means that, the
|
||||
time it takes to perform an operation grows at the same rate as the length
|
||||
@@ -3793,9 +3793,6 @@ defmodule Enum do
|
||||
iex> Enum.unzip([{:a, 1}, {:b, 2}, {:c, 3}])
|
||||
{[:a, :b, :c], [1, 2, 3]}
|
||||
|
||||
iex> Enum.unzip(%{a: 1, b: 2})
|
||||
{[:a, :b], [1, 2]}
|
||||
|
||||
"""
|
||||
@spec unzip(t) :: {[element], [element]}
|
||||
|
||||
@@ -4051,7 +4048,7 @@ defmodule Enum do
|
||||
...> end)
|
||||
[{1, 2, 3}, {1, 2, 3}]
|
||||
|
||||
iex> enums = [[1, 2], %{a: 3, b: 4}, [5, 6]]
|
||||
iex> enums = [[1, 2], [a: 3, b: 4], [5, 6]]
|
||||
...> Enum.zip_reduce(enums, [], fn elements, acc ->
|
||||
...> [List.to_tuple(elements) | acc]
|
||||
...> end)
|
||||
|
||||
@@ -31,7 +31,21 @@ defmodule Exception do
|
||||
@type arity_or_args :: non_neg_integer | list
|
||||
@type location :: keyword
|
||||
|
||||
@doc """
|
||||
Receives the arguments given to `raise/2` and returns the exception struct.
|
||||
|
||||
The default implementation accepts either a set of keyword arguments
|
||||
that is merged into the struct or a string to be used as the exception's message.
|
||||
"""
|
||||
@callback exception(term) :: t
|
||||
|
||||
@doc """
|
||||
Receives the exception struct and must return its message.
|
||||
|
||||
Most commonly exceptions have a message field which by default is accessed
|
||||
by this function. However, if an exception does not have a message field,
|
||||
this function must be explicitly implemented.
|
||||
"""
|
||||
@callback message(t) :: String.t()
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -1764,7 +1764,7 @@ defmodule File do
|
||||
## Examples
|
||||
|
||||
# Read a utf8 text file which may include BOM
|
||||
File.stream!("./test/test.txt", encoding: :utf8, trim_bom: true)
|
||||
File.stream!("./test/test.txt", [:trim_bom, encoding: :utf8])
|
||||
|
||||
# Read in 2048 byte chunks rather than lines
|
||||
File.stream!("./test/test.data", 2048)
|
||||
|
||||
+20
-1
@@ -275,6 +275,8 @@ defmodule Float do
|
||||
-56.0
|
||||
iex> Float.ceil(34.251, 2)
|
||||
34.26
|
||||
iex> Float.ceil(-0.01)
|
||||
-0.0
|
||||
|
||||
"""
|
||||
@spec ceil(float, precision_range) :: float
|
||||
@@ -332,6 +334,8 @@ defmodule Float do
|
||||
-6.0
|
||||
iex> Float.round(12.341444444444441, 15)
|
||||
12.341444444444441
|
||||
iex> Float.round(-0.01)
|
||||
-0.0
|
||||
|
||||
"""
|
||||
@spec round(float, precision_range) :: float
|
||||
@@ -340,8 +344,13 @@ defmodule Float do
|
||||
# and could be implemented in the future.
|
||||
def round(float, precision \\ 0)
|
||||
|
||||
def round(float, 0) when float == 0.0, do: float
|
||||
|
||||
def round(float, 0) when is_float(float) do
|
||||
float |> :erlang.round() |> :erlang.float()
|
||||
case float |> :erlang.round() |> :erlang.float() do
|
||||
zero when zero == 0.0 and float < 0.0 -> -0.0
|
||||
rounded -> rounded
|
||||
end
|
||||
end
|
||||
|
||||
def round(float, precision) when is_float(float) and precision in @precision_range do
|
||||
@@ -365,6 +374,8 @@ defmodule Float do
|
||||
case rounding do
|
||||
:ceil when sign === 0 -> 1 / power_of_10(precision)
|
||||
:floor when sign === 1 -> -1 / power_of_10(precision)
|
||||
:ceil when sign === 1 -> minus_zero()
|
||||
:half_up when sign === 1 -> minus_zero()
|
||||
_ -> 0.0
|
||||
end
|
||||
|
||||
@@ -394,6 +405,9 @@ defmodule Float do
|
||||
boundary = den <<< 52
|
||||
|
||||
cond do
|
||||
num == 0 and sign == 1 ->
|
||||
minus_zero()
|
||||
|
||||
num == 0 ->
|
||||
0.0
|
||||
|
||||
@@ -408,6 +422,11 @@ defmodule Float do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO remove once we require Erlang/OTP 27+
|
||||
# This function tricks the compiler to avoid this bug in previous versions:
|
||||
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
|
||||
defp minus_zero, do: -0.0
|
||||
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, initial)
|
||||
end
|
||||
|
||||
@@ -535,7 +535,8 @@ end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
fields = Map.keys(struct) -- [:__exception__, :__struct__]
|
||||
fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__])
|
||||
|
||||
only = Keyword.get(options, :only, fields)
|
||||
except = Keyword.get(options, :except, [])
|
||||
optional = Keyword.get(options, :optional, [])
|
||||
@@ -545,7 +546,7 @@ defimpl Inspect, for: Any do
|
||||
:ok = validate_option(:optional, optional, fields, module)
|
||||
|
||||
inspect_module =
|
||||
if fields == only and except == [] do
|
||||
if fields == Enum.sort(only) and except == [] do
|
||||
Inspect.Map
|
||||
else
|
||||
Inspect.Any
|
||||
|
||||
@@ -176,12 +176,17 @@ defmodule IO.ANSI.Docs do
|
||||
process_code(rest, [line], indent, options)
|
||||
end
|
||||
|
||||
defp process(["```" <> _line | rest], text, indent, options) do
|
||||
process_fenced_code_block(rest, text, indent, options, _delimiter = "```")
|
||||
defp process(["```mermaid" <> _line | rest], text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
|
||||
rest
|
||||
|> Enum.drop_while(&(&1 != "```"))
|
||||
|> Enum.drop(1)
|
||||
|> process([], indent, options)
|
||||
end
|
||||
|
||||
defp process(["~~~" <> _line | rest], text, indent, options) do
|
||||
process_fenced_code_block(rest, text, indent, options, _delimiter = "~~~")
|
||||
defp process(["```" <> _line | rest], text, indent, options) do
|
||||
process_fenced_code_block(rest, text, indent, options, _delimiter = "```")
|
||||
end
|
||||
|
||||
defp process(["<!--" <> line | rest], text, indent, options) do
|
||||
@@ -388,7 +393,7 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp process_fenced_code([line | rest], code, indent, options, delimiter) do
|
||||
if line === delimiter do
|
||||
if line == delimiter do
|
||||
process_code(rest, code, indent, options)
|
||||
else
|
||||
process_fenced_code(rest, [line | code], indent, options, delimiter)
|
||||
|
||||
@@ -2076,8 +2076,9 @@ defmodule Kernel do
|
||||
defp invalid_concat_left_argument_error(arg) do
|
||||
:erlang.error(
|
||||
ArgumentError.exception(
|
||||
"the left argument of <> operator inside a match should always be a literal " <>
|
||||
"binary because its size can't be verified. Got: #{arg}"
|
||||
"cannot perform prefix match because the left operand of <> has unknown size. " <>
|
||||
"The left operand of <> inside a match should either be a literal binary or " <>
|
||||
"an existing variable with the pin operator (such as ^some_var). Got: #{arg}"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -455,7 +455,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
# No more queue, nothing waiting, this cycle is done
|
||||
defp spawn_workers([], spawned, waiting, files, result, warnings, errors, state)
|
||||
when map_size(spawned) == 0 and map_size(waiting) == 0 do
|
||||
[] = errors
|
||||
# Print any spurious error that we may have found
|
||||
Enum.map(errors, fn {diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
end)
|
||||
|
||||
[] = files
|
||||
cycle_return = each_cycle_return(state.each_cycle.())
|
||||
state = cycle_timing(result, state)
|
||||
@@ -510,8 +514,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
if deadlocked do
|
||||
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
|
||||
else
|
||||
deadlock_errors = handle_deadlock(waiting, files)
|
||||
{return_error(deadlock_errors ++ errors, warnings), state}
|
||||
return_error(warnings, errors, state, fn ->
|
||||
handle_deadlock(waiting, files)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -681,12 +686,13 @@ defmodule Kernel.ParallelCompiler do
|
||||
state = %{state | timer_ref: timer_ref}
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:diagnostic, %{severity: :warning} = diagnostic} ->
|
||||
warnings = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | warnings]
|
||||
{:diagnostic, %{severity: :warning, file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
warnings = [%{diagnostic | file: file && Path.absname(file)} | warnings]
|
||||
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:diagnostic, %{severity: :error} = diagnostic} ->
|
||||
errors = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | errors]
|
||||
{:diagnostic, %{severity: :error} = diagnostic, read_snippet} ->
|
||||
errors = [{diagnostic, read_snippet} | errors]
|
||||
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:file_ok, child_pid, ref, file, lexical} ->
|
||||
@@ -706,10 +712,13 @@ defmodule Kernel.ParallelCompiler do
|
||||
spawn_workers(queue, new_spawned, waiting, new_files, result, warnings, errors, state)
|
||||
|
||||
{:file_error, child_pid, file, {kind, reason, stack}} ->
|
||||
print_error(file, kind, reason, stack)
|
||||
{_file, _new_spawned, new_files} = discard_file_pid(spawned, files, child_pid)
|
||||
terminate(new_files)
|
||||
{return_error([to_error(file, kind, reason, stack) | errors], warnings), state}
|
||||
|
||||
return_error(warnings, errors, state, fn ->
|
||||
print_error(file, kind, reason, stack)
|
||||
[to_error(file, kind, reason, stack)]
|
||||
end)
|
||||
|
||||
{:DOWN, ref, :process, pid, reason} when is_map_key(spawned, ref) ->
|
||||
# async spawned processes have no file, so we always have to delete the ref directly
|
||||
@@ -718,18 +727,27 @@ defmodule Kernel.ParallelCompiler do
|
||||
{file, spawned, files} = discard_file_pid(spawned, files, pid)
|
||||
|
||||
if file do
|
||||
print_error(file.file, :exit, reason, [])
|
||||
terminate(files)
|
||||
{return_error([to_error(file.file, :exit, reason, []) | errors], warnings), state}
|
||||
|
||||
return_error(warnings, errors, state, fn ->
|
||||
print_error(file.file, :exit, reason, [])
|
||||
[to_error(file.file, :exit, reason, [])]
|
||||
end)
|
||||
else
|
||||
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp return_error(errors, warnings) do
|
||||
defp return_error(warnings, errors, state, fun) do
|
||||
errors =
|
||||
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
%{diagnostic | file: file && Path.absname(file)}
|
||||
end)
|
||||
|
||||
info = %{compile_warnings: Enum.reverse(warnings), runtime_warnings: []}
|
||||
{:error, Enum.reverse(errors), info}
|
||||
{{:error, Enum.reverse(errors, fun.()), info}, state}
|
||||
end
|
||||
|
||||
defp update_result(result, kind, module, value) do
|
||||
|
||||
@@ -115,7 +115,7 @@ defmodule Macro do
|
||||
* `:from_brackets` - Used to determine whether a call to `Access.get/3` is from
|
||||
bracket syntax.
|
||||
|
||||
* `:from_interpolation` - Used to determine whether a call to `Access.get/3` is
|
||||
* `:from_interpolation` - Used to determine whether a call to `Kernel.to_string/1` is
|
||||
from interpolation.
|
||||
|
||||
* `:generated` - Whether the code should be considered as generated by
|
||||
@@ -136,8 +136,9 @@ defmodule Macro do
|
||||
|
||||
* `:closing` - contains metadata about the closing pair, such as a `}`
|
||||
in a tuple or in a map, or such as the closing `)` in a function call
|
||||
with parens. The `:closing` does not delimit the end of expression if
|
||||
there are `:do` and `:end` metadata (when `:token_metadata` is true)
|
||||
with parens (when `:token_metadata` is true). If the function call
|
||||
has a do-end block attached to it, its metadata is found under the
|
||||
`:do` and `:end` metadata
|
||||
|
||||
* `:column` - the column number of the AST node (when `:columns` is true).
|
||||
Note column information is always discarded from quoted code.
|
||||
@@ -154,8 +155,10 @@ defmodule Macro do
|
||||
`do`-`end` blocks (when `:token_metadata` is true)
|
||||
|
||||
* `:end_of_expression` - denotes when the end of expression effectively
|
||||
happens. Available for all expressions except the last one inside a
|
||||
`__block__` (when `:token_metadata` is true)
|
||||
happens (when `:token_metadata` is true). This is only available for
|
||||
direct children of a `__block__`, and it is either the location of a
|
||||
newline or of the `;` character. The last expression of `__block__`
|
||||
does not have this metadata.
|
||||
|
||||
* `:indentation` - indentation of a sigil heredoc
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.keys(%{a: 1, b: 2})
|
||||
Map.keys(%{a: 1, b: 2})
|
||||
[:a, :b]
|
||||
|
||||
"""
|
||||
@@ -161,7 +161,7 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.values(%{a: 1, b: 2})
|
||||
Map.values(%{a: 1, b: 2})
|
||||
[1, 2]
|
||||
|
||||
"""
|
||||
|
||||
@@ -167,8 +167,9 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
defp collect_results(count, diagnostics) do
|
||||
receive do
|
||||
{:diagnostic, diagnostic} ->
|
||||
diagnostic = format_diagnostic_file(diagnostic)
|
||||
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
diagnostic = %{diagnostic | file: file && Path.absname(file)}
|
||||
collect_results(count, [diagnostic | diagnostics])
|
||||
|
||||
{__MODULE__, _module, new_diagnostics} ->
|
||||
@@ -287,11 +288,6 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_diagnostic_file(%{file: file} = diagnostic) do
|
||||
%{diagnostic | file: file && Path.absname(file)}
|
||||
end
|
||||
|
||||
## Warning helpers
|
||||
|
||||
defp group_warnings(warnings) do
|
||||
|
||||
@@ -424,6 +424,8 @@ defmodule Path do
|
||||
defp relative_to_unforced(_, _, original), do: join(original)
|
||||
|
||||
defp relative_to_forced(path, path, _original), do: "."
|
||||
defp relative_to_forced(["."], _path, _original), do: "."
|
||||
defp relative_to_forced(path, ["."], _original), do: join(path)
|
||||
defp relative_to_forced([h | t1], [h | t2], original), do: relative_to_forced(t1, t2, original)
|
||||
|
||||
# this should only happen if we have two paths on different drives on windows
|
||||
|
||||
@@ -79,6 +79,27 @@ defmodule Port do
|
||||
are for advanced usage within the VM. Also consider using `System.cmd/3`
|
||||
if all you want is to execute a program and retrieve its return value.
|
||||
|
||||
> #### Windows argument splitting and untrusted arguments {: .warning}
|
||||
>
|
||||
> On Unix systems, arguments are passed to a new operating system
|
||||
> process as an array of strings but on Windows it is up to the child
|
||||
> process to parse them and some Windows programs may apply their own
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing
|
||||
>
|
||||
> This is particularly troublesome when invoking `.bat` or `.com` files
|
||||
> as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> vulnerable to malicious input and can be used to run arbitrary shell
|
||||
> commands.
|
||||
>
|
||||
> Therefore, if you are running on Windows and you execute batch
|
||||
> files or `.com` applications, you must not pass untrusted input as
|
||||
> arguments to the program. You may avoid accidentally executing them
|
||||
> by explicitly passing the extension of the program you want to run,
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
>
|
||||
> This affects both `spawn` and `spawn_executable`.
|
||||
|
||||
### spawn
|
||||
|
||||
The `:spawn` tuple receives a binary that is going to be executed as a
|
||||
|
||||
@@ -448,7 +448,7 @@ defmodule Protocol do
|
||||
## Examples
|
||||
|
||||
# Get Elixir's ebin directory path and retrieve all protocols
|
||||
iex> path = :code.lib_dir(:elixir, :ebin)
|
||||
iex> path = Application.app_dir(:elixir, "ebin")
|
||||
iex> mods = Protocol.extract_protocols([path])
|
||||
iex> Enumerable in mods
|
||||
true
|
||||
@@ -477,7 +477,7 @@ defmodule Protocol do
|
||||
## Examples
|
||||
|
||||
# Get Elixir's ebin directory path and retrieve all protocols
|
||||
iex> path = :code.lib_dir(:elixir, :ebin)
|
||||
iex> path = Application.app_dir(:elixir, "ebin")
|
||||
iex> mods = Protocol.extract_impls(Enumerable, [path])
|
||||
iex> List in mods
|
||||
true
|
||||
|
||||
@@ -1301,16 +1301,16 @@ defmodule Registry do
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value)
|
||||
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}])
|
||||
[{"world", self(), :value}, {"hello", self(), :value}]
|
||||
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) |> Enum.sort()
|
||||
[{"hello", self(), :value}, {"world", self(), :value}]
|
||||
|
||||
Get all keys in the registry:
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value)
|
||||
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}])
|
||||
["world", "hello"]
|
||||
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
|
||||
["hello", "world"]
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
|
||||
@@ -966,6 +966,7 @@ defmodule String do
|
||||
|
||||
def capitalize(string, mode) when is_binary(string) do
|
||||
case :unicode_util.gc(string) do
|
||||
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
|
||||
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[] -> ""
|
||||
|
||||
@@ -1005,6 +1005,25 @@ defmodule System do
|
||||
`Port` module describes this problem and possible solutions under
|
||||
the "Zombie processes" section.
|
||||
|
||||
> #### Windows argument splitting and untrusted arguments {: .warning}
|
||||
>
|
||||
> On Unix systems, arguments are passed to a new operating system
|
||||
> process as an array of strings but on Windows it is up to the child
|
||||
> process to parse them and some Windows programs may apply their own
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing
|
||||
>
|
||||
> This is particularly troublesome when invoking `.bat` or `.com` files
|
||||
> as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> vulnerable to malicious input and can be used to run arbitrary shell
|
||||
> commands.
|
||||
>
|
||||
> Therefore, if you are running on Windows and you execute batch
|
||||
> files or `.com` applications, you must not pass untrusted input as
|
||||
> arguments to the program. You may avoid accidentally executing them
|
||||
> by explicitly passing the extension of the program you want to run,
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> System.cmd("echo", ["hello"])
|
||||
|
||||
@@ -40,7 +40,7 @@ defmodule Task do
|
||||
as they are *always* sent. If you are not expecting a reply,
|
||||
consider using `Task.start_link/1` as detailed below.
|
||||
|
||||
2. async tasks link the caller and the spawned process. This
|
||||
2. Async tasks link the caller and the spawned process. This
|
||||
means that, if the caller crashes, the task will crash
|
||||
too and vice-versa. This is on purpose: if the process
|
||||
meant to receive the result no longer exists, there is
|
||||
|
||||
@@ -152,7 +152,7 @@ defmodule Task.Supervisor do
|
||||
Starts a task that can be awaited on.
|
||||
|
||||
The `supervisor` must be a reference as defined in `Supervisor`.
|
||||
The task will still be linked to the caller, see `Task.async/3` for
|
||||
The task will still be linked to the caller, see `Task.async/1` for
|
||||
more information and `async_nolink/3` for a non-linked variant.
|
||||
|
||||
Raises an error if `supervisor` has reached the maximum number of
|
||||
@@ -174,7 +174,7 @@ defmodule Task.Supervisor do
|
||||
Starts a task that can be awaited on.
|
||||
|
||||
The `supervisor` must be a reference as defined in `Supervisor`.
|
||||
The task will still be linked to the caller, see `Task.async/3` for
|
||||
The task will still be linked to the caller, see `Task.async/1` for
|
||||
more information and `async_nolink/3` for a non-linked variant.
|
||||
|
||||
Raises an error if `supervisor` has reached the maximum number of
|
||||
@@ -196,7 +196,7 @@ defmodule Task.Supervisor do
|
||||
Starts a task that can be awaited on.
|
||||
|
||||
The `supervisor` must be a reference as defined in `Supervisor`.
|
||||
The task won't be linked to the caller, see `Task.async/3` for
|
||||
The task won't be linked to the caller, see `Task.async/1` for
|
||||
more information.
|
||||
|
||||
Raises an error if `supervisor` has reached the maximum number of
|
||||
@@ -283,7 +283,7 @@ defmodule Task.Supervisor do
|
||||
Starts a task that can be awaited on.
|
||||
|
||||
The `supervisor` must be a reference as defined in `Supervisor`.
|
||||
The task won't be linked to the caller, see `Task.async/3` for
|
||||
The task won't be linked to the caller, see `Task.async/1` for
|
||||
more information.
|
||||
|
||||
Raises an error if `supervisor` has reached the maximum number of
|
||||
|
||||
@@ -656,16 +656,16 @@ defmodule URI do
|
||||
scheme = String.downcase(scheme, :ascii)
|
||||
|
||||
case map do
|
||||
%{port: port} when port != :undefined ->
|
||||
%{port: port} when is_integer(port) ->
|
||||
%{uri | scheme: scheme}
|
||||
|
||||
%{} ->
|
||||
case default_port(scheme) do
|
||||
nil -> %{uri | scheme: scheme}
|
||||
port -> %{uri | scheme: scheme, port: port}
|
||||
end
|
||||
%{uri | scheme: scheme, port: default_port(scheme)}
|
||||
end
|
||||
|
||||
%{port: :undefined} ->
|
||||
%{uri | port: nil}
|
||||
|
||||
%{} ->
|
||||
uri
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document outlines potential anti-patterns related to your code and particular Elixir idioms and features.
|
||||
|
||||
## Comments
|
||||
## Comments overuse
|
||||
|
||||
#### Problem
|
||||
|
||||
@@ -251,7 +251,9 @@ end
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To address this anti-pattern, related arguments can be grouped using maps, structs, or even tuples. This effectively reduces the number of arguments, simplifying the function's interface. In the case of `loan/6`, its arguments were grouped into two different maps, thereby reducing its arity to `loan/2`:
|
||||
To address this anti-pattern, related arguments can be grouped using key-value data structures, such as maps, structs, or even keyword lists in the case of optional arguments. This effectively reduces the number of arguments and the key-value data structures adds clarity to the caller.
|
||||
|
||||
For this particular example, the arguments to `loan/6` can be grouped into two different maps, thereby reducing its arity to `loan/2`:
|
||||
|
||||
```elixir
|
||||
defmodule Library do
|
||||
@@ -315,7 +317,7 @@ When a key is expected to exist in a map, it must be accessed using the `map.key
|
||||
|
||||
When a key is optional, the `map[:key]` notation must be used instead. This way, if the informed key does not exist, `nil` is returned. This is the dynamic notation, as it also supports dynamic key access, such as `map[some_var]`.
|
||||
|
||||
When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpected missing, you will have a `nil` value propagate through the system, instead of raising on map access.
|
||||
When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpectedly missing, you will have a `nil` value propagate through the system, instead of raising on map access.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -370,7 +372,7 @@ iex> Graphics.plot(point_2d)
|
||||
{2, 3, nil}
|
||||
iex> Graphics.plot(bad_point)
|
||||
** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that
|
||||
graphic.ex:4: Graphics.plot/1 # <= the :z key does not exist!
|
||||
graphic.ex:4: Graphics.plot/1 # <= the :x key does not exist!
|
||||
```
|
||||
|
||||
Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples.
|
||||
@@ -379,17 +381,17 @@ An alternative to refactor this anti-pattern is to use pattern matching, definin
|
||||
|
||||
```elixir
|
||||
defmodule Graphics do
|
||||
# 2d
|
||||
def plot(%{x: x, y: y}) do
|
||||
# Some other code...
|
||||
{x, y}
|
||||
end
|
||||
|
||||
# 3d
|
||||
def plot(%{x: x, y: y, z: z}) do
|
||||
# Some other code...
|
||||
{x, y, z}
|
||||
end
|
||||
|
||||
# 2d
|
||||
def plot(%{x: x, y: y}) do
|
||||
# Some other code...
|
||||
{x, y}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ end
|
||||
|
||||
```elixir
|
||||
iex> AlternativeInteger.parse("13")
|
||||
{13, ""}
|
||||
13
|
||||
iex> AlternativeInteger.parse("13", discard_rest: true)
|
||||
13
|
||||
iex> AlternativeInteger.parse("13", discard_rest: false)
|
||||
@@ -167,7 +167,7 @@ defmodule MyModule do
|
||||
end
|
||||
```
|
||||
|
||||
This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.html#trailing-bang-foo).
|
||||
This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.md#trailing-bang-foo).
|
||||
|
||||
Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read!/1` is implemented as:
|
||||
|
||||
@@ -245,11 +245,11 @@ end
|
||||
|
||||
#### Problem
|
||||
|
||||
Using multi-clause functions in Elixir, to group functions of the same name, is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which configures an anti-pattern.
|
||||
Using multi-clause functions is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which is an anti-pattern.
|
||||
|
||||
#### Example
|
||||
|
||||
A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition, in a way the behaviour of each clause is completely distinct from the other ones. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them.
|
||||
A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behaviour of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them.
|
||||
|
||||
Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated.
|
||||
|
||||
@@ -274,7 +274,7 @@ If updating an animal is completely different from updating a product and requir
|
||||
|
||||
#### Refactoring
|
||||
|
||||
As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's current users, so be careful!
|
||||
As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's callers, so be careful!
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
@@ -374,7 +374,7 @@ iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi")
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern and make the library more adaptable and flexible, this type of configuration must be performed via parameters in function calls. The code shown below performs the refactoring of the `split/1` function by accepting [keyword lists](`Keyword`) as a new optional parameter. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application:
|
||||
To remove this anti-pattern, this type of configuration should be performed using a parameter passed to the function. The code shown below performs the refactoring of the `split/1` function by accepting [keyword lists](`Keyword`) as a new optional parameter. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application:
|
||||
|
||||
```elixir
|
||||
defmodule DashSplitter do
|
||||
@@ -392,7 +392,55 @@ iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #<= default config is u
|
||||
["Lucas", "Francisco-da-Matta-Vegi"]
|
||||
```
|
||||
|
||||
#### Additional Remarks
|
||||
Of course, not all uses of the application environment by libraries are incorrect. One example is using configuration to replace a component (or dependency) of a library by another that must behave the exact same. Consider a library that needs to parse CSV files. The library author may pick one package to use as default parser but allow its users to swap to different implementations via the application environment. At the end of the day, choosing a different CSV parser should not change the outcome, and library authors can even enforce this by [defining behaviours](../references/typespecs.md#behaviours) with the exact semantics they expect.
|
||||
|
||||
#### Additional remarks: Supervision trees
|
||||
|
||||
In practice, libraries may require additional configuration beyond keyword lists. For example, if a library needs to start a supervision tree, how can the user of said library customize its supervision tree? Given the supervision tree itself is global (as it belongs to the library), library authors may be tempted to use the application configuration once more.
|
||||
|
||||
One solution is for the library to provide its own child specification, instead of starting the supervision tree itself. This allows the user to start all necessary processes under its own supervision tree, potentially passing custom configuration options during initialization.
|
||||
|
||||
You can see this pattern in practice in projects like [Nx](https://github.com/elixir-nx/nx) and [DNS Cluster](https://github.com/phoenixframework/dns_cluster). These libraries require that you list processes under your own supervision tree:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{DNSCluster, query: "my.subdomain"}
|
||||
]
|
||||
```
|
||||
|
||||
In such cases, if the users of `DNSCluster` need to configure DNSCluster per environment, they can be the ones reading from the application environment, without the library forcing them to:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{DNSCluster, query: Application.get_env(:my_app, :dns_cluster_query) || :ignore}
|
||||
]
|
||||
```
|
||||
|
||||
Some libraries, such as [Ecto](https://github.com/elixir-ecto/ecto), allow you to pass your application name as an option (called `:otp_app` or similar) and then automatically read the environment from *your* application. While this addresses the issue with the application environment being global, as they read from each individual application, it comes at the cost of some indirection, compared to the example above where users explicitly read their application environment from their own code, whenever desired.
|
||||
|
||||
#### Additional remarks: Compile-time configuration
|
||||
|
||||
A similar discussion entails compile-time configuration. What if a library author requires some configuration to be provided at compilation time?
|
||||
|
||||
Once again, instead of forcing users of your library to provide compile-time configuration, you may want to allow users of your library to generate the code themselves. That's the approach taken by libraries such as [Ecto](https://github.com/elixir-ecto/ecto):
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Repo do
|
||||
use Ecto.Repo, adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
```
|
||||
|
||||
Instead of forcing developers to share a single repository, Ecto allows its users to define as many repositories as they want. Given the `:adapter` configuration is required at compile-time, it is a required value on `use Ecto.Repo`. If developers want to configure the adapter per environment, then it is their choice:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Repo do
|
||||
use Ecto.Repo, adapter: Application.compile_env(:my_app, :repo_adapter)
|
||||
end
|
||||
```
|
||||
|
||||
On the other hand, [code generation comes with its own anti-patterns](macro-anti-patterns.md), and must be considered carefully. That's to say: while using the application environment for libraries is discouraged, especially compile-time configuration, in some cases they may be the best option. For example, consider a library needs to parse CSV or JSON files to generate code based on data files. In such cases, it is best to provide reasonable defaults and make them customizable via the application environment, instead of asking each user of your library to generate the exact same code.
|
||||
|
||||
#### Additional remarks: Mix tasks
|
||||
|
||||
For Mix tasks and related tools, it may be necessary to provide per-project configuration. For example, imagine you have a `:linter` project, which supports setting the output file and the verbosity level. You may choose to configure it through application environment:
|
||||
|
||||
@@ -418,7 +466,7 @@ def project do
|
||||
end
|
||||
```
|
||||
|
||||
Additonally, if a Mix task is available, you can also accept these options as command line arguments (see `OptionParser`):
|
||||
Additionally, if a Mix task is available, you can also accept these options as command line arguments (see `OptionParser`):
|
||||
|
||||
```bash
|
||||
mix linter --output-file /path/to/output.json --verbosity 3
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This document outlines potential anti-patterns related to meta-programming.
|
||||
|
||||
## Large code generation by macros
|
||||
## Large code generation
|
||||
|
||||
#### Problem
|
||||
|
||||
@@ -24,7 +24,7 @@ defmodule Routes do
|
||||
end
|
||||
|
||||
if not is_atom(handler) do
|
||||
raise ArgumentError, "route must be a module"
|
||||
raise ArgumentError, "handler must be a module"
|
||||
end
|
||||
|
||||
@store_route_for_compilation {route, handler}
|
||||
@@ -51,7 +51,7 @@ defmodule Routes do
|
||||
end
|
||||
|
||||
if not is_atom(handler) do
|
||||
raise ArgumentError, "route must be a module"
|
||||
raise ArgumentError, "handler must be a module"
|
||||
end
|
||||
|
||||
Module.put_attribute(module, :store_route_for_compilation, {route, handler})
|
||||
@@ -165,7 +165,7 @@ error: imported ModuleA.foo/0 conflicts with local function
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, we recommend library authors to avoid providing `__using__/1` callbacks whenever it can be replaced by `alias/1` or `import/1` directives. In the following code, we assume `use Library` is no longer available and `ClientApp` was refactored in this way, and with that, the code is clearer and the conflict as previously shown no longer exists:
|
||||
To remove this anti-pattern, we recommend library authors avoid providing `__using__/1` callbacks whenever it can be replaced by `alias/1` or `import/1` directives. In the following code, we assume `use Library` is no longer available and `ClientApp` was refactored in this way, and with that, the code is clearer and the conflict as previously shown no longer exists:
|
||||
|
||||
```elixir
|
||||
defmodule ClientApp do
|
||||
|
||||
@@ -262,8 +262,8 @@ defmodule Counter do
|
||||
|
||||
@doc "Starts a counter process."
|
||||
def start_link(opts \\ []) do
|
||||
initial_valye = Keyword.get(opts, :initial_value, 0)
|
||||
name = Keywoird.get(opts, :name, __MODULE__)
|
||||
initial_value = Keyword.get(opts, :initial_value, 0)
|
||||
name = Keyword.get(opts, :name, __MODULE__)
|
||||
GenServer.start(__MODULE__, initial_value, name: name)
|
||||
end
|
||||
|
||||
@@ -310,7 +310,7 @@ iex> Counter.bump(Counter, 7)
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. Both are initialized to `0`. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
|
||||
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
|
||||
|
||||
```elixir
|
||||
defmodule SupervisedProcess.Application do
|
||||
|
||||
@@ -707,11 +707,11 @@ fruits = ["apple", "banana", "grape", "orange", "pear"]
|
||||
iex> Enum.slide(fruits, 2, 0)
|
||||
["grape", "apple", "banana", "orange", "pear"]
|
||||
iex> Enum.slide(fruits, 2, 4)
|
||||
["apple", "banana", "orange", "pear", "grape", ]
|
||||
["apple", "banana", "orange", "pear", "grape"]
|
||||
iex> Enum.slide(fruits, 1..3, 0)
|
||||
["banana", "grape", "orange", "apple", "pear"]
|
||||
iex> Enum.slide(fruits, 1..3, 4)
|
||||
["banana", "pear", "grape", "orange", "apple"]
|
||||
["apple", "pear", "banana", "grape", "orange"]
|
||||
```
|
||||
|
||||
## Reversing
|
||||
|
||||
@@ -227,7 +227,7 @@ Elixir also supports string interpolation:
|
||||
```elixir
|
||||
iex> string = "world"
|
||||
iex> "hello #{string}!"
|
||||
"hello world"
|
||||
"hello world!"
|
||||
```
|
||||
|
||||
String concatenation requires both sides to be strings but interpolation supports any data type that may be converted to a string:
|
||||
@@ -325,6 +325,6 @@ iex> 1 === 1.0
|
||||
false
|
||||
```
|
||||
|
||||
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](Kernel.html#module-structural-comparison).
|
||||
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`).
|
||||
|
||||
Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward.
|
||||
|
||||
@@ -257,7 +257,7 @@ iex> heartbeats_per_minute = [99, 97, 116]
|
||||
~c"cat"
|
||||
```
|
||||
|
||||
You can always for charlists to be printed in their list representation by calling the `inspect/2` function:
|
||||
You can always force charlists to be printed in their list representation by calling the `inspect/2` function:
|
||||
|
||||
```elixir
|
||||
iex> inspect(heartbeats_per_minute, charlists: :as_list)
|
||||
|
||||
@@ -13,7 +13,7 @@ iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
|
||||
[2, 12]
|
||||
```
|
||||
|
||||
The `Enum` module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code. For a general overview of all functions in the `Enum` module, see [the `Enum` cheatsheet](enum-cheat.html).
|
||||
The `Enum` module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code. For a general overview of all functions in the `Enum` module, see [the `Enum` cheatsheet](enum-cheat.cheatmd).
|
||||
|
||||
Elixir also provides ranges (see `Range`), which are also enumerable:
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ defmodule Unless do
|
||||
end
|
||||
```
|
||||
|
||||
The function receives the arguments and passes them to `if/2`. However, as we learned in the [previous guide](quote-and-unquote.html), the macro will receive quoted expressions, inject them into the quote, and finally return another quoted expression.
|
||||
The function receives the arguments and passes them to `if/2`. However, as we learned in the [previous guide](quote-and-unquote.md), the macro will receive quoted expressions, inject them into the quote, and finally return another quoted expression.
|
||||
|
||||
Let's start `iex` with the module above:
|
||||
|
||||
@@ -223,7 +223,7 @@ It is important that a macro is defined before its usage. Failing to define a ma
|
||||
|
||||
```elixir
|
||||
iex> defmodule Sample do
|
||||
...> def four, do: two + two
|
||||
...> def four, do: two() + two()
|
||||
...> defmacrop two, do: 2
|
||||
...> end
|
||||
** (CompileError) iex:2: function two/0 undefined
|
||||
|
||||
@@ -183,7 +183,7 @@ A GUI should pop up containing all sorts of information about our system, from g
|
||||
|
||||
In the Applications tab, you will see all applications currently running in your system alongside their supervision tree. You can select the `kv` application to explore it further:
|
||||
|
||||
<img src="images/kv-observer.png" alt="Observer GUI screenshot" />
|
||||
<img src="assets/kv-observer.png" alt="Observer GUI screenshot" />
|
||||
|
||||
Not only that, as you create new buckets on the terminal, you should see new processes spawned in the supervision tree shown in Observer:
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Finally, these operators appear in the precedence table above but are only meani
|
||||
* `=>` - see [`%{}`](`%{}/1`)
|
||||
* `when` - see [Guards](patterns-and-guards.md#guards)
|
||||
* `<-` - see [`for`](`for/1`) and [`with`](`with/1`)
|
||||
* `\\` - see [Default arguments](Kernel.html#def/2-default-arguments)
|
||||
* `\\` - see [Default arguments](`Kernel#def/2-default-arguments`)
|
||||
|
||||
## Comparison operators
|
||||
|
||||
|
||||
@@ -159,13 +159,13 @@ iex> [head | tail] = []
|
||||
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator ([`++`](`++/2`)):
|
||||
|
||||
```elixir
|
||||
iex> 'hello ' ++ world = 'hello world'
|
||||
'hello world'
|
||||
iex> ~c"hello " ++ world = ~c"hello world"
|
||||
~c"hello world"
|
||||
iex> world
|
||||
'world'
|
||||
~c"world"
|
||||
```
|
||||
|
||||
Which is equivalent to matching on `[?h, ?e, ?l, ?l, ?o, ?\s | world]`. Suffix matches (`hello ++ ' world'`) are not valid patterns.
|
||||
Which is equivalent to matching on `[?h, ?e, ?l, ?l, ?o, ?\s | world]`. Suffix matches (`hello ++ ~c" world"`) are not valid patterns.
|
||||
|
||||
### Maps
|
||||
|
||||
@@ -274,7 +274,7 @@ Not all expressions are allowed in guard clauses, but only a handful of them. Th
|
||||
|
||||
### List of allowed functions and operators
|
||||
|
||||
You can find the built-in list of guards [in the `Kernel` module](Kernel.html#guards). Here is an overview:
|
||||
You can find the built-in list of guards [in the `Kernel` module](`Kernel#guards`). Here is an overview:
|
||||
|
||||
* comparison operators ([`==`](`==/2`), [`!=`](`!=/2`), [`===`](`===/2`), [`!==`](`!==/2`),
|
||||
[`<`](`</2`), [`<=`](`<=/2`), [`>`](`>/2`), [`>=`](`>=/2`))
|
||||
|
||||
@@ -92,7 +92,7 @@ Variables in Elixir must start with an underscore or a Unicode letter that is no
|
||||
|
||||
### Non-qualified calls (local calls)
|
||||
|
||||
Non-qualified calls, such as `add(1, 2)`, must start with characters and then follow the same rules as as variables, which are optionally followed by parentheses, and then arguments.
|
||||
Non-qualified calls, such as `add(1, 2)`, must start with characters and then follow the same rules as variables, which are optionally followed by parentheses, and then arguments.
|
||||
|
||||
Parentheses are required for zero-arity calls (i.e. calls without arguments), to avoid ambiguity with variables. If parentheses are used, they must immediately follow the function name *without spaces*. For example, `add (1, 2)` is a syntax error, since `(1, 2)` is treated as an invalid block which is attempted to be given as a single argument to `add`.
|
||||
|
||||
@@ -104,7 +104,7 @@ As many programming languages, Elixir also support operators as non-qualified ca
|
||||
|
||||
### Qualified calls (remote calls)
|
||||
|
||||
Qualified calls, such as `Math.add(1, 2)`, must start with characters and then follow the same rules as as variables, which are optionally followed by parentheses, and then arguments. Qualified calls also support operators, such as `Kernel.+(1, 2)`. Elixir also allows the function name to be written between double- or single-quotes, allowing any character in between the quotes, such as `Math."++add++"(1, 2)`.
|
||||
Qualified calls, such as `Math.add(1, 2)`, must start with characters and then follow the same rules as variables, which are optionally followed by parentheses, and then arguments. Qualified calls also support operators, such as `Kernel.+(1, 2)`. Elixir also allows the function name to be written between double- or single-quotes, allowing any character in between the quotes, such as `Math."++add++"(1, 2)`.
|
||||
|
||||
Similar to non-qualified calls, parentheses have different meaning for zero-arity calls (i.e. calls without arguments). If parentheses are used, such as `mod.fun()`, it means a function call. If parenthesis are skipped, such as `map.field`, it means accessing a field of a map.
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ end
|
||||
```
|
||||
|
||||
This code generates a warning letting you know that you are mistakenly implementing `parse/0` instead of `parse/1`.
|
||||
You can read more about `@impl` in the [module documentation](Module.html#module-impl).
|
||||
You can read more about `@impl` in the [module documentation](`Module#module-impl`).
|
||||
|
||||
### Using behaviours
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ Unicode atoms in Elixir follow the identifier rule above with the following modi
|
||||
* `<Start>` additionally includes the code point `_` (005F)
|
||||
* `<Continue>` additionally includes the code point `@` (0040)
|
||||
|
||||
Note atoms can also be quoted, which allows any characters, such as `:"hello elixir"`. All Elixir operators are also valid atoms, such as `:+`, `:@`, `:|>`, and others. The full description of valid atoms is available in the ["Atoms" section in the syntax reference](syntax-reference.html#atoms).
|
||||
Note atoms can also be quoted, which allows any characters, such as `:"hello elixir"`. All Elixir operators are also valid atoms, such as `:+`, `:@`, `:|>`, and others. The full description of valid atoms is available in the ["Atoms" section in the syntax reference](syntax-reference.md#atoms).
|
||||
|
||||
#### Variables, local calls, and remote calls
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
|
||||
[
|
||||
assets: "lib/elixir/pages/images",
|
||||
extras: [
|
||||
"lib/elixir/pages/getting-started/introduction.md",
|
||||
"lib/elixir/pages/getting-started/basic-types.md",
|
||||
|
||||
@@ -36,9 +36,9 @@ expand(BitstrMeta, Fun, [{'::', Meta, [Left, Right]} | T], Acc, S, E, Alignment,
|
||||
MatchOrRequireSize = RequireSize or is_match_size(T, EL),
|
||||
EType = expr_type(ELeft),
|
||||
ExpectSize = case ELeft of
|
||||
_ when not MatchOrRequireSize -> optional;
|
||||
{'^', _, [{_, _, _}]} -> {infer, ELeft};
|
||||
_ when MatchOrRequireSize -> required;
|
||||
_ -> optional
|
||||
_ -> required
|
||||
end,
|
||||
{ERight, EAlignment, SS, ES} = expand_specs(EType, Meta, Right, SL, OriginalS, EL, ExpectSize),
|
||||
|
||||
|
||||
@@ -115,14 +115,17 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) ->
|
||||
},
|
||||
|
||||
case get(elixir_code_diagnostics) of
|
||||
undefined -> print_diagnostic(Diagnostic, ReadSnippet);
|
||||
{Tail, true} -> put(elixir_code_diagnostics, {[print_diagnostic(Diagnostic, ReadSnippet) | Tail], true});
|
||||
{Tail, false} -> put(elixir_code_diagnostics, {[Diagnostic | Tail], false})
|
||||
end,
|
||||
undefined ->
|
||||
case get(elixir_compiler_info) of
|
||||
undefined -> print_diagnostic(Diagnostic, ReadSnippet);
|
||||
{CompilerPid, _} -> CompilerPid ! {diagnostic, Diagnostic, ReadSnippet}
|
||||
end;
|
||||
|
||||
case get(elixir_compiler_info) of
|
||||
undefined -> ok;
|
||||
{CompilerPid, _} -> CompilerPid ! {diagnostic, Diagnostic}
|
||||
{Tail, true} ->
|
||||
put(elixir_code_diagnostics, {[print_diagnostic(Diagnostic, ReadSnippet) | Tail], true});
|
||||
|
||||
{Tail, false} ->
|
||||
put(elixir_code_diagnostics, {[Diagnostic | Tail], false})
|
||||
end,
|
||||
|
||||
ok.
|
||||
|
||||
@@ -118,6 +118,7 @@ invalid_module_name(Module) ->
|
||||
compile(Line, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
File = ?key(E, file),
|
||||
check_module_availability(Module, Line, E),
|
||||
elixir_env:trace(defmodule, E),
|
||||
|
||||
CompilerModules = compiler_modules(),
|
||||
{Tables, Ref} = build(Module, Line, File, E),
|
||||
|
||||
@@ -292,13 +292,13 @@ bracket_arg -> open_bracket container_expr close_bracket : build_access_arg('$1'
|
||||
bracket_arg -> open_bracket container_expr ',' close_bracket : build_access_arg('$1', '$2', '$4').
|
||||
bracket_arg -> open_bracket container_expr ',' container_args close_bracket : error_too_many_access_syntax('$3').
|
||||
|
||||
bracket_expr -> dot_bracket_identifier bracket_arg : build_access(build_no_parens('$1', nil), '$2').
|
||||
bracket_expr -> dot_bracket_identifier bracket_arg : build_access(build_no_parens('$1', nil), meta_with_from_brackets('$2')).
|
||||
bracket_expr -> access_expr bracket_arg : build_access('$1', meta_with_from_brackets('$2')).
|
||||
|
||||
bracket_at_expr -> at_op_eol dot_bracket_identifier bracket_arg :
|
||||
build_access(build_unary_op('$1', build_no_parens('$2', nil)), '$3').
|
||||
build_access(build_unary_op('$1', build_no_parens('$2', nil)), meta_with_from_brackets('$3')).
|
||||
bracket_at_expr -> at_op_eol access_expr bracket_arg :
|
||||
build_access(build_unary_op('$1', '$2'), '$3').
|
||||
build_access(build_unary_op('$1', '$2'), meta_with_from_brackets('$3')).
|
||||
|
||||
%% Blocks
|
||||
|
||||
|
||||
@@ -296,6 +296,28 @@ defmodule Code.Formatter.GeneralTest do
|
||||
assert_same code, @short_length
|
||||
end
|
||||
|
||||
test "keeps parens if argument includes keyword list" do
|
||||
assert_same """
|
||||
fn [] when is_integer(x) ->
|
||||
x + 42
|
||||
end
|
||||
"""
|
||||
|
||||
bad = """
|
||||
fn (input: x) when is_integer(x) ->
|
||||
x + 42
|
||||
end
|
||||
"""
|
||||
|
||||
good = """
|
||||
fn [input: x] when is_integer(x) ->
|
||||
x + 42
|
||||
end
|
||||
"""
|
||||
|
||||
assert_format bad, good
|
||||
end
|
||||
|
||||
test "with a single clause, followed by a newline, and can fit in one line" do
|
||||
assert_same """
|
||||
fn
|
||||
|
||||
@@ -210,10 +210,15 @@ defmodule Code.Formatter.LiteralsTest do
|
||||
test "with escapes" do
|
||||
assert_format ~S['f\a\b\ro'], ~S[~c"f\a\b\ro"]
|
||||
assert_format ~S['single \' quote'], ~S[~c"single ' quote"]
|
||||
assert_format ~S['double " quote'], ~S[~c"double \" quote"]
|
||||
assert_format ~S['double " quote'], ~S[~c'double " quote']
|
||||
assert_format ~S['escaped \" quote'], ~S[~c'escaped \" quote']
|
||||
assert_format ~S['\\"'], ~S[~c'\\"']
|
||||
|
||||
assert_same ~S['f\a\b\ro'], @keep_charlists
|
||||
assert_same ~S['single \' quote'], @keep_charlists
|
||||
assert_same ~S['double " quote'], @keep_charlists
|
||||
assert_same ~S['escaped \" quote'], @keep_charlists
|
||||
assert_same ~S['\\"'], @keep_charlists
|
||||
end
|
||||
|
||||
test "keeps literal new lines" do
|
||||
@@ -235,13 +240,15 @@ defmodule Code.Formatter.LiteralsTest do
|
||||
|
||||
test "with interpolation" do
|
||||
assert_format ~S['one #{2} three'], ~S[~c"one #{2} three"]
|
||||
assert_format ~S['#{1}\n \\ " \"'], ~S[~c'#{1}\n \\ " \"']
|
||||
|
||||
assert_same ~S['one #{2} three'], @keep_charlists
|
||||
assert_same ~S['#{1}\n \\ " \"'], @keep_charlists
|
||||
end
|
||||
|
||||
test "with escape and interpolation" do
|
||||
assert_format ~S['one\n\'#{2}\'\nthree'], ~S[~c"one\n'#{2}'\nthree"]
|
||||
assert_format ~S['one\n"#{2}"\nthree'], ~S[~c"one\n\"#{2}\"\nthree"]
|
||||
assert_format ~S['one\n"#{2}"\nthree'], ~S[~c'one\n"#{2}"\nthree']
|
||||
|
||||
assert_same ~S['one\n\'#{2}\'\nthree'], @keep_charlists
|
||||
end
|
||||
|
||||
@@ -539,6 +539,10 @@ defmodule Code.Normalizer.QuotedASTTest do
|
||||
"\e[34m[\e[0m\e[32ma:\e[0m \e[33m1\e[0m, \e[32mb:\e[0m \e[33m2\e[0m\e[34m]\e[0m"
|
||||
end
|
||||
|
||||
test "keyword list with :do as operand" do
|
||||
assert quoted_to_string(quote(do: a = [do: 1])) == "a = [do: 1]"
|
||||
end
|
||||
|
||||
test "interpolation" do
|
||||
assert quoted_to_string(quote(do: "foo#{bar}baz")) == ~S["foo#{bar}baz"]
|
||||
end
|
||||
@@ -704,10 +708,10 @@ defmodule Code.Normalizer.QuotedASTTest do
|
||||
~S[~c"one\n'#{2}'\nthree"]
|
||||
|
||||
assert Code.string_to_quoted!(~S/'one\n"#{2}"\nthree'/) |> quoted_to_string(escape: false) ==
|
||||
~s[~c"one\n\\"\#{2}\\"\nthree"]
|
||||
~s[~c'one\n"\#{2}"\nthree']
|
||||
|
||||
assert Code.string_to_quoted!(~S/'one\n"#{2}"\nthree'/) |> quoted_to_string() ==
|
||||
~S[~c"one\n\"#{2}\"\nthree"]
|
||||
~S[~c'one\n"#{2}"\nthree']
|
||||
end
|
||||
|
||||
test "atoms" do
|
||||
|
||||
@@ -56,9 +56,9 @@ defmodule EnumTest do
|
||||
end
|
||||
|
||||
test "mix and match" do
|
||||
enums = [[1, 2], %{a: 3, b: 4}, [5, 6]]
|
||||
enums = [[1, 2], 3..4, [5, 6]]
|
||||
result = Enum.zip_reduce(enums, [], fn elements, acc -> [List.to_tuple(elements) | acc] end)
|
||||
assert result == [{2, {:b, 4}, 6}, {1, {:a, 3}, 5}]
|
||||
assert result == [{2, 4, 6}, {1, 3, 5}]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -412,7 +412,7 @@ defmodule EnumTest do
|
||||
assert Enum.into([a: 1, b: 2], %{c: 3}) == %{a: 1, b: 2, c: 3}
|
||||
assert Enum.into(MapSet.new(a: 1, b: 2), %{}) == %{a: 1, b: 2}
|
||||
assert Enum.into(MapSet.new(a: 1, b: 2), %{c: 3}) == %{a: 1, b: 2, c: 3}
|
||||
assert Enum.into(%{a: 1, b: 2}, []) == [a: 1, b: 2]
|
||||
assert Enum.into(%{a: 1, b: 2}, []) |> Enum.sort() == [a: 1, b: 2]
|
||||
assert Enum.into(1..3, []) == [1, 2, 3]
|
||||
assert Enum.into(["H", "i"], "") == "Hi"
|
||||
end
|
||||
@@ -1430,7 +1430,7 @@ defmodule EnumTest do
|
||||
test "unzip/1" do
|
||||
assert Enum.unzip([{:a, 1}, {:b, 2}, {:c, 3}]) == {[:a, :b, :c], [1, 2, 3]}
|
||||
assert Enum.unzip([]) == {[], []}
|
||||
assert Enum.unzip(%{a: 1, b: 2}) == {[:a, :b], [1, 2]}
|
||||
assert Enum.unzip(%{a: 1}) == {[:a], [1]}
|
||||
assert Enum.unzip(foo: "a", bar: "b") == {[:foo, :bar], ["a", "b"]}
|
||||
|
||||
assert_raise FunctionClauseError, fn -> Enum.unzip([{:a, 1}, {:b, 2, "foo"}]) end
|
||||
|
||||
@@ -5,6 +5,17 @@ defmodule FloatTest do
|
||||
|
||||
doctest Float
|
||||
|
||||
# TODO remove and replace by assert once we require Erlang/OTP 27+
|
||||
# We can't easily distinguish between -0.0 and +0.0 on previous version
|
||||
defmacrop float_assert({:===, _, [left, right]}) do
|
||||
quote do
|
||||
# note: these are pure functions so no need to use bind_quoted
|
||||
# we favor a useful error message instead
|
||||
assert unquote(left) === unquote(right)
|
||||
assert to_string(unquote(left)) === to_string(unquote(right))
|
||||
end
|
||||
end
|
||||
|
||||
test "parse/1" do
|
||||
assert Float.parse("12") === {12.0, ""}
|
||||
assert Float.parse("-12") === {-12.0, ""}
|
||||
@@ -45,39 +56,39 @@ defmodule FloatTest do
|
||||
end
|
||||
|
||||
test "floor/1" do
|
||||
assert Float.floor(12.524235) === 12.0
|
||||
assert Float.floor(-12.5) === -13.0
|
||||
assert Float.floor(-12.524235) === -13.0
|
||||
assert Float.floor(7.5e3) === 7500.0
|
||||
assert Float.floor(7.5432e3) === 7543.0
|
||||
assert Float.floor(7.5e-3) === 0.0
|
||||
assert Float.floor(-12.32453e4) === -123_246.0
|
||||
assert Float.floor(-12.32453e-10) === -1.0
|
||||
assert Float.floor(0.32453e-10) === 0.0
|
||||
assert Float.floor(-0.32453e-10) === -1.0
|
||||
assert Float.floor(1.32453e-10) === 0.0
|
||||
float_assert Float.floor(12.524235) === 12.0
|
||||
float_assert Float.floor(-12.5) === -13.0
|
||||
float_assert Float.floor(-12.524235) === -13.0
|
||||
float_assert Float.floor(7.5e3) === 7500.0
|
||||
float_assert Float.floor(7.5432e3) === 7543.0
|
||||
float_assert Float.floor(7.5e-3) === 0.0
|
||||
float_assert Float.floor(-12.32453e4) === -123_246.0
|
||||
float_assert Float.floor(-12.32453e-10) === -1.0
|
||||
float_assert Float.floor(0.32453e-10) === 0.0
|
||||
float_assert Float.floor(-0.32453e-10) === -1.0
|
||||
float_assert Float.floor(1.32453e-10) === 0.0
|
||||
end
|
||||
|
||||
describe "floor/2" do
|
||||
test "with 0.0" do
|
||||
for precision <- 0..15 do
|
||||
assert Float.floor(0.0, precision) === 0.0
|
||||
assert Float.floor(-0.0, precision) === -0.0
|
||||
float_assert Float.floor(0.0, precision) === 0.0
|
||||
float_assert Float.floor(-0.0, precision) === -0.0
|
||||
end
|
||||
end
|
||||
|
||||
test "floor/2 with precision" do
|
||||
assert Float.floor(12.524235, 0) === 12.0
|
||||
assert Float.floor(-12.524235, 0) === -13.0
|
||||
float_assert Float.floor(12.524235, 0) === 12.0
|
||||
float_assert Float.floor(-12.524235, 0) === -13.0
|
||||
|
||||
assert Float.floor(12.52, 2) === 12.51
|
||||
assert Float.floor(-12.52, 2) === -12.52
|
||||
float_assert Float.floor(12.52, 2) === 12.51
|
||||
float_assert Float.floor(-12.52, 2) === -12.52
|
||||
|
||||
assert Float.floor(12.524235, 2) === 12.52
|
||||
assert Float.floor(-12.524235, 3) === -12.525
|
||||
float_assert Float.floor(12.524235, 2) === 12.52
|
||||
float_assert Float.floor(-12.524235, 3) === -12.525
|
||||
|
||||
assert Float.floor(12.32453e-20, 2) === 0.0
|
||||
assert Float.floor(-12.32453e-20, 2) === -0.01
|
||||
float_assert Float.floor(12.32453e-20, 2) === 0.0
|
||||
float_assert Float.floor(-12.32453e-20, 2) === -0.01
|
||||
|
||||
assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn ->
|
||||
Float.floor(1.1, 16)
|
||||
@@ -85,68 +96,73 @@ defmodule FloatTest do
|
||||
end
|
||||
|
||||
test "with subnormal floats" do
|
||||
assert Float.floor(-5.0e-324, 0) === -1.0
|
||||
assert Float.floor(-5.0e-324, 1) === -0.1
|
||||
assert Float.floor(-5.0e-324, 2) === -0.01
|
||||
assert Float.floor(-5.0e-324, 15) === -0.000000000000001
|
||||
float_assert Float.floor(-5.0e-324, 0) === -1.0
|
||||
float_assert Float.floor(-5.0e-324, 1) === -0.1
|
||||
float_assert Float.floor(-5.0e-324, 2) === -0.01
|
||||
float_assert Float.floor(-5.0e-324, 15) === -0.000000000000001
|
||||
|
||||
for precision <- 0..15 do
|
||||
assert Float.floor(5.0e-324, precision) === 0.0
|
||||
float_assert Float.floor(5.0e-324, precision) === 0.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "ceil/1" do
|
||||
assert Float.ceil(12.524235) === 13.0
|
||||
assert Float.ceil(-12.5) === -12.0
|
||||
assert Float.ceil(-12.524235) === -12.0
|
||||
assert Float.ceil(7.5e3) === 7500.0
|
||||
assert Float.ceil(7.5432e3) === 7544.0
|
||||
assert Float.ceil(7.5e-3) === 1.0
|
||||
assert Float.ceil(-12.32453e4) === -123_245.0
|
||||
assert Float.ceil(-12.32453e-10) === 0.0
|
||||
assert Float.ceil(0.32453e-10) === 1.0
|
||||
assert Float.ceil(-0.32453e-10) === 0.0
|
||||
assert Float.ceil(1.32453e-10) === 1.0
|
||||
assert Float.ceil(0.0) === 0.0
|
||||
float_assert Float.ceil(12.524235) === 13.0
|
||||
float_assert Float.ceil(-12.5) === -12.0
|
||||
float_assert Float.ceil(-12.524235) === -12.0
|
||||
float_assert Float.ceil(7.5e3) === 7500.0
|
||||
float_assert Float.ceil(7.5432e3) === 7544.0
|
||||
float_assert Float.ceil(7.5e-3) === 1.0
|
||||
float_assert Float.ceil(-12.32453e4) === -123_245.0
|
||||
float_assert Float.ceil(-12.32453e-10) === -0.0
|
||||
float_assert Float.ceil(0.32453e-10) === 1.0
|
||||
float_assert Float.ceil(-0.32453e-10) === -0.0
|
||||
float_assert Float.ceil(1.32453e-10) === 1.0
|
||||
float_assert Float.ceil(0.0) === 0.0
|
||||
end
|
||||
|
||||
describe "ceil/2" do
|
||||
test "with 0.0" do
|
||||
for precision <- 0..15 do
|
||||
assert Float.ceil(0.0, precision) === 0.0
|
||||
assert Float.ceil(-0.0, precision) === -0.0
|
||||
float_assert Float.ceil(0.0, precision) === 0.0
|
||||
float_assert Float.ceil(-0.0, precision) === -0.0
|
||||
end
|
||||
end
|
||||
|
||||
test "with regular floats" do
|
||||
assert Float.ceil(12.524235, 0) === 13.0
|
||||
assert Float.ceil(-12.524235, 0) === -12.0
|
||||
float_assert Float.ceil(12.524235, 0) === 13.0
|
||||
float_assert Float.ceil(-12.524235, 0) === -12.0
|
||||
|
||||
assert Float.ceil(12.52, 2) === 12.52
|
||||
assert Float.ceil(-12.52, 2) === -12.51
|
||||
float_assert Float.ceil(12.52, 2) === 12.52
|
||||
float_assert Float.ceil(-12.52, 2) === -12.51
|
||||
|
||||
assert Float.ceil(12.524235, 2) === 12.53
|
||||
assert Float.ceil(-12.524235, 3) === -12.524
|
||||
float_assert Float.ceil(12.524235, 2) === 12.53
|
||||
float_assert Float.ceil(-12.524235, 3) === -12.524
|
||||
|
||||
assert Float.ceil(12.32453e-20, 2) === 0.01
|
||||
assert Float.ceil(-12.32453e-20, 2) === 0.0
|
||||
float_assert Float.ceil(12.32453e-20, 2) === 0.01
|
||||
float_assert Float.ceil(-12.32453e-20, 2) === -0.0
|
||||
|
||||
assert Float.ceil(0.0, 2) === 0.0
|
||||
float_assert Float.ceil(0.0, 2) === 0.0
|
||||
|
||||
assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn ->
|
||||
Float.ceil(1.1, 16)
|
||||
end
|
||||
end
|
||||
|
||||
test "with small floats rounded up to -0.0" do
|
||||
float_assert Float.ceil(-0.1, 0) === -0.0
|
||||
float_assert Float.ceil(-0.01, 1) === -0.0
|
||||
end
|
||||
|
||||
test "with subnormal floats" do
|
||||
assert Float.ceil(5.0e-324, 0) === 1.0
|
||||
assert Float.ceil(5.0e-324, 1) === 0.1
|
||||
assert Float.ceil(5.0e-324, 2) === 0.01
|
||||
assert Float.ceil(5.0e-324, 15) === 0.000000000000001
|
||||
float_assert Float.ceil(5.0e-324, 0) === 1.0
|
||||
float_assert Float.ceil(5.0e-324, 1) === 0.1
|
||||
float_assert Float.ceil(5.0e-324, 2) === 0.01
|
||||
float_assert Float.ceil(5.0e-324, 15) === 0.000000000000001
|
||||
|
||||
for precision <- 0..15 do
|
||||
assert Float.ceil(-5.0e-324, precision) === -0.0
|
||||
float_assert Float.ceil(-5.0e-324, precision) === -0.0
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -154,28 +170,39 @@ defmodule FloatTest do
|
||||
describe "round/2" do
|
||||
test "with 0.0" do
|
||||
for precision <- 0..15 do
|
||||
assert Float.round(0.0, precision) === 0.0
|
||||
assert Float.round(-0.0, precision) === -0.0
|
||||
float_assert Float.round(0.0, precision) === 0.0
|
||||
float_assert Float.round(-0.0, precision) === -0.0
|
||||
end
|
||||
end
|
||||
|
||||
test "with regular floats" do
|
||||
assert Float.round(5.5675, 3) === 5.567
|
||||
assert Float.round(-5.5674, 3) === -5.567
|
||||
assert Float.round(5.5, 3) === 5.5
|
||||
assert Float.round(5.5e-10, 10) === 5.0e-10
|
||||
assert Float.round(5.5e-10, 8) === 0.0
|
||||
assert Float.round(5.0, 0) === 5.0
|
||||
float_assert Float.round(5.5675, 3) === 5.567
|
||||
float_assert Float.round(-5.5674, 3) === -5.567
|
||||
float_assert Float.round(5.5, 3) === 5.5
|
||||
float_assert Float.round(5.5e-10, 10) === 5.0e-10
|
||||
float_assert Float.round(5.5e-10, 8) === 0.0
|
||||
float_assert Float.round(5.0, 0) === 5.0
|
||||
|
||||
assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn ->
|
||||
Float.round(1.1, 16)
|
||||
end
|
||||
end
|
||||
|
||||
test "with small floats rounded to +0.0 / -0.0" do
|
||||
float_assert Float.round(0.01, 0) === 0.0
|
||||
float_assert Float.round(0.01, 1) === 0.0
|
||||
|
||||
float_assert Float.round(-0.01, 0) === -0.0
|
||||
float_assert Float.round(-0.01, 1) === -0.0
|
||||
|
||||
float_assert Float.round(-0.49999, 0) === -0.0
|
||||
float_assert Float.round(-0.049999, 1) === -0.0
|
||||
end
|
||||
|
||||
test "with subnormal floats" do
|
||||
for precision <- 0..15 do
|
||||
assert Float.round(5.0e-324, precision) === 0.0
|
||||
assert Float.round(-5.0e-324, precision) === -0.0
|
||||
float_assert Float.round(5.0e-324, precision) === 0.0
|
||||
float_assert Float.round(-5.0e-324, precision) === -0.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,8 +187,11 @@ defmodule IO.ANSI.DocsTest do
|
||||
assert result == "line\n\e[0m\n\e[36m code\n code2\e[0m\n\e[0m\nline2\n\e[0m"
|
||||
result = format_markdown("line\n```elixir\ncode\ncode2\n```\nline2\n")
|
||||
assert result == "line\n\e[0m\n\e[36m code\n code2\e[0m\n\e[0m\nline2\n\e[0m"
|
||||
result = format_markdown("line\n~~~elixir\ncode\n```\n~~~\nline2\n")
|
||||
assert result == "line\n\e[0m\n\e[36m code\n ```\e[0m\n\e[0m\nline2\n\e[0m"
|
||||
end
|
||||
|
||||
test "mermaid fenced code block is discarded" do
|
||||
result = format_markdown("line\n```mermaid\ncode\ncode2\n```\nline2\n")
|
||||
assert result == "line\n\e[0m\nline2\n\e[0m"
|
||||
end
|
||||
|
||||
test "* list is converted" do
|
||||
|
||||
@@ -119,7 +119,7 @@ defmodule Kernel.BinaryTest do
|
||||
Code.eval_string(~s["foo" <> 1])
|
||||
end
|
||||
|
||||
message = ~r"left argument of <> operator inside a match"
|
||||
message = ~r"cannot perform prefix match because the left operand of <> has unknown size."
|
||||
|
||||
assert_raise ArgumentError, message, fn ->
|
||||
Code.eval_string(~s[a <> "b" = "ab"])
|
||||
@@ -255,6 +255,12 @@ defmodule Kernel.BinaryTest do
|
||||
assert <<1::size((^foo).bar)>> = <<1::5>>
|
||||
end
|
||||
|
||||
test "bitsyntax size with pinned integer" do
|
||||
a = 1
|
||||
b = <<2, 3>>
|
||||
assert <<^a, ^b::binary>> = <<1, 2, 3>>
|
||||
end
|
||||
|
||||
test "automatic size computation of matched bitsyntax variable" do
|
||||
var = "foo"
|
||||
<<^var::binary, rest::binary>> = "foobar"
|
||||
|
||||
@@ -196,6 +196,28 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
purge([QuickExample])
|
||||
end
|
||||
|
||||
test "does not crash on external reports" do
|
||||
[fixture] =
|
||||
write_tmp(
|
||||
"compile_quoted",
|
||||
quick_example: """
|
||||
defmodule CompileQuoted do
|
||||
try do
|
||||
Code.compile_quoted({:fn, [], [{:->, [], [[], quote(do: unknown_var)]}]})
|
||||
rescue
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
"""
|
||||
)
|
||||
|
||||
assert capture_io(:stderr, fn ->
|
||||
assert {:ok, [CompileQuoted], []} = Kernel.ParallelCompiler.compile([fixture])
|
||||
end) =~ "undefined variable \"unknown_var\""
|
||||
after
|
||||
purge([CompileQuoted])
|
||||
end
|
||||
|
||||
test "does not hang on missing dependencies" do
|
||||
[fixture] =
|
||||
write_tmp(
|
||||
@@ -305,7 +327,7 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
msg =
|
||||
capture_io(:stderr, fn ->
|
||||
fixtures = [foo, bar]
|
||||
assert {:error, [foo_error, bar_error], []} = Kernel.ParallelCompiler.compile(fixtures)
|
||||
assert {:error, [bar_error, foo_error], []} = Kernel.ParallelCompiler.compile(fixtures)
|
||||
assert bar_error == {bar, nil, "deadlocked waiting on module FooDeadlock"}
|
||||
assert foo_error == {foo, nil, "deadlocked waiting on module BarDeadlock"}
|
||||
end)
|
||||
@@ -393,7 +415,7 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
|
||||
capture_io(:stderr, fn ->
|
||||
fixtures = [foo, bar]
|
||||
assert {:error, [foo_error, bar_error], []} = Kernel.ParallelCompiler.compile(fixtures)
|
||||
assert {:error, [bar_error, foo_error], []} = Kernel.ParallelCompiler.compile(fixtures)
|
||||
assert {^bar, nil, "deadlocked waiting on module FooAsyncDeadlock"} = bar_error
|
||||
assert {^foo, nil, "deadlocked waiting on pmap [#PID<" <> _} = foo_error
|
||||
end)
|
||||
|
||||
@@ -201,6 +201,19 @@ defmodule Kernel.TracersTest do
|
||||
end
|
||||
""")
|
||||
|
||||
assert_receive {:defmodule, %{module: Sample, function: nil}}
|
||||
assert_receive {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}}
|
||||
after
|
||||
:code.purge(Sample)
|
||||
:code.delete(Sample)
|
||||
end
|
||||
|
||||
test "traces dynamic modules" do
|
||||
compile_string("""
|
||||
Module.create(Sample, :ok, __ENV__)
|
||||
""")
|
||||
|
||||
assert_receive {:defmodule, %{module: Sample, function: nil}}
|
||||
assert_receive {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}}
|
||||
after
|
||||
:code.purge(Sample)
|
||||
@@ -217,6 +230,35 @@ defmodule Kernel.TracersTest do
|
||||
assert meta[:from_interpolation]
|
||||
end
|
||||
|
||||
test "traces bracket access" do
|
||||
compile_string("""
|
||||
foo = %{bar: 3}
|
||||
foo[:bar]
|
||||
""")
|
||||
|
||||
assert_receive {{:remote_function, meta, Access, :get, 2}, _env}
|
||||
assert meta[:from_brackets]
|
||||
|
||||
compile_string("""
|
||||
defmodule Foo do
|
||||
@foo %{bar: 3}
|
||||
def a() do
|
||||
@foo[:bar]
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
assert_receive {{:remote_function, meta, Access, :get, 2}, _env}
|
||||
assert meta[:from_brackets]
|
||||
|
||||
compile_string("""
|
||||
%{bar: 3}[:bar]
|
||||
""")
|
||||
|
||||
assert_receive {{:remote_function, meta, Access, :get, 2}, _env}
|
||||
assert meta[:from_brackets]
|
||||
end
|
||||
|
||||
"""
|
||||
# Make sure this module is compiled with column information
|
||||
defmodule MacroWithColumn do
|
||||
|
||||
@@ -316,6 +316,8 @@ defmodule PathTest do
|
||||
assert Path.relative_to("./foo/../bar/..", File.cwd!()) == "."
|
||||
|
||||
# both relative
|
||||
assert Path.relative_to("usr/local/foo", ".") == "usr/local/foo"
|
||||
assert Path.relative_to(".", "usr/local/foo") == "."
|
||||
assert Path.relative_to("usr/local/foo", "usr/local") == "foo"
|
||||
assert Path.relative_to("usr/local/foo", "etc") == "../usr/local/foo"
|
||||
assert Path.relative_to(~c"usr/local/foo", "etc") == "../usr/local/foo"
|
||||
|
||||
@@ -161,13 +161,15 @@ defmodule Protocol.ConsolidationTest do
|
||||
end
|
||||
|
||||
test "consolidation extracts protocols" do
|
||||
protos = Protocol.extract_protocols([:code.lib_dir(:elixir, :ebin)])
|
||||
protos = Protocol.extract_protocols([Application.app_dir(:elixir, "ebin")])
|
||||
assert Enumerable in protos
|
||||
assert Inspect in protos
|
||||
end
|
||||
|
||||
test "consolidation extracts implementations with charlist path" do
|
||||
protos = Protocol.extract_impls(Enumerable, [:code.lib_dir(:elixir, :ebin)])
|
||||
protos =
|
||||
Protocol.extract_impls(Enumerable, [to_charlist(Application.app_dir(:elixir, "ebin"))])
|
||||
|
||||
assert List in protos
|
||||
assert Function in protos
|
||||
end
|
||||
|
||||
@@ -254,6 +254,7 @@ defmodule StringTest do
|
||||
|
||||
test "capitalize/1" do
|
||||
assert String.capitalize("") == ""
|
||||
assert String.capitalize("1") == "1"
|
||||
assert String.capitalize("abc") == "Abc"
|
||||
assert String.capitalize("ABC") == "Abc"
|
||||
assert String.capitalize("c b a") == "C b a"
|
||||
|
||||
@@ -277,6 +277,32 @@ defmodule URITest do
|
||||
test "preserves an empty query" do
|
||||
assert URI.new!("http://foo.com/?").query == ""
|
||||
end
|
||||
|
||||
test "without scheme, undefined port after host translates to nil" do
|
||||
assert URI.new!("//https://www.example.com") ==
|
||||
%URI{
|
||||
scheme: nil,
|
||||
userinfo: nil,
|
||||
host: "https",
|
||||
port: nil,
|
||||
path: "//www.example.com",
|
||||
query: nil,
|
||||
fragment: nil
|
||||
}
|
||||
end
|
||||
|
||||
test "with scheme, undefined port after host translates to nil" do
|
||||
assert URI.new!("myscheme://myhost:/path/info") ==
|
||||
%URI{
|
||||
scheme: "myscheme",
|
||||
userinfo: nil,
|
||||
host: "myhost",
|
||||
port: nil,
|
||||
path: "/path/info",
|
||||
query: nil,
|
||||
fragment: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
test "http://http://http://@http://http://?http://#http://" do
|
||||
|
||||
@@ -42,16 +42,20 @@ defmodule ExUnit.CaptureLog do
|
||||
@doc """
|
||||
Captures Logger messages generated when evaluating `fun`.
|
||||
|
||||
Returns the binary which is the captured output.
|
||||
Returns the binary which is the captured output. The captured log
|
||||
messages will be formatted using `Logger.default_formatter/1`. Any
|
||||
option, besides the `:level`, will be forwarded as an override to
|
||||
the default formatter.
|
||||
|
||||
This function mutes the `:console` backend and captures any log
|
||||
This function mutes the default logger handler and captures any log
|
||||
messages sent to Logger from the calling processes. It is possible
|
||||
to ensure explicit log messages from other processes are captured
|
||||
by waiting for their exit or monitor signal.
|
||||
|
||||
Note that when the `async` is set to `true`, the messages from another
|
||||
test might be captured. This is OK as long you consider such cases in
|
||||
your assertions.
|
||||
Note that when the `async` is set to `true` on `use ExUnit.Case`,
|
||||
messages from other tests might be captured. This is OK as long
|
||||
you consider such cases in your assertions, typically by using
|
||||
the `=~/2` operator to perform partial matches.
|
||||
|
||||
It is possible to configure the level to capture with `:level`,
|
||||
which will set the capturing level for the duration of the
|
||||
@@ -63,10 +67,6 @@ defmodule ExUnit.CaptureLog do
|
||||
configured in this function, no message will be captured.
|
||||
The behaviour is undetermined if async tests change Logger level.
|
||||
|
||||
The format, metadata and colors can be configured with `:format`,
|
||||
`:metadata` and `:colors` respectively. These three options
|
||||
defaults to the `:console` backend configuration parameters.
|
||||
|
||||
To get the result of the evaluation along with the captured log,
|
||||
use `with_log/2`.
|
||||
"""
|
||||
|
||||
@@ -286,10 +286,10 @@ defmodule ExUnit.Diff do
|
||||
defp diff_pin({:^, _, [var]} = pin, right, %{pins: pins} = env) do
|
||||
identifier = var_context(var)
|
||||
%{^identifier => pin_value} = pins
|
||||
{diff, post_env} = diff_value(pin_value, right, env)
|
||||
{diff, post_env} = diff_value(pin_value, right, %{env | context: :===})
|
||||
|
||||
diff_left = update_diff_meta(pin, not diff.equivalent?)
|
||||
{%{diff | left: diff_left}, post_env}
|
||||
{%{diff | left: diff_left}, %{post_env | context: :match}}
|
||||
end
|
||||
|
||||
# Vars
|
||||
|
||||
@@ -96,9 +96,9 @@ defmodule ExUnit.DocTest do
|
||||
values are treated as comments in Elixir code due to the leading
|
||||
`#` sign, they require special care when being used in doctests.
|
||||
|
||||
Imagine you have a map that contains a MapSet and is printed as:
|
||||
Imagine you have a map that contains a `DateTime` and is printed as:
|
||||
|
||||
%{users: #MapSet<[:foo, :bar]>}
|
||||
%{datetime: #DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>}
|
||||
|
||||
If you try to match on such an expression, `doctest` will fail to compile.
|
||||
There are two ways to resolve this.
|
||||
@@ -106,20 +106,20 @@ defmodule ExUnit.DocTest do
|
||||
The first is to rely on the fact that doctest can compare internal
|
||||
structures as long as they are at the root. So one could write:
|
||||
|
||||
iex> map = %{users: Enum.into([:foo, :bar], MapSet.new())}
|
||||
iex> map.users
|
||||
#MapSet<[:foo, :bar]>
|
||||
iex> map = %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")}
|
||||
iex> map.datetime
|
||||
#DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>
|
||||
|
||||
Whenever a doctest starts with "#Name<", `doctest` will perform a string
|
||||
comparison. For example, the above test will perform the following match:
|
||||
|
||||
inspect(map.users) == "#MapSet<[:foo, :bar]>"
|
||||
inspect(map.datetime) == "#DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>"
|
||||
|
||||
Alternatively, since doctest results are actually evaluated, you can have
|
||||
the MapSet building expression as the doctest result:
|
||||
the `DateTime` building expression as the doctest result:
|
||||
|
||||
iex> %{users: Enum.into([:foo, :bar], MapSet.new())}
|
||||
%{users: Enum.into([:foo, :bar], MapSet.new())}
|
||||
iex> %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")}
|
||||
%{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")}
|
||||
|
||||
The downside of this approach is that the doctest result is not really
|
||||
what users would see in the terminal.
|
||||
|
||||
@@ -45,13 +45,13 @@ defmodule ExUnit.Filters do
|
||||
end
|
||||
|
||||
defp extract_line_numbers(file_path) do
|
||||
case String.split(file_path, ":") do
|
||||
case Path.relative_to_cwd(file_path) |> String.split(":") do
|
||||
[path] ->
|
||||
{path, []}
|
||||
|
||||
[path | parts] ->
|
||||
{path_parts, line_numbers} = Enum.split_while(parts, &(to_line_number(&1) == nil))
|
||||
path = Enum.join([path | path_parts], ":")
|
||||
path = Enum.join([path | path_parts], ":") |> Path.split() |> Path.join()
|
||||
lines = for n <- line_numbers, valid_number = validate_line_number(n), do: valid_number
|
||||
|
||||
case lines do
|
||||
|
||||
@@ -120,8 +120,6 @@ defmodule ExUnit.Formatter do
|
||||
|
||||
* `:error_info` - Should format error information.
|
||||
|
||||
* `:error_info` - Should format error information.
|
||||
|
||||
* `:test_module_info` - Should format test module information. The message returned
|
||||
when this key is passed precedes messages such as `"failure on setup_all callback [...]"`.
|
||||
|
||||
|
||||
@@ -216,7 +216,14 @@ defmodule ExUnit.CaptureIOTest do
|
||||
end)
|
||||
|
||||
capture_io("\"a", fn ->
|
||||
assert :io.scan_erl_form(~c">") == {:error, {1, :erl_scan, {:string, 34, ~c"a"}}, 1}
|
||||
error =
|
||||
if System.otp_release() >= "27" do
|
||||
{1, :erl_scan, {:unterminated, :string, ~c"a"}}
|
||||
else
|
||||
{1, :erl_scan, {:string, 34, ~c"a"}}
|
||||
end
|
||||
|
||||
assert :io.scan_erl_form(~c">") == {:error, error, 1}
|
||||
assert :io.scan_erl_form(~c">") == {:eof, 1}
|
||||
end)
|
||||
|
||||
|
||||
@@ -251,7 +251,12 @@ defmodule ExUnit.DiffTest do
|
||||
refute_diff([:a, [:c, :b]] = [:a, [:b, :c]], "[:a, [-:c-, :b]]", "[:a, [:b, +:c+]]")
|
||||
refute_diff(:a = [:a, [:b, :c]], "-:a-", "+[:a, [:b, :c]]+")
|
||||
|
||||
pins = %{{:a, nil} => :a, {:b, nil} => :b, {:list_ab, nil} => [:a, :b]}
|
||||
pins = %{
|
||||
{:a, nil} => :a,
|
||||
{:b, nil} => :b,
|
||||
{:list_ab, nil} => [:a, :b],
|
||||
{:list_tuple, nil} => [{:foo}]
|
||||
}
|
||||
|
||||
assert_diff(x = [], [x: []], pins)
|
||||
assert_diff(x = [:a, :b], [x: [:a, :b]], pins)
|
||||
@@ -282,6 +287,9 @@ defmodule ExUnit.DiffTest do
|
||||
|
||||
refute_diff([:a, :b] = :a, "-[:a, :b]-", "+:a+")
|
||||
refute_diff([:foo] = [:foo, {:a, :b, :c}], "[:foo]", "[:foo, +{:a, :b, :c}+]")
|
||||
|
||||
refute_diff([{:foo}] = [{:bar}], "[{-:foo-}]", "[{+:bar+}]")
|
||||
refute_diff(^list_tuple = [{:bar}], "-^list_tuple-", "[{+:bar+}]", pins)
|
||||
end
|
||||
|
||||
test "improper lists" do
|
||||
|
||||
@@ -196,30 +196,36 @@ defmodule ExUnit.FiltersTest do
|
||||
test "file paths with line numbers" do
|
||||
unix_path = "test/some/path.exs"
|
||||
windows_path = "C:\\some\\path.exs"
|
||||
unix_path_with_dot = "./test/some/path.exs"
|
||||
|
||||
for path <- [unix_path, windows_path, unix_path_with_dot] do
|
||||
fixed_path = path |> Path.split() |> Path.join() |> Path.relative_to_cwd()
|
||||
|
||||
for path <- [unix_path, windows_path] do
|
||||
assert ExUnit.Filters.parse_path("#{path}:123") ==
|
||||
{path, [exclude: [:test], include: [location: {path, 123}]]}
|
||||
{fixed_path, [exclude: [:test], include: [location: {fixed_path, 123}]]}
|
||||
|
||||
assert ExUnit.Filters.parse_path(path) == {path, []}
|
||||
assert ExUnit.Filters.parse_path(path) == {fixed_path, []}
|
||||
|
||||
assert ExUnit.Filters.parse_path("#{path}:123notreallyalinenumber123") ==
|
||||
{"#{path}:123notreallyalinenumber123", []}
|
||||
{"#{fixed_path}:123notreallyalinenumber123", []}
|
||||
|
||||
assert ExUnit.Filters.parse_path("#{path}:123:456") ==
|
||||
{path, [exclude: [:test], include: [location: {path, [123, 456]}]]}
|
||||
{fixed_path, [exclude: [:test], include: [location: {fixed_path, [123, 456]}]]}
|
||||
|
||||
assert ExUnit.Filters.parse_path("#{path}:123notalinenumber123:456") ==
|
||||
{"#{path}:123notalinenumber123",
|
||||
[exclude: [:test], include: [location: {"#{path}:123notalinenumber123", 456}]]}
|
||||
{"#{fixed_path}:123notalinenumber123",
|
||||
[
|
||||
exclude: [:test],
|
||||
include: [location: {"#{fixed_path}:123notalinenumber123", 456}]
|
||||
]}
|
||||
|
||||
output =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert ExUnit.Filters.parse_path("#{path}:123:456notalinenumber456") ==
|
||||
{path, [{:exclude, [:test]}, {:include, [location: {path, 123}]}]}
|
||||
{fixed_path, [{:exclude, [:test]}, {:include, [location: {fixed_path, 123}]}]}
|
||||
|
||||
assert ExUnit.Filters.parse_path("#{path}:123:0:-789:456") ==
|
||||
{path, [exclude: [:test], include: [location: {path, [123, 456]}]]}
|
||||
{fixed_path, [exclude: [:test], include: [location: {fixed_path, [123, 456]}]]}
|
||||
end)
|
||||
|
||||
assert output =~ "invalid line number given as ExUnit filter: 456notalinenumber456"
|
||||
@@ -231,25 +237,25 @@ defmodule ExUnit.FiltersTest do
|
||||
test "multiple file paths with line numbers" do
|
||||
unix_path = "test/some/path.exs"
|
||||
windows_path = "C:\\some\\path.exs"
|
||||
other_unix_path = "test/some/other_path.exs"
|
||||
other_unix_path = "test//some//other_path.exs"
|
||||
other_windows_path = "C:\\some\\other_path.exs"
|
||||
|
||||
for {path, other_path} <- [
|
||||
{unix_path, other_unix_path},
|
||||
{windows_path, other_windows_path}
|
||||
] do
|
||||
for {path, other_path} <- [{unix_path, other_unix_path}, {windows_path, other_windows_path}] do
|
||||
fixed_path = path |> Path.split() |> Path.join()
|
||||
fixed_other_path = other_path |> Path.split() |> Path.join()
|
||||
|
||||
assert ExUnit.Filters.parse_paths([path, "#{other_path}:456:789"]) ==
|
||||
{[path, other_path],
|
||||
{[fixed_path, fixed_other_path],
|
||||
[
|
||||
exclude: [:test],
|
||||
include: [location: {other_path, [456, 789]}]
|
||||
include: [location: {fixed_other_path, [456, 789]}]
|
||||
]}
|
||||
|
||||
assert ExUnit.Filters.parse_paths(["#{path}:123", "#{other_path}:456"]) ==
|
||||
{[path, other_path],
|
||||
{[fixed_path, fixed_other_path],
|
||||
[
|
||||
exclude: [:test],
|
||||
include: [location: {path, 123}, location: {other_path, 456}]
|
||||
include: [location: {fixed_path, 123}, location: {fixed_other_path, 456}]
|
||||
]}
|
||||
|
||||
output =
|
||||
@@ -258,12 +264,12 @@ defmodule ExUnit.FiltersTest do
|
||||
"#{path}:123:0:-789:456",
|
||||
"#{other_path}:321:0:-987:654"
|
||||
]) ==
|
||||
{[path, other_path],
|
||||
{[fixed_path, fixed_other_path],
|
||||
[
|
||||
exclude: [:test],
|
||||
include: [
|
||||
location: {path, [123, 456]},
|
||||
location: {other_path, [321, 654]}
|
||||
location: {fixed_path, [123, 456]},
|
||||
location: {fixed_other_path, [321, 654]}
|
||||
]
|
||||
]}
|
||||
end)
|
||||
|
||||
@@ -197,9 +197,7 @@ defmodule IEx.Autocomplete do
|
||||
end
|
||||
|
||||
defp expand_signatures([_ | _] = signatures, _shell) do
|
||||
[head | tail] = Enum.sort(signatures, &(String.length(&1) <= String.length(&2)))
|
||||
if tail != [], do: IO.write("\n" <> (tail |> Enum.reverse() |> Enum.join("\n")))
|
||||
yes("", [head])
|
||||
yes("", Enum.sort_by(signatures, &String.length/1))
|
||||
end
|
||||
|
||||
defp expand_signatures([], shell), do: expand_local_or_var("", shell)
|
||||
|
||||
@@ -253,9 +253,10 @@ defmodule IEx.Evaluator do
|
||||
if path do
|
||||
[path]
|
||||
else
|
||||
Enum.map([".", System.get_env("IEX_HOME", "~")], fn dir ->
|
||||
dir |> Path.join(".iex.exs") |> Path.expand()
|
||||
end)
|
||||
# Do not assume there is a $HOME
|
||||
for dir <- [".", System.get_env("IEX_HOME") || System.user_home()],
|
||||
dir != nil,
|
||||
do: dir |> Path.join(".iex.exs") |> Path.expand()
|
||||
end
|
||||
|
||||
path = Enum.find(candidates, &File.regular?/1)
|
||||
|
||||
+49
-21
@@ -69,17 +69,22 @@ defmodule IEx.Helpers do
|
||||
import IEx, only: [dont_display_result: 0]
|
||||
|
||||
@doc """
|
||||
Recompiles the current Mix project.
|
||||
Recompiles the current Mix project or Mix install
|
||||
dependencies.
|
||||
|
||||
This helper only works when IEx is started with a Mix
|
||||
project, for example, `iex -S mix`. Note this function
|
||||
simply recompiles Elixir modules, without reloading
|
||||
configuration, recompiling dependencies, or restarting
|
||||
applications.
|
||||
This helper requires either `Mix.install/2` to have been
|
||||
called within the current IEx session or for IEx to be
|
||||
started alongside, for example, `iex -S mix`.
|
||||
|
||||
Therefore, any long running process may crash on recompilation,
|
||||
as changed modules will be temporarily removed and recompiled,
|
||||
without going through the proper code change callback.
|
||||
In the `Mix.install/1` case, it will recompile any outdated
|
||||
path dependency declared during install. Within a project,
|
||||
it will recompile any outdated module.
|
||||
|
||||
Note this function simply recompiles Elixir modules, without
|
||||
reloading configuration or restarting applications. This means
|
||||
any long running process may crash on recompilation, as changed
|
||||
modules will be temporarily removed and recompiled, without
|
||||
going through the proper code change callback.
|
||||
|
||||
If you want to reload a single module, consider using
|
||||
`r(ModuleName)` instead.
|
||||
@@ -93,23 +98,46 @@ defmodule IEx.Helpers do
|
||||
|
||||
"""
|
||||
def recompile(options \\ []) do
|
||||
if mix_started?() do
|
||||
config = Mix.Project.config()
|
||||
consolidation = Mix.Project.consolidation_path(config)
|
||||
reenable_tasks(config)
|
||||
cond do
|
||||
not mix_started?() ->
|
||||
IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix"))
|
||||
:error
|
||||
|
||||
force? = Keyword.get(options, :force, false)
|
||||
args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation]
|
||||
args = if force?, do: ["--force" | args], else: args
|
||||
Mix.installed?() ->
|
||||
Mix.in_install_project(fn ->
|
||||
# TODO: remove this once Mix requires Hex with the fix from
|
||||
# https://github.com/hexpm/hex/pull/1015
|
||||
# Context: Mix.install/1 starts :hex if necessary and stops
|
||||
# it afterwards. Calling compile here may require hex to be
|
||||
# started and that should happen automatically, but because
|
||||
# of a bug it is not (fixed in the linked PR).
|
||||
_ = Application.ensure_all_started(:hex)
|
||||
|
||||
{result, _} = Mix.Task.run("compile", args)
|
||||
result
|
||||
else
|
||||
IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix"))
|
||||
:error
|
||||
do_recompile(options)
|
||||
# Just as with Mix.install/2 we clear all task invocations,
|
||||
# so that we can recompile the dependencies again next time
|
||||
Mix.Task.clear()
|
||||
:ok
|
||||
end)
|
||||
|
||||
true ->
|
||||
do_recompile(options)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_recompile(options) do
|
||||
config = Mix.Project.config()
|
||||
consolidation = Mix.Project.consolidation_path(config)
|
||||
reenable_tasks(config)
|
||||
|
||||
force? = Keyword.get(options, :force, false)
|
||||
args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation]
|
||||
args = if force?, do: ["--force" | args], else: args
|
||||
|
||||
{result, _} = Mix.Task.run("compile", args)
|
||||
result
|
||||
end
|
||||
|
||||
defp mix_started? do
|
||||
List.keyfind(Application.started_applications(), :mix, 0) != nil
|
||||
end
|
||||
|
||||
@@ -492,11 +492,8 @@ defmodule IEx.AutocompleteTest do
|
||||
|
||||
eval("import Enum; import Protocol")
|
||||
|
||||
assert ExUnit.CaptureIO.capture_io(fn ->
|
||||
send(self(), expand(~c"reduce("))
|
||||
end) == "\nreduce(enumerable, acc, fun)"
|
||||
|
||||
assert_received {:yes, ~c"", [~c"reduce(enumerable, fun)"]}
|
||||
assert expand(~c"reduce(") ==
|
||||
{:yes, ~c"", [~c"reduce(enumerable, fun)", ~c"reduce(enumerable, acc, fun)"]}
|
||||
|
||||
assert expand(~c"take(") == {:yes, ~c"", [~c"take(enumerable, amount)"]}
|
||||
assert expand(~c"derive(") == {:yes, ~c"", [~c"derive(protocol, module, options \\\\ [])"]}
|
||||
|
||||
@@ -144,7 +144,7 @@ defmodule IEx.HelpersTest do
|
||||
describe "open" do
|
||||
@iex_helpers "iex/lib/iex/helpers.ex"
|
||||
@elixir_erl "elixir/src/elixir.erl"
|
||||
@lists_erl "#{:code.lib_dir(:stdlib, :src)}/lists.erl"
|
||||
@lists_erl Application.app_dir(:stdlib, "src/lists.erl")
|
||||
@httpc_erl "src/http_client/httpc.erl"
|
||||
@editor System.get_env("ELIXIR_EDITOR")
|
||||
|
||||
@@ -332,17 +332,26 @@ defmodule IEx.HelpersTest do
|
||||
assert help =~ "Welcome to Interactive Elixir"
|
||||
end
|
||||
|
||||
@tag :erlang_doc
|
||||
test "prints Erlang module documentation" do
|
||||
captured = capture_io(fn -> h(:timer) end)
|
||||
assert captured =~ "This module provides useful functions related to time."
|
||||
end
|
||||
|
||||
@tag :erlang_doc
|
||||
test "prints Erlang module function specs" do
|
||||
captured = capture_io(fn -> h(:timer.sleep() / 1) end)
|
||||
assert captured =~ ":timer.sleep/1"
|
||||
assert captured =~ "-spec sleep(Time) -> ok when Time :: timeout()."
|
||||
|
||||
# TODO Fix for OTP 27 once specs are available
|
||||
if System.otp_release() < "27" do
|
||||
assert captured =~ "-spec sleep(Time) -> ok when Time :: timeout()."
|
||||
else
|
||||
assert captured =~ "sleep(Time)"
|
||||
end
|
||||
end
|
||||
|
||||
@tag :erlang_doc
|
||||
test "handles non-existing Erlang module function" do
|
||||
captured = capture_io(fn -> h(:timer.baz() / 1) end)
|
||||
assert captured =~ "No documentation for :timer.baz was found"
|
||||
@@ -802,7 +811,7 @@ defmodule IEx.HelpersTest do
|
||||
|
||||
assert capture_io(fn -> b(NoMix.run()) end) == "Could not load module NoMix, got: nofile\n"
|
||||
|
||||
assert capture_io(fn -> b(Exception.message() / 1) end) ==
|
||||
assert capture_io(fn -> b(Exception.message() / 1) end) =~
|
||||
"@callback message(t()) :: String.t()\n\n"
|
||||
|
||||
assert capture_io(fn -> b(:gen_server.handle_cast() / 2) end) =~
|
||||
@@ -1008,23 +1017,47 @@ defmodule IEx.HelpersTest do
|
||||
cleanup_modules([TypeSample])
|
||||
end
|
||||
|
||||
test "prints all types in erlang module" do
|
||||
@tag :erlang_doc
|
||||
test "prints all types in Erlang module" do
|
||||
captured = capture_io(fn -> t(:queue) end)
|
||||
assert captured =~ "-type queue() :: queue(_)"
|
||||
assert captured =~ "-opaque queue(Item)"
|
||||
|
||||
# TODO Fix for OTP 27 once specs are available
|
||||
if System.otp_release() < "27" do
|
||||
assert captured =~ "-type queue() :: queue(_)"
|
||||
assert captured =~ "-opaque queue(Item)"
|
||||
else
|
||||
assert captured =~ "queue()"
|
||||
assert captured =~ "queue(Item)"
|
||||
end
|
||||
end
|
||||
|
||||
test "prints single type from erlang module" do
|
||||
@tag :erlang_doc
|
||||
test "prints single type from Erlang module" do
|
||||
captured = capture_io(fn -> t(:erlang.iovec()) end)
|
||||
assert captured =~ "-type iovec() :: [binary()]"
|
||||
|
||||
# TODO Fix for OTP 27 once specs are available
|
||||
if System.otp_release() < "27" do
|
||||
assert captured =~ "-type iovec() :: [binary()]"
|
||||
else
|
||||
assert captured =~ "iovec()"
|
||||
end
|
||||
|
||||
assert captured =~ "A list of binaries."
|
||||
|
||||
captured = capture_io(fn -> t(:erlang.iovec() / 0) end)
|
||||
assert captured =~ "-type iovec() :: [binary()]"
|
||||
|
||||
# TODO Fix for OTP 27 once specs are available
|
||||
if System.otp_release() < "27" do
|
||||
assert captured =~ "-type iovec() :: [binary()]"
|
||||
else
|
||||
assert captured =~ "iovec()"
|
||||
end
|
||||
|
||||
assert captured =~ "A list of binaries."
|
||||
end
|
||||
|
||||
test "handles non-existing types from erlang module" do
|
||||
@tag :erlang_doc
|
||||
test "handles non-existing types from Erlang module" do
|
||||
captured = capture_io(fn -> t(:erlang.foo()) end)
|
||||
assert captured =~ "No type information for :erlang.foo was found or :erlang.foo is private"
|
||||
|
||||
|
||||
@@ -7,11 +7,19 @@ IEx.configure(colors: [enabled: false])
|
||||
{line_exclude, line_include} =
|
||||
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
|
||||
|
||||
erlang_doc_exclude =
|
||||
if match?({:docs_v1, _, _, _, %{}, _, _}, Code.fetch_docs(:array)) do
|
||||
[]
|
||||
else
|
||||
IO.puts("Erlang/OTP compiled without docs, some tests are excluded...")
|
||||
[:erlang_doc]
|
||||
end
|
||||
|
||||
ExUnit.start(
|
||||
assert_receive_timeout: assert_timeout,
|
||||
trace: !!System.get_env("TRACE"),
|
||||
include: line_include,
|
||||
exclude: line_exclude
|
||||
exclude: line_exclude ++ erlang_doc_exclude
|
||||
)
|
||||
|
||||
defmodule IEx.Case do
|
||||
|
||||
@@ -867,9 +867,12 @@ defmodule Logger do
|
||||
|
||||
defp update_translators(updater) do
|
||||
:elixir_config.serial(fn ->
|
||||
translators = updater.(Application.fetch_env!(:logger, :translators))
|
||||
Application.put_env(:logger, :translators, translators)
|
||||
|
||||
with %{filters: filters} <- :logger.get_primary_config(),
|
||||
{{_, {fun, config}}, filters} <- List.keytake(filters, :logger_translator, 0) do
|
||||
config = update_in(config.translators, updater)
|
||||
config = %{config | translators: translators}
|
||||
:ok = :logger.set_primary_config(:filters, filters ++ [logger_translator: {fun, config}])
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -404,11 +404,9 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
defp report_crash(min_level, crashed, extra, linked) do
|
||||
[
|
||||
{:pid, pid},
|
||||
{:registered_name, name},
|
||||
{:error_info, {kind, reason, stack}} | crashed
|
||||
] = crashed
|
||||
{pid, crashed} = Keyword.pop_first(crashed, :pid)
|
||||
{name, crashed} = Keyword.pop_first(crashed, :registered_name)
|
||||
{{kind, reason, stack}, crashed} = Keyword.pop_first(crashed, :error_info)
|
||||
|
||||
dictionary = crashed[:dictionary]
|
||||
reason = Exception.normalize(kind, reason, stack)
|
||||
@@ -459,6 +457,10 @@ defmodule Logger.Translator do
|
||||
[prefix, "Ancestors: ", inspect(ancestors) | crash_info(min_level, debug, prefix)]
|
||||
end
|
||||
|
||||
defp crash_info(min_level, [{:process_label, _} | info], prefix) do
|
||||
crash_info(min_level, info, prefix)
|
||||
end
|
||||
|
||||
defp crash_info(:debug, debug, prefix) do
|
||||
for {key, value} <- debug do
|
||||
crash_debug(key, value, prefix)
|
||||
|
||||
@@ -58,7 +58,9 @@ defmodule Logger.Backends.HandlerTest do
|
||||
end
|
||||
|
||||
test "add_translator/1 and remove_translator/1 for logger formats" do
|
||||
refute {CustomTranslator, :t} in Application.fetch_env!(:logger, :translators)
|
||||
assert Logger.add_translator({CustomTranslator, :t})
|
||||
assert {CustomTranslator, :t} in Application.fetch_env!(:logger, :translators)
|
||||
|
||||
assert capture_log(fn ->
|
||||
:logger.info(~c"hello: ~p", [:ok])
|
||||
|
||||
+140
-30
@@ -352,8 +352,6 @@ defmodule Mix do
|
||||
* `MIX_INSTALL_DIR` - (since v1.12.0) specifies directory where `Mix.install/2` keeps
|
||||
install cache
|
||||
|
||||
* `MIX_INSTALL_FORCE` - (since v1.13.0) runs `Mix.install/2` with empty install cache
|
||||
|
||||
* `MIX_PATH` - appends extra code paths
|
||||
|
||||
* `MIX_PROFILE` - a list of comma-separated Mix tasks to profile the time spent on
|
||||
@@ -379,6 +377,8 @@ defmodule Mix do
|
||||
"""
|
||||
|
||||
@mix_install_project __MODULE__.InstallProject
|
||||
@mix_install_app :mix_install
|
||||
@mix_install_app_string Atom.to_string(@mix_install_app)
|
||||
|
||||
use Application
|
||||
|
||||
@@ -661,9 +661,6 @@ defmodule Mix do
|
||||
This function can only be called outside of a Mix project and only with the
|
||||
same dependencies in the given VM.
|
||||
|
||||
**Note:** this feature is currently experimental and it may change
|
||||
in future releases.
|
||||
|
||||
## Options
|
||||
|
||||
* `:force` - if `true`, runs with empty install cache. This is useful when you want
|
||||
@@ -779,6 +776,21 @@ defmodule Mix do
|
||||
The contents inside `defmodule` will only be expanded and executed
|
||||
after `Mix.install/2` runs, which means that any struct, macros,
|
||||
and imports will be correctly handled.
|
||||
|
||||
## Environment variables
|
||||
|
||||
The `MIX_INSTALL_DIR` environment variable configures the directory that
|
||||
caches all `Mix.install/2`.
|
||||
|
||||
The `MIX_INSTALL_FORCE` is available since Elixir v1.13.0 and forces
|
||||
`Mix.install/2` to discard any previously cached entry of the current install.
|
||||
|
||||
The `MIX_INSTALL_RESTORE_PROJECT_DIR` environment variable may be specified
|
||||
since Elixir v1.16.2. It should point to a previous installation directory,
|
||||
which can be obtained with `Mix.install_project_dir/0` (after calling `Mix.install/2`).
|
||||
Using a restore dir may speed up the installation, since matching dependencies
|
||||
do not need be refetched nor recompiled. This environment variable is ignored
|
||||
if `:force` is enabled.
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def install(deps, opts \\ [])
|
||||
@@ -848,50 +860,49 @@ defmodule Mix do
|
||||
Application.put_all_env(config, persistent: true)
|
||||
System.put_env(system_env)
|
||||
|
||||
install_dir = install_dir(id)
|
||||
install_project_dir = install_project_dir(id)
|
||||
|
||||
if Keyword.fetch!(opts, :verbose) do
|
||||
Mix.shell().info("Mix.install/2 using #{install_dir}")
|
||||
Mix.shell().info("Mix.install/2 using #{install_project_dir}")
|
||||
end
|
||||
|
||||
if force? do
|
||||
File.rm_rf!(install_dir)
|
||||
File.rm_rf!(install_project_dir)
|
||||
end
|
||||
|
||||
config = [
|
||||
version: "0.1.0",
|
||||
build_embedded: false,
|
||||
build_per_environment: true,
|
||||
build_path: "_build",
|
||||
lockfile: "mix.lock",
|
||||
deps_path: "deps",
|
||||
dynamic_config = [
|
||||
deps: deps,
|
||||
app: :mix_install,
|
||||
erlc_paths: [],
|
||||
elixirc_paths: [],
|
||||
compilers: [],
|
||||
consolidate_protocols: consolidate_protocols?,
|
||||
config_path: config_path,
|
||||
prune_code_paths: false
|
||||
config_path: config_path
|
||||
]
|
||||
|
||||
config = install_project_config(dynamic_config)
|
||||
|
||||
started_apps = Application.started_applications()
|
||||
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
|
||||
build_dir = Path.join(install_dir, "_build")
|
||||
build_dir = Path.join(install_project_dir, "_build")
|
||||
external_lockfile = expand_path(opts[:lockfile], deps, :lockfile, "mix.lock")
|
||||
|
||||
try do
|
||||
first_build? = not File.dir?(build_dir)
|
||||
File.mkdir_p!(install_dir)
|
||||
|
||||
File.cd!(install_dir, fn ->
|
||||
restore_dir = System.get_env("MIX_INSTALL_RESTORE_PROJECT_DIR")
|
||||
|
||||
if first_build? and restore_dir != nil and not force? do
|
||||
File.cp_r(restore_dir, install_project_dir)
|
||||
remove_dep(install_project_dir, @mix_install_app_string)
|
||||
end
|
||||
|
||||
File.mkdir_p!(install_project_dir)
|
||||
|
||||
File.cd!(install_project_dir, fn ->
|
||||
if config_path do
|
||||
Mix.Task.rerun("loadconfig")
|
||||
end
|
||||
|
||||
cond do
|
||||
external_lockfile ->
|
||||
md5_path = Path.join(install_dir, "merge.lock.md5")
|
||||
md5_path = Path.join(install_project_dir, "merge.lock.md5")
|
||||
|
||||
old_md5 =
|
||||
case File.read(md5_path) do
|
||||
@@ -902,7 +913,7 @@ defmodule Mix do
|
||||
new_md5 = external_lockfile |> File.read!() |> :erlang.md5()
|
||||
|
||||
if old_md5 != new_md5 do
|
||||
lockfile = Path.join(install_dir, "mix.lock")
|
||||
lockfile = Path.join(install_project_dir, "mix.lock")
|
||||
old_lock = Mix.Dep.Lock.read(lockfile)
|
||||
new_lock = Mix.Dep.Lock.read(external_lockfile)
|
||||
Mix.Dep.Lock.write(Map.merge(old_lock, new_lock), file: lockfile)
|
||||
@@ -940,13 +951,22 @@ defmodule Mix do
|
||||
end
|
||||
end
|
||||
|
||||
Mix.State.put(:installed, id)
|
||||
if restore_dir do
|
||||
remove_leftover_deps(install_project_dir)
|
||||
end
|
||||
|
||||
Mix.State.put(:installed, {id, dynamic_config})
|
||||
:ok
|
||||
after
|
||||
Mix.ProjectStack.pop()
|
||||
# Clear all tasks invoked during installation, since there
|
||||
# is no reason to keep this in memory. Additionally this
|
||||
# allows us to rerun tasks for the dependencies later on,
|
||||
# such as recompilation
|
||||
Mix.Task.clear()
|
||||
end
|
||||
|
||||
^id when not force? ->
|
||||
{^id, _dynamic_config} when not force? ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
@@ -972,7 +992,35 @@ defmodule Mix do
|
||||
Path.join(app_dir, relative_path)
|
||||
end
|
||||
|
||||
defp install_dir(cache_id) do
|
||||
defp remove_leftover_deps(install_project_dir) do
|
||||
build_lib_dir = Path.join([install_project_dir, "_build", "dev", "lib"])
|
||||
|
||||
deps = File.ls!(build_lib_dir)
|
||||
|
||||
loaded_deps =
|
||||
for {app, _description, _version} <- Application.loaded_applications(),
|
||||
into: MapSet.new(),
|
||||
do: Atom.to_string(app)
|
||||
|
||||
# We want to keep :mix_install, but it has no application
|
||||
loaded_deps = MapSet.put(loaded_deps, @mix_install_app_string)
|
||||
|
||||
for dep <- deps, not MapSet.member?(loaded_deps, dep) do
|
||||
remove_dep(install_project_dir, dep)
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_dep(install_project_dir, dep) do
|
||||
build_lib_dir = Path.join([install_project_dir, "_build", "dev", "lib"])
|
||||
deps_dir = Path.join(install_project_dir, "deps")
|
||||
|
||||
build_path = Path.join(build_lib_dir, dep)
|
||||
File.rm_rf(build_path)
|
||||
dep_path = Path.join(deps_dir, dep)
|
||||
File.rm_rf(dep_path)
|
||||
end
|
||||
|
||||
defp install_project_dir(cache_id) do
|
||||
install_root =
|
||||
System.get_env("MIX_INSTALL_DIR") ||
|
||||
Path.join(Mix.Utils.mix_cache(), "installs")
|
||||
@@ -981,12 +1029,74 @@ defmodule Mix do
|
||||
Path.join([install_root, version, cache_id])
|
||||
end
|
||||
|
||||
defp install_project_config(dynamic_config) do
|
||||
[
|
||||
version: "0.1.0",
|
||||
build_embedded: false,
|
||||
build_per_environment: true,
|
||||
build_path: "_build",
|
||||
lockfile: "mix.lock",
|
||||
deps_path: "deps",
|
||||
app: @mix_install_app,
|
||||
erlc_paths: [],
|
||||
elixirc_paths: [],
|
||||
compilers: [],
|
||||
prune_code_paths: false
|
||||
] ++ dynamic_config
|
||||
end
|
||||
|
||||
@doc false
|
||||
def in_install_project(fun) do
|
||||
case safe_get_installed() do
|
||||
{id, dynamic_config} ->
|
||||
config = install_project_config(dynamic_config)
|
||||
|
||||
install_project_dir = install_project_dir(id)
|
||||
|
||||
File.cd!(install_project_dir, fn ->
|
||||
:ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile")
|
||||
|
||||
try do
|
||||
fun.()
|
||||
after
|
||||
Mix.ProjectStack.pop()
|
||||
end
|
||||
end)
|
||||
|
||||
nil ->
|
||||
Mix.raise("trying to call Mix.in_install_project/1, but Mix.install/2 was never called")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the directory where the current `Mix.install/2` project
|
||||
resides.
|
||||
"""
|
||||
@doc since: "1.16.2"
|
||||
@spec install_project_dir() :: Path.t() | nil
|
||||
def install_project_dir() do
|
||||
case safe_get_installed() do
|
||||
{id, _dynamic_config} -> install_project_dir(id)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether `Mix.install/2` was called in the current node.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
def installed? do
|
||||
Mix.State.get(:installed) != nil
|
||||
safe_get_installed() != nil
|
||||
end
|
||||
|
||||
defp safe_get_installed() do
|
||||
if mix_started?() do
|
||||
Mix.State.get(:installed)
|
||||
end
|
||||
end
|
||||
|
||||
defp mix_started?() do
|
||||
Process.whereis(Mix.State) != nil
|
||||
end
|
||||
|
||||
defp stop_apps([]), do: :ok
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule Mix.Rebar do
|
||||
@doc """
|
||||
Returns the path to the available `rebar` command.
|
||||
"""
|
||||
# TODO: Remove on Elixir v1.18 because phx_new and other installers rely on it.
|
||||
# TODO: Remove on Elixir v1.20 because phx_new and other installers rely on it.
|
||||
def rebar_cmd(:rebar) do
|
||||
Mix.shell().error("[warning] :rebar is no longer supported in Mix, falling back to :rebar3")
|
||||
rebar_cmd(:rebar3)
|
||||
@@ -218,7 +218,7 @@ defmodule Mix.Rebar do
|
||||
defp wrap_cmd(rebar) do
|
||||
cond do
|
||||
not match?({:win32, _}, :os.type()) ->
|
||||
rebar
|
||||
String.replace(rebar, " ", "\\ ")
|
||||
|
||||
String.ends_with?(rebar, ".cmd") ->
|
||||
"\"#{String.replace(rebar, "/", "\\")}\""
|
||||
|
||||
@@ -131,6 +131,14 @@ defmodule Mix.SCM do
|
||||
Mix.State.get(:scm)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes the given SCM from the list of available SCMs.
|
||||
"""
|
||||
@doc since: "1.16.2"
|
||||
def delete(mod) when is_atom(mod) do
|
||||
Mix.State.update(:scm, &List.delete(&1, mod))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prepends the given SCM module to the list of available SCMs.
|
||||
"""
|
||||
|
||||
@@ -43,7 +43,11 @@ defmodule Mix.Tasks.Compile.All do
|
||||
Code.delete_paths(current_paths -- loaded_paths)
|
||||
end
|
||||
|
||||
Code.prepend_paths(loaded_paths -- current_paths, cache: true)
|
||||
# Add the current compilation path. compile.elixir and compile.erlang
|
||||
# will also add this path, but only if they run, so we always add it
|
||||
# here too. Furthermore, we don't cache it as we may still write to it.
|
||||
compile_path = to_charlist(Mix.Project.compile_path())
|
||||
Code.prepend_paths([compile_path | loaded_paths -- current_paths], cache: true)
|
||||
|
||||
result =
|
||||
if "--no-compile" in args do
|
||||
@@ -64,12 +68,6 @@ defmodule Mix.Tasks.Compile.All do
|
||||
Mix.AppLoader.write_cache(app_cache, Map.new(loaded_modules))
|
||||
end
|
||||
|
||||
# Add the current compilation path. compile.elixir and compile.erlang
|
||||
# will also add this path, but only if they run, so we always add it
|
||||
# here too. Furthermore, we don't cache it as we may still write to it.
|
||||
compile_path = to_charlist(Mix.Project.compile_path())
|
||||
_ = Code.prepend_path(compile_path)
|
||||
|
||||
unless "--no-app-loading" in args do
|
||||
app = config[:app]
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ defmodule Mix.Tasks.Compile.Leex do
|
||||
# TODO: Remove me in Elixir v2.0
|
||||
unless :leex in List.wrap(project[:compilers]) do
|
||||
IO.warn(
|
||||
"in order to compile .xrl files, you must add \"compilers: [:leex] ++ Mix.compilers()\" to the \"def project\" section of your mix.exs"
|
||||
"in order to compile .xrl files, you must add \"compilers: [:leex] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Mix.Tasks.Compile.Yecc do
|
||||
# TODO: Remove me in Elixir v2.0
|
||||
unless :yecc in List.wrap(project[:compilers]) do
|
||||
IO.warn(
|
||||
"in order to compile .yrl files, you must add \"compilers: [:yecc] ++ Mix.compilers()\" to the \"def project\" section of your mix.exs"
|
||||
"in order to compile .yrl files, you must add \"compilers: [:yecc] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
{"TERM", "dumb"}
|
||||
]
|
||||
|
||||
cmd = "#{rebar_cmd(dep)} bare compile --paths #{lib_path}"
|
||||
cmd = "#{rebar_cmd(dep)} bare compile --paths #{escape_path(lib_path)}"
|
||||
do_command(dep, config, cmd, false, env)
|
||||
|
||||
# Check if we have any new symlinks after compilation
|
||||
@@ -220,6 +220,11 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
true
|
||||
end
|
||||
|
||||
defp escape_path(path) do
|
||||
escape = if match?({:win32, _}, :os.type()), do: "^ ", else: "\\ "
|
||||
String.replace(path, " ", escape)
|
||||
end
|
||||
|
||||
defp rebar_config(dep) do
|
||||
dep.extra
|
||||
|> Mix.Rebar.dependency_config()
|
||||
|
||||
@@ -1021,7 +1021,7 @@ defmodule Mix.Tasks.Release do
|
||||
* `--no-deps-check` - does not check dependencies
|
||||
* `--no-elixir-version-check` - does not check Elixir version
|
||||
* `--no-compile` - does not compile before assembling the release
|
||||
* `--overwrite` - if there is an existing release version, overwrite it
|
||||
* `--overwrite` - overwrite existing files instead of prompting the user for action
|
||||
* `--path` - the path of the release
|
||||
* `--quiet` - does not write progress to the standard output
|
||||
* `--version` - the version of the release
|
||||
|
||||
@@ -219,7 +219,7 @@ defmodule Mix.RebarTest do
|
||||
# We run only on Unix because Windows has a hard time
|
||||
# removing the Rebar executable after executed.
|
||||
@tag :unix
|
||||
test "applies variables from :system_env option when compiling dependencies" do
|
||||
test "applies variables from :system_env option on config/compilation" do
|
||||
in_tmp("applies variables from system_env", fn ->
|
||||
Mix.Project.push(RebarAsDepWithEnv)
|
||||
|
||||
@@ -233,6 +233,27 @@ defmodule Mix.RebarTest do
|
||||
end)
|
||||
end
|
||||
|
||||
# We run only on Unix because Windows has a hard time
|
||||
# removing the Rebar executable after executed.
|
||||
@tag :unix
|
||||
test "gets and compiles dependencies with MIX_REBAR3 with spaces" do
|
||||
in_tmp("rebar3 env with spaces", fn ->
|
||||
File.cp!(Mix.Rebar.local_rebar_path(:rebar3), "rebar3")
|
||||
System.put_env("MIX_REBAR3", Path.absname("rebar3"))
|
||||
assert Mix.Rebar.rebar_cmd(:rebar3) =~ " "
|
||||
|
||||
Mix.Project.push(RebarAsDep)
|
||||
Mix.Tasks.Deps.Get.run([])
|
||||
assert_received {:mix_shell, :info, ["* Getting git_rebar " <> _]}
|
||||
|
||||
Mix.Tasks.Deps.Compile.run([])
|
||||
assert_received {:mix_shell, :run, ["===> Compiling git_rebar\n"]}
|
||||
assert_received {:mix_shell, :run, ["===> Compiling rebar_dep\n"]}
|
||||
end)
|
||||
after
|
||||
System.delete_env("MIX_REBAR3")
|
||||
end
|
||||
|
||||
test "gets and compiles dependencies with Mix" do
|
||||
in_tmp("get and compile dependencies with Mix", fn ->
|
||||
Mix.Project.push(RebarAsDep)
|
||||
|
||||
@@ -12,10 +12,14 @@ defmodule Mix.SCMTest do
|
||||
test "prepends an SCM" do
|
||||
Mix.SCM.prepend(Hello)
|
||||
assert Enum.at(Mix.SCM.available(), 0) == Hello
|
||||
Mix.SCM.delete(Hello)
|
||||
assert Hello not in Mix.SCM.available()
|
||||
end
|
||||
|
||||
test "appends an SCM" do
|
||||
Mix.SCM.append(Hello)
|
||||
assert Enum.at(Mix.SCM.available(), -1) == Hello
|
||||
Mix.SCM.delete(Hello)
|
||||
assert Hello not in Mix.SCM.available()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,6 +19,14 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
|
||||
File.write!("lib/a.ex", """
|
||||
defmodule A, do: :ok
|
||||
|
||||
# Also make sure that we access the ebin directory during compilation
|
||||
true = to_charlist(Mix.Project.compile_path()) in :code.get_path()
|
||||
""")
|
||||
|
||||
Mix.Tasks.Compile.Elixir.run(["--verbose"])
|
||||
|
||||
assert File.regular?("_build/shared/lib/sample/ebin/Elixir.A.beam")
|
||||
@@ -32,6 +40,14 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
test "compiles a project with per environment build" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
|
||||
File.write!("lib/a.ex", """
|
||||
defmodule A, do: :ok
|
||||
|
||||
# Also make sure that we access the ebin directory during compilation
|
||||
true = to_charlist(Mix.Project.compile_path()) in :code.get_path()
|
||||
""")
|
||||
|
||||
Mix.Tasks.Compile.Elixir.run(["--verbose"])
|
||||
|
||||
assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam")
|
||||
@@ -772,7 +788,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles mtime changed files if content changed but not length" do
|
||||
test "recompiles mtime changed files if content changed but not length" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
@@ -872,7 +888,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles size changed files" do
|
||||
test "recompiles size changed files" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
past = @old_time
|
||||
@@ -894,7 +910,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles dependent changed modules" do
|
||||
test "recompiles dependent changed modules" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
File.write!("lib/a.ex", "defmodule A, do: B.module_info()")
|
||||
@@ -914,7 +930,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles dependent changed modules without beam files" do
|
||||
test "recompiles dependent changed modules without beam files" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
|
||||
@@ -943,7 +959,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
Code.put_compiler_option(:ignore_module_conflict, false)
|
||||
end
|
||||
|
||||
test "compiles dependent changed modules even on removal" do
|
||||
test "recompiles dependent changed modules even on removal" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
File.write!("lib/a.ex", "defmodule A, do: B.module_info()")
|
||||
@@ -964,7 +980,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles dependent changed on conflict" do
|
||||
test "recompiles dependent changed on conflict" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
|
||||
@@ -991,7 +1007,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles dependent changed external resources" do
|
||||
test "recompiles dependent changed external resources" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
tmp = tmp_path("c.eex")
|
||||
|
||||
@@ -223,6 +223,15 @@ defmodule Mix.Tasks.TestTest do
|
||||
output = mix(["test", "test/passing_and_failing_test_failed.exs", "--failed"])
|
||||
assert output =~ "1 test, 1 failure"
|
||||
|
||||
# Plus line
|
||||
output = mix(["test", "test/passing_and_failing_test_failed.exs:5", "--failed"])
|
||||
assert output =~ "1 test, 1 failure"
|
||||
|
||||
if windows?() do
|
||||
output = mix(["test", "test\\passing_and_failing_test_failed.exs:5", "--failed"])
|
||||
assert output =~ "1 test, 1 failure"
|
||||
end
|
||||
|
||||
# `--failed` composes with an `--only` filter by running the intersection.
|
||||
# Of the failing tests, 1 is tagged with `@tag :foo`.
|
||||
# Of the passing tests, 1 is tagged with `@tag :foo`.
|
||||
@@ -509,17 +518,28 @@ defmodule Mix.Tasks.TestTest do
|
||||
refute output =~ "==> foo"
|
||||
refute output =~ "Paths given to \"mix test\" did not match any directory/file"
|
||||
|
||||
output = mix(["test", "apps/foo/test/foo_tests.exs:9", "apps/bar/test/bar_tests.exs:5"])
|
||||
casing =
|
||||
if windows?() do
|
||||
"apps\\bar\\test\\bar_tests.exs:5"
|
||||
else
|
||||
"apps/bar/test/bar_tests.exs:5"
|
||||
end
|
||||
|
||||
output = mix(["test", "apps/foo/test/foo_tests.exs:9", casing])
|
||||
|
||||
assert output =~ """
|
||||
Excluding tags: [:test]
|
||||
Including tags: [location: {"test/foo_tests.exs", 9}]
|
||||
"""
|
||||
|
||||
assert output =~ "1 test, 0 failures\n"
|
||||
|
||||
assert output =~ """
|
||||
Excluding tags: [:test]
|
||||
Including tags: [location: {"test/bar_tests.exs", 5}]
|
||||
"""
|
||||
|
||||
assert output =~ "4 tests, 0 failures, 3 excluded\n"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
+148
-34
@@ -39,7 +39,7 @@ defmodule MixTest do
|
||||
assert Protocol.consolidated?(InstallTest.Protocol)
|
||||
|
||||
assert_received {:mix_shell, :info, ["==> install_test"]}
|
||||
assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]}
|
||||
assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]}
|
||||
assert_received {:mix_shell, :info, ["Generated install_test app"]}
|
||||
refute_received _
|
||||
|
||||
@@ -67,7 +67,7 @@ defmodule MixTest do
|
||||
|
||||
assert File.dir?(Path.join(tmp_dir, "installs"))
|
||||
assert_received {:mix_shell, :info, ["==> install_test"]}
|
||||
assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]}
|
||||
assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]}
|
||||
assert_received {:mix_shell, :info, ["Generated install_test app"]}
|
||||
refute_received _
|
||||
|
||||
@@ -263,30 +263,26 @@ defmodule MixTest do
|
||||
[
|
||||
{:git_repo, git: fixture_path("git_repo")}
|
||||
],
|
||||
lockfile: lockfile,
|
||||
verbose: true
|
||||
lockfile: lockfile
|
||||
)
|
||||
|
||||
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
|
||||
assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]}
|
||||
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev
|
||||
after
|
||||
purge([GitRepo, GitRepo.MixProject])
|
||||
|
||||
install_project_dir = Mix.install_project_dir()
|
||||
assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev
|
||||
end
|
||||
|
||||
test ":lockfile merging", %{tmp_dir: tmp_dir} do
|
||||
[rev1, rev2 | _] = get_git_repo_revs("git_repo")
|
||||
|
||||
Mix.install(
|
||||
[
|
||||
{:git_repo, git: fixture_path("git_repo")}
|
||||
],
|
||||
verbose: true
|
||||
)
|
||||
Mix.install([
|
||||
{:git_repo, git: fixture_path("git_repo")}
|
||||
])
|
||||
|
||||
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
|
||||
assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]}
|
||||
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev1
|
||||
|
||||
install_project_dir = Mix.install_project_dir()
|
||||
assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev1
|
||||
|
||||
Mix.Project.push(GitApp)
|
||||
lockfile = Path.join(tmp_dir, "lock")
|
||||
@@ -300,9 +296,7 @@ defmodule MixTest do
|
||||
lockfile: lockfile
|
||||
)
|
||||
|
||||
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev1
|
||||
after
|
||||
purge([GitRepo, GitRepo.MixProject])
|
||||
assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev1
|
||||
end
|
||||
|
||||
test ":lockfile with application name", %{tmp_dir: tmp_dir} do
|
||||
@@ -318,15 +312,12 @@ defmodule MixTest do
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")},
|
||||
{:git_repo, git: fixture_path("git_repo")}
|
||||
],
|
||||
lockfile: :install_test,
|
||||
verbose: true
|
||||
lockfile: :install_test
|
||||
)
|
||||
|
||||
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
|
||||
assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]}
|
||||
assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev
|
||||
after
|
||||
purge([GitRepo, GitRepo.MixProject])
|
||||
install_project_dir = Mix.install_project_dir()
|
||||
assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev
|
||||
end
|
||||
|
||||
test ":lockfile that does not exist" do
|
||||
@@ -335,6 +326,73 @@ defmodule MixTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "restore dir", %{tmp_dir: tmp_dir} do
|
||||
with_cleanup(fn ->
|
||||
Mix.install([
|
||||
{:git_repo, git: fixture_path("git_repo")}
|
||||
])
|
||||
|
||||
assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]}
|
||||
assert_received {:mix_shell, :info, ["==> git_repo"]}
|
||||
assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]}
|
||||
assert_received {:mix_shell, :info, ["Generated git_repo app"]}
|
||||
refute_received _
|
||||
|
||||
install_project_dir = Mix.install_project_dir()
|
||||
build_lib_path = Path.join([install_project_dir, "_build", "dev", "lib"])
|
||||
deps_path = Path.join([install_project_dir, "deps"])
|
||||
|
||||
assert File.ls!(build_lib_path) |> Enum.sort() == ["git_repo", "mix_install"]
|
||||
assert File.ls!(deps_path) == ["git_repo"]
|
||||
|
||||
System.put_env("MIX_INSTALL_RESTORE_PROJECT_DIR", install_project_dir)
|
||||
end)
|
||||
|
||||
# Adding a dependency
|
||||
|
||||
with_cleanup(fn ->
|
||||
Mix.install([
|
||||
{:git_repo, git: fixture_path("git_repo")},
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")}
|
||||
])
|
||||
|
||||
assert_received {:mix_shell, :info, ["==> install_test"]}
|
||||
assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]}
|
||||
assert_received {:mix_shell, :info, ["Generated install_test app"]}
|
||||
refute_received _
|
||||
|
||||
install_project_dir = Mix.install_project_dir()
|
||||
build_lib_path = Path.join([install_project_dir, "_build", "dev", "lib"])
|
||||
deps_path = Path.join([install_project_dir, "deps"])
|
||||
|
||||
assert File.ls!(build_lib_path) |> Enum.sort() ==
|
||||
["git_repo", "install_test", "mix_install"]
|
||||
|
||||
assert File.ls!(deps_path) == ["git_repo"]
|
||||
|
||||
System.put_env("MIX_INSTALL_RESTORE_PROJECT_DIR", install_project_dir)
|
||||
end)
|
||||
|
||||
# Removing a dependency
|
||||
|
||||
with_cleanup(fn ->
|
||||
Mix.install([
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")}
|
||||
])
|
||||
|
||||
refute_received _
|
||||
|
||||
install_project_dir = Mix.install_project_dir()
|
||||
build_lib_path = Path.join([install_project_dir, "_build", "dev", "lib"])
|
||||
deps_path = Path.join([install_project_dir, "deps"])
|
||||
|
||||
assert File.ls!(build_lib_path) |> Enum.sort() == ["install_test", "mix_install"]
|
||||
assert File.ls!(deps_path) == []
|
||||
end)
|
||||
after
|
||||
System.delete_env("MIX_INSTALL_RESTORE_PROJECT_DIR")
|
||||
end
|
||||
|
||||
test "installed?", %{tmp_dir: tmp_dir} do
|
||||
refute Mix.installed?()
|
||||
|
||||
@@ -345,20 +403,42 @@ defmodule MixTest do
|
||||
assert Mix.installed?()
|
||||
end
|
||||
|
||||
test "in_install_project", %{tmp_dir: tmp_dir} do
|
||||
Mix.install([
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")}
|
||||
])
|
||||
|
||||
Mix.in_install_project(fn ->
|
||||
config = Mix.Project.config()
|
||||
assert [{:install_test, [path: _]}] = config[:deps]
|
||||
end)
|
||||
end
|
||||
|
||||
test "in_install_project recompile", %{tmp_dir: tmp_dir} do
|
||||
Mix.install([
|
||||
{:install_test, path: Path.join(tmp_dir, "install_test")}
|
||||
])
|
||||
|
||||
File.write!("#{tmp_dir}/install_test/lib/install_test.ex", """
|
||||
defmodule InstallTest do
|
||||
def hello do
|
||||
:universe
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
Mix.in_install_project(fn ->
|
||||
Mix.Task.run("compile")
|
||||
assert apply(InstallTest, :hello, []) == :universe
|
||||
end)
|
||||
end
|
||||
|
||||
defp test_project(%{tmp_dir: tmp_dir}) do
|
||||
path = :code.get_path()
|
||||
|
||||
on_exit(fn ->
|
||||
:code.set_path(path)
|
||||
purge([InstallTest, InstallTest.MixProject, InstallTest.Protocol])
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
Application.stop(:git_repo)
|
||||
Application.unload(:git_repo)
|
||||
|
||||
Application.stop(:install_test)
|
||||
Application.unload(:install_test)
|
||||
end)
|
||||
cleanup_deps()
|
||||
end)
|
||||
|
||||
Mix.State.put(:installed, nil)
|
||||
@@ -384,7 +464,9 @@ defmodule MixTest do
|
||||
:world
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
File.write!("#{tmp_dir}/install_test/lib/install_test_protocol.ex", """
|
||||
defprotocol InstallTest.Protocol do
|
||||
def foo(x)
|
||||
end
|
||||
@@ -392,5 +474,37 @@ defmodule MixTest do
|
||||
|
||||
[tmp_dir: tmp_dir]
|
||||
end
|
||||
|
||||
defp with_cleanup(fun) do
|
||||
path = :code.get_path()
|
||||
|
||||
try do
|
||||
fun.()
|
||||
after
|
||||
:code.set_path(path)
|
||||
cleanup_deps()
|
||||
|
||||
Mix.State.clear_cache()
|
||||
Mix.State.put(:installed, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_deps() do
|
||||
purge([
|
||||
GitRepo,
|
||||
GitRepo.MixProject,
|
||||
InstallTest,
|
||||
InstallTest.MixProject,
|
||||
InstallTest.Protocol
|
||||
])
|
||||
|
||||
ExUnit.CaptureLog.capture_log(fn ->
|
||||
Application.stop(:git_repo)
|
||||
Application.unload(:git_repo)
|
||||
|
||||
Application.stop(:install_test)
|
||||
Application.unload(:install_test)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user