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 |
+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,56 @@ 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
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.16.1
|
||||
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.1
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -708,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
+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