diff --git a/TODO.md b/TODO.md index 626fc16..2e46e0d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,13 @@ -- Validate before build +x- Validate before build +x- Named nodes with graph name +x- Add documentation +x- Use nid to connect +X- Get nid for a node +- A node must have a function +- The function in Broadcast and Funnel nodes can be optional - Add Supervisors functionality -- Named nodes with graph prefix -- Improve test +- Use counters for stats +x- Improve test - Improve API + - Update to GenStage diff --git a/lib/exstreme/gnode/behaviour.ex b/lib/exstreme/gnode/behaviour.ex index d92b484..01c4a20 100644 --- a/lib/exstreme/gnode/behaviour.ex +++ b/lib/exstreme/gnode/behaviour.ex @@ -1,20 +1,25 @@ defmodule Exstreme.GNode.Behaviour do @moduledoc """ + This behaviour defines what means to be a node in the Graph """ + @doc """ + Every node in the Graph must use this macro + """ defmacro __using__(_) do quote do use GenServer defmodule Data do @moduledoc """ + The data for each node """ alias __MODULE__ @type graph_func :: ((term, Data.t) -> {:ok, term} | :error) - @type t :: %Data{next: [pid], pid: pid, nid: atom, func: graph_func, funnel_queue: [term], received_counter: non_neg_integer, sent_counter: non_neg_integer, opts: [key: term]} - defstruct [next: [], pid: nil, nid: nil, func: nil, funnel_queue: [], received_counter: 0, sent_counter: 0, opts: []] + @type t :: %Data{next: [atom], nid: atom, func: graph_func, funnel_queue: [term], received_counter: non_neg_integer, sent_counter: non_neg_integer, opts: [key: term]} + defstruct [next: [], nid: nil, func: nil, funnel_queue: [], received_counter: 0, sent_counter: 0, opts: []] #TODO use counters end @@ -28,21 +33,21 @@ defmodule Exstreme.GNode.Behaviour do func = Keyword.get(params, :func) nid = Keyword.get(params, :nid) opts = Keyword.drop(params, [:func, :type, :nid]) - {:ok, %Data{func: func, pid: self, nid: nid, opts: opts}} + {:ok, %Data{func: func, nid: nid, opts: opts}} end - def handle_cast({:connect, to_pid}, data) do - new_data = update_in(data.next, fn(next) -> [to_pid | next] end) + def handle_cast({:connect, to_nid}, data) do + new_data = update_in(data.next, fn(next) -> [to_nid | next] end) {:noreply, new_data} end - def handle_cast({:send_next, next, msg}, data) do + def handle_info({:send_next, next, msg}, data) do Enum.each(next, &(GenServer.cast(&1, {:next, data, msg}))) {:noreply, data} end def send_next(pid, next, msg) do - GenServer.cast(pid, {:send_next, next, msg}) + send pid, {:send_next, next, msg} end end end diff --git a/lib/exstreme/gnode/broadcast.ex b/lib/exstreme/gnode/broadcast.ex index 63776d8..2d0f91e 100644 --- a/lib/exstreme/gnode/broadcast.ex +++ b/lib/exstreme/gnode/broadcast.ex @@ -1,8 +1,10 @@ defmodule Exstreme.GNode.Broadcast do use Exstreme.GNode.Behaviour + # Broadcasts the message to the next nodes def handle_cast({:next, _, msg}, data) do - send_next(self, data.next, msg) + {:ok, result} = data.func.(msg, data) + send_next(self, data.next, result) {:noreply, data} end end diff --git a/lib/exstreme/gnode/common.ex b/lib/exstreme/gnode/common.ex index afe2a3f..964b696 100644 --- a/lib/exstreme/gnode/common.ex +++ b/lib/exstreme/gnode/common.ex @@ -1,6 +1,7 @@ defmodule Exstreme.GNode.Common do use Exstreme.GNode.Behaviour + # Sends the result to the next one def handle_cast({:next, _, msg}, data) do {:ok, result} = data.func.(msg, data) send_next(self, data.next, result) diff --git a/lib/exstreme/gnode/funnel.ex b/lib/exstreme/gnode/funnel.ex index 2d9cb81..45d240d 100644 --- a/lib/exstreme/gnode/funnel.ex +++ b/lib/exstreme/gnode/funnel.ex @@ -1,6 +1,8 @@ defmodule Exstreme.GNode.Funnel do use Exstreme.GNode.Behaviour + # Receives a message and saves it in a messages map queue, + # when all the messages are done sends a the map def handle_cast({:next, from_data, msg}, data) do {result, new_queue} = data.funnel_queue @@ -10,8 +12,10 @@ defmodule Exstreme.GNode.Funnel do {:noreply, update_in(data.funnel_queue, fn(_) -> new_queue end)} end + #private + # Sends the result to the next one defp send_message(result, data) do if result != nil do new_msg = Map.values(result) @@ -20,15 +24,17 @@ defmodule Exstreme.GNode.Funnel do end end + # Adds a message in the map queue defp add_queue(queue, from, msg) do idx = Enum.find_index(queue, &(!Map.has_key?(&1, from))) if idx != nil do List.update_at(queue, idx, &(Map.put(&1, from, msg))) else - [Map.new |> Map.put(from, msg) | queue] + queue ++ [Map.new |> Map.put(from, msg)] end end + # Gets a message from the map queue defp get_queue(queue = [head | tail], before_nodes) do before_nodes_set = MapSet.new(before_nodes) if MapSet.equal?(MapSet.new(Map.keys(head)), before_nodes_set) do diff --git a/lib/exstreme/gnode/supervisor.ex b/lib/exstreme/gnode/supervisor.ex new file mode 100644 index 0000000..41b931f --- /dev/null +++ b/lib/exstreme/gnode/supervisor.ex @@ -0,0 +1,3 @@ +defmodule Exstreme.Supervisor do + +end diff --git a/lib/exstreme/graph.ex b/lib/exstreme/graph.ex index 7f34270..26ba3c6 100644 --- a/lib/exstreme/graph.ex +++ b/lib/exstreme/graph.ex @@ -1,14 +1,17 @@ defmodule Exstreme.Graph do @moduledoc """ + Provides information for a Graph """ alias __MODULE__ @typedoc """ + Represents the Graph data """ @type t :: %Graph{name: String.t,params: [key: term], nodes: %{key: [key: term]}, connections: %{key: atom}} defstruct name: '', params: [], nodes: %{}, connections: %{} @doc """ + Counts the Graph nodes """ @spec count_nodes(t) :: non_neg_integer def count_nodes(%Graph{nodes: nodes}) do @@ -18,6 +21,7 @@ defmodule Exstreme.Graph do end @doc """ + Counts the connections """ @spec count_connections(t) :: non_neg_integer def count_connections(%Graph{connections: connections}) do @@ -28,6 +32,7 @@ defmodule Exstreme.Graph do end @doc """ + Counts the connected, unconnected, begin and end nodes """ @spec connections_stats(t) :: %{key: integer} def connections_stats(graph) do @@ -39,6 +44,7 @@ defmodule Exstreme.Graph do end @doc """ + Gets the starting node """ @spec find_start_node(t) :: [atom] def find_start_node(%Graph{nodes: nodes, connections: connections}) do @@ -53,6 +59,7 @@ defmodule Exstreme.Graph do end @doc """ + Gets the last nodes """ @spec find_last_node(t) :: [atom] def find_last_node(%Graph{nodes: nodes, connections: connections}) do @@ -66,6 +73,9 @@ defmodule Exstreme.Graph do |> Enum.filter(is_last?) end + @doc """ + Gets the nodes before the current one + """ @spec get_before_nodes(t, atom) :: [atom] def get_before_nodes(%Graph{connections: connections}, node) do compare_func = @@ -77,8 +87,11 @@ defmodule Exstreme.Graph do end) |> Enum.uniq end + @doc """ + Gets the nodes after the current one + """ @spec get_after_nodes(t, atom) :: [atom] - def get_after_nodes(%Graph{nodes: nodes, connections: connections}, node) do + def get_after_nodes(%Graph{connections: connections}, node) do compare_func = fn(current_node, {from, to}) -> {current_node == from, to} @@ -88,8 +101,21 @@ defmodule Exstreme.Graph do end) |> Enum.uniq end + @doc """ + Gets the name in the Graph for one node + """ + @spec nid(t, atom) :: atom + def nid(%Graph{name: name}, node) do + [char, rest] = + node + |> Atom.to_string + |> String.codepoints + String.to_atom("#{char}_#{name}_#{rest}") + end + # private + # Map the connections to the kind of connection @spec map_to_connections(t) :: [atom] defp map_to_connections(%Graph{nodes: nodes, connections: connections}) do to_connections = @@ -107,11 +133,13 @@ defmodule Exstreme.Graph do |> Enum.map(to_connections) end + # Checks if a node is the first position of a connection @spec at_first?(%{key: atom}, atom) :: boolean - defp at_first?(connections, node) do + defp at_first?(connections, node) do Map.has_key?(connections, node) end + # Checks if a node is the last position of a connection @spec at_last?(%{key: atom}, atom) :: boolean defp at_last?(connections, node) do connections diff --git a/lib/exstreme/graph_builder.ex b/lib/exstreme/graph_builder.ex index 975dbd8..24bf80d 100644 --- a/lib/exstreme/graph_builder.ex +++ b/lib/exstreme/graph_builder.ex @@ -1,5 +1,6 @@ defmodule Exstreme.GraphBuilder do @moduledoc """ + Builds the Graph into a Supervision tree of process """ alias Exstreme.GNode.Broadcast alias Exstreme.GNode.Funnel @@ -22,6 +23,7 @@ defmodule Exstreme.GraphBuilder do #private + # Returns a Graph with the before_nodes and after_nodes set @spec update_nodes_relations(Graph.t) :: Graph.t defp update_nodes_relations(graph) do update_node_func = @@ -37,6 +39,7 @@ defmodule Exstreme.GraphBuilder do update_in(graph.nodes, &(&1 |> Enum.map(update_node_func) |> Map.new)) end + # Starts all the nodes @spec start_nodes(Graph.t) :: Graph.t defp start_nodes(graph) do update_in(graph.nodes, fn(nodes) -> @@ -46,25 +49,31 @@ defmodule Exstreme.GraphBuilder do end) end + # Starts a node @spec start_node({atom, [term: any]}) :: {atom, [key: any]} defp start_node({node, params}) do type = Keyword.get(params, :type) - params = Keyword.put(params, :nid, node) - {:ok, pid} = + params = + params + |> Keyword.put(:nid, node) + |> Keyword.put_new(:func, fn(data, _) -> {:ok, data} end) + {:ok, _} = case type do :broadcast -> Broadcast.start_link(params) :funnel -> Funnel.start_link(params) _ -> Common.start_link(params) end - {node, Keyword.put(params, :pid, pid)} + {node, params} end + # Connects all nodes @spec connect_nodes(Graph.t) :: Graph.t defp connect_nodes(graph = %Graph{nodes: nodes, connections: connections}) do Enum.each(connections, &(connect_pair(&1, nodes))) graph end + # Connects two nodes @spec connect_pair({atom, [atom]}, %{key: [key: term]}) :: no_return defp connect_pair({from, to}, nodes) when is_list(to) do Enum.map(1..Enum.count(to), fn(_) -> from end) @@ -72,10 +81,11 @@ defmodule Exstreme.GraphBuilder do |> Enum.each(&(connect_pair(&1, nodes))) end + # Connects two nodes @spec connect_pair({atom, atom}, %{key: [key: term]}) :: no_return defp connect_pair({from, to}, nodes) when is_atom(to) do - pid_from = Keyword.get(nodes[from], :pid) - pid_to = Keyword.get(nodes[to], :pid) - GenServer.cast(pid_from, {:connect, pid_to}) + nid_from = Keyword.get(nodes[from], :nid) + nid_to = Keyword.get(nodes[to], :nid) + GenServer.cast(nid_from, {:connect, nid_to}) end end diff --git a/lib/exstreme/graph_creator.ex b/lib/exstreme/graph_creator.ex index c046296..236173b 100644 --- a/lib/exstreme/graph_creator.ex +++ b/lib/exstreme/graph_creator.ex @@ -1,5 +1,6 @@ defmodule Exstreme.GraphCreator do @moduledoc """ + Creates a Graph representation """ alias Exstreme.Graph @@ -11,6 +12,7 @@ defmodule Exstreme.GraphCreator do @type update_map_func :: (%{key: atom} -> %{key: atom}) @doc """ + Creates a Graph generating a name """ @spec create_graph([key: term]) :: Graph.t def create_graph(params) do @@ -22,85 +24,103 @@ defmodule Exstreme.GraphCreator do end @doc """ + Creates a Graph with a given name """ @spec create_graph(String.t, [key: term]) :: Graph.t def create_graph(name, params), do: %Graph{name: name, params: params} @doc """ + Creates a simple node """ @spec create_node(Graph.t, [key: term]) :: {Graph.t, atom} - def create_node(graph = %Graph{name: name, nodes: nodes}, params \\ []) do - key = next_node_key(name, nodes) + def create_node(graph = %Graph{nodes: nodes}, params \\ []) do + key = next_node_key(graph, nodes) new_graph = update_in(graph.nodes, &(Map.put(&1, key, params))) {new_graph, key} end @doc """ + Creates a broadcast node """ @spec create_broadcast(Graph.t, [key: term]) :: {Graph.t, atom} - def create_broadcast(graph = %Graph{name: name, nodes: nodes}, params \\ []) do - key = next_broadcast_key(name, nodes) + def create_broadcast(graph = %Graph{nodes: nodes}, params \\ []) do + key = next_broadcast_key(graph, nodes) new_graph = update_in(graph.nodes, &(Map.put(&1, key, params))) {new_graph, key} end @doc """ + Creates a funnel node """ @spec create_funnel(Graph.t, [key: term]) :: {Graph.t, atom} - def create_funnel(graph = %Graph{name: name, nodes: nodes}, params \\ []) do - key = next_funnel_key(name, nodes) + def create_funnel(graph = %Graph{nodes: nodes}, params \\ []) do + key = next_funnel_key(graph, nodes) new_graph = update_in(graph.nodes, &(Map.put(&1, key, params))) {new_graph, key} end @doc """ + Adds a connection between two nodes """ @spec add_connection(Graph.t, atom, atom) :: Graph.t - def add_connection(graph = %Graph{nodes: nodes}, start, finish) when start !== finish do - if has_node(nodes, start) and has_node(nodes, finish) do - update_in(graph.connections, store_connection_fn(start, finish)) + def add_connection(graph = %Graph{nodes: nodes}, start, finish) do + if start !== finish do + if validate_node_exist(nodes, start) and validate_node_exist(nodes, finish) do + update_in(graph.connections, store_connection_fn(start, finish)) + end + else + raise ArgumentError, message: "You can't connect to the same node" end end # private + # Adds a connection to the connections map @spec store_connection_fn(atom, atom) :: update_map_func defp store_connection_fn(start, finish) do fn(connections) -> add_connection = fn(keywords, start, finish) -> - Map.update(keywords, start, finish, fn(value)-> - if is_list(value) do + Map.update(keywords, start, finish, fn + (value) when is_list(value) -> [finish | value] - else + (value) -> [finish, value] - end end) end # validates before adding connections - |> validate_repeated(start, finish) - |> validate_repeated(finish, start) |> validate_position(start, :start) |> validate_position(finish, :end) + |> validate_repeated(start, finish) + |> validate_repeated(finish, start) |> add_connection.(start, finish) end end # validations before adding a node + # Validates when a relation already exists @spec validate_repeated(%{key: atom}, atom, atom) :: %{key: atom} defp validate_repeated(connections, start, finish) do - if Enum.any?(connections, &(&1 == {start, finish})) do + validate = fn + ({key, list}) when is_list(list) -> + key == start && Enum.member?(list, finish) + (connection) -> + connection == {start, finish} + end + + if Enum.any?(connections, validate) do raise ArgumentError, message: "there is already a connection like that" else connections end end + # Validates the node position @spec validate_position(%{key: atom}, atom, :start | :end) :: %{key: atom} defp validate_position(connections, node, position) do case node |> Atom.to_string |> String.first do @@ -111,29 +131,35 @@ defmodule Exstreme.GraphCreator do end end + # Validates if a normal node can be at the beginning of the relation @spec validate_position_node(%{key: atom}, atom, :start) :: %{key: atom} defp validate_position_node(connections, node, :start) do validate_position_start(connections, node,"the node can't be twice at start position #{node}") end + # Validates if a normal node can be at the end of the relation @spec validate_position_node(%{key: atom}, atom, :end) :: %{key: atom} defp validate_position_node(connections, node, :end) do validate_position_end(connections, node,"the node can't be twice at end position") end + # Validates if a broadcast node can be at the beginning of the relation @spec validate_position_broadcast(%{key: atom}, atom, :start) :: %{key: atom} defp validate_position_broadcast(connections, _node, :start), do: connections + # Validates if a broadcast node can be at the end of the relation @spec validate_position_broadcast(%{key: atom}, atom, :end) :: %{key: atom} defp validate_position_broadcast(connections, bct, :end) do validate_position_end(connections, bct, "the broadcast can't be twice at end position") end + # Validates if a funnel node can be at the beginning of the relation @spec validate_position_funnel(%{key: atom}, atom, :start) :: %{key: atom} defp validate_position_funnel(connections, node, :start) do validate_position_start(connections, node,"the funnel can't be twice at start position #{node}") end + # Validates if a funnel node can be at the normal of the relation @spec validate_position_funnel(%{key: atom}, atom, :end) :: %{key: atom} defp validate_position_funnel(connections, _node, :end), do: connections @@ -160,8 +186,9 @@ defmodule Exstreme.GraphCreator do end end - @spec has_node(%{key: atom}, atom) :: true - defp has_node(nodes, node) do + # Validates the node exist on the graph + @spec validate_node_exist(%{key: [key: term]}, atom) :: true + defp validate_node_exist(nodes, node) do if Map.has_key?(nodes, node) do true else @@ -169,30 +196,33 @@ defmodule Exstreme.GraphCreator do end end - @spec next_node_key(String.t, %{key: atom}) :: atom - defp next_node_key(name, nodes) do - next_key(name, nodes, "n") + # Gets the next key for the node, these begin with the 'n' letter + @spec next_node_key(Graph.t, %{key: atom}) :: atom + defp next_node_key(graph, nodes) do + next_key(graph, nodes, "n") end - @spec next_broadcast_key(String.t, %{key: atom}) :: atom - defp next_broadcast_key(name, nodes) do - next_key(name, nodes, "b") + # Gets the next key for the broadcast, these begin with the 'b' letter + @spec next_broadcast_key(Graph.t, %{key: atom}) :: atom + defp next_broadcast_key(graph, nodes) do + next_key(graph, nodes, "b") end - @spec next_funnel_key(String.t, %{key: atom}) :: atom - defp next_funnel_key(name, nodes) do - next_key(name, nodes, "f") + # Gets the next key for the funnel, these begin with the 'f' letter + @spec next_funnel_key(Graph.t, %{key: atom}) :: atom + defp next_funnel_key(graph, nodes) do + next_key(graph, nodes, "f") end - @spec next_key(String.t, %{key: atom}, String.t) :: atom - defp next_key(name, map, letter) do - prefix = "#{letter}_#{name}_" + # Gets the next key according to the given letter + @spec next_key(Graph.t, %{key: atom}, String.t) :: atom + defp next_key(graph, map, letter) do count = map |> Map.keys |> Enum.map(&Atom.to_string/1) - |> Enum.filter(&(String.starts_with?(&1, prefix))) + |> Enum.filter(&(String.starts_with?(&1, letter))) |> Enum.count - String.to_atom("#{prefix}#{count + 1}") + Graph.nid(graph, String.to_atom("#{letter}#{count + 1}")) end end diff --git a/lib/exstreme/graph_validator.ex b/lib/exstreme/graph_validator.ex index 06a2b20..590b493 100644 --- a/lib/exstreme/graph_validator.ex +++ b/lib/exstreme/graph_validator.ex @@ -1,13 +1,16 @@ defmodule Exstreme.GraphValidator do alias Exstreme.Graph @moduledoc """ + Validates the Graph """ @typedoc """ + Validation error """ @type error :: {:error, String.t} @doc """ + Ensures the Graph is valid """ @spec validate(Graph.t) :: :ok | error def validate(graph) do @@ -19,6 +22,7 @@ defmodule Exstreme.GraphValidator do # private + # Validates that the Graph is not empty @spec validate_must_have_connections(Graph.t) :: :ok | error defp validate_must_have_connections(graph) do nodes_amount = Graph.count_nodes(graph) @@ -30,6 +34,7 @@ defmodule Exstreme.GraphValidator do end end + # Validates it must have just one start node and must be a common one @spec validate_start_nodes(Graph.t) :: :ok | error defp validate_start_nodes(graph) do start_nodes = Graph.find_start_node(graph) @@ -39,11 +44,13 @@ defmodule Exstreme.GraphValidator do do: :ok end + # Validates it must have just one start node @spec validate_should_start_with_one_node([atom, ...]) :: :ok | error defp validate_should_start_with_one_node([_]), do: :ok defp validate_should_start_with_one_node(_), do: {:error, ""} + # The start node must be a common one @spec validate_should_start_with_node([atom, ...]) :: :ok | error defp validate_should_start_with_node([start_node]) do start_char = start_node |> Atom.to_string |> String.first @@ -54,6 +61,7 @@ defmodule Exstreme.GraphValidator do end end + # The Graph is connected @spec validate_connectivity(Graph.t) :: :ok | error defp validate_connectivity(graph) do stats = Graph.connections_stats(graph) diff --git a/test/exstreme/graph_builder_test.exs b/test/exstreme/graph_builder_test.exs index f1954f1..982bdf4 100644 --- a/test/exstreme/graph_builder_test.exs +++ b/test/exstreme/graph_builder_test.exs @@ -8,11 +8,11 @@ defmodule Exstreme.GraphBuilderTest do test "creates a graph and check built params" do graph_built = GraphBuilder.build(create_graph) assert graph_built != create_graph - Enum.each(graph_built.nodes, fn({_, params}) -> - assert Keyword.has_key?(params, :pid) + Enum.each(graph_built.nodes, fn({nid, params}) -> assert Keyword.has_key?(params, :after_nodes) assert Keyword.has_key?(params, :before_nodes) - assert Keyword.get(params, :pid) != nil + assert Keyword.get(params, :nid) != nil + assert nid |> Process.whereis |> Process.alive? == true end) end @@ -20,21 +20,17 @@ defmodule Exstreme.GraphBuilderTest do graph_built = GraphBuilder.build(create_graph) [start_node] = Graph.find_start_node(graph_built) [last_node] = Graph.find_last_node(graph_built) - start_node_pid = Keyword.get(graph_built.nodes[start_node], :pid) - last_node_pid = Keyword.get(graph_built.nodes[last_node], :pid) - GenServer.cast(last_node_pid, {:connect, self}) - GenServer.cast(start_node_pid, {:next, self, {:sum, 0}}) - assert_receive {_, {:next, last_node_pid, {:sum, 2}}} + GenServer.cast(last_node, {:connect, self}) + GenServer.cast(start_node, {:next, self, {:sum, 0}}) + assert_receive {_, {:next, _, {:sum, 2}}} end test "sends a message to the graph with many nodes" do graph_built = GraphBuilder.build(graph_many_nodes) [start_node] = Graph.find_start_node(graph_built) [last_node] = Graph.find_last_node(graph_built) - start_node_pid = Keyword.get(graph_built.nodes[start_node], :pid) - last_node_pid = Keyword.get(graph_built.nodes[last_node], :pid) - GenServer.cast(last_node_pid, {:connect, self}) - GenServer.cast(start_node_pid, {:next, self, {:sum, 0}}) - assert_receive {_, {:next, last_node_pid, {:sum, 7}}} + GenServer.cast(last_node, {:connect, self}) + GenServer.cast(start_node, {:next, self, {:sum, 0}}) + assert_receive {_, {:next, _, {:sum, 7}}} end end diff --git a/test/exstreme/graph_creator_test.exs b/test/exstreme/graph_creator_test.exs index 026261c..6d5496b 100644 --- a/test/exstreme/graph_creator_test.exs +++ b/test/exstreme/graph_creator_test.exs @@ -5,82 +5,87 @@ defmodule Exstreme.GraphCreatorTest do alias Exstreme.Graph doctest Exstreme.GraphCreator + setup do + graph = create_graph + {:ok, graph: graph, n1: Graph.nid(graph, :n1), n2: Graph.nid(graph, :n2 )} + end + describe "when the graph is valid" do - test "creates a valid graph struct" do + test "creates a valid graph struct", %{graph: graph} do compare_graph = %Graph{ name: graph_name, nodes: %{n_demo_1: params, n_demo_2: params}, connections: %{n_demo_1: :n_demo_2} } - assert create_graph == compare_graph + assert graph == compare_graph end end describe "when the graph is invalid" do - test "throws an error when adding again the relation" do + test "throws an error when adding again the relation", %{graph: graph, n1: n1, n2: n2} do assert_raise ArgumentError, fn -> - create_graph - |> GraphCreator.add_connection(:n1, :n2) + graph + |> GraphCreator.add_connection(n1, n2) end end - test "throws an error when adding again a self relation" do - assert_raise FunctionClauseError, fn -> - create_graph - |> GraphCreator.add_connection(:n1, :n1) + test "throws an error when adding again a self relation", %{graph: graph, n1: n1} do + assert_raise ArgumentError, "You can't connect to the same node", fn -> + graph + |> GraphCreator.add_connection(n1, n1) end end - test "throws an error when adding a cicle relation" do + test "throws an error when adding a cicle relation", %{graph: graph, n1: n1, n2: n2} do assert_raise ArgumentError, "there is already a connection like that", fn -> - create_graph - |> GraphCreator.add_connection(:n2, :n1) + graph + |> GraphCreator.add_connection(n2, n1) end end end describe "adding nodes" do - test "can create n3 and add a relation between n2 and n3" do + test "can create n3 and add a relation between n2 and n3", %{graph: graph, n2: n2} do compare_graph = %Graph{ name: graph_name, - nodes: %{n1: params, n2: params, n3: params}, - connections: %{n1: :n2, n2: :n3} + nodes: %{n_demo_1: params, n_demo_2: params, n_demo_3: params}, + connections: %{n_demo_1: :n_demo_2, n_demo_2: :n_demo_3} } - {graph, n3} = GraphCreator.create_node(create_graph, params) - new_graph = GraphCreator.add_connection(graph, :n2, n3) + {graph, n3} = GraphCreator.create_node(graph, params) + new_graph = GraphCreator.add_connection(graph, n2, n3) assert new_graph == compare_graph end - test "can add a broadcast an many nodes to the broadcast" do + test "can add a broadcast an many nodes to the broadcast", %{graph: graph, n2: n2} do compare_graph = %Graph{ name: graph_name, - nodes: %{n1: params, n2: params, b1: params_broadcast, n3: params, n4: params}, - connections: %{n1: :n2, n2: :b1, b1: [:n4, :n3]} + nodes: %{n_demo_1: params, n_demo_2: params, b_demo_1: params_broadcast, n_demo_3: params, n_demo_4: params}, + connections: %{n_demo_1: :n_demo_2, n_demo_2: :b_demo_1, b_demo_1: [:n_demo_4, :n_demo_3]} } - {graph, b1} = GraphCreator.create_broadcast(create_graph, params_broadcast) + {graph, b1} = GraphCreator.create_broadcast(graph, params_broadcast) {graph, n3} = GraphCreator.create_node(graph, params) {graph, n4} = GraphCreator.create_node(graph, params) new_graph = graph - |> GraphCreator.add_connection(:n2, b1) + |> GraphCreator.add_connection(n2, b1) |> GraphCreator.add_connection(b1, n3) |> GraphCreator.add_connection(b1, n4) assert new_graph == compare_graph end - test "can add a funnel" do + test "can add a funnel", %{graph: graph, n2: n2} do compare_graph = %Graph{ name: graph_name, - nodes: %{n1: params, n2: params, b1: params_broadcast, n3: params, n4: params, f1: params_funnel, n5: params}, - connections: %{n1: :n2, n2: :b1, b1: [:n4, :n3], n3: :f1, n4: :f1, f1: :n5} + nodes: %{n_demo_1: params, n_demo_2: params, b_demo_1: params_broadcast, n_demo_3: params, n_demo_4: params, f_demo_1: params_funnel, n_demo_5: params}, + connections: %{n_demo_1: :n_demo_2, n_demo_2: :b_demo_1, b_demo_1: [:n_demo_4, :n_demo_3], n_demo_3: :f_demo_1, n_demo_4: :f_demo_1, f_demo_1: :n_demo_5} } - {graph, b1} = GraphCreator.create_broadcast(create_graph, params_broadcast) + {graph, b1} = GraphCreator.create_broadcast( graph, params_broadcast) {graph, n3} = GraphCreator.create_node(graph, params) {graph, n4} = GraphCreator.create_node(graph, params) {graph, f1} = GraphCreator.create_funnel(graph, params_funnel) @@ -88,7 +93,7 @@ defmodule Exstreme.GraphCreatorTest do new_graph = graph - |> GraphCreator.add_connection(:n2, b1) + |> GraphCreator.add_connection(n2, b1) |> GraphCreator.add_connection(b1, n3) |> GraphCreator.add_connection(b1, n4) |> GraphCreator.add_connection(n3, f1) diff --git a/test/exstreme/graph_test.exs b/test/exstreme/graph_test.exs index 87164b2..249da43 100644 --- a/test/exstreme/graph_test.exs +++ b/test/exstreme/graph_test.exs @@ -4,30 +4,37 @@ defmodule Exstreme.GraphTest do alias Exstreme.Graph doctest Exstreme.Graph - test "graph with many nodes has 7 nodes and 7 connections" do - assert Graph.count_nodes(graph_many_nodes) == 7 - assert Graph.count_connections(graph_many_nodes) == 7 + setup do + graph = graph_many_nodes + {:ok, graph: graph, n1: Graph.nid(graph, :n1), + n3: Graph.nid(graph, :n3), n4: Graph.nid(graph, :n4), + n5: Graph.nid(graph, :n5), f1: Graph.nid(graph, :f1)} end - test "connection stats for graph with many nodes" do - assert Graph.connections_stats(graph_many_nodes) == %{begin: 1, connected: 5, end: 1} + test "graph with many nodes has 7 nodes and 7 connections", %{graph: graph} do + assert Graph.count_nodes(graph) == 7 + assert Graph.count_connections(graph) == 7 end - test "the start node is n1" do - assert Graph.find_start_node(graph_many_nodes) == [:n1] + test "connection stats for graph with many nodes", %{graph: graph} do + assert Graph.connections_stats(graph) == %{begin: 1, connected: 5, end: 1} end - test "the last node is n5" do - assert Graph.find_last_node(graph_many_nodes) == [:n5] + test "the start node is n1", %{graph: graph, n1: n1} do + assert Graph.find_start_node(graph) == [n1] end - test "the nodes before f1 are n4 and n3" do - res = Graph.get_before_nodes(graph_many_nodes, :f1) - assert res == [:n4, :n3] + test "the last node is n5", %{graph: graph, n5: n5} do + assert Graph.find_last_node(graph) == [n5] end - test "the nodes after f1 are n4 and n3" do - res = Graph.get_after_nodes(graph_many_nodes, :f1) - assert res == [:n5] + test "the nodes before f1 are n4 and n3", %{graph: graph, f1: f1, n3: n3, n4: n4} do + res = Graph.get_before_nodes(graph, f1) + assert res == [n4, n3] + end + + test "the nodes after f1 are n4 and n3", %{graph: graph, f1: f1, n5: n5} do + res = Graph.get_after_nodes(graph, f1) + assert res == [n5] end end diff --git a/test/support/common.ex b/test/support/common.ex index 0652466..e263648 100644 --- a/test/support/common.ex +++ b/test/support/common.ex @@ -65,9 +65,16 @@ defmodule Exstreme.Common do GraphCreator.add_connection(graph, n1, n2) end + def funnel_func(values, _) do + reduced = Enum.reduce(values, 0, fn({:sum, num}, acc) -> + num + acc + end) + {:ok, {:sum, reduced}} + end + def params, do: [type: :common, func: fn({:sum, acc}, _) -> {:ok, {:sum, acc + 1}} end] - def params_funnel, do: [type: :funnel, func: fn(values, _) -> {:ok, {:sum, Enum.reduce(values, 0, fn({:sum, num}, acc) -> num + acc end)}} end] + def params_funnel, do: [type: :funnel, func: &funnel_func/2] def params_broadcast, do: [type: :broadcast] end