Looks like I’ve worked out what’s going on here (if not why).
test
defmodule Mana.AppVersionTest do
use ExUnit.Case
alias Mana.AppVersion
setup do
{:ok,
%{
android: "AppName/0.1.0 (com.redbubble; build:1; Android 28)",
ios: "AppName/3.7.2 (com.redbubble; build:250; iOS Alamofire 53)",
other: "Mozilla/5.0 (Windows; rv:17.0) Gecko/20100101 Firefox/17.0"
}}
end
describe "from_headers/1" do
test "with android user agent", %{android: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.from_headers(headers) == {:android, "0.1.0"}
end
test "with ios user agent", %{ios: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.from_headers(headers) == {:ios, "3.7.2"}
end
test "with browser user agent", %{other: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.from_headers(headers) == {:other, nil}
end
end
describe "app versions" do
test "Grab the version from the user agent for iOS", %{ios: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.app_version_from_headers(headers) == "3.7.2"
end
test "Grab the version from the user agent for Android", %{android: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.app_version_from_headers(headers) == "0.1.0"
end
test "Grab the version from the user agent if it's just the version number" do
headers = ["User-Agent": "3.8.0"]
assert AppVersion.app_version_from_headers(headers) == "3.8.0"
end
test "Returns nil if it doesn't find the version" do
headers = ["User-Agent": "AppName"]
assert AppVersion.app_version_from_headers(headers) == nil
end
test "Returns nil if it is passed in nil" do
headers = ["User-Agent": nil]
assert AppVersion.app_version_from_headers(headers) == nil
end
end
describe "app platform" do
test "for iOS", %{ios: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.app_platform_from_headers(headers) == :ios
end
test "for Android", %{android: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.app_platform_from_headers(headers) == :android
end
test "when headers are empty" do
assert AppVersion.app_platform_from_headers([]) == :other
end
test "when not specified", %{other: user_agent} do
headers = ["User-Agent": user_agent]
assert AppVersion.app_platform_from_headers(headers) == :other
end
end
end
implementation
defmodule Mana.AppVersion do
@moduledoc """
Extract the app version and platform from the user agent.
"""
def from_headers(headers) do
case headers |> Keyword.fetch(:"User-Agent") do
:error ->
nil
{:ok, _user_agent} ->
{
headers |> app_platform_from_headers(),
headers |> app_version_from_headers()
}
end
end
def app_version_from_headers(nil), do: nil
def app_version_from_headers(headers) do
case headers |> Keyword.fetch(:"User-Agent") do
:error ->
nil
{:ok, user_agent} ->
user_agent
|> app_version()
end
end
def app_platform_from_headers(nil), do: nil
def app_platform_from_headers(headers) do
case headers |> Keyword.fetch(:"User-Agent") do
:error ->
:other
{:ok, user_agent} ->
user_agent
|> String.split(" ")
|> app_platform()
end
end
# App version detection
defp app_version(nil), do: nil
defp app_version(user_agent) do
result =
user_agent
|> String.split(" ")
|> List.first()
|> String.split("/")
|> List.last()
case Version.parse(result) do
{:ok, _} -> result
_ -> nil
end
end
# App platform detection
defp app_platform(nil), do: :other
defp app_platform([]), do: :other
defp app_platform([<<"AppName"::utf8, "/", _version::binary>> | platform]) do
cond do
platform
|> Enum.join(" ")
|> String.match?(~r/android/i) ->
:android
platform
|> Enum.join(" ")
|> String.match?(~r/ios/i) ->
:ios
true ->
:other
end
end
defp app_platform([_ | _]), do: :other
end
error output
1) test app platform for iOS (Mana.AppVersionTest)
test/mana/app_version_test.exs:59
Assertion with == failed
code: assert AppVersion.app_platform_from_headers(headers) == :ios
left: :other
right: :ios
stacktrace:
test/mana/app_version_test.exs:61: (test)
....
2) test app platform for Android (Mana.AppVersionTest)
test/mana/app_version_test.exs:64
Assertion with == failed
code: assert AppVersion.app_platform_from_headers(headers) == :android
left: :other
right: :android
stacktrace:
test/mana/app_version_test.exs:66: (test)
.
3) test from_headers/1 with ios user agent (Mana.AppVersionTest)
test/mana/app_version_test.exs:20
Assertion with == failed
code: assert AppVersion.from_headers(headers) == {:ios, "3.7.2"}
left: {:other, "3.7.2"}
right: {:ios, "3.7.2"}
stacktrace:
test/mana/app_version_test.exs:22: (test)
.
4) test from_headers/1 with android user agent (Mana.AppVersionTest)
test/mana/app_version_test.exs:15
Assertion with == failed
code: assert AppVersion.from_headers(headers) == {:android, "0.1.0"}
left: {:other, "0.1.0"}
right: {:android, "0.1.0"}
stacktrace:
test/mana/app_version_test.exs:17: (test)
Finished in 0.07 seconds
12 tests, 4 failures
success case
- modify the implementation in my editor (spacemacs)
- run the test in my editor
- test passes
failure case
- modify the implementation in my editor
- run the test in my terminal
- test fails
The failure case can be reproduced in either direction. If you edit the implementation, wherever you run the test next will pass. If you then run the test in the opposite environment (without requiring recompilation) the test will fail. So, it seems to me there’s a difference in the compilation between the two envs.
I have this in my .spacemacs
config:
alchemist-mix-command "~/.asdf/shims/mix"
alchemist-execute-command "~/.asdf/shims/elixir"
alchemist-compile-command "~/.asdf/shims/elixirc"
The paths to the executables:
craigread@C02T17ZGGTFM /Users/craigread/src/mana/app 2.2.8 18a1ba5
⚡ which mix (production-us-east-1/default)
/Users/craigread/.asdf/shims/mix
craigread@C02T17ZGGTFM /Users/craigread/src/mana/app 2.2.8 18a1ba5
⚡ which elixirc (production-us-east-1/default)
/Users/craigread/.asdf/shims/elixirc
craigread@C02T17ZGGTFM /Users/craigread/src/mana/app 2.2.8 18a1ba5
⚡ which elixir (production-us-east-1/default)
/Users/craigread/.asdf/shims/elixir
The recursive version of this code doesn’t have the same behaviour.