github.com/wtsi-ssg/wrstat/v4@v4.5.1/neaten/neaten_test.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2022 Genome Research Ltd.
     3   *
     4   * Author: Sendu Bala <sb10@sanger.ac.uk>
     5   * Author: Kyle Mace <km34@sanger.ac.uk>
     6   *
     7   * Permission is hereby granted, free of charge, to any person obtaining
     8   * a copy of this software and associated documentation files (the
     9   * "Software"), to deal in the Software without restriction, including
    10   * without limitation the rights to use, copy, modify, merge, publish,
    11   * distribute, sublicense, and/or sell copies of the Software, and to
    12   * permit persons to whom the Software is furnished to do so, subject to
    13   * the following conditions:
    14   *
    15   * The above copyright notice and this permission notice shall be included
    16   * in all copies or substantial portions of the Software.
    17   *
    18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    19   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    20   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    21   * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    22   * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    23   * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    24   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    25   ******************************************************************************/
    26  
    27  package neaten
    28  
    29  import (
    30  	"io/fs"
    31  	"os"
    32  	"path/filepath"
    33  	"syscall"
    34  	"testing"
    35  	"time"
    36  
    37  	. "github.com/smartystreets/goconvey/convey"
    38  )
    39  
    40  const modePermUser = 0770
    41  
    42  func TestTidy(t *testing.T) { //nolint:gocognit
    43  	date := "20220829"
    44  	srcUniversal := "cci4fafnu1ia052l75sg"
    45  	srcUniqueGo := "cci4fafnu1ia052l75t0"
    46  	srcUniquePerl := "cci4fafnu1ia052l75tg"
    47  
    48  	Convey("Given existing source and dest dirs you can tidy the source", t, func() {
    49  		tmpDir := t.TempDir()
    50  		srcDir := filepath.Join(tmpDir, "src", srcUniversal)
    51  		destDir := filepath.Join(tmpDir, "dest", "final")
    52  		interestUniqueDir1 := createTestPath([]string{srcDir, "go", srcUniqueGo})
    53  		interestUniqueDir2 := createTestPath([]string{srcDir, "perl", srcUniquePerl})
    54  
    55  		combineSuffixes, dbSuffixes, baseSuffixes := buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl,
    56  			interestUniqueDir1, interestUniqueDir2)
    57  
    58  		createTestDirWithDifferentPerms(destDir)
    59  
    60  		test := Tidy{
    61  			SrcDir:  srcDir,
    62  			DestDir: destDir,
    63  			Date:    date,
    64  
    65  			CombineFileSuffixes: combineSuffixes,
    66  			DBFileSuffixes:      dbSuffixes,
    67  			BaseFileSuffixes:    baseSuffixes,
    68  
    69  			CombineFileGlobPattern:  "%s/*/*/%s",
    70  			DBFileGlobPattern:       "%s/*/*/%s",
    71  			WalkFilePathGlobPattern: "%s/*/*/*%s",
    72  
    73  			DestDirPerms: modePermUser,
    74  		}
    75  
    76  		disableDeletion := false
    77  
    78  		err := test.Up(disableDeletion)
    79  
    80  		Convey("And the combine files are moved from the source dir to the dest dir", func() {
    81  			combineFileSuffixes := [4]string{".logs.gz", ".byusergroup.gz", ".bygroup", ".stats.gz"}
    82  
    83  			for _, suffix := range combineFileSuffixes {
    84  				final1 := filepath.Join(destDir, date+"_go."+srcUniqueGo+"."+srcUniversal+suffix)
    85  				_, err = os.Stat(final1)
    86  				So(err, ShouldBeNil)
    87  
    88  				final2 := filepath.Join(destDir, date+"_perl."+srcUniquePerl+"."+srcUniversal+suffix)
    89  				_, err = os.Stat(final2)
    90  				So(err, ShouldBeNil)
    91  			}
    92  		})
    93  
    94  		Convey("And the contents of the .basedirs and .dgut.dbs dir exist", func() {
    95  			dbsPath := filepath.Join(destDir, date+"_"+srcUniversal)
    96  			dbsSuffixes := [5]string{
    97  				".basedirs",
    98  				".dgut.dbs/0/dgut.db",
    99  				".dgut.dbs/0/dgut.db.children",
   100  				".dgut.dbs/1/dgut.db",
   101  				".dgut.dbs/1/dgut.db.children"}
   102  
   103  			for _, suffix := range dbsSuffixes {
   104  				_, err = os.Stat(dbsPath + suffix)
   105  				So(err, ShouldBeNil)
   106  			}
   107  		})
   108  
   109  		Convey("And the .dgut.dbs.updated file exists in the dest dir", func() {
   110  			expectedFileName := filepath.Join(destDir, ".dgut.dbs.updated")
   111  
   112  			_, err = os.Stat(expectedFileName)
   113  			So(err, ShouldBeNil)
   114  		})
   115  
   116  		Convey("And the mtime of the .dgut.dbs file matches the oldest mtime of the walk log files", func() {
   117  			err = os.RemoveAll(tmpDir)
   118  			So(err, ShouldBeNil)
   119  
   120  			buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl, interestUniqueDir1, interestUniqueDir2)
   121  
   122  			createTestDirWithDifferentPerms(destDir)
   123  
   124  			newMtimeFile := filepath.Join(interestUniqueDir1, "walk.1.log")
   125  			expectedMTime := time.Date(2006, time.April, 1, 3, 4, 5, 0, time.UTC)
   126  			expectedATime := time.Date(2007, time.March, 2, 4, 5, 6, 0, time.UTC)
   127  
   128  			err = os.Chtimes(newMtimeFile, expectedATime, expectedMTime)
   129  			So(err, ShouldBeNil)
   130  
   131  			err = test.Up(disableDeletion)
   132  			So(err, ShouldBeNil)
   133  
   134  			dbsFileMTime := getMTime(filepath.Join(destDir, ".dgut.dbs.updated"))
   135  
   136  			So(dbsFileMTime, ShouldEqual, expectedMTime)
   137  		})
   138  
   139  		Convey("And the moved file permissions match those of the dest dir", func() {
   140  			destDirPerm, errs := os.Stat(destDir)
   141  			So(errs, ShouldBeNil)
   142  
   143  			err = filepath.WalkDir(destDir, func(path string, d fs.DirEntry, err error) error {
   144  				if err != nil {
   145  					return err
   146  				}
   147  
   148  				pathPerm, err := os.Stat(path)
   149  				if err != nil {
   150  					return err
   151  				}
   152  
   153  				So(permissionsAndOwnershipSame(destDirPerm, pathPerm), ShouldBeTrue)
   154  
   155  				return nil
   156  			})
   157  			So(err, ShouldBeNil)
   158  		})
   159  
   160  		Convey("And up deletes the source directory after the files have been moved", func() {
   161  			err = os.RemoveAll(tmpDir)
   162  			So(err, ShouldBeNil)
   163  
   164  			buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl, interestUniqueDir1, interestUniqueDir2)
   165  
   166  			createTestDirWithDifferentPerms(destDir)
   167  
   168  			err = test.Up(disableDeletion)
   169  			So(err, ShouldBeNil)
   170  
   171  			_, err = os.Stat(srcDir)
   172  			So(err, ShouldNotBeNil)
   173  		})
   174  
   175  		Convey("And up does not delete the source directory after the files have been moved if the arg is false", func() {
   176  			err = os.RemoveAll(tmpDir)
   177  			So(err, ShouldBeNil)
   178  
   179  			buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl, interestUniqueDir1, interestUniqueDir2)
   180  
   181  			createTestDirWithDifferentPerms(destDir)
   182  
   183  			err = test.Up(true)
   184  			So(err, ShouldBeNil)
   185  
   186  			_, err = os.Stat(srcDir)
   187  			So(err, ShouldBeNil)
   188  		})
   189  
   190  		Convey("And it also works if the dest dir doesn't exist", func() {
   191  			err := os.RemoveAll(destDir)
   192  			So(err, ShouldBeNil)
   193  
   194  			err = os.RemoveAll(srcDir)
   195  			So(err, ShouldBeNil)
   196  
   197  			buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl, interestUniqueDir1, interestUniqueDir2)
   198  
   199  			err = test.Up(disableDeletion)
   200  			So(err, ShouldBeNil)
   201  
   202  			_, err = os.Stat(destDir)
   203  			So(err, ShouldBeNil)
   204  		})
   205  
   206  		Convey("And it doesn't work if source dir doesn't exist", func() {
   207  			err := os.RemoveAll(srcDir)
   208  			So(err, ShouldBeNil)
   209  
   210  			err = test.Up(disableDeletion)
   211  			So(err, ShouldNotBeNil)
   212  
   213  			_, err = os.Stat(srcDir)
   214  			So(err, ShouldNotBeNil)
   215  		})
   216  
   217  		Convey("And it doesn't work if source or dest is an incorrect relative path", func() {
   218  			err := os.RemoveAll(destDir)
   219  			So(err, ShouldBeNil)
   220  
   221  			err = os.RemoveAll(srcDir)
   222  			So(err, ShouldBeNil)
   223  
   224  			buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl, interestUniqueDir1, interestUniqueDir2)
   225  
   226  			relDir := filepath.Join(tmpDir, "rel")
   227  			err = os.MkdirAll(relDir, modePermUser)
   228  			So(err, ShouldBeNil)
   229  
   230  			err = os.Chdir(relDir)
   231  			So(err, ShouldBeNil)
   232  
   233  			err = os.RemoveAll(relDir)
   234  			So(err, ShouldBeNil)
   235  
   236  			test.SrcDir = "../src/" + srcUniversal
   237  			test.DestDir = "../dest/final"
   238  
   239  			err = test.Up(disableDeletion)
   240  			So(err, ShouldNotBeNil)
   241  		})
   242  	})
   243  }
   244  
   245  func buildSrcDir(srcDir, srcUniqueGo, srcUniquePerl, interestUniqueDir1, interestUniqueDir2 string) (map[string]string,
   246  	map[string]string, map[string]string) {
   247  	walkFileSuffixes := [5]string{"log", "byusergroup", "bygroup", "stats", "dgut"}
   248  	combineFileSuffixes := [4]string{"combine.log.gz", "combine.byusergroup.gz", "combine.bygroup", "combine.stats.gz"}
   249  
   250  	for i := range walkFileSuffixes {
   251  		createTestPath([]string{interestUniqueDir1}, "walk.1."+walkFileSuffixes[i])
   252  		createTestPath([]string{interestUniqueDir2}, "walk.1."+walkFileSuffixes[i])
   253  	}
   254  
   255  	for i := range combineFileSuffixes {
   256  		createTestPath([]string{interestUniqueDir1}, combineFileSuffixes[i])
   257  		createTestPath([]string{interestUniqueDir2}, combineFileSuffixes[i])
   258  	}
   259  
   260  	goDBDir := []string{srcDir, "go", srcUniqueGo, "combine.dgut.db"}
   261  	perlDBDir := []string{srcDir, "perl", srcUniquePerl, "combine.dgut.db"}
   262  	combineDirSuffixes := [2]string{"dgut.db", "dgut.db.children"}
   263  
   264  	for i := range combineDirSuffixes {
   265  		createTestPath(goDBDir, combineDirSuffixes[i])
   266  		createTestPath(perlDBDir, combineDirSuffixes[i])
   267  	}
   268  
   269  	createTestPath([]string{srcDir}, "base.dirs")
   270  
   271  	inputOutputCombineSuffixes := map[string]string{
   272  		combineFileSuffixes[0]: "logs.gz",
   273  		combineFileSuffixes[1]: "byusergroup.gz",
   274  		combineFileSuffixes[2]: "bygroup",
   275  		combineFileSuffixes[3]: "stats.gz",
   276  	}
   277  
   278  	inputOutputDBSuffixes := map[string]string{
   279  		"combine.dgut.db": "dgut.dbs"}
   280  
   281  	inputOutputBaseSuffixes := map[string]string{
   282  		"base.dirs": "basedirs"}
   283  
   284  	return inputOutputCombineSuffixes, inputOutputDBSuffixes, inputOutputBaseSuffixes
   285  }
   286  
   287  // createTestPath takes a set of subdirectory names and an optional file
   288  // basename and creates a directory and empty file out of them. Returns the
   289  // directory.
   290  func createTestPath(dirs []string, basename ...string) string {
   291  	wholeDir := filepath.Join(dirs...)
   292  
   293  	err := os.MkdirAll(wholeDir, modePermUser)
   294  	So(err, ShouldBeNil)
   295  
   296  	if len(basename) == 1 {
   297  		err = createFile(filepath.Join(wholeDir, basename[0]))
   298  		So(err, ShouldBeNil)
   299  	}
   300  
   301  	return wholeDir
   302  }
   303  
   304  // createTestDirWithDifferentPerms creates the given directory with different
   305  // group ownership and rw permissions than normal.
   306  func createTestDirWithDifferentPerms(dir string) {
   307  	err := os.MkdirAll(dir, 0777)
   308  	So(err, ShouldBeNil)
   309  
   310  	destUID := os.Getuid()
   311  	destGroups, err := os.Getgroups()
   312  	So(err, ShouldBeNil)
   313  
   314  	err = os.Lchown(dir, destUID, destGroups[1])
   315  	So(err, ShouldBeNil)
   316  }
   317  
   318  // getMTime takes a filePath and returns its Mtime.
   319  func getMTime(filePath string) time.Time {
   320  	FileInfo, err := os.Stat(filePath)
   321  	So(err, ShouldBeNil)
   322  
   323  	fileMTime := FileInfo.ModTime()
   324  
   325  	return fileMTime
   326  }
   327  
   328  // permissionsAndOwnershipSame takes two fileinfos and returns whether their
   329  // permissions and ownerships are the same.
   330  func permissionsAndOwnershipSame(a, b fs.FileInfo) bool {
   331  	return readWritePermissionsSame(a, b) && userAndGroupOwnershipSame(a, b)
   332  }
   333  
   334  // userAndGroupOwnershipSame tests if the given fileinfos have the same UID and
   335  // GID.
   336  func userAndGroupOwnershipSame(a, b fs.FileInfo) bool {
   337  	aUID, aGID := getUIDAndGID(a)
   338  	bUID, bGID := getUIDAndGID(b)
   339  
   340  	return aUID == bUID && aGID == bGID
   341  }
   342  
   343  // matchReadWrite ensures that the given file with the current fileinfo has the
   344  // same user,group,other read&write permissions as the desired fileinfo.
   345  func readWritePermissionsSame(a, b fs.FileInfo) bool {
   346  	aRW := a.Mode() & modeRW
   347  	bRW := b.Mode() & modeRW
   348  
   349  	return aRW == bRW
   350  }
   351  
   352  func TestTouch(t *testing.T) {
   353  	Convey("Touch updates a file's a and mtime to now, in local time", t, func() {
   354  		tdir := t.TempDir()
   355  		path := filepath.Join(tdir, "file")
   356  
   357  		err := createFile(path)
   358  		So(err, ShouldBeNil)
   359  
   360  		before := time.Now().Add(-10 * time.Second)
   361  		err = os.Chtimes(path, before, before)
   362  		So(err, ShouldBeNil)
   363  
   364  		recent := time.Now()
   365  		err = Touch(path)
   366  		So(err, ShouldBeNil)
   367  
   368  		info, err := os.Stat(path)
   369  		So(err, ShouldBeNil)
   370  
   371  		a := info.Sys().(*syscall.Stat_t).Atim //nolint:forcetypeassert
   372  		atime := time.Unix(a.Sec, a.Nsec)
   373  
   374  		So(atime, ShouldHappenAfter, recent)
   375  		So(info.ModTime(), ShouldHappenAfter, recent)
   376  		So(atime, ShouldEqual, info.ModTime())
   377  	})
   378  }
   379  func TestDeleteAllPrefixedFiles(t *testing.T) {
   380  	Convey("Only files with a matching prefix should be deleted", t, func() {
   381  		tdir := t.TempDir()
   382  		for _, name := range []string{"aaa", "aab", "baa"} {
   383  			path := filepath.Join(tdir, name)
   384  			err := createFile(path)
   385  			So(err, ShouldBeNil)
   386  		}
   387  
   388  		err := DeleteAllPrefixedDirEntries(tdir, "a")
   389  		So(err, ShouldBeNil)
   390  
   391  		entries, err := os.ReadDir(tdir)
   392  		So(err, ShouldBeNil)
   393  		So(len(entries), ShouldEqual, 1)
   394  		So(entries[0].Name(), ShouldEqual, "baa")
   395  	})
   396  }