Files
dingtalk-workspace-cli/internal/event/consume/coverage_edges_test.go

294 lines
10 KiB
Go

package consume
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net"
"os"
"runtime"
"strings"
"syscall"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/busctl"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
)
type errorFormatter struct{ err error }
func (f errorFormatter) Render(transport.Event) ([]byte, error) { return nil, f.err }
type errorSink struct{ err error }
func (s errorSink) Write(transport.Event, []byte) error { return s.err }
func (s errorSink) Close() error { return s.err }
func TestCrossPlatformCoverageFormatterPipelineRouterAndSinkEdges(t *testing.T) {
wantErr := errors.New("synthetic failure")
oldMarshal, oldIndent, oldCompact := marshalEvent, marshalEventIndent, marshalCompact
oldMkdir, oldWrite, oldRename, oldRemove := mkdirSinkDir, writeSinkFile, renameSinkFile, removeSinkFile
t.Cleanup(func() {
marshalEvent, marshalEventIndent, marshalCompact = oldMarshal, oldIndent, oldCompact
mkdirSinkDir, writeSinkFile, renameSinkFile, removeSinkFile = oldMkdir, oldWrite, oldRename, oldRemove
})
marshalEvent = func(any) ([]byte, error) { return nil, wantErr }
if _, err := (ndjsonFormatter{}).Render(transport.Event{}); !errors.Is(err, wantErr) {
t.Fatalf("ndjson marshal error = %v", err)
}
marshalEventIndent = func(any, string, string) ([]byte, error) { return nil, wantErr }
if _, err := (prettyFormatter{}).Render(transport.Event{}); !errors.Is(err, wantErr) {
t.Fatalf("pretty marshal error = %v", err)
}
marshalCompact = func(any) ([]byte, error) { return nil, wantErr }
if _, err := (compactFormatter{}).Render(transport.Event{}); !errors.Is(err, wantErr) {
t.Fatalf("compact marshal error = %v", err)
}
pipeline := NewPipeline(errorFormatter{err: wantErr}, errorSink{})
if err := pipeline.Deliver(transport.Event{}); !errors.Is(err, wantErr) {
t.Fatalf("formatter pipeline error = %v", err)
}
pipeline = NewPipeline(rawFormatter{}, errorSink{err: wantErr})
if err := pipeline.Deliver(transport.Event{}); !errors.Is(err, wantErr) {
t.Fatalf("sink pipeline error = %v", err)
}
if err := pipeline.Close(); !errors.Is(err, wantErr) {
t.Fatalf("pipeline close error = %v", err)
}
if _, err := BuildPipeline(Format("invalid"), "", nil, nil); err == nil {
t.Fatal("invalid pipeline format should fail")
}
if p, err := BuildPipeline(FormatNDJSON, "", nil, nil); err != nil || p == nil {
t.Fatalf("nil stdout pipeline = %#v, %v", p, err)
}
if (*Router)(nil).Rules() != nil || (*Router)(nil).Match(transport.Event{}) != "" {
t.Fatal("nil router should have no rules or match")
}
if rules := NewRouter([]Route{}).Rules(); rules == nil {
t.Fatal("non-nil router should return its rule slice")
}
if err := ValidateNoOutputConflict(validRunConfig(), "captured.out"); err != nil {
t.Fatalf("global output without stream sink: %v", err)
}
if err := NewStdoutSink(errorWriter{err: testBrokenPipeError()}).Write(transport.Event{}, []byte("x")); !errors.Is(err, ErrPipeClosed) {
t.Fatalf("broken pipe error = %v", err)
}
mkdirSinkDir = func(string, os.FileMode) error { return wantErr }
if err := NewFileDirSink("dir").Write(transport.Event{}, nil); !errors.Is(err, wantErr) {
t.Fatalf("sink mkdir error = %v", err)
}
mkdirSinkDir = oldMkdir
writeSinkFile = func(string, []byte, os.FileMode) error { return wantErr }
if err := atomicWrite("path", nil); !errors.Is(err, wantErr) {
t.Fatalf("atomic write error = %v", err)
}
writeSinkFile = func(string, []byte, os.FileMode) error { return nil }
renameSinkFile = func(string, string) error { return wantErr }
removeSinkFile = func(string) error { return nil }
if err := atomicWrite("path", nil); !errors.Is(err, wantErr) {
t.Fatalf("atomic rename error = %v", err)
}
}
type errorWriter struct{ err error }
func (w errorWriter) Write([]byte) (int, error) { return 0, w.err }
func validRunConfig() Config {
return Config{WorkDir: "work", IPCEndpoint: "endpoint", ClientID: "client", Stdout: io.Discard, Stderr: io.Discard, Format: FormatNDJSON}
}
func TestCrossPlatformCoverageRunSetupAndDiscoveryEdges(t *testing.T) {
if err := Run(context.Background(), Config{}); err == nil {
t.Fatal("missing required run config should fail")
}
if err := Run(context.Background(), Config{WorkDir: "w", IPCEndpoint: "e", ClientID: "c", Quiet: true, DryRun: true}); err != nil {
t.Fatalf("defaulted quiet dry run: %v", err)
}
cfg := validRunConfig()
cfg.Format = Format("invalid")
if err := Run(context.Background(), cfg); err == nil {
t.Fatal("invalid format should fail before discovery")
}
oldDiscover := discoverBus
t.Cleanup(func() { discoverBus = oldDiscover })
wantErr := errors.New("discover failed")
discoverBus = func(busctl.DiscoverConfig) (net.Conn, error) { return nil, wantErr }
if err := Run(context.Background(), validRunConfig()); !errors.Is(err, wantErr) {
t.Fatalf("discover error = %v", err)
}
discoverBus = func(busctl.DiscoverConfig) (net.Conn, error) { return &faultConn{writeErr: wantErr}, nil }
if err := Run(context.Background(), validRunConfig()); !errors.Is(err, wantErr) {
t.Fatalf("hello write error = %v", err)
}
discoverBus = pipeDiscover(t, nil, true)
if err := Run(context.Background(), validRunConfig()); err == nil || !strings.Contains(err.Error(), "hello_ack") {
t.Fatalf("ack read error = %v", err)
}
discoverBus = pipeDiscover(t, [][]byte{mustJSONFrame(t, transport.HelloAck{Type: transport.FrameTypeHeartbeat})}, false)
if err := Run(context.Background(), validRunConfig()); err == nil || !strings.Contains(err.Error(), "unexpected") {
t.Fatalf("wrong ack error = %v", err)
}
}
func TestCrossPlatformCoverageRunFrameAndDeliveryEdges(t *testing.T) {
oldDiscover := discoverBus
t.Cleanup(func() { discoverBus = oldDiscover })
ack := mustJSONFrame(t, transport.HelloAck{Type: transport.FrameTypeHelloAck, BusPID: 1})
frames := [][]byte{
ack,
[]byte("{"),
[]byte(`{"type":"event","event_id":{}}`),
mustJSONFrame(t, transport.SourceState{Type: transport.FrameTypeSourceState, State: "running"}),
mustJSONFrame(t, transport.Heartbeat{Type: transport.FrameTypeHeartbeat}),
[]byte(`{"type":"future"}`),
mustJSONFrame(t, transport.Bye{Type: transport.FrameTypeBye, Reason: "done"}),
}
discoverBus = pipeDiscover(t, frames, true)
cfg := validRunConfig()
cfg.Stderr = &bytes.Buffer{}
if err := Run(context.Background(), cfg); err != nil {
t.Fatalf("mixed frames run: %v", err)
}
discoverBus = pipeDiscover(t, [][]byte{ack}, true)
if err := Run(context.Background(), validRunConfig()); err != nil {
t.Fatalf("clean peer EOF run: %v", err)
}
event := mustJSONFrame(t, transport.Event{Type: transport.FrameTypeEvent, EventID: "id", EventType: "type", Data: `{}`})
for _, tc := range []struct {
name string
writer io.Writer
want error
}{
{"pipe closed", errorWriter{err: testBrokenPipeError()}, nil},
{"delivery failure", errorWriter{err: errors.New("output failed")}, errors.New("want error")},
} {
discoverBus = pipeDiscover(t, [][]byte{ack, event}, true)
cfg = validRunConfig()
cfg.Stdout = tc.writer
err := Run(context.Background(), cfg)
if tc.want == nil && err != nil {
t.Errorf("%s run error = %v", tc.name, err)
}
if tc.want != nil && err == nil {
t.Errorf("%s run should fail", tc.name)
}
}
discoverBus = pipeDiscover(t, [][]byte{ack, event}, true)
cfg = validRunConfig()
cfg.MaxEvents = 1
if err := Run(context.Background(), cfg); err != nil {
t.Fatalf("max-events run: %v", err)
}
readErr := errors.New("read failed")
discoverBus = func(busctl.DiscoverConfig) (net.Conn, error) {
return &faultConn{readData: append(append([]byte{}, ack...), '\n'), readErr: readErr}, nil
}
if err := Run(context.Background(), validRunConfig()); !errors.Is(err, readErr) {
t.Fatalf("frame read error = %v", err)
}
discoverBus = pipeDiscover(t, [][]byte{ack}, false)
cfg = validRunConfig()
cfg.Duration = 10 * time.Millisecond
if err := Run(context.Background(), cfg); err != nil {
t.Fatalf("duration cancellation run: %v", err)
}
}
func testBrokenPipeError() error {
if runtime.GOOS == "windows" {
// ERROR_BROKEN_PIPE. Use syscall.Errno so this generic test file
// stays buildable without importing the Windows-only package.
return syscall.Errno(109)
}
return syscall.EPIPE
}
func mustJSONFrame(t *testing.T, value any) []byte {
t.Helper()
raw, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal frame: %v", err)
}
return raw
}
func pipeDiscover(t *testing.T, frames [][]byte, closeAfter bool) func(busctl.DiscoverConfig) (net.Conn, error) {
t.Helper()
return func(busctl.DiscoverConfig) (net.Conn, error) {
client, server := net.Pipe()
go func() {
defer func() {
if closeAfter {
_ = server.Close()
}
}()
var hello transport.Hello
if err := transport.NewReader(server).ReadJSON(&hello); err != nil {
_ = server.Close()
return
}
for _, frame := range frames {
if _, err := server.Write(append(append([]byte{}, frame...), '\n')); err != nil {
return
}
}
}()
return client, nil
}
}
type faultConn struct {
readData []byte
readErr error
writeErr error
}
func (c *faultConn) Read(p []byte) (int, error) {
if len(c.readData) > 0 {
n := copy(p, c.readData)
c.readData = c.readData[n:]
return n, nil
}
return 0, c.readErr
}
func (c *faultConn) Write(p []byte) (int, error) {
if c.writeErr != nil {
return 0, c.writeErr
}
return len(p), nil
}
func (*faultConn) Close() error { return nil }
func (*faultConn) LocalAddr() net.Addr { return consumeAddr("local") }
func (*faultConn) RemoteAddr() net.Addr { return consumeAddr("remote") }
func (*faultConn) SetDeadline(time.Time) error { return nil }
func (*faultConn) SetReadDeadline(time.Time) error { return nil }
func (*faultConn) SetWriteDeadline(time.Time) error { return nil }
type consumeAddr string
func (a consumeAddr) Network() string { return "test" }
func (a consumeAddr) String() string { return string(a) }
func TestCrossPlatformCoverageWatchStdinEOFCancelledAfterData(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
called := false
watchStdinEOF(ctx, strings.NewReader("data"), io.Discard, func() { called = true })
if called {
t.Fatal("watchStdinEOF called shutdown after context cancellation")
}
}