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

Runs every activated plugin's `Mob.Plugin.SelfTest` on a device.

A plugin declares `selftest: Module` in its manifest; `run_all/3` calls
`Module.run/1` on the device's node, one plugin at a time, and returns one
entry per activated plugin. `mix mob.selftest` prints those as a table;
mob_ci records them per nightly cell (invariant P12).

The entry's `:result` is always one of the contract's three shapes:

  * `:pass`
  * `{:fail, reason}` — also what a self-test that raised, exited, threw,
    timed out, is not on the device or returned something outside the
    contract gets, and what a plugin whose manifest failed signature
    verification gets. The reason says which.
  * `{:skip, :needs_hardware | :needs_user | reason}` — including plugins
    with no `selftest:` in their manifest (`module: nil`), so a plugin
    without one is visible, not silently absent.

Permissions are not granted here: the node is already running, and on an
iOS simulator a privacy change can terminate the app. Callers grant
**before** launching the app with `grant_permissions/4`, as
`mix mob.selftest` does.

# `ctx`

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

What a self-test is told about where it runs (`Mob.Plugin.SelfTest.ctx/0`).

# `entry`

```elixir
@type entry() :: %{
  plugin: atom(),
  module: module() | nil,
  result: term(),
  ms: non_neg_integer()
}
```

One plugin's outcome. `:ms` is the wall time of the call on the host.

# `grant`

```elixir
@type grant() :: %{
  plugin: atom(),
  permission: String.t(),
  status: :ok | {:error, String.t()}
}
```

A granted (or attempted) permission.

# `plugin`

```elixir
@type plugin() ::
  {atom() | Path.t(), map() | nil} | {atom() | Path.t(), map() | nil, term()}
```

An activated plugin as `MobDev.Plugin.activated_with_verify/0` (3-tuple) or
`activated/0` (2-tuple) lists them; the first element is the plugin's name
or its dependency directory.

# `failures`

```elixir
@spec failures([entry()]) :: [entry()]
```

The entries whose result is `{:fail, _}`.

# `grant_permissions`

```elixir
@spec grant_permissions(MobDev.Device.t(), [plugin()], String.t(), (String.t(),
                                                              [String.t()] -&gt;
                                                                {String.t(),
                                                                 integer()})) ::
  [
    grant()
  ]
```

Grants the permissions the plugins' manifests declare to `bundle_id` on
`device`, so self-tests do not hit a system prompt. Call it **before**
launching the app: a simulator may terminate a running app whose privacy
settings change.

  * Android emulator: each `android.permissions` entry via
    `adb -s <serial> shell pm grant`. Only runtime permissions are
    grantable; a normal or signature permission answers with an error,
    which is recorded, not raised.
  * iOS simulator: each `permissions: [%{capability: cap}]` whose
    capability maps to a `simctl privacy` service.
  * Physical devices: nothing is granted (returns `[]`); a self-test that
    needs a permission the user has not given skips with `:needs_user`.

Returns one `t:grant/0` per attempt. `cmd` is `fn exe, argv -> {output, status} end`.

# `run_all`

```elixir
@spec run_all(node(), ctx(), keyword()) :: [entry()]
```

Runs the self-test of every activated plugin on `node` and returns an entry per plugin.

Options:

  * `:plugins` — `t:plugin/0` list (default:
    `MobDev.Plugin.activated_with_verify/0`, read from the host project's
    `mob.exs` and deps). A plugin whose manifest failed verification is a
    `{:fail, _}` entry; one with no manifest (tier 0) is a skip.
  * `:timeout_ms` — per self-test (default 30000). A test
    still running at the deadline is killed on the device, so the next
    plugin's test never runs beside it.
  * `:boot_timeout_ms` — how long to wait for the plugins' OTP applications
    to be started on the node before the first test (default 15000). mob
    starts them after the node is up, so a run right after a relaunch
    would otherwise test a plugin whose supervisor is not there yet. A
    plugin whose application never starts is tested anyway and reports it.

# `summary`

```elixir
@spec summary([entry()]) :: String.t()
```

One line: `N passed, N failed, N skipped`.

# `table`

```elixir
@spec table([entry()]) :: [String.t()]
```

The table `mix mob.selftest` prints for one device: a header plus one
line per entry, `plugin  outcome  ms  detail`.

---

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