The conformance suite every venue package runs in its own CI.
This is the mechanism. Prose in six CLAUDE.md files drifts; a suite that runs in
five CI pipelines cannot.
Why this lives in lib/ and not test/support/
It was in test/support/ first, following the reference repo, and that does not
work for a suite consumers run. elixirc_paths(:test) governs this package's own
build; a dependency is not compiled in the :test environment, so the file shipped
inside the tarball and was never compiled into the consumer. A venue package's
use DpExchange.Core.AdapterContract failed with module not loaded and could not be
found — the suite present on disk and absent from the code path.
Shipping a file is not the same as shipping a module. A public testing API belongs in
lib/, which is why Ecto.Adapters.SQL.Sandbox and Phoenix.ConnTest live there.
use ExUnit.Case appears only inside the generated block, so nothing here needs
ExUnit at compile time and this compiles cleanly as a dependency.
defmodule DpExchange.Coinbase.ContractTest do
use DpExchange.Core.AdapterContract,
venue: DpExchange.Coinbase,
symbol_format: DpExchange.Coinbase.SymbolFormat,
sample_pairs: ~w(BTC-USDC ETH-USDC),
credentials: %{api_key: "test", api_secret: "test"}
endOptions of use
:venue- required, the facade module.:fakeand:sample_pairs- not optional in practice: group 1 fails without them.:symbol_format- theSymbolNormalizermodule; group 4 is generated only when named.:credentials- the credentials map the calls carry (default%{}).:endpoint_opts-%{{name, arity} => keyword()}, what the fake needs to answer.:endpoint_symbols-%{{name, arity} => symbol}for an endpoint narrower than:sample_pairs.:order_args-%{order_request | order_requests | order_changes => value}; an unknown key raises at compile time.:absent_feed_opts- options pointing the streaming calls at a feed nothing registered (default[feed: :dp_exchange_contract_absent_feed]).:package_root- where groups 16, 18 and 19 scan compiled modules (default"lib").:unlisted_symboland:padded_credentials- the opt-in facts of group 41.:unknown_order_id- group 49's order id nobody placed, in the venue's own id format (default: a text id; a venue with numeric ids names a number).:ambiguous_place_order- opt-in for group 42: a 1-arity function (fn request -> ... endor&Mod.fun/1) that takes an order request map and runs the venue's realplace_order/3path, with a transport fault injected after the request is sent (a Req plug that returns a transport error, or a 503 on every attempt). Group 42 asserts that answer is{:error, {:order_unconfirmed, id, reason}}, and that the same function given a request refused before sending (no:order_type) is a plain or refused error, never:order_unconfirmed. Absent, group 42 is skipped, like group 41's facts.:process_probe- opt-in for group 51: a{module, function}naming a 1-arity function that starts the venue's own long-lived process or processes (its feed, its socket) with no network (the fake transport, a closed port, a stub) from a credentials map and answers{:ok, pid}or{:ok, [pid]}. Group 51 renders:sys.get_status/2of each, which is what OTP prints in a crash report, and fails if the secret it passed in is in it. Absent, group 51 is skipped, like group 41's facts, and a venue holding a credential in a process it never probed is a venue this suite has not examined.
What it asserts, and what it deliberately does not
Fifty-four groups, listed in assertions/0. The load-bearing one is capabilities and
behaviour agreeing in both directions: over-declaring fails in a caller's hands at
runtime, and under-declaring hides working functionality. Holding both is what makes
capabilities/0 trustworthy enough for a consumer to branch on instead of branching on
venue identity.
Maturity is asserted for presence, not for truth. Every active endpoint must declare
:proven or :experimental, because an absent value is the failure this prevents.
Whether a :proven claim is true is not machine-checkable, and a suite that pretended
otherwise would be worse than one that admits the limit.
No assertion may name a socket, a channel string, a transport module or a polling interval. If one does, mechanism has leaked through the facade and the assertion is the bug, not the venue.
The fake satisfies the same suite as the real adapter
That is the ratchet. The suite's job is not to anticipate every way a fake can diverge — it is to make each discovered divergence permanent. The reference implementation this pattern comes from went from nothing to over a thousand lines by absorbing thirteen gaps its first consumer found, and none reached an external user across thousands of downloads.
Every gap found becomes a new assertion here. A gap fixed only in one venue's fake is a gap the next venue will reintroduce.
Summary
Functions
What is wrong with a fake's answer to an endpoint its capabilities/0 declares active,
or nil when nothing is.
What is wrong with a venue's answers to an ambiguous write, or nil. Assertion 42.
The position of the amount among name/arity's arguments, from the suite's own argument
table (arg_shape/3), or nil when the call takes none.
The writes whose amount assertion 47 checks, when capabilities/0 declares them active.
What is wrong with the answer of an endpoint to assertion 5's question, or nil: does a
success hold the Core.Types struct (one of modules) its callback promises?
How a call to name/arity is shaped, position by position: :credentials, :symbol,
:timeframe, :opts, or one of the order arguments.
The assertion groups, for documentation and for a venue package to report against.
The identity fault of a venue's asset_classes/0 answer, or nil. Assertion 3: a non-empty
list of the classes a host switches on.
What is wrong with a Balance a fake returned, or nil. Assertion 24.
What is wrong with the answer to a depth request, or nil: {:too_many_levels, side, n}
when a side carries more than depth levels.
What is wrong with the answer to a request carrying limit:, given the answer baseline
without it, or nil. Only what the convention requires: Coinbase refuses :limit
({:unsupported_option, :limit}) because its old option meant depth, while a venue whose
book has no :limit ignores it as Core's usage rules let it, and one with a native :limit
may refuse a value ({:invalid_option, :limit, v}). So a success and those two refusals are
fine; a raise (:raised) or any other answer ({:answered, answer}) is not. Nothing is
judged when the baseline itself was not a success.
What is wrong with one side of an order book a fake returned, or nil. Assertion 25.
The :depth assertion 38 asks for: one level per side.
Whether the unbounded candle ask of assertion 37 gave the group anything to check, or
{:no_candles, answer}.
The ranges assertion 37 re-asks with, as {range, must_include}, chosen from the fake's own
bars (oldest first). Two or more bars: start the first bar's opened_at and end the last
one's, which must keep the first and drop the last. One bar: start alone (keeps it) and
end alone (drops it). None: nothing to ask.
What is wrong with the candles a fake returned for timeframe, or nil. Assertion 37.
Drives fake through subscribe_notices/1, subscribe/2 of pairs, subscribe/2 of
unlisted when given, and unsubscribe/2, then collects every Core.Notice delivered to
this process until none arrives for wait milliseconds. Assertion 45.
Whether a term holds a pid, port or reference anywhere inside it, structs and map keys included. Assertion 11: a facade answer is data, never a process the caller must manage.
Runs each {name, args, expected} of cases against module and returns the ones whose
answer is not {:error, expected}, as {name, arity, expected, fault} with fault as
refusal_fault/2 names it. Empty means every case was refused as the convention says.
What is wrong with get_symbols/1's answer without a query, given the declared
catalog_access, or nil. Assertion 12.
What is wrong with venue.child_spec/1 where a secret is concerned, or nil. Assertion 22.
What is wrong with a venue's coverage_by_kind/1 answer, given the coverage/1 map of the
same moment and the declared streamable kinds, or nil. Assertion 15.
What is wrong with a call's answer when its credentials were stripped, or nil. Assertion 17.
The endpoints assertion 17 asks a credential_benefit: :required venue's fake to refuse
when called with credentials stripped.
The names of the Core.Venue callbacks whose first argument is credentials(), read from
the behaviour's own @callback specs.
The calls of cases that module can be asked, as {name, args, expected} triples:
exported at that arity and not declared :unsupported in caps.
What is wrong with a fake's answers for the two opt-in family reasons, or nil. Assertion 41.
The modules in modules that belong to an application outside permitted.
A failure message for case_faults/2's result, one line per call that was not refused.
The optional callbacks module implements at the wrong arity, as {name, arity, arities}:
the callback's name is exported at arities but not at the one the behaviour declares, so a
caller at the declared arity gets an UndefinedFunctionError. Assertion 1.
The {name, args, expected} cases of assertion 36: get_historical_prices/4 with an
:include_partial that is not a boolean must answer {:error, {:invalid_option, :include_partial, value}}. nil is not tried: several venues read it as an absent option.
What is wrong with a write's answer to the invalid value as its amount, or nil.
Assertion 47, venue rule W4.
The invalid amounts assertion 47 hands every amount-taking write: {label, value}.
Order ids get_order/3 and cancel_order/3 must refuse as {:invalid_order_id, value}:
nil and a whitespace-only string.
Single-symbol values get_price/2 and get_top_of_book/2 must refuse as
{:invalid_symbol, value}: nil, a number, and a whitespace-only string.
:to values a streaming call must refuse as {:invalid_subscriber, value}: neither a pid
nor an atom other than nil, true and false. nil is not here: it means the caller.
Symbol lists the streaming calls must refuse as {:invalid_symbols, value}: not a list, a
list holding a non-string or a blank string (whitespace-only is blank), and an improper
list. sample is a symbol the venue serves, so the bad lists are bad for exactly one reason.
What is wrong with a fake's subscribe/2 given a lower-case pair, or nil. Assertion 29.
What is wrong with the answers to market_price_requests/2, or nil. Assertion 34.
The requests that put a :price on a market order, one for each time in force the venue
might want for it, or [] when caps does not support :market.
What is wrong with a value (or list of values) a fake returned, or nil. Assertion 39.
The public functions of venue that fake does not export, as a sorted list of
{name, arity}. Assertion 44.
The callbacks Core.Venue requires that module does not export, as {name, arity}.
Assertion 1.
What is wrong with a fake's subscribe/2 given to: nil, or nil. Assertion 27.
What is wrong with a no-request call's answer to timeout: 5_000, or nil. Assertion 46.
Whether a run of cases against module examined nothing it should have: no case is
eligible (eligible_cases/3) although caps declares at least one of the cases' calls
active, which means that call is not exported at its arity.
Whether assertion 39 examined nothing: endpoints were active but none of their answers
was checked (examined holds one boolean per endpoint, true when a value was inspected).
The rule violations in notices, as {kind, fault} pairs, judged by
DpExchange.Core.Notice.conformance_faults/1. [] is conforming, and so is an empty list:
judging nothing is not a fault, which is why group 45 also reports how many it judged.
The {name, args, expected} cases of assertion 35. opts_for gives, for a callback name,
the option lists to try; the suite passes the venue's own options with credentials stripped
and an empty list, because the id is checked before the account option and before
credentials.
The malformed place_order/3 requests of assertion 34, as {request, expected}, built
from base, a request the venue accepts: a non-map ({:invalid_order_request, request}),
a request with no :order_type ({:missing_field, :order_type}) and one with a key the
builder does not read ({:unsupported_order_field, key}).
The requests of assertion 52, as {request, reason}, built from base, a request the venue
accepts: for each of :quantity and :price that base carries, one request per value of
invalid_amounts/0 (a float, zero, a negative, NaN, infinity) in its place.
What is wrong with an Order or Fill a fake answered, or nil. Assertion 54.
How many Order and Fill structs answer holds (a struct, a list of them, or
{:ok, either}), the number assertion 54 actually judged.
The {name, args, expected} cases of assertion 50, from the suite's own argument lists for
preview_order/3 ([credentials, request, opts]) and replace_order/4
([credentials, id, changes, opts]).
The applications a package may link against: its own, every declared dependency, and what those depend on at runtime, as strings.
The width assertions 36 and 37 ask get_historical_prices/4 for: "1h" when caps declares
it, else the first declared width the shared vocabulary gives a fixed length, else the first
declared width, or nil when none is declared.
What is wrong with a venue's running process where a secret is concerned, or nil.
Assertion 51.
The Core.Types struct(s) each Core.Venue callback's success is typed as, read from the
behaviour's own @callback specs.
The identity fault of a venue's provider_name/0 answer, or nil. Assertion 3.
What is wrong with a ranged answer, or nil. :start is inclusive and :end exclusive, on
opened_at: {:outside_range, opened_at} for a bar outside range, {:missing, opened_at} for one of must_include that was dropped.
What is wrong with a call's answer to an argument the family refuses locally, or nil.
Assertions 32 to 35.
The identity fault of a venue's runtime_id/0 answer, or nil. Assertion 3.
The {name, args, expected} cases of assertion 33. opts_for gives the options for a
callback name; the suite passes options with credentials stripped, because the refusal is
decided before credentials are read.
What is wrong with a staking write's answer to a valid call, or nil. Assertion 48, venue
rule W5.
The {kind, endpoint} pairs a package declares streamable while saying the venue does not
serve the matching pull endpoint at all (absent). Assertion 8: the two cannot both be
true.
What is wrong with a streaming call made while no feed runs, or nil. Assertion 28.
The {name, args, expected} cases of assertion 32, for the streaming calls given opts.
Whether result is a success that gave assertion 5 a struct to check: {:ok, struct} or
{:ok, [struct | _]}.
What is wrong with a message subscribe/2 pushed, or nil when nothing is. Assertion 20.
What is wrong with a fake's subscription set after four calls, or nil. Assertion 26.
What is wrong with the times on a struct a fake returned, or nil. Assertions 14 and 23.
What is wrong with a call's answer to width, a timeframe the venue does not declare, or
nil. Assertion 21.
The {name, arity} of each call in cases that caps declares active and module does not
export, in the order first met.
The reads and cancels assertion 49 asks about an unknown id: the order callbacks Core.Venue
documents {:refused, :not_found} for. Other id-taking callbacks (get_conversion/2,
get_payment_method/3, get_watchlist/2) have no documented answer for an unknown id, so
none is asserted.
What is wrong with a fake's answer to an order id nobody placed, or nil. Assertion 49,
venue rule W6.
Whether an endpoint's answer to the probe is evidence the options were checked: it could be
judged (unknown_option_judged?/1), or it is the {:unknown_option, key} refusal itself.
The callbacks assertion 43 cannot hand an unknown option, each with the written reason.
What is wrong with a read's answer to a call carrying unknown_option_probe/0, or nil.
Assertion 43.
unknown_option_fault/1, for an endpoint whose plain call is the baseline.
Whether assertion 43 could judge an endpoint's answer to the probe.
The option key assertion 43 hands every read: one no venue reads and none can mistake for its own.
The callbacks of callbacks assertion 43 probes: all of them, less
unknown_option_exclusions/0.
The five reads assertion 43 judged before it covered every endpoint. They are judged on the probe alone, with no baseline call: a refusal of the probe key there is a fault whatever the plain call would have answered.
The order id assertion 49 asks about: one no write of the suite ever minted.
The kinds in streamable or authenticated_streamable that this contract has no word for
(Capabilities.data_kinds/0). Assertion 8.
The endpoints venue_does_not_serve/0 names that caps does not declare :unsupported.
Assertion 8: an endpoint the venue does not serve cannot also be one the package answers.
The timeframe assertion 21 asks get_historical_prices/4 for: the first width of
Timeframe.nameable/0 that caps.historical_timeframes does not name, or nil when the
venue declares the whole vocabulary and there is no width left to prove a refusal with.
What is wrong with a call's answer to an endpoint capabilities/0 declares :unsupported,
or nil when nothing is. Assertion 12's under-declaring direction.
The options use DpExchange.Core.AdapterContract accepts.
Checks the order_args: option of use DpExchange.Core.AdapterContract. It must be a map
whose keys are drawn from :order_request, :order_requests and :order_changes.
Validates the options given to use DpExchange.Core.AdapterContract, returning them.
Whether value, the success of a price endpoint, gave assertion 39 anything to inspect.
What is wrong with the volume window on a Quote a fake returned, or nil. Assertion 30.
What is wrong with a fake's wanted/1, or nil. Assertion 40.
Functions
@spec active_endpoint_fault(term()) :: nil | :not_supported | :raised
What is wrong with a fake's answer to an endpoint its capabilities/0 declares active,
or nil when nothing is.
:not_supported— the fake answered{:error, :not_supported}: the declaration and the behaviour disagree (assertion 12, over-declaring).:raised— the call raised.{:raised, error}is how the suite'scall_on/3reports it. A raise is not an answer, and treating it as one let a fake that crashed on every endpoint pass "does not answer :not_supported".
Any other answer, refusals and errors included, is not this check's business.
What is wrong with a venue's answers to an ambiguous write, or nil. Assertion 42.
ambiguous is what place_order/3 answered when a transport fault came after the
request was sent (a timeout, a dropped connection, a 503 on every attempt). It must be
{:error, {:order_unconfirmed, id, reason}}: the order may have been placed, so a plain
{:error, _} invites the caller to retry into a duplicate, and {:refused, _} claims
permanence the venue never gave. id is a client order id, a list of them, or nil when
the venue has none and none was generated. When the request carried a binary
client_order_id (sent_id) it is that id: nil there is {:unconfirmed_id, nil}.
presend is what the same path answered for a request refused before anything was
sent. It must be an {:error, _} or {:refused, _} that is not :order_unconfirmed:
nothing left the process, so claiming uncertainty would make a caller reconcile an order
that never existed.
{:not_unconfirmed, answer}, {:unconfirmed_id, id}, {:presend_unconfirmed, answer}
or {:presend_not_refused, answer}.
@spec amount_position(atom(), non_neg_integer()) :: non_neg_integer() | nil
The position of the amount among name/arity's arguments, from the suite's own argument
table (arg_shape/3), or nil when the call takes none.
@spec amount_write_endpoints() :: [{atom(), non_neg_integer()}]
The writes whose amount assertion 47 checks, when capabilities/0 declares them active.
What is wrong with the answer of an endpoint to assertion 5's question, or nil: does a
success hold the Core.Types struct (one of modules) its callback promises?
Returns a short description of the offence: a struct of another module, a raw map, a list
holding something else, a bare value ({:ok, nil}, {:ok, 5}; only {:ok, :cancelled} is
allowed, as cancel_order/3 types it) or a raise. A refusal or an error is not a success and
is not judged.
@spec arg_shape(atom(), non_neg_integer(), boolean()) :: [atom()]
How a call to name/arity is shaped, position by position: :credentials, :symbol,
:timeframe, :opts, or one of the order arguments.
credentialed? is whether the callback takes its credential positionally — see
credentialed_callbacks/0. A per-callback override wins over the shape the arity implies.
Both the call assertions and assertion 17's credential-stripped call are built from this one
table; the stripped call used to rebuild its own and ignore the overrides, so for
place_order/3 and replace_order/4 it sent a malformed order, the fake raised, and
"no {:ok, _} with credentials stripped" held for a reason that had nothing to do with
credentials.
@spec assertions() :: [{pos_integer(), String.t()}]
The assertion groups, for documentation and for a venue package to report against.
The identity fault of a venue's asset_classes/0 answer, or nil. Assertion 3: a non-empty
list of the classes a host switches on.
@spec balance_fault(term()) :: nil | {:wrong_struct, term()} | {:currency | :timestamp | :provider, term()}
What is wrong with a Balance a fake returned, or nil. Assertion 24.
{:currency, value} unless it is non-blank text (an amount attributable to no asset cannot
be sized, booked or reconciled), {:timestamp, value} unless a DateTime, {:provider, value} when empty. :balance is deliberately not judged: it may honestly be nil.
@spec book_depth_fault(term(), pos_integer()) :: nil | {:too_many_levels, :bids | :asks, non_neg_integer()} | {:answered, term()}
What is wrong with the answer to a depth request, or nil: {:too_many_levels, side, n}
when a side carries more than depth levels.
{:error, {:invalid_option, :depth, _}} is nil: the venue's own minimum, the family's
refusal for a value it cannot honour. Any other non-success is {:answered, answer}.
It was nil for every refusal, so a fake that refused depth: 1 for an unrelated reason
(a missing option, a raise) passed an assertion documented as "the fake must honour
:depth" without a single book being examined. The ask is made with the same arguments as
the baseline book of group 25, which already succeeded.
What is wrong with the answer to a request carrying limit:, given the answer baseline
without it, or nil. Only what the convention requires: Coinbase refuses :limit
({:unsupported_option, :limit}) because its old option meant depth, while a venue whose
book has no :limit ignores it as Core's usage rules let it, and one with a native :limit
may refuse a value ({:invalid_option, :limit, v}). So a success and those two refusals are
fine; a raise (:raised) or any other answer ({:answered, answer}) is not. Nothing is
judged when the baseline itself was not a success.
@spec book_ordering_fault(term(), :asc | :desc) :: nil | {:too_thin, non_neg_integer()} | {:unreadable_level, term()} | {:unsorted, [Decimal.t()]}
What is wrong with one side of an order book a fake returned, or nil. Assertion 25.
direction is :desc for bids and :asc for asks. {:too_thin, n}: fewer than two levels,
which no comparator can order wrongly, so the assertion would pass having checked nothing.
{:unreadable_level, level}: a level that is not {Decimal, size}. {:unsorted, prices}:
the prices are not best first.
@spec book_probe_depth() :: pos_integer()
The :depth assertion 38 asks for: one level per side.
Whether the unbounded candle ask of assertion 37 gave the group anything to check, or
{:no_candles, answer}.
Only a non-empty {:ok, candles} is something to check. A refusal, a raise or an empty
list used to skip the whole group, which then reported green having examined no candle.
@spec candle_range_probes([struct()], String.t() | nil) :: [ {keyword(), [DateTime.t()]} ]
The ranges assertion 37 re-asks with, as {range, must_include}, chosen from the fake's own
bars (oldest first). Two or more bars: start the first bar's opened_at and end the last
one's, which must keep the first and drop the last. One bar: start alone (keeps it) and
end alone (drops it). None: nothing to ask.
With timeframe given and a fixed width, the one-bar ask that must keep the bar is
start: the bar and end: one width later, not start: alone. A start: with no end: runs
to the venue's "now", and a venue that bounds a window by its width (Coinbase's 350 buckets)
refuses a fake's old fixed bar for being too far back, which is a refusal about the clock the
fake was written against and not about the range. A width Core has no rule for (1w) keeps
start: alone.
@spec candle_series_fault([struct()], String.t(), [atom()]) :: nil | {:wrong_timeframe | :unordered | :unaligned, term()}
What is wrong with the candles a fake returned for timeframe, or nil. Assertion 37.
{:wrong_timeframe, label}— a bar labelled with a width other than the one asked for. A bar stamped"1d"that was asked for as"1h"is a missing granularity becoming the closest one in the label, which is how every bar then gets joined to the wrong series; group 21 holds the refusal of an unserved width, and this holds the label of a served one. Judged first.{:unordered, opened_at}— a bar that is not later than the one before it. Oldest first, strictly.{:unaligned, opened_at}— a bar not on its width's grid. The grid isTimeframe.aligned?/2(strict, UTC), except that for a venue that is not crypto-only (asset_classes) a width wider than 30 minutes and shorter than a day is session-anchored (US equity sessions open at 09:30 ET) and only needs 30-minute alignment. Daily and longer bars are mapped to 00:00 UTC of the session date and stay strict. Widths the vocabulary does not model are not judged.
@spec captured_notice_faults( module(), [String.t()], keyword(), String.t() | nil, timeout() ) :: {non_neg_integer(), list()}
Drives fake through subscribe_notices/1, subscribe/2 of pairs, subscribe/2 of
unlisted when given, and unsubscribe/2, then collects every Core.Notice delivered to
this process until none arrives for wait milliseconds. Assertion 45.
Returns {judged, faults}: how many notices were captured, and notice_batch_faults/1 of
them. A call that raises is {0, [{:raised, error}]}. judged of 0 means the fake
emitted nothing and the group verified nothing; the fakes in this family register a notice
subscriber silently, so that is the usual case, and it must not be read as coverage.
Whether a term holds a pid, port or reference anywhere inside it, structs and map keys included. Assertion 11: a facade answer is data, never a process the caller must manage.
@spec case_faults(module(), [{atom(), list(), term()}]) :: [ {atom(), arity(), term(), :raised | {:answered, term()}} ]
Runs each {name, args, expected} of cases against module and returns the ones whose
answer is not {:error, expected}, as {name, arity, expected, fault} with fault as
refusal_fault/2 names it. Empty means every case was refused as the convention says.
The arguments are not kept in the result: they carry the suite's credentials, and a failure message prints what it is given.
@spec catalog_access_fault(:enumerable | :query_only, term()) :: nil | :query_required | {:answered, term()}
What is wrong with get_symbols/1's answer without a query, given the declared
catalog_access, or nil. Assertion 12.
:query_only means there is no list-everything call, so the answer must be exactly
{:error, {:query_required, venue}} (usage-rules.md). Any other answer is
{:answered, answer}: it was {:error, _}, so a fake refusing for a missing credential or
any unrelated reason satisfied a :query_only claim it never demonstrated. :enumerable
must not demand a query: :query_required otherwise.
What is wrong with venue.child_spec/1 where a secret is concerned, or nil. Assertion 22.
Hands child_spec/1 a :credentials map holding one unique canary under every secret key
name this family's venues use, and looks for the canary in inspect/2 of the answer, which
is what OTP prints on every child crash. {:leaked, rendered} when it is there. A
child_spec/1 that drops the credentials altogether is not a fault, and one that raises is
not caught here: the group fails on the raise, as it should.
Public so that a teeth test can hand it a child_spec/1 that stores the raw map, which the
inline version of this check could never be shown to catch.
@spec coverage_by_kind_fault(%{required(atom()) => map()}, map(), [atom()]) :: nil | {:union_mismatch, [String.t()], [String.t()]} | {:undeclared_kinds, [atom()]}
What is wrong with a venue's coverage_by_kind/1 answer, given the coverage/1 map of the
same moment and the declared streamable kinds, or nil. Assertion 15.
{:union_mismatch, kind_symbols, coverage_symbols} (both sorted) when the symbols across
every kind are not exactly the symbols coverage/1 reports, the two being meant as one
fact; {:undeclared_kinds, kinds} when it reports a kind streamable does not declare. The
union is judged first.
What is wrong with a call's answer when its credentials were stripped, or nil. Assertion 17.
{:succeeded, answer}-{:ok, _}: the fake served a call it could not have signed.{:raised, answer}- a raise, exit or throw ({:raised, e},{:exit, r},{:throw, v}from the suite'scall_on/3). A raise is not a refusal: the family answers with tuples, and a fake that crashes on%{}credentials holds the gate for a reason that has nothing to do with credentials.
Any other answer is nil: the refusals ({:error, _}, {:refused, _}) and the callbacks that
return a bare value, which the group walks too and which cannot be {:ok, _}.
@spec credential_gated_endpoints(DpExchange.Core.Capabilities.t(), [atom()]) :: [ {atom(), arity()} ]
The endpoints assertion 17 asks a credential_benefit: :required venue's fake to refuse
when called with credentials stripped.
Every facade callback that is answerable (not child_spec/1 or start_link/1, which
start something), active in caps, not one of the two whose own doc types the
credential credentials() | nil, not declared in Capabilities.no_venue_contact, and not
market_status/1 on a venue whose asset_classes are exactly [:crypto] (crypto trades
continuously, so there is no session for a credential to gate).
This is public, and the assertion calls it, so that the selection can be shown to fail: the previous tests of this decision rebuilt it by hand beside the fixtures and asserted what the fixtures did, so no change to the decision could break them. The assertion also refuses an empty result, because a loop over no endpoints reports green having examined none.
@spec credentialed_callbacks() :: [atom()]
The names of the Core.Venue callbacks whose first argument is credentials(), read from
the behaviour's own @callback specs.
The suite shapes a call's arguments by whether the callback takes its credential
positionally. That used to be a hand-written list of eleven names, and it rotted in the
way a list does: list_payment_methods/2, get_payment_method/3, get_notional_balances/3,
list_custody_fees/2, get_transactions/2, place_orders/3, cancel_all_orders/2,
preview_order/3, preview_replace/4, close_position/3 and get_trade_volume/2 all take
credentials() first and none was on it. Each was therefore called with a symbol where the
credential belongs, a fake that matched on its credentials raised, and the raise was taken
for an answer by every assertion that tolerates one. A union such as
credentials() | nil (test_connection/2) counts.
@spec eligible_cases(module(), DpExchange.Core.Capabilities.t(), [ {atom(), list(), term()} ]) :: [ {atom(), list(), term()} ]
The calls of cases that module can be asked, as {name, args, expected} triples:
exported at that arity and not declared :unsupported in caps.
A callback a venue legitimately declares :unsupported is skipped the way the suite's
other assertions skip it. It is not a pass: assertion 12 holds the declaration to the
behaviour.
What is wrong with a fake's answers for the two opt-in family reasons, or nil. Assertion 41.
unlisted is a well-formed symbol the fake does not list and price_answer is what its
get_price/2 said for it: anything but {:refused, :not_listed} is
{:unlisted_symbol, answer}. padded_answer is what a credentialed read said for a
credentials map with surrounding whitespace, from a venue whose runtime_id/0 is venue:
anything but {:error, {:invalid_credentials, venue}} is {:padded_credentials, answer}.
A nil for either answer is a reason the venue did not opt in to, and is not judged.
The modules in modules that belong to an application outside permitted.
build_lib_dir is Path.join(Mix.Project.build_path(), "lib"). A module found under it
belongs to the application named by its first path segment. A module found anywhere else is
OTP or the Elixir standard library and is permitted, and a module found nowhere is a
host-application module this package depends on without saying so, which is foreign.
The previous check could not fire for a dependency. It looked for /deps/<name>/ in
the module's path, but Mix compiles a dependency into _build/<env>/lib/<name>/ebin and
loads it from there, never from deps/. Every dependency module therefore matched nothing
and was permitted, so a package linking an undeclared dependency passed. Only the
:non_existing branch ever rejected anything.
A failure message for case_faults/2's result, one line per call that was not refused.
The optional callbacks module implements at the wrong arity, as {name, arity, arities}:
the callback's name is exported at arities but not at the one the behaviour declares, so a
caller at the declared arity gets an UndefinedFunctionError. Assertion 1.
@spec include_partial_refusal_cases(String.t(), String.t(), keyword()) :: [ {atom(), list(), term()} ]
The {name, args, expected} cases of assertion 36: get_historical_prices/4 with an
:include_partial that is not a boolean must answer {:error, {:invalid_option, :include_partial, value}}. nil is not tried: several venues read it as an absent option.
What is wrong with a write's answer to the invalid value as its amount, or nil.
Assertion 47, venue rule W4.
The answer must be exactly {:error, {:invalid_amount, value}}, the value as given: a venue
that echoes a different term has rewritten what the caller sent. {:accepted, answer} is a
success, the dangerous one (a withdrawal of NaN was sent); {:wrong_error, answer} is any
other answer, which names another reason or another tag; {:raised, answer} is a raise,
exit or throw.
The invalid amounts assertion 47 hands every amount-taking write: {label, value}.
Venue rule W4 (DpExchange.Core.Venue): a float, zero, a negative, NaN and infinity are
each {:error, {:invalid_amount, value}}. NaN and infinity are Decimals, which is how a
validator that compares with Decimal.gt?/2 before checking Decimal.nan?/1 raises.
@spec invalid_order_ids() :: [term()]
Order ids get_order/3 and cancel_order/3 must refuse as {:invalid_order_id, value}:
nil and a whitespace-only string.
@spec invalid_single_symbols() :: [term()]
Single-symbol values get_price/2 and get_top_of_book/2 must refuse as
{:invalid_symbol, value}: nil, a number, and a whitespace-only string.
@spec invalid_subscribers() :: [term()]
:to values a streaming call must refuse as {:invalid_subscriber, value}: neither a pid
nor an atom other than nil, true and false. nil is not here: it means the caller.
Symbol lists the streaming calls must refuse as {:invalid_symbols, value}: not a list, a
list holding a non-string or a blank string (whitespace-only is blank), and an improper
list. sample is a symbol the venue serves, so the bad lists are bad for exactly one reason.
@spec lower_case_delivery_fault(module(), String.t(), keyword(), non_neg_integer()) :: nil | :not_delivered
What is wrong with a fake's subscribe/2 given a lower-case pair, or nil. Assertion 29.
The real facade upper-cases subscribed symbols, so the payload arrives under pair (the
canonical form). :not_delivered when nothing arrived under it within wait milliseconds.
What is wrong with the answers to market_price_requests/2, or nil. Assertion 34.
Every answer must be {:error, {:unexpected_field, :price}} or a refusal of the duration
that was tried ({:unsupported_order_combination, type, tif},
{:missing_field, :time_in_force}, {:unexpected_field, :time_in_force}), and at least one
must be the price refusal.
{:answered, answer}— an answer that is neither: a success (the price was dropped and a different order placed than the one asked for), a raise, any other error.:price_never_refused— every duration was refused for being the wrong duration, so nothing showed what the venue does with the price.
@spec market_price_requests(map(), DpExchange.Core.Capabilities.t()) :: [map()]
The requests that put a :price on a market order, one for each time in force the venue
might want for it, or [] when caps does not support :market.
The time in force a venue takes on a market order is the venue's own (one wants :ioc, one
none at all), and the check order is time in force before price fields. So the request is
sent with no duration and with each duration caps names, and market_price_fault/1
judges the set.
What is wrong with a value (or list of values) a fake returned, or nil. Assertion 39.
A traded price (Quote.price, Trade.price, a candle's open, high, low and close) is above
zero: {:non_positive, field, value}. A TopOfBook bid or ask is nil or above zero. An
OrderBook level has a price and a size above zero and each side is best first
({:non_positive, {side, :price | :size}, value}, {:unreadable_level, side, level},
{:unsorted, side}). Any other value is not judged.
The public functions of venue that fake does not export, as a sorted list of
{name, arity}. Assertion 44.
A function on the facade and not on the Fake is an UndefinedFunctionError in a consumer's
async: true suite and an answer in production: the consumer cannot fake what the Fake
does not have. [] is conforming. The Fake may export more (test helpers); that is not
judged here.
The callbacks Core.Venue requires that module does not export, as {name, arity}.
Assertion 1.
@spec nil_recipient_fault(module(), [String.t()], keyword(), non_neg_integer()) :: nil | {:raised, Exception.t()} | {:escaped, atom(), term()} | :nothing_delivered
What is wrong with a fake's subscribe/2 given to: nil, or nil. Assertion 27.
A forwarded nil is a caller that said nothing, and nothing means the caller: it must not
raise and must deliver to the calling process. wait is how long to wait for the delivery.
{:raised, error} or {:escaped, kind, reason}; :nothing_delivered.
What is wrong with a no-request call's answer to timeout: 5_000, or nil. Assertion 46.
subscribe_notices/1 sends no HTTP request, so a transport key there is
read by nothing: a caller who passed timeout: believed the call was bounded and it was
not. It must be {:error, {:unknown_option, :timeout}}. {:ignored, answer} is any
answer that carried on (:ok, {:ok, _}); {:raised, answer} a raise, exit or throw;
{:wrong_error, answer} anything else.
@spec nothing_checked?(module(), DpExchange.Core.Capabilities.t(), [ {atom(), list(), term()} ]) :: boolean()
Whether a run of cases against module examined nothing it should have: no case is
eligible (eligible_cases/3) although caps declares at least one of the cases' calls
active, which means that call is not exported at its arity.
A venue that declares every call of cases :unsupported has nothing to hold and is
false; the declaration itself is held by assertion 12.
Whether assertion 39 examined nothing: endpoints were active but none of their answers
was checked (examined holds one boolean per endpoint, true when a value was inspected).
No active endpoint at all is not a failure (the venue declares them all :unsupported).
@spec notice_batch_faults([term()]) :: [ {atom() | nil, DpExchange.Core.Notice.fault()} ]
The rule violations in notices, as {kind, fault} pairs, judged by
DpExchange.Core.Notice.conformance_faults/1. [] is conforming, and so is an empty list:
judging nothing is not a fault, which is why group 45 also reports how many it judged.
The {name, args, expected} cases of assertion 35. opts_for gives, for a callback name,
the option lists to try; the suite passes the venue's own options with credentials stripped
and an empty list, because the id is checked before the account option and before
credentials.
The malformed place_order/3 requests of assertion 34, as {request, expected}, built
from base, a request the venue accepts: a non-map ({:invalid_order_request, request}),
a request with no :order_type ({:missing_field, :order_type}) and one with a key the
builder does not read ({:unsupported_order_field, key}).
The requests of assertion 52, as {request, reason}, built from base, a request the venue
accepts: for each of :quantity and :price that base carries, one request per value of
invalid_amounts/0 (a float, zero, a negative, NaN, infinity) in its place.
reason is {:invalid_quantity, value} or {:invalid_price, value}, the value as given
(Core.Venue.place_order/3, "Quantities and prices accept a Decimal, an integer or a
numeric string, and refuse a float, zero and negative values, and NaN and infinity"). Only
one field is bad in each request, so the refusal can only be for that field. A key base
does not carry is not probed: adding a :price to a market order would be refused for a
different reason ({:unexpected_field, :price}), and a venue whose sizing is a
:quote_size has no :quantity to spoil. [] therefore means base had neither, and the
group refuses to pass having examined nothing.
What is wrong with an Order or Fill a fake answered, or nil. Assertion 54.
answer is a struct, a list of them, or {:ok, either}; anything else (a refusal, :ok, a
map) is not judged. Each struct is rebuilt through its own new/1 (Types.Order.new/1,
Types.Fill.new/1), so a fake's order is held to everything the type refuses: a side, type,
status or time in force outside the vocabulary, a status beside a fill figure that
contradicts it, a negative size, a float, a leg that is not an OrderLeg. The fault is
{:order, message} or {:fill, message}, the constructor's own ArgumentError message.
Groups 5 and 31 judge only that a success is an Order and that its id is accepted back; a
fake that answered %Order{status: :filled, filled_quantity: 0} or side: "buy" passed
both. Every venue in the family builds its orders as struct literals, which skips the
constructor, so this is the only place the constructor's refusals reach a venue's Fake.
@spec order_values_examined(term()) :: non_neg_integer()
How many Order and Fill structs answer holds (a struct, a list of them, or
{:ok, either}), the number assertion 54 actually judged.
A group over a fake that refused every call, or answered {:ok, []} everywhere, judged
nothing and must not read as a pass; assertion 54 requires at least one when
place_order/3 is active.
The {name, args, expected} cases of assertion 50, from the suite's own argument lists for
preview_order/3 ([credentials, request, opts]) and replace_order/4
([credentials, id, changes, opts]).
preview_order/3 is handed each malformed request of order_request_cases/1, and must answer
the tuple place_order/3 answers for it. replace_order/4 is handed an empty change map and
values that are not maps, and must answer {:invalid_order_request, value}: a replace that
changes nothing is not a replace, and a non-map never raises.
The applications a package may link against: its own, every declared dependency, and what those depend on at runtime, as strings.
deps is Mix.Project.config()[:deps] in any of Mix's three shapes. The purity assertion
read it with for {dep, _rest} <- deps, which matches a two-element tuple only, so every
dependency declared with a requirement AND options — {:jason, "~> 1.4", optional: true},
{:foo, "~> 1.0", only: :test} — was silently dropped from the declared set.
@spec probe_timeframe(DpExchange.Core.Capabilities.t()) :: String.t() | nil
The width assertions 36 and 37 ask get_historical_prices/4 for: "1h" when caps declares
it, else the first declared width the shared vocabulary gives a fixed length, else the first
declared width, or nil when none is declared.
A venue is asked for a width it serves: asking for one it does not would be refused for the width before the option under test was ever read.
@spec process_credential_fault({module(), atom()} | nil) :: nil | {:leaked, pid(), String.t()} | {:probe_failed, term()}
What is wrong with a venue's running process where a secret is concerned, or nil.
Assertion 51.
probe is the :process_probe option: {module, function}, a 1-arity function that starts
the venue's own process or processes with no network from a credentials map. It is handed a
map holding one unique canary under every secret key name this family's venues use, and
must answer {:ok, pid} or {:ok, [pid, ...]}. Each process is asked for
:sys.get_status/2, which carries its process dictionary and the state it would print in a
crash report, rendered with inspect/2; the canary in it is {:leaked, pid, excerpt}. The
processes are stopped afterwards whatever the answer.
{:probe_failed, answer} when the probe starts nothing it can examine (an error, an empty
list, a non-pid, a process that does not answer to :sys): a probe that examined nothing
must not read as a pass. nil for no probe at all: the group is opt-in.
Public so that a teeth test can hand it a process that keeps the raw map, which the inline version could never be shown to catch.
@spec promised_types() :: %{required({atom(), non_neg_integer()}) => [module()]}
The Core.Types struct(s) each Core.Venue callback's success is typed as, read from the
behaviour's own @callback specs.
Returns %{{name, arity} => [module]} for every callback whose success type names a
DpExchange.Core.Types.* struct, and omits the rest. Read from the compiled specs rather
than written as a table, so it cannot drift from the contract it restates. Used by
assertion 5.
The identity fault of a venue's provider_name/0 answer, or nil. Assertion 3.
@spec range_fault(keyword(), term(), [DateTime.t()]) :: nil | {:outside_range | :missing | :not_answered, term()}
What is wrong with a ranged answer, or nil. :start is inclusive and :end exclusive, on
opened_at: {:outside_range, opened_at} for a bar outside range, {:missing, opened_at} for one of must_include that was dropped.
A range chosen to keep a bar must be answered. The ranges come from the fake's own bars, so
a window around them is one the venue has data for, and an answer that is not {:ok, list} is
{:not_answered, answer}: a fake refusing every range it was asked passed this group for
having examined no bound. When must_include is empty (the ask that must drop the bar) a
refusal stays nil: a venue may decline a window with nothing in it, and a refusal there says
nothing about the bounds.
What is wrong with a call's answer to an argument the family refuses locally, or nil.
Assertions 32 to 35.
expected is the whole error term, for example {:invalid_symbols, nil}; the answer must
be exactly {:error, expected}.
:raised— the call raised, exited or threw ({:raised, error},{:exit, reason}or{:throw, value}from the suite'scall_on/3). A raise is not a refusal.{:answered, answer}— any other answer::okfor a malformed list,{:ok, _},{:error, :feed_not_started}where a symbol was wrong, a refusal after credentials.
The identity fault of a venue's runtime_id/0 answer, or nil. Assertion 3.
The id is an atom equal to the module's last segment, underscored: DpExchange.Coinbase is
:coinbase. to_string/1 is compared, because nil, true and false stringify to words
no module segment is.
The {name, args, expected} cases of assertion 33. opts_for gives the options for a
callback name; the suite passes options with credentials stripped, because the refusal is
decided before credentials are read.
@spec staking_return_fault(term()) :: nil | {:not_a_staking_transaction | :unexamined | :raised, term()}
What is wrong with a staking write's answer to a valid call, or nil. Assertion 48, venue
rule W5.
stake/3 and unstake/3 are typed result(Types.StakingTransaction.t()), and the Fake
returns that struct as the real path does. {:not_a_staking_transaction, answer} is a
success of another kind (a raw map); {:unexamined, answer} is a refusal or error, which
proves nothing about the return and is how a fake that cannot be asked passes unseen (declare
what it needs in endpoint_opts:, or declare the endpoint :unsupported);
{:raised, answer} a raise, exit or throw.
The {kind, endpoint} pairs a package declares streamable while saying the venue does not
serve the matching pull endpoint at all (absent). Assertion 8: the two cannot both be
true.
@spec streaming_call_fault((-> term())) :: nil | {:escaped, atom(), term()} | {:not_a_value, term()}
What is wrong with a streaming call made while no feed runs, or nil. Assertion 28.
call is a zero-arity function making the call. A streaming callback returns a value
(:ok, {:error, _} or a map); {:escaped, kind, reason} when it exited, threw or raised
in the caller, {:not_a_value, result} for any other answer.
The {name, args, expected} cases of assertion 32, for the streaming calls given opts.
Every malformed symbol list on subscribe/2, unsubscribe/2 and update_symbols/2; every
bad :to on subscribe/2 and subscribe_notices/1; and one call wrong in both ways, which
must answer for the symbols, because the symbols are checked first. opts decides whether
this drives a fake or the real facade with no feed running.
Whether result is a success that gave assertion 5 a struct to check: {:ok, struct} or
{:ok, [struct | _]}.
Assertion 5 takes a refusal, an error and an empty list as "not a success, so nothing to
compare", which is right for each answer alone. Across a whole fake it is not: a fake that
refused every endpoint (a missing endpoint_opts:) or returned {:ok, []} everywhere
passed the group having compared nothing. The group requires at least one answer for which
this is true.
@spec subscribed_push_fault({:dp_exchange, term(), term()}, atom()) :: nil | :wrong_tag | :not_a_core_struct
What is wrong with a message subscribe/2 pushed, or nil when nothing is. Assertion 20.
message is the {:dp_exchange, runtime_id, payload} the suite received and runtime_id
is the venue's own DpExchange.Core.Venue.runtime_id/0.
:wrong_tag— tagged with something other than the venue'sruntime_id/0, so a process subscribed to several venues cannot tell them apart. Judged first.:not_a_core_struct— the payload is not aDpExchange.Core.Types.*struct: raw venue data forwarded to subscribers, the shape with a wired decoder that never ran.
What is wrong with a fake's subscription set after four calls, or nil. Assertion 26.
subscribe/2 adds to the live set, update_symbols/2 replaces it, unsubscribe/2 removes
only what it names; the set is read back through coverage/1. Needs two symbols the fake
serves. The fault names the first rule broken: {:never_covered, symbol} (nothing to compare
with), {:subscribe_dropped, symbols}, {:subscribe_missed, symbol},
{:unsubscribe_left, symbol}, {:unsubscribe_overreached, symbol},
{:update_not_taken, symbol}, {:update_left, symbol} or {:state_not_restored, symbols}.
The last step unsubscribes the one symbol the set still holds, and the set must then be empty again: coverage that retains anything for a symbol no longer subscribed is state that grows by one entry for every distinct symbol a venue is ever asked about.
@spec time_fault(term(), module()) :: nil | {:wrong_struct, term()} | {:observed_at, term()} | {:venue_time, term()}
What is wrong with the times on a struct a fake returned, or nil. Assertions 14 and 23.
module is the struct the endpoint promises (Quote, OrderBook or TopOfBook).
{:wrong_struct, value}; {:observed_at, value} unless it is a DateTime; {:venue_time, value} unless it is a DateTime or nil: the venue's own time, never an epoch integer, a
NaiveDateTime or a stand-in.
What is wrong with a call's answer to width, a timeframe the venue does not declare, or
nil. Assertion 21.
The answer must be exactly {:error, {:unsupported_timeframe, width}}, the width as given
(DpExchange.Core.Venue.get_historical_prices/4). :substituted is {:ok, _}: the venue
served the width it has in place of the one asked for, a missing granularity becoming the
closest one, which mislabels every candle it touches. {:wrong_answer, answer} is any other
answer: a different error, a refusal, a raise, or the tag with another value. It used to be
nil for all of them, so a fake that answered {:error, :boom} to every width it was asked
for passed a group documented as "refused, never served at the nearest width"; the refusal
was assumed, not seen.
@spec unexported_calls(module(), DpExchange.Core.Capabilities.t(), [ {atom(), list(), term()} ]) :: [ {atom(), arity()} ]
The {name, arity} of each call in cases that caps declares active and module does not
export, in the order first met.
eligible_cases/3 drops such a case without a word, so a fake missing one of two calls was
judged on the other and passed. Used by assertions 33 and 35, whose calls
(get_price/2, get_top_of_book/2, get_order/3, cancel_order/3) are required
Core.Venue callbacks, so a declaration of one active with no export behind it is a fake that
cannot be asked, not a call the venue does not serve.
@spec unknown_id_endpoints() :: [{atom(), non_neg_integer()}]
The reads and cancels assertion 49 asks about an unknown id: the order callbacks Core.Venue
documents {:refused, :not_found} for. Other id-taking callbacks (get_conversion/2,
get_payment_method/3, get_watchlist/2) have no documented answer for an unknown id, so
none is asserted.
What is wrong with a fake's answer to an order id nobody placed, or nil. Assertion 49,
venue rule W6.
The real path answers a 404 {:refused, :not_found} (DpExchange.Core.Venue.cancel_order/3).
{:invented, answer} is a success: the fake made up an order for an id it never issued, so a
consumer's test of its not-found handling can never run. {:wrong_answer, answer} is any
other answer, including a refusal with another reason, or an error from a call the fake could
not place for lack of endpoint_opts:; {:raised, answer} a raise, exit or throw.
Whether an endpoint's answer to the probe is evidence the options were checked: it could be
judged (unknown_option_judged?/1), or it is the {:unknown_option, key} refusal itself.
A refusal of the probe is proof whatever the plain call did, so an endpoint the fake refuses for a venue reason (an order id it has not placed) still counts when it refused the option too. Assertion 43 requires at least one such answer.
@spec unknown_option_exclusions() :: %{ required({atom(), non_neg_integer()}) => String.t() }
The callbacks assertion 43 cannot hand an unknown option, each with the written reason.
Everything else the facade declares is probed. An exclusion is a decision, so it is listed
here rather than skipped inside the group: a callback absent from both unknown_option_probed/1
and this map would be silently unprobed, and the teeth tests hold that the two cover the
whole behaviour.
What is wrong with a read's answer to a call carrying unknown_option_probe/0, or nil.
Assertion 43.
A facade call given an option it does not read must refuse it,
{:error, {:unknown_option, :dp_contract_unknown_option}}, not ignore it: an ignored
misspelling (limt:) is a plausible answer to a different question. {:error, :not_supported} is also fine, since an endpoint the venue does not serve answers that
whatever the options. {:ignored, answer} is a success or a refusal that carried on as if
the key were absent; {:wrong_error, answer} is any other answer, which names a different
key or is not the family tag; {:raised, answer} is a raise, exit or throw.
@spec unknown_option_fault(:strict | term(), term()) :: nil | {:ignored | :wrong_error | :raised, term()}
unknown_option_fault/1, for an endpoint whose plain call is the baseline.
With :strict or a baseline that succeeded, the answer is judged by unknown_option_fault/1.
With any other baseline the fake refused the generated call for its own reason, so the
options were never reached and only two things are faults: a raise, and a success (a probe
cannot turn a refusal into a success).
Whether assertion 43 could judge an endpoint's answer to the probe.
mode is :strict (judged on the probe alone) or the answer the same call gave without the
probe. A write or a read the fake refuses for its own reasons before it reads the options (a
venue-specific refusal of the generated arguments) gives no evidence about the options, so it
is not judged; the group requires at least one judged answer.
@spec unknown_option_probe() :: :dp_contract_unknown_option
The option key assertion 43 hands every read: one no venue reads and none can mistake for its own.
@spec unknown_option_probed([{atom(), non_neg_integer()}]) :: [ {atom(), non_neg_integer()} ]
The callbacks of callbacks assertion 43 probes: all of them, less
unknown_option_exclusions/0.
@spec unknown_option_strict() :: [{atom(), non_neg_integer()}]
The five reads assertion 43 judged before it covered every endpoint. They are judged on the probe alone, with no baseline call: a refusal of the probe key there is a fault whatever the plain call would have answered.
@spec unknown_order_id() :: String.t()
The order id assertion 49 asks about: one no write of the suite ever minted.
@spec unknown_stream_kinds(DpExchange.Core.Capabilities.t()) :: [term()]
The kinds in streamable or authenticated_streamable that this contract has no word for
(Capabilities.data_kinds/0). Assertion 8.
@spec unrecorded_absences([{atom(), arity()}], DpExchange.Core.Capabilities.t()) :: [ {atom(), arity()} ]
The endpoints venue_does_not_serve/0 names that caps does not declare :unsupported.
Assertion 8: an endpoint the venue does not serve cannot also be one the package answers.
@spec unserved_timeframe(DpExchange.Core.Capabilities.t()) :: String.t() | nil
The timeframe assertion 21 asks get_historical_prices/4 for: the first width of
Timeframe.nameable/0 that caps.historical_timeframes does not name, or nil when the
venue declares the whole vocabulary and there is no width left to prove a refusal with.
@spec unsupported_fault(term()) :: nil | :string_refusal | :raised | :served | :other
What is wrong with a call's answer to an endpoint capabilities/0 declares :unsupported,
or nil when nothing is. Assertion 12's under-declaring direction.
nil—{:error, :not_supported}, the atom the contract promises.:string_refusal—{:error, "not_supported"}. A caller matching the atom misses it and treats a refusal as an unrecognised error.:raised— the call raised ({:raised, error}from the suite'scall_on/3).:served—{:ok, _}: declared unsupported but it works, which hides functionality a consumer could have used.:other— any other answer, which is neither the atom nor a success.
@spec use_options() :: [atom()]
The options use DpExchange.Core.AdapterContract accepts.
Anything else is refused by validate_use_opts!/1: a misspelled option (fkae:,
sample_pair:) used to be ignored, and the group it was meant to switch on then ran against
its default or was skipped, which reads as a pass.
Checks the order_args: option of use DpExchange.Core.AdapterContract. It must be a map
whose keys are drawn from :order_request, :order_requests and :order_changes.
An unknown key raises at compile time instead of being ignored. A misspelt key that was silently dropped would leave the suite's default order in place, and the assertion it was meant to reach would go on failing, or passing, for a reason nobody declared.
Validates the options given to use DpExchange.Core.AdapterContract, returning them.
Raises ArgumentError for a non-keyword list, or a key outside use_options/0.
Whether value, the success of a price endpoint, gave assertion 39 anything to inspect.
A struct does, except an OrderBook with no level on either side and a TopOfBook with
neither a bid nor an ask (nothing there for market_value_fault/1 to hold). A list does when
one of its elements does, so {:ok, []} from get_trades/2 or an empty candle list does
not. Anything else, a bare map or a scalar, is not something market_value_fault/1 judges.
{:ok, _} of any shape used to count as examined, so a fake answering {:ok, []} for every
price endpoint passed the group having looked at no price, size or level at all.
@spec volume_window_fault(term()) :: nil | {:wrong_struct, term()} | {:window_without_volume | :unstated_window, term()}
What is wrong with the volume window on a Quote a fake returned, or nil. Assertion 30.
A quote with no volume names no window: {:window_without_volume, window}. A quote with a
volume names which quantity it is (one of Quote.volume_windows/0): {:unstated_window, window}.
What is wrong with a fake's wanted/1, or nil. Assertion 40.
A fake that does not export wanted/1 has not adopted the optional callback and is never
faulted. Otherwise, after subscribe/2 of pairs, wanted/1 is a list or a MapSet
({:not_a_collection, value}), holds every symbol subscribed
({:subscribed_not_wanted, symbols}) and holds every symbol coverage/1 reports
({:covered_not_wanted, symbols}), because what arrived was asked for. The set is cleared
with update_symbols/2 first, so an earlier subscription cannot satisfy it.