| // Copyright 2025 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| package oswrapper | |
| import ( | |
| "io" | |
| "io/fs" | |
| "os" | |
| "regexp" | |
| "testing" | |
| "time" | |
| "github.com/stretchr/testify/require" | |
| ) | |
| func TestUsableAsOsWrapper(t *testing.T) { | |
| f := func(osWrapper OSWrapper) { | |
| _ = osWrapper.Environ() | |
| } | |
| wrapper := CreateMemMapOSWrapper() | |
| f(wrapper) | |
| } | |
| func TestEnviron(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| environ := wrapper.Environ() | |
| require.Equal(t, []string{}, environ) | |
| wrapper.MemMapEnvironProvider.Environment = map[string]string{ | |
| "HOME": "/tmp", | |
| "PWD": "/local", | |
| } | |
| environ = wrapper.Environ() | |
| require.Equal(t, []string{"HOME=/tmp", "PWD=/local"}, environ) | |
| } | |
| func TestGetenv(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| require.Equal(t, "", wrapper.Getenv("HOME")) | |
| wrapper.MemMapEnvironProvider.Environment = map[string]string{ | |
| "HOME": "/tmp", | |
| } | |
| require.Equal(t, "/tmp", wrapper.Getenv("HOME")) | |
| } | |
| func TestGetwd(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| wd, err := wrapper.Getwd() | |
| require.NoErrorf(t, err, "Failed to get wd: %v", err) | |
| require.Equal(t, "/", wd) | |
| wrapper.MemMapEnvironProvider.Environment = map[string]string{ | |
| "PWD": "/local", | |
| } | |
| wd, err = wrapper.Getwd() | |
| require.NoErrorf(t, err, "Failed to get wd: %v", err) | |
| require.Equal(t, "/local", wd) | |
| } | |
| func TestUserHomeDir(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| environment map[string]string | |
| want string | |
| }{ | |
| { | |
| name: "Default", | |
| environment: map[string]string{}, | |
| want: "/", | |
| }, | |
| { | |
| name: "POSIX", | |
| environment: map[string]string{ | |
| "HOME": "/posix", | |
| "USERPROFILE": "/windows", | |
| }, | |
| want: "/posix", | |
| }, | |
| { | |
| name: "Windows", | |
| environment: map[string]string{ | |
| "USERPROFILE": "/windows", | |
| }, | |
| want: "/windows", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| wrapper.MemMapEnvironProvider.Environment = testCase.environment | |
| home, err := wrapper.UserHomeDir() | |
| require.NoErrorf(t, err, "Failed to get home dir: %v", err) | |
| require.Equal(t, testCase.want, home) | |
| }) | |
| } | |
| } | |
| func TestOpen_Nonexistent(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| _, err := wrapper.Open("/foo.txt") | |
| require.ErrorContains(t, err, "open /foo.txt: file does not exist") | |
| } | |
| func TestOpenFile_Nonexistent(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| _, err := wrapper.OpenFile("/foo.txt", os.O_RDONLY, 0o700) | |
| require.ErrorContains(t, err, "open /foo.txt: file does not exist") | |
| } | |
| func TestReadFile_Nonexistent(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| _, err := wrapper.ReadFile("/foo.txt") | |
| require.ErrorContains(t, err, "open /foo.txt: file does not exist") | |
| } | |
| func TestStat_Nonexistent(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| _, err := wrapper.Stat("/foo.txt") | |
| require.ErrorContains(t, err, "open /foo.txt: file does not exist") | |
| } | |
| func TestWalk_Nonexistent(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| walkfunc := func(root string, info fs.FileInfo, err error) error { | |
| require.Equal(t, "/nonexistent", root) | |
| require.Equal(t, nil, info) | |
| require.ErrorContains(t, err, "open /nonexistent: file does not exist") | |
| return err | |
| } | |
| err := wrapper.Walk("/nonexistent", walkfunc) | |
| require.ErrorContains(t, err, "open /nonexistent: file does not exist") | |
| } | |
| func TestWriteFile_Open(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| err := wrapper.WriteFile("/foo.txt", []byte("content"), 0o700) | |
| require.NoErrorf(t, err, "Failed to write file: %v", err) | |
| file, err := wrapper.Open("/foo.txt") | |
| require.NoErrorf(t, err, "Failed to open file: %v", err) | |
| buffer := make([]byte, 7) | |
| bytesRead, err := file.Read(buffer) | |
| require.NoErrorf(t, err, "Failed to read from file: %v", err) | |
| require.Equal(t, []byte("content"), buffer) | |
| require.Equal(t, 7, bytesRead) | |
| bytesRead, err = file.Read(buffer) | |
| require.Error(t, io.EOF, "Did not get EOF") | |
| require.Equal(t, 0, bytesRead) | |
| } | |
| func TestCreate_Walk_NonexistentDirectory(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| _, err := wrapper.Create("/parent/foo.txt") | |
| require.NoErrorf(t, err, "Got error creating file without parent dir: %v", err) | |
| type input struct { | |
| root string | |
| info fs.FileInfo | |
| err error | |
| } | |
| inputs := []input{} | |
| walkfunc := func(root string, info fs.FileInfo, err error) error { | |
| inputs = append(inputs, input{root: root, info: info, err: err}) | |
| return err | |
| } | |
| err = wrapper.Walk("/parent", walkfunc) | |
| require.NoErrorf(t, err, "Got error walking: %v", err) | |
| require.Equal(t, 2, len(inputs)) | |
| require.Equal(t, "/parent", inputs[0].root) | |
| require.True(t, inputs[0].info.IsDir()) | |
| require.Equal(t, "/parent/foo.txt", inputs[1].root) | |
| require.False(t, inputs[1].info.IsDir()) | |
| } | |
| func TestCreate_ReadFile(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| file, err := wrapper.Create("/foo.txt") | |
| require.NoErrorf(t, err, "Failed to create file: %v", err) | |
| bytesWritten, err := file.Write([]byte("asdf")) | |
| require.NoErrorf(t, err, "Failed to write to file: %v", err) | |
| require.Equal(t, 4, bytesWritten) | |
| contents, err := wrapper.ReadFile("/foo.txt") | |
| require.NoErrorf(t, err, "Failed to read file: %v", err) | |
| require.Equal(t, []byte("asdf"), contents) | |
| } | |
| func TestMkdir_Exists(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| err := wrapper.Mkdir("/parent", 0o700) | |
| require.NoErrorf(t, err, "Got error creating directory: %v", err) | |
| err = wrapper.Mkdir("/parent", 0o700) | |
| require.ErrorContains(t, err, "mkdir /parent: file already exists") | |
| } | |
| func TestMkdir_MkdirAll(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| err := wrapper.Mkdir("/parent", 0o700) | |
| require.NoErrorf(t, err, "Error creating parent: %v", err) | |
| err = wrapper.MkdirAll("/parent/child/grandchild", 0o600) | |
| require.NoErrorf(t, err, "Error creating grandchild: %v", err) | |
| type input struct { | |
| root string | |
| info fs.FileInfo | |
| err error | |
| } | |
| inputs := []input{} | |
| walkfunc := func(root string, info fs.FileInfo, err error) error { | |
| inputs = append(inputs, input{root: root, info: info, err: err}) | |
| return err | |
| } | |
| err = wrapper.Walk("/parent", walkfunc) | |
| require.NoErrorf(t, err, "Got error walking: %v", err) | |
| require.Equal(t, len(inputs), 3) | |
| require.Equal(t, "/parent", inputs[0].root) | |
| require.True(t, inputs[0].info.IsDir()) | |
| require.Equal(t, os.FileMode(0o700)|fs.ModeDir, inputs[0].info.Mode()) | |
| require.Equal(t, "/parent/child", inputs[1].root) | |
| require.True(t, inputs[1].info.IsDir()) | |
| require.Equal(t, os.FileMode(0o600)|fs.ModeDir, inputs[1].info.Mode()) | |
| require.Equal(t, "/parent/child/grandchild", inputs[2].root) | |
| require.True(t, inputs[2].info.IsDir()) | |
| require.Equal(t, os.FileMode(0o600)|fs.ModeDir, inputs[2].info.Mode()) | |
| } | |
| func TestMkdirAll_Exists(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| err := wrapper.MkdirAll("/parent", 0o700) | |
| require.NoErrorf(t, err, "Got error creating directory: %v", err) | |
| err = wrapper.MkdirAll("/parent", 0o700) | |
| require.NoErrorf(t, err, "Got error creating directory second time: %v", err) | |
| } | |
| func TestMkdirTemp(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| pattern string | |
| re string | |
| }{ | |
| { | |
| name: "Default behavior", | |
| pattern: "tempdir", | |
| re: "/tmp/tempdir\\d+", | |
| }, | |
| { | |
| name: "Single star front", | |
| pattern: "*tempdir", | |
| re: "/tmp/\\d+tempdir", | |
| }, | |
| { | |
| name: "Single star middle", | |
| pattern: "temp*dir", | |
| re: "/tmp/temp\\d+dir", | |
| }, | |
| { | |
| name: "Multiple stars", | |
| pattern: "temp*dir*", | |
| re: "/tmp/temp\\*dir\\d+", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| dir, err := wrapper.MkdirTemp("/tmp", testCase.pattern) | |
| require.NoErrorf(t, err, "Error creating temporary directory: %v", err) | |
| re, err := regexp.Compile(testCase.re) | |
| require.NoErrorf(t, err, "Error compiling regexp: %s", err) | |
| require.True(t, re.Match([]byte(dir))) | |
| }) | |
| } | |
| } | |
| func TestRemove(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| filesToCreate []string | |
| dirsToCreate []string | |
| toRemove string | |
| wantErr bool | |
| wantErrMsg string | |
| }{ | |
| { | |
| name: "Non-existent", | |
| toRemove: "/foo.txt", | |
| wantErr: true, | |
| wantErrMsg: "remove /foo.txt: file does not exist", | |
| }, | |
| { | |
| name: "File", | |
| filesToCreate: []string{"/foo.txt"}, | |
| toRemove: "/foo.txt", | |
| }, | |
| { | |
| name: "Empty dir", | |
| dirsToCreate: []string{"/foo"}, | |
| toRemove: "/foo", | |
| }, | |
| { | |
| // This differs from the real os implementation, as removing a non-empty | |
| // dir is supposed to fail. | |
| name: "Non-empty dir", | |
| dirsToCreate: []string{"/foo/bar"}, | |
| toRemove: "/foo", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| for _, f := range testCase.filesToCreate { | |
| _, err := wrapper.Create(f) | |
| require.NoErrorf(t, err, "Failed to create file: %v", err) | |
| } | |
| for _, d := range testCase.dirsToCreate { | |
| err := wrapper.MkdirAll(d, 0o700) | |
| require.NoErrorf(t, err, "Failed to create directory: %v", err) | |
| } | |
| err := wrapper.Remove(testCase.toRemove) | |
| if testCase.wantErr { | |
| require.ErrorContains(t, err, testCase.wantErrMsg) | |
| } else { | |
| require.NoErrorf(t, err, "Got unexpected error: %v") | |
| } | |
| // Make sure things were actually deleted. | |
| walkfunc := func(root string, info fs.FileInfo, err error) error { | |
| require.Equal(t, "/", root) | |
| return err | |
| } | |
| err = wrapper.Walk("/", walkfunc) | |
| require.NoErrorf(t, err, "Error walking: %v", err) | |
| }) | |
| } | |
| } | |
| func TestRemoveAll(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| filesToCreate []string | |
| dirsToCreate []string | |
| toRemove string | |
| }{ | |
| { | |
| name: "Non-existent", | |
| toRemove: "/foo.txt", | |
| }, | |
| { | |
| name: "File", | |
| filesToCreate: []string{"/foo.txt"}, | |
| toRemove: "/foo.txt", | |
| }, | |
| { | |
| name: "Empty dir", | |
| dirsToCreate: []string{"/foo"}, | |
| toRemove: "/foo", | |
| }, | |
| { | |
| // This differs from the real os implementation, as removing a non-empty | |
| // dir is supposed to fail. | |
| name: "Non-empty dir", | |
| dirsToCreate: []string{"/foo/bar"}, | |
| toRemove: "/foo", | |
| }, | |
| } | |
| for _, testCase := range tests { | |
| t.Run(testCase.name, func(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| for _, f := range testCase.filesToCreate { | |
| _, err := wrapper.Create(f) | |
| require.NoErrorf(t, err, "Failed to create file: %v", err) | |
| } | |
| for _, d := range testCase.dirsToCreate { | |
| err := wrapper.MkdirAll(d, 0o700) | |
| require.NoErrorf(t, err, "Failed to create directory: %v", err) | |
| } | |
| err := wrapper.RemoveAll(testCase.toRemove) | |
| require.NoErrorf(t, err, "Got unexpected error: %v") | |
| // Make sure things were actually deleted. | |
| walkfunc := func(root string, info fs.FileInfo, err error) error { | |
| require.Equal(t, "/", root) | |
| return err | |
| } | |
| err = wrapper.Walk("/", walkfunc) | |
| require.NoErrorf(t, err, "Error walking: %v", err) | |
| }) | |
| } | |
| } | |
| func TestWriteFile_OpenFile(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| err := wrapper.WriteFile("/foo.txt", []byte("content"), 0o700) | |
| require.NoErrorf(t, err, "Failed to write file: %v", err) | |
| file, err := wrapper.OpenFile("/foo.txt", os.O_RDONLY, 0o700) | |
| require.NoErrorf(t, err, "Failed to open file: %v", err) | |
| buffer := make([]byte, 7) | |
| bytesRead, err := file.Read(buffer) | |
| require.NoErrorf(t, err, "Failed to read from file: %v", err) | |
| require.Equal(t, []byte("content"), buffer) | |
| require.Equal(t, 7, bytesRead) | |
| bytesRead, err = file.Read(buffer) | |
| require.Errorf(t, io.EOF, "Did not get EOF: %v", err) | |
| require.Equal(t, 0, bytesRead) | |
| } | |
| func TestWriteFile_ReadFile(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| err := wrapper.WriteFile("/foo.txt", []byte("content"), 0o700) | |
| require.NoErrorf(t, err, "Failed to write file: %v", err) | |
| contents, err := wrapper.ReadFile("/foo.txt") | |
| require.NoErrorf(t, err, "Failed to read file: %v", err) | |
| require.Equal(t, []byte("content"), contents) | |
| } | |
| func TestWriteFile_Stat(t *testing.T) { | |
| wrapper := CreateMemMapOSWrapper() | |
| approxWriteTime := time.Now() | |
| err := wrapper.WriteFile("/foo.txt", []byte("content"), 0o700) | |
| require.NoErrorf(t, err, "Failed to write file: %v", err) | |
| fileInfo, err := wrapper.Stat("/foo.txt") | |
| require.NoErrorf(t, err, "Got error stat-ing file: %v", err) | |
| require.Equal(t, "foo.txt", fileInfo.Name()) | |
| require.Equal(t, int64(7), fileInfo.Size()) | |
| require.Equal(t, os.FileMode(0o700), fileInfo.Mode()) | |
| // Assert that the actual mod time is within 10ms of when we requested the | |
| // write. | |
| require.LessOrEqual(t, fileInfo.ModTime().UnixMilli()-approxWriteTime.UnixMilli(), int64(10)) | |
| require.False(t, fileInfo.IsDir()) | |
| } |