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  }