Compare commits

..
77 Commits
Author SHA1 Message Date
José Valim 327063cc84 Release v1.16.3 2024-05-21 00:14:25 +02:00
José Valim 795d0a9583 More fixes 2024-05-20 16:51:13 +02:00
José Valim 1150ebad03 Update CHANGELOG 2024-05-06 12:50:25 +02:00
José Valim 18feec4cd8 Add brackets around keyword lists when formatting with when, closes #13503 2024-05-06 12:44:54 +02:00
José Valim 7d5920206b Check that docs are not hidden in suite, closes #13516 2024-05-02 01:26:48 +02:00
José Valim a694447601 Ensure translators are persisted across logger restarts 2024-05-01 11:40:12 +02:00
José Valim 19ae388dda Add .bat/.com disclaimers to System.cmd and Port 2024-04-17 11:29:02 +02:00
José Valim 6b5edea5f1 Fix --dbg handling in bin/elixir, closes #13482 2024-04-08 09:31:20 +02:00
Arne Bedurftig b736c23af9 Fix typo in basic types (#13481)
The exclamation mark was missing in the result of the concatenation.
2024-04-07 20:58:08 +02:00
Theodor Fiedler 63f0000ace Translate :undefined URI port to nil (#13464)
Resolves #13462

When a url string is schema less and the host is followed by a colon
without setting the actual port, `:uri_string.parse/1` returns the
port to be `:undefined`. As long as the url string is parseable,
we should translate `:undefined` to `nil`, in order to ensure we return
a valid URI struct.
2024-04-04 12:43:16 +02:00
José Valim 5e8b187d08 Ensure compile paths are available during compilation, closes #13458 2024-04-01 12:18:01 +02:00
Jean Klingler aea1a47b8d Only infer size in pinned variable when needed (#13423) 2024-03-20 19:00:17 +09:00
Jean Klingler eeba8e992d Fix Enum.slide/3 example in cheatsheet (#13054) 2024-03-16 17:09:52 +01:00
Steven C dd42dd8620 Fix typo around Enum.slide/3 in the Enum cheatsheet (#13053) 2024-03-16 17:09:45 +01:00
José Valim ed1ddfd338 Add parens to private macro example, closes #13411 2024-03-13 12:28:36 +01:00
José Valim d17df054e2 Skip tests if Erlang was compiled without docs, closes #13322 2024-03-10 19:07:22 +01:00
José Valim 41f6ee3f2e Release v1.16.2 2024-03-10 11:54:45 +01:00
José Valim 9d31768f42 Update CHANGELOG 2024-03-07 11:25:06 +01:00
José Valim 558e0ba6a6 Discard mermaid fenced blocks from ansi docs 2024-03-07 11:06:07 +01:00
Jean Klingler ab1e3448f0 Replace deprecated :code.lib_dir/2 usages (#13395) 2024-03-06 13:00:19 +01:00
Jean Klingler 17cd103a28 Logger handles :process_label from OTP27 (#13392) 2024-03-06 13:00:19 +01:00
José Valim 60e6ceae10 Correct task link in docs, closes #13388 2024-03-06 12:59:20 +01:00
José Valim f64ccd0871 Improve capture_log docs with latest Logger 2024-03-05 18:37:17 +01:00
Jonatan Kłosko 6c463b2310 Ensure install-related functions do not crash when Mix is not started (#13391) 2024-03-04 20:45:03 +01:00
H e310dd91d6 Remove duplicate line in formatter_callback doc (#13387) 2024-03-04 15:46:47 +09:00
Jonatan Kłosko 29ef147a22 Remove consolidated when restoring Mix.install/2 dir (#13382) 2024-03-01 09:34:26 +01:00
Jonatan Kłosko 74ab38439e Fix Mix.install_project_dir/0 spec (#13381) 2024-03-01 09:34:26 +01:00
José Valim 5654752026 Add @doc since to install_project_dir 2024-02-29 20:30:48 +01:00
Jonatan Kłosko e78538a42b Add environment variable for reusing Mix.install/2 installation (#13378) 2024-02-29 19:20:17 +01:00
Jonatan Kłosko 079b92e98e Support recompiling local Mix.install/2 dependencies (#13375) 2024-02-29 08:41:11 +01:00
José Valim 325877a224 Clarify the meaning of --overwrite, closes #13371 2024-02-26 20:16:27 +01:00
José Valim 097c4cf672 Emit defmodule tracing event 2024-02-26 18:28:47 +01:00
Łukasz Samson fe16fd3442 Add documentation to Exception callbacks (#13367) 2024-02-24 16:23:16 +01:00
Jean Klingler e520e8ce96 Fix unexpected rounding signs on OPT26- (#13365) 2024-02-24 16:05:09 +01:00
Jean Klingler dd004539c6 Fixes to support 0TP27 (#13351)
* Fix non-deterministic key-value tests

* Fix non-deterministic Enum tests

* Fix :only option when deriving Inspect

* Float.ceil and Float.floor return -0.0 for negative numbers

* Fix non-deterministic Registry doctest

* Simplify check
2024-02-24 16:03:58 +01:00
Jean Klingler 2512cbaff0 Fix doctest example since Inspect.MapSet changed (#13366) 2024-02-24 22:21:18 +09:00
sabiwara a99a9cf47a Fix test due to backport 2024-02-24 00:11:43 +09:00
Jean Klingler 677ad618e9 Fix charlist formatting issue on '\"' (#13364) 2024-02-24 00:02:38 +09:00
Philip Munksgaard ff309747b7 Escape pinned values when computing diff (#13354)
Take these two tests:

```elixir
  test "correctly colored" do
    assert [{:foo}] = [{:bar}]
  end

  test "incorrectly colored" do
    val = [{:foo}]
    assert ^val = [{:bar}]
  end
```

That's because when diffing a pin, we were not converting the underlying
diff context from match to ===.

This fixes #13348
2024-02-17 12:51:27 +01:00
José Valim e5ec5ce7f6 Add API for deleting SCM
This is used by Hex when its application is stopped
to revert its changes to Mix state.
2024-02-15 12:22:53 +01:00
Jean Klingler f3e669c471 Fix :trim_doc example in doc (#13340) 2024-02-13 07:39:31 +09:00
Jean Klingler bf915aa2e7 Fix <> match test (#13327) 2024-02-09 17:03:42 +09:00
Jean KlinglerandJosé Valim ef3e2796ef Update argument error message when matching with <> (#13325)
* Update argument error message when matching with <>

* Update lib/elixir/lib/kernel.ex

Co-authored-by: José Valim <jose.valim@gmail.com>

---------

Co-authored-by: José Valim <jose.valim@gmail.com>
2024-02-09 16:50:08 +09:00
Mitchell Hanberg d8cc841ab0 Include from_brackets metadata in all cases (#13317) 2024-02-05 09:49:40 +01:00
José Valim 7098ac8a16 Preserve . semantics in Path.relative_to, closes #13310 2024-02-01 12:41:26 +01:00
José Valim e0658bb55a Release v1.16.1 2024-01-31 10:24:07 +01:00
José Valim cf8c28c34b Fix autocompletion on Erlang/OTP 26, closes #13307 2024-01-31 10:12:58 +01:00
José Valim 60dcd14390 Fix Rebar3 env var with spaces (#13303) 2024-01-30 20:21:33 +01:00
Steve Johns 8344e218a6 docs: fix grammar errors in syntax reference docs (#13302) 2024-01-30 19:57:23 +01:00
José Valim 740b2d74df Escape rebar3 paths 2024-01-29 10:19:10 +01:00
José Valim da0189e641 Fix docs link
Related to #13284.
2024-01-27 11:46:53 +01:00
José Valim ad778a6f29 Fix capitalize for single codepoint (#13268) 2024-01-19 19:43:03 +01:00
José Valim c35edd1ee8 Improve end_of_expression docs 2024-01-18 08:14:04 +09:00
José Valim bb8ad4406b Improve ast metadata docs 2024-01-18 08:13:57 +09:00
Jean Klingler 111b48dcf4 Resolve relative paths in exunit filter (#13258) 2024-01-18 08:12:32 +09:00
Mitchell Hanberg 6dccefe687 Fix :from_interpolation docs (#13251) 2024-01-18 08:12:03 +09:00
Roman 9ea950c9af Macro anti-patterns: fix incorrect error message in code examples (#13259) 2024-01-18 08:09:13 +09:00
José Valim 7d100fff9f Always log errors at the end of compilation 2024-01-17 23:16:04 +01:00
Roman 6b69c7f5ac Fix the explanation to match the explained code example in docs (#13255)
The description incorrectly states that both processes are initialized with 0, while in the code the second process receives a non-default initial value.
2024-01-15 13:42:51 +01:00
Jean Klingler 691d402341 Fix Code.Normalizer for keyword operand with :do key (#13250) 2024-01-13 17:59:16 +09:00
José Valim b96211a325 Do not assume there is a $HOME, closes #13127 2024-01-10 13:01:55 +01:00
José Valim 17b9ba61d9 Do not crash parallel compiler on external reports, closes #13224 2024-01-10 10:17:03 +01:00
Hussien Liban f83b4e4e84 Update design-anti-patterns.md (#13210)
Not setting an option returns just the integer
2024-01-08 20:39:08 +09:00
Adebisi AdeyeyeandAdebisi Adeyeye 4de331597a Fix typo in docs: initial_valye -> initial_value (#13211)
Co-authored-by: Adebisi Adeyeye <adebisi.adeyeye@proebb.com>
2024-01-08 20:39:08 +09:00
Tomás Grüner 6e295998ef Fix typo in docs: Keywoird -> Keyword (#13205) 2024-01-08 20:39:08 +09:00
Will DouglasandWill Douglas f2dae095f8 Fix typo in docs: for -> force (#13215)
Co-authored-by: Will Douglas <will.cavalcanti@vmtecnologia.io>
2024-01-08 20:39:08 +09:00
Jean Klingler 6f5715fc30 Replace single quotes in charlist in doc (#13233) 2024-01-08 20:39:08 +09:00
José Valim 1ece71a831 Handle Windows separators on mix test (#13232)
Closes #13225.
2024-01-07 23:07:20 +01:00
Ryan B. Harvey 197351dd73 Fix a comment typo in the Code Anti-Patterns doc page (#13219) 2024-01-02 18:03:37 +01:00
José Valim 0d671dafea Improve anti-pattern titles 2024-01-02 16:42:17 +01:00
José Valim 9ae7c39125 Update CHANGELOG, closes #13217 2023-12-31 18:20:20 +01:00
José Valim ba8fb4dff1 Improve yecc/leex warnings, closes #13213 2023-12-28 17:12:51 +01:00
José Valim 194661197e Add more examples to app config anti-pattern (#13204) 2023-12-26 09:57:32 +01:00
Travis Vander Hoop 298acd1cdb Fix typos and tweak language in anti-pattern docs (#13208) 2023-12-26 09:57:32 +01:00
Artem Solomatin 97c608c346 Fix typo in design-anti-patterns doc (#13207) 2023-12-26 09:57:32 +01:00
José Valim f84bc19bf2 Additional clarity on long list of parameters anti-patterns 2023-12-24 14:54:45 +01:00
Alex Martsinovich 91375778cb Fix example in non-assertive map access antipattern (#13201) 2023-12-23 10:31:57 +01:00
92 changed files with 1199 additions and 403 deletions
+4 -1
View File
@@ -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
]
+73
View File
@@ -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`
-1
View File
@@ -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)"
+1 -1
View File
@@ -1 +1 @@
1.16.0
1.16.3
+3 -3
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
+29 -15
View File
@@ -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
+5 -5
View File
@@ -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
+2 -5
View File
@@ -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)
+14
View File
@@ -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 """
+1 -1
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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
+10 -5
View File
@@ -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)
+3 -2
View File
@@ -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
+31 -13
View File
@@ -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
+8 -5
View File
@@ -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
+2 -2
View File
@@ -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]
"""
+3 -7
View File
@@ -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
+2
View File
@@ -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
+21
View File
@@ -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
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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"
+1
View File
@@ -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)
[] -> ""
+19
View File
@@ -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"])
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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
+5 -5
View File
@@ -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:
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+1
View File
@@ -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",
+2 -2
View File
@@ -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),
+10 -7
View File
@@ -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.
+1
View File
@@ -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),
+3 -3
View File
@@ -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
+4 -4
View File
@@ -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
+91 -64
View File
@@ -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
+5 -2
View File
@@ -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
+2
View File
@@ -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
+1
View File
@@ -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"
+26
View File
@@ -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
+9 -9
View File
@@ -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`.
"""
+2 -2
View File
@@ -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
+9 -9
View File
@@ -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.
+2 -2
View File
@@ -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
-2
View File
@@ -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 [...]"`.
+8 -1
View File
@@ -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)
+9 -1
View File
@@ -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
+27 -21
View File
@@ -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)
+1 -3
View File
@@ -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)
+4 -3
View File
@@ -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
View File
@@ -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
+2 -5
View File
@@ -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 \\\\ [])"]}
+43 -10
View File
@@ -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"
+9 -1
View File
@@ -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
+4 -1
View File
@@ -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)
+7 -5
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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, "/", "\\")}\""
+8
View File
@@ -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.
"""
+5 -7
View File
@@ -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]
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+6 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+22 -1
View File
@@ -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)
+4
View File
@@ -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
+23 -7
View File
@@ -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")
+21 -1
View File
@@ -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
View File
@@ -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