Skip to content

Commit 2bb7d49

Browse files
committed
T2.1i: the case format carries rows_per_call and the failed call
T2.8c's cases need two inputs no case could carry: the caller's rows_per_call (main's 1fc5a68) and the call the scripted endpoint fails (T2.1a built the endpoint's fail_call for these cases, serve and answer_call, but no case field reached it). - contract/case.schema.json: input.materialize.rows_per_call, any JSON scalar but null, passed as written, so the values the reference refuses (0, -1, 2.5, 3.0, true, "3") are a case's input; and input.fail_call, a whole number from 0 to 2**64 - 1, the call's number from zero. - run_python.py passes rows_per_call to materialize as written and serves the endpoint with the case's fail_call; its loader refuses a fail_call written with a fraction, as JSON Schema alone does not. - sdm-testkit's loader reads both (rows_per_call kept as the text the case wrote, so 2**64 and 3.0 pass through nothing that changes them; fail_call as a u64). The test host's use of them (rows_per_call in the call's values, predict answered through an Endpoint failing the case's fail_call) stays with multi-call predict, which lands after: the gate builds a candidate with its checkpoint's case.rs, so the case format lands alone first (lesson 13). - malformed-cases.json: the refused and accepted values of both, which both loaders' tests run. The host's reading of RequestState.batches and CoreError.answered is the implementer's (T2.1b fixed them in the contract). Signed-off-by: Adi Wadaskar <awadaskar@nvidia.com> (cherry picked from commit 6577580758d764116f1090716398e3a64da8777a)
1 parent d339e88 commit 2bb7d49

4 files changed

Lines changed: 199 additions & 4 deletions

File tree

‎contract/case.schema.json‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@
5454
"from_source": {"$ref": "#/$defs/from_source"},
5555
"edits": {"$ref": "#/$defs/edits"},
5656
"pql": {"type": "string", "minLength": 1},
57-
"materialize": {"$ref": "#/$defs/materialize"}
57+
"materialize": {"$ref": "#/$defs/materialize"},
58+
"fail_call": {
59+
"description": "The call the scripted endpoint fails, numbered from zero in the order the calls are received (T2.1a's serve(fail_call=), for main's multi-call predict, T2.8c): that call is answered 500 scripted_failure and every other by the rule; absent, no call fails. A JSON integer written as one, below 2**64.",
60+
"type": "integer",
61+
"minimum": 0,
62+
"maximum": 18446744073709551615
63+
}
5864
}
5965
},
6066
"until": {
@@ -264,6 +270,10 @@
264270
"top_k": {
265271
"description": "How many of the candidates a ranking or forecast answers, a JSON integer written as one; a value the reference refuses (0) is the case's input.",
266272
"type": "integer"
273+
},
274+
"rows_per_call": {
275+
"description": "How many entities one model call scores (main's 1fc5a686, T2.8c), passed as written: a JSON integer as an integer of any size, a number written with a fraction or an exponent as a float, true or false as a bool, a string as text, so a value the reference refuses is the case's input.",
276+
"type": ["integer", "number", "boolean", "string"]
267277
}
268278
}
269279
},

‎contract/runners/run_python.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ def _invalid(validate: Any, document: Any) -> str | None:
153153

154154
def _indices(given: dict[str, Any]) -> str | None:
155155
r"""An entity key is a JSON integer literal of any size, or a string,
156-
in ``indices`` and ``candidates`` alike; ``top_k`` is an integer.
156+
in ``indices`` and ``candidates`` alike; ``top_k`` and ``fail_call``
157+
are integers (``rows_per_call`` is passed as written, whatever it is).
157158
158159
JSON Schema counts ``1.0`` as an integer; ``json`` reads it as a float,
159160
and a bool is no index either, so the type read is checked too, as
@@ -166,6 +167,8 @@ def _indices(given: dict[str, Any]) -> str | None:
166167
return f'input/materialize/{key}/{position} is not an integer'
167168
if 'top_k' in spec and type(spec['top_k']) is not int:
168169
return 'input/materialize/top_k is not an integer'
170+
if 'fail_call' in given and type(given['fail_call']) is not int:
171+
return 'input/fail_call is not an integer'
169172
return None
170173

171174

@@ -2432,6 +2435,10 @@ def _materialize_arguments(spec: dict[str, Any]) -> dict[str, Any]:
24322435
arguments['candidates'] = list(spec['candidates'])
24332436
if 'top_k' in spec:
24342437
arguments['top_k'] = spec['top_k']
2438+
if 'rows_per_call' in spec:
2439+
# As written (T2.8c's cases): a value the reference refuses is the
2440+
# case's input.
2441+
arguments['rows_per_call'] = spec['rows_per_call']
24352442
return arguments
24362443

24372444

@@ -2651,7 +2658,7 @@ def _call(
26512658
if log is not None:
26522659
log.stage = stage
26532660
with (
2654-
serve() as (url, received),
2661+
serve(fail_call=given.get('fail_call')) as (url, received),
26552662
warnings.catch_warnings(record=True) as shown,
26562663
):
26572664
warnings.simplefilter('always')

‎contract/tests/malformed-cases.json‎

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"about": "Changes to base.adventureworks's case that both case loaders must refuse, and ones both must take: contract/runners/run_python.py's validator (tests/test_contract_cases.py) and sdm-testkit's case::load (tests/s0_cases.rs). Each refused or accepted change sets the value at path, and each omitted one removes the key at path (which both must take), a list of keys and positions into the case (T1.21b; from T1.21c the schema's minProperties, minItems and minLength, core.materialize.seed required, and expect's if/then/else; from T1.21d core.materialize naming a schema, refused; from T1.24b the edits set_primary_key, set_time_column, set_stype and set_relationships; from T2.1a materialize's candidates and top_k, while a databases or exclusion change, which needs its partner, is held in tests/test_t21a_case_format.py through both keys).",
2+
"about": "Changes to base.adventureworks's case that both case loaders must refuse, and ones both must take: contract/runners/run_python.py's validator (tests/test_contract_cases.py) and sdm-testkit's case::load (tests/s0_cases.rs). Each refused or accepted change sets the value at path, and each omitted one removes the key at path (which both must take), a list of keys and positions into the case (T1.21b; from T1.21c the schema's minProperties, minItems and minLength, core.materialize.seed required, and expect's if/then/else; from T1.21d core.materialize naming a schema, refused; from T1.24b the edits set_primary_key, set_time_column, set_stype and set_relationships; from T2.1a materialize's candidates and top_k; from T2.8c's test author materialize's rows_per_call, any JSON scalar but null, and input.fail_call, a whole number from 0 to 2**64 - 1, while a databases or exclusion change, which needs its partner, is held in tests/test_t21a_case_format.py through both keys).",
33
"case": "contract/cases/s1/base.adventureworks.json",
44
"refused": [
55
{
@@ -915,6 +915,85 @@
915915
"top_k"
916916
],
917917
"value": null
918+
},
919+
{
920+
"why": "rows_per_call null",
921+
"path": [
922+
"input",
923+
"materialize",
924+
"rows_per_call"
925+
],
926+
"value": null
927+
},
928+
{
929+
"why": "rows_per_call a list",
930+
"path": [
931+
"input",
932+
"materialize",
933+
"rows_per_call"
934+
],
935+
"value": [
936+
3
937+
]
938+
},
939+
{
940+
"why": "rows_per_call an object",
941+
"path": [
942+
"input",
943+
"materialize",
944+
"rows_per_call"
945+
],
946+
"value": {
947+
"n": 3
948+
}
949+
},
950+
{
951+
"why": "fail_call as text",
952+
"path": [
953+
"input",
954+
"fail_call"
955+
],
956+
"value": "2"
957+
},
958+
{
959+
"why": "fail_call written with a fraction",
960+
"path": [
961+
"input",
962+
"fail_call"
963+
],
964+
"value": 2.0
965+
},
966+
{
967+
"why": "fail_call as a boolean",
968+
"path": [
969+
"input",
970+
"fail_call"
971+
],
972+
"value": true
973+
},
974+
{
975+
"why": "fail_call below zero",
976+
"path": [
977+
"input",
978+
"fail_call"
979+
],
980+
"value": -1
981+
},
982+
{
983+
"why": "fail_call null",
984+
"path": [
985+
"input",
986+
"fail_call"
987+
],
988+
"value": null
989+
},
990+
{
991+
"why": "fail_call past what a u64 holds",
992+
"path": [
993+
"input",
994+
"fail_call"
995+
],
996+
"value": 18446744073709551616
918997
}
919998
],
920999
"accepted": [
@@ -1158,6 +1237,76 @@
11581237
"top_k"
11591238
],
11601239
"value": -1
1240+
},
1241+
{
1242+
"why": "rows_per_call of 3",
1243+
"path": [
1244+
"input",
1245+
"materialize",
1246+
"rows_per_call"
1247+
],
1248+
"value": 3
1249+
},
1250+
{
1251+
"why": "rows_per_call of 0, an input the reference refuses, carried as a case",
1252+
"path": [
1253+
"input",
1254+
"materialize",
1255+
"rows_per_call"
1256+
],
1257+
"value": 0
1258+
},
1259+
{
1260+
"why": "rows_per_call written with a fraction, an input the reference refuses",
1261+
"path": [
1262+
"input",
1263+
"materialize",
1264+
"rows_per_call"
1265+
],
1266+
"value": 2.5
1267+
},
1268+
{
1269+
"why": "rows_per_call as a boolean, an input the reference refuses",
1270+
"path": [
1271+
"input",
1272+
"materialize",
1273+
"rows_per_call"
1274+
],
1275+
"value": true
1276+
},
1277+
{
1278+
"why": "rows_per_call as text, an input the reference refuses",
1279+
"path": [
1280+
"input",
1281+
"materialize",
1282+
"rows_per_call"
1283+
],
1284+
"value": "3"
1285+
},
1286+
{
1287+
"why": "rows_per_call past int64, a JSON integer all the same",
1288+
"path": [
1289+
"input",
1290+
"materialize",
1291+
"rows_per_call"
1292+
],
1293+
"value": 18446744073709551616
1294+
},
1295+
{
1296+
"why": "fail_call of 0, the first call",
1297+
"path": [
1298+
"input",
1299+
"fail_call"
1300+
],
1301+
"value": 0
1302+
},
1303+
{
1304+
"why": "fail_call of 2",
1305+
"path": [
1306+
"input",
1307+
"fail_call"
1308+
],
1309+
"value": 2
11611310
}
11621311
],
11631312
"omitted": [

‎rust/crates/sdm-testkit/src/case.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ pub struct Input {
128128
#[serde(deserialize_with = "text")]
129129
pub pql: String,
130130
pub materialize: Materialize,
131+
/// The call the scripted endpoint fails, numbered from zero in the
132+
/// order received (T2.1a, for `main`'s multi-call `predict`, T2.8c); a
133+
/// JSON integer written as one, `1.0` refused as the Python loader
134+
/// refuses it.
135+
#[serde(default, deserialize_with = "present")]
136+
pub fail_call: Option<u64>,
131137
}
132138

133139
/// Keyword arguments to `Schema.from_source`, each as the case gives it.
@@ -434,6 +440,29 @@ pub struct Materialize {
434440
/// refuses, is a case's input.
435441
#[serde(default, deserialize_with = "present")]
436442
pub top_k: Option<Integer>,
443+
/// How many entities one call scores (`main`'s 1fc5a686, T2.8c), kept
444+
/// as written, whatever JSON scalar it is, so a value the reference
445+
/// refuses is the case's input.
446+
#[serde(default, deserialize_with = "present")]
447+
pub rows_per_call: Option<Written>,
448+
}
449+
450+
/// A JSON scalar other than `null` (a number, a bool or a string), kept as
451+
/// the text the case wrote: a number of any size or form passes through
452+
/// nothing that would change it.
453+
#[derive(Debug, Clone, PartialEq, Eq)]
454+
pub struct Written(pub String);
455+
456+
impl<'de> Deserialize<'de> for Written {
457+
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Written, D::Error> {
458+
let raw = Box::<RawValue>::deserialize(deserializer)?;
459+
let text = raw.get().trim();
460+
match text.as_bytes().first() {
461+
Some(b'"' | b'-' | b'0'..=b'9') => Ok(Written(text.to_owned())),
462+
_ if text == "true" || text == "false" => Ok(Written(text.to_owned())),
463+
_ => Err(D::Error::custom("not a number, a bool or a string")),
464+
}
465+
}
437466
}
438467

439468
/// One entity key: a JSON integer literal of any size, kept as its

0 commit comments

Comments
 (0)