Repository navigation
Expand file tree
/
Copy pathqualification_test.go
More file actions
657 lines (596 loc) · 23.2 KB
/
Copy pathqualification_test.go
File metadata and controls
657 lines (596 loc) · 23.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
package main_test
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
"github.com/codefly-dev/core/resources"
corerunnable "github.com/codefly-dev/core/runnable"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/codefly-dev/runnable-python/pkg/generate"
"github.com/codefly-dev/runnable-python/pkg/pack"
"github.com/codefly-dev/runnable-python/pkg/prepare"
"github.com/codefly-dev/runnable-python/pkg/recipe"
)
// The per-call facts a caller presents. The Work Context is carried verbatim
// and never parsed, here or in the harness.
const (
invocationID = "inv-1"
workContext = "eyJ0eXAiOiJjb2RlZmx5LndvcmstY29udGV4dC92MSJ9.signed"
)
const declaration = `kind: runnable
name: word-count
description: Count the words of a text.
version: 0.1.0
agent:
kind: codefly:runnable
name: python
version: 0.0.2
publisher: codefly.dev
contract:
protocol: codefly.runnable.served/v1
input:
fields:
- name: text
type: string
- name: options
type: object
optional: true
fields:
- name: stop_words
type: array
nullable: true
items:
type: string
output:
fields:
- name: count
type: integer
entrypoint:
handler: handler.py
inputs: [pyproject.toml, uv.lock]
execution:
facilities: [generated-service, kubernetes]
timeout: 2m
cancellation: none
recovery: recompute
payload:
max-input-bytes: 65536
workspace-configuration-dependencies: [openai]
`
const implementation = `"""Count the words of a text."""
from codefly_runnable import Context
from codefly_types import Input, Output
def handle(context: Context, input: Input) -> Output:
stop_words = set()
options = input.get("options")
if options is not None and options["stop_words"] is not None:
stop_words = set(options["stop_words"])
words = [word for word in input["text"].split() if word not in stop_words]
return {"count": len(words)}
`
// TestANativePackageCompletesAnInvocation walks one release the way the CLI
// will: declare, generate, prepare, package, install elsewhere, invoke. The
// package is unpacked under a different path than it was built in, so a
// package that only ran from its build directory fails here.
func TestANativePackageCompletesAnInvocation(t *testing.T) {
ctx := context.Background()
workspace := t.TempDir()
builderPython := filepath.Join(workspace, "builder-python")
t.Setenv("UV_PYTHON_INSTALL_DIR", builderPython)
t.Setenv("UV_PYTHON_BIN_DIR", filepath.Join(workspace, "builder-bin"))
source := filepath.Join(workspace, "runnables", "word-count")
runnable := declare(t, ctx, source)
if err := generate.Scaffold(runnable, source); err != nil {
t.Fatalf("scaffold: %v", err)
}
if err := generate.Generate(runnable, source); err != nil {
t.Fatalf("generate: %v", err)
}
// Verify a locked third-party dependency is carried into the installed package.
project := filepath.Join(source, generate.ProjectFile)
write(t, project, strings.Replace(read(t, project), "dependencies = []", "dependencies = [\"idna==3.10\"]", 1))
write(t, filepath.Join(source, "handler.py"), "import idna\nassert idna.__version__ == \"3.10\"\n"+implementation)
prepared, err := prepare.Prepare(ctx, runnable, source, filepath.Join(workspace, "build", "native"))
if err != nil {
t.Fatalf("prepare: %v", err)
}
if !strings.HasPrefix(prepared.Toolchain, "python-"+prepare.DefaultPythonVersion+".") {
t.Fatalf("toolchain %q is not the declared interpreter", prepared.Toolchain)
}
// A source edit after preparation must not rewrite the archived build's evidence.
write(t, filepath.Join(source, "handler.py"), "def handle(context, input):\n return {\"count\": 999}\n")
output := filepath.Join(workspace, "out")
archive := filepath.Join(output, "word-count-0.1.0.tar.gz")
evidence, err := pack.Native(runnable, runnable.Agent, prepared, archive)
if err != nil {
t.Fatalf("pack: %v", err)
}
if _, err := evidence.Write(output); err != nil {
t.Fatalf("write evidence: %v", err)
}
artifact := artifacts(t, evidence)[0]
pkg := packageFromEvidence(t, runnable, evidence)
if artifact["kind"] != "ARCHIVE" {
t.Fatalf("artifact kind is %v", artifact["kind"])
}
installed := filepath.Join(workspace, "installed", "word-count")
unpack(t, archive, installed)
// Remove access to both the prepared tree and the builder's interpreter.
// The installed command must use only its own archive contents.
if err := os.RemoveAll(prepared.Root); err != nil {
t.Fatal(err)
}
if err := os.Rename(builderPython, builderPython+"-unavailable"); err != nil {
t.Fatal(err)
}
archivedHandler, err := os.ReadFile(filepath.Join(installed, prepare.SourceDirectory, "handler.py"))
if err != nil {
t.Fatal(err)
}
var build struct {
Handler struct {
Digest string `json:"digest"`
} `json:"handler"`
}
if err := json.Unmarshal(evidence.Build, &build); err != nil {
t.Fatal(err)
}
if want := fmt.Sprintf("sha256:%x", sha256.Sum256(archivedHandler)); build.Handler.Digest != want {
t.Fatalf("build evidence describes a different handler: got %s, archive %s", build.Handler.Digest, want)
}
harness := serve(t, installed, command(t, artifact), pkg)
empty := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "one two three"})
if got := count(t, empty); got != 3 {
t.Fatalf("count = %d, want 3\n%s", got, harness.stderr.String())
}
if !corerunnable.ServedOutcomeIsCertain(empty.GetOutcome()) {
t.Fatal("a validated answer must prove the effect committed")
}
// The package serves: it does not exit after one call. Every call below is
// answered by the same process, which is the whole difference from the
// placement this replaced.
filtered := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{
"text": "one two three",
"options": map[string]any{"stop_words": []string{"two"}},
})
if got := count(t, filtered); got != 2 {
t.Fatalf("filtered count = %d, want 2", got)
}
nulled := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{
"text": "one two three four",
"options": map[string]any{"stop_words": nil},
})
if got := count(t, nulled); got != 4 {
t.Fatalf("nullable count = %d, want 4", got)
}
}
// TestAnInstalledPackageRefusesAnInvalidPayload proves the harness enforces
// the contract in the package the launcher actually runs, not only in the
// repository's own tests.
func TestAnInstalledPackageRefusesAnInvalidPayload(t *testing.T) {
ctx := context.Background()
workspace := t.TempDir()
source := filepath.Join(workspace, "runnables", "word-count")
runnable := declare(t, ctx, source)
if err := generate.Scaffold(runnable, source); err != nil {
t.Fatalf("scaffold: %v", err)
}
if err := generate.Generate(runnable, source); err != nil {
t.Fatalf("generate: %v", err)
}
write(t, filepath.Join(source, "handler.py"), implementation)
prepared, err := prepare.Prepare(ctx, runnable, source, filepath.Join(workspace, "build", "native"))
if err != nil {
t.Fatalf("prepare: %v", err)
}
evidence, err := pack.Native(runnable, runnable.Agent, prepared, filepath.Join(workspace, "artifact.tar.gz"))
require.NoError(t, err)
harness := serve(t, prepared.Root, prepared.Command, packageFromEvidence(t, runnable, evidence))
completion := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": 3})
if completion.GetOutcome() == basev0.RunnableServedOutcome_SERVED_SUCCEEDED {
t.Fatalf("a payload the contract refuses was accepted: %s", completion.GetResult().GetOutput())
}
// A refused payload never reached the handler, so nothing committed — but
// "proven not committed" is the operation asserting it in its own
// vocabulary, and a refused request is not the operation speaking.
if completion.GetFailureCode() != "" {
t.Fatalf("a refused payload is not the operation's own failure: %q", completion.GetFailureCode())
}
if corerunnable.ServedOutcomeIsCertain(completion.GetOutcome()) {
t.Fatalf("a refused payload leaves the outcome unproven, got %s", completion.GetOutcome())
}
// The harness answers again afterwards: a refusal ends a call, not the
// process that serves them.
second := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "one two"})
if second.GetOutcome() != basev0.RunnableServedOutcome_SERVED_SUCCEEDED {
t.Fatalf("the call after a refused one: %s %q\n--- stderr ---\n%s", second.GetOutcome(), second.GetMessage(), harness.stderr.String())
}
}
// TestAnInstalledPackageRefusesACallWithNoIdentity proves the identity slot in
// the package a placement actually runs, not only in the harness suite. A call
// with no Work Context is refused rather than run under whatever identity the
// process happens to have, which is the state the required slot removes — and
// the handler's own declared failure is the one answer that proves no effect
// committed.
func TestAnInstalledPackageRefusesACallWithNoIdentity(t *testing.T) {
ctx := context.Background()
workspace := t.TempDir()
source := filepath.Join(workspace, "runnables", "word-count")
runnable := declare(t, ctx, source)
require.NoError(t, generate.Scaffold(runnable, source))
require.NoError(t, generate.Generate(runnable, source))
write(t, filepath.Join(source, "handler.py"), `from codefly_runnable import Context, HandlerFailure
def handle(context: Context, input) -> dict:
if input["text"] == "declined":
raise HandlerFailure("card_declined", "the issuer declined")
if input["text"] == "untyped":
raise RuntimeError("something went wrong")
return {"count": len(context.invocation.work_context.split())}
`)
prepared, err := prepare.Prepare(ctx, runnable, source, filepath.Join(workspace, "build", "native"))
require.NoError(t, err)
evidence, err := pack.Native(runnable, runnable.Agent, prepared, filepath.Join(workspace, "artifact.tar.gz"))
require.NoError(t, err)
harness := serve(t, prepared.Root, prepared.Command, packageFromEvidence(t, runnable, evidence))
// The handler sees the caller's capability verbatim: forwarding it is the
// only thing a handler may do with one, and the harness neither parses nor
// reconstructs it.
carried := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "a"})
require.Equal(t, len(strings.Fields(workContext)), count(t, carried))
none := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "a"},
map[string]string{corerunnable.WorkContextHeader: ""})
require.NotEqual(t, basev0.RunnableServedOutcome_SERVED_SUCCEEDED, none.GetOutcome())
require.False(t, corerunnable.ServedOutcomeIsCertain(none.GetOutcome()),
"a refused call is not the operation asserting anything")
// The harness writes its diagnostics from its own thread, after the answer
// has gone, so the refusal reaches stderr shortly after it reaches the caller.
require.Eventually(t, func() bool { return strings.Contains(harness.stderr.String(), "Work Context") },
5*time.Second, 20*time.Millisecond, "the refusal was not explained on stderr:\n%s", harness.stderr.String())
declined := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "declined"})
require.Equal(t, basev0.RunnableServedOutcome_SERVED_OWNER_FAILED, declined.GetOutcome())
require.Equal(t, "card_declined", declined.GetFailureCode())
require.True(t, corerunnable.ServedOutcomeIsCertain(declined.GetOutcome()),
"the operation's own typed code proves no effect committed")
untyped := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "untyped"})
require.Empty(t, untyped.GetFailureCode(), "an untyped error asserts nothing on the handler's behalf")
require.False(t, corerunnable.ServedOutcomeIsCertain(untyped.GetOutcome()))
}
// TestTheImageRecipeDescribesALinuxBuild checks the recipe the CLI's image
// executor consumes; building the image is the CLI's half.
func TestTheImageRecipeDescribesALinuxBuild(t *testing.T) {
ctx := context.Background()
workspace := t.TempDir()
source := filepath.Join(workspace, "runnables", "word-count")
runnable := declare(t, ctx, source)
if err := generate.Scaffold(runnable, source); err != nil {
t.Fatalf("scaffold: %v", err)
}
if err := generate.Generate(runnable, source); err != nil {
t.Fatalf("generate: %v", err)
}
prepared, err := prepare.Prepare(ctx, runnable, source, filepath.Join(workspace, "build", "native"))
if err != nil {
t.Fatalf("prepare: %v", err)
}
image := filepath.Join(workspace, "build", "image")
written, err := recipe.Write(runnable, prepared, image)
if err != nil {
t.Fatalf("recipe: %v", err)
}
if written.Schema != recipe.Schema {
t.Fatalf("schema = %q", written.Schema)
}
for _, expected := range []string{
filepath.Join(image, recipe.DockerfileName),
filepath.Join(image, recipe.RecipeFile),
filepath.Join(image, prepare.RequirementsFile),
filepath.Join(image, prepare.EntryFile),
filepath.Join(image, prepare.SourceDirectory, "handler.py"),
filepath.Join(image, prepare.SourceDirectory, generate.GeneratedDirectory, generate.ContractFile),
} {
if _, err := os.Stat(expected); err != nil {
t.Errorf("image context is missing %s", filepath.Base(expected))
}
}
if _, err := os.Stat(filepath.Join(image, prepare.Environment)); !os.IsNotExist(err) {
t.Error("the image context carries the build host's interpreter")
}
dockerfile := read(t, filepath.Join(image, recipe.DockerfileName))
if !strings.Contains(dockerfile, "--require-hashes") {
t.Error("the image resolves dependencies instead of installing the locked set")
}
}
func declare(t *testing.T, ctx context.Context, dir string) *resources.Runnable {
t.Helper()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
write(t, filepath.Join(dir, resources.RunnableConfigurationName), declaration)
runnable, err := resources.LoadRunnableFromDir(ctx, dir)
if err != nil {
t.Fatalf("load declaration: %v", err)
}
return runnable
}
// served is a generated package running the way a placement runs it: the
// allocated address in the environment, and nothing else.
type served struct {
address string
process *exec.Cmd
stderr *capture
pkg *basev0.RunnablePackage
}
// capture is a process's stderr as a test reads it while the process is still
// writing: exec copies into it from a goroutine of its own.
type capture struct {
mu sync.Mutex
written strings.Builder
}
func (c *capture) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
return c.written.Write(p)
}
func (c *capture) String() string {
c.mu.Lock()
defer c.mu.Unlock()
return c.written.String()
}
// serve starts the installed package and waits until it answers. The port is
// chosen by the operating system and handed over the way a placement hands one
// over: nothing here picks a number.
func serve(t *testing.T, root string, argv []string, pkg *basev0.RunnablePackage) served {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
address := listener.Addr().String()
require.NoError(t, listener.Close())
stderr := &capture{}
process := exec.Command(filepath.Join(root, argv[0]), argv[1:]...)
process.Dir = root
process.Env = append(os.Environ(), corerunnable.ListenAddressEnv+"="+address)
process.Stderr = stderr
require.NoError(t, process.Start())
t.Cleanup(func() {
_ = process.Process.Signal(os.Interrupt)
done := make(chan struct{})
go func() { _, _ = process.Process.Wait(); close(done) }()
select {
case <-done:
case <-time.After(45 * time.Second):
_ = process.Process.Kill()
}
})
for range 600 {
if process.ProcessState != nil {
break
}
connection, err := net.DialTimeout("tcp", address, 50*time.Millisecond)
if err == nil {
require.NoError(t, connection.Close())
return served{address: address, process: process, stderr: stderr, pkg: pkg}
}
time.Sleep(25 * time.Millisecond)
}
t.Fatalf("the package never answered on %s\n--- stderr ---\n%s", address, stderr.String())
return served{}
}
// call sends one call the way the durable-work runtime's invoker sends one —
// core's header spellings, core's procedure, the bounded input document as the
// whole body — and classifies the answer with core's own ClassifyServed. Every
// constant is read from core, so a harness that drifted from the contract fails
// here rather than in a live composition.
func (s served) call(t *testing.T, procedure string, payload map[string]any, headers ...map[string]string) *basev0.RunnableServedCompletion {
t.Helper()
body, err := json.Marshal(payload)
require.NoError(t, err)
inv := s.invocation(t, body)
request, err := http.NewRequest(http.MethodPost, "http://"+s.address+procedure, bytes.NewReader(body))
require.NoError(t, err)
request.Header.Set("Content-Type", "application/json")
request.Header.Set(corerunnable.WorkContextHeader, workContext)
request.Header.Set(corerunnable.EffectHeader, inv.GetEffectId())
request.Header.Set(corerunnable.DeadlineHeader, inv.GetDeadline().AsTime().Format(corerunnable.DeadlineFormat))
for _, overrides := range headers {
for name, value := range overrides {
if value == "" {
request.Header.Del(name)
continue
}
request.Header.Set(name, value)
}
}
observed := corerunnable.Call{CalledAt: time.Now().UTC(), AuthorityResolved: true}
response, err := (&http.Client{Timeout: 2 * time.Minute}).Do(request)
observed.AnsweredAt = time.Now().UTC()
if err != nil {
observed.Trouble = err
} else {
defer func() { require.NoError(t, response.Body.Close()) }()
observed.Answered = true
observed.FailureCode = response.Header.Get(corerunnable.FailureCodeHeader)
answered, err := io.ReadAll(io.LimitReader(response.Body, 1<<20))
require.NoError(t, err)
observed.Response = s.resultOf(t, response.StatusCode, answered)
if response.StatusCode != http.StatusOK && observed.FailureCode == "" {
observed.Trouble = fmt.Errorf("owner answered %d: %s", response.StatusCode, answered)
}
}
completion, err := corerunnable.ClassifyServed(inv, s.pkg, observed)
require.NoError(t, err)
return completion
}
// resultOf turns the owner's answer into the RunnableResult core classifies. A
// 200 is the bounded output document; anything else answered no output at all,
// and a result invented for it would be a completion the owner never gave.
func (s served) resultOf(t *testing.T, status int, body []byte) []byte {
t.Helper()
if status != http.StatusOK {
return nil
}
document, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&basev0.RunnableResult{
Protocol: corerunnable.ServedProtocolV1,
InvocationId: invocationID,
Status: basev0.RunnableResult_SUCCEEDED,
Output: body,
})
require.NoError(t, err)
return document
}
func (s served) invocation(t *testing.T, input []byte) *basev0.RunnableInvocation {
t.Helper()
issued := time.Now().UTC()
inv, err := corerunnable.PrepareInvocation(&basev0.RunnableInvocation{
Protocol: corerunnable.ServedProtocolV1,
Runnable: s.pkg.GetIdentity(),
InvocationId: invocationID,
IntentId: "intent-1",
EffectId: "effect-1",
IssuedAt: timestamppb.New(issued),
Deadline: timestamppb.New(issued.Add(time.Minute)),
Input: input,
Identity: &basev0.RunnableInvocationIdentity{
Carrier: &basev0.RunnableInvocationIdentity_WorkContext{WorkContext: workContext},
},
}, s.pkg)
require.NoError(t, err)
return inv
}
// count reads the operation's own output out of a classified success.
func count(t *testing.T, completion *basev0.RunnableServedCompletion) int {
t.Helper()
require.Equal(t, basev0.RunnableServedOutcome_SERVED_SUCCEEDED, completion.GetOutcome(), completion.GetMessage())
var output struct {
Count int `json:"count"`
}
require.NoError(t, json.Unmarshal(completion.GetResult().GetOutput(), &output))
return output.Count
}
func packageFromEvidence(t *testing.T, r *resources.Runnable, evidence *pack.Evidence) *basev0.RunnablePackage {
t.Helper()
build := &basev0.RunnableBuild{}
require.NoError(t, protojson.Unmarshal(evidence.Build, build))
var documents []json.RawMessage
require.NoError(t, json.Unmarshal(evidence.Artifacts, &documents))
var artifacts []*basev0.RunnableArtifact
for _, document := range documents {
artifact := &basev0.RunnableArtifact{}
require.NoError(t, protojson.Unmarshal(document, artifact))
artifacts = append(artifacts, artifact)
}
return preparedPackage(t, r, &basev0.RunnableIdentity{Name: r.Name, Module: "text", Workspace: "proof", Version: r.Version}, build, artifacts)
}
func preparedPackage(t *testing.T, r *resources.Runnable, identity *basev0.RunnableIdentity, build *basev0.RunnableBuild, artifacts []*basev0.RunnableArtifact) *basev0.RunnablePackage {
t.Helper()
declaration, err := r.Proto(t.Context())
require.NoError(t, err)
pkg, err := corerunnable.PreparePackage(&basev0.RunnablePackage{
Schema: corerunnable.PackageSchemaV1, Identity: identity, Agent: declaration.GetAgent(),
Contract: declaration.GetContract(), Execution: declaration.GetExecution(), Build: build, Artifacts: artifacts,
ServiceDependencies: declaration.GetServiceDependencies(), WorkspaceConfigurationDependencies: declaration.GetWorkspaceConfigurationDependencies(),
})
require.NoError(t, err)
return pkg
}
func artifacts(t *testing.T, evidence *pack.Evidence) []map[string]any {
t.Helper()
var recorded []map[string]any
if err := json.Unmarshal(evidence.Artifacts, &recorded); err != nil {
t.Fatalf("artifacts: %v", err)
}
if len(recorded) == 0 {
t.Fatal("the evidence records no artifact")
}
return recorded
}
func command(t *testing.T, artifact map[string]any) []string {
t.Helper()
raw, ok := artifact["command"].([]any)
if !ok || len(raw) == 0 {
t.Fatalf("the native artifact carries no launch command: %v", artifact)
}
argv := make([]string, 0, len(raw))
for _, part := range raw {
argv = append(argv, fmt.Sprint(part))
}
return argv
}
func unpack(t *testing.T, archive string, target string) {
t.Helper()
file, err := os.Open(archive)
if err != nil {
t.Fatal(err)
}
defer file.Close()
decompressed, err := gzip.NewReader(file)
if err != nil {
t.Fatal(err)
}
reader := tar.NewReader(decompressed)
for {
header, err := reader.Next()
if err == io.EOF {
return
}
if err != nil {
t.Fatal(err)
}
path := filepath.Join(target, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatal(err)
}
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink(header.Linkname, path); err != nil {
t.Fatal(err)
}
default:
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
content, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode())
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(content, reader); err != nil {
t.Fatal(err)
}
content.Close()
}
}
}
func write(t *testing.T, path string, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func read(t *testing.T, path string) string {
t.Helper()
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return string(content)
}