go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/output_test.go (about) 1 // Copyright 2019 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package lucicfg 16 17 import ( 18 "context" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "testing" 23 24 . "github.com/smartystreets/goconvey/convey" 25 ) 26 27 func TestOutput(t *testing.T) { 28 t.Parallel() 29 30 ctx := context.Background() 31 32 Convey("With temp dir", t, func() { 33 tmp, err := ioutil.TempDir("", "lucicfg") 34 So(err, ShouldBeNil) 35 defer os.RemoveAll(tmp) 36 37 path := func(p string) string { 38 return filepath.Join(tmp, filepath.FromSlash(p)) 39 } 40 41 read := func(p string) string { 42 body, err := os.ReadFile(path(p)) 43 So(err, ShouldBeNil) 44 return string(body) 45 } 46 47 write := func(p, body string) { 48 So(os.WriteFile(path(p), []byte(body), 0600), ShouldBeNil) 49 } 50 51 original := map[string][]byte{ 52 "a.cfg": []byte("a\n"), 53 "subdir/b.cfg": []byte("b\n"), 54 } 55 So(os.Mkdir(path("subdir"), 0700), ShouldBeNil) 56 for k, v := range original { 57 write(k, string(v)) 58 } 59 60 Convey("Writing", func() { 61 out := Output{ 62 Data: map[string]Datum{ 63 "a": BlobDatum("111"), 64 "dir/a": BlobDatum("222"), 65 }, 66 } 67 changed, unchanged, err := out.Write(tmp, false) 68 So(changed, ShouldResemble, []string{"a", "dir/a"}) 69 So(unchanged, ShouldHaveLength, 0) 70 So(err, ShouldBeNil) 71 72 So(read("a"), ShouldResemble, "111") 73 So(read("dir/a"), ShouldResemble, "222") 74 75 out.Data["a"] = BlobDatum("333") 76 changed, unchanged, err = out.Write(tmp, false) 77 So(changed, ShouldResemble, []string{"a"}) 78 So(unchanged, ShouldResemble, []string{"dir/a"}) 79 So(err, ShouldBeNil) 80 81 So(read("a"), ShouldResemble, "333") 82 }) 83 84 Convey("DiscardChangesToUntracked", func() { 85 generated := func() Output { 86 return Output{ 87 Data: map[string]Datum{ 88 "a.cfg": BlobDatum("new a\n"), 89 "subdir/b.cfg": BlobDatum("new b\n"), 90 }, 91 } 92 } 93 94 Convey("No untracked", func() { 95 out := generated() 96 So(out.DiscardChangesToUntracked(ctx, []string{"**/*"}, "-"), ShouldBeNil) 97 So(out.Data, ShouldResemble, generated().Data) 98 }) 99 100 Convey("Untracked files are restored from disk", func() { 101 out := generated() 102 So(out.DiscardChangesToUntracked(ctx, []string{"!*/b.cfg"}, tmp), ShouldBeNil) 103 So(out.Data, ShouldResemble, map[string]Datum{ 104 "a.cfg": generated().Data["a.cfg"], 105 "subdir/b.cfg": BlobDatum(original["subdir/b.cfg"]), 106 }) 107 }) 108 109 Convey("Untracked files are discarded when dumping to stdout", func() { 110 out := generated() 111 So(out.DiscardChangesToUntracked(ctx, []string{"!*/b.cfg"}, "-"), ShouldBeNil) 112 So(out.Data, ShouldResemble, map[string]Datum{ 113 "a.cfg": generated().Data["a.cfg"], 114 }) 115 }) 116 117 Convey("Untracked files are discarded if don't exist on disk", func() { 118 out := Output{ 119 Data: map[string]Datum{ 120 "c.cfg": BlobDatum("generated"), 121 }, 122 } 123 So(out.DiscardChangesToUntracked(ctx, []string{"!c.cfg"}, tmp), ShouldBeNil) 124 So(out.Data, ShouldHaveLength, 0) 125 }) 126 }) 127 128 Convey("Reading", func() { 129 out := Output{ 130 Data: map[string]Datum{ 131 "m1": BlobDatum("111"), 132 "m2": BlobDatum("222"), 133 }, 134 } 135 136 Convey("Success", func() { 137 write("m1", "new 1") 138 write("m2", "new 2") 139 140 So(out.Read(tmp), ShouldBeNil) 141 So(out.Data, ShouldResemble, map[string]Datum{ 142 "m1": BlobDatum("new 1"), 143 "m2": BlobDatum("new 2"), 144 }) 145 }) 146 147 Convey("Missing file", func() { 148 write("m1", "new 1") 149 So(out.Read(tmp), ShouldNotBeNil) 150 }) 151 }) 152 153 Convey("Compares protos semantically", func() { 154 // Write the initial version. 155 out := Output{ 156 Data: map[string]Datum{ 157 "m1": &MessageDatum{Header: "", Message: testMessage(111, 0)}, 158 "m2": &MessageDatum{Header: "# Header\n", Message: testMessage(222, 0)}, 159 }, 160 } 161 changed, unchanged, err := out.Write(tmp, false) 162 So(changed, ShouldResemble, []string{"m1", "m2"}) 163 So(unchanged, ShouldHaveLength, 0) 164 So(err, ShouldBeNil) 165 166 So(read("m1"), ShouldResemble, "i: 111\n") 167 So(read("m2"), ShouldResemble, "# Header\ni: 222\n") 168 169 Convey("Ignores formatting", func() { 170 // Mutate m2 in insignificant way (strip the header). 171 write("m2", "i: 222") 172 173 // If using semantic comparison, recognizes nothing has changed. 174 cmp, err := out.Compare(tmp, true) 175 So(err, ShouldBeNil) 176 So(cmp, ShouldResemble, map[string]CompareResult{ 177 "m1": Identical, 178 "m2": SemanticallyEqual, 179 }) 180 181 // Byte-to-byte comparison recognizes the change. 182 cmp, err = out.Compare(tmp, false) 183 So(err, ShouldBeNil) 184 So(cmp, ShouldResemble, map[string]CompareResult{ 185 "m1": Identical, 186 "m2": Different, 187 }) 188 189 Convey("Write, force=false", func() { 190 // Output didn't really change, so nothing is overwritten. 191 changed, unchanged, err := out.Write(tmp, false) 192 So(err, ShouldBeNil) 193 So(changed, ShouldHaveLength, 0) 194 So(unchanged, ShouldResemble, []string{"m1", "m2"}) 195 }) 196 197 Convey("Write, force=true", func() { 198 // We ask to overwrite files even if they all are semantically same. 199 changed, unchanged, err := out.Write(tmp, true) 200 So(err, ShouldBeNil) 201 So(changed, ShouldResemble, []string{"m2"}) 202 So(unchanged, ShouldResemble, []string{"m1"}) 203 204 // Overwrote it on disk. 205 So(read("m2"), ShouldResemble, "# Header\ni: 222\n") 206 }) 207 }) 208 209 Convey("Detects real changes", func() { 210 // Overwrite m2 with something semantically different. 211 write("m2", "i: 333") 212 213 // Detected it. 214 cmp, err := out.Compare(tmp, true) 215 So(err, ShouldBeNil) 216 So(cmp, ShouldResemble, map[string]CompareResult{ 217 "m1": Identical, 218 "m2": Different, 219 }) 220 221 // Writes it to disk, even when force=false. 222 changed, unchanged, err := out.Write(tmp, false) 223 So(err, ShouldBeNil) 224 So(changed, ShouldResemble, []string{"m2"}) 225 So(unchanged, ShouldResemble, []string{"m1"}) 226 }) 227 228 Convey("Handles bad protos", func() { 229 // Overwrite m2 with some garbage. 230 write("m2", "not a text proto") 231 232 // Detected the file as changed. 233 cmp, err := out.Compare(tmp, true) 234 So(err, ShouldBeNil) 235 So(cmp, ShouldResemble, map[string]CompareResult{ 236 "m1": Identical, 237 "m2": Different, 238 }) 239 }) 240 }) 241 }) 242 243 Convey("ConfigSets", t, func() { 244 out := Output{ 245 Data: map[string]Datum{ 246 "f1": BlobDatum("0"), 247 "dir1/f2": BlobDatum("1"), 248 "dir1/f3": BlobDatum("2"), 249 "dir1/sub/f4": BlobDatum("3"), 250 "dir2/f5": BlobDatum("4"), 251 }, 252 Roots: map[string]string{}, 253 } 254 255 // Same data, as raw bytes. 256 everything := map[string][]byte{} 257 for k, v := range out.Data { 258 everything[k], _ = v.Bytes() 259 } 260 261 configSets := func() []ConfigSet { 262 cs, err := out.ConfigSets() 263 So(err, ShouldBeNil) 264 return cs 265 } 266 267 Convey("No roots", func() { 268 So(configSets(), ShouldHaveLength, 0) 269 }) 270 271 Convey("Empty set", func() { 272 out.Roots["set"] = "zzz" 273 So(configSets(), ShouldResemble, []ConfigSet{ 274 { 275 Name: "set", 276 Data: map[string][]byte{}, 277 }, 278 }) 279 }) 280 281 Convey("`.` root", func() { 282 out.Roots["set"] = "." 283 So(configSets(), ShouldResemble, []ConfigSet{ 284 { 285 Name: "set", 286 Data: everything, 287 }, 288 }) 289 }) 290 291 Convey("Subdir root", func() { 292 out.Roots["set"] = "dir1/." 293 So(configSets(), ShouldResemble, []ConfigSet{ 294 { 295 Name: "set", 296 Data: map[string][]byte{ 297 "f2": []byte("1"), 298 "f3": []byte("2"), 299 "sub/f4": []byte("3"), 300 }, 301 }, 302 }) 303 }) 304 305 Convey("Multiple roots", func() { 306 out.Roots["set1"] = "dir1" 307 out.Roots["set2"] = "dir2" 308 out.Roots["set3"] = "dir1/sub" // intersecting sets are OK 309 So(configSets(), ShouldResemble, []ConfigSet{ 310 { 311 Name: "set1", 312 Data: map[string][]byte{ 313 "f2": []byte("1"), 314 "f3": []byte("2"), 315 "sub/f4": []byte("3"), 316 }, 317 }, 318 { 319 Name: "set2", 320 Data: map[string][]byte{ 321 "f5": []byte("4"), 322 }, 323 }, 324 { 325 Name: "set3", 326 Data: map[string][]byte{ 327 "f4": []byte("3"), 328 }, 329 }, 330 }) 331 }) 332 }) 333 }