Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
405 changes: 395 additions & 10 deletions tools/probe/corpus/claims.json

Large diffs are not rendered by default.

189 changes: 131 additions & 58 deletions tools/probe/corpus/harness/DecimalSpelling.cs

Large diffs are not rendered by default.

37 changes: 33 additions & 4 deletions tools/probe/corpus/harness/Equality.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ static object Make(Type v, Type w, object value)
x1 = Make(v, w, a1); x2 = Make(v, w, a2); y = Make(v, w, b);
if (!cw.Equals(v.GetProperty("Value")!.GetValue(x1), a1)) return $"Value of {v.Name} is not the value it was made from";
}
var fails = new List<string>();
// each failure goes to the obligation it breaks: the value object's own equality and wherever it is held, its ordering, its bases
List<string> eqF = [], ordF = [], baseF = [];
var fails = eqF;
void Expect(bool got, bool want, string what) { if (got != want) fails.Add($"{what}: {got}, expected {want}"); }

// the value object's own equality
Expand All @@ -86,6 +88,7 @@ static object Make(Type v, Type w, object value)
if (Op(v, "op_Inequality", x1, x2) is { } n12) Expect((bool)n12, !eq12, "!= of equal values");

// ordering: generated where the wrapped type is comparable, ordinal for a string, zero exactly when equal
fails = ordF;
var icv = typeof(IComparable<>).MakeGenericType(v);
if (!closed)
{
Expand Down Expand Up @@ -113,8 +116,13 @@ static object Make(Type v, Type w, object value)
}

// through every base, and every IEquatable<TBase> it implements
var bases = 0;
for (var t = v.BaseType; t is not null; t = t.BaseType)
{
// object and ValueType are every value object's, a base of none: held as one, it is its own equality
var own = t == typeof(object) || t == typeof(ValueType);
if (!own) bases++;
fails = own ? eqF : baseF;
var c = Comparer(t);
Expect(c.Equals(x1, x2), eq12, $"EqualityComparer<{t.Name}> of equal values");
Expect(c.Equals(x1, y), false, $"EqualityComparer<{t.Name}> of unequal values");
Expand All @@ -127,12 +135,14 @@ static object Make(Type v, Type w, object value)
}
foreach (var i in v.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEquatable<>)))
{
fails = i.GetGenericArguments()[0] == v ? eqF : baseF; // IEquatable<TSelf> is its own equality, IEquatable<TBase> a base's
var m = i.GetMethod("Equals")!;
Expect((bool)m.Invoke(x1, [x2])!, eq12, $"{Jx.Name(i)}.Equals of equal values");
Expect((bool)m.Invoke(x1, [y])!, false, $"{Jx.Name(i)}.Equals of unequal values");
}

// wherever it is held
fails = eqF;
var position = r["shape.position"];
var containing = position == "root" ? "class" : r["shape.containing"];
var e = new Entry(kind, wrapped, v, v, () => x1, () => null, typeof(L05PolyBase), typeof(L05PolyDerived<>).MakeGenericType(v), typeof(L05PolyBase), typeof(L05PolyDerived<>).MakeGenericType(v));
Expand Down Expand Up @@ -175,8 +185,27 @@ static object Make(Type v, Type w, object value)
break;
}
}
if (fails.Count > 0) return $"{Jx.Name(v)} ({kind}, {wrapped}, {inh}, {site}, {position} in {containing}): " + string.Join("; ", fails);
r.Note(NoteKeys.Compared, $"{v.Name} {position}");
return null;
string What(List<string> f) => $"{Jx.Name(v)} ({kind}, {wrapped}, {inh}, {site}, {position} in {containing}): " + string.Join("; ", f);
var f1 = r.Check("equality-is-the-underlying-values", Baseline.Twin, c =>
{
if (eqF.Count > 0) return What(eqF);
c.Note(NoteKeys.Compared, $"{v.Name} {position}: equality, against EqualityComparer<{(w is null ? v.Name : w.Name)}>");
return null;
});
var f2 = r.Check("ordering-follows-the-underlying-type", Baseline.Twin, c =>
{
if (ordF.Count > 0) return What(ordF);
c.Note(NoteKeys.Compared, $"{v.Name}: ordering{(icv.IsAssignableFrom(v) ? "" : ", none generated where the wrapped type has none")}");
return null;
});
// a value object with a base record: each base compares it as it does (the chain is shape.inheritance's)
var f3 = bases == 0 ? null : r.Check("through-every-base", Baseline.Twin, c =>
{
_ = c["shape.inheritance"];
if (baseF.Count > 0) return What(baseF);
c.Note(NoteKeys.Compared, $"{v.Name}: {bases} base type(s)");
return null;
});
return f1 ?? f2 ?? f3;
}
}
2 changes: 2 additions & 0 deletions tools/probe/corpus/harness/JsonFamily.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public static string Rewrite(string json, IEnumerable<JTok> at, Func<JTok, strin

public static string Cut(string? s) => s is null ? "(null)" : s.Length > 220 ? s[..220] + "..." : s;
public static string Name(Type t) => t.IsGenericType ? t.Name[..t.Name.IndexOf('`')] + "<" + string.Join(", ", t.GetGenericArguments().Select(Name)) + ">" : t.Name;
// Text-matching site, kept in this step (WS2 step 4): the depth refusal is told by the message's words. Baseline gap: no twin in the
// row shows System.Text.Json charging a converter a level, and no record or fact cites it (the three-sided differential).
/// <summary>The serializer's depth limit under json.MaxDepth=3, which any converter is charged a level against (design note D1).</summary>
public static bool Depth(Row r, Exception? x) => x is JsonException && x.Message.Contains("maximum allowed depth") && r.Is("json.MaxDepth", "3");
/// <summary>A refusal's message with the type the other side holds named instead: the serializer names the shape it refused.</summary>
Expand Down
123 changes: 69 additions & 54 deletions tools/probe/corpus/harness/OffTheWire.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static class OffTheWire
var position = r["shape.position"];
var containing = position == "root" ? "class" : r["shape.containing"];
var shape = r["or.shape"]; var op = r["or.operation"];
_ = r["input.json"]; // the row's input, which every check of the row runs under; the read applies it
if (shape == "Result-E" && op is "query-syntax" or "FirstOrNone")
{ r.Substituted("or.operation", "Match", "Result<TError> has no " + op + " (claims.json excludes the pair)"); op = "Match"; }
var voValue = Make.Vo(e, e.Valid());
Expand All @@ -61,70 +62,84 @@ public static class OffTheWire
if (nullableUnderContext)
return x is InvalidOperationException ? null : $"{dir}: a source-generated context refuses an {name}? property with the serializer's InvalidOperationException; got {(x is null ? "no exception" : Jx.Ex(x))}";
if (x is not NotSupportedException) return $"{dir}: expected NotSupportedException naming {name}, got {(x is null ? "no exception" : Jx.Ex(x))}";
// Text-matching site, kept in this step (WS2 step 4): the refusal is told TypeKit's by its words ("not a wire type"). Baseline
// gap: a NotSupportedException of the serializer's own is the same type, and no twin in the row refuses as the serializer alone does.
if (!x.Message.Contains(name) || !x.Message.Contains("not a wire type")) return $"{dir}: the NotSupportedException does not name {name} as no wire type: {Jx.Cut(x.Message)}";
return null;
}

// the baseline: the application's own converter for the type wins, in both directions
var (jOwn, xOwn) = Jx.Ser(ho, oOwn);
var (jv, xv) = Jx.Ser(hv, o);
if (xOwn is not null)
var refused = Baseline.Refusal;
var fw = r.Check("never-written", refused, c =>
{
if (xv is not null && xv.GetType() == xOwn.GetType()) { r.Note(NoteKeys.BothThrew, "the options refuse the shape itself: " + Jx.Ex(xv)); return null; }
return $"write: a converter registered on the options for {name} does not take precedence: {Jx.Ex(xOwn)}";
}
var ownWritten = Jx.Tokens(jOwn!).Any(t => t.Text == "own-or");

// writing
var (jo, xo) = Jx.Ser(ho, o);
var wrote = false;
if (xo is null)
{
if (ownWritten || jo != jOwn) return $"write: {name} was written: {Jx.Cut(jo)}";
r.Note(NoteKeys.JsonMemberLeftOutByOptions, "write: " + Jx.Cut(jo));
}
else { if (Refused(xo, "write") is { } f) return f; wrote = true; }

// reading: the value object's document with the row's input, where the Option or Result stands
var full = Jx.Full(o);
var (jf, xf) = Jx.Ser(hv, full);
// does the application's converter get called there? Its own document, read under the same options, says so
var (jOwnFull, _) = Jx.Ser(ho, Jx.Full(oOwn));
string input, text;
if (xf is null) { input = r["input.json"]; text = JsonParity.InputFor(input, e, full, jf!, position); }
else
{
// the value object's own shape cannot be written here (a value object over a class under Preserve, a class as a key): the
// document with the application's converter has a token where the Option or Result stands, and is read as it is
if (jOwnFull is null) { r.Note(NoteKeys.BothThrew, "read: no document has the member: " + Jx.Ex(xf)); return null; }
_ = r["input.json"]; r.Substituted("input.json", "valid", "the value object's document cannot be written: " + xf.GetType().Name);
input = "valid"; text = jOwnFull;
}
var (_, xOwnRead) = Jx.De(jOwnFull ?? "null", ho.Declared, oOwn);
if (xOwnRead is not null) return $"read: a converter registered on the options for {name} does not take precedence: {Jx.Ex(xOwnRead)}";
var memberRead = OwnReads() > 0;
var (vr, xr) = Jx.De(text, ho.Declared, o);
if (xr is null && input == "null" && position == "in-patch" && Jx.Member(ho.Declared, vr) is { } patch && patch.GetType().GetProperty("State")!.GetValue(patch)!.ToString() == "Null")
{
// a JSON null is the patch's own null (docs/merge-patches.md): it holds no Option or Result, and none was read
r.Note(NoteKeys.Compared, $"read null: the Patch<{name}> is null, nothing of {name} read");
if (xOwn is not null)
{
if (xv is not null && xv.GetType() == xOwn.GetType()) return c.Inconclusive("write: the options refuse the shape itself, with the application's converter alike: " + Jx.Ex(xv));
return $"write: a converter registered on the options for {name} does not take precedence: {Jx.Ex(xOwn)}";
}
var ownWritten = Jx.Tokens(jOwn!).Any(t => t.Text == "own-or");
var (jo, xo) = Jx.Ser(ho, o);
if (xo is null)
{
if (ownWritten || jo != jOwn) return $"write: {name} was written: {Jx.Cut(jo)}";
// the options leave the member out, the application's converter's document alike: no converter ran
c.Note(NoteKeys.JsonMemberLeftOutByOptions, "write: " + Jx.Cut(jo));
return c.Inconclusive("write: the options leave the member out, so no converter runs: " + Jx.Cut(jo));
}
if (Refused(xo, "write") is { } f) return f;
c.Note(NoteKeys.Compared, $"{name} by {op}, {position}: write refused");
return null;
}
if (xr is null)
});
var fr = await r.Check("never-read", refused, async c =>
{
if (memberRead) return $"read {input}: {name} was read without an exception\n text={Jx.Cut(text)}";
r.Note(NoteKeys.JsonMemberLeftOutByOptions, "read " + input); // not-promised a-missing-property-reaches-no-converter
if (!wrote) r.Note(NoteKeys.JsonBothDirectionsLeftOutByOptions);
}
else if (Refused(xr, "read " + input) is { } f)
{
// the serializer may refuse the document before the member: the value object's shape refuses it with the very same exception
var (_, xvr) = Jx.De(text, hv.Declared, o);
if (xvr is not null && xvr.GetType() == xr.GetType() && xvr.Message == Jx.Swap(Jx.Swap(xr.Message, ho.Declared, hv.Declared), orType, v)) r.Note(NoteKeys.JsonReadRefusedByStjAlone, input + ": " + Jx.Ex(xr));
else return f + $"\n text={Jx.Cut(text)}";
}
r.Note(NoteKeys.Compared, $"{name} by {op}, {position}");
return null;
await Task.CompletedTask;
if (xOwn is not null) return c.Inconclusive("read: the options refuse the shape itself, so no document has the member: " + Jx.Ex(xOwn));
// reading: the value object's document with the row's input, where the Option or Result stands
var full = Jx.Full(o);
var (jf, xf) = Jx.Ser(hv, full);
// does the application's converter get called there? Its own document, read under the same options, says so
var (jOwnFull, _) = Jx.Ser(ho, Jx.Full(oOwn));
string input, text;
if (xf is null) { input = c["input.json"]; text = JsonParity.InputFor(input, e, full, jf!, position); }
else
{
// the value object's own shape cannot be written here (a value object over a class under Preserve, a class as a key): the
// document with the application's converter has a token where the Option or Result stands, and is read as it is
if (jOwnFull is null) return c.Inconclusive("read: no document has the member: " + Jx.Ex(xf));
_ = c["input.json"]; c.Substituted("input.json", "valid", "the value object's document cannot be written: " + xf.GetType().Name);
input = "valid"; text = jOwnFull;
}
var (_, xOwnRead) = Jx.De(jOwnFull ?? "null", ho.Declared, oOwn);
if (xOwnRead is not null) return $"read: a converter registered on the options for {name} does not take precedence: {Jx.Ex(xOwnRead)}";
var memberRead = OwnReads() > 0;
var (vr, xr) = Jx.De(text, ho.Declared, o);
if (xr is null && input == "null" && position == "in-patch" && Jx.Member(ho.Declared, vr) is { } patch && patch.GetType().GetProperty("State")!.GetValue(patch)!.ToString() == "Null")
// a JSON null is the patch's own null (docs/merge-patches.md): it holds no Option or Result, and none was read
return c.Inapplicable(Cite.Docs("docs/merge-patches.md#tell-absent-null-and-a-value-apart"), $"read null: the Patch<{name}> is null, nothing of {name} read");
if (xr is null)
{
if (memberRead) return $"read {input}: {name} was read without an exception\n text={Jx.Cut(text)}";
c.Note(NoteKeys.JsonMemberLeftOutByOptions, "read " + input);
return c.Inapplicable(Cite.NotPromised("a-missing-property-reaches-no-converter"), "read " + input);
}
if (Refused(xr, "read " + input) is { } f)
{
// the serializer may refuse the document before the member: the value object's shape refuses it with the very same exception
var (_, xvr) = Jx.De(text, hv.Declared, o);
if (xvr is not null && xvr.GetType() == xr.GetType() && xvr.Message == Jx.Swap(Jx.Swap(xr.Message, ho.Declared, hv.Declared), orType, v))
{
// the framework's refusal, shown by the value object's own shape in the same row: a pass, as decision 99's document is
c.Note(NoteKeys.JsonReadRefusedByStjAlone, input + ": " + Jx.Ex(xr));
return null;
}
return f + $"\n text={Jx.Cut(text)}";
}
c.Note(NoteKeys.Compared, $"{name} by {op}, {position}: read {input} refused");
return null;
});
return fw ?? fr;
}

static async Task<(Type, object)> Produce<V>(string shape, string op, V v) where V : notnull
Expand Down
Loading
Loading