334 lines
11 KiB
Go
334 lines
11 KiB
Go
package busctl
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
dwsevent "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/bus"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
|
|
)
|
|
|
|
var errBusctlInjected = errors.New("injected busctl failure")
|
|
|
|
func TestCrossPlatformCoverageDiscoverValidationAndFailures(t *testing.T) {
|
|
for name, cfg := range map[string]DiscoverConfig{
|
|
"workdir": {IPCEndpoint: "endpoint", ClientID: "id"},
|
|
"endpoint": {WorkDir: t.TempDir(), ClientID: "id"},
|
|
"client": {WorkDir: t.TempDir(), IPCEndpoint: "endpoint"},
|
|
} {
|
|
if _, err := Discover(cfg); err == nil {
|
|
t.Fatalf("%s validation error expected", name)
|
|
}
|
|
}
|
|
|
|
origDial := discoverDial
|
|
origMkdir := discoverMkdirAll
|
|
origSpawn := discoverSpawn
|
|
t.Cleanup(func() {
|
|
discoverDial = origDial
|
|
discoverMkdirAll = origMkdir
|
|
discoverSpawn = origSpawn
|
|
})
|
|
discoverDial = func(string) (net.Conn, error) { return nil, errBusctlInjected }
|
|
discoverMkdirAll = func(string, os.FileMode) error { return errBusctlInjected }
|
|
if _, err := Discover(DiscoverConfig{WorkDir: t.TempDir(), IPCEndpoint: "x", ClientID: "id"}); err == nil {
|
|
t.Fatal("mkdir error expected")
|
|
}
|
|
discoverMkdirAll = func(string, os.FileMode) error { return nil }
|
|
discoverSpawn = func(SpawnConfig) (int, error) { return 0, ErrSpawnFailed }
|
|
if _, err := Discover(DiscoverConfig{
|
|
WorkDir: t.TempDir(), IPCEndpoint: "x", ClientID: "id",
|
|
DialDeadline: time.Millisecond, DialBackoff: time.Millisecond, DialMaxBackoff: time.Nanosecond,
|
|
}); err == nil {
|
|
t.Fatal("deadline error expected")
|
|
}
|
|
if got := MetaPath("dir"); got != filepath.Join("dir", "bus.meta") {
|
|
t.Fatalf("MetaPath = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageWaitReadyRejectsShortSuccessfulRead(t *testing.T) {
|
|
original := busctlReadFull
|
|
t.Cleanup(func() { busctlReadFull = original })
|
|
busctlReadFull = func(io.Reader, []byte) (int, error) { return 0, nil }
|
|
read, write, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer read.Close()
|
|
defer write.Close()
|
|
if err := waitReady(read); !errors.Is(err, ErrSpawnFailed) {
|
|
t.Fatalf("waitReady(short read) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSpawnAndReadyFailureEdges(t *testing.T) {
|
|
origExecutable := spawnExecutable
|
|
origPipe := spawnPipe
|
|
origTimeout := ReadyTimeout
|
|
t.Cleanup(func() {
|
|
spawnExecutable = origExecutable
|
|
spawnPipe = origPipe
|
|
ReadyTimeout = origTimeout
|
|
})
|
|
if _, err := Spawn(SpawnConfig{}); err == nil {
|
|
t.Fatal("client ID validation expected")
|
|
}
|
|
spawnExecutable = func() (string, error) { return "", errBusctlInjected }
|
|
if _, err := Spawn(SpawnConfig{ClientID: "id"}); err == nil {
|
|
t.Fatal("executable error expected")
|
|
}
|
|
spawnExecutable = origExecutable
|
|
spawnPipe = func() (*os.File, *os.File, error) { return nil, nil, errBusctlInjected }
|
|
if _, err := Spawn(SpawnConfig{ClientID: "id"}); err == nil {
|
|
t.Fatal("pipe error expected")
|
|
}
|
|
spawnPipe = origPipe
|
|
if _, err := Spawn(SpawnConfig{ClientID: "id", ExecPath: filepath.Join(t.TempDir(), "missing")}); err == nil {
|
|
t.Fatal("start error expected")
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
data []byte
|
|
want error
|
|
}{
|
|
{"ready", []byte{'R'}, nil},
|
|
{"failed", []byte{'E'}, ErrSpawnFailed},
|
|
{"unexpected", []byte{'X'}, errBusctlInjected},
|
|
{"eof", nil, ErrSpawnFailed},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
pr, pw, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tc.data) > 0 {
|
|
_, _ = pw.Write(tc.data)
|
|
}
|
|
_ = pw.Close()
|
|
err = waitReady(pr)
|
|
_ = pr.Close()
|
|
if tc.want == nil && err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc.want != nil && tc.want == ErrSpawnFailed && !errors.Is(err, tc.want) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if tc.name == "unexpected" && err == nil {
|
|
t.Fatal("unexpected byte error expected")
|
|
}
|
|
})
|
|
}
|
|
pr, pw, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = pr.Close()
|
|
if err := waitReady(pr); err == nil || errors.Is(err, ErrSpawnFailed) {
|
|
t.Fatalf("closed read error = %v", err)
|
|
}
|
|
_ = pw.Close()
|
|
pr, pw, err = os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ReadyTimeout = time.Millisecond
|
|
if err := waitReady(pr); !errors.Is(err, ErrSpawnTimeout) {
|
|
t.Fatalf("timeout error = %v", err)
|
|
}
|
|
_ = pr.Close()
|
|
_ = pw.Close()
|
|
}
|
|
|
|
func TestCrossPlatformCoverageStatusFilesystemEdges(t *testing.T) {
|
|
origReadDir := statusReadDir
|
|
origStat := statusStat
|
|
t.Cleanup(func() { statusReadDir = origReadDir; statusStat = origStat })
|
|
configDir := t.TempDir()
|
|
eventsDir := filepath.Join(configDir, "events")
|
|
statusReadDir = func(path string) ([]os.DirEntry, error) {
|
|
if path == eventsDir {
|
|
return nil, errBusctlInjected
|
|
}
|
|
return origReadDir(path)
|
|
}
|
|
if _, err := EnumerateBuses(configDir, ""); !errors.Is(err, errBusctlInjected) {
|
|
t.Fatalf("events scan error = %v", err)
|
|
}
|
|
statusReadDir = origReadDir
|
|
|
|
configDir = t.TempDir()
|
|
legacy := makeBusDir(t, configDir, "open", "legacy", true, 0)
|
|
_ = legacy
|
|
sourceDir := filepath.Join(configDir, "events", "open", string(dwsevent.SourceKindPersonalStream), "identity")
|
|
if err := os.MkdirAll(sourceDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := bus.WriteMeta(sourceDir, bus.Meta{
|
|
ClientID: "client", Edition: "open", SourceKind: dwsevent.SourceKindPersonalStream,
|
|
IdentityHash: "meta-identity",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, err := EnumerateBuses(configDir, "")
|
|
if err != nil || len(entries) != 2 {
|
|
t.Fatalf("entries = %#v, %v", entries, err)
|
|
}
|
|
var personal BusEntry
|
|
for _, entry := range entries {
|
|
_ = entry.IPCEndpoint()
|
|
if entry.SourceKind == dwsevent.SourceKindPersonalStream {
|
|
personal = entry
|
|
}
|
|
}
|
|
if personal.IdentityHash != "meta-identity" || personal.ClientIDHash != "meta-identity" {
|
|
t.Fatalf("personal meta = %#v", personal)
|
|
}
|
|
if got := FindBusByIdentity(configDir, "open", dwsevent.SourceKindPersonalStream, "identity"); got == nil {
|
|
t.Fatal("personal identity not found")
|
|
}
|
|
if got := FindBusByIdentity(configDir, "open", "", "missing"); got != nil {
|
|
t.Fatalf("missing identity = %#v", got)
|
|
}
|
|
|
|
root := filepath.Join(configDir, "events")
|
|
statusReadDir = func(path string) ([]os.DirEntry, error) {
|
|
if path != root {
|
|
return nil, errBusctlInjected
|
|
}
|
|
return origReadDir(path)
|
|
}
|
|
if got, err := EnumerateBuses(configDir, ""); err != nil || len(got) != 0 {
|
|
t.Fatalf("edition scan skip = %#v, %v", got, err)
|
|
}
|
|
statusReadDir = func(path string) ([]os.DirEntry, error) {
|
|
if filepath.Base(path) == string(dwsevent.SourceKindPersonalStream) {
|
|
return nil, errBusctlInjected
|
|
}
|
|
return origReadDir(path)
|
|
}
|
|
if _, err := EnumerateBuses(configDir, ""); err != nil {
|
|
t.Fatalf("identity scan should be skipped: %v", err)
|
|
}
|
|
|
|
statusStat = func(string) (os.FileInfo, error) { return nil, nil }
|
|
if got := FindBusByClientID(configDir, "open", "virtual"); got == nil {
|
|
t.Fatal("virtual primary path should be inspected")
|
|
}
|
|
}
|
|
|
|
type queryErrorConn struct {
|
|
writes int
|
|
failAt int
|
|
}
|
|
|
|
func (c *queryErrorConn) Read([]byte) (int, error) { return 0, errBusctlInjected }
|
|
func (c *queryErrorConn) Write(p []byte) (int, error) {
|
|
c.writes++
|
|
if c.failAt > 0 && c.writes >= c.failAt {
|
|
return 0, errBusctlInjected
|
|
}
|
|
return len(p), nil
|
|
}
|
|
func (*queryErrorConn) Close() error { return nil }
|
|
func (*queryErrorConn) LocalAddr() net.Addr { return dummyAddr("local") }
|
|
func (*queryErrorConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
|
|
func (*queryErrorConn) SetDeadline(time.Time) error { return errBusctlInjected }
|
|
func (*queryErrorConn) SetReadDeadline(time.Time) error { return nil }
|
|
func (*queryErrorConn) SetWriteDeadline(time.Time) error { return nil }
|
|
|
|
type dummyAddr string
|
|
|
|
func (a dummyAddr) Network() string { return string(a) }
|
|
func (a dummyAddr) String() string { return string(a) }
|
|
|
|
func TestCrossPlatformCoverageQueryStatusAndEntryEdges(t *testing.T) {
|
|
origDial := statusDial
|
|
origQuery := queryStatus
|
|
t.Cleanup(func() { statusDial = origDial; queryStatus = origQuery })
|
|
statusDial = func(string) (net.Conn, error) { return nil, errBusctlInjected }
|
|
if _, err := QueryStatus("x"); err == nil {
|
|
t.Fatal("dial error expected")
|
|
}
|
|
for failAt := 1; failAt <= 4; failAt++ {
|
|
statusDial = func(string) (net.Conn, error) { return &queryErrorConn{failAt: failAt}, nil }
|
|
_, _ = QueryStatus("x")
|
|
}
|
|
statusDial = func(string) (net.Conn, error) { return &queryErrorConn{}, nil }
|
|
if _, err := QueryStatus("x"); err == nil {
|
|
t.Fatal("read error expected")
|
|
}
|
|
|
|
entry := BusEntry{State: BusStateNotRunning}
|
|
if got := QueryEntry(entry); got.Live != nil {
|
|
t.Fatalf("not-running live = %#v", got.Live)
|
|
}
|
|
entry.State = BusStateRunning
|
|
queryStatus = func(string) (*transport.StatusResp, error) { return nil, errBusctlInjected }
|
|
if got := QueryEntry(entry); got.Live != nil {
|
|
t.Fatalf("failed live = %#v", got.Live)
|
|
}
|
|
want := &transport.StatusResp{Type: transport.FrameTypeStatusResp}
|
|
queryStatus = func(string) (*transport.StatusResp, error) { return want, nil }
|
|
if got := QueryEntry(entry); got.Live != want {
|
|
t.Fatalf("live = %#v", got.Live)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageStopInjectedEdges(t *testing.T) {
|
|
origRead := stopReadHolderPID
|
|
origValidateOwner := stopValidateHolderOwner
|
|
origAlive := stopAlive
|
|
origFind := stopFindProcess
|
|
origSignal := stopSignalProcess
|
|
t.Cleanup(func() {
|
|
stopReadHolderPID = origRead
|
|
stopValidateHolderOwner = origValidateOwner
|
|
stopAlive = origAlive
|
|
stopFindProcess = origFind
|
|
stopSignalProcess = origSignal
|
|
})
|
|
if err := Stop(StopConfig{}); err == nil {
|
|
t.Fatal("workdir validation expected")
|
|
}
|
|
stopReadHolderPID = func(string) int { return 123 }
|
|
stopAlive = func(int) bool { return true }
|
|
stopFindProcess = func(int) (*os.Process, error) { return nil, errBusctlInjected }
|
|
if err := Stop(StopConfig{WorkDir: "x"}); err == nil {
|
|
t.Fatal("find error expected")
|
|
}
|
|
proc, err := os.FindProcess(os.Getpid())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stopFindProcess = func(int) (*os.Process, error) { return proc, nil }
|
|
stopValidateHolderOwner = func(string, int) (bool, error) { return true, nil }
|
|
stopSignalProcess = func(*os.Process, os.Signal) error { return os.ErrProcessDone }
|
|
if err := Stop(StopConfig{WorkDir: "x"}); err != nil {
|
|
t.Fatalf("done signal = %v", err)
|
|
}
|
|
stopSignalProcess = func(*os.Process, os.Signal) error { return errBusctlInjected }
|
|
if err := Stop(StopConfig{WorkDir: "x"}); err == nil {
|
|
t.Fatal("signal error expected")
|
|
}
|
|
stopSignalProcess = func(*os.Process, os.Signal) error { return nil }
|
|
calls := 0
|
|
stopAlive = func(int) bool { calls++; return calls < 3 }
|
|
if err := Stop(StopConfig{WorkDir: "x", Timeout: time.Second}); err != nil {
|
|
t.Fatalf("poll exit = %v", err)
|
|
}
|
|
stopAlive = func(int) bool { return true }
|
|
if err := Stop(StopConfig{WorkDir: "x", Timeout: time.Millisecond}); err == nil {
|
|
t.Fatal("timeout expected")
|
|
}
|
|
}
|
|
|
|
var _ io.Reader = (*queryErrorConn)(nil)
|