# `AtpClient.StarExec`
[🔗](https://github.com/jcschuster/AtpClient/blob/v0.6.0/lib/atp_client/star_exec.ex#L1)

Client for self-hosted StarExec instances.

StarExec is primarily a web application backed by Tomcat, and its
programmatic surface is the same set of URLs that the web UI talks to.
This module authenticates against that surface and exposes the subset of
operations needed to submit benchmarks to pre-configured solvers, poll for
completion, and fetch solver output.

## Assumptions

The standard StarExec deployment exposes:

  * Tomcat form-based authentication at `/starexec/j_security_check`
    with the `j_username` and `j_password` fields;
  * A JSON job endpoint at `/services/details/job/{job_id}` returning the
    Gson form of `org.starexec.data.to.Job` (used to detect completion);
  * A ZIP download of all pair stdouts at
    `/secure/download?type=j_outputs&id={job_id}`.

All of these paths are configurable; see `AtpClient.Config`.

Because StarExec's job-creation form accepts a large number of fields and
the exact set varies by release, `create_job/3` is kept minimal and
deliberately flexible — consumers pass the multipart form fields their
instance expects, and this module handles authentication and session
cookies around the call.

## Example

    {:ok, session} = AtpClient.StarExec.login()
    {:ok, job_info} = AtpClient.StarExec.get_job(session, 1234)
    :ok = AtpClient.StarExec.logout(session)

## Configuration

    config :atp_client, :starexec,
      base_url: "https://starexec.example.org",
      username: "me",
      password: System.get_env("STAREXEC_PASS")

# `benchmark_id`

```elixir
@type benchmark_id() :: pos_integer()
```

# `job_id`

```elixir
@type job_id() :: non_neg_integer() | String.t()
```

# `space_id`

```elixir
@type space_id() :: pos_integer()
```

# `create_job`

```elixir
@spec create_job(AtpClient.StarExec.Session.t(), map(), keyword()) ::
  {:ok, job_id()} | {:error, term()}
```

Creates a new StarExec job and returns the numeric job id.

The `fields` map is sent as the form body and must contain everything the
deployment's `/secure/add/job` handler expects. A typical field set includes
`"name"`, `"desc"`, `"queue"`, `"sid"` (space id), `"cpuTimeout"`,
`"wallclockTimeout"`, `"benchProcess"`, `"traversal"`, plus any
solver/benchmark selection fields. Refer to your StarExec instance's form
for the authoritative list.

StarExec replies to a successful submission with a 302 to
`/secure/details/job.jsp?id=<n>`; this function parses `<n>` out of the
Location header for you. For richer diagnostics on non-redirect responses,
the error tuple carries the raw status and any StarExec
`STATUS_MESSAGE_STRING` cookie value.

## Return

  * `{:ok, job_id}` on a 302/303 with a parsable id
    (`STAREXEC_URL/secure/details/job.jsp?id=<n>` or `/jobs/<n>`);
  * `{:error, {:create_job_failed, %{status: status, message: msg | nil}}}`
    on a non-redirect response — `msg` is the `STATUS_MESSAGE_STRING`
    cookie value if set (StarExec's own validation message);
  * `{:error, {:no_job_id, location}}` if the redirect landed but the
    Location header didn't match a known job-id pattern;
  * anything `request/4` can propagate.

Callers that want the raw response can use the low-level `request/4`
helper directly.

# `delete_job`

```elixir
@spec delete_job(AtpClient.StarExec.Session.t(), job_id(), keyword()) ::
  :ok | {:error, term()}
```

Asks the StarExec instance to delete the given job, freeing the remote
compute slot. Used by `wait_for_job/3` and `prove/3` to release resources
when the calling process is cancelled.

StarExec's delete endpoint (`POST /starexec/services/delete/job`) expects
the job id(s) in the `selectedIds[]` form field; the underlying servlet
flips the `deleted` column synchronously and then queues the on-disk
cleanup off-thread, so a 200 response is returned quickly even for large
jobs.

The endpoint path is configurable via `:delete_job_path` for older
StarExec instances that mount it elsewhere.

# `get_job`

```elixir
@spec get_job(AtpClient.StarExec.Session.t(), job_id(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Retrieves the JSON status of a StarExec job.

# `get_job_output`

```elixir
@spec get_job_output(AtpClient.StarExec.Session.t(), job_id(), keyword()) ::
  {:ok, binary()} | {:error, term()}
```

Downloads the per-pair stdout archive ("j_outputs") for a finished job.
StarExec has no JSON endpoint that returns a single pair's stdout in
isolation; the official `StarexecCommand` CLI uses this download path
instead, and it works without knowing the pair ids.

Returns the raw ZIP bytes. Use `:zip.extract(bytes, [:memory])` (Erlang
stdlib) to read the individual stdout files out of the archive.

# `list_space_benchmarks`

```elixir
@spec list_space_benchmarks(AtpClient.StarExec.Session.t(), space_id(), keyword()) ::
  {:ok, [%{id: benchmark_id(), name: String.t()}]} | {:error, term()}
```

Lists every benchmark in `space_id`.

StarExec's REST surface for this is a DataTables-style endpoint that
returns each row as `[anchor_html, type_html]`; this function parses the
anchor to recover the structured `[%{id: …, name: …}]` we want.

# `login`

```elixir
@spec login(keyword()) :: {:ok, AtpClient.StarExec.Session.t()} | {:error, term()}
```

Authenticates against the configured StarExec instance and returns a
`Session` holding the session cookies needed for subsequent requests.

## Options

  * `:base_url`, `:username`, `:password` — override configuration;
  * `:login_path` — override the auth endpoint (default
    `/starexec/j_security_check`);
  * `:request_timeout_ms`.

# `logout`

```elixir
@spec logout(
  AtpClient.StarExec.Session.t(),
  keyword()
) :: :ok | {:error, term()}
```

Terminates a StarExec session. Errors during logout are returned but are
usually safe to ignore.

# `prove`

```elixir
@spec prove(AtpClient.StarExec.Session.t(), binary(), keyword()) ::
  AtpClient.ResultNormalization.atp_result() | {:error, term()}
```

End-to-end: upload `problem_text` as a fresh benchmark, run it against the
configured solver, wait for completion, and return the normalized result.

This is the "single shot" entry point that mirrors the other backends'
prove/2-style helpers. It is implemented purely in terms of the lower-level
functions in this module, so anything it does can be done by hand for
finer control.

## Required options

  * `:space_id` — target StarExec space (used for both the upload and the
    job).
  * `:solver_cfg_id` — solver *configuration* id (not solver id).

## Common options

  * `:queue_id` (default from config; `1` if unset) — worker queue id.
  * `:cpu_timeout_s` (default from config; `60` if unset).
  * `:wallclock_timeout_s` (default `cpu_timeout_s * 2`).
  * `:benchmark_type` (default `1`).
  * `:timeout_ms` — wall-clock budget for the whole pipeline; passed to
    both `wait_for_benchmark/4` and `wait_for_job/3`.
  * `:raw` — when `true`, skip `interpret_result/1` and return the
    fetched solver output string as `{:ok, stdout}`.

All other StarExec options (`:base_url`, `:username`, `:password`, …) are
consumed by `request/4` as usual.

# `request`

```elixir
@spec request(AtpClient.StarExec.Session.t(), atom(), String.t(), keyword()) ::
  {:ok, Req.Response.t()} | {:error, term()}
```

Low-level helper: issue an HTTP request against `session.base_url` carrying
the session cookies. Consumers can use this to reach StarExec endpoints
that are not yet wrapped by this module.

Supported options include anything `Req.request/1` accepts, plus
`:request_timeout_ms` which is translated to `:receive_timeout`.

# `upload_benchmark`

```elixir
@spec upload_benchmark(
  AtpClient.StarExec.Session.t(),
  space_id(),
  binary(),
  keyword()
) ::
  {:ok, String.t()} | {:error, term()}
```

Uploads a single TPTP problem to a StarExec space and returns the
benchmark's filename.

StarExec accepts only archives (.zip / .tar / .tgz) at the benchmark upload
endpoint, so this function wraps `problem_text` in an in-memory ZIP whose
sole entry is the new benchmark file. The benchmark's name inside StarExec
becomes the entry's filename (returned as `{:ok, name}` for use with
`wait_for_benchmark/4`).

The upload is processed asynchronously on the server, so this function
returns as soon as the request is accepted. Use `wait_for_benchmark/4`
(or `prove/3`, which composes both) to obtain the new benchmark id once
StarExec has finished extracting and validating it.

## Options

  * `:name` — base name (without `.p`) for the uploaded file. Defaults to
    a random UUID-ish string so concurrent uploads don't collide.
  * `:benchmark_type` — benchmark processor id. Defaults to `1`
    (StarExec's "no type" processor), which accepts any text.
  * `:request_timeout_ms`.

## Examples

    {:ok, name} = AtpClient.StarExec.upload_benchmark(session, 42,
      "fof(c, conjecture, $true).", name: "smoke")
    # name is "smoke.p", ready to hand to wait_for_benchmark/4.

# `wait_for_benchmark`

```elixir
@spec wait_for_benchmark(
  AtpClient.StarExec.Session.t(),
  space_id(),
  String.t(),
  keyword()
) ::
  {:ok, benchmark_id()} | {:error, term()}
```

Polls `list_space_benchmarks/3` until a benchmark named `name` appears, then
returns its id. Fails with `{:error, :timeout}` if the benchmark has not
appeared within `:timeout_ms` (default 60 s).

# `wait_for_job`

```elixir
@spec wait_for_job(AtpClient.StarExec.Session.t(), job_id(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Polls `get_job/3` until the returned JSON reports that the job is complete
or the per-call `:timeout_ms` elapses.

"Complete" is defined as a `completed` field equal to the `totalJobPairs`
field (or a `jobComplete` flag being truthy). The exact JSON shape depends
on the StarExec version; if it differs on your deployment, pass a custom
predicate via `:complete_fun`, which receives the decoded job map and
returns a boolean.

## Cancellation

This call installs a small helper process that monitors the calling
process. If the caller dies (`Process.exit`, `Task.shutdown`, …) while
the job is still running, the helper issues `delete_job/3` so the remote
StarExec job does not run to completion on the cluster. If the job has
already reached a terminal state (`complete_fun.(info)` is true) by the
time the caller dies, the `delete_job` request is skipped.

---

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