Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/hex_solver/assignment.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule HexSolver.Assignment do
@moduledoc false

alias HexSolver.{Assignment, Term}
alias HexSolver.{Assignment, Incompatibility, Term}

defstruct term: nil,
decision_level: nil,
Expand All @@ -17,7 +17,7 @@ defmodule HexSolver.Assignment do
end

def to_string(%Assignment{term: term}) do
Kernel.to_string(term)
Term.to_string(term)
end

defimpl String.Chars do
Expand All @@ -34,10 +34,10 @@ defmodule HexSolver.Assignment do
},
_opts
) do
"#Assignment<term: #{term}#{maybe(", cause: ", cause)}, level: #{decision_level}, index: #{index}>"
"#Assignment<term: #{Term.to_string(term)}#{maybe(", cause: ", cause)}, level: #{decision_level}, index: #{index}>"
end

defp maybe(_prefix, nil), do: ""
defp maybe(prefix, value), do: "#{prefix}#{value}"
defp maybe(prefix, value), do: "#{prefix}#{Incompatibility.to_string(value)}"
end
end
1 change: 1 addition & 0 deletions lib/hex_solver/constraint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ defprotocol HexSolver.Constraint do
def union(left, right)
def compare(left, right)
def to_requirement(constraint)
def to_string(constraint)
end
3 changes: 3 additions & 0 deletions lib/hex_solver/constraints/impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ defmodule HexSolver.Constraints.Impl do

def to_requirement(constraint),
do: unquote(__CALLER__.module).to_requirement(constraint)

def to_string(constraint),
do: unquote(__CALLER__.module).to_string(constraint)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/hex_solver/constraints/range.ex
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ defmodule HexSolver.Constraints.Range do

defimpl Inspect do
def inspect(range, _opts) do
"#Range<#{range}>"
"#Range<#{HexSolver.Constraints.Range.to_string(range)}>"
end
end
end
4 changes: 2 additions & 2 deletions lib/hex_solver/constraints/union.ex
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ defmodule HexSolver.Constraints.Union do
end

def to_string(%Union{ranges: ranges}) do
Enum.map_join(ranges, " or ", &Kernel.to_string/1)
Enum.map_join(ranges, " or ", &Constraint.to_string/1)
end

defimpl String.Chars do
Expand All @@ -166,7 +166,7 @@ defmodule HexSolver.Constraints.Union do

defimpl Inspect do
def inspect(union, _opts) do
"#Union<#{union}>"
"#Union<#{HexSolver.Constraints.Union.to_string(union)}>"
end
end
end
4 changes: 4 additions & 0 deletions lib/hex_solver/constraints/version.ex
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ defmodule HexSolver.Constraints.Version do
Kernel.to_string(version)
end

def to_string(%Version{} = version) do
Kernel.to_string(version)
end

def max(left, right) do
case compare(left, right) do
:lt -> right
Expand Down
7 changes: 4 additions & 3 deletions lib/hex_solver/incompatibility.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ defmodule HexSolver.Incompatibility do
def to_string(%Incompatibility{cause: :no_versions, terms: terms}, opts) do
[%Term{positive: true} = term] = terms

"no versions of #{package_name(term, opts)} match #{bright(term.package_range.constraint, opts)}"
"no versions of #{package_name(term, opts)} match #{bright(Constraint.to_string(term.package_range.constraint), opts)}"
end

def to_string(%Incompatibility{cause: :package_not_found, terms: terms}, opts) do
Expand All @@ -83,7 +83,8 @@ defmodule HexSolver.Incompatibility do

def to_string(%Incompatibility{cause: :root, terms: terms}, opts) do
[%Term{positive: false} = term] = terms
"#{package_name(term, opts)} is #{bright(term.package_range.constraint, opts)}"

"#{package_name(term, opts)} is #{bright(Constraint.to_string(term.package_range.constraint), opts)}"
end

def to_string(%Incompatibility{terms: []}, _opts) do
Expand Down Expand Up @@ -409,7 +410,7 @@ defmodule HexSolver.Incompatibility do

defp term_abs(%Term{} = term), do: %Term{term | positive: true}

defp bright_term_abs(term, opts), do: bright(term_abs(term), opts)
defp bright_term_abs(term, opts), do: bright(Term.to_string(term_abs(term)), opts)

defimpl String.Chars do
defdelegate to_string(incompatibility), to: HexSolver.Incompatibility
Expand Down
8 changes: 4 additions & 4 deletions lib/hex_solver/package_range.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule HexSolver.PackageRange do
@moduledoc false

alias HexSolver.PackageRange
alias HexSolver.{Constraint, PackageRange}
alias HexSolver.Constraints.Range

defstruct repo: nil,
Expand All @@ -18,17 +18,17 @@ defmodule HexSolver.PackageRange do
do: "#{repo}/#{name}#{constraint(constraint)}"

defp constraint(%Range{min: nil, max: nil}), do: ""
defp constraint(constraint), do: " #{constraint}"
defp constraint(constraint), do: " " <> Constraint.to_string(constraint)

defimpl String.Chars do
defdelegate to_string(package_range), to: HexSolver.PackageRange
end

defimpl Inspect do
def inspect(%{repo: nil, name: name, constraint: constraint}, _opts),
do: "#PackageRange<#{name} #{constraint}>"
do: "#PackageRange<#{name} #{Constraint.to_string(constraint)}>"

def inspect(%{repo: repo, name: name, constraint: constraint}, _opts),
do: "#PackageRange<#{repo}/#{name} #{constraint}>"
do: "#PackageRange<#{repo}/#{name} #{Constraint.to_string(constraint)}>"
end
end
15 changes: 8 additions & 7 deletions lib/hex_solver/solver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule HexSolver.Solver do
@moduledoc false

alias HexSolver.{
Assignment,
Incompatibility,
PackageLister,
PackageRange,
Expand Down Expand Up @@ -96,7 +97,7 @@ defmodule HexSolver.Solver do
defp propagate_incompatibility([], unsatisfied, incompatibility, state, _opts) do
# Only one term in the incompatibility was unsatisfied
unsatisfied = %{unsatisfied | positive: not unsatisfied.positive}
Logger.debug("RESOLVER: derived #{unsatisfied}")
Logger.debug("RESOLVER: derived #{Term.to_string(unsatisfied)}")

solution = PartialSolution.derive(state.solution, unsatisfied, incompatibility)
{:ok, unsatisfied.package_range.name, %{state | solution: solution}}
Expand Down Expand Up @@ -146,7 +147,7 @@ defmodule HexSolver.Solver do
state.solution
else
package_range = %PackageRange{package_range | constraint: version}
Logger.debug("RESOLVER: selecting #{package_range}")
Logger.debug("RESOLVER: selecting #{PackageRange.to_string(package_range)}")
PartialSolution.decide(state.solution, package_range)
end

Expand All @@ -164,7 +165,7 @@ defmodule HexSolver.Solver do
end

defp add_incompatibility(state, incompatibility) do
Logger.debug("RESOLVER: fact #{incompatibility}")
Logger.debug("RESOLVER: fact #{Incompatibility.to_string(incompatibility)}")

incompatibilities =
Enum.reduce(incompatibility.terms, state.incompatibilities, fn term, incompatibilities ->
Expand All @@ -189,7 +190,7 @@ defmodule HexSolver.Solver do
# incompatibility that encapsulates the root cause of the conflict and backtracks
# until the new incompatibility will allow propagation to deduce new assignments.
defp conflict_resolution(state, incompatibility) do
Logger.debug("RESOLVER: conflict #{incompatibility}")
Logger.debug("RESOLVER: conflict #{Incompatibility.to_string(incompatibility)}")
do_conflict_resolution(state, incompatibility, false)
catch
:throw, {__MODULE__, :conflict_resolution, incompatibility, state} ->
Expand Down Expand Up @@ -319,9 +320,9 @@ defmodule HexSolver.Solver do

Logger.debug("""
RESOLVER: conflict resolution
#{resolution.most_recent_term} is#{partially} satisfied by #{resolution.most_recent_satisfier}
which is caused by #{resolution.most_recent_satisfier.cause}
thus #{incompatibility}\
#{Term.to_string(resolution.most_recent_term)} is#{partially} satisfied by #{Assignment.to_string(resolution.most_recent_satisfier)}
which is caused by #{Incompatibility.to_string(resolution.most_recent_satisfier.cause)}
thus #{Incompatibility.to_string(incompatibility)}\
""")

do_conflict_resolution(state, incompatibility, true)
Expand Down
4 changes: 2 additions & 2 deletions lib/hex_solver/term.ex
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ defmodule HexSolver.Term do
end

def to_string(%Term{package_range: package_range, positive: positive}) do
"#{positive(positive)}#{package_range}"
"#{positive(positive)}#{PackageRange.to_string(package_range)}"
end

defp positive(true), do: ""
Expand All @@ -137,7 +137,7 @@ defmodule HexSolver.Term do
%Term{package_range: package_range, positive: positive, optional: optional},
_opts
) do
"#Term<#{positive(positive)}#{package_range}#{optional(optional)}>"
"#Term<#{positive(positive)}#{PackageRange.to_string(package_range)}#{optional(optional)}>"
end

defp positive(true), do: ""
Expand Down
59 changes: 59 additions & 0 deletions test/hex_solver/failure_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,63 @@ defmodule HexSolver.FailureTest do
So, because "your app" depends on "top2", version solving failed.\
"""
end

# Projects consolidate String.Chars without the impls from archives, such as
# Hex which vendors the solver, so formatting can't dispatch on solver structs
test "formats failures without String.Chars dispatch on solver structs" do
Registry.put("foo", "1.0.0", [{"bar", "~> 2.0 or ~> 3.0"}])
Registry.put("bar", "1.0.0", [])
Registry.put("bar", "2.0.0", [{"baz", "~> 2.0"}])
Registry.put("baz", "1.0.0", [])

dispatched =
trace_string_chars(fn ->
assert {:error, incompatibility} =
HexSolver.Solver.run(
Registry,
to_dependencies([{"foo", "~> 1.0"}, {"baz", "~> 1.0"}]),
[],
[]
)

Failure.write(incompatibility)
Failure.write(incompatibility, ansi: true)
inspect(incompatibility)
end)

assert dispatched != []
refute Enum.any?(dispatched, &solver_struct?/1)
end

defp trace_string_chars(fun) do
tracer = spawn_link(fn -> collect_string_chars([]) end)
:erlang.trace_pattern({String.Chars, :to_string, 1}, true, [])
:erlang.trace(self(), true, [:call, {:tracer, tracer}])

try do
fun.()
after
:erlang.trace(self(), false, [:call])
:erlang.trace_pattern({String.Chars, :to_string, 1}, false, [])
end

ref = :erlang.trace_delivered(self())
assert_receive {:trace_delivered, _pid, ^ref}
send(tracer, {:values, self()})
assert_receive {:values, values}
values
end

defp collect_string_chars(values) do
receive do
{:trace, _pid, :call, {String.Chars, :to_string, [value]}} ->
collect_string_chars([value | values])

{:values, pid} ->
send(pid, {:values, values})
end
end

defp solver_struct?(%module{}), do: String.starts_with?(inspect(module), "HexSolver.")
defp solver_struct?(_value), do: false
end
Loading