diff --git a/lib/hex_solver/assignment.ex b/lib/hex_solver/assignment.ex index 58271b5..74ba494 100644 --- a/lib/hex_solver/assignment.ex +++ b/lib/hex_solver/assignment.ex @@ -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, @@ -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 @@ -34,10 +34,10 @@ defmodule HexSolver.Assignment do }, _opts ) do - "#Assignment" + "#Assignment" 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 diff --git a/lib/hex_solver/constraint.ex b/lib/hex_solver/constraint.ex index a81714d..0071329 100644 --- a/lib/hex_solver/constraint.ex +++ b/lib/hex_solver/constraint.ex @@ -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 diff --git a/lib/hex_solver/constraints/impl.ex b/lib/hex_solver/constraints/impl.ex index 8e4f092..6597b2e 100644 --- a/lib/hex_solver/constraints/impl.ex +++ b/lib/hex_solver/constraints/impl.ex @@ -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 diff --git a/lib/hex_solver/constraints/range.ex b/lib/hex_solver/constraints/range.ex index 76e77d1..86f72d2 100644 --- a/lib/hex_solver/constraints/range.ex +++ b/lib/hex_solver/constraints/range.ex @@ -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 diff --git a/lib/hex_solver/constraints/union.ex b/lib/hex_solver/constraints/union.ex index c678da9..0db6356 100644 --- a/lib/hex_solver/constraints/union.ex +++ b/lib/hex_solver/constraints/union.ex @@ -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 @@ -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 diff --git a/lib/hex_solver/constraints/version.ex b/lib/hex_solver/constraints/version.ex index e42f499..b8db504 100644 --- a/lib/hex_solver/constraints/version.ex +++ b/lib/hex_solver/constraints/version.ex @@ -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 diff --git a/lib/hex_solver/incompatibility.ex b/lib/hex_solver/incompatibility.ex index 8783d15..39b22c5 100644 --- a/lib/hex_solver/incompatibility.ex +++ b/lib/hex_solver/incompatibility.ex @@ -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 @@ -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 @@ -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 diff --git a/lib/hex_solver/package_range.ex b/lib/hex_solver/package_range.ex index 0697d84..8e0a867 100644 --- a/lib/hex_solver/package_range.ex +++ b/lib/hex_solver/package_range.ex @@ -1,7 +1,7 @@ defmodule HexSolver.PackageRange do @moduledoc false - alias HexSolver.PackageRange + alias HexSolver.{Constraint, PackageRange} alias HexSolver.Constraints.Range defstruct repo: nil, @@ -18,7 +18,7 @@ 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 @@ -26,9 +26,9 @@ defmodule HexSolver.PackageRange do 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 diff --git a/lib/hex_solver/solver.ex b/lib/hex_solver/solver.ex index 43bc97f..0845cce 100644 --- a/lib/hex_solver/solver.ex +++ b/lib/hex_solver/solver.ex @@ -2,6 +2,7 @@ defmodule HexSolver.Solver do @moduledoc false alias HexSolver.{ + Assignment, Incompatibility, PackageLister, PackageRange, @@ -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}} @@ -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 @@ -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 -> @@ -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} -> @@ -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) diff --git a/lib/hex_solver/term.ex b/lib/hex_solver/term.ex index 9620fc5..ea5e533 100644 --- a/lib/hex_solver/term.ex +++ b/lib/hex_solver/term.ex @@ -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: "" @@ -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: "" diff --git a/test/hex_solver/failure_test.exs b/test/hex_solver/failure_test.exs index ba0c875..81730d6 100644 --- a/test/hex_solver/failure_test.exs +++ b/test/hex_solver/failure_test.exs @@ -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