diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8792e9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: Run tests and upload coverage + +on: + push + +jobs: + test: + name: Run tests and collect coverage + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + + - name: Install dependencies + run: go mod download + + - name: Run tests + run: go test -coverprofile=coverage.txt + + - name: Upload results to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/component/component.go b/component/component.go index f125cc8..c88cda6 100644 --- a/component/component.go +++ b/component/component.go @@ -54,14 +54,14 @@ func (c *Component) Activate() (aRes hop.ActivationResult) { } if !errors.Is(err, ErrWaitingForInputKeepInputs) { - c.Inputs.ClearAll() + c.Inputs.ClearSignal() } return } //Clear Inputs - c.Inputs.ClearAll() + c.Inputs.ClearSignal() if err != nil { aRes = hop.ActivationResult{ @@ -84,11 +84,11 @@ func (c *Component) Activate() (aRes hop.ActivationResult) { func (c *Component) FlushOutputs() { for _, out := range c.Outputs { - if !out.HasSignal() || len(out.Pipes) == 0 { + if !out.HasSignal() || len(out.Pipes()) == 0 { continue } - for _, pipe := range out.Pipes { + for _, pipe := range out.Pipes() { //Multiplexing pipe.Flush() } diff --git a/examples/basic.go b/examples/basic.go index cb456d7..b6b595c 100644 --- a/examples/basic.go +++ b/examples/basic.go @@ -16,12 +16,8 @@ func main() { c1 := &component.Component{ Name: "adder", Description: "adds 2 to the input", - Inputs: port.Ports{ - "num": &port.Port{}, - }, - Outputs: port.Ports{ - "res": &port.Port{}, - }, + Inputs: port.NewPorts("num"), + Outputs: port.NewPorts("res"), ActivationFunc: func(inputs port.Ports, outputs port.Ports) error { num := inputs.ByName("num").Signal().Payload().(int) outputs.ByName("res").PutSignal(signal.New(num + 2)) @@ -32,12 +28,8 @@ func main() { c2 := &component.Component{ Name: "multiplier", Description: "multiplies by 3", - Inputs: port.Ports{ - "num": &port.Port{}, - }, - Outputs: port.Ports{ - "res": &port.Port{}, - }, + Inputs: port.NewPorts("num"), + Outputs: port.NewPorts("res"), ActivationFunc: func(inputs port.Ports, outputs port.Ports) error { num := inputs.ByName("num").Signal().Payload().(int) outputs.ByName("res").PutSignal(signal.New(num * 3)) diff --git a/port/pipe.go b/port/pipe.go index 6e4bf82..f6823a4 100644 --- a/port/pipe.go +++ b/port/pipe.go @@ -1,12 +1,23 @@ package port +// Pipe is the connection between two ports type Pipe struct { From *Port To *Port } +// Pipes is a useful collection type type Pipes []*Pipe +// NewPipe returns new pipe +func NewPipe(from *Port, to *Port) *Pipe { + return &Pipe{ + From: from, + To: to, + } +} + +// Flush makes the signals flow from "From" to "To" port (From is not cleared) func (p *Pipe) Flush() { ForwardSignal(p.From, p.To) } diff --git a/port/pipe_test.go b/port/pipe_test.go new file mode 100644 index 0000000..3cf4da6 --- /dev/null +++ b/port/pipe_test.go @@ -0,0 +1,77 @@ +package port + +import ( + "github.com/hovsep/fmesh/signal" + "reflect" + "testing" +) + +func TestNewPipe(t *testing.T) { + p1, p2 := NewPort(), NewPort() + + type args struct { + from *Port + to *Port + } + tests := []struct { + name string + args args + want *Pipe + }{ + { + name: "happy path", + args: args{ + from: p1, + to: p2, + }, + want: &Pipe{ + From: p1, + To: p2, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewPipe(tt.args.from, tt.args.to); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewPipe() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPipe_Flush(t *testing.T) { + portWithSignal := NewPort() + portWithSignal.PutSignal(signal.New(777)) + + portWithMultipleSignals := NewPort() + portWithMultipleSignals.PutSignal(signal.New(11, 12)) + + emptyPort := NewPort() + + tests := []struct { + name string + before *Pipe + after *Pipe + }{ + { + name: "flush to empty port", + before: NewPipe(portWithSignal, emptyPort), + after: NewPipe(portWithSignal, portWithSignal), + }, + { + name: "flush to port with signal", + before: NewPipe(portWithSignal, portWithMultipleSignals), + after: NewPipe(portWithSignal, &Port{ + signal: signal.New(777, 11, 12), + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before.Flush() + if !reflect.DeepEqual(tt.before, tt.after) { + t.Errorf("Flush() = %v, want %v", tt.before, tt.after) + } + }) + } +} diff --git a/port/port.go b/port/port.go index 40fe0f0..e05a367 100644 --- a/port/port.go +++ b/port/port.go @@ -4,57 +4,79 @@ import ( "github.com/hovsep/fmesh/signal" ) +// Port defines a connectivity point of a component type Port struct { - signal *signal.Signal - Pipes Pipes //Refs to Pipes connected to that port (no in\out semantics) + signal *signal.Signal //Current signal set on the port + pipes Pipes //Refs to pipes connected to this port (without in\out semantics) } +// Ports is just useful collection type type Ports map[string]*Port +// NewPort creates a new port +func NewPort() *Port { + return &Port{} +} + +// NewPorts creates a new port with the given name +func NewPorts(names ...string) Ports { + ports := make(Ports, len(names)) + for _, name := range names { + ports[name] = NewPort() + } + return ports +} + +func (p *Port) Pipes() Pipes { + return p.pipes +} + +// Signal returns current signal set on the port func (p *Port) Signal() *signal.Signal { return p.signal } +// PutSignal adds a signal to current signal func (p *Port) PutSignal(sig *signal.Signal) { - p.signal = sig.Merge(p.Signal()) + p.signal = sig.Combine(p.Signal()) } +// ClearSignal removes current signal from the port +// @TODO: check if this affects the signal itself, as it is a pointer func (p *Port) ClearSignal() { p.signal = nil } +// HasSignal says whether port signal is set or not func (p *Port) HasSignal() bool { return p.signal != nil } -// Adds pipe reference to port, so all Pipes of the port are easily iterable (no in\out semantics) +// Adds pipe reference to the port, so all pipes of the port are easily accessible func (p *Port) addPipeRef(pipe *Pipe) { - p.Pipes = append(p.Pipes, pipe) + if pipe.From == nil || pipe.To == nil { + return + } + p.pipes = append(p.pipes, pipe) } -// PipeTo creates multiple pipes to other ports +// PipeTo creates one or multiple pipes to other port(s) func (p *Port) PipeTo(toPorts ...*Port) { for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } + newPipe := NewPipe(p, toPort) p.addPipeRef(newPipe) toPort.addPipeRef(newPipe) } } -// @TODO: this type must have good tooling for working with collection -// like adding new ports, filtering and so on - -// @TODO: add error handling (e.g. when port does not exist) +// ByName returns a port by its name func (ports Ports) ByName(name string) *Port { return ports[name] } -// Deprecated, use ByName instead -func (ports Ports) ManyByName(names ...string) Ports { +// ByNames returns multiple ports by their names +func (ports Ports) ByNames(names ...string) Ports { selectedPorts := make(Ports) for _, name := range names { @@ -66,6 +88,7 @@ func (ports Ports) ManyByName(names ...string) Ports { return selectedPorts } +// AnyHasSignal returns true if at least one port in collection has signal func (ports Ports) AnyHasSignal() bool { for _, p := range ports { if p.HasSignal() { @@ -76,6 +99,7 @@ func (ports Ports) AnyHasSignal() bool { return false } +// AllHaveSignal returns true when all ports in collection have signal func (ports Ports) AllHaveSignal() bool { for _, p := range ports { if !p.HasSignal() { @@ -86,18 +110,21 @@ func (ports Ports) AllHaveSignal() bool { return true } +// PutSignal puts a signal to all the port in collection func (ports Ports) PutSignal(sig *signal.Signal) { for _, p := range ports { p.PutSignal(sig) } } -func (ports Ports) ClearAll() { +// ClearSignal removes signals from all ports in collection +func (ports Ports) ClearSignal() { for _, p := range ports { p.ClearSignal() } } +// ForwardSignal puts a signal from source port to dest port, without removing it on source port func ForwardSignal(source *Port, dest *Port) { dest.PutSignal(source.Signal()) } diff --git a/port/port_test.go b/port/port_test.go new file mode 100644 index 0000000..cd7c6ea --- /dev/null +++ b/port/port_test.go @@ -0,0 +1,528 @@ +package port + +import ( + "github.com/hovsep/fmesh/signal" + "reflect" + "testing" +) + +func TestNewPorts(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + args args + want Ports + }{ + { + name: "no names", + args: args{ + names: nil, + }, + want: Ports{}, + }, + { + name: "happy path", + args: args{ + names: []string{"i1", "i2"}, + }, + want: Ports{ + "i1": {}, + "i2": {}, + }, + }, + { + name: "duplicate names are ignored", + args: args{ + names: []string{"i1", "i2", "i1"}, + }, + want: Ports{ + "i1": {}, + "i2": {}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewPorts(tt.args.names...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewPorts() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPort_HasSignal(t *testing.T) { + portWithSignal := NewPort() + portWithSignal.PutSignal(signal.New(123)) + + portWithEmptySignal := NewPort() + portWithEmptySignal.PutSignal(signal.New()) + + tests := []struct { + name string + port *Port + want bool + }{ + { + name: "empty port", + port: NewPort(), + want: false, + }, + { + name: "port has normal signal", + port: portWithSignal, + want: true, + }, + { + name: "port has empty signal", + port: portWithEmptySignal, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.port.HasSignal(); got != tt.want { + t.Errorf("HasSignal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPort_Pipes(t *testing.T) { + destPort1, destPort2, destPort3 := NewPort(), NewPort(), NewPort() + portWithOnePipe := NewPort() + portWithOnePipe.PipeTo(destPort1) + + portWithMultiplePipes := NewPort() + portWithMultiplePipes.PipeTo(destPort2, destPort3) + + tests := []struct { + name string + port *Port + want Pipes + }{ + { + name: "no pipes", + port: NewPort(), + want: nil, + }, + { + name: "one pipe", + port: portWithOnePipe, + want: Pipes{ + { + From: portWithOnePipe, + To: destPort1, + }, + }, + }, + { + name: "multiple pipes", + port: portWithMultiplePipes, + want: Pipes{ + { + From: portWithMultiplePipes, + To: destPort2, + }, + { + From: portWithMultiplePipes, + To: destPort3, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.port.Pipes(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Pipes() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPort_Signal(t *testing.T) { + portWithSignal := NewPort() + portWithSignal.PutSignal(signal.New(123)) + + portWithEmptySignal := NewPort() + portWithEmptySignal.PutSignal(signal.New()) + + tests := []struct { + name string + port *Port + want *signal.Signal + }{ + { + name: "no signal", + port: NewPort(), + want: nil, + }, + { + name: "with signal", + port: portWithSignal, + want: signal.New(123), + }, + { + name: "with empty signal", + port: portWithEmptySignal, + want: signal.New(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.port.Signal(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPorts_AllHaveSignal(t *testing.T) { + oneEmptyPorts := NewPorts("p1", "p2", "p3") + oneEmptyPorts.PutSignal(signal.New(123)) + oneEmptyPorts.ByName("p2").ClearSignal() + + allWithSignalPorts := NewPorts("out1", "out2", "out3") + allWithSignalPorts.PutSignal(signal.New(77)) + + allWithEmptySignalPorts := NewPorts("in1", "in2", "in3") + allWithEmptySignalPorts.PutSignal(signal.New()) + + tests := []struct { + name string + ports Ports + want bool + }{ + { + name: "all empty", + ports: NewPorts("p1", "p2"), + want: false, + }, + { + name: "one empty", + ports: oneEmptyPorts, + want: false, + }, + { + name: "all set", + ports: allWithSignalPorts, + want: true, + }, + { + name: "all set with empty signals", + ports: allWithEmptySignalPorts, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ports.AllHaveSignal(); got != tt.want { + t.Errorf("AllHaveSignal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPorts_AnyHasSignal(t *testing.T) { + oneEmptyPorts := NewPorts("p1", "p2", "p3") + oneEmptyPorts.PutSignal(signal.New(123)) + oneEmptyPorts.ByName("p2").ClearSignal() + + tests := []struct { + name string + ports Ports + want bool + }{ + { + name: "one empty", + ports: oneEmptyPorts, + want: true, + }, + { + name: "all empty", + ports: NewPorts("p1", "p2", "p3"), + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ports.AnyHasSignal(); got != tt.want { + t.Errorf("AnyHasSignal() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPorts_ByName(t *testing.T) { + portsWithSignals := NewPorts("p1", "p2") + portsWithSignals.PutSignal(signal.New(12)) + + type args struct { + name string + } + tests := []struct { + name string + ports Ports + args args + want *Port + }{ + { + name: "empty port found", + ports: NewPorts("p1", "p2"), + args: args{ + name: "p1", + }, + want: &Port{}, + }, + { + name: "port with signal found", + ports: portsWithSignals, + args: args{ + name: "p2", + }, + want: &Port{ + signal: signal.New(12), + }, + }, + { + name: "port not found", + ports: NewPorts("p1", "p2"), + args: args{ + name: "p3", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ports.ByName(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ByName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPorts_ByNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + ports Ports + args args + want Ports + }{ + { + name: "single port found", + ports: NewPorts("p1", "p2"), + args: args{ + names: []string{"p1"}, + }, + want: Ports{ + "p1": NewPort(), + }, + }, + { + name: "multiple ports found", + ports: NewPorts("p1", "p2"), + args: args{ + names: []string{"p1", "p2"}, + }, + want: Ports{ + "p1": NewPort(), + "p2": NewPort(), + }, + }, + { + name: "single port not found", + ports: NewPorts("p1", "p2"), + args: args{ + names: []string{"p7"}, + }, + want: Ports{}, + }, + { + name: "some ports not found", + ports: NewPorts("p1", "p2"), + args: args{ + names: []string{"p1", "p2", "p3"}, + }, + want: Ports{ + "p1": NewPort(), + "p2": NewPort(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ports.ByNames(tt.args.names...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ByNames() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPort_ClearSignal(t *testing.T) { + portWithSignal := NewPort() + portWithSignal.PutSignal(signal.New(111)) + + tests := []struct { + name string + before *Port + after *Port + }{ + { + name: "happy path", + before: portWithSignal, + after: NewPort(), + }, + { + name: "cleaning empty port", + before: NewPort(), + after: NewPort(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before.ClearSignal() + if !reflect.DeepEqual(tt.before, tt.after) { + t.Errorf("ClearSignal() = %v, want %v", tt.before, tt.after) + } + }) + } +} + +func TestPort_PipeTo(t *testing.T) { + p1, p2, p3, p4 := NewPort(), NewPort(), NewPort(), NewPort() + + type args struct { + toPorts []*Port + } + tests := []struct { + name string + before *Port + after *Port + args args + }{ + { + name: "happy path", + before: p1, + after: &Port{ + pipes: Pipes{ + { + From: p1, + To: p2, + }, + { + From: p1, + To: p3, + }, + }, + }, + args: args{ + toPorts: []*Port{p2, p3}, + }, + }, + { + name: "invalid ports are ignored", + before: p4, + after: &Port{ + pipes: Pipes{ + { + From: p4, + To: p2, + }, + }, + }, + args: args{ + toPorts: []*Port{p2, nil}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before.PipeTo(tt.args.toPorts...) + if !reflect.DeepEqual(tt.before, tt.after) { + t.Errorf("PipeTo() = %v, want %v", tt.before, tt.after) + } + }) + } +} + +func TestPort_PutSignal(t *testing.T) { + portWithSingleSignal := NewPort() + portWithSingleSignal.PutSignal(signal.New(11)) + + portWithMultipleSignals := NewPort() + portWithMultipleSignals.PutSignal(signal.New(11, 12)) + + type args struct { + sig *signal.Signal + } + tests := []struct { + name string + before *Port + after *Port + args args + }{ + { + name: "single signal to empty port", + before: NewPort(), + after: &Port{ + signal: signal.New(11), + }, + args: args{ + sig: signal.New(11), + }, + }, + { + name: "multiple signals to empty port", + before: NewPort(), + after: &Port{ + signal: signal.New(11, 12), + }, + args: args{ + sig: signal.New(11, 12), + }, + }, + { + name: "single signal to port with single signal", + before: portWithSingleSignal, + after: &Port{ + signal: signal.New(12, 11), //Notice LIFO order + }, + args: args{ + sig: signal.New(12), + }, + }, + { + name: "single signal to port with multiple signals", + before: portWithMultipleSignals, + after: &Port{ + signal: signal.New(13, 11, 12), //Notice LIFO order + }, + args: args{ + sig: signal.New(13), + }, + }, + { + name: "multiple signals to port with multiple signals", + before: portWithMultipleSignals, + after: &Port{ + signal: signal.New(13, 14, 11, 12), //Notice LIFO order + }, + args: args{ + sig: signal.New(13, 14), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before.PutSignal(tt.args.sig) + if !reflect.DeepEqual(tt.before, tt.after) { + t.Errorf("ClearSignal() = %v, want %v", tt.before, tt.after) + } + }) + } +} diff --git a/signal/signal.go b/signal/signal.go index ef790c6..2c0058e 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -15,12 +15,17 @@ func (s *Signal) Len() int { return len(s.payloads) } +// HasPayload must be used to check whether signal carries at least 1 payload +func (s *Signal) HasPayload() bool { + return s.Len() > 0 +} + // Payloads returns all payloads func (s *Signal) Payloads() []any { return s.payloads } -// Payload returns the first payloads (useful when you are sure there is just one payloads) +// Payload returns the first payload (useful when you are sure signal has only one payload) // It panics when used with signal that carries multiple payloads func (s *Signal) Payload() any { if s.Len() != 1 { @@ -29,8 +34,8 @@ func (s *Signal) Payload() any { return s.payloads[0] } -// Merge returns a new signal which payloads is combined from 2 original signals -func (s *Signal) Merge(anotherSignal *Signal) *Signal { +// Combine returns a new signal with combined payloads of 2 original signals +func (s *Signal) Combine(anotherSignal *Signal) *Signal { //Merging with nothing if anotherSignal == nil || anotherSignal.Payloads() == nil { return s diff --git a/signal/signal_test.go b/signal/signal_test.go index ff3390d..0f5e374 100644 --- a/signal/signal_test.go +++ b/signal/signal_test.go @@ -88,7 +88,7 @@ func TestSignal_Len(t *testing.T) { } } -func TestSignal_Merge(t *testing.T) { +func TestSignal_Combine(t *testing.T) { tests := []struct { name string sigA *Signal @@ -130,8 +130,8 @@ func TestSignal_Merge(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.sigA.Merge(tt.sigB); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Merge() = %v, want %v", got, tt.want) + if got := tt.sigA.Combine(tt.sigB); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Combine() = %v, want %v", got, tt.want) } }) }