blatyo

blatyo

Conduit Core Team

How to get better diff comparison for strings with newlines?

I’m testing some complex ecto sql builder stuff. So I pass data to generate an ecto query and I want to validate that it produces the right query. So, I’m using the Repo.to_sql function to generate SQL and then asserting that matches the expected SQL.

That works OK, but when the SQL is long, it makes it hard to understand which parts of the query differ. I wrote something that can format SQL, because I thought it’d improve the diff output that exunit provides. However, that just added some more whitespace to a single line diff between the two SQL queries. Here’s an example:

What I would like is to have a test failure that would wrap on the newlines instead. Ideally, it would look something like this with the diff coloring elixir does:

  1) test formats SQL (SQLFormatterTest)
     test/sql_formatter_test.exs:5
     Assertion with == failed
     code:  assert SQLFormatter.format("SELECT * FROM users") == "SELECT\n  *\nFROM\n  user\nWHERE\n  id = 1\n"
     left:  """
            SELECT
              *
            FROM
              users
            """
     right: """
            SELECT
              *
            FROM
              user
            WHERE
              id = 1
            """
     stacktrace:
       test/sql_formatter_test.exs:6: (test)

I’m curious if anyone has any suggestions for how to get better test output?

Marked As Solved

sodapopcan

sodapopcan

The problem I run into here is that format_test_failure seems to escape \n as \\n. AFAIK (and I would love to be proved wrong) I think you have to do the formatting yourself.

This is a cobbled together version from a formatter I have. I was hooking into the standard formatter and using format_test_failure so I had to remove that which of course removes all the meta data like test name and line number (though it’s easily added as all of that info is available between error and meta). All this has is left and right but it does work if you want to use it as a starting point!

Again, hopefully I’m wrong and there is an easier way.

defmodule Formatter do
  use GenServer

  def init(_opts) do
    {:ok, %{failures: []}}
  end

  def handle_cast({:test_finished, %{state: {:failed, errors}} = _test} = _event, state) do
    error =
      errors
      |> Enum.map(fn {:error, error, _meta} ->
        left =
          String.split(error.left, "\n")
          |> Enum.map(& "      " <> &1)
          |> Enum.join("\n")

        right =
          String.split(error.right, "\n")
          |> Enum.map(&"       " <> &1)
          |> Enum.join("\n")

        "left: \"\"\"\n" <> left <> "\"\"\"\n\nright: \"\"\"\n" <> right <> "\"\"\""
      end)
      |> Enum.join("\n")

    state = %{state | failures: [error | state.failures]}

    {:noreply, state}
  end

 def handle_cast({:suite_finished, _} = _event, state) do
   if Enum.any?(state.failures) do
     failures =
       state.failures
       |> Enum.reverse()
       |> Enum.join("\n")

     IO.puts("""

       Failures:

     #{failures}
     """)
   end

   {:noreply, state}
 end

  def handle_cast(_event, state) do
    {:noreply, state}
  end
end

ExUnit.start(formatters: [Formatter])

defmodule TestIt do
  use ExUnit.Case

  test "thing" do
    assert """
    line 1
    line 2
    line 3
    """ == """
    line oops
    line 2
    line 3
    """
  end
end

Also Liked

egze

egze

I’ve been doing similar things at work for showing meaningful diffs between 2 JSON files. I had to normalize both: sort keys (json keys don’t have order), pretty print, and only then diff.

See these links for inspiration:

  1. Semantic Diff for SQL
  2. pg_query fingerprint
  3. difftastic for SQL
codeanpeace

codeanpeace

It sounds like something you could do with the ExUnit.Formatter module.

format_test_failure(test, failures, counter, width, formatter)
Receives a test and formats its failures.

Examples

iex> failure = {:error, catch_error(raise "oops"), _stacktrace = []}
iex> formatter_cb = fn _key, value -> value end
iex> test = %ExUnit.Test{name: :"it works", module: MyTest, tags: %{file: "file.ex", line: 7}}
iex> format_test_failure(test, [failure], 1, 80, formatter_cb)
"  1) it works (MyTest)\n     file.ex:7\n     ** (RuntimeError) oops\n"

source: docs for format_test_failure/5

blatyo

blatyo

Conduit Core Team

Thanks for the suggestions. I was definitely in the headspace of thinking I’d have to write a custom assertion. I forgot ex_unit even had formatters, so that will be a useful area to explore.

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call t...
New

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47930 226
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement