defmodule FarmbotCore.Asset.CriteriaRetrieverTest do use ExUnit.Case, async: true use Mimic alias FarmbotCore.Asset.{ CriteriaRetriever, Point, PointGroup, Repo } setup :verify_on_exit! @fake_point_group %PointGroup{ criteria: %{ "day" => %{"op" => ">", "days_ago" => 4}, "string_eq" => %{ "openfarm_slug" => ["five", "nine"], "meta.created_by" => ["plant-detection"] }, "number_eq" => %{"radius" => [6, 10, 11]}, "number_lt" => %{"x" => 7}, "number_gt" => %{"z" => 8} } } @simple_point_group %PointGroup{ point_ids: [], sort_type: "xy_ascending", criteria: %{ "day" => %{ "op" => "<", "days_ago" => 0 }, "string_eq" => %{ "pointer_type" => ["Plant"] }, "number_eq" => %{}, "number_lt" => %{}, "number_gt" => %{} } } # Use this is a fake "Timex.now()" value when mocking. @now ~U[2222-12-10 02:22:22.222222Z] @five_days_ago ~U[2222-12-05 01:11:11.111111Z] @n 1000 def rand, do: Enum.random(0..@n) / 1 def point!(%{id: id} = params) do point = Map.merge( %Point{ id: id, name: "point #{id}", meta: %{}, plant_stage: "planted", created_at: @now, pointer_type: "Plant", radius: 10.0, tool_id: nil, discarded_at: nil, gantry_mounted: false, x: 0.0, y: 0.0, z: 0.0 }, params ) Repo.insert!(point) point end def point_group_with_fake_points do Repo.delete_all(PointGroup) Repo.delete_all(Point) whitelist = [ %Point{id: 1, x: rand(), y: rand(), z: rand()}, %Point{id: 2, x: rand(), y: rand(), z: rand()}, %Point{id: 3, x: rand(), y: rand(), z: rand()} ] exclusion = [ %Point{created_at: @now}, %Point{openfarm_slug: "not five"}, %Point{radius: 10.0}, %Point{x: 10.0}, %Point{z: 6.0} ] incusion = [ # INCLUSION POINTS %Point{created_at: ~U[2222-12-04 02:22:22.222222Z]}, %Point{openfarm_slug: "five"}, %Point{radius: 6.0}, %Point{x: 7.0}, %Point{z: 8.0} ] points = whitelist ++ exclusion ++ incusion Enum.map(points, fn p -> Repo.insert!(p) end) pg = %PointGroup{@fake_point_group | point_ids: [1, 2, 3]} Repo.insert!(pg) pg end test "direct match on `pointer_type` via `string_eq`" do Repo.delete_all(PointGroup) Repo.delete_all(Point) point!(%{id: 1, pointer_type: "Plant"}) point!(%{id: 2, pointer_type: "Weed"}) point!(%{id: 3, pointer_type: "ToolSlot"}) point!(%{id: 4, pointer_type: "GenericPointer"}) result = CriteriaRetriever.run(@simple_point_group) assert Enum.count(result) == 1 end test "run/1" do expect(Timex, :now, fn -> @now end) pg = point_group_with_fake_points() point!(%{ id: 888, created_at: @five_days_ago, openfarm_slug: "five", meta: %{"created_by" => "not-plant-detection"}, radius: 10.0, x: 6.0, z: 9.0 }) perfect_match = point!(%{ id: 999, created_at: @five_days_ago, openfarm_slug: "five", meta: %{"created_by" => "plant-detection"}, radius: 10.0, x: 6.0, z: 9.0 }) expected = [perfect_match.id, 1, 2, 3] results = Enum.map(CriteriaRetriever.run(pg), fn p -> p.id end) assert Enum.count(expected) == Enum.count(results) Enum.map(expected, fn id -> assert Enum.member?(results, id) end) end @tag :capture_log test "point group that does not define criteria" do Repo.delete_all(PointGroup) Repo.delete_all(Point) whitelist = [88457, 88455] point!(%{ created_at: ~U[2020-01-09 19:09:39.176321Z], discarded_at: nil, gantry_mounted: false, id: 88455, meta: %{}, name: "Spinach", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 400.0, y: 100.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:39.413318Z], discarded_at: nil, gantry_mounted: false, id: 88456, meta: %{}, name: "Spinach", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 400.0, y: 300.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:39.610901Z], discarded_at: nil, gantry_mounted: false, id: 88457, meta: %{}, name: "Spinach", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 600.0, y: 100.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:39.824048Z], discarded_at: nil, gantry_mounted: false, id: 88458, meta: %{}, name: "Spinach", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 600.0, y: 300.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:40.012075Z], discarded_at: nil, gantry_mounted: false, id: 88459, meta: %{}, name: "Spinach", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 800.0, y: 300.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:40.202385Z], discarded_at: nil, gantry_mounted: false, id: 88460, meta: %{}, name: "Spinach", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 800.0, y: 100.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:40.402777Z], discarded_at: nil, gantry_mounted: false, id: 88461, meta: %{}, name: "Broccoli", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 600.0, y: 700.0, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:40.776337Z], discarded_at: nil, gantry_mounted: false, id: 88462, meta: %{}, name: "Beets", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 800.0, y: 1.1e3, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:40.960424Z], discarded_at: nil, gantry_mounted: false, id: 88463, meta: %{}, name: "Beets", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 600.0, y: 1.1e3, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:41.171967Z], discarded_at: nil, gantry_mounted: false, id: 88464, meta: %{}, name: "Beets", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 400.0, y: 1.1e3, z: 0.0 }) point!(%{ created_at: ~U[2020-01-09 19:09:41.393021Z], discarded_at: nil, gantry_mounted: false, id: 88465, meta: %{}, name: "Beets", openfarm_slug: nil, plant_stage: "planned", planted_at: nil, pointer_type: "Plant", radius: 25.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 200.0, y: 1.1e3, z: 0.0 }) point!(%{ created_at: ~U[2020-02-21 01:41:07.714000Z], discarded_at: nil, gantry_mounted: true, id: 88798, meta: %{}, name: "Tool Slot", openfarm_slug: nil, plant_stage: nil, planted_at: nil, pointer_type: "ToolSlot", radius: nil, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 0.0, y: 0.0, z: -20.0 }) point!(%{ created_at: ~U[2020-02-21 18:30:56.301000Z], discarded_at: nil, gantry_mounted: false, id: 88805, meta: %{}, name: "Slot", openfarm_slug: nil, plant_stage: nil, planted_at: nil, pointer_type: "ToolSlot", radius: nil, tool_id: 12143, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 200.0, y: 200.0, z: 0.0 }) point!(%{ created_at: ~U[2020-02-21 18:47:45.170000Z], discarded_at: nil, gantry_mounted: false, id: 88806, meta: %{}, name: "Slot", openfarm_slug: nil, plant_stage: nil, planted_at: nil, pointer_type: "ToolSlot", radius: nil, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 0.0, y: 0.0, z: 0.0 }) point!(%{ created_at: ~U[2020-02-21 20:01:59.960000Z], discarded_at: nil, gantry_mounted: false, id: 88828, meta: %{ "color" => "green", "created_by" => "farm-designer", "type" => "point" }, name: "Created Point", openfarm_slug: nil, plant_stage: nil, planted_at: nil, pointer_type: "GenericPointer", radius: 15.0, tool_id: nil, updated_at: ~U[1970-11-07 16:52:31.618000Z], x: 1.0, y: 1.0, z: 0.0 }) point!(%{ created_at: ~U[2020-02-29 21:08:40.934000Z], discarded_at: nil, gantry_mounted: false, id: 88887, meta: %{ "color" => "red", "created_by" => "farm-designer", "type" => "weed" }, name: "Created Weed", openfarm_slug: nil, plant_stage: nil, planted_at: nil, pointer_type: "GenericPointer", radius: 15.0, tool_id: nil, updated_at: ~U[2020-02-29 21:08:40.934000Z], x: 100.0, y: 100.0, z: 0.0 }) pg = %PointGroup{ created_at: ~U[2020-02-29 21:14:33.337000Z], criteria: %{ "day" => %{"days_ago" => 0, "op" => "<"}, "number_eq" => %{}, "number_gt" => %{}, "number_lt" => %{}, "string_eq" => %{} }, id: 201, name: "Test (Broke?)", point_ids: whitelist, sort_type: "xy_ascending", updated_at: ~U[2020-03-02 21:55:26.973000Z] } Repo.insert!(pg) results = Enum.map(CriteriaRetriever.run(pg), fn p -> p.id end) assert Enum.count(whitelist) == Enum.count(results) Enum.map(whitelist, fn id -> assert Enum.member?(results, id) end) end test "edge case: Filter by crop type" do Repo.delete_all(PointGroup) Repo.delete_all(Point) ok = point!(%{id: 1, pointer_type: "Plant", openfarm_slug: "spinach"}) point!(%{id: 2, pointer_type: "Plant", openfarm_slug: "beetroot"}) point!(%{id: 3, pointer_type: "Weed", openfarm_slug: "thistle"}) point!(%{id: 4, pointer_type: "Weed", openfarm_slug: "spinach"}) pg = %PointGroup{ :id => 241, :point_ids => [], :criteria => %{ "day" => %{ "op" => "<", "days_ago" => 0 }, "string_eq" => %{ "pointer_type" => ["Plant"], "openfarm_slug" => ["spinach"] }, "number_eq" => %{}, "number_lt" => %{}, "number_gt" => %{} } } ids = CriteriaRetriever.run(pg) |> Enum.map(fn p -> p.id end) assert Enum.member?(ids, ok.id) assert Enum.count(ids) == 1 end test "edge case: Retrieves by `day` criteria only" do Repo.delete_all(PointGroup) Repo.delete_all(Point) days_ago4 = Timex.shift(@now, days: -4) days_ago2 = Timex.shift(@now, days: -2) expect(Timex, :now, fn -> @now end) point!(%{id: 1, pointer_type: "Plant", created_at: days_ago4}) p2 = point!(%{id: 2, pointer_type: "Plant", created_at: days_ago2}) pg1 = %PointGroup{ id: 212, created_at: Timex.shift(@now, hours: -1), updated_at: Timex.shift(@now, hours: -1), name: "Less than 2 days ago", point_ids: [], sort_type: "yx_descending", criteria: %{ day: %{"op" => "<", "days_ago" => 3}, string_eq: %{}, number_eq: %{}, number_lt: %{}, number_gt: %{} } } ids = CriteriaRetriever.run(pg1) |> Enum.map(fn p -> p.id end) assert Enum.count(ids) == 1 assert Enum.member?(ids, p2.id) end test "edge case: Filter by slot direction" do Repo.delete_all(PointGroup) Repo.delete_all(Point) ok = point!(%{id: 1, pointer_type: "ToolSlot", pullout_direction: 3}) point!(%{id: 2, pointer_type: "Weed", pullout_direction: 3}) point!(%{id: 3, pointer_type: "ToolSlot", pullout_direction: 4}) point!(%{id: 4, pointer_type: "GenericPointer", pullout_direction: 2}) pg = %PointGroup{ :id => 242, :name => "Filter by slot direction", :point_ids => [], :sort_type => "xy_ascending", :criteria => %{ "day" => %{ "op" => "<", "days_ago" => 0 }, "string_eq" => %{ "pointer_type" => ["ToolSlot"] }, "number_eq" => %{ "pullout_direction" => [3] }, "number_lt" => %{}, "number_gt" => %{} } } ids = CriteriaRetriever.run(pg) |> Enum.map(fn p -> p.id end) assert Enum.member?(ids, ok.id) assert Enum.count(ids) == 1 end end