github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/fstest/fstest.go (about)

     1  // Package fstest defines filesystem test cases that help validate host
     2  // functions implementing WASI and `GOOS=js GOARCH=wasm`. Tests are defined
     3  // here to reduce duplication and drift.
     4  //
     5  // Here's an example using this inside code that compiles to wasm.
     6  //
     7  //	if err := fstest.WriteTestFiles(tmpDir); err != nil {
     8  //		log.Panicln(err)
     9  //	}
    10  //	if err := fstest.TestFS(os.DirFS(tmpDir)); err != nil {
    11  //		log.Panicln(err)
    12  //	}
    13  //
    14  // Failures found here should result in new tests in the appropriate package,
    15  // for example, gojs, sysfs or wasi_snapshot_preview1.
    16  //
    17  // This package must have no dependencies. Otherwise, compiling this with
    18  // TinyGo or `GOOS=js GOARCH=wasm` can become bloated or complicated.
    19  package fstest
    20  
    21  import (
    22  	"io/fs"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"runtime"
    27  	"testing/fstest"
    28  	"time"
    29  )
    30  
    31  var files = []struct {
    32  	name string
    33  	file *fstest.MapFile
    34  }{
    35  	{
    36  		name: ".", // is defined only for the sake of assigning mtim.
    37  		file: &fstest.MapFile{
    38  			Mode:    fs.ModeDir | 0o755,
    39  			ModTime: time.Unix(1609459200, 0),
    40  		},
    41  	},
    42  	{name: "empty.txt", file: &fstest.MapFile{Mode: 0o600}},
    43  	{name: "emptydir", file: &fstest.MapFile{Mode: fs.ModeDir | 0o755}},
    44  	{name: "animals.txt", file: &fstest.MapFile{Data: []byte(`bear
    45  cat
    46  shark
    47  dinosaur
    48  human
    49  `), Mode: 0o644, ModTime: time.Unix(1667482413, 0)}},
    50  	{name: "sub", file: &fstest.MapFile{
    51  		Mode:    fs.ModeDir | 0o755,
    52  		ModTime: time.Unix(1640995200, 0),
    53  	}},
    54  	{name: "sub/test.txt", file: &fstest.MapFile{
    55  		Data:    []byte("greet sub dir\n"),
    56  		Mode:    0o444,
    57  		ModTime: time.Unix(1672531200, 0),
    58  	}},
    59  	{name: "dir", file: &fstest.MapFile{Mode: fs.ModeDir | 0o755}},    // for readDir tests...
    60  	{name: "dir/-", file: &fstest.MapFile{Mode: 0o400}},               // len = 24+1 = 25
    61  	{name: "dir/a-", file: &fstest.MapFile{Mode: fs.ModeDir | 0o755}}, // len = 24+2 = 26
    62  	{name: "dir/ab-", file: &fstest.MapFile{Mode: 0o400}},             // len = 24+3 = 27
    63  }
    64  
    65  // FS includes all test files.
    66  var FS = func() fstest.MapFS {
    67  	testFS := make(fstest.MapFS, len(files))
    68  	for _, nf := range files {
    69  		testFS[nf.name] = nf.file
    70  	}
    71  	return testFS
    72  }()
    73  
    74  // WriteTestFiles writes files defined in FS to the given directory.
    75  // This is used for implementations like os.DirFS.
    76  func WriteTestFiles(tmpDir string) (err error) {
    77  	// Don't use a map as the iteration order is inconsistent and can result in
    78  	// files created prior to their directories.
    79  	for _, nf := range files {
    80  		if err = writeTestFile(tmpDir, nf.name, nf.file); err != nil {
    81  			return
    82  		}
    83  	}
    84  
    85  	// The below is similar to code in os_test.go. In summary, Windows mtime
    86  	// can be inconsistent between DirEntry.Info and File.Stat. The latter is
    87  	// authoritative (via GetFileInformationByHandle), but the former can be
    88  	// out of sync (via FindFirstFile). Explicitly calling os.Chtimes syncs
    89  	// these. See golang.org/issues/42637.
    90  	if runtime.GOOS == "windows" {
    91  		return filepath.WalkDir(tmpDir, func(path string, d fs.DirEntry, err error) error {
    92  			if err != nil {
    93  				return err
    94  			}
    95  			info, err := d.Info()
    96  			if err != nil {
    97  				return err
    98  			}
    99  
   100  			// os.Stat uses GetFileInformationByHandle internally.
   101  			st, err := os.Stat(path)
   102  			if err != nil {
   103  				return err
   104  			} else if st.ModTime() == info.ModTime() {
   105  				return nil // synced!
   106  			}
   107  
   108  			// Otherwise, we need to sync the timestamps.
   109  			atimeNsec, mtimeNsec := timesFromFileInfo(st)
   110  			return os.Chtimes(path, time.Unix(0, atimeNsec), time.Unix(0, mtimeNsec))
   111  		})
   112  	}
   113  	return
   114  }
   115  
   116  // TestFS runs fstest.TestFS on the given input which is either FS or includes
   117  // files written by WriteTestFiles.
   118  func TestFS(testfs fs.FS) error {
   119  	expected := make([]string, 0, len(files))
   120  	for _, nf := range files[1:] { // skip "."
   121  		expected = append(expected, nf.name)
   122  	}
   123  	return fstest.TestFS(testfs, expected...)
   124  }
   125  
   126  var defaultTime = time.Unix(1577836800, 0)
   127  
   128  func writeTestFile(tmpDir, name string, file *fstest.MapFile) (err error) {
   129  	fullPath := path.Join(tmpDir, name)
   130  	if mode := file.Mode; mode&fs.ModeDir != 0 {
   131  		if name != "." {
   132  			err = os.Mkdir(fullPath, mode)
   133  		}
   134  	} else {
   135  		err = os.WriteFile(fullPath, file.Data, mode)
   136  	}
   137  
   138  	if err != nil {
   139  		return
   140  	}
   141  
   142  	mtim := file.ModTime
   143  	if mtim.Unix() == 0 {
   144  		mtim = defaultTime
   145  	}
   146  	return os.Chtimes(fullPath, mtim, mtim)
   147  }