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