diff --git a/Makefile b/Makefile index 029b8439..e4117f15 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ DOCKERFILE = deployments/Dockerfile CGO_ENABLED ?= 1 .PHONY: init generate build clean tidy update sync check test coverage benchmark lint fmt vet doc docker-build +all: lint test build init: @cp .go.work go.work diff --git a/cmd/pkg/cli/debug.go b/cmd/pkg/cli/debug.go index a79cec5e..4992123a 100644 --- a/cmd/pkg/cli/debug.go +++ b/cmd/pkg/cli/debug.go @@ -43,13 +43,10 @@ type debugView interface { // Various debug view types type ( - errDebugView struct{ err error } - frameDebugView struct{ frame *agent.Frame } - framesDebugView struct{ frames []*agent.Frame } - breakpointDebugView struct { - id int - breakpoint *debug.Breakpoint - } + errDebugView struct{ err error } + frameDebugView struct{ frame *agent.Frame } + framesDebugView struct{ frames []*agent.Frame } + breakpointDebugView struct{ breakpoint *debug.Breakpoint } breakpointsDebugView struct{ breakpoints []*debug.Breakpoint } symbolDebugView struct{ symbol *symbol.Symbol } symbolsDebugView struct{ symbols []*symbol.Symbol } @@ -143,43 +140,58 @@ func (m *debugModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "quit", "q": return m, tea.Quit case "break", "b": - var sb *symbol.Symbol - if len(args) > 1 { - sb = m.findSymbol(args[1]) - if sb == nil { + var bps []*debug.Breakpoint + if len(args) <= 1 { + bp := debug.NewBreakpoint() + m.debugger.AddBreakpoint(bp) + + bps = append(bps, bp) + } else { + sbs := m.findSymbols(args[1]) + if len(sbs) == 0 { m.view = &errDebugView{err: fmt.Errorf("symbol '%s' not found", args[1])} return m, nil } - } - var inPort *port.InPort - var outPort *port.OutPort - if len(args) > 2 { - inPort, outPort = m.findPort(sb, args[2]) - if inPort == nil && outPort == nil { - m.view = &errDebugView{err: fmt.Errorf("port '%s' not found on symbol '%s'", args[2], sb.Name())} - return m, nil + for _, sb := range sbs { + var inPort *port.InPort + var outPort *port.OutPort + if len(args) > 2 { + inPort, outPort = m.findPort(sb, args[2]) + if inPort == nil && outPort == nil { + continue + } + } + + bp := debug.NewBreakpoint( + debug.WithSymbol(sb), + debug.WithInPort(inPort), + debug.WithOutPort(outPort), + ) + m.debugger.AddBreakpoint(bp) + + bps = append(bps, bp) } } - bp := debug.NewBreakpoint( - debug.WithSymbol(sb), - debug.WithInPort(inPort), - debug.WithOutPort(outPort), - ) - m.debugger.AddBreakpoint(bp) - - bps := m.debugger.Breakpoints() - m.view = &breakpointDebugView{id: len(bps) - 1, breakpoint: bp} + if len(bps) == 1 { + m.view = &breakpointDebugView{breakpoint: bps[0]} + } else { + m.view = &breakpointsDebugView{breakpoints: bps} + } - return m, func() tea.Msg { - if m.debugger.Pause(context.Background()) { - if m.debugger.Breakpoint() == bp { - return m.debugger.Frame() + var cmds []tea.Cmd + for _, bp := range bps { + cmds = append(cmds, func() tea.Msg { + if m.debugger.Pause(context.Background()) { + if m.debugger.Breakpoint() == bp { + return m.debugger.Frame() + } } - } - return nil + return nil + }) } + return m, tea.Batch(cmds...) case "continue", "c": m.view = nil @@ -190,12 +202,14 @@ func (m *debugModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return nil } case "delete", "d": - bps := m.debugger.Breakpoints() - var bp *debug.Breakpoint if len(args) > 1 { - if i, err := strconv.Atoi(args[1]); err == nil && i < len(bps) { - bp = bps[i] + bps := m.debugger.Breakpoints() + for _, b := range bps { + if b.ID().String() == args[1] { + bp = b + break + } } } else { bp = m.debugger.Breakpoint() @@ -230,14 +244,21 @@ func (m *debugModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.view = &symbolsDebugView{symbols: sbs} return m, nil case "symbol", "sb": - var sb *symbol.Symbol + var sbs []*symbol.Symbol if len(args) > 1 { - sb = m.findSymbol(args[1]) + sbs = m.findSymbols(args[1]) + } else { + sbs = []*symbol.Symbol{m.debugger.Symbol()} + } + + if len(sbs) == 0 { + m.view = &errDebugView{err: fmt.Errorf("symbol '%s' not found", args[1])} + } else if len(sbs) == 1 { + m.view = &symbolDebugView{symbol: sbs[0]} } else { - sb = m.debugger.Symbol() + m.view = &symbolsDebugView{symbols: sbs} } - m.view = &symbolDebugView{symbol: sb} return m, nil case "processes", "procs": procs := m.agent.Processes() @@ -299,13 +320,14 @@ func (m *debugModel) nextInput(msg tea.Msg) tea.Cmd { return cmd } -func (m *debugModel) findSymbol(key string) *symbol.Symbol { +func (m *debugModel) findSymbols(key string) []*symbol.Symbol { + var symbols []*symbol.Symbol for _, sb := range m.agent.Symbols() { if sb.ID().String() == key || sb.Name() == key { - return sb + symbols = append(symbols, sb) } } - return nil + return symbols } func (m *debugModel) findPort(sb *symbol.Symbol, name string) (*port.InPort, *port.OutPort) { @@ -419,7 +441,7 @@ func (v *breakpointDebugView) Interface() map[string]any { return nil } - value := map[string]any{"id": v.id} + value := map[string]any{"id": v.breakpoint.ID()} sb := v.breakpoint.Symbol() if sb != nil { @@ -450,8 +472,8 @@ func (v *breakpointsDebugView) View() string { writer := resource.NewWriter(buffer) values := make([]any, 0, len(v.breakpoints)) - for i, b := range v.breakpoints { - value := (&breakpointDebugView{id: i, breakpoint: b}).Interface() + for _, b := range v.breakpoints { + value := (&breakpointDebugView{breakpoint: b}).Interface() values = append(values, value) } diff --git a/cmd/pkg/cli/debug_test.go b/cmd/pkg/cli/debug_test.go index 338deb27..588c29eb 100644 --- a/cmd/pkg/cli/debug_test.go +++ b/cmd/pkg/cli/debug_test.go @@ -32,7 +32,7 @@ func TestNewDebugger(t *testing.T) { } func TestDebugModel_Update(t *testing.T) { - t.Run("break", func(t *testing.T) { + t.Run("break ", func(t *testing.T) { a := agent.New() defer a.Close() @@ -65,6 +65,39 @@ func TestDebugModel_Update(t *testing.T) { assert.Len(t, d.Breakpoints(), 1) }) + t.Run("break", func(t *testing.T) { + a := agent.New() + defer a.Close() + + d := debug.NewDebugger(a) + defer d.Close() + + m := &debugModel{ + input: textinput.New(), + agent: a, + debugger: d, + } + + sb := &symbol.Symbol{ + Spec: &spec.Meta{ + ID: uuid.Must(uuid.NewV7()), + Kind: faker.UUIDHyphenated(), + Namespace: resource.DefaultNamespace, + Name: faker.UUIDHyphenated(), + }, + Node: node.NewOneToOneNode(nil), + } + defer sb.Close() + + m.agent.Load(sb) + defer m.agent.Unload(sb) + + m.input.SetValue("break") + m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + + assert.Len(t, d.Breakpoints(), 1) + }) + t.Run("continue", func(t *testing.T) { a := agent.New() defer a.Close() @@ -101,7 +134,7 @@ func TestDebugModel_Update(t *testing.T) { // TODO: assert }) - t.Run("delete", func(t *testing.T) { + t.Run("delete ", func(t *testing.T) { a := agent.New() defer a.Close() @@ -131,7 +164,7 @@ func TestDebugModel_Update(t *testing.T) { m.input.SetValue(fmt.Sprintf("break %s %s", sb.Name(), node.PortIn)) m.Update(tea.KeyMsg{Type: tea.KeyEnter}) - m.input.SetValue(fmt.Sprintf("delete %d", 0)) + m.input.SetValue(fmt.Sprintf("delete %s", d.Breakpoints()[0].ID())) m.Update(tea.KeyMsg{Type: tea.KeyEnter}) assert.Len(t, d.Breakpoints(), 0) diff --git a/cmd/pkg/resource/writer.go b/cmd/pkg/resource/writer.go index 3edb8b6b..4a39f700 100644 --- a/cmd/pkg/resource/writer.go +++ b/cmd/pkg/resource/writer.go @@ -3,6 +3,7 @@ package resource import ( "fmt" "io" + "math" "slices" "strings" @@ -61,13 +62,18 @@ func (w *Writer) Write(value any) error { counts[key]++ } } - sizes := map[string]int{} + + scores := map[string]float64{} for _, element := range elements { - for key, value := range element { - sizes[key] += len(fmt.Sprint(value)) + for key, val := range element { + scores[key] += 1.0 - 1.0/float64(len(fmt.Sprint(val))) } } + for key, score := range scores { + scores[key] = score / float64(counts[key]) + } + var keys []string for key, count := range counts { if count >= len(elements)/2 { @@ -79,8 +85,12 @@ func (w *Writer) Write(value any) error { if diff := counts[y] - counts[x]; diff != 0 { return diff } - if diff := sizes[x] - sizes[y]; diff != 0 { - return diff + if diff := scores[x] - scores[y]; math.Abs(diff) > 0.1 { + if diff > 0 { + return 1 + } else { + return -1 + } } return strings.Compare(x, y) }) diff --git a/cmd/pkg/resource/writer_test.go b/cmd/pkg/resource/writer_test.go index df689558..3e521351 100644 --- a/cmd/pkg/resource/writer_test.go +++ b/cmd/pkg/resource/writer_test.go @@ -18,7 +18,7 @@ func TestWriter_Write(t *testing.T) { "key1": "value1", "key2": 123, }, - expected: " KEY2 KEY1 \n 123 value1 ", + expected: " KEY1 KEY2 \n value1 123 ", }, { input: []map[string]any{ @@ -31,7 +31,7 @@ func TestWriter_Write(t *testing.T) { "key2": 456, }, }, - expected: " KEY2 KEY1 \n 123 value1 \n 456 value2 ", + expected: " KEY1 KEY2 \n value1 123 \n value2 456 ", }, { input: []map[string]any{ @@ -42,7 +42,7 @@ func TestWriter_Write(t *testing.T) { "key2": 456, }, }, - expected: " KEY2 KEY1 \n value1 \n 456 ", + expected: " KEY1 KEY2 \n value1 \n 456 ", }, } diff --git a/ext/pkg/network/http.go b/ext/pkg/network/http.go index 9babc8de..68e98568 100644 --- a/ext/pkg/network/http.go +++ b/ext/pkg/network/http.go @@ -53,12 +53,12 @@ const KindHTTP = "http" // NewHTTPNodeCodec creates a new codec for HTTPNode. func NewHTTPNodeCodec() scheme.Codec { return scheme.CodecWithType(func(spec *HTTPNodeSpec) (node.Node, error) { - url, err := url.Parse(spec.URL) + parse, err := url.Parse(spec.URL) if err != nil { return nil, err } - n := NewHTTPNode(url) + n := NewHTTPNode(parse) n.SetTimeout(spec.Timeout) return n, nil }) @@ -67,7 +67,7 @@ func NewHTTPNodeCodec() scheme.Codec { // NewHTTPNode creates a new HTTPNode instance. func NewHTTPNode(url *url.URL) *HTTPNode { transport := &http.Transport{} - http2.ConfigureTransport(transport) + _ = http2.ConfigureTransport(transport) client := &http.Client{ Transport: transport, @@ -176,10 +176,10 @@ func (n *HTTPNode) action(proc *process.Process, inPck *packet.Packet) (*packet. } // NewHTTPPayload creates a new HTTPPayload with the given HTTP status code and optional body. -func NewHTTPPayload(status int, bodys ...types.Value) *HTTPPayload { +func NewHTTPPayload(status int, bodies ...types.Value) *HTTPPayload { var body types.Value = types.NewString(http.StatusText(status)) - if len(bodys) > 0 { - body = bodys[0] + if len(bodies) > 0 { + body = bodies[0] } return &HTTPPayload{ Header: http.Header{}, diff --git a/ext/pkg/network/listener.go b/ext/pkg/network/listener.go index 8f801414..c685eea1 100644 --- a/ext/pkg/network/listener.go +++ b/ext/pkg/network/listener.go @@ -60,7 +60,7 @@ func NewListenNodeCodec() scheme.Codec { n := NewHTTPListenNode(fmt.Sprintf("%s:%d", spec.Host, spec.Port)) if spec.Cert != "" || spec.Key != "" { if err := n.TLS(spec.Cert, spec.Key); err != nil { - n.Close() + _ = n.Close() return nil, err } } @@ -226,7 +226,7 @@ func (n *HTTPListenNode) ServeHTTP(w http.ResponseWriter, r *http.Request) { if w, ok := proc.LoadAndDelete(KeyHTTPResponseWriter).(http.ResponseWriter); ok { n.negotiate(req, res) - n.write(w, res) + _ = n.write(w, res) } } diff --git a/ext/pkg/network/protocol.go b/ext/pkg/network/protocol.go index 5c6b5ba5..755163c2 100644 --- a/ext/pkg/network/protocol.go +++ b/ext/pkg/network/protocol.go @@ -3,7 +3,6 @@ package network import "github.com/pkg/errors" const ProtocolHTTP = "http" -const ProtocolH2C = "h2c" const ProtocolWebsocket = "websocket" var ErrInvalidProtocol = errors.New("protocol is invalid") diff --git a/ext/pkg/network/websocket.go b/ext/pkg/network/websocket.go index 2cce48a4..a396f822 100644 --- a/ext/pkg/network/websocket.go +++ b/ext/pkg/network/websocket.go @@ -182,7 +182,7 @@ func (n *WebSocketConnNode) connect(proc *process.Process) { child := proc.Fork() child.AddExitHook(process.ExitFunc(func(_ error) { - conn.Close() + _ = conn.Close() })) ioReader.Receive(packet.None) diff --git a/pkg/debug/breakpoint.go b/pkg/debug/breakpoint.go index b92e9d11..5d66c336 100644 --- a/pkg/debug/breakpoint.go +++ b/pkg/debug/breakpoint.go @@ -1,6 +1,7 @@ package debug import ( + "github.com/gofrs/uuid" "sync" "github.com/siyul-park/uniflow/pkg/agent" @@ -11,6 +12,7 @@ import ( // Breakpoint represents a synchronization point in a process where execution can be paused and resumed. type Breakpoint struct { + id uuid.UUID process *process.Process symbol *symbol.Symbol inPort *port.InPort @@ -28,6 +30,7 @@ var _ agent.Watcher = (*Breakpoint)(nil) // NewBreakpoint creates a new Breakpoint with optional configurations. func NewBreakpoint(options ...func(*Breakpoint)) *Breakpoint { b := &Breakpoint{ + id: uuid.Must(uuid.NewV7()), in: make(chan *agent.Frame), out: make(chan *agent.Frame), done: make(chan struct{}), @@ -58,6 +61,11 @@ func WithOutPort(port *port.OutPort) func(*Breakpoint) { return func(b *Breakpoint) { b.outPort = port } } +// ID returns the unique identifier. +func (b *Breakpoint) ID() uuid.UUID { + return b.id +} + // Next advances to the next frame, returning false if closed. func (b *Breakpoint) Next() bool { b.Done() diff --git a/pkg/debug/breakpoint_test.go b/pkg/debug/breakpoint_test.go index 25891451..89e13059 100644 --- a/pkg/debug/breakpoint_test.go +++ b/pkg/debug/breakpoint_test.go @@ -37,6 +37,7 @@ func TestNewBreakpoint(t *testing.T) { ) defer b.Close() + assert.NotZero(t, proc.ID()) assert.Equal(t, proc, b.Process()) assert.Equal(t, sb, b.Symbol()) assert.Equal(t, sb.In(node.PortIn), b.InPort())