Feat/error logs (#16)

* added list of tasks to achieve

* update deps

* update elixir version in CI/CD pipeline

* update todo with branch push checks

* underscores separation of large numbers

* update elixir version and deps

* add basic module  docs

* remove IO.inspect call

* check if list is empty rather than traversing list for length

* revert empty list code

* duration tests

* add mime type tests

* use logger instread of inspect, remove todos

* replace list length check with Enum.empty

* replace list length check with !Enum.empty?

* numbers readability fix by using underscores on large numbers

* iucn parser replaced case with multiclause function and added test

* replace map and join with map_join

* fix multiple spec issues where map() must be inside a list
This commit is contained in:
2025-12-06 23:41:59 +01:00
committed by GitHub
parent 3d3059e77f
commit 02372f2076
27 changed files with 241 additions and 45 deletions
+2 -2
View File
@@ -15,8 +15,8 @@ jobs:
- name: Setup BEAM / Elixir - name: Setup BEAM / Elixir
uses: erlef/setup-beam@v1 uses: erlef/setup-beam@v1
with: with:
otp-version: 26 otp-version: 28
elixir-version: 1.16.2 elixir-version: 1.18.4
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v4 uses: actions/cache@v4
+6 -2
View File
@@ -15,19 +15,20 @@ If you wish to invite this bot to your server, use [this link.](https://discord.
### Dependencies ### Dependencies
This application has been built and tested with Elixir 1.16 / OTP 26. This application has been built and tested with Elixir 1.18 / OTP 28.
```elixir ```elixir
# mix.exs # mix.exs
defp deps do defp deps do
[ [
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:nostrum, "~> 0.10"}, {:nostrum, "~> 0.10"},
{:httpoison, "~> 2.2"}, {:httpoison, "~> 2.2"},
{:image, "~> 0.55"}, {:image, "~> 0.55"},
{:jason, "~> 1.4"}, {:jason, "~> 1.4"},
{:logger_webhook_backend, "~> 0.0.2"}, {:logger_webhook_backend, "~> 0.0.4"},
{:plug, "~> 1.12"}, {:plug, "~> 1.12"},
{:plug_cowboy, "~> 2.7"}, {:plug_cowboy, "~> 2.7"},
{:quantum, "~> 3.5"} {:quantum, "~> 3.5"}
@@ -40,9 +41,12 @@ defp deps do
Below are the environment variables that you need to set for the program to function: Below are the environment variables that you need to set for the program to function:
``` ```
# needed in development and production
PLANTID_DISCORD_BOT_TOKEN= client secret from the bot's application PLANTID_DISCORD_BOT_TOKEN= client secret from the bot's application
PLANTID_LOGS_DISCORD_WEBHOOK_URL= webhook url for the log channel PLANTID_LOGS_DISCORD_WEBHOOK_URL= webhook url for the log channel
PLANTNET_API_KEY= API key for the PlantNet service PLANTNET_API_KEY= API key for the PlantNet service
# needed in production
PLANTID_FILESERVER_URL URL and port(if needed) for the http file server PLANTID_FILESERVER_URL URL and port(if needed) for the http file server
``` ```
+27
View File
@@ -0,0 +1,27 @@
# 📝 Project TODOs
## 🚀 High Priority
- [x] Update deps and run on Elixir 1.18 / otp 28
- [ ] Run static analysis tools and fix code
- [ ] Increase test coverage
- [ ] Fix spec errors
- [ ] Improve error return message
## 📦 Medium Priority
- [ ] - add credo, dialyxir and test to GH actions
- [ ] - use Req instead of HTTPoison
## 🛠️ Low Priority
- [ ] add checks on branch push / PR creation
- [ ] add specs where missing
## 📚 Documentation
- [ ]
---
✅ Keep this file updated as tasks are completed. Use `git diff` to track progress.
+1
View File
@@ -1,4 +1,5 @@
defmodule PlantIdDiscordBot.Application do defmodule PlantIdDiscordBot.Application do
@moduledoc false
use Application use Application
@impl true @impl true
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Consumer.Commands do defmodule PlantIdDiscordBot.Consumer.Commands do
@moduledoc """
Application commands.
"""
def global_application_commands do def global_application_commands do
[ [
%{ %{
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Metrics.Message do defmodule PlantIdDiscordBot.Metrics.Message do
@moduledoc """
Metrics messaging.
"""
def send() do def send() do
PlantIdDiscordBot.Metrics.requests() PlantIdDiscordBot.Metrics.requests()
|> format_message() |> format_message()
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Metrics.Requests do defmodule PlantIdDiscordBot.Metrics.Requests do
@moduledoc """
Basic metrics with usage count per guild.
"""
use Agent use Agent
alias PlantIdDiscordBot.Metrics.Requests alias PlantIdDiscordBot.Metrics.Requests
alias PlantIdDiscordBot.ProcessRegistry alias PlantIdDiscordBot.ProcessRegistry
+1 -1
View File
@@ -6,7 +6,7 @@ defmodule PlantIdDiscordBotTest.Mocks.Nostrum.Api do
# def create_message(_channel_id, content), do: {:ok, content} # def create_message(_channel_id, content), do: {:ok, content}
def create_message(_channel_id, content) do def create_message(_channel_id, content) do
send(self(), {:create_message, 123456, content}) send(self(), {:create_message, 123_456, content})
{:ok, content} {:ok, content}
end end
+7 -2
View File
@@ -26,10 +26,15 @@ defmodule PlantIdDiscordBot.Consumer do
end end
def handle_event({:MESSAGE_CREATE, %{attachments: attachments} = message, _ws_state}) do def handle_event({:MESSAGE_CREATE, %{attachments: attachments} = message, _ws_state}) do
if length(attachments) > 0 do if !Enum.empty?(attachments) do
# deprecated -> Nostrum.Api.Channel.start_typing/1 in v1.0
Api.start_typing!(message.channel_id) Api.start_typing!(message.channel_id)
Cog.PlantNetMessage.id(message) Cog.PlantNetMessage.id(message)
end end
# if length(attachments) > 0 do
# # deprecated -> Nostrum.Api.Channel.start_typing/1 in v1.0
# Api.start_typing!(message.channel_id)
# Cog.PlantNetMessage.id(message)
# end
end end
end end
+5
View File
@@ -1,4 +1,9 @@
defmodule PlantIdDiscordBot.Cog.Info do defmodule PlantIdDiscordBot.Cog.Info do
@moduledoc """
Functions for using the /info application command.
Returns a message embed of application commands available to the user.
"""
use Nostrum.Consumer use Nostrum.Consumer
import Nostrum.Struct.Embed import Nostrum.Struct.Embed
alias PlantIdDiscordBot.Utils alias PlantIdDiscordBot.Utils
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Cog.PlantNetMessage do defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
@moduledoc """
Functions for sending and receiving data from PLantNet
"""
require Logger require Logger
use Nostrum.Consumer use Nostrum.Consumer
@@ -49,7 +52,6 @@ defmodule PlantIdDiscordBot.Cog.PlantNetMessage do
nil nil
end end
# TODO improve composition
if saved_images do if saved_images do
try do try do
prepare_images(saved_images) prepare_images(saved_images)
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.FileServer do defmodule PlantIdDiscordBot.FileServer do
@moduledoc """
File server for temporary storage of images until a response is made.
"""
use Plug.Builder use Plug.Builder
plug(Plug.Logger) plug(Plug.Logger)
+3 -3
View File
@@ -2,6 +2,8 @@ defmodule PlantIdDiscordBot.FileServer.File do
@moduledoc """ @moduledoc """
File utilities File utilities
""" """
require Logger
alias PlantIdDiscordBot.FileServer.ImageConverter alias PlantIdDiscordBot.FileServer.ImageConverter
@image_path Application.compile_env(:plantid_discord_bot, :image_path) @image_path Application.compile_env(:plantid_discord_bot, :image_path)
@@ -56,7 +58,6 @@ defmodule PlantIdDiscordBot.FileServer.File do
|> File.read() |> File.read()
end end
# TODO make into a task for async deletion and deal with errors
@spec delete_files!([String.t()]) :: :ok @spec delete_files!([String.t()]) :: :ok
def delete_files!(filenames) do def delete_files!(filenames) do
tasks = tasks =
@@ -65,8 +66,7 @@ defmodule PlantIdDiscordBot.FileServer.File do
try do try do
File.rm!(Path.join(@image_path, filename)) File.rm!(Path.join(@image_path, filename))
rescue rescue
# TODO Logger e -> Logger.error(e)
e -> IO.inspect(e)
end end
end) end)
end) end)
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Guild do defmodule PlantIdDiscordBot.Guild do
@moduledoc """
Guild related functions.
"""
alias Nostrum.Cache.GuildCache alias Nostrum.Cache.GuildCache
def get_guild_name!(guild_id) do def get_guild_name!(guild_id) do
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.ProcessRegistry do defmodule PlantIdDiscordBot.ProcessRegistry do
@moduledoc """
Process registry.
"""
def start_link do def start_link do
Registry.start_link(keys: :unique, name: __MODULE__) Registry.start_link(keys: :unique, name: __MODULE__)
end end
+6
View File
@@ -1,4 +1,10 @@
defmodule PlantIdDiscordBot.RateLimiter do defmodule PlantIdDiscordBot.RateLimiter do
@moduledoc """
Basic rate limiter to prevent one guild from calling too many requests in 24 hours.
Custom limits have been set for some guilds that are trusted.
"""
use GenServer use GenServer
require Logger require Logger
+3
View File
@@ -1,4 +1,7 @@
defmodule PlantIdDiscordBot.Utils do defmodule PlantIdDiscordBot.Utils do
@moduledoc """
Bot related utility functions.
"""
def get_uptime() do def get_uptime() do
start_time = Application.get_env(:plantid_discord_bot, :start_time) start_time = Application.get_env(:plantid_discord_bot, :start_time)
+32 -21
View File
@@ -33,7 +33,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
@doc """ @doc """
Parses the response from the PlantNet API into a map. Parses the response from the PlantNet API into a map.
""" """
@spec parse(String.t()) :: map() @spec parse(String.t()) :: String.t()
def parse(response) do def parse(response) do
response response
|> to_map!() |> to_map!()
@@ -85,7 +85,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
"My best guess is **#{best_guess_name}** with a confidence of **#{score}%**.#{get_common_names(best_result)}\n\nSpecies info from plant databases:\n[GBIF](<#{best_result["gbif_url"]}>) | [PFAF](<#{best_result["pfaf_url"]}>) | [POWO](<#{best_result["powo_url"]}>)#{if best_result_iucn_category, do: "\n\nConservation status: #{iucn_parser(best_result_iucn_category)}"}#{get_alternatives(other_results)}" "My best guess is **#{best_guess_name}** with a confidence of **#{score}%**.#{get_common_names(best_result)}\n\nSpecies info from plant databases:\n[GBIF](<#{best_result["gbif_url"]}>) | [PFAF](<#{best_result["pfaf_url"]}>) | [POWO](<#{best_result["powo_url"]}>)#{if best_result_iucn_category, do: "\n\nConservation status: #{iucn_parser(best_result_iucn_category)}"}#{get_alternatives(other_results)}"
end end
@spec generate_gbif_url(map()) :: map() @spec generate_gbif_url([map()]) :: [map()]
defp generate_gbif_url(data) do defp generate_gbif_url(data) do
Enum.map(data, fn result -> Enum.map(data, fn result ->
gbif_id = result["gbif"]["id"] gbif_id = result["gbif"]["id"]
@@ -93,7 +93,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
end) end)
end end
@spec generate_pfaf_url(map()) :: map() @spec generate_pfaf_url([map()]) :: [map()]
defp generate_pfaf_url(data) do defp generate_pfaf_url(data) do
Enum.map(data, fn result -> Enum.map(data, fn result ->
pfaf_slug = String.replace(result["species"]["scientificNameWithoutAuthor"], " ", "+") pfaf_slug = String.replace(result["species"]["scientificNameWithoutAuthor"], " ", "+")
@@ -104,7 +104,7 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
end) end)
end end
@spec generate_powo_url(map()) :: map() @spec generate_powo_url([map()]) :: [map()]
defp generate_powo_url(data) do defp generate_powo_url(data) do
Enum.map(data, fn result -> Enum.map(data, fn result ->
powo_id = result["powo"]["id"] powo_id = result["powo"]["id"]
@@ -123,29 +123,40 @@ defmodule PlantIdDiscordBot.PlantNet.Parser do
end end
@spec iucn_parser(String.t()) :: String.t() @spec iucn_parser(String.t()) :: String.t()
defp iucn_parser(abbreviation) do def iucn_parser("DD"), do: "Data Deficient"
case abbreviation do def iucn_parser("LC"), do: "Least Concern"
"DD" -> "Data Deficient" def iucn_parser("NT"), do: "Near Threatened"
"LC" -> "Least Concern" def iucn_parser("VU"), do: "Vulnerable"
"NT" -> "Near Threatened" def iucn_parser("EN"), do: "Endangered"
"VU" -> "Vulnerable" def iucn_parser("CR"), do: "Critically Endangered"
"EN" -> "Endangered" def iucn_parser("EW"), do: "Extinct in the Wild"
"CR" -> "Critically Endangered" def iucn_parser("EX"), do: "Extinct"
"EW" -> "Extinct in the Wild" def iucn_parser("NE"), do: "Not Evaluated"
"EX" -> "Extinct" def iucn_parser(_), do: "Unknown"
"NE" -> "Not Evaluated"
_ -> "Unknown"
end
end
@spec get_alternatives(map()) :: String.t() @spec get_alternatives(map()) :: String.t()
defp get_alternatives(data) do defp get_alternatives(data) do
if length(data) > 0 do if !Enum.empty?(data) do
alternatives = alternatives =
Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"]) Enum.map_join(data, ", ", & &1["species"]["scientificNameWithoutAuthor"])
|> Enum.join(", ")
"\n\nAlternatives include **#{alternatives}**." "\n\nAlternatives include **#{alternatives}**."
end end
# if !Enum.empty?(data) do
# alternatives =
# Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"])
# |> Enum.join(", ")
# "\n\nAlternatives include **#{alternatives}**."
# end
# if length(data) > 0 do
# alternatives =
# Enum.map(data, & &1["species"]["scientificNameWithoutAuthor"])
# |> Enum.join(", ")
# "\n\nAlternatives include **#{alternatives}**."
# end
end end
end end
+3
View File
@@ -1,3 +1,6 @@
defmodule PlantIdDiscordBot.Scheduler do defmodule PlantIdDiscordBot.Scheduler do
@moduledoc """
CRON job scheduler.
"""
use Quantum, otp_app: :plantid_discord_bot use Quantum, otp_app: :plantid_discord_bot
end end
+11 -1
View File
@@ -1,11 +1,21 @@
# https://rosettacode.org/wiki/Convert_seconds_to_compound_duration
defmodule PlantIdDiscordBot.Utils.Duration do defmodule PlantIdDiscordBot.Utils.Duration do
@moduledoc """
Duration conversion functions.
"""
@minute 60 @minute 60
@hour @minute * 60 @hour @minute * 60
@day @hour * 24 @day @hour * 24
@week @day * 7 @week @day * 7
@divisor [@week, @day, @hour, @minute, 1] @divisor [@week, @day, @hour, @minute, 1]
@doc """
Convert seconds to a time string.
# https://rosettacode.org/wiki/Convert_seconds_to_compound_duration
## Examples
iex> PlantIdDiscordBot.Utils.Duration.sec_to_str(65)
iex> "1m 5s"
"""
def sec_to_str(sec) do def sec_to_str(sec) do
{_, [s, m, h, d, w]} = {_, [s, m, h, d, w]} =
Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} -> Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} ->
+3 -2
View File
@@ -4,8 +4,8 @@ defmodule PlantidDiscordBot.MixProject do
def project do def project do
[ [
app: :plantid_discord_bot, app: :plantid_discord_bot,
version: "0.2.0", version: "0.2.1",
elixir: "~> 1.16", elixir: "~> 1.8",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
deps: deps() deps: deps()
] ]
@@ -24,6 +24,7 @@ defmodule PlantidDiscordBot.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:nostrum, "~> 0.10"}, {:nostrum, "~> 0.10"},
{:httpoison, "~> 2.2"}, {:httpoison, "~> 2.2"},
+3
View File
@@ -1,4 +1,5 @@
%{ %{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castle": {:hex, :castle, "0.3.0", "47b1a550b2348a6d7e60e43ded1df19dca601ed21ef6f267c3dbb1b3a301fbf5", [:mix], [{:forecastle, "~> 0.1.0", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "dbdc1c171520c4591101938a3d342dec70d36b7f5b102a5c138098581e35fcef"}, "castle": {:hex, :castle, "0.3.0", "47b1a550b2348a6d7e60e43ded1df19dca601ed21ef6f267c3dbb1b3a301fbf5", [:mix], [{:forecastle, "~> 0.1.0", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "dbdc1c171520c4591101938a3d342dec70d36b7f5b102a5c138098581e35fcef"},
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
@@ -6,10 +7,12 @@
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"},
"crontab": {:hex, :crontab, "1.1.14", "233fcfdc2c74510cabdbcb800626babef414e7cb13cea11ddf62e10e16e2bf76", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "4e3b9950bc22ae8d0395ffb5f4b127a140005cba95745abf5ff9ee7e8203c6fa"}, "crontab": {:hex, :crontab, "1.1.14", "233fcfdc2c74510cabdbcb800626babef414e7cb13cea11ddf62e10e16e2bf76", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "4e3b9950bc22ae8d0395ffb5f4b127a140005cba95745abf5ff9ee7e8203c6fa"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"}, "forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"},
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
"gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
+4 -4
View File
@@ -7,14 +7,14 @@ defmodule PlantIdDiscordBot.ErrorHandlingTest do
test "do_identification/1 returns invokes logger on error" do test "do_identification/1 returns invokes logger on error" do
message = PlantNetFixtures.Message.message() message = PlantNetFixtures.Message.message()
log = capture_log(fn -> log =
capture_log(fn ->
PlantIdDiscordBot.Cog.PlantNetMessage.do_identification(message) PlantIdDiscordBot.Cog.PlantNetMessage.do_identification(message)
assert_received {:create_message, 123456, content: "An error has occured. Please try again later."} assert_received {:create_message, 123_456,
content: "An error has occured. Please try again later."}
end) end)
IO.inspect(log)
assert log =~ "guild_id=#{message.guild_id}" assert log =~ "guild_id=#{message.guild_id}"
assert log =~ "guild_name=#{@guild.get_guild_name!(message.guild_id)}" assert log =~ "guild_name=#{@guild.get_guild_name!(message.guild_id)}"
end end
+43
View File
@@ -0,0 +1,43 @@
defmodule PlantIdDiscordBotTest.FileServer.MimeTypes do
use ExUnit.Case, async: true
alias PlantIdDiscordBot.FileServer.MimeTypes
doctest MimeTypes
describe "detect_mime_type/1" do
test "detects webp" do
bin = <<0x52, 0x49, 0x46, 0x46, 0x00>>
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/webp"}
end
test "detects jpeg" do
bin = <<0xFF, 0xD8, 0xFF, 0x00>>
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/jpeg"}
end
test "detects png" do
bin = <<0x89, 0x50, 0x4E, 0x47, 0x00>>
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/png"}
end
test "detects gif" do
bin = <<0x47, 0x49, 0x46, 0x38, 0x00>>
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/gif"}
end
test "detects bmp" do
bin = <<0x42, 0x4D, 0x00>>
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/bmp"}
end
test "detects tiff" do
bin = <<0x49, 0x49, 0x2A, 0x00, 0x00>>
assert MimeTypes.detect_mime_type(bin) == {:ok, "image/tiff"}
end
test "returns error for unsupported type" do
bin = <<0x00, 0x11, 0x22, 0x33>>
assert MimeTypes.detect_mime_type(bin) == {:error, "Unsupported file type"}
end
end
end
+15
View File
@@ -63,4 +63,19 @@ defmodule PlantIdDiscordBotTest.PlantNet.Parser do
"My best guess is **Prunus cerasifera** with a confidence of **88%**.\n\nSpecies info from plant databases:\n[GBIF](<https://www.gbif.org/species/3021730>) | [PFAF](<https://pfaf.org/user/Plant.aspx?LatinName=/Prunus+cerasifera>) | [POWO](<https://powo.science.kew.org/taxon/729568-1>)\n\nConservation status: Data Deficient\n\nAlternatives include **Prunus × cistena**." "My best guess is **Prunus cerasifera** with a confidence of **88%**.\n\nSpecies info from plant databases:\n[GBIF](<https://www.gbif.org/species/3021730>) | [PFAF](<https://pfaf.org/user/Plant.aspx?LatinName=/Prunus+cerasifera>) | [POWO](<https://powo.science.kew.org/taxon/729568-1>)\n\nConservation status: Data Deficient\n\nAlternatives include **Prunus × cistena**."
end end
end end
describe "iucn_parser/1" do
test "return correct string for abbreviation" do
assert Parser.iucn_parser("DD") == "Data Deficient"
assert Parser.iucn_parser("LC") == "Least Concern"
assert Parser.iucn_parser("NT") == "Near Threatened"
assert Parser.iucn_parser("VU") == "Vulnerable"
assert Parser.iucn_parser("EN") == "Endangered"
assert Parser.iucn_parser("CR") == "Critically Endangered"
assert Parser.iucn_parser("EW") == "Extinct in the Wild"
assert Parser.iucn_parser("EX") == "Extinct"
assert Parser.iucn_parser("NE") == "Not Evaluated"
assert Parser.iucn_parser("ABC") == "Unknown"
end
end
end end
+2 -2
View File
@@ -2,9 +2,9 @@ ExUnit.start()
defmodule PlantNetFixtures.Message do defmodule PlantNetFixtures.Message do
@message %{ @message %{
channel_id: 1178600825380155412, channel_id: 1_178_600_825_380_155_412,
guild_id: 1_002_507_312_159_797_318, guild_id: 1_002_507_312_159_797_318,
attachments: [%{url: "http://invalid-url.com/image.jpg"}], attachments: [%{url: "http://invalid-url.com/image.jpg"}]
} }
def message(), do: @message def message(), do: @message
+39
View File
@@ -0,0 +1,39 @@
defmodule PlantIdDiscordBotTest.Utils.Duration do
use ExUnit.Case, async: true
alias PlantIdDiscordBot.Utils.Duration
doctest Duration
describe "sec_to_str/1" do
test "seconds" do
assert Duration.sec_to_str(5) == "5s"
refute Duration.sec_to_str(60) == "60s"
end
test "minutes" do
assert Duration.sec_to_str(60) == "1m"
assert Duration.sec_to_str(75) == "1m 15s"
refute Duration.sec_to_str(3600) == "60m"
end
test "hours" do
assert Duration.sec_to_str(3600) == "1h"
assert Duration.sec_to_str(7205) == "2h 5s"
assert Duration.sec_to_str(36_300) == "10h 5m"
assert Duration.sec_to_str(36_315) == "10h 5m 15s"
refute Duration.sec_to_str(86_400) == "24h"
end
test "days" do
assert Duration.sec_to_str(86_400) == "1d"
assert Duration.sec_to_str(86_715) == "1d 5m 15s"
refute Duration.sec_to_str(604_800) == "7d"
end
test "weeks" do
assert Duration.sec_to_str(604_800) == "1w"
assert Duration.sec_to_str(6_048_000) == "10w"
end
end
end