# `Mob.Plugin.SelfTest`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/plugin/self_test.ex#L1)

A plugin's own on-device proof that it works in the host it was built into.

A plugin declares the module in its manifest (`priv/mob_plugin.exs`):

    %{
      name: :mob_location,
      # ...
      selftest: MobLocation.SelfTest
    }

and mob_dev's `mix mob.selftest` (or mob_ci, invariant P12) calls
`run/1` on the device over distribution for every activated plugin. The
result is attributed to that plugin: a plugin whose self-test fails on its
own has a bug; one that fails only when other plugins are active has a
conflict. Without a self-test the ecosystem's CI can only prove that the
plugin's NIF module loaded, which says nothing about whether `nif_init`
ran, the Kotlin bridge registered or the Objective-C delegate exists.

## The three outcomes

  * `:pass` — the plugin did its job on this device. The test MUST get a
    real answer back from the plugin's native code (a NIF export, a JNI
    bridge call, an Objective-C or Swift entry point) or, for a pure-Elixir
    plugin, from its real API path (a store, a verifier, a client). Loading
    a module, checking `function_exported?/3` or reading config proves
    nothing about the build and does not count. Prefer a side-effect-free
    export: a status query, a version string, a stop/cancel that is a no-op
    while nothing runs. `erlang:nif_error(nif_not_loaded)` from the stub
    means the native library was not linked; that is a `{:fail, _}`.

  * `{:fail, reason}` — the plugin is broken here. `reason` is a sentence
    for a human reading a CI report: say what was called, what came back
    and what was expected. A raise, exit, throw, timeout or a return value
    outside this contract is also counted as a failure by the runner, so a
    test does not need to rescue: `{:ok, _} = :my_nif.status()` is a fine
    assertion.

  * `{:skip, why}` — the test cannot run on this device, not a verdict on
    the plugin. `:needs_hardware` for a sensor, radio or camera this
    device does not have; `:needs_user` when a person must tap a system
    dialog or unlock something; a string for anything else (say what is
    missing). Reach for a skip only after the test has done everything it
    can without that resource: a plugin that can prove its NIF initialised
    and *then* finds no GPS passes; a GPS fix is a feature, not the proof.
    Base `:needs_hardware` on what the native side reports is absent (no
    NFC controller, no LiDAR, no cellular radio), not on
    `device: :emulator` alone: a skip that is really "I did not look"
    hides a hole, and one that assumes a phone has everything fails on
    the tablet without a radio.

## The context

`run/1` receives `%{platform: :ios | :android, device: :simulator |
:emulator | :physical}` so the test can choose what to prove: a simulator
has no camera, an emulator has a fake GPS, a physical device usually has
the hardware but needs the user for permissions. The runner pre-grants the
permissions the manifest declares on emulators and simulators before
calling the test, so `:needs_user` is for prompts that cannot be granted
from the host.

## Writing one

    defmodule MobLocation.SelfTest do
      @behaviour Mob.Plugin.SelfTest

      @impl true
      def run(%{platform: platform}) do
        # location_stop/0 is a no-op while no updates run, but it goes
        # through the NIF into CLLocationManager / the Kotlin bridge, so
        # :ok proves the native side is linked and initialised.
        case :mob_location_nif.location_stop() do
          :ok -> :pass
          other -> {:fail, "location_stop/0 on #{platform} returned #{inspect(other)}, expected :ok"}
        end
      end
    end

Keep it under a few seconds: the runner's default timeout is 30 s and the
test runs once per activated plugin, per device, per nightly cell. Leave
the device as you found it (stop what you start, delete what you write).

# `ctx`

```elixir
@type ctx() :: %{
  platform: :ios | :android,
  device: :simulator | :emulator | :physical
}
```

Where the test is running.

# `result`

```elixir
@type result() :: :pass | {:fail, String.t()} | {:skip, skip_reason()}
```

What `run/1` returns.

# `skip_reason`

```elixir
@type skip_reason() :: :needs_hardware | :needs_user | String.t()
```

Why the test could not run on this device.

# `run`

```elixir
@callback run(ctx()) :: result()
```

Prove the plugin works on this device, or say why that cannot be checked.
See the module documentation for what each outcome means.

# `result?`

```elixir
@spec result?(term()) :: boolean()
```

Whether `term` is a result `run/1` may return.

The runner treats anything else as a failure: a test that returns `:ok` or
`true` has not said whether the plugin works.

    iex> Mob.Plugin.SelfTest.result?(:pass)
    true
    iex> Mob.Plugin.SelfTest.result?({:skip, :needs_hardware})
    true
    iex> Mob.Plugin.SelfTest.result?({:fail, "status/0 returned :error"})
    true
    iex> Mob.Plugin.SelfTest.result?(:ok)
    false
    iex> Mob.Plugin.SelfTest.result?({:skip, :no_reason})
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
