Current section

Files

Jump to
membrane_core lib membrane testing pipeline.ex
Raw

lib/membrane/testing/pipeline.ex

defmodule Membrane.Testing.Pipeline do
@moduledoc """
This Pipeline was created to reduce testing boilerplate and ease communication with it's elements.
It also provides utility for receiving messages when `Pipeline` playback state changes
and notifications it receives.
## Starting Pipeline
When you want a build Pipeline to test your elements you need three things:
- Pipeline Module
- List of elements
- Links between those elements
When creating pipelines for tests the only essential part is the list of elements.
In most cases during the tests elements are linked in a way that
`:output` pad is linked to `:input` pad of subsequent element.
So we only need to pass a list of elements and links can be generated automatically.
To start a testing pipeline you need to build `Membrane.Testing.Pipeline.Options` struct
and pass to to `Membrane.Testing.Pipeline.start_link/2`.
```
options = %Membrane.Testing.Pipeline.Options {
elements: [
el1: MembraneElement1,
el2: MembraneElement2,
...
]
}
{:ok, pipeline} = Membrane.Testing.Pipeline.start_link(options)
```
See `Membrane.Testing.Pipeline.Options` for available options.
Links are generated by `populate_links/1`.
## Receiving notifications about callbacks invocation
In some cases, you want to get a notification when `Pipeline` reaches a certain state.
You can achieve that by monitoring pipeline callbacks.
```
options = %Membrane.Testing.Pipeline.Options {
monitored_callbacks: [:handle_prepared_to_playing],
test_process: pid
...
}
```
First, you need to configure which callbacks are to be monitored by putting their names
into `monitored_callbacks` field of `Options` struct. Check `t:Membrane.Testing.Pipeline.Options.pipeline_callback/0`
for list of available callback names.
You also need to pass a `PID` of the process that will receive messages.
```
import Membrane.Testing.Pipeline.Assertions
assert_receive_message :handle_prepared_to_playing
```
Then it is a matter of waiting for the right message to come.
You can do that easily by using `Membrane.Testing.Pipeline.Assertions.assert_receive_message/3`.
## Messaging children
You can send messages to children using their names specified in the elements list.
Please check `message_child/3` for more details.
"""
use Membrane.Pipeline
alias Membrane.Element
alias Membrane.Pipeline.Spec
defmodule Options do
@moduledoc """
Structure representing `options` passed to testing pipeline.
## Monitored Callbacks
A message will be sent to `test process` if the invoked callback is in the list of monitored callbacks.
See `t:pipeline_callback/0` for available callbacks.
## Test Process
`pid` of process that shall receive messages when Pipeline invokes playback state change callback
and receives notification.
## Elements
List of element specs.
## Links
Map describing links between elements.
If links are not present or set to nil they will be populated automatically based on elements order
using default pad names.
"""
@enforce_keys [:elements]
defstruct @enforce_keys ++ [:monitored_callbacks, :links, :test_process]
@typedoc """
Defines supported callback names.
"""
@type pipeline_callback ::
:handle_notification
| :handle_playing_to_prepared
| :handle_prepared_to_playing
| :handle_prepared_to_stopped
| :handle_stopped_to_prepared
@type t :: %__MODULE__{
monitored_callbacks: pipeline_callback() | nil,
test_process: pid() | nil,
elements: Spec.children_spec_t(),
links: Spec.links_spec_t() | nil
}
end
@doc """
Links subsequent elements using default pads (linking `:input` to `:output` of previous element).
## Examples
iex> Pipeline.populate_links([el1: MembraneElement1, el2: MembraneElement2])
%{{:el1, :output} => {:el2, :input}}
"""
@spec populate_links(elements :: Spec.children_spec_t()) :: Spec.links_spec_t()
def populate_links(elements) do
Enum.chunk_every(elements, 2, 1, :discard)
|> Enum.map(fn [{output_name, _}, {input_name, _}] ->
{{output_name, :output}, {input_name, :input}}
end)
|> Enum.into(%{})
end
@doc """
Sends message to a child by Element name.
## Examples
Knowing that `pipeline` has child named `sink`, message can be sent as follows:
message_child(pipeline, :sink, {:message, "to handle"})
"""
@spec message_child(pid(), Element.name_t(), any()) :: :ok
def message_child(pipeline, child, message) do
send(pipeline, {:for_element, child, message})
:ok
end
@doc """
Starts the Testing Pipeline and links it to the current process.
Proxy for `Membrane.Pipeline.start_link/3`.
"""
@spec start_link(pipeline_options :: Options.t(), process_options :: GenServer.options()) ::
GenServer.on_start()
def start_link(%Options{} = options, process_options \\ []) do
Membrane.Pipeline.start_link(__MODULE__, options, process_options)
end
@impl true
def handle_init(options)
def handle_init(%Options{test_process: nil, monitored_callbacks: list})
when is_list(list) and length(list) > 0,
do: {:error, :no_pid_when_monitoring}
def handle_init(%Options{monitored_callbacks: nil} = options),
do: handle_init(%Options{options | monitored_callbacks: []})
def handle_init(%Options{links: nil, elements: elements} = options) do
new_links = populate_links(elements)
handle_init(%Options{options | links: new_links})
end
def handle_init(args) do
%Options{elements: elements, links: links} = args
spec = %Membrane.Pipeline.Spec{
children: elements,
links: links
}
new_state = Map.take(args, [:monitored_callbacks, :test_process])
{{:ok, spec}, new_state}
end
@impl true
def handle_stopped_to_prepared(state),
do: notify_parent(:handle_stopped_to_prepared, state)
@impl true
def handle_playing_to_prepared(state),
do: notify_parent(:handle_playing_to_prepared, state)
@impl true
def handle_prepared_to_playing(state),
do: notify_parent(:handle_prepared_to_playing, state)
@impl true
def handle_prepared_to_stopped(state),
do: notify_parent(:handle_prepared_to_stopped, state)
@impl true
def handle_notification(notification, from, state),
do: notify_parent({:handle_notification, {notification, from}}, state)
@impl true
def handle_other({:for_element, element, message}, state) do
{{:ok, forward: {element, message}}, state}
end
def handle_other(message, state),
do: notify_parent({:handle_other, message}, state)
defp get_callback_name(name) when is_atom(name), do: name
defp get_callback_name({name, _}), do: name
defp notify_parent(message, state) do
%{test_process: parent, monitored_callbacks: monitored_callbacks} = state
if get_callback_name(message) in monitored_callbacks do
send(parent, {__MODULE__, message})
end
{:ok, state}
end
end