- Notifications
You must be signed in to change notification settings - Fork1.1k
test: add golden file test for ConvertState#20832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
3db70e018fa1ff37aa13ecde7d657cbbc202ee6b405d4938fFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,16 +2,19 @@ package terraform_test | ||
| import ( | ||
| "context" | ||
| "crypto/sha256" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "slices" | ||
| "sort" | ||
| "strings" | ||
| "testing" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/uuid" | ||
| tfjson "github.com/hashicorp/terraform-json" | ||
| "github.com/stretchr/testify/require" | ||
| protobuf "google.golang.org/protobuf/proto" | ||
| @@ -30,6 +33,115 @@ func ctxAndLogger(t *testing.T) (context.Context, slog.Logger) { | ||
| return context.Background(), testutil.Logger(t) | ||
| } | ||
| // TestConvertStateGolden compares the output of ConvertState to a golden | ||
| // file to prevent regressions. If the logic changes, update the golden files | ||
| // accordingly. | ||
| // | ||
| // This was created to aid in refactoring `ConvertState`. | ||
| func TestConvertStateGolden(t *testing.T) { | ||
| t.Parallel() | ||
| testResourceDirectories := filepath.Join("testdata", "resources") | ||
| entries, err := os.ReadDir(testResourceDirectories) | ||
| require.NoError(t, err) | ||
| for _, testDirectory := range entries { | ||
| if !testDirectory.IsDir() { | ||
| continue | ||
| } | ||
| testFiles, err := os.ReadDir(filepath.Join(testResourceDirectories, testDirectory.Name())) | ||
| require.NoError(t, err) | ||
| // ConvertState works on both a plan file and a state file. | ||
| // The test should create a golden file for both. | ||
| for _, step := range []string{"plan", "state"} { | ||
| srcIdc := slices.IndexFunc(testFiles, func(entry os.DirEntry) bool { | ||
| return strings.HasSuffix(entry.Name(), fmt.Sprintf(".tf%s.json", step)) | ||
| }) | ||
| dotIdx := slices.IndexFunc(testFiles, func(entry os.DirEntry) bool { | ||
| return strings.HasSuffix(entry.Name(), fmt.Sprintf(".tf%s.dot", step)) | ||
| }) | ||
| // If the directory is missing these files, we cannot run ConvertState | ||
| // on it. So it's skipped. | ||
| if srcIdc == -1 || dotIdx == -1 { | ||
| continue | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This should log indicating so. | ||
| } | ||
| t.Run(step+"_"+testDirectory.Name(), func(t *testing.T) { | ||
| t.Parallel() | ||
| testDirectoryPath := filepath.Join(testResourceDirectories, testDirectory.Name()) | ||
| planFile := filepath.Join(testDirectoryPath, testFiles[srcIdc].Name()) | ||
| dotFile := filepath.Join(testDirectoryPath, testFiles[dotIdx].Name()) | ||
| ctx := testutil.Context(t, testutil.WaitMedium) | ||
| logger := slogtest.Make(t, nil) | ||
| // Gather plan | ||
| tfStepRaw, err := os.ReadFile(planFile) | ||
| require.NoError(t, err) | ||
| var modules []*tfjson.StateModule | ||
| switch step { | ||
| case "plan": | ||
| var tfPlan tfjson.Plan | ||
| err = json.Unmarshal(tfStepRaw, &tfPlan) | ||
| require.NoError(t, err) | ||
| modules = []*tfjson.StateModule{tfPlan.PlannedValues.RootModule} | ||
| if tfPlan.PriorState != nil { | ||
| modules = append(modules, tfPlan.PriorState.Values.RootModule) | ||
| } | ||
| case "state": | ||
| var tfState tfjson.State | ||
| err = json.Unmarshal(tfStepRaw, &tfState) | ||
| require.NoError(t, err) | ||
| modules = []*tfjson.StateModule{tfState.Values.RootModule} | ||
| default: | ||
| t.Fatalf("unknown step: %s", step) | ||
| } | ||
| // Gather graph | ||
| dotFileRaw, err := os.ReadFile(dotFile) | ||
| require.NoError(t, err) | ||
| // expectedOutput is `any` to support errors too. If `ConvertState` returns an | ||
| // error, that error is the golden file output. | ||
| var expectedOutput any | ||
| state, err := terraform.ConvertState(ctx, modules, string(dotFileRaw), logger) | ||
| if err == nil { | ||
| sortResources(state.Resources) | ||
| sortExternalAuthProviders(state.ExternalAuthProviders) | ||
| deterministicAppIDs(state.Resources) | ||
| expectedOutput = state | ||
| } else { | ||
| // Write the error to the file then. Track errors as much as valid paths. | ||
| expectedOutput = err.Error() | ||
| } | ||
| expPath := filepath.Join(testDirectoryPath, fmt.Sprintf("converted_state.%s.golden", step)) | ||
| if *updateGoldenFiles { | ||
| gotBytes, err := json.MarshalIndent(expectedOutput, "", " ") | ||
| require.NoError(t, err, "marshaling converted state to JSON") | ||
| // Newline at end of file for git purposes | ||
| err = os.WriteFile(expPath, append(gotBytes, '\n'), 0o600) | ||
| require.NoError(t, err) | ||
| return | ||
| } | ||
| gotBytes, err := json.Marshal(expectedOutput) | ||
| require.NoError(t, err, "marshaling converted state to JSON") | ||
| expBytes, err := os.ReadFile(expPath) | ||
| require.NoError(t, err) | ||
| require.JSONEq(t, string(expBytes), string(gotBytes), "converted state") | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| func TestConvertResources(t *testing.T) { | ||
| t.Parallel() | ||
| // nolint:dogsled | ||
| @@ -1669,3 +1781,18 @@ func sortExternalAuthProviders(providers []*proto.ExternalAuthProviderResource) | ||
| return strings.Compare(providers[i].Id, providers[j].Id) == -1 | ||
| }) | ||
| } | ||
| // deterministicAppIDs handles setting agent app ids to something deterministic. | ||
| // In plan files, ids are not present. In state files, they are. | ||
| // It is simpler for comparisons if we just set it to something deterministic. | ||
| func deterministicAppIDs(resources []*proto.Resource) { | ||
| for _, resource := range resources { | ||
| for _, agent := range resource.Agents { | ||
| for _, app := range agent.Apps { | ||
| data := sha256.Sum256([]byte(app.Slug + app.DisplayName)) | ||
| id, _ := uuid.FromBytes(data[:16]) | ||
| app.Id = id.String() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "a", | ||
| "type": "coder_ai_task" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [ | ||
| { | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| } | ||
| ], | ||
| "HasAITasks": true, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "a", | ||
| "type": "coder_ai_task" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [ | ||
| { | ||
| "id": "c4f032b8-97e4-42b0-aa2f-30a9e698f8d4", | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| } | ||
| ], | ||
| "HasAITasks": true, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "a", | ||
| "type": "coder_ai_task" | ||
| }, | ||
| { | ||
| "name": "b", | ||
| "type": "coder_ai_task" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [ | ||
| { | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| { | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| } | ||
| ], | ||
| "HasAITasks": true, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "a", | ||
| "type": "coder_ai_task" | ||
| }, | ||
| { | ||
| "name": "b", | ||
| "type": "coder_ai_task" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [ | ||
| { | ||
| "id": "89e6ab36-2e98-4d13-9b4c-69b7588b7e1d", | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd", | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| } | ||
| ], | ||
| "HasAITasks": true, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "a", | ||
| "type": "coder_ai_task" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [ | ||
| { | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| } | ||
| ], | ||
| "HasAITasks": true, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "a", | ||
| "type": "coder_ai_task" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [ | ||
| { | ||
| "id": "89e6ab36-2e98-4d13-9b4c-69b7588b7e1d", | ||
| "sidebar_app": { | ||
| "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| }, | ||
| "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" | ||
| } | ||
| ], | ||
| "HasAITasks": true, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "example", | ||
| "type": "null_resource", | ||
| "agents": [ | ||
| { | ||
| "name": "main", | ||
| "operating_system": "linux", | ||
| "architecture": "amd64", | ||
| "Auth": { | ||
| "Token": "" | ||
| }, | ||
| "connection_timeout_seconds": 120, | ||
| "display_apps": { | ||
| "vscode": true, | ||
| "web_terminal": true, | ||
| "ssh_helper": true, | ||
| "port_forwarding_helper": true | ||
| }, | ||
| "resources_monitoring": {}, | ||
| "api_key_scope": "all" | ||
| } | ||
| ], | ||
| "module_path": "module.module" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [], | ||
| "HasAITasks": false, | ||
| "HasExternalAgents": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "Resources": [ | ||
| { | ||
| "name": "example", | ||
| "type": "null_resource", | ||
| "agents": [ | ||
| { | ||
| "id": "8cb7c83a-eddb-45e9-a78c-4b50d0f10e5e", | ||
| "name": "main", | ||
| "operating_system": "linux", | ||
| "architecture": "amd64", | ||
| "Auth": { | ||
| "Token": "59bcf169-14fe-497d-9a97-709c1d837848" | ||
| }, | ||
| "connection_timeout_seconds": 120, | ||
| "display_apps": { | ||
| "vscode": true, | ||
| "web_terminal": true, | ||
| "ssh_helper": true, | ||
| "port_forwarding_helper": true | ||
| }, | ||
| "resources_monitoring": {}, | ||
| "api_key_scope": "all" | ||
| } | ||
| ], | ||
| "module_path": "module.module" | ||
| } | ||
| ], | ||
| "Parameters": [], | ||
| "Presets": [], | ||
| "ExternalAuthProviders": [], | ||
| "AITasks": [], | ||
| "HasAITasks": false, | ||
| "HasExternalAgents": false | ||
| } |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.