diff --git a/component/activation_result.go b/component/activation_result.go index 10cc6a5..ea5ae85 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -10,9 +10,6 @@ type ActivationResult struct { err error } -// ActivationResults is a collection -type ActivationResults map[string]*ActivationResult - // ActivationResultCode denotes a specific info about how a component been activated or why not activated at all type ActivationResultCode int @@ -43,6 +40,36 @@ func NewActivationResult(componentName string) *ActivationResult { } } +// ComponentName getter +func (ar *ActivationResult) ComponentName() string { + return ar.componentName +} + +// Activated getter +func (ar *ActivationResult) Activated() bool { + return ar.activated +} + +// Error getter +func (ar *ActivationResult) Error() error { + return ar.err +} + +// Code getter +func (ar *ActivationResult) Code() ActivationResultCode { + return ar.code +} + +// HasError returns true when activation result has an error +func (ar *ActivationResult) HasError() bool { + return ar.code == ActivationCodeReturnedError && ar.Error() != nil +} + +// HasPanic returns true when activation result is derived from panic +func (ar *ActivationResult) HasPanic() bool { + return ar.code == ActivationCodePanicked && ar.Error() != nil +} + // SetActivated setter func (ar *ActivationResult) SetActivated(activated bool) *ActivationResult { ar.activated = activated @@ -83,43 +110,10 @@ func (c *Component) newActivationCodeWaitingForInput() *ActivationResult { // newActivationCodeReturnedError builds a specific activation result func (c *Component) newActivationCodeReturnedError(err error) *ActivationResult { - return NewActivationResult(c.Name()). - SetActivated(true). - WithActivationCode(ActivationCodeReturnedError). - WithError(fmt.Errorf("component returned an error: %w", err)) + return NewActivationResult(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeReturnedError).WithError(fmt.Errorf("component returned an error: %w", err)) } // newActivationCodePanicked builds a specific activation result func (c *Component) newActivationCodePanicked(err error) *ActivationResult { return NewActivationResult(c.Name()).SetActivated(true).WithActivationCode(ActivationCodePanicked).WithError(err) } - -// ComponentName getter -func (ar *ActivationResult) ComponentName() string { - return ar.componentName -} - -// Activated getter -func (ar *ActivationResult) Activated() bool { - return ar.activated -} - -// Error getter -func (ar *ActivationResult) Error() error { - return ar.err -} - -// Code getter -func (ar *ActivationResult) Code() ActivationResultCode { - return ar.code -} - -// HasError returns true when activation result has an error -func (ar *ActivationResult) HasError() bool { - return ar.code == ActivationCodeReturnedError && ar.Error() != nil -} - -// HasPanic returns true when activation result is derived from panic -func (ar *ActivationResult) HasPanic() bool { - return ar.code == ActivationCodePanicked && ar.Error() != nil -} diff --git a/component/activation_result_collection.go b/component/activation_result_collection.go new file mode 100644 index 0000000..956128d --- /dev/null +++ b/component/activation_result_collection.go @@ -0,0 +1,47 @@ +package component + +// ActivationResultCollection is a collection +type ActivationResultCollection map[string]*ActivationResult + +// NewActivationResultCollection creates empty collection +func NewActivationResultCollection() ActivationResultCollection { + return make(ActivationResultCollection) +} + +// Add adds multiple activation results +func (collection ActivationResultCollection) Add(activationResults ...*ActivationResult) ActivationResultCollection { + for _, activationResult := range activationResults { + collection[activationResult.ComponentName()] = activationResult + } + return collection +} + +// HasErrors tells whether the collection contains at least one activation result with error and respective code +func (collection ActivationResultCollection) HasErrors() bool { + for _, ar := range collection { + if ar.HasError() { + return true + } + } + return false +} + +// HasPanics tells whether the collection contains at least one activation result with panic and respective code +func (collection ActivationResultCollection) HasPanics() bool { + for _, ar := range collection { + if ar.HasPanic() { + return true + } + } + return false +} + +// HasActivatedComponents tells when at least one component in the cycle has activated +func (collection ActivationResultCollection) HasActivatedComponents() bool { + for _, ar := range collection { + if ar.Activated() { + return true + } + } + return false +} diff --git a/component/activation_result_collection_test.go b/component/activation_result_collection_test.go new file mode 100644 index 0000000..440b471 --- /dev/null +++ b/component/activation_result_collection_test.go @@ -0,0 +1,76 @@ +package component + +import ( + "errors" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestActivationResultCollection_Add(t *testing.T) { + type args struct { + activationResults []*ActivationResult + } + tests := []struct { + name string + collection ActivationResultCollection + args args + assertions func(t *testing.T, collection ActivationResultCollection) + }{ + { + name: "adding nothing to empty collection", + collection: NewActivationResultCollection(), + args: args{ + activationResults: nil, + }, + assertions: func(t *testing.T, collection ActivationResultCollection) { + assert.Len(t, collection, 0) + assert.False(t, collection.HasErrors()) + assert.False(t, collection.HasPanics()) + assert.False(t, collection.HasActivatedComponents()) + }, + }, + { + name: "adding to empty collection", + collection: NewActivationResultCollection(), + args: args{ + activationResults: []*ActivationResult{ + NewComponent("c1").newActivationResultOK(), + NewComponent("c2").newActivationCodeReturnedError(errors.New("oops")), + }, + }, + assertions: func(t *testing.T, collection ActivationResultCollection) { + assert.Len(t, collection, 2) + assert.True(t, collection.HasActivatedComponents()) + assert.True(t, collection.HasErrors()) + assert.False(t, collection.HasPanics()) + }, + }, + { + name: "adding to existing collection", + collection: NewActivationResultCollection().Add( + NewComponent("c1").newActivationResultOK(), + NewComponent("c2").newActivationResultOK(), + ), + args: args{ + activationResults: []*ActivationResult{ + NewComponent("c4").newActivationCodeNoInput(), + NewComponent("c5").newActivationCodePanicked(errors.New("panic")), + }, + }, + assertions: func(t *testing.T, collection ActivationResultCollection) { + assert.Len(t, collection, 4) + assert.True(t, collection.HasPanics()) + assert.False(t, collection.HasErrors()) + assert.True(t, collection.HasActivatedComponents()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.collection.Add(tt.args.activationResults...) + if tt.assertions != nil { + tt.assertions(t, tt.collection) + } + }) + } +} diff --git a/component/collection.go b/component/collection.go new file mode 100644 index 0000000..3ba87b1 --- /dev/null +++ b/component/collection.go @@ -0,0 +1,22 @@ +package component + +// ComponentCollection is a collection of components with useful methods +type Collection map[string]*Component + +// NewComponentCollection creates empty collection +func NewComponentCollection() Collection { + return make(Collection) +} + +// ByName returns a component by its name +func (collection Collection) ByName(name string) *Component { + return collection[name] +} + +// Add adds new components to existing collection +func (collection Collection) Add(newComponents ...*Component) Collection { + for _, component := range newComponents { + collection[component.Name()] = component + } + return collection +} diff --git a/component/collection_test.go b/component/collection_test.go new file mode 100644 index 0000000..4d4e559 --- /dev/null +++ b/component/collection_test.go @@ -0,0 +1,109 @@ +package component + +import ( + "github.com/hovsep/fmesh/port" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestCollection_ByName(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + components Collection + args args + want *Component + }{ + { + name: "component found", + components: NewComponentCollection().Add(NewComponent("c1"), NewComponent("c2")), + args: args{ + name: "c2", + }, + want: &Component{ + name: "c2", + description: "", + inputs: port.Collection{}, + outputs: port.Collection{}, + f: nil, + }, + }, + { + name: "component not found", + components: NewComponentCollection().Add(NewComponent("c1"), NewComponent("c2")), + args: args{ + name: "c3", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.components.ByName(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ByName() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCollection_Add(t *testing.T) { + type args struct { + components []*Component + } + tests := []struct { + name string + collection Collection + args args + assertions func(t *testing.T, collection Collection) + }{ + { + name: "adding nothing to empty collection", + collection: NewComponentCollection(), + args: args{ + components: nil, + }, + assertions: func(t *testing.T, collection Collection) { + assert.Len(t, collection, 0) + }, + }, + { + name: "adding to empty collection", + collection: NewComponentCollection(), + args: args{ + components: []*Component{NewComponent("c1"), NewComponent("c2")}, + }, + assertions: func(t *testing.T, collection Collection) { + assert.Len(t, collection, 2) + assert.NotNil(t, collection.ByName("c1")) + assert.NotNil(t, collection.ByName("c2")) + assert.Nil(t, collection.ByName("c999")) + }, + }, + { + name: "adding to existing collection", + collection: NewComponentCollection().Add(NewComponent("c1"), NewComponent("c2")), + args: args{ + components: []*Component{NewComponent("c3"), NewComponent("c4")}, + }, + assertions: func(t *testing.T, collection Collection) { + assert.Len(t, collection, 4) + assert.NotNil(t, collection.ByName("c1")) + assert.NotNil(t, collection.ByName("c2")) + assert.NotNil(t, collection.ByName("c3")) + assert.NotNil(t, collection.ByName("c4")) + assert.Nil(t, collection.ByName("c999")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.collection.Add(tt.args.components...) + if tt.assertions != nil { + tt.assertions(t, tt.collection) + } + }) + } +} diff --git a/component/component.go b/component/component.go index e5a86e8..ee6b8e8 100644 --- a/component/component.go +++ b/component/component.go @@ -6,35 +6,24 @@ import ( "github.com/hovsep/fmesh/port" ) -type ActivationFunc func(inputs port.Ports, outputs port.Ports) error +type ActivationFunc func(inputs port.Collection, outputs port.Collection) error // Component defines a main building block of FMesh type Component struct { name string description string - inputs port.Ports - outputs port.Ports + inputs port.Collection + outputs port.Collection f ActivationFunc } -// Components is a useful collection type -type Components map[string]*Component - // NewComponent creates a new empty component -// TODO: rename all constructors to New func NewComponent(name string) *Component { - return &Component{name: name} -} - -// NewComponents creates a collection of components -// names are optional and can be used to create multiple empty components in one call -// @TODO: rename all such constructors to NewCollection -func NewComponents(names ...string) Components { - components := make(Components, len(names)) - for _, name := range names { - components[name] = NewComponent(name) + return &Component{ + name: name, + inputs: port.NewPortsCollection(), + outputs: port.NewPortsCollection(), } - return components } // WithDescription sets a description @@ -43,15 +32,15 @@ func (c *Component) WithDescription(description string) *Component { return c } -// WithInputs creates and sets input ports +// WithInputs ads input ports func (c *Component) WithInputs(portNames ...string) *Component { - c.inputs = port.NewPorts(portNames...) + c.inputs.Add(port.NewPortGroup(portNames...)...) return c } -// WithOutputs creates and sets output ports +// WithOutputs adds output ports func (c *Component) WithOutputs(portNames ...string) *Component { - c.outputs = port.NewPorts(portNames...) + c.outputs.Add(port.NewPortGroup(portNames...)...) return c } @@ -72,12 +61,12 @@ func (c *Component) Description() string { } // Inputs getter -func (c *Component) Inputs() port.Ports { +func (c *Component) Inputs() port.Collection { return c.inputs } // Outputs getter -func (c *Component) Outputs() port.Ports { +func (c *Component) Outputs() port.Collection { return c.outputs } @@ -141,27 +130,6 @@ func (c *Component) MaybeActivate() (activationResult *ActivationResult) { // FlushOutputs pushed signals out of the component outputs to pipes and clears outputs func (c *Component) FlushOutputs() { for _, out := range c.outputs { - if !out.HasSignal() || len(out.Pipes()) == 0 { - continue - } - - for _, pipe := range out.Pipes() { - //Multiplexing - pipe.Flush() - } - out.ClearSignal() - } -} - -// ByName returns a component by its name -func (components Components) ByName(name string) *Component { - return components[name] -} - -// Add adds new components to existing collection -func (components Components) Add(newComponents ...*Component) Components { - for _, component := range newComponents { - components[component.Name()] = component + out.Flush() } - return components } diff --git a/component/component_test.go b/component/component_test.go index af7e4c2..2875d4d 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -9,6 +9,75 @@ import ( "testing" ) +func TestNewComponent(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want *Component + }{ + { + name: "empty name is valid", + args: args{ + name: "", + }, + want: &Component{ + name: "", + description: "", + inputs: port.Collection{}, + outputs: port.Collection{}, + f: nil, + }, + }, + { + name: "with name", + args: args{ + name: "multiplier", + }, + want: &Component{ + name: "multiplier", + description: "", + inputs: port.Collection{}, + outputs: port.Collection{}, + f: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, NewComponent(tt.args.name), "NewComponent(%v)", tt.args.name) + }) + } +} + +func TestComponent_Name(t *testing.T) { + tests := []struct { + name string + component *Component + want string + }{ + { + name: "empty name", + component: NewComponent(""), + want: "", + }, + { + name: "with name", + component: NewComponent("c1"), + want: "c1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.component.Name(); got != tt.want { + t.Errorf("Name() = %v, want %v", got, tt.want) + } + }) + } +} + func TestComponent_Description(t *testing.T) { tests := []struct { name string @@ -55,7 +124,8 @@ func TestComponent_FlushOutputs(t *testing.T) { component: NewComponent("c1"), destPort: nil, assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { - assert.Nil(t, componentAfterFlush.Outputs()) + assert.NotNil(t, componentAfterFlush.Outputs()) + assert.Empty(t, componentAfterFlush.Outputs()) }, }, { @@ -90,17 +160,17 @@ func TestComponent_Inputs(t *testing.T) { tests := []struct { name string component *Component - want port.Ports + want port.Collection }{ { name: "no inputs", component: NewComponent("c1"), - want: nil, + want: port.Collection{}, }, { name: "with inputs", component: NewComponent("c1").WithInputs("i1", "i2"), - want: port.Ports{ + want: port.Collection{ "i1": port.NewPort("i1"), "i2": port.NewPort("i2"), }, @@ -115,47 +185,21 @@ func TestComponent_Inputs(t *testing.T) { } } -func TestComponent_Name(t *testing.T) { - tests := []struct { - name string - component *Component - want string - }{ - { - name: "empty name", - component: NewComponent(""), - want: "", - }, - { - name: "with name", - component: NewComponent("c1"), - want: "c1", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.component.Name(); got != tt.want { - t.Errorf("Name() = %v, want %v", got, tt.want) - } - }) - } -} - func TestComponent_Outputs(t *testing.T) { tests := []struct { name string component *Component - want port.Ports + want port.Collection }{ { name: "no outputs", component: NewComponent("c1"), - want: nil, + want: port.Collection{}, }, { name: "with outputs", component: NewComponent("c1").WithOutputs("o1", "o2"), - want: port.Ports{ + want: port.Collection{ "o1": port.NewPort("o1"), "o2": port.NewPort("o2"), }, @@ -184,7 +228,7 @@ func TestComponent_WithActivationFunc(t *testing.T) { name: "happy path", component: NewComponent("c1"), args: args{ - f: func(inputs port.Ports, outputs port.Ports) error { + f: func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("out1").PutSignal(signal.New(23)) return nil }, @@ -196,8 +240,10 @@ func TestComponent_WithActivationFunc(t *testing.T) { componentAfter := tt.component.WithActivationFunc(tt.args.f) //Compare activation functions by they result and error - testInputs1, testInputs2 := port.NewPorts("in1", "in2"), port.NewPorts("in1", "in2") - testOutputs1, testOutputs2 := port.NewPorts("out1", "out2"), port.NewPorts("out1", "out2") + testInputs1 := port.NewPortsCollection().Add(port.NewPortGroup("in1", "in2")...) + testInputs2 := port.NewPortsCollection().Add(port.NewPortGroup("in1", "in2")...) + testOutputs1 := port.NewPortsCollection().Add(port.NewPortGroup("out1", "out2")...) + testOutputs2 := port.NewPortsCollection().Add(port.NewPortGroup("out1", "out2")...) err1 := componentAfter.f(testInputs1, testOutputs1) err2 := tt.args.f(testInputs2, testOutputs2) assert.Equal(t, err1, err2) @@ -225,8 +271,8 @@ func TestComponent_WithDescription(t *testing.T) { want: &Component{ name: "c1", description: "descr", - inputs: nil, - outputs: nil, + inputs: port.Collection{}, + outputs: port.Collection{}, f: nil, }, }, @@ -259,11 +305,11 @@ func TestComponent_WithInputs(t *testing.T) { want: &Component{ name: "c1", description: "", - inputs: port.Ports{ + inputs: port.Collection{ "p1": port.NewPort("p1"), "p2": port.NewPort("p2"), }, - outputs: nil, + outputs: port.Collection{}, f: nil, }, }, @@ -276,8 +322,8 @@ func TestComponent_WithInputs(t *testing.T) { want: &Component{ name: "c1", description: "", - inputs: port.Ports{}, - outputs: nil, + inputs: port.Collection{}, + outputs: port.Collection{}, f: nil, }, }, @@ -310,8 +356,8 @@ func TestComponent_WithOutputs(t *testing.T) { want: &Component{ name: "c1", description: "", - inputs: nil, - outputs: port.Ports{ + inputs: port.Collection{}, + outputs: port.Collection{ "p1": port.NewPort("p1"), "p2": port.NewPort("p2"), }, @@ -327,8 +373,8 @@ func TestComponent_WithOutputs(t *testing.T) { want: &Component{ name: "c1", description: "", - inputs: nil, - outputs: port.Ports{}, + inputs: port.Collection{}, + outputs: port.Collection{}, f: nil, }, }, @@ -342,94 +388,7 @@ func TestComponent_WithOutputs(t *testing.T) { } } -func TestComponents_ByName(t *testing.T) { - type args struct { - name string - } - tests := []struct { - name string - components Components - args args - want *Component - }{ - { - name: "component found", - components: NewComponents("c1", "c2"), - args: args{ - name: "c2", - }, - want: &Component{ - name: "c2", - description: "", - inputs: nil, - outputs: nil, - f: nil, - }, - }, - { - name: "component not found", - components: NewComponents("c1", "c2"), - args: args{ - name: "c3", - }, - want: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.components.ByName(tt.args.name); !reflect.DeepEqual(got, tt.want) { - t.Errorf("ByName() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestNew(t *testing.T) { - type args struct { - name string - } - tests := []struct { - name string - args args - want *Component - }{ - { - name: "happy path", - args: args{ - name: "c1", - }, - want: &Component{ - name: "c1", - description: "", - inputs: nil, - outputs: nil, - f: nil, - }, - }, - { - name: "empty name is valid", - args: args{ - name: "", - }, - want: &Component{ - name: "", - description: "", - inputs: nil, - outputs: nil, - f: nil, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewComponent(tt.args.name); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewComponent() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestComponent_Activate(t *testing.T) { +func TestComponent_MaybeActivate(t *testing.T) { tests := []struct { name string getComponent func() *Component @@ -453,12 +412,31 @@ func TestComponent_Activate(t *testing.T) { SetActivated(false). WithActivationCode(ActivationCodeNoFunction), }, + { + name: "no input", + getComponent: func() *Component { + c := NewComponent("c1"). + WithInputs("i1", "i2"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + + if !inputs.ByNames("i1", "i2").AllHaveSignal() { + return ErrWaitingForInputResetInputs + } + + return nil + }) + return c + }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(ActivationCodeNoInput), + }, { name: "component is waiting for input", getComponent: func() *Component { c := NewComponent("c1"). WithInputs("i1", "i2"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { if !inputs.ByNames("i1", "i2").AllHaveSignal() { return ErrWaitingForInputResetInputs @@ -479,7 +457,7 @@ func TestComponent_Activate(t *testing.T) { getComponent: func() *Component { c := NewComponent("c1"). WithInputs("i1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return errors.New("test error") }) //Only one input set @@ -497,7 +475,7 @@ func TestComponent_Activate(t *testing.T) { c := NewComponent("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { port.ForwardSignal(inputs.ByName("i1"), outputs.ByName("o1")) return nil }) @@ -515,7 +493,7 @@ func TestComponent_Activate(t *testing.T) { c := NewComponent("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { port.ForwardSignal(inputs.ByName("i1"), outputs.ByName("o1")) panic(errors.New("oh shrimps")) return nil @@ -535,7 +513,7 @@ func TestComponent_Activate(t *testing.T) { c := NewComponent("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { port.ForwardSignal(inputs.ByName("i1"), outputs.ByName("o1")) panic("oh shrimps") return nil @@ -565,110 +543,3 @@ func TestComponent_Activate(t *testing.T) { }) } } - -func TestComponents_Add(t *testing.T) { - type args struct { - component *Component - } - tests := []struct { - name string - components Components - args args - want Components - }{ - { - name: "adding to empty collection", - components: NewComponents(), - args: args{ - component: NewComponent("c1").WithDescription("descr"), - }, - want: Components{ - "c1": {name: "c1", description: "descr"}, - }, - }, - { - name: "adding to existing collection", - components: NewComponents("c1", "c2"), - args: args{ - component: NewComponent("c3").WithDescription("descr"), - }, - want: Components{ - "c1": {name: "c1"}, - "c2": {name: "c2"}, - "c3": {name: "c3", description: "descr"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, tt.components.Add(tt.args.component), "Add(%v)", tt.args.component) - }) - } -} - -func TestNewComponent(t *testing.T) { - type args struct { - name string - } - tests := []struct { - name string - args args - want *Component - }{ - { - name: "empty name is valid", - args: args{ - name: "", - }, - want: &Component{}, - }, - { - name: "with name", - args: args{ - name: "multiplier", - }, - want: &Component{ - name: "multiplier", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, NewComponent(tt.args.name), "NewComponent(%v)", tt.args.name) - }) - } -} - -func TestNewComponents(t *testing.T) { - type args struct { - names []string - } - tests := []struct { - name string - args args - want Components - }{ - { - name: "without specifying names", - args: args{ - names: nil, - }, - want: Components{}, - }, - { - name: "with names", - args: args{ - names: []string{"c1", "c2"}, - }, - want: Components{ - "c1": {name: "c1"}, - "c2": {name: "c2"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, NewComponents(tt.args.names...), "NewComponents(%v)", tt.args.names) - }) - } -} diff --git a/cycle/result.go b/cycle/result.go index a9e14ee..6fd1f73 100644 --- a/cycle/result.go +++ b/cycle/result.go @@ -9,7 +9,7 @@ import ( type Result struct { sync.Mutex cycleNumber uint - activationResults component.ActivationResults + activationResults component.ActivationResultCollection } // Results contains the results of several activation cycles @@ -18,7 +18,7 @@ type Results []*Result // NewResult creates a new cycle result func NewResult() *Result { return &Result{ - activationResults: make(component.ActivationResults), + activationResults: make(component.ActivationResultCollection), } } @@ -38,45 +38,23 @@ func (cycleResult *Result) CycleNumber() uint { } // ActivationResults getter -func (cycleResult *Result) ActivationResults() component.ActivationResults { +func (cycleResult *Result) ActivationResults() component.ActivationResultCollection { return cycleResult.activationResults } -// WithActivationResults adds multiple activation results -func (cycleResult *Result) WithActivationResults(activationResults ...*component.ActivationResult) *Result { - for _, activationResult := range activationResults { - cycleResult.activationResults[activationResult.ComponentName()] = activationResult - } - return cycleResult -} - // HasErrors tells whether the cycle is ended wih activation errors (at lease one component returned an error) func (cycleResult *Result) HasErrors() bool { - for _, ar := range cycleResult.activationResults { - if ar.HasError() { - return true - } - } - return false + return cycleResult.ActivationResults().HasErrors() } // HasPanics tells whether the cycle is ended wih panic(at lease one component panicked) func (cycleResult *Result) HasPanics() bool { - for _, ar := range cycleResult.activationResults { - if ar.HasPanic() { - return true - } - } - return false + return cycleResult.ActivationResults().HasPanics() } +// HasActivatedComponents tells when at least one component in the cycle has activated func (cycleResult *Result) HasActivatedComponents() bool { - for _, ar := range cycleResult.activationResults { - if ar.Activated() { - return true - } - } - return false + return cycleResult.ActivationResults().HasActivatedComponents() } // Add adds cycle results to existing collection @@ -86,3 +64,9 @@ func (cycleResults Results) Add(newCycleResults ...*Result) Results { } return cycleResults } + +// WithActivationResults adds multiple activation results +func (cycleResult *Result) WithActivationResults(activationResults ...*component.ActivationResult) *Result { + cycleResult.activationResults = cycleResult.ActivationResults().Add(activationResults...) + return cycleResult +} diff --git a/cycle/result_test.go b/cycle/result_test.go index 03c2e10..e5d2e9c 100644 --- a/cycle/result_test.go +++ b/cycle/result_test.go @@ -33,13 +33,13 @@ func TestResults_Add(t *testing.T) { want: Results{ { cycleNumber: 1, - activationResults: component.ActivationResults{ + activationResults: component.ActivationResultCollection{ "c1": component.NewActivationResult("c1").SetActivated(false), }, }, { cycleNumber: 2, - activationResults: component.ActivationResults{ + activationResults: component.ActivationResultCollection{ "c1": component.NewActivationResult("c1").SetActivated(true), }, }, @@ -64,7 +64,7 @@ func TestNewResult(t *testing.T) { name: "happy path", want: &Result{ cycleNumber: 0, - activationResults: component.ActivationResults{}, + activationResults: component.ActivationResultCollection{}, }, }, } @@ -100,17 +100,17 @@ func TestResult_ActivationResults(t *testing.T) { tests := []struct { name string cycleResult *Result - want component.ActivationResults + want component.ActivationResultCollection }{ { name: "no activation results", cycleResult: NewResult(), - want: component.ActivationResults{}, + want: component.ActivationResultCollection{}, }, { name: "happy path", cycleResult: NewResult().WithActivationResults(component.NewActivationResult("c1").SetActivated(true).WithActivationCode(component.ActivationCodeOK)), - want: component.ActivationResults{ + want: component.ActivationResultCollection{ "c1": component.NewActivationResult("c1").SetActivated(true).WithActivationCode(component.ActivationCodeOK), }, }, @@ -118,7 +118,7 @@ func TestResult_ActivationResults(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.cycleResult.ActivationResults(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("ActivationResults() = %v, want %v", got, tt.want) + t.Errorf("ActivationResultCollection() = %v, want %v", got, tt.want) } }) } @@ -287,7 +287,7 @@ func TestResult_SetCycleNumber(t *testing.T) { }, want: &Result{ cycleNumber: 23, - activationResults: component.ActivationResults{}, + activationResults: component.ActivationResultCollection{}, }, }, } @@ -329,7 +329,7 @@ func TestResult_WithActivationResults(t *testing.T) { }, want: &Result{ cycleNumber: 0, - activationResults: component.ActivationResults{ + activationResults: component.ActivationResultCollection{ "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), }, @@ -353,7 +353,7 @@ func TestResult_WithActivationResults(t *testing.T) { }, want: &Result{ cycleNumber: 0, - activationResults: component.ActivationResults{ + activationResults: component.ActivationResultCollection{ "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), "c3": component.NewActivationResult("c3").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError), diff --git a/examples/basic.go b/examples/basic.go index 60dec9d..269fe7a 100644 --- a/examples/basic.go +++ b/examples/basic.go @@ -17,7 +17,7 @@ func main() { WithDescription("adds 2 to the input"). WithInputs("num"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { num := inputs.ByName("num").Signal().Payload().(int) outputs.ByName("res").PutSignal(signal.New(num + 2)) return nil @@ -27,7 +27,7 @@ func main() { WithDescription("multiplies by 3"). WithInputs("num"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { num := inputs.ByName("num").Signal().Payload().(int) outputs.ByName("res").PutSignal(signal.New(num * 3)) return nil diff --git a/fmesh.go b/fmesh.go index d3c05b6..fde4d03 100644 --- a/fmesh.go +++ b/fmesh.go @@ -10,13 +10,13 @@ import ( type FMesh struct { name string description string - components component.Components + components component.Collection errorHandlingStrategy ErrorHandlingStrategy } // New creates a new f-mesh func New(name string) *FMesh { - return &FMesh{name: name, components: component.NewComponents()} + return &FMesh{name: name, components: component.NewComponentCollection()} } // Name getter @@ -29,7 +29,7 @@ func (fm *FMesh) Description() string { return fm.description } -func (fm *FMesh) Components() component.Components { +func (fm *FMesh) Components() component.Collection { return fm.components } @@ -72,7 +72,7 @@ func (fm *FMesh) runCycle() *cycle.Result { case aRes := <-activationResultsChan: //@TODO :check for closed channel cycleResult.Lock() - cycleResult = cycleResult.WithActivationResults(aRes) + cycleResult.ActivationResults().Add(aRes) cycleResult.Unlock() case <-doneChan: return diff --git a/fmesh_test.go b/fmesh_test.go index cc979e8..a21ea94 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -26,7 +26,7 @@ func TestNew(t *testing.T) { name: "", }, want: &FMesh{ - components: component.Components{}, + components: component.Collection{}, }, }, { @@ -36,7 +36,7 @@ func TestNew(t *testing.T) { }, want: &FMesh{ name: "fm1", - components: component.Components{}, + components: component.Collection{}, }, }, } @@ -68,7 +68,7 @@ func TestFMesh_WithDescription(t *testing.T) { want: &FMesh{ name: "fm1", description: "", - components: component.Components{}, + components: component.Collection{}, errorHandlingStrategy: 0, }, }, @@ -81,7 +81,7 @@ func TestFMesh_WithDescription(t *testing.T) { want: &FMesh{ name: "fm1", description: "descr", - components: component.Components{}, + components: component.Collection{}, errorHandlingStrategy: 0, }, }, @@ -113,7 +113,7 @@ func TestFMesh_WithErrorHandlingStrategy(t *testing.T) { }, want: &FMesh{ name: "fm1", - components: component.Components{}, + components: component.Collection{}, errorHandlingStrategy: StopOnFirstError, }, }, @@ -125,7 +125,7 @@ func TestFMesh_WithErrorHandlingStrategy(t *testing.T) { }, want: &FMesh{ name: "fm1", - components: component.Components{}, + components: component.Collection{}, errorHandlingStrategy: IgnoreAll, }, }, @@ -158,7 +158,7 @@ func TestFMesh_WithComponents(t *testing.T) { want: &FMesh{ name: "fm1", description: "", - components: component.Components{}, + components: component.Collection{}, errorHandlingStrategy: 0, }, }, @@ -172,7 +172,7 @@ func TestFMesh_WithComponents(t *testing.T) { }, want: &FMesh{ name: "fm1", - components: component.Components{ + components: component.Collection{ "c1": component.NewComponent("c1"), }, }, @@ -188,7 +188,7 @@ func TestFMesh_WithComponents(t *testing.T) { }, want: &FMesh{ name: "fm1", - components: component.Components{ + components: component.Collection{ "c1": component.NewComponent("c1"), "c2": component.NewComponent("c2"), }, @@ -207,7 +207,7 @@ func TestFMesh_WithComponents(t *testing.T) { }, want: &FMesh{ name: "fm1", - components: component.Components{ + components: component.Collection{ "c1": component.NewComponent("c1").WithDescription("descr1"), "c2": component.NewComponent("c2").WithDescription("descr3"), "c4": component.NewComponent("c4").WithDescription("descr4"), @@ -298,7 +298,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component simply puts a constant on o1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(signal.New(77)) return nil }), @@ -323,7 +323,7 @@ func TestFMesh_Run(t *testing.T) { component.NewComponent("c1"). WithDescription("This component just returns an unexpected error"). WithInputs("i1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return errors.New("boom") })), initFM: func(fm *FMesh) { @@ -350,7 +350,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component just sends a number to c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(signal.New(10)) return nil }), @@ -358,7 +358,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c1 and passes it to c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) return nil }), @@ -366,14 +366,14 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component returns an error, but the mesh is configured to ignore errors"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return errors.New("boom") }), component.NewComponent("c4"). WithDescription("This component receives a number from c2 and panics"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { panic("no way") return nil }), @@ -451,7 +451,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component just sends a number to c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(signal.New(10)) return nil }), @@ -459,7 +459,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c1 and passes it to c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) return nil }), @@ -467,14 +467,14 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component returns an error, but the mesh is configured to ignore errors"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return errors.New("boom") }), component.NewComponent("c4"). WithDescription("This component receives a number from c2 and panics, but the mesh is configured to ignore even panics"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) // Even component panicked, it managed to set some data on output "o1" @@ -486,7 +486,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) return nil }), @@ -625,7 +625,7 @@ func TestFMesh_Run(t *testing.T) { //Compare cycle results one by one for i := 0; i < len(got); i++ { assert.Equal(t, tt.want[i].CycleNumber(), got[i].CycleNumber()) - assert.Equal(t, len(tt.want[i].ActivationResults()), len(got[i].ActivationResults()), "ActivationResults len mismatch") + assert.Equal(t, len(tt.want[i].ActivationResults()), len(got[i].ActivationResults()), "ActivationResultCollection len mismatch") //Compare activation results for componentName, gotActivationResult := range got[i].ActivationResults() { @@ -663,7 +663,7 @@ func TestFMesh_runCycle(t *testing.T) { WithDescription("I do not have any input signal set, hence I will never be activated"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { outputs.ByName("o1").PutSignal(signal.New("this signal will never be sent")) return nil }), @@ -677,7 +677,7 @@ func TestFMesh_runCycle(t *testing.T) { WithDescription("I'm waiting for specific input"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { if !inputs.ByNames("i1", "i2").AllHaveSignal() { return component.ErrWaitingForInputKeepInputs } @@ -697,13 +697,13 @@ func TestFMesh_runCycle(t *testing.T) { { name: "all components activated in one cycle (concurrently)", fm: New("test").WithComponents( - component.NewComponent("c1").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + component.NewComponent("c1").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return nil }), - component.NewComponent("c2").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + component.NewComponent("c2").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return nil }), - component.NewComponent("c3").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + component.NewComponent("c3").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { return nil }), ), diff --git a/port/collection.go b/port/collection.go new file mode 100644 index 0000000..5f44ca9 --- /dev/null +++ b/port/collection.go @@ -0,0 +1,78 @@ +package port + +import ( + "github.com/hovsep/fmesh/signal" +) + +// Collection is a port collection with useful methods +type Collection map[string]*Port + +// NewPortsCollection creates empty collection +func NewPortsCollection() Collection { + return make(Collection) +} + +// ByName returns a port by its name +func (collection Collection) ByName(name string) *Port { + return collection[name] +} + +// ByNames returns multiple ports by their names +func (collection Collection) ByNames(names ...string) Collection { + selectedPorts := make(Collection) + + for _, name := range names { + if p, ok := collection[name]; ok { + selectedPorts[name] = p + } + } + + return selectedPorts +} + +// AnyHasSignal returns true if at least one port in collection has signal +func (collection Collection) AnyHasSignal() bool { + for _, p := range collection { + if p.HasSignal() { + return true + } + } + + return false +} + +// AllHaveSignal returns true when all ports in collection have signal +func (collection Collection) AllHaveSignal() bool { + for _, p := range collection { + if !p.HasSignal() { + return false + } + } + + return true +} + +// PutSignal puts a signal to all the port in collection +func (collection Collection) PutSignal(sig *signal.Signal) { + for _, p := range collection { + p.PutSignal(sig) + } +} + +// ClearSignal removes signals from all ports in collection +func (collection Collection) ClearSignal() { + for _, p := range collection { + p.ClearSignal() + } +} + +func (collection Collection) Add(ports ...*Port) Collection { + for _, port := range ports { + if port == nil { + continue + } + collection[port.Name()] = port + } + + return collection +} diff --git a/port/collection_test.go b/port/collection_test.go new file mode 100644 index 0000000..ff6e219 --- /dev/null +++ b/port/collection_test.go @@ -0,0 +1,261 @@ +package port + +import ( + "github.com/hovsep/fmesh/signal" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestCollection_AllHaveSignal(t *testing.T) { + oneEmptyPorts := NewPortsCollection().Add(NewPortGroup("p1", "p2", "p3")...) + oneEmptyPorts.PutSignal(signal.New(123)) + oneEmptyPorts.ByName("p2").ClearSignal() + + allWithSignalPorts := NewPortsCollection().Add(NewPortGroup("out1", "out2", "out3")...) + allWithSignalPorts.PutSignal(signal.New(77)) + + allWithEmptySignalPorts := NewPortsCollection().Add(NewPortGroup("in1", "in2", "in3")...) + allWithEmptySignalPorts.PutSignal(signal.New()) + + tests := []struct { + name string + ports Collection + want bool + }{ + { + name: "all empty", + ports: NewPortsCollection().Add(NewPortGroup("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 TestCollection_AnyHasSignal(t *testing.T) { + oneEmptyPorts := NewPortsCollection().Add(NewPortGroup("p1", "p2", "p3")...) + oneEmptyPorts.PutSignal(signal.New(123)) + oneEmptyPorts.ByName("p2").ClearSignal() + + tests := []struct { + name string + ports Collection + want bool + }{ + { + name: "one empty", + ports: oneEmptyPorts, + want: true, + }, + { + name: "all empty", + ports: NewPortsCollection().Add(NewPortGroup("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 TestCollection_ByName(t *testing.T) { + portsWithSignals := NewPortsCollection().Add(NewPortGroup("p1", "p2")...) + portsWithSignals.PutSignal(signal.New(12)) + + type args struct { + name string + } + tests := []struct { + name string + ports Collection + args args + want *Port + }{ + { + name: "empty port found", + ports: NewPortsCollection().Add(NewPortGroup("p1", "p2")...), + args: args{ + name: "p1", + }, + want: &Port{name: "p1", pipes: Collection{}}, + }, + { + name: "port with signal found", + ports: portsWithSignals, + args: args{ + name: "p2", + }, + want: &Port{ + name: "p2", + signal: signal.New(12), + pipes: Collection{}, + }, + }, + { + name: "port not found", + ports: NewPortsCollection().Add(NewPortGroup("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 TestCollection_ByNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + ports Collection + args args + want Collection + }{ + { + name: "single port found", + ports: NewPortsCollection().Add(NewPortGroup("p1", "p2")...), + args: args{ + names: []string{"p1"}, + }, + want: Collection{ + "p1": &Port{ + name: "p1", + pipes: Collection{}, + }, + }, + }, + { + name: "multiple ports found", + ports: NewPortsCollection().Add(NewPortGroup("p1", "p2")...), + args: args{ + names: []string{"p1", "p2"}, + }, + want: Collection{ + "p1": &Port{name: "p1", pipes: Collection{}}, + "p2": &Port{name: "p2", pipes: Collection{}}, + }, + }, + { + name: "single port not found", + ports: NewPortsCollection().Add(NewPortGroup("p1", "p2")...), + args: args{ + names: []string{"p7"}, + }, + want: Collection{}, + }, + { + name: "some ports not found", + ports: NewPortsCollection().Add(NewPortGroup("p1", "p2")...), + args: args{ + names: []string{"p1", "p2", "p3"}, + }, + want: Collection{ + "p1": &Port{name: "p1", pipes: Collection{}}, + "p2": &Port{name: "p2", pipes: Collection{}}, + }, + }, + } + 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 TestCollection_ClearSignal(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + ports := NewPortsCollection().Add(NewPortGroup("p1", "p2", "p3")...) + ports.PutSignal(signal.New(1, 2, 3)) + assert.True(t, ports.AllHaveSignal()) + ports.ClearSignal() + assert.False(t, ports.AnyHasSignal()) + }) +} + +func TestCollection_Add(t *testing.T) { + type args struct { + ports []*Port + } + tests := []struct { + name string + collection Collection + args args + assertions func(t *testing.T, collection Collection) + }{ + { + name: "adding nothing to empty collection", + collection: NewPortsCollection(), + args: args{ + ports: nil, + }, + assertions: func(t *testing.T, collection Collection) { + assert.Len(t, collection, 0) + }, + }, + { + name: "adding to empty collection", + collection: NewPortsCollection(), + args: args{ + ports: NewPortGroup("p1", "p2"), + }, + assertions: func(t *testing.T, collection Collection) { + assert.Len(t, collection, 2) + assert.Len(t, collection.ByNames("p1", "p2"), 2) + }, + }, + { + name: "adding to existing collection", + collection: NewPortsCollection().Add(NewPortGroup("p1", "p2")...), + args: args{ + ports: NewPortGroup("p3", "p4"), + }, + assertions: func(t *testing.T, collection Collection) { + assert.Len(t, collection, 4) + assert.Len(t, collection.ByNames("p1", "p2", "p3", "p4"), 4) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.collection.Add(tt.args.ports...) + if tt.assertions != nil { + tt.assertions(t, tt.collection) + } + }) + } +} diff --git a/port/group.go b/port/group.go new file mode 100644 index 0000000..4e58643 --- /dev/null +++ b/port/group.go @@ -0,0 +1,13 @@ +package port + +// Group is just a slice of ports (useful to pass multiple ports as variadic argument) +type Group []*Port + +// NewPortGroup creates multiple ports +func NewPortGroup(names ...string) Group { + group := make(Group, len(names)) + for _, name := range names { + group = append(group, NewPort(name)) + } + return group +} diff --git a/port/pipe.go b/port/pipe.go deleted file mode 100644 index 3dd99af..0000000 --- a/port/pipe.go +++ /dev/null @@ -1,25 +0,0 @@ -package port - -//@TODO:the pipe type can be potentially removed - -// 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 deleted file mode 100644 index e6a067b..0000000 --- a/port/pipe_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package port - -import ( - "github.com/hovsep/fmesh/signal" - "reflect" - "testing" -) - -func TestNewPipe(t *testing.T) { - p1, p2 := NewPort("p1"), NewPort("p2") - - 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") - portWithSignal.PutSignal(signal.New(777)) - - portWithMultipleSignals := NewPort("portWithMultipleSignals") - portWithMultipleSignals.PutSignal(signal.New(11, 12)) - - emptyPort := NewPort("emptyPort") - - tests := []struct { - name string - before *Pipe - after *Pipe - }{ - { - name: "flush to empty port", - before: NewPipe(portWithSignal, emptyPort), - after: &Pipe{ - From: &Port{ - name: "portWithSignal", - signal: signal.New(777), //Flush does not clear source port - }, - To: &Port{ - name: "emptyPort", - signal: signal.New(777), - }, - }, - }, - { - name: "flush to port with signal", - before: NewPipe(portWithSignal, portWithMultipleSignals), - after: &Pipe{ - From: portWithSignal, - To: &Port{ - name: "portWithMultipleSignals", - 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 e2799c8..599f70b 100644 --- a/port/port.go +++ b/port/port.go @@ -8,24 +8,15 @@ import ( type Port struct { name string signal *signal.Signal //Current signal set on the port - pipes Pipes //Refs to all outbound pipes connected to this port + pipes Collection //Refs to all outbound pipes connected to this port } -// Ports is just useful collection type -type Ports map[string]*Port - // NewPort creates a new port func NewPort(name string) *Port { - return &Port{name: name} -} - -// 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(name) + return &Port{ + name: name, + pipes: NewPortsCollection(), } - return ports } // Name getter @@ -33,12 +24,7 @@ func (p *Port) Name() string { return p.name } -// Pipes getter -func (p *Port) Pipes() Pipes { - return p.pipes -} - -// Signal returns current signal set on the port +// Signal getter func (p *Port) Signal() *signal.Signal { return p.signal } @@ -58,78 +44,30 @@ func (p *Port) HasSignal() bool { return p.signal != nil } -// 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) -} - // PipeTo creates one or multiple pipes to other port(s) func (p *Port) PipeTo(toPorts ...*Port) { for _, toPort := range toPorts { if toPort == nil { continue } - newPipe := NewPipe(p, toPort) - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } -} - -// ByName returns a port by its name -func (ports Ports) ByName(name string) *Port { - return ports[name] -} - -// ByNames returns multiple ports by their names -func (ports Ports) ByNames(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } + p.pipes.Add(toPort) } - - 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() { - return true - } +// Flush pushed current signal to pipes and clears the port +func (p *Port) Flush() { + if !p.HasSignal() || len(p.pipes) == 0 { + return } - return false -} - -// AllHaveSignal returns true when all ports in collection have signal -func (ports Ports) AllHaveSignal() bool { - for _, p := range ports { - if !p.HasSignal() { - return false - } - } - - 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) - } -} - -// ClearSignal removes signals from all ports in collection -func (ports Ports) ClearSignal() { - for _, p := range ports { - p.ClearSignal() + for _, outboundPort := range p.pipes { + //Multiplexing + ForwardSignal(p, outboundPort) } + p.ClearSignal() } -// ForwardSignal puts a signal from source port to dest port, without removing it on source port +// ForwardSignal puts a signal from source port to destination 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 index 612b660..55cba02 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -7,52 +7,6 @@ import ( "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": {name: "i1"}, - "i2": {name: "i2"}, - }, - }, - { - name: "duplicate names are ignored", - args: args{ - names: []string{"i1", "i2", "i1"}, - }, - want: Ports{ - "i1": {name: "i1"}, - "i2": {name: "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") portWithSignal.PutSignal(signal.New(123)) @@ -90,58 +44,6 @@ func TestPort_HasSignal(t *testing.T) { } } -func TestPort_Pipes(t *testing.T) { - destPort1, destPort2, destPort3 := NewPort("destPort1"), NewPort("destPort2"), NewPort("destPort3") - portWithOnePipe := NewPort("portWithOnePipe") - portWithOnePipe.PipeTo(destPort1) - - portWithMultiplePipes := NewPort("portWithMultiplePipes") - portWithMultiplePipes.PipeTo(destPort2, destPort3) - - tests := []struct { - name string - port *Port - want Pipes - }{ - { - name: "no pipes", - port: NewPort("noPipes"), - 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") portWithSignal.PutSignal(signal.New(123)) @@ -179,192 +81,6 @@ func TestPort_Signal(t *testing.T) { } } -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: "p1"}, - }, - { - name: "port with signal found", - ports: portsWithSignals, - args: args{ - name: "p2", - }, - want: &Port{ - name: "p2", - 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": &Port{name: "p1"}, - }, - }, - { - name: "multiple ports found", - ports: NewPorts("p1", "p2"), - args: args{ - names: []string{"p1", "p2"}, - }, - want: Ports{ - "p1": &Port{name: "p1"}, - "p2": &Port{name: "p2"}, - }, - }, - { - 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": &Port{name: "p1"}, - "p2": &Port{name: "p2"}, - }, - }, - } - 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") portWithSignal.PutSignal(signal.New(111)) @@ -377,12 +93,12 @@ func TestPort_ClearSignal(t *testing.T) { { name: "happy path", before: portWithSignal, - after: &Port{name: "portWithSignal"}, + after: &Port{name: "portWithSignal", pipes: Collection{}}, }, { name: "cleaning empty port", before: NewPort("emptyPort"), - after: &Port{name: "emptyPort"}, + after: &Port{name: "emptyPort", pipes: Collection{}}, }, } for _, tt := range tests { @@ -411,17 +127,8 @@ func TestPort_PipeTo(t *testing.T) { name: "happy path", before: p1, after: &Port{ - name: "p1", - pipes: Pipes{ - { - From: p1, - To: p2, - }, - { - From: p1, - To: p3, - }, - }, + name: "p1", + pipes: NewPortsCollection().Add(p2, p3), }, args: args{ toPorts: []*Port{p2, p3}, @@ -431,13 +138,8 @@ func TestPort_PipeTo(t *testing.T) { name: "invalid ports are ignored", before: p4, after: &Port{ - name: "p4", - pipes: Pipes{ - { - From: p4, - To: p2, - }, - }, + name: "p4", + pipes: NewPortsCollection().Add(p2), }, args: args{ toPorts: []*Port{p2, nil}, @@ -479,6 +181,7 @@ func TestPort_PutSignal(t *testing.T) { after: &Port{ name: "emptyPort", signal: signal.New(11), + pipes: Collection{}, }, args: args{ sig: signal.New(11), @@ -490,6 +193,7 @@ func TestPort_PutSignal(t *testing.T) { after: &Port{ name: "p", signal: signal.New(11, 12), + pipes: Collection{}, }, args: args{ sig: signal.New(11, 12), @@ -501,6 +205,7 @@ func TestPort_PutSignal(t *testing.T) { after: &Port{ name: "portWithSingleSignal", signal: signal.New(12, 11), //Notice LIFO order + pipes: Collection{}, }, args: args{ sig: signal.New(12), @@ -512,6 +217,7 @@ func TestPort_PutSignal(t *testing.T) { after: &Port{ name: "portWithMultipleSignals", signal: signal.New(13, 11, 12), //Notice LIFO order + pipes: Collection{}, }, args: args{ sig: signal.New(13), @@ -523,6 +229,7 @@ func TestPort_PutSignal(t *testing.T) { after: &Port{ name: "portWithMultipleSignals2", signal: signal.New(13, 14, 55, 66), //Notice LIFO order + pipes: Collection{}, }, args: args{ sig: signal.New(13, 14), @@ -539,16 +246,6 @@ func TestPort_PutSignal(t *testing.T) { } } -func TestPorts_ClearSignal(t *testing.T) { - t.Run("happy path", func(t *testing.T) { - ports := NewPorts("p1", "p2", "p3") - ports.PutSignal(signal.New(1, 2, 3)) - assert.True(t, ports.AllHaveSignal()) - ports.ClearSignal() - assert.False(t, ports.AnyHasSignal()) - }) -} - func TestPort_Name(t *testing.T) { tests := []struct { name string @@ -582,14 +279,14 @@ func TestNewPort(t *testing.T) { args: args{ name: "", }, - want: &Port{name: ""}, + want: &Port{name: "", pipes: Collection{}}, }, { name: "with name", args: args{ name: "p1", }, - want: &Port{name: "p1"}, + want: &Port{name: "p1", pipes: Collection{}}, }, } for _, tt := range tests { @@ -598,3 +295,69 @@ func TestNewPort(t *testing.T) { }) } } + +func TestPort_Flush(t *testing.T) { + portWithSignal1 := NewPort("portWithSignal1") + portWithSignal1.PutSignal(signal.New(777)) + + portWithSignal2 := NewPort("portWithSignal2") + portWithSignal2.PutSignal(signal.New(888)) + + portWithMultipleSignals := NewPort("portWithMultipleSignals") + portWithMultipleSignals.PutSignal(signal.New(11, 12)) + + emptyPort := NewPort("emptyPort") + + tests := []struct { + name string + source *Port + dest *Port + assertions func(t *testing.T, source *Port, dest *Port) + }{ + { + name: "port with no signal", + source: NewPort("empty_src"), + dest: NewPort("empty_dest"), + assertions: func(t *testing.T, source *Port, dest *Port) { + assert.False(t, source.HasSignal()) + assert.False(t, dest.HasSignal()) + }, + }, + { + name: "flush to empty port", + source: portWithSignal1, + dest: emptyPort, + assertions: func(t *testing.T, source *Port, dest *Port) { + //Source port is clear + assert.False(t, source.HasSignal()) + + //Signal transferred to destination port + assert.True(t, dest.HasSignal()) + assert.Equal(t, dest.Signal().Payload().(int), 777) + }, + }, + { + name: "flush to port with signal", + source: portWithSignal2, + dest: portWithMultipleSignals, + assertions: func(t *testing.T, source *Port, dest *Port) { + //Source port is clear + assert.False(t, source.HasSignal()) + + //Destination port now has 1 more signal + assert.True(t, dest.HasSignal()) + assert.Equal(t, 3, dest.Signal().Len()) + assert.Contains(t, dest.Signal().Payloads(), 888) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.source.PipeTo(tt.dest) + tt.source.Flush() + if tt.assertions != nil { + tt.assertions(t, tt.source, tt.dest) + } + }) + } +}