test(guest): resolver round trip with the host handler
- commit
- 0ee31e3b6e653e7fdba409eb52ddbf4097c92bd7
- author
- brice · · 2026-10-13 16:40 UTC
- parent
- 92205ff
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +82 -0
A internal/guest/resolver_test.go +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package guest | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "net" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "bnema.dev/ermite-runner/internal/execstream" | |
| 11 | + | "bnema.dev/ermite-runner/internal/execstream/execpb" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // The resolver of the executor and the Resolve handler of the host meet on the | |
| 15 | + | // stream: requests are answered by id, in any order, and an error of the host | |
| 16 | + | // reaches the caller. | |
| 17 | + | func TestResolverRoundTrip(t *testing.T) { | |
| 18 | + | ln, err := net.Listen("tcp", "127.0.0.1:0") | |
| 19 | + | if err != nil { | |
| 20 | + | t.Skip(err) | |
| 21 | + | } | |
| 22 | + | defer func() { _ = ln.Close() }() | |
| 23 | + | guestSide, err := net.Dial("tcp", ln.Addr().String()) | |
| 24 | + | if err != nil { | |
| 25 | + | t.Fatal(err) | |
| 26 | + | } | |
| 27 | + | hostSide, err := ln.Accept() | |
| 28 | + | if err != nil { | |
| 29 | + | t.Fatal(err) | |
| 30 | + | } | |
| 31 | + | defer func() { _ = hostSide.Close() }() | |
| 32 | + | defer func() { _ = guestSide.Close() }() | |
| 33 | + | ||
| 34 | + | gconn := execstream.NewConn(guestSide) | |
| 35 | + | res := newResolver(gconn) | |
| 36 | + | go func() { // the guest's frame reader | |
| 37 | + | for { | |
| 38 | + | f, err := gconn.Recv() | |
| 39 | + | if err != nil { | |
| 40 | + | return | |
| 41 | + | } | |
| 42 | + | if r := f.GetResolveResponse(); r != nil { | |
| 43 | + | res.deliver(r) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | }() | |
| 47 | + | // The executor says Hello first, as it does at startup. | |
| 48 | + | if err := gconn.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}}); err != nil { | |
| 49 | + | t.Fatal(err) | |
| 50 | + | } | |
| 51 | + | done := make(chan struct{}) | |
| 52 | + | go func() { | |
| 53 | + | defer close(done) | |
| 54 | + | _, _, _ = execstream.DriveJob(hostSide, &execpb.Run{}, nil, execstream.JobHandler{ | |
| 55 | + | Resolve: func(r *execpb.ResolveRequest) *execpb.ResolveResponse { | |
| 56 | + | if len(r.Uses) == 1 && r.Uses[0] == "bad@v1" { | |
| 57 | + | return &execpb.ResolveResponse{Error: "no"} | |
| 58 | + | } | |
| 59 | + | out := &execpb.ResolveResponse{} | |
| 60 | + | for _, u := range r.Uses { | |
| 61 | + | out.Actions = append(out.Actions, &execpb.ResolvedAction{Ref: u, Token: "t-" + u, ResolvedSha: "sha"}) | |
| 62 | + | } | |
| 63 | + | return out | |
| 64 | + | }, | |
| 65 | + | }) | |
| 66 | + | }() | |
| 67 | + | // DriveJob sends Run after Hello; the guest ignores it. | |
| 68 | + | got, err := res.resolve(context.Background(), []string{"a/b@v1", "c/d@v2"}) | |
| 69 | + | if err != nil || len(got) != 2 || got[1].Ref != "c/d@v2" || got[1].Token != "t-c/d@v2" || got[0].ResolvedSHA != "sha" { | |
| 70 | + | t.Fatalf("%+v %v", got, err) | |
| 71 | + | } | |
| 72 | + | if _, err := res.resolve(context.Background(), []string{"bad@v1"}); err == nil || err.Error() != "no" { | |
| 73 | + | t.Fatalf("error not passed: %v", err) | |
| 74 | + | } | |
| 75 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 76 | + | cancel() | |
| 77 | + | _ = guestSide.Close() | |
| 78 | + | if _, err := res.resolve(ctx, []string{"x/y@z"}); err == nil { | |
| 79 | + | t.Error("a closed stream must fail") | |
| 80 | + | } | |
| 81 | + | <-done | |
| 82 | + | } | |
| @@ -0,0 +1,82 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package guest | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "net" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "bnema.dev/ermite-runner/internal/execstream" | |||
| 11 | + | "bnema.dev/ermite-runner/internal/execstream/execpb" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // The resolver of the executor and the Resolve handler of the host meet on the | |||
| 15 | + | // stream: requests are answered by id, in any order, and an error of the host | |||
| 16 | + | // reaches the caller. | |||
| 17 | + | func TestResolverRoundTrip(t *testing.T) { | |||
| 18 | + | ln, err := net.Listen("tcp", "127.0.0.1:0") | |||
| 19 | + | if err != nil { | |||
| 20 | + | t.Skip(err) | |||
| 21 | + | } | |||
| 22 | + | defer func() { _ = ln.Close() }() | |||
| 23 | + | guestSide, err := net.Dial("tcp", ln.Addr().String()) | |||
| 24 | + | if err != nil { | |||
| 25 | + | t.Fatal(err) | |||
| 26 | + | } | |||
| 27 | + | hostSide, err := ln.Accept() | |||
| 28 | + | if err != nil { | |||
| 29 | + | t.Fatal(err) | |||
| 30 | + | } | |||
| 31 | + | defer func() { _ = hostSide.Close() }() | |||
| 32 | + | defer func() { _ = guestSide.Close() }() | |||
| 33 | + | ||||
| 34 | + | gconn := execstream.NewConn(guestSide) | |||
| 35 | + | res := newResolver(gconn) | |||
| 36 | + | go func() { // the guest's frame reader | |||
| 37 | + | for { | |||
| 38 | + | f, err := gconn.Recv() | |||
| 39 | + | if err != nil { | |||
| 40 | + | return | |||
| 41 | + | } | |||
| 42 | + | if r := f.GetResolveResponse(); r != nil { | |||
| 43 | + | res.deliver(r) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | }() | |||
| 47 | + | // The executor says Hello first, as it does at startup. | |||
| 48 | + | if err := gconn.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}}); err != nil { | |||
| 49 | + | t.Fatal(err) | |||
| 50 | + | } | |||
| 51 | + | done := make(chan struct{}) | |||
| 52 | + | go func() { | |||
| 53 | + | defer close(done) | |||
| 54 | + | _, _, _ = execstream.DriveJob(hostSide, &execpb.Run{}, nil, execstream.JobHandler{ | |||
| 55 | + | Resolve: func(r *execpb.ResolveRequest) *execpb.ResolveResponse { | |||
| 56 | + | if len(r.Uses) == 1 && r.Uses[0] == "bad@v1" { | |||
| 57 | + | return &execpb.ResolveResponse{Error: "no"} | |||
| 58 | + | } | |||
| 59 | + | out := &execpb.ResolveResponse{} | |||
| 60 | + | for _, u := range r.Uses { | |||
| 61 | + | out.Actions = append(out.Actions, &execpb.ResolvedAction{Ref: u, Token: "t-" + u, ResolvedSha: "sha"}) | |||
| 62 | + | } | |||
| 63 | + | return out | |||
| 64 | + | }, | |||
| 65 | + | }) | |||
| 66 | + | }() | |||
| 67 | + | // DriveJob sends Run after Hello; the guest ignores it. | |||
| 68 | + | got, err := res.resolve(context.Background(), []string{"a/b@v1", "c/d@v2"}) | |||
| 69 | + | if err != nil || len(got) != 2 || got[1].Ref != "c/d@v2" || got[1].Token != "t-c/d@v2" || got[0].ResolvedSHA != "sha" { | |||
| 70 | + | t.Fatalf("%+v %v", got, err) | |||
| 71 | + | } | |||
| 72 | + | if _, err := res.resolve(context.Background(), []string{"bad@v1"}); err == nil || err.Error() != "no" { | |||
| 73 | + | t.Fatalf("error not passed: %v", err) | |||
| 74 | + | } | |||
| 75 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 76 | + | cancel() | |||
| 77 | + | _ = guestSide.Close() | |||
| 78 | + | if _, err := res.resolve(ctx, []string{"x/y@z"}); err == nil { | |||
| 79 | + | t.Error("a closed stream must fail") | |||
| 80 | + | } | |||
| 81 | + | <-done | |||
| 82 | + | } | |||