diff --git a/devnet-sdk/kt/fs/fs.go b/devnet-sdk/kt/fs/fs.go index cd4841c1f71bf..e1e4d9d3f5d55 100644 --- a/devnet-sdk/kt/fs/fs.go +++ b/devnet-sdk/kt/fs/fs.go @@ -5,16 +5,19 @@ import ( "bytes" "compress/gzip" "context" + "fmt" "io" "os" "path/filepath" + "github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings" "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services" "github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context" ) // EnclaveContextIface abstracts the EnclaveContext for testing type EnclaveContextIface interface { + GetAllFilesArtifactNamesAndUuids(ctx context.Context) ([]*kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid, error) DownloadFilesArtifact(ctx context.Context, name string) ([]byte, error) UploadFiles(pathToUpload string, artifactName string) (services.FilesArtifactUUID, services.FileArtifactName, error) } @@ -46,6 +49,20 @@ type Artifact struct { reader *tar.Reader } +func (fs *EnclaveFS) GetAllArtifactNames(ctx context.Context) ([]string, error) { + artifacts, err := fs.enclaveCtx.GetAllFilesArtifactNamesAndUuids(ctx) + if err != nil { + return nil, err + } + + names := make([]string, len(artifacts)) + for i, artifact := range artifacts { + names[i] = artifact.GetFileName() + } + + return names, nil +} + func (fs *EnclaveFS) GetArtifact(ctx context.Context, name string) (*Artifact, error) { artifact, err := fs.enclaveCtx.DownloadFilesArtifact(ctx, name) if err != nil { @@ -73,6 +90,47 @@ func NewArtifactFileWriter(path string, writer io.Writer) *ArtifactFileWriter { } } +func (a *Artifact) Download(path string) error { + for { + header, err := a.reader.Next() + if err == io.EOF { + return nil + } + if err != nil { + return fmt.Errorf("failed to read tar header: %w", err) + } + + fpath := filepath.Join(path, filepath.Clean(header.Name)) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to create directory %s: %w", fpath, err) + } + case tar.TypeReg: + // Create parent directories if they don't exist + if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { + return fmt.Errorf("failed to create directory for %s: %w", fpath, err) + } + + // Create the file + f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode)) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", fpath, err) + } + + // Copy contents from tar reader to file + if _, err := io.Copy(f, a.reader); err != nil { + f.Close() + return fmt.Errorf("failed to write contents to %s: %w", fpath, err) + } + f.Close() + default: + return fmt.Errorf("unsupported file type %d for %s", header.Typeflag, header.Name) + } + } +} + func (a *Artifact) ExtractFiles(writers ...*ArtifactFileWriter) error { paths := make(map[string]io.Writer) for _, writer := range writers { diff --git a/devnet-sdk/kt/fs/fs_test.go b/devnet-sdk/kt/fs/fs_test.go index 94c2401653ca2..d202e544b6f5a 100644 --- a/devnet-sdk/kt/fs/fs_test.go +++ b/devnet-sdk/kt/fs/fs_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" + "github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings" "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services" "github.com/stretchr/testify/require" ) @@ -53,6 +54,12 @@ func (m *mockEnclaveContext) UploadFiles(pathToUpload string, artifactName strin return "test-uuid", services.FileArtifactName(artifactName), err } +func (m *mockEnclaveContext) GetAllFilesArtifactNamesAndUuids(ctx context.Context) ([]*kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid, error) { + return nil, nil +} + +var _ EnclaveContextIface = (*mockEnclaveContext)(nil) + func createTarGzArtifact(t *testing.T, files map[string]string) []byte { var buf bytes.Buffer gzWriter := gzip.NewWriter(&buf) diff --git a/devnet-sdk/shell/env/kt_fetch.go b/devnet-sdk/shell/env/kt_fetch.go index 2a8639ff1d856..d2a58b2aff0f2 100644 --- a/devnet-sdk/shell/env/kt_fetch.go +++ b/devnet-sdk/shell/env/kt_fetch.go @@ -11,13 +11,14 @@ import ( ) const ( - KurtosisDevnetEnvArtifactName = "devnet" - KurtosisDevnetEnvArtifactPath = "env.json" + KurtosisDevnetEnvArtifactNamePrefix = "devnet-descriptor-" + KurtosisDevnetEnvArtifactPath = "env.json" ) // EnclaveFS is an interface that both our mock and the real implementation satisfy type EnclaveFS interface { GetArtifact(ctx context.Context, name string) (*ktfs.Artifact, error) + GetAllArtifactNames(ctx context.Context) ([]string, error) Close() error } @@ -30,6 +31,10 @@ func (w *enclaveFSWrapper) GetArtifact(ctx context.Context, name string) (*ktfs. return w.fs.GetArtifact(ctx, name) } +func (w *enclaveFSWrapper) GetAllArtifactNames(ctx context.Context) ([]string, error) { + return w.fs.GetAllArtifactNames(ctx) +} + func (w *enclaveFSWrapper) Close() error { // The underlying EnclaveFS doesn't have a Close method, but we need it for our interface return nil @@ -49,11 +54,11 @@ var NewEnclaveFS NewEnclaveFSFunc = func(ctx context.Context, enclave string) (E } // parseKurtosisURL parses a Kurtosis URL of the form kt://enclave/artifact/file -// If artifact is omitted, it defaults to "devnet" +// If artifact is omitted, it defaults to "" // If file is omitted, it defaults to "env.json" func parseKurtosisURL(u *url.URL) (enclave, artifactName, fileName string) { enclave = u.Host - artifactName = KurtosisDevnetEnvArtifactName + artifactName = "" fileName = KurtosisDevnetEnvArtifactPath // Trim both prefix and suffix slashes before splitting @@ -69,6 +74,43 @@ func parseKurtosisURL(u *url.URL) (enclave, artifactName, fileName string) { return } +func getDefaultDescriptor(ctx context.Context, fs EnclaveFS) (string, error) { + prefix := KurtosisDevnetEnvArtifactNamePrefix + + names, err := fs.GetAllArtifactNames(ctx) + if err != nil { + return "", err + } + + var maxSuffix int + var maxName string + for _, name := range names { + if !strings.HasPrefix(name, prefix) { + continue + } + + // Extract the suffix after the prefix + suffix := name[len(prefix):] + // Parse the suffix as a number + var num int + if _, err := fmt.Sscanf(suffix, "%d", &num); err != nil { + continue // Skip if suffix is not a valid number + } + + // Update maxName if this number is larger + if maxName == "" || num > maxSuffix { + maxSuffix = num + maxName = name + } + } + + if maxName == "" { + return "", fmt.Errorf("no descriptor found with valid numerical suffix") + } + + return maxName, nil +} + // fetchKurtosisData reads data from a Kurtosis artifact func fetchKurtosisData(u *url.URL) (string, []byte, error) { enclave, artifactName, fileName := parseKurtosisURL(u) @@ -78,6 +120,14 @@ func fetchKurtosisData(u *url.URL) (string, []byte, error) { return "", nil, fmt.Errorf("error creating enclave fs: %w", err) } + if artifactName == "" { + artifactName, err = getDefaultDescriptor(context.Background(), fs) + if err != nil { + return "", nil, fmt.Errorf("error getting default descriptor: %w", err) + } + fmt.Printf("Using default descriptor: %s\n", artifactName) + } + art, err := fs.GetArtifact(context.Background(), artifactName) if err != nil { return "", nil, fmt.Errorf("error getting artifact: %w", err) diff --git a/devnet-sdk/shell/env/kt_fetch_test.go b/devnet-sdk/shell/env/kt_fetch_test.go index 8580e5ee3526c..9ef67d37dbfe7 100644 --- a/devnet-sdk/shell/env/kt_fetch_test.go +++ b/devnet-sdk/shell/env/kt_fetch_test.go @@ -12,20 +12,38 @@ import ( ) // testFS implements EnclaveFS for testing -type testFS struct{} +type testFS struct { + artifacts map[string]bool +} func (m *testFS) GetArtifact(_ context.Context, name string) (*ktfs.Artifact, error) { if name == "error" { return nil, fmt.Errorf("mock error") } + if !m.artifacts[name] { + return nil, fmt.Errorf("artifact %s not found", name) + } // We don't need to return a real artifact since we're only testing error cases return nil, nil } +func (m *testFS) GetAllArtifactNames(_ context.Context) ([]string, error) { + if m.artifacts == nil { + return nil, nil + } + names := make([]string, 0, len(m.artifacts)) + for name := range m.artifacts { + names = append(names, name) + } + return names, nil +} + func (m *testFS) Close() error { return nil } +var _ EnclaveFS = (*testFS)(nil) + func TestParseKurtosisURL(t *testing.T) { tests := []struct { name string @@ -39,7 +57,7 @@ func TestParseKurtosisURL(t *testing.T) { name: "basic url", urlStr: "kt://myenclave", wantEnclave: "myenclave", - wantArtifact: "devnet", + wantArtifact: "", wantFile: "env.json", }, { @@ -106,11 +124,20 @@ func TestFetchKurtosisDataErrors(t *testing.T) { name: "error getting artifact", setupMock: func() { NewEnclaveFS = func(_ context.Context, _ string) (EnclaveFS, error) { - return &testFS{}, nil + return &testFS{artifacts: map[string]bool{"error": true}}, nil } }, urlStr: "kt://myenclave/error", }, + { + name: "no default descriptor", + setupMock: func() { + NewEnclaveFS = func(_ context.Context, _ string) (EnclaveFS, error) { + return &testFS{artifacts: map[string]bool{}}, nil + } + }, + urlStr: "kt://myenclave", + }, } for _, tt := range tests { @@ -128,3 +155,62 @@ func TestFetchKurtosisDataErrors(t *testing.T) { }) } } + +func TestGetDefaultDescriptor(t *testing.T) { + tests := []struct { + name string + artifacts map[string]bool + wantName string + wantErrText string + }{ + { + name: "finds highest numbered descriptor", + artifacts: map[string]bool{ + "devnet-descriptor-1": true, + "devnet-descriptor-5": true, + "devnet-descriptor-10": true, + "other": true, + }, + wantName: "devnet-descriptor-10", + }, + { + name: "handles non-numeric suffixes", + artifacts: map[string]bool{ + "devnet-descriptor-1": true, + "devnet-descriptor-5": true, + "devnet-descriptor-abc": true, + "devnet-descriptor-10": true, + "other": true, + "devnet-descriptor-def": true, + }, + wantName: "devnet-descriptor-10", + }, + { + name: "no descriptors", + artifacts: map[string]bool{}, + wantErrText: "no descriptor found with valid numerical suffix", + }, + { + name: "no valid descriptors", + artifacts: map[string]bool{ + "other": true, + "devnet-abc": true, + }, + wantErrText: "no descriptor found with valid numerical suffix", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := &testFS{artifacts: tt.artifacts} + got, err := getDefaultDescriptor(context.Background(), fs) + if tt.wantErrText != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrText) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantName, got) + }) + } +} diff --git a/devnet-sdk/testing/systest/provider.go b/devnet-sdk/testing/systest/provider.go index 76974c54c5069..87da920d0d036 100644 --- a/devnet-sdk/testing/systest/provider.go +++ b/devnet-sdk/testing/systest/provider.go @@ -13,6 +13,3 @@ type defaultProvider struct{} func (p *defaultProvider) NewSystemFromURL(url string) (system.System, error) { return system.NewSystemFromURL(url) } - -// currentPackage is the current package implementation -var currentPackage systemProvider = &defaultProvider{} diff --git a/devnet-sdk/testing/systest/systest.go b/devnet-sdk/testing/systest/systest.go index 587b35a8985f0..1291fe8683b1b 100644 --- a/devnet-sdk/testing/systest/systest.go +++ b/devnet-sdk/testing/systest/systest.go @@ -47,12 +47,6 @@ func (e *PreconditionError) Unwrap() error { // Any other result indicates this acquirer was selected and its result (success or failure) should be used. type SystemAcquirer func(t BasicT) (system.System, error) -// systemAcquirers is the list of ways to acquire a system, tried in order -var systemAcquirers = []SystemAcquirer{ - acquireFromEnvURL, - // Add more acquirers here as needed -} - // tryAcquirers attempts to acquire a system using the provided acquirers in order. // Each acquirer is tried in sequence until one returns a non-(nil,nil) result. // If an acquirer returns (nil, nil), it is skipped and the next one is tried. @@ -70,19 +64,6 @@ func tryAcquirers(t BasicT, acquirers []SystemAcquirer) (system.System, error) { return nil, fmt.Errorf("no acquirer was able to create a system") } -// acquireFromEnvURL attempts to create a system from the URL specified in the environment variable. -func acquireFromEnvURL(t BasicT) (system.System, error) { - url := os.Getenv(env.EnvURLVar) - if url == "" { - return nil, nil // Skip this acquirer - } - sys, err := currentPackage.NewSystemFromURL(url) - if err != nil { - return nil, fmt.Errorf("failed to create system from URL %q: %w", url, err) - } - return sys, nil -} - type PreconditionValidator func(t T, sys system.System) (context.Context, error) type SystemTestFunc func(t T, sys system.System) type InteropSystemTestFunc func(t T, sys system.InteropSystem) @@ -91,11 +72,29 @@ type InteropSystemTestFunc func(t T, sys system.InteropSystem) type systemTestHelper interface { SystemTest(t BasicT, f SystemTestFunc, validators ...PreconditionValidator) InteropSystemTest(t BasicT, f InteropSystemTestFunc, validators ...PreconditionValidator) + WithAcquirers(acquirers []SystemAcquirer) *basicSystemTestHelper + WithProvider(provider systemProvider) *basicSystemTestHelper } // basicSystemTestHelper provides a basic implementation of systemTestHelper using environment variables type basicSystemTestHelper struct { expectPreconditionsMet bool + acquirers []SystemAcquirer + provider systemProvider + envGetter envGetter +} + +// acquireFromEnvURL attempts to create a system from the URL specified in the environment variable. +func (h *basicSystemTestHelper) acquireFromEnvURL(t BasicT) (system.System, error) { + url := h.envGetter.Getenv(env.EnvURLVar) + if url == "" { + return nil, nil // Skip this acquirer + } + sys, err := h.provider.NewSystemFromURL(url) + if err != nil { + return nil, fmt.Errorf("failed to create system from URL %q: %w", url, err) + } + return sys, nil } func (h *basicSystemTestHelper) handlePreconditionError(t BasicT, err error) { @@ -117,9 +116,10 @@ func (h *basicSystemTestHelper) SystemTest(t BasicT, f SystemTestFunc, validator wt = wt.WithContext(ctx) - sys, err := tryAcquirers(t, systemAcquirers) + sys, err := tryAcquirers(t, h.acquirers) if err != nil { - t.Fatalf("failed to acquire system: %v", err) + h.handlePreconditionError(t, err) + return } for _, validator := range validators { @@ -151,9 +151,33 @@ func newBasicSystemTestHelper(envGetter envGetter) *basicSystemTestHelper { if err != nil { expectPreconditionsMet = false // empty string or invalid value returns false } - return &basicSystemTestHelper{ + + helper := &basicSystemTestHelper{ expectPreconditionsMet: expectPreconditionsMet, + provider: &defaultProvider{}, + envGetter: envGetter, } + + // Set up acquirers after helper is constructed so we can use the method + helper.acquirers = []SystemAcquirer{ + helper.acquireFromEnvURL, + } + + return helper +} + +// WithAcquirers returns a new helper with the specified acquirers +func (h *basicSystemTestHelper) WithAcquirers(acquirers []SystemAcquirer) *basicSystemTestHelper { + newHelper := *h + newHelper.acquirers = acquirers + return &newHelper +} + +// WithProvider returns a new helper with the specified provider +func (h *basicSystemTestHelper) WithProvider(provider systemProvider) *basicSystemTestHelper { + newHelper := *h + newHelper.provider = provider + return &newHelper } // SystemTest delegates to the default helper diff --git a/devnet-sdk/testing/systest/systest_test.go b/devnet-sdk/testing/systest/systest_test.go index ae82f9119393f..2f657d3306a60 100644 --- a/devnet-sdk/testing/systest/systest_test.go +++ b/devnet-sdk/testing/systest/systest_test.go @@ -16,6 +16,7 @@ type mockSystemTestHelper struct { systemTestCalls int interopTestCalls int preconditionErrors []error + systemAcquirer func() (system.System, error) } func (h *mockSystemTestHelper) handlePreconditionError(t BasicT, err error) { @@ -30,12 +31,17 @@ func (h *mockSystemTestHelper) handlePreconditionError(t BasicT, err error) { func (h *mockSystemTestHelper) SystemTest(t BasicT, f SystemTestFunc, validators ...PreconditionValidator) { h.systemTestCalls++ wt := NewT(t) - sys := newMockSystem() ctx, cancel := context.WithCancel(wt.Context()) defer cancel() wt = wt.WithContext(ctx) + sys, err := h.systemAcquirer() + if err != nil { + h.handlePreconditionError(t, err) + return + } + for _, validator := range validators { ctx, err := validator(wt, sys) if err != nil { @@ -50,23 +56,13 @@ func (h *mockSystemTestHelper) SystemTest(t BasicT, f SystemTestFunc, validators func (h *mockSystemTestHelper) InteropSystemTest(t BasicT, f InteropSystemTestFunc, validators ...PreconditionValidator) { h.interopTestCalls++ - wt := NewT(t) - sys := newMockInteropSystem() - - ctx, cancel := context.WithCancel(wt.Context()) - defer cancel() - wt = wt.WithContext(ctx) - - for _, validator := range validators { - ctx, err := validator(wt, sys) - if err != nil { - h.handlePreconditionError(t, err) - return + h.SystemTest(t, func(t T, sys system.System) { + if sys, ok := sys.(system.InteropSystem); ok { + f(t, sys) + } else { + h.handlePreconditionError(t, fmt.Errorf("interop test requested, but system is not an interop system")) } - wt = wt.WithContext(ctx) - } - - f(wt, sys) + }, validators...) } // mockEnvGetter implements envGetter for testing @@ -118,74 +114,138 @@ func TestSystemTestHelper(t *testing.T) { // TestSystemTest tests the main SystemTest function func TestSystemTest(t *testing.T) { - withTestSystem(t, func() (system.System, error) { - return newMockSystem(), nil - }, func(t *testing.T) { - t.Run("basic system test", func(t *testing.T) { - called := false - SystemTest(t, func(t T, sys system.System) { - called = true - require.NotNil(t, sys) + t.Run("system acquisition failure", func(t *testing.T) { + testCases := []struct { + name string + expectMet bool + expectSkip bool + expectFatal bool + }{ + { + name: "preconditions not expected skips test", + expectMet: false, + expectSkip: true, + expectFatal: false, + }, + { + name: "preconditions expected fails test", + expectMet: true, + expectSkip: false, + expectFatal: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + helper := &mockSystemTestHelper{ + expectPreconditionsMet: tc.expectMet, + systemAcquirer: func() (system.System, error) { + return nil, fmt.Errorf("failed to acquire system") + }, + } + + recorder := &mockTBRecorder{mockTB: mockTB{name: "test"}} + helper.SystemTest(recorder, func(t T, sys system.System) { + t.Fatal("test function should not be called") + }) + + require.Equal(t, tc.expectSkip, recorder.skipped, "unexpected skip state") + require.Equal(t, tc.expectFatal, recorder.failed, "unexpected fatal state") + require.Len(t, helper.preconditionErrors, 1, "expected one precondition error") + require.Contains(t, helper.preconditionErrors[0].Error(), "failed to acquire system") }) - require.True(t, called) + } + }) + + t.Run("successful system acquisition", func(t *testing.T) { + helper := &mockSystemTestHelper{ + systemAcquirer: func() (system.System, error) { + return newMockSystem(), nil + }, + } + + called := false + helper.SystemTest(t, func(t T, sys system.System) { + called = true + require.NotNil(t, sys) }) + require.True(t, called) + }) - t.Run("with validator", func(t *testing.T) { - validatorCalled := false - testCalled := false + t.Run("with validator", func(t *testing.T) { + helper := &mockSystemTestHelper{ + systemAcquirer: func() (system.System, error) { + return newMockSystem(), nil + }, + } - validator := func(t T, sys system.System) (context.Context, error) { - validatorCalled = true - return t.Context(), nil - } + validatorCalled := false + testCalled := false - SystemTest(t, func(t T, sys system.System) { - testCalled = true - }, validator) + validator := func(t T, sys system.System) (context.Context, error) { + validatorCalled = true + return t.Context(), nil + } - require.True(t, validatorCalled) - require.True(t, testCalled) - }) + helper.SystemTest(t, func(t T, sys system.System) { + testCalled = true + }, validator) - t.Run("multiple validators", func(t *testing.T) { - validatorCount := 0 + require.True(t, validatorCalled) + require.True(t, testCalled) + }) - validator := func(t T, sys system.System) (context.Context, error) { - validatorCount++ - return t.Context(), nil - } + t.Run("multiple validators", func(t *testing.T) { + helper := &mockSystemTestHelper{ + systemAcquirer: func() (system.System, error) { + return newMockSystem(), nil + }, + } - SystemTest(t, func(t T, sys system.System) {}, validator, validator, validator) - require.Equal(t, 3, validatorCount) - }) + validatorCount := 0 + validator := func(t T, sys system.System) (context.Context, error) { + validatorCount++ + return t.Context(), nil + } + + helper.SystemTest(t, func(t T, sys system.System) {}, validator, validator, validator) + require.Equal(t, 3, validatorCount) }) } // TestInteropSystemTest tests the InteropSystemTest function func TestInteropSystemTest(t *testing.T) { t.Run("skips non-interop system", func(t *testing.T) { - withTestSystem(t, func() (system.System, error) { - return newMockSystem(), nil - }, func(t *testing.T) { - called := false - InteropSystemTest(t, func(t T, sys system.InteropSystem) { - called = true - }) - require.False(t, called) + helper := &mockSystemTestHelper{ + systemAcquirer: func() (system.System, error) { + return newMockSystem(), nil + }, + } + + recorder := &mockTBRecorder{mockTB: mockTB{name: "test"}} + called := false + helper.InteropSystemTest(recorder, func(t T, sys system.InteropSystem) { + called = true }) + require.False(t, called) + require.Len(t, helper.preconditionErrors, 1) + require.Contains(t, helper.preconditionErrors[0].Error(), "interop test requested") }) t.Run("runs with interop system", func(t *testing.T) { - withTestSystem(t, func() (system.System, error) { - return newMockInteropSystem(), nil - }, func(t *testing.T) { - called := false - InteropSystemTest(t, func(t T, sys system.InteropSystem) { - called = true - require.NotNil(t, sys.InteropSet()) - }) - require.True(t, called) + helper := &mockSystemTestHelper{ + systemAcquirer: func() (system.System, error) { + return newMockInteropSystem(), nil + }, + } + + called := false + helper.InteropSystemTest(t, func(t T, sys system.InteropSystem) { + called = true + require.NotNil(t, sys.InteropSet()) }) + require.True(t, called) + require.Empty(t, helper.preconditionErrors) }) } @@ -226,6 +286,9 @@ func TestPreconditionHandling(t *testing.T) { t.Run(tc.name, func(t *testing.T) { helper := &mockSystemTestHelper{ expectPreconditionsMet: tc.expectMet, + systemAcquirer: func() (system.System, error) { + return newMockSystem(), nil + }, } recorder := &mockTBRecorder{mockTB: mockTB{name: "test"}} diff --git a/devnet-sdk/testing/systest/testing_test.go b/devnet-sdk/testing/systest/testing_test.go index 62f2b5f1645f5..64c407233ae00 100644 --- a/devnet-sdk/testing/systest/testing_test.go +++ b/devnet-sdk/testing/systest/testing_test.go @@ -140,24 +140,6 @@ func (p *testPackage) NewSystemFromURL(string) (system.System, error) { return p.creator() } -// withTestSystem runs a test with a custom system creator -func withTestSystem(t *testing.T, creator testSystemCreator, f func(t *testing.T)) { - // Save original acquirers and restore after test - origAcquirers := systemAcquirers - defer func() { - systemAcquirers = origAcquirers - }() - - // Replace acquirers with just our test creator - systemAcquirers = []SystemAcquirer{ - func(t BasicT) (system.System, error) { - return creator() - }, - } - - f(t) -} - // TestNewT tests the creation and basic functionality of the test wrapper func TestNewT(t *testing.T) { t.Run("wraps *testing.T correctly", func(t *testing.T) { @@ -290,14 +272,8 @@ func TestTryAcquirers(t *testing.T) { }) } -// Update TestSystemAcquisition to match new behavior +// TestSystemAcquisition tests the system acquisition functionality func TestSystemAcquisition(t *testing.T) { - // Save original acquirers and restore after test - origAcquirers := systemAcquirers - defer func() { - systemAcquirers = origAcquirers - }() - t.Run("uses first non-skip acquirer (success)", func(t *testing.T) { sys1, sys2 := newMockSystem(), newMockSystem() acquirers := []SystemAcquirer{ @@ -305,78 +281,184 @@ func TestSystemAcquisition(t *testing.T) { mockAcquirer(sys1, nil), // selected and succeeds mockAcquirer(sys2, nil), // not reached } - systemAcquirers = acquirers + + helper := newBasicSystemTestHelper(&mockEnvGetter{}). + WithAcquirers(acquirers) var acquiredSys system.System - SystemTest(t, func(t T, sys system.System) { + helper.SystemTest(t, func(t T, sys system.System) { acquiredSys = sys }) require.Equal(t, sys1, acquiredSys) }) t.Run("fails when selected acquirer fails", func(t *testing.T) { - expectedErr := fmt.Errorf("selected acquirer failed") - systemAcquirers = []SystemAcquirer{ - mockAcquirer(nil, nil), // skipped - mockAcquirer(nil, expectedErr), // selected and fails + testCases := []struct { + name string + expectMet bool + expectSkip bool + expectFatal bool + }{ + { + name: "preconditions not expected skips test", + expectMet: false, + expectSkip: true, + expectFatal: false, + }, + { + name: "preconditions expected fails test", + expectMet: true, + expectSkip: false, + expectFatal: true, + }, } - mock := &mockTB{name: "mock"} - SystemTest(mock, func(t T, sys system.System) { - require.Fail(t, "should not reach here") - }) - require.True(t, mock.failed) - require.Contains(t, mock.lastError, expectedErr.Error()) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expectedErr := fmt.Errorf("selected acquirer failed") + acquirers := []SystemAcquirer{ + mockAcquirer(nil, nil), // skipped + mockAcquirer(nil, expectedErr), // selected and fails + } + + // Create a new helper with the right configuration + helper := newBasicSystemTestHelper(&mockEnvGetter{}). + WithAcquirers(acquirers) + helper.expectPreconditionsMet = tc.expectMet + + recorder := &mockTBRecorder{mockTB: mockTB{name: "test"}} + helper.SystemTest(recorder, func(t T, sys system.System) { + require.Fail(t, "should not reach here") + }) + + require.Equal(t, tc.expectSkip, recorder.skipped, "unexpected skip state") + require.Equal(t, tc.expectFatal, recorder.failed, "unexpected fatal state") + if tc.expectSkip { + require.Contains(t, recorder.skipMsg, expectedErr.Error()) + } + if tc.expectFatal { + require.Contains(t, recorder.fatalMsg, expectedErr.Error()) + } + }) + } }) t.Run("fails when all acquirers skip", func(t *testing.T) { - systemAcquirers = []SystemAcquirer{ - mockAcquirer(nil, nil), - mockAcquirer(nil, nil), + testCases := []struct { + name string + expectMet bool + expectSkip bool + expectFatal bool + }{ + { + name: "preconditions not expected skips test", + expectMet: false, + expectSkip: true, + expectFatal: false, + }, + { + name: "preconditions expected fails test", + expectMet: true, + expectSkip: false, + expectFatal: true, + }, } - mock := &mockTB{name: "mock"} - SystemTest(mock, func(t T, sys system.System) { - require.Fail(t, "should not reach here") - }) - require.True(t, mock.failed) - require.Contains(t, mock.lastError, "no acquirer was able to create a system") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + acquirers := []SystemAcquirer{ + mockAcquirer(nil, nil), + mockAcquirer(nil, nil), + } + + // Create a new helper with the right configuration + helper := newBasicSystemTestHelper(&mockEnvGetter{}). + WithAcquirers(acquirers) + helper.expectPreconditionsMet = tc.expectMet + + recorder := &mockTBRecorder{mockTB: mockTB{name: "test"}} + helper.SystemTest(recorder, func(t T, sys system.System) { + require.Fail(t, "should not reach here") + }) + + require.Equal(t, tc.expectSkip, recorder.skipped, "unexpected skip state") + require.Equal(t, tc.expectFatal, recorder.failed, "unexpected fatal state") + if tc.expectSkip { + require.Contains(t, recorder.skipMsg, "no acquirer was able to create a system") + } + if tc.expectFatal { + require.Contains(t, recorder.fatalMsg, "no acquirer was able to create a system") + } + }) + } }) t.Run("acquireFromEnvURL behavior", func(t *testing.T) { - // Save original env var and package - origEnvFile := os.Getenv(env.EnvURLVar) - origPkg := currentPackage - defer func() { - os.Setenv(env.EnvURLVar, origEnvFile) - currentPackage = origPkg - }() + // Create a mockEnvGetter with the original env value + origEnv := &mockEnvGetter{ + values: map[string]string{ + env.EnvURLVar: os.Getenv(env.EnvURLVar), + }, + } t.Run("skips when env var not set", func(t *testing.T) { - os.Unsetenv(env.EnvURLVar) - sys, err := acquireFromEnvURL(t) + helper := newBasicSystemTestHelper(&mockEnvGetter{ + values: make(map[string]string), + }) + sys, err := helper.acquireFromEnvURL(t) require.NoError(t, err) require.Nil(t, sys) }) t.Run("fails with error for invalid URL", func(t *testing.T) { - os.Setenv(env.EnvURLVar, "invalid://url") - sys, err := acquireFromEnvURL(t) + helper := newBasicSystemTestHelper(&mockEnvGetter{ + values: map[string]string{ + env.EnvURLVar: "invalid://url", + }, + }).WithProvider(&testPackage{ + creator: func() (system.System, error) { + return nil, fmt.Errorf("invalid URL") + }, + }) + sys, err := helper.acquireFromEnvURL(t) require.Error(t, err) require.Nil(t, sys) }) t.Run("succeeds with valid URL", func(t *testing.T) { - // Set up test package that returns a mock system - currentPackage = &testPackage{ + mockSys := newMockSystem() + helper := newBasicSystemTestHelper(&mockEnvGetter{ + values: map[string]string{ + env.EnvURLVar: "file:///valid/url", + }, + }).WithProvider(&testPackage{ creator: func() (system.System, error) { - return newMockSystem(), nil + return mockSys, nil }, - } - os.Setenv(env.EnvURLVar, "file:///valid/url") - sys, err := acquireFromEnvURL(t) + }) + sys, err := helper.acquireFromEnvURL(t) require.NoError(t, err) - require.NotNil(t, sys) + require.Equal(t, mockSys, sys) + }) + + // Verify original environment is preserved by running a test with the original env + t.Run("preserves original environment", func(t *testing.T) { + helper := newBasicSystemTestHelper(origEnv) + sys, err := helper.acquireFromEnvURL(t) + if origEnv.values[env.EnvURLVar] == "" { + require.NoError(t, err) + require.Nil(t, sys) + } else { + // If there was a value, we'd need a provider to handle it properly + helper = helper.WithProvider(&testPackage{ + creator: func() (system.System, error) { + return newMockSystem(), nil + }, + }) + sys, err = helper.acquireFromEnvURL(t) + require.NoError(t, err) + require.NotNil(t, sys) + } }) }) } diff --git a/kurtosis-devnet/pkg/deploy/deploy.go b/kurtosis-devnet/pkg/deploy/deploy.go index af7339b0ba279..6bbf328f9a4ed 100644 --- a/kurtosis-devnet/pkg/deploy/deploy.go +++ b/kurtosis-devnet/pkg/deploy/deploy.go @@ -8,6 +8,7 @@ import ( "io" "log" "os" + "strings" ktfs "github.com/ethereum-optimism/optimism/devnet-sdk/kt/fs" "github.com/ethereum-optimism/optimism/devnet-sdk/shell/env" @@ -39,6 +40,7 @@ type Deployer struct { engineManager EngineManager templateFile string dataFile string + newEnclaveFS func(ctx context.Context, enclave string) (*ktfs.EnclaveFS, error) } func WithKurtosisDeployer(ktDeployer DeployerFunc) DeployerOption { @@ -95,12 +97,19 @@ func WithEnclave(enclave string) DeployerOption { } } +func WithNewEnclaveFSFunc(newEnclaveFS func(ctx context.Context, enclave string) (*ktfs.EnclaveFS, error)) DeployerOption { + return func(d *Deployer) { + d.newEnclaveFS = newEnclaveFS + } +} + func NewDeployer(opts ...DeployerOption) *Deployer { d := &Deployer{ kurtosisBinary: "kurtosis", ktDeployer: func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error) { return kurtosis.NewKurtosisDeployer(opts...) }, + newEnclaveFS: ktfs.NewEnclaveFS, } for _, opt := range opts { opt(d) @@ -146,7 +155,7 @@ func (d *Deployer) deployEnvironment(ctx context.Context, r io.Reader) (*kurtosi } // Upload the environment info to the enclave. - fs, err := ktfs.NewEnclaveFS(ctx, d.enclave) + fs, err := d.newEnclaveFS(ctx, d.enclave) if err != nil { return nil, fmt.Errorf("error getting enclave fs: %w", err) } @@ -158,13 +167,45 @@ func (d *Deployer) deployEnvironment(ctx context.Context, r io.Reader) (*kurtosi return nil, fmt.Errorf("error encoding environment: %w", err) } - if err := fs.PutArtifact(ctx, env.KurtosisDevnetEnvArtifactName, ktfs.NewArtifactFileReader(env.KurtosisDevnetEnvArtifactPath, envBuf)); err != nil { + descName, err := getNextDevnetDescriptor(ctx, fs) + if err != nil { + return nil, fmt.Errorf("error getting next devnet descriptor: %w", err) + } + + if err := fs.PutArtifact(ctx, descName, ktfs.NewArtifactFileReader(env.KurtosisDevnetEnvArtifactPath, envBuf)); err != nil { return nil, fmt.Errorf("error putting environment artifact: %w", err) } return info, nil } +func getNextDevnetDescriptor(ctx context.Context, fs *ktfs.EnclaveFS) (string, error) { + artifactNames, err := fs.GetAllArtifactNames(ctx) + if err != nil { + return "", fmt.Errorf("error getting artifact names: %w", err) + } + + maxNum := -1 + for _, artifactName := range artifactNames { + if !strings.HasPrefix(artifactName, env.KurtosisDevnetEnvArtifactNamePrefix) { + continue + } + + numStr := strings.TrimPrefix(artifactName, env.KurtosisDevnetEnvArtifactNamePrefix) + num := 0 + if _, err := fmt.Sscanf(numStr, "%d", &num); err != nil { + log.Printf("Warning: invalid devnet descriptor format: %s", artifactName) + continue + } + + if num > maxNum { + maxNum = num + } + } + + return fmt.Sprintf("%s%d", env.KurtosisDevnetEnvArtifactNamePrefix, maxNum+1), nil +} + func (d *Deployer) renderTemplate(buildDir string, urlBuilder func(path ...string) string) (*bytes.Buffer, error) { t := &Templater{ baseDir: d.baseDir, diff --git a/kurtosis-devnet/pkg/deploy/deploy_test.go b/kurtosis-devnet/pkg/deploy/deploy_test.go index 36ac00f6ff7a5..9b2b08237d663 100644 --- a/kurtosis-devnet/pkg/deploy/deploy_test.go +++ b/kurtosis-devnet/pkg/deploy/deploy_test.go @@ -9,8 +9,11 @@ import ( "path/filepath" "testing" + ktfs "github.com/ethereum-optimism/optimism/devnet-sdk/kt/fs" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec" + "github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings" + "github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,6 +43,30 @@ func (m *mockDeployerForTest) GetEnvironmentInfo(ctx context.Context, spec *spec return &kurtosis.KurtosisEnvironment{}, nil } +// mockEnclaveContext implements EnclaveContextIface for testing +type mockEnclaveContext struct { + artifacts []string +} + +func (m *mockEnclaveContext) GetAllFilesArtifactNamesAndUuids(ctx context.Context) ([]*kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid, error) { + result := make([]*kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid, len(m.artifacts)) + for i, name := range m.artifacts { + result[i] = &kurtosis_core_rpc_api_bindings.FilesArtifactNameAndUuid{ + FileName: name, + FileUuid: "test-uuid", + } + } + return result, nil +} + +func (m *mockEnclaveContext) DownloadFilesArtifact(ctx context.Context, name string) ([]byte, error) { + return nil, nil +} + +func (m *mockEnclaveContext) UploadFiles(pathToUpload string, artifactName string) (services.FilesArtifactUUID, services.FileArtifactName, error) { + return "", "", nil +} + func TestDeploy(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -68,12 +95,24 @@ func TestDeploy(t *testing.T) { return &mockDeployerForTest{baseDir: tmpDir}, nil } + // Create a mock EnclaveFS function + mockEnclaveFSFunc := func(ctx context.Context, enclave string) (*ktfs.EnclaveFS, error) { + mockCtx := &mockEnclaveContext{ + artifacts: []string{ + "devnet-descriptor-1", + "devnet-descriptor-2", + }, + } + return ktfs.NewEnclaveFSWithContext(mockCtx), nil + } + d := NewDeployer( WithBaseDir(tmpDir), WithKurtosisDeployer(mockDeployerFunc), WithDryRun(true), WithTemplateFile(templatePath), WithDataFile(dataPath), + WithNewEnclaveFSFunc(mockEnclaveFSFunc), ) env, err := d.Deploy(ctx, deployConfig) @@ -92,3 +131,71 @@ func TestDeploy(t *testing.T) { require.NoError(t, err) assert.Equal(t, "value", envData["test"]) } + +func TestGetNextDevnetDescriptor(t *testing.T) { + tests := []struct { + name string + artifacts []string + wantName string + wantErrText string + }{ + { + name: "increments highest numbered descriptor", + artifacts: []string{ + "devnet-descriptor-1", + "devnet-descriptor-5", + "devnet-descriptor-10", + "other", + }, + wantName: "devnet-descriptor-11", + }, + { + name: "handles non-numeric suffixes", + artifacts: []string{ + "devnet-descriptor-1", + "devnet-descriptor-5", + "devnet-descriptor-abc", + "devnet-descriptor-10", + "other", + "devnet-descriptor-def", + }, + wantName: "devnet-descriptor-11", + }, + { + name: "no descriptors", + artifacts: []string{}, + wantName: "devnet-descriptor-0", + }, + { + name: "no valid descriptors", + artifacts: []string{ + "other", + "devnet-abc", + }, + wantName: "devnet-descriptor-0", + }, + { + name: "handles negative numbers", + artifacts: []string{ + "devnet-descriptor--1", + "devnet-descriptor-5", + }, + wantName: "devnet-descriptor-6", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCtx := &mockEnclaveContext{artifacts: tt.artifacts} + fs := ktfs.NewEnclaveFSWithContext(mockCtx) + got, err := getNextDevnetDescriptor(context.Background(), fs) + if tt.wantErrText != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrText) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantName, got) + }) + } +}