Compare commits

...
52 Commits
Author SHA1 Message Date
José Valim 0397a46bb7 Release v1.4.1 2017-01-26 13:02:51 +01:00
Eric Meadows-Jönsson b87e58e7e3 Run make clean for erlang.mk (#5698)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-26 12:59:35 +01:00
José Valim 6cb09a77e6 Update CHANGELOG 2017-01-24 14:13:11 +01:00
José Valim 12edab720c Remove warning when making private functions overridable 2017-01-23 17:01:49 +01:00
José Valim 74f9a34015 Only pass overrides from parent to child rebar dep (#5687)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-23 15:52:00 +01:00
Eric Meadows-Jönsson 49af97a800 Support rebar3 dependency package declaration (#5678)
Currently only {app, “~> 1.0.0”, {pkg, hex_package}} is supported. This
change will also support {app, {pkg, hex_package}}.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-23 12:44:08 +01:00
José Valim b806ee8559 override will likely be impl 2017-01-20 23:02:54 +01:00
James Fish 7d09c2b857 Wait for message before crash in exception tests
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-17 16:41:08 -07:00
José Valim f38646781b Do not warn on unused override attribute 2017-01-17 16:17:32 -07:00
José Valim b5777de582 appended -> prepended, closes #5656
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-14 11:11:45 +01:00
Andrea Leopardi 67f77ec1cd Fix Path.join/1 for lists of one element (#5639)
Also fix the spec for Path.join/1 to mention that the input list of paths has to
be non-empty, and fix the documentation to mention that this function takes a
list of paths (Path.t), not strings like it said before.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-10 11:02:40 +01:00
Saša Jurić 8a91f648a8 Fix typespecs in Calendar (#5635)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-09 20:23:16 +01:00
Devon Estes 7f61b0ff05 Add - to Regex.escape/1 (#5626)
We were not escaping the `-` character, commonly found in character
classes, in cases where it is being used on its own. I've added a test
for this new escape behavior, and also implemented the change.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-08 18:16:06 +01:00
José Valim c830fde31e Improve task docs, closes #5618 2017-01-05 11:02:53 +01:00
Matt Widman 019e6b85e6 Correct documentation referencing System.schedulers_online/1 to System.schedulers_online/0 (#5568)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-05 10:56:17 +01:00
José Valim 097fe87eb7 Disable ansi detection for powershell to avoid false positives
Closes #5615
2017-01-05 10:49:00 +01:00
Wojtek Mach 6ec1e7f7b4 Add new functions Time.utc_now/0, NaiveDateTime.utc_now/0 to CHANGELOG (#5617) 2017-01-05 10:42:23 +01:00
José Valim f045705ac5 Release v1.4.0 2017-01-05 00:09:35 +01:00
José Valim 2b4f573092 Cleanup exit messages on Task.async_stream (#5612) 2017-01-05 00:04:20 +01:00
Michal Muskala 7cf4369157 Issue warning instead of error for unhandled messages
While the change to start logging unhandled messages from handle_info/2 is a
good one, the choice to issue "error" level events was a poor one. Code that
worked perfectly fine on previous versions suddenly started issuing errors,
which is unexpected. A "warn" level log seems to be a better choice, at least
initially. The log level can be revisited in the future.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2017-01-01 18:25:35 +01:00
Gal Tsubery 4135ab1c0d Fix ceil function with zero as input (#5594)
Zero has special encoding as IEEE754 floating point number with a 0
value for exponent. This encoding requires special handling in
current Float.round implementation.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-27 10:47:09 +01:00
José Valim 89f7dbc64d Improve reading in error message
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-26 01:05:35 +01:00
Povilas Skruibis ec5afe2226 Fix system_test (#5528)
Git since v2.11 no longer defaults to 7-hexdigits for short object
abbreviation.

See https://github.com/git/git/commit/e6c587c733b4634030b353f4024794b08bc86892

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-22 23:36:22 +01:00
Aleksei Magusev 345ce9c592 Improve style of examples in Kernel.SpecialForms 2016-12-20 11:13:29 +01:00
José Valim 9c734ac6ee last_day_of_month -> days_in_month 2016-12-19 11:33:25 +01:00
José Valim 9388198ca0 Ensure autocomplete does not raise on struct keys, closes #5576
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-19 10:08:02 +01:00
Kelvin Stinghen 6ce366c7d4 Better behavior for the default test task (#5561)
Before, if the task didn't find a test_helper.exs file, it failed. Now, if
no test_helper.exs is found, a warning is emitted.

Beside this, some messages on empty states were improved.
2016-12-16 15:38:09 +01:00
José Valim 10f9b78b4f Make sure put_attribute still returns a quoted expression
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-09 20:39:20 +01:00
José Valim eb8c734b96 Move extract_files to proper test file
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-09 11:47:15 +01:00
José Valim c55cf5ac90 Do not traverse filesystem on empty paths
See #5551

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-09 11:26:20 +01:00
José Valim d28cc754b6 Use non-deprecated calls in Access.key/2 2016-12-07 11:55:20 +01:00
José Valim 1819addc75 Also mention Access.key! 2016-12-07 11:00:16 +01:00
José Valim 21e649a1f2 Add deprecation and warning of Access.key/1 behaviour, closes #5548 2016-12-07 10:53:44 +01:00
Andrea Leopardi c599956de1 Soft-deprecate (instead of hard-) Stream.uniq/2 in 1.4
The hard-deprecation should be done in version 1.5.
2016-12-07 10:36:45 +01:00
Myron Marston ca7f09a8b1 Mention new IO.inspect/2 :label option in changelog (#5547) 2016-12-07 10:31:12 +01:00
Eric Entin d32ad190ea Update CHANGELOG.md
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-06 21:50:07 +01:00
José Valim 4635631a56 Do not interpolate in defprotocol docs
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-06 17:46:46 +01:00
José Valim 2edf718f84 Do not promote Blank protocol in docs
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-06 17:37:42 +01:00
José Valim 16a14c1f6d Release v1.4.0-rc.1 2016-12-05 21:51:41 +01:00
Andrea Leopardi cfa6cf220e Use :extra_applications instead of :applications in escript.build (#5534) 2016-12-05 13:47:54 +01:00
José Valim 5e1b7d9470 Fix expansion_test
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-03 22:56:12 +01:00
José Valim 38baa729da Annotate the context for variables as zero-arity funs in quotes
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-12-03 22:42:35 +01:00
Andrea Leopardi 5fe53f7b7a Hard-deprecate the Set module (#5513)
It will be removed in Elixir 2.0.
2016-12-01 10:30:50 +01:00
José Valim 276b0d3c72 Ensure comprehensions with guards and filters keep proper ordering, closes #5509
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-30 20:00:38 +01:00
Henrik Nyh abb8817a0a inflect -> infer (#5503) 2016-11-30 09:36:07 +01:00
José Valim 5309843d2b Unify testing of evaluator related functionality
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-29 17:52:33 +01:00
Myron Marston 1a2c2dcda6 Improve IEx autocomplete to support navigating map atom keys (#5488)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-29 17:52:27 +01:00
José Valim 03b6c07215 Include proper line numbers on unused module attribute warning, closes #5490
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-29 11:15:51 +01:00
José Valim d10b9d5b29 Improve docs for archives
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-29 09:59:50 +01:00
José Valim e38be075c3 Add DateTime.from_iso8601/1
Thanks to Lau Taarnskov for suggestions and code review.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-29 00:15:27 +01:00
José Valim 28824e8171 Ensure child task cannot link after parent unlinks
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-11-28 15:07:29 +01:00
José Valim 3edc407713 Update CHANGELOG 2016-11-28 12:28:57 +01:00
58 changed files with 1243 additions and 820 deletions
+56 -7
View File
@@ -1,6 +1,6 @@
# Changelog for Elixir v1.4
Elixir v1.4 brings new features, enhancements and bug fixes into Elixir. The most notable changes are the addition of the `Registry` module and the `Task.async_stream/3` and `Task.async_stream/5` which aids developers in writing concurrent software. Those two features and a couple other improvements are described in detail below followed by the complete list of changes.
Elixir v1.4 brings new features, enhancements and bug fixes into Elixir. The most notable changes are the addition of the `Registry` module and the `Task.async_stream/3` and `Task.async_stream/5` which aid developers in writing concurrent software. Those two features and a couple other improvements are described in detail below followed by the complete list of changes.
## Registry
@@ -55,9 +55,9 @@ While the snippet above works fine in many occasions, for large collections it w
The `Task.async_stream` functions are also lazy, allowing developers to partially consume the stream until a condition is reached. Furthermore, `Task.Supervisor.async_stream/4` and `Task.Supervisor.async_stream/6` can be used to ensure the concurrent tasks are spawned under a given supervisor.
## Application inflection
## Application inference
Mix v1.4 now automatically inflects the list of applications that are required on runtime from your dependencies list.
Mix v1.4 now automatically infers the list of applications that are required on runtime from your dependencies list.
In previous Mix versions, most of your dependencies had to be added both to your dependencies list and applications list. Here is how a `mix.exs` would look like:
@@ -72,7 +72,7 @@ In previous Mix versions, most of your dependencies had to be added both to your
This was error prone as many developers would not list their dependencies in their applications list.
Mix v1.4 now automatically inflects your applications list as long as you leave the `:applications` key empty. The `mix.exs` above can be rewritten to:
Mix v1.4 now automatically infers your applications list as long as you leave the `:applications` key empty. The `mix.exs` above can be rewritten to:
def application do
[extra_applications: [:logger]]
@@ -91,7 +91,45 @@ Finally, if there is a dependency you don't want to include in the application r
We hope this feature provides a more streamlined workflow for developers who are building releases for their Elixir projects.
## v1.4.0-rc.0 (2016-11-28)
## Mix install from SCM
Mix v1.4 can now install escripts and archives from both Git and Hex, providing you with even more options for distributing Elixir code.
This makes it possible to distribute CLI applications written in Elixir by publishing a package which builds an escript to Hex. [`ex_doc`](https://hex.pm/packages/ex_doc) has been updated to serve as an example of how to use this new functionality.
Simply running:
mix escript.install hex ex_doc
will fetch `ex_doc` and its dependencies, build them, and then install `ex_doc` to `~/.mix/escripts` (by default). After adding `~/.mix/escripts` to your `PATH`, running `ex_doc` is as simple as:
ex_doc
You can now also install archives from Hex in this way. Since they are fetched and built on the user's machine, they do not have the same limitations as pre-built archives. However, keep in mind archives run alongside every Mix project, which may lead to conflicts. For this reason, escripts is the preferred format.
It is also possible to install escripts and archives by providing a Git/GitHub repo. See `mix help escript.install` and `mix help archive.install` for more details.
## v1.4.1 (2017-01-26)
### 1. Bug fixes
#### Elixir
* [Kernel] Remove warning when making private functions overridable
* [Path] Ensure `Path.join/1` returns strings for lists of one element
* [Regex] Ensure `Regex.escape/1` also escapes `-`
#### IEx
* [IEx] Disable ANSI detection for powershell to avoid false positives
#### Mix
* [Mix.Make] Run `make clean` for `erlang.mk`
* [Mix.Rebar] Support all of rebar3 dependency package declaration
* [Mix.Rebar] Only pass overrides from parent to child in Rebar dep
## v1.4.0 (2017-01-05)
### 1. Enhancements
@@ -101,14 +139,16 @@ We hope this feature provides a more streamlined workflow for developers who are
* [Calendar] Support `NaiveDateTime.add/3` and `NaiveDateTime.diff/3` for adding seconds (up to microseconds) as well as the difference between two NaiveDateTimes in seconds (up to microseconds)
* [Calendar] Add `Date.leap_year?/1` and `Date.day_of_week/1`
* [Calendar] Ensure `Date`, `Time` and `NaiveDateTime` APIs work with any struct that provides the same set of fields as their respective struct. For example, a `NaiveDateTime` can be given to `Date` since it contains a superset of the fields in the `Date` struct
* [Calendar] Add `Time.utc_now/0` and `NaiveDateTime.utc_now/0`
* [Enum] Add `Enum.map_every/2` that invokes the given function with every nth item
* [Enum] Add `min/2`, `max/2`, `min_max/2`, `min_by/3`, `max_by/3`, and `min_max_by/3` that allow a function specifying the default value when the enumerable is empty
* [Enum] Introduce `Enum.zip/1` to zip multiple entries at once
* [Float] Introduce `Float.ratio/1` that returns a tuple with the numerator and denominator as integers to retrieve the given float
* [GenServer] Log error on default `handle_info/2` implementation
* [GenServer] Log warn on default `handle_info/2` implementation
* [Inspect] Support syntax coloring via the `:syntax_color` option
* [Integer] `Integer.digits/2` now accepts negative integers
* [Integer] Add `Integer.mod/2` and `Integer.floor_div/2`
* [IO] Add `:label` option to `IO.inspect/2` to help distinguish multiple `IO.inspect/2` calls.
* [Kernel] Recognize merge conflict markers in source and provide a readable error message
* [Kernel] Warn on unused module attributes
* [Kernel] Improve compiler message on unexpected end of line
@@ -135,6 +175,7 @@ We hope this feature provides a more streamlined workflow for developers who are
* [IEx.Autocomplete] Stop appending a trailing dot when autocompleting modules in IEx
* [IEx.Autocomplete] Support autocompletion for structs
* [IEx.Autocomplete] Improve IEx autocomplete to support navigating map atom keys
* [IEx.Helpers] `c/1` now compiles in memory by default to avoid common issue where `.beam` files remain at projects root directory
* [IEx.Helpers] Add info about protocols in `i/1`
* [IEx.Server] Support interrupting IEx evaluation through the Ctrl+G prompt
@@ -142,12 +183,14 @@ We hope this feature provides a more streamlined workflow for developers who are
#### Mix
* [mix archive] Compress archive files built by `mix archive` as they are now unzipped during installation
* [mix compile] Automatically inflect the list of applications for Mix projects
* [mix archive] Install from SCM
* [mix compile] Automatically infer the list of applications for Mix projects
* [mix cmd] Add the ability to specify one or more apps in `mix cmd`
* [mix deps] Warn if there are non-applications in the `apps` directory for umbrella projects
* [mix deps] Add warning for invalid paths on `mix deps.clean`
* [mix deps] Add `Mix.Project.apps_paths` that returns the paths to children applications in umbrella projects
* [mix deps] Add `MIX_REBAR` environment variable for overriding local rebar
* [mix escript] Install from SCM
* [mix new] Check directory existence in `mix new` and ask how to proceed if one exists
* [mix new] Applications built with the `--sup` flag now have an individual module to work as application callback
* [mix test] Add `--formatter` option to `mix test`
@@ -157,9 +200,12 @@ We hope this feature provides a more streamlined workflow for developers who are
#### Elixir
* [Access] Do not accept nils in `Access.key/1` and `Access.key/2` in favor of explicit default values (or Access.key!/1 if you expect the key to always be available)
* [Float] Avoid multiple roundings in `Float.ceil/2`, `Float.floor/2` and `Float.round/2`
* [Kernel] Don't crash in `macro_exported?/3` when dealing with Erlang modules
* [Kernel] Ensure locals calls are rewritten when calling a local function or macro from inside a module
* [Kernel] Annotate the context for variables as zero-arity funs in quotes
* [Kernel.SpecialForms] Ensure comprehensions with guards and filters keep proper ordering,
* [Kernel.SpecialForms] Produce meaningful warning when with's else clauses have no effect
* [Macro] Wrap fn calls in parens in `Macro.to_string/2`
* [Macro] Do not print aliases as keys inside keyword lists in `Macro.to_string/2`
@@ -167,6 +213,7 @@ We hope this feature provides a more streamlined workflow for developers who are
* [Stream] Ensure `Stream.take/2` does not consume next element on `:suspend`
* [String] Fix infinite recursion in `String.replace_leading/3` and `String.replace_trailing/3` when given an empty string
* [Task] Fix `Task.shutdown/1,2` infinite block when task has no monitor
* [Task] Ensure task cannot link after parents unlinks
#### ExUnit
@@ -197,12 +244,14 @@ We hope this feature provides a more streamlined workflow for developers who are
#### Elixir
* [Access] `Access.key/1` is deprecated due to erratic behaviour for missing keys, please use Access.key/2 instead with proper default values
* [Behaviour] The `Behaviour` module is deprecated. Callbacks may now be defined directly via the `@callback` attribute
* [Enum] Deprecate `Enum.uniq/2` in favor of `Enum.uniq_by/2`
* [Float] `Float.to_char_list/2` and `Float.to_string/2` are deprecated (use the :erlang functions if such conversions are desired)
* [Kernel] Deprecate support for making private functions overridable. Overridable functions must always be public as they must be contracts
* [Kernel] Warn if variable is used as a function call
* [OptionParser] Deprecate aliases with multiple letters, such as `-abc`
* [Set] Deprecate the `Set` module
* [Stream] Deprecate `Stream.uniq/2` in favor of `Stream.uniq_by/2`
#### IEx
+1 -1
View File
@@ -1,7 +1,7 @@
REBAR ?= "$(CURDIR)/rebar"
PREFIX ?= /usr/local
SHARE_PREFIX ?= $(PREFIX)/share
CANONICAL := master/
CANONICAL :=
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
ERLC := erlc -I lib/elixir/include
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
+1 -1
View File
@@ -1 +1 @@
1.4.0-rc.0
1.4.1
-21
View File
@@ -100,27 +100,6 @@ for /d %%d in ("%originPath%..\lib\*.") do (
)
setlocal disabledelayedexpansion
rem ******* detect ANSI terminal support ********************
timeout 0 2>nul >nul || goto run
where /Q powershell || goto run
set ASSERT_ANSI= ^
$err = 1; ^
$Kernel32 = Add-Type -Name 'Kernel32' -PassThru -MemberDefinition ' ^
[DllImport(\"Kernel32.dll\", SetLastError = true)] ^
public static extern IntPtr GetStdHandle(int nStdHandle); ^
[DllImport(\"Kernel32.dll\", SetLastError = true)] ^
public static extern bool GetConsoleMode(IntPtr hWnd, ref UInt32 lpMode); ^
'; ^
$StdoutHandle = $Kernel32::GetStdHandle(-11); ^
$ConsoleMode = New-Object -TypeName UInt32; ^
$null = $Kernel32::GetConsoleMode($StdoutHandle, [ref]$ConsoleMode); ^
if ($ConsoleMode -band 0x4) { $err = 0 } ^
exit $err
powershell -NoProfile -NonInteractive -Command %ASSERT_ANSI% || goto run
set parsErlang=%parsErlang% -elixir ansi_enabled true
:run
if not %runMode% == "iex" (
set beforeExtra=-noshell -s elixir start_cli %beforeExtra%
+29 -7
View File
@@ -358,6 +358,28 @@ defmodule Access do
## Accessors
@doc false
def key(key) do
IO.warn "Access.key/1 is deprecated due to erratic behaviour for missing keys, " <>
"please use Access.key/2 instead with proper default values " <>
"(or Access.key!/1 if you expect the key to always be available)"
fn
:get, data, next ->
next.(Map.get(to_map(data), key))
:get_and_update, data, next ->
value = Map.get(to_map(data), key)
case next.(value) do
{get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)}
end
end
end
defp to_map(nil), do: %{}
defp to_map(%{} = map), do: map
defp to_map(data), do: raise "Access.key/1 expected a map/struct or nil, got: #{inspect data}"
@doc """
Returns a function that accesses the given key in a map/struct.
@@ -367,13 +389,13 @@ defmodule Access do
The returned function uses the default value if the key does not exist.
This can be used to specify defaults and safely traverse missing keys:
iex> get_in(%{}, [Access.key(:user, %{}), Access.key(:name)])
iex> get_in(%{}, [Access.key(:user, %{}), Access.key(:name, nil)])
nil
Such is also useful when using update functions, allowing us to introduce
values as we traverse the data-structure for updates:
iex> put_in(%{}, [Access.key(:user, %{}), Access.key(:name)], "Mary")
iex> put_in(%{}, [Access.key(:user, %{}), Access.key(:name, nil)], "Mary")
%{user: %{name: "Mary"}}
## Examples
@@ -381,23 +403,23 @@ defmodule Access do
iex> map = %{user: %{name: "john"}}
iex> get_in(map, [Access.key(:unknown, %{}), Access.key(:name, "john")])
"john"
iex> get_and_update_in(map, [Access.key(:user), Access.key(:name)], fn
iex> get_and_update_in(map, [Access.key!(:user), Access.key!(:name)], fn
...> prev -> {prev, String.upcase(prev)}
...> end)
{"john", %{user: %{name: "JOHN"}}}
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
{"john", %{user: %{}}}
An error is raised if the accessed structure is not a map or a struct:
iex> get_in(nil, [Access.key(:foo)])
iex> get_in(nil, [Access.key(:foo, nil)])
** (BadMapError) expected a map, got: nil
iex> get_in([], [Access.key(:foo)])
iex> get_in([], [Access.key(:foo, nil)])
** (BadMapError) expected a map, got: []
"""
def key(key, default \\ nil) do
def key(key, default) do
fn
:get, data, next ->
next.(Map.get(data, key, default))
+106 -10
View File
@@ -51,24 +51,24 @@ defmodule Calendar do
@type std_offset :: integer
@typedoc "Any map/struct that contains the date fields"
@type date :: %{calendar: calendar, year: year, month: month, day: day}
@type date :: %{optional(any) => any, calendar: calendar, year: year, month: month, day: day}
@typedoc "Any map/struct that contains the time fields"
@type time :: %{hour: hour, minute: minute, second: second, microsecond: microsecond}
@type time :: %{optional(any) => any, hour: hour, minute: minute, second: second, microsecond: microsecond}
@typedoc "Any map/struct that contains the naive_datetime fields"
@type naive_date_time :: %{calendar: calendar, year: year, month: month, day: day,
@type naive_date_time :: %{optional(any) => any, calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}
@typedoc "Any map/struct that contains the datetime fields"
@type date_time :: %{calendar: calendar, year: year, month: month, day: day,
@type date_time :: %{optional(any) => any, calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}
@doc """
Returns the last day of the month for the given year-month pair.
Returns how many days there are in the given year-month.
"""
@callback last_day_of_month(year, month) :: day
@callback days_in_month(year, month) :: day
@doc """
Returns true if the given year is a leap year.
@@ -90,7 +90,7 @@ defmodule Calendar do
@callback date_to_string(year, month, day) :: String.t
@doc """
Coverts the date time (without time zone) into a string according to the calendar.
Converts the date time (without time zone) into a string according to the calendar.
"""
@callback naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) :: String.t
@@ -175,6 +175,24 @@ defmodule Date do
calendar.leap_year?(year)
end
@doc """
Returns the number of days in the given date month.
## Examples
iex> Date.days_in_month(~D[1900-01-13])
31
iex> Date.days_in_month(~D[1900-02-09])
28
iex> Date.days_in_month(~N[2000-02-20 01:23:45])
29
"""
@spec days_in_month(Calendar.date) :: Calendar.day
def days_in_month(%{calendar: calendar, year: year, month: month}) do
calendar.days_in_month(year, month)
end
@doc """
Builds a new ISO date.
@@ -1598,9 +1616,7 @@ defmodule DateTime do
WARNING: the ISO 8601 datetime format does not contain the time zone nor
its abbreviation, which means information is lost when converting to such
format. This is also why this module does not provide a `from_iso8601/1`
function, as it is impossible to build a proper `DateTime` from only the
information in the ISO 8601 string.
format.
### Examples
@@ -1630,6 +1646,86 @@ defmodule DateTime do
time_zone, zone_abbr, utc_offset, std_offset)
end
@doc """
Parses the extended "Date and time of day" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Since ISO8601 does not include the proper time zone, the given
string will be converted to UTC and its offset in seconds will be
returned as part of this function. Therefore offset information
must be present in the string.
As specified in the standard, the separator "T" may be omitted if
desired as there is no ambiguity within this function.
Time representations with reduced accuracy are not supported.
## Examples
iex> DateTime.from_iso8601("2015-01-23T23:50:07Z")
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 23, microsecond: {0, 0}, minute: 50, month: 1, second: 7, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 2015, zone_abbr: "UTC"}, 0}
iex> DateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 21, microsecond: {123000, 3}, minute: 20, month: 1, second: 7, std_offset: 0,
time_zone: "Etc/UTC", utc_offset: 0, year: 2015, zone_abbr: "UTC"}, 9000}
iex> DateTime.from_iso8601("2015-01-23P23:50:07")
{:error, :invalid_format}
iex> DateTime.from_iso8601("2015-01-23 23:50:07A")
{:error, :invalid_format}
iex> DateTime.from_iso8601("2015-01-23T23:50:07")
{:error, :missing_offset}
iex> DateTime.from_iso8601("2015-01-23 23:50:61")
{:error, :invalid_time}
iex> DateTime.from_iso8601("2015-01-32 23:50:07")
{:error, :invalid_date}
iex> DateTime.from_iso8601("2015-01-23T23:50:07.123-00:00")
{:error, :invalid_format}
iex> DateTime.from_iso8601("2015-01-23T23:50:07.123-00:60")
{:error, :invalid_format}
"""
@spec from_iso8601(String.t) :: {:ok, t, Calendar.utc_offset} | {:error, atom}
def from_iso8601(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep,
hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>) when sep in [?\s, ?T] do
with {year, ""} <- Integer.parse(year),
{month, ""} <- Integer.parse(month),
{day, ""} <- Integer.parse(day),
{hour, ""} <- Integer.parse(hour),
{min, ""} <- Integer.parse(min),
{sec, ""} <- Integer.parse(sec),
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
{:ok, date} <- Calendar.ISO.date(year, month, day),
{:ok, time} <- Time.new(hour, min, sec, microsec),
{:ok, offset} <- parse_offset(rest) do
%{year: year, month: month, day: day} = date
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
erl = {{year, month, day}, {hour, minute, second}}
seconds = :calendar.datetime_to_gregorian_seconds(erl)
{{year, month, day}, {hour, minute, second}} =
:calendar.gregorian_seconds_to_datetime(seconds - offset)
{:ok, %DateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}, offset}
else
{:error, reason} -> {:error, reason}
_ -> {:error, :invalid_format}
end
end
def from_iso8601(_) do
{:error, :invalid_format}
end
defp parse_offset(rest) do
case Calendar.ISO.parse_offset(rest) do
{offset, ""} when is_integer(offset) -> {:ok, offset}
{nil, ""} -> {:error, :missing_offset}
_ -> {:error, :invalid_format}
end
end
@doc """
Converts the given datetime to a string according to its calendar.
+12 -12
View File
@@ -17,32 +17,32 @@ defmodule Calendar.ISO do
@type day :: 1..31
@doc """
Returns the last day of the month for the given year.
Returns how many days there are in the given year-month.
## Examples
iex> Calendar.ISO.last_day_of_month(1900, 1)
iex> Calendar.ISO.days_in_month(1900, 1)
31
iex> Calendar.ISO.last_day_of_month(1900, 2)
iex> Calendar.ISO.days_in_month(1900, 2)
28
iex> Calendar.ISO.last_day_of_month(2000, 2)
iex> Calendar.ISO.days_in_month(2000, 2)
29
iex> Calendar.ISO.last_day_of_month(2001, 2)
iex> Calendar.ISO.days_in_month(2001, 2)
28
iex> Calendar.ISO.last_day_of_month(2004, 2)
iex> Calendar.ISO.days_in_month(2004, 2)
29
iex> Calendar.ISO.last_day_of_month(2004, 4)
iex> Calendar.ISO.days_in_month(2004, 4)
30
"""
@spec last_day_of_month(year, month) :: 28..31
def last_day_of_month(year, month)
@spec days_in_month(year, month) :: 28..31
def days_in_month(year, month)
def last_day_of_month(year, 2) do
def days_in_month(year, 2) do
if leap_year?(year), do: 29, else: 28
end
def last_day_of_month(_, month) when month in [4, 6, 9, 11], do: 30
def last_day_of_month(_, month) when month in 1..12, do: 31
def days_in_month(_, month) when month in [4, 6, 9, 11], do: 30
def days_in_month(_, month) when month in 1..12, do: 31
@doc """
Returns if the given year is a leap year.
+1 -1
View File
@@ -647,7 +647,7 @@ defmodule UndefinedFunctionError do
def message(%{reason: :"function not exported", module: module, function: function, arity: arity, exports: exports}) do
suffix =
if macro_exported?(module, function, arity) do
" but #{inspect(module)} defines a macro with the same name and arity." <>
". However there is a macro with the same name and arity." <>
" Be sure to require #{inspect(module)} if you intend to invoke this macro"
else
did_you_mean(module, function, arity, exports)
+2 -1
View File
@@ -203,7 +203,8 @@ defmodule Float do
count = count - exp + 1023
cond do
count <= 0 -> # There is no decimal precision
count <= 0 or # There is no decimal precision
(0 == exp and <<0::52>> == significant) -> #zero or minus zero
float
count >= 104 -> # Precision beyond 15 digits
+2 -2
View File
@@ -568,8 +568,8 @@ defmodule GenServer do
{_, []} -> self()
{_, name} -> name
end
:error_logger.error_msg('~p ~p received unexpected message in handle_info/2: ~p~n',
[__MODULE__, proc, msg])
:error_logger.warning_msg('~p ~p received unexpected message in handle_info/2: ~p~n',
[__MODULE__, proc, msg])
{:noreply, state}
end
+71 -64
View File
@@ -2399,7 +2399,11 @@ defmodule Kernel do
# @attribute(value)
defp do_at([arg], meta, name, function?, env) do
read? = :lists.keymember(:context, 1, meta)
line =
case :lists.keymember(:context, 1, meta) do
true -> nil
false -> env.line
end
cond do
function? ->
@@ -2413,11 +2417,11 @@ defmodule Kernel do
{stack, _} = :elixir_quote.escape(env_stacktrace(env), false)
arg = {env.line, arg}
quote do: Module.put_attribute(__MODULE__, unquote(name), unquote(arg),
unquote(stack), unquote(read?))
unquote(stack), unquote(line))
true ->
quote do: Module.put_attribute(__MODULE__, unquote(name), unquote(arg),
nil, unquote(read?))
nil, unquote(line))
end
end
@@ -3704,7 +3708,7 @@ defmodule Kernel do
end
end
@doc """
@doc ~S"""
Defines a protocol.
A protocol specifies an API that should be defined by its
@@ -3712,43 +3716,46 @@ defmodule Kernel do
## Examples
In Elixir, only `false` and `nil` are considered falsy values.
Everything else evaluates to `true` in `if/2` clauses. Depending
on the application, it may be important to specify a `blank?`
protocol that returns a boolean for other data types that should
be considered "blank". For instance, an empty list or an empty
binary could be considered blank.
In Elixir, we have two verbs for checking how many items there
are in a data structure: `length` and `size`. `length` means the
information must be computed. For example, `length(list)` needs to
traverse the whole list to calculate its length. On the other hand,
`tuple_size(tuple)` and `byte_size(binary)` do not depend on the
tuple and binary size as the size information is precomputed in
the data structure.
Such protocol could be implemented as follows:
Although Elixir includes specific functions such as `tuple_size`,
`binary_size` and `map_size`, sometimes we want to be able to
retrieve the size of a data structure regardless of its type.
In Elixir we can write polymorphic code, i.e. code that works
with different shapes/types, by using protocols. A size protocol
could be implemented as follows:
defprotocol Blank do
@doc "Returns `true` if `data` is considered blank/empty"
def blank?(data)
defprotocol Size do
@doc "Calculates the size (and not the length!) of a data structure"
def size(data)
end
Now that the protocol is defined it can be implemented. It needs to be
implemented for each Elixir type; for example:
Now that the protocol can be implemented for every data structure
the protocol may have a compliant implementation for:
# Integers are never blank
defimpl Blank, for: Integer do
def blank?(number), do: false
defimpl Size, for: Binary do
def size(binary), do: byte_size(binary)
end
# The only blank list is the empty one
defimpl Blank, for: List do
def blank?([]), do: true
def blank?(_), do: false
defimpl Size, for: Map do
def size(map), do: map_size(map)
end
# The only blank atoms are "false" and "nil"
defimpl Blank, for: Atom do
def blank?(false), do: true
def blank?(nil), do: true
def blank?(_), do: false
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
The implementation of the `Blank` protocol would need to be defined for all
Elixir types. The available types are:
Notice we didn't implement it for lists as we don't have the
`size` information on lists, rather its value needs to be
computed with `length`.
It is possible to implement protocols for all Elixir types:
* Structs (see below)
* `Tuple`
@@ -3768,11 +3775,11 @@ defmodule Kernel do
The real benefit of protocols comes when mixed with structs.
For instance, Elixir ships with many data types implemented as
structs, like `MapSet`. We can implement the `Blank` protocol
structs, like `MapSet`. We can implement the `Size` protocol
for those types as well:
defimpl Blank, for: MapSet do
def blank?(enum_like), do: Enum.empty?(enum_like)
defimpl Size, for: MapSet do
def size(map_set), do: MapSet.size(map_set)
end
When implementing a protocol for a struct, the `:for` option can
@@ -3782,54 +3789,54 @@ defmodule Kernel do
defmodule User do
defstruct [:email, :name]
defimpl Blank do
def blank?(%User{}), do: false
defimpl Size do
def size(%User{}), do: 2 # two fields
end
end
If a protocol is not found for a given type, it will fallback to
`Any`. Protocols that are implemented for maps don't work by default
on structs; look at `defstruct/1` for more information about deriving
If a protocol implementation is not found for a given type,
invoking the protocol will raise unless it is configured to
fallback to `Any`. Conveniences for building implementations
on top of existing ones are also available, look at `defstruct/1`
for more information about deriving
protocols.
## Fallback to any
In some cases, it may be convenient to provide a default
implementation for all types. This can be achieved by
setting the `@fallback_to_any` attribute to `true` in the protocol
implementation for all types. This can be achieved by setting
the `@fallback_to_any` attribute to `true` in the protocol
definition:
defprotocol Blank do
defprotocol Size do
@fallback_to_any true
def blank?(data)
def size(data)
end
The `Blank` protocol can now be implemented for `Any`:
The `Size` protocol can now be implemented for `Any`:
defimpl Blank, for: Any do
def blank?(_), do: true
defimpl Size, for: Any do
def size(_), do: 0
end
One may wonder why such behaviour (fallback to any) is not the default one.
It is two-fold: first, the majority of protocols cannot
implement an action in a generic way for all types; in fact,
providing a default implementation may be harmful, because users
may rely on the default implementation instead of providing a
specialized one.
Second, falling back to `Any` adds an extra lookup to all types,
which is unnecessary overhead unless an implementation for `Any` is
required.
Although the implementation above is arguably not a reasonable
one. For example, it makes no sense to say a PID or an Integer
have a size of 0. That's one of the reasons why `@fallback_to_any`
is an opt-in behaviour. For the majority of protocols, raising
an error when a protocol is not implemented is the proper behaviour.
## Types
Defining a protocol automatically defines a type named `t`, which
can be used as follows:
@spec present?(Blank.t) :: boolean
def present?(blank) do
not Blank.blank?(blank)
@spec print_size(Size.t) :: :ok
def print_size(data) do
IO.puts(case Size.size(data) do
0 -> "data has no items"
1 -> "data has one item"
n -> "data has #{n} items"
end)
end
The `@spec` above expresses that all types allowed to implement the
@@ -3868,12 +3875,12 @@ defmodule Kernel do
all implementations are known up-front, Elixir provides a feature
called protocol consolidation. For this reason, all protocols are
compiled with `debug_info` set to `true`, regardless of the option
set by `elixirc` compiler. The debug info though may be removed
after consolidation.
set by `elixirc` compiler. The debug info though may be removed after
consolidation.
For more information on how to apply protocol consolidation to
a given project, please check the functions in the `Protocol`
module or the `mix compile.protocols` task.
Protocol consolidation is applied by default to all Mix projects.
For applying consolidation manually, please check the functions in
the `Protocol` module or the `mix compile.protocols` task.
"""
defmacro defprotocol(name, do_block)
+8 -8
View File
@@ -222,7 +222,7 @@ defmodule Kernel.SpecialForms do
iex> x = 1
iex> <<x::8>> == <<x::size(8)>>
true
iex> <<x::8 * 4>> == <<x::size(8)-unit(4)>>
iex> <<x::8*4>> == <<x::size(8)-unit(4)>>
true
This syntax reflects the fact the effective size is given by
@@ -293,7 +293,7 @@ defmodule Kernel.SpecialForms do
defmodule ImageTyper
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, rest::binary>>), do: :png
@@ -1241,7 +1241,7 @@ defmodule Kernel.SpecialForms do
[2, 4, 6, 8]
# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x*y
iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]
Filters can also be given:
@@ -1263,7 +1263,7 @@ defmodule Kernel.SpecialForms do
need to organize bitstring streams:
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels >>, do: {r, g, b}
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
Variable assignments inside the comprehension, be it in generators,
@@ -1301,7 +1301,7 @@ defmodule Kernel.SpecialForms do
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, width * height}
...> do: {:ok, width * height}
{:ok, 150}
If all clauses match, the `do` block is executed, returning its result.
@@ -1310,14 +1310,14 @@ defmodule Kernel.SpecialForms do
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, width * height}
...> do: {:ok, width * height}
:error
Guards can be used in patterns as well:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob"),
...> do: {:ok, to_string(role)}
...> do: {:ok, to_string(role)}
{:ok, "admin"}
As in `for/1`, variables bound inside `with/1` won't leak;
@@ -1328,7 +1328,7 @@ defmodule Kernel.SpecialForms do
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, double_width * height}
...> do: {:ok, double_width * height}
{:ok, 300}
iex> width
nil
+16 -24
View File
@@ -808,13 +808,6 @@ defmodule Module do
"cannot make private macro #{name}/#{arity} overridable, overriding " <>
"private macros is not supported"
clause ->
{{_def, kind, _line, _file, _check, _location, _defaults}, _clauses} = clause
# TODO: Remove on v2.0
if kind == :defp do
IO.warn "making private functions (#{name}/#{arity} in this case) overridable is deprecated"
end
neighbours =
if :elixir_compiler.get_opt(:internal) do
[]
@@ -843,8 +836,6 @@ defmodule Module do
@doc """
Puts a module attribute with key and value in the given module.
It returns the value of the attribute after normalization.
## Examples
defmodule MyModule do
@@ -852,9 +843,9 @@ defmodule Module do
end
"""
@spec put_attribute(module, key :: atom, value :: term) :: term
@spec put_attribute(module, key :: atom, value :: term) :: :ok
def put_attribute(module, key, value) do
put_attribute(module, key, value, nil, false)
put_attribute(module, key, value, nil, nil)
end
@doc """
@@ -910,7 +901,7 @@ defmodule Module do
table = data_table_for(module)
case :ets.take(table, key) do
[{_, value, _accumulated? = true, _}] ->
:ets.insert(table, {key, [], true, true})
:ets.insert(table, {key, [], true, nil})
value
[{_, value, _, _}] ->
value
@@ -920,9 +911,10 @@ defmodule Module do
end
@doc """
Registers an attribute. By registering an attribute, a developer
is able to customize how Elixir will store and accumulate the
attribute values.
Registers an attribute.
By registering an attribute, a developer is able to customize
how Elixir will store and accumulate the attribute values.
## Options
@@ -960,7 +952,7 @@ defmodule Module do
end
if Keyword.get(opts, :accumulate) do
:ets.insert_new(table, {new, [], _accumulated? = true, _read? = true}) ||
:ets.insert_new(table, {new, [], _accumulated? = true, _unread_line = nil}) ||
:ets.update_element(table, new, {3, true})
end
@@ -1031,7 +1023,7 @@ defmodule Module do
[] -> [value]
end
:ets.insert(table, {key, new, true, true})
:ets.insert(table, {key, new, true, nil})
end
@doc false
@@ -1043,7 +1035,7 @@ defmodule Module do
case :ets.lookup(table, key) do
[{^key, val, _, _}] ->
:ets.update_element(table, key, {4, true})
:ets.update_element(table, key, {4, nil})
val
[] when is_list(stack) ->
# TODO: Consider raising instead of warning on v2.0 as it usually cascades
@@ -1058,25 +1050,25 @@ defmodule Module do
@doc false
# Used internally by Kernel's @.
# This function is private and must be used only internally.
def put_attribute(module, key, value, stack, read?) when is_atom(key) do
def put_attribute(module, key, value, stack, unread_line) when is_atom(key) do
assert_not_compiled!(:put_attribute, module)
table = data_table_for(module)
value = preprocess_attribute(key, value)
case :ets.lookup(table, key) do
[{^key, {line, <<_::binary>>}, accumulated?, _read?}]
[{^key, {line, <<_::binary>>}, accumulated?, _unread_line}]
when key in [:doc, :typedoc, :moduledoc] and is_list(stack) ->
IO.warn "redefining @#{key} attribute previously set at line #{line}", stack
:ets.insert(table, {key, value, accumulated?, read?})
:ets.insert(table, {key, value, accumulated?, unread_line})
[{^key, current, _accumulated? = true, _read?}] ->
:ets.insert(table, {key, [value | current], true, read?})
:ets.insert(table, {key, [value | current], true, unread_line})
_ ->
:ets.insert(table, {key, value, false, read?})
:ets.insert(table, {key, value, false, unread_line})
end
value
:ok
end
## Helpers
+5 -5
View File
@@ -428,10 +428,10 @@ defmodule Path do
end
@doc """
Joins a list of strings.
Joins a list of paths.
This function should be used to convert a list of strings to a path.
Note that any trailing slash is removed on join.
This function should be used to convert a list of paths to a path.
Note that any trailing slash is removed when joining.
## Examples
@@ -445,11 +445,11 @@ defmodule Path do
"/foo/bar"
"""
@spec join([t]) :: binary
@spec join(nonempty_list(t)) :: binary
def join([name1, name2 | rest]), do:
join([join(name1, name2) | rest])
def join([name]), do:
name
IO.chardata_to_string(name)
@doc """
Joins two paths.
+1 -1
View File
@@ -629,7 +629,7 @@ defmodule Regex do
[get_index(string, h) | get_indexes(string, t, arity - 1)]
end
{:ok, pattern} = :re.compile(~S"[.^$*+?()\[\]{}\\\|\s#]", [:unicode])
{:ok, pattern} = :re.compile(~S"[.^$*+?()\[\]{}\\\|\s#-]", [:unicode])
@escape_pattern pattern
@doc ~S"""
+3 -1
View File
@@ -9,7 +9,9 @@ defmodule Set do
@type values :: [ value ]
@type t :: map
# TODO: Deprecate every function by 1.4
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
defmacrop target(set) do
quote do
case unquote(set) do
-1
View File
@@ -902,7 +902,6 @@ defmodule Stream do
@doc false
def uniq(enum, fun) do
IO.warn "Stream.uniq/2 is deprecated, use Stream.uniq_by/2 instead"
uniq_by(enum, fun)
end
+16 -14
View File
@@ -56,28 +56,27 @@ defmodule Task do
## Supervised tasks
It is also possible to spawn a task under a supervisor
with `start_link/1` and `start_link/3`:
Task.start_link(fn -> IO.puts "ok" end)
Such tasks can be mounted in your supervision tree as:
It is also possible to spawn a task under a supervisor:
import Supervisor.Spec
children = [
#
worker(Task, [fn -> IO.puts "ok" end])
]
Internally the supervisor will invoke `Task.start_link/1`.
Since these tasks are supervised and not directly linked to
the caller, they cannot be awaited on. Note `start_link/1`,
unlike `async/1`, returns `{:ok, pid}` (which is
the result expected by supervision trees).
By default, most supervision strategies will try to restart
a worker after it exits regardless of the reason. If you design the
task to terminate normally (as in the example with `IO.puts/2` above),
consider passing `restart: :transient` in the options to `Supervisor.Spec.worker/3`.
a worker after it exits regardless of the reason. If you design
the task to terminate normally (as in the example with `IO.puts/2`
above), consider passing `restart: :transient` in the options
to `Supervisor.Spec.worker/3`.
## Dynamically supervised tasks
@@ -286,16 +285,18 @@ defmodule Task do
Returns a stream that runs the given `module`, `function` and `args`
concurrently on each item in `enumerable`.
Each item will be appended to the given `args` and processed by its
own task. The tasks will be linked to the current process similar to
`async/3`.
Each item will be prepended to the given `args` and processed by its
own task. The tasks will be linked to an intermediate process that is
then linked to the current process. This means a failure in a task
terminates the current process and a failure in the current process
terminates all tasks.
When streamed, each task will emit `{:ok, val}` upon successful
completion or `{:exit, val}` if the caller is trapping exits. Results
are emitted in the same order as the original `enumerable`.
The level of concurrency can be controlled via the `:max_concurrency`
option and defaults to `System.schedulers_online/1`. The timeout
option and defaults to `System.schedulers_online/0`. The timeout
can also be given as option and defaults to 5000 and it defaults to
the maximum amount of time to wait without a task reply.
@@ -307,9 +308,10 @@ defmodule Task do
## Options
* `:max_concurrency` - sets the maximum number of tasks to run
at the same time. Defaults to `System.schedulers_online/1`.
at the same time. Defaults to `System.schedulers_online/0`.
* `:timeout` - the maximum amount of time to wait without
receiving a task reply (across all running tasks).
Defaults to `5000`.
## Example
+81 -68
View File
@@ -10,29 +10,21 @@ defmodule Task.Supervised do
{:ok, :proc_lib.spawn_link(__MODULE__, :noreply, [info, fun])}
end
def start_link(caller, link, info, fun) do
{:ok, spawn_link(caller, link, info, fun)}
def start_link(caller, monitor, info, fun) do
{:ok, spawn_link(caller, monitor, info, fun)}
end
def spawn_link(caller, link \\ :nolink, info, fun) do
:proc_lib.spawn_link(__MODULE__, :reply, [caller, link, info, fun])
def spawn_link(caller, monitor \\ :nomonitor, info, fun) do
:proc_lib.spawn_link(__MODULE__, :reply, [caller, monitor, info, fun])
end
def reply(caller, link, info, mfa) do
def reply(caller, monitor, info, mfa) do
initial_call(mfa)
case link do
:link ->
try do
Process.link(caller)
catch
:error, :noproc ->
exit({:shutdown, :noproc})
end
reply(caller, nil, @ref_timeout, info, mfa)
case monitor do
:monitor ->
mref = Process.monitor(caller)
reply(caller, mref, @ref_timeout, info, mfa)
:nolink ->
:nomonitor ->
reply(caller, nil, :infinity, info, mfa)
end
end
@@ -157,37 +149,41 @@ defmodule Task.Supervised do
parent = self()
# Start a process responsible for translating down messages.
{monitor_pid, monitor_ref} = spawn_monitor(fn -> stream_monitor(parent) end)
{:trap_exit, trap_exit} =
Process.info(self(), :trap_exit)
{monitor_pid, monitor_ref} =
Process.spawn(fn -> stream_monitor(parent, mfa, spawn, trap_exit) end, [:link, :monitor])
send(monitor_pid, {parent, monitor_ref})
stream_reduce(acc, max_concurrency, 0, 0, %{}, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
end
defp stream_reduce({:halt, acc}, _max, _spawned, _delivered, waiting, next,
_reducer, _mfa, _spawn, monitor_pid, monitor_ref, timeout) do
defp stream_reduce({:halt, acc}, _max, _spawned, _delivered, _waiting, next,
_reducer, monitor_pid, monitor_ref, timeout) do
stream_close(monitor_pid, monitor_ref, timeout)
is_function(next) && next.({:halt, []})
stream_close(waiting, monitor_pid, monitor_ref, timeout)
{:halted, acc}
end
defp stream_reduce({:suspend, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout) do
reducer, monitor_pid, monitor_ref, timeout) do
{:suspended, acc, &stream_reduce(&1, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)}
reducer, monitor_pid, monitor_ref, timeout)}
end
# All spawned, all delivered, next is done.
defp stream_reduce({:cont, acc}, _max, spawned, delivered, waiting, next,
_reducer, _mfa, _spawn, monitor_pid, monitor_ref, timeout)
defp stream_reduce({:cont, acc}, _max, spawned, delivered, _waiting, next,
_reducer, monitor_pid, monitor_ref, timeout)
when spawned == delivered and next == :done do
stream_close(waiting, monitor_pid, monitor_ref, timeout)
stream_close(monitor_pid, monitor_ref, timeout)
{:done, acc}
end
# No more tasks to spawn because max == 0 or next is done.
# No more tasks to spawned because max == 0 or next is done.
defp stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
when max == 0
when next == :done do
receive do
@@ -195,7 +191,7 @@ defmodule Task.Supervised do
%{^position => {pid, :running}} = waiting
waiting = Map.put(waiting, position, {pid, {:ok, value}})
stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
{:down, {^monitor_ref, position}, reason} ->
waiting =
case waiting do
@@ -203,53 +199,53 @@ defmodule Task.Supervised do
%{^position => {_, :running}} -> Map.put(waiting, position, {nil, {:exit, reason}})
end
stream_deliver({:cont, acc}, max + 1, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
{:DOWN, ^monitor_ref, _, ^monitor_pid, reason} ->
stream_cleanup_inbox(monitor_ref)
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({reason, {__MODULE__, :stream, [timeout]}})
after
timeout ->
stream_close(waiting, monitor_pid, monitor_ref, timeout)
stream_close(monitor_pid, monitor_ref, timeout)
exit({:timeout, {__MODULE__, :stream, [timeout]}})
end
end
defp stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout) do
reducer, monitor_pid, monitor_ref, timeout) do
try do
next.({:cont, []})
catch
kind, reason ->
stacktrace = System.stacktrace
stream_close(waiting, monitor_pid, monitor_ref, timeout)
stream_close(monitor_pid, monitor_ref, timeout)
:erlang.raise(kind, reason, stacktrace)
else
{:suspended, [value], next} ->
waiting = stream_spawn(value, spawned, waiting, mfa, spawn, monitor_pid, monitor_ref)
waiting = stream_spawn(value, spawned, waiting, monitor_pid, monitor_ref, timeout)
stream_reduce({:cont, acc}, max - 1, spawned + 1, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
{_, [value]} ->
waiting = stream_spawn(value, spawned, waiting, mfa, spawn, monitor_pid, monitor_ref)
waiting = stream_spawn(value, spawned, waiting, monitor_pid, monitor_ref, timeout)
stream_reduce({:cont, acc}, max - 1, spawned + 1, delivered, waiting, :done,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
{_, []} ->
stream_reduce({:cont, acc}, max, spawned, delivered, waiting, :done,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
end
end
defp stream_deliver({:suspend, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout) do
reducer, monitor_pid, monitor_ref, timeout) do
{:suspended, acc, &stream_deliver(&1, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)}
reducer, monitor_pid, monitor_ref, timeout)}
end
defp stream_deliver({:halt, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout) do
reducer, monitor_pid, monitor_ref, timeout) do
stream_reduce({:halt, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
end
defp stream_deliver({:cont, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout) do
reducer, monitor_pid, monitor_ref, timeout) do
case waiting do
%{^delivered => {nil, reply}} ->
try do
@@ -258,38 +254,44 @@ defmodule Task.Supervised do
kind, reason ->
stacktrace = System.stacktrace
is_function(next) && next.({:halt, []})
stream_close(waiting, monitor_pid, monitor_ref, timeout)
stream_close(monitor_pid, monitor_ref, timeout)
:erlang.raise(kind, reason, stacktrace)
else
pair ->
stream_deliver(pair, max, spawned, delivered + 1, Map.delete(waiting, delivered), next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
end
%{} ->
stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next,
reducer, mfa, spawn, monitor_pid, monitor_ref, timeout)
reducer, monitor_pid, monitor_ref, timeout)
end
end
defp stream_close(waiting, monitor_pid, monitor_ref, timeout) do
for {_, {pid, _}} <- waiting do
pid && Process.unlink(pid)
end
send(monitor_pid, {:down, monitor_ref})
defp stream_close(monitor_pid, monitor_ref, timeout) do
send(monitor_pid, {:stop, monitor_ref})
receive do
{:DOWN, ^monitor_ref, _, _, {:shutdown, ^monitor_ref}} ->
{:DOWN, ^monitor_ref, _, _, :normal} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
:ok
{:DOWN, ^monitor_ref, _, _, reason} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({reason, {__MODULE__, :stream, [timeout]}})
end
stream_cleanup_inbox(monitor_ref)
end
defp stream_cleanup_inbox(monitor_pid, monitor_ref) do
receive do
{:EXIT, ^monitor_pid, _} -> stream_cleanup_inbox(monitor_ref)
after
0 -> stream_cleanup_inbox(monitor_ref)
end
end
defp stream_cleanup_inbox(monitor_ref) do
receive do
{{^monitor_ref, _}, _} ->
stream_cleanup_inbox(monitor_ref)
{:DOWN, {^monitor_ref, _}, _} ->
{:down, {^monitor_ref, _}, _} ->
stream_cleanup_inbox(monitor_ref)
after
0 ->
@@ -300,38 +302,47 @@ defmodule Task.Supervised do
defp stream_mfa({mod, fun, args}, arg), do: {mod, fun, [arg | args]}
defp stream_mfa(fun, arg), do: {:erlang, :apply, [fun, [arg]]}
defp stream_spawn(value, spawned, waiting, mfa, spawn, monitor_pid, monitor_ref) do
owner = self()
{type, pid} = spawn.(owner, stream_mfa(mfa, value))
send(monitor_pid, {:up, owner, monitor_ref, spawned, type, pid})
Map.put(waiting, spawned, {pid, :running})
defp stream_spawn(value, spawned, waiting, monitor_pid, monitor_ref, timeout) do
send(monitor_pid, {:spawn, spawned, value})
receive do
{:spawned, {^monitor_ref, ^spawned}, pid} ->
send(pid, {self(), {monitor_ref, spawned}})
Map.put(waiting, spawned, {pid, :running})
{:DOWN, ^monitor_ref, _, ^monitor_pid, reason} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({reason, {__MODULE__, :stream, [timeout]}})
end
end
defp stream_monitor(parent_pid) do
defp stream_monitor(parent_pid, mfa, spawn, trap_exit) do
Process.flag(:trap_exit, trap_exit)
parent_ref = Process.monitor(parent_pid)
receive do
{^parent_pid, monitor_ref} ->
stream_monitor(parent_pid, parent_ref, monitor_ref, %{})
stream_monitor(parent_pid, parent_ref, mfa, spawn, monitor_ref, %{})
{:DOWN, ^parent_ref, _, _, reason} ->
exit(reason)
end
end
defp stream_monitor(parent_pid, parent_ref, monitor_ref, counters) do
defp stream_monitor(parent_pid, parent_ref, mfa, spawn, monitor_ref, counters) do
receive do
{:up, owner, ^monitor_ref, counter, type, pid} ->
{:spawn, counter, value} ->
{type, pid} = spawn.(parent_pid, stream_mfa(mfa, value))
ref = Process.monitor(pid)
send(pid, {owner, {monitor_ref, counter}})
send(parent_pid, {:spawned, {monitor_ref, counter}, pid})
counters = Map.put(counters, ref, {counter, type, pid})
stream_monitor(parent_pid, parent_ref, monitor_ref, counters)
{:down, ^monitor_ref} ->
stream_monitor(parent_pid, parent_ref, mfa, spawn, monitor_ref, counters)
{:stop, ^monitor_ref} ->
Process.flag(:trap_exit, true)
for {ref, {_counter, _, pid}} <- counters do
Process.exit(pid, :kill)
receive do
{:DOWN, ^ref, _, _, _} -> :ok
end
end
exit({:shutdown, monitor_ref})
exit(:normal)
{:DOWN, ^parent_ref, _, _, reason} ->
for {_ref, {_counter, :link, pid}} <- counters do
Process.exit(pid, reason)
@@ -340,7 +351,9 @@ defmodule Task.Supervised do
{:DOWN, ref, _, _, reason} ->
{{counter, _, _}, counters} = Map.pop(counters, ref)
send(parent_pid, {:down, {monitor_ref, counter}, reason})
stream_monitor(parent_pid, parent_ref, monitor_ref, counters)
stream_monitor(parent_pid, parent_ref, mfa, spawn, monitor_ref, counters)
{:EXIT, _, _} ->
stream_monitor(parent_pid, parent_ref, mfa, spawn, monitor_ref, counters)
end
end
end
+11 -10
View File
@@ -106,7 +106,7 @@ defmodule Task.Supervisor do
"""
@spec async_nolink(Supervisor.supervisor, module, atom, [term]) :: Task.t
def async_nolink(supervisor, module, fun, args) do
do_async(supervisor, :monitor, module, fun, args)
do_async(supervisor, :nolink, module, fun, args)
end
@doc """
@@ -122,7 +122,7 @@ defmodule Task.Supervisor do
are emitted in the same order as the original `enumerable`.
The level of concurrency can be controlled via the `:max_concurrency`
option and defaults to `System.schedulers_online/1`. The timeout
option and defaults to `System.schedulers_online/0`. The timeout
can also be given as option and defaults to 5000 and it defaults to
the maximum amount of time to wait without a task reply.
@@ -133,9 +133,10 @@ defmodule Task.Supervisor do
## Options
* `:max_concurrency` - sets the maximum number of tasks to run
at the same time. Defaults to `System.schedulers_online/1`.
at the same time. Defaults to `System.schedulers_online/0`.
* `:timeout` - the maximum amount of time to wait without
receiving a task reply (across all running tasks).
Defaults to `5000`.
## Examples
@@ -181,7 +182,7 @@ defmodule Task.Supervisor do
Enumerable.t
def async_stream_nolink(supervisor, enumerable, module, function, args, options \\ [])
when is_atom(module) and is_atom(function) and is_list(args) do
build_stream(supervisor, :monitor, enumerable, {module, function, args}, options)
build_stream(supervisor, :nolink, enumerable, {module, function, args}, options)
end
@doc """
@@ -197,7 +198,7 @@ defmodule Task.Supervisor do
@spec async_stream_nolink(Supervisor.supervisor, Enumerable.t, (term -> term), Keyword.t) ::
Enumerable.t
def async_stream_nolink(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do
build_stream(supervisor, :monitor, enumerable, fun, options)
build_stream(supervisor, :nolink, enumerable, fun, options)
end
@doc """
@@ -250,7 +251,7 @@ defmodule Task.Supervisor do
defp do_async(supervisor, link_type, module, fun, args) do
owner = self()
args = [owner, link_type, get_info(owner), {module, fun, args}]
args = [owner, :monitor, get_info(owner), {module, fun, args}]
{:ok, pid} = Supervisor.start_child(supervisor, args)
if link_type == :link, do: Process.link(pid)
ref = Process.monitor(pid)
@@ -258,12 +259,12 @@ defmodule Task.Supervisor do
%Task{pid: pid, ref: ref, owner: owner}
end
defp build_stream(supervisor, type, enumerable, fun, options) do
defp build_stream(supervisor, link_type, enumerable, fun, options) do
&Task.Supervised.stream(enumerable, &1, &2, fun, options, fn owner, mfa ->
args = [owner, type, get_info(owner), mfa]
args = [owner, :monitor, get_info(owner), mfa]
{:ok, pid} = Supervisor.start_child(supervisor, args)
if type == :link, do: Process.link(pid)
{type, pid}
if link_type == :link, do: Process.link(pid)
{link_type, pid}
end)
end
end
+3
View File
@@ -353,5 +353,8 @@ deprecation('Elixir.HashSet', _, _) ->
"use the MapSet module instead";
deprecation('Elixir.Dict', _, _) ->
"use the Map module for working with maps or the Keyword module for working with keyword lists";
deprecation('Elixir.Set', _, _) ->
"use the MapSet module for working with sets";
deprecation(_, _, _) ->
false.
+3 -1
View File
@@ -114,7 +114,9 @@ translate_gen(_Meta, Left, Right, T, S) ->
SF = SL#elixir_scope{extra=S#elixir_scope.extra, extra_guards=nil},
{TT, {TFilters, TS}} = translate_filters(T, SF),
Guards = ExtraGuards ++ translate_guards(TLeftGuards) ++ TFilters,
%% The list of guards is kept in reverse order
Guards = TFilters ++ translate_guards(TLeftGuards) ++ ExtraGuards,
{TLeft, TRight, Guards, TT, TS}.
translate_guards([]) ->
+23 -23
View File
@@ -85,7 +85,7 @@ do_compile(Line, Module, Block, Vars, E) ->
Forms3 = attributes_form(Line, File, Data, PersistedAttrs, Forms2),
elixir_locals:ensure_no_import_conflict(Line, File, Module, All),
warn_unused_attributes(Line, File, Data, PersistedAttrs),
warn_unused_attributes(File, Data, PersistedAttrs),
Location = {elixir_utils:characters_to_list(elixir_utils:relative_to_cwd(File)), Line},
@@ -152,26 +152,26 @@ build(Line, File, Module, Docs, Lexical) ->
end,
ets:insert(Data, [
% {Key, Value, Accumulate?, Read?}
{after_compile, [], true, true},
{before_compile, [], true, true},
{behaviour, [], true, true},
{compile, [], true, true},
{derive, [], true, true},
{dialyzer, [], true, true},
{external_resource, [], true, true},
{moduledoc, nil, false, true},
{on_definition, OnDefinition, true, true},
{on_load, [], true, true},
% {Key, Value, Accumulate?, UnreadLine}
{after_compile, [], true, nil},
{before_compile, [], true, nil},
{behaviour, [], true, nil},
{compile, [], true, nil},
{derive, [], true, nil},
{dialyzer, [], true, nil},
{external_resource, [], true, nil},
{moduledoc, nil, false, nil},
{on_definition, OnDefinition, true, nil},
{on_load, [], true, nil},
% Types
{callback, [], true, true},
{opaque, [], true, true},
{optional_callbacks, [], true, true},
{macrocallback, [], true, true},
{spec, [], true, true},
{type, [], true, true},
{typep, [], true, true}
{callback, [], true, nil},
{opaque, [], true, nil},
{optional_callbacks, [], true, nil},
{macrocallback, [], true, nil},
{spec, [], true, nil},
{type, [], true, nil},
{typep, [], true, nil}
]),
Persisted = [behaviour, on_load, compile, external_resource, dialyzer, vsn],
@@ -425,11 +425,11 @@ check_module_availability(Line, File, Module) ->
ok
end.
warn_unused_attributes(Line, File, Data, PersistedAttrs) ->
ReservedAttrs = [after_compile, before_compile, moduledoc, on_definition | PersistedAttrs],
Keys = ets:select(Data, [{{'$1', '_', '_', false}, [{is_atom, '$1'}], ['$1']}]),
warn_unused_attributes(File, Data, PersistedAttrs) ->
ReservedAttrs = [after_compile, before_compile, moduledoc, on_definition, impl | PersistedAttrs],
Keys = ets:select(Data, [{{'$1', '_', '_', '$2'}, [{is_atom, '$1'}, {is_integer, '$2'}], [['$1', '$2']]}]),
[elixir_errors:form_warn([{line, Line}], File, ?MODULE, {unused_attribute, Key}) ||
Key <- Keys, not lists:member(Key, ReservedAttrs)].
[Key, Line] <- Keys, not lists:member(Key, ReservedAttrs)].
% __INFO__
+33 -24
View File
@@ -189,12 +189,15 @@ do_quote({'__aliases__', Meta, [H | T]} = Alias, #elixir_quote{aliases_hygiene=t
Aliases when is_list(Aliases) -> false
end,
AliasMeta = keystore(alias, keydelete(counter, Meta), Annotation),
do_quote_tuple({'__aliases__', AliasMeta, [H | T]}, Q, E);
do_quote_tuple('__aliases__', AliasMeta, [H | T], Q, E);
%% Vars
do_quote({Left, Meta, nil}, #elixir_quote{vars_hygiene=true, imports_hygiene=true} = Q, E) when is_atom(Left) ->
do_quote_import(Left, Meta, Q#elixir_quote.context, Q, E);
do_quote({Left, Meta, nil}, #elixir_quote{vars_hygiene=true} = Q, E) when is_atom(Left) ->
do_quote_tuple({Left, Meta, Q#elixir_quote.context}, Q, E);
do_quote_tuple(Left, Meta, Q#elixir_quote.context, Q, E);
%% Unquote
@@ -207,28 +210,11 @@ do_quote({{'.', Meta, [Left, unquote]}, _, [Expr]}, #elixir_quote{unquote=true}
%% Imports
do_quote({'&', Meta, [{'/', _, [{F, _, C}, A]}] = Args},
#elixir_quote{imports_hygiene=true} = Q, E) when is_atom(F), is_integer(A), is_atom(C) ->
#elixir_quote{imports_hygiene=true} = Q, E) when is_atom(F), is_integer(A), is_atom(C) ->
do_quote_fa('&', Meta, Args, F, A, Q, E);
do_quote({Name, Meta, ArgsOrAtom}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name) ->
Arity = case is_atom(ArgsOrAtom) of
true -> 0;
false -> length(ArgsOrAtom)
end,
NewMeta = case (keyfind(import, Meta) == false) andalso
elixir_dispatch:find_import(Meta, Name, Arity, E) of
false ->
case (Arity == 1) andalso keyfind(ambiguous_op, Meta) of
{ambiguous_op, nil} -> keystore(ambiguous_op, Meta, Q#elixir_quote.context);
_ -> Meta
end;
Receiver ->
keystore(import, keystore(context, Meta, Q#elixir_quote.context), Receiver)
end,
Annotated = annotate({Name, NewMeta, ArgsOrAtom}, Q#elixir_quote.context),
do_quote_tuple(Annotated, Q, E);
do_quote_import(Name, Meta, ArgsOrAtom, Q, E);
do_quote({_, _, _} = Tuple, #elixir_quote{escape=false} = Q, E) ->
Annotated = annotate(Tuple, Q#elixir_quote.context),
@@ -300,6 +286,26 @@ bad_escape(Arg) ->
"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
"PIDs and remote functions in the format &Mod.fun/arity">>).
do_quote_import(Name, Meta, ArgsOrAtom, #elixir_quote{imports_hygiene=true} = Q, E) ->
Arity = case is_atom(ArgsOrAtom) of
true -> 0;
false -> length(ArgsOrAtom)
end,
NewMeta = case (keyfind(import, Meta) == false) andalso
elixir_dispatch:find_import(Meta, Name, Arity, E) of
false ->
case (Arity == 1) andalso keyfind(ambiguous_op, Meta) of
{ambiguous_op, nil} -> keystore(ambiguous_op, Meta, Q#elixir_quote.context);
_ -> Meta
end;
Receiver ->
keystore(import, keystore(context, Meta, Q#elixir_quote.context), Receiver)
end,
Annotated = annotate({Name, NewMeta, ArgsOrAtom}, Q#elixir_quote.context),
do_quote_tuple(Annotated, Q, E).
do_quote_call(Left, Meta, Expr, Args, Q, E) ->
All = [meta(Meta, Q), Left, {unquote, Meta, [Expr]}, Args,
Q#elixir_quote.context],
@@ -313,13 +319,16 @@ do_quote_fa(Target, Meta, Args, F, A, Q, E) ->
false -> Meta;
Receiver -> keystore(import_fa, Meta, {Receiver, Q#elixir_quote.context})
end,
do_quote_tuple({Target, NewMeta, Args}, Q, E).
do_quote_tuple(Target, NewMeta, Args, Q, E).
do_quote_tuple({Left, Meta, Right}, Q, E) ->
do_quote_tuple(Left, Meta, Right, Q, E).
% In a def unquote(name)(args) expression name will be an atom literal,
% thus location: :keep will not have enough information to generate the proper file/line annotation.
% This alters metadata to force Elixir to show the file to which the definition is added
% instead of the file where definition is quoted (i.e. we behave the opposite to location: :keep).
do_quote_tuple({Left, Meta, [{{unquote, _, _}, _, _}, _] = Right}, Q, E) when ?defs(Left) ->
do_quote_tuple(Left, Meta, [{{unquote, _, _}, _, _}, _] = Right, Q, E) when ?defs(Left) ->
{TLeft, LQ} = do_quote(Left, Q, E),
{[Head, Body], RQ} = do_quote(Right, LQ, E),
{'{}', [], [HLeft, HMeta, HRight]} = Head,
@@ -327,7 +336,7 @@ do_quote_tuple({Left, Meta, [{{unquote, _, _}, _, _}, _] = Right}, Q, E) when ?d
NewHead = {'{}', [], [HLeft, NewMeta, HRight]},
{{'{}', [], [TLeft, meta(Meta, Q), [NewHead, Body]]}, RQ};
do_quote_tuple({Left, Meta, Right}, Q, E) ->
do_quote_tuple(Left, Meta, Right, Q, E) ->
{TLeft, LQ} = do_quote(Left, Q, E),
{TRight, RQ} = do_quote(Right, LQ, E),
{{'{}', [], [TLeft, meta(Meta, Q), TRight]}, RQ}.
+28 -23
View File
@@ -264,16 +264,16 @@ defmodule ExceptionTest do
end
test "format_exit with call with exception" do
Process.flag(:trap_exit, true)
# Fake reason to prevent error_logger printing to stdout
fsm_reason = {%ArgumentError{}, [{:not_a_real_module, :function, 0, []}]}
reason = try do
:gen_fsm.sync_send_event(spawn(fn() ->
Process.sleep(200)
exit(fsm_reason)
end), :hello)
catch
:exit, reason -> reason
end
exit_reason = {%ArgumentError{}, [{:not_a_real_module, :function, 0, []}]}
exit_fun = fn() -> receive do: (_ -> exit(exit_reason)) end
reason =
try do
:gen_fsm.sync_send_event(spawn_link(exit_fun), :hello)
catch
:exit, reason -> reason
end
formatted = Exception.format_exit(reason)
assert formatted =~ ~r"exited in: :gen_fsm\.sync_send_event\(#PID<\d+\.\d+\.\d+>, :hello\)"
@@ -283,14 +283,16 @@ defmodule ExceptionTest do
end
test "format_exit with nested calls" do
Process.flag(:trap_exit, true)
# Fake reason to prevent error_logger printing to stdout
event_fun = fn() ->
Process.sleep(200)
exit(:normal)
end
server_pid = spawn(fn()-> :gen_event.call(spawn(event_fun), :handler, :hello) end)
exit_fun = fn() -> receive do: (_ -> exit(:normal)) end
outer_pid =
spawn_link(fn()->
Process.flag(:trap_exit, true)
:gen_event.call(spawn_link(exit_fun), :handler, :hello)
end)
reason = try do
:gen_server.call(server_pid, :hi)
:gen_server.call(outer_pid, :hi)
catch
:exit, reason -> reason
end
@@ -302,15 +304,17 @@ defmodule ExceptionTest do
end
test "format_exit with nested calls and exception" do
Process.flag(:trap_exit, true)
# Fake reason to prevent error_logger printing to stdout
event_reason = {%ArgumentError{}, [{:not_a_real_module, :function, 0, []}]}
event_fun = fn() ->
Process.sleep(200)
exit(event_reason)
end
server_pid = spawn(fn()-> :gen_event.call(spawn(event_fun), :handler, :hello) end)
exit_reason = {%ArgumentError{}, [{:not_a_real_module, :function, 0, []}]}
exit_fun = fn() -> receive do: (_ -> exit(exit_reason)) end
outer_pid =
spawn_link(fn()->
Process.flag(:trap_exit, true)
:gen_event.call(spawn_link(exit_fun), :handler, :hello)
end)
reason = try do
:gen_server.call(server_pid, :hi)
:gen_server.call(outer_pid, :hi)
catch
:exit, reason -> reason
end
@@ -386,8 +390,9 @@ defmodule ExceptionTest do
end
test "UndefinedFunctionError when the mfa is a macro but require wasn't called" do
_ = Code.ensure_loaded(Integer)
assert %UndefinedFunctionError{module: Integer, function: :is_odd, arity: 1} |> message ==
"function Integer.is_odd/1 is undefined or private but Integer defines " <>
"function Integer.is_odd/1 is undefined or private. However there is " <>
"a macro with the same name and arity. Be sure to require Integer if " <>
"you intend to invoke this macro"
end
+3
View File
@@ -80,6 +80,7 @@ defmodule FloatTest do
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
end
test "ceil with precision" do
@@ -94,6 +95,8 @@ defmodule FloatTest do
assert Float.ceil(12.32453e-20, 2) === 0.01
assert Float.ceil(-12.32453e-20, 2) === 0.0
assert Float.ceil(0.0, 2) === 0.0
end
test "round" do
@@ -3,6 +3,8 @@ Code.require_file "test_helper.exs", __DIR__
defmodule GenServerTest do
use ExUnit.Case, async: true
import ExUnit.CaptureLog
defmodule Stack do
use GenServer
@@ -161,4 +163,13 @@ defmodule GenServerTest do
{:ok, _} = GenServer.start(Stack, [], name: :stack_for_stop)
assert GenServer.stop(:stack_for_stop, :normal) == :ok
end
test "handle_info/2 warning" do
{:ok, pid} = GenServer.start(Stack, [])
log = capture_log(fn ->
send(pid, :foo)
GenServer.stop(pid)
end)
assert log =~ "[warn] GenServerTest.Stack #{inspect pid} received unexpected message in handle_info/2: :foo"
end
end
@@ -51,6 +51,12 @@ defmodule Kernel.ComprehensionTest do
assert for(x when x == 3 when x == 7 <- 1..10, do: x) == [3, 7]
end
test "for comprehensions with guards and filters" do
assert for({var, _} when is_atom(var) <- [{:foo, 1}, {2, :bar}],
var = Atom.to_string(var),
do: var) == ["foo"]
end
test "for comprehensions with map key matching" do
maps = [%{x: 1}, %{y: 2}, %{x: 3}]
assert for(%{x: v} <- maps, do: v * 2) == [2, 6]
@@ -153,7 +153,7 @@ defmodule Kernel.ExpansionTest do
end
test "locals: in guards" do
assert expand(quote(do: fn pid when :erlang.==(pid, self) -> pid end)) ==
assert expand_and_clean(quote(do: fn pid when :erlang.==(pid, self) -> pid end), [:import, :context]) ==
quote(do: fn pid when :erlang.==(pid, :erlang.self()) -> pid end)
end
@@ -337,22 +337,22 @@ defmodule Kernel.ExpansionTest do
## Cond
test "cond: expands each clause" do
assert expand_and_clean(quote do: (cond do x = 1 -> x; _ -> x end)) ==
assert expand(quote do: (cond do x = 1 -> x; _ -> x end)) ==
quote do: (cond do x = 1 -> x; _ -> x() end)
end
test "cond: does not share lexical scope between clauses" do
assert expand_and_clean(quote do: (cond do 1 -> import List; 2 -> flatten([1, 2, 3]) end)) ==
assert expand(quote do: (cond do 1 -> import List; 2 -> flatten([1, 2, 3]) end)) ==
quote do: (cond do 1 -> import :"Elixir.List", []; 2 -> flatten([1, 2, 3]) end)
end
test "cond: does not leaks vars on head" do
assert expand_and_clean(quote do: (cond do x = 1 -> x; y = 2 -> y end; :erlang.+(x, y))) ==
assert expand(quote do: (cond do x = 1 -> x; y = 2 -> y end; :erlang.+(x, y))) ==
quote do: (cond do x = 1 -> x; y = 2 -> y end; :erlang.+(x(), y()))
end
test "cond: leaks vars" do
assert expand_and_clean(quote do: (cond do 1 -> x = 1; 2 -> y = 2 end; :erlang.+(x, y))) ==
assert expand(quote do: (cond do 1 -> x = 1; 2 -> y = 2 end; :erlang.+(x, y))) ==
quote do: (cond do 1 -> x = 1; 2 -> y = 2 end; :erlang.+(x, y))
end
@@ -365,27 +365,27 @@ defmodule Kernel.ExpansionTest do
## Case
test "case: expands each clause" do
assert expand_and_clean(quote do: (case w do x -> x; _ -> x end)) ==
assert expand(quote do: (case w do x -> x; _ -> x end)) ==
quote do: (case w() do x -> x; _ -> x() end)
end
test "case: does not share lexical scope between clauses" do
assert expand_and_clean(quote do: (case w do 1 -> import List; 2 -> flatten([1, 2, 3]) end)) ==
assert expand(quote do: (case w do 1 -> import List; 2 -> flatten([1, 2, 3]) end)) ==
quote do: (case w() do 1 -> import :"Elixir.List", []; 2 -> flatten([1, 2, 3]) end)
end
test "case: expands guards" do
assert expand_and_clean(quote do: (case w do x when x when __ENV__.context -> true end)) ==
assert expand(quote do: (case w do x when x when __ENV__.context -> true end)) ==
quote do: (case w() do x when x when :guard -> true end)
end
test "case: does not leaks vars on head" do
assert expand_and_clean(quote do: (case w do x -> x; y -> y end; :erlang.+(x, y))) ==
assert expand(quote do: (case w do x -> x; y -> y end; :erlang.+(x, y))) ==
quote do: (case w() do x -> x; y -> y end; :erlang.+(x(), y()))
end
test "case: leaks vars" do
assert expand_and_clean(quote do: (case w do x -> x = x; y -> y = y end; :erlang.+(x, y))) ==
assert expand(quote do: (case w do x -> x = x; y -> y = y end; :erlang.+(x, y))) ==
quote do: (case w() do x -> x = x; y -> y = y end; :erlang.+(x, y))
end
@@ -398,32 +398,32 @@ defmodule Kernel.ExpansionTest do
## Receive
test "receive: expands each clause" do
assert expand_and_clean(quote do: (receive do x -> x; _ -> x end)) ==
assert expand(quote do: (receive do x -> x; _ -> x end)) ==
quote do: (receive do x -> x; _ -> x() end)
end
test "receive: does not share lexical scope between clauses" do
assert expand_and_clean(quote do: (receive do 1 -> import List; 2 -> flatten([1, 2, 3]) end)) ==
assert expand(quote do: (receive do 1 -> import List; 2 -> flatten([1, 2, 3]) end)) ==
quote do: (receive do 1 -> import :"Elixir.List", []; 2 -> flatten([1, 2, 3]) end)
end
test "receive: expands guards" do
assert expand_and_clean(quote do: (receive do x when x when __ENV__.context -> true end)) ==
assert expand(quote do: (receive do x when x when __ENV__.context -> true end)) ==
quote do: (receive do x when x when :guard -> true end)
end
test "receive: does not leaks clause vars" do
assert expand_and_clean(quote do: (receive do x -> x; y -> y end; :erlang.+(x, y))) ==
assert expand(quote do: (receive do x -> x; y -> y end; :erlang.+(x, y))) ==
quote do: (receive do x -> x; y -> y end; :erlang.+(x(), y()))
end
test "receive: leaks vars" do
assert expand_and_clean(quote do: (receive do x -> x = x; y -> y = y end; :erlang.+(x, y))) ==
assert expand(quote do: (receive do x -> x = x; y -> y = y end; :erlang.+(x, y))) ==
quote do: (receive do x -> x = x; y -> y = y end; :erlang.+(x, y))
end
test "receive: leaks vars on after" do
assert expand_and_clean(quote do: (receive do x -> x = x after y -> y; w = y end; :erlang.+(x, w))) ==
assert expand(quote do: (receive do x -> x = x after y -> y; w = y end; :erlang.+(x, w))) ==
quote do: (receive do x -> x = x after y() -> y(); w = y() end; :erlang.+(x, w))
end
@@ -540,8 +540,8 @@ defmodule Kernel.ExpansionTest do
13
end
defp expand_and_clean(expr) do
cleaner = &Keyword.drop(&1, [:export])
defp expand_and_clean(expr, vars) do
cleaner = &Keyword.drop(&1, vars)
expr
|> expand_env(__ENV__)
|> elem(0)
@@ -434,4 +434,18 @@ defmodule Kernel.QuoteTest.ImportsHygieneTest do
test "explicitly overridden imports" do
assert with_length() == 5
end
defmodule BinaryUtils do
defmacro int32 do
quote do
integer-size(32)
end
end
end
test "checks the context also for variables to zero-arity functions" do
import BinaryUtils
{:int32, meta, __MODULE__} = quote do: int32
assert meta[:import] == BinaryUtils
end
end
@@ -685,12 +685,11 @@ defmodule Kernel.WarningTest do
Code.eval_string """
defmodule Sample do
@at "Something"
Module.put_attribute(__MODULE__, :put_attribute, "Something")
end
"""
end)
assert content =~ "module attribute @at was set but never used"
assert content =~ "module attribute @put_attribute was set but never used"
assert content =~ "nofile:2"
after
purge Sample
end
+1 -1
View File
@@ -60,7 +60,7 @@ defmodule ModuleTest do
test "module attributes returns value" do
in_module do
assert (@return [:foo, :bar]) == [:foo, :bar]
assert (@return [:foo, :bar]) == :ok
_ = @return
end
end
+2
View File
@@ -211,6 +211,8 @@ defmodule PathTest do
assert Path.join(['/foo/', "/bar/"]) == "/foo/bar"
assert Path.join(["/", ""]) == "/"
assert Path.join(["/", "", "bar"]) == "/bar"
assert Path.join(['foo', [?b, "a", ?r]]) == "foo/bar"
assert Path.join([[?f, 'o', "o"]]) == "foo"
end
test "join two" do
+1
View File
@@ -247,6 +247,7 @@ defmodule RegexTest do
assert matches_escaped?("# lol")
assert matches_escaped?("\\A.^$*+?()[{\\| \t\n\x20\\z #hello\u202F\u205F")
assert Regex.match? Regex.compile!("[" <> Regex.escape("!-#") <> "]"), "-"
assert Regex.escape("{}") == "\\{\\}"
assert Regex.escape("[]") == "\\[\\]"
+1 -1
View File
@@ -13,7 +13,7 @@ defmodule SystemTest do
assert is_binary build_info[:version]
if build_info[:revision] != "" do
assert String.length(build_info[:revision]) == 7
assert String.length(build_info[:revision]) >= 7
end
version_file = Path.join([__DIR__, "../../../..", "VERSION"]) |> Path.expand
+1
View File
@@ -492,6 +492,7 @@ defmodule TaskTest do
Process.flag(:trap_exit, true)
assert 1..4 |> Task.async_stream(&exit/1, @opts) |> Enum.to_list ==
[exit: 1, exit: 2, exit: 3, exit: 4]
refute_received {:EXIT, _, _}
end
test "shuts down unused tasks" do
+106 -31
View File
@@ -1,20 +1,22 @@
defmodule IEx.Autocomplete do
@moduledoc false
def expand('') do
def expand(expr, server \\ IEx.Server)
def expand('', _server) do
expand_import("")
end
def expand([h | t]=expr) do
def expand([h | t]=expr, server) do
cond do
h === ?. and t != [] ->
expand_dot(reduce(t))
expand_dot(reduce(t), server)
h === ?: and t == [] ->
expand_erlang_modules()
identifier?(h) ->
expand_expr(reduce(expr))
expand_expr(reduce(expr), server)
(h == ?/) and t != [] and identifier?(hd(t)) ->
expand_expr(reduce(t))
expand_expr(reduce(t), server)
h in '([{' ->
expand('')
true ->
@@ -26,31 +28,33 @@ defmodule IEx.Autocomplete do
(h in ?a..?z) or (h in ?A..?Z) or (h in ?0..?9) or h in [?_, ??, ?!]
end
defp expand_dot(expr) do
defp expand_dot(expr, server) do
case Code.string_to_quoted expr do
{:ok, atom} when is_atom(atom) ->
expand_call(atom, "")
expand_call(atom, "", server)
{:ok, {:__aliases__, _, list}} ->
expand_elixir_modules(list, "")
expand_elixir_modules(list, "", server)
{:ok, {_, _, _} = ast_node} ->
expand_call(ast_node, "", server)
_ ->
no()
end
end
defp expand_expr(expr) do
defp expand_expr(expr, server) do
case Code.string_to_quoted expr do
{:ok, atom} when is_atom(atom) ->
expand_erlang_modules(Atom.to_string(atom))
{:ok, {atom, _, nil}} when is_atom(atom) ->
expand_import(Atom.to_string(atom))
{:ok, {:__aliases__, _, [root]}} ->
expand_elixir_modules([], Atom.to_string(root))
expand_elixir_modules([], Atom.to_string(root), server)
{:ok, {:__aliases__, _, [h | _] = list}} when is_atom(h) ->
hint = Atom.to_string(List.last(list))
list = Enum.take(list, length(list) - 1)
expand_elixir_modules(list, hint)
{:ok, {{:., _, [mod, fun]}, _, []}} when is_atom(fun) ->
expand_call(mod, Atom.to_string(fun))
expand_elixir_modules(list, hint, server)
{:ok, {{:., _, [ast_node, fun]}, _, []}} when is_atom(fun) ->
expand_call(ast_node, Atom.to_string(fun), server)
_ ->
no()
end
@@ -105,21 +109,37 @@ defmodule IEx.Autocomplete do
## Expand calls
# :atom.fun
defp expand_call(mod, hint) when is_atom(mod) do
defp expand_call(mod, hint, _server) when is_atom(mod) do
expand_require(mod, hint)
end
# Elixir.fun
defp expand_call({:__aliases__, _, list}, hint) do
expand_alias(list)
defp expand_call({:__aliases__, _, list}, hint, server) do
expand_alias(list, server)
|> normalize_module
|> expand_require(hint)
end
defp expand_call(_, _) do
# variable.fun_or_key
defp expand_call({_, _, _} = ast_node, hint, server) do
case value_from_binding(ast_node, server) do
{:ok, mod} when is_atom(mod) -> expand_call(mod, hint, server)
{:ok, map} when is_map(map) -> expand_map_field_access(map, hint)
_otherwise -> no()
end
end
defp expand_call(_, _, _) do
no()
end
defp expand_map_field_access(map, hint) do
case match_map_fields(map, hint) do
[%{kind: :map_key, name: name, value_is_map: false}] when name == hint -> no()
map_fields when is_list(map_fields) -> format_expansion(map_fields, hint)
end
end
defp expand_require(mod, hint) do
format_expansion match_module_funs(mod, hint), hint
end
@@ -145,26 +165,27 @@ defmodule IEx.Autocomplete do
## Elixir modules
defp expand_elixir_modules([], hint) do
expand_elixir_modules(Elixir, hint, match_aliases(hint))
defp expand_elixir_modules([], hint, server) do
aliases = match_aliases(hint, server)
expand_elixir_modules_from_aliases(Elixir, hint, aliases)
end
defp expand_elixir_modules(list, hint) do
expand_alias(list)
defp expand_elixir_modules(list, hint, server) do
expand_alias(list, server)
|> normalize_module
|> expand_elixir_modules(hint, [])
|> expand_elixir_modules_from_aliases(hint, [])
end
defp expand_elixir_modules(mod, hint, aliases) do
defp expand_elixir_modules_from_aliases(mod, hint, aliases) do
aliases
|> Kernel.++(match_elixir_modules(mod, hint))
|> Kernel.++(match_module_funs(mod, hint))
|> format_expansion(hint)
end
defp expand_alias([name | rest] = list) do
defp expand_alias([name | rest] = list, server) do
module = Module.concat(Elixir, name)
Enum.find_value env_aliases(), list, fn {alias, mod} ->
Enum.find_value aliases_from_env(server), list, fn {alias, mod} ->
if alias === module do
case Atom.to_string(mod) do
"Elixir." <> mod ->
@@ -176,12 +197,8 @@ defmodule IEx.Autocomplete do
end
end
defp env_aliases do
Application.get_env(:iex, :autocomplete_server).current_env.aliases
end
defp match_aliases(hint) do
for {alias, _mod} <- env_aliases(),
defp match_aliases(hint, server) do
for {alias, _mod} <- aliases_from_env(server),
[name] = Module.split(alias),
starts_with?(name, hint) do
%{kind: :module, type: :alias, name: name}
@@ -269,6 +286,14 @@ defmodule IEx.Autocomplete do
end
end
defp match_map_fields(map, hint) do
for {key, value} <- Map.to_list(map),
is_atom(key),
key = Atom.to_string(key),
String.starts_with?(key, hint),
do: %{kind: :map_key, name: key, value_is_map: is_map(value)}
end
defp get_module_funs(mod) do
docs = Code.get_docs(mod, :docs) || []
module_info_funs(mod) |> Enum.reject(&hidden_fun?(&1, docs))
@@ -316,6 +341,10 @@ defmodule IEx.Autocomplete do
for a <- :lists.sort(arities), do: "#{name}/#{a}"
end
defp to_entries(%{kind: :map_key, name: name}) do
[name]
end
defp to_uniq_entries(%{kind: :module}) do
[]
end
@@ -324,6 +353,10 @@ defmodule IEx.Autocomplete do
to_entries(fun)
end
defp to_uniq_entries(%{kind: :map_key}) do
[]
end
defp to_hint(%{kind: :module, name: name}, hint) when name == hint do
format_hint(name, name) <> "."
end
@@ -336,8 +369,50 @@ defmodule IEx.Autocomplete do
format_hint(name, hint)
end
defp to_hint(%{kind: :map_key, name: name, value_is_map: true}, hint) when name == hint do
format_hint(name, hint) <> "."
end
defp to_hint(%{kind: :map_key, name: name}, hint) do
format_hint(name, hint)
end
defp format_hint(name, hint) do
hint_size = byte_size(hint)
:binary.part(name, hint_size, byte_size(name) - hint_size)
end
defp aliases_from_env(server) do
with evaluator when is_pid(evaluator) <- server.evaluator,
{:ok, aliases} <- IEx.Evaluator.value_from_env(evaluator, :aliases) do
aliases
else
_ -> []
end
end
defp value_from_binding(ast_node, server) do
with evaluator when is_pid(evaluator) <- server.evaluator(),
{var, map_key_path} <- extract_from_ast(ast_node, []) do
IEx.Evaluator.value_from_binding(evaluator, var, map_key_path)
else
_ -> :error
end
end
defp extract_from_ast(var_name, acc) when is_atom(var_name) do
{var_name, acc}
end
defp extract_from_ast({var_name, _, nil}, acc) when is_atom(var_name) do
{var_name, acc}
end
defp extract_from_ast({{:., _, [ast_node, fun]}, _, []}, acc) when is_atom(fun) do
extract_from_ast(ast_node, [fun | acc])
end
defp extract_from_ast(_ast_node, _acc) do
:error
end
end
+46 -2
View File
@@ -24,20 +24,64 @@ defmodule IEx.Evaluator do
end
end
@doc """
Gets a value out of the binding, using the provided
variable name and map key path.
"""
@spec value_from_binding(pid, atom, [atom]) :: {:ok, any} | :error
def value_from_binding(evaluator, var_name, map_key_path) do
ref = make_ref()
send evaluator, {:value_from_binding, ref, self(), var_name, map_key_path}
receive do
{^ref, result} -> result
after
5000 -> :error
end
end
@doc """
Returns the current session environment if a session exists.
"""
@spec value_from_env(pid, atom) :: {:ok, term} | :error
def value_from_env(evaluator, key) do
ref = make_ref()
send evaluator, {:value_from_env, ref, self(), key}
receive do
{^ref, result} -> result
after
5000 -> :error
end
end
defp loop(server, history, state) do
receive do
{:eval, ^server, code, iex_state} ->
{result, history, state} = eval(code, iex_state, history, state)
send server, {:evaled, self(), result}
loop(server, history, state)
{:peek_env, receiver} ->
send receiver, {:peek_env, state.env}
{:value_from_env, ref, receiver, key} ->
send receiver, {ref, Map.fetch(state.env, key)}
loop(server, history, state)
{:value_from_binding, ref, receiver, var_name, map_key_path} ->
value = traverse_binding(state.binding, var_name, map_key_path)
send receiver, {ref, value}
loop(server, history, state)
{:done, ^server} ->
:ok
end
end
defp traverse_binding(binding, var_name, map_key_path) do
accumulator = Keyword.fetch(binding, var_name)
Enum.reduce map_key_path, accumulator, fn
key, {:ok, map} when is_map(map) -> Map.fetch(map, key)
_key, _acc -> :error
end
end
defp loop_state(opts) do
env =
if env = opts[:env] do
+13 -16
View File
@@ -43,19 +43,15 @@ defmodule IEx.Server do
end
@doc """
Returns the current session environment if a session exists.
Returns the PID of the IEx evaluator process if it exists.
"""
@spec current_env :: Macro.Env.t
def current_env() do
case IEx.Server.whereis() do
nil -> %Macro.Env{}
server ->
send(server, {:peek_env, self()})
receive do
{:peek_env, %Macro.Env{} = env} -> env
after
5000 -> %Macro.Env{}
end
@spec evaluator :: pid | nil
def evaluator() do
case IEx.Server.local do
nil -> nil
pid ->
{:dictionary, dictionary} = Process.info(pid, :dictionary)
dictionary[:evaluator]
end
end
@@ -144,7 +140,11 @@ defmodule IEx.Server do
loop(run_state(opts), evaluator, Process.monitor(evaluator))
end
defp start_evaluator(opts) do
@doc """
Starst an evaluator using the provided options.
"""
@spec start_evaluator(Keyword.t) :: pid
def start_evaluator(opts) do
self_pid = self()
self_leader = Process.group_leader
evaluator = opts[:evaluator] ||
@@ -192,9 +192,6 @@ defmodule IEx.Server do
exit_loop(evaluator, evaluator_ref)
{:input, ^input, {:error, :terminated}} ->
exit_loop(evaluator, evaluator_ref)
{:peek_env, receiver} ->
send evaluator, {:peek_env, receiver}
wait_input(state, evaluator, evaluator_ref, input)
msg ->
handle_take_over(msg, evaluator, evaluator_ref, input, fn ->
wait_input(state, evaluator, evaluator_ref, input)
-1
View File
@@ -11,7 +11,6 @@ defmodule IEx.Mixfile do
[registered: [IEx.Supervisor, IEx.Config],
mod: {IEx.App, []},
env: [
autocomplete_server: IEx.Server,
colors: [],
inspect: [pretty: true],
history_size: 20,
+92 -18
View File
@@ -3,8 +3,29 @@ Code.require_file "../test_helper.exs", __DIR__
defmodule IEx.AutocompleteTest do
use ExUnit.Case, async: true
setup context do
previous_line = context[:previous_line]
if previous_line do
ExUnit.CaptureIO.capture_io(fn ->
evaluator = IEx.Server.start_evaluator([])
Process.put(:evaluator, evaluator)
send evaluator, {:eval, self(), previous_line <> "\n", %IEx.State{}}
assert_receive {:evaled, _, _}
end)
end
:ok
end
defmodule MyServer do
def evaluator do
Process.get(:evaluator)
end
end
def expand(expr) do
IEx.Autocomplete.expand(Enum.reverse expr)
IEx.Autocomplete.expand(Enum.reverse(expr), MyServer)
end
test "Erlang module completion" do
@@ -93,6 +114,62 @@ defmodule IEx.AutocompleteTest do
assert expand('String.printable?/') == {:yes, '', ['printable?/1']}
end
@tag previous_line: "mod = String"
test "function completion using a variable bound to a module" do
assert expand('mod.print') == {:yes, 'able?', []}
end
@tag previous_line: "map = %{foo: 1, bar_1: 23, bar_2: 14}"
test "map atom key completion is supported" do
assert expand('map.f') == {:yes, 'oo', []}
assert expand('map.b') == {:yes, 'ar_', []}
assert expand('map.bar_') == {:yes, '', ['bar_1', 'bar_2']}
assert expand('map.c') == {:no, '', []}
assert expand('map.') == {:yes, '', ['bar_1', 'bar_2', 'foo']}
assert expand('map.foo') == {:no, '', []}
end
@tag previous_line: "map = %{nested: %{deeply: %{foo: 1, bar_1: 23, bar_2: 14, mod: String, num: 1}}}"
test "nested map atom key completion is supported" do
assert expand('map.nested.deeply.f') == {:yes, 'oo', []}
assert expand('map.nested.deeply.b') == {:yes, 'ar_', []}
assert expand('map.nested.deeply.bar_') == {:yes, '', ['bar_1', 'bar_2']}
assert expand('map.nested.deeply.') == {:yes, '', ['bar_1', 'bar_2', 'foo', 'mod', 'num']}
assert expand('map.nested.deeply.mod.print') == {:yes, 'able?', []}
assert expand('map.nested') == {:yes, '.', []}
assert expand('map.nested.deeply') == {:yes, '.', []}
assert expand('map.nested.deeply.foo') == {:no, '', []}
assert expand('map.nested.deeply.c') == {:no, '', []}
assert expand('map.a.b.c.f') == {:no, '', []}
end
@tag previous_line: ~s(map = %{"foo" => 1})
test "map string key completion is not supported" do
assert expand('map.f') == {:no, '', []}
end
@tag previous_line: "num = 5; map = %{nested: %{num: 23}}"
test "autocompletion off a bound variable only works for modules and maps" do
assert expand('num.print') == {:no, '', []}
assert expand('map.nested.num.f') == {:no, '', []}
assert expand('map.nested.num.key.f') == {:no, '', []}
end
@tag previous_line: "map = %{nested: %{deeply: %{num: 23}}}"
test "autocompletion using access syntax does is not supported" do
assert expand('map[:nested][:deeply].n') == {:no, '', []}
assert expand('map[:nested].deeply.n') == {:no, '', []}
assert expand('map.nested.[:deeply].n') == {:no, '', []}
end
@tag previous_line: "num = 5"
test "autocompletion off of unbound variables is not supported" do
assert expand('other_var.f') == {:no, '', []}
assert expand('a.b.c.d') == {:no, '', []}
end
test "macro completion" do
{:yes, '', list} = expand('Kernel.is_')
assert is_list(list)
@@ -136,23 +213,15 @@ defmodule IEx.AutocompleteTest do
assert expand('IEx.AutocompleteTest.SublevelTest.') == {:yes, 'LevelA', []}
end
defmodule MyServer do
def current_env do
%Macro.Env{aliases: [{MyList, List}, {EList, :lists}]}
end
end
@tag previous_line: "alias List, as: MyList"
test "complete aliases of Elixir modules" do
Application.put_env(:iex, :autocomplete_server, MyServer)
assert expand('MyL') == {:yes, 'ist', []}
assert expand('MyList') == {:yes, '.', []}
assert expand('MyList.to_integer') == {:yes, [], ['to_integer/1', 'to_integer/2']}
end
@tag previous_line: "alias :lists, as: EList"
test "complete aliases of Erlang modules" do
Application.put_env(:iex, :autocomplete_server, MyServer)
assert expand('EL') == {:yes, 'ist', []}
assert expand('EList') == {:yes, '.', []}
assert expand('EList.map') == {:yes, [], ['map/2', 'mapfoldl/3', 'mapfoldr/3']}
@@ -160,21 +229,21 @@ defmodule IEx.AutocompleteTest do
test "completion for functions added when compiled module is reloaded" do
{:module, _, bytecode, _} =
defmodule Elixir.Sample do
defmodule Sample do
def foo(), do: 0
end
File.write!("Elixir.Sample.beam", bytecode)
File.write!("Elixir.IEx.AutocompleteTest.Sample.beam", bytecode)
assert Code.get_docs(Sample, :docs)
assert expand('Sample.foo') == {:yes, '', ['foo/0']}
assert expand('IEx.AutocompleteTest.Sample.foo') == {:yes, '', ['foo/0']}
Code.compiler_options(ignore_module_conflict: true)
defmodule Elixir.Sample do
defmodule Sample do
def foo(), do: 0
def foobar(), do: 0
end
assert expand('Sample.foo') == {:yes, '', ['foo/0', 'foobar/0']}
assert expand('IEx.AutocompleteTest.Sample.foo') == {:yes, '', ['foo/0', 'foobar/0']}
after
File.rm("Elixir.Sample.beam")
File.rm("Elixir.IEx.AutocompleteTest.Sample.beam")
Code.compiler_options(ignore_module_conflict: false)
:code.purge(Sample)
:code.delete(Sample)
@@ -184,7 +253,12 @@ defmodule IEx.AutocompleteTest do
defstruct [:my_val]
end
test "completion for structs" do
test "completion for struct names" do
assert expand('%IEx.AutocompleteTest.MyStr') == {:yes, 'uct', []}
end
@tag previous_line: "struct = %IEx.AutocompleteTest.MyStruct{}"
test "completion for struct keys" do
assert expand('struct.my') == {:yes, '_val', []}
end
end
+33 -19
View File
@@ -150,6 +150,21 @@ defmodule Logger.TranslatorTest do
"""s
end
test "translates Task async_stream crashes with neighbour" do
fun = fn -> Task.async_stream([:oops], :erlang, :error, []) |> Enum.to_list() end
{:ok, pid} = Task.start(__MODULE__, :task, [self(), fun])
assert capture_log(:debug, fn ->
ref = Process.monitor(pid)
send(pid, :go)
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
end) =~ ~r"""
Neighbours:
#{inspect pid}
Initial Call: Logger\.TranslatorTest\.task/2
"""
end
test "translates Task undef module crash" do
assert capture_log(fn ->
{:ok, pid} = Task.start(:module_does_not_exist, :undef, [])
@@ -244,24 +259,6 @@ defmodule Logger.TranslatorTest do
"""
end
test "translates :proc_lib crashes" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
assert capture_log(:info, fn ->
ref = Process.monitor(pid)
send(pid, :message)
send(pid, :go)
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
end) =~ ~r"""
\[error\] Process #PID<\d+\.\d+\.\d+> terminating
\*\* \(exit\) an exception was raised:
\*\* \(RuntimeError\) oops
.*
Initial Call: Logger.TranslatorTest.task/1
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
"""s
end
test "translates Process crashes" do
assert capture_log(:info, fn ->
{_, ref} = spawn_monitor(fn() -> raise "oops" end)
@@ -275,6 +272,24 @@ defmodule Logger.TranslatorTest do
"""
end
test "translates :proc_lib crashes" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
assert capture_log(:info, fn ->
ref = Process.monitor(pid)
send(pid, :go)
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
end) =~ ~r"""
\[error\] Process #PID<\d+\.\d+\.\d+> terminating
\*\* \(exit\) an exception was raised:
\*\* \(RuntimeError\) oops
.*
Initial Call: Logger.TranslatorTest.task/1
Ancestors: \[#PID<\d+\.\d+\.\d+>\]
"""s
end
test "translates :proc_lib crashes with name" do
{:ok, pid} = Task.start_link(__MODULE__, :task,
[self(), fn() ->
@@ -648,5 +663,4 @@ defmodule Logger.TranslatorTest do
:proc_lib.init_ack({:ok, self()})
receive do: ({:EXIT, _, _} -> exit(:stop))
end
end
+4
View File
@@ -36,6 +36,10 @@ defmodule Mix.Compilers.Test do
Mix.shell.info "No stale tests."
:noop
[] when test_patterns == [] ->
Mix.shell.info "There are no tests to run"
:noop
[] ->
Mix.shell.error "Test patterns did not match any file: " <> Enum.join(test_patterns, ", ")
:noop
+18 -14
View File
@@ -264,11 +264,13 @@ defmodule Mix.Dep.Loader do
{dep, deps}
end
defp rebar_dep(%Mix.Dep{app: app, opts: opts} = dep, children, manager) do
config = File.cd!(opts[:dest], fn ->
Mix.Rebar.load_config(".")
end)
extra = Mix.Rebar.merge_config(dep.extra, config)
defp rebar_dep(%Mix.Dep{app: app, opts: opts, extra: overrides} = dep, children, manager) do
config =
File.cd!(opts[:dest], fn -> Mix.Rebar.load_config(".") end)
config =
Mix.Rebar.apply_overrides(app, config, overrides)
deps =
if children do
from = Path.join(opts[:dest], "rebar.config")
@@ -277,9 +279,10 @@ defmodule Mix.Dep.Loader do
# files in the dependency
Enum.map(children, &to_dep(&1, from, manager))
else
rebar_children(app, config, extra, manager)
rebar_children(config, manager, opts[:dest])
end
{%{dep | extra: extra}, deps}
{%{dep | extra: config}, deps}
end
defp make_dep(dep) do
@@ -301,17 +304,18 @@ defmodule Mix.Dep.Loader do
|> elem(0)
end
defp rebar_children(app, root_config, extra, manager) do
from = Path.absname("rebar.config")
defp rebar_children(root_config, manager, dest) do
from = Path.absname(Path.join(dest, "rebar.config"))
Mix.Rebar.recur(root_config, fn config ->
app
|> Mix.Rebar.deps(config, overrides(manager, extra))
|> Enum.map(fn dep -> %{to_dep(dep, from, manager) | extra: extra} end)
overrides = overrides(manager, config)
config
|> Mix.Rebar.deps()
|> Enum.map(fn dep -> %{to_dep(dep, from, manager) | extra: overrides} end)
end) |> Enum.concat
end
defp overrides(:rebar3, extra), do: extra[:overrides] || []
defp overrides(_, _extra), do: []
defp overrides(:rebar3, config), do: config[:overrides] || []
defp overrides(_, _config), do: []
defp validate_app(%Mix.Dep{opts: opts, requirement: req, app: app} = dep) do
opts_app = opts[:app]
+59 -146
View File
@@ -61,108 +61,6 @@ defmodule Mix.Rebar do
end
end
@doc """
Merges a rebar3 parent config with a child config.
"""
# From https://github.com/rebar/rebar3/blob/b1da2ec0674df89599564252734bd4d794436425/src/rebar_opts.erl#L103
def merge_config(old, new) do
Keyword.merge(old, new, fn
:deps, old, _new -> old
{:deps, _}, _old, new -> new
:plugins, _old, new -> new
{:plugins, _}, _old, new -> new
:profiles, old, new -> merge_config(old, new)
:mib_first_files, value, value -> value
:mib_first_files, old, new -> old ++ new
:relx, old, new -> tuple_merge(new, old)
_key, old, new when is_list(new) ->
case :io_lib.printable_list(new) do
true when new == [] ->
if :io_lib.printable_list(old), do: new, else: old
true ->
new
false ->
tuple_merge(old, new)
end
_key, _old, new -> new
end)
end
# From https://github.com/rebar/rebar3/blob/b1da2ec0674df89599564252734bd4d794436425/src/rebar_utils.erl#L282
defp tuple_merge(old, new),
do: do_tuple_merge(tuple_sort(old), tuple_sort(new))
defp do_tuple_merge(old, []),
do: old
defp do_tuple_merge(olds, [new | news]),
do: do_tuple_umerge_dedup(umerge(:new, olds, [], news, new), [])
defp umerge(_, [], [], acc, current),
do: [current | acc]
defp umerge(:new, [], news, acc, current),
do: Enum.reverse(news, [current | acc])
defp umerge(:old, olds, [], acc, current),
do: Enum.reverse(olds, [current | acc])
defp umerge(:new, [old | olds], news, acc, current) do
{dir, merged, new_current} = compare({:new, current}, {:old, old})
umerge(dir, olds, news, [merged | acc], new_current)
end
defp umerge(:old, olds, [new | news], acc, current) do
{dir, merged, new_current} = compare({:new, new}, {:old, current})
umerge(dir, olds, news, [merged | acc], new_current)
end
defp compare({priority, a}, {secondary, b}) when is_tuple(a) and is_tuple(b) do
ka = elem(a, 0)
kb = elem(b, 0)
cond do
ka == kb -> {secondary, a, b}
ka < kb -> {secondary, a, b}
ka > kb -> {priority, b, a}
end
end
defp compare({priority, a}, {secondary, b}) when not is_tuple(a) and not is_tuple(b) do
cond do
a == b -> {secondary, a, b}
a < b -> {secondary, a, b}
a > b -> {priority, b, a}
end
end
defp compare({priority, a}, {secondary, b}) when is_tuple(a) and not is_tuple(b) do
ka = elem(a, 0)
cond do
ka == b -> {secondary, a, b}
ka < b -> {secondary, a, b}
ka > b -> {priority, b, a}
end
end
defp compare({priority, a}, {secondary, b}) when not is_tuple(a) and is_tuple(b) do
kb = elem(b, 0)
cond do
a == kb -> {secondary, a, b}
a < kb -> {secondary, a, b}
a > kb -> {priority, b, a}
end
end
defp do_tuple_umerge_dedup([], acc), do: acc
defp do_tuple_umerge_dedup([h | t], acc) do
if h in t do
do_tuple_umerge_dedup(t, acc)
else
do_tuple_umerge_dedup(t, [h | acc])
end
end
defp tuple_sort(list) do
Enum.sort(list, fn
a, b when is_tuple(a) and is_tuple(b) -> elem(a, 0) <= elem(b, 0)
a, b when is_tuple(a) -> elem(a, 0) <= b
a, b when is_tuple(b) -> a <= elem(b, 0)
a, b -> a <= b
end)
end
@doc """
Serializes a Rebar config to a term file.
"""
@@ -178,7 +76,7 @@ defmodule Mix.Rebar do
def dependency_config(config) do
Enum.map(config, fn
{:erl_opts, opts} ->
{:erl_opts, Enum.reject(opts, &(&1 == :warnings_as_errors))}
{:erl_opts, List.delete(opts, :warnings_as_errors)}
other ->
other
end)
@@ -187,10 +85,9 @@ defmodule Mix.Rebar do
@doc """
Parses the dependencies in given `rebar.config` to Mix's dependency format.
"""
def deps(app, config, overrides) do
def deps(config) do
# We don't have to handle rebar3 profiles because dependencies
# are always in the default profile which cannot be customized
config = apply_overrides(app, config, overrides)
if deps = config[:deps] do
Enum.map(deps, &parse_dep/1)
else
@@ -201,23 +98,23 @@ defmodule Mix.Rebar do
@doc """
Runs `fun` for the given config and for each `sub_dirs` in the
given Rebar config.
"""
def recur(config, fun) when is_binary(config) do
recur(load_config(config), fun)
end
`sub_dirs` is only supported in Rebar 2. In Rebar 3, the equivalent
to umbrella apps cannot be used as dependencies, so we don't need
to worry about such cases in Mix.
"""
def recur(config, fun) do
subs =
(config[:sub_dirs] || [])
|> Enum.map(&Path.wildcard(&1))
|> Enum.concat
|> Enum.flat_map(&Path.wildcard(&1))
|> Enum.filter(&File.dir?(&1))
|> Enum.map(&recur(&1, fun))
|> Enum.concat
|> Enum.flat_map(&recur(load_config(&1), fun))
[fun.(config) | subs]
end
# Translate a rebar dependency declaration to a mix declaration
# From http://www.rebar3.org/docs/dependencies#section-declaring-dependencies
defp parse_dep(app) when is_atom(app) do
{app, ">= 0.0.0"}
end
@@ -226,10 +123,6 @@ defmodule Mix.Rebar do
{app, List.to_string(req)}
end
defp parse_dep({app, req, {:pkg, package}}) when is_list(req) do
{app, List.to_string(req), hex: package}
end
defp parse_dep({app, source}) when is_tuple(source) do
parse_dep({app, nil, source, []})
end
@@ -239,25 +132,33 @@ defmodule Mix.Rebar do
end
defp parse_dep({app, req, source, opts}) do
[scm, url | source] = Tuple.to_list(source)
ref =
case source do
["" | _] -> [branch: "HEAD"]
[{:branch, branch} | _] -> [branch: to_string(branch)]
[{:tag, tag} | _] -> [tag: to_string(tag)]
[{:ref, ref} | _] -> [ref: to_string(ref)]
[ref | _] -> [ref: to_string(ref)]
_ -> []
end
source = parse_source(source)
compile =
if :proplists.get_value(:raw, opts, false),
do: [compile: false],
else: []
mix_opts = [{scm, to_string(url)}] ++ ref ++ compile
{app, compile_req(req), mix_opts}
{app, compile_req(req), source ++ compile}
end
defp parse_source({:pkg, pkg}) do
[hex: pkg]
end
defp parse_source(source) do
[scm, url | source] = Tuple.to_list(source)
ref =
case source do
["" | _] -> [branch: "HEAD"]
[{:branch, branch} | _] -> [branch: to_string(branch)]
[{:tag, tag} | _] -> [tag: to_string(tag)]
[{:ref, ref} | _] -> [ref: to_string(ref)]
[ref | _] -> [ref: to_string(ref)]
_ -> []
end
[{scm, to_string(url)}] ++ ref
end
defp compile_req(nil) do
@@ -265,11 +166,18 @@ defmodule Mix.Rebar do
end
defp compile_req(req) do
case Regex.compile(List.to_string(req)) do
{:ok, re} ->
re
{:error, reason} ->
Mix.raise "Unable to compile version regex: #{inspect req}, #{reason}"
req = List.to_string(req)
case Version.parse_requirement(req) do
{:ok, _} ->
req
:error ->
case Regex.compile(req) do
{:ok, re} ->
re
{:error, reason} ->
Mix.raise "Unable to compile version regex: #{inspect req}, #{reason}"
end
end
end
@@ -312,9 +220,11 @@ defmodule Mix.Rebar do
end
end
defp apply_overrides(app, config, overrides) do
@doc """
Applies the given overrides for app config.
"""
def apply_overrides(app, config, overrides) do
# Inefficient. We want the order we get here though.
config =
Enum.reduce(overrides, config, fn
{:override, overrides}, config ->
@@ -327,7 +237,7 @@ defmodule Mix.Rebar do
config =
Enum.reduce(overrides, config, fn
{:override, oapp, overrides}, config when oapp == app ->
{:override, ^app, overrides}, config ->
Enum.reduce(overrides, config, fn {key, value}, config ->
Keyword.put(config, key, value)
end)
@@ -335,14 +245,17 @@ defmodule Mix.Rebar do
config
end)
Enum.reduce(overrides, config, fn
{:add, oapp, overrides}, config when oapp == app ->
Enum.reduce(overrides, config, fn {key, value}, config ->
old_value = Keyword.get(config, key, [])
Keyword.put(config, key, value ++ old_value)
config =
Enum.reduce(overrides, config, fn
{:add, ^app, overrides}, config ->
Enum.reduce(overrides, config, fn {key, value}, config ->
old_value = Keyword.get(config, key, [])
Keyword.put(config, key, value ++ old_value)
end)
_, config ->
config
end)
_, config ->
config
end)
Keyword.update(config, :overrides, overrides, &(overrides ++ &1))
end
end
+14 -4
View File
@@ -7,14 +7,24 @@ defmodule Mix.Tasks.Archive.Build do
Builds an archive according to the specification of the
[Erlang Archive Format](http://www.erlang.org/doc/man/code.html).
Archives are meant to contain small projects, usually installed
locally. Archives may be installed into a Mix environment by
running `mix archive.install`. Once installed, the archive is
available to all Mix projects. For this reason, the functionality
behind archives is limited. For instance, archives do not include
dependencies, as those would conflict with any dependency in a
Mix project after the archive is installed. In general, we recommend
the usage of archives to be limited for extensions of Mix, such
as custom SCMs, package managers, etc. For general scripts to be
installed into machines, please see `mix escript.build`.
The archive will be created in the current directory (which is
expected to be the project root), unless an argument `-o` is
provided with the file name.
Archives are meant to contain small projects, usually installed
locally. By default, this command archives the current project
but the `-i` and `-o` options can be used to archive any directory.
For example, `mix archive.build` with no options translates to:
By default, this command archives the current project but the `-i`
option can be used to archive any directory. For example,
`mix archive.build` with no options translates to:
mix archive.build -i _build/ENV/lib/APP -o APP-VERSION.ez
+6 -5
View File
@@ -6,14 +6,15 @@ defmodule Mix.Tasks.Archive.Install do
@moduledoc """
Installs an archive locally.
If no argument is supplied but there is an archive in the project's root directory
(created with `mix archive.build`), then the archive will be installed
locally. For example:
If no argument is supplied but there is an archive in the project's
root directory (created with `mix archive.build`), then the archive
will be installed locally. For example:
mix do archive.build, archive.install
If an argument is provided, it should be a local path or a URL to a prebuilt archive,
a git repository, a github repository, or a hex package.
If an argument is provided, it should be a local path or a URL to a
prebuilt archive, a git repository, a github repository, or a hex
package.
mix archive.install archive.ez
mix archive.install path/to/archive.ez
+1 -1
View File
@@ -18,7 +18,7 @@ defmodule Mix.Tasks.Compile.App do
function in your `mix.exs` with the following options:
* `:applications` - all applications your application depends
on at runtime. By default, this list is automatically inflected
on at runtime. By default, this list is automatically inferred
from your dependencies. Any extra Erlang/Elixir dependency
must be specified in `:extra_applications`. Mix and other tools
use the application list in order to start your dependencies
+25 -7
View File
@@ -191,19 +191,29 @@ defmodule Mix.Tasks.Deps.Compile do
Mix.raise "\"#{manager}\" installation failed"
end
defp do_make(%{opts: opts} = dep, config) do
os_type = :os.type
defp do_make(dep, config) do
command = make_command(dep)
do_command(dep, config, command, true, [{"IS_DEP", "1"}])
end
defp make_command(dep) do
makefile_win? = makefile_win?(dep)
command =
cond do
match?({:win32, _}, os_type) and File.regular?(Path.join(opts[:dest], "Makefile.win")) ->
case :os.type do
{:win32, _} when makefile_win? ->
"nmake /F Makefile.win"
match?({:unix, type} when type in [:freebsd, :openbsd], os_type) ->
{:unix, type} when type in [:freebsd, :openbsd] ->
"gmake"
true ->
_ ->
"make"
end
do_command(dep, config, command, true, [{"IS_DEP", "1"}])
if erlang_mk?(dep) do
"#{command} clean && #{command}"
else
command
end
end
defp do_compile(%Mix.Dep{opts: opts} = dep, config) do
@@ -248,4 +258,12 @@ defmodule Mix.Tasks.Deps.Compile do
req
end
end
defp erlang_mk?(%Mix.Dep{opts: opts}) do
File.regular?(Path.join(opts[:dest], "erlang.mk"))
end
defp makefile_win?(%Mix.Dep{opts: opts}) do
File.regular?(Path.join(opts[:dest], "Makefile.win"))
end
end
+5 -2
View File
@@ -197,10 +197,13 @@ defmodule Mix.Tasks.Escript.Build do
extra_apps =
if function_exported?(mod, :application, 0) do
mod.application[:applications]
app = mod.application()
Keyword.get(app, :applications, []) ++ Keyword.get(app, :extra_applications, [])
else
[]
end
Enum.filter(extra_apps || [], &(&1 in [:eex, :ex_unit, :mix, :iex, :logger]))
Enum.filter(extra_apps, &(&1 in [:eex, :ex_unit, :mix, :iex, :logger]))
end
defp app_files(app) do
+11 -3
View File
@@ -122,8 +122,8 @@ defmodule Mix.Tasks.Test do
## Configuration
* `:test_paths` - list of paths containing test files, defaults to
`["test"]`. It is expected all test paths to contain a `test_helper.exs`
file.
`["test"]` if the `test` directory exists, otherwise it defaults to `[]`.
It is expected all test paths to contain a `test_helper.exs` file.
* `:test_pattern` - a pattern to load test files, defaults to `*_test.exs`.
@@ -225,7 +225,7 @@ defmodule Mix.Tasks.Test do
ex_unit_opts = ex_unit_opts(opts)
ExUnit.configure(ex_unit_opts)
test_paths = project[:test_paths] || ["test"]
test_paths = project[:test_paths] || default_test_paths()
Enum.each(test_paths, &require_test_helper(&1))
ExUnit.configure(merge_helper_opts(ex_unit_opts))
@@ -365,4 +365,12 @@ defmodule Mix.Tasks.Test do
Mix.raise "Cannot run tests because test helper file #{inspect file} does not exist"
end
end
defp default_test_paths do
if File.dir?("test") do
["test"]
else
[]
end
end
end
+5 -1
View File
@@ -138,7 +138,11 @@ defmodule Mix.Utils do
def extract_files(paths, pattern) do
Enum.flat_map(paths, fn path ->
if File.regular?(path), do: [path], else: Path.wildcard("#{path}/**/#{pattern}")
case :elixir_utils.read_file_type(path) do
{:ok, :directory} -> Path.wildcard("#{path}/**/#{pattern}")
{:ok, :regular} -> [path]
_ -> []
end
end) |> Enum.uniq
end
+213 -194
View File
@@ -33,212 +33,231 @@ defmodule Mix.RebarTest do
end
end
test "load Rebar config" do
path = MixTest.Case.fixture_path("rebar_dep")
config = Mix.Rebar.load_config(path)
assert config[:sub_dirs] == ['apps/*']
assert config[:SCRIPT] == 'rebar.config.script'
end
test "execute rebar.config.script on dependency directory" do
path = MixTest.Case.fixture_path("rebar_dep_script")
config = Mix.Rebar.load_config(path)
assert config[:dir] == {:ok, String.to_charlist(path)}
end
test "parse Rebar dependencies" do
config = [deps: [{:git_rebar, '~> 1.0'}]]
assert [{:git_rebar, "~> 1.0"}] ==
Mix.Rebar.deps(:foo, config, [])
config = [deps: [{:git_rebar, '~> 1.0', {:pkg, :rebar_fork}}]]
assert [{:git_rebar, "~> 1.0", hex: :rebar_fork}] ==
Mix.Rebar.deps(:foo, config, [])
config = [deps: [{:git_rebar, '0.1..*', {:git, '../../test/fixtures/git_rebar', :master}}]]
assert [{:git_rebar, ~r"0.1..*", [git: "../../test/fixtures/git_rebar", ref: "master"]}] ==
Mix.Rebar.deps(:foo, config, [])
config = [deps: [{:git_rebar, {:git, '../../test/fixtures/git_rebar', :master}}]]
assert [{:git_rebar, ">= 0.0.0", [git: "../../test/fixtures/git_rebar", ref: "master"]}] ==
Mix.Rebar.deps(:foo, config, [])
config = [deps: [{:git_rebar, '0.1..*', {:git, '../../test/fixtures/git_rebar'}, [:raw]}]]
assert [{:git_rebar, ~r"0.1..*", [git: "../../test/fixtures/git_rebar", compile: false]}] ==
Mix.Rebar.deps(:foo, config, [])
config = [deps: [{:git_rebar, '', {:git, '../../test/fixtures/git_rebar', {:ref, '64691eb'}}}]]
assert [{:git_rebar, ~r"", [git: "../../test/fixtures/git_rebar", ref: "64691eb"]}] ==
Mix.Rebar.deps(:foo, config, [])
config = [deps: {:git_rebar, '~> 2.0'}]
overrides = [{:override, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert [{:git_rebar, "~> 1.0"}] ==
Mix.Rebar.deps(:foo, config, overrides)
config = [deps: [{:git_rebar, '~> 2.0'}]]
overrides = [{:override, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert [{:git_rebar, "~> 1.0"}] ==
Mix.Rebar.deps(:foo, config, overrides)
config = [deps: [{:git_rebar, '~> 2.0'}]]
overrides = [{:override, :bar, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert [{:git_rebar, "~> 2.0"}] ==
Mix.Rebar.deps(:foo, config, overrides)
config = [deps: [{:git_rebar, '~> 2.0'}]]
overrides = [{:override, :foo, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert [{:git_rebar, "~> 1.0"}] ==
Mix.Rebar.deps(:foo, config, overrides)
config = [deps: [{:git_rebar, '~> 1.0'}]]
overrides = [{:add, :foo, [deps: [{:git_rebar2, '~> 2.0'}]]}]
assert [{:git_rebar2, "~> 2.0"}, {:git_rebar, "~> 1.0"}] ==
Mix.Rebar.deps(:foo, config, overrides)
end
test "convert rebar config to dependency config" do
config = Mix.Rebar.load_config(fixture_path("rebar_dep"))
dep_config = Mix.Rebar.dependency_config(config)
assert config[:erl_opts] == [:warnings_as_errors]
assert dep_config[:erl_opts] == []
end
test "parse Rebar dependencies from rebar.config" do
Mix.Project.push(RebarAsDep)
deps = Mix.Dep.loaded([])
assert Enum.find(deps, &(&1.app == :rebar_dep))
assert Enum.find(deps, fn %Mix.Dep{app: app, opts: opts} ->
if app == :git_rebar do
assert Enum.find(opts, &match?({:git, _}, &1))
assert Enum.find(opts, &match?({:ref, "master"}, &1))
true
end
end)
end
test "inherit Rebar manager" do
Mix.Project.push(Rebar3AsDep)
deps = Mix.Dep.loaded([])
assert Enum.all?(deps, &(&1.manager == :rebar3))
end
test "Rebar overrides" do
Mix.Project.push(RebarOverrideAsDep)
in_tmp "Rebar overrides", fn ->
Mix.Tasks.Deps.Get.run []
assert Mix.Dep.loaded([]) |> Enum.map(& &1.app) ==
[:git_repo, :git_rebar, :rebar_override]
describe "load_config/1" do
test "loads rebar.config" do
path = MixTest.Case.fixture_path("rebar_dep")
config = Mix.Rebar.load_config(path)
assert config[:sub_dirs] == ['apps/*']
assert config[:SCRIPT] == 'rebar.config.script'
end
test "loads rebar.config.script on dependency directory" do
path = MixTest.Case.fixture_path("rebar_dep_script")
config = Mix.Rebar.load_config(path)
assert config[:dir] == {:ok, String.to_charlist(path)}
end
after
purge [GitRepo.Mixfile]
end
test "recurs over sub dirs" do
path = MixTest.Case.fixture_path("rebar_dep")
describe "deps/1" do
test "parses Rebar dependencies" do
config = [deps: [{:git_rebar, '~> 1.0'}]]
assert [{:git_rebar, "~> 1.0"}] ==
Mix.Rebar.deps(config)
File.cd! path, fn ->
config = Mix.Rebar.load_config(path)
config = [deps: [{:git_rebar, '~> 1.0', {:pkg, :rebar_fork}}]]
assert [{:git_rebar, "~> 1.0", hex: :rebar_fork}] ==
Mix.Rebar.deps(config)
Mix.Rebar.recur(config, fn config ->
if config[:sub_dirs] == ['from_apps_another'] do
Process.put(:inside_apps_another, true)
config = [deps: [{:git_rebar, {:pkg, :rebar_fork}}]]
assert [{:git_rebar, ">= 0.0.0", hex: :rebar_fork}] ==
Mix.Rebar.deps(config)
config = [deps: [{:git_rebar, '0.1..*', {:git, '../../test/fixtures/git_rebar', :master}}]]
assert [{:git_rebar, ~r"0.1..*", [git: "../../test/fixtures/git_rebar", ref: "master"]}] ==
Mix.Rebar.deps(config)
config = [deps: [{:git_rebar, {:git, '../../test/fixtures/git_rebar', :master}}]]
assert [{:git_rebar, ">= 0.0.0", [git: "../../test/fixtures/git_rebar", ref: "master"]}] ==
Mix.Rebar.deps(config)
config = [deps: [{:git_rebar, '0.1..*', {:git, '../../test/fixtures/git_rebar'}, [:raw]}]]
assert [{:git_rebar, ~r"0.1..*", [git: "../../test/fixtures/git_rebar", compile: false]}] ==
Mix.Rebar.deps(config)
config = [deps: [{:git_rebar, '', {:git, '../../test/fixtures/git_rebar', {:ref, '64691eb'}}}]]
assert [{:git_rebar, ~r"", [git: "../../test/fixtures/git_rebar", ref: "64691eb"]}] ==
Mix.Rebar.deps(config)
end
end
describe "apply_overrides/3" do
test "applies overrides" do
config = [deps: {:git_rebar, '~> 2.0'}]
overrides = [{:override, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert Mix.Rebar.apply_overrides(:foo, config, overrides) ==
[deps: [{:git_rebar, '~> 1.0'}], overrides: overrides]
config = [deps: [{:git_rebar, '~> 2.0'}]]
overrides = [{:override, :bar, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert Mix.Rebar.apply_overrides(:foo, config, overrides) ==
[deps: [{:git_rebar, '~> 2.0'}], overrides: overrides]
config = [deps: [{:git_rebar, '~> 2.0'}]]
overrides = [{:override, :foo, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert Mix.Rebar.apply_overrides(:foo, config, overrides) ==
[deps: [{:git_rebar, '~> 1.0'}], overrides: overrides]
config = [deps: [{:git_rebar, '~> 1.0'}]]
overrides = [{:add, :foo, [deps: [{:git_rebar2, '~> 2.0'}]]}]
assert Mix.Rebar.apply_overrides(:foo, config, overrides) ==
[deps: [{:git_rebar2, '~> 2.0'}, {:git_rebar, '~> 1.0'}], overrides: overrides]
end
test "concatenates overrides" do
config = [deps: {:git_rebar, '~> 2.0'}, overrides: [{:add, :bar, []}]]
overrides = [{:override, [deps: [{:git_rebar, '~> 1.0'}]]}]
assert Mix.Rebar.apply_overrides(:foo, config, overrides) ==
[deps: [{:git_rebar, '~> 1.0'}], overrides: overrides ++ [{:add, :bar, []}]]
end
end
describe "dependency_config/1" do
test "converts Rebar config to dependency config" do
config = Mix.Rebar.load_config(fixture_path("rebar_dep"))
dep_config = Mix.Rebar.dependency_config(config)
assert config[:erl_opts] == [:warnings_as_errors]
assert dep_config[:erl_opts] == []
end
end
describe "recur/1" do
test "recurs over sub dirs" do
path = MixTest.Case.fixture_path("rebar_dep")
File.cd! path, fn ->
config = Mix.Rebar.load_config(path)
Mix.Rebar.recur(config, fn config ->
if config[:sub_dirs] == ['from_apps_another'] do
Process.put(:inside_apps_another, true)
end
end)
end
unless Process.get(:inside_apps_another) do
flunk "Expected inside_apps_another to return true"
end
end
end
describe "integration with Mix" do
test "inherits Rebar manager" do
Mix.Project.push(Rebar3AsDep)
deps = Mix.Dep.loaded([])
assert Enum.all?(deps, &(&1.manager == :rebar3))
end
test "parses Rebar dependencies from rebar.config" do
Mix.Project.push(RebarAsDep)
deps = Mix.Dep.loaded([])
assert Enum.find(deps, &(&1.app == :rebar_dep))
assert Enum.find(deps, fn %Mix.Dep{app: app, opts: opts} ->
if app == :git_rebar do
assert Enum.find(opts, &match?({:git, _}, &1))
assert Enum.find(opts, &match?({:ref, "master"}, &1))
true
end
end)
end
unless Process.get(:inside_apps_another) do
flunk "Expected inside_apps_another to return true"
end
end
test "handles Rebar overrides" do
Mix.Project.push(RebarOverrideAsDep)
test "get and compile dependencies for Rebar" do
Mix.Project.push(RebarAsDep)
in_tmp "get and compile dependencies for Rebar", fn ->
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"]}
assert :git_rebar.any_function == :ok
assert :rebar_dep.any_function == :ok
load_paths = Mix.Dep.loaded([])
|> Enum.map(&Mix.Dep.load_paths(&1))
|> Enum.concat
assert File.exists?("_build/dev/lib/rebar_dep/ebin/rebar_dep.beam")
assert File.exists?("_build/dev/lib/git_rebar/ebin/git_rebar.beam")
# Assert we have no .compile.lock as a .compile.lock
# means we check for the Elixir version on every command.
refute File.exists?("_build/dev/lib/rebar_dep/.compile.lock")
refute File.exists?("_build/dev/lib/git_rebar/.compile.lock")
assert Enum.any?(load_paths, &String.ends_with?(&1, "git_rebar/ebin"))
assert Enum.any?(load_paths, &String.ends_with?(&1, "rebar_dep/ebin"))
end
end
test "get and compile dependencies for rebar3" do
Mix.Project.push(Rebar3AsDep)
in_tmp "get and compile dependencies for rebar3", fn ->
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"]}
assert :git_rebar.any_function == :ok
assert :rebar_dep.any_function == :ok
load_paths = Mix.Dep.loaded([])
|> Enum.map(&Mix.Dep.load_paths(&1))
|> Enum.concat
assert File.exists?("_build/dev/lib/rebar_dep/ebin/rebar_dep.beam")
assert File.exists?("_build/dev/lib/git_rebar/ebin/git_rebar.beam")
# Assert we have no .compile.lock as a .compile.lock
# means we check for the Elixir version on every command.
refute File.exists?("_build/dev/lib/rebar_dep/.compile.lock")
refute File.exists?("_build/dev/lib/git_rebar/.compile.lock")
assert Enum.any?(load_paths, &String.ends_with?(&1, "git_rebar/ebin"))
assert Enum.any?(load_paths, &String.ends_with?(&1, "rebar_dep/ebin"))
end
end
test "get and compile dependencies for Rebar with Mix" do
Mix.Project.push(RebarAsDep)
in_tmp "get and compile dependencies for Rebar with Mix", fn ->
File.write! MixTest.Case.tmp_path("rebar_dep/mix.exs"), """
defmodule RebarDep.Mixfile do
use Mix.Project
def project do
[app: :rebar_dep,
version: "0.0.1"]
end
in_tmp "Rebar overrides", fn ->
Mix.Tasks.Deps.Get.run []
assert Mix.Dep.loaded([]) |> Enum.map(& &1.app) ==
[:git_repo, :git_rebar, :rebar_override]
end
"""
after
purge [GitRepo.Mixfile]
end
Mix.Tasks.Deps.Compile.run []
assert_received {:mix_shell, :info, ["==> rebar_dep"]}
assert_received {:mix_shell, :info, ["Generated rebar_dep app"]}
assert File.regular?("_build/dev/lib/rebar_dep/ebin/rebar_dep.app")
test "gets and compiles dependencies for Rebar" do
Mix.Project.push(RebarAsDep)
in_tmp "get and compile dependencies for Rebar", fn ->
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"]}
assert :git_rebar.any_function == :ok
assert :rebar_dep.any_function == :ok
load_paths = Mix.Dep.loaded([])
|> Enum.map(&Mix.Dep.load_paths(&1))
|> Enum.concat
assert File.exists?("_build/dev/lib/rebar_dep/ebin/rebar_dep.beam")
assert File.exists?("_build/dev/lib/git_rebar/ebin/git_rebar.beam")
# Assert we have no .compile.lock as a .compile.lock
# means we check for the Elixir version on every command.
refute File.exists?("_build/dev/lib/rebar_dep/.compile.lock")
refute File.exists?("_build/dev/lib/git_rebar/.compile.lock")
assert Enum.any?(load_paths, &String.ends_with?(&1, "git_rebar/ebin"))
assert Enum.any?(load_paths, &String.ends_with?(&1, "rebar_dep/ebin"))
end
end
test "gets and compiles dependencies for rebar3" do
Mix.Project.push(Rebar3AsDep)
in_tmp "get and compile dependencies for rebar3", fn ->
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"]}
assert :git_rebar.any_function == :ok
assert :rebar_dep.any_function == :ok
load_paths = Mix.Dep.loaded([])
|> Enum.map(&Mix.Dep.load_paths(&1))
|> Enum.concat
assert File.exists?("_build/dev/lib/rebar_dep/ebin/rebar_dep.beam")
assert File.exists?("_build/dev/lib/git_rebar/ebin/git_rebar.beam")
# Assert we have no .compile.lock as a .compile.lock
# means we check for the Elixir version on every command.
refute File.exists?("_build/dev/lib/rebar_dep/.compile.lock")
refute File.exists?("_build/dev/lib/git_rebar/.compile.lock")
assert Enum.any?(load_paths, &String.ends_with?(&1, "git_rebar/ebin"))
assert Enum.any?(load_paths, &String.ends_with?(&1, "rebar_dep/ebin"))
end
end
test "gets and compiles dependencies for Rebar with Mix" do
Mix.Project.push(RebarAsDep)
in_tmp "get and compile dependencies for Rebar with Mix", fn ->
File.write! MixTest.Case.tmp_path("rebar_dep/mix.exs"), """
defmodule RebarDep.Mixfile do
use Mix.Project
def project do
[app: :rebar_dep,
version: "0.0.1"]
end
end
"""
Mix.Tasks.Deps.Compile.run []
assert_received {:mix_shell, :info, ["==> rebar_dep"]}
assert_received {:mix_shell, :info, ["Generated rebar_dep app"]}
assert File.regular?("_build/dev/lib/rebar_dep/ebin/rebar_dep.app")
end
after
File.rm MixTest.Case.tmp_path("rebar_dep/mix.exs")
end
after
File.rm MixTest.Case.tmp_path("rebar_dep/mix.exs")
end
end
+1 -1
View File
@@ -85,7 +85,7 @@ defmodule Mix.Tasks.Compile.AppTest do
end
end
test "automatically inflects applications" do
test "automatically infers applications" do
Mix.Project.push CustomDeps
in_fixture "no_mixfile", fn ->
+1 -1
View File
@@ -47,7 +47,7 @@ defmodule Mix.Tasks.EscriptTest do
end
def application do
[applications: []]
[applications: [], extra_applications: [:crypto]]
end
end
+13 -1
View File
@@ -108,6 +108,14 @@ defmodule Mix.Tasks.TestTest do
end
end
test "logs test absence for a project with no test paths" do
in_fixture "test_stale", fn ->
File.rm_rf! "test"
assert_run_output "There are no tests to run"
end
end
test "--listen-on-stdin: runs tests after input" do
in_fixture "test_stale", fn ->
port = mix_port(~w[test --stale --listen-on-stdin])
@@ -190,6 +198,10 @@ defmodule Mix.Tasks.TestTest do
end
defp assert_stale_run_output(opts \\ [], expected) do
assert mix(~w[test --stale] ++ opts) =~ expected
assert_run_output(["--stale" | opts], expected)
end
defp assert_run_output(opts \\ [], expected) do
assert mix(["test" | opts]) =~ expected
end
end
+5
View File
@@ -33,6 +33,11 @@ defmodule Mix.UtilsTest do
assert Path.basename(hd(files)) == "local.sample.ex"
end
test "extract files with empty string returns empty list" do
assert Mix.Utils.extract_files([""], ".ex") == []
end
test "extract stale" do
time = {{2030, 1, 1}, {0, 0, 0}}
assert Mix.Utils.extract_stale([__ENV__.file], [time]) == []
+1 -1
View File
@@ -1,6 +1,6 @@
{application, elixir,
[{description, "elixir"},
{vsn, "1.4.0-rc.0"},
{vsn, "1.4.1"},
{modules, [
elixir
]},