Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbf5e2bc7b | ||
|
|
ceafd0cdab | ||
|
|
87710cd495 | ||
|
|
6e2c29c9d4 | ||
|
|
f7b32487c0 | ||
|
|
eedc9bc757 | ||
|
|
f053df848f | ||
|
|
0caa75760b | ||
|
|
87da507c24 | ||
|
|
f73e6f8711 | ||
|
|
3610d59214 | ||
|
|
6243fb2755 | ||
|
|
7c5abe35e6 | ||
|
|
fc8314508f | ||
|
|
091ce84e18 | ||
|
|
bbce69b58e | ||
|
|
56779fc25b | ||
|
|
13b432c48d | ||
|
|
effb685854 | ||
|
|
a51c1beb77 | ||
|
|
8a226218e0 | ||
|
|
a9559ef045 | ||
|
|
cb4310111b | ||
|
|
46cab56dfd | ||
|
|
7aebe55236 | ||
|
|
8aab3ce628 | ||
|
|
59d6c803df | ||
|
|
72eb5c73a5 | ||
|
|
982aba3519 |
+58
-1
@@ -23,7 +23,7 @@ def format(message, opts) do
|
||||
end
|
||||
```
|
||||
|
||||
The `if` block above is implicitly changing the value in `message`. Now imagine we want to move the `if` block to its own function to clean up the implementation:
|
||||
The `if` block above is implicitly rebinding the outer scope variable `message` to a new value. Now imagine we want to move the `if` block to its own function to clean up the implementation:
|
||||
|
||||
```elixir
|
||||
def format(message, opts) do
|
||||
@@ -268,6 +268,59 @@ end
|
||||
|
||||
By restricting hierarchies in favor of named setups, it is straight-forward for the developer to glance at each describe block and know exactly the setup steps involved.
|
||||
|
||||
## v1.3.4 (2016-10-09)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Ensure the compiler does not generate unecessary variable bindings inside case statements. This improves the code emitted and make sure "unused variable warnings" are not mistakenly silenced
|
||||
* [Kernel] Move `raise` checks to runtime to avoid crashing cover on Erlang 19.1
|
||||
* [Protocol] Do not emit warnings when using protocols on opaque types
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.CaptureLog] Flush Erlang's `:error_logger` before capturing to avoid mixed messages
|
||||
|
||||
## v1.3.3 (2016-09-17)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [DateTime] Support negative integer in `DateTime.from_unix/2`
|
||||
* [Kernel.LexicalTracker] Do not consider remote typespecs as a compile-time dependency
|
||||
* [Kernel.ParallelCompiler] Do not emit deadlock messages when the process is waiting on itself
|
||||
* [Kernel.Typespec] Mark struct update syntax as generated to avoid false positives from dialyzer
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Make ExUnit server timeout configurable
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Use `:ansi_color` if one is available in metadata
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add support for the `:sparse` option in `Mix.SCM.Git`
|
||||
* [Mix] Skip dependendency loading if `MIX_NO_DEPS` is set to `1`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [System] Use `NUL` instead of `/dev/null` on Windows when building `System.build_info`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Autocomplete] Resolves issue with autocompletion on structs not working
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Also store external resources that are not part of the current working directory in compilation manifest
|
||||
* [Mix] Always include the compiled file source in manifests
|
||||
|
||||
## v1.3.2 (2016-07-15)
|
||||
|
||||
### 1. Enhancements
|
||||
@@ -464,3 +517,7 @@ This release deprecates many APIs that have been soft-deprecated in previous Eli
|
||||
* [String] `String.valid_character?/1` is deprecated in favor of `String.valid?/1` with pattern matching
|
||||
* [Task] `Task.find/2` is deprecated in favor of explicit message matching
|
||||
* [URI] Passing a non-map to `URI.decode_query/2` is deprecated
|
||||
|
||||
## v1.2
|
||||
|
||||
The CHANGELOG for v1.2 releases can be found [in the v1.2 branch](https://github.com/elixir-lang/elixir/blob/v1.2/CHANGELOG.md).
|
||||
|
||||
+48
-11
@@ -1004,19 +1004,32 @@ defmodule DateTime do
|
||||
{:ok, %DateTime{calendar: Calendar.ISO, day: 23, hour: 22, microsecond: {211914, 3}, minute: 53,
|
||||
month: 1, second: 43, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
||||
year: 46302, zone_abbr: "UTC"}}
|
||||
|
||||
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
|
||||
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
|
||||
|
||||
iex> DateTime.from_unix(-12345678910)
|
||||
{:ok, %DateTime{calendar: Calendar.ISO, day: 13, hour: 4, microsecond: {0, 0}, minute: 44,
|
||||
month: 10, second: 50, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
||||
year: 1578, zone_abbr: "UTC"}}
|
||||
|
||||
When a Unix time before that moment is passed to `from_unix/2`, `:error` will be returned.
|
||||
"""
|
||||
@spec from_unix(non_neg_integer, :native | System.time_unit) :: {:ok, DateTime.t}
|
||||
def from_unix(integer, unit \\ :seconds) when is_integer(integer) and integer >= 0 do
|
||||
@spec from_unix(integer, :native | System.time_unit) :: {:ok, DateTime.t}
|
||||
def from_unix(integer, unit \\ :seconds) when is_integer(integer) do
|
||||
total = System.convert_time_unit(integer, unit, :microseconds)
|
||||
microsecond = rem(total, 1_000_000)
|
||||
precision = precision_for_unit(unit)
|
||||
{{year, month, day}, {hour, minute, second}} =
|
||||
:calendar.gregorian_seconds_to_datetime(@unix_epoch + div(total, 1_000_000))
|
||||
if total < -@unix_epoch * 1_000_000 do
|
||||
:error
|
||||
else
|
||||
microsecond = rem(total, 1_000_000)
|
||||
precision = precision_for_unit(unit)
|
||||
{{year, month, day}, {hour, minute, second}} =
|
||||
:calendar.gregorian_seconds_to_datetime(@unix_epoch + div(total, 1_000_000))
|
||||
|
||||
{:ok, %DateTime{year: year, month: month, day: day,
|
||||
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision},
|
||||
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
|
||||
{:ok, %DateTime{year: year, month: month, day: day,
|
||||
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision},
|
||||
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
|
||||
end
|
||||
end
|
||||
|
||||
def precision_for_unit(unit) do
|
||||
@@ -1034,6 +1047,13 @@ defmodule DateTime do
|
||||
@doc """
|
||||
Converts the given Unix time to DateTime.
|
||||
|
||||
The integer can be given in different unit
|
||||
according to `System.convert_time_unit/3` and it will
|
||||
be converted to microseconds internally.
|
||||
|
||||
Unix times are always in UTC and therefore the DateTime
|
||||
will be returned in UTC.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.from_unix!(1464096368)
|
||||
@@ -1046,6 +1066,15 @@ defmodule DateTime do
|
||||
month: 5, second: 8, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
||||
year: 2015, zone_abbr: "UTC"}
|
||||
|
||||
Negative Unix times are supported, up to -#{@unix_epoch} seconds,
|
||||
which is equivalent to "0000-01-01T00:00:00Z" or 0 gregorian seconds.
|
||||
|
||||
iex> DateTime.from_unix(-12345678910)
|
||||
{:ok, %DateTime{calendar: Calendar.ISO, day: 13, hour: 4, microsecond: {0, 0}, minute: 44,
|
||||
month: 10, second: 50, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0,
|
||||
year: 1578, zone_abbr: "UTC"}}
|
||||
|
||||
When a Unix time before that moment is passed to `from_unix!/2`, an ArgumentError will be raised.
|
||||
"""
|
||||
@spec from_unix!(non_neg_integer, :native | System.time_unit) :: DateTime.t
|
||||
def from_unix!(integer, unit \\ :seconds) when is_atom(unit) do
|
||||
@@ -1057,7 +1086,7 @@ defmodule DateTime do
|
||||
Converts the given DateTime to Unix time.
|
||||
|
||||
The DateTime is expected to be using the ISO calendar
|
||||
with a year greater than or equal to 1970.
|
||||
with a year greater than or equal to 0.
|
||||
|
||||
It will return the integer with the given unit,
|
||||
according to `System.convert_time_unit/3`.
|
||||
@@ -1073,11 +1102,19 @@ defmodule DateTime do
|
||||
iex> DateTime.to_unix(dt)
|
||||
1416517099
|
||||
|
||||
iex> flamel = %DateTime{calendar: Calendar.ISO, day: 22, hour: 8, microsecond: {527771, 6},
|
||||
...> minute: 2, month: 3, second: 25, std_offset: 0, time_zone: "Etc/UTC",
|
||||
...> utc_offset: 0, year: 1418, zone_abbr: "UTC"}
|
||||
iex> DateTime.to_unix(flamel)
|
||||
-17412508655
|
||||
|
||||
"""
|
||||
@spec to_unix(DateTime.t, System.time_unit) :: non_neg_integer
|
||||
def to_unix(datetime, unit \\ :seconds)
|
||||
|
||||
def to_unix(%DateTime{calendar: Calendar.ISO, std_offset: std_offset, utc_offset: utc_offset,
|
||||
hour: hour, minute: minute, second: second, microsecond: {microsecond, _},
|
||||
year: year, month: month, day: day}, unit \\ :seconds) when year >= 1970 do
|
||||
year: year, month: month, day: day}, unit) when year >= 0 do
|
||||
seconds =
|
||||
:calendar.datetime_to_gregorian_seconds({{year, month, day}, {hour, minute, second}})
|
||||
|> Kernel.-(utc_offset)
|
||||
|
||||
@@ -861,7 +861,7 @@ defmodule Enum do
|
||||
|
||||
def find_index(enumerable, fun) do
|
||||
res =
|
||||
Enumerable.reduce(enumerable, {:cont, {:not_found, 0}}, fn(entry, {status, index}) ->
|
||||
Enumerable.reduce(enumerable, {:cont, {:not_found, 0}}, fn(entry, {_status, index}) ->
|
||||
if fun.(entry), do: {:halt, {:found, index}}, else: {:cont, {:not_found, index + 1}}
|
||||
end)
|
||||
|
||||
|
||||
@@ -1350,31 +1350,9 @@ defmodule Kernel do
|
||||
:erlang.error unquote(alias).exception([])
|
||||
end
|
||||
_ ->
|
||||
generated = fn fun, var ->
|
||||
{fun, [generated: true, line: -1], [{var, [], __MODULE__}]}
|
||||
quote do
|
||||
:erlang.error Kernel.Utils.raise(unquote(msg))
|
||||
end
|
||||
|
||||
{fun, meta, [arg, [do: clauses]]} =
|
||||
quote do
|
||||
case unquote(msg) do
|
||||
msg when unquote(generated.(:is_binary, :msg)) ->
|
||||
:erlang.error RuntimeError.exception(msg)
|
||||
atom when unquote(generated.(:is_atom, :atom)) ->
|
||||
:erlang.error atom.exception([])
|
||||
%{__struct__: struct, __exception__: true} = other when is_atom(struct) ->
|
||||
:erlang.error other
|
||||
other ->
|
||||
message = "raise/1 expects an alias, string or exception as the first argument, got: #{inspect other}"
|
||||
:erlang.error ArgumentError.exception(message)
|
||||
end
|
||||
end
|
||||
|
||||
clauses =
|
||||
:lists.map(fn {:->, meta, args} ->
|
||||
{:->, [generated: true] ++ Keyword.put(meta, :line, -1), args}
|
||||
end, clauses)
|
||||
|
||||
{fun, meta, [arg, [do: clauses]]}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
:erlang.spawn_monitor fn ->
|
||||
# Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
|
||||
:erlang.put(:elixir_compiler_pid, parent)
|
||||
:erlang.put(:elixir_compiler_file, file)
|
||||
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
|
||||
|
||||
exit(try do
|
||||
@@ -206,8 +207,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
{:waiting, kind, child, ref, on, defining} ->
|
||||
# Oops, we already got it, do not put it on waiting.
|
||||
# OR
|
||||
# We're waiting on ourselves, send :found so that we can crash with a better error
|
||||
waiting =
|
||||
if :lists.any(&match?({^kind, ^on}, &1), result) do
|
||||
if :lists.any(&match?({^kind, ^on}, &1), result) or on in defining do
|
||||
send child, {ref, :found}
|
||||
waiting
|
||||
else
|
||||
|
||||
@@ -47,21 +47,22 @@ defmodule Kernel.ParallelRequire do
|
||||
wait_for_messages(files, waiting, callbacks, schedulers, result)
|
||||
end
|
||||
|
||||
defp spawn_requires([h | t], waiting, callbacks, schedulers, result) do
|
||||
defp spawn_requires([file | files], waiting, callbacks, schedulers, result) do
|
||||
parent = self()
|
||||
{pid, ref} = :erlang.spawn_monitor fn ->
|
||||
:erlang.put(:elixir_compiler_pid, parent)
|
||||
:erlang.put(:elixir_compiler_file, file)
|
||||
|
||||
exit(try do
|
||||
new = Code.require_file(h) || []
|
||||
{:required, Enum.map(new, &elem(&1, 0)), h}
|
||||
new = Code.require_file(file) || []
|
||||
{:required, Enum.map(new, &elem(&1, 0)), file}
|
||||
catch
|
||||
kind, reason ->
|
||||
{:failure, kind, reason, System.stacktrace}
|
||||
end)
|
||||
end
|
||||
|
||||
spawn_requires(t, [{pid, ref} | waiting], callbacks, schedulers, result)
|
||||
spawn_requires(files, [{pid, ref} | waiting], callbacks, schedulers, result)
|
||||
end
|
||||
|
||||
defp wait_for_messages(files, waiting, callbacks, schedulers, result) do
|
||||
|
||||
@@ -777,6 +777,8 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
defp typespec({:%, _, [name, {:%{}, meta, fields}]}, vars, caller) do
|
||||
# We cannot set a function name to avoid tracking
|
||||
# as a compile time dependency, because for structs it actually is one.
|
||||
module = Macro.expand(name, caller)
|
||||
|
||||
struct =
|
||||
@@ -813,6 +815,8 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
defp typespec({:record, meta, [atom, fields]}, vars, caller) do
|
||||
# We cannot set a function name to avoid tracking
|
||||
# as a compile time dependency because for records it actually is one.
|
||||
case Macro.expand({atom, [], [{atom, [], []}]}, caller) do
|
||||
keyword when is_list(keyword) ->
|
||||
types =
|
||||
@@ -883,7 +887,9 @@ defmodule Kernel.Typespec do
|
||||
|
||||
# Handle remote calls
|
||||
defp typespec({{:., meta, [remote, name]}, _, args} = orig, vars, caller) do
|
||||
remote = Macro.expand remote, caller
|
||||
# We set a function name to avoid tracking
|
||||
# aliases in typespecs as compile time dependencies.
|
||||
remote = Macro.expand remote, %{caller | function: {:typespec, 0}}
|
||||
unless is_atom(remote) do
|
||||
compile_error(caller, "invalid remote in typespec: #{Macro.to_string(orig)}")
|
||||
end
|
||||
|
||||
@@ -3,6 +3,9 @@ import Kernel, except: [destructure: 2, defdelegate: 2, defstruct: 2]
|
||||
defmodule Kernel.Utils do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Callback for destructure.
|
||||
"""
|
||||
def destructure(list, count) when is_list(list), do: destructure_list(list, count)
|
||||
def destructure(nil, count), do: destructure_nil(count)
|
||||
|
||||
@@ -13,6 +16,9 @@ defmodule Kernel.Utils do
|
||||
defp destructure_nil(0), do: []
|
||||
defp destructure_nil(count), do: [nil | destructure_nil(count - 1)]
|
||||
|
||||
@doc """
|
||||
Callback for defdelegate.
|
||||
"""
|
||||
def defdelegate(fun, opts) do
|
||||
append_first = Keyword.get(opts, :append_first, false)
|
||||
|
||||
@@ -56,6 +62,9 @@ defmodule Kernel.Utils do
|
||||
"defdelegate/2 only accepts function parameters, got: #{Macro.to_string(code)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Callback for defstruct.
|
||||
"""
|
||||
def defstruct(module, fields) do
|
||||
case fields do
|
||||
fs when is_list(fs) ->
|
||||
@@ -85,10 +94,30 @@ defmodule Kernel.Utils do
|
||||
Module.get_attribute(module, :derive)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Announcing callback for defstruct.
|
||||
"""
|
||||
def announce_struct(module) do
|
||||
case :erlang.get(:elixir_compiler_pid) do
|
||||
:undefined -> :ok
|
||||
pid -> send(pid, {:struct_available, module})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Callback for raise.
|
||||
"""
|
||||
def raise(msg) when is_binary(msg) do
|
||||
RuntimeError.exception(msg)
|
||||
end
|
||||
def raise(atom) when is_atom(atom) do
|
||||
atom.exception([])
|
||||
end
|
||||
def raise(%{__struct__: struct, __exception__: true} = exception) when is_atom(struct) do
|
||||
exception
|
||||
end
|
||||
def raise(other) do
|
||||
ArgumentError.exception("raise/1 expects an alias, string or exception as " <>
|
||||
"the first argument, got: #{inspect other}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -451,6 +451,16 @@ defmodule Protocol do
|
||||
end
|
||||
end, builtin)
|
||||
|
||||
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
|
||||
# destructuring opaque types is illegal, Dialyzer will think none of the
|
||||
# previous clauses matches opaque types, and without this clause, will
|
||||
# conclude that impl_for can't handle an opaque argument). This is a hack
|
||||
# since it relies on Dialyzer not being smart enough to conclude that all
|
||||
# opaque types will get the any_impl_for/0 implementation.
|
||||
Kernel.def impl_for(_) do
|
||||
any_impl_for()
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec impl_for!(term) :: atom | no_return
|
||||
Kernel.def impl_for!(data) do
|
||||
|
||||
@@ -350,6 +350,7 @@ defmodule String do
|
||||
defp parts_to_index(:infinity), do: 0
|
||||
defp parts_to_index(n) when is_integer(n) and n > 0, do: n
|
||||
|
||||
defp split_each("", _pattern, true, 1), do: []
|
||||
defp split_each(string, _pattern, _trim, 1) when is_binary(string), do: [string]
|
||||
defp split_each(string, pattern, trim, count) do
|
||||
case do_splitter(string, pattern, trim) do
|
||||
@@ -402,6 +403,7 @@ defmodule String do
|
||||
end
|
||||
|
||||
defp maybe_compile_pattern(""), do: ""
|
||||
defp maybe_compile_pattern(pattern) when is_tuple(pattern), do: pattern
|
||||
defp maybe_compile_pattern(pattern), do: :binary.compile_pattern(pattern)
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -112,7 +112,15 @@ defmodule System do
|
||||
# Tries to run "git rev-parse --short HEAD". In the case of success returns
|
||||
# the short revision hash. If that fails, returns an empty string.
|
||||
defmacrop get_revision do
|
||||
:os.cmd('git rev-parse --short HEAD 2> /dev/null')
|
||||
null =
|
||||
case :os.type do
|
||||
{:win32, _} -> 'NUL'
|
||||
_ -> '/dev/null'
|
||||
end
|
||||
|
||||
'git rev-parse --short HEAD 2> '
|
||||
|> Kernel.++(null)
|
||||
|> :os.cmd()
|
||||
|> strip
|
||||
end
|
||||
|
||||
|
||||
@@ -198,17 +198,17 @@ normalize_vars(Key, {Ref, Counter, _Safe},
|
||||
% Generate match vars by checking if they were updated
|
||||
% or not and assigning the previous value.
|
||||
|
||||
generate_match_vars([{Key, Value, Expr} | T], ClauseVars, Left, Right) ->
|
||||
generate_match_vars([{Key, {Value, _, _}, Expr} | T], ClauseVars, Left, Right) ->
|
||||
case maps:find(Key, ClauseVars) of
|
||||
{ok, Value} ->
|
||||
{ok, {Value, _, _}} ->
|
||||
generate_match_vars(T, ClauseVars, Left, Right);
|
||||
{ok, Clause} ->
|
||||
{ok, {Clause, _, _}} ->
|
||||
generate_match_vars(T, ClauseVars,
|
||||
[{var, 0, element(1, Value)} | Left],
|
||||
[{var, 0, element(1, Clause)} | Right]);
|
||||
[{var, 0, Value} | Left],
|
||||
[{var, 0, Clause} | Right]);
|
||||
error ->
|
||||
generate_match_vars(T, ClauseVars,
|
||||
[{var, 0, element(1, Value)} | Left], [Expr | Right])
|
||||
[{var, 0, Value} | Left], [Expr | Right])
|
||||
end;
|
||||
|
||||
generate_match_vars([], _ClauseVars, Left, Right) ->
|
||||
|
||||
@@ -92,9 +92,9 @@ translate_struct(Meta, Name, {'%{}', MapMeta, Args}, S) ->
|
||||
|
||||
{TMap, TS} = translate_map(MapMeta, Assocs, Var, VS),
|
||||
|
||||
{{'case', Ann, TUpdate, [
|
||||
{{'case', ?generated, TUpdate, [
|
||||
{clause, Ann, [Match], [], [TMap]},
|
||||
{clause, Ann, [Var], [], [elixir_utils:erl_call(Ann, erlang, error, [Error])]}
|
||||
{clause, ?generated, [Var], [], [elixir_utils:erl_call(Ann, erlang, error, [Error])]}
|
||||
]}, TS};
|
||||
match ->
|
||||
translate_map(MapMeta, Assocs ++ [{'__struct__', Name}], nil, US);
|
||||
|
||||
@@ -378,7 +378,7 @@ load_form(Line, Data, Forms, Opts, E) ->
|
||||
ok;
|
||||
PID ->
|
||||
Ref = make_ref(),
|
||||
PID ! {module_available, self(), Ref, ?m(E, file), Module, Binary},
|
||||
PID ! {module_available, self(), Ref, get(elixir_compiler_file), Module, Binary},
|
||||
receive {Ref, ack} -> ok end
|
||||
end;
|
||||
_ ->
|
||||
|
||||
@@ -66,4 +66,25 @@ defmodule DateTimeTest do
|
||||
utc_offset: -12600, std_offset: 3600, time_zone: "Brazil/Manaus"}
|
||||
assert to_string(dt) == "2000-02-29 23:00:07-02:30 BRM Brazil/Manaus"
|
||||
end
|
||||
|
||||
test "from_unix/2 works with Unix times back to 0 Gregorian Seconds" do
|
||||
assert DateTime.from_unix(-62167219200) == {:ok,
|
||||
%DateTime{calendar: Calendar.ISO, day: 1, hour: 0, microsecond: {0, 0},
|
||||
minute: 0, month: 1, second: 0, std_offset: 0, time_zone: "Etc/UTC",
|
||||
utc_offset: 0, year: 0, zone_abbr: "UTC"}}
|
||||
|
||||
assert DateTime.from_unix(-62167219201) == :error
|
||||
end
|
||||
|
||||
test "to_unix/2 works with Unix times back to 0 Gregorian Seconds" do
|
||||
gregorian_0 = %DateTime{calendar: Calendar.ISO, day: 1, hour: 0, microsecond: {0, 0},
|
||||
minute: 0, month: 1, second: 0, std_offset: 0, time_zone: "Etc/UTC",
|
||||
utc_offset: 0, year: 0, zone_abbr: "UTC"}
|
||||
before_gregorian_0 = %DateTime{gregorian_0 | year: -1}
|
||||
|
||||
assert DateTime.to_unix(gregorian_0) == -62167219200
|
||||
assert_raise FunctionClauseError, fn ->
|
||||
DateTime.to_unix(before_gregorian_0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule Dialyzer.ProtocolOpaque do
|
||||
def circus() do
|
||||
Dialyzer.ProtocolOpaque.Entity.speak(Dialyzer.ProtocolOpaque.Duck.new)
|
||||
end
|
||||
end
|
||||
|
||||
defprotocol Dialyzer.ProtocolOpaque.Entity do
|
||||
def speak(entity)
|
||||
end
|
||||
|
||||
defmodule Dialyzer.ProtocolOpaque.Duck do
|
||||
@opaque t :: %__MODULE__{}
|
||||
defstruct feathers: :white_and_grey
|
||||
|
||||
@spec new :: t
|
||||
def new(), do: %__MODULE__{}
|
||||
|
||||
defimpl Dialyzer.ProtocolOpaque.Entity do
|
||||
def speak(%Dialyzer.ProtocolOpaque.Duck{}), do: "Quack!"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
defmodule Dialyzer.StructUpdate do
|
||||
defstruct [:foo]
|
||||
|
||||
def update(%__MODULE__{} = struct) do
|
||||
%__MODULE__{struct | foo: :bar}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
defmodule Undef do
|
||||
def undef do
|
||||
%__MODULE__{}
|
||||
end
|
||||
end
|
||||
@@ -76,6 +76,18 @@ defmodule Kernel.DialyzerTest do
|
||||
assert_dialyze_no_warnings! context
|
||||
end
|
||||
|
||||
test "no warnings on struct update", context do
|
||||
copy_beam! context, Dialyzer.StructUpdate
|
||||
assert_dialyze_no_warnings! context
|
||||
end
|
||||
|
||||
test "no warnings on protocol calls with opaque types", context do
|
||||
copy_beam! context, Dialyzer.ProtocolOpaque
|
||||
copy_beam! context, Dialyzer.ProtocolOpaque.Entity
|
||||
copy_beam! context, Dialyzer.ProtocolOpaque.Duck
|
||||
assert_dialyze_no_warnings! context
|
||||
end
|
||||
|
||||
defp copy_beam!(context, module) do
|
||||
name = "#{module}.beam"
|
||||
File.cp! Path.join(context[:base_dir], name),
|
||||
|
||||
@@ -28,6 +28,13 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "emits struct undefined error when local struct is undefined" do
|
||||
fixtures = [fixture_path("parallel_struct/undef.ex")]
|
||||
assert capture_io(fn ->
|
||||
assert catch_exit(Kernel.ParallelCompiler.files(fixtures)) == {:shutdown, 1}
|
||||
end) =~ "Undef.__struct__/1 is undefined, cannot expand struct Undef"
|
||||
end
|
||||
|
||||
test "does not hang on missing dependencies" do
|
||||
fixtures = [fixture_path("parallel_compiler/bat.ex")]
|
||||
assert capture_io(fn ->
|
||||
|
||||
@@ -66,6 +66,9 @@ defmodule StringTest do
|
||||
assert String.split("abé", "", trim: true, parts: 2) == ["a", "bé"]
|
||||
|
||||
assert String.split("noël", "") == ["n", "o", "ë", "l", ""]
|
||||
|
||||
assert String.split("x-", "-", parts: 2, trim: true) == ["x"]
|
||||
assert String.split("x-x-", "-", parts: 3, trim: true) == ["x", "x"]
|
||||
end
|
||||
|
||||
test "split with regex" do
|
||||
@@ -79,6 +82,14 @@ defmodule StringTest do
|
||||
assert String.split("a,b", ~r{\.}) == ["a,b"]
|
||||
end
|
||||
|
||||
test "split with compiled pattern" do
|
||||
pattern = :binary.compile_pattern("-")
|
||||
|
||||
assert String.split("x-", pattern) == ["x", ""]
|
||||
assert String.split("x-", pattern, parts: 2, trim: true) == ["x"]
|
||||
assert String.split("x-x-", pattern, parts: 3, trim: true) == ["x", "x"]
|
||||
end
|
||||
|
||||
test "splitter" do
|
||||
assert String.splitter("a,b,c", ",") |> Enum.to_list == ["a", "b", "c"]
|
||||
assert String.splitter("a,b", ".") |> Enum.to_list == ["a,b"]
|
||||
|
||||
@@ -181,6 +181,9 @@ defmodule ExUnit do
|
||||
and print them on test failure. Can be overridden for individual tests via
|
||||
`@tag capture_log: false`. Defaults to `false`.
|
||||
|
||||
* `:case_load_timeout` - the timeout to be used when loading a test case.
|
||||
Defaults to `60_000` milliseconds.
|
||||
|
||||
* `:colors` - a keyword list of colors to be used by some formatters.
|
||||
The only option so far is `[enabled: boolean]` which defaults to `IO.ANSI.enabled?/0`
|
||||
|
||||
|
||||
@@ -37,7 +37,15 @@ defmodule ExUnit.CaptureLog do
|
||||
Returns the binary which is the captured output.
|
||||
|
||||
This function mutes the `:console` backend
|
||||
and captures any log messages sent to Logger.
|
||||
and captures any log messages sent to Logger from the calling
|
||||
processes. It is possible to ensure explicit log messages from other
|
||||
processes are captured by waiting for their exit or monitor
|
||||
signal. This does not guarantee to capture VM generated log messages
|
||||
when a process that is spawned using a low level `Kernel` spawn function
|
||||
(e.g. `Kernel.spawn/1`) raises an exception that isn't rescued or a throw
|
||||
that isn't caught. A `Task`, or other OTP process, will send explicit logs
|
||||
that are sent before its exit or monitor signal and will not cause VM
|
||||
generated log messages.
|
||||
|
||||
Note that when the `async` is set to `true`,
|
||||
the messages from another test might be captured.
|
||||
@@ -59,6 +67,7 @@ defmodule ExUnit.CaptureLog do
|
||||
{:ok, string_io} = StringIO.open("")
|
||||
|
||||
try do
|
||||
_ = :gen_event.which_handlers(:error_logger)
|
||||
:ok = add_capture(string_io, opts)
|
||||
ref = ExUnit.CaptureServer.log_capture_on(self())
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
defmodule ExUnit.Server do
|
||||
@moduledoc false
|
||||
@timeout 60_000
|
||||
|
||||
use GenServer
|
||||
|
||||
@@ -21,11 +20,13 @@ defmodule ExUnit.Server do
|
||||
end
|
||||
|
||||
def take_async_cases(count) do
|
||||
GenServer.call(__MODULE__, {:take_async_cases, count}, @timeout)
|
||||
timeout = Application.fetch_env!(:ex_unit, :case_load_timeout)
|
||||
GenServer.call(__MODULE__, {:take_async_cases, count}, timeout)
|
||||
end
|
||||
|
||||
def take_sync_cases() do
|
||||
GenServer.call(__MODULE__, :take_sync_cases, @timeout)
|
||||
timeout = Application.fetch_env!(:ex_unit, :case_load_timeout)
|
||||
GenServer.call(__MODULE__, :take_sync_cases, timeout)
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
@@ -18,6 +18,7 @@ defmodule ExUnit.Mixfile do
|
||||
assert_receive_timeout: 100,
|
||||
autorun: true,
|
||||
capture_log: false,
|
||||
case_load_timeout: 60_000,
|
||||
colors: [],
|
||||
exclude: [],
|
||||
include: [],
|
||||
|
||||
@@ -59,9 +59,12 @@ defmodule IEx.Autocomplete do
|
||||
defp reduce(expr) do
|
||||
Enum.reduce(' ([{', expr, fn token, acc ->
|
||||
hd(:string.tokens(acc, [token]))
|
||||
end) |> Enum.reverse |> strip_ampersand
|
||||
end) |> Enum.reverse |> strip_ampersand |> strip_percent
|
||||
end
|
||||
|
||||
defp strip_percent([?% | t]), do: t
|
||||
defp strip_percent(expr), do: expr
|
||||
|
||||
defp strip_ampersand([?& | t]), do: t
|
||||
defp strip_ampersand(expr), do: expr
|
||||
|
||||
|
||||
@@ -179,4 +179,13 @@ defmodule IEx.AutocompleteTest do
|
||||
:code.purge(Sample)
|
||||
:code.delete(Sample)
|
||||
end
|
||||
|
||||
|
||||
defmodule MyStruct do
|
||||
defstruct my_val: "val"
|
||||
end
|
||||
|
||||
test "completion for structs" do
|
||||
assert expand('%IEx.AutocompleteTest.MyStr') == {:yes, 'uct.', []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,7 +86,7 @@ defmodule Logger.Backends.Console do
|
||||
colors = configure_colors(config)
|
||||
max_buffer = Keyword.get(config, :max_buffer, 32)
|
||||
%{state | format: format, metadata: Enum.reverse(metadata),
|
||||
level: level, colors: colors, device: device, max_buffer: max_buffer}
|
||||
level: level, colors: colors, device: device, max_buffer: max_buffer}
|
||||
end
|
||||
|
||||
defp configure_merge(env, options) do
|
||||
@@ -149,7 +149,7 @@ defmodule Logger.Backends.Console do
|
||||
%{format: format, metadata: keys, colors: colors} = state
|
||||
format
|
||||
|> Logger.Formatter.format(level, msg, ts, take_metadata(md, keys))
|
||||
|> color_event(level, colors)
|
||||
|> color_event(level, colors, md)
|
||||
end
|
||||
|
||||
defp take_metadata(metadata, keys) do
|
||||
@@ -161,10 +161,11 @@ defmodule Logger.Backends.Console do
|
||||
end
|
||||
end
|
||||
|
||||
defp color_event(data, _level, %{enabled: false}), do: data
|
||||
defp color_event(data, _level, %{enabled: false}, _md), do: data
|
||||
|
||||
defp color_event(data, level, %{enabled: true} = colors) do
|
||||
[IO.ANSI.format_fragment(Map.fetch!(colors, level), true), data | IO.ANSI.reset]
|
||||
defp color_event(data, level, %{enabled: true} = colors, md) do
|
||||
color = md[:ansi_color] || Map.fetch!(colors, level)
|
||||
[IO.ANSI.format_fragment(color, true), data | IO.ANSI.reset]
|
||||
end
|
||||
|
||||
defp log_buffer(%{buffer_size: 0, buffer: []} = state), do: state
|
||||
|
||||
@@ -109,4 +109,12 @@ defmodule Logger.Backends.ConsoleTest do
|
||||
Logger.error("hello")
|
||||
end) == IO.ANSI.cyan() <> "hello" <> IO.ANSI.reset()
|
||||
end
|
||||
|
||||
test "can use colors from metadata" do
|
||||
Logger.configure_backend(:console, [format: "$message", colors: [enabled: true]])
|
||||
|
||||
assert capture_log(fn ->
|
||||
Logger.log(:error, "hello", ansi_color: :yellow)
|
||||
end) == IO.ANSI.yellow() <> "hello" <> IO.ANSI.reset()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -246,10 +246,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
|
||||
defp get_external_resources(module, cwd) do
|
||||
for file <- Module.get_attribute(module, :external_resource),
|
||||
File.regular?(file),
|
||||
relative = Path.relative_to(file, cwd),
|
||||
Path.type(relative) == :relative,
|
||||
do: relative
|
||||
do: Path.relative_to(file, cwd)
|
||||
end
|
||||
|
||||
defp each_file(source) do
|
||||
|
||||
@@ -264,9 +264,6 @@ defmodule Mix.Compilers.Test do
|
||||
|
||||
defp get_external_resources(module, cwd) do
|
||||
for file <- Module.get_attribute(module, :external_resource),
|
||||
File.regular?(file),
|
||||
relative = Path.relative_to(file, cwd),
|
||||
Path.type(relative) == :relative,
|
||||
do: relative
|
||||
do: Path.relative_to(file, cwd)
|
||||
end
|
||||
end
|
||||
|
||||
+10
-7
@@ -71,12 +71,15 @@ defmodule Mix.Dep do
|
||||
against the lock.
|
||||
"""
|
||||
def cached do
|
||||
if project = Mix.Project.get do
|
||||
key = {:cached_deps, project}
|
||||
Mix.ProjectStack.read_cache(key) ||
|
||||
Mix.ProjectStack.write_cache(key, loaded(env: Mix.env))
|
||||
else
|
||||
loaded(env: Mix.env)
|
||||
cond do
|
||||
System.get_env("MIX_NO_DEPS") in ~w(1 true) ->
|
||||
[]
|
||||
project = Mix.Project.get ->
|
||||
key = {:cached_deps, project}
|
||||
Mix.ProjectStack.read_cache(key) ||
|
||||
Mix.ProjectStack.write_cache(key, loaded(env: Mix.env))
|
||||
true ->
|
||||
loaded(env: Mix.env)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -264,7 +267,7 @@ defmodule Mix.Dep do
|
||||
do: "the dependency was built with another SCM, run \"#{mix_env_var}mix deps.compile\""
|
||||
|
||||
defp dep_status(%Mix.Dep{app: app, requirement: req, manager: manager, opts: opts, from: from}) do
|
||||
opts = Keyword.drop(opts, [:dest, :build, :lock, :manager])
|
||||
opts = Keyword.drop(opts, [:dest, :build, :lock, :manager, :checkout])
|
||||
opts = opts ++ (if manager, do: [manager: manager], else: [])
|
||||
info = if req, do: {app, req, opts}, else: {app, opts}
|
||||
"\n > In #{Path.relative_to_cwd(from)}:\n #{inspect info}\n"
|
||||
|
||||
+81
-34
@@ -25,6 +25,11 @@ defmodule Mix.SCM.Git do
|
||||
end
|
||||
|
||||
def accepts_options(_app, opts) do
|
||||
opts =
|
||||
opts
|
||||
|> Keyword.put(:checkout, opts[:dest])
|
||||
|> sparse_opts()
|
||||
|
||||
cond do
|
||||
gh = opts[:github] ->
|
||||
opts
|
||||
@@ -41,7 +46,9 @@ defmodule Mix.SCM.Git do
|
||||
|
||||
def checked_out?(opts) do
|
||||
# Are we inside a Git repository?
|
||||
File.regular?(Path.join(opts[:dest], ".git/HEAD"))
|
||||
opts[:checkout]
|
||||
|> Path.join(".git/HEAD")
|
||||
|> File.regular?
|
||||
end
|
||||
|
||||
def lock_status(opts) do
|
||||
@@ -50,7 +57,7 @@ defmodule Mix.SCM.Git do
|
||||
|
||||
cond do
|
||||
lock_rev = get_lock_rev(lock, opts) ->
|
||||
File.cd!(opts[:dest], fn ->
|
||||
File.cd!(opts[:checkout], fn ->
|
||||
%{origin: origin, rev: rev} = get_rev_info()
|
||||
if get_lock_repo(lock) == origin and lock_rev == rev do
|
||||
:ok
|
||||
@@ -76,29 +83,75 @@ defmodule Mix.SCM.Git do
|
||||
|
||||
def checkout(opts) do
|
||||
assert_git!()
|
||||
|
||||
path = opts[:dest]
|
||||
location = opts[:git]
|
||||
|
||||
_ = File.rm_rf!(path)
|
||||
git!(~s(clone --no-checkout --progress "#{location}" "#{path}"))
|
||||
|
||||
File.cd! path, fn -> do_checkout(opts) end
|
||||
path = opts[:checkout]
|
||||
File.rm_rf!(path)
|
||||
File.mkdir_p!(path)
|
||||
File.cd!(path, fn ->
|
||||
git!("init --quiet")
|
||||
git!("--git-dir=.git remote add origin \"#{opts[:git]}\"")
|
||||
checkout(path, opts)
|
||||
end)
|
||||
end
|
||||
|
||||
def update(opts) do
|
||||
assert_git!()
|
||||
path = opts[:checkout]
|
||||
File.cd! path, fn -> checkout(path, opts) end
|
||||
end
|
||||
|
||||
File.cd! opts[:dest], fn ->
|
||||
location = opts[:git]
|
||||
update_origin(location)
|
||||
defp checkout(_path, opts) do
|
||||
# Set configuration
|
||||
sparse_toggle(opts)
|
||||
update_origin(opts[:git])
|
||||
|
||||
command = IO.iodata_to_binary(["--git-dir=.git fetch --force",
|
||||
progress_switch(git_version()),
|
||||
tags_switch(opts[:tag])])
|
||||
# Fetch external data
|
||||
command = IO.iodata_to_binary(["--git-dir=.git fetch --force --quiet",
|
||||
progress_switch(git_version()),
|
||||
tags_switch(opts[:tag])])
|
||||
git!(command)
|
||||
|
||||
git!(command)
|
||||
do_checkout(opts)
|
||||
# Migrate the git repo
|
||||
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
|
||||
git!("--git-dir=.git checkout --quiet #{rev}")
|
||||
|
||||
if opts[:submodules] do
|
||||
git!("--git-dir=.git submodule update --init --recursive")
|
||||
end
|
||||
|
||||
# Get the new repo lock
|
||||
get_lock(opts)
|
||||
end
|
||||
|
||||
defp sparse_opts(opts) do
|
||||
if opts[:sparse] do
|
||||
dest = Path.join(opts[:dest], opts[:sparse])
|
||||
Keyword.put(opts, :dest, dest)
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp sparse_toggle(opts) do
|
||||
cond do
|
||||
sparse = opts[:sparse] ->
|
||||
sparse_check(git_version())
|
||||
git!("--git-dir=.git config core.sparsecheckout true")
|
||||
File.write!(".git/info/sparse-checkout", sparse)
|
||||
File.exists?(".git/info/sparse-checkout") ->
|
||||
File.write!(".git/info/sparse-checkout", "*")
|
||||
git!("--git-dir=.git read-tree -mu HEAD")
|
||||
git!("--git-dir=.git config core.sparsecheckout false")
|
||||
File.rm(".git/info/sparse-checkout")
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp sparse_check(version) do
|
||||
unless {1, 7, 0} <= version do
|
||||
version = version |> Tuple.to_list |> Enum.join(".")
|
||||
Mix.raise "Git >= 1.7.0 is required to use sparse checkout. " <>
|
||||
"You are running version #{version}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,24 +164,17 @@ defmodule Mix.SCM.Git do
|
||||
## Helpers
|
||||
|
||||
defp validate_git_options(opts) do
|
||||
case Keyword.take(opts, [:branch, :ref, :tag]) do
|
||||
[] -> opts
|
||||
[_] -> opts
|
||||
_ ->
|
||||
Mix.raise "You should specify only one of branch, ref or tag, and only once. " <>
|
||||
"Error on Git dependency: #{opts[:git]}"
|
||||
end
|
||||
err = "You should specify only one of branch, ref or tag, and only once. " <>
|
||||
"Error on Git dependency: #{opts[:git]}"
|
||||
validate_single_uniq(opts, [:branch, :ref, :tag], err)
|
||||
end
|
||||
|
||||
defp do_checkout(opts) do
|
||||
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
|
||||
git!("--git-dir=.git checkout --quiet #{rev}")
|
||||
|
||||
if opts[:submodules] do
|
||||
git!("--git-dir=.git submodule update --init --recursive")
|
||||
defp validate_single_uniq(opts, take, error) do
|
||||
case Keyword.take(opts, take) do
|
||||
[] -> opts
|
||||
[_] -> opts
|
||||
_ -> Mix.raise error
|
||||
end
|
||||
|
||||
get_lock(opts)
|
||||
end
|
||||
|
||||
defp get_lock(opts) do
|
||||
@@ -146,7 +192,8 @@ defmodule Mix.SCM.Git do
|
||||
defp get_lock_rev(_, _), do: nil
|
||||
|
||||
defp get_lock_opts(opts) do
|
||||
lock_opts = Keyword.take(opts, [:branch, :ref, :tag])
|
||||
lock_opts = Keyword.take(opts, [:branch, :ref, :tag, :sparse])
|
||||
|
||||
if opts[:submodules] do
|
||||
lock_opts ++ [submodules: true]
|
||||
else
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule Mix.Tasks.App.Start do
|
||||
applications are started in permanent mode
|
||||
|
||||
* `:consolidate_protocols` - when `true`, loads consolidated
|
||||
protocols before start
|
||||
protocols before start. The default value is `true`.
|
||||
|
||||
* `:elixir` - matches the current elixir version against the
|
||||
given requirement
|
||||
|
||||
@@ -14,7 +14,8 @@ defmodule Mix.Tasks.Compile do
|
||||
`[:yecc, :leex, :erlang, :elixir, :xref, :app]`
|
||||
|
||||
* `:consolidate_protocols` - when `true`, runs protocol
|
||||
consolidation via the `compile.protocols` task
|
||||
consolidation via the `compile.protocols` task. The default
|
||||
value is `true`.
|
||||
|
||||
* `:build_embedded` - when `true`, activates protocol
|
||||
consolidation and does not generate symlinks in builds
|
||||
|
||||
@@ -9,7 +9,8 @@ defmodule Mix.Tasks.Compile.Protocols do
|
||||
|
||||
This task is automatically invoked whenever the project
|
||||
enables `:consolidate_protocols` or `:build_embedded` in
|
||||
its configuration.
|
||||
its configuration. As `:consolidate_protocols` is enabled
|
||||
in the default configuration, this is the default case.
|
||||
|
||||
## Consolidation
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ defmodule Mix.Tasks.Deps.Loadpaths do
|
||||
path
|
||||
end
|
||||
|
||||
prune_deps(load_paths, "--no-deps-check" in args)
|
||||
unless System.get_env("MIX_NO_DEPS") in ~w(1 true) do
|
||||
prune_deps(load_paths, "--no-deps-check" in args)
|
||||
end
|
||||
end
|
||||
|
||||
# If the build is per environment, we should be able to look
|
||||
|
||||
@@ -32,7 +32,7 @@ defmodule Mix.Tasks.Loadpaths do
|
||||
|
||||
# --no-deps is used only internally. It has no purpose
|
||||
# from Mix.CLI because running a task may load deps.
|
||||
unless "--no-deps" in args or System.get_env("MIX_NO_DEPS") in ~w(1 true) do
|
||||
unless "--no-deps" in args do
|
||||
Mix.Task.run "deps.loadpaths", args
|
||||
end
|
||||
|
||||
|
||||
@@ -13,28 +13,28 @@ defmodule Mix.Tasks.Xref do
|
||||
## Xref modes
|
||||
|
||||
The `xref` task expects a mode as first argument:
|
||||
|
||||
|
||||
mix xref MODE
|
||||
|
||||
|
||||
All available modes are discussed below.
|
||||
|
||||
|
||||
### warnings
|
||||
|
||||
Prints warnings for violated cross reference checks:
|
||||
|
||||
|
||||
mix xref warnings
|
||||
|
||||
|
||||
This is the mode used by Mix during compilation.
|
||||
|
||||
### unreachable
|
||||
|
||||
Prints all unreachable "file:line: module.function/arity" entries:
|
||||
|
||||
|
||||
mix xref unreachable
|
||||
|
||||
|
||||
The "file:line" represents the file and line a call to an unknown
|
||||
"module.function/arity" is made.
|
||||
|
||||
|
||||
### callers CALLEE
|
||||
|
||||
Prints all callers of the given `CALLEE`, which can be one of: `Module`,
|
||||
@@ -48,7 +48,7 @@ defmodule Mix.Tasks.Xref do
|
||||
|
||||
Prints a file dependency graph where an edge from `A` to `B` indicates
|
||||
that `A` depends on `B`.
|
||||
|
||||
|
||||
mix xref graph --format dot
|
||||
|
||||
The following options are accepted:
|
||||
@@ -106,6 +106,8 @@ defmodule Mix.Tasks.Xref do
|
||||
{opts, args} =
|
||||
OptionParser.parse!(args, strict: @switches)
|
||||
|
||||
Mix.Task.run("loadpaths")
|
||||
|
||||
if Keyword.get(opts, :compile, true) do
|
||||
Mix.Task.run("compile")
|
||||
end
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
git_repo
|
||||
git_sparse_repo
|
||||
deps_on_git_repo
|
||||
git_rebar
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{sub_dirs, ["apps/*"]}.
|
||||
|
||||
{deps, [
|
||||
{git_rebar, "0.1..*", {git, "../../test/fixtures/git_rebar", master}}
|
||||
]}.
|
||||
|
||||
{overrides, [{add, rebar_dep, [{src_dirs, ["apps/rebar_dep/src"]}]}]}.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
CONFIG ++ [{'SCRIPT', SCRIPT}].
|
||||
CONFIG ++ [{'SCRIPT', SCRIPT}] ++ [{deps, [{git_rebar, "0.1..*", {git, filename:absname("../../test/fixtures/git_rebar"), master}}]}].
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{deps, [
|
||||
{git_rebar, {git, "../../test/fixtures/git_rebar"}}
|
||||
]}.
|
||||
|
||||
{overrides, [
|
||||
{override, git_rebar, [{deps, [{git_repo, {git, "../../test/fixtures/git_repo"}}]}]}
|
||||
]}.
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{deps, [
|
||||
{git_rebar, {git, filename:absname("../../test/fixtures/git_rebar")}}
|
||||
]},
|
||||
{overrides, [
|
||||
{override, git_rebar, [{deps, [{git_repo, {git, filename:absname("../../test/fixtures/git_repo")}}]}]}
|
||||
]}
|
||||
].
|
||||
@@ -106,7 +106,7 @@ defmodule Mix.RebarTest do
|
||||
|
||||
assert Enum.find(deps, fn %Mix.Dep{app: app, opts: opts} ->
|
||||
if app == :git_rebar do
|
||||
assert Enum.find(opts, &match?({:git, "../../test/fixtures/git_rebar"}, &1))
|
||||
assert Enum.find(opts, &match?({:git, _}, &1))
|
||||
assert Enum.find(opts, &match?({:ref, "master"}, &1))
|
||||
true
|
||||
end
|
||||
@@ -155,7 +155,7 @@ defmodule Mix.RebarTest do
|
||||
|
||||
in_tmp "get and compile dependencies for Rebar", fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
assert_received {:mix_shell, :info, ["* Getting git_rebar (../../test/fixtures/git_rebar)"]}
|
||||
assert_received {:mix_shell, :info, ["* Getting git_rebar" <> _]}
|
||||
|
||||
Mix.Tasks.Deps.Compile.run []
|
||||
assert_received {:mix_shell, :run, ["==> git_rebar (compile)\n"]}
|
||||
@@ -185,7 +185,7 @@ defmodule Mix.RebarTest do
|
||||
|
||||
in_tmp "get and compile dependencies for rebar3", fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
assert_received {:mix_shell, :info, ["* Getting git_rebar (../../test/fixtures/git_rebar)"]}
|
||||
assert_received {:mix_shell, :info, ["* Getting git_rebar " <> _]}
|
||||
|
||||
Mix.Tasks.Deps.Compile.run []
|
||||
assert_received {:mix_shell, :run, ["===> Compiling git_rebar\n"]}
|
||||
|
||||
@@ -172,25 +172,43 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
|
||||
test "compiles dependent changed files" do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
tmp = tmp_path("c.eex")
|
||||
File.touch!("lib/a.eex")
|
||||
|
||||
File.write!("lib/a.ex", """
|
||||
defmodule A do
|
||||
@external_resource "lib/b.eex"
|
||||
@external_resource "lib/a.eex"
|
||||
@external_resource #{inspect tmp}
|
||||
def a, do: :ok
|
||||
end
|
||||
""")
|
||||
|
||||
# Compiles with missing external resources
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == :ok
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == :noop
|
||||
Mix.shell.flush
|
||||
purge [A, B]
|
||||
|
||||
# Update local existing resource
|
||||
File.touch!("lib/a.eex", {{2020, 1, 1}, {0, 0, 0}})
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == :ok
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
|
||||
# Does not update on old existing resource
|
||||
File.touch!("lib/a.eex", {{1970, 1, 1}, {0, 0, 0}})
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == :noop
|
||||
Mix.shell.flush
|
||||
purge [A, B]
|
||||
|
||||
# Update external existing resource
|
||||
File.touch!(tmp, {{2020, 1, 1}, {0, 0, 0}})
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == :ok
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
end
|
||||
after
|
||||
File.rm tmp_path("c.eex")
|
||||
end
|
||||
|
||||
test "does not recompile empty files" do
|
||||
@@ -231,4 +249,28 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
end
|
||||
end
|
||||
|
||||
test "does not treat remote typespecs as compile time dependencies" do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
File.write!("lib/b.ex", """
|
||||
defmodule B do
|
||||
@type t :: A.t
|
||||
end
|
||||
""")
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == :ok
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
|
||||
Mix.shell.flush
|
||||
purge [A, B]
|
||||
|
||||
future = {{2020, 1, 1}, {0, 0, 0}}
|
||||
File.touch!("lib/a.ex", future)
|
||||
Mix.Tasks.Compile.Elixir.run ["--verbose"]
|
||||
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,6 +69,20 @@ defmodule Mix.Tasks.DepsGitTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "gets and updates Git repos with sparse checkout" do
|
||||
Process.put(:git_repo_opts, sparse: "sparse_dir")
|
||||
Mix.Project.push GitApp
|
||||
|
||||
in_fixture "no_mixfile", fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
message = "* Getting git_repo (#{fixture_path("git_repo")})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
refute File.exists?("deps/git_repo/mix.exs")
|
||||
assert File.exists?("deps/git_repo/sparse_dir/mix.exs")
|
||||
assert File.read!("mix.lock") =~ "sparse: \"sparse_dir\""
|
||||
end
|
||||
end
|
||||
|
||||
test "handles invalid .git directory" do
|
||||
Mix.Project.push GitApp
|
||||
|
||||
@@ -265,6 +279,61 @@ defmodule Mix.Tasks.DepsGitTest do
|
||||
purge [GitRepo, GitRepo.Mixfile]
|
||||
end
|
||||
|
||||
test "updates the repo when sparse is turned off" do
|
||||
Process.put(:git_repo_opts, sparse: "sparse_dir")
|
||||
Mix.Project.push GitApp
|
||||
|
||||
in_fixture "no_mixfile", fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
refute File.exists?("deps/git_repo/lib/git_repo.ex")
|
||||
|
||||
# Flush the errors we got, move to a clean slate
|
||||
Mix.shell.flush
|
||||
Mix.Task.clear
|
||||
Process.delete(:git_repo_opts)
|
||||
Mix.Project.pop
|
||||
Mix.Project.push GitApp
|
||||
|
||||
# Calling get should update the dependency
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
refute File.read!("mix.lock") =~ "sparse_dir"
|
||||
assert File.exists?("deps/git_repo/lib/git_repo.ex")
|
||||
|
||||
message = "* Updating git_repo (#{fixture_path("git_repo")})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
|
||||
# Check we got no error
|
||||
refute_received {:mix_shell, :error, _}
|
||||
end
|
||||
end
|
||||
|
||||
test "updates the repo when sparse is turned on" do
|
||||
Mix.Project.push GitApp
|
||||
|
||||
in_fixture "no_mixfile", fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
assert File.exists?("deps/git_repo/lib/git_repo.ex")
|
||||
|
||||
# Flush the errors we got, move to a clean slate
|
||||
Mix.shell.flush
|
||||
Mix.Task.clear
|
||||
Process.put(:git_repo_opts, sparse: "sparse_dir")
|
||||
Mix.Project.pop
|
||||
Mix.Project.push GitApp
|
||||
|
||||
# Calling get should update the dependency
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
assert File.read!("mix.lock") =~ "sparse_dir"
|
||||
refute File.exists?("deps/git_repo/lib/git_repo.ex")
|
||||
|
||||
message = "* Updating git_repo (#{fixture_path("git_repo")})"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
|
||||
# Check we got no error
|
||||
refute_received {:mix_shell, :error, _}
|
||||
end
|
||||
end
|
||||
|
||||
test "updates the repo and the lock when the mixfile updates" do
|
||||
Mix.Project.push GitApp
|
||||
[last, first | _] = get_git_repo_revs
|
||||
@@ -304,7 +373,7 @@ defmodule Mix.Tasks.DepsGitTest do
|
||||
exception = assert_raise Mix.Error, fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
end
|
||||
assert Exception.message(exception) =~ "Command \"git clone"
|
||||
assert Exception.message(exception) =~ "Command \"git --git-dir=.git fetch"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -364,7 +433,7 @@ defmodule Mix.Tasks.DepsGitTest do
|
||||
Mix.Project.push(name, file)
|
||||
end
|
||||
|
||||
defp get_git_repo_revs do
|
||||
defp get_git_repo_revs() do
|
||||
File.cd! fixture_path("git_repo"), fn ->
|
||||
Regex.split ~r(\r?\n), System.cmd("git", ["log", "--format=%H"]) |> elem(0)
|
||||
end
|
||||
|
||||
@@ -564,6 +564,32 @@ defmodule Mix.Tasks.XrefTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "graph: with dynamic module" do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
File.write! "lib/a.ex", """
|
||||
B.define()
|
||||
"""
|
||||
|
||||
File.write! "lib/b.ex", """
|
||||
defmodule B do
|
||||
def define do
|
||||
defmodule A do
|
||||
end
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
assert Mix.Task.run("xref", ["graph"]) == :ok
|
||||
|
||||
assert """
|
||||
Compiling 2 files (.ex)
|
||||
Generated sample app
|
||||
lib/a.ex
|
||||
lib/b.ex
|
||||
""" = receive_until_no_messages([])
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_graph(opts \\ [], dot \\ false, expected) do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
File.write! "lib/a.ex", """
|
||||
|
||||
@@ -230,6 +230,30 @@ unless File.dir?(target) do
|
||||
end
|
||||
"""
|
||||
|
||||
## Sparse
|
||||
subdir = Path.join(target, "sparse_dir")
|
||||
File.mkdir_p!(Path.join(subdir, "lib"))
|
||||
|
||||
File.write! Path.join(subdir, "mix.exs"), """
|
||||
## Auto-generated fixture
|
||||
defmodule GitSparseRepo.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :git_sparse_repo, version: "0.1.0"]
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
File.write! Path.join(subdir, "lib/git_sparse_repo.ex"), """
|
||||
## Auto-generated fixture
|
||||
defmodule GitSparseRepo do
|
||||
def hello do
|
||||
"World"
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
File.cd! target, fn ->
|
||||
System.cmd("git", ~w[add .])
|
||||
System.cmd("git", ~w[commit -m "lib"])
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{application, elixir,
|
||||
[{description, "elixir"},
|
||||
{vsn, "1.3.2"},
|
||||
{vsn, "1.3.4"},
|
||||
{modules, [
|
||||
elixir
|
||||
]},
|
||||
|
||||
Reference in New Issue
Block a user