|
| 1 | +package main_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "io" |
| 6 | + "os" |
| 7 | + "os/exec" |
| 8 | + "path/filepath" |
| 9 | + "runtime" |
| 10 | + "testing" |
| 11 | + "time" |
| 12 | + |
| 13 | + "github.com/codefly-dev/core/agents/manager" |
| 14 | + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" |
| 15 | + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" |
| 16 | + builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0" |
| 17 | + "github.com/codefly-dev/core/resources" |
| 18 | + corerunnable "github.com/codefly-dev/core/runnable" |
| 19 | + "github.com/stretchr/testify/require" |
| 20 | + "google.golang.org/grpc/codes" |
| 21 | + "google.golang.org/grpc/status" |
| 22 | + "google.golang.org/protobuf/proto" |
| 23 | +) |
| 24 | + |
| 25 | +func TestBuilderPackagesThePreparedSnapshotOverGRPC(t *testing.T) { |
| 26 | + ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute) |
| 27 | + defer cancel() |
| 28 | + root := t.TempDir() |
| 29 | + t.Setenv(resources.CodeflyHomeEnv, filepath.Join(root, "home")) |
| 30 | + t.Setenv(manager.AgentSourceEnv, "local") |
| 31 | + agent, err := resources.ParseAgent(ctx, resources.RunnableAgent, "codefly.dev/python:0.0.2") |
| 32 | + require.NoError(t, err) |
| 33 | + binary, err := agent.Path(ctx) |
| 34 | + require.NoError(t, err) |
| 35 | + require.NoError(t, os.MkdirAll(filepath.Dir(binary), 0700)) |
| 36 | + compile := exec.CommandContext(ctx, "go", "build", "-o", binary, ".") |
| 37 | + output, err := compile.CombinedOutput() |
| 38 | + require.NoError(t, err, string(output)) |
| 39 | + connection, err := manager.Load(ctx, agent, manager.WithoutSandbox(), manager.WithoutPrincipal(), manager.WithLogWriter(io.Discard)) |
| 40 | + require.NoError(t, err) |
| 41 | + defer connection.Close() |
| 42 | + client := builderv0.NewBuilderClient(connection.GRPCConn()) |
| 43 | + info, err := agentv0.NewAgentClient(connection.GRPCConn()).GetAgentInformation(ctx, &agentv0.AgentInformationRequest{}) |
| 44 | + require.NoError(t, err) |
| 45 | + require.Equal(t, []*agentv0.Capability{{Type: agentv0.Capability_BUILDER}}, info.GetCapabilities()) |
| 46 | + |
| 47 | + workspaceDir := filepath.Join(root, "workspace") |
| 48 | + require.NoError(t, os.Mkdir(workspaceDir, 0700)) |
| 49 | + write(t, filepath.Join(workspaceDir, "workspace.codefly.yaml"), "kind: workspace\nname: proof\nlayout: flat\nservices: []\n") |
| 50 | + workspace, err := resources.LoadWorkspaceFromDir(ctx, workspaceDir) |
| 51 | + require.NoError(t, err) |
| 52 | + module, err := workspace.LoadModuleFromName(ctx, "proof") |
| 53 | + require.NoError(t, err) |
| 54 | + r, err := module.NewRunnable(ctx, "word-count", agent, "handler.py") |
| 55 | + require.NoError(t, err) |
| 56 | + r.Contract.Input.Fields = []*resources.RunnableField{{Name: "text", Type: resources.RunnableFieldType("string")}} |
| 57 | + r.Contract.Output.Fields = []*resources.RunnableField{{Name: "count", Type: resources.RunnableFieldType("integer")}} |
| 58 | + require.NoError(t, r.Save(ctx)) |
| 59 | + location, err := workspace.RunnableLocationOf(r) |
| 60 | + require.NoError(t, err) |
| 61 | + wire, err := location.Proto() |
| 62 | + require.NoError(t, err) |
| 63 | + loaded, err := client.Load(ctx, &builderv0.LoadRequest{Runnable: wire}) |
| 64 | + require.NoError(t, err) |
| 65 | + require.Equal(t, builderv0.LoadStatus_READY, loaded.GetState().GetState()) |
| 66 | + created, err := client.Create(ctx, &builderv0.CreateRequest{}) |
| 67 | + require.NoError(t, err) |
| 68 | + require.Equal(t, builderv0.CreateStatus_CREATED, created.GetState().GetState()) |
| 69 | + require.FileExists(t, filepath.Join(r.Dir(), "handler.py")) |
| 70 | + require.FileExists(t, filepath.Join(r.Dir(), "codefly_types.py")) |
| 71 | + write(t, filepath.Join(r.Dir(), "handler.py"), "def handle(context, input):\n return {\"count\": len(input[\"text\"].split())}\n") |
| 72 | + |
| 73 | + rejectedOutput := filepath.Join(r.Dir(), "build", "prepared") |
| 74 | + _, err = client.RunnableBuildInputs(ctx, &builderv0.RunnableBuildInputsRequest{OutputDirectory: rejectedOutput}) |
| 75 | + require.Equal(t, codes.InvalidArgument, status.Code(err)) |
| 76 | + require.NoDirExists(t, filepath.Join(r.Dir(), "build")) |
| 77 | + sourceLink := filepath.Join(root, "source-link") |
| 78 | + require.NoError(t, os.Symlink(r.Dir(), sourceLink)) |
| 79 | + _, err = client.RunnableBuildInputs(ctx, &builderv0.RunnableBuildInputsRequest{OutputDirectory: filepath.Join(sourceLink, "build")}) |
| 80 | + require.Equal(t, codes.InvalidArgument, status.Code(err)) |
| 81 | + require.NoDirExists(t, filepath.Join(r.Dir(), "build")) |
| 82 | + |
| 83 | + prepared, err := client.RunnableBuildInputs(ctx, &builderv0.RunnableBuildInputsRequest{OutputDirectory: filepath.Join(root, "prepared")}) |
| 84 | + require.NoError(t, err) |
| 85 | + require.Equal(t, builderv0.RunnableBuildInputsStatus_SUCCESS, prepared.GetState().GetState()) |
| 86 | + |
| 87 | + // Preparing again into the caller's populated directory is refused by name |
| 88 | + // rather than from inside preparation, and the snapshot already prepared |
| 89 | + // survives the refusal: the Package below still emits it. |
| 90 | + _, err = client.RunnableBuildInputs(ctx, &builderv0.RunnableBuildInputsRequest{OutputDirectory: filepath.Join(root, "prepared")}) |
| 91 | + require.Equal(t, codes.AlreadyExists, status.Code(err)) |
| 92 | + |
| 93 | + write(t, filepath.Join(r.Dir(), "handler.py"), "def handle(context, input):\n return {\"count\": 999}\n") |
| 94 | + _, err = client.Package(ctx, &builderv0.PackageRequest{Targets: []*builderv0.PackageTarget{{Os: "other", Architecture: runtime.GOARCH}}, OutputDirectory: filepath.Join(root, "unsupported")}) |
| 95 | + require.Equal(t, codes.Unimplemented, status.Code(err)) |
| 96 | + require.NoDirExists(t, filepath.Join(root, "unsupported")) |
| 97 | + response, err := client.Package(ctx, &builderv0.PackageRequest{OutputDirectory: filepath.Join(root, "artifacts")}) |
| 98 | + require.NoError(t, err) |
| 99 | + require.Equal(t, builderv0.PackageStatus_SUCCESS, response.GetState().GetState()) |
| 100 | + r, err = resources.LoadRunnableFromDir(ctx, r.Dir()) |
| 101 | + require.NoError(t, err) |
| 102 | + emitted := response.GetArtifacts()[0] |
| 103 | + pkg := preparedPackage(t, r, wire.GetIdentity(), prepared.GetBuild(), []*basev0.RunnableArtifact{{ |
| 104 | + Kind: basev0.RunnableArtifact_NATIVE, Platform: runtime.GOOS + "/" + runtime.GOARCH, |
| 105 | + Reference: filepath.Base(emitted.GetPath()), Digest: "sha256:" + emitted.GetSha256(), Command: emitted.GetCommand(), |
| 106 | + }}) |
| 107 | + require.NoError(t, corerunnable.VerifyPackage(pkg)) |
| 108 | + require.True(t, proto.Equal(prepared.GetBuild(), pkg.GetBuild())) |
| 109 | + require.True(t, proto.Equal(wire.GetIdentity(), pkg.GetIdentity())) |
| 110 | + require.Len(t, pkg.GetArtifacts(), 1) |
| 111 | + require.NotEmpty(t, pkg.GetArtifacts()[0].GetCommand()) |
| 112 | + installed := filepath.Join(root, "installed") |
| 113 | + unpack(t, response.GetArtifacts()[0].GetPath(), installed) |
| 114 | + result := invoke(t, installed, pkg.GetArtifacts()[0].GetCommand(), pkg, map[string]any{"text": "one two three"}) |
| 115 | + require.Equal(t, 0, result.exit, result.stderr) |
| 116 | + require.Equal(t, 3, result.count(t), |
| 117 | + "the package must execute the prepared handler, so the refused retry discarded no snapshot") |
| 118 | + |
| 119 | + write(t, filepath.Join(root, "prepared", "runnable", "handler.py"), "def handle(context, input): return {\"count\": 888}\n") |
| 120 | + _, err = client.Package(ctx, &builderv0.PackageRequest{OutputDirectory: filepath.Join(root, "tampered")}) |
| 121 | + require.Equal(t, codes.FailedPrecondition, status.Code(err)) |
| 122 | + require.NoDirExists(t, filepath.Join(root, "tampered"), "reject changed snapshot before emitting any archive") |
| 123 | + |
| 124 | + wrong := proto.Clone(wire).(*basev0.RunnableLocation) |
| 125 | + wrong.Identity.Version = "9.0.0" |
| 126 | + _, err = client.Load(ctx, &builderv0.LoadRequest{Runnable: wrong}) |
| 127 | + require.Equal(t, codes.InvalidArgument, status.Code(err)) |
| 128 | + _, err = client.Package(ctx, &builderv0.PackageRequest{OutputDirectory: filepath.Join(root, "stale")}) |
| 129 | + require.Equal(t, codes.FailedPrecondition, status.Code(err), "a failed load must clear the previous snapshot") |
| 130 | +} |
0 commit comments