github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/depsfile/locks_file_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package depsfile 5 6 import ( 7 "bufio" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "strings" 12 "testing" 13 14 "github.com/google/go-cmp/cmp" 15 "github.com/terramate-io/tf/addrs" 16 "github.com/terramate-io/tf/getproviders" 17 "github.com/terramate-io/tf/tfdiags" 18 ) 19 20 func TestLoadLocksFromFile(t *testing.T) { 21 // For ease of test maintenance we treat every file under 22 // test-data/locks-files as a test case which is subject 23 // at least to testing that it produces an expected set 24 // of diagnostics represented via specially-formatted comments 25 // in the fixture files (which might be the empty set, if 26 // there are no such comments). 27 // 28 // Some of the files also have additional assertions that 29 // are encoded in the test code below. These must pass 30 // in addition to the standard diagnostics tests, if present. 31 files, err := ioutil.ReadDir("testdata/locks-files") 32 if err != nil { 33 t.Fatal(err.Error()) 34 } 35 36 for _, info := range files { 37 testName := filepath.Base(info.Name()) 38 filename := filepath.Join("testdata/locks-files", testName) 39 t.Run(testName, func(t *testing.T) { 40 f, err := os.Open(filename) 41 if err != nil { 42 t.Fatal(err.Error()) 43 } 44 defer f.Close() 45 const errorPrefix = "# ERROR: " 46 const warningPrefix = "# WARNING: " 47 wantErrors := map[int]string{} 48 wantWarnings := map[int]string{} 49 sc := bufio.NewScanner(f) 50 lineNum := 1 51 for sc.Scan() { 52 l := sc.Text() 53 if pos := strings.Index(l, errorPrefix); pos != -1 { 54 wantSummary := l[pos+len(errorPrefix):] 55 wantErrors[lineNum] = wantSummary 56 } 57 if pos := strings.Index(l, warningPrefix); pos != -1 { 58 wantSummary := l[pos+len(warningPrefix):] 59 wantWarnings[lineNum] = wantSummary 60 } 61 lineNum++ 62 } 63 if err := sc.Err(); err != nil { 64 t.Fatal(err.Error()) 65 } 66 67 locks, diags := LoadLocksFromFile(filename) 68 gotErrors := map[int]string{} 69 gotWarnings := map[int]string{} 70 for _, diag := range diags { 71 summary := diag.Description().Summary 72 if diag.Source().Subject == nil { 73 // We don't expect any sourceless diagnostics here. 74 t.Errorf("unexpected sourceless diagnostic: %s", summary) 75 continue 76 } 77 lineNum := diag.Source().Subject.Start.Line 78 switch sev := diag.Severity(); sev { 79 case tfdiags.Error: 80 gotErrors[lineNum] = summary 81 case tfdiags.Warning: 82 gotWarnings[lineNum] = summary 83 default: 84 t.Errorf("unexpected diagnostic severity %s", sev) 85 } 86 } 87 88 if diff := cmp.Diff(wantErrors, gotErrors); diff != "" { 89 t.Errorf("wrong errors\n%s", diff) 90 } 91 if diff := cmp.Diff(wantWarnings, gotWarnings); diff != "" { 92 t.Errorf("wrong warnings\n%s", diff) 93 } 94 95 switch testName { 96 // These are the file-specific test assertions. Not all files 97 // need custom test assertions in addition to the standard 98 // diagnostics assertions implemented above, so the cases here 99 // don't need to be exhaustive for all files. 100 // 101 // Please keep these in alphabetical order so the list is easy 102 // to scan! 103 104 case "empty.hcl": 105 if got, want := len(locks.providers), 0; got != want { 106 t.Errorf("wrong number of providers %d; want %d", got, want) 107 } 108 109 case "valid-provider-locks.hcl": 110 if got, want := len(locks.providers), 3; got != want { 111 t.Errorf("wrong number of providers %d; want %d", got, want) 112 } 113 114 t.Run("version-only", func(t *testing.T) { 115 if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/version-only")); lock != nil { 116 if got, want := lock.Version().String(), "1.0.0"; got != want { 117 t.Errorf("wrong version\ngot: %s\nwant: %s", got, want) 118 } 119 if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ""; got != want { 120 t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) 121 } 122 if got, want := len(lock.hashes), 0; got != want { 123 t.Errorf("wrong number of hashes %d; want %d", got, want) 124 } 125 } 126 }) 127 128 t.Run("version-and-constraints", func(t *testing.T) { 129 if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/version-and-constraints")); lock != nil { 130 if got, want := lock.Version().String(), "1.2.0"; got != want { 131 t.Errorf("wrong version\ngot: %s\nwant: %s", got, want) 132 } 133 if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), "~> 1.2"; got != want { 134 t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) 135 } 136 if got, want := len(lock.hashes), 0; got != want { 137 t.Errorf("wrong number of hashes %d; want %d", got, want) 138 } 139 } 140 }) 141 142 t.Run("all-the-things", func(t *testing.T) { 143 if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/all-the-things")); lock != nil { 144 if got, want := lock.Version().String(), "3.0.10"; got != want { 145 t.Errorf("wrong version\ngot: %s\nwant: %s", got, want) 146 } 147 if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ">= 3.0.2"; got != want { 148 t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) 149 } 150 wantHashes := []getproviders.Hash{ 151 getproviders.MustParseHash("test:placeholder-hash-1"), 152 getproviders.MustParseHash("test:placeholder-hash-2"), 153 getproviders.MustParseHash("test:placeholder-hash-3"), 154 } 155 if diff := cmp.Diff(wantHashes, lock.hashes); diff != "" { 156 t.Errorf("wrong hashes\n%s", diff) 157 } 158 } 159 }) 160 } 161 }) 162 } 163 } 164 165 func TestLoadLocksFromFileAbsent(t *testing.T) { 166 t.Run("lock file is a directory", func(t *testing.T) { 167 // This can never happen when Terraform is the one generating the 168 // lock file, but might arise if the user makes a directory with the 169 // lock file's name for some reason. (There is no actual reason to do 170 // so, so that would always be a mistake.) 171 locks, diags := LoadLocksFromFile("testdata") 172 if len(locks.providers) != 0 { 173 t.Errorf("returned locks has providers; expected empty locks") 174 } 175 if !diags.HasErrors() { 176 t.Fatalf("LoadLocksFromFile succeeded; want error") 177 } 178 // This is a generic error message from HCL itself, so upgrading HCL 179 // in future might cause a different error message here. 180 want := `Failed to read file: The configuration file "testdata" could not be read.` 181 got := diags.Err().Error() 182 if got != want { 183 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 184 } 185 }) 186 t.Run("lock file doesn't exist", func(t *testing.T) { 187 locks, diags := LoadLocksFromFile("testdata/nonexist.hcl") 188 if len(locks.providers) != 0 { 189 t.Errorf("returned locks has providers; expected empty locks") 190 } 191 if !diags.HasErrors() { 192 t.Fatalf("LoadLocksFromFile succeeded; want error") 193 } 194 // This is a generic error message from HCL itself, so upgrading HCL 195 // in future might cause a different error message here. 196 want := `Failed to read file: The configuration file "testdata/nonexist.hcl" could not be read.` 197 got := diags.Err().Error() 198 if got != want { 199 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 200 } 201 }) 202 } 203 204 func TestSaveLocksToFile(t *testing.T) { 205 locks := NewLocks() 206 207 fooProvider := addrs.MustParseProviderSourceString("test/foo") 208 barProvider := addrs.MustParseProviderSourceString("test/bar") 209 bazProvider := addrs.MustParseProviderSourceString("test/baz") 210 booProvider := addrs.MustParseProviderSourceString("test/boo") 211 oneDotOh := getproviders.MustParseVersion("1.0.0") 212 oneDotTwo := getproviders.MustParseVersion("1.2.0") 213 atLeastOneDotOh := getproviders.MustParseVersionConstraints(">= 1.0.0") 214 pessimisticOneDotOh := getproviders.MustParseVersionConstraints("~> 1") 215 abbreviatedOneDotTwo := getproviders.MustParseVersionConstraints("1.2") 216 hashes := []getproviders.Hash{ 217 getproviders.MustParseHash("test:cccccccccccccccccccccccccccccccccccccccccccccccc"), 218 getproviders.MustParseHash("test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), 219 getproviders.MustParseHash("test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 220 } 221 locks.SetProvider(fooProvider, oneDotOh, atLeastOneDotOh, hashes) 222 locks.SetProvider(barProvider, oneDotTwo, pessimisticOneDotOh, nil) 223 locks.SetProvider(bazProvider, oneDotTwo, nil, nil) 224 locks.SetProvider(booProvider, oneDotTwo, abbreviatedOneDotTwo, nil) 225 226 dir := t.TempDir() 227 228 filename := filepath.Join(dir, LockFilePath) 229 diags := SaveLocksToFile(locks, filename) 230 if diags.HasErrors() { 231 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 232 } 233 234 fileInfo, err := os.Stat(filename) 235 if err != nil { 236 t.Fatalf(err.Error()) 237 } 238 if mode := fileInfo.Mode(); mode&0111 != 0 { 239 t.Fatalf("Expected lock file to be non-executable: %o", mode) 240 } 241 242 gotContentBytes, err := ioutil.ReadFile(filename) 243 if err != nil { 244 t.Fatalf(err.Error()) 245 } 246 gotContent := string(gotContentBytes) 247 wantContent := `# This file is maintained automatically by "terraform init". 248 # Manual edits may be lost in future updates. 249 250 provider "registry.terraform.io/test/bar" { 251 version = "1.2.0" 252 constraints = "~> 1.0" 253 } 254 255 provider "registry.terraform.io/test/baz" { 256 version = "1.2.0" 257 } 258 259 provider "registry.terraform.io/test/boo" { 260 version = "1.2.0" 261 constraints = "1.2.0" 262 } 263 264 provider "registry.terraform.io/test/foo" { 265 version = "1.0.0" 266 constraints = ">= 1.0.0" 267 hashes = [ 268 "test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 269 "test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 270 "test:cccccccccccccccccccccccccccccccccccccccccccccccc", 271 ] 272 } 273 ` 274 if diff := cmp.Diff(wantContent, gotContent); diff != "" { 275 t.Errorf("wrong result\n%s", diff) 276 } 277 }