system/tmpfiles: do not fail for smaller files
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Test / Create distribution (push) Successful in 34s
				
			
		
			
				
	
				Test / Sandbox (push) Successful in 2m23s
				
			
		
			
				
	
				Test / Hpkg (push) Successful in 4m9s
				
			
		
			
				
	
				Test / Sandbox (race detector) (push) Successful in 4m31s
				
			
		
			
				
	
				Test / Hakurei (race detector) (push) Successful in 5m15s
				
			
		
			
				
	
				Test / Hakurei (push) Successful in 2m11s
				
			
		
			
				
	
				Test / Flake checks (push) Successful in 1m27s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 34s
				
			Test / Sandbox (push) Successful in 2m23s
				
			Test / Hpkg (push) Successful in 4m9s
				
			Test / Sandbox (race detector) (push) Successful in 4m31s
				
			Test / Hakurei (race detector) (push) Successful in 5m15s
				
			Test / Hakurei (push) Successful in 2m11s
				
			Test / Flake checks (push) Successful in 1m27s
				
			The limit is meant to be an upper bound. Handle EOF and print verbose message for it instead of failing. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
		
							parent
							
								
									323d132c40
								
							
						
					
					
						commit
						da2b9c01ce
					
				| @ -1,12 +1,20 @@ | ||||
| package system | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"io/fs" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"hakurei.app/system/acl" | ||||
| 	"hakurei.app/system/dbus" | ||||
| ) | ||||
| 
 | ||||
| type osFile interface { | ||||
| 	Name() string | ||||
| 	io.Writer | ||||
| 	fs.File | ||||
| } | ||||
| 
 | ||||
| // syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. | ||||
| // syscallDispatcher is embedded in [I], so all methods must be unexported. | ||||
| type syscallDispatcher interface { | ||||
| @ -15,6 +23,10 @@ type syscallDispatcher interface { | ||||
| 	// just synchronising access is not enough, as this is for test instrumentation. | ||||
| 	new(f func(k syscallDispatcher)) | ||||
| 
 | ||||
| 	// stat provides os.Stat. | ||||
| 	stat(name string) (os.FileInfo, error) | ||||
| 	// open provides [os.Open]. | ||||
| 	open(name string) (osFile, error) | ||||
| 	// mkdir provides os.Mkdir. | ||||
| 	mkdir(name string, perm os.FileMode) error | ||||
| 	// chmod provides os.Chmod. | ||||
| @ -48,6 +60,8 @@ type direct struct{} | ||||
| 
 | ||||
| func (k direct) new(f func(k syscallDispatcher)) { go f(k) } | ||||
| 
 | ||||
| func (k direct) stat(name string) (os.FileInfo, error)     { return os.Stat(name) } | ||||
| func (k direct) open(name string) (osFile, error)          { return os.Open(name) } | ||||
| func (k direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } | ||||
| func (k direct) chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } | ||||
| func (k direct) link(oldname, newname string) error        { return os.Link(oldname, newname) } | ||||
|  | ||||
| @ -1,10 +1,13 @@ | ||||
| package system | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 	"io/fs" | ||||
| 	"os" | ||||
| 	"reflect" | ||||
| 	"slices" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 	"unsafe" | ||||
| 
 | ||||
| 	"hakurei.app/container/stub" | ||||
| @ -180,6 +183,34 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| type stubFi struct { | ||||
| 	size  int64 | ||||
| 	isDir bool | ||||
| } | ||||
| 
 | ||||
| func (stubFi) Name() string       { panic("unreachable") } | ||||
| func (fi stubFi) Size() int64     { return fi.size } | ||||
| func (stubFi) Mode() fs.FileMode  { panic("unreachable") } | ||||
| func (stubFi) ModTime() time.Time { panic("unreachable") } | ||||
| func (fi stubFi) IsDir() bool     { return fi.isDir } | ||||
| func (stubFi) Sys() any           { panic("unreachable") } | ||||
| 
 | ||||
| type readerOsFile struct { | ||||
| 	closed bool | ||||
| 	io.Reader | ||||
| } | ||||
| 
 | ||||
| func (*readerOsFile) Name() string               { panic("unreachable") } | ||||
| func (*readerOsFile) Write([]byte) (int, error)  { panic("unreachable") } | ||||
| func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") } | ||||
| func (r *readerOsFile) Close() error { | ||||
| 	if r.closed { | ||||
| 		return os.ErrClosed | ||||
| 	} | ||||
| 	r.closed = true | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // InternalNew initialises [I] with a stub syscallDispatcher. | ||||
| func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) { | ||||
| 	k := stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want) | ||||
| @ -192,6 +223,28 @@ type kstub struct{ *stub.Stub[syscallDispatcher] } | ||||
| 
 | ||||
| func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } | ||||
| 
 | ||||
| func (k *kstub) stat(name string) (fi os.FileInfo, err error) { | ||||
| 	k.Helper() | ||||
| 	expect := k.Expects("stat") | ||||
| 	err = expect.Error( | ||||
| 		stub.CheckArg(k.Stub, "name", name, 0)) | ||||
| 	if err == nil { | ||||
| 		fi = expect.Ret.(os.FileInfo) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (k *kstub) open(name string) (f osFile, err error) { | ||||
| 	k.Helper() | ||||
| 	expect := k.Expects("open") | ||||
| 	err = expect.Error( | ||||
| 		stub.CheckArg(k.Stub, "name", name, 0)) | ||||
| 	if err == nil { | ||||
| 		f = expect.Ret.(osFile) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (k *kstub) mkdir(name string, perm os.FileMode) error { | ||||
| 	k.Helper() | ||||
| 	return k.Expects("mkdir").Error( | ||||
|  | ||||
| @ -28,15 +28,15 @@ type tmpfileOp struct { | ||||
| 
 | ||||
| func (t *tmpfileOp) Type() Enablement { return Process } | ||||
| 
 | ||||
| func (t *tmpfileOp) apply(*I) error { | ||||
| 	msg.Verbose("copying", t) | ||||
| 
 | ||||
| func (t *tmpfileOp) apply(sys *I) error { | ||||
| 	if t.payload == nil { | ||||
| 		// this is a misuse of the API; do not return an error message | ||||
| 		// this is a misuse of the API; do not return a wrapped error | ||||
| 		return errors.New("invalid payload") | ||||
| 	} | ||||
| 
 | ||||
| 	if b, err := os.Stat(t.src); err != nil { | ||||
| 	sys.verbose("copying", t) | ||||
| 
 | ||||
| 	if b, err := sys.stat(t.src); err != nil { | ||||
| 		return newOpError("tmpfile", err, false) | ||||
| 	} else { | ||||
| 		if b.IsDir() { | ||||
| @ -47,9 +47,20 @@ func (t *tmpfileOp) apply(*I) error { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if f, err := os.Open(t.src); err != nil { | ||||
| 	var r io.ReadCloser | ||||
| 	if f, err := sys.open(t.src); err != nil { | ||||
| 		return newOpError("tmpfile", err, false) | ||||
| 	} else if _, err = io.CopyN(t.buf, f, t.n); err != nil { | ||||
| 	} else { | ||||
| 		r = f | ||||
| 	} | ||||
| 	if n, err := io.CopyN(t.buf, r, t.n); err != nil { | ||||
| 		if !errors.Is(err, io.EOF) { | ||||
| 			_ = r.Close() | ||||
| 			return newOpError("tmpfile", err, false) | ||||
| 		} | ||||
| 		sys.verbosef("copied %d bytes from %q", n, t.src) | ||||
| 	} | ||||
| 	if err := r.Close(); err != nil { | ||||
| 		return newOpError("tmpfile", err, false) | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -1,44 +1,142 @@ | ||||
| package system | ||||
| 
 | ||||
| import ( | ||||
| 	"strconv" | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"hakurei.app/container/stub" | ||||
| ) | ||||
| 
 | ||||
| func TestCopyFile(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		tcOp | ||||
| 		cap int | ||||
| 		n   int64 | ||||
| 	}{ | ||||
| 		{tcOp{Process, "/home/ophestra/xdg/config/pulse/cookie"}, 256, 256}, | ||||
| 	} | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run("copy file "+tc.path+" with cap = "+strconv.Itoa(tc.cap)+" n = "+strconv.Itoa(int(tc.n)), func(t *testing.T) { | ||||
| 			sys := New(t.Context(), 150) | ||||
| 			sys.CopyFile(new([]byte), tc.path, tc.cap, tc.n) | ||||
| 			tc.test(t, sys.ops, []Op{ | ||||
| 				&tmpfileOp{nil, tc.path, tc.n, nil}, | ||||
| 			}, "CopyFile") | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| type errorReader struct{} | ||||
| 
 | ||||
| func TestTmpfile_String(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		src  string | ||||
| 		n    int64 | ||||
| 		want string | ||||
| 	}{ | ||||
| 		{"/home/ophestra/xdg/config/pulse/cookie", 256, | ||||
| func (errorReader) Read([]byte) (int, error) { return 0, stub.UniqueError(0xdeadbeef) } | ||||
| 
 | ||||
| func TestTmpfileOp(t *testing.T) { | ||||
| 	// 255 bytes | ||||
| 	const paSample = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" | ||||
| 
 | ||||
| 	checkOpBehaviour(t, []opBehaviourTestCase{ | ||||
| 		{"payload", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			nil, "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, nil, errors.New("invalid payload"), nil, nil}, | ||||
| 
 | ||||
| 		{"stat", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, nil, stub.UniqueError(1)), | ||||
| 		}, &OpError{Op: "tmpfile", Err: stub.UniqueError(1)}, nil, nil}, | ||||
| 
 | ||||
| 		{"stat EISDIR", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, true}, nil), | ||||
| 		}, &OpError{Op: "tmpfile", Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.EISDIR}}, nil, nil}, | ||||
| 
 | ||||
| 		{"stat ENOMEM", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1<<8 + 1, false}, nil), | ||||
| 		}, &OpError{Op: "tmpfile", Err: &os.PathError{Op: "stat", Path: "/home/ophestra/xdg/config/pulse/cookie", Err: syscall.ENOMEM}}, nil, nil}, | ||||
| 
 | ||||
| 		{"open", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil), | ||||
| 			call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, nil, stub.UniqueError(0)), | ||||
| 		}, &OpError{Op: "tmpfile", Err: stub.UniqueError(0)}, nil, nil}, | ||||
| 
 | ||||
| 		{"reader", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil), | ||||
| 			call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{true, errorReader{}}, nil), | ||||
| 		}, &OpError{Op: "tmpfile", Err: stub.UniqueError(0xdeadbeef)}, nil, nil}, | ||||
| 
 | ||||
| 		{"closed", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil), | ||||
| 			call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{true, strings.NewReader(paSample + "=")}, nil), | ||||
| 		}, &OpError{Op: "tmpfile", Err: os.ErrClosed}, nil, nil}, | ||||
| 
 | ||||
| 		{"success full", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil), | ||||
| 			call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{false, strings.NewReader(paSample + "=")}, nil), | ||||
| 		}, nil, nil, nil}, | ||||
| 
 | ||||
| 		{"success", 0xdead, 0xff, &tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}, []stub.Call{ | ||||
| 			call("verbose", stub.ExpectArgs{[]any{"copying", &tmpfileOp{new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }()}}}, nil, nil), | ||||
| 			call("stat", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, stubFi{1 << 8, false}, nil), | ||||
| 			call("open", stub.ExpectArgs{"/home/ophestra/xdg/config/pulse/cookie"}, &readerOsFile{false, strings.NewReader(paSample)}, nil), | ||||
| 			call("verbosef", stub.ExpectArgs{"copied %d bytes from %q", []any{int64(1<<8 - 1), "/home/ophestra/xdg/config/pulse/cookie"}}, nil, nil), | ||||
| 		}, nil, nil, nil}, | ||||
| 	}) | ||||
| 
 | ||||
| 	checkOpsBuilder(t, "CopyFile", []opsBuilderTestCase{ | ||||
| 		{"pulse", 0xcafebabe, func(_ *testing.T, sys *I) { | ||||
| 			sys.CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1<<8, 1<<8) | ||||
| 		}, []Op{&tmpfileOp{ | ||||
| 			new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, | ||||
| 			func() *bytes.Buffer { buf := new(bytes.Buffer); buf.Grow(1 << 8); return buf }(), | ||||
| 		}}, stub.Expect{}}, | ||||
| 	}) | ||||
| 
 | ||||
| 	checkOpIs(t, []opIsTestCase{ | ||||
| 		{"nil", (*tmpfileOp)(nil), (*tmpfileOp)(nil), false}, | ||||
| 		{"zero", new(tmpfileOp), new(tmpfileOp), true}, | ||||
| 
 | ||||
| 		{"n differs", &tmpfileOp{ | ||||
| 			src: "/home/ophestra/xdg/config/pulse/cookie", | ||||
| 			n:   1 << 7, | ||||
| 		}, &tmpfileOp{ | ||||
| 			src: "/home/ophestra/xdg/config/pulse/cookie", | ||||
| 			n:   1 << 8, | ||||
| 		}, false}, | ||||
| 
 | ||||
| 		{"src differs", &tmpfileOp{ | ||||
| 			src: "/home/ophestra/xdg/config/pulse", | ||||
| 			n:   1 << 8, | ||||
| 		}, &tmpfileOp{ | ||||
| 			src: "/home/ophestra/xdg/config/pulse/cookie", | ||||
| 			n:   1 << 8, | ||||
| 		}, false}, | ||||
| 
 | ||||
| 		{"equals", &tmpfileOp{ | ||||
| 			src: "/home/ophestra/xdg/config/pulse/cookie", | ||||
| 			n:   1 << 8, | ||||
| 		}, &tmpfileOp{ | ||||
| 			src: "/home/ophestra/xdg/config/pulse/cookie", | ||||
| 			n:   1 << 8, | ||||
| 		}, true}, | ||||
| 	}) | ||||
| 
 | ||||
| 	checkOpMeta(t, []opMetaTestCase{ | ||||
| 		{"pulse", &tmpfileOp{nil, "/home/ophestra/xdg/config/pulse/cookie", 1 << 8, nil}, | ||||
| 			Process, "/home/ophestra/xdg/config/pulse/cookie", | ||||
| 			`up to 256 bytes from "/home/ophestra/xdg/config/pulse/cookie"`}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.want, func(t *testing.T) { | ||||
| 			if got := (&tmpfileOp{src: tc.src, n: tc.n}).String(); got != tc.want { | ||||
| 				t.Errorf("String() = %v, want %v", got, tc.want) | ||||
| 			} | ||||
| 	}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user