github.com/triarius/goreleaser@v1.12.5/internal/pipe/sbom/sbom_test.go (about) 1 package sbom 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "sort" 8 "strings" 9 "testing" 10 11 "github.com/triarius/goreleaser/internal/artifact" 12 "github.com/triarius/goreleaser/pkg/config" 13 "github.com/triarius/goreleaser/pkg/context" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 ) 17 18 func TestDescription(t *testing.T) { 19 require.NotEmpty(t, Pipe{}.String()) 20 } 21 22 func TestSBOMCatalogDefault(t *testing.T) { 23 defaultArgs := []string{"$artifact", "--file", "$document", "--output", "spdx-json"} 24 defaultSboms := []string{ 25 "{{ .ArtifactName }}.sbom", 26 } 27 defaultCmd := "syft" 28 tests := []struct { 29 configs []config.SBOM 30 artifact string 31 cmd string 32 sboms []string 33 args []string 34 env []string 35 err bool 36 }{ 37 { 38 configs: []config.SBOM{ 39 { 40 // empty 41 }, 42 }, 43 artifact: "archive", 44 cmd: defaultCmd, 45 sboms: defaultSboms, 46 args: defaultArgs, 47 env: []string{ 48 "SYFT_FILE_METADATA_CATALOGER_ENABLED=true", 49 }, 50 }, 51 { 52 configs: []config.SBOM{ 53 { 54 Artifacts: "package", 55 }, 56 }, 57 artifact: "package", 58 cmd: defaultCmd, 59 sboms: defaultSboms, 60 args: defaultArgs, 61 }, 62 { 63 configs: []config.SBOM{ 64 { 65 Artifacts: "archive", 66 }, 67 }, 68 artifact: "archive", 69 cmd: defaultCmd, 70 sboms: defaultSboms, 71 args: defaultArgs, 72 env: []string{ 73 "SYFT_FILE_METADATA_CATALOGER_ENABLED=true", 74 }, 75 }, 76 { 77 configs: []config.SBOM{ 78 { 79 Artifacts: "archive", 80 Env: []string{ 81 "something=something-else", 82 }, 83 }, 84 }, 85 artifact: "archive", 86 cmd: defaultCmd, 87 sboms: defaultSboms, 88 args: defaultArgs, 89 env: []string{ 90 "something=something-else", 91 }, 92 }, 93 { 94 configs: []config.SBOM{ 95 { 96 Artifacts: "any", 97 }, 98 }, 99 artifact: "any", 100 cmd: defaultCmd, 101 sboms: []string{}, 102 args: defaultArgs, 103 }, 104 { 105 configs: []config.SBOM{ 106 { 107 Artifacts: "binary", 108 }, 109 }, 110 artifact: "binary", 111 cmd: defaultCmd, 112 sboms: []string{"{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.sbom"}, 113 args: defaultArgs, 114 }, 115 { 116 configs: []config.SBOM{ 117 { 118 Artifacts: "source", 119 }, 120 }, 121 artifact: "source", 122 cmd: defaultCmd, 123 sboms: defaultSboms, 124 args: defaultArgs, 125 env: []string{ 126 "SYFT_FILE_METADATA_CATALOGER_ENABLED=true", 127 }, 128 }, 129 { 130 // multiple documents are not allowed when artifacts != "any" 131 configs: []config.SBOM{ 132 { 133 Artifacts: "binary", 134 Documents: []string{ 135 "doc1", 136 "doc2", 137 }, 138 }, 139 }, 140 err: true, 141 }, 142 } 143 144 for _, test := range tests { 145 t.Run(fmt.Sprintf("artifact=%q", test.configs[0].Artifacts), func(t *testing.T) { 146 ctx := &context.Context{ 147 Config: config.Project{ 148 SBOMs: test.configs, 149 }, 150 } 151 err := Pipe{}.Default(ctx) 152 if test.err { 153 require.Error(t, err) 154 return 155 } 156 require.NoError(t, err) 157 require.Equal(t, ctx.Config.SBOMs[0].Cmd, test.cmd) 158 require.Equal(t, ctx.Config.SBOMs[0].Documents, test.sboms) 159 require.Equal(t, ctx.Config.SBOMs[0].Args, test.args) 160 require.Equal(t, ctx.Config.SBOMs[0].Env, test.env) 161 require.Equal(t, ctx.Config.SBOMs[0].Artifacts, test.artifact) 162 }) 163 } 164 } 165 166 func TestSBOMCatalogInvalidArtifacts(t *testing.T) { 167 ctx := context.New(config.Project{}) 168 ctx.Config.SBOMs = []config.SBOM{ 169 {Artifacts: "foo"}, 170 } 171 err := Pipe{}.Run(ctx) 172 require.EqualError(t, err, "invalid list of artifacts to catalog: foo") 173 } 174 175 func TestSeveralSBOMsWithTheSameID(t *testing.T) { 176 ctx := &context.Context{ 177 Config: config.Project{ 178 SBOMs: []config.SBOM{ 179 { 180 ID: "a", 181 }, 182 { 183 ID: "a", 184 }, 185 }, 186 }, 187 } 188 require.EqualError(t, Pipe{}.Default(ctx), "found 2 sboms with the ID 'a', please fix your config") 189 } 190 191 func TestSkipCataloging(t *testing.T) { 192 t.Run("skip", func(t *testing.T) { 193 require.True(t, Pipe{}.Skip(context.New(config.Project{}))) 194 }) 195 196 t.Run("skip SBOM cataloging", func(t *testing.T) { 197 ctx := context.New(config.Project{ 198 SBOMs: []config.SBOM{ 199 { 200 Artifacts: "all", 201 }, 202 }, 203 }) 204 ctx.SkipSBOMCataloging = true 205 require.True(t, Pipe{}.Skip(ctx)) 206 }) 207 208 t.Run("dont skip", func(t *testing.T) { 209 ctx := context.New(config.Project{ 210 SBOMs: []config.SBOM{ 211 { 212 Artifacts: "all", 213 }, 214 }, 215 }) 216 require.False(t, Pipe{}.Skip(ctx)) 217 }) 218 } 219 220 func TestSBOMCatalogArtifacts(t *testing.T) { 221 tests := []struct { 222 desc string 223 ctx *context.Context 224 sbomPaths []string 225 sbomNames []string 226 expectedErrMsg string 227 }{ 228 { 229 desc: "catalog errors", 230 expectedErrMsg: "cataloging artifacts: exit failed", 231 ctx: context.New( 232 config.Project{ 233 SBOMs: []config.SBOM{ 234 { 235 Artifacts: "binary", 236 Cmd: "exit", 237 Args: []string{"1"}, 238 }, 239 }, 240 }, 241 ), 242 }, 243 { 244 desc: "invalid args template", 245 expectedErrMsg: `cataloging artifacts failed: arg "${FOO}-{{ .foo }{{}}{": invalid template: template: tmpl:1: unexpected "}" in operand`, 246 ctx: context.New( 247 config.Project{ 248 SBOMs: []config.SBOM{ 249 { 250 Artifacts: "binary", 251 Cmd: "exit", 252 Args: []string{"${FOO}-{{ .foo }{{}}{"}, 253 }, 254 }, 255 Env: []string{ 256 "FOO=BAR", 257 }, 258 }, 259 ), 260 }, 261 { 262 desc: "catalog source archives", 263 ctx: context.New( 264 config.Project{ 265 SBOMs: []config.SBOM{ 266 {Artifacts: "source"}, 267 }, 268 }, 269 ), 270 sbomPaths: []string{"artifact5.tar.gz.sbom"}, 271 sbomNames: []string{"artifact5.tar.gz.sbom"}, 272 }, 273 { 274 desc: "catalog archives", 275 ctx: context.New( 276 config.Project{ 277 SBOMs: []config.SBOM{ 278 {Artifacts: "archive"}, 279 }, 280 }, 281 ), 282 sbomPaths: []string{"artifact1.sbom", "artifact2.sbom"}, 283 sbomNames: []string{"artifact1.sbom", "artifact2.sbom"}, 284 }, 285 { 286 desc: "catalog linux packages", 287 ctx: context.New( 288 config.Project{ 289 SBOMs: []config.SBOM{ 290 {Artifacts: "package"}, 291 }, 292 }, 293 ), 294 sbomPaths: []string{"package1.deb.sbom"}, 295 sbomNames: []string{"package1.deb.sbom"}, 296 }, 297 { 298 desc: "catalog binaries", 299 ctx: context.New( 300 config.Project{ 301 SBOMs: []config.SBOM{ 302 {Artifacts: "binary"}, 303 }, 304 }, 305 ), 306 sbomPaths: []string{ 307 "artifact3-name_1.2.2_linux_amd64.sbom", 308 "artifact4-name_1.2.2_linux_amd64.sbom", 309 }, 310 sbomNames: []string{ 311 "artifact3-name_1.2.2_linux_amd64.sbom", 312 "artifact4-name_1.2.2_linux_amd64.sbom", 313 }, 314 }, 315 { 316 desc: "manual cataloging", 317 ctx: context.New( 318 config.Project{ 319 SBOMs: []config.SBOM{ 320 { 321 Artifacts: "any", 322 Args: []string{ 323 "--file", 324 "$document0", 325 "--output", 326 "spdx-json", 327 "artifact5.tar.gz", 328 }, 329 Documents: []string{ 330 "final.sbom", 331 }, 332 }, 333 }, 334 }, 335 ), 336 sbomPaths: []string{"final.sbom"}, 337 sbomNames: []string{"final.sbom"}, 338 }, 339 { 340 desc: "multiple SBOM configs", 341 ctx: context.New( 342 config.Project{ 343 Env: []string{ 344 "SBOM_SUFFIX=s2-ish", 345 }, 346 SBOMs: []config.SBOM{ 347 { 348 ID: "s1", 349 Artifacts: "binary", 350 }, 351 { 352 ID: "s2", 353 Artifacts: "archive", 354 Documents: []string{"{{ .ArtifactName }}.{{ .Env.SBOM_SUFFIX }}.sbom"}, 355 }, 356 }, 357 }, 358 ), 359 sbomPaths: []string{ 360 "artifact1.s2-ish.sbom", 361 "artifact2.s2-ish.sbom", 362 "artifact3-name_1.2.2_linux_amd64.sbom", 363 "artifact4-name_1.2.2_linux_amd64.sbom", 364 }, 365 sbomNames: []string{ 366 "artifact1.s2-ish.sbom", 367 "artifact2.s2-ish.sbom", 368 "artifact3-name_1.2.2_linux_amd64.sbom", 369 "artifact4-name_1.2.2_linux_amd64.sbom", 370 }, 371 }, 372 { 373 desc: "catalog artifacts with filtered by ID", 374 ctx: context.New( 375 config.Project{ 376 SBOMs: []config.SBOM{ 377 { 378 Artifacts: "binary", 379 IDs: []string{"foo"}, 380 }, 381 }, 382 }, 383 ), 384 sbomPaths: []string{ 385 "artifact3-name_1.2.2_linux_amd64.sbom", 386 }, 387 sbomNames: []string{ 388 "artifact3-name_1.2.2_linux_amd64.sbom", 389 }, 390 }, 391 { 392 desc: "catalog binary artifacts with env in arguments", 393 ctx: context.New( 394 config.Project{ 395 SBOMs: []config.SBOM{ 396 { 397 Artifacts: "binary", 398 Args: []string{ 399 "--file", 400 "$document", 401 "--output", 402 "spdx-json", 403 "$artifact", 404 }, 405 Documents: []string{ 406 "{{ .ArtifactName }}.{{ .Env.TEST_USER }}.sbom", 407 }, 408 }, 409 }, 410 Env: []string{ 411 "TEST_USER=test-user-name", 412 }, 413 }, 414 ), 415 sbomPaths: []string{ 416 "artifact3-name.test-user-name.sbom", 417 "artifact4.test-user-name.sbom", 418 }, 419 sbomNames: []string{ 420 "artifact3-name.test-user-name.sbom", 421 "artifact4.test-user-name.sbom", 422 }, 423 }, 424 { 425 desc: "cataloging 'any' artifacts fails", 426 ctx: context.New( 427 config.Project{ 428 SBOMs: []config.SBOM{ 429 { 430 Artifacts: "any", 431 Cmd: "false", 432 }, 433 }, 434 }, 435 ), 436 expectedErrMsg: "cataloging artifacts: false failed: exit status 1: ", 437 }, 438 } 439 440 for _, test := range tests { 441 t.Run(test.desc, func(t *testing.T) { 442 testSBOMCataloging(t, test.ctx, test.sbomPaths, test.sbomNames, test.expectedErrMsg) 443 }) 444 } 445 } 446 447 func testSBOMCataloging(tb testing.TB, ctx *context.Context, sbomPaths, sbomNames []string, expectedErrMsg string) { 448 tb.Helper() 449 tmpdir := tb.TempDir() 450 451 ctx.Config.Dist = tmpdir 452 ctx.Version = "1.2.2" 453 454 // create some fake artifacts 455 artifacts := []string{"artifact1", "artifact2", "artifact3", "package1.deb"} 456 require.NoError(tb, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm)) 457 for _, f := range artifacts { 458 file := filepath.Join(tmpdir, f) 459 require.NoError(tb, os.WriteFile(file, []byte("foo"), 0o644)) 460 } 461 require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0o644)) 462 artifacts = append(artifacts, "linux_amd64/artifact4") 463 require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0o644)) 464 artifacts = append(artifacts, "artifact5.tar.gz") 465 ctx.Artifacts.Add(&artifact.Artifact{ 466 Name: "artifact1", 467 Path: filepath.Join(tmpdir, "artifact1"), 468 Type: artifact.UploadableArchive, 469 Extra: map[string]interface{}{ 470 artifact.ExtraID: "foo", 471 }, 472 }) 473 ctx.Artifacts.Add(&artifact.Artifact{ 474 Name: "artifact2", 475 Path: filepath.Join(tmpdir, "artifact2"), 476 Type: artifact.UploadableArchive, 477 Extra: map[string]interface{}{ 478 artifact.ExtraID: "foo3", 479 }, 480 }) 481 ctx.Artifacts.Add(&artifact.Artifact{ 482 Name: "artifact3-name", 483 Path: filepath.Join(tmpdir, "artifact3"), 484 Goos: "linux", 485 Goarch: "amd64", 486 Type: artifact.UploadableBinary, 487 Extra: map[string]interface{}{ 488 artifact.ExtraID: "foo", 489 artifact.ExtraBinary: "artifact3-name", 490 }, 491 }) 492 ctx.Artifacts.Add(&artifact.Artifact{ 493 Name: "artifact4", 494 Path: filepath.Join(tmpdir, "linux_amd64", "artifact4"), 495 Goos: "linux", 496 Goarch: "amd64", 497 Type: artifact.Binary, 498 Extra: map[string]interface{}{ 499 artifact.ExtraID: "foo3", 500 artifact.ExtraBinary: "artifact4-name", 501 }, 502 }) 503 ctx.Artifacts.Add(&artifact.Artifact{ 504 Name: "artifact5.tar.gz", 505 Path: filepath.Join(tmpdir, "artifact5.tar.gz"), 506 Type: artifact.UploadableSourceArchive, 507 }) 508 ctx.Artifacts.Add(&artifact.Artifact{ 509 Name: "package1.deb", 510 Path: filepath.Join(tmpdir, "package1.deb"), 511 Type: artifact.LinuxPackage, 512 Extra: map[string]interface{}{ 513 artifact.ExtraID: "foo", 514 }, 515 }) 516 517 // configure the pipeline 518 require.NoError(tb, Pipe{}.Default(ctx)) 519 520 // run the pipeline 521 if expectedErrMsg != "" { 522 err := Pipe{}.Run(ctx) 523 require.Error(tb, err) 524 require.Contains(tb, err.Error(), expectedErrMsg) 525 return 526 } 527 528 require.NoError(tb, Pipe{}.Run(ctx)) 529 530 // ensure all artifacts have an ID 531 for _, arti := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() { 532 require.NotEmptyf(tb, arti.ID(), ".Extra.ID on %s", arti.Path) 533 } 534 535 // verify that only the artifacts and the sboms are in the dist dir 536 gotFiles := []string{} 537 538 require.NoError(tb, filepath.Walk(tmpdir, 539 func(path string, info os.FileInfo, err error) error { 540 if err != nil { 541 return err 542 } 543 if info.IsDir() { 544 return nil 545 } 546 relPath, err := filepath.Rel(tmpdir, path) 547 if err != nil { 548 return err 549 } 550 gotFiles = append(gotFiles, relPath) 551 return nil 552 }), 553 ) 554 555 wantFiles := append(artifacts, sbomPaths...) 556 sort.Strings(wantFiles) 557 require.ElementsMatch(tb, wantFiles, gotFiles, "SBOM paths differ") 558 559 var sbomArtifacts []string 560 for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.SBOM)).List() { 561 sbomArtifacts = append(sbomArtifacts, sig.Name) 562 } 563 564 require.ElementsMatch(tb, sbomArtifacts, sbomNames, "SBOM names differ") 565 } 566 567 func Test_subprocessDistPath(t *testing.T) { 568 cwd, err := os.Getwd() 569 require.NoError(t, err) 570 571 tests := []struct { 572 name string 573 distDir string 574 pathRelativeToCwd string 575 expects string 576 }{ 577 { 578 name: "relative dist with anchor", 579 distDir: "./dist", 580 pathRelativeToCwd: "dist/my.sbom", 581 expects: "my.sbom", 582 }, 583 { 584 name: "relative dist without anchor", 585 distDir: "dist", 586 pathRelativeToCwd: "dist/my.sbom", 587 expects: "my.sbom", 588 }, 589 { 590 name: "relative dist with nested resource", 591 distDir: "dist", 592 pathRelativeToCwd: "dist/something/my.sbom", 593 expects: "something/my.sbom", 594 }, 595 { 596 name: "absolute dist with nested resource", 597 distDir: filepath.Join(cwd, "dist/"), 598 pathRelativeToCwd: "dist/something/my.sbom", 599 expects: "something/my.sbom", 600 }, 601 } 602 for _, test := range tests { 603 t.Run(test.name, func(t *testing.T) { 604 actual, err := subprocessDistPath(test.distDir, test.pathRelativeToCwd) 605 require.NoError(t, err) 606 assert.Equal(t, test.expects, actual) 607 }) 608 } 609 } 610 611 func Test_templateNames(t *testing.T) { 612 art := artifact.Artifact{ 613 Name: "name-it", 614 Path: "to/a/place", 615 Goos: "darwin", 616 Goarch: "amd64", 617 Type: artifact.Binary, 618 Extra: map[string]interface{}{ 619 artifact.ExtraID: "id-it", 620 "Binary": "binary-name", 621 }, 622 } 623 624 wd, err := os.Getwd() 625 require.NoError(t, err) 626 627 tests := []struct { 628 name string 629 dist string 630 version string 631 cfg config.SBOM 632 artifact artifact.Artifact 633 expectedValues map[string]string 634 expectedPaths []string 635 }{ 636 { 637 name: "default configuration", 638 artifact: art, 639 cfg: config.SBOM{}, 640 dist: "/somewhere/to/dist", 641 expectedPaths: []string{ 642 "/somewhere/to/dist/name-it.sbom", 643 }, 644 expectedValues: map[string]string{ 645 "artifact": "to/a/place", 646 "artifactID": "id-it", 647 "document": "/somewhere/to/dist/name-it.sbom", 648 "document0": "/somewhere/to/dist/name-it.sbom", 649 }, 650 }, 651 { 652 name: "default configuration + relative dist", 653 artifact: art, 654 cfg: config.SBOM{}, 655 dist: "somewhere/to/dist", 656 expectedPaths: []string{ 657 filepath.Join(wd, "somewhere/to/dist/name-it.sbom"), 658 }, 659 expectedValues: map[string]string{ 660 "artifact": "to/a/place", // note: this is always relative to ${dist} 661 "artifactID": "id-it", 662 "document": filepath.Join(wd, "somewhere/to/dist/name-it.sbom"), 663 "document0": filepath.Join(wd, "somewhere/to/dist/name-it.sbom"), 664 }, 665 }, 666 { 667 name: "custom document using $artifact", 668 // note: this configuration is probably a misconfiguration since it is placing SBOMs within each bin 669 // directory, however, it will behave as correctly as possible. 670 artifact: art, 671 cfg: config.SBOM{ 672 Documents: []string{ 673 // note: the artifact name is probably an incorrect value here since it can't express all attributes 674 // of the binary (os, arch, etc), so builds with multiple architectures will create SBOMs with the 675 // same name. 676 "${artifact}.cdx.sbom", 677 }, 678 }, 679 dist: "somewhere/to/dist", 680 expectedPaths: []string{ 681 filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"), 682 }, 683 expectedValues: map[string]string{ 684 "artifact": "to/a/place", 685 "artifactID": "id-it", 686 "document": filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"), 687 "document0": filepath.Join(wd, "somewhere/to/dist/to/a/place.cdx.sbom"), 688 }, 689 }, 690 { 691 name: "custom document using build vars", 692 artifact: art, 693 cfg: config.SBOM{ 694 Documents: []string{ 695 "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom", 696 }, 697 }, 698 version: "1.0.0", 699 dist: "somewhere/to/dist", 700 expectedPaths: []string{ 701 filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), 702 }, 703 expectedValues: map[string]string{ 704 "artifact": "to/a/place", 705 "artifactID": "id-it", 706 "document": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), 707 "document0": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), 708 }, 709 }, 710 { 711 name: "env vars with go templated options", 712 artifact: art, 713 cfg: config.SBOM{ 714 Documents: []string{ 715 "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cdx.sbom", 716 }, 717 Env: []string{ 718 "with-env-var=value", 719 "custom-os={{ .Os }}-unique", 720 "custom-arch={{ .Arch }}-unique", 721 }, 722 }, 723 version: "1.0.0", 724 dist: "somewhere/to/dist", 725 expectedPaths: []string{ 726 filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), 727 }, 728 expectedValues: map[string]string{ 729 "artifact": "to/a/place", 730 "artifactID": "id-it", 731 "with-env-var": "value", 732 "custom-os": "darwin-unique", 733 "custom-arch": "amd64-unique", 734 "document": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), 735 "document0": filepath.Join(wd, "somewhere/to/dist/binary-name_1.0.0_darwin_amd64.cdx.sbom"), 736 }, 737 }, 738 } 739 for _, tt := range tests { 740 t.Run(tt.name, func(t *testing.T) { 741 ctx := context.New(config.Project{ 742 Dist: tt.dist, 743 }) 744 ctx.Version = tt.version 745 746 cfg := tt.cfg 747 require.NoError(t, setConfigDefaults(&cfg)) 748 749 var inputArgs []string 750 var expectedArgs []string 751 for key, value := range tt.expectedValues { 752 inputArgs = append(inputArgs, fmt.Sprintf("${%s}", key)) 753 expectedArgs = append(expectedArgs, value) 754 } 755 cfg.Args = inputArgs 756 757 actualArgs, actualEnvs, actualPaths, err := applyTemplate(ctx, cfg, &tt.artifact) 758 require.NoError(t, err) 759 760 assert.Equal(t, tt.expectedPaths, actualPaths, "paths differ") 761 762 assert.Equal(t, expectedArgs, actualArgs, "arguments differ") 763 764 actualEnv := make(map[string]string) 765 for _, str := range actualEnvs { 766 k, v, ok := strings.Cut(str, "=") 767 require.True(t, ok) 768 actualEnv[k] = v 769 } 770 771 for k, v := range tt.expectedValues { 772 assert.Equal(t, v, actualEnv[k]) 773 } 774 }) 775 } 776 }