This directory is the language-independent contract between sysml-grpc and its clients. A
scenario states one call and what the service must answer; nothing here names a transport, a
programming language, or a client's object model. The reference runner is
cmd/conformance, which builds and starts the service itself:
make conformance # the CI gate; writes bin/conformance-report.json and .xml
go run ./cmd/conformance -v # print each scenario's normalized response
go run ./cmd/conformance -run evaluate # only the scenarios whose id matches
go run ./cmd/conformance -binary ./bin/sysml-grpc # test a binary already built
go run ./cmd/conformance -protocols grpc,connect,connect-json-report <file> writes the machine-readable summary (- writes it to stdout). -junit <file>
additionally writes the same results as JUnit XML — one suite per configuration and protocol, one
case per scenario — the format CI systems render natively as a test report. The top-level
object contains aggregate totals and a protocols list. Each protocol entry retains the
capabilities and one result per scenario with its outcome, status, duration and, when it
disagreed, the list of mismatches. The default protocols are grpc, connect (protobuf body)
and connect-json (JSON body); all three runs use one service process. Use -transport grpc -protocols grpc to exercise a grpc-go-only service. Connect protocol clients issue unary POSTs
to /sysml.SysMLService/<Method> with application/proto or application/json bodies.
scenarios/*.json— the scenarios, grouped by RPC and run in file then declaration order.fixtures/*.sysml— the models scenarios parse. A scenario names a fixture, never a path.
{
"id": "evaluate/a_subject_is_evaluated_against_that_objects_value",
"description": "Why this is part of the contract, in one sentence.",
"rpc": "Evaluate",
"requires_capabilities": ["evaluate_subject"],
"model": { "fixture": "vehicle.sysml" },
"request": { "model_hash": "${model_hash}", "expression": "mass", "subject_symbol_id": "Demo::sedan" },
"expect": { "response": { "result": { "real_value": 1200.0 } } }
}| Field | Meaning |
|---|---|
id |
Unique; a report and -run address a scenario by it. |
rpc |
Method name, bare (Evaluate) or qualified (sysml.SysMLService/Evaluate). |
requires_capabilities |
Names GetServerInfo must report for expect to apply. |
expect_without_capability |
What a service not reporting them must answer instead. |
model |
A fixture parsed once per run before the call; its hash fills ${model_hash}. |
request |
The request as protobuf-JSON. |
expect |
What the answer must be, by the rules below. |
Two placeholders may appear in a request: ${model_hash} is the hash the service gave model,
and ${fixture:<name>} is a fixture's source, for the RPCs that take content inline.
Every field of expect is optional and all of them must hold. An absent status means the call
must succeed.
| Field | Meaning |
|---|---|
status |
Canonical gRPC status name (OK, INVALID_ARGUMENT, NOT_FOUND, UNIMPLEMENTED, …). |
status_message_contains |
Substring the status message must contain. |
response |
Tree compared field by field against the answer; see below. |
non_empty |
Paths that must hold a value other than their default. |
absent |
Paths that must be unset or hold their default. |
contains |
Path → substring its text must contain. |
contains_all |
Path → strings all of which must be there: substrings of text, or members of a list. |
counts |
Path → exact number of entries of the list or map there. |
min_counts |
Path → lower bound on that number. |
A path is dotted, walking fields, map keys and list indices —
instance.feature_values.mass.value.real_value — and * takes one field from every entry of a
list or map, as in elements.*.id.
Getting these wrong is how a conformance suite becomes flaky or vacuous, so they are stated rather than left to the runner:
responseis compared where it is named, exactly. A field the expectation does not mention is not compared, so adding a field to the schema does not fail a scenario. Every field it does mention must be equal, including the length of any list it names: a list of two expected entries does not match three actual ones.- An unset field and a field holding its default are the same thing, because that is what
they are on the wire.
"error": ""matches an answer that carries no error, andabsentandnon_emptyread the default the same way. - Reals compare within a relative tolerance of 1e-9, which admits a different summation
order and admits no difference a model would state. Integers, booleans and strings compare
exactly. An enum compares by the name the schema gives it (
EDIT_FAILURE_UNKNOWN_TARGET), not by number, so renumbering is caught and renaming is legible. - A status is compared by code, spelled canonically (
NOT_FOUND), never by message text unlessstatus_message_containssays so. - A refused call and a failure the answer reports are different things, and a scenario says
which it expects.
statusis the transport's verdict;error(andfailure,failure_reason) are fields of a successful answer. A scenario expectingNOT_FOUNDfails if the service answersOKwith an error field, and the other way round.
These cannot be compared literally, so the runner replaces them before comparing:
| Value | Becomes | Why |
|---|---|---|
ServerInfoResponse.version |
${version} |
A build string; the contract is capabilities, not versions. |
| Any string equal to the model hash of the scenario's model | ${model_hash} |
Content-addressed and free to change with the parser. |
Any absolute path (Span.file, echoed request paths) |
${path} |
Names the machine the service ran on. A relative name is kept. |
Runtime instance ids (Instance.id, Value.instance_id, Verdict.instance_id, Function.self_id) |
@1, @2, … |
Assigned per call. Labelled in order of first appearance, so a scenario can still state that a feature value names the same object as an entry of instances. |
- Timing. Durations are recorded in the report and compared to nothing.
- Diagnostic message text and spans. Scenarios pin the number of diagnostics and their
severity; wording is not a wire contract, and a message is asserted only where a scenario says so explicitly withcontains. - Field order. Map keys are compared as a map, and repeated fields keep the order the
service sent them, which for
verdicts,states_visitedandappliedis part of the contract and is compared.
A client selects behaviour by capability name, never by version string, so a scenario needing
one names it in requires_capabilities. If GetServerInfo does not report it, the scenario is
skipped — and a skip fails the run unless -allow-skips is passed, so a service quietly losing
a capability does not turn a gate green. A scenario may instead state what a service lacking the
capability must answer, in expect_without_capability; that is where the suite pins that an
unsupported request is refused with UNIMPLEMENTED rather than silently ignored.
What a request asks for is fixed per capability:
| Capability | Request-side contract when unavailable |
|---|---|
convert |
Refuse Convert. |
verification |
Refuse VerifyConstraint, VerifyRequirement, VerifySatisfaction and EvaluateCalc. |
query |
Refuse Query. |
oslc_query |
Refuse Query only when oslc_query is set; structured queries still use query. |
apply_edits |
Refuse ApplyEdits. |
authoring |
Refuse ApplyEdits only when an operation is add_member or delete. |
inline_language |
Refuse ParseFile only when inline content names a language. |
strict_conformance |
Refuse ParseFile only when strict_conformance is true. |
evaluate_subject |
Refuse Evaluate only when subject_symbol_id is set. |
schedule |
Refuse ExecuteAction, ExecuteState and RunAnalysis only when schedule is set; an empty field runs under the default policy. |
type_facts |
Response-population capability: omit type facts; no request asks for them. |
symbol_attributes |
Response-population capability: omit symbol attributes; no request asks for them. |
feature_values |
Response-population capability: omit instance feature values; no request asks for them. |
enum_values |
Response-population capability: encode enum values as unsupported nulls; no request asks for them. |
unset_value |
Response-population capability: encode unset values as unsupported nulls; no request asks for them. |
complex_values |
Response-population capability: encode complex numbers as unsupported nulls; no request asks for them. |
structured_values |
Encode arrays, vectors and vector quantities as unsupported nulls; refuse a request carrying one, at any depth. |
measurement_refs |
Encode bare measurement references as unsupported nulls; refuse a request carrying one, at any depth. |
function_values |
Encode functions (a calc read as a value) as unsupported nulls; refuse a request carrying one, at any depth. |
The default service reports and supports every capability above. make conformance also starts a
second service with strict_conformance and oslc_query withheld, verifies that its advertisement
is exactly the default list minus those names, and requires both fallback expectations to execute
under gRPC, Connect and Connect-JSON. The exact default GetServerInfo scenario is replaced in that
configuration by this stronger set comparison.
Withholding is test-only. cmd/conformance passes
OPENSYSML_TEST_WITHHOLD_CAPABILITIES to the child process it starts; normal startup strips no
capability, and the variable is not a supported service configuration interface.
A suite that passes against a broken service is worse than none, so what the scenarios catch is verified rather than assumed:
cmd/conformance's own tests pin the comparison rules — tolerance, list length, default handling, path lookup, id labelling, status naming — with cases that must fail as well as cases that must pass.TestEveryRPCIsCoveredfails if an RPC of the service is reached by no scenario, andTestTheSuiteCoversBothKindsOfFailurefails if the suite stops pinning refused requests or in-band failures.- The suite is run against deliberately mutated builds of the service; the mutations and the scenarios that caught them are recorded in the pull request that added the suite.
The scenarios are the specification and cmd/conformance is one reading of it. A runner in
another language needs: protobuf-JSON decoding of request into the RPC's request message,
the normalization table above, the comparison rules above, capability gating from
GetServerInfo, and the same report shape. Nothing else in this directory is gRPC-specific:
rpc names a method of sysml.SysMLService, and how that method is reached is the transport's
business.