diff --git a/refactor/extract_module.go b/refactor/extract_module.go new file mode 100644 index 0000000..582479a --- /dev/null +++ b/refactor/extract_module.go @@ -0,0 +1,89 @@ +package refactor + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/raymyers/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" +) + +func Extract(addresses []string, toFolder, configPath string) (*UpdatePlan, error) { + filenames, err := filepath.Glob(configPath + "/*.tf") + if err != nil { + return nil, err + } + if err != nil { + return nil, err + } + plan := newUpdatePlan() + var parsedOutFile *hclwrite.File + toFile := filepath.Join(configPath, toFolder, "main.tf") + if _, err := os.Stat(toFile); errors.Is(err, os.ErrNotExist) { + parsedOutFile, err = ParseHclBytes([]byte{}, toFile) + if err != nil { + return nil, err + } + } else { + parsedOutFile, err = ParseHclFile(toFile) + if err != nil { + return nil, err + } + } + + beforeOutText := string(parsedOutFile.Bytes()) + for _, filename := range filenames { + fromPath, _ := filepath.Abs(filename) + toPath, _ := filepath.Abs(toFile) + if fromPath != "" && fromPath != toPath { + parsedInFile, err := ParseHclFile(filename) + beforeText := string(parsedInFile.Bytes()) + for _, fromAddressText := range addresses { + fromAddress := ParseAddress(fromAddressText) + + if err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + if moveAddrToFile(fromAddress, parsedInFile, parsedOutFile) { + if fromAddress.elementType == TypeResource { + moduleName := filepath.Base(toFolder) + toAddress := ParseAddress("module." + moduleName + "." + strings.Join(fromAddress.labels, ".")) + AddModuleBlock(parsedInFile, moduleName, toFolder) + AddMovedBlock(parsedInFile, fromAddress, toAddress) + } + } + } + afterText := string(parsedInFile.Bytes()) + if err != nil { + return nil, err + } + + diffText, err := diffText(beforeText, afterText, 3) + if len(diffText) > 0 { + fmt.Printf("Diff for %v\n%v\n", filename, diffText) + plan.addFileUpdate(&FileUpdate{filename, beforeText, afterText}) + } + } + } + afterOutText := string(parsedOutFile.Bytes()) + diffText, err := diffText(beforeOutText, afterOutText, 3) + if len(diffText) > 0 { + fmt.Printf("Diff for %v\n%v\n", toFile, diffText) + plan.addFileUpdate(&FileUpdate{toFile, beforeOutText, afterOutText}) + } + return &plan, nil +} + +func AddModuleBlock(file *hclwrite.File, moduleName, toFolder string) { + file.Body().AppendNewline() + movedBlock := file.Body().AppendNewBlock("module", []string{moduleName}) + + movedBlock.Body().SetAttributeValue("source", cty.StringVal(toFolder)) +} diff --git a/refactor/extract_module_test.go b/refactor/extract_module_test.go new file mode 100644 index 0000000..61a807e --- /dev/null +++ b/refactor/extract_module_test.go @@ -0,0 +1,33 @@ +package refactor + +import ( + "testing" +) + +func TestExtract(t *testing.T) { + cases := []struct { + name string + args []string + ok bool + from string + to string + }{ + { + name: "case_data_single_block", + args: []string{"a.a", "mymodule"}, + ok: true, + from: "test_data/extract_module/case_one_resource/from", + to: "test_data/extract_module/case_one_resource/to", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + plan, err := Extract(tc.args[0:], tc.args[len(tc.args)-1], tc.from) + if (err == nil) != tc.ok { + + } + assertMockCmdFileOutput(t, tc.name, tc.from, tc.to, plan) + }) + } +} diff --git a/refactor/mv.go b/refactor/mv.go index 49bf01f..ba71d2a 100644 --- a/refactor/mv.go +++ b/refactor/mv.go @@ -3,7 +3,6 @@ package refactor import ( "errors" "fmt" - "io/ioutil" "os" "path/filepath" @@ -15,18 +14,18 @@ func Mv(fromAddressString, toFile, configPath string) (*UpdatePlan, error) { if err != nil { return nil, err } - if err != nil { - return nil, err - } plan := newUpdatePlan() var parsedOutFile *hclwrite.File - if _, err := os.Stat(toFile); errors.Is(err, os.ErrNotExist) { - parsedOutFile, err = ParseHclBytes([]byte{}, toFile) + + // Assume toFile is relative to config path. Will this always be true? + toFilePath := filepath.Join(configPath, toFile) + if _, err := os.Stat(toFilePath); errors.Is(err, os.ErrNotExist) { + parsedOutFile, err = ParseHclBytes([]byte{}, toFilePath) if err != nil { return nil, err } } else { - parsedOutFile, err = ParseHclFile(toFile) + parsedOutFile, err = ParseHclFile(toFilePath) if err != nil { return nil, err } @@ -35,8 +34,7 @@ func Mv(fromAddressString, toFile, configPath string) (*UpdatePlan, error) { beforeOutText := string(parsedOutFile.Bytes()) for _, filename := range filenames { fromPath, _ := filepath.Abs(filename) - toPath, _ := filepath.Abs(toFile) - if fromPath != "" && fromPath != toPath { + if fromPath != "" && fromPath != toFilePath { parsedInFile, err := ParseHclFile(filename) if err != nil { return nil, err @@ -61,8 +59,8 @@ func Mv(fromAddressString, toFile, configPath string) (*UpdatePlan, error) { afterOutText := string(parsedOutFile.Bytes()) diffText, err := diffText(beforeOutText, afterOutText, 3) if len(diffText) > 0 { - fmt.Printf("Diff for %v\n%v\n", toFile, diffText) - plan.addFileUpdate(&FileUpdate{toFile, beforeOutText, afterOutText}) + fmt.Printf("Diff for %v\n%v\n", toFilePath, diffText) + plan.addFileUpdate(&FileUpdate{toFilePath, beforeOutText, afterOutText}) } return &plan, nil } @@ -75,11 +73,7 @@ func findOrCreateLocalsBlock(parsedFile *hclwrite.File) *hclwrite.Block { return parsedFile.Body().AppendNewBlock("locals", []string{}) } -func writeParsedFile(parsedFile *hclwrite.File, toFile string) error { - return ioutil.WriteFile(toFile, parsedFile.Bytes(), 0644) -} - -func moveLocals(parsedInFile, parsedOutFile *hclwrite.File) { +func moveLocals(parsedInFile, parsedOutFile *hclwrite.File) bool { for _, block := range parsedInFile.Body().Blocks() { if block.Type() == "locals" { @@ -93,12 +87,13 @@ func moveLocals(parsedInFile, parsedOutFile *hclwrite.File) { if !parsedInFile.Body().RemoveBlock(block) { fmt.Printf("WARN locals block could not be removed\n") } + return true } } + return false } -func moveLocal(localName string, parsedInFile, parsedOutFile *hclwrite.File) { - fmt.Printf("moveLocal %v\n", localName) +func moveLocal(localName string, parsedInFile, parsedOutFile *hclwrite.File) bool { for _, block := range parsedInFile.Body().Blocks() { if block.Type() == "locals" { @@ -106,11 +101,15 @@ func moveLocal(localName string, parsedInFile, parsedOutFile *hclwrite.File) { if attr != nil { toLocalsBlock := findOrCreateLocalsBlock(parsedOutFile) toLocalsBlock.Body().AppendUnstructuredTokens(attr.BuildTokens(nil)) + + block.Body().RemoveAttribute(localName) + // This can leave an empty block. Maybe check for that. + return true } - block.Body().RemoveAttribute(localName) - // This can leave an empty block. Maybe check for that. + } } + return false } func labelsEqual(a, b []string) bool { @@ -132,30 +131,30 @@ func min(a, b int) int { return b } -func moveBlock(addr *Address, parsedInFile, parsedOutFile *hclwrite.File) { +func moveBlock(addr *Address, parsedInFile, parsedOutFile *hclwrite.File) bool { + found := false addrLabels := addr.labels for _, block := range parsedInFile.Body().Blocks() { blockLabelsLimited := block.Labels()[0:min(len(addrLabels), len(block.Labels()))] if string(addr.BlockType()) == block.Type() && matchLabels(addr.labels, blockLabelsLimited) { - fmt.Printf("## Block matched %v %v\n", block.Type(), block.Labels()) + // fmt.Printf("## Block matched %v %v\n", block.Type(), block.Labels()) parsedOutFile.Body().AppendNewline() parsedOutFile.Body().AppendBlock(block) if !parsedInFile.Body().RemoveBlock(block) { fmt.Printf("WARN locals block could not be removed\n") } + found = true } } + return found } -func moveAddrToFile(addr *Address, parsedInFile, parsedOutFile *hclwrite.File) error { +func moveAddrToFile(addr *Address, parsedInFile, parsedOutFile *hclwrite.File) bool { if addr.elementType == TypeLocal && len(addr.labels) == 0 { - moveLocals(parsedInFile, parsedOutFile) + return moveLocals(parsedInFile, parsedOutFile) } else if addr.elementType == TypeLocal { localName := addr.labels[0] - moveLocal(localName, parsedInFile, parsedOutFile) - } else { - moveBlock(addr, parsedInFile, parsedOutFile) + return moveLocal(localName, parsedInFile, parsedOutFile) } - - return nil + return moveBlock(addr, parsedInFile, parsedOutFile) } diff --git a/refactor/mv_test.go b/refactor/mv_test.go index 5056b3c..177d328 100644 --- a/refactor/mv_test.go +++ b/refactor/mv_test.go @@ -1,16 +1,7 @@ package refactor import ( - "bytes" - "io" - "io/ioutil" - "log" - "os" - "path/filepath" - "strings" "testing" - - "github.com/stretchr/testify/assert" ) func TestMv(t *testing.T) { @@ -25,78 +16,78 @@ func TestMv(t *testing.T) { name: "case_data_single_block", args: []string{"data", "data.tf"}, ok: true, - from: "test_data/tf_org/case_data_single_block/from", - to: "test_data/tf_org/case_data_single_block/to", + from: "test_data/mv/case_data_single_block/from", + to: "test_data/mv/case_data_single_block/to", }, { name: "case_data_single_block_qualified", args: []string{"data.a", "data.tf"}, ok: true, - from: "test_data/tf_org/case_data_single_block/from", - to: "test_data/tf_org/case_data_single_block/to", + from: "test_data/mv/case_data_single_block/from", + to: "test_data/mv/case_data_single_block/to", }, { name: "case_data_single_block_fully_qualified", args: []string{"data.a.b", "data.tf"}, ok: true, - from: "test_data/tf_org/case_data_single_block/from", - to: "test_data/tf_org/case_data_single_block/to", + from: "test_data/mv/case_data_single_block/from", + to: "test_data/mv/case_data_single_block/to", }, { name: "case_data_single_block_new_file", args: []string{"data", "data.tf"}, ok: true, - from: "test_data/tf_org/case_data_single_block_new_file/from", - to: "test_data/tf_org/case_data_single_block_new_file/to", + from: "test_data/mv/case_data_single_block_new_file/from", + to: "test_data/mv/case_data_single_block_new_file/to", }, { name: "case_all_data_blocks", args: []string{"data", "data.tf"}, ok: true, - from: "test_data/tf_org/case_all_data_blocks/from", - to: "test_data/tf_org/case_all_data_blocks/to", + from: "test_data/mv/case_all_data_blocks/from", + to: "test_data/mv/case_all_data_blocks/to", }, { name: "case_all_vars", args: []string{"variable", "variables.tf"}, ok: true, - from: "test_data/tf_org/case_all_vars/from", - to: "test_data/tf_org/case_all_vars/to", + from: "test_data/mv/case_all_vars/from", + to: "test_data/mv/case_all_vars/to", }, { name: "case_single_var", args: []string{"variable.a", "variables.tf"}, ok: true, - from: "test_data/tf_org/case_single_var/from", - to: "test_data/tf_org/case_single_var/to", + from: "test_data/mv/case_single_var/from", + to: "test_data/mv/case_single_var/to", }, { name: "case_all_locals", args: []string{"locals", "locals.tf"}, ok: true, - from: "test_data/tf_org/case_all_locals/from", - to: "test_data/tf_org/case_all_locals/to", + from: "test_data/mv/case_all_locals/from", + to: "test_data/mv/case_all_locals/to", }, { name: "case_single_local", args: []string{"local.b", "locals.tf"}, ok: true, - from: "test_data/tf_org/case_single_local/from", - to: "test_data/tf_org/case_single_local/to", + from: "test_data/mv/case_single_local/from", + to: "test_data/mv/case_single_local/to", }, { name: "case_resource_type", args: []string{"resource.a", "dest.tf"}, ok: true, - from: "test_data/tf_org/case_resource_type/from", - to: "test_data/tf_org/case_resource_type/to", + from: "test_data/mv/case_resource_type/from", + to: "test_data/mv/case_resource_type/to", }, { name: "case_all_outputs", args: []string{"output", "outputs.tf"}, ok: true, - from: "test_data/tf_org/case_all_outputs/from", - to: "test_data/tf_org/case_all_outputs/to", + from: "test_data/mv/case_all_outputs/from", + to: "test_data/mv/case_all_outputs/to", }, } @@ -110,85 +101,3 @@ func TestMv(t *testing.T) { }) } } - -func copyFile(src string, dest string) { - sourceFile, err := os.Open(src) - if err != nil { - log.Fatal(err) - } - defer sourceFile.Close() - - // Create new file - newFile, err := os.Create(dest) - if err != nil { - log.Fatal(err) - } - defer newFile.Close() - - _, err = io.Copy(newFile, sourceFile) - if err != nil { - log.Fatal(err) - } -} - -func fileNames(vs []os.FileInfo) []string { - vsm := make([]string, len(vs)) - for i, v := range vs { - vsm[i] = v.Name() - } - return vsm -} - -// assertMockCmd is a high-level test helper to run a given mock command with -// arguments and check if an error and its stdout are expected. -func assertMockCmdFileOutput(t *testing.T, name string, from string, to string, plan *UpdatePlan) { - expectedFiles, err := ioutil.ReadDir(to) - if err != nil { - log.Fatal(err) - } - startingFiles, err := ioutil.ReadDir(from) - - filenameToContents := make(map[string]string) - for _, file := range startingFiles { - filePath := filepath.Join(from, file.Name()) - buf, err := ioutil.ReadFile(filePath) - if err != nil { - log.Fatalf("Loading %v - %v", filePath, err) - } - filenameToContents[filepath.Base(file.Name())] = string(buf) - } - for _, update := range plan.FileUpdates { - filenameToContents[filepath.Base(update.Filename)] = update.AfterText - } - - if len(expectedFiles) != len(filenameToContents) { - actualFilenames := []string{} - for k := range filenameToContents { - actualFilenames = append(actualFilenames, k) - } - t.Fatalf("Expected files to be %v, but found %v", fileNames(expectedFiles), actualFilenames) - } - for _, file := range expectedFiles { - assertFileHasContents(t, to+"/"+file.Name(), filenameToContents[filepath.Base(file.Name())]) - } -} - -func assertFileHasContents(t *testing.T, expectedFile string, actualContents string) { - expectedBuf, err := ioutil.ReadFile(expectedFile) - if err != nil { - log.Fatalf("Loading %v - %v", expectedFile, err) - } - expected := strings.TrimSpace(string(normalizeNewlines(expectedBuf))) - actual := strings.TrimSpace(string(normalizeNewlines([]byte(actualContents)))) - assert.Equal(t, expected, actual, "File %v", expectedFile) -} - -// NormalizeNewlines normalizes \r\n (windows) and \r (mac) -// into \n (unix) -func normalizeNewlines(d []byte) []byte { - // replace CR LF \r\n (windows) with LF \n (unix) - d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1) - // replace CF \r (mac) with LF \n (unix) - d = bytes.Replace(d, []byte{13}, []byte{10}, -1) - return d -} diff --git a/refactor/references.go b/refactor/references.go new file mode 100644 index 0000000..29bf347 --- /dev/null +++ b/refactor/references.go @@ -0,0 +1,43 @@ +package refactor + +import ( + "strings" + + "github.com/raymyers/hcl/v2/hclwrite" +) + +func findReferencingExpresssions(b *hclwrite.Body, address *Address) []*hclwrite.Expression { + var matched []*hclwrite.Expression + addReferencingExpresssions(b, address, &matched) + return matched +} + +func addReferencingExpresssions(body *hclwrite.Body, address *Address, matched *[]*hclwrite.Expression) { + addressRef := address.RefNameArray() + for _, attr := range body.Attributes() { + expr := attr.Expr() + for _, varTrav := range expr.Variables() { + travLabelsToMatch := traversalLabels(varTrav) + println("====") + println(strings.Join(travLabelsToMatch, "::")) + println(strings.Join(addressRef, "::")) + if len(travLabelsToMatch) > len(addressRef) { + travLabelsToMatch = travLabelsToMatch[0:len(addressRef)] + } + if matchLabels(addressRef, travLabelsToMatch) { + *matched = append(*matched, expr) + } + } + } + for _, block := range body.Blocks() { + addReferencingExpresssions(block.Body(), address, matched) + } +} + +func traversalLabels(trav *hclwrite.Traversal) []string { + // hclwrite currently has almost no API for Traversal, working around. + buf := strings.Builder{} + trav.BuildTokens(nil).WriteTo(&buf) + // This won't work in all cases, like arrays. + return strings.Split(strings.TrimSpace(buf.String()), ".") +} diff --git a/refactor/references_test.go b/refactor/references_test.go new file mode 100644 index 0000000..4b0d07f --- /dev/null +++ b/refactor/references_test.go @@ -0,0 +1,92 @@ +package refactor + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReferences(t *testing.T) { + cases := []struct { + name string + addr string + src string + count int + }{ + { + name: "one_ref", + addr: "a.b.c", + src: ` + b { + a = a.b.c + b = a.b.d + } + `, + count: 1, + }, + { + name: "two_refs", + addr: "a.b.c", + src: ` + b { + a = a.b.c + } + d { + z = a.b.c + } + `, + count: 2, + }, + { + name: "ref_in_nested_block", + addr: "a.b.c", + src: ` + b { + d { + z = a.b.c + } + } + `, + count: 1, + }, + { + name: "addr_shorter", + addr: "a.b", + src: ` + b { + y = a.b.d + z = a.b.c + } + `, + count: 2, + }, + { + name: "addr_longer", + addr: "a.b.c.d", + src: ` + b { + z = a.b.c + } + `, + count: 0, + }, + { + name: "interpolation", + addr: "a.b", + src: ` + b { + z = "prefix${a.b.c}" + } + `, + count: 1, + }, + } + for _, tc := range cases { + hclFile, err := ParseHclBytes([]byte(tc.src), "test.tf") + if err != nil { + t.Fatalf("unexpected err = %s", err) + } + found := findReferencingExpresssions(hclFile.Body(), ParseAddress(tc.addr)) + assert.Equal(t, tc.count, len(found), "Case %v", tc.name) + } +} diff --git a/refactor/rename.go b/refactor/rename.go index 2f8931f..62d3a05 100644 --- a/refactor/rename.go +++ b/refactor/rename.go @@ -42,13 +42,19 @@ func Rename(fromAddressString, toAddressString, configPath string) (*UpdatePlan, return &plan, nil } -func createTraversal(labels []string) (traversal hcl.Traversal) { +func createTraversal(address *Address) (traversal hcl.Traversal) { + labels := address.labels[1:] + root := address.labels[0] + if address.elementType == TypeModule { + root = "module" + labels = address.labels + } traversal = hcl.Traversal{ hcl.TraverseRoot{ - Name: labels[0], + Name: root, }, } - for _, label := range labels[1:] { + for _, label := range labels { traversal = append(traversal, hcl.TraverseAttr{ Name: label, }) @@ -68,11 +74,7 @@ func RenameInFile(filename string, file *hclwrite.File, fromAddress, toAddress * block.SetType(string(toAddress.BlockType())) block.SetLabels(toAddress.labels) if fromAddress.elementType == TypeResource && toAddress.elementType == TypeResource { - file.Body().AppendNewline() - movedBlock := file.Body().AppendNewBlock("moved", []string{}) - - movedBlock.Body().SetAttributeTraversal("from", createTraversal(fromAddress.labels)) - movedBlock.Body().SetAttributeTraversal("to", createTraversal(toAddress.labels)) + AddMovedBlock(file, fromAddress, toAddress) } } } @@ -81,6 +83,14 @@ func RenameInFile(filename string, file *hclwrite.File, fromAddress, toAddress * return nil } +func AddMovedBlock(file *hclwrite.File, fromAddress, toAddress *Address) { + file.Body().AppendNewline() + movedBlock := file.Body().AppendNewBlock("moved", []string{}) + + movedBlock.Body().SetAttributeTraversal("from", createTraversal(fromAddress)) + movedBlock.Body().SetAttributeTraversal("to", createTraversal(toAddress)) +} + func RenameLocalInFile(filename string, file *hclwrite.File, fromAddress, toAddress *Address) error { fromName := fromAddress.labels[0] toName := toAddress.labels[0] diff --git a/refactor/test_data/extract_module/case_one_resource/from/main.tf b/refactor/test_data/extract_module/case_one_resource/from/main.tf new file mode 100644 index 0000000..b3d0e35 --- /dev/null +++ b/refactor/test_data/extract_module/case_one_resource/from/main.tf @@ -0,0 +1,5 @@ +resource "a" "a" { +} + +resource "a" "b" { +} diff --git a/refactor/test_data/extract_module/case_one_resource/to/main.tf b/refactor/test_data/extract_module/case_one_resource/to/main.tf new file mode 100644 index 0000000..52ded8f --- /dev/null +++ b/refactor/test_data/extract_module/case_one_resource/to/main.tf @@ -0,0 +1,12 @@ + +resource "a" "b" { +} + +module "mymodule" { + source = "mymodule" +} + +moved { + from = a.a + to = module.mymodule.a.a +} diff --git a/refactor/test_data/tf_org/case_all_outputs/to/a.tf b/refactor/test_data/extract_module/case_one_resource/to/mymodule/main.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_outputs/to/a.tf rename to refactor/test_data/extract_module/case_one_resource/to/mymodule/main.tf diff --git a/refactor/test_data/tf_org/case_all_data_blocks/from/a.tf b/refactor/test_data/mv/case_all_data_blocks/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_data_blocks/from/a.tf rename to refactor/test_data/mv/case_all_data_blocks/from/a.tf diff --git a/refactor/test_data/tf_org/case_all_data_blocks/from/a2.tf b/refactor/test_data/mv/case_all_data_blocks/from/a2.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_data_blocks/from/a2.tf rename to refactor/test_data/mv/case_all_data_blocks/from/a2.tf diff --git a/refactor/test_data/tf_org/case_all_data_blocks/to/a.tf b/refactor/test_data/mv/case_all_data_blocks/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_data_blocks/to/a.tf rename to refactor/test_data/mv/case_all_data_blocks/to/a.tf diff --git a/refactor/test_data/tf_org/case_all_data_blocks/to/a2.tf b/refactor/test_data/mv/case_all_data_blocks/to/a2.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_data_blocks/to/a2.tf rename to refactor/test_data/mv/case_all_data_blocks/to/a2.tf diff --git a/refactor/test_data/tf_org/case_all_data_blocks/to/data.tf b/refactor/test_data/mv/case_all_data_blocks/to/data.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_data_blocks/to/data.tf rename to refactor/test_data/mv/case_all_data_blocks/to/data.tf diff --git a/refactor/test_data/tf_org/case_all_locals/from/a.tf b/refactor/test_data/mv/case_all_locals/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_locals/from/a.tf rename to refactor/test_data/mv/case_all_locals/from/a.tf diff --git a/refactor/test_data/tf_org/case_all_locals/from/b.tf b/refactor/test_data/mv/case_all_locals/from/b.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_locals/from/b.tf rename to refactor/test_data/mv/case_all_locals/from/b.tf diff --git a/refactor/test_data/tf_org/case_all_locals/to/a.tf b/refactor/test_data/mv/case_all_locals/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_locals/to/a.tf rename to refactor/test_data/mv/case_all_locals/to/a.tf diff --git a/refactor/test_data/tf_org/case_all_locals/to/b.tf b/refactor/test_data/mv/case_all_locals/to/b.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_locals/to/b.tf rename to refactor/test_data/mv/case_all_locals/to/b.tf diff --git a/refactor/test_data/tf_org/case_all_locals/to/locals.tf b/refactor/test_data/mv/case_all_locals/to/locals.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_locals/to/locals.tf rename to refactor/test_data/mv/case_all_locals/to/locals.tf diff --git a/refactor/test_data/tf_org/case_all_outputs/from/a.tf b/refactor/test_data/mv/case_all_outputs/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_outputs/from/a.tf rename to refactor/test_data/mv/case_all_outputs/from/a.tf diff --git a/refactor/test_data/mv/case_all_outputs/to/a.tf b/refactor/test_data/mv/case_all_outputs/to/a.tf new file mode 100644 index 0000000..798e517 --- /dev/null +++ b/refactor/test_data/mv/case_all_outputs/to/a.tf @@ -0,0 +1,2 @@ +resource "a" "a" { +} diff --git a/refactor/test_data/tf_org/case_all_outputs/to/outputs.tf b/refactor/test_data/mv/case_all_outputs/to/outputs.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_outputs/to/outputs.tf rename to refactor/test_data/mv/case_all_outputs/to/outputs.tf diff --git a/refactor/test_data/tf_org/case_all_vars/from/a.tf b/refactor/test_data/mv/case_all_vars/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_vars/from/a.tf rename to refactor/test_data/mv/case_all_vars/from/a.tf diff --git a/refactor/test_data/tf_org/case_all_vars/to/a.tf b/refactor/test_data/mv/case_all_vars/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_vars/to/a.tf rename to refactor/test_data/mv/case_all_vars/to/a.tf diff --git a/refactor/test_data/tf_org/case_all_vars/to/variables.tf b/refactor/test_data/mv/case_all_vars/to/variables.tf similarity index 100% rename from refactor/test_data/tf_org/case_all_vars/to/variables.tf rename to refactor/test_data/mv/case_all_vars/to/variables.tf diff --git a/refactor/test_data/tf_org/case_data_single_block/from/a.tf b/refactor/test_data/mv/case_data_single_block/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block/from/a.tf rename to refactor/test_data/mv/case_data_single_block/from/a.tf diff --git a/refactor/test_data/tf_org/case_data_single_block/from/data.tf b/refactor/test_data/mv/case_data_single_block/from/data.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block/from/data.tf rename to refactor/test_data/mv/case_data_single_block/from/data.tf diff --git a/refactor/test_data/tf_org/case_data_single_block/to/a.tf b/refactor/test_data/mv/case_data_single_block/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block/to/a.tf rename to refactor/test_data/mv/case_data_single_block/to/a.tf diff --git a/refactor/test_data/tf_org/case_data_single_block/to/data.tf b/refactor/test_data/mv/case_data_single_block/to/data.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block/to/data.tf rename to refactor/test_data/mv/case_data_single_block/to/data.tf diff --git a/refactor/test_data/tf_org/case_data_single_block_new_file/from/a.tf b/refactor/test_data/mv/case_data_single_block_new_file/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block_new_file/from/a.tf rename to refactor/test_data/mv/case_data_single_block_new_file/from/a.tf diff --git a/refactor/test_data/tf_org/case_data_single_block_new_file/to/a.tf b/refactor/test_data/mv/case_data_single_block_new_file/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block_new_file/to/a.tf rename to refactor/test_data/mv/case_data_single_block_new_file/to/a.tf diff --git a/refactor/test_data/tf_org/case_data_single_block_new_file/to/data.tf b/refactor/test_data/mv/case_data_single_block_new_file/to/data.tf similarity index 100% rename from refactor/test_data/tf_org/case_data_single_block_new_file/to/data.tf rename to refactor/test_data/mv/case_data_single_block_new_file/to/data.tf diff --git a/refactor/test_data/tf_org/case_resource_type/from/a.tf b/refactor/test_data/mv/case_resource_type/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_resource_type/from/a.tf rename to refactor/test_data/mv/case_resource_type/from/a.tf diff --git a/refactor/test_data/tf_org/case_resource_type/to/a.tf b/refactor/test_data/mv/case_resource_type/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_resource_type/to/a.tf rename to refactor/test_data/mv/case_resource_type/to/a.tf diff --git a/refactor/test_data/tf_org/case_resource_type/to/dest.tf b/refactor/test_data/mv/case_resource_type/to/dest.tf similarity index 100% rename from refactor/test_data/tf_org/case_resource_type/to/dest.tf rename to refactor/test_data/mv/case_resource_type/to/dest.tf diff --git a/refactor/test_data/tf_org/case_single_local/from/a.tf b/refactor/test_data/mv/case_single_local/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_single_local/from/a.tf rename to refactor/test_data/mv/case_single_local/from/a.tf diff --git a/refactor/test_data/tf_org/case_single_local/to/a.tf b/refactor/test_data/mv/case_single_local/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_single_local/to/a.tf rename to refactor/test_data/mv/case_single_local/to/a.tf diff --git a/refactor/test_data/tf_org/case_single_local/to/locals.tf b/refactor/test_data/mv/case_single_local/to/locals.tf similarity index 100% rename from refactor/test_data/tf_org/case_single_local/to/locals.tf rename to refactor/test_data/mv/case_single_local/to/locals.tf diff --git a/refactor/test_data/tf_org/case_single_var/from/a.tf b/refactor/test_data/mv/case_single_var/from/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_single_var/from/a.tf rename to refactor/test_data/mv/case_single_var/from/a.tf diff --git a/refactor/test_data/tf_org/case_single_var/to/a.tf b/refactor/test_data/mv/case_single_var/to/a.tf similarity index 100% rename from refactor/test_data/tf_org/case_single_var/to/a.tf rename to refactor/test_data/mv/case_single_var/to/a.tf diff --git a/refactor/test_data/tf_org/case_single_var/to/variables.tf b/refactor/test_data/mv/case_single_var/to/variables.tf similarity index 100% rename from refactor/test_data/tf_org/case_single_var/to/variables.tf rename to refactor/test_data/mv/case_single_var/to/variables.tf diff --git a/refactor/test_helper.go b/refactor/test_helper.go new file mode 100644 index 0000000..c19c634 --- /dev/null +++ b/refactor/test_helper.go @@ -0,0 +1,97 @@ +package refactor + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func fileNames(vs []os.FileInfo) []string { + vsm := make([]string, len(vs)) + for i, v := range vs { + vsm[i] = v.Name() + } + return vsm +} + +// assertMockCmd is a high-level test helper to run a given mock command with +// arguments and check if an error and its stdout are expected. +func assertMockCmdFileOutput(t *testing.T, name string, from string, to string, plan *UpdatePlan) { + expectedFiles := []string{} + err := filepath.Walk(to, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Ext(info.Name()) == ".tf" { + rel := relativise(to, path) + expectedFiles = append(expectedFiles, rel) + } + return nil + }) + if err != nil { + log.Println(err) + } + startingFiles, err := ioutil.ReadDir(from) + + filenameToContents := make(map[string]string) + for _, file := range startingFiles { + filePath := filepath.Join(from, file.Name()) + buf, err := ioutil.ReadFile(filePath) + if err != nil { + log.Fatalf("Loading %v - %v", filePath, err) + } + filenameToContents[file.Name()] = string(buf) + } + for _, update := range plan.FileUpdates { + filenameToContents[relativise(from, update.Filename)] = update.AfterText + } + + if len(expectedFiles) != len(filenameToContents) { + actualFilenames := []string{} + for k := range filenameToContents { + actualFilenames = append(actualFilenames, k) + } + t.Fatalf("Expected files to be %v, but found %v", expectedFiles, actualFilenames) + } + for _, file := range expectedFiles { + if actualContents, ok := filenameToContents[file]; ok { + assertFileHasContents(t, filepath.Join(to, file), actualContents) + } else { + t.Fatalf("Didn't find found %v in:\n%v", file, filenameToContents) + } + } +} + +func relativise(from, name string) string { + absUpdatePath, _ := filepath.Abs(name) + absFromPath, _ := filepath.Abs(from) + relUpdatePath, _ := filepath.Rel(absFromPath, absUpdatePath) + return relUpdatePath +} + +func assertFileHasContents(t *testing.T, expectedFile string, actualContents string) { + expectedBuf, err := ioutil.ReadFile(expectedFile) + if err != nil { + log.Fatalf("Loading %v - %v", expectedFile, err) + } + expected := strings.TrimSpace(string(normalizeNewlines(expectedBuf))) + actual := strings.TrimSpace(string(normalizeNewlines([]byte(actualContents)))) + assert.Equal(t, expected, actual, "File %v", expectedFile) +} + +// NormalizeNewlines normalizes \r\n (windows) and \r (mac) +// into \n (unix) +func normalizeNewlines(d []byte) []byte { + // replace CR LF \r\n (windows) with LF \n (unix) + d = bytes.Replace(d, []byte{13, 10}, []byte{10}, -1) + // replace CF \r (mac) with LF \n (unix) + d = bytes.Replace(d, []byte{13}, []byte{10}, -1) + return d +}