|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +// Copyright 2026 The Hyperlight Authors. |
| 3 | + |
| 4 | +// Checks the naming and type cases in edge.wit. |
| 5 | + |
| 6 | +using System.Runtime.CompilerServices; |
| 7 | +using Hyperlight.Tests.Edge; |
| 8 | +using Hyperlight.Tests.Edge.Test.Edge.Types; |
| 9 | +using Dep1 = Hyperlight.Tests.Edge.Test.Dep.V1_0_0; |
| 10 | +using Dep2 = Hyperlight.Tests.Edge.Test.Dep.V2_0_0; |
| 11 | + |
| 12 | +static class EdgeTests |
| 13 | +{ |
| 14 | + public static void Run() |
| 15 | + { |
| 16 | + var guest = new Guest(); |
| 17 | + var component = new EdgeComponent(guest); |
| 18 | + |
| 19 | + void Sent(string function, byte[] args, string what) => |
| 20 | + Check.True(guest.Function == function && guest.Args.Length == 1 && guest.Args[0].SequenceEqual(args), what); |
| 21 | + |
| 22 | + // Fields named like their record or like object members get a suffix. |
| 23 | + var r = new R { R_ = 1, ToString_ = 2 }; |
| 24 | + Check.True(component.Ops.Keyword(@class: r) == r, "keyword round trip"); |
| 25 | + Sent("test:edge/ops#keyword", [1, 2], "keyword encoding"); |
| 26 | + Check.True(r.ToString().Contains("ToString_ = 2"), "record ToString"); |
| 27 | + |
| 28 | + foreach (var type in new[] { typeof(R), typeof(P), typeof(Lists) }) |
| 29 | + { |
| 30 | + Check.True(type.GetProperties().All(p => p.IsDefined(typeof(RequiredMemberAttribute), false)), $"{type.Name} required"); |
| 31 | + } |
| 32 | + |
| 33 | + // Aliases resolve to the type they name. |
| 34 | + IReadOnlyList<P> points = [new P { X = 1 }, new P { X = 2 }]; |
| 35 | + Check.True(component.Ops.Alias(points).SequenceEqual(points), "alias round trip"); |
| 36 | + Sent("test:edge/ops#alias", [2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0], "alias encoding"); |
| 37 | + |
| 38 | + // Lists inside records and variant cases compare by element. |
| 39 | + Lists MakeLists(byte last) => new() { Id = 1, Names = ["a", "b"], Pair = (2, [3, last]) }; |
| 40 | + var lists = MakeLists(4); |
| 41 | + Check.True(lists == MakeLists(4) && lists.GetHashCode() == MakeLists(4).GetHashCode(), "lists equal"); |
| 42 | + Check.True(lists != MakeLists(5), "tuple bytes differ"); |
| 43 | + Check.True(lists != (lists with { Names = ["a"] }), "names differ"); |
| 44 | + Check.True(component.Ops.Structural(lists) == lists, "lists round trip"); |
| 45 | + V items = new V.Items([1, 2]); |
| 46 | + Check.True(items == new V.Items([1, 2]) && items.GetHashCode() == new V.Items([1, 2]).GetHashCode(), "items equal"); |
| 47 | + Check.True(items != new V.Items([1]), "items differ"); |
| 48 | + |
| 49 | + // Cases named like Match, Value or a keyword get escaped names and parameters. |
| 50 | + string Describe(V v) => v.Match( |
| 51 | + none: () => "none", |
| 52 | + match: m => $"match {m}", |
| 53 | + @class: c => $"class {c}", |
| 54 | + items: i => $"items {i.Count}", |
| 55 | + value: x => $"value {x}"); |
| 56 | + V[] cases = [new V.None(), new V.Match_(7), new V.Class("c"), new V.Value_(9), items]; |
| 57 | + string[] expected = ["none", "match 7", "class c", "value 9", "items 2"]; |
| 58 | + for (var i = 0; i < cases.Length; i++) |
| 59 | + { |
| 60 | + Check.True(Describe(cases[i]) == expected[i], $"match {expected[i]}"); |
| 61 | + var switched = ""; |
| 62 | + cases[i].Switch( |
| 63 | + none: () => switched = "none", |
| 64 | + match: m => switched = $"match {m}", |
| 65 | + @class: c => switched = $"class {c}", |
| 66 | + items: i => switched = $"items {i.Count}", |
| 67 | + value: x => switched = $"value {x}"); |
| 68 | + Check.True(switched == expected[i], $"switch {expected[i]}"); |
| 69 | + Check.True(component.Ops.Cases(cases[i]) == cases[i], $"{expected[i]} round trip"); |
| 70 | + } |
| 71 | + Sent("test:edge/ops#cases", [3, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0], "items encoding"); |
| 72 | + |
| 73 | + Check.True(component.Ops.Single(new ValueTuple<ushort>(0x1234)).Item1 == 0x1234, "single tuple round trip"); |
| 74 | + Sent("test:edge/ops#single", [0x34, 0x12], "single tuple encoding"); |
| 75 | + |
| 76 | + component.Other.G(); |
| 77 | + Check.True(guest.Function == "other#g" && guest.Args.Length == 0, "inline interface call"); |
| 78 | + |
| 79 | + // Exports named like members of the component class get escaped names. |
| 80 | + component.RegisterImports_.R(); |
| 81 | + Check.True(guest.Function == "register-imports#r", "export named register-imports"); |
| 82 | + component.EdgeComponent_.C(); |
| 83 | + Check.True(guest.Function == "edge-component#c", "export named like the component class"); |
| 84 | + |
| 85 | + // Each version of an imported package has its own namespace and wire name. |
| 86 | + var registry = new Registry(); |
| 87 | + EdgeComponent.RegisterImports(registry, new DepV1(), new DepV2()); |
| 88 | + Check.True(registry.Call("test:dep/t@1.0.0#f", [5, 0, 0, 0]).SequenceEqual(new byte[] { 105, 0, 0, 0 }), "dep 1.0.0"); |
| 89 | + Check.True(registry.Call("test:dep/t@2.0.0#f", [5, 0, 0, 0]).SequenceEqual(new byte[] { 205, 0, 0, 0 }), "dep 2.0.0"); |
| 90 | + } |
| 91 | + |
| 92 | + sealed class DepV1 : Dep1.IT |
| 93 | + { |
| 94 | + public uint F(Dep1.T.R x) => x.X + 100; |
| 95 | + } |
| 96 | + |
| 97 | + sealed class DepV2 : Dep2.IT |
| 98 | + { |
| 99 | + public uint F(Dep2.T.R x) => x.X + 200; |
| 100 | + } |
| 101 | +} |
0 commit comments