From 021bd14124cf10dce0153b7ef2869dd04075253d Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 10 Nov 2024 02:42:21 +0200 Subject: [PATCH 01/41] Update README.md # Conflicts: # README.md # Conflicts: # README.md --- README.md | 53 ++++++++++++++++++++++------------------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ac37dfd..217ccf3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -
f-mesh

f-mesh

@@ -7,41 +6,33 @@

What is it?

-

F-Mesh is a simplistic FBP-inspired framework in Go. -It allows you to express your program as a mesh of interconnected components. -You can think of it as a simple functions orchestrator. +

F-Mesh is a functions orchestrator inspired by FBP. +It allows you to express your program as a mesh of interconnected components (or more formally as a computational graph).

Main concepts:

What it is not?

-

F-mesh is not a classical FBP implementation, and it is not fully async. It does not support long-running components or wall-time events (like timers and tickers)

-

The framework is not suitable for implementing complex concurrent systems

+

F-mesh is not a classical FBP implementation, it does not support long-running components or wall-time events (like timers and tickers)

+

Example:

```go - // Create f-mesh - fm := fmesh.New("hello world"). +fm := fmesh.New("hello world"). WithComponents( component.New("concat"). WithInputs("i1", "i2"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - word1 := inputs.ByName("i1").Signals().FirstPayload().(string) - word2 := inputs.ByName("i2").Signals().FirstPayload().(string) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + word1 := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + word2 := inputs.ByName("i2").FirstSignalPayloadOrDefault("").(string) outputs.ByName("res").PutSignals(signal.New(word1 + word2)) return nil @@ -49,24 +40,24 @@ You can think of it as a simple functions orchestrator. component.New("case"). WithInputs("i1"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - inputString := inputs.ByName("i1").Signals().FirstPayload().(string) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputString := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) outputs.ByName("res").PutSignals(signal.New(strings.ToTitle(inputString))) return nil })). - .WithConfig(fmesh.Config{ - ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, - CyclesLimit: 10, - }) + WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) fm.Components().ByName("concat").Outputs().ByName("res").PipeTo( fm.Components().ByName("case").Inputs().ByName("i1"), ) // Init inputs - fm.Components().ByName("concat").Inputs().ByName("i1").PutSignals(signal.New("hello ")) - fm.Components().ByName("concat").Inputs().ByName("i2").PutSignals(signal.New("world !")) + fm.Components().ByName("concat").InputByName("i1").PutSignals(signal.New("hello ")) + fm.Components().ByName("concat").InputByName("i2").PutSignals(signal.New("world !")) // Run the mesh _, err := fm.Run() @@ -78,8 +69,8 @@ You can think of it as a simple functions orchestrator. } //Extract results - results := fm.Components().ByName("case").Outputs().ByName("res").Signals().FirstPayload() - fmt.Printf("Result is :%v", results) + results := fm.Components().ByName("case").OutputByName("res").FirstSignalPayloadOrNil() + fmt.Printf("Result is : %v", results) ``` - -

Version 0.1.0 coming soon

+See more in ```examples``` directory. +

Version 0.1.0 coming soon

\ No newline at end of file From d83411887933fc888b9c8ea15776c47847ca69eb Mon Sep 17 00:00:00 2001 From: hovsep Date: Sat, 5 Oct 2024 00:36:28 +0300 Subject: [PATCH 02/41] Waiting for inputs: add tests --- component/component_test.go | 50 +++++++++ fmesh.go | 25 +++-- fmesh_test.go | 201 ++++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+), 8 deletions(-) diff --git a/component/component_test.go b/component/component_test.go index 0c8da6a..3bbac18 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -494,6 +494,56 @@ func TestComponent_MaybeActivate(t *testing.T) { WithActivationCode(ActivationCodePanicked). WithError(errors.New("panicked with: oh shrimps")), }, + { + name: "component is waiting for inputs", + getComponent: func() *Component { + c1 := New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + if !inputs.ByNames("i1", "i2").AllHaveSignals() { + return NewErrWaitForInputs(false) + } + return nil + }) + + // Only one input set + c1.Inputs().ByName("i1").PutSignals(signal.New(123)) + + return c1 + }, + wantActivationResult: &ActivationResult{ + componentName: "c1", + activated: true, + code: ActivationCodeWaitingForInputsClear, + err: NewErrWaitForInputs(false), + }, + }, + { + name: "component is waiting for inputs and wants to keep them", + getComponent: func() *Component { + c1 := New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + if !inputs.ByNames("i1", "i2").AllHaveSignals() { + return NewErrWaitForInputs(true) + } + return nil + }) + + // Only one input set + c1.Inputs().ByName("i1").PutSignals(signal.New(123)) + + return c1 + }, + wantActivationResult: &ActivationResult{ + componentName: "c1", + activated: true, + code: ActivationCodeWaitingForInputsKeep, + err: NewErrWaitForInputs(true), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/fmesh.go b/fmesh.go index 7736097..8c02f88 100644 --- a/fmesh.go +++ b/fmesh.go @@ -103,20 +103,29 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) { activationResult := cycle.ActivationResults().ByComponentName(c.Name()) if !activationResult.Activated() { + // Component did not activate, so it did not create new output signals, hence nothing to drain continue } + // By default, all outputs are flushed and all inputs are cleared + shouldFlushOutputs := true + shouldClearInputs := true + if component.IsWaitingForInput(activationResult) { - if !component.WantsToKeepInputs(activationResult) { - c.ClearInputs() - } - // Components waiting for inputs are not flushed - continue + // @TODO: maybe we should clear outputs + // in order to prevent leaking outputs from previous cycle + // (if outputs were set before returning errWaitingForInputs) + shouldFlushOutputs = false + shouldClearInputs = !component.WantsToKeepInputs(activationResult) } - // Normally components are fully drained - c.FlushOutputs() - c.ClearInputs() + if shouldFlushOutputs { + c.FlushOutputs() + } + + if shouldClearInputs { + c.ClearInputs() + } } } diff --git a/fmesh_test.go b/fmesh_test.go index f773528..e582de9 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -784,3 +784,204 @@ func TestFMesh_mustStop(t *testing.T) { }) } } + +func TestFMesh_drainComponents(t *testing.T) { + type args struct { + cycle *cycle.Cycle + } + tests := []struct { + name string + getFM func() *FMesh + args args + assertions func(t *testing.T, fm *FMesh) + }{ + { + name: "component not activated", + getFM: func() *FMesh { + fm := New("fm"). + WithComponents( + component.New("c1"). + WithDescription("This component has no activation function"). + WithInputs("i1"). + WithOutputs("o1")) + + fm.Components(). + ByName("c1"). + Inputs(). + ByName("i1"). + PutSignals(signal.New("input signal")) + + return fm + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput)), + }, + assertions: func(t *testing.T, fm *FMesh) { + // Assert that inputs are not cleared + assert.True(t, fm.Components().ByName("c1").Inputs().ByName("i1").HasSignals()) + }, + }, + { + name: "component fully drained", + getFM: func() *FMesh { + c1 := component.New("c1"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + return nil + }) + + c2 := component.New("c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + return nil + }) + + // Pipe + c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + + // Simulate activation of c1 + c1.Outputs().ByName("o1").PutSignals(signal.New("this signal is generated by c1")) + + return New("fm").WithComponents(c1, c2) + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput)), + }, + assertions: func(t *testing.T, fm *FMesh) { + // c1 output is cleared + assert.False(t, fm.Components().ByName("c1").Outputs().ByName("o1").HasSignals()) + + // c2 input received flushed signal + assert.True(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) + + assert.Equal(t, "this signal is generated by c1", fm.Components().ByName("c2").Inputs().ByName("i1").Signals().FirstPayload().(string)) + }, + }, + { + name: "component is waiting for inputs", + getFM: func() *FMesh { + + c1 := component.New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + return nil + }) + + c2 := component.New("c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + return nil + }) + + // Pipe + c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + + // Simulate activation of c1 + // NOTE: normally component should not create any output signal if it is waiting for inputs + // but technically there is no limitation to do that and then return the special error to wait for inputs. + // F-mesh just never flushes components waiting for inputs, so this test checks that + c1.Outputs().ByName("o1").PutSignals(signal.New("this signal is generated by c1")) + + // Also simulate input signal on one port + c1.Inputs().ByName("i1").PutSignals(signal.New("this is input signal for c1")) + + return New("fm").WithComponents(c1, c2) + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeWaitingForInputsClear). + WithError(component.NewErrWaitForInputs(false)), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + }, + assertions: func(t *testing.T, fm *FMesh) { + // As c1 is waiting for inputs it's outputs must not be flushed + assert.False(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) + + // The inputs must be cleared + assert.False(t, fm.Components().ByName("c1").Inputs().AnyHasSignals()) + }, + }, + { + name: "component is waiting for inputs and wants to keep input signals", + getFM: func() *FMesh { + + c1 := component.New("c1"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + return nil + }) + + c2 := component.New("c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + return nil + }) + + // Pipe + c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + + // Simulate activation of c1 + // NOTE: normally component should not create any output signal if it is waiting for inputs + // but technically there is no limitation to do that and then return the special error to wait for inputs. + // F-mesh just never flushes components waiting for inputs, so this test checks that + c1.Outputs().ByName("o1").PutSignals(signal.New("this signal is generated by c1")) + + // Also simulate input signal on one port + c1.Inputs().ByName("i1").PutSignals(signal.New("this is input signal for c1")) + + return New("fm").WithComponents(c1, c2) + }, + args: args{ + cycle: cycle.New(). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeWaitingForInputsKeep). + WithError(component.NewErrWaitForInputs(true)), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + }, + assertions: func(t *testing.T, fm *FMesh) { + // As c1 is waiting for inputs it's outputs must not be flushed + assert.False(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) + + // The inputs must NOT be cleared + assert.True(t, fm.Components().ByName("c1").Inputs().AnyHasSignals()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fm := tt.getFM() + fm.drainComponents(tt.args.cycle) + if tt.assertions != nil { + tt.assertions(t, fm) + } + }) + } +} From 5a84753798da8fdf927791d67b7d1e92f316a829 Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 7 Oct 2024 16:23:02 +0300 Subject: [PATCH 03/41] Minor: comment fixed --- component/activation_result.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/activation_result.go b/component/activation_result.go index 9a64eda..3b4982e 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -32,7 +32,7 @@ const ( // ActivationCodePanicked : component is activated, but panicked ActivationCodePanicked - // ActivationCodeWaitingForInputs : component waits for specific inputs, but all input signals in current activation cycle may be cleared (default behaviour) + // ActivationCodeWaitingForInputsClear : component waits for specific inputs, but all input signals in current activation cycle may be cleared (default behaviour) ActivationCodeWaitingForInputsClear // ActivationCodeWaitingForInputsKeep : component waits for specific inputs, but wants to keep current input signals for the next cycle From 7a81503d4df465422cc4100c2c6da84100a7443f Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 15:09:47 +0300 Subject: [PATCH 04/41] Extract common name property --- common/named_entity.go | 15 +++++++++ common/named_entity_test.go | 65 ++++++++++++++++++++++++++++++++++++ component/collection_test.go | 9 +---- component/component.go | 14 +++----- component/component_test.go | 51 +++++----------------------- fmesh.go | 14 +++----- fmesh_test.go | 39 +++++----------------- port/collection_test.go | 44 ++++-------------------- port/port.go | 13 +++----- port/port_test.go | 47 ++++---------------------- 10 files changed, 124 insertions(+), 187 deletions(-) create mode 100644 common/named_entity.go create mode 100644 common/named_entity_test.go diff --git a/common/named_entity.go b/common/named_entity.go new file mode 100644 index 0000000..ea46bcd --- /dev/null +++ b/common/named_entity.go @@ -0,0 +1,15 @@ +package common + +type NamedEntity struct { + name string +} + +// NewNamedEntity constructor +func NewNamedEntity(name string) NamedEntity { + return NamedEntity{name: name} +} + +// Name getter +func (n NamedEntity) Name() string { + return n.name +} diff --git a/common/named_entity_test.go b/common/named_entity_test.go new file mode 100644 index 0000000..eb94d59 --- /dev/null +++ b/common/named_entity_test.go @@ -0,0 +1,65 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewNamedEntity(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want NamedEntity + }{ + { + name: "empty name is valid", + args: args{ + name: "", + }, + want: NamedEntity{ + name: "", + }, + }, + { + name: "with name", + args: args{ + name: "component1", + }, + want: NamedEntity{ + name: "component1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, NewNamedEntity(tt.args.name)) + }) + } +} + +func TestNamedEntity_Name(t *testing.T) { + tests := []struct { + name string + namedEntity NamedEntity + want string + }{ + { + name: "empty name", + namedEntity: NewNamedEntity(""), + want: "", + }, + { + name: "with name", + namedEntity: NewNamedEntity("port2"), + want: "port2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.namedEntity.Name()) + }) + } +} diff --git a/component/collection_test.go b/component/collection_test.go index 306b53d..f891ac1 100644 --- a/component/collection_test.go +++ b/component/collection_test.go @@ -1,7 +1,6 @@ package component import ( - "github.com/hovsep/fmesh/port" "github.com/stretchr/testify/assert" "testing" ) @@ -22,13 +21,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "c2", }, - want: &Component{ - name: "c2", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, - }, + want: New("c2"), }, { name: "component not found", diff --git a/component/component.go b/component/component.go index 2161d70..d3ef28b 100644 --- a/component/component.go +++ b/component/component.go @@ -3,6 +3,7 @@ package component import ( "errors" "fmt" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/port" ) @@ -10,7 +11,7 @@ type ActivationFunc func(inputs port.Collection, outputs port.Collection) error // Component defines a main building block of FMesh type Component struct { - name string + common.NamedEntity description string inputs port.Collection outputs port.Collection @@ -20,9 +21,9 @@ type Component struct { // New creates initialized component func New(name string) *Component { return &Component{ - name: name, - inputs: port.NewCollection(), - outputs: port.NewCollection(), + NamedEntity: common.NewNamedEntity(name), + inputs: port.NewCollection(), + outputs: port.NewCollection(), } } @@ -62,11 +63,6 @@ func (c *Component) WithActivationFunc(f ActivationFunc) *Component { return c } -// Name getter -func (c *Component) Name() string { - return c.name -} - // Description getter func (c *Component) Description() string { return c.description diff --git a/component/component_test.go b/component/component_test.go index 3bbac18..83209cd 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -2,6 +2,7 @@ package component import ( "errors" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/port" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -22,26 +23,14 @@ func TestNewComponent(t *testing.T) { args: args{ name: "", }, - want: &Component{ - name: "", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, - }, + want: New(""), }, { name: "with name", args: args{ name: "multiplier", }, - want: &Component{ - name: "multiplier", - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, - }, + want: New("multiplier"), }, } for _, tt := range tests { @@ -51,30 +40,6 @@ func TestNewComponent(t *testing.T) { } } -func TestComponent_Name(t *testing.T) { - tests := []struct { - name string - component *Component - want string - }{ - { - name: "empty name", - component: New(""), - want: "", - }, - { - name: "with name", - component: New("c1"), - want: "c1", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.component.Name()) - }) - } -} - func TestComponent_Description(t *testing.T) { tests := []struct { name string @@ -266,7 +231,7 @@ func TestComponent_WithDescription(t *testing.T) { description: "descr", }, want: &Component{ - name: "c1", + NamedEntity: common.NewNamedEntity("c1"), description: "descr", inputs: port.Collection{}, outputs: port.Collection{}, @@ -298,7 +263,7 @@ func TestComponent_WithInputs(t *testing.T) { portNames: []string{"p1", "p2"}, }, want: &Component{ - name: "c1", + NamedEntity: common.NewNamedEntity("c1"), description: "", inputs: port.Collection{ "p1": port.New("p1"), @@ -315,7 +280,7 @@ func TestComponent_WithInputs(t *testing.T) { portNames: nil, }, want: &Component{ - name: "c1", + NamedEntity: common.NewNamedEntity("c1"), description: "", inputs: port.Collection{}, outputs: port.Collection{}, @@ -347,7 +312,7 @@ func TestComponent_WithOutputs(t *testing.T) { portNames: []string{"p1", "p2"}, }, want: &Component{ - name: "c1", + NamedEntity: common.NewNamedEntity("c1"), description: "", inputs: port.Collection{}, outputs: port.Collection{ @@ -364,7 +329,7 @@ func TestComponent_WithOutputs(t *testing.T) { portNames: nil, }, want: &Component{ - name: "c1", + NamedEntity: common.NewNamedEntity("c1"), description: "", inputs: port.Collection{}, outputs: port.Collection{}, diff --git a/fmesh.go b/fmesh.go index 8c02f88..7538f93 100644 --- a/fmesh.go +++ b/fmesh.go @@ -1,6 +1,7 @@ package fmesh import ( + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "sync" @@ -22,7 +23,7 @@ var defaultConfig = Config{ // FMesh is the functional mesh type FMesh struct { - name string + common.NamedEntity description string components component.Collection config Config @@ -31,17 +32,12 @@ type FMesh struct { // New creates a new f-mesh func New(name string) *FMesh { return &FMesh{ - name: name, - components: component.NewCollection(), - config: defaultConfig, + NamedEntity: common.NewNamedEntity(name), + components: component.NewCollection(), + config: defaultConfig, } } -// Name getter -func (fm *FMesh) Name() string { - return fm.name -} - // Description getter func (fm *FMesh) Description() string { return fm.description diff --git a/fmesh_test.go b/fmesh_test.go index e582de9..b86f670 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -2,6 +2,7 @@ package fmesh import ( "errors" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "github.com/hovsep/fmesh/port" @@ -35,9 +36,9 @@ func TestNew(t *testing.T) { name: "fm1", }, want: &FMesh{ - name: "fm1", - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + components: component.Collection{}, + config: defaultConfig, }, }, } @@ -65,7 +66,7 @@ func TestFMesh_WithDescription(t *testing.T) { description: "", }, want: &FMesh{ - name: "fm1", + NamedEntity: common.NewNamedEntity("fm1"), description: "", components: component.Collection{}, config: defaultConfig, @@ -78,7 +79,7 @@ func TestFMesh_WithDescription(t *testing.T) { description: "descr", }, want: &FMesh{ - name: "fm1", + NamedEntity: common.NewNamedEntity("fm1"), description: "descr", components: component.Collection{}, config: defaultConfig, @@ -112,8 +113,8 @@ func TestFMesh_WithConfig(t *testing.T) { }, }, want: &FMesh{ - name: "fm1", - components: component.Collection{}, + NamedEntity: common.NewNamedEntity("fm1"), + components: component.Collection{}, config: Config{ ErrorHandlingStrategy: IgnoreAll, CyclesLimit: 9999, @@ -197,30 +198,6 @@ func TestFMesh_WithComponents(t *testing.T) { } } -func TestFMesh_Name(t *testing.T) { - tests := []struct { - name string - fm *FMesh - want string - }{ - { - name: "empty name is valid", - fm: New(""), - want: "", - }, - { - name: "with name", - fm: New("fm1"), - want: "fm1", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.fm.Name()) - }) - } -} - func TestFMesh_Description(t *testing.T) { tests := []struct { name string diff --git a/port/collection_test.go b/port/collection_test.go index e33ebb3..eef3819 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -81,7 +81,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "p1", }, - want: &Port{name: "p1", pipes: Group{}, signals: signal.Group{}}, + want: New("p1"), }, { name: "port with signals found", @@ -89,11 +89,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "p2", }, - want: &Port{ - name: "p2", - signals: signal.NewGroup().With(signal.New(12)), - pipes: Group{}, - }, + want: New("p2").WithSignals(signal.New(12)), }, { name: "port not found", @@ -132,32 +128,15 @@ func TestCollection_ByNames(t *testing.T) { args: args{ names: []string{"p1"}, }, - want: Collection{ - "p1": &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, - }, + want: NewCollection().With(New("p1")), }, { name: "multiple ports found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + ports: NewCollection().With(NewGroup("p1", "p2", "p3", "p4")...), args: args{ names: []string{"p1", "p2"}, }, - want: Collection{ - "p1": &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, - "p2": &Port{ - name: "p2", - pipes: Group{}, - signals: signal.Group{}, - }, - }, + want: NewCollection().With(NewGroup("p1", "p2")...), }, { name: "single port not found", @@ -173,18 +152,7 @@ func TestCollection_ByNames(t *testing.T) { args: args{ names: []string{"p1", "p2", "p3"}, }, - want: Collection{ - "p1": &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, - "p2": &Port{ - name: "p2", - pipes: Group{}, - signals: signal.Group{}, - }, - }, + want: NewCollection().With(NewGroup("p1", "p2")...), }, } for _, tt := range tests { diff --git a/port/port.go b/port/port.go index 5b26c4e..a9318d0 100644 --- a/port/port.go +++ b/port/port.go @@ -1,12 +1,13 @@ package port import ( + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) // Port defines a connectivity point of a component type Port struct { - name string + common.NamedEntity signals signal.Group //Signal buffer pipes Group //Outbound pipes } @@ -14,15 +15,11 @@ type Port struct { // New creates a new port func New(name string) *Port { return &Port{ - name: name, - pipes: NewGroup(), - signals: signal.NewGroup(), + NamedEntity: common.NewNamedEntity(name), + pipes: NewGroup(), + signals: signal.NewGroup(), } -} -// Name getter -func (p *Port) Name() string { - return p.name } // Signals getter diff --git a/port/port_test.go b/port/port_test.go index 98b12cb..b82bb94 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -63,12 +63,12 @@ func TestPort_Clear(t *testing.T) { { name: "happy path", before: New("p").WithSignals(signal.New(111)), - after: &Port{name: "p", pipes: Group{}, signals: signal.Group{}}, + after: New("p"), }, { name: "cleaning empty port", before: New("emptyPort"), - after: &Port{name: "emptyPort", pipes: Group{}, signals: signal.Group{}}, + after: New("emptyPort"), }, } for _, tt := range tests { @@ -94,11 +94,7 @@ func TestPort_PipeTo(t *testing.T) { { name: "happy path", before: p1, - after: &Port{ - name: "p1", - pipes: Group{p2, p3}, - signals: signal.Group{}, - }, + after: New("p1").withPipes(p2, p3), args: args{ toPorts: []*Port{p2, p3}, }, @@ -106,11 +102,7 @@ func TestPort_PipeTo(t *testing.T) { { name: "invalid ports are ignored", before: p4, - after: &Port{ - name: "p4", - pipes: Group{p2}, - signals: signal.Group{}, - }, + after: New("p4").withPipes(p2), args: args{ toPorts: []*Port{p2, nil}, }, @@ -183,25 +175,6 @@ func TestPort_PutSignals(t *testing.T) { } } -func TestPort_Name(t *testing.T) { - tests := []struct { - name string - port *Port - want string - }{ - { - name: "happy path", - port: New("p777"), - want: "p777", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.port.Name()) - }) - } -} - func TestNewPort(t *testing.T) { type args struct { name string @@ -216,22 +189,14 @@ func TestNewPort(t *testing.T) { args: args{ name: "", }, - want: &Port{ - name: "", - pipes: Group{}, - signals: signal.Group{}, - }, + want: New(""), }, { name: "with name", args: args{ name: "p1", }, - want: &Port{ - name: "p1", - pipes: Group{}, - signals: signal.Group{}, - }, + want: New("p1"), }, } for _, tt := range tests { From 486c11c84a7ea2ee8ba2eae43d21df6a423aa131 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 15:44:52 +0300 Subject: [PATCH 05/41] Extract common description property --- common/described_entity.go | 15 ++++++++ common/described_entity_test.go | 65 +++++++++++++++++++++++++++++++++ component/component.go | 15 +++----- component/component_test.go | 64 ++++++++++---------------------- fmesh.go | 13 ++----- fmesh_test.go | 40 ++++---------------- 6 files changed, 117 insertions(+), 95 deletions(-) create mode 100644 common/described_entity.go create mode 100644 common/described_entity_test.go diff --git a/common/described_entity.go b/common/described_entity.go new file mode 100644 index 0000000..8696a49 --- /dev/null +++ b/common/described_entity.go @@ -0,0 +1,15 @@ +package common + +type DescribedEntity struct { + description string +} + +// NewDescribedEntity constructor +func NewDescribedEntity(description string) DescribedEntity { + return DescribedEntity{description: description} +} + +// Description getter +func (d DescribedEntity) Description() string { + return d.description +} diff --git a/common/described_entity_test.go b/common/described_entity_test.go new file mode 100644 index 0000000..91e5627 --- /dev/null +++ b/common/described_entity_test.go @@ -0,0 +1,65 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewDescribedEntity(t *testing.T) { + type args struct { + description string + } + tests := []struct { + name string + args args + want DescribedEntity + }{ + { + name: "empty description", + args: args{ + description: "", + }, + want: DescribedEntity{ + description: "", + }, + }, + { + name: "with description", + args: args{ + description: "component1 is used to generate logs", + }, + want: DescribedEntity{ + description: "component1 is used to generate logs", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, NewDescribedEntity(tt.args.description)) + }) + } +} + +func TestDescribedEntity_Description(t *testing.T) { + tests := []struct { + name string + describedEntity DescribedEntity + want string + }{ + { + name: "empty description", + describedEntity: NewDescribedEntity(""), + want: "", + }, + { + name: "with description", + describedEntity: NewDescribedEntity("component2 is used to handle errors"), + want: "component2 is used to handle errors", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.describedEntity.Description()) + }) + } +} diff --git a/component/component.go b/component/component.go index d3ef28b..957bc32 100644 --- a/component/component.go +++ b/component/component.go @@ -12,10 +12,10 @@ type ActivationFunc func(inputs port.Collection, outputs port.Collection) error // Component defines a main building block of FMesh type Component struct { common.NamedEntity - description string - inputs port.Collection - outputs port.Collection - f ActivationFunc + common.DescribedEntity + inputs port.Collection + outputs port.Collection + f ActivationFunc } // New creates initialized component @@ -29,7 +29,7 @@ func New(name string) *Component { // WithDescription sets a description func (c *Component) WithDescription(description string) *Component { - c.description = description + c.DescribedEntity = common.NewDescribedEntity(description) return c } @@ -63,11 +63,6 @@ func (c *Component) WithActivationFunc(f ActivationFunc) *Component { return c } -// Description getter -func (c *Component) Description() string { - return c.description -} - // Inputs getter func (c *Component) Inputs() port.Collection { return c.inputs diff --git a/component/component_test.go b/component/component_test.go index 83209cd..ebf23fe 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -40,30 +40,6 @@ func TestNewComponent(t *testing.T) { } } -func TestComponent_Description(t *testing.T) { - tests := []struct { - name string - component *Component - want string - }{ - { - name: "no description", - component: New("c1"), - want: "", - }, - { - name: "with description", - component: New("c1").WithDescription("descr"), - want: "descr", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.component.Description()) - }) - } -} - func TestComponent_FlushOutputs(t *testing.T) { sink := port.New("sink") @@ -231,11 +207,11 @@ func TestComponent_WithDescription(t *testing.T) { description: "descr", }, want: &Component{ - NamedEntity: common.NewNamedEntity("c1"), - description: "descr", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity("descr"), + inputs: port.Collection{}, + outputs: port.Collection{}, + f: nil, }, }, } @@ -263,8 +239,8 @@ func TestComponent_WithInputs(t *testing.T) { portNames: []string{"p1", "p2"}, }, want: &Component{ - NamedEntity: common.NewNamedEntity("c1"), - description: "", + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), inputs: port.Collection{ "p1": port.New("p1"), "p2": port.New("p2"), @@ -280,11 +256,11 @@ func TestComponent_WithInputs(t *testing.T) { portNames: nil, }, want: &Component{ - NamedEntity: common.NewNamedEntity("c1"), - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + inputs: port.Collection{}, + outputs: port.Collection{}, + f: nil, }, }, } @@ -312,9 +288,9 @@ func TestComponent_WithOutputs(t *testing.T) { portNames: []string{"p1", "p2"}, }, want: &Component{ - NamedEntity: common.NewNamedEntity("c1"), - description: "", - inputs: port.Collection{}, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + inputs: port.Collection{}, outputs: port.Collection{ "p1": port.New("p1"), "p2": port.New("p2"), @@ -329,11 +305,11 @@ func TestComponent_WithOutputs(t *testing.T) { portNames: nil, }, want: &Component{ - NamedEntity: common.NewNamedEntity("c1"), - description: "", - inputs: port.Collection{}, - outputs: port.Collection{}, - f: nil, + NamedEntity: common.NewNamedEntity("c1"), + DescribedEntity: common.NewDescribedEntity(""), + inputs: port.Collection{}, + outputs: port.Collection{}, + f: nil, }, }, } diff --git a/fmesh.go b/fmesh.go index 7538f93..1ef3cb0 100644 --- a/fmesh.go +++ b/fmesh.go @@ -24,9 +24,9 @@ var defaultConfig = Config{ // FMesh is the functional mesh type FMesh struct { common.NamedEntity - description string - components component.Collection - config Config + common.DescribedEntity + components component.Collection + config Config } // New creates a new f-mesh @@ -38,18 +38,13 @@ func New(name string) *FMesh { } } -// Description getter -func (fm *FMesh) Description() string { - return fm.description -} - func (fm *FMesh) Components() component.Collection { return fm.components } // WithDescription sets a description func (fm *FMesh) WithDescription(description string) *FMesh { - fm.description = description + fm.DescribedEntity = common.NewDescribedEntity(description) return fm } diff --git a/fmesh_test.go b/fmesh_test.go index b86f670..40bf0c9 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -66,10 +66,10 @@ func TestFMesh_WithDescription(t *testing.T) { description: "", }, want: &FMesh{ - NamedEntity: common.NewNamedEntity("fm1"), - description: "", - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity(""), + components: component.Collection{}, + config: defaultConfig, }, }, { @@ -79,10 +79,10 @@ func TestFMesh_WithDescription(t *testing.T) { description: "descr", }, want: &FMesh{ - NamedEntity: common.NewNamedEntity("fm1"), - description: "descr", - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity("descr"), + components: component.Collection{}, + config: defaultConfig, }, }, } @@ -198,30 +198,6 @@ func TestFMesh_WithComponents(t *testing.T) { } } -func TestFMesh_Description(t *testing.T) { - tests := []struct { - name string - fm *FMesh - want string - }{ - { - name: "empty description", - fm: New("fm1"), - want: "", - }, - { - name: "with description", - fm: New("fm1").WithDescription("descr"), - want: "descr", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.fm.Description()) - }) - } -} - func TestFMesh_Run(t *testing.T) { tests := []struct { name string From 1222a6eeb45b5ef26d053ddca478bec38c7618c2 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 17:58:15 +0300 Subject: [PATCH 06/41] Minor: refactor traits --- common/described_entity.go | 4 ++-- common/named_entity.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/described_entity.go b/common/described_entity.go index 8696a49..df8fc7e 100644 --- a/common/described_entity.go +++ b/common/described_entity.go @@ -10,6 +10,6 @@ func NewDescribedEntity(description string) DescribedEntity { } // Description getter -func (d DescribedEntity) Description() string { - return d.description +func (e DescribedEntity) Description() string { + return e.description } diff --git a/common/named_entity.go b/common/named_entity.go index ea46bcd..55d3f8c 100644 --- a/common/named_entity.go +++ b/common/named_entity.go @@ -10,6 +10,6 @@ func NewNamedEntity(name string) NamedEntity { } // Name getter -func (n NamedEntity) Name() string { - return n.name +func (e NamedEntity) Name() string { + return e.name } From a5fc1a1474cff23045f2659acec47694dd1f3178 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 17:58:29 +0300 Subject: [PATCH 07/41] New trait: labeled entity --- common/labeled_entity.go | 69 ++++++ common/labeled_entity_test.go | 419 ++++++++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100644 common/labeled_entity.go create mode 100644 common/labeled_entity_test.go diff --git a/common/labeled_entity.go b/common/labeled_entity.go new file mode 100644 index 0000000..3df05d8 --- /dev/null +++ b/common/labeled_entity.go @@ -0,0 +1,69 @@ +package common + +type LabelsCollection map[string]string + +type LabeledEntity struct { + labels LabelsCollection +} + +// NewLabeledEntity constructor +func NewLabeledEntity(labels LabelsCollection) LabeledEntity { + + return LabeledEntity{labels: labels} +} + +// Labels getter +func (e *LabeledEntity) Labels() LabelsCollection { + return e.labels +} + +// SetLabels overwrites labels collection +func (e *LabeledEntity) SetLabels(labels LabelsCollection) { + e.labels = labels +} + +// AddLabel adds or updates(if label already exists) single label +func (e *LabeledEntity) AddLabel(label string, value string) { + if e.labels == nil { + e.labels = make(LabelsCollection) + } + e.labels[label] = value +} + +// AddLabels adds or updates(if label already exists) multiple labels +func (e *LabeledEntity) AddLabels(labels LabelsCollection) { + for label, value := range labels { + e.AddLabel(label, value) + } +} + +// DeleteLabel deletes given label +func (e *LabeledEntity) DeleteLabel(label string) { + delete(e.labels, label) +} + +// HasLabel returns true when entity has given label or false otherwise +func (e *LabeledEntity) HasLabel(label string) bool { + _, ok := e.labels[label] + return ok +} + +// HasAllLabels checks if entity has all labels +func (e *LabeledEntity) HasAllLabels(label ...string) bool { + for _, l := range label { + if !e.HasLabel(l) { + return false + } + } + return true +} + +// HasAnyLabel checks if entity has at least one of given labels +func (e *LabeledEntity) HasAnyLabel(label ...string) bool { + for _, l := range label { + if e.HasLabel(l) { + return true + } + } + return false +} diff --git a/common/labeled_entity_test.go b/common/labeled_entity_test.go new file mode 100644 index 0000000..6d45069 --- /dev/null +++ b/common/labeled_entity_test.go @@ -0,0 +1,419 @@ +package common + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewLabeledEntity(t *testing.T) { + type args struct { + labels LabelsCollection + } + tests := []struct { + name string + args args + want LabeledEntity + }{ + { + name: "empty labels", + args: args{ + labels: nil, + }, + want: LabeledEntity{ + labels: nil, + }, + }, + { + name: "with labels", + args: args{ + labels: LabelsCollection{ + "label1": "value1", + }, + }, + want: LabeledEntity{ + labels: LabelsCollection{ + "label1": "value1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, NewLabeledEntity(tt.args.labels)) + }) + } +} + +func TestLabeledEntity_Labels(t *testing.T) { + tests := []struct { + name string + labeledEntity LabeledEntity + want LabelsCollection + }{ + { + name: "no labels", + labeledEntity: NewLabeledEntity(nil), + want: nil, + }, + { + name: "with labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + want: LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.labeledEntity.Labels()) + }) + } +} + +func TestLabeledEntity_SetLabels(t *testing.T) { + type args struct { + labels LabelsCollection + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "setting to empty labels collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + labels: LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }, labeledEntity.Labels()) + }, + }, + { + name: "setting to non-empty labels collection", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + "l99": "val1", + }), + args: args{ + labels: LabelsCollection{ + "l4": "v4", + "l5": "v5", + "l6": "v6", + "l99": "val2", + }, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l4": "v4", + "l5": "v5", + "l6": "v6", + "l99": "val2", + }, labeledEntity.Labels()) + }, + }, + { + name: "setting nil", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + "l99": "val1", + }), + args: args{ + labels: nil, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Nil(t, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.SetLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_AddLabel(t *testing.T) { + type args struct { + label string + value string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "adding to empty labels collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + label: "l1", + value: "v1", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + }, labeledEntity.Labels()) + }, + }, + { + name: "adding to non-empty labels collection", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + }), + args: args{ + label: "l2", + value: "v2", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, labeledEntity.Labels()) + }, + }, + { + name: "overwriting a label", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l2", + value: "v3", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v3", + }, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.AddLabel(tt.args.label, tt.args.value) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_AddLabels(t *testing.T) { + type args struct { + labels LabelsCollection + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "adding to non-empty labels collection", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + labels: LabelsCollection{ + "l3": "v100", + "l4": "v4", + "l5": "v5", + }, + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v100", + "l4": "v4", + "l5": "v5", + }, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.AddLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_DeleteLabel(t *testing.T) { + type args struct { + label string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + assertions func(t *testing.T, labeledEntity LabeledEntity) + }{ + { + name: "label found and deleted", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l1", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l2": "v2", + }, labeledEntity.Labels()) + }, + }, + { + name: "label not found, no-op", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l3", + }, + assertions: func(t *testing.T, labeledEntity LabeledEntity) { + assert.Equal(t, LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, labeledEntity.Labels()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.labeledEntity.DeleteLabel(tt.args.label) + if tt.assertions != nil { + tt.assertions(t, tt.labeledEntity) + } + }) + } +} + +func TestLabeledEntity_HasAllLabels(t *testing.T) { + type args struct { + label []string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + want bool + }{ + { + name: "empty collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + label: []string{"l1"}, + }, + want: false, + }, + { + name: "has all labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l1", "l2"}, + }, + want: true, + }, + { + name: "does not have all labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l1", "l2", "l4"}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.labeledEntity.HasAllLabels(tt.args.label...)) + }) + } +} + +func TestLabeledEntity_HasAnyLabel(t *testing.T) { + type args struct { + label []string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + want bool + }{ + { + name: "empty collection", + labeledEntity: NewLabeledEntity(nil), + args: args{ + label: []string{"l1"}, + }, + want: false, + }, + { + name: "has some labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l1", "l10"}, + }, + want: true, + }, + { + name: "does not have any of labels", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + "l3": "v3", + }), + args: args{ + label: []string{"l10", "l20", "l4"}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.labeledEntity.HasAnyLabel(tt.args.label...)) + }) + } +} From 740b0d1ae5b06c6ab7244b88727a7d4bca953dc8 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 18:02:21 +0300 Subject: [PATCH 08/41] Add labels to ports --- port/port.go | 7 +++++++ port/port_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/port/port.go b/port/port.go index a9318d0..d988ef3 100644 --- a/port/port.go +++ b/port/port.go @@ -8,6 +8,7 @@ import ( // Port defines a connectivity point of a component type Port struct { common.NamedEntity + common.LabeledEntity signals signal.Group //Signal buffer pipes Group //Outbound pipes } @@ -92,6 +93,12 @@ func (p *Port) withPipes(destPorts ...*Port) *Port { return p } +// WithLabels sets labels and returns the port +func (p *Port) WithLabels(labels common.LabelsCollection) *Port { + p.LabeledEntity.SetLabels(labels) + return p +} + // ForwardSignals copies all signals from source port to destination port, without clearing the source port func ForwardSignals(source *Port, dest *Port) { dest.PutSignals(source.Signals()...) diff --git a/port/port_test.go b/port/port_test.go index b82bb94..3f5dba4 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -1,6 +1,7 @@ package port import ( + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" "testing" @@ -299,3 +300,38 @@ func TestPort_Flush(t *testing.T) { }) } } + +func TestPort_WithLabels(t *testing.T) { + type args struct { + labels common.LabelsCollection + } + tests := []struct { + name string + port *Port + args args + assertions func(t *testing.T, port *Port) + }{ + { + name: "happy path", + port: New("p1"), + args: args{ + labels: common.LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, + }, + assertions: func(t *testing.T, port *Port) { + assert.Len(t, port.Labels(), 2) + assert.True(t, port.HasAllLabels("l1", "l2")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + portAfter := tt.port.WithLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, portAfter) + } + }) + } +} From c43b15362ae32964f1ff9516fef6bc072861378c Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 18:05:13 +0300 Subject: [PATCH 09/41] Add labels to components --- component/component.go | 7 +++++++ component/component_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/component/component.go b/component/component.go index 957bc32..e1506a6 100644 --- a/component/component.go +++ b/component/component.go @@ -13,6 +13,7 @@ type ActivationFunc func(inputs port.Collection, outputs port.Collection) error type Component struct { common.NamedEntity common.DescribedEntity + common.LabeledEntity inputs port.Collection outputs port.Collection f ActivationFunc @@ -63,6 +64,12 @@ func (c *Component) WithActivationFunc(f ActivationFunc) *Component { return c } +// WithLabels sets labels and returns the component +func (c *Component) WithLabels(labels common.LabelsCollection) *Component { + c.LabeledEntity.SetLabels(labels) + return c +} + // Inputs getter func (c *Component) Inputs() port.Collection { return c.inputs diff --git a/component/component_test.go b/component/component_test.go index ebf23fe..fbbabfd 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -599,3 +599,38 @@ func TestComponent_WithOutputsIndexed(t *testing.T) { }) } } + +func TestComponent_WithLabels(t *testing.T) { + type args struct { + labels common.LabelsCollection + } + tests := []struct { + name string + component *Component + args args + assertions func(t *testing.T, component *Component) + }{ + { + name: "happy path", + component: New("c1"), + args: args{ + labels: common.LabelsCollection{ + "l1": "v1", + "l2": "v2", + }, + }, + assertions: func(t *testing.T, component *Component) { + assert.Len(t, component.Labels(), 2) + assert.True(t, component.HasAllLabels("l1", "l2")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + componentAfter := tt.component.WithLabels(tt.args.labels) + if tt.assertions != nil { + tt.assertions(t, componentAfter) + } + }) + } +} From 8251f16223b44c0ad7091861544312acecdc6d6d Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 18:12:12 +0300 Subject: [PATCH 10/41] LabeledEntity: add method --- common/labeled_entity.go | 15 +++++++++ common/labeled_entity_test.go | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/common/labeled_entity.go b/common/labeled_entity.go index 3df05d8..1297ed9 100644 --- a/common/labeled_entity.go +++ b/common/labeled_entity.go @@ -1,11 +1,15 @@ package common +import "errors" + type LabelsCollection map[string]string type LabeledEntity struct { labels LabelsCollection } +var errLabelNotFound = errors.New("label not found") + // NewLabeledEntity constructor func NewLabeledEntity(labels LabelsCollection) LabeledEntity { @@ -17,6 +21,17 @@ func (e *LabeledEntity) Labels() LabelsCollection { return e.labels } +// Label returns the value of single label or nil if it is not found +func (e *LabeledEntity) Label(label string) (string, error) { + value, ok := e.labels[label] + + if !ok { + return "", errLabelNotFound + } + + return value, nil +} + // SetLabels overwrites labels collection func (e *LabeledEntity) SetLabels(labels LabelsCollection) { e.labels = labels diff --git a/common/labeled_entity_test.go b/common/labeled_entity_test.go index 6d45069..c514e2d 100644 --- a/common/labeled_entity_test.go +++ b/common/labeled_entity_test.go @@ -417,3 +417,65 @@ func TestLabeledEntity_HasAnyLabel(t *testing.T) { }) } } + +func TestLabeledEntity_Label(t *testing.T) { + type args struct { + label string + } + tests := []struct { + name string + labeledEntity LabeledEntity + args args + want string + wantErr bool + }{ + { + name: "no labels", + labeledEntity: LabeledEntity{ + labels: nil, + }, + args: args{ + label: "l1", + }, + want: "", + wantErr: true, + }, + { + name: "label found", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l2", + }, + want: "v2", + wantErr: false, + }, + { + name: "label not found", + labeledEntity: NewLabeledEntity(LabelsCollection{ + "l1": "v1", + "l2": "v2", + }), + args: args{ + label: "l3", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.labeledEntity.Label(tt.args.label) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.want, got) + }) + } +} From de0c6cd48cf6755f623a3a3474a01a76273cde73 Mon Sep 17 00:00:00 2001 From: hovsep Date: Tue, 8 Oct 2024 14:05:44 +0300 Subject: [PATCH 11/41] WIP: dot exporter --- export/dot.go | 80 ++++++++++++++++++++++++++++ export/exporter.go | 7 +++ go.mod | 1 + go.sum | 2 + integration_tests/piping/fan_test.go | 6 +++ port/port.go | 5 ++ 6 files changed, 101 insertions(+) create mode 100644 export/dot.go create mode 100644 export/exporter.go diff --git a/export/dot.go b/export/dot.go new file mode 100644 index 0000000..451ac16 --- /dev/null +++ b/export/dot.go @@ -0,0 +1,80 @@ +package export + +import ( + "bytes" + "fmt" + "github.com/hovsep/fmesh" + "github.com/lucasepe/dot" +) + +type dotExporter struct { +} + +func NewDotExporter() Exporter { + return &dotExporter{} +} + +// Export returns the f-mesh represented as digraph in DOT language +func (d *dotExporter) Export(fm *fmesh.FMesh) []byte { + // Setup graph + graph := dot.NewGraph(dot.Directed) + graph. + Attr("layout", "dot"). + Attr("splines", "ortho") + + for componentName, c := range fm.Components() { + // Component subgraph (wrapper) + componentSubgraph := graph.NewSubgraph() + componentSubgraph. + NodeBaseAttrs(). + Attr("width", "1.0").Attr("height", "1.0") + componentSubgraph. + Attr("label", componentName). + Attr("cluster", "true"). + Attr("style", "rounded"). + Attr("color", "black"). + Attr("bgcolor", "lightgrey"). + Attr("margin", "20") + + // Component node + componentNode := componentSubgraph.Node() + componentNode.Attr("label", "𝑓") + if c.Description() != "" { + componentNode.Attr("label", c.Description()) + } + componentNode. + Attr("color", "blue"). + Attr("shape", "rect"). + Attr("group", componentName) + + // Input ports + for portName := range c.Inputs() { + portNode := componentSubgraph.NodeWithID(fmt.Sprintf("%s.inputs.%s", componentName, portName)) + portNode. + Attr("label", portName). + Attr("shape", "circle"). + Attr("group", componentName) + + componentSubgraph.Edge(portNode, componentNode) + } + + // Output ports + for portName, port := range c.Outputs() { + portNode := componentSubgraph.NodeWithID(fmt.Sprintf("%s.inputs.%s", componentName, portName)) + portNode. + Attr("label", portName). + Attr("shape", "circle"). + Attr("group", componentName) + + componentSubgraph.Edge(componentNode, portNode) + + // Pipes + //@TODO + } + } + + buf := new(bytes.Buffer) + graph.Write(buf) + + return buf.Bytes() +} diff --git a/export/exporter.go b/export/exporter.go new file mode 100644 index 0000000..4ab7567 --- /dev/null +++ b/export/exporter.go @@ -0,0 +1,7 @@ +package export + +import "github.com/hovsep/fmesh" + +type Exporter interface { + Export(fm *fmesh.FMesh) []byte +} diff --git a/go.mod b/go.mod index b38dabe..ec2fd61 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/lucasepe/dot v0.4.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 60ce688..c22838b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/lucasepe/dot v0.4.3 h1:gqQaH00pPlyK21GgCtoYI70pwOsrjsjJFBgegYm2+/E= +github.com/lucasepe/dot v0.4.3/go.mod h1:5gEWjskJdc7e0jMJJLg/PVNv5ynTLvdTYq9OTgphX8Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index b45fe19..4ee34cd 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -1,9 +1,11 @@ package integration_tests import ( + "fmt" "github.com/hovsep/fmesh" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" + "github.com/hovsep/fmesh/export" "github.com/hovsep/fmesh/port" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -147,6 +149,10 @@ func Test_Fan(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fm := tt.setupFM() + + exp := export.NewDotExporter() + fmt.Println(string(exp.Export(fm))) + tt.setInputs(fm) cycles, err := fm.Run() tt.assertions(t, fm, cycles, err) diff --git a/port/port.go b/port/port.go index d988ef3..2b8f6cb 100644 --- a/port/port.go +++ b/port/port.go @@ -28,6 +28,11 @@ func (p *Port) Signals() signal.Group { return p.signals } +// Pipes getter +func (p *Port) Pipes() Group { + return p.pipes +} + // setSignals sets signals field func (p *Port) setSignals(signals signal.Group) { p.signals = signals From aa7dc91266b63de250eebcb1f95aff3916d73d50 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 9 Oct 2024 18:51:41 +0300 Subject: [PATCH 12/41] Dot exporter initial code --- export/dot.go | 72 ++++++++++++++++++++++++++++++++-------------- export/exporter.go | 3 +- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/export/dot.go b/export/dot.go index 451ac16..b09e31a 100644 --- a/export/dot.go +++ b/export/dot.go @@ -10,71 +10,101 @@ import ( type dotExporter struct { } +const nodeIDLabel = "export/dot/id" + func NewDotExporter() Exporter { return &dotExporter{} } // Export returns the f-mesh represented as digraph in DOT language -func (d *dotExporter) Export(fm *fmesh.FMesh) []byte { - // Setup graph +func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { + // Setup main graph graph := dot.NewGraph(dot.Directed) graph. Attr("layout", "dot"). Attr("splines", "ortho") - for componentName, c := range fm.Components() { + for _, component := range fm.Components() { // Component subgraph (wrapper) componentSubgraph := graph.NewSubgraph() componentSubgraph. NodeBaseAttrs(). Attr("width", "1.0").Attr("height", "1.0") componentSubgraph. - Attr("label", componentName). + Attr("label", component.Name()). Attr("cluster", "true"). Attr("style", "rounded"). Attr("color", "black"). Attr("bgcolor", "lightgrey"). Attr("margin", "20") - // Component node + // Create component node and subgraph (cluster) componentNode := componentSubgraph.Node() componentNode.Attr("label", "𝑓") - if c.Description() != "" { - componentNode.Attr("label", c.Description()) + if component.Description() != "" { + componentNode.Attr("label", component.Description()) } componentNode. Attr("color", "blue"). Attr("shape", "rect"). - Attr("group", componentName) + Attr("group", component.Name()) + + // Create nodes for input ports + for _, port := range component.Inputs() { + portID := getPortID(component.Name(), "input", port.Name()) - // Input ports - for portName := range c.Inputs() { - portNode := componentSubgraph.NodeWithID(fmt.Sprintf("%s.inputs.%s", componentName, portName)) + //Mark input ports to be able to find their respective nodes later when adding pipes + port.AddLabel(nodeIDLabel, portID) + + portNode := componentSubgraph.NodeWithID(portID) portNode. - Attr("label", portName). + Attr("label", port.Name()). Attr("shape", "circle"). - Attr("group", componentName) + Attr("group", component.Name()) componentSubgraph.Edge(portNode, componentNode) } - // Output ports - for portName, port := range c.Outputs() { - portNode := componentSubgraph.NodeWithID(fmt.Sprintf("%s.inputs.%s", componentName, portName)) + // Create nodes for output ports + for _, port := range component.Outputs() { + portID := getPortID(component.Name(), "output", port.Name()) + portNode := componentSubgraph.NodeWithID(portID) portNode. - Attr("label", portName). + Attr("label", port.Name()). Attr("shape", "circle"). - Attr("group", componentName) + Attr("group", component.Name()) componentSubgraph.Edge(componentNode, portNode) + } + } + + // Create edges representing pipes (all ports must exist at this point) + for _, component := range fm.Components() { + for _, srcPort := range component.Outputs() { + for _, destPort := range srcPort.Pipes() { + // Any destination port in any pipe is input port, but we do not know in which component + // so we use the label we added earlier + destPortID, err := destPort.Label(nodeIDLabel) + if err != nil { + return nil, err + } + // Clean up and leave the f-mesh as it was before export + destPort.DeleteLabel(nodeIDLabel) - // Pipes - //@TODO + // Any source port in any pipe is always output port, so we can build its node ID + srcPortNode := graph.FindNodeByID(getPortID(component.Name(), "output", srcPort.Name())) + destPortNode := graph.FindNodeByID(destPortID) + graph.Edge(srcPortNode, destPortNode) + } } } buf := new(bytes.Buffer) graph.Write(buf) - return buf.Bytes() + return buf.Bytes(), nil +} + +func getPortID(componentName string, portKind string, portName string) string { + return fmt.Sprintf("component/%s/%s/%s", componentName, portKind, portName) } diff --git a/export/exporter.go b/export/exporter.go index 4ab7567..8888403 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -2,6 +2,7 @@ package export import "github.com/hovsep/fmesh" +// Exporter is the common interface for all formats type Exporter interface { - Export(fm *fmesh.FMesh) []byte + Export(fm *fmesh.FMesh) ([]byte, error) } From 466a9e3cd6d4d5aa367cab10eaad98f38135c14e Mon Sep 17 00:00:00 2001 From: hovsep Date: Thu, 10 Oct 2024 22:10:31 +0300 Subject: [PATCH 13/41] Dot exporter refactored --- common/labeled_entity.go | 7 +- export/dot.go | 185 ++++++++++++++++++--------- export/dot_test.go | 76 +++++++++++ integration_tests/piping/fan_test.go | 6 - 4 files changed, 206 insertions(+), 68 deletions(-) create mode 100644 export/dot_test.go diff --git a/common/labeled_entity.go b/common/labeled_entity.go index 1297ed9..d8e86a3 100644 --- a/common/labeled_entity.go +++ b/common/labeled_entity.go @@ -1,6 +1,9 @@ package common -import "errors" +import ( + "errors" + "fmt" +) type LabelsCollection map[string]string @@ -26,7 +29,7 @@ func (e *LabeledEntity) Label(label string) (string, error) { value, ok := e.labels[label] if !ok { - return "", errLabelNotFound + return "", fmt.Errorf("%w , label: %s", errLabelNotFound, label) } return value, nil diff --git a/export/dot.go b/export/dot.go index b09e31a..ce55f42 100644 --- a/export/dot.go +++ b/export/dot.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" "github.com/lucasepe/dot" ) @@ -18,93 +20,156 @@ func NewDotExporter() Exporter { // Export returns the f-mesh represented as digraph in DOT language func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { - // Setup main graph - graph := dot.NewGraph(dot.Directed) - graph. - Attr("layout", "dot"). - Attr("splines", "ortho") + if len(fm.Components()) == 0 { + return nil, nil + } - for _, component := range fm.Components() { - // Component subgraph (wrapper) - componentSubgraph := graph.NewSubgraph() - componentSubgraph. - NodeBaseAttrs(). - Attr("width", "1.0").Attr("height", "1.0") - componentSubgraph. - Attr("label", component.Name()). - Attr("cluster", "true"). - Attr("style", "rounded"). - Attr("color", "black"). - Attr("bgcolor", "lightgrey"). - Attr("margin", "20") - - // Create component node and subgraph (cluster) - componentNode := componentSubgraph.Node() - componentNode.Attr("label", "𝑓") - if component.Description() != "" { - componentNode.Attr("label", component.Description()) - } - componentNode. - Attr("color", "blue"). - Attr("shape", "rect"). - Attr("group", component.Name()) + graph, err := buildGraph(fm) - // Create nodes for input ports - for _, port := range component.Inputs() { - portID := getPortID(component.Name(), "input", port.Name()) + if err != nil { + return nil, err + } - //Mark input ports to be able to find their respective nodes later when adding pipes - port.AddLabel(nodeIDLabel, portID) + buf := new(bytes.Buffer) + graph.Write(buf) - portNode := componentSubgraph.NodeWithID(portID) - portNode. - Attr("label", port.Name()). - Attr("shape", "circle"). - Attr("group", component.Name()) + return buf.Bytes(), nil +} - componentSubgraph.Edge(portNode, componentNode) - } +// buildGraph returns a graph representing the given f-mesh +func buildGraph(fm *fmesh.FMesh) (*dot.Graph, error) { + mainGraph := getMainGraph(fm) - // Create nodes for output ports - for _, port := range component.Outputs() { - portID := getPortID(component.Name(), "output", port.Name()) - portNode := componentSubgraph.NodeWithID(portID) - portNode. - Attr("label", port.Name()). - Attr("shape", "circle"). - Attr("group", component.Name()) + addComponents(mainGraph, fm.Components()) - componentSubgraph.Edge(componentNode, portNode) - } + err := addPipes(mainGraph, fm.Components()) + if err != nil { + return nil, err } + return mainGraph, nil +} - // Create edges representing pipes (all ports must exist at this point) - for _, component := range fm.Components() { - for _, srcPort := range component.Outputs() { +// addPipes adds pipes representation to the graph +func addPipes(graph *dot.Graph, components component.Collection) error { + for _, c := range components { + for _, srcPort := range c.Outputs() { for _, destPort := range srcPort.Pipes() { // Any destination port in any pipe is input port, but we do not know in which component // so we use the label we added earlier destPortID, err := destPort.Label(nodeIDLabel) if err != nil { - return nil, err + return fmt.Errorf("failed to add pipe: %w", err) } // Clean up and leave the f-mesh as it was before export destPort.DeleteLabel(nodeIDLabel) // Any source port in any pipe is always output port, so we can build its node ID - srcPortNode := graph.FindNodeByID(getPortID(component.Name(), "output", srcPort.Name())) + srcPortNode := graph.FindNodeByID(getPortID(c.Name(), "output", srcPort.Name())) destPortNode := graph.FindNodeByID(destPortID) - graph.Edge(srcPortNode, destPortNode) + graph.Edge(srcPortNode, destPortNode).Attr("minlen", 3) } } } + return nil +} - buf := new(bytes.Buffer) - graph.Write(buf) +// addComponents adds components representation to the graph +func addComponents(graph *dot.Graph, components component.Collection) { + for _, c := range components { + // Component + componentSubgraph := getComponentSubgraph(graph, c) + componentNode := getComponentNode(componentSubgraph, c) - return buf.Bytes(), nil + // Input ports + for _, p := range c.Inputs() { + portNode := getPortNode(c, p, "input", componentSubgraph) + componentSubgraph.Edge(portNode, componentNode) + } + + // Output ports + for _, p := range c.Outputs() { + portNode := getPortNode(c, p, "output", componentSubgraph) + componentSubgraph.Edge(componentNode, portNode) + } + } +} + +// getPortNode creates and returns a node representing one port +func getPortNode(c *component.Component, port *port.Port, portKind string, componentSubgraph *dot.Graph) *dot.Node { + portID := getPortID(c.Name(), portKind, port.Name()) + + //Mark ports to be able to find their respective nodes later when adding pipes + port.AddLabel(nodeIDLabel, portID) + + portNode := componentSubgraph.NodeWithID(portID) + portNode. + Attr("label", port.Name()). + Attr("shape", "circle"). + Attr("group", c.Name()) + return portNode +} + +// getComponentSubgraph creates component subgraph and returns it +func getComponentSubgraph(graph *dot.Graph, component *component.Component) *dot.Graph { + componentSubgraph := graph.NewSubgraph() + componentSubgraph. + NodeBaseAttrs(). + Attr("width", "1.0").Attr("height", "1.0") + componentSubgraph. + Attr("label", component.Name()). + Attr("cluster", "true"). + Attr("style", "rounded"). + Attr("color", "black"). + Attr("bgcolor", "lightgrey"). + Attr("margin", "20") + + return componentSubgraph +} + +// getComponentNodeCreate creates component node and returns it +func getComponentNode(componentSubgraph *dot.Graph, component *component.Component) *dot.Node { + componentNode := componentSubgraph.Node() + componentNode.Attr("label", "𝑓") + if component.Description() != "" { + componentNode.Attr("label", component.Description()) + } + componentNode. + Attr("color", "blue"). + Attr("shape", "rect"). + Attr("group", component.Name()) + return componentNode +} + +// getMainGraph creates and returns the main (root) graph +func getMainGraph(fm *fmesh.FMesh) *dot.Graph { + graph := dot.NewGraph(dot.Directed) + graph. + Attr("layout", "dot"). + Attr("splines", "ortho") + + if fm.Description() != "" { + addDescription(graph, fm.Description()) + } + + return graph +} + +func addDescription(graph *dot.Graph, description string) { + descriptionSubgraph := graph.NewSubgraph() + descriptionSubgraph. + Attr("label", "Description:"). + Attr("color", "green"). + Attr("fontcolor", "green"). + Attr("style", "dashed") + descriptionNode := descriptionSubgraph.Node() + descriptionNode. + Attr("shape", "plaintext"). + Attr("color", "green"). + Attr("fontcolor", "green"). + Attr("label", description) } +// getPortID returns unique ID used to locate ports while building pipe edges func getPortID(componentName string, portKind string, portName string) string { return fmt.Sprintf("component/%s/%s/%s", componentName, portKind, portName) } diff --git a/export/dot_test.go b/export/dot_test.go new file mode 100644 index 0000000..2f1a682 --- /dev/null +++ b/export/dot_test.go @@ -0,0 +1,76 @@ +package export + +import ( + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_dotExporter_Export(t *testing.T) { + type args struct { + fm *fmesh.FMesh + } + tests := []struct { + name string + args args + assertions func(t *testing.T, data []byte, err error) + }{ + { + name: "empty f-mesh", + args: args{ + fm: fmesh.New("fm"), + }, + assertions: func(t *testing.T, data []byte, err error) { + assert.NoError(t, err) + assert.Empty(t, data) + }, + }, + { + name: "happy path", + args: args{ + fm: func() *fmesh.FMesh { + adder := component.New("adder"). + WithDescription("This component adds 2 numbers"). + WithInputs("num1", "num2"). + WithOutputs("result"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + //The activation func can be even empty, does not affect export + return nil + }) + + multiplier := component.New("multiplier"). + WithDescription("This component multiplies number by 3"). + WithInputs("num"). + WithOutputs("result"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + //The activation func can be even empty, does not affect export + return nil + }) + + adder.Outputs().ByName("result").PipeTo(multiplier.Inputs().ByName("num")) + + fm := fmesh.New("fm"). + WithDescription("This f-mesh has just one component"). + WithComponents(adder, multiplier) + return fm + }(), + }, + assertions: func(t *testing.T, data []byte, err error) { + assert.NoError(t, err) + assert.NotEmpty(t, data) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exporter := NewDotExporter() + + got, err := exporter.Export(tt.args.fm) + if tt.assertions != nil { + tt.assertions(t, got, err) + } + }) + } +} diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index 4ee34cd..b45fe19 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -1,11 +1,9 @@ package integration_tests import ( - "fmt" "github.com/hovsep/fmesh" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" - "github.com/hovsep/fmesh/export" "github.com/hovsep/fmesh/port" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -149,10 +147,6 @@ func Test_Fan(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fm := tt.setupFM() - - exp := export.NewDotExporter() - fmt.Println(string(exp.Export(fm))) - tt.setInputs(fm) cycles, err := fm.Run() tt.assertions(t, fm, cycles, err) From f1951559627a0634d6577d37810cbff90f89cb81 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sat, 12 Oct 2024 02:57:44 +0300 Subject: [PATCH 14/41] [WIP] Export with cycles --- component/activation_result.go | 21 ++++ export/dot.go | 170 ++++++++++++++++++++++++++++----- export/dot_test.go | 80 ++++++++++++++++ export/exporter.go | 9 +- 4 files changed, 256 insertions(+), 24 deletions(-) diff --git a/component/activation_result.go b/component/activation_result.go index 3b4982e..a69a24a 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -16,6 +16,27 @@ type ActivationResult struct { // ActivationResultCode denotes a specific info about how a component been activated or why not activated at all type ActivationResultCode int +func (a ActivationResultCode) String() string { + switch a { + case ActivationCodeOK: + return "OK" + case ActivationCodeNoInput: + return "No input" + case ActivationCodeNoFunction: + return "Activation function is missing" + case ActivationCodeReturnedError: + return "Returned error" + case ActivationCodePanicked: + return "Panicked" + case ActivationCodeWaitingForInputsClear: + return "Component is waiting for input" + case ActivationCodeWaitingForInputsKeep: + return "Component is waiting for input and wants to keep all inputs till next cycle" + default: + return "Unsupported code" + } +} + const ( // ActivationCodeOK : component is activated and did not return any errors ActivationCodeOK ActivationResultCode = iota diff --git a/export/dot.go b/export/dot.go index ce55f42..28f5d4a 100644 --- a/export/dot.go +++ b/export/dot.go @@ -4,7 +4,8 @@ import ( "bytes" "fmt" "github.com/hovsep/fmesh" - "github.com/hovsep/fmesh/component" + fmeshcomponent "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/cycle" "github.com/hovsep/fmesh/port" "github.com/lucasepe/dot" ) @@ -12,7 +13,11 @@ import ( type dotExporter struct { } -const nodeIDLabel = "export/dot/id" +const ( + nodeIDLabel = "export/dot/id" + portKindInput = "input" + portKindOutput = "output" +) func NewDotExporter() Exporter { return &dotExporter{} @@ -36,11 +41,52 @@ func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { return buf.Bytes(), nil } +// ExportWithCycles returns multiple graphs showing the state of the given f-mesh in each activation cycle +func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, cycles cycle.Collection) ([][]byte, error) { + if len(fm.Components()) == 0 { + return nil, nil + } + + if len(cycles) == 0 { + return nil, nil + } + + results := make([][]byte, len(cycles)) + + for cycleNumber, c := range cycles { + graphForCycle, err := buildGraphForCycle(fm, c, cycleNumber) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + graphForCycle.Write(buf) + + results[cycleNumber] = buf.Bytes() + } + + return results, nil +} + // buildGraph returns a graph representing the given f-mesh func buildGraph(fm *fmesh.FMesh) (*dot.Graph, error) { mainGraph := getMainGraph(fm) - addComponents(mainGraph, fm.Components()) + addComponents(mainGraph, fm.Components(), nil) + + err := addPipes(mainGraph, fm.Components()) + if err != nil { + return nil, err + } + return mainGraph, nil +} + +func buildGraphForCycle(fm *fmesh.FMesh, activationCycle *cycle.Cycle, cycleNumber int) (*dot.Graph, error) { + mainGraph := getMainGraph(fm) + + addCycleInfo(mainGraph, activationCycle, cycleNumber) + + addComponents(mainGraph, fm.Components(), activationCycle.ActivationResults()) err := addPipes(mainGraph, fm.Components()) if err != nil { @@ -50,7 +96,7 @@ func buildGraph(fm *fmesh.FMesh) (*dot.Graph, error) { } // addPipes adds pipes representation to the graph -func addPipes(graph *dot.Graph, components component.Collection) error { +func addPipes(graph *dot.Graph, components fmeshcomponent.Collection) error { for _, c := range components { for _, srcPort := range c.Outputs() { for _, destPort := range srcPort.Pipes() { @@ -58,15 +104,15 @@ func addPipes(graph *dot.Graph, components component.Collection) error { // so we use the label we added earlier destPortID, err := destPort.Label(nodeIDLabel) if err != nil { - return fmt.Errorf("failed to add pipe: %w", err) + return fmt.Errorf("failed to add pipe to port: %s : %w", destPort.Name(), err) } - // Clean up and leave the f-mesh as it was before export + // Delete label, as it is not needed anymore destPort.DeleteLabel(nodeIDLabel) // Any source port in any pipe is always output port, so we can build its node ID - srcPortNode := graph.FindNodeByID(getPortID(c.Name(), "output", srcPort.Name())) + srcPortNode := graph.FindNodeByID(getPortID(c.Name(), portKindOutput, srcPort.Name())) destPortNode := graph.FindNodeByID(destPortID) - graph.Edge(srcPortNode, destPortNode).Attr("minlen", 3) + graph.Edge(srcPortNode, destPortNode).Attr("minlen", "3") } } } @@ -74,28 +120,32 @@ func addPipes(graph *dot.Graph, components component.Collection) error { } // addComponents adds components representation to the graph -func addComponents(graph *dot.Graph, components component.Collection) { +func addComponents(graph *dot.Graph, components fmeshcomponent.Collection, activationResults fmeshcomponent.ActivationResultCollection) { for _, c := range components { // Component - componentSubgraph := getComponentSubgraph(graph, c) - componentNode := getComponentNode(componentSubgraph, c) + var activationResult *fmeshcomponent.ActivationResult + if activationResults != nil { + activationResult = activationResults.ByComponentName(c.Name()) + } + componentSubgraph := getComponentSubgraph(graph, c, activationResult) + componentNode := getComponentNode(componentSubgraph, c, activationResult) // Input ports for _, p := range c.Inputs() { - portNode := getPortNode(c, p, "input", componentSubgraph) + portNode := getPortNode(c, p, portKindInput, componentSubgraph) componentSubgraph.Edge(portNode, componentNode) } // Output ports for _, p := range c.Outputs() { - portNode := getPortNode(c, p, "output", componentSubgraph) + portNode := getPortNode(c, p, portKindOutput, componentSubgraph) componentSubgraph.Edge(componentNode, portNode) } } } // getPortNode creates and returns a node representing one port -func getPortNode(c *component.Component, port *port.Port, portKind string, componentSubgraph *dot.Graph) *dot.Node { +func getPortNode(c *fmeshcomponent.Component, port *port.Port, portKind string, componentSubgraph *dot.Graph) *dot.Node { portID := getPortID(c.Name(), portKind, port.Name()) //Mark ports to be able to find their respective nodes later when adding pipes @@ -110,7 +160,7 @@ func getPortNode(c *component.Component, port *port.Port, portKind string, compo } // getComponentSubgraph creates component subgraph and returns it -func getComponentSubgraph(graph *dot.Graph, component *component.Component) *dot.Graph { +func getComponentSubgraph(graph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Graph { componentSubgraph := graph.NewSubgraph() componentSubgraph. NodeBaseAttrs(). @@ -123,17 +173,52 @@ func getComponentSubgraph(graph *dot.Graph, component *component.Component) *dot Attr("bgcolor", "lightgrey"). Attr("margin", "20") + // In cycle + if activationResult != nil { + switch activationResult.Code() { + case fmeshcomponent.ActivationCodeOK: + componentSubgraph.Attr("bgcolor", "green") + case fmeshcomponent.ActivationCodeNoInput: + componentSubgraph.Attr("bgcolor", "yellow") + case fmeshcomponent.ActivationCodeNoFunction: + componentSubgraph.Attr("bgcolor", "gray") + case fmeshcomponent.ActivationCodeReturnedError: + componentSubgraph.Attr("bgcolor", "red") + case fmeshcomponent.ActivationCodePanicked: + componentSubgraph.Attr("bgcolor", "pink") + case fmeshcomponent.ActivationCodeWaitingForInputsClear: + componentSubgraph.Attr("bgcolor", "blue") + case fmeshcomponent.ActivationCodeWaitingForInputsKeep: + componentSubgraph.Attr("bgcolor", "purple") + default: + } + } + return componentSubgraph } -// getComponentNodeCreate creates component node and returns it -func getComponentNode(componentSubgraph *dot.Graph, component *component.Component) *dot.Node { +// getComponentNode creates component node and returns it +func getComponentNode(componentSubgraph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Node { componentNode := componentSubgraph.Node() - componentNode.Attr("label", "𝑓") + label := "𝑓" + if component.Description() != "" { - componentNode.Attr("label", component.Description()) + label = component.Description() + } + + if activationResult != nil { + + if activationResult.Error() != nil { + errorNode := componentSubgraph.Node() + errorNode. + Attr("shape", "note"). + Attr("label", activationResult.Error().Error()) + componentSubgraph.Edge(componentNode, errorNode) + } } + componentNode. + Attr("label", label). Attr("color", "blue"). Attr("shape", "rect"). Attr("group", component.Name()) @@ -154,21 +239,60 @@ func getMainGraph(fm *fmesh.FMesh) *dot.Graph { return graph } +// addDescription adds f-mesh description to graph func addDescription(graph *dot.Graph, description string) { - descriptionSubgraph := graph.NewSubgraph() - descriptionSubgraph. + subgraph := graph.NewSubgraph() + subgraph. Attr("label", "Description:"). Attr("color", "green"). Attr("fontcolor", "green"). Attr("style", "dashed") - descriptionNode := descriptionSubgraph.Node() - descriptionNode. + node := subgraph.Node() + node. Attr("shape", "plaintext"). Attr("color", "green"). Attr("fontcolor", "green"). Attr("label", description) } +// addCycleInfo adds useful insights about current cycle +func addCycleInfo(graph *dot.Graph, activationCycle *cycle.Cycle, cycleNumber int) { + subgraph := graph.NewSubgraph() + subgraph. + Attr("label", "Cycle info:"). + Attr("style", "dashed") + subgraph.NodeBaseAttrs(). + Attr("shape", "plaintext") + + // Current cycle number + cycleNumberNode := subgraph.Node() + cycleNumberNode.Attr("label", fmt.Sprintf("Current cycle: %d", cycleNumber)) + + // Stats + stats := getCycleStats(activationCycle) + statNode := subgraph.Node() + tableRows := dot.HTML("") + for statName, statValue := range stats { + //@TODO: keep order + tableRows = tableRows + dot.HTML(fmt.Sprintf("", statName, statValue)) + } + tableRows = tableRows + "
%s : %d
" + statNode.Attr("label", tableRows) +} + +// getCycleStats returns basic cycle stats +func getCycleStats(activationCycle *cycle.Cycle) map[string]int { + stats := make(map[string]int) + for _, ar := range activationCycle.ActivationResults() { + if ar.Activated() { + stats["Activated"]++ + } + + stats[ar.Code().String()]++ + } + return stats +} + // getPortID returns unique ID used to locate ports while building pipe edges func getPortID(componentName string, portKind string, portName string) string { return fmt.Sprintf("component/%s/%s/%s", componentName, portKind, portName) diff --git a/export/dot_test.go b/export/dot_test.go index 2f1a682..3455c60 100644 --- a/export/dot_test.go +++ b/export/dot_test.go @@ -4,6 +4,7 @@ import ( "github.com/hovsep/fmesh" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" "testing" ) @@ -74,3 +75,82 @@ func Test_dotExporter_Export(t *testing.T) { }) } } + +func Test_dotExporter_ExportWithCycles(t *testing.T) { + type args struct { + fm *fmesh.FMesh + } + tests := []struct { + name string + args args + assertions func(t *testing.T, data [][]byte, err error) + }{ + { + name: "empty f-mesh", + args: args{ + fm: fmesh.New("fm"), + }, + assertions: func(t *testing.T, data [][]byte, err error) { + assert.NoError(t, err) + assert.Empty(t, data) + }, + }, + { + name: "happy path", + args: args{ + fm: func() *fmesh.FMesh { + adder := component.New("adder"). + WithDescription("This component adds 2 numbers"). + WithInputs("num1", "num2"). + WithOutputs("result"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + num1 := inputs.ByName("num1").Signals().FirstPayload().(int) + num2 := inputs.ByName("num2").Signals().FirstPayload().(int) + + outputs.ByName("result").PutSignals(signal.New(num1 + num2)) + return nil + }) + + multiplier := component.New("multiplier"). + WithDescription("This component multiplies number by 3"). + WithInputs("num"). + WithOutputs("result"). + WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + num := inputs.ByName("num").Signals().FirstPayload().(int) + outputs.ByName("result").PutSignals(signal.New(num * 3)) + return nil + }) + + adder.Outputs().ByName("result").PipeTo(multiplier.Inputs().ByName("num")) + + fm := fmesh.New("fm"). + WithDescription("This f-mesh has just one component"). + WithComponents(adder, multiplier) + + adder.Inputs().ByName("num1").PutSignals(signal.New(15)) + adder.Inputs().ByName("num2").PutSignals(signal.New(12)) + + return fm + }(), + }, + assertions: func(t *testing.T, data [][]byte, err error) { + assert.NoError(t, err) + assert.NotEmpty(t, data) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + cycles, err := tt.args.fm.Run() + assert.NoError(t, err) + + exporter := NewDotExporter() + + got, err := exporter.ExportWithCycles(tt.args.fm, cycles) + if tt.assertions != nil { + tt.assertions(t, got, err) + } + }) + } +} diff --git a/export/exporter.go b/export/exporter.go index 8888403..7877b37 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -1,8 +1,15 @@ package export -import "github.com/hovsep/fmesh" +import ( + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/cycle" +) // Exporter is the common interface for all formats type Exporter interface { + // Export returns f-mesh representation in some format Export(fm *fmesh.FMesh) ([]byte, error) + + // ExportWithCycles returns representations of f-mesh during multiple cycles + ExportWithCycles(fm *fmesh.FMesh, cycles cycle.Collection) ([][]byte, error) } From 706e6bc3dfaa9dc65c32241331cd379f0967925d Mon Sep 17 00:00:00 2001 From: hovsep Date: Sat, 12 Oct 2024 15:52:15 +0300 Subject: [PATCH 15/41] Cycle refactored: add number field --- cycle/cycle.go | 12 ++++++++++++ fmesh.go | 10 ++++++---- fmesh_test.go | 18 ++++++------------ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/cycle/cycle.go b/cycle/cycle.go index 72b8118..524d575 100644 --- a/cycle/cycle.go +++ b/cycle/cycle.go @@ -8,6 +8,7 @@ import ( // Cycle contains the info about given activation cycle type Cycle struct { sync.Mutex + number int activationResults component.ActivationResultCollection } @@ -43,3 +44,14 @@ func (cycle *Cycle) WithActivationResults(activationResults ...*component.Activa cycle.activationResults = cycle.ActivationResults().Add(activationResults...) return cycle } + +// Number returns sequence number +func (cycle *Cycle) Number() int { + return cycle.number +} + +// WithNumber sets the sequence number +func (cycle *Cycle) WithNumber(number int) *Cycle { + cycle.number = number + return cycle +} diff --git a/fmesh.go b/fmesh.go index 1ef3cb0..2bd4165 100644 --- a/fmesh.go +++ b/fmesh.go @@ -123,21 +123,23 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) { // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) func (fm *FMesh) Run() (cycle.Collection, error) { allCycles := cycle.NewCollection() + cycleNumber := 0 for { - cycleResult := fm.runCycle() + cycleResult := fm.runCycle().WithNumber(cycleNumber) allCycles = allCycles.With(cycleResult) - mustStop, err := fm.mustStop(cycleResult, len(allCycles)) + mustStop, err := fm.mustStop(cycleResult) if mustStop { return allCycles, err } fm.drainComponents(cycleResult) + cycleNumber++ } } -func (fm *FMesh) mustStop(cycleResult *cycle.Cycle, cycleNum int) (bool, error) { - if (fm.config.CyclesLimit > 0) && (cycleNum > fm.config.CyclesLimit) { +func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error) { + if (fm.config.CyclesLimit > 0) && (cycleResult.Number() > fm.config.CyclesLimit) { return true, ErrReachedMaxAllowedCycles } diff --git a/fmesh_test.go b/fmesh_test.go index 40bf0c9..02f6554 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -637,7 +637,6 @@ func TestFMesh_runCycle(t *testing.T) { func TestFMesh_mustStop(t *testing.T) { type args struct { cycleResult *cycle.Cycle - cycleNum int } tests := []struct { name string @@ -654,8 +653,7 @@ func TestFMesh_mustStop(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ), - cycleNum: 5, + ).WithNumber(5), }, want: false, wantErr: nil, @@ -668,8 +666,7 @@ func TestFMesh_mustStop(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ), - cycleNum: 1001, + ).WithNumber(1001), }, want: true, wantErr: ErrReachedMaxAllowedCycles, @@ -682,8 +679,7 @@ func TestFMesh_mustStop(t *testing.T) { component.NewActivationResult("c1"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), - ), - cycleNum: 5, + ).WithNumber(5), }, want: true, wantErr: nil, @@ -700,8 +696,7 @@ func TestFMesh_mustStop(t *testing.T) { SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). WithError(errors.New("c1 activation finished with error")), - ), - cycleNum: 5, + ).WithNumber(5), }, want: true, wantErr: ErrHitAnErrorOrPanic, @@ -717,8 +712,7 @@ func TestFMesh_mustStop(t *testing.T) { SetActivated(true). WithActivationCode(component.ActivationCodePanicked). WithError(errors.New("c1 panicked")), - ), - cycleNum: 5, + ).WithNumber(5), }, want: true, wantErr: ErrHitAPanic, @@ -726,7 +720,7 @@ func TestFMesh_mustStop(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.fmesh.mustStop(tt.args.cycleResult, tt.args.cycleNum) + got, err := tt.fmesh.mustStop(tt.args.cycleResult) if tt.wantErr != nil { assert.EqualError(t, err, tt.wantErr.Error()) } else { From 7de1c4f6548a0b0345eb41fd077b89804c4176b4 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sat, 12 Oct 2024 17:05:23 +0300 Subject: [PATCH 16/41] Dot exporter: add config --- export/dot.go | 299 --------------------------------- export/dot/config.go | 137 +++++++++++++++ export/dot/dot.go | 315 +++++++++++++++++++++++++++++++++++ export/{ => dot}/dot_test.go | 2 +- export/exporter.go | 4 +- 5 files changed, 455 insertions(+), 302 deletions(-) delete mode 100644 export/dot.go create mode 100644 export/dot/config.go create mode 100644 export/dot/dot.go rename export/{ => dot}/dot_test.go (99%) diff --git a/export/dot.go b/export/dot.go deleted file mode 100644 index 28f5d4a..0000000 --- a/export/dot.go +++ /dev/null @@ -1,299 +0,0 @@ -package export - -import ( - "bytes" - "fmt" - "github.com/hovsep/fmesh" - fmeshcomponent "github.com/hovsep/fmesh/component" - "github.com/hovsep/fmesh/cycle" - "github.com/hovsep/fmesh/port" - "github.com/lucasepe/dot" -) - -type dotExporter struct { -} - -const ( - nodeIDLabel = "export/dot/id" - portKindInput = "input" - portKindOutput = "output" -) - -func NewDotExporter() Exporter { - return &dotExporter{} -} - -// Export returns the f-mesh represented as digraph in DOT language -func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { - if len(fm.Components()) == 0 { - return nil, nil - } - - graph, err := buildGraph(fm) - - if err != nil { - return nil, err - } - - buf := new(bytes.Buffer) - graph.Write(buf) - - return buf.Bytes(), nil -} - -// ExportWithCycles returns multiple graphs showing the state of the given f-mesh in each activation cycle -func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, cycles cycle.Collection) ([][]byte, error) { - if len(fm.Components()) == 0 { - return nil, nil - } - - if len(cycles) == 0 { - return nil, nil - } - - results := make([][]byte, len(cycles)) - - for cycleNumber, c := range cycles { - graphForCycle, err := buildGraphForCycle(fm, c, cycleNumber) - if err != nil { - return nil, err - } - - buf := new(bytes.Buffer) - graphForCycle.Write(buf) - - results[cycleNumber] = buf.Bytes() - } - - return results, nil -} - -// buildGraph returns a graph representing the given f-mesh -func buildGraph(fm *fmesh.FMesh) (*dot.Graph, error) { - mainGraph := getMainGraph(fm) - - addComponents(mainGraph, fm.Components(), nil) - - err := addPipes(mainGraph, fm.Components()) - if err != nil { - return nil, err - } - return mainGraph, nil -} - -func buildGraphForCycle(fm *fmesh.FMesh, activationCycle *cycle.Cycle, cycleNumber int) (*dot.Graph, error) { - mainGraph := getMainGraph(fm) - - addCycleInfo(mainGraph, activationCycle, cycleNumber) - - addComponents(mainGraph, fm.Components(), activationCycle.ActivationResults()) - - err := addPipes(mainGraph, fm.Components()) - if err != nil { - return nil, err - } - return mainGraph, nil -} - -// addPipes adds pipes representation to the graph -func addPipes(graph *dot.Graph, components fmeshcomponent.Collection) error { - for _, c := range components { - for _, srcPort := range c.Outputs() { - for _, destPort := range srcPort.Pipes() { - // Any destination port in any pipe is input port, but we do not know in which component - // so we use the label we added earlier - destPortID, err := destPort.Label(nodeIDLabel) - if err != nil { - return fmt.Errorf("failed to add pipe to port: %s : %w", destPort.Name(), err) - } - // Delete label, as it is not needed anymore - destPort.DeleteLabel(nodeIDLabel) - - // Any source port in any pipe is always output port, so we can build its node ID - srcPortNode := graph.FindNodeByID(getPortID(c.Name(), portKindOutput, srcPort.Name())) - destPortNode := graph.FindNodeByID(destPortID) - graph.Edge(srcPortNode, destPortNode).Attr("minlen", "3") - } - } - } - return nil -} - -// addComponents adds components representation to the graph -func addComponents(graph *dot.Graph, components fmeshcomponent.Collection, activationResults fmeshcomponent.ActivationResultCollection) { - for _, c := range components { - // Component - var activationResult *fmeshcomponent.ActivationResult - if activationResults != nil { - activationResult = activationResults.ByComponentName(c.Name()) - } - componentSubgraph := getComponentSubgraph(graph, c, activationResult) - componentNode := getComponentNode(componentSubgraph, c, activationResult) - - // Input ports - for _, p := range c.Inputs() { - portNode := getPortNode(c, p, portKindInput, componentSubgraph) - componentSubgraph.Edge(portNode, componentNode) - } - - // Output ports - for _, p := range c.Outputs() { - portNode := getPortNode(c, p, portKindOutput, componentSubgraph) - componentSubgraph.Edge(componentNode, portNode) - } - } -} - -// getPortNode creates and returns a node representing one port -func getPortNode(c *fmeshcomponent.Component, port *port.Port, portKind string, componentSubgraph *dot.Graph) *dot.Node { - portID := getPortID(c.Name(), portKind, port.Name()) - - //Mark ports to be able to find their respective nodes later when adding pipes - port.AddLabel(nodeIDLabel, portID) - - portNode := componentSubgraph.NodeWithID(portID) - portNode. - Attr("label", port.Name()). - Attr("shape", "circle"). - Attr("group", c.Name()) - return portNode -} - -// getComponentSubgraph creates component subgraph and returns it -func getComponentSubgraph(graph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Graph { - componentSubgraph := graph.NewSubgraph() - componentSubgraph. - NodeBaseAttrs(). - Attr("width", "1.0").Attr("height", "1.0") - componentSubgraph. - Attr("label", component.Name()). - Attr("cluster", "true"). - Attr("style", "rounded"). - Attr("color", "black"). - Attr("bgcolor", "lightgrey"). - Attr("margin", "20") - - // In cycle - if activationResult != nil { - switch activationResult.Code() { - case fmeshcomponent.ActivationCodeOK: - componentSubgraph.Attr("bgcolor", "green") - case fmeshcomponent.ActivationCodeNoInput: - componentSubgraph.Attr("bgcolor", "yellow") - case fmeshcomponent.ActivationCodeNoFunction: - componentSubgraph.Attr("bgcolor", "gray") - case fmeshcomponent.ActivationCodeReturnedError: - componentSubgraph.Attr("bgcolor", "red") - case fmeshcomponent.ActivationCodePanicked: - componentSubgraph.Attr("bgcolor", "pink") - case fmeshcomponent.ActivationCodeWaitingForInputsClear: - componentSubgraph.Attr("bgcolor", "blue") - case fmeshcomponent.ActivationCodeWaitingForInputsKeep: - componentSubgraph.Attr("bgcolor", "purple") - default: - } - } - - return componentSubgraph -} - -// getComponentNode creates component node and returns it -func getComponentNode(componentSubgraph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Node { - componentNode := componentSubgraph.Node() - label := "𝑓" - - if component.Description() != "" { - label = component.Description() - } - - if activationResult != nil { - - if activationResult.Error() != nil { - errorNode := componentSubgraph.Node() - errorNode. - Attr("shape", "note"). - Attr("label", activationResult.Error().Error()) - componentSubgraph.Edge(componentNode, errorNode) - } - } - - componentNode. - Attr("label", label). - Attr("color", "blue"). - Attr("shape", "rect"). - Attr("group", component.Name()) - return componentNode -} - -// getMainGraph creates and returns the main (root) graph -func getMainGraph(fm *fmesh.FMesh) *dot.Graph { - graph := dot.NewGraph(dot.Directed) - graph. - Attr("layout", "dot"). - Attr("splines", "ortho") - - if fm.Description() != "" { - addDescription(graph, fm.Description()) - } - - return graph -} - -// addDescription adds f-mesh description to graph -func addDescription(graph *dot.Graph, description string) { - subgraph := graph.NewSubgraph() - subgraph. - Attr("label", "Description:"). - Attr("color", "green"). - Attr("fontcolor", "green"). - Attr("style", "dashed") - node := subgraph.Node() - node. - Attr("shape", "plaintext"). - Attr("color", "green"). - Attr("fontcolor", "green"). - Attr("label", description) -} - -// addCycleInfo adds useful insights about current cycle -func addCycleInfo(graph *dot.Graph, activationCycle *cycle.Cycle, cycleNumber int) { - subgraph := graph.NewSubgraph() - subgraph. - Attr("label", "Cycle info:"). - Attr("style", "dashed") - subgraph.NodeBaseAttrs(). - Attr("shape", "plaintext") - - // Current cycle number - cycleNumberNode := subgraph.Node() - cycleNumberNode.Attr("label", fmt.Sprintf("Current cycle: %d", cycleNumber)) - - // Stats - stats := getCycleStats(activationCycle) - statNode := subgraph.Node() - tableRows := dot.HTML("") - for statName, statValue := range stats { - //@TODO: keep order - tableRows = tableRows + dot.HTML(fmt.Sprintf("", statName, statValue)) - } - tableRows = tableRows + "
%s : %d
" - statNode.Attr("label", tableRows) -} - -// getCycleStats returns basic cycle stats -func getCycleStats(activationCycle *cycle.Cycle) map[string]int { - stats := make(map[string]int) - for _, ar := range activationCycle.ActivationResults() { - if ar.Activated() { - stats["Activated"]++ - } - - stats[ar.Code().String()]++ - } - return stats -} - -// getPortID returns unique ID used to locate ports while building pipe edges -func getPortID(componentName string, portKind string, portName string) string { - return fmt.Sprintf("component/%s/%s/%s", componentName, portKind, portName) -} diff --git a/export/dot/config.go b/export/dot/config.go new file mode 100644 index 0000000..c8589c6 --- /dev/null +++ b/export/dot/config.go @@ -0,0 +1,137 @@ +package dot + +import fmeshcomponent "github.com/hovsep/fmesh/component" + +type attributesMap map[string]string + +type ComponentConfig struct { + Subgraph attributesMap + SubgraphNodeBaseAttrs attributesMap + Node attributesMap + NodeDefaultLabel string + ErrorNode attributesMap + SubgraphAttributesByActivationResultCode map[fmeshcomponent.ActivationResultCode]attributesMap +} + +type PortConfig struct { + Node attributesMap +} + +type LegendConfig struct { + Subgraph attributesMap + Node attributesMap +} + +type PipeConfig struct { + Edge attributesMap +} + +type Config struct { + MainGraph attributesMap + Component ComponentConfig + Port PortConfig + Pipe PipeConfig + Legend LegendConfig +} + +var ( + defaultConfig = &Config{ + MainGraph: attributesMap{ + "layout": "dot", + "splines": "ortho", + }, + Component: ComponentConfig{ + Subgraph: attributesMap{ + "cluster": "true", + "style": "rounded", + "color": "black", + "margin": "20", + "penwidth": "5", + }, + SubgraphNodeBaseAttrs: attributesMap{ + "fontname": "Courier New", + "width": "1.0", + "height": "1.0", + "penwidth": "2.5", + "style": "filled", + }, + Node: attributesMap{ + "shape": "rect", + "color": "#9dddea", + "style": "filled", + }, + NodeDefaultLabel: "𝑓", + ErrorNode: nil, + SubgraphAttributesByActivationResultCode: map[fmeshcomponent.ActivationResultCode]attributesMap{ + fmeshcomponent.ActivationCodeOK: { + "color": "green", + }, + fmeshcomponent.ActivationCodeNoInput: { + "color": "yellow", + }, + fmeshcomponent.ActivationCodeNoFunction: { + "color": "gray", + }, + fmeshcomponent.ActivationCodeReturnedError: { + "color": "red", + }, + fmeshcomponent.ActivationCodePanicked: { + "color": "pink", + }, + fmeshcomponent.ActivationCodeWaitingForInputsClear: { + "color": "blue", + }, + fmeshcomponent.ActivationCodeWaitingForInputsKeep: { + "color": "purple", + }, + }, + }, + Port: PortConfig{ + Node: attributesMap{ + "shape": "circle", + }, + }, + Pipe: PipeConfig{ + Edge: attributesMap{ + "minlen": "3", + "penwidth": "2", + "color": "#e437ea", + }, + }, + Legend: LegendConfig{ + Subgraph: attributesMap{ + "style": "dashed,filled", + "fillcolor": "#e2c6fc", + }, + Node: attributesMap{ + "shape": "plaintext", + "color": "green", + "fontname": "Courier New", + }, + }, + } + + legendTemplate = ` + + {{ if .meshDescription }} + + + + {{ end }} + + {{ if .cycleNumber }} + + + + {{ end }} + + {{ if .stats }} + {{ range .stats }} + + + + {{ end }} + {{ end }} +
Description:{{ .meshDescription }}
Cycle:{{ .cycleNumber }}
{{ .Name }}:{{ .Value }}
+ ` +) diff --git a/export/dot/dot.go b/export/dot/dot.go new file mode 100644 index 0000000..270a1d8 --- /dev/null +++ b/export/dot/dot.go @@ -0,0 +1,315 @@ +package dot + +import ( + "bytes" + "fmt" + "github.com/hovsep/fmesh" + fmeshcomponent "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/cycle" + "github.com/hovsep/fmesh/export" + "github.com/hovsep/fmesh/port" + "github.com/lucasepe/dot" + "html/template" + "sort" +) + +type statEntry struct { + Name string + Value int +} + +type dotExporter struct { + config *Config +} + +const ( + nodeIDLabel = "export/dot/id" + portKindInput = "input" + portKindOutput = "output" +) + +// NewDotExporter returns exporter with default configuration +func NewDotExporter() export.Exporter { + return NewDotExporterWithConfig(defaultConfig) +} + +// NewDotExporterWithConfig returns exporter with custom configuration +func NewDotExporterWithConfig(config *Config) export.Exporter { + return &dotExporter{ + config: config, + } +} + +// Export returns the f-mesh as DOT-graph +func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { + if len(fm.Components()) == 0 { + return nil, nil + } + + graph, err := d.buildGraph(fm, nil) + + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + graph.Write(buf) + + return buf.Bytes(), nil +} + +// ExportWithCycles returns multiple graphs showing the state of the given f-mesh in each activation cycle +func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Collection) ([][]byte, error) { + if len(fm.Components()) == 0 { + return nil, nil + } + + if len(activationCycles) == 0 { + return nil, nil + } + + results := make([][]byte, len(activationCycles)) + + for _, activationCycle := range activationCycles { + graphForCycle, err := d.buildGraph(fm, activationCycle) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + graphForCycle.Write(buf) + + results[activationCycle.Number()] = buf.Bytes() + } + + return results, nil +} + +// buildGraph returns f-mesh as a graph +// activationCycle may be passed optionally to get a representation of f-mesh in a given activation cycle +func (d *dotExporter) buildGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle) (*dot.Graph, error) { + mainGraph, err := d.getMainGraph(fm, activationCycle) + if err != nil { + return nil, err + } + + d.addComponents(mainGraph, fm.Components(), activationCycle) + + err = d.addPipes(mainGraph, fm.Components()) + if err != nil { + return nil, err + } + return mainGraph, nil +} + +// getMainGraph creates and returns the main (root) graph +func (d *dotExporter) getMainGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle) (*dot.Graph, error) { + graph := dot.NewGraph(dot.Directed) + + setAttrMap(&graph.AttributesMap, d.config.MainGraph) + + err := d.addLegend(graph, fm, activationCycle) + if err != nil { + return nil, fmt.Errorf("failed to build main graph: %w", err) + } + + return graph, nil +} + +// addPipes adds pipes representation to the graph +func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Collection) error { + for _, c := range components { + for _, srcPort := range c.Outputs() { + for _, destPort := range srcPort.Pipes() { + // Any destination port in any pipe is input port, but we do not know in which component + // so we use the label we added earlier + destPortID, err := destPort.Label(nodeIDLabel) + if err != nil { + return fmt.Errorf("failed to add pipe to port: %s : %w", destPort.Name(), err) + } + // Delete label, as it is not needed anymore + destPort.DeleteLabel(nodeIDLabel) + + // Any source port in any pipe is always output port, so we can build its node ID + srcPortNode := graph.FindNodeByID(getPortID(c.Name(), portKindOutput, srcPort.Name())) + destPortNode := graph.FindNodeByID(destPortID) + + graph.Edge(srcPortNode, destPortNode, func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Pipe.Edge) + }) + } + } + } + return nil +} + +// addComponents adds components representation to the graph +func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent.Collection, activationCycle *cycle.Cycle) { + for _, c := range components { + // Component + var activationResult *fmeshcomponent.ActivationResult + if activationCycle != nil { + activationResult = activationCycle.ActivationResults().ByComponentName(c.Name()) + } + componentSubgraph := d.getComponentSubgraph(graph, c, activationResult) + componentNode := d.getComponentNode(componentSubgraph, c, activationResult) + + // Input ports + for _, p := range c.Inputs() { + portNode := d.getPortNode(c, p, portKindInput, componentSubgraph) + componentSubgraph.Edge(portNode, componentNode) + } + + // Output ports + for _, p := range c.Outputs() { + portNode := d.getPortNode(c, p, portKindOutput, componentSubgraph) + componentSubgraph.Edge(componentNode, portNode) + } + } +} + +// getPortNode creates and returns a node representing one port +func (d *dotExporter) getPortNode(c *fmeshcomponent.Component, port *port.Port, portKind string, componentSubgraph *dot.Graph) *dot.Node { + portID := getPortID(c.Name(), portKind, port.Name()) + + //Mark ports to be able to find their respective nodes later when adding pipes + port.AddLabel(nodeIDLabel, portID) + + portNode := componentSubgraph.NodeWithID(portID, func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Port.Node) + a.Attr("label", port.Name()).Attr("group", c.Name()) + }) + + return portNode +} + +// getComponentSubgraph creates component subgraph and returns it +func (d *dotExporter) getComponentSubgraph(graph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Graph { + componentSubgraph := graph.NewSubgraph() + + setAttrMap(componentSubgraph.NodeBaseAttrs(), d.config.Component.SubgraphNodeBaseAttrs) + setAttrMap(&componentSubgraph.AttributesMap, d.config.Component.Subgraph) + + // Set cycle specific attributes + if activationResult != nil { + if attributesByCode, ok := d.config.Component.SubgraphAttributesByActivationResultCode[activationResult.Code()]; ok { + setAttrMap(&componentSubgraph.AttributesMap, attributesByCode) + } + } + + componentSubgraph.Attr("label", component.Name()) + + return componentSubgraph +} + +// getComponentNode creates component node and returns it +func (d *dotExporter) getComponentNode(componentSubgraph *dot.Graph, component *fmeshcomponent.Component, activationResult *fmeshcomponent.ActivationResult) *dot.Node { + componentNode := componentSubgraph.Node(func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Component.Node) + }) + + label := d.config.Component.NodeDefaultLabel + + if component.Description() != "" { + label = component.Description() + } + + if activationResult != nil { + if activationResult.Error() != nil { + errorNode := componentSubgraph.Node(func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Component.ErrorNode) + }) + errorNode. + Attr("label", activationResult.Error().Error()) + componentSubgraph.Edge(componentNode, errorNode) + } + } + + componentNode. + Attr("label", label). + Attr("group", component.Name()) + return componentNode +} + +// addLegend adds useful information about f-mesh and (optionally) current activation cycle +func (d *dotExporter) addLegend(graph *dot.Graph, fm *fmesh.FMesh, activationCycle *cycle.Cycle) error { + subgraph := graph.NewSubgraph() + + setAttrMap(&subgraph.AttributesMap, d.config.Legend.Subgraph) + subgraph.Attr("label", "Legend:") + + legendData := make(map[string]any) + legendData["meshDescription"] = fmt.Sprintf("This mesh consist of %d components", len(fm.Components())) + if fm.Description() != "" { + legendData["meshDescription"] = fm.Description() + } + + if activationCycle != nil { + legendData["cycleNumber"] = activationCycle.Number() + legendData["stats"] = getCycleStats(activationCycle) + } + + legendHTML := new(bytes.Buffer) + err := template.Must( + template.New("legend"). + Parse(legendTemplate)). + Execute(legendHTML, legendData) + + if err != nil { + return fmt.Errorf("failed to render legend: %w", err) + } + + subgraph.Node(func(a *dot.AttributesMap) { + setAttrMap(a, d.config.Legend.Node) + a.Attr("label", dot.HTML(legendHTML.String())) + }) + + return nil +} + +// getCycleStats returns basic cycle stats +func getCycleStats(activationCycle *cycle.Cycle) []*statEntry { + statsMap := map[string]*statEntry{ + // Number of activated must be shown always + "activated": { + Name: "Activated", + Value: 0, + }, + } + for _, ar := range activationCycle.ActivationResults() { + if ar.Activated() { + statsMap["activated"].Value++ + } + + if entryByCode, ok := statsMap[ar.Code().String()]; ok { + entryByCode.Value++ + } else { + statsMap[ar.Code().String()] = &statEntry{ + Name: ar.Code().String(), + Value: 1, + } + } + } + // Convert to slice to preserve keys order + statsList := make([]*statEntry, 0) + for _, entry := range statsMap { + statsList = append(statsList, entry) + } + + sort.Slice(statsList, func(i, j int) bool { + return statsList[i].Name < statsList[j].Name + }) + return statsList +} + +// getPortID returns unique ID used to locate ports while building pipe edges +func getPortID(componentName string, portKind string, portName string) string { + return fmt.Sprintf("component/%s/%s/%s", componentName, portKind, portName) +} + +// setAttrMap sets all attributes to target +func setAttrMap(target *dot.AttributesMap, attributes attributesMap) { + for attrName, attrValue := range attributes { + target.Attr(attrName, attrValue) + } +} diff --git a/export/dot_test.go b/export/dot/dot_test.go similarity index 99% rename from export/dot_test.go rename to export/dot/dot_test.go index 3455c60..7726b47 100644 --- a/export/dot_test.go +++ b/export/dot/dot_test.go @@ -1,4 +1,4 @@ -package export +package dot import ( "github.com/hovsep/fmesh" diff --git a/export/exporter.go b/export/exporter.go index 7877b37..a70499b 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -10,6 +10,6 @@ type Exporter interface { // Export returns f-mesh representation in some format Export(fm *fmesh.FMesh) ([]byte, error) - // ExportWithCycles returns representations of f-mesh during multiple cycles - ExportWithCycles(fm *fmesh.FMesh, cycles cycle.Collection) ([][]byte, error) + // ExportWithCycles returns the f-mesh state representation in each activation cycle + ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Collection) ([][]byte, error) } From ceea8c0f41f858c556cfd9d09ec7f32507a517d7 Mon Sep 17 00:00:00 2001 From: hovsep Date: Tue, 15 Oct 2024 03:42:08 +0300 Subject: [PATCH 17/41] [WIP] Add chainable trait to signal and signal group --- common/chainable.go | 17 +++ component/component_test.go | 12 +- export/dot/dot_test.go | 20 ++- fmesh_test.go | 7 +- integration_tests/computation/math_test.go | 20 ++- integration_tests/piping/fan_test.go | 24 +++- .../ports/waiting_for_inputs_test.go | 26 +++- port/collection.go | 8 +- port/collection_test.go | 18 +-- port/port.go | 35 ++++- port/port_test.go | 64 ++++----- signal/group.go | 123 ++++++++++++++---- signal/group_test.go | 101 +++++++++----- signal/signal.go | 15 ++- signal/signal_test.go | 25 +++- 15 files changed, 371 insertions(+), 144 deletions(-) create mode 100644 common/chainable.go diff --git a/common/chainable.go b/common/chainable.go new file mode 100644 index 0000000..008168e --- /dev/null +++ b/common/chainable.go @@ -0,0 +1,17 @@ +package common + +type Chainable struct { + err error +} + +func (c *Chainable) SetError(err error) { + c.err = err +} + +func (c *Chainable) HasError() bool { + return c.err != nil +} + +func (c *Chainable) Error() error { + return c.err +} diff --git a/component/component_test.go b/component/component_test.go index fbbabfd..1e7d25d 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -79,9 +79,11 @@ func TestComponent_FlushOutputs(t *testing.T) { component: componentWithAllOutputsSet, destPort: sink, assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { - assert.Contains(t, destPort.Signals().AllPayloads(), 777) - assert.Contains(t, destPort.Signals().AllPayloads(), 888) - assert.Len(t, destPort.Signals().AllPayloads(), 2) + allPayloads, err := destPort.Signals().AllPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 777) + assert.Contains(t, allPayloads, 888) + assert.Len(t, allPayloads, 2) // Signals are disposed when port is flushed assert.False(t, componentAfterFlush.Outputs().AnyHasSignals()) }, @@ -183,8 +185,8 @@ func TestComponent_WithActivationFunc(t *testing.T) { assert.Equal(t, err1, err2) //Compare signals without keys (because they are random) - assert.ElementsMatch(t, testOutputs1.ByName("out1").Signals(), testOutputs2.ByName("out1").Signals()) - assert.ElementsMatch(t, testOutputs1.ByName("out2").Signals(), testOutputs2.ByName("out2").Signals()) + assert.ElementsMatch(t, testOutputs1.ByName("out1").Signals().SignalsOrNil(), testOutputs2.ByName("out1").Signals().SignalsOrNil()) + assert.ElementsMatch(t, testOutputs1.ByName("out2").Signals().SignalsOrNil(), testOutputs2.ByName("out2").Signals().SignalsOrNil()) }) } diff --git a/export/dot/dot_test.go b/export/dot/dot_test.go index 7726b47..65e1a57 100644 --- a/export/dot/dot_test.go +++ b/export/dot/dot_test.go @@ -104,10 +104,17 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithInputs("num1", "num2"). WithOutputs("result"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num1 := inputs.ByName("num1").Signals().FirstPayload().(int) - num2 := inputs.ByName("num2").Signals().FirstPayload().(int) + num1, err := inputs.ByName("num1").Signals().FirstPayload() + if err != nil { + return err + } - outputs.ByName("result").PutSignals(signal.New(num1 + num2)) + num2, err := inputs.ByName("num2").Signals().FirstPayload() + if err != nil { + return err + } + + outputs.ByName("result").PutSignals(signal.New(num1.(int) + num2.(int))) return nil }) @@ -116,8 +123,11 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithInputs("num"). WithOutputs("result"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num := inputs.ByName("num").Signals().FirstPayload().(int) - outputs.ByName("result").PutSignals(signal.New(num * 3)) + num, err := inputs.ByName("num").Signals().FirstPayload() + if err != nil { + return err + } + outputs.ByName("result").PutSignals(signal.New(num.(int) * 3)) return nil }) diff --git a/fmesh_test.go b/fmesh_test.go index 02f6554..22f1597 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -595,7 +595,7 @@ func TestFMesh_runCycle(t *testing.T) { // Sets output outputs.ByName("o1").PutSignals(signal.New(1)) - outputs.ByName("o2").PutSignals(signal.NewGroup(2, 3, 4, 5)...) + outputs.ByName("o2").PutSignals(signal.NewGroup(2, 3, 4, 5).SignalsOrNil()...) return nil }), component.New("c3"). @@ -813,8 +813,9 @@ func TestFMesh_drainComponents(t *testing.T) { // c2 input received flushed signal assert.True(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) - - assert.Equal(t, "this signal is generated by c1", fm.Components().ByName("c2").Inputs().ByName("i1").Signals().FirstPayload().(string)) + sig, err := fm.Components().ByName("c2").Inputs().ByName("i1").Signals().FirstPayload() + assert.NoError(t, err) + assert.Equal(t, "this signal is generated by c1", sig.(string)) }, }, { diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index 38c5fce..e42c9de 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -25,8 +25,11 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num := inputs.ByName("num").Signals().FirstPayload().(int) - outputs.ByName("res").PutSignals(signal.New(num + 2)) + num, err := inputs.ByName("num").Signals().FirstPayload() + if err != nil { + return err + } + outputs.ByName("res").PutSignals(signal.New(num.(int) + 2)) return nil }) @@ -35,8 +38,11 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num := inputs.ByName("num").Signals().FirstPayload().(int) - outputs.ByName("res").PutSignals(signal.New(num * 3)) + num, err := inputs.ByName("num").Signals().FirstPayload() + if err != nil { + return err + } + outputs.ByName("res").PutSignals(signal.New(num.(int) * 3)) return nil }) @@ -55,8 +61,10 @@ func Test_Math(t *testing.T) { assert.Len(t, cycles, 3) resultSignals := fm.Components().ByName("c2").Outputs().ByName("res").Signals() - assert.Len(t, resultSignals, 1) - assert.Equal(t, 102, resultSignals.FirstPayload().(int)) + sig, err := resultSignals.FirstPayload() + assert.NoError(t, err) + assert.Len(t, resultSignals.SignalsOrNil(), 1) + assert.Equal(t, 102, sig.(int)) }, }, } diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index b45fe19..d89bd0d 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -78,9 +78,14 @@ func Test_Fan(t *testing.T) { assert.True(t, c3.Outputs().ByName("o1").HasSignals()) //All 3 signals are the same (literally the same address in memory) - sig1, sig2, sig3 := c1.Outputs().ByName("o1").Signals(), c2.Outputs().ByName("o1").Signals(), c3.Outputs().ByName("o1").Signals() - assert.Equal(t, sig1.FirstPayload(), sig2.FirstPayload()) - assert.Equal(t, sig2.FirstPayload(), sig3.FirstPayload()) + sig1, err := c1.Outputs().ByName("o1").Signals().FirstPayload() + assert.NoError(t, err) + sig2, err := c2.Outputs().ByName("o1").Signals().FirstPayload() + assert.NoError(t, err) + sig3, err := c3.Outputs().ByName("o1").Signals().FirstPayload() + assert.NoError(t, err) + assert.Equal(t, sig1, sig2) + assert.Equal(t, sig2, sig3) }, }, { @@ -136,11 +141,18 @@ func Test_Fan(t *testing.T) { //The signal is combined and consist of 3 payloads resultSignals := fm.Components().ByName("consumer").Outputs().ByName("o1").Signals() - assert.Len(t, resultSignals, 3) + assert.Len(t, resultSignals.SignalsOrNil(), 3) //And they are all different - assert.NotEqual(t, resultSignals.FirstPayload(), resultSignals[1].Payload()) - assert.NotEqual(t, resultSignals[1].Payload(), resultSignals[2].Payload()) + sig0, err := resultSignals.FirstPayload() + assert.NoError(t, err) + sig1, err := resultSignals.SignalsOrNil()[1].Payload() + assert.NoError(t, err) + sig2, err := resultSignals.SignalsOrNil()[2].Payload() + assert.NoError(t, err) + + assert.NotEqual(t, sig0, sig1) + assert.NotEqual(t, sig1, sig2) }, }, } diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index 9c790c2..cdfb7b0 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -26,8 +26,11 @@ func Test_WaitingForInputs(t *testing.T) { WithInputs("i1"). WithOutputs("o1"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - inputNum := inputs.ByName("i1").Signals().FirstPayload().(int) - outputs.ByName("o1").PutSignals(signal.New(inputNum * 2)) + inputNum, err := inputs.ByName("i1").Signals().FirstPayload() + if err != nil { + return err + } + outputs.ByName("o1").PutSignals(signal.New(inputNum.(int) * 2)) return nil }) } @@ -47,9 +50,17 @@ func Test_WaitingForInputs(t *testing.T) { return component.NewErrWaitForInputs(true) } - inputNum1 := inputs.ByName("i1").Signals().FirstPayload().(int) - inputNum2 := inputs.ByName("i2").Signals().FirstPayload().(int) - outputs.ByName("o1").PutSignals(signal.New(inputNum1 + inputNum2)) + inputNum1, err := inputs.ByName("i1").Signals().FirstPayload() + if err != nil { + return err + } + + inputNum2, err := inputs.ByName("i2").Signals().FirstPayload() + if err != nil { + return err + } + + outputs.ByName("o1").PutSignals(signal.New(inputNum1.(int) + inputNum2.(int))) return nil }) @@ -79,8 +90,9 @@ func Test_WaitingForInputs(t *testing.T) { }, assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { assert.NoError(t, err) - result := fm.Components().ByName("sum").Outputs().ByName("o1").Signals().FirstPayload().(int) - assert.Equal(t, 16, result) + result, err := fm.Components().ByName("sum").Outputs().ByName("o1").Signals().FirstPayload() + assert.NoError(t, err) + assert.Equal(t, 16, result.(int)) }, }, } diff --git a/port/collection.go b/port/collection.go index b361027..3cd1512 100644 --- a/port/collection.go +++ b/port/collection.go @@ -101,10 +101,14 @@ func (collection Collection) WithIndexed(prefix string, startIndex int, endIndex } // Signals returns all signals of all ports in the group -func (collection Collection) Signals() signal.Group { +func (collection Collection) Signals() *signal.Group { group := signal.NewGroup() for _, p := range collection { - group = append(group, p.Signals()...) + signals, err := p.Signals().Signals() + if err != nil { + return group.WithError(err) + } + group = group.With(signals...) } return group } diff --git a/port/collection_test.go b/port/collection_test.go index eef3819..0736579 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -164,7 +164,7 @@ func TestCollection_ByNames(t *testing.T) { func TestCollection_ClearSignal(t *testing.T) { t.Run("happy path", func(t *testing.T) { - ports := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.NewGroup(1, 2, 3)...) + ports := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.New(1), signal.New(2), signal.New(3)) assert.True(t, ports.AllHaveSignals()) ports.Clear() assert.False(t, ports.AnyHasSignals()) @@ -241,17 +241,19 @@ func TestCollection_Flush(t *testing.T) { name: "all ports in collection are flushed", collection: NewCollection().With( New("src"). - WithSignals(signal.NewGroup(1, 2, 3)...). + WithSignalGroups(signal.NewGroup(1, 2, 3)). withPipes(New("dst1"), New("dst2")), ), assertions: func(t *testing.T, collection Collection) { assert.Len(t, collection, 1) assert.False(t, collection.ByName("src").HasSignals()) for _, destPort := range collection.ByName("src").pipes { - assert.Len(t, destPort.Signals(), 3) - assert.Contains(t, destPort.Signals().AllPayloads(), 1) - assert.Contains(t, destPort.Signals().AllPayloads(), 2) - assert.Contains(t, destPort.Signals().AllPayloads(), 3) + assert.Len(t, destPort.Signals().SignalsOrNil(), 3) + allPayloads, err := destPort.Signals().AllPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 1) + assert.Contains(t, allPayloads, 2) + assert.Contains(t, allPayloads, 3) } }, }, @@ -362,7 +364,7 @@ func TestCollection_Signals(t *testing.T) { tests := []struct { name string collection Collection - want signal.Group + want *signal.Group }{ { name: "empty collection", @@ -373,7 +375,7 @@ func TestCollection_Signals(t *testing.T) { name: "non-empty collection", collection: NewCollection(). WithIndexed("p", 1, 3). - withSignals(signal.NewGroup(1, 2, 3)...). + withSignals(signal.New(1), signal.New(2), signal.New(3)). withSignals(signal.New("test")), want: signal.NewGroup(1, 2, 3, "test", 1, 2, 3, "test", 1, 2, 3, "test"), }, diff --git a/port/port.go b/port/port.go index 2b8f6cb..5e849a6 100644 --- a/port/port.go +++ b/port/port.go @@ -9,8 +9,8 @@ import ( type Port struct { common.NamedEntity common.LabeledEntity - signals signal.Group //Signal buffer - pipes Group //Outbound pipes + signals *signal.Group //TODO rename to signal buffer + pipes Group //Outbound pipes } // New creates a new port @@ -24,7 +24,7 @@ func New(name string) *Port { } // Signals getter -func (p *Port) Signals() signal.Group { +func (p *Port) Signals() *signal.Group { return p.signals } @@ -34,7 +34,7 @@ func (p *Port) Pipes() Group { } // setSignals sets signals field -func (p *Port) setSignals(signals signal.Group) { +func (p *Port) setSignals(signals *signal.Group) { p.signals = signals } @@ -44,12 +44,25 @@ func (p *Port) PutSignals(signals ...*signal.Signal) { p.setSignals(p.Signals().With(signals...)) } -// WithSignals adds signals and returns the port +// WithSignals puts signals and returns the port func (p *Port) WithSignals(signals ...*signal.Signal) *Port { p.PutSignals(signals...) return p } +// WithSignalGroups puts groups of signals and returns the port +func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { + for _, group := range signalGroups { + signals, err := group.Signals() + if err != nil { + //@TODO add error handling + } + p.PutSignals(signals...) + } + + return p +} + // Clear removes all signals from the port func (p *Port) Clear() { p.setSignals(signal.NewGroup()) @@ -71,7 +84,11 @@ func (p *Port) Flush() { // HasSignals says whether port signals is set or not func (p *Port) HasSignals() bool { - return len(p.Signals()) > 0 + signals, err := p.Signals().Signals() + if err != nil { + // TODO::add error handling + } + return len(signals) > 0 } // HasPipes says whether port has outbound pipes @@ -106,5 +123,9 @@ func (p *Port) WithLabels(labels common.LabelsCollection) *Port { // ForwardSignals copies all signals from source port to destination port, without clearing the source port func ForwardSignals(source *Port, dest *Port) { - dest.PutSignals(source.Signals()...) + signals, err := source.Signals().Signals() + if err != nil { + //@TODO::add error handling + } + dest.PutSignals(signals...) } diff --git a/port/port_test.go b/port/port_test.go index 3f5dba4..52ecff4 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -35,12 +35,12 @@ func TestPort_Signals(t *testing.T) { tests := []struct { name string port *Port - want signal.Group + want *signal.Group }{ { name: "no signals", port: New("noSignal"), - want: signal.Group{}, + want: signal.NewGroup(), }, { name: "with signal", @@ -124,54 +124,54 @@ func TestPort_PutSignals(t *testing.T) { tests := []struct { name string port *Port - signalsAfter signal.Group + signalsAfter []*signal.Signal args args }{ { name: "single signal to empty port", port: New("emptyPort"), - signalsAfter: signal.NewGroup(11), + signalsAfter: signal.NewGroup(11).SignalsOrNil(), args: args{ - signals: signal.NewGroup(11), + signals: signal.NewGroup(11).SignalsOrNil(), }, }, { name: "multiple signals to empty port", port: New("p"), - signalsAfter: signal.NewGroup(11, 12), + signalsAfter: signal.NewGroup(11, 12).SignalsOrNil(), args: args{ - signals: signal.NewGroup(11, 12), + signals: signal.NewGroup(11, 12).SignalsOrNil(), }, }, { name: "single signal to port with single signal", port: New("p").WithSignals(signal.New(11)), - signalsAfter: signal.NewGroup(11, 12), + signalsAfter: signal.NewGroup(11, 12).SignalsOrNil(), args: args{ - signals: signal.NewGroup(12), + signals: signal.NewGroup(12).SignalsOrNil(), }, }, { name: "single signals to port with multiple signals", - port: New("p").WithSignals(signal.NewGroup(11, 12)...), - signalsAfter: signal.NewGroup(11, 12, 13), + port: New("p").WithSignalGroups(signal.NewGroup(11, 12)), + signalsAfter: signal.NewGroup(11, 12, 13).SignalsOrNil(), args: args{ - signals: signal.NewGroup(13), + signals: signal.NewGroup(13).SignalsOrNil(), }, }, { name: "multiple signals to port with multiple signals", - port: New("p").WithSignals(signal.NewGroup(55, 66)...), - signalsAfter: signal.NewGroup(55, 66, 13, 14), //Notice LIFO order + port: New("p").WithSignalGroups(signal.NewGroup(55, 66)), + signalsAfter: signal.NewGroup(55, 66, 13, 14).SignalsOrNil(), args: args{ - signals: signal.NewGroup(13, 14), + signals: signal.NewGroup(13, 14).SignalsOrNil(), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.port.PutSignals(tt.args.signals...) - assert.ElementsMatch(t, tt.signalsAfter, tt.port.Signals()) + assert.ElementsMatch(t, tt.signalsAfter, tt.port.Signals().SignalsOrNil()) }) } } @@ -239,10 +239,10 @@ func TestPort_Flush(t *testing.T) { }{ { name: "port with signals and no pipes is not flushed", - srcPort: New("p").WithSignals(signal.NewGroup(1, 2, 3)...), + srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)), assertions: func(t *testing.T, srcPort *Port) { assert.True(t, srcPort.HasSignals()) - assert.Len(t, srcPort.Signals(), 3) + assert.Len(t, srcPort.Signals().SignalsOrNil(), 3) assert.False(t, srcPort.HasPipes()) }, }, @@ -256,7 +256,7 @@ func TestPort_Flush(t *testing.T) { }, { name: "flush to empty ports", - srcPort: New("p").WithSignals(signal.NewGroup(1, 2, 3)...). + srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)). withPipes( New("p1"), New("p2")), @@ -265,28 +265,32 @@ func TestPort_Flush(t *testing.T) { assert.True(t, srcPort.HasPipes()) for _, destPort := range srcPort.pipes { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Signals(), 3) - assert.Contains(t, destPort.Signals().AllPayloads(), 1) - assert.Contains(t, destPort.Signals().AllPayloads(), 2) - assert.Contains(t, destPort.Signals().AllPayloads(), 3) + assert.Len(t, destPort.Signals().SignalsOrNil(), 3) + allPayloads, err := destPort.Signals().AllPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 1) + assert.Contains(t, allPayloads, 2) + assert.Contains(t, allPayloads, 3) } }, }, { name: "flush to non empty ports", - srcPort: New("p").WithSignals(signal.NewGroup(1, 2, 3)...). + srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)). withPipes( - New("p1").WithSignals(signal.NewGroup(4, 5, 6)...), - New("p2").WithSignals(signal.NewGroup(7, 8, 9)...)), + New("p1").WithSignalGroups(signal.NewGroup(4, 5, 6)), + New("p2").WithSignalGroups(signal.NewGroup(7, 8, 9))), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) for _, destPort := range srcPort.pipes { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Signals(), 6) - assert.Contains(t, destPort.Signals().AllPayloads(), 1) - assert.Contains(t, destPort.Signals().AllPayloads(), 2) - assert.Contains(t, destPort.Signals().AllPayloads(), 3) + assert.Len(t, destPort.Signals().SignalsOrNil(), 6) + allPayloads, err := destPort.Signals().AllPayloads() + assert.NoError(t, err) + assert.Contains(t, allPayloads, 1) + assert.Contains(t, allPayloads, 2) + assert.Contains(t, allPayloads, 3) } }, }, diff --git a/signal/group.go b/signal/group.go index eb5d5ea..6470689 100644 --- a/signal/group.go +++ b/signal/group.go @@ -1,53 +1,130 @@ package signal +import ( + "errors" + "github.com/hovsep/fmesh/common" +) + // Group represents a list of signals -type Group []*Signal +type Group struct { + *common.Chainable + signals []*Signal +} // NewGroup creates empty group -func NewGroup(payloads ...any) Group { - group := make(Group, len(payloads)) +func NewGroup(payloads ...any) *Group { + signals := make([]*Signal, len(payloads)) for i, payload := range payloads { - group[i] = New(payload) + signals[i] = New(payload) + } + return &Group{ + Chainable: &common.Chainable{}, + signals: signals, } - return group } // First returns the first signal in the group -func (group Group) First() *Signal { - return group[0] +func (group *Group) First() *Signal { + if group.HasError() { + sig := New(nil) + sig.SetError(group.Error()) + return sig + } + + return group.signals[0] } // FirstPayload returns the first signal payload -func (group Group) FirstPayload() any { +func (group *Group) FirstPayload() (any, error) { + if group.HasError() { + return nil, group.Error() + } + return group.First().Payload() } // AllPayloads returns a slice with all payloads of the all signals in the group -func (group Group) AllPayloads() []any { - all := make([]any, len(group), len(group)) - for i, sig := range group { - all[i] = sig.Payload() +func (group *Group) AllPayloads() ([]any, error) { + if group.HasError() { + return nil, group.Error() + } + + all := make([]any, len(group.signals)) + var err error + for i, sig := range group.signals { + all[i], err = sig.Payload() + if err != nil { + return nil, err + } } - return all + return all, nil } // With returns the group with added signals -func (group Group) With(signals ...*Signal) Group { - newGroup := make(Group, len(group)+len(signals)) - copy(newGroup, group) +func (group *Group) With(signals ...*Signal) *Group { + if group.HasError() { + // Do nothing, but propagate error + return group + } + + newSignals := make([]*Signal, len(group.signals)+len(signals)) + copy(newSignals, group.signals) for i, sig := range signals { - newGroup[len(group)+i] = sig + if sig == nil { + group.SetError(errors.New("signal is nil")) + return group + } + + if sig.HasError() { + group.SetError(sig.Error()) + return group + } + + newSignals[len(group.signals)+i] = sig } - return newGroup + return group.withSignals(newSignals) } // WithPayloads returns a group with added signals created from provided payloads -func (group Group) WithPayloads(payloads ...any) Group { - newGroup := make(Group, len(group)+len(payloads)) - copy(newGroup, group) +func (group *Group) WithPayloads(payloads ...any) *Group { + if group.HasError() { + // Do nothing, but propagate error + return group + } + + newSignals := make([]*Signal, len(group.signals)+len(payloads)) + copy(newSignals, group.signals) for i, p := range payloads { - newGroup[len(group)+i] = New(p) + newSignals[len(group.signals)+i] = New(p) + } + return group.withSignals(newSignals) +} + +// withSignals sets signals +func (group *Group) withSignals(signals []*Signal) *Group { + group.signals = signals + return group +} + +// Signals getter +func (group *Group) Signals() ([]*Signal, error) { + if group.HasError() { + return nil, group.Error() + } + return group.signals, nil +} + +// SignalsOrNil returns signals or nil in case of any error +func (group *Group) SignalsOrNil() []*Signal { + if group.HasError() { + return nil } - return newGroup + return group.signals +} + +// WithError returns group with error +func (group *Group) WithError(err error) *Group { + group.SetError(err) + return group } diff --git a/signal/group_test.go b/signal/group_test.go index b57984e..97f8805 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -1,6 +1,7 @@ package signal import ( + "errors" "github.com/stretchr/testify/assert" "testing" ) @@ -10,48 +11,53 @@ func TestNewGroup(t *testing.T) { payloads []any } tests := []struct { - name string - args args - want Group + name string + args args + assertions func(t *testing.T, group *Group) }{ { name: "no payloads", args: args{ payloads: nil, }, - want: Group{}, + assertions: func(t *testing.T, group *Group) { + signals, err := group.Signals() + assert.NoError(t, err) + assert.Len(t, signals, 0) + }, }, { name: "with payloads", args: args{ payloads: []any{1, nil, 3}, }, - want: Group{ - &Signal{ - payload: []any{1}, - }, - &Signal{ - payload: []any{nil}, - }, - &Signal{ - payload: []any{3}, - }, + assertions: func(t *testing.T, group *Group) { + signals, err := group.Signals() + assert.NoError(t, err) + assert.Len(t, signals, 3) + assert.Contains(t, signals, New(1)) + assert.Contains(t, signals, New(nil)) + assert.Contains(t, signals, New(3)) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, NewGroup(tt.args.payloads...)) + group := NewGroup(tt.args.payloads...) + if tt.assertions != nil { + tt.assertions(t, group) + } }) } } func TestGroup_FirstPayload(t *testing.T) { tests := []struct { - name string - group Group - want any - wantPanic bool + name string + group *Group + want any + wantErrorString string + wantPanic bool }{ { name: "empty group", @@ -76,10 +82,16 @@ func TestGroup_FirstPayload(t *testing.T) { t.Run(tt.name, func(t *testing.T) { if tt.wantPanic { assert.Panics(t, func() { - tt.group.FirstPayload() + _, _ = tt.group.FirstPayload() }) } else { - assert.Equal(t, tt.want, tt.group.FirstPayload()) + got, err := tt.group.FirstPayload() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } } }) } @@ -87,9 +99,10 @@ func TestGroup_FirstPayload(t *testing.T) { func TestGroup_AllPayloads(t *testing.T) { tests := []struct { - name string - group Group - want []any + name string + group *Group + want []any + wantErrorString string }{ { name: "empty group", @@ -104,7 +117,13 @@ func TestGroup_AllPayloads(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.group.AllPayloads()) + got, err := tt.group.AllPayloads() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } }) } } @@ -115,9 +134,9 @@ func TestGroup_With(t *testing.T) { } tests := []struct { name string - group Group + group *Group args args - want Group + want *Group }{ { name: "no addition to empty group", @@ -139,7 +158,7 @@ func TestGroup_With(t *testing.T) { name: "addition to empty group", group: NewGroup(), args: args{ - signals: NewGroup(3, 4, 5), + signals: NewGroup(3, 4, 5).SignalsOrNil(), }, want: NewGroup(3, 4, 5), }, @@ -147,14 +166,32 @@ func TestGroup_With(t *testing.T) { name: "addition to group", group: NewGroup(1, 2, 3), args: args{ - signals: NewGroup(4, 5, 6), + signals: NewGroup(4, 5, 6).SignalsOrNil(), }, want: NewGroup(1, 2, 3, 4, 5, 6), }, + { + name: "error handling", + group: NewGroup(1, 2, 3). + With(New("valid before invalid")). + With(nil). + WithPayloads(4, 5, 6), + args: args{ + signals: NewGroup(7, nil, 9).SignalsOrNil(), + }, + want: NewGroup(1, 2, 3, "valid before invalid").WithError(errors.New("signal is nil")), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.group.With(tt.args.signals...)) + got := tt.group.With(tt.args.signals...) + if tt.want.HasError() { + assert.Error(t, got.Error()) + assert.EqualError(t, got.Error(), tt.want.Error().Error()) + } else { + assert.NoError(t, got.Error()) + } + assert.Equal(t, tt.want, got) }) } } @@ -165,9 +202,9 @@ func TestGroup_WithPayloads(t *testing.T) { } tests := []struct { name string - group Group + group *Group args args - want Group + want *Group }{ { name: "no addition to empty group", diff --git a/signal/signal.go b/signal/signal.go index 7c9bcc1..7dbdce5 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -1,16 +1,25 @@ package signal +import "github.com/hovsep/fmesh/common" + // Signal is a wrapper around the data flowing between components type Signal struct { + *common.Chainable payload []any //Slice is used in order to support nil payload } // New creates a new signal from the given payloads func New(payload any) *Signal { - return &Signal{payload: []any{payload}} + return &Signal{ + Chainable: &common.Chainable{}, + payload: []any{payload}, + } } // Payload getter -func (s *Signal) Payload() any { - return s.payload[0] +func (s *Signal) Payload() (any, error) { + if s.HasError() { + return nil, s.Error() + } + return s.payload[0], nil } diff --git a/signal/signal_test.go b/signal/signal_test.go index a520683..ed901eb 100644 --- a/signal/signal_test.go +++ b/signal/signal_test.go @@ -1,6 +1,7 @@ package signal import ( + "github.com/hovsep/fmesh/common" "github.com/stretchr/testify/assert" "testing" ) @@ -20,7 +21,8 @@ func TestNew(t *testing.T) { payload: nil, }, want: &Signal{ - payload: []any{nil}, + payload: []any{nil}, + Chainable: &common.Chainable{}, }, }, { @@ -28,8 +30,9 @@ func TestNew(t *testing.T) { args: args{ payload: []any{123, "hello", []int{1, 2, 3}, map[string]int{"key": 42}, []byte{}, nil}, }, - want: &Signal{payload: []any{ - []any{123, "hello", []int{1, 2, 3}, map[string]int{"key": 42}, []byte{}, nil}}, + want: &Signal{ + payload: []any{[]any{123, "hello", []int{1, 2, 3}, map[string]int{"key": 42}, []byte{}, nil}}, + Chainable: &common.Chainable{}, }, }, } @@ -42,9 +45,10 @@ func TestNew(t *testing.T) { func TestSignal_Payload(t *testing.T) { tests := []struct { - name string - signal *Signal - want any + name string + signal *Signal + want any + wantErrorString string }{ { name: "nil payload is valid", @@ -59,7 +63,14 @@ func TestSignal_Payload(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.signal.Payload()) + got, err := tt.signal.Payload() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } + }) } } From 51829a3e581de13c2f76ee0c238f78544f661736 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 16 Oct 2024 01:44:42 +0300 Subject: [PATCH 18/41] [WIP] Increase coverage --- common/chainable.go | 4 + signal/group.go | 30 ++++--- signal/group_test.go | 195 ++++++++++++++++++++++++++++++++++++------ signal/signal.go | 22 ++++- signal/signal_test.go | 31 +++++++ 5 files changed, 244 insertions(+), 38 deletions(-) diff --git a/common/chainable.go b/common/chainable.go index 008168e..0a08a0a 100644 --- a/common/chainable.go +++ b/common/chainable.go @@ -4,6 +4,10 @@ type Chainable struct { err error } +func NewChainable() *Chainable { + return &Chainable{} +} + func (c *Chainable) SetError(err error) { c.err = err } diff --git a/signal/group.go b/signal/group.go index 6470689..1333ed5 100644 --- a/signal/group.go +++ b/signal/group.go @@ -13,22 +13,25 @@ type Group struct { // NewGroup creates empty group func NewGroup(payloads ...any) *Group { + newGroup := &Group{ + Chainable: common.NewChainable(), + } + signals := make([]*Signal, len(payloads)) for i, payload := range payloads { signals[i] = New(payload) } - return &Group{ - Chainable: &common.Chainable{}, - signals: signals, - } + return newGroup.withSignals(signals) } // First returns the first signal in the group func (group *Group) First() *Signal { if group.HasError() { - sig := New(nil) - sig.SetError(group.Error()) - return sig + return New(nil).WithError(group.Error()) + } + + if len(group.signals) == 0 { + return New(nil).WithError(errors.New("group has no signals")) } return group.signals[0] @@ -71,13 +74,11 @@ func (group *Group) With(signals ...*Signal) *Group { copy(newSignals, group.signals) for i, sig := range signals { if sig == nil { - group.SetError(errors.New("signal is nil")) - return group + return group.WithError(errors.New("signal is nil")) } if sig.HasError() { - group.SetError(sig.Error()) - return group + return group.WithError(sig.Error()) } newSignals[len(group.signals)+i] = sig @@ -117,8 +118,13 @@ func (group *Group) Signals() ([]*Signal, error) { // SignalsOrNil returns signals or nil in case of any error func (group *Group) SignalsOrNil() []*Signal { + return group.SignalsOrDefault(nil) +} + +// SignalsOrDefault returns signals or default in case of any error +func (group *Group) SignalsOrDefault(defaultSignals []*Signal) []*Signal { if group.HasError() { - return nil + return defaultSignals } return group.signals } diff --git a/signal/group_test.go b/signal/group_test.go index 97f8805..fd00690 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -57,41 +57,38 @@ func TestGroup_FirstPayload(t *testing.T) { group *Group want any wantErrorString string - wantPanic bool }{ { - name: "empty group", - group: NewGroup(), - want: nil, - wantPanic: true, + name: "empty group", + group: NewGroup(), + want: nil, + wantErrorString: "group has no signals", }, { - name: "first is nil", - group: NewGroup(nil, 123), - want: nil, - wantPanic: false, + name: "first is nil", + group: NewGroup(nil, 123), + want: nil, }, { - name: "first is not nil", - group: NewGroup([]string{"1", "2"}, 123), - want: []string{"1", "2"}, - wantPanic: false, + name: "first is not nil", + group: NewGroup([]string{"1", "2"}, 123), + want: []string{"1", "2"}, + }, + { + name: "with error in chain", + group: NewGroup(3, 4, 5).WithError(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.wantPanic { - assert.Panics(t, func() { - _, _ = tt.group.FirstPayload() - }) + got, err := tt.group.FirstPayload() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) } else { - got, err := tt.group.FirstPayload() - if tt.wantErrorString != "" { - assert.Error(t, err) - assert.EqualError(t, err, tt.wantErrorString) - } else { - assert.Equal(t, tt.want, got) - } + assert.Equal(t, tt.want, got) } }) } @@ -114,6 +111,18 @@ func TestGroup_AllPayloads(t *testing.T) { group: NewGroup(1, nil, 3, []int{4, 5, 6}, map[byte]byte{7: 8}), want: []any{1, nil, 3, []int{4, 5, 6}, map[byte]byte{7: 8}}, }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", + }, + { + name: "with error in signal", + group: NewGroup().withSignals([]*Signal{New(33).WithError(errors.New("some error in signal"))}), + want: nil, + wantErrorString: "some error in signal", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -171,7 +180,7 @@ func TestGroup_With(t *testing.T) { want: NewGroup(1, 2, 3, 4, 5, 6), }, { - name: "error handling", + name: "with error in chain", group: NewGroup(1, 2, 3). With(New("valid before invalid")). With(nil). @@ -181,6 +190,17 @@ func TestGroup_With(t *testing.T) { }, want: NewGroup(1, 2, 3, "valid before invalid").WithError(errors.New("signal is nil")), }, + { + name: "with error in signal", + group: NewGroup(1, 2, 3).With(New(44).WithError(errors.New("some error in signal"))), + args: args{ + signals: []*Signal{New(456)}, + }, + want: NewGroup(1, 2, 3). + With(New(44). + WithError(errors.New("some error in signal"))). + WithError(errors.New("some error in signal")), // error propagated from signal to group + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -245,3 +265,128 @@ func TestGroup_WithPayloads(t *testing.T) { }) } } + +func TestGroup_First(t *testing.T) { + tests := []struct { + name string + group *Group + want *Signal + }{ + { + name: "empty group", + group: NewGroup(), + want: New(nil).WithError(errors.New("group has no signals")), + }, + { + name: "happy path", + group: NewGroup(3, 5, 7), + want: New(3), + }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + want: New(nil).WithError(errors.New("some error in chain")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.group.First() + if tt.want.HasError() { + assert.True(t, got.HasError()) + assert.Error(t, got.Error()) + assert.EqualError(t, got.Error(), tt.want.Error().Error()) + } else { + assert.Equal(t, tt.want, tt.group.First()) + } + }) + } +} + +func TestGroup_Signals(t *testing.T) { + tests := []struct { + name string + group *Group + want []*Signal + wantErrorString string + }{ + { + name: "empty group", + group: NewGroup(), + want: []*Signal{}, + wantErrorString: "", + }, + { + name: "with signals", + group: NewGroup(1, nil, 3), + want: []*Signal{New(1), New(nil), New(3)}, + wantErrorString: "", + }, + { + name: "with error in chain", + group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.group.Signals() + if tt.wantErrorString != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.wantErrorString) + } else { + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestGroup_SignalsOrDefault(t *testing.T) { + type args struct { + defaultSignals []*Signal + } + tests := []struct { + name string + group *Group + args args + want []*Signal + }{ + { + name: "empty group", + group: NewGroup(), + args: args{ + defaultSignals: nil, + }, + want: []*Signal{}, // Empty group has empty slice of signals + }, + { + name: "with signals", + group: NewGroup(1, 2, 3), + args: args{ + defaultSignals: []*Signal{New(4), New(5)}, //Default must be ignored + }, + want: []*Signal{New(1), New(2), New(3)}, + }, + { + name: "with error in chain and nil default", + group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + args: args{ + defaultSignals: nil, + }, + want: nil, + }, + { + name: "with error in chain and default", + group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + args: args{ + defaultSignals: []*Signal{New(4), New(5)}, + }, + want: []*Signal{New(4), New(5)}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.group.SignalsOrDefault(tt.args.defaultSignals)) + }) + } +} diff --git a/signal/signal.go b/signal/signal.go index 7dbdce5..3bb9a25 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -11,7 +11,7 @@ type Signal struct { // New creates a new signal from the given payloads func New(payload any) *Signal { return &Signal{ - Chainable: &common.Chainable{}, + Chainable: common.NewChainable(), payload: []any{payload}, } } @@ -23,3 +23,23 @@ func (s *Signal) Payload() (any, error) { } return s.payload[0], nil } + +// PayloadOrNil returns payload or nil in case of error +func (s *Signal) PayloadOrNil() any { + return s.PayloadOrDefault(nil) +} + +// PayloadOrDefault returns payload or provided default value in case of error +func (s *Signal) PayloadOrDefault(defaultValue any) any { + payload, err := s.Payload() + if err != nil { + return defaultValue + } + return payload +} + +// WithError returns signal with error +func (s *Signal) WithError(err error) *Signal { + s.SetError(err) + return s +} diff --git a/signal/signal_test.go b/signal/signal_test.go index ed901eb..1d8ba34 100644 --- a/signal/signal_test.go +++ b/signal/signal_test.go @@ -1,6 +1,7 @@ package signal import ( + "errors" "github.com/hovsep/fmesh/common" "github.com/stretchr/testify/assert" "testing" @@ -60,6 +61,12 @@ func TestSignal_Payload(t *testing.T) { signal: New(123), want: 123, }, + { + name: "with error in chain", + signal: New(123).WithError(errors.New("some error in chain")), + want: nil, + wantErrorString: "some error in chain", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -74,3 +81,27 @@ func TestSignal_Payload(t *testing.T) { }) } } + +func TestSignal_PayloadOrNil(t *testing.T) { + tests := []struct { + name string + signal *Signal + want any + }{ + { + name: "payload returned", + signal: New(123), + want: 123, + }, + { + name: "nil returned", + signal: New(123).WithError(errors.New("some error in chain")), + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.signal.PayloadOrNil()) + }) + } +} From 2f61c2829ba89781f988cdae86f23a2bb08600df Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 16 Oct 2024 01:56:11 +0300 Subject: [PATCH 19/41] Rename port.signals to buffer --- common/labeled_entity.go | 1 - component/component_test.go | 8 ++-- export/dot/dot_test.go | 6 +-- fmesh_test.go | 2 +- integration_tests/computation/math_test.go | 6 +-- integration_tests/piping/fan_test.go | 8 ++-- .../ports/waiting_for_inputs_test.go | 8 ++-- port/collection.go | 12 +++--- port/collection_test.go | 6 +-- port/port.go | 41 ++++++++++--------- port/port_test.go | 26 ++++++------ 11 files changed, 62 insertions(+), 62 deletions(-) diff --git a/common/labeled_entity.go b/common/labeled_entity.go index d8e86a3..140a715 100644 --- a/common/labeled_entity.go +++ b/common/labeled_entity.go @@ -15,7 +15,6 @@ var errLabelNotFound = errors.New("label not found") // NewLabeledEntity constructor func NewLabeledEntity(labels LabelsCollection) LabeledEntity { - return LabeledEntity{labels: labels} } diff --git a/component/component_test.go b/component/component_test.go index 1e7d25d..3efb709 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -79,12 +79,12 @@ func TestComponent_FlushOutputs(t *testing.T) { component: componentWithAllOutputsSet, destPort: sink, assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { - allPayloads, err := destPort.Signals().AllPayloads() + allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 777) assert.Contains(t, allPayloads, 888) assert.Len(t, allPayloads, 2) - // Signals are disposed when port is flushed + // Buffer is cleared when port is flushed assert.False(t, componentAfterFlush.Outputs().AnyHasSignals()) }, }, @@ -185,8 +185,8 @@ func TestComponent_WithActivationFunc(t *testing.T) { assert.Equal(t, err1, err2) //Compare signals without keys (because they are random) - assert.ElementsMatch(t, testOutputs1.ByName("out1").Signals().SignalsOrNil(), testOutputs2.ByName("out1").Signals().SignalsOrNil()) - assert.ElementsMatch(t, testOutputs1.ByName("out2").Signals().SignalsOrNil(), testOutputs2.ByName("out2").Signals().SignalsOrNil()) + assert.ElementsMatch(t, testOutputs1.ByName("out1").Buffer().SignalsOrNil(), testOutputs2.ByName("out1").Buffer().SignalsOrNil()) + assert.ElementsMatch(t, testOutputs1.ByName("out2").Buffer().SignalsOrNil(), testOutputs2.ByName("out2").Buffer().SignalsOrNil()) }) } diff --git a/export/dot/dot_test.go b/export/dot/dot_test.go index 65e1a57..1bfe6aa 100644 --- a/export/dot/dot_test.go +++ b/export/dot/dot_test.go @@ -104,12 +104,12 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithInputs("num1", "num2"). WithOutputs("result"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num1, err := inputs.ByName("num1").Signals().FirstPayload() + num1, err := inputs.ByName("num1").Buffer().FirstPayload() if err != nil { return err } - num2, err := inputs.ByName("num2").Signals().FirstPayload() + num2, err := inputs.ByName("num2").Buffer().FirstPayload() if err != nil { return err } @@ -123,7 +123,7 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithInputs("num"). WithOutputs("result"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num, err := inputs.ByName("num").Signals().FirstPayload() + num, err := inputs.ByName("num").Buffer().FirstPayload() if err != nil { return err } diff --git a/fmesh_test.go b/fmesh_test.go index 22f1597..c1d94da 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -813,7 +813,7 @@ func TestFMesh_drainComponents(t *testing.T) { // c2 input received flushed signal assert.True(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) - sig, err := fm.Components().ByName("c2").Inputs().ByName("i1").Signals().FirstPayload() + sig, err := fm.Components().ByName("c2").Inputs().ByName("i1").Buffer().FirstPayload() assert.NoError(t, err) assert.Equal(t, "this signal is generated by c1", sig.(string)) }, diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index e42c9de..8296015 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -25,7 +25,7 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num, err := inputs.ByName("num").Signals().FirstPayload() + num, err := inputs.ByName("num").Buffer().FirstPayload() if err != nil { return err } @@ -38,7 +38,7 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - num, err := inputs.ByName("num").Signals().FirstPayload() + num, err := inputs.ByName("num").Buffer().FirstPayload() if err != nil { return err } @@ -60,7 +60,7 @@ func Test_Math(t *testing.T) { assert.NoError(t, err) assert.Len(t, cycles, 3) - resultSignals := fm.Components().ByName("c2").Outputs().ByName("res").Signals() + resultSignals := fm.Components().ByName("c2").Outputs().ByName("res").Buffer() sig, err := resultSignals.FirstPayload() assert.NoError(t, err) assert.Len(t, resultSignals.SignalsOrNil(), 1) diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index d89bd0d..1ac431f 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -78,11 +78,11 @@ func Test_Fan(t *testing.T) { assert.True(t, c3.Outputs().ByName("o1").HasSignals()) //All 3 signals are the same (literally the same address in memory) - sig1, err := c1.Outputs().ByName("o1").Signals().FirstPayload() + sig1, err := c1.Outputs().ByName("o1").Buffer().FirstPayload() assert.NoError(t, err) - sig2, err := c2.Outputs().ByName("o1").Signals().FirstPayload() + sig2, err := c2.Outputs().ByName("o1").Buffer().FirstPayload() assert.NoError(t, err) - sig3, err := c3.Outputs().ByName("o1").Signals().FirstPayload() + sig3, err := c3.Outputs().ByName("o1").Buffer().FirstPayload() assert.NoError(t, err) assert.Equal(t, sig1, sig2) assert.Equal(t, sig2, sig3) @@ -140,7 +140,7 @@ func Test_Fan(t *testing.T) { assert.True(t, fm.Components().ByName("consumer").Outputs().ByName("o1").HasSignals()) //The signal is combined and consist of 3 payloads - resultSignals := fm.Components().ByName("consumer").Outputs().ByName("o1").Signals() + resultSignals := fm.Components().ByName("consumer").Outputs().ByName("o1").Buffer() assert.Len(t, resultSignals.SignalsOrNil(), 3) //And they are all different diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index cdfb7b0..a7fa6c6 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -26,7 +26,7 @@ func Test_WaitingForInputs(t *testing.T) { WithInputs("i1"). WithOutputs("o1"). WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - inputNum, err := inputs.ByName("i1").Signals().FirstPayload() + inputNum, err := inputs.ByName("i1").Buffer().FirstPayload() if err != nil { return err } @@ -50,12 +50,12 @@ func Test_WaitingForInputs(t *testing.T) { return component.NewErrWaitForInputs(true) } - inputNum1, err := inputs.ByName("i1").Signals().FirstPayload() + inputNum1, err := inputs.ByName("i1").Buffer().FirstPayload() if err != nil { return err } - inputNum2, err := inputs.ByName("i2").Signals().FirstPayload() + inputNum2, err := inputs.ByName("i2").Buffer().FirstPayload() if err != nil { return err } @@ -90,7 +90,7 @@ func Test_WaitingForInputs(t *testing.T) { }, assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { assert.NoError(t, err) - result, err := fm.Components().ByName("sum").Outputs().ByName("o1").Signals().FirstPayload() + result, err := fm.Components().ByName("sum").Outputs().ByName("o1").Buffer().FirstPayload() assert.NoError(t, err) assert.Equal(t, 16, result.(int)) }, diff --git a/port/collection.go b/port/collection.go index 3cd1512..37d708e 100644 --- a/port/collection.go +++ b/port/collection.go @@ -30,7 +30,7 @@ func (collection Collection) ByNames(names ...string) Collection { return selectedPorts } -// AnyHasSignals returns true if at least one port in collection has signals +// AnyHasSignals returns true if at least one port in collection has buffer func (collection Collection) AnyHasSignals() bool { for _, p := range collection { if p.HasSignals() { @@ -41,7 +41,7 @@ func (collection Collection) AnyHasSignals() bool { return false } -// AllHaveSignals returns true when all ports in collection have signals +// AllHaveSignals returns true when all ports in collection have buffer func (collection Collection) AllHaveSignals() bool { for _, p := range collection { if !p.HasSignals() { @@ -52,14 +52,14 @@ func (collection Collection) AllHaveSignals() bool { return true } -// PutSignals adds signals to every port in collection +// PutSignals adds buffer to every port in collection func (collection Collection) PutSignals(signals ...*signal.Signal) { for _, p := range collection { p.PutSignals(signals...) } } -// withSignals adds signals to every port in collection and returns the collection +// withSignals adds buffer to every port in collection and returns the collection func (collection Collection) withSignals(signals ...*signal.Signal) Collection { collection.PutSignals(signals...) return collection @@ -100,11 +100,11 @@ func (collection Collection) WithIndexed(prefix string, startIndex int, endIndex return collection.With(NewIndexedGroup(prefix, startIndex, endIndex)...) } -// Signals returns all signals of all ports in the group +// Signals returns all buffer of all ports in the group func (collection Collection) Signals() *signal.Group { group := signal.NewGroup() for _, p := range collection { - signals, err := p.Signals().Signals() + signals, err := p.Buffer().Signals() if err != nil { return group.WithError(err) } diff --git a/port/collection_test.go b/port/collection_test.go index 0736579..4ac3dfd 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -84,7 +84,7 @@ func TestCollection_ByName(t *testing.T) { want: New("p1"), }, { - name: "port with signals found", + name: "port with buffer found", collection: NewCollection().With(NewGroup("p1", "p2")...).withSignals(signal.New(12)), args: args{ name: "p2", @@ -248,8 +248,8 @@ func TestCollection_Flush(t *testing.T) { assert.Len(t, collection, 1) assert.False(t, collection.ByName("src").HasSignals()) for _, destPort := range collection.ByName("src").pipes { - assert.Len(t, destPort.Signals().SignalsOrNil(), 3) - allPayloads, err := destPort.Signals().AllPayloads() + assert.Len(t, destPort.Buffer().SignalsOrNil(), 3) + allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) assert.Contains(t, allPayloads, 2) diff --git a/port/port.go b/port/port.go index 5e849a6..1e53112 100644 --- a/port/port.go +++ b/port/port.go @@ -9,23 +9,24 @@ import ( type Port struct { common.NamedEntity common.LabeledEntity - signals *signal.Group //TODO rename to signal buffer - pipes Group //Outbound pipes + buffer *signal.Group + pipes Group //Outbound pipes } // New creates a new port func New(name string) *Port { return &Port{ - NamedEntity: common.NewNamedEntity(name), - pipes: NewGroup(), - signals: signal.NewGroup(), + NamedEntity: common.NewNamedEntity(name), + LabeledEntity: common.NewLabeledEntity(nil), + pipes: NewGroup(), + buffer: signal.NewGroup(), } } -// Signals getter -func (p *Port) Signals() *signal.Group { - return p.signals +// Buffer getter +func (p *Port) Buffer() *signal.Group { + return p.buffer } // Pipes getter @@ -33,24 +34,24 @@ func (p *Port) Pipes() Group { return p.pipes } -// setSignals sets signals field +// setSignals sets buffer field func (p *Port) setSignals(signals *signal.Group) { - p.signals = signals + p.buffer = signals } -// PutSignals adds signals +// PutSignals adds buffer // @TODO: rename func (p *Port) PutSignals(signals ...*signal.Signal) { - p.setSignals(p.Signals().With(signals...)) + p.setSignals(p.Buffer().With(signals...)) } -// WithSignals puts signals and returns the port +// WithSignals puts buffer and returns the port func (p *Port) WithSignals(signals ...*signal.Signal) *Port { p.PutSignals(signals...) return p } -// WithSignalGroups puts groups of signals and returns the port +// WithSignalGroups puts groups of buffer and returns the port func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { for _, group := range signalGroups { signals, err := group.Signals() @@ -63,12 +64,12 @@ func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { return p } -// Clear removes all signals from the port +// Clear removes all buffer from the port func (p *Port) Clear() { p.setSignals(signal.NewGroup()) } -// Flush pushes signals to pipes and clears the port +// Flush pushes buffer to pipes and clears the port // @TODO: hide this method from user func (p *Port) Flush() { if !p.HasSignals() || !p.HasPipes() { @@ -82,9 +83,9 @@ func (p *Port) Flush() { p.Clear() } -// HasSignals says whether port signals is set or not +// HasSignals says whether port buffer is set or not func (p *Port) HasSignals() bool { - signals, err := p.Signals().Signals() + signals, err := p.Buffer().Signals() if err != nil { // TODO::add error handling } @@ -121,9 +122,9 @@ func (p *Port) WithLabels(labels common.LabelsCollection) *Port { return p } -// ForwardSignals copies all signals from source port to destination port, without clearing the source port +// ForwardSignals copies all buffer from source port to destination port, without clearing the source port func ForwardSignals(source *Port, dest *Port) { - signals, err := source.Signals().Signals() + signals, err := source.Buffer().Signals() if err != nil { //@TODO::add error handling } diff --git a/port/port_test.go b/port/port_test.go index 52ecff4..ee83abc 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -19,7 +19,7 @@ func TestPort_HasSignals(t *testing.T) { want: false, }, { - name: "port has normal signals", + name: "port has normal buffer", port: New("p").WithSignals(signal.New(123)), want: true, }, @@ -38,7 +38,7 @@ func TestPort_Signals(t *testing.T) { want *signal.Group }{ { - name: "no signals", + name: "no buffer", port: New("noSignal"), want: signal.NewGroup(), }, @@ -50,7 +50,7 @@ func TestPort_Signals(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.port.Signals()) + assert.Equal(t, tt.want, tt.port.Buffer()) }) } } @@ -136,7 +136,7 @@ func TestPort_PutSignals(t *testing.T) { }, }, { - name: "multiple signals to empty port", + name: "multiple buffer to empty port", port: New("p"), signalsAfter: signal.NewGroup(11, 12).SignalsOrNil(), args: args{ @@ -152,7 +152,7 @@ func TestPort_PutSignals(t *testing.T) { }, }, { - name: "single signals to port with multiple signals", + name: "single buffer to port with multiple buffer", port: New("p").WithSignalGroups(signal.NewGroup(11, 12)), signalsAfter: signal.NewGroup(11, 12, 13).SignalsOrNil(), args: args{ @@ -160,7 +160,7 @@ func TestPort_PutSignals(t *testing.T) { }, }, { - name: "multiple signals to port with multiple signals", + name: "multiple buffer to port with multiple buffer", port: New("p").WithSignalGroups(signal.NewGroup(55, 66)), signalsAfter: signal.NewGroup(55, 66, 13, 14).SignalsOrNil(), args: args{ @@ -171,7 +171,7 @@ func TestPort_PutSignals(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.port.PutSignals(tt.args.signals...) - assert.ElementsMatch(t, tt.signalsAfter, tt.port.Signals().SignalsOrNil()) + assert.ElementsMatch(t, tt.signalsAfter, tt.port.Buffer().SignalsOrNil()) }) } } @@ -238,11 +238,11 @@ func TestPort_Flush(t *testing.T) { assertions func(t *testing.T, srcPort *Port) }{ { - name: "port with signals and no pipes is not flushed", + name: "port with buffer and no pipes is not flushed", srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)), assertions: func(t *testing.T, srcPort *Port) { assert.True(t, srcPort.HasSignals()) - assert.Len(t, srcPort.Signals().SignalsOrNil(), 3) + assert.Len(t, srcPort.Buffer().SignalsOrNil(), 3) assert.False(t, srcPort.HasPipes()) }, }, @@ -265,8 +265,8 @@ func TestPort_Flush(t *testing.T) { assert.True(t, srcPort.HasPipes()) for _, destPort := range srcPort.pipes { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Signals().SignalsOrNil(), 3) - allPayloads, err := destPort.Signals().AllPayloads() + assert.Len(t, destPort.Buffer().SignalsOrNil(), 3) + allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) assert.Contains(t, allPayloads, 2) @@ -285,8 +285,8 @@ func TestPort_Flush(t *testing.T) { assert.True(t, srcPort.HasPipes()) for _, destPort := range srcPort.pipes { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Signals().SignalsOrNil(), 6) - allPayloads, err := destPort.Signals().AllPayloads() + assert.Len(t, destPort.Buffer().SignalsOrNil(), 6) + allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) assert.Contains(t, allPayloads, 2) From 08f674d4fc7608f05c46b3535b052b4cfb06cc1d Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 16 Oct 2024 02:31:25 +0300 Subject: [PATCH 20/41] [WIP] added chainable to port and port group --- component/component.go | 4 +- port/collection.go | 192 ++++++++++++++++++++++++++++++++-------- port/collection_test.go | 136 ++++++++++++++-------------- port/group.go | 86 ++++++++++++++---- port/group_test.go | 37 ++++---- port/port.go | 116 ++++++++++++++++++------ port/port_test.go | 26 +++--- signal/group.go | 10 ++- 8 files changed, 427 insertions(+), 180 deletions(-) diff --git a/component/component.go b/component/component.go index e1506a6..d0870e3 100644 --- a/component/component.go +++ b/component/component.go @@ -14,8 +14,8 @@ type Component struct { common.NamedEntity common.DescribedEntity common.LabeledEntity - inputs port.Collection - outputs port.Collection + inputs *port.Collection + outputs *port.Collection f ActivationFunc } diff --git a/port/collection.go b/port/collection.go index 37d708e..064a5fa 100644 --- a/port/collection.go +++ b/port/collection.go @@ -1,38 +1,64 @@ package port import ( + "errors" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) -// Collection is a port collection with useful methods -type Collection map[string]*Port +// Collection is a port collection +// indexed by name, hence it can not carry +// 2 ports with same name. Optimized for lookups +type Collection struct { + *common.Chainable + ports map[string]*Port +} // NewCollection creates empty collection -func NewCollection() Collection { - return make(Collection) +func NewCollection() *Collection { + return &Collection{ + Chainable: common.NewChainable(), + ports: make(map[string]*Port), + } } // ByName returns a port by its name -func (collection Collection) ByName(name string) *Port { - return collection[name] +func (collection *Collection) ByName(name string) *Port { + if collection.HasError() { + return nil + } + port, ok := collection.ports[name] + if !ok { + collection.SetError(errors.New("port not found")) + return nil + } + return port } // ByNames returns multiple ports by their names -func (collection Collection) ByNames(names ...string) Collection { - selectedPorts := make(Collection) +func (collection *Collection) ByNames(names ...string) *Collection { + if collection.HasError() { + return collection + } + + selectedPorts := make(map[string]*Port) for _, name := range names { - if p, ok := collection[name]; ok { + if p, ok := collection.ports[name]; ok { selectedPorts[name] = p } } - return selectedPorts + return collection.withPorts(selectedPorts) } -// AnyHasSignals returns true if at least one port in collection has buffer -func (collection Collection) AnyHasSignals() bool { - for _, p := range collection { +// AnyHasSignals returns true if at least one port in collection has signals +func (collection *Collection) AnyHasSignals() bool { + if collection.HasError() { + return false + } + + for _, p := range collection.ports { if p.HasSignals() { return true } @@ -41,9 +67,13 @@ func (collection Collection) AnyHasSignals() bool { return false } -// AllHaveSignals returns true when all ports in collection have buffer -func (collection Collection) AllHaveSignals() bool { - for _, p := range collection { +// AllHaveSignals returns true when all ports in collection have signals +func (collection *Collection) AllHaveSignals() bool { + if collection.HasError() { + return false + } + + for _, p := range collection.ports { if !p.HasSignals() { return false } @@ -53,57 +83,101 @@ func (collection Collection) AllHaveSignals() bool { } // PutSignals adds buffer to every port in collection -func (collection Collection) PutSignals(signals ...*signal.Signal) { - for _, p := range collection { +// @TODO: return collection +func (collection *Collection) PutSignals(signals ...*signal.Signal) *Collection { + if collection.HasError() { + return collection + } + + for _, p := range collection.ports { p.PutSignals(signals...) + if p.HasError() { + return collection.WithError(p.Error()) + } } -} -// withSignals adds buffer to every port in collection and returns the collection -func (collection Collection) withSignals(signals ...*signal.Signal) Collection { - collection.PutSignals(signals...) return collection } // Clear clears all ports in collection -func (collection Collection) Clear() { - for _, p := range collection { +func (collection *Collection) Clear() *Collection { + for _, p := range collection.ports { p.Clear() + + if p.HasError() { + return collection.WithError(p.Error()) + } } + return collection } // Flush flushes all ports in collection -func (collection Collection) Flush() { - for _, p := range collection { +func (collection *Collection) Flush() *Collection { + if collection.HasError() { + return collection + } + + for _, p := range collection.ports { p.Flush() + + if p.HasError() { + return collection.WithError(p.Error()) + } } + return collection } // PipeTo creates pipes from each port in collection to given destination ports -func (collection Collection) PipeTo(destPorts ...*Port) { - for _, p := range collection { +func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { + for _, p := range collection.ports { p.PipeTo(destPorts...) + + if p.HasError() { + return collection.WithError(p.Error()) + } } + + return collection } // With adds ports to collection and returns it -func (collection Collection) With(ports ...*Port) Collection { +func (collection *Collection) With(ports ...*Port) *Collection { + if collection.HasError() { + return collection + } + for _, port := range ports { - collection[port.Name()] = port + collection.ports[port.Name()] = port + + if port.HasError() { + return collection.WithError(port.Error()) + } } return collection } // WithIndexed creates ports with names like "o1","o2","o3" and so on -func (collection Collection) WithIndexed(prefix string, startIndex int, endIndex int) Collection { - return collection.With(NewIndexedGroup(prefix, startIndex, endIndex)...) +func (collection *Collection) WithIndexed(prefix string, startIndex int, endIndex int) *Collection { + if collection.HasError() { + return collection + } + + indexedPorts, err := NewIndexedGroup(prefix, startIndex, endIndex).Ports() + if err != nil { + return collection.WithError(err) + } + return collection.With(indexedPorts...) } -// Signals returns all buffer of all ports in the group -func (collection Collection) Signals() *signal.Group { +// Signals returns all signals of all ports in the collection +func (collection *Collection) Signals() *signal.Group { + if collection.HasError() { + return signal.NewGroup().WithError(collection.Error()) + } + group := signal.NewGroup() - for _, p := range collection { + for _, p := range collection.ports { signals, err := p.Buffer().Signals() if err != nil { return group.WithError(err) @@ -112,3 +186,51 @@ func (collection Collection) Signals() *signal.Group { } return group } + +// withPorts sets ports +func (collection *Collection) withPorts(ports map[string]*Port) *Collection { + if collection.HasError() { + return collection + } + + collection.ports = ports + return collection +} + +// Ports getter +// @TODO:maybe better to hide all errors within chainable and ask user to check error ? +func (collection *Collection) Ports() (map[string]*Port, error) { + if collection.HasError() { + return nil, collection.Error() + } + return collection.ports, nil +} + +// PortsOrNil returns ports or nil in case of any error +func (collection *Collection) PortsOrNil() map[string]*Port { + return collection.PortsOrDefault(nil) +} + +// PortsOrDefault returns ports or default in case of any error +func (collection *Collection) PortsOrDefault(defaultPorts map[string]*Port) map[string]*Port { + if collection.HasError() { + return defaultPorts + } + + ports, err := collection.Ports() + if err != nil { + return defaultPorts + } + return ports +} + +// WithError returns group with error +func (collection *Collection) WithError(err error) *Collection { + collection.SetError(err) + return collection +} + +// Len returns number of ports in collection +func (collection *Collection) Len() int { + return len(collection.ports) +} diff --git a/port/collection_test.go b/port/collection_test.go index 4ac3dfd..c2adf53 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -7,17 +7,17 @@ import ( ) func TestCollection_AllHaveSignal(t *testing.T) { - oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.New(123)) + oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...).PutSignals(signal.New(123)) oneEmptyPorts.ByName("p2").Clear() tests := []struct { name string - ports Collection + ports *Collection want bool }{ { name: "all empty", - ports: NewCollection().With(NewGroup("p1", "p2")...), + ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), want: false, }, { @@ -27,7 +27,7 @@ func TestCollection_AllHaveSignal(t *testing.T) { }, { name: "all set", - ports: NewCollection().With(NewGroup("out1", "out2", "out3")...).withSignals(signal.New(77)), + ports: NewCollection().With(NewGroup("out1", "out2", "out3").PortsOrNil()...).PutSignals(signal.New(77)), want: true, }, } @@ -39,12 +39,12 @@ func TestCollection_AllHaveSignal(t *testing.T) { } func TestCollection_AnyHasSignal(t *testing.T) { - oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.New(123)) + oneEmptyPorts := NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...).PutSignals(signal.New(123)) oneEmptyPorts.ByName("p2").Clear() tests := []struct { name string - ports Collection + ports *Collection want bool }{ { @@ -54,7 +54,7 @@ func TestCollection_AnyHasSignal(t *testing.T) { }, { name: "all empty", - ports: NewCollection().With(NewGroup("p1", "p2", "p3")...), + ports: NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...), want: false, }, } @@ -71,13 +71,13 @@ func TestCollection_ByName(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args want *Port }{ { name: "empty port found", - collection: NewCollection().With(NewGroup("p1", "p2")...), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ name: "p1", }, @@ -85,7 +85,7 @@ func TestCollection_ByName(t *testing.T) { }, { name: "port with buffer found", - collection: NewCollection().With(NewGroup("p1", "p2")...).withSignals(signal.New(12)), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).PutSignals(signal.New(12)), args: args{ name: "p2", }, @@ -93,7 +93,7 @@ func TestCollection_ByName(t *testing.T) { }, { name: "port not found", - collection: NewCollection().With(NewGroup("p1", "p2")...), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ name: "p3", }, @@ -102,12 +102,8 @@ func TestCollection_ByName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotPort := tt.collection.ByName(tt.args.name) - if tt.want == nil { - assert.Nil(t, gotPort) - } else { - //Compare everything, but nror - } + got := tt.collection.ByName(tt.args.name) + assert.Equal(t, tt.want, got) }) } } @@ -118,13 +114,13 @@ func TestCollection_ByNames(t *testing.T) { } tests := []struct { name string - ports Collection + ports *Collection args args - want Collection + want *Collection }{ { name: "single port found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p1"}, }, @@ -132,27 +128,27 @@ func TestCollection_ByNames(t *testing.T) { }, { name: "multiple ports found", - ports: NewCollection().With(NewGroup("p1", "p2", "p3", "p4")...), + ports: NewCollection().With(NewGroup("p1", "p2", "p3", "p4").PortsOrNil()...), args: args{ names: []string{"p1", "p2"}, }, - want: NewCollection().With(NewGroup("p1", "p2")...), + want: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), }, { name: "single port not found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p7"}, }, - want: Collection{}, + want: NewCollection(), }, { name: "some ports not found", - ports: NewCollection().With(NewGroup("p1", "p2")...), + ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p1", "p2", "p3"}, }, - want: NewCollection().With(NewGroup("p1", "p2")...), + want: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), }, } for _, tt := range tests { @@ -164,7 +160,7 @@ func TestCollection_ByNames(t *testing.T) { func TestCollection_ClearSignal(t *testing.T) { t.Run("happy path", func(t *testing.T) { - ports := NewCollection().With(NewGroup("p1", "p2", "p3")...).withSignals(signal.New(1), signal.New(2), signal.New(3)) + ports := NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...).PutSignals(signal.New(1), signal.New(2), signal.New(3)) assert.True(t, ports.AllHaveSignals()) ports.Clear() assert.False(t, ports.AnyHasSignals()) @@ -177,9 +173,9 @@ func TestCollection_With(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "adding nothing to empty collection", @@ -187,30 +183,30 @@ func TestCollection_With(t *testing.T) { args: args{ ports: nil, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { name: "adding to empty collection", collection: NewCollection(), args: args{ - ports: NewGroup("p1", "p2"), + ports: NewGroup("p1", "p2").PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 2) - assert.Len(t, collection.ByNames("p1", "p2"), 2) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 2) + assert.Equal(t, collection.ByNames("p1", "p2").Len(), 2) }, }, { name: "adding to non-empty collection", - collection: NewCollection().With(NewGroup("p1", "p2")...), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ - ports: NewGroup("p3", "p4"), + ports: NewGroup("p3", "p4").PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 4) - assert.Len(t, collection.ByNames("p1", "p2", "p3", "p4"), 4) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 4) + assert.Equal(t, collection.ByNames("p1", "p2", "p3", "p4").Len(), 4) }, }, } @@ -227,14 +223,14 @@ func TestCollection_With(t *testing.T) { func TestCollection_Flush(t *testing.T) { tests := []struct { name string - collection Collection - assertions func(t *testing.T, collection Collection) + collection *Collection + assertions func(t *testing.T, collection *Collection) }{ { name: "empty collection", collection: NewCollection(), - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { @@ -242,13 +238,13 @@ func TestCollection_Flush(t *testing.T) { collection: NewCollection().With( New("src"). WithSignalGroups(signal.NewGroup(1, 2, 3)). - withPipes(New("dst1"), New("dst2")), + PipeTo(New("dst1"), New("dst2")), ), - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 1) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 1) assert.False(t, collection.ByName("src").HasSignals()) - for _, destPort := range collection.ByName("src").pipes { - assert.Len(t, destPort.Buffer().SignalsOrNil(), 3) + for _, destPort := range collection.ByName("src").Pipes().PortsOrNil() { + assert.Equal(t, destPort.Buffer().Len(), 3) allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) @@ -274,31 +270,31 @@ func TestCollection_PipeTo(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "empty collection", collection: NewCollection(), args: args{ - destPorts: NewIndexedGroup("dest_", 1, 3), + destPorts: NewIndexedGroup("dest_", 1, 3).PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { name: "add pipes to each port in collection", - collection: NewCollection().With(NewIndexedGroup("p", 1, 3)...), + collection: NewCollection().With(NewIndexedGroup("p", 1, 3).PortsOrNil()...), args: args{ - destPorts: NewIndexedGroup("dest", 1, 5), + destPorts: NewIndexedGroup("dest", 1, 5).PortsOrNil(), }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 3) - for _, p := range collection { + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 3) + for _, p := range collection.PortsOrNil() { assert.True(t, p.HasPipes()) - assert.Len(t, p.pipes, 5) + assert.Equal(t, p.Pipes().Len(), 5) } }, }, @@ -321,9 +317,9 @@ func TestCollection_WithIndexed(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "adding to empty collection", @@ -333,20 +329,20 @@ func TestCollection_WithIndexed(t *testing.T) { startIndex: 1, endIndex: 3, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 3) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 3) }, }, { name: "adding to non-empty collection", - collection: NewCollection().With(NewGroup("p1", "p2", "p3")...), + collection: NewCollection().With(NewGroup("p1", "p2", "p3").PortsOrNil()...), args: args{ prefix: "p", startIndex: 4, endIndex: 5, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 5) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, collection.Len(), 5) }, }, } @@ -363,7 +359,7 @@ func TestCollection_WithIndexed(t *testing.T) { func TestCollection_Signals(t *testing.T) { tests := []struct { name string - collection Collection + collection *Collection want *signal.Group }{ { @@ -375,8 +371,8 @@ func TestCollection_Signals(t *testing.T) { name: "non-empty collection", collection: NewCollection(). WithIndexed("p", 1, 3). - withSignals(signal.New(1), signal.New(2), signal.New(3)). - withSignals(signal.New("test")), + PutSignals(signal.New(1), signal.New(2), signal.New(3)). + PutSignals(signal.New("test")), want: signal.NewGroup(1, 2, 3, "test", 1, 2, 3, "test", 1, 2, 3, "test"), }, } diff --git a/port/group.go b/port/group.go index 988e01f..49df71d 100644 --- a/port/group.go +++ b/port/group.go @@ -1,42 +1,96 @@ package port -import "fmt" +import ( + "fmt" + "github.com/hovsep/fmesh/common" +) -// Group is just a slice of ports (useful to pass multiple ports as variadic argument) -type Group []*Port +// Group represents a list of ports +// can carry multiple ports with same name +// no lookup methods +type Group struct { + *common.Chainable + ports []*Port +} // NewGroup creates multiple ports -func NewGroup(names ...string) Group { - group := make(Group, len(names)) +func NewGroup(names ...string) *Group { + newGroup := &Group{ + Chainable: common.NewChainable(), + } + ports := make([]*Port, len(names)) for i, name := range names { - group[i] = New(name) + ports[i] = New(name) } - return group + return newGroup.withPorts(ports) } // NewIndexedGroup is useful to create group of ports with same prefix // NOTE: endIndex is inclusive, e.g. NewIndexedGroup("p", 0, 0) will create one port with name "p0" -func NewIndexedGroup(prefix string, startIndex int, endIndex int) Group { +func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { if startIndex > endIndex { return nil } - group := make(Group, endIndex-startIndex+1) + ports := make([]*Port, endIndex-startIndex+1) for i := startIndex; i <= endIndex; i++ { - group[i-startIndex] = New(fmt.Sprintf("%s%d", prefix, i)) + ports[i-startIndex] = New(fmt.Sprintf("%s%d", prefix, i)) } - return group + return NewGroup().withPorts(ports) } // With adds ports to group -func (group Group) With(ports ...*Port) Group { - newGroup := make(Group, len(group)+len(ports)) - copy(newGroup, group) +func (group *Group) With(ports ...*Port) *Group { + if group.HasError() { + return group + } + + newPorts := make([]*Port, len(group.ports)+len(ports)) + copy(newPorts, group.ports) for i, port := range ports { - newGroup[len(group)+i] = port + newPorts[len(group.ports)+i] = port + } + + return group.withPorts(newPorts) +} + +// withPorts sets ports +func (group *Group) withPorts(ports []*Port) *Group { + group.ports = ports + return group +} + +// Ports getter +func (group *Group) Ports() ([]*Port, error) { + if group.HasError() { + return nil, group.Error() } + return group.ports, nil +} + +// PortsOrNil returns ports or nil in case of any error +func (group *Group) PortsOrNil() []*Port { + return group.PortsOrDefault(nil) +} + +// PortsOrDefault returns ports or default in case of any error +func (group *Group) PortsOrDefault(defaultPorts []*Port) []*Port { + ports, err := group.Ports() + if err != nil { + return defaultPorts + } + return ports +} + +// WithError returns group with error +func (group *Group) WithError(err error) *Group { + group.SetError(err) + return group +} - return newGroup +// Len returns number of ports in group +func (group *Group) Len() int { + return len(group.ports) } diff --git a/port/group_test.go b/port/group_test.go index 3fb8a4f..a29db44 100644 --- a/port/group_test.go +++ b/port/group_test.go @@ -1,6 +1,7 @@ package port import ( + "github.com/hovsep/fmesh/common" "github.com/stretchr/testify/assert" "testing" ) @@ -12,23 +13,27 @@ func TestNewGroup(t *testing.T) { tests := []struct { name string args args - want Group + want *Group }{ { name: "empty group", args: args{ names: nil, }, - want: Group{}, + want: &Group{ + Chainable: common.NewChainable(), + ports: []*Port{}, + }, }, { name: "non-empty group", args: args{ names: []string{"p1", "p2"}, }, - want: Group{ - New("p1"), - New("p2"), + want: &Group{ + Chainable: common.NewChainable(), + ports: []*Port{New("p1"), + New("p2")}, }, }, } @@ -48,7 +53,7 @@ func TestNewIndexedGroup(t *testing.T) { tests := []struct { name string args args - want Group + want *Group }{ { name: "empty prefix is valid", @@ -91,9 +96,9 @@ func TestGroup_With(t *testing.T) { } tests := []struct { name string - group Group + group *Group args args - assertions func(t *testing.T, group Group) + assertions func(t *testing.T, group *Group) }{ { name: "adding nothing to empty group", @@ -101,28 +106,28 @@ func TestGroup_With(t *testing.T) { args: args{ ports: nil, }, - assertions: func(t *testing.T, group Group) { - assert.Len(t, group, 0) + assertions: func(t *testing.T, group *Group) { + assert.Zero(t, group.Len()) }, }, { name: "adding to empty group", group: NewGroup(), args: args{ - ports: NewGroup("p1", "p2", "p3"), + ports: NewGroup("p1", "p2", "p3").PortsOrNil(), }, - assertions: func(t *testing.T, group Group) { - assert.Len(t, group, 3) + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 3) }, }, { name: "adding to non-empty group", group: NewIndexedGroup("p", 1, 3), args: args{ - ports: NewGroup("p4", "p5", "p6"), + ports: NewGroup("p4", "p5", "p6").PortsOrNil(), }, - assertions: func(t *testing.T, group Group) { - assert.Len(t, group, 6) + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 6) }, }, } diff --git a/port/port.go b/port/port.go index 1e53112..166486d 100644 --- a/port/port.go +++ b/port/port.go @@ -9,8 +9,9 @@ import ( type Port struct { common.NamedEntity common.LabeledEntity + *common.Chainable buffer *signal.Group - pipes Group //Outbound pipes + pipes *Group //Outbound pipes } // New creates a new port @@ -18,6 +19,7 @@ func New(name string) *Port { return &Port{ NamedEntity: common.NewNamedEntity(name), LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), pipes: NewGroup(), buffer: signal.NewGroup(), } @@ -26,11 +28,18 @@ func New(name string) *Port { // Buffer getter func (p *Port) Buffer() *signal.Group { + if p.HasError() { + return p.buffer.WithError(p.Error()) + } return p.buffer } // Pipes getter -func (p *Port) Pipes() Group { +// @TODO maybe better to return []*Port directly +func (p *Port) Pipes() *Group { + if p.HasError() { + return p.pipes.WithError(p.Error()) + } return p.pipes } @@ -39,94 +48,149 @@ func (p *Port) setSignals(signals *signal.Group) { p.buffer = signals } -// PutSignals adds buffer +// PutSignals adds signals to buffer // @TODO: rename -func (p *Port) PutSignals(signals ...*signal.Signal) { +func (p *Port) PutSignals(signals ...*signal.Signal) *Port { + if p.HasError() { + return p + } p.setSignals(p.Buffer().With(signals...)) + return p } // WithSignals puts buffer and returns the port func (p *Port) WithSignals(signals ...*signal.Signal) *Port { - p.PutSignals(signals...) - return p + if p.HasError() { + return p + } + + return p.PutSignals(signals...) } // WithSignalGroups puts groups of buffer and returns the port func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { + if p.HasError() { + return p + } for _, group := range signalGroups { signals, err := group.Signals() if err != nil { - //@TODO add error handling + return p.WithError(err) } p.PutSignals(signals...) + if p.HasError() { + return p + } } return p } -// Clear removes all buffer from the port -func (p *Port) Clear() { +// Clear removes all signals from the port buffer +func (p *Port) Clear() *Port { + if p.HasError() { + return p + } p.setSignals(signal.NewGroup()) + return p } // Flush pushes buffer to pipes and clears the port // @TODO: hide this method from user -func (p *Port) Flush() { +func (p *Port) Flush() *Port { + if p.HasError() { + return p + } + if !p.HasSignals() || !p.HasPipes() { - return + //@TODO maybe better to return explicit errors + return nil } - for _, outboundPort := range p.pipes { + pipes, err := p.pipes.Ports() + if err != nil { + return p.WithError(err) + } + + for _, outboundPort := range pipes { //Fan-Out - ForwardSignals(p, outboundPort) + err = ForwardSignals(p, outboundPort) + if err != nil { + return p.WithError(err) + } } - p.Clear() + return p.Clear() } // HasSignals says whether port buffer is set or not func (p *Port) HasSignals() bool { + if p.HasError() { + //@TODO: add logging here + return false + } signals, err := p.Buffer().Signals() if err != nil { - // TODO::add error handling + //@TODO: add logging here + return false } return len(signals) > 0 } // HasPipes says whether port has outbound pipes func (p *Port) HasPipes() bool { - return len(p.pipes) > 0 + if p.HasError() { + //@TODO: add logging here + return false + } + pipes, err := p.pipes.Ports() + if err != nil { + //@TODO: add logging here + return false + } + + return len(pipes) > 0 } // PipeTo creates one or multiple pipes to other port(s) // @TODO: hide this method from AF -func (p *Port) PipeTo(destPorts ...*Port) { +func (p *Port) PipeTo(destPorts ...*Port) *Port { + if p.HasError() { + return p + } for _, destPort := range destPorts { if destPort == nil { continue } p.pipes = p.pipes.With(destPort) } -} - -// withPipes adds pipes and returns the port -func (p *Port) withPipes(destPorts ...*Port) *Port { - for _, destPort := range destPorts { - p.PipeTo(destPort) - } return p } // WithLabels sets labels and returns the port func (p *Port) WithLabels(labels common.LabelsCollection) *Port { + if p.HasError() { + return p + } + p.LabeledEntity.SetLabels(labels) return p } // ForwardSignals copies all buffer from source port to destination port, without clearing the source port -func ForwardSignals(source *Port, dest *Port) { +func ForwardSignals(source *Port, dest *Port) error { signals, err := source.Buffer().Signals() if err != nil { - //@TODO::add error handling + return err } dest.PutSignals(signals...) + if dest.HasError() { + return dest.Error() + } + return nil +} + +// WithError returns port with error +func (p *Port) WithError(err error) *Port { + p.SetError(err) + return p } diff --git a/port/port_test.go b/port/port_test.go index ee83abc..15272dc 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -95,7 +95,7 @@ func TestPort_PipeTo(t *testing.T) { { name: "happy path", before: p1, - after: New("p1").withPipes(p2, p3), + after: New("p1").PipeTo(p2, p3), args: args{ toPorts: []*Port{p2, p3}, }, @@ -103,7 +103,7 @@ func TestPort_PipeTo(t *testing.T) { { name: "invalid ports are ignored", before: p4, - after: New("p4").withPipes(p2), + after: New("p4").PipeTo(p2), args: args{ toPorts: []*Port{p2, nil}, }, @@ -170,8 +170,8 @@ func TestPort_PutSignals(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.port.PutSignals(tt.args.signals...) - assert.ElementsMatch(t, tt.signalsAfter, tt.port.Buffer().SignalsOrNil()) + portAfter := tt.port.PutSignals(tt.args.signals...) + assert.ElementsMatch(t, tt.signalsAfter, portAfter.Buffer().SignalsOrNil()) }) } } @@ -220,7 +220,7 @@ func TestPort_HasPipes(t *testing.T) { }, { name: "with pipes", - port: New("p1").withPipes(New("p2")), + port: New("p1").PipeTo(New("p2")), want: true, }, } @@ -242,13 +242,13 @@ func TestPort_Flush(t *testing.T) { srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)), assertions: func(t *testing.T, srcPort *Port) { assert.True(t, srcPort.HasSignals()) - assert.Len(t, srcPort.Buffer().SignalsOrNil(), 3) + assert.Equal(t, srcPort.Buffer().Len(), 3) assert.False(t, srcPort.HasPipes()) }, }, { name: "empty port with pipes is not flushed", - srcPort: New("p").withPipes(New("p1"), New("p2")), + srcPort: New("p").PipeTo(New("p1"), New("p2")), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) @@ -257,15 +257,15 @@ func TestPort_Flush(t *testing.T) { { name: "flush to empty ports", srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)). - withPipes( + PipeTo( New("p1"), New("p2")), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) - for _, destPort := range srcPort.pipes { + for _, destPort := range srcPort.Pipes().PortsOrNil() { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Buffer().SignalsOrNil(), 3) + assert.Equal(t, destPort.Buffer().Len(), 3) allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) @@ -277,15 +277,15 @@ func TestPort_Flush(t *testing.T) { { name: "flush to non empty ports", srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)). - withPipes( + PipeTo( New("p1").WithSignalGroups(signal.NewGroup(4, 5, 6)), New("p2").WithSignalGroups(signal.NewGroup(7, 8, 9))), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) - for _, destPort := range srcPort.pipes { + for _, destPort := range srcPort.Pipes().PortsOrNil() { assert.True(t, destPort.HasSignals()) - assert.Len(t, destPort.Buffer().SignalsOrNil(), 6) + assert.Equal(t, destPort.Buffer().Len(), 6) allPayloads, err := destPort.Buffer().AllPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) diff --git a/signal/group.go b/signal/group.go index 1333ed5..fdb8046 100644 --- a/signal/group.go +++ b/signal/group.go @@ -123,10 +123,11 @@ func (group *Group) SignalsOrNil() []*Signal { // SignalsOrDefault returns signals or default in case of any error func (group *Group) SignalsOrDefault(defaultSignals []*Signal) []*Signal { - if group.HasError() { + signals, err := group.Signals() + if err != nil { return defaultSignals } - return group.signals + return signals } // WithError returns group with error @@ -134,3 +135,8 @@ func (group *Group) WithError(err error) *Group { group.SetError(err) return group } + +// Len returns number of signals in group +func (group *Group) Len() int { + return len(group.signals) +} From 5a8fd68215efe275aff42a5135fda678290a0227 Mon Sep 17 00:00:00 2001 From: hovsep Date: Fri, 18 Oct 2024 01:28:08 +0300 Subject: [PATCH 21/41] [WIP] added chainable to component --- common/chainable.go | 6 +- component/activation_result.go | 15 ++- component/component.go | 100 ++++++++++++++-- component/component_test.go | 108 +++++++++--------- export/dot/dot.go | 37 ++++-- export/dot/dot_test.go | 8 +- fmesh_test.go | 40 +++---- integration_tests/computation/math_test.go | 4 +- integration_tests/piping/fan_test.go | 16 +-- .../ports/waiting_for_inputs_test.go | 4 +- port/collection.go | 66 +++++------ port/group.go | 12 +- port/port.go | 44 +++---- signal/group.go | 34 +++--- signal/group_test.go | 42 +++---- signal/signal.go | 10 +- signal/signal_test.go | 4 +- 17 files changed, 328 insertions(+), 222 deletions(-) diff --git a/common/chainable.go b/common/chainable.go index 0a08a0a..542b38a 100644 --- a/common/chainable.go +++ b/common/chainable.go @@ -8,14 +8,14 @@ func NewChainable() *Chainable { return &Chainable{} } -func (c *Chainable) SetError(err error) { +func (c *Chainable) SetChainError(err error) { c.err = err } -func (c *Chainable) HasError() bool { +func (c *Chainable) HasChainError() bool { return c.err != nil } -func (c *Chainable) Error() error { +func (c *Chainable) ChainError() error { return c.err } diff --git a/component/activation_result.go b/component/activation_result.go index a69a24a..30422f7 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -3,10 +3,12 @@ package component import ( "errors" "fmt" + "github.com/hovsep/fmesh/common" ) // ActivationResult defines the result (possibly an error) of the activation of given component in given cycle type ActivationResult struct { + *common.Chainable componentName string activated bool code ActivationResultCode @@ -18,6 +20,8 @@ type ActivationResultCode int func (a ActivationResultCode) String() string { switch a { + case ActivationCodeUndefined: + return "UNDEFINED" case ActivationCodeOK: return "OK" case ActivationCodeNoInput: @@ -38,8 +42,11 @@ func (a ActivationResultCode) String() string { } const ( + // ActivationCodeUndefined : used for error handling as zero instance + ActivationCodeUndefined ActivationResultCode = iota + // ActivationCodeOK : component is activated and did not return any errors - ActivationCodeOK ActivationResultCode = iota + ActivationCodeOK // ActivationCodeNoInput : component is not activated because it has no input set ActivationCodeNoInput @@ -173,3 +180,9 @@ func IsWaitingForInput(activationResult *ActivationResult) bool { func WantsToKeepInputs(activationResult *ActivationResult) bool { return activationResult.Code() == ActivationCodeWaitingForInputsKeep } + +// WithChainError returns activation result with chain error +func (ar *ActivationResult) WithChainError(err error) *ActivationResult { + ar.SetChainError(err) + return ar +} diff --git a/component/component.go b/component/component.go index d0870e3..e1e1320 100644 --- a/component/component.go +++ b/component/component.go @@ -7,13 +7,14 @@ import ( "github.com/hovsep/fmesh/port" ) -type ActivationFunc func(inputs port.Collection, outputs port.Collection) error +type ActivationFunc func(inputs *port.Collection, outputs *port.Collection) error // Component defines a main building block of FMesh type Component struct { common.NamedEntity common.DescribedEntity common.LabeledEntity + *common.Chainable inputs *port.Collection outputs *port.Collection f ActivationFunc @@ -22,72 +23,127 @@ type Component struct { // New creates initialized component func New(name string) *Component { return &Component{ - NamedEntity: common.NewNamedEntity(name), - inputs: port.NewCollection(), - outputs: port.NewCollection(), + NamedEntity: common.NewNamedEntity(name), + DescribedEntity: common.NewDescribedEntity(""), + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection(), + outputs: port.NewCollection(), } } // WithDescription sets a description func (c *Component) WithDescription(description string) *Component { + if c.HasChainError() { + return c + } + c.DescribedEntity = common.NewDescribedEntity(description) return c } // WithInputs ads input ports func (c *Component) WithInputs(portNames ...string) *Component { - c.inputs = c.Inputs().With(port.NewGroup(portNames...)...) + if c.HasChainError() { + return c + } + + ports, err := port.NewGroup(portNames...).Ports() + if err != nil { + return c.WithChainError(err) + } + c.inputs = c.Inputs().With(ports...) return c } // WithOutputs adds output ports func (c *Component) WithOutputs(portNames ...string) *Component { - c.outputs = c.Outputs().With(port.NewGroup(portNames...)...) + if c.HasChainError() { + return c + } + ports, err := port.NewGroup(portNames...).Ports() + if err != nil { + return c.WithChainError(err) + } + c.outputs = c.Outputs().With(ports...) return c } // WithInputsIndexed creates multiple prefixed ports func (c *Component) WithInputsIndexed(prefix string, startIndex int, endIndex int) *Component { + if c.HasChainError() { + return c + } + c.inputs = c.Inputs().WithIndexed(prefix, startIndex, endIndex) return c } // WithOutputsIndexed creates multiple prefixed ports func (c *Component) WithOutputsIndexed(prefix string, startIndex int, endIndex int) *Component { + if c.HasChainError() { + return c + } + c.outputs = c.Outputs().WithIndexed(prefix, startIndex, endIndex) return c } // WithActivationFunc sets activation function func (c *Component) WithActivationFunc(f ActivationFunc) *Component { + if c.HasChainError() { + return c + } + c.f = f return c } // WithLabels sets labels and returns the component func (c *Component) WithLabels(labels common.LabelsCollection) *Component { + if c.HasChainError() { + return c + } c.LabeledEntity.SetLabels(labels) return c } // Inputs getter -func (c *Component) Inputs() port.Collection { +func (c *Component) Inputs() *port.Collection { + if c.HasChainError() { + return port.NewCollection().WithChainError(c.ChainError()) + } + return c.inputs } // Outputs getter -func (c *Component) Outputs() port.Collection { +func (c *Component) Outputs() *port.Collection { + if c.HasChainError() { + return port.NewCollection().WithChainError(c.ChainError()) + } + return c.outputs } // hasActivationFunction checks when activation function is set func (c *Component) hasActivationFunction() bool { + if c.HasChainError() { + return false + } + return c.f != nil } // MaybeActivate tries to run the activation function if all required conditions are met // @TODO: hide this method from user +// @TODO: can we remove named return ? func (c *Component) MaybeActivate() (activationResult *ActivationResult) { + if c.HasChainError() { + activationResult = NewActivationResult(c.Name()).WithChainError(c.ChainError()) + return + } + defer func() { if r := recover(); r != nil { activationResult = c.newActivationResultPanicked(fmt.Errorf("panicked with: %v", r)) @@ -124,13 +180,35 @@ 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 { +func (c *Component) FlushOutputs() *Component { + if c.HasChainError() { + return c + } + + ports, err := c.outputs.Ports() + if err != nil { + return c.WithChainError(err) + } + for _, out := range ports { out.Flush() + if out.HasChainError() { + return c.WithChainError(out.ChainError()) + } } + return c } // ClearInputs clears all input ports -func (c *Component) ClearInputs() { +func (c *Component) ClearInputs() *Component { + if c.HasChainError() { + return c + } c.Inputs().Clear() + return c +} + +// WithChainError returns component with error +func (c *Component) WithChainError(err error) *Component { + c.SetChainError(err) + return c } diff --git a/component/component_test.go b/component/component_test.go index 3efb709..6c83bcc 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -63,7 +63,7 @@ func TestComponent_FlushOutputs(t *testing.T) { destPort: nil, assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { assert.NotNil(t, componentAfterFlush.Outputs()) - assert.Empty(t, componentAfterFlush.Outputs()) + assert.Zero(t, componentAfterFlush.Outputs().Len()) }, }, { @@ -101,20 +101,17 @@ func TestComponent_Inputs(t *testing.T) { tests := []struct { name string component *Component - want port.Collection + want *port.Collection }{ { name: "no inputs", component: New("c1"), - want: port.Collection{}, + want: port.NewCollection(), }, { name: "with inputs", component: New("c1").WithInputs("i1", "i2"), - want: port.Collection{ - "i1": port.New("i1"), - "i2": port.New("i2"), - }, + want: port.NewCollection().With(port.New("i1"), port.New("i2")), }, } for _, tt := range tests { @@ -128,20 +125,17 @@ func TestComponent_Outputs(t *testing.T) { tests := []struct { name string component *Component - want port.Collection + want *port.Collection }{ { name: "no outputs", component: New("c1"), - want: port.Collection{}, + want: port.NewCollection(), }, { name: "with outputs", component: New("c1").WithOutputs("o1", "o2"), - want: port.Collection{ - "o1": port.New("o1"), - "o2": port.New("o2"), - }, + want: port.NewCollection().With(port.New("o1"), port.New("o2")), }, } for _, tt := range tests { @@ -164,7 +158,7 @@ func TestComponent_WithActivationFunc(t *testing.T) { name: "happy path", component: New("c1"), args: args{ - f: func(inputs port.Collection, outputs port.Collection) error { + f: func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("out1").PutSignals(signal.New(23)) return nil }, @@ -176,10 +170,10 @@ func TestComponent_WithActivationFunc(t *testing.T) { componentAfter := tt.component.WithActivationFunc(tt.args.f) //Compare activation functions by they result and error - testInputs1 := port.NewCollection().With(port.NewGroup("in1", "in2")...) - testInputs2 := port.NewCollection().With(port.NewGroup("in1", "in2")...) - testOutputs1 := port.NewCollection().With(port.NewGroup("out1", "out2")...) - testOutputs2 := port.NewCollection().With(port.NewGroup("out1", "out2")...) + testInputs1 := port.NewCollection().With(port.NewGroup("in1", "in2").PortsOrNil()...) + testInputs2 := port.NewCollection().With(port.NewGroup("in1", "in2").PortsOrNil()...) + testOutputs1 := port.NewCollection().With(port.NewGroup("out1", "out2").PortsOrNil()...) + testOutputs2 := port.NewCollection().With(port.NewGroup("out1", "out2").PortsOrNil()...) err1 := componentAfter.f(testInputs1, testOutputs1) err2 := tt.args.f(testInputs2, testOutputs2) assert.Equal(t, err1, err2) @@ -211,8 +205,10 @@ func TestComponent_WithDescription(t *testing.T) { want: &Component{ NamedEntity: common.NewNamedEntity("c1"), DescribedEntity: common.NewDescribedEntity("descr"), - inputs: port.Collection{}, - outputs: port.Collection{}, + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection(), + outputs: port.NewCollection(), f: nil, }, }, @@ -243,12 +239,11 @@ func TestComponent_WithInputs(t *testing.T) { want: &Component{ NamedEntity: common.NewNamedEntity("c1"), DescribedEntity: common.NewDescribedEntity(""), - inputs: port.Collection{ - "p1": port.New("p1"), - "p2": port.New("p2"), - }, - outputs: port.Collection{}, - f: nil, + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection().With(port.New("p1"), port.New("p2")), + outputs: port.NewCollection(), + f: nil, }, }, { @@ -260,8 +255,10 @@ func TestComponent_WithInputs(t *testing.T) { want: &Component{ NamedEntity: common.NewNamedEntity("c1"), DescribedEntity: common.NewDescribedEntity(""), - inputs: port.Collection{}, - outputs: port.Collection{}, + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection(), + outputs: port.NewCollection(), f: nil, }, }, @@ -292,12 +289,11 @@ func TestComponent_WithOutputs(t *testing.T) { want: &Component{ NamedEntity: common.NewNamedEntity("c1"), DescribedEntity: common.NewDescribedEntity(""), - inputs: port.Collection{}, - outputs: port.Collection{ - "p1": port.New("p1"), - "p2": port.New("p2"), - }, - f: nil, + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection(), + outputs: port.NewCollection().With(port.New("p1"), port.New("p2")), + f: nil, }, }, { @@ -309,8 +305,10 @@ func TestComponent_WithOutputs(t *testing.T) { want: &Component{ NamedEntity: common.NewNamedEntity("c1"), DescribedEntity: common.NewDescribedEntity(""), - inputs: port.Collection{}, - outputs: port.Collection{}, + LabeledEntity: common.NewLabeledEntity(nil), + Chainable: common.NewChainable(), + inputs: port.NewCollection(), + outputs: port.NewCollection(), f: nil, }, }, @@ -352,9 +350,8 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) - return nil + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) }) return c }, @@ -367,7 +364,7 @@ func TestComponent_MaybeActivate(t *testing.T) { getComponent: func() *Component { c := New("c1"). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("test error") }) //Only one input set @@ -385,9 +382,8 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) - return nil + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) }) //Only one input set c.Inputs().ByName("i1").PutSignals(signal.New(123)) @@ -403,8 +399,7 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { panic(errors.New("oh shrimps")) return nil }) @@ -423,8 +418,7 @@ func TestComponent_MaybeActivate(t *testing.T) { c := New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { - port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { panic("oh shrimps") return nil }) @@ -443,7 +437,7 @@ func TestComponent_MaybeActivate(t *testing.T) { c1 := New("c1"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { if !inputs.ByNames("i1", "i2").AllHaveSignals() { return NewErrWaitForInputs(false) } @@ -468,7 +462,7 @@ func TestComponent_MaybeActivate(t *testing.T) { c1 := New("c1"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { if !inputs.ByNames("i1", "i2").AllHaveSignals() { return NewErrWaitForInputs(true) } @@ -525,8 +519,8 @@ func TestComponent_WithInputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Outputs(), 2) - assert.Len(t, component.Inputs(), 3) + assert.Equal(t, component.Outputs().Len(), 2) + assert.Equal(t, component.Inputs().Len(), 3) }, }, { @@ -538,8 +532,8 @@ func TestComponent_WithInputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Outputs(), 2) - assert.Len(t, component.Inputs(), 5) + assert.Equal(t, component.Outputs().Len(), 2) + assert.Equal(t, component.Inputs().Len(), 5) }, }, } @@ -574,8 +568,8 @@ func TestComponent_WithOutputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Inputs(), 2) - assert.Len(t, component.Outputs(), 3) + assert.Equal(t, component.Inputs().Len(), 2) + assert.Equal(t, component.Outputs().Len(), 3) }, }, { @@ -587,8 +581,8 @@ func TestComponent_WithOutputsIndexed(t *testing.T) { endIndex: 3, }, assertions: func(t *testing.T, component *Component) { - assert.Len(t, component.Inputs(), 2) - assert.Len(t, component.Outputs(), 5) + assert.Equal(t, component.Inputs().Len(), 2) + assert.Equal(t, component.Outputs().Len(), 5) }, }, } diff --git a/export/dot/dot.go b/export/dot/dot.go index 270a1d8..fb02f26 100644 --- a/export/dot/dot.go +++ b/export/dot/dot.go @@ -90,14 +90,17 @@ func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.C func (d *dotExporter) buildGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle) (*dot.Graph, error) { mainGraph, err := d.getMainGraph(fm, activationCycle) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get main graph: %w", err) } - d.addComponents(mainGraph, fm.Components(), activationCycle) + err = d.addComponents(mainGraph, fm.Components(), activationCycle) + if err != nil { + return nil, fmt.Errorf("failed to add components: %w", err) + } err = d.addPipes(mainGraph, fm.Components()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to add pipes: %w", err) } return mainGraph, nil } @@ -119,8 +122,17 @@ func (d *dotExporter) getMainGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle // addPipes adds pipes representation to the graph func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Collection) error { for _, c := range components { - for _, srcPort := range c.Outputs() { - for _, destPort := range srcPort.Pipes() { + srcPorts, err := c.Outputs().Ports() + if err != nil { + return err + } + + for _, srcPort := range srcPorts { + destPorts, err := srcPort.Pipes().Ports() + if err != nil { + return err + } + for _, destPort := range destPorts { // Any destination port in any pipe is input port, but we do not know in which component // so we use the label we added earlier destPortID, err := destPort.Label(nodeIDLabel) @@ -144,7 +156,7 @@ func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Colle } // addComponents adds components representation to the graph -func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent.Collection, activationCycle *cycle.Cycle) { +func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent.Collection, activationCycle *cycle.Cycle) error { for _, c := range components { // Component var activationResult *fmeshcomponent.ActivationResult @@ -155,17 +167,26 @@ func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent. componentNode := d.getComponentNode(componentSubgraph, c, activationResult) // Input ports - for _, p := range c.Inputs() { + inputPorts, err := c.Inputs().Ports() + if err != nil { + return err + } + for _, p := range inputPorts { portNode := d.getPortNode(c, p, portKindInput, componentSubgraph) componentSubgraph.Edge(portNode, componentNode) } // Output ports - for _, p := range c.Outputs() { + outputPorts, err := c.Outputs().Ports() + if err != nil { + return err + } + for _, p := range outputPorts { portNode := d.getPortNode(c, p, portKindOutput, componentSubgraph) componentSubgraph.Edge(componentNode, portNode) } } + return nil } // getPortNode creates and returns a node representing one port diff --git a/export/dot/dot_test.go b/export/dot/dot_test.go index 1bfe6aa..e12df7b 100644 --- a/export/dot/dot_test.go +++ b/export/dot/dot_test.go @@ -36,7 +36,7 @@ func Test_dotExporter_Export(t *testing.T) { WithDescription("This component adds 2 numbers"). WithInputs("num1", "num2"). WithOutputs("result"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //The activation func can be even empty, does not affect export return nil }) @@ -45,7 +45,7 @@ func Test_dotExporter_Export(t *testing.T) { WithDescription("This component multiplies number by 3"). WithInputs("num"). WithOutputs("result"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //The activation func can be even empty, does not affect export return nil }) @@ -103,7 +103,7 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithDescription("This component adds 2 numbers"). WithInputs("num1", "num2"). WithOutputs("result"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { num1, err := inputs.ByName("num1").Buffer().FirstPayload() if err != nil { return err @@ -122,7 +122,7 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithDescription("This component multiplies number by 3"). WithInputs("num"). WithOutputs("result"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { num, err := inputs.ByName("num").Buffer().FirstPayload() if err != nil { return err diff --git a/fmesh_test.go b/fmesh_test.go index c1d94da..34f757e 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -223,7 +223,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component simply puts a constant on o1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(77)) return nil }), @@ -250,7 +250,7 @@ func TestFMesh_Run(t *testing.T) { component.New("c1"). WithDescription("This component just returns an unexpected error"). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("boom") })), initFM: func(fm *FMesh) { @@ -278,7 +278,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component just sends a number to c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(10)) return nil }), @@ -286,7 +286,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.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), @@ -294,14 +294,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.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("boom") }), component.New("c4"). WithDescription("This component receives a number from c2 and panics"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { panic("no way") return nil }), @@ -378,7 +378,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component just sends a number to c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(10)) return nil }), @@ -386,7 +386,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.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), @@ -394,14 +394,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.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return errors.New("boom") }), component.New("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.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) // Even component panicked, it managed to set some data on output "o1" @@ -413,7 +413,7 @@ func TestFMesh_Run(t *testing.T) { WithDescription("This component receives a number from c4"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil }), @@ -583,7 +583,7 @@ func TestFMesh_runCycle(t *testing.T) { component.New("c1"). WithDescription(""). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { // No output return nil }), @@ -591,7 +591,7 @@ func TestFMesh_runCycle(t *testing.T) { WithDescription(""). WithInputs("i1"). WithOutputs("o1", "o2"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { // Sets output outputs.ByName("o1").PutSignals(signal.New(1)) @@ -601,7 +601,7 @@ func TestFMesh_runCycle(t *testing.T) { component.New("c3"). WithDescription(""). WithInputs("i1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { // No output return nil }), @@ -778,14 +778,14 @@ func TestFMesh_drainComponents(t *testing.T) { c1 := component.New("c1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return nil }) c2 := component.New("c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return nil }) @@ -825,14 +825,14 @@ func TestFMesh_drainComponents(t *testing.T) { c1 := component.New("c1"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return nil }) c2 := component.New("c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return nil }) @@ -877,14 +877,14 @@ func TestFMesh_drainComponents(t *testing.T) { c1 := component.New("c1"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return nil }) c2 := component.New("c2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { return nil }) diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index 8296015..c50a919 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -24,7 +24,7 @@ func Test_Math(t *testing.T) { WithDescription("adds 2 to the input"). WithInputs("num"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { num, err := inputs.ByName("num").Buffer().FirstPayload() if err != nil { return err @@ -37,7 +37,7 @@ func Test_Math(t *testing.T) { WithDescription("multiplies by 3"). WithInputs("num"). WithOutputs("res"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { num, err := inputs.ByName("num").Buffer().FirstPayload() if err != nil { return err diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index 1ac431f..24580bd 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -26,7 +26,7 @@ func Test_Fan(t *testing.T) { component.New("producer"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(time.Now())) return nil }), @@ -34,7 +34,7 @@ func Test_Fan(t *testing.T) { component.New("consumer1"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass received signal to output port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil @@ -43,7 +43,7 @@ func Test_Fan(t *testing.T) { component.New("consumer2"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass received signal to output port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil @@ -52,7 +52,7 @@ func Test_Fan(t *testing.T) { component.New("consumer3"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass received signal to output port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil @@ -94,7 +94,7 @@ func Test_Fan(t *testing.T) { producer1 := component.New("producer1"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(rand.Int())) return nil }) @@ -102,7 +102,7 @@ func Test_Fan(t *testing.T) { producer2 := component.New("producer2"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(rand.Int())) return nil }) @@ -110,14 +110,14 @@ func Test_Fan(t *testing.T) { producer3 := component.New("producer3"). WithInputs("start"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { outputs.ByName("o1").PutSignals(signal.New(rand.Int())) return nil }) consumer := component.New("consumer"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { //Bypass port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) return nil diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index a7fa6c6..c6d9c1f 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -25,7 +25,7 @@ func Test_WaitingForInputs(t *testing.T) { WithDescription("This component just doubles the input"). WithInputs("i1"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { inputNum, err := inputs.ByName("i1").Buffer().FirstPayload() if err != nil { return err @@ -45,7 +45,7 @@ func Test_WaitingForInputs(t *testing.T) { WithDescription("This component just sums 2 inputs"). WithInputs("i1", "i2"). WithOutputs("o1"). - WithActivationFunc(func(inputs port.Collection, outputs port.Collection) error { + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { if !inputs.ByNames("i1", "i2").AllHaveSignals() { return component.NewErrWaitForInputs(true) } diff --git a/port/collection.go b/port/collection.go index 064a5fa..dfa8b28 100644 --- a/port/collection.go +++ b/port/collection.go @@ -24,12 +24,12 @@ func NewCollection() *Collection { // ByName returns a port by its name func (collection *Collection) ByName(name string) *Port { - if collection.HasError() { + if collection.HasChainError() { return nil } port, ok := collection.ports[name] if !ok { - collection.SetError(errors.New("port not found")) + collection.SetChainError(errors.New("port not found")) return nil } return port @@ -37,24 +37,24 @@ func (collection *Collection) ByName(name string) *Port { // ByNames returns multiple ports by their names func (collection *Collection) ByNames(names ...string) *Collection { - if collection.HasError() { + if collection.HasChainError() { return collection } - selectedPorts := make(map[string]*Port) + selectedPorts := NewCollection() for _, name := range names { if p, ok := collection.ports[name]; ok { - selectedPorts[name] = p + selectedPorts.With(p) } } - return collection.withPorts(selectedPorts) + return selectedPorts } // AnyHasSignals returns true if at least one port in collection has signals func (collection *Collection) AnyHasSignals() bool { - if collection.HasError() { + if collection.HasChainError() { return false } @@ -69,7 +69,7 @@ func (collection *Collection) AnyHasSignals() bool { // AllHaveSignals returns true when all ports in collection have signals func (collection *Collection) AllHaveSignals() bool { - if collection.HasError() { + if collection.HasChainError() { return false } @@ -85,14 +85,14 @@ func (collection *Collection) AllHaveSignals() bool { // PutSignals adds buffer to every port in collection // @TODO: return collection func (collection *Collection) PutSignals(signals ...*signal.Signal) *Collection { - if collection.HasError() { + if collection.HasChainError() { return collection } for _, p := range collection.ports { p.PutSignals(signals...) - if p.HasError() { - return collection.WithError(p.Error()) + if p.HasChainError() { + return collection.WithChainError(p.ChainError()) } } @@ -104,8 +104,8 @@ func (collection *Collection) Clear() *Collection { for _, p := range collection.ports { p.Clear() - if p.HasError() { - return collection.WithError(p.Error()) + if p.HasChainError() { + return collection.WithChainError(p.ChainError()) } } return collection @@ -113,15 +113,15 @@ func (collection *Collection) Clear() *Collection { // Flush flushes all ports in collection func (collection *Collection) Flush() *Collection { - if collection.HasError() { + if collection.HasChainError() { return collection } for _, p := range collection.ports { p.Flush() - if p.HasError() { - return collection.WithError(p.Error()) + if p.HasChainError() { + return collection.WithChainError(p.ChainError()) } } return collection @@ -132,8 +132,8 @@ func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { for _, p := range collection.ports { p.PipeTo(destPorts...) - if p.HasError() { - return collection.WithError(p.Error()) + if p.HasChainError() { + return collection.WithChainError(p.ChainError()) } } @@ -142,15 +142,15 @@ func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { // With adds ports to collection and returns it func (collection *Collection) With(ports ...*Port) *Collection { - if collection.HasError() { + if collection.HasChainError() { return collection } for _, port := range ports { collection.ports[port.Name()] = port - if port.HasError() { - return collection.WithError(port.Error()) + if port.HasChainError() { + return collection.WithChainError(port.ChainError()) } } @@ -159,28 +159,28 @@ func (collection *Collection) With(ports ...*Port) *Collection { // WithIndexed creates ports with names like "o1","o2","o3" and so on func (collection *Collection) WithIndexed(prefix string, startIndex int, endIndex int) *Collection { - if collection.HasError() { + if collection.HasChainError() { return collection } indexedPorts, err := NewIndexedGroup(prefix, startIndex, endIndex).Ports() if err != nil { - return collection.WithError(err) + return collection.WithChainError(err) } return collection.With(indexedPorts...) } // Signals returns all signals of all ports in the collection func (collection *Collection) Signals() *signal.Group { - if collection.HasError() { - return signal.NewGroup().WithError(collection.Error()) + if collection.HasChainError() { + return signal.NewGroup().WithChainError(collection.ChainError()) } group := signal.NewGroup() for _, p := range collection.ports { signals, err := p.Buffer().Signals() if err != nil { - return group.WithError(err) + return group.WithChainError(err) } group = group.With(signals...) } @@ -189,7 +189,7 @@ func (collection *Collection) Signals() *signal.Group { // withPorts sets ports func (collection *Collection) withPorts(ports map[string]*Port) *Collection { - if collection.HasError() { + if collection.HasChainError() { return collection } @@ -200,8 +200,8 @@ func (collection *Collection) withPorts(ports map[string]*Port) *Collection { // Ports getter // @TODO:maybe better to hide all errors within chainable and ask user to check error ? func (collection *Collection) Ports() (map[string]*Port, error) { - if collection.HasError() { - return nil, collection.Error() + if collection.HasChainError() { + return nil, collection.ChainError() } return collection.ports, nil } @@ -213,7 +213,7 @@ func (collection *Collection) PortsOrNil() map[string]*Port { // PortsOrDefault returns ports or default in case of any error func (collection *Collection) PortsOrDefault(defaultPorts map[string]*Port) map[string]*Port { - if collection.HasError() { + if collection.HasChainError() { return defaultPorts } @@ -224,9 +224,9 @@ func (collection *Collection) PortsOrDefault(defaultPorts map[string]*Port) map[ return ports } -// WithError returns group with error -func (collection *Collection) WithError(err error) *Collection { - collection.SetError(err) +// WithChainError returns group with error +func (collection *Collection) WithChainError(err error) *Collection { + collection.SetChainError(err) return collection } diff --git a/port/group.go b/port/group.go index 49df71d..b05af61 100644 --- a/port/group.go +++ b/port/group.go @@ -43,7 +43,7 @@ func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { // With adds ports to group func (group *Group) With(ports ...*Port) *Group { - if group.HasError() { + if group.HasChainError() { return group } @@ -64,8 +64,8 @@ func (group *Group) withPorts(ports []*Port) *Group { // Ports getter func (group *Group) Ports() ([]*Port, error) { - if group.HasError() { - return nil, group.Error() + if group.HasChainError() { + return nil, group.ChainError() } return group.ports, nil } @@ -84,9 +84,9 @@ func (group *Group) PortsOrDefault(defaultPorts []*Port) []*Port { return ports } -// WithError returns group with error -func (group *Group) WithError(err error) *Group { - group.SetError(err) +// WithChainError returns group with error +func (group *Group) WithChainError(err error) *Group { + group.SetChainError(err) return group } diff --git a/port/port.go b/port/port.go index 166486d..18a0be7 100644 --- a/port/port.go +++ b/port/port.go @@ -28,8 +28,8 @@ func New(name string) *Port { // Buffer getter func (p *Port) Buffer() *signal.Group { - if p.HasError() { - return p.buffer.WithError(p.Error()) + if p.HasChainError() { + return p.buffer.WithChainError(p.ChainError()) } return p.buffer } @@ -37,8 +37,8 @@ func (p *Port) Buffer() *signal.Group { // Pipes getter // @TODO maybe better to return []*Port directly func (p *Port) Pipes() *Group { - if p.HasError() { - return p.pipes.WithError(p.Error()) + if p.HasChainError() { + return p.pipes.WithChainError(p.ChainError()) } return p.pipes } @@ -51,7 +51,7 @@ func (p *Port) setSignals(signals *signal.Group) { // PutSignals adds signals to buffer // @TODO: rename func (p *Port) PutSignals(signals ...*signal.Signal) *Port { - if p.HasError() { + if p.HasChainError() { return p } p.setSignals(p.Buffer().With(signals...)) @@ -60,7 +60,7 @@ func (p *Port) PutSignals(signals ...*signal.Signal) *Port { // WithSignals puts buffer and returns the port func (p *Port) WithSignals(signals ...*signal.Signal) *Port { - if p.HasError() { + if p.HasChainError() { return p } @@ -69,16 +69,16 @@ func (p *Port) WithSignals(signals ...*signal.Signal) *Port { // WithSignalGroups puts groups of buffer and returns the port func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { - if p.HasError() { + if p.HasChainError() { return p } for _, group := range signalGroups { signals, err := group.Signals() if err != nil { - return p.WithError(err) + return p.WithChainError(err) } p.PutSignals(signals...) - if p.HasError() { + if p.HasChainError() { return p } } @@ -88,7 +88,7 @@ func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { // Clear removes all signals from the port buffer func (p *Port) Clear() *Port { - if p.HasError() { + if p.HasChainError() { return p } p.setSignals(signal.NewGroup()) @@ -98,7 +98,7 @@ func (p *Port) Clear() *Port { // Flush pushes buffer to pipes and clears the port // @TODO: hide this method from user func (p *Port) Flush() *Port { - if p.HasError() { + if p.HasChainError() { return p } @@ -109,14 +109,14 @@ func (p *Port) Flush() *Port { pipes, err := p.pipes.Ports() if err != nil { - return p.WithError(err) + return p.WithChainError(err) } for _, outboundPort := range pipes { //Fan-Out err = ForwardSignals(p, outboundPort) if err != nil { - return p.WithError(err) + return p.WithChainError(err) } } return p.Clear() @@ -124,7 +124,7 @@ func (p *Port) Flush() *Port { // HasSignals says whether port buffer is set or not func (p *Port) HasSignals() bool { - if p.HasError() { + if p.HasChainError() { //@TODO: add logging here return false } @@ -138,7 +138,7 @@ func (p *Port) HasSignals() bool { // HasPipes says whether port has outbound pipes func (p *Port) HasPipes() bool { - if p.HasError() { + if p.HasChainError() { //@TODO: add logging here return false } @@ -154,7 +154,7 @@ func (p *Port) HasPipes() bool { // PipeTo creates one or multiple pipes to other port(s) // @TODO: hide this method from AF func (p *Port) PipeTo(destPorts ...*Port) *Port { - if p.HasError() { + if p.HasChainError() { return p } for _, destPort := range destPorts { @@ -168,7 +168,7 @@ func (p *Port) PipeTo(destPorts ...*Port) *Port { // WithLabels sets labels and returns the port func (p *Port) WithLabels(labels common.LabelsCollection) *Port { - if p.HasError() { + if p.HasChainError() { return p } @@ -183,14 +183,14 @@ func ForwardSignals(source *Port, dest *Port) error { return err } dest.PutSignals(signals...) - if dest.HasError() { - return dest.Error() + if dest.HasChainError() { + return dest.ChainError() } return nil } -// WithError returns port with error -func (p *Port) WithError(err error) *Port { - p.SetError(err) +// WithChainError returns port with error +func (p *Port) WithChainError(err error) *Port { + p.SetChainError(err) return p } diff --git a/signal/group.go b/signal/group.go index fdb8046..2793bcf 100644 --- a/signal/group.go +++ b/signal/group.go @@ -26,12 +26,12 @@ func NewGroup(payloads ...any) *Group { // First returns the first signal in the group func (group *Group) First() *Signal { - if group.HasError() { - return New(nil).WithError(group.Error()) + if group.HasChainError() { + return New(nil).WithChainError(group.ChainError()) } if len(group.signals) == 0 { - return New(nil).WithError(errors.New("group has no signals")) + return New(nil).WithChainError(errors.New("group has no signals")) } return group.signals[0] @@ -39,8 +39,8 @@ func (group *Group) First() *Signal { // FirstPayload returns the first signal payload func (group *Group) FirstPayload() (any, error) { - if group.HasError() { - return nil, group.Error() + if group.HasChainError() { + return nil, group.ChainError() } return group.First().Payload() @@ -48,8 +48,8 @@ func (group *Group) FirstPayload() (any, error) { // AllPayloads returns a slice with all payloads of the all signals in the group func (group *Group) AllPayloads() ([]any, error) { - if group.HasError() { - return nil, group.Error() + if group.HasChainError() { + return nil, group.ChainError() } all := make([]any, len(group.signals)) @@ -65,7 +65,7 @@ func (group *Group) AllPayloads() ([]any, error) { // With returns the group with added signals func (group *Group) With(signals ...*Signal) *Group { - if group.HasError() { + if group.HasChainError() { // Do nothing, but propagate error return group } @@ -74,11 +74,11 @@ func (group *Group) With(signals ...*Signal) *Group { copy(newSignals, group.signals) for i, sig := range signals { if sig == nil { - return group.WithError(errors.New("signal is nil")) + return group.WithChainError(errors.New("signal is nil")) } - if sig.HasError() { - return group.WithError(sig.Error()) + if sig.HasChainError() { + return group.WithChainError(sig.ChainError()) } newSignals[len(group.signals)+i] = sig @@ -89,7 +89,7 @@ func (group *Group) With(signals ...*Signal) *Group { // WithPayloads returns a group with added signals created from provided payloads func (group *Group) WithPayloads(payloads ...any) *Group { - if group.HasError() { + if group.HasChainError() { // Do nothing, but propagate error return group } @@ -110,8 +110,8 @@ func (group *Group) withSignals(signals []*Signal) *Group { // Signals getter func (group *Group) Signals() ([]*Signal, error) { - if group.HasError() { - return nil, group.Error() + if group.HasChainError() { + return nil, group.ChainError() } return group.signals, nil } @@ -130,9 +130,9 @@ func (group *Group) SignalsOrDefault(defaultSignals []*Signal) []*Signal { return signals } -// WithError returns group with error -func (group *Group) WithError(err error) *Group { - group.SetError(err) +// WithChainError returns group with error +func (group *Group) WithChainError(err error) *Group { + group.SetChainError(err) return group } diff --git a/signal/group_test.go b/signal/group_test.go index fd00690..bb737d3 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -76,7 +76,7 @@ func TestGroup_FirstPayload(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(3, 4, 5).WithError(errors.New("some error in chain")), + group: NewGroup(3, 4, 5).WithChainError(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, @@ -113,13 +113,13 @@ func TestGroup_AllPayloads(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, { name: "with error in signal", - group: NewGroup().withSignals([]*Signal{New(33).WithError(errors.New("some error in signal"))}), + group: NewGroup().withSignals([]*Signal{New(33).WithChainError(errors.New("some error in signal"))}), want: nil, wantErrorString: "some error in signal", }, @@ -188,28 +188,28 @@ func TestGroup_With(t *testing.T) { args: args{ signals: NewGroup(7, nil, 9).SignalsOrNil(), }, - want: NewGroup(1, 2, 3, "valid before invalid").WithError(errors.New("signal is nil")), + want: NewGroup(1, 2, 3, "valid before invalid").WithChainError(errors.New("signal is nil")), }, { name: "with error in signal", - group: NewGroup(1, 2, 3).With(New(44).WithError(errors.New("some error in signal"))), + group: NewGroup(1, 2, 3).With(New(44).WithChainError(errors.New("some error in signal"))), args: args{ signals: []*Signal{New(456)}, }, want: NewGroup(1, 2, 3). With(New(44). - WithError(errors.New("some error in signal"))). - WithError(errors.New("some error in signal")), // error propagated from signal to group + WithChainError(errors.New("some error in signal"))). + WithChainError(errors.New("some error in signal")), // error propagated from signal to group }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.group.With(tt.args.signals...) - if tt.want.HasError() { - assert.Error(t, got.Error()) - assert.EqualError(t, got.Error(), tt.want.Error().Error()) + if tt.want.HasChainError() { + assert.Error(t, got.ChainError()) + assert.EqualError(t, got.ChainError(), tt.want.ChainError().Error()) } else { - assert.NoError(t, got.Error()) + assert.NoError(t, got.ChainError()) } assert.Equal(t, tt.want, got) }) @@ -275,7 +275,7 @@ func TestGroup_First(t *testing.T) { { name: "empty group", group: NewGroup(), - want: New(nil).WithError(errors.New("group has no signals")), + want: New(nil).WithChainError(errors.New("group has no signals")), }, { name: "happy path", @@ -284,17 +284,17 @@ func TestGroup_First(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), - want: New(nil).WithError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), + want: New(nil).WithChainError(errors.New("some error in chain")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.group.First() - if tt.want.HasError() { - assert.True(t, got.HasError()) - assert.Error(t, got.Error()) - assert.EqualError(t, got.Error(), tt.want.Error().Error()) + if tt.want.HasChainError() { + assert.True(t, got.HasChainError()) + assert.Error(t, got.ChainError()) + assert.EqualError(t, got.ChainError(), tt.want.ChainError().Error()) } else { assert.Equal(t, tt.want, tt.group.First()) } @@ -323,7 +323,7 @@ func TestGroup_Signals(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, @@ -369,7 +369,7 @@ func TestGroup_SignalsOrDefault(t *testing.T) { }, { name: "with error in chain and nil default", - group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), args: args{ defaultSignals: nil, }, @@ -377,7 +377,7 @@ func TestGroup_SignalsOrDefault(t *testing.T) { }, { name: "with error in chain and default", - group: NewGroup(1, 2, 3).WithError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), args: args{ defaultSignals: []*Signal{New(4), New(5)}, }, diff --git a/signal/signal.go b/signal/signal.go index 3bb9a25..591e65d 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -18,8 +18,8 @@ func New(payload any) *Signal { // Payload getter func (s *Signal) Payload() (any, error) { - if s.HasError() { - return nil, s.Error() + if s.HasChainError() { + return nil, s.ChainError() } return s.payload[0], nil } @@ -38,8 +38,8 @@ func (s *Signal) PayloadOrDefault(defaultValue any) any { return payload } -// WithError returns signal with error -func (s *Signal) WithError(err error) *Signal { - s.SetError(err) +// WithChainError returns signal with error +func (s *Signal) WithChainError(err error) *Signal { + s.SetChainError(err) return s } diff --git a/signal/signal_test.go b/signal/signal_test.go index 1d8ba34..8d38ffb 100644 --- a/signal/signal_test.go +++ b/signal/signal_test.go @@ -63,7 +63,7 @@ func TestSignal_Payload(t *testing.T) { }, { name: "with error in chain", - signal: New(123).WithError(errors.New("some error in chain")), + signal: New(123).WithChainError(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, @@ -95,7 +95,7 @@ func TestSignal_PayloadOrNil(t *testing.T) { }, { name: "nil returned", - signal: New(123).WithError(errors.New("some error in chain")), + signal: New(123).WithChainError(errors.New("some error in chain")), want: nil, }, } From 4433f9137e3e5b4144aa05f8ee2c7764eeb19f24 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 20 Oct 2024 23:30:42 +0300 Subject: [PATCH 22/41] Add some shortcut methods --- component/component.go | 20 +++++++ component/component_test.go | 20 +++---- export/dot/dot_test.go | 14 ++--- fmesh_test.go | 56 +++++++++---------- integration_tests/computation/math_test.go | 10 ++-- integration_tests/piping/fan_test.go | 38 ++++++------- .../ports/waiting_for_inputs_test.go | 22 ++++---- port/port.go | 38 ++++++++++++- port/port_test.go | 6 +- signal/signal.go | 4 +- 10 files changed, 141 insertions(+), 87 deletions(-) diff --git a/component/component.go b/component/component.go index e1e1320..15a0d13 100644 --- a/component/component.go +++ b/component/component.go @@ -126,6 +126,26 @@ func (c *Component) Outputs() *port.Collection { return c.outputs } +// OutputByName is shortcut method +func (c *Component) OutputByName(name string) *port.Port { + outputPort := c.Outputs().ByName(name) + if outputPort.HasChainError() { + c.SetChainError(outputPort.ChainError()) + return nil + } + return outputPort +} + +// InputByName is shortcut method +func (c *Component) InputByName(name string) *port.Port { + inputPort := c.Inputs().ByName(name) + if inputPort.HasChainError() { + c.SetChainError(inputPort.ChainError()) + return nil + } + return inputPort +} + // hasActivationFunction checks when activation function is set func (c *Component) hasActivationFunction() bool { if c.HasChainError() { diff --git a/component/component_test.go b/component/component_test.go index 6c83bcc..bdc4130 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -79,7 +79,7 @@ func TestComponent_FlushOutputs(t *testing.T) { component: componentWithAllOutputsSet, destPort: sink, assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { - allPayloads, err := destPort.Buffer().AllPayloads() + allPayloads, err := destPort.AllSignalsPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 777) assert.Contains(t, allPayloads, 888) @@ -179,8 +179,8 @@ func TestComponent_WithActivationFunc(t *testing.T) { assert.Equal(t, err1, err2) //Compare signals without keys (because they are random) - assert.ElementsMatch(t, testOutputs1.ByName("out1").Buffer().SignalsOrNil(), testOutputs2.ByName("out1").Buffer().SignalsOrNil()) - assert.ElementsMatch(t, testOutputs1.ByName("out2").Buffer().SignalsOrNil(), testOutputs2.ByName("out2").Buffer().SignalsOrNil()) + assert.ElementsMatch(t, testOutputs1.ByName("out1").AllSignalsOrNil(), testOutputs2.ByName("out1").AllSignalsOrNil()) + assert.ElementsMatch(t, testOutputs1.ByName("out2").AllSignalsOrNil(), testOutputs2.ByName("out2").AllSignalsOrNil()) }) } @@ -337,7 +337,7 @@ func TestComponent_MaybeActivate(t *testing.T) { name: "component with inputs set, but no activation func", getComponent: func() *Component { c := New("c1").WithInputs("i1") - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -368,7 +368,7 @@ func TestComponent_MaybeActivate(t *testing.T) { return errors.New("test error") }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -386,7 +386,7 @@ func TestComponent_MaybeActivate(t *testing.T) { return port.ForwardSignals(inputs.ByName("i1"), outputs.ByName("o1")) }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -404,7 +404,7 @@ func TestComponent_MaybeActivate(t *testing.T) { return nil }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -423,7 +423,7 @@ func TestComponent_MaybeActivate(t *testing.T) { return nil }) //Only one input set - c.Inputs().ByName("i1").PutSignals(signal.New(123)) + c.InputByName("i1").PutSignals(signal.New(123)) return c }, wantActivationResult: NewActivationResult("c1"). @@ -445,7 +445,7 @@ func TestComponent_MaybeActivate(t *testing.T) { }) // Only one input set - c1.Inputs().ByName("i1").PutSignals(signal.New(123)) + c1.InputByName("i1").PutSignals(signal.New(123)) return c1 }, @@ -470,7 +470,7 @@ func TestComponent_MaybeActivate(t *testing.T) { }) // Only one input set - c1.Inputs().ByName("i1").PutSignals(signal.New(123)) + c1.InputByName("i1").PutSignals(signal.New(123)) return c1 }, diff --git a/export/dot/dot_test.go b/export/dot/dot_test.go index e12df7b..238c661 100644 --- a/export/dot/dot_test.go +++ b/export/dot/dot_test.go @@ -50,7 +50,7 @@ func Test_dotExporter_Export(t *testing.T) { return nil }) - adder.Outputs().ByName("result").PipeTo(multiplier.Inputs().ByName("num")) + adder.OutputByName("result").PipeTo(multiplier.InputByName("num")) fm := fmesh.New("fm"). WithDescription("This f-mesh has just one component"). @@ -104,12 +104,12 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithInputs("num1", "num2"). WithOutputs("result"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - num1, err := inputs.ByName("num1").Buffer().FirstPayload() + num1, err := inputs.ByName("num1").FirstSignalPayload() if err != nil { return err } - num2, err := inputs.ByName("num2").Buffer().FirstPayload() + num2, err := inputs.ByName("num2").FirstSignalPayload() if err != nil { return err } @@ -123,7 +123,7 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { WithInputs("num"). WithOutputs("result"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - num, err := inputs.ByName("num").Buffer().FirstPayload() + num, err := inputs.ByName("num").FirstSignalPayload() if err != nil { return err } @@ -131,14 +131,14 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { return nil }) - adder.Outputs().ByName("result").PipeTo(multiplier.Inputs().ByName("num")) + adder.OutputByName("result").PipeTo(multiplier.InputByName("num")) fm := fmesh.New("fm"). WithDescription("This f-mesh has just one component"). WithComponents(adder, multiplier) - adder.Inputs().ByName("num1").PutSignals(signal.New(15)) - adder.Inputs().ByName("num2").PutSignals(signal.New(12)) + adder.InputByName("num1").PutSignals(signal.New(15)) + adder.InputByName("num2").PutSignals(signal.New(12)) return fm }(), diff --git a/fmesh_test.go b/fmesh_test.go index 34f757e..48b8e2d 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -230,7 +230,7 @@ func TestFMesh_Run(t *testing.T) { ), initFM: func(fm *FMesh) { //Fire the mesh - fm.Components().ByName("c1").Inputs().ByName("i1").PutSignals(signal.New("start c1")) + fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New("start c1")) }, want: cycle.NewCollection().With( cycle.New(). @@ -254,7 +254,7 @@ func TestFMesh_Run(t *testing.T) { return errors.New("boom") })), initFM: func(fm *FMesh) { - fm.Components().ByName("c1").Inputs().ByName("i1").PutSignals(signal.New("start")) + fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New("start")) }, want: cycle.NewCollection().With( cycle.New(). @@ -309,12 +309,12 @@ func TestFMesh_Run(t *testing.T) { initFM: func(fm *FMesh) { c1, c2, c3, c4 := fm.Components().ByName("c1"), fm.Components().ByName("c2"), fm.Components().ByName("c3"), fm.Components().ByName("c4") //Piping - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) - c2.Outputs().ByName("o1").PipeTo(c4.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + c2.OutputByName("o1").PipeTo(c4.InputByName("i1")) //Input data - c1.Inputs().ByName("i1").PutSignals(signal.New("start c1")) - c3.Inputs().ByName("i1").PutSignals(signal.New("start c3")) + c1.InputByName("i1").PutSignals(signal.New("start c1")) + c3.InputByName("i1").PutSignals(signal.New("start c3")) }, want: cycle.NewCollection().With( cycle.New(). @@ -421,13 +421,13 @@ func TestFMesh_Run(t *testing.T) { initFM: func(fm *FMesh) { c1, c2, c3, c4, c5 := fm.Components().ByName("c1"), fm.Components().ByName("c2"), fm.Components().ByName("c3"), fm.Components().ByName("c4"), fm.Components().ByName("c5") //Piping - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) - c2.Outputs().ByName("o1").PipeTo(c4.Inputs().ByName("i1")) - c4.Outputs().ByName("o1").PipeTo(c5.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) + c2.OutputByName("o1").PipeTo(c4.InputByName("i1")) + c4.OutputByName("o1").PipeTo(c5.InputByName("i1")) //Input data - c1.Inputs().ByName("i1").PutSignals(signal.New("start c1")) - c3.Inputs().ByName("i1").PutSignals(signal.New("start c3")) + c1.InputByName("i1").PutSignals(signal.New("start c1")) + c3.InputByName("i1").PutSignals(signal.New("start c3")) }, want: cycle.NewCollection().With( //c1 and c3 activated, c3 finishes with error @@ -607,9 +607,9 @@ func TestFMesh_runCycle(t *testing.T) { }), ), initFM: func(fm *FMesh) { - fm.Components().ByName("c1").Inputs().ByName("i1").PutSignals(signal.New(1)) - fm.Components().ByName("c2").Inputs().ByName("i1").PutSignals(signal.New(2)) - fm.Components().ByName("c3").Inputs().ByName("i1").PutSignals(signal.New(3)) + fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New(1)) + fm.Components().ByName("c2").InputByName("i1").PutSignals(signal.New(2)) + fm.Components().ByName("c3").InputByName("i1").PutSignals(signal.New(3)) }, want: cycle.New().WithActivationResults( component.NewActivationResult("c1"). @@ -769,7 +769,7 @@ func TestFMesh_drainComponents(t *testing.T) { }, assertions: func(t *testing.T, fm *FMesh) { // Assert that inputs are not cleared - assert.True(t, fm.Components().ByName("c1").Inputs().ByName("i1").HasSignals()) + assert.True(t, fm.Components().ByName("c1").InputByName("i1").HasSignals()) }, }, { @@ -790,10 +790,10 @@ func TestFMesh_drainComponents(t *testing.T) { }) // Pipe - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) // Simulate activation of c1 - c1.Outputs().ByName("o1").PutSignals(signal.New("this signal is generated by c1")) + c1.OutputByName("o1").PutSignals(signal.New("this signal is generated by c1")) return New("fm").WithComponents(c1, c2) }, @@ -809,11 +809,11 @@ func TestFMesh_drainComponents(t *testing.T) { }, assertions: func(t *testing.T, fm *FMesh) { // c1 output is cleared - assert.False(t, fm.Components().ByName("c1").Outputs().ByName("o1").HasSignals()) + assert.False(t, fm.Components().ByName("c1").OutputByName("o1").HasSignals()) // c2 input received flushed signal - assert.True(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) - sig, err := fm.Components().ByName("c2").Inputs().ByName("i1").Buffer().FirstPayload() + assert.True(t, fm.Components().ByName("c2").InputByName("i1").HasSignals()) + sig, err := fm.Components().ByName("c2").InputByName("i1").FirstSignalPayload() assert.NoError(t, err) assert.Equal(t, "this signal is generated by c1", sig.(string)) }, @@ -837,16 +837,16 @@ func TestFMesh_drainComponents(t *testing.T) { }) // Pipe - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) // Simulate activation of c1 // NOTE: normally component should not create any output signal if it is waiting for inputs // but technically there is no limitation to do that and then return the special error to wait for inputs. // F-mesh just never flushes components waiting for inputs, so this test checks that - c1.Outputs().ByName("o1").PutSignals(signal.New("this signal is generated by c1")) + c1.OutputByName("o1").PutSignals(signal.New("this signal is generated by c1")) // Also simulate input signal on one port - c1.Inputs().ByName("i1").PutSignals(signal.New("this is input signal for c1")) + c1.InputByName("i1").PutSignals(signal.New("this is input signal for c1")) return New("fm").WithComponents(c1, c2) }, @@ -864,7 +864,7 @@ func TestFMesh_drainComponents(t *testing.T) { }, assertions: func(t *testing.T, fm *FMesh) { // As c1 is waiting for inputs it's outputs must not be flushed - assert.False(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) + assert.False(t, fm.Components().ByName("c2").InputByName("i1").HasSignals()) // The inputs must be cleared assert.False(t, fm.Components().ByName("c1").Inputs().AnyHasSignals()) @@ -889,16 +889,16 @@ func TestFMesh_drainComponents(t *testing.T) { }) // Pipe - c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + c1.OutputByName("o1").PipeTo(c2.InputByName("i1")) // Simulate activation of c1 // NOTE: normally component should not create any output signal if it is waiting for inputs // but technically there is no limitation to do that and then return the special error to wait for inputs. // F-mesh just never flushes components waiting for inputs, so this test checks that - c1.Outputs().ByName("o1").PutSignals(signal.New("this signal is generated by c1")) + c1.OutputByName("o1").PutSignals(signal.New("this signal is generated by c1")) // Also simulate input signal on one port - c1.Inputs().ByName("i1").PutSignals(signal.New("this is input signal for c1")) + c1.InputByName("i1").PutSignals(signal.New("this is input signal for c1")) return New("fm").WithComponents(c1, c2) }, @@ -916,7 +916,7 @@ func TestFMesh_drainComponents(t *testing.T) { }, assertions: func(t *testing.T, fm *FMesh) { // As c1 is waiting for inputs it's outputs must not be flushed - assert.False(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignals()) + assert.False(t, fm.Components().ByName("c2").InputByName("i1").HasSignals()) // The inputs must NOT be cleared assert.True(t, fm.Components().ByName("c1").Inputs().AnyHasSignals()) diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index c50a919..c19dc8c 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -25,7 +25,7 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - num, err := inputs.ByName("num").Buffer().FirstPayload() + num, err := inputs.ByName("num").FirstSignalPayload() if err != nil { return err } @@ -38,7 +38,7 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - num, err := inputs.ByName("num").Buffer().FirstPayload() + num, err := inputs.ByName("num").FirstSignalPayload() if err != nil { return err } @@ -46,7 +46,7 @@ func Test_Math(t *testing.T) { return nil }) - c1.Outputs().ByName("res").PipeTo(c2.Inputs().ByName("num")) + c1.OutputByName("res").PipeTo(c2.InputByName("num")) return fmesh.New("fm").WithComponents(c1, c2).WithConfig(fmesh.Config{ ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, @@ -54,13 +54,13 @@ func Test_Math(t *testing.T) { }) }, setInputs: func(fm *fmesh.FMesh) { - fm.Components().ByName("c1").Inputs().ByName("num").PutSignals(signal.New(32)) + fm.Components().ByName("c1").InputByName("num").PutSignals(signal.New(32)) }, assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { assert.NoError(t, err) assert.Len(t, cycles, 3) - resultSignals := fm.Components().ByName("c2").Outputs().ByName("res").Buffer() + resultSignals := fm.Components().ByName("c2").OutputByName("res").Buffer() sig, err := resultSignals.FirstPayload() assert.NoError(t, err) assert.Len(t, resultSignals.SignalsOrNil(), 1) diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index 24580bd..23044bc 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -59,30 +59,30 @@ func Test_Fan(t *testing.T) { }), ) - fm.Components().ByName("producer").Outputs().ByName("o1").PipeTo( - fm.Components().ByName("consumer1").Inputs().ByName("i1"), - fm.Components().ByName("consumer2").Inputs().ByName("i1"), - fm.Components().ByName("consumer3").Inputs().ByName("i1")) + fm.Components().ByName("producer").OutputByName("o1").PipeTo( + fm.Components().ByName("consumer1").InputByName("i1"), + fm.Components().ByName("consumer2").InputByName("i1"), + fm.Components().ByName("consumer3").InputByName("i1")) return fm }, setInputs: func(fm *fmesh.FMesh) { //Fire the mesh - fm.Components().ByName("producer").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer").InputByName("start").PutSignals(signal.New(struct{}{})) }, assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { //All consumers received a signal c1, c2, c3 := fm.Components().ByName("consumer1"), fm.Components().ByName("consumer2"), fm.Components().ByName("consumer3") - assert.True(t, c1.Outputs().ByName("o1").HasSignals()) - assert.True(t, c2.Outputs().ByName("o1").HasSignals()) - assert.True(t, c3.Outputs().ByName("o1").HasSignals()) + assert.True(t, c1.OutputByName("o1").HasSignals()) + assert.True(t, c2.OutputByName("o1").HasSignals()) + assert.True(t, c3.OutputByName("o1").HasSignals()) //All 3 signals are the same (literally the same address in memory) - sig1, err := c1.Outputs().ByName("o1").Buffer().FirstPayload() + sig1, err := c1.OutputByName("o1").FirstSignalPayload() assert.NoError(t, err) - sig2, err := c2.Outputs().ByName("o1").Buffer().FirstPayload() + sig2, err := c2.OutputByName("o1").FirstSignalPayload() assert.NoError(t, err) - sig3, err := c3.Outputs().ByName("o1").Buffer().FirstPayload() + sig3, err := c3.OutputByName("o1").FirstSignalPayload() assert.NoError(t, err) assert.Equal(t, sig1, sig2) assert.Equal(t, sig2, sig3) @@ -123,24 +123,24 @@ func Test_Fan(t *testing.T) { return nil }) - producer1.Outputs().ByName("o1").PipeTo(consumer.Inputs().ByName("i1")) - producer2.Outputs().ByName("o1").PipeTo(consumer.Inputs().ByName("i1")) - producer3.Outputs().ByName("o1").PipeTo(consumer.Inputs().ByName("i1")) + producer1.OutputByName("o1").PipeTo(consumer.InputByName("i1")) + producer2.OutputByName("o1").PipeTo(consumer.InputByName("i1")) + producer3.OutputByName("o1").PipeTo(consumer.InputByName("i1")) return fmesh.New("multiplexer").WithComponents(producer1, producer2, producer3, consumer) }, setInputs: func(fm *fmesh.FMesh) { - fm.Components().ByName("producer1").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) - fm.Components().ByName("producer2").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) - fm.Components().ByName("producer3").Inputs().ByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer1").InputByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer2").InputByName("start").PutSignals(signal.New(struct{}{})) + fm.Components().ByName("producer3").InputByName("start").PutSignals(signal.New(struct{}{})) }, assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { assert.NoError(t, err) //Consumer received a signal - assert.True(t, fm.Components().ByName("consumer").Outputs().ByName("o1").HasSignals()) + assert.True(t, fm.Components().ByName("consumer").OutputByName("o1").HasSignals()) //The signal is combined and consist of 3 payloads - resultSignals := fm.Components().ByName("consumer").Outputs().ByName("o1").Buffer() + resultSignals := fm.Components().ByName("consumer").OutputByName("o1").Buffer() assert.Len(t, resultSignals.SignalsOrNil(), 3) //And they are all different diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index c6d9c1f..d1235f7 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -26,7 +26,7 @@ func Test_WaitingForInputs(t *testing.T) { WithInputs("i1"). WithOutputs("o1"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - inputNum, err := inputs.ByName("i1").Buffer().FirstPayload() + inputNum, err := inputs.ByName("i1").FirstSignalPayload() if err != nil { return err } @@ -50,12 +50,12 @@ func Test_WaitingForInputs(t *testing.T) { return component.NewErrWaitForInputs(true) } - inputNum1, err := inputs.ByName("i1").Buffer().FirstPayload() + inputNum1, err := inputs.ByName("i1").FirstSignalPayload() if err != nil { return err } - inputNum2, err := inputs.ByName("i2").Buffer().FirstPayload() + inputNum2, err := inputs.ByName("i2").FirstSignalPayload() if err != nil { return err } @@ -65,15 +65,15 @@ func Test_WaitingForInputs(t *testing.T) { }) //This chain consist of 3 components: d1->d2->d3 - d1.Outputs().ByName("o1").PipeTo(d2.Inputs().ByName("i1")) - d2.Outputs().ByName("o1").PipeTo(d3.Inputs().ByName("i1")) + d1.OutputByName("o1").PipeTo(d2.InputByName("i1")) + d2.OutputByName("o1").PipeTo(d3.InputByName("i1")) //This chain has only 2: d4->d5 - d4.Outputs().ByName("o1").PipeTo(d5.Inputs().ByName("i1")) + d4.OutputByName("o1").PipeTo(d5.InputByName("i1")) //Both chains go into summator - d3.Outputs().ByName("o1").PipeTo(s.Inputs().ByName("i1")) - d5.Outputs().ByName("o1").PipeTo(s.Inputs().ByName("i2")) + d3.OutputByName("o1").PipeTo(s.InputByName("i1")) + d5.OutputByName("o1").PipeTo(s.InputByName("i2")) return fmesh.New("fm"). WithComponents(d1, d2, d3, d4, d5, s). @@ -85,12 +85,12 @@ func Test_WaitingForInputs(t *testing.T) { }, setInputs: func(fm *fmesh.FMesh) { //Put 1 signal to each chain so they start in the same cycle - fm.Components().ByName("d1").Inputs().ByName("i1").PutSignals(signal.New(1)) - fm.Components().ByName("d4").Inputs().ByName("i1").PutSignals(signal.New(2)) + fm.Components().ByName("d1").InputByName("i1").PutSignals(signal.New(1)) + fm.Components().ByName("d4").InputByName("i1").PutSignals(signal.New(2)) }, assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { assert.NoError(t, err) - result, err := fm.Components().ByName("sum").Outputs().ByName("o1").Buffer().FirstPayload() + result, err := fm.Components().ByName("sum").OutputByName("o1").FirstSignalPayload() assert.NoError(t, err) assert.Equal(t, 16, result.(int)) }, diff --git a/port/port.go b/port/port.go index 18a0be7..6fd2e14 100644 --- a/port/port.go +++ b/port/port.go @@ -128,7 +128,7 @@ func (p *Port) HasSignals() bool { //@TODO: add logging here return false } - signals, err := p.Buffer().Signals() + signals, err := p.AllSignals() if err != nil { //@TODO: add logging here return false @@ -178,7 +178,7 @@ func (p *Port) WithLabels(labels common.LabelsCollection) *Port { // ForwardSignals copies all buffer from source port to destination port, without clearing the source port func ForwardSignals(source *Port, dest *Port) error { - signals, err := source.Buffer().Signals() + signals, err := source.AllSignals() if err != nil { return err } @@ -194,3 +194,37 @@ func (p *Port) WithChainError(err error) *Port { p.SetChainError(err) return p } + +// FirstSignalPayload is shortcut method +func (p *Port) FirstSignalPayload() (any, error) { + return p.Buffer().FirstPayload() +} + +// FirstSignalPayloadOrNil is shortcut method +func (p *Port) FirstSignalPayloadOrNil() any { + return p.Buffer().First().PayloadOrNil() +} + +// FirstSignalPayloadOrDefault is shortcut method +func (p *Port) FirstSignalPayloadOrDefault(defaultPayload any) any { + return p.Buffer().First().PayloadOrDefault(defaultPayload) +} + +// AllSignals is shortcut method +func (p *Port) AllSignals() ([]*signal.Signal, error) { + return p.Buffer().Signals() +} + +// AllSignalsOrNil is shortcut method +func (p *Port) AllSignalsOrNil() []*signal.Signal { + return p.Buffer().SignalsOrNil() +} + +func (p *Port) AllSignalsOrDefault(defaultSignals []*signal.Signal) []*signal.Signal { + return p.Buffer().SignalsOrDefault(defaultSignals) +} + +// AllSignalsPayloads is shortcut method +func (p *Port) AllSignalsPayloads() ([]any, error) { + return p.Buffer().AllPayloads() +} diff --git a/port/port_test.go b/port/port_test.go index 15272dc..f22be15 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -171,7 +171,7 @@ func TestPort_PutSignals(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { portAfter := tt.port.PutSignals(tt.args.signals...) - assert.ElementsMatch(t, tt.signalsAfter, portAfter.Buffer().SignalsOrNil()) + assert.ElementsMatch(t, tt.signalsAfter, portAfter.AllSignalsOrNil()) }) } } @@ -266,7 +266,7 @@ func TestPort_Flush(t *testing.T) { for _, destPort := range srcPort.Pipes().PortsOrNil() { assert.True(t, destPort.HasSignals()) assert.Equal(t, destPort.Buffer().Len(), 3) - allPayloads, err := destPort.Buffer().AllPayloads() + allPayloads, err := destPort.AllSignalsPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) assert.Contains(t, allPayloads, 2) @@ -286,7 +286,7 @@ func TestPort_Flush(t *testing.T) { for _, destPort := range srcPort.Pipes().PortsOrNil() { assert.True(t, destPort.HasSignals()) assert.Equal(t, destPort.Buffer().Len(), 6) - allPayloads, err := destPort.Buffer().AllPayloads() + allPayloads, err := destPort.AllSignalsPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) assert.Contains(t, allPayloads, 2) diff --git a/signal/signal.go b/signal/signal.go index 591e65d..44bec3a 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -30,10 +30,10 @@ func (s *Signal) PayloadOrNil() any { } // PayloadOrDefault returns payload or provided default value in case of error -func (s *Signal) PayloadOrDefault(defaultValue any) any { +func (s *Signal) PayloadOrDefault(defaultPayload any) any { payload, err := s.Payload() if err != nil { - return defaultValue + return defaultPayload } return payload } From 72e02b3e6f4c8d56d2c2f0a7c4b91d209e8eef17 Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 21 Oct 2024 00:35:49 +0300 Subject: [PATCH 23/41] Make fmesh chainable --- component/collection.go | 65 +++++++++++-- component/collection_test.go | 18 ++-- export/dot/dot.go | 19 ++-- export/dot/dot_test.go | 10 -- fmesh.go | 94 +++++++++++++++--- fmesh_test.go | 96 ++++++++++++------- integration_tests/computation/math_test.go | 10 +- .../ports/waiting_for_inputs_test.go | 17 +--- port/collection.go | 22 ++--- port/port.go | 6 +- port/port_test.go | 4 +- signal/group.go | 18 ++-- signal/group_test.go | 26 ++--- 13 files changed, 260 insertions(+), 145 deletions(-) diff --git a/component/collection.go b/component/collection.go index af30267..0f2d77a 100644 --- a/component/collection.go +++ b/component/collection.go @@ -1,22 +1,73 @@ package component +import ( + "errors" + "github.com/hovsep/fmesh/common" +) + +type ComponentsMap map[string]*Component + // Collection is a collection of components with useful methods -type Collection map[string]*Component +type Collection struct { + *common.Chainable + components ComponentsMap +} // NewCollection creates empty collection -func NewCollection() Collection { - return make(Collection) +func NewCollection() *Collection { + return &Collection{ + Chainable: common.NewChainable(), + components: make(ComponentsMap), + } } // ByName returns a component by its name -func (collection Collection) ByName(name string) *Component { - return collection[name] +func (collection *Collection) ByName(name string) *Component { + if collection.HasChainError() { + return nil + } + + component, ok := collection.components[name] + + if !ok { + collection.SetChainError(errors.New("component not found")) + return nil + } + + return component } // With adds components and returns the collection -func (collection Collection) With(components ...*Component) Collection { +func (collection *Collection) With(components ...*Component) *Collection { + if collection.HasChainError() { + return collection + } + for _, component := range components { - collection[component.Name()] = component + collection.components[component.Name()] = component + + if component.HasChainError() { + return collection.WithChainError(component.ChainError()) + } } + + return collection +} + +// WithChainError returns group with error +func (collection *Collection) WithChainError(err error) *Collection { + collection.SetChainError(err) return collection } + +// Len returns number of ports in collection +func (collection *Collection) Len() int { + return len(collection.components) +} + +func (collection *Collection) Components() (ComponentsMap, error) { + if collection.HasChainError() { + return nil, collection.ChainError() + } + return collection.components, nil +} diff --git a/component/collection_test.go b/component/collection_test.go index f891ac1..0be13d3 100644 --- a/component/collection_test.go +++ b/component/collection_test.go @@ -11,7 +11,7 @@ func TestCollection_ByName(t *testing.T) { } tests := []struct { name string - components Collection + components *Collection args args want *Component }{ @@ -45,9 +45,9 @@ func TestCollection_With(t *testing.T) { } tests := []struct { name string - collection Collection + collection *Collection args args - assertions func(t *testing.T, collection Collection) + assertions func(t *testing.T, collection *Collection) }{ { name: "adding nothing to empty collection", @@ -55,8 +55,8 @@ func TestCollection_With(t *testing.T) { args: args{ components: nil, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 0) + assertions: func(t *testing.T, collection *Collection) { + assert.Zero(t, collection.Len()) }, }, { @@ -65,8 +65,8 @@ func TestCollection_With(t *testing.T) { args: args{ components: []*Component{New("c1"), New("c2")}, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 2) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, 2, collection.Len()) assert.NotNil(t, collection.ByName("c1")) assert.NotNil(t, collection.ByName("c2")) assert.Nil(t, collection.ByName("c999")) @@ -78,8 +78,8 @@ func TestCollection_With(t *testing.T) { args: args{ components: []*Component{New("c3"), New("c4")}, }, - assertions: func(t *testing.T, collection Collection) { - assert.Len(t, collection, 4) + assertions: func(t *testing.T, collection *Collection) { + assert.Equal(t, 4, collection.Len()) assert.NotNil(t, collection.ByName("c1")) assert.NotNil(t, collection.ByName("c2")) assert.NotNil(t, collection.ByName("c3")) diff --git a/export/dot/dot.go b/export/dot/dot.go index fb02f26..daa8ce6 100644 --- a/export/dot/dot.go +++ b/export/dot/dot.go @@ -42,7 +42,7 @@ func NewDotExporterWithConfig(config *Config) export.Exporter { // Export returns the f-mesh as DOT-graph func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { - if len(fm.Components()) == 0 { + if fm.Components().Len() == 0 { return nil, nil } @@ -60,7 +60,7 @@ func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { // ExportWithCycles returns multiple graphs showing the state of the given f-mesh in each activation cycle func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Collection) ([][]byte, error) { - if len(fm.Components()) == 0 { + if fm.Components().Len() == 0 { return nil, nil } @@ -93,12 +93,17 @@ func (d *dotExporter) buildGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle) return nil, fmt.Errorf("failed to get main graph: %w", err) } - err = d.addComponents(mainGraph, fm.Components(), activationCycle) + components, err := fm.Components().Components() + if err != nil { + return nil, fmt.Errorf("failed to get components: %w", err) + } + + err = d.addComponents(mainGraph, components, activationCycle) if err != nil { return nil, fmt.Errorf("failed to add components: %w", err) } - err = d.addPipes(mainGraph, fm.Components()) + err = d.addPipes(mainGraph, components) if err != nil { return nil, fmt.Errorf("failed to add pipes: %w", err) } @@ -120,7 +125,7 @@ func (d *dotExporter) getMainGraph(fm *fmesh.FMesh, activationCycle *cycle.Cycle } // addPipes adds pipes representation to the graph -func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Collection) error { +func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.ComponentsMap) error { for _, c := range components { srcPorts, err := c.Outputs().Ports() if err != nil { @@ -156,7 +161,7 @@ func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Colle } // addComponents adds components representation to the graph -func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent.Collection, activationCycle *cycle.Cycle) error { +func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent.ComponentsMap, activationCycle *cycle.Cycle) error { for _, c := range components { // Component var activationResult *fmeshcomponent.ActivationResult @@ -260,7 +265,7 @@ func (d *dotExporter) addLegend(graph *dot.Graph, fm *fmesh.FMesh, activationCyc subgraph.Attr("label", "Legend:") legendData := make(map[string]any) - legendData["meshDescription"] = fmt.Sprintf("This mesh consist of %d components", len(fm.Components())) + legendData["meshDescription"] = fmt.Sprintf("This mesh consist of %d components", fm.Components().Len()) if fm.Description() != "" { legendData["meshDescription"] = fm.Description() } diff --git a/export/dot/dot_test.go b/export/dot/dot_test.go index 238c661..9cf92dd 100644 --- a/export/dot/dot_test.go +++ b/export/dot/dot_test.go @@ -85,16 +85,6 @@ func Test_dotExporter_ExportWithCycles(t *testing.T) { args args assertions func(t *testing.T, data [][]byte, err error) }{ - { - name: "empty f-mesh", - args: args{ - fm: fmesh.New("fm"), - }, - assertions: func(t *testing.T, data [][]byte, err error) { - assert.NoError(t, err) - assert.Empty(t, data) - }, - }, { name: "happy path", args: args{ diff --git a/fmesh.go b/fmesh.go index 2bd4165..1d07671 100644 --- a/fmesh.go +++ b/fmesh.go @@ -1,6 +1,8 @@ package fmesh import ( + "errors" + "fmt" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" @@ -25,54 +27,85 @@ var defaultConfig = Config{ type FMesh struct { common.NamedEntity common.DescribedEntity - components component.Collection + *common.Chainable + components *component.Collection config Config } // New creates a new f-mesh func New(name string) *FMesh { return &FMesh{ - NamedEntity: common.NewNamedEntity(name), - components: component.NewCollection(), - config: defaultConfig, + NamedEntity: common.NewNamedEntity(name), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + config: defaultConfig, } } -func (fm *FMesh) Components() component.Collection { +// Components getter +func (fm *FMesh) Components() *component.Collection { + if fm.HasChainError() { + return nil + } return fm.components } // WithDescription sets a description func (fm *FMesh) WithDescription(description string) *FMesh { + if fm.HasChainError() { + return fm + } + fm.DescribedEntity = common.NewDescribedEntity(description) return fm } // WithComponents adds components to f-mesh func (fm *FMesh) WithComponents(components ...*component.Component) *FMesh { + if fm.HasChainError() { + return fm + } + for _, c := range components { fm.components = fm.components.With(c) + if c.HasChainError() { + return fm.WithChainError(c.ChainError()) + } } return fm } // WithConfig sets the configuration and returns the f-mesh func (fm *FMesh) WithConfig(config Config) *FMesh { + if fm.HasChainError() { + return fm + } + fm.config = config return fm } // runCycle runs one activation cycle (tries to activate ready components) -func (fm *FMesh) runCycle() *cycle.Cycle { - newCycle := cycle.New() +func (fm *FMesh) runCycle() (*cycle.Cycle, error) { + if fm.HasChainError() { + return nil, fm.ChainError() + } - if len(fm.components) == 0 { - return newCycle + if fm.Components().Len() == 0 { + return nil, errors.New("failed to run cycle: no components found") } + newCycle := cycle.New() + var wg sync.WaitGroup - for _, c := range fm.components { + components, err := fm.Components().Components() + if err != nil { + return nil, fmt.Errorf("failed to run cycle: %w", err) + } + + for _, c := range components { wg.Add(1) go func(component *component.Component, cycle *cycle.Cycle) { @@ -85,12 +118,21 @@ func (fm *FMesh) runCycle() *cycle.Cycle { } wg.Wait() - return newCycle + return newCycle, nil } // DrainComponents drains the data from activated components -func (fm *FMesh) drainComponents(cycle *cycle.Cycle) { - for _, c := range fm.Components() { +func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { + if fm.HasChainError() { + return fm.ChainError() + } + + components, err := fm.Components().Components() + if err != nil { + return fmt.Errorf("failed to drain components: %w", err) + } + + for _, c := range components { activationResult := cycle.ActivationResults().ByComponentName(c.Name()) if !activationResult.Activated() { @@ -118,14 +160,23 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) { c.ClearInputs() } } + return nil } // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) func (fm *FMesh) Run() (cycle.Collection, error) { + if fm.HasChainError() { + return nil, fm.ChainError() + } + allCycles := cycle.NewCollection() cycleNumber := 0 for { - cycleResult := fm.runCycle().WithNumber(cycleNumber) + cycleResult, err := fm.runCycle() + if err != nil { + return nil, err + } + cycleResult.WithNumber(cycleNumber) allCycles = allCycles.With(cycleResult) mustStop, err := fm.mustStop(cycleResult) @@ -133,12 +184,19 @@ func (fm *FMesh) Run() (cycle.Collection, error) { return allCycles, err } - fm.drainComponents(cycleResult) + err = fm.drainComponents(cycleResult) + if err != nil { + return nil, err + } cycleNumber++ } } func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error) { + if fm.HasChainError() { + return false, fm.ChainError() + } + if (fm.config.CyclesLimit > 0) && (cycleResult.Number() > fm.config.CyclesLimit) { return true, ErrReachedMaxAllowedCycles } @@ -166,3 +224,9 @@ func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error) { return true, ErrUnsupportedErrorHandlingStrategy } } + +// WithChainError returns f-mesh with error +func (c *FMesh) WithChainError(err error) *FMesh { + c.SetChainError(err) + return c +} diff --git a/fmesh_test.go b/fmesh_test.go index 48b8e2d..d399f67 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -26,8 +26,11 @@ func TestNew(t *testing.T) { name: "", }, want: &FMesh{ - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity(""), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + config: defaultConfig, }, }, { @@ -36,9 +39,11 @@ func TestNew(t *testing.T) { name: "fm1", }, want: &FMesh{ - NamedEntity: common.NewNamedEntity("fm1"), - components: component.Collection{}, - config: defaultConfig, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), + config: defaultConfig, }, }, } @@ -68,7 +73,8 @@ func TestFMesh_WithDescription(t *testing.T) { want: &FMesh{ NamedEntity: common.NewNamedEntity("fm1"), DescribedEntity: common.NewDescribedEntity(""), - components: component.Collection{}, + Chainable: common.NewChainable(), + components: component.NewCollection(), config: defaultConfig, }, }, @@ -81,7 +87,8 @@ func TestFMesh_WithDescription(t *testing.T) { want: &FMesh{ NamedEntity: common.NewNamedEntity("fm1"), DescribedEntity: common.NewDescribedEntity("descr"), - components: component.Collection{}, + Chainable: common.NewChainable(), + components: component.NewCollection(), config: defaultConfig, }, }, @@ -113,8 +120,10 @@ func TestFMesh_WithConfig(t *testing.T) { }, }, want: &FMesh{ - NamedEntity: common.NewNamedEntity("fm1"), - components: component.Collection{}, + NamedEntity: common.NewNamedEntity("fm1"), + DescribedEntity: common.NewDescribedEntity(""), + Chainable: common.NewChainable(), + components: component.NewCollection(), config: Config{ ErrorHandlingStrategy: IgnoreAll, CyclesLimit: 9999, @@ -134,10 +143,10 @@ func TestFMesh_WithComponents(t *testing.T) { components []*component.Component } tests := []struct { - name string - fm *FMesh - args args - wantComponents component.Collection + name string + fm *FMesh + args args + assertions func(t *testing.T, fm *FMesh) }{ { name: "no components", @@ -145,7 +154,9 @@ func TestFMesh_WithComponents(t *testing.T) { args: args{ components: nil, }, - wantComponents: component.Collection{}, + assertions: func(t *testing.T, fm *FMesh) { + assert.Zero(t, fm.Components().Len()) + }, }, { name: "with single component", @@ -155,8 +166,9 @@ func TestFMesh_WithComponents(t *testing.T) { component.New("c1"), }, }, - wantComponents: component.Collection{ - "c1": component.New("c1"), + assertions: func(t *testing.T, fm *FMesh) { + assert.Equal(t, fm.Components().Len(), 1) + assert.NotNil(t, fm.Components().ByName("c1")) }, }, { @@ -168,9 +180,10 @@ func TestFMesh_WithComponents(t *testing.T) { component.New("c2"), }, }, - wantComponents: component.Collection{ - "c1": component.New("c1"), - "c2": component.New("c2"), + assertions: func(t *testing.T, fm *FMesh) { + assert.Equal(t, fm.Components().Len(), 2) + assert.NotNil(t, fm.Components().ByName("c1")) + assert.NotNil(t, fm.Components().ByName("c2")) }, }, { @@ -180,20 +193,25 @@ func TestFMesh_WithComponents(t *testing.T) { components: []*component.Component{ component.New("c1").WithDescription("descr1"), component.New("c2").WithDescription("descr2"), - component.New("c2").WithDescription("descr3"), //This will overwrite the previous one + component.New("c2").WithDescription("new descr for c2"), //This will overwrite the previous one component.New("c4").WithDescription("descr4"), }, }, - wantComponents: component.Collection{ - "c1": component.New("c1").WithDescription("descr1"), - "c2": component.New("c2").WithDescription("descr3"), - "c4": component.New("c4").WithDescription("descr4"), + assertions: func(t *testing.T, fm *FMesh) { + assert.Equal(t, fm.Components().Len(), 3) + assert.NotNil(t, fm.Components().ByName("c1")) + assert.NotNil(t, fm.Components().ByName("c2")) + assert.NotNil(t, fm.Components().ByName("c4")) + assert.Equal(t, "new descr for c2", fm.Components().ByName("c2").Description()) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.wantComponents, tt.fm.WithComponents(tt.args.components...).Components()) + fmAfter := tt.fm.WithComponents(tt.args.components...) + if tt.assertions != nil { + tt.assertions(t, fmAfter) + } }) } } @@ -209,8 +227,8 @@ func TestFMesh_Run(t *testing.T) { { name: "empty mesh stops after first cycle", fm: New("fm"), - want: cycle.NewCollection().With(cycle.New()), - wantErr: false, + want: nil, + wantErr: true, }, { name: "unsupported error handling strategy", @@ -567,15 +585,17 @@ func TestFMesh_Run(t *testing.T) { func TestFMesh_runCycle(t *testing.T) { tests := []struct { - name string - fm *FMesh - initFM func(fm *FMesh) - want *cycle.Cycle + name string + fm *FMesh + initFM func(fm *FMesh) + want *cycle.Cycle + wantError bool }{ { - name: "empty mesh", - fm: New("empty mesh"), - want: cycle.New(), + name: "empty mesh", + fm: New("empty mesh"), + want: nil, + wantError: true, }, { name: "all components activated in one cycle (concurrently)", @@ -629,7 +649,13 @@ func TestFMesh_runCycle(t *testing.T) { if tt.initFM != nil { tt.initFM(tt.fm) } - assert.Equal(t, tt.want, tt.fm.runCycle()) + cycleResult, err := tt.fm.runCycle() + if tt.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, cycleResult) + } }) } } diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index c19dc8c..49ce780 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -25,10 +25,7 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - num, err := inputs.ByName("num").FirstSignalPayload() - if err != nil { - return err - } + num := inputs.ByName("num").FirstSignalPayloadOrNil() outputs.ByName("res").PutSignals(signal.New(num.(int) + 2)) return nil }) @@ -38,10 +35,7 @@ func Test_Math(t *testing.T) { WithInputs("num"). WithOutputs("res"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - num, err := inputs.ByName("num").FirstSignalPayload() - if err != nil { - return err - } + num := inputs.ByName("num").FirstSignalPayloadOrDefault(0) outputs.ByName("res").PutSignals(signal.New(num.(int) * 3)) return nil }) diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index d1235f7..a9939f4 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -26,10 +26,8 @@ func Test_WaitingForInputs(t *testing.T) { WithInputs("i1"). WithOutputs("o1"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - inputNum, err := inputs.ByName("i1").FirstSignalPayload() - if err != nil { - return err - } + inputNum := inputs.ByName("i1").FirstSignalPayloadOrDefault(0) + outputs.ByName("o1").PutSignals(signal.New(inputNum.(int) * 2)) return nil }) @@ -50,15 +48,8 @@ func Test_WaitingForInputs(t *testing.T) { return component.NewErrWaitForInputs(true) } - inputNum1, err := inputs.ByName("i1").FirstSignalPayload() - if err != nil { - return err - } - - inputNum2, err := inputs.ByName("i2").FirstSignalPayload() - if err != nil { - return err - } + inputNum1 := inputs.ByName("i1").FirstSignalPayloadOrDefault(0) + inputNum2 := inputs.ByName("i2").FirstSignalPayloadOrDefault(0) outputs.ByName("o1").PutSignals(signal.New(inputNum1.(int) + inputNum2.(int))) return nil diff --git a/port/collection.go b/port/collection.go index dfa8b28..2bc6d7b 100644 --- a/port/collection.go +++ b/port/collection.go @@ -6,19 +6,21 @@ import ( "github.com/hovsep/fmesh/signal" ) +type PortMap map[string]*Port + // Collection is a port collection // indexed by name, hence it can not carry // 2 ports with same name. Optimized for lookups type Collection struct { *common.Chainable - ports map[string]*Port + ports PortMap } // NewCollection creates empty collection func NewCollection() *Collection { return &Collection{ Chainable: common.NewChainable(), - ports: make(map[string]*Port), + ports: make(PortMap), } } @@ -187,19 +189,9 @@ func (collection *Collection) Signals() *signal.Group { return group } -// withPorts sets ports -func (collection *Collection) withPorts(ports map[string]*Port) *Collection { - if collection.HasChainError() { - return collection - } - - collection.ports = ports - return collection -} - // Ports getter // @TODO:maybe better to hide all errors within chainable and ask user to check error ? -func (collection *Collection) Ports() (map[string]*Port, error) { +func (collection *Collection) Ports() (PortMap, error) { if collection.HasChainError() { return nil, collection.ChainError() } @@ -207,12 +199,12 @@ func (collection *Collection) Ports() (map[string]*Port, error) { } // PortsOrNil returns ports or nil in case of any error -func (collection *Collection) PortsOrNil() map[string]*Port { +func (collection *Collection) PortsOrNil() PortMap { return collection.PortsOrDefault(nil) } // PortsOrDefault returns ports or default in case of any error -func (collection *Collection) PortsOrDefault(defaultPorts map[string]*Port) map[string]*Port { +func (collection *Collection) PortsOrDefault(defaultPorts PortMap) PortMap { if collection.HasChainError() { return defaultPorts } diff --git a/port/port.go b/port/port.go index 6fd2e14..8c336a1 100644 --- a/port/port.go +++ b/port/port.go @@ -211,16 +211,16 @@ func (p *Port) FirstSignalPayloadOrDefault(defaultPayload any) any { } // AllSignals is shortcut method -func (p *Port) AllSignals() ([]*signal.Signal, error) { +func (p *Port) AllSignals() (signal.Signals, error) { return p.Buffer().Signals() } // AllSignalsOrNil is shortcut method -func (p *Port) AllSignalsOrNil() []*signal.Signal { +func (p *Port) AllSignalsOrNil() signal.Signals { return p.Buffer().SignalsOrNil() } -func (p *Port) AllSignalsOrDefault(defaultSignals []*signal.Signal) []*signal.Signal { +func (p *Port) AllSignalsOrDefault(defaultSignals signal.Signals) signal.Signals { return p.Buffer().SignalsOrDefault(defaultSignals) } diff --git a/port/port_test.go b/port/port_test.go index f22be15..eec8170 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -119,12 +119,12 @@ func TestPort_PipeTo(t *testing.T) { func TestPort_PutSignals(t *testing.T) { type args struct { - signals []*signal.Signal + signals signal.Signals } tests := []struct { name string port *Port - signalsAfter []*signal.Signal + signalsAfter signal.Signals args args }{ { diff --git a/signal/group.go b/signal/group.go index 2793bcf..4bfdc05 100644 --- a/signal/group.go +++ b/signal/group.go @@ -5,10 +5,12 @@ import ( "github.com/hovsep/fmesh/common" ) +type Signals []*Signal + // Group represents a list of signals type Group struct { *common.Chainable - signals []*Signal + signals Signals } // NewGroup creates empty group @@ -17,7 +19,7 @@ func NewGroup(payloads ...any) *Group { Chainable: common.NewChainable(), } - signals := make([]*Signal, len(payloads)) + signals := make(Signals, len(payloads)) for i, payload := range payloads { signals[i] = New(payload) } @@ -70,7 +72,7 @@ func (group *Group) With(signals ...*Signal) *Group { return group } - newSignals := make([]*Signal, len(group.signals)+len(signals)) + newSignals := make(Signals, len(group.signals)+len(signals)) copy(newSignals, group.signals) for i, sig := range signals { if sig == nil { @@ -94,7 +96,7 @@ func (group *Group) WithPayloads(payloads ...any) *Group { return group } - newSignals := make([]*Signal, len(group.signals)+len(payloads)) + newSignals := make(Signals, len(group.signals)+len(payloads)) copy(newSignals, group.signals) for i, p := range payloads { newSignals[len(group.signals)+i] = New(p) @@ -103,13 +105,13 @@ func (group *Group) WithPayloads(payloads ...any) *Group { } // withSignals sets signals -func (group *Group) withSignals(signals []*Signal) *Group { +func (group *Group) withSignals(signals Signals) *Group { group.signals = signals return group } // Signals getter -func (group *Group) Signals() ([]*Signal, error) { +func (group *Group) Signals() (Signals, error) { if group.HasChainError() { return nil, group.ChainError() } @@ -117,12 +119,12 @@ func (group *Group) Signals() ([]*Signal, error) { } // SignalsOrNil returns signals or nil in case of any error -func (group *Group) SignalsOrNil() []*Signal { +func (group *Group) SignalsOrNil() Signals { return group.SignalsOrDefault(nil) } // SignalsOrDefault returns signals or default in case of any error -func (group *Group) SignalsOrDefault(defaultSignals []*Signal) []*Signal { +func (group *Group) SignalsOrDefault(defaultSignals Signals) Signals { signals, err := group.Signals() if err != nil { return defaultSignals diff --git a/signal/group_test.go b/signal/group_test.go index bb737d3..b3a5253 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -119,7 +119,7 @@ func TestGroup_AllPayloads(t *testing.T) { }, { name: "with error in signal", - group: NewGroup().withSignals([]*Signal{New(33).WithChainError(errors.New("some error in signal"))}), + group: NewGroup().withSignals(Signals{New(33).WithChainError(errors.New("some error in signal"))}), want: nil, wantErrorString: "some error in signal", }, @@ -139,7 +139,7 @@ func TestGroup_AllPayloads(t *testing.T) { func TestGroup_With(t *testing.T) { type args struct { - signals []*Signal + signals Signals } tests := []struct { name string @@ -194,7 +194,7 @@ func TestGroup_With(t *testing.T) { name: "with error in signal", group: NewGroup(1, 2, 3).With(New(44).WithChainError(errors.New("some error in signal"))), args: args{ - signals: []*Signal{New(456)}, + signals: Signals{New(456)}, }, want: NewGroup(1, 2, 3). With(New(44). @@ -306,19 +306,19 @@ func TestGroup_Signals(t *testing.T) { tests := []struct { name string group *Group - want []*Signal + want Signals wantErrorString string }{ { name: "empty group", group: NewGroup(), - want: []*Signal{}, + want: Signals{}, wantErrorString: "", }, { name: "with signals", group: NewGroup(1, nil, 3), - want: []*Signal{New(1), New(nil), New(3)}, + want: Signals{New(1), New(nil), New(3)}, wantErrorString: "", }, { @@ -343,13 +343,13 @@ func TestGroup_Signals(t *testing.T) { func TestGroup_SignalsOrDefault(t *testing.T) { type args struct { - defaultSignals []*Signal + defaultSignals Signals } tests := []struct { name string group *Group args args - want []*Signal + want Signals }{ { name: "empty group", @@ -357,15 +357,15 @@ func TestGroup_SignalsOrDefault(t *testing.T) { args: args{ defaultSignals: nil, }, - want: []*Signal{}, // Empty group has empty slice of signals + want: Signals{}, // Empty group has empty slice of signals }, { name: "with signals", group: NewGroup(1, 2, 3), args: args{ - defaultSignals: []*Signal{New(4), New(5)}, //Default must be ignored + defaultSignals: Signals{New(4), New(5)}, //Default must be ignored }, - want: []*Signal{New(1), New(2), New(3)}, + want: Signals{New(1), New(2), New(3)}, }, { name: "with error in chain and nil default", @@ -379,9 +379,9 @@ func TestGroup_SignalsOrDefault(t *testing.T) { name: "with error in chain and default", group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), args: args{ - defaultSignals: []*Signal{New(4), New(5)}, + defaultSignals: Signals{New(4), New(5)}, }, - want: []*Signal{New(4), New(5)}, + want: Signals{New(4), New(5)}, }, } for _, tt := range tests { From 8048ccaf23d29d5ca907e1cde0e03bf6b1c2950f Mon Sep 17 00:00:00 2001 From: hovsep Date: Thu, 24 Oct 2024 01:13:40 +0300 Subject: [PATCH 24/41] Minor: refactor receiver names --- component/collection.go | 38 ++++++++--------- fmesh.go | 6 +-- port/group.go | 46 ++++++++++----------- signal/group.go | 92 ++++++++++++++++++++--------------------- 4 files changed, 91 insertions(+), 91 deletions(-) diff --git a/component/collection.go b/component/collection.go index 0f2d77a..87c2038 100644 --- a/component/collection.go +++ b/component/collection.go @@ -22,15 +22,15 @@ func NewCollection() *Collection { } // ByName returns a component by its name -func (collection *Collection) ByName(name string) *Component { - if collection.HasChainError() { +func (c *Collection) ByName(name string) *Component { + if c.HasChainError() { return nil } - component, ok := collection.components[name] + component, ok := c.components[name] if !ok { - collection.SetChainError(errors.New("component not found")) + c.SetChainError(errors.New("component not found")) return nil } @@ -38,36 +38,36 @@ func (collection *Collection) ByName(name string) *Component { } // With adds components and returns the collection -func (collection *Collection) With(components ...*Component) *Collection { - if collection.HasChainError() { - return collection +func (c *Collection) With(components ...*Component) *Collection { + if c.HasChainError() { + return c } for _, component := range components { - collection.components[component.Name()] = component + c.components[component.Name()] = component if component.HasChainError() { - return collection.WithChainError(component.ChainError()) + return c.WithChainError(component.ChainError()) } } - return collection + return c } // WithChainError returns group with error -func (collection *Collection) WithChainError(err error) *Collection { - collection.SetChainError(err) - return collection +func (c *Collection) WithChainError(err error) *Collection { + c.SetChainError(err) + return c } // Len returns number of ports in collection -func (collection *Collection) Len() int { - return len(collection.components) +func (c *Collection) Len() int { + return len(c.components) } -func (collection *Collection) Components() (ComponentsMap, error) { - if collection.HasChainError() { - return nil, collection.ChainError() +func (c *Collection) Components() (ComponentsMap, error) { + if c.HasChainError() { + return nil, c.ChainError() } - return collection.components, nil + return c.components, nil } diff --git a/fmesh.go b/fmesh.go index 1d07671..0b265d5 100644 --- a/fmesh.go +++ b/fmesh.go @@ -226,7 +226,7 @@ func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error) { } // WithChainError returns f-mesh with error -func (c *FMesh) WithChainError(err error) *FMesh { - c.SetChainError(err) - return c +func (fm *FMesh) WithChainError(err error) *FMesh { + fm.SetChainError(err) + return fm } diff --git a/port/group.go b/port/group.go index b05af61..588e273 100644 --- a/port/group.go +++ b/port/group.go @@ -42,42 +42,42 @@ func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { } // With adds ports to group -func (group *Group) With(ports ...*Port) *Group { - if group.HasChainError() { - return group +func (g *Group) With(ports ...*Port) *Group { + if g.HasChainError() { + return g } - newPorts := make([]*Port, len(group.ports)+len(ports)) - copy(newPorts, group.ports) + newPorts := make([]*Port, len(g.ports)+len(ports)) + copy(newPorts, g.ports) for i, port := range ports { - newPorts[len(group.ports)+i] = port + newPorts[len(g.ports)+i] = port } - return group.withPorts(newPorts) + return g.withPorts(newPorts) } // withPorts sets ports -func (group *Group) withPorts(ports []*Port) *Group { - group.ports = ports - return group +func (g *Group) withPorts(ports []*Port) *Group { + g.ports = ports + return g } // Ports getter -func (group *Group) Ports() ([]*Port, error) { - if group.HasChainError() { - return nil, group.ChainError() +func (g *Group) Ports() ([]*Port, error) { + if g.HasChainError() { + return nil, g.ChainError() } - return group.ports, nil + return g.ports, nil } // PortsOrNil returns ports or nil in case of any error -func (group *Group) PortsOrNil() []*Port { - return group.PortsOrDefault(nil) +func (g *Group) PortsOrNil() []*Port { + return g.PortsOrDefault(nil) } // PortsOrDefault returns ports or default in case of any error -func (group *Group) PortsOrDefault(defaultPorts []*Port) []*Port { - ports, err := group.Ports() +func (g *Group) PortsOrDefault(defaultPorts []*Port) []*Port { + ports, err := g.Ports() if err != nil { return defaultPorts } @@ -85,12 +85,12 @@ func (group *Group) PortsOrDefault(defaultPorts []*Port) []*Port { } // WithChainError returns group with error -func (group *Group) WithChainError(err error) *Group { - group.SetChainError(err) - return group +func (g *Group) WithChainError(err error) *Group { + g.SetChainError(err) + return g } // Len returns number of ports in group -func (group *Group) Len() int { - return len(group.ports) +func (g *Group) Len() int { + return len(g.ports) } diff --git a/signal/group.go b/signal/group.go index 4bfdc05..7f94e96 100644 --- a/signal/group.go +++ b/signal/group.go @@ -27,36 +27,36 @@ func NewGroup(payloads ...any) *Group { } // First returns the first signal in the group -func (group *Group) First() *Signal { - if group.HasChainError() { - return New(nil).WithChainError(group.ChainError()) +func (g *Group) First() *Signal { + if g.HasChainError() { + return New(nil).WithChainError(g.ChainError()) } - if len(group.signals) == 0 { + if len(g.signals) == 0 { return New(nil).WithChainError(errors.New("group has no signals")) } - return group.signals[0] + return g.signals[0] } // FirstPayload returns the first signal payload -func (group *Group) FirstPayload() (any, error) { - if group.HasChainError() { - return nil, group.ChainError() +func (g *Group) FirstPayload() (any, error) { + if g.HasChainError() { + return nil, g.ChainError() } - return group.First().Payload() + return g.First().Payload() } // AllPayloads returns a slice with all payloads of the all signals in the group -func (group *Group) AllPayloads() ([]any, error) { - if group.HasChainError() { - return nil, group.ChainError() +func (g *Group) AllPayloads() ([]any, error) { + if g.HasChainError() { + return nil, g.ChainError() } - all := make([]any, len(group.signals)) + all := make([]any, len(g.signals)) var err error - for i, sig := range group.signals { + for i, sig := range g.signals { all[i], err = sig.Payload() if err != nil { return nil, err @@ -66,66 +66,66 @@ func (group *Group) AllPayloads() ([]any, error) { } // With returns the group with added signals -func (group *Group) With(signals ...*Signal) *Group { - if group.HasChainError() { +func (g *Group) With(signals ...*Signal) *Group { + if g.HasChainError() { // Do nothing, but propagate error - return group + return g } - newSignals := make(Signals, len(group.signals)+len(signals)) - copy(newSignals, group.signals) + newSignals := make(Signals, len(g.signals)+len(signals)) + copy(newSignals, g.signals) for i, sig := range signals { if sig == nil { - return group.WithChainError(errors.New("signal is nil")) + return g.WithChainError(errors.New("signal is nil")) } if sig.HasChainError() { - return group.WithChainError(sig.ChainError()) + return g.WithChainError(sig.ChainError()) } - newSignals[len(group.signals)+i] = sig + newSignals[len(g.signals)+i] = sig } - return group.withSignals(newSignals) + return g.withSignals(newSignals) } // WithPayloads returns a group with added signals created from provided payloads -func (group *Group) WithPayloads(payloads ...any) *Group { - if group.HasChainError() { +func (g *Group) WithPayloads(payloads ...any) *Group { + if g.HasChainError() { // Do nothing, but propagate error - return group + return g } - newSignals := make(Signals, len(group.signals)+len(payloads)) - copy(newSignals, group.signals) + newSignals := make(Signals, len(g.signals)+len(payloads)) + copy(newSignals, g.signals) for i, p := range payloads { - newSignals[len(group.signals)+i] = New(p) + newSignals[len(g.signals)+i] = New(p) } - return group.withSignals(newSignals) + return g.withSignals(newSignals) } // withSignals sets signals -func (group *Group) withSignals(signals Signals) *Group { - group.signals = signals - return group +func (g *Group) withSignals(signals Signals) *Group { + g.signals = signals + return g } // Signals getter -func (group *Group) Signals() (Signals, error) { - if group.HasChainError() { - return nil, group.ChainError() +func (g *Group) Signals() (Signals, error) { + if g.HasChainError() { + return nil, g.ChainError() } - return group.signals, nil + return g.signals, nil } // SignalsOrNil returns signals or nil in case of any error -func (group *Group) SignalsOrNil() Signals { - return group.SignalsOrDefault(nil) +func (g *Group) SignalsOrNil() Signals { + return g.SignalsOrDefault(nil) } // SignalsOrDefault returns signals or default in case of any error -func (group *Group) SignalsOrDefault(defaultSignals Signals) Signals { - signals, err := group.Signals() +func (g *Group) SignalsOrDefault(defaultSignals Signals) Signals { + signals, err := g.Signals() if err != nil { return defaultSignals } @@ -133,12 +133,12 @@ func (group *Group) SignalsOrDefault(defaultSignals Signals) Signals { } // WithChainError returns group with error -func (group *Group) WithChainError(err error) *Group { - group.SetChainError(err) - return group +func (g *Group) WithChainError(err error) *Group { + g.SetChainError(err) + return g } // Len returns number of signals in group -func (group *Group) Len() int { - return len(group.signals) +func (g *Group) Len() int { + return len(g.signals) } From 46a4a63ceca84dc45d708439082237f2ebdfc403 Mon Sep 17 00:00:00 2001 From: hovsep Date: Thu, 24 Oct 2024 02:47:09 +0300 Subject: [PATCH 25/41] Make Cycle chainable --- component/activation_result.go | 1 + component/collection.go | 3 +- component/component.go | 20 ++- cycle/collection.go | 19 --- cycle/collection_test.go | 64 -------- cycle/cycle.go | 10 +- cycle/group.go | 58 +++++++ cycle/group_test.go | 67 ++++++++ export/dot/dot.go | 2 +- export/exporter.go | 2 +- fmesh.go | 56 +++++-- fmesh_test.go | 24 +-- integration_tests/computation/math_test.go | 4 +- .../error_handling/chainable_api_test.go | 155 ++++++++++++++++++ integration_tests/piping/fan_test.go | 6 +- .../ports/waiting_for_inputs_test.go | 4 +- port/collection.go | 5 +- port/collection_test.go | 3 +- port/errors.go | 7 + port/group.go | 2 +- port/port.go | 3 + signal/errors.go | 8 + signal/group.go | 5 +- signal/group_test.go | 2 +- 24 files changed, 399 insertions(+), 131 deletions(-) delete mode 100644 cycle/collection.go delete mode 100644 cycle/collection_test.go create mode 100644 cycle/group.go create mode 100644 cycle/group_test.go create mode 100644 integration_tests/error_handling/chainable_api_test.go create mode 100644 port/errors.go create mode 100644 signal/errors.go diff --git a/component/activation_result.go b/component/activation_result.go index 30422f7..8ec3619 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -72,6 +72,7 @@ const ( func NewActivationResult(componentName string) *ActivationResult { return &ActivationResult{ componentName: componentName, + Chainable: common.NewChainable(), } } diff --git a/component/collection.go b/component/collection.go index 87c2038..c663d41 100644 --- a/component/collection.go +++ b/component/collection.go @@ -5,6 +5,7 @@ import ( "github.com/hovsep/fmesh/common" ) +// @TODO: make type unexported type ComponentsMap map[string]*Component // Collection is a collection of components with useful methods @@ -24,7 +25,7 @@ func NewCollection() *Collection { // ByName returns a component by its name func (c *Collection) ByName(name string) *Component { if c.HasChainError() { - return nil + return New("").WithChainError(c.ChainError()) } component, ok := c.components[name] diff --git a/component/component.go b/component/component.go index 15a0d13..5fffd7f 100644 --- a/component/component.go +++ b/component/component.go @@ -138,10 +138,12 @@ func (c *Component) OutputByName(name string) *port.Port { // InputByName is shortcut method func (c *Component) InputByName(name string) *port.Port { + if c.HasChainError() { + return port.New("").WithChainError(c.ChainError()) + } inputPort := c.Inputs().ByName(name) if inputPort.HasChainError() { c.SetChainError(inputPort.ChainError()) - return nil } return inputPort } @@ -159,6 +161,22 @@ func (c *Component) hasActivationFunction() bool { // @TODO: hide this method from user // @TODO: can we remove named return ? func (c *Component) MaybeActivate() (activationResult *ActivationResult) { + //Bubble up chain errors from ports + for _, p := range c.Inputs().PortsOrNil() { + if p.HasChainError() { + c.Inputs().SetChainError(p.ChainError()) + c.SetChainError(c.Inputs().ChainError()) + break + } + } + for _, p := range c.Outputs().PortsOrNil() { + if p.HasChainError() { + c.Outputs().SetChainError(p.ChainError()) + c.SetChainError(c.Outputs().ChainError()) + break + } + } + if c.HasChainError() { activationResult = NewActivationResult(c.Name()).WithChainError(c.ChainError()) return diff --git a/cycle/collection.go b/cycle/collection.go deleted file mode 100644 index 1008d8f..0000000 --- a/cycle/collection.go +++ /dev/null @@ -1,19 +0,0 @@ -package cycle - -// Collection contains the results of several activation cycles -type Collection []*Cycle - -// NewCollection creates a collection -func NewCollection() Collection { - return make(Collection, 0) -} - -// With adds cycle results to existing collection -func (collection Collection) With(cycleResults ...*Cycle) Collection { - newCollection := make(Collection, len(collection)+len(cycleResults)) - copy(newCollection, collection) - for i, cycleResult := range cycleResults { - newCollection[len(collection)+i] = cycleResult - } - return newCollection -} diff --git a/cycle/collection_test.go b/cycle/collection_test.go deleted file mode 100644 index 2f4f580..0000000 --- a/cycle/collection_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package cycle - -import ( - "github.com/hovsep/fmesh/component" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestNewCollection(t *testing.T) { - tests := []struct { - name string - want Collection - }{ - { - name: "happy path", - want: Collection{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, NewCollection()) - }) - } -} - -func TestCollection_Add(t *testing.T) { - type args struct { - cycleResults []*Cycle - } - tests := []struct { - name string - cycleResults Collection - args args - want Collection - }{ - { - name: "happy path", - cycleResults: NewCollection(), - args: args{ - cycleResults: []*Cycle{ - New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false)), - New().WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), - }, - }, - want: Collection{ - { - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(false), - }, - }, - { - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(true), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.cycleResults.With(tt.args.cycleResults...)) - }) - } -} diff --git a/cycle/cycle.go b/cycle/cycle.go index 524d575..8a3fffc 100644 --- a/cycle/cycle.go +++ b/cycle/cycle.go @@ -1,13 +1,15 @@ package cycle import ( + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "sync" ) -// Cycle contains the info about given activation cycle +// Cycle contains the info about one activation cycle type Cycle struct { sync.Mutex + *common.Chainable number int activationResults component.ActivationResultCollection } @@ -55,3 +57,9 @@ func (cycle *Cycle) WithNumber(number int) *Cycle { cycle.number = number return cycle } + +// WithChainError returns cycle with error +func (cycle *Cycle) WithChainError(err error) *Cycle { + cycle.SetChainError(err) + return cycle +} diff --git a/cycle/group.go b/cycle/group.go new file mode 100644 index 0000000..3f1bc12 --- /dev/null +++ b/cycle/group.go @@ -0,0 +1,58 @@ +package cycle + +import "github.com/hovsep/fmesh/common" + +// Cycles contains the results of several activation cycles +type Cycles []*Cycle + +type Group struct { + *common.Chainable + cycles Cycles +} + +// NewGroup creates a group of cycles +func NewGroup() *Group { + newGroup := &Group{ + Chainable: common.NewChainable(), + } + cycles := make(Cycles, 0) + return newGroup.withCycles(cycles) +} + +// With adds cycle results to existing collection +func (g *Group) With(cycles ...*Cycle) *Group { + newCycles := make(Cycles, len(g.cycles)+len(cycles)) + copy(newCycles, g.cycles) + for i, c := range cycles { + newCycles[len(g.cycles)+i] = c + } + return g.withCycles(newCycles) +} + +// withSignals sets signals +func (g *Group) withCycles(cycles Cycles) *Group { + g.cycles = cycles + return g +} + +// Cycles getter +func (g *Group) Cycles() (Cycles, error) { + if g.HasChainError() { + return nil, g.ChainError() + } + return g.cycles, nil +} + +// CyclesOrNil returns signals or nil in case of any error +func (g *Group) CyclesOrNil() Cycles { + return g.CyclesOrDefault(nil) +} + +// CyclesOrDefault returns signals or default in case of any error +func (g *Group) CyclesOrDefault(defaultCycles Cycles) Cycles { + signals, err := g.Cycles() + if err != nil { + return defaultCycles + } + return signals +} diff --git a/cycle/group_test.go b/cycle/group_test.go new file mode 100644 index 0000000..41b19cf --- /dev/null +++ b/cycle/group_test.go @@ -0,0 +1,67 @@ +package cycle + +import ( + "github.com/hovsep/fmesh/component" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewGroup(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + group := NewGroup() + assert.NotNil(t, group) + }) +} + +func TestGroup_With(t *testing.T) { + type args struct { + cycles []*Cycle + } + tests := []struct { + name string + group *Group + args args + want *Group + }{ + { + name: "no addition to empty group", + group: NewGroup(), + args: args{ + cycles: nil, + }, + want: NewGroup(), + }, + { + name: "adding to existing group", + group: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), + args: args{ + cycles: nil, + }, + want: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), + }, + { + name: "adding to empty group", + group: NewGroup(), + args: args{ + cycles: []*Cycle{New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))}, + }, + want: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), + }, + { + name: "adding to existing group", + group: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(true))), + args: args{ + cycles: []*Cycle{New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))}, + }, + want: NewGroup().With( + New().WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), + New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false)), + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.group.With(tt.args.cycles...)) + }) + } +} diff --git a/export/dot/dot.go b/export/dot/dot.go index daa8ce6..6a97026 100644 --- a/export/dot/dot.go +++ b/export/dot/dot.go @@ -59,7 +59,7 @@ func (d *dotExporter) Export(fm *fmesh.FMesh) ([]byte, error) { } // ExportWithCycles returns multiple graphs showing the state of the given f-mesh in each activation cycle -func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Collection) ([][]byte, error) { +func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Cycles) ([][]byte, error) { if fm.Components().Len() == 0 { return nil, nil } diff --git a/export/exporter.go b/export/exporter.go index a70499b..8d114e7 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -11,5 +11,5 @@ type Exporter interface { Export(fm *fmesh.FMesh) ([]byte, error) // ExportWithCycles returns the f-mesh state representation in each activation cycle - ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Collection) ([][]byte, error) + ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.Cycles) ([][]byte, error) } diff --git a/fmesh.go b/fmesh.go index 0b265d5..8e6ac99 100644 --- a/fmesh.go +++ b/fmesh.go @@ -46,7 +46,7 @@ func New(name string) *FMesh { // Components getter func (fm *FMesh) Components() *component.Collection { if fm.HasChainError() { - return nil + return component.NewCollection().WithChainError(fm.ChainError()) } return fm.components } @@ -106,6 +106,9 @@ func (fm *FMesh) runCycle() (*cycle.Cycle, error) { } for _, c := range components { + if c.HasChainError() { + fm.SetChainError(c.ChainError()) + } wg.Add(1) go func(component *component.Component, cycle *cycle.Cycle) { @@ -135,6 +138,10 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { for _, c := range components { activationResult := cycle.ActivationResults().ByComponentName(c.Name()) + if activationResult.HasChainError() { + return activationResult.ChainError() + } + if !activationResult.Activated() { // Component did not activate, so it did not create new output signals, hence nothing to drain continue @@ -164,24 +171,42 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { } // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) -func (fm *FMesh) Run() (cycle.Collection, error) { +func (fm *FMesh) Run() (cycle.Cycles, error) { if fm.HasChainError() { return nil, fm.ChainError() } - allCycles := cycle.NewCollection() + allCycles := cycle.NewGroup() cycleNumber := 0 for { cycleResult, err := fm.runCycle() + if err != nil { return nil, err } + + //Bubble up chain errors from activation results + for _, ar := range cycleResult.ActivationResults() { + if ar.HasChainError() { + fm.SetChainError(ar.ChainError()) + break + } + } + cycleResult.WithNumber(cycleNumber) allCycles = allCycles.With(cycleResult) - mustStop, err := fm.mustStop(cycleResult) + mustStop, chainError, stopError := fm.mustStop(cycleResult) + if chainError != nil { + return nil, chainError + } + if mustStop { - return allCycles, err + cycles, err := allCycles.Cycles() + if err != nil { + return nil, err + } + return cycles, stopError } err = fm.drainComponents(cycleResult) @@ -192,36 +217,37 @@ func (fm *FMesh) Run() (cycle.Collection, error) { } } -func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error) { +// mustStop defines when f-mesh must stop after activation cycle +func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error, error) { if fm.HasChainError() { - return false, fm.ChainError() + return false, fm.ChainError(), nil } if (fm.config.CyclesLimit > 0) && (cycleResult.Number() > fm.config.CyclesLimit) { - return true, ErrReachedMaxAllowedCycles + return true, nil, ErrReachedMaxAllowedCycles } //Check if we are done (no components activated during the cycle => all inputs are processed) if !cycleResult.HasActivatedComponents() { - return true, nil + return true, nil, nil } //Check if mesh must stop because of configured error handling strategy switch fm.config.ErrorHandlingStrategy { case StopOnFirstErrorOrPanic: if cycleResult.HasErrors() || cycleResult.HasPanics() { - return true, ErrHitAnErrorOrPanic + return true, nil, ErrHitAnErrorOrPanic } - return false, nil + return false, nil, nil case StopOnFirstPanic: if cycleResult.HasPanics() { - return true, ErrHitAPanic + return true, nil, ErrHitAPanic } - return false, nil + return false, nil, nil case IgnoreAll: - return false, nil + return false, nil, nil default: - return true, ErrUnsupportedErrorHandlingStrategy + return true, nil, ErrUnsupportedErrorHandlingStrategy } } diff --git a/fmesh_test.go b/fmesh_test.go index d399f67..1b4aac4 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -221,7 +221,7 @@ func TestFMesh_Run(t *testing.T) { name string fm *FMesh initFM func(fm *FMesh) - want cycle.Collection + want cycle.Cycles wantErr bool }{ { @@ -250,12 +250,12 @@ func TestFMesh_Run(t *testing.T) { //Fire the mesh fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New("start c1")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( cycle.New(). WithActivationResults(component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK)), - ), + ).CyclesOrNil(), wantErr: true, }, { @@ -274,7 +274,7 @@ func TestFMesh_Run(t *testing.T) { initFM: func(fm *FMesh) { fm.Components().ByName("c1").InputByName("i1").PutSignals(signal.New("start")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( cycle.New(). WithActivationResults( component.NewActivationResult("c1"). @@ -282,7 +282,7 @@ func TestFMesh_Run(t *testing.T) { WithActivationCode(component.ActivationCodeReturnedError). WithError(errors.New("component returned an error: boom")), ), - ), + ).CyclesOrNil(), wantErr: true, }, { @@ -334,7 +334,7 @@ func TestFMesh_Run(t *testing.T) { c1.InputByName("i1").PutSignals(signal.New("start c1")) c3.InputByName("i1").PutSignals(signal.New("start c3")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( cycle.New(). WithActivationResults( component.NewActivationResult("c1"). @@ -382,7 +382,7 @@ func TestFMesh_Run(t *testing.T) { WithActivationCode(component.ActivationCodePanicked). WithError(errors.New("panicked with: no way")), ), - ), + ).CyclesOrNil(), wantErr: true, }, { @@ -447,7 +447,7 @@ func TestFMesh_Run(t *testing.T) { c1.InputByName("i1").PutSignals(signal.New("start c1")) c3.InputByName("i1").PutSignals(signal.New("start c3")) }, - want: cycle.NewCollection().With( + want: cycle.NewGroup().With( //c1 and c3 activated, c3 finishes with error cycle.New(). WithActivationResults( @@ -545,7 +545,7 @@ func TestFMesh_Run(t *testing.T) { SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), ), - ), + ).CyclesOrNil(), wantErr: false, }, } @@ -746,11 +746,11 @@ func TestFMesh_mustStop(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.fmesh.mustStop(tt.args.cycleResult) + got, _, stopErr := tt.fmesh.mustStop(tt.args.cycleResult) if tt.wantErr != nil { - assert.EqualError(t, err, tt.wantErr.Error()) + assert.EqualError(t, stopErr, tt.wantErr.Error()) } else { - assert.NoError(t, err) + assert.NoError(t, stopErr) } assert.Equal(t, tt.want, got) diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index 49ce780..35c784b 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -15,7 +15,7 @@ func Test_Math(t *testing.T) { name string setupFM func() *fmesh.FMesh setInputs func(fm *fmesh.FMesh) - assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) + assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) }{ { name: "add and multiply", @@ -50,7 +50,7 @@ func Test_Math(t *testing.T) { setInputs: func(fm *fmesh.FMesh) { fm.Components().ByName("c1").InputByName("num").PutSignals(signal.New(32)) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { assert.NoError(t, err) assert.Len(t, cycles, 3) diff --git a/integration_tests/error_handling/chainable_api_test.go b/integration_tests/error_handling/chainable_api_test.go new file mode 100644 index 0000000..e85c8ec --- /dev/null +++ b/integration_tests/error_handling/chainable_api_test.go @@ -0,0 +1,155 @@ +package error_handling + +import ( + "errors" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Signal(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "no errors", + test: func(t *testing.T) { + sig := signal.New(123) + _, err := sig.Payload() + assert.False(t, sig.HasChainError()) + assert.NoError(t, err) + + _ = sig.PayloadOrDefault(555) + assert.False(t, sig.HasChainError()) + + _ = sig.PayloadOrNil() + assert.False(t, sig.HasChainError()) + }, + }, + { + name: "error propagated from group to signal", + test: func(t *testing.T) { + emptyGroup := signal.NewGroup() + + sig := emptyGroup.First() + assert.True(t, sig.HasChainError()) + assert.Error(t, sig.ChainError()) + + _, err := sig.Payload() + assert.Error(t, err) + assert.EqualError(t, err, signal.ErrNoSignalsInGroup.Error()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} + +func Test_FMesh(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "no errors", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1").WithInputs("num1", "num2"). + WithOutputs("sum").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }), + ) + + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10)) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.False(t, fm.HasChainError()) + assert.NoError(t, err) + }, + }, + { + name: "error propagated from component", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1"). + WithInputs("num1", "num2"). + WithOutputs("sum"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }). + WithChainError(errors.New("some error in component")), + ) + + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10)) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.True(t, fm.HasChainError()) + assert.Error(t, err) + assert.EqualError(t, err, "some error in component") + }, + }, + { + name: "error propagated from port", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1"). + WithInputs("num1", "num2"). + WithOutputs("sum"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }), + ) + + //Trying to search port by wrong name must lead to error which will bubble up at f-mesh level + fm.Components().ByName("c1").InputByName("num777").PutSignals(signal.New(10)) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.True(t, fm.HasChainError()) + assert.Error(t, err) + assert.EqualError(t, err, "port not found") + }, + }, + { + name: "error propagated from signal", + test: func(t *testing.T) { + fm := fmesh.New("test").WithComponents( + component.New("c1").WithInputs("num1", "num2"). + WithOutputs("sum").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + num1 := inputs.ByName("num1").FirstSignalPayloadOrDefault(0).(int) + num2 := inputs.ByName("num2").FirstSignalPayloadOrDefault(0).(int) + outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) + return nil + }), + ) + + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10).WithChainError(errors.New("some error in input signal"))) + fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) + + _, err := fm.Run() + assert.True(t, fm.HasChainError()) + assert.Error(t, err) + assert.EqualError(t, err, "some error in input signal") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, tt.test) + } +} diff --git a/integration_tests/piping/fan_test.go b/integration_tests/piping/fan_test.go index 23044bc..6ad6126 100644 --- a/integration_tests/piping/fan_test.go +++ b/integration_tests/piping/fan_test.go @@ -17,7 +17,7 @@ func Test_Fan(t *testing.T) { name string setupFM func() *fmesh.FMesh setInputs func(fm *fmesh.FMesh) - assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) + assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) }{ { name: "fan-out (3 pipes from 1 source port)", @@ -70,7 +70,7 @@ func Test_Fan(t *testing.T) { //Fire the mesh fm.Components().ByName("producer").InputByName("start").PutSignals(signal.New(struct{}{})) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { //All consumers received a signal c1, c2, c3 := fm.Components().ByName("consumer1"), fm.Components().ByName("consumer2"), fm.Components().ByName("consumer3") assert.True(t, c1.OutputByName("o1").HasSignals()) @@ -134,7 +134,7 @@ func Test_Fan(t *testing.T) { fm.Components().ByName("producer2").InputByName("start").PutSignals(signal.New(struct{}{})) fm.Components().ByName("producer3").InputByName("start").PutSignals(signal.New(struct{}{})) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { assert.NoError(t, err) //Consumer received a signal assert.True(t, fm.Components().ByName("consumer").OutputByName("o1").HasSignals()) diff --git a/integration_tests/ports/waiting_for_inputs_test.go b/integration_tests/ports/waiting_for_inputs_test.go index a9939f4..424428d 100644 --- a/integration_tests/ports/waiting_for_inputs_test.go +++ b/integration_tests/ports/waiting_for_inputs_test.go @@ -15,7 +15,7 @@ func Test_WaitingForInputs(t *testing.T) { name string setupFM func() *fmesh.FMesh setInputs func(fm *fmesh.FMesh) - assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) + assertions func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) }{ { name: "waiting for longer chain", @@ -79,7 +79,7 @@ func Test_WaitingForInputs(t *testing.T) { fm.Components().ByName("d1").InputByName("i1").PutSignals(signal.New(1)) fm.Components().ByName("d4").InputByName("i1").PutSignals(signal.New(2)) }, - assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Collection, err error) { + assertions: func(t *testing.T, fm *fmesh.FMesh, cycles cycle.Cycles, err error) { assert.NoError(t, err) result, err := fm.Components().ByName("sum").OutputByName("o1").FirstSignalPayload() assert.NoError(t, err) diff --git a/port/collection.go b/port/collection.go index 2bc6d7b..09ab582 100644 --- a/port/collection.go +++ b/port/collection.go @@ -1,11 +1,11 @@ package port import ( - "errors" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) +// @TODO: make type unexported type PortMap map[string]*Port // Collection is a port collection @@ -31,8 +31,7 @@ func (collection *Collection) ByName(name string) *Port { } port, ok := collection.ports[name] if !ok { - collection.SetChainError(errors.New("port not found")) - return nil + return New("").WithChainError(ErrPortNotFoundInCollection) } return port } diff --git a/port/collection_test.go b/port/collection_test.go index c2adf53..f7518a0 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -1,6 +1,7 @@ package port import ( + "errors" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" "testing" @@ -97,7 +98,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "p3", }, - want: nil, + want: New("").WithChainError(errors.New("port not found")), }, } for _, tt := range tests { diff --git a/port/errors.go b/port/errors.go new file mode 100644 index 0000000..f69c5d5 --- /dev/null +++ b/port/errors.go @@ -0,0 +1,7 @@ +package port + +import "errors" + +var ( + ErrPortNotFoundInCollection = errors.New("port not found") +) diff --git a/port/group.go b/port/group.go index 588e273..2a878e8 100644 --- a/port/group.go +++ b/port/group.go @@ -10,7 +10,7 @@ import ( // no lookup methods type Group struct { *common.Chainable - ports []*Port + ports []*Port //@TODO: extract type } // NewGroup creates multiple ports diff --git a/port/port.go b/port/port.go index 8c336a1..2d2223b 100644 --- a/port/port.go +++ b/port/port.go @@ -45,6 +45,9 @@ func (p *Port) Pipes() *Group { // setSignals sets buffer field func (p *Port) setSignals(signals *signal.Group) { + if signals.HasChainError() { + p.SetChainError(signals.ChainError()) + } p.buffer = signals } diff --git a/signal/errors.go b/signal/errors.go new file mode 100644 index 0000000..7091e37 --- /dev/null +++ b/signal/errors.go @@ -0,0 +1,8 @@ +package signal + +import "errors" + +var ( + ErrNoSignalsInGroup = errors.New("group has no signals") + ErrInvalidSignal = errors.New("signal is invalid") +) diff --git a/signal/group.go b/signal/group.go index 7f94e96..7316734 100644 --- a/signal/group.go +++ b/signal/group.go @@ -1,7 +1,6 @@ package signal import ( - "errors" "github.com/hovsep/fmesh/common" ) @@ -33,7 +32,7 @@ func (g *Group) First() *Signal { } if len(g.signals) == 0 { - return New(nil).WithChainError(errors.New("group has no signals")) + return New(nil).WithChainError(ErrNoSignalsInGroup) } return g.signals[0] @@ -76,7 +75,7 @@ func (g *Group) With(signals ...*Signal) *Group { copy(newSignals, g.signals) for i, sig := range signals { if sig == nil { - return g.WithChainError(errors.New("signal is nil")) + return g.WithChainError(ErrInvalidSignal) } if sig.HasChainError() { diff --git a/signal/group_test.go b/signal/group_test.go index b3a5253..f1f0a36 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -188,7 +188,7 @@ func TestGroup_With(t *testing.T) { args: args{ signals: NewGroup(7, nil, 9).SignalsOrNil(), }, - want: NewGroup(1, 2, 3, "valid before invalid").WithChainError(errors.New("signal is nil")), + want: NewGroup(1, 2, 3, "valid before invalid").WithChainError(errors.New("signal is invalid")), }, { name: "with error in signal", From d8693a3934e9e0dfc0d712b9ddc183e6fccfe021 Mon Sep 17 00:00:00 2001 From: hovsep Date: Tue, 29 Oct 2024 01:12:40 +0200 Subject: [PATCH 26/41] Propagate chain error to activation cycle --- cycle/cycle.go | 1 + cycle/cycle_test.go | 53 +++++++------------ cycle/group.go | 5 ++ cycle/group_test.go | 34 +++++++----- fmesh.go | 21 +++++--- .../error_handling/chainable_api_test.go | 4 +- 6 files changed, 62 insertions(+), 56 deletions(-) diff --git a/cycle/cycle.go b/cycle/cycle.go index 8a3fffc..eaf75d2 100644 --- a/cycle/cycle.go +++ b/cycle/cycle.go @@ -17,6 +17,7 @@ type Cycle struct { // New creates a new cycle func New() *Cycle { return &Cycle{ + Chainable: common.NewChainable(), activationResults: component.NewActivationResultCollection(), } } diff --git a/cycle/cycle_test.go b/cycle/cycle_test.go index 88e3f91..bca2854 100644 --- a/cycle/cycle_test.go +++ b/cycle/cycle_test.go @@ -8,22 +8,11 @@ import ( ) func TestNew(t *testing.T) { - tests := []struct { - name string - want *Cycle - }{ - { - name: "happy path", - want: &Cycle{ - activationResults: component.ActivationResultCollection{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, New()) - }) - } + t.Run("happy path", func(t *testing.T) { + cycle := New() + assert.NotNil(t, cycle) + assert.False(t, cycle.HasChainError()) + }) } func TestCycle_ActivationResults(t *testing.T) { @@ -170,10 +159,10 @@ func TestCycle_WithActivationResults(t *testing.T) { activationResults []*component.ActivationResult } tests := []struct { - name string - cycleResult *Cycle - args args - want *Cycle + name string + cycleResult *Cycle + args args + wantActivationResults component.ActivationResultCollection }{ { name: "nothing added", @@ -181,7 +170,7 @@ func TestCycle_WithActivationResults(t *testing.T) { args: args{ activationResults: nil, }, - want: New(), + wantActivationResults: component.NewActivationResultCollection(), }, { name: "adding to empty collection", @@ -192,11 +181,9 @@ func TestCycle_WithActivationResults(t *testing.T) { component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), }, }, - want: &Cycle{ - activationResults: component.ActivationResultCollection{ - "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), - }, + wantActivationResults: component.ActivationResultCollection{ + "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), }, }, { @@ -215,19 +202,17 @@ func TestCycle_WithActivationResults(t *testing.T) { component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), }, }, - want: &Cycle{ - 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), - "c4": component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), - }, + wantActivationResults: 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), + "c4": component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.cycleResult.WithActivationResults(tt.args.activationResults...)) + assert.Equal(t, tt.wantActivationResults, tt.cycleResult.WithActivationResults(tt.args.activationResults...).ActivationResults()) }) } } diff --git a/cycle/group.go b/cycle/group.go index 3f1bc12..5aba75c 100644 --- a/cycle/group.go +++ b/cycle/group.go @@ -56,3 +56,8 @@ func (g *Group) CyclesOrDefault(defaultCycles Cycles) Cycles { } return signals } + +// Len returns number of cycles in group +func (g *Group) Len() int { + return len(g.cycles) +} diff --git a/cycle/group_test.go b/cycle/group_test.go index 41b19cf..3749038 100644 --- a/cycle/group_test.go +++ b/cycle/group_test.go @@ -18,10 +18,10 @@ func TestGroup_With(t *testing.T) { cycles []*Cycle } tests := []struct { - name string - group *Group - args args - want *Group + name string + group *Group + args args + assertions func(t *testing.T, group *Group) }{ { name: "no addition to empty group", @@ -29,15 +29,19 @@ func TestGroup_With(t *testing.T) { args: args{ cycles: nil, }, - want: NewGroup(), + assertions: func(t *testing.T, group *Group) { + assert.Zero(t, group.Len()) + }, }, { - name: "adding to existing group", + name: "adding nothing to existing group", group: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), args: args{ cycles: nil, }, - want: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 1) + }, }, { name: "adding to empty group", @@ -45,7 +49,9 @@ func TestGroup_With(t *testing.T) { args: args{ cycles: []*Cycle{New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))}, }, - want: NewGroup().With(New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))), + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 1) + }, }, { name: "adding to existing group", @@ -53,15 +59,17 @@ func TestGroup_With(t *testing.T) { args: args{ cycles: []*Cycle{New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false))}, }, - want: NewGroup().With( - New().WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), - New().WithActivationResults(component.NewActivationResult("c1").SetActivated(false)), - ), + assertions: func(t *testing.T, group *Group) { + assert.Equal(t, group.Len(), 2) + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.group.With(tt.args.cycles...)) + groupAfter := tt.group.With(tt.args.cycles...) + if tt.assertions != nil { + tt.assertions(t, groupAfter) + } }) } } diff --git a/fmesh.go b/fmesh.go index 8e6ac99..8ff0889 100644 --- a/fmesh.go +++ b/fmesh.go @@ -121,6 +121,15 @@ func (fm *FMesh) runCycle() (*cycle.Cycle, error) { } wg.Wait() + + //Bubble up chain errors from activation results + for _, ar := range newCycle.ActivationResults() { + if ar.HasChainError() { + newCycle.SetChainError(ar.ChainError()) + break + } + } + return newCycle, nil } @@ -185,15 +194,13 @@ func (fm *FMesh) Run() (cycle.Cycles, error) { return nil, err } - //Bubble up chain errors from activation results - for _, ar := range cycleResult.ActivationResults() { - if ar.HasChainError() { - fm.SetChainError(ar.ChainError()) - break - } + cycleResult.WithNumber(cycleNumber) + + if cycleResult.HasChainError() { + fm.SetChainError(cycleResult.ChainError()) + return nil, fmt.Errorf("chain error occurred in cycle #%d : %w", cycleResult.Number(), cycleResult.ChainError()) } - cycleResult.WithNumber(cycleNumber) allCycles = allCycles.With(cycleResult) mustStop, chainError, stopError := fm.mustStop(cycleResult) diff --git a/integration_tests/error_handling/chainable_api_test.go b/integration_tests/error_handling/chainable_api_test.go index e85c8ec..24880ba 100644 --- a/integration_tests/error_handling/chainable_api_test.go +++ b/integration_tests/error_handling/chainable_api_test.go @@ -123,7 +123,7 @@ func Test_FMesh(t *testing.T) { _, err := fm.Run() assert.True(t, fm.HasChainError()) assert.Error(t, err) - assert.EqualError(t, err, "port not found") + assert.EqualError(t, err, "chain error occurred in cycle #0 : port not found") }, }, { @@ -145,7 +145,7 @@ func Test_FMesh(t *testing.T) { _, err := fm.Run() assert.True(t, fm.HasChainError()) assert.Error(t, err) - assert.EqualError(t, err, "some error in input signal") + assert.EqualError(t, err, "chain error occurred in cycle #0 : some error in input signal") }, }, } From e66e41b7004d2971152b34919460c926118f1e22 Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 30 Oct 2024 01:10:08 +0200 Subject: [PATCH 27/41] Increase coverage --- component/component.go | 55 ++++++++------ component/component_test.go | 146 +++++++++++++++++++++++++++++------- port/collection.go | 18 ++--- port/collection_test.go | 46 ++++++++---- port/port.go | 18 ++--- port/port_test.go | 142 +++++++++++++++++++++++++++++------ signal/group_test.go | 3 +- 7 files changed, 320 insertions(+), 108 deletions(-) diff --git a/component/component.go b/component/component.go index 5fffd7f..5833b54 100644 --- a/component/component.go +++ b/component/component.go @@ -42,6 +42,25 @@ func (c *Component) WithDescription(description string) *Component { return c } +// withInputPorts sets input ports collection +func (c *Component) withInputPorts(collection *port.Collection) *Component { + if collection.HasChainError() { + return c.WithChainError(collection.ChainError()) + } + c.inputs = collection + return c +} + +// withOutputPorts sets input ports collection +func (c *Component) withOutputPorts(collection *port.Collection) *Component { + if collection.HasChainError() { + return c.WithChainError(collection.ChainError()) + } + + c.outputs = collection + return c +} + // WithInputs ads input ports func (c *Component) WithInputs(portNames ...string) *Component { if c.HasChainError() { @@ -52,8 +71,7 @@ func (c *Component) WithInputs(portNames ...string) *Component { if err != nil { return c.WithChainError(err) } - c.inputs = c.Inputs().With(ports...) - return c + return c.withInputPorts(c.Inputs().With(ports...)) } // WithOutputs adds output ports @@ -61,12 +79,12 @@ func (c *Component) WithOutputs(portNames ...string) *Component { if c.HasChainError() { return c } + ports, err := port.NewGroup(portNames...).Ports() if err != nil { return c.WithChainError(err) } - c.outputs = c.Outputs().With(ports...) - return c + return c.withOutputPorts(c.Outputs().With(ports...)) } // WithInputsIndexed creates multiple prefixed ports @@ -75,8 +93,7 @@ func (c *Component) WithInputsIndexed(prefix string, startIndex int, endIndex in return c } - c.inputs = c.Inputs().WithIndexed(prefix, startIndex, endIndex) - return c + return c.withInputPorts(c.Inputs().WithIndexed(prefix, startIndex, endIndex)) } // WithOutputsIndexed creates multiple prefixed ports @@ -85,8 +102,7 @@ func (c *Component) WithOutputsIndexed(prefix string, startIndex int, endIndex i return c } - c.outputs = c.Outputs().WithIndexed(prefix, startIndex, endIndex) - return c + return c.withOutputPorts(c.Outputs().WithIndexed(prefix, startIndex, endIndex)) } // WithActivationFunc sets activation function @@ -130,8 +146,7 @@ func (c *Component) Outputs() *port.Collection { func (c *Component) OutputByName(name string) *port.Port { outputPort := c.Outputs().ByName(name) if outputPort.HasChainError() { - c.SetChainError(outputPort.ChainError()) - return nil + return port.New("").WithChainError(outputPort.ChainError()) } return outputPort } @@ -143,7 +158,7 @@ func (c *Component) InputByName(name string) *port.Port { } inputPort := c.Inputs().ByName(name) if inputPort.HasChainError() { - c.SetChainError(inputPort.ChainError()) + return port.New("").WithChainError(inputPort.ChainError()) } return inputPort } @@ -161,20 +176,12 @@ func (c *Component) hasActivationFunction() bool { // @TODO: hide this method from user // @TODO: can we remove named return ? func (c *Component) MaybeActivate() (activationResult *ActivationResult) { - //Bubble up chain errors from ports - for _, p := range c.Inputs().PortsOrNil() { - if p.HasChainError() { - c.Inputs().SetChainError(p.ChainError()) - c.SetChainError(c.Inputs().ChainError()) - break - } + if c.Inputs().HasChainError() { + c.SetChainError(c.Inputs().ChainError()) } - for _, p := range c.Outputs().PortsOrNil() { - if p.HasChainError() { - c.Outputs().SetChainError(p.ChainError()) - c.SetChainError(c.Outputs().ChainError()) - break - } + + if c.Outputs().HasChainError() { + c.SetChainError(c.Outputs().ChainError()) } if c.HasChainError() { diff --git a/component/component_test.go b/component/component_test.go index bdc4130..ae0168a 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -41,44 +41,42 @@ func TestNewComponent(t *testing.T) { } func TestComponent_FlushOutputs(t *testing.T) { - sink := port.New("sink") - - componentWithNoOutputs := New("c1") - componentWithCleanOutputs := New("c1").WithOutputs("o1", "o2") - - componentWithAllOutputsSet := New("c1").WithOutputs("o1", "o2") - componentWithAllOutputsSet.Outputs().ByNames("o1").PutSignals(signal.New(777)) - componentWithAllOutputsSet.Outputs().ByNames("o2").PutSignals(signal.New(888)) - componentWithAllOutputsSet.Outputs().ByNames("o1", "o2").PipeTo(sink) - tests := []struct { - name string - component *Component - destPort *port.Port //Where the component flushes ALL it's inputs - assertions func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) + name string + getComponent func() *Component + assertions func(t *testing.T, componentAfterFlush *Component) }{ { - name: "no outputs", - component: componentWithNoOutputs, - destPort: nil, - assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { + name: "no outputs", + getComponent: func() *Component { + return New("c1") + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { assert.NotNil(t, componentAfterFlush.Outputs()) assert.Zero(t, componentAfterFlush.Outputs().Len()) }, }, { - name: "output has no signal set", - component: componentWithCleanOutputs, - destPort: nil, - assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { + name: "output has no signal set", + getComponent: func() *Component { + return New("c1").WithOutputs("o1", "o2") + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { assert.False(t, componentAfterFlush.Outputs().AnyHasSignals()) }, }, { - name: "happy path", - component: componentWithAllOutputsSet, - destPort: sink, - assertions: func(t *testing.T, componentAfterFlush *Component, destPort *port.Port) { + name: "happy path", + getComponent: func() *Component { + sink := port.New("sink") + c := New("c1").WithOutputs("o1", "o2") + c.Outputs().ByNames("o1").PutSignals(signal.New(777)) + c.Outputs().ByNames("o2").PutSignals(signal.New(888)) + c.Outputs().ByNames("o1", "o2").PipeTo(sink) + return c + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { + destPort := componentAfterFlush.OutputByName("o1").Pipes().PortsOrNil()[0] allPayloads, err := destPort.AllSignalsPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 777) @@ -88,11 +86,25 @@ func TestComponent_FlushOutputs(t *testing.T) { assert.False(t, componentAfterFlush.Outputs().AnyHasSignals()) }, }, + { + name: "with chain error", + getComponent: func() *Component { + sink := port.New("sink") + c := New("c").WithOutputs("o1").WithChainError(errors.New("some error")) + //Lines below are ignored as error immediately propagates up to component level + c.Outputs().ByName("o1").PipeTo(sink) + c.Outputs().ByName("o1").PutSignals(signal.New("signal from component with chain error")) + return c + }, + assertions: func(t *testing.T, componentAfterFlush *Component) { + assert.False(t, componentAfterFlush.OutputByName("o1").HasPipes()) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.component.FlushOutputs() - tt.assertions(t, tt.component, tt.destPort) + componentAfter := tt.getComponent().FlushOutputs() + tt.assertions(t, componentAfter) }) } } @@ -481,6 +493,28 @@ func TestComponent_MaybeActivate(t *testing.T) { err: NewErrWaitForInputs(true), }, }, + { + name: "with chain error from input port", + getComponent: func() *Component { + c := New("c").WithInputs("i1").WithOutputs("o1") + c.Inputs().With(port.New("p").WithChainError(errors.New("some error"))) + return c + }, + wantActivationResult: NewActivationResult("c"). + WithActivationCode(ActivationCodeUndefined). + WithChainError(errors.New("some error")), + }, + { + name: "with chain error from output port", + getComponent: func() *Component { + c := New("c").WithInputs("i1").WithOutputs("o1") + c.Outputs().With(port.New("p").WithChainError(errors.New("some error"))) + return c + }, + wantActivationResult: NewActivationResult("c"). + WithActivationCode(ActivationCodeUndefined). + WithChainError(errors.New("some error")), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -630,3 +664,59 @@ func TestComponent_WithLabels(t *testing.T) { }) } } + +func TestComponent_ShortcutMethods(t *testing.T) { + t.Run("InputByName", func(t *testing.T) { + c := New("c").WithInputs("a", "b", "c") + assert.Equal(t, port.New("b"), c.InputByName("b")) + }) + + t.Run("OutputByName", func(t *testing.T) { + c := New("c").WithOutputs("a", "b", "c") + assert.Equal(t, port.New("b"), c.OutputByName("b")) + }) +} + +func TestComponent_ClearInputs(t *testing.T) { + tests := []struct { + name string + getComponent func() *Component + assertions func(t *testing.T, componentAfter *Component) + }{ + { + name: "no side effects", + getComponent: func() *Component { + return New("c").WithInputs("i1").WithOutputs("o1") + }, + assertions: func(t *testing.T, componentAfter *Component) { + assert.Equal(t, 1, componentAfter.Inputs().Len()) + assert.Equal(t, 1, componentAfter.Outputs().Len()) + assert.False(t, componentAfter.Inputs().AnyHasSignals()) + assert.False(t, componentAfter.Outputs().AnyHasSignals()) + }, + }, + { + name: "only inputs are cleared", + getComponent: func() *Component { + c := New("c").WithInputs("i1").WithOutputs("o1") + c.Inputs().ByName("i1").PutSignals(signal.New(10)) + c.Outputs().ByName("o1").PutSignals(signal.New(20)) + return c + }, + assertions: func(t *testing.T, componentAfter *Component) { + assert.Equal(t, 1, componentAfter.Inputs().Len()) + assert.Equal(t, 1, componentAfter.Outputs().Len()) + assert.False(t, componentAfter.Inputs().AnyHasSignals()) + assert.True(t, componentAfter.Outputs().ByName("o1").HasSignals()) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + componentAfter := tt.getComponent().ClearInputs() + if tt.assertions != nil { + tt.assertions(t, componentAfter) + } + }) + } +} diff --git a/port/collection.go b/port/collection.go index 09ab582..6dde735 100644 --- a/port/collection.go +++ b/port/collection.go @@ -27,10 +27,11 @@ func NewCollection() *Collection { // ByName returns a port by its name func (collection *Collection) ByName(name string) *Port { if collection.HasChainError() { - return nil + return New("").WithChainError(collection.ChainError()) } port, ok := collection.ports[name] if !ok { + collection.SetChainError(ErrPortNotFoundInCollection) return New("").WithChainError(ErrPortNotFoundInCollection) } return port @@ -39,7 +40,7 @@ func (collection *Collection) ByName(name string) *Port { // ByNames returns multiple ports by their names func (collection *Collection) ByNames(names ...string) *Collection { if collection.HasChainError() { - return collection + return NewCollection().WithChainError(collection.ChainError()) } selectedPorts := NewCollection() @@ -84,10 +85,9 @@ func (collection *Collection) AllHaveSignals() bool { } // PutSignals adds buffer to every port in collection -// @TODO: return collection func (collection *Collection) PutSignals(signals ...*signal.Signal) *Collection { if collection.HasChainError() { - return collection + return NewCollection().WithChainError(collection.ChainError()) } for _, p := range collection.ports { @@ -115,7 +115,7 @@ func (collection *Collection) Clear() *Collection { // Flush flushes all ports in collection func (collection *Collection) Flush() *Collection { if collection.HasChainError() { - return collection + return NewCollection().WithChainError(collection.ChainError()) } for _, p := range collection.ports { @@ -144,15 +144,15 @@ func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { // With adds ports to collection and returns it func (collection *Collection) With(ports ...*Port) *Collection { if collection.HasChainError() { - return collection + return NewCollection().WithChainError(collection.ChainError()) } for _, port := range ports { - collection.ports[port.Name()] = port - if port.HasChainError() { return collection.WithChainError(port.ChainError()) } + + collection.ports[port.Name()] = port } return collection @@ -161,7 +161,7 @@ func (collection *Collection) With(ports ...*Port) *Collection { // WithIndexed creates ports with names like "o1","o2","o3" and so on func (collection *Collection) WithIndexed(prefix string, startIndex int, endIndex int) *Collection { if collection.HasChainError() { - return collection + return NewCollection().WithChainError(collection.ChainError()) } indexedPorts, err := NewIndexedGroup(prefix, startIndex, endIndex).Ports() diff --git a/port/collection_test.go b/port/collection_test.go index f7518a0..99db3cb 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -98,7 +98,15 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "p3", }, - want: New("").WithChainError(errors.New("port not found")), + want: New("").WithChainError(ErrPortNotFoundInCollection), + }, + { + name: "with chain error", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithChainError(errors.New("some error")), + args: args{ + name: "p1", + }, + want: New("").WithChainError(errors.New("some error")), }, } for _, tt := range tests { @@ -114,47 +122,55 @@ func TestCollection_ByNames(t *testing.T) { names []string } tests := []struct { - name string - ports *Collection - args args - want *Collection + name string + collection *Collection + args args + want *Collection }{ { - name: "single port found", - ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), + name: "single port found", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p1"}, }, want: NewCollection().With(New("p1")), }, { - name: "multiple ports found", - ports: NewCollection().With(NewGroup("p1", "p2", "p3", "p4").PortsOrNil()...), + name: "multiple ports found", + collection: NewCollection().With(NewGroup("p1", "p2", "p3", "p4").PortsOrNil()...), args: args{ names: []string{"p1", "p2"}, }, want: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), }, { - name: "single port not found", - ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), + name: "single port not found", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p7"}, }, want: NewCollection(), }, { - name: "some ports not found", - ports: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), + name: "some ports not found", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ names: []string{"p1", "p2", "p3"}, }, want: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), }, + { + name: "with chain error", + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithChainError(errors.New("some error")), + args: args{ + names: []string{"p1", "p2"}, + }, + want: NewCollection().WithChainError(errors.New("some error")), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.ports.ByNames(tt.args.names...)) + assert.Equal(t, tt.want, tt.collection.ByNames(tt.args.names...)) }) } } @@ -246,7 +262,7 @@ func TestCollection_Flush(t *testing.T) { assert.False(t, collection.ByName("src").HasSignals()) for _, destPort := range collection.ByName("src").Pipes().PortsOrNil() { assert.Equal(t, destPort.Buffer().Len(), 3) - allPayloads, err := destPort.Buffer().AllPayloads() + allPayloads, err := destPort.AllSignalsPayloads() assert.NoError(t, err) assert.Contains(t, allPayloads, 1) assert.Contains(t, allPayloads, 2) diff --git a/port/port.go b/port/port.go index 2d2223b..5e6652f 100644 --- a/port/port.go +++ b/port/port.go @@ -27,6 +27,7 @@ func New(name string) *Port { } // Buffer getter +// @TODO: maybe we can hide this and return signals to user code func (p *Port) Buffer() *signal.Group { if p.HasChainError() { return p.buffer.WithChainError(p.ChainError()) @@ -43,12 +44,13 @@ func (p *Port) Pipes() *Group { return p.pipes } -// setSignals sets buffer field -func (p *Port) setSignals(signals *signal.Group) { - if signals.HasChainError() { - p.SetChainError(signals.ChainError()) +// withBuffer sets buffer field +func (p *Port) withBuffer(buffer *signal.Group) *Port { + if buffer.HasChainError() { + return p.WithChainError(buffer.ChainError()) } - p.buffer = signals + p.buffer = buffer + return p } // PutSignals adds signals to buffer @@ -57,8 +59,7 @@ func (p *Port) PutSignals(signals ...*signal.Signal) *Port { if p.HasChainError() { return p } - p.setSignals(p.Buffer().With(signals...)) - return p + return p.withBuffer(p.Buffer().With(signals...)) } // WithSignals puts buffer and returns the port @@ -94,8 +95,7 @@ func (p *Port) Clear() *Port { if p.HasChainError() { return p } - p.setSignals(signal.NewGroup()) - return p + return p.withBuffer(signal.NewGroup()) } // Flush pushes buffer to pipes and clears the port diff --git a/port/port_test.go b/port/port_test.go index eec8170..c428784 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -1,6 +1,7 @@ package port import ( + "errors" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -31,14 +32,14 @@ func TestPort_HasSignals(t *testing.T) { } } -func TestPort_Signals(t *testing.T) { +func TestPort_Buffer(t *testing.T) { tests := []struct { name string port *Port want *signal.Group }{ { - name: "no buffer", + name: "empty buffer", port: New("noSignal"), want: signal.NewGroup(), }, @@ -47,6 +48,11 @@ func TestPort_Signals(t *testing.T) { port: New("p").WithSignals(signal.New(123)), want: signal.NewGroup(123), }, + { + name: "with chain error", + port: New("p").WithChainError(errors.New("some error")), + want: signal.NewGroup().WithChainError(errors.New("some error")), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -122,56 +128,90 @@ func TestPort_PutSignals(t *testing.T) { signals signal.Signals } tests := []struct { - name string - port *Port - signalsAfter signal.Signals - args args + name string + port *Port + args args + assertions func(t *testing.T, portAfter *Port) }{ { - name: "single signal to empty port", - port: New("emptyPort"), - signalsAfter: signal.NewGroup(11).SignalsOrNil(), + name: "single signal to empty port", + port: New("emptyPort"), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11), portAfter.Buffer()) + }, args: args{ signals: signal.NewGroup(11).SignalsOrNil(), }, }, { - name: "multiple buffer to empty port", - port: New("p"), - signalsAfter: signal.NewGroup(11, 12).SignalsOrNil(), + name: "multiple buffer to empty port", + port: New("p"), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11, 12), portAfter.Buffer()) + }, args: args{ signals: signal.NewGroup(11, 12).SignalsOrNil(), }, }, { - name: "single signal to port with single signal", - port: New("p").WithSignals(signal.New(11)), - signalsAfter: signal.NewGroup(11, 12).SignalsOrNil(), + name: "single signal to port with single signal", + port: New("p").WithSignals(signal.New(11)), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11, 12), portAfter.Buffer()) + }, args: args{ signals: signal.NewGroup(12).SignalsOrNil(), }, }, { - name: "single buffer to port with multiple buffer", - port: New("p").WithSignalGroups(signal.NewGroup(11, 12)), - signalsAfter: signal.NewGroup(11, 12, 13).SignalsOrNil(), + name: "single buffer to port with multiple buffer", + port: New("p").WithSignalGroups(signal.NewGroup(11, 12)), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(11, 12, 13), portAfter.Buffer()) + }, args: args{ signals: signal.NewGroup(13).SignalsOrNil(), }, }, { - name: "multiple buffer to port with multiple buffer", - port: New("p").WithSignalGroups(signal.NewGroup(55, 66)), - signalsAfter: signal.NewGroup(55, 66, 13, 14).SignalsOrNil(), + name: "multiple buffer to port with multiple buffer", + port: New("p").WithSignalGroups(signal.NewGroup(55, 66)), + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, signal.NewGroup(55, 66, 13, 14), portAfter.Buffer()) + }, args: args{ signals: signal.NewGroup(13, 14).SignalsOrNil(), }, }, + { + name: "chain error propagated from buffer", + port: New("p"), + assertions: func(t *testing.T, portAfter *Port) { + assert.Zero(t, portAfter.Buffer().Len()) + assert.True(t, portAfter.Buffer().HasChainError()) + }, + args: args{ + signals: signal.Signals{signal.New(111).WithChainError(errors.New("some error in signal"))}, + }, + }, + { + name: "with chain error", + port: New("p").WithChainError(errors.New("some error in port")), + args: args{ + signals: signal.Signals{signal.New(123)}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.True(t, portAfter.HasChainError()) + assert.Zero(t, portAfter.Buffer().Len()) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { portAfter := tt.port.PutSignals(tt.args.signals...) - assert.ElementsMatch(t, tt.signalsAfter, portAfter.AllSignalsOrNil()) + if tt.assertions != nil { + tt.assertions(t, portAfter) + } }) } } @@ -339,3 +379,61 @@ func TestPort_WithLabels(t *testing.T) { }) } } + +func TestPort_Pipes(t *testing.T) { + tests := []struct { + name string + port *Port + want *Group + }{ + { + name: "no pipes", + port: New("p"), + want: NewGroup(), + }, + { + name: "with pipes", + port: New("p1").PipeTo(New("p2"), New("p3")), + want: NewGroup("p2", "p3"), + }, + { + name: "with chain error", + port: New("p").WithChainError(errors.New("some error")), + want: NewGroup().WithChainError(errors.New("some error")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.port.Pipes()) + }) + } +} + +func TestPort_ShortcutGetters(t *testing.T) { + t.Run("FirstSignalPayload", func(t *testing.T) { + port := New("p").WithSignalGroups(signal.NewGroup(4, 7, 6, 5)) + payload, err := port.FirstSignalPayload() + assert.NoError(t, err) + assert.Equal(t, 4, payload) + }) + + t.Run("FirstSignalPayloadOrNil", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + assert.Nil(t, port.FirstSignalPayloadOrNil()) + }) + + t.Run("FirstSignalPayloadOrDefault", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + assert.Equal(t, 888, port.FirstSignalPayloadOrDefault(888)) + }) + + t.Run("AllSignalsOrNil", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + assert.Nil(t, port.AllSignalsOrNil()) + }) + + t.Run("AllSignalsOrDefault", func(t *testing.T) { + port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + assert.Equal(t, signal.NewGroup(999).SignalsOrNil(), port.AllSignalsOrDefault(signal.NewGroup(999).SignalsOrNil())) + }) +} diff --git a/signal/group_test.go b/signal/group_test.go index f1f0a36..bd24e12 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -24,6 +24,7 @@ func TestNewGroup(t *testing.T) { signals, err := group.Signals() assert.NoError(t, err) assert.Len(t, signals, 0) + assert.Zero(t, group.Len()) }, }, { @@ -34,7 +35,7 @@ func TestNewGroup(t *testing.T) { assertions: func(t *testing.T, group *Group) { signals, err := group.Signals() assert.NoError(t, err) - assert.Len(t, signals, 3) + assert.Equal(t, group.Len(), 3) assert.Contains(t, signals, New(1)) assert.Contains(t, signals, New(nil)) assert.Contains(t, signals, New(3)) From 34c1a7f2dd919ee31b4eaec377b7adf49e06538f Mon Sep 17 00:00:00 2001 From: hovsep Date: Wed, 30 Oct 2024 01:38:27 +0200 Subject: [PATCH 28/41] Return chainable with error instead of separate error when possible --- component/component.go | 29 +++++++++++++++++++++++++---- fmesh.go | 24 +++++++++--------------- fmesh_test.go | 8 +++++--- port/errors.go | 4 +++- port/group.go | 2 +- port/group_test.go | 2 +- port/port.go | 2 +- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/component/component.go b/component/component.go index 5833b54..97bbc62 100644 --- a/component/component.go +++ b/component/component.go @@ -172,18 +172,39 @@ func (c *Component) hasActivationFunction() bool { return c.f != nil } -// MaybeActivate tries to run the activation function if all required conditions are met -// @TODO: hide this method from user -// @TODO: can we remove named return ? -func (c *Component) MaybeActivate() (activationResult *ActivationResult) { +// propagateChainErrors propagates up all chain errors that might have not been propagated yet +func (c *Component) propagateChainErrors() { if c.Inputs().HasChainError() { c.SetChainError(c.Inputs().ChainError()) + return } if c.Outputs().HasChainError() { c.SetChainError(c.Outputs().ChainError()) + return + } + + for _, p := range c.Inputs().PortsOrNil() { + if p.HasChainError() { + c.SetChainError(p.ChainError()) + return + } } + for _, p := range c.Outputs().PortsOrNil() { + if p.HasChainError() { + c.SetChainError(p.ChainError()) + return + } + } +} + +// MaybeActivate tries to run the activation function if all required conditions are met +// @TODO: hide this method from user +// @TODO: can we remove named return ? +func (c *Component) MaybeActivate() (activationResult *ActivationResult) { + c.propagateChainErrors() + if c.HasChainError() { activationResult = NewActivationResult(c.Name()).WithChainError(c.ChainError()) return diff --git a/fmesh.go b/fmesh.go index 8ff0889..741ec3b 100644 --- a/fmesh.go +++ b/fmesh.go @@ -87,22 +87,22 @@ func (fm *FMesh) WithConfig(config Config) *FMesh { } // runCycle runs one activation cycle (tries to activate ready components) -func (fm *FMesh) runCycle() (*cycle.Cycle, error) { +func (fm *FMesh) runCycle() *cycle.Cycle { + newCycle := cycle.New() + if fm.HasChainError() { - return nil, fm.ChainError() + return newCycle.WithChainError(fm.ChainError()) } if fm.Components().Len() == 0 { - return nil, errors.New("failed to run cycle: no components found") + return newCycle.WithChainError(errors.New("failed to run cycle: no components found")) } - newCycle := cycle.New() - var wg sync.WaitGroup components, err := fm.Components().Components() if err != nil { - return nil, fmt.Errorf("failed to run cycle: %w", err) + return newCycle.WithChainError(fmt.Errorf("failed to run cycle: %w", err)) } for _, c := range components { @@ -130,7 +130,7 @@ func (fm *FMesh) runCycle() (*cycle.Cycle, error) { } } - return newCycle, nil + return newCycle } // DrainComponents drains the data from activated components @@ -188,13 +188,7 @@ func (fm *FMesh) Run() (cycle.Cycles, error) { allCycles := cycle.NewGroup() cycleNumber := 0 for { - cycleResult, err := fm.runCycle() - - if err != nil { - return nil, err - } - - cycleResult.WithNumber(cycleNumber) + cycleResult := fm.runCycle().WithNumber(cycleNumber) if cycleResult.HasChainError() { fm.SetChainError(cycleResult.ChainError()) @@ -216,7 +210,7 @@ func (fm *FMesh) Run() (cycle.Cycles, error) { return cycles, stopError } - err = fm.drainComponents(cycleResult) + err := fm.drainComponents(cycleResult) if err != nil { return nil, err } diff --git a/fmesh_test.go b/fmesh_test.go index 1b4aac4..4457c10 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -649,11 +649,13 @@ func TestFMesh_runCycle(t *testing.T) { if tt.initFM != nil { tt.initFM(tt.fm) } - cycleResult, err := tt.fm.runCycle() + cycleResult := tt.fm.runCycle() if tt.wantError { - assert.Error(t, err) + assert.True(t, cycleResult.HasChainError()) + assert.Error(t, cycleResult.ChainError()) } else { - assert.NoError(t, err) + assert.False(t, cycleResult.HasChainError()) + assert.NoError(t, cycleResult.ChainError()) assert.Equal(t, tt.want, cycleResult) } }) diff --git a/port/errors.go b/port/errors.go index f69c5d5..d444ac8 100644 --- a/port/errors.go +++ b/port/errors.go @@ -3,5 +3,7 @@ package port import "errors" var ( - ErrPortNotFoundInCollection = errors.New("port not found") + ErrPortNotFoundInCollection = errors.New("port not found") + ErrPortNotReadyForFlush = errors.New("port is not ready for flush") + ErrInvalidRangeForIndexedGroup = errors.New("start index can not be greater than end index") ) diff --git a/port/group.go b/port/group.go index 2a878e8..ca8cb71 100644 --- a/port/group.go +++ b/port/group.go @@ -29,7 +29,7 @@ func NewGroup(names ...string) *Group { // NOTE: endIndex is inclusive, e.g. NewIndexedGroup("p", 0, 0) will create one port with name "p0" func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { if startIndex > endIndex { - return nil + return NewGroup().WithChainError(ErrInvalidRangeForIndexedGroup) } ports := make([]*Port, endIndex-startIndex+1) diff --git a/port/group_test.go b/port/group_test.go index a29db44..e987a7a 100644 --- a/port/group_test.go +++ b/port/group_test.go @@ -80,7 +80,7 @@ func TestNewIndexedGroup(t *testing.T) { startIndex: 999, endIndex: 5, }, - want: nil, + want: NewGroup().WithChainError(ErrInvalidRangeForIndexedGroup), }, } for _, tt := range tests { diff --git a/port/port.go b/port/port.go index 5e6652f..a7c5c9f 100644 --- a/port/port.go +++ b/port/port.go @@ -107,7 +107,7 @@ func (p *Port) Flush() *Port { if !p.HasSignals() || !p.HasPipes() { //@TODO maybe better to return explicit errors - return nil + return New("").WithChainError(ErrPortNotReadyForFlush) } pipes, err := p.pipes.Ports() From a0071b58c924ba71ab804e83ba732e60ca219241 Mon Sep 17 00:00:00 2001 From: hovsep Date: Fri, 1 Nov 2024 23:46:07 +0200 Subject: [PATCH 29/41] Extract Ports type --- port/collection_test.go | 4 ++-- port/group.go | 18 ++++++++++-------- port/group_test.go | 6 +++--- port/port_test.go | 6 +++--- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/port/collection_test.go b/port/collection_test.go index 99db3cb..3b65268 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -186,7 +186,7 @@ func TestCollection_ClearSignal(t *testing.T) { func TestCollection_With(t *testing.T) { type args struct { - ports []*Port + ports Ports } tests := []struct { name string @@ -283,7 +283,7 @@ func TestCollection_Flush(t *testing.T) { func TestCollection_PipeTo(t *testing.T) { type args struct { - destPorts []*Port + destPorts Ports } tests := []struct { name string diff --git a/port/group.go b/port/group.go index ca8cb71..d86a7c6 100644 --- a/port/group.go +++ b/port/group.go @@ -5,12 +5,14 @@ import ( "github.com/hovsep/fmesh/common" ) +type Ports []*Port + // Group represents a list of ports // can carry multiple ports with same name // no lookup methods type Group struct { *common.Chainable - ports []*Port //@TODO: extract type + ports Ports } // NewGroup creates multiple ports @@ -18,7 +20,7 @@ func NewGroup(names ...string) *Group { newGroup := &Group{ Chainable: common.NewChainable(), } - ports := make([]*Port, len(names)) + ports := make(Ports, len(names)) for i, name := range names { ports[i] = New(name) } @@ -32,7 +34,7 @@ func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { return NewGroup().WithChainError(ErrInvalidRangeForIndexedGroup) } - ports := make([]*Port, endIndex-startIndex+1) + ports := make(Ports, endIndex-startIndex+1) for i := startIndex; i <= endIndex; i++ { ports[i-startIndex] = New(fmt.Sprintf("%s%d", prefix, i)) @@ -47,7 +49,7 @@ func (g *Group) With(ports ...*Port) *Group { return g } - newPorts := make([]*Port, len(g.ports)+len(ports)) + newPorts := make(Ports, len(g.ports)+len(ports)) copy(newPorts, g.ports) for i, port := range ports { newPorts[len(g.ports)+i] = port @@ -57,13 +59,13 @@ func (g *Group) With(ports ...*Port) *Group { } // withPorts sets ports -func (g *Group) withPorts(ports []*Port) *Group { +func (g *Group) withPorts(ports Ports) *Group { g.ports = ports return g } // Ports getter -func (g *Group) Ports() ([]*Port, error) { +func (g *Group) Ports() (Ports, error) { if g.HasChainError() { return nil, g.ChainError() } @@ -71,12 +73,12 @@ func (g *Group) Ports() ([]*Port, error) { } // PortsOrNil returns ports or nil in case of any error -func (g *Group) PortsOrNil() []*Port { +func (g *Group) PortsOrNil() Ports { return g.PortsOrDefault(nil) } // PortsOrDefault returns ports or default in case of any error -func (g *Group) PortsOrDefault(defaultPorts []*Port) []*Port { +func (g *Group) PortsOrDefault(defaultPorts Ports) Ports { ports, err := g.Ports() if err != nil { return defaultPorts diff --git a/port/group_test.go b/port/group_test.go index e987a7a..36026e2 100644 --- a/port/group_test.go +++ b/port/group_test.go @@ -22,7 +22,7 @@ func TestNewGroup(t *testing.T) { }, want: &Group{ Chainable: common.NewChainable(), - ports: []*Port{}, + ports: Ports{}, }, }, { @@ -32,7 +32,7 @@ func TestNewGroup(t *testing.T) { }, want: &Group{ Chainable: common.NewChainable(), - ports: []*Port{New("p1"), + ports: Ports{New("p1"), New("p2")}, }, }, @@ -92,7 +92,7 @@ func TestNewIndexedGroup(t *testing.T) { func TestGroup_With(t *testing.T) { type args struct { - ports []*Port + ports Ports } tests := []struct { name string diff --git a/port/port_test.go b/port/port_test.go index c428784..e36367b 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -90,7 +90,7 @@ func TestPort_PipeTo(t *testing.T) { p1, p2, p3, p4 := New("p1"), New("p2"), New("p3"), New("p4") type args struct { - toPorts []*Port + toPorts Ports } tests := []struct { name string @@ -103,7 +103,7 @@ func TestPort_PipeTo(t *testing.T) { before: p1, after: New("p1").PipeTo(p2, p3), args: args{ - toPorts: []*Port{p2, p3}, + toPorts: Ports{p2, p3}, }, }, { @@ -111,7 +111,7 @@ func TestPort_PipeTo(t *testing.T) { before: p4, after: New("p4").PipeTo(p2), args: args{ - toPorts: []*Port{p2, nil}, + toPorts: Ports{p2, nil}, }, }, } From 77261595eef589d173be2e933cf499aa351ae849 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sat, 2 Nov 2024 03:25:45 +0200 Subject: [PATCH 30/41] Label port with direction and validate pipes using those labels --- common/labeled_entity.go | 15 +- component/component.go | 21 ++- component/component_test.go | 78 +++++++--- export/dot/dot.go | 22 ++- integration_tests/computation/math_test.go | 1 - port/collection.go | 20 ++- port/collection_test.go | 44 ++++-- port/errors.go | 8 +- port/group.go | 8 ++ port/port.go | 59 ++++---- port/port_test.go | 157 +++++++++++++++++---- signal/group.go | 1 + 12 files changed, 321 insertions(+), 113 deletions(-) diff --git a/common/labeled_entity.go b/common/labeled_entity.go index 140a715..c10ada6 100644 --- a/common/labeled_entity.go +++ b/common/labeled_entity.go @@ -11,7 +11,9 @@ type LabeledEntity struct { labels LabelsCollection } -var errLabelNotFound = errors.New("label not found") +var ( + ErrLabelNotFound = errors.New("label not found") +) // NewLabeledEntity constructor func NewLabeledEntity(labels LabelsCollection) LabeledEntity { @@ -28,12 +30,21 @@ func (e *LabeledEntity) Label(label string) (string, error) { value, ok := e.labels[label] if !ok { - return "", fmt.Errorf("%w , label: %s", errLabelNotFound, label) + return "", fmt.Errorf("label %s not found, %w", label, ErrLabelNotFound) } return value, nil } +// LabelOrDefault returns label value or default value in case of any error +func (e *LabeledEntity) LabelOrDefault(label string, defaultValue string) string { + value, err := e.Label(label) + if err != nil { + return defaultValue + } + return value +} + // SetLabels overwrites labels collection func (e *LabeledEntity) SetLabels(labels LabelsCollection) { e.labels = labels diff --git a/component/component.go b/component/component.go index 97bbc62..00a9f38 100644 --- a/component/component.go +++ b/component/component.go @@ -27,8 +27,12 @@ func New(name string) *Component { DescribedEntity: common.NewDescribedEntity(""), LabeledEntity: common.NewLabeledEntity(nil), Chainable: common.NewChainable(), - inputs: port.NewCollection(), - outputs: port.NewCollection(), + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), } } @@ -144,9 +148,13 @@ func (c *Component) Outputs() *port.Collection { // OutputByName is shortcut method func (c *Component) OutputByName(name string) *port.Port { + if c.HasChainError() { + return port.New("").WithChainError(c.ChainError()) + } outputPort := c.Outputs().ByName(name) if outputPort.HasChainError() { - return port.New("").WithChainError(outputPort.ChainError()) + c.SetChainError(outputPort.ChainError()) + return port.New("").WithChainError(c.ChainError()) } return outputPort } @@ -158,7 +166,8 @@ func (c *Component) InputByName(name string) *port.Port { } inputPort := c.Inputs().ByName(name) if inputPort.HasChainError() { - return port.New("").WithChainError(inputPort.ChainError()) + c.SetChainError(inputPort.ChainError()) + return port.New("").WithChainError(c.ChainError()) } return inputPort } @@ -222,7 +231,7 @@ func (c *Component) MaybeActivate() (activationResult *ActivationResult) { return } - if !c.inputs.AnyHasSignals() { + if !c.Inputs().AnyHasSignals() { //No inputs set, stop here activationResult = c.newActivationResultNoInput() return @@ -251,7 +260,7 @@ func (c *Component) FlushOutputs() *Component { return c } - ports, err := c.outputs.Ports() + ports, err := c.Outputs().Ports() if err != nil { return c.WithChainError(err) } diff --git a/component/component_test.go b/component/component_test.go index ae0168a..1856dcf 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -68,7 +68,9 @@ func TestComponent_FlushOutputs(t *testing.T) { { name: "happy path", getComponent: func() *Component { - sink := port.New("sink") + sink := port.New("sink").WithLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }) c := New("c1").WithOutputs("o1", "o2") c.Outputs().ByNames("o1").PutSignals(signal.New(777)) c.Outputs().ByNames("o2").PutSignals(signal.New(888)) @@ -118,12 +120,16 @@ func TestComponent_Inputs(t *testing.T) { { name: "no inputs", component: New("c1"), - want: port.NewCollection(), + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), }, { name: "with inputs", component: New("c1").WithInputs("i1", "i2"), - want: port.NewCollection().With(port.New("i1"), port.New("i2")), + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }).With(port.New("i1"), port.New("i2")), }, } for _, tt := range tests { @@ -142,12 +148,16 @@ func TestComponent_Outputs(t *testing.T) { { name: "no outputs", component: New("c1"), - want: port.NewCollection(), + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), }, { name: "with outputs", component: New("c1").WithOutputs("o1", "o2"), - want: port.NewCollection().With(port.New("o1"), port.New("o2")), + want: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }).With(port.New("o1"), port.New("o2")), }, } for _, tt := range tests { @@ -219,9 +229,13 @@ func TestComponent_WithDescription(t *testing.T) { DescribedEntity: common.NewDescribedEntity("descr"), LabeledEntity: common.NewLabeledEntity(nil), Chainable: common.NewChainable(), - inputs: port.NewCollection(), - outputs: port.NewCollection(), - f: nil, + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, } @@ -253,9 +267,13 @@ func TestComponent_WithInputs(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), LabeledEntity: common.NewLabeledEntity(nil), Chainable: common.NewChainable(), - inputs: port.NewCollection().With(port.New("p1"), port.New("p2")), - outputs: port.NewCollection(), - f: nil, + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }).With(port.New("p1"), port.New("p2")), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, { @@ -269,9 +287,13 @@ func TestComponent_WithInputs(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), LabeledEntity: common.NewLabeledEntity(nil), Chainable: common.NewChainable(), - inputs: port.NewCollection(), - outputs: port.NewCollection(), - f: nil, + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, } @@ -303,9 +325,13 @@ func TestComponent_WithOutputs(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), LabeledEntity: common.NewLabeledEntity(nil), Chainable: common.NewChainable(), - inputs: port.NewCollection(), - outputs: port.NewCollection().With(port.New("p1"), port.New("p2")), - f: nil, + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }).With(port.New("p1"), port.New("p2")), + f: nil, }, }, { @@ -319,9 +345,13 @@ func TestComponent_WithOutputs(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), LabeledEntity: common.NewLabeledEntity(nil), Chainable: common.NewChainable(), - inputs: port.NewCollection(), - outputs: port.NewCollection(), - f: nil, + inputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), + outputs: port.NewCollection().WithDefaultLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), + f: nil, }, }, } @@ -668,12 +698,16 @@ func TestComponent_WithLabels(t *testing.T) { func TestComponent_ShortcutMethods(t *testing.T) { t.Run("InputByName", func(t *testing.T) { c := New("c").WithInputs("a", "b", "c") - assert.Equal(t, port.New("b"), c.InputByName("b")) + assert.Equal(t, port.New("b").WithLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionIn, + }), c.InputByName("b")) }) t.Run("OutputByName", func(t *testing.T) { c := New("c").WithOutputs("a", "b", "c") - assert.Equal(t, port.New("b"), c.OutputByName("b")) + assert.Equal(t, port.New("b").WithLabels(common.LabelsCollection{ + port.DirectionLabel: port.DirectionOut, + }), c.OutputByName("b")) }) } diff --git a/export/dot/dot.go b/export/dot/dot.go index 6a97026..8267a43 100644 --- a/export/dot/dot.go +++ b/export/dot/dot.go @@ -23,9 +23,7 @@ type dotExporter struct { } const ( - nodeIDLabel = "export/dot/id" - portKindInput = "input" - portKindOutput = "output" + nodeIDLabel = "export/dot/id" ) // NewDotExporter returns exporter with default configuration @@ -148,7 +146,7 @@ func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Compo destPort.DeleteLabel(nodeIDLabel) // Any source port in any pipe is always output port, so we can build its node ID - srcPortNode := graph.FindNodeByID(getPortID(c.Name(), portKindOutput, srcPort.Name())) + srcPortNode := graph.FindNodeByID(getPortID(c.Name(), port.DirectionOut, srcPort.Name())) destPortNode := graph.FindNodeByID(destPortID) graph.Edge(srcPortNode, destPortNode, func(a *dot.AttributesMap) { @@ -177,7 +175,7 @@ func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent. return err } for _, p := range inputPorts { - portNode := d.getPortNode(c, p, portKindInput, componentSubgraph) + portNode := d.getPortNode(c, p, componentSubgraph) componentSubgraph.Edge(portNode, componentNode) } @@ -187,7 +185,7 @@ func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent. return err } for _, p := range outputPorts { - portNode := d.getPortNode(c, p, portKindOutput, componentSubgraph) + portNode := d.getPortNode(c, p, componentSubgraph) componentSubgraph.Edge(componentNode, portNode) } } @@ -195,15 +193,15 @@ func (d *dotExporter) addComponents(graph *dot.Graph, components fmeshcomponent. } // getPortNode creates and returns a node representing one port -func (d *dotExporter) getPortNode(c *fmeshcomponent.Component, port *port.Port, portKind string, componentSubgraph *dot.Graph) *dot.Node { - portID := getPortID(c.Name(), portKind, port.Name()) +func (d *dotExporter) getPortNode(c *fmeshcomponent.Component, p *port.Port, componentSubgraph *dot.Graph) *dot.Node { + portID := getPortID(c.Name(), p.LabelOrDefault(port.DirectionLabel, ""), p.Name()) //Mark ports to be able to find their respective nodes later when adding pipes - port.AddLabel(nodeIDLabel, portID) + p.AddLabel(nodeIDLabel, portID) portNode := componentSubgraph.NodeWithID(portID, func(a *dot.AttributesMap) { setAttrMap(a, d.config.Port.Node) - a.Attr("label", port.Name()).Attr("group", c.Name()) + a.Attr("label", p.Name()).Attr("group", c.Name()) }) return portNode @@ -329,8 +327,8 @@ func getCycleStats(activationCycle *cycle.Cycle) []*statEntry { } // getPortID returns unique ID used to locate ports while building pipe edges -func getPortID(componentName string, portKind string, portName string) string { - return fmt.Sprintf("component/%s/%s/%s", componentName, portKind, portName) +func getPortID(componentName string, portDirection string, portName string) string { + return fmt.Sprintf("component/%s/%s/%s", componentName, portDirection, portName) } // setAttrMap sets all attributes to target diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/math_test.go index 35c784b..5e56f5e 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/math_test.go @@ -41,7 +41,6 @@ func Test_Math(t *testing.T) { }) c1.OutputByName("res").PipeTo(c2.InputByName("num")) - return fmesh.New("fm").WithComponents(c1, c2).WithConfig(fmesh.Config{ ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, CyclesLimit: 10, diff --git a/port/collection.go b/port/collection.go index 6dde735..d2bde06 100644 --- a/port/collection.go +++ b/port/collection.go @@ -14,13 +14,16 @@ type PortMap map[string]*Port type Collection struct { *common.Chainable ports PortMap + // Labels added by default to each port in collection + defaultLabels common.LabelsCollection } // NewCollection creates empty collection func NewCollection() *Collection { return &Collection{ - Chainable: common.NewChainable(), - ports: make(PortMap), + Chainable: common.NewChainable(), + ports: make(PortMap), + defaultLabels: common.LabelsCollection{}, } } @@ -32,7 +35,7 @@ func (collection *Collection) ByName(name string) *Port { port, ok := collection.ports[name] if !ok { collection.SetChainError(ErrPortNotFoundInCollection) - return New("").WithChainError(ErrPortNotFoundInCollection) + return New("").WithChainError(collection.ChainError()) } return port } @@ -43,7 +46,7 @@ func (collection *Collection) ByNames(names ...string) *Collection { return NewCollection().WithChainError(collection.ChainError()) } - selectedPorts := NewCollection() + selectedPorts := NewCollection().WithDefaultLabels(collection.defaultLabels) for _, name := range names { if p, ok := collection.ports[name]; ok { @@ -131,7 +134,7 @@ func (collection *Collection) Flush() *Collection { // PipeTo creates pipes from each port in collection to given destination ports func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { for _, p := range collection.ports { - p.PipeTo(destPorts...) + p = p.PipeTo(destPorts...) if p.HasChainError() { return collection.WithChainError(p.ChainError()) @@ -151,7 +154,7 @@ func (collection *Collection) With(ports ...*Port) *Collection { if port.HasChainError() { return collection.WithChainError(port.ChainError()) } - + port.AddLabels(collection.defaultLabels) collection.ports[port.Name()] = port } @@ -225,3 +228,8 @@ func (collection *Collection) WithChainError(err error) *Collection { func (collection *Collection) Len() int { return len(collection.ports) } + +func (collection *Collection) WithDefaultLabels(labels common.LabelsCollection) *Collection { + collection.defaultLabels = labels + return collection +} diff --git a/port/collection_test.go b/port/collection_test.go index 3b65268..63ffe0d 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -2,6 +2,7 @@ package port import ( "errors" + "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" "testing" @@ -77,20 +78,28 @@ func TestCollection_ByName(t *testing.T) { want *Port }{ { - name: "empty port found", - collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...), + name: "empty port found", + collection: NewCollection().WithDefaultLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).With(NewGroup("p1", "p2").PortsOrNil()...), args: args{ name: "p1", }, - want: New("p1"), + want: New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }), }, { - name: "port with buffer found", - collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).PutSignals(signal.New(12)), + name: "port with buffer found", + collection: NewCollection().WithDefaultLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).With(NewGroup("p1", "p2").PortsOrNil()...).PutSignals(signal.New(12)), args: args{ name: "p2", }, - want: New("p2").WithSignals(signal.New(12)), + want: New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).WithSignals(signal.New(12)), }, { name: "port not found", @@ -254,8 +263,17 @@ func TestCollection_Flush(t *testing.T) { name: "all ports in collection are flushed", collection: NewCollection().With( New("src"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }). WithSignalGroups(signal.NewGroup(1, 2, 3)). - PipeTo(New("dst1"), New("dst2")), + PipeTo(New("dst1"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), New("dst2"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), ), assertions: func(t *testing.T, collection *Collection) { assert.Equal(t, collection.Len(), 1) @@ -302,10 +320,16 @@ func TestCollection_PipeTo(t *testing.T) { }, }, { - name: "add pipes to each port in collection", - collection: NewCollection().With(NewIndexedGroup("p", 1, 3).PortsOrNil()...), + name: "add pipes to each port in collection", + collection: NewCollection().With(NewIndexedGroup("p", 1, 3).WithPortLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PortsOrNil()...), args: args{ - destPorts: NewIndexedGroup("dest", 1, 5).PortsOrNil(), + destPorts: NewIndexedGroup("dest", 1, 5). + WithPortLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }). + PortsOrNil(), }, assertions: func(t *testing.T, collection *Collection) { assert.Equal(t, collection.Len(), 3) diff --git a/port/errors.go b/port/errors.go index d444ac8..b636f8c 100644 --- a/port/errors.go +++ b/port/errors.go @@ -1,9 +1,13 @@ package port -import "errors" +import ( + "errors" +) var ( ErrPortNotFoundInCollection = errors.New("port not found") - ErrPortNotReadyForFlush = errors.New("port is not ready for flush") ErrInvalidRangeForIndexedGroup = errors.New("start index can not be greater than end index") + ErrNilPort = errors.New("port is nil") + ErrMissingLabel = errors.New("port is missing required label") + ErrInvalidPipeDirection = errors.New("pipe must go from output to input") ) diff --git a/port/group.go b/port/group.go index d86a7c6..3949126 100644 --- a/port/group.go +++ b/port/group.go @@ -96,3 +96,11 @@ func (g *Group) WithChainError(err error) *Group { func (g *Group) Len() int { return len(g.ports) } + +// WithPortLabels sets labels on each port within the group and returns it +func (g *Group) WithPortLabels(labels common.LabelsCollection) *Group { + for _, p := range g.PortsOrNil() { + p.WithLabels(labels) + } + return g +} diff --git a/port/port.go b/port/port.go index a7c5c9f..ca45359 100644 --- a/port/port.go +++ b/port/port.go @@ -1,10 +1,17 @@ package port import ( + "fmt" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) +const ( + DirectionLabel = "fmesh:port:direction" + DirectionIn = "in" + DirectionOut = "out" +) + // Port defines a connectivity point of a component type Port struct { common.NamedEntity @@ -106,8 +113,9 @@ func (p *Port) Flush() *Port { } if !p.HasSignals() || !p.HasPipes() { - //@TODO maybe better to return explicit errors - return New("").WithChainError(ErrPortNotReadyForFlush) + //Log,this + //Nothing to flush + return p } pipes, err := p.pipes.Ports() @@ -127,31 +135,12 @@ func (p *Port) Flush() *Port { // HasSignals says whether port buffer is set or not func (p *Port) HasSignals() bool { - if p.HasChainError() { - //@TODO: add logging here - return false - } - signals, err := p.AllSignals() - if err != nil { - //@TODO: add logging here - return false - } - return len(signals) > 0 + return len(p.AllSignalsOrNil()) > 0 } // HasPipes says whether port has outbound pipes func (p *Port) HasPipes() bool { - if p.HasChainError() { - //@TODO: add logging here - return false - } - pipes, err := p.pipes.Ports() - if err != nil { - //@TODO: add logging here - return false - } - - return len(pipes) > 0 + return p.Pipes().Len() > 0 } // PipeTo creates one or multiple pipes to other port(s) @@ -160,15 +149,35 @@ func (p *Port) PipeTo(destPorts ...*Port) *Port { if p.HasChainError() { return p } + for _, destPort := range destPorts { - if destPort == nil { - continue + if err := validatePipe(p, destPort); err != nil { + p.SetChainError(fmt.Errorf("pipe validation failed: %w", err)) + return New("").WithChainError(p.ChainError()) } p.pipes = p.pipes.With(destPort) } return p } +func validatePipe(srcPort *Port, dstPort *Port) error { + if srcPort == nil || dstPort == nil { + return ErrNilPort + } + + srcDir, dstDir := srcPort.LabelOrDefault(DirectionLabel, ""), dstPort.LabelOrDefault(DirectionLabel, "") + + if srcDir == "" || dstDir == "" { + return ErrMissingLabel + } + + if srcDir == "in" || dstDir == "out" { + return ErrInvalidPipeDirection + } + + return nil +} + // WithLabels sets labels and returns the port func (p *Port) WithLabels(labels common.LabelsCollection) *Port { if p.HasChainError() { diff --git a/port/port_test.go b/port/port_test.go index e36367b..f503a4c 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -87,38 +87,99 @@ func TestPort_Clear(t *testing.T) { } func TestPort_PipeTo(t *testing.T) { - p1, p2, p3, p4 := New("p1"), New("p2"), New("p3"), New("p4") + outputPorts := NewCollection(). + WithDefaultLabels( + common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).With( + NewIndexedGroup("out", 1, 3).PortsOrNil()..., + ) + inputPorts := NewCollection(). + WithDefaultLabels( + common.LabelsCollection{ + DirectionLabel: DirectionIn, + }).With( + NewIndexedGroup("in", 1, 3).PortsOrNil()..., + ) type args struct { toPorts Ports } tests := []struct { - name string - before *Port - after *Port - args args + name string + before *Port + assertions func(t *testing.T, portAfter *Port) + args args }{ { name: "happy path", - before: p1, - after: New("p1").PipeTo(p2, p3), + before: outputPorts.ByName("out1"), + args: args{ + toPorts: Ports{inputPorts.ByName("in2"), inputPorts.ByName("in3")}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.False(t, portAfter.HasChainError()) + assert.NoError(t, portAfter.ChainError()) + assert.Equal(t, 2, portAfter.Pipes().Len()) + }, + }, + { + name: "port must have direction label", + before: New("out_without_dir"), + args: args{ + toPorts: Ports{inputPorts.ByName("in1")}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasChainError()) + assert.Error(t, portAfter.ChainError()) + }, + }, + { + name: "nil port is not allowed", + before: outputPorts.ByName("out3"), args: args{ - toPorts: Ports{p2, p3}, + toPorts: Ports{inputPorts.ByName("in2"), nil}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasChainError()) + assert.Error(t, portAfter.ChainError()) }, }, { - name: "invalid ports are ignored", - before: p4, - after: New("p4").PipeTo(p2), + name: "piping from input ports is not allowed", + before: inputPorts.ByName("in1"), args: args{ - toPorts: Ports{p2, nil}, + toPorts: Ports{ + inputPorts.ByName("in2"), outputPorts.ByName("out1"), + }, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasChainError()) + assert.Error(t, portAfter.ChainError()) + }, + }, + { + name: "piping to output ports is not allowed", + before: outputPorts.ByName("out1"), + args: args{ + toPorts: Ports{outputPorts.ByName("out2")}, + }, + assertions: func(t *testing.T, portAfter *Port) { + assert.Equal(t, "", portAfter.Name()) + assert.True(t, portAfter.HasChainError()) + assert.Error(t, portAfter.ChainError()) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.before.PipeTo(tt.args.toPorts...) - assert.Equal(t, tt.after, tt.before) + portAfter := tt.before.PipeTo(tt.args.toPorts...) + if tt.assertions != nil { + tt.assertions(t, portAfter) + } }) } } @@ -260,7 +321,11 @@ func TestPort_HasPipes(t *testing.T) { }, { name: "with pipes", - port: New("p1").PipeTo(New("p2")), + port: New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PipeTo(New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), want: true, }, } @@ -287,8 +352,21 @@ func TestPort_Flush(t *testing.T) { }, }, { - name: "empty port with pipes is not flushed", - srcPort: New("p").PipeTo(New("p1"), New("p2")), + name: "empty port with pipes is not flushed", + srcPort: New("p"). + WithLabels( + common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PipeTo( + New("p1"). + WithLabels( + common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), New("p2"). + WithLabels( + common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) @@ -296,10 +374,16 @@ func TestPort_Flush(t *testing.T) { }, { name: "flush to empty ports", - srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)). + srcPort: New("p").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).WithSignalGroups(signal.NewGroup(1, 2, 3)). PipeTo( - New("p1"), - New("p2")), + New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), + New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) @@ -316,10 +400,17 @@ func TestPort_Flush(t *testing.T) { }, { name: "flush to non empty ports", - srcPort: New("p").WithSignalGroups(signal.NewGroup(1, 2, 3)). + srcPort: New("p").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }). + WithSignalGroups(signal.NewGroup(1, 2, 3)). PipeTo( - New("p1").WithSignalGroups(signal.NewGroup(4, 5, 6)), - New("p2").WithSignalGroups(signal.NewGroup(7, 8, 9))), + New("p1").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }).WithSignalGroups(signal.NewGroup(4, 5, 6)), + New("p2").WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }).WithSignalGroups(signal.NewGroup(7, 8, 9))), assertions: func(t *testing.T, srcPort *Port) { assert.False(t, srcPort.HasSignals()) assert.True(t, srcPort.HasPipes()) @@ -337,9 +428,9 @@ func TestPort_Flush(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.srcPort.Flush() + portAfter := tt.srcPort.Flush() if tt.assertions != nil { - tt.assertions(t, tt.srcPort) + tt.assertions(t, portAfter) } }) } @@ -393,8 +484,20 @@ func TestPort_Pipes(t *testing.T) { }, { name: "with pipes", - port: New("p1").PipeTo(New("p2"), New("p3")), - want: NewGroup("p2", "p3"), + port: New("p1"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionOut, + }).PipeTo( + New("p2"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), New("p3"). + WithLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + })), + want: NewGroup("p2", "p3").WithPortLabels(common.LabelsCollection{ + DirectionLabel: DirectionIn, + }), }, { name: "with chain error", diff --git a/signal/group.go b/signal/group.go index 7316734..782020e 100644 --- a/signal/group.go +++ b/signal/group.go @@ -32,6 +32,7 @@ func (g *Group) First() *Signal { } if len(g.signals) == 0 { + g.SetChainError(ErrNoSignalsInGroup) return New(nil).WithChainError(ErrNoSignalsInGroup) } From 9210d952b5e318acb5ad29e44527b466827b594f Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 4 Nov 2024 22:56:35 +0200 Subject: [PATCH 31/41] Chainable API: polish error handling --- common/chainable.go | 1 + component/component.go | 18 ++++++++++++++---- fmesh.go | 6 ++++-- port/collection.go | 13 ++++++++----- port/port.go | 22 +++++++++++++++------- signal/group.go | 8 +++++--- signal/group_test.go | 2 +- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/common/chainable.go b/common/chainable.go index 542b38a..517cea9 100644 --- a/common/chainable.go +++ b/common/chainable.go @@ -16,6 +16,7 @@ func (c *Chainable) HasChainError() bool { return c.err != nil } +// @TODO: rename to Err() func (c *Chainable) ChainError() error { return c.err } diff --git a/component/component.go b/component/component.go index 00a9f38..ebacc34 100644 --- a/component/component.go +++ b/component/component.go @@ -48,6 +48,9 @@ func (c *Component) WithDescription(description string) *Component { // withInputPorts sets input ports collection func (c *Component) withInputPorts(collection *port.Collection) *Component { + if c.HasChainError() { + return c + } if collection.HasChainError() { return c.WithChainError(collection.ChainError()) } @@ -57,6 +60,9 @@ func (c *Component) withInputPorts(collection *port.Collection) *Component { // withOutputPorts sets input ports collection func (c *Component) withOutputPorts(collection *port.Collection) *Component { + if c.HasChainError() { + return c + } if collection.HasChainError() { return c.WithChainError(collection.ChainError()) } @@ -73,8 +79,10 @@ func (c *Component) WithInputs(portNames ...string) *Component { ports, err := port.NewGroup(portNames...).Ports() if err != nil { - return c.WithChainError(err) + c.SetChainError(err) + return New("").WithChainError(c.ChainError()) } + return c.withInputPorts(c.Inputs().With(ports...)) } @@ -86,7 +94,8 @@ func (c *Component) WithOutputs(portNames ...string) *Component { ports, err := port.NewGroup(portNames...).Ports() if err != nil { - return c.WithChainError(err) + c.SetChainError(err) + return New("").WithChainError(c.ChainError()) } return c.withOutputPorts(c.Outputs().With(ports...)) } @@ -262,10 +271,11 @@ func (c *Component) FlushOutputs() *Component { ports, err := c.Outputs().Ports() if err != nil { - return c.WithChainError(err) + c.SetChainError(err) + return New("").WithChainError(c.ChainError()) } for _, out := range ports { - out.Flush() + out = out.Flush() if out.HasChainError() { return c.WithChainError(out.ChainError()) } diff --git a/fmesh.go b/fmesh.go index 741ec3b..ec4f9b8 100644 --- a/fmesh.go +++ b/fmesh.go @@ -95,14 +95,16 @@ func (fm *FMesh) runCycle() *cycle.Cycle { } if fm.Components().Len() == 0 { - return newCycle.WithChainError(errors.New("failed to run cycle: no components found")) + fm.SetChainError(errors.New("failed to run cycle: no components found")) + return newCycle.WithChainError(fm.ChainError()) } var wg sync.WaitGroup components, err := fm.Components().Components() if err != nil { - return newCycle.WithChainError(fmt.Errorf("failed to run cycle: %w", err)) + fm.SetChainError(fmt.Errorf("failed to run cycle: %w", err)) + return newCycle.WithChainError(fm.ChainError()) } for _, c := range components { diff --git a/port/collection.go b/port/collection.go index d2bde06..5541d82 100644 --- a/port/collection.go +++ b/port/collection.go @@ -46,6 +46,7 @@ func (collection *Collection) ByNames(names ...string) *Collection { return NewCollection().WithChainError(collection.ChainError()) } + //Preserve collection config selectedPorts := NewCollection().WithDefaultLabels(collection.defaultLabels) for _, name := range names { @@ -122,7 +123,7 @@ func (collection *Collection) Flush() *Collection { } for _, p := range collection.ports { - p.Flush() + p = p.Flush() if p.HasChainError() { return collection.WithChainError(p.ChainError()) @@ -147,7 +148,7 @@ func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { // With adds ports to collection and returns it func (collection *Collection) With(ports ...*Port) *Collection { if collection.HasChainError() { - return NewCollection().WithChainError(collection.ChainError()) + return collection } for _, port := range ports { @@ -164,12 +165,13 @@ func (collection *Collection) With(ports ...*Port) *Collection { // WithIndexed creates ports with names like "o1","o2","o3" and so on func (collection *Collection) WithIndexed(prefix string, startIndex int, endIndex int) *Collection { if collection.HasChainError() { - return NewCollection().WithChainError(collection.ChainError()) + return collection } indexedPorts, err := NewIndexedGroup(prefix, startIndex, endIndex).Ports() if err != nil { - return collection.WithChainError(err) + collection.SetChainError(err) + return NewCollection().WithChainError(collection.ChainError()) } return collection.With(indexedPorts...) } @@ -184,7 +186,8 @@ func (collection *Collection) Signals() *signal.Group { for _, p := range collection.ports { signals, err := p.Buffer().Signals() if err != nil { - return group.WithChainError(err) + collection.SetChainError(err) + return signal.NewGroup().WithChainError(collection.ChainError()) } group = group.With(signals...) } diff --git a/port/port.go b/port/port.go index ca45359..b2f2aa5 100644 --- a/port/port.go +++ b/port/port.go @@ -37,7 +37,7 @@ func New(name string) *Port { // @TODO: maybe we can hide this and return signals to user code func (p *Port) Buffer() *signal.Group { if p.HasChainError() { - return p.buffer.WithChainError(p.ChainError()) + return signal.NewGroup().WithChainError(p.ChainError()) } return p.buffer } @@ -46,15 +46,20 @@ func (p *Port) Buffer() *signal.Group { // @TODO maybe better to return []*Port directly func (p *Port) Pipes() *Group { if p.HasChainError() { - return p.pipes.WithChainError(p.ChainError()) + return NewGroup().WithChainError(p.ChainError()) } return p.pipes } // withBuffer sets buffer field func (p *Port) withBuffer(buffer *signal.Group) *Port { + if p.HasChainError() { + return p + } + if buffer.HasChainError() { - return p.WithChainError(buffer.ChainError()) + p.SetChainError(buffer.ChainError()) + return New("").WithChainError(p.ChainError()) } p.buffer = buffer return p @@ -86,11 +91,12 @@ func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { for _, group := range signalGroups { signals, err := group.Signals() if err != nil { - return p.WithChainError(err) + p.SetChainError(err) + return New("").WithChainError(p.ChainError()) } p.PutSignals(signals...) if p.HasChainError() { - return p + return New("").WithChainError(p.ChainError()) } } @@ -120,14 +126,16 @@ func (p *Port) Flush() *Port { pipes, err := p.pipes.Ports() if err != nil { - return p.WithChainError(err) + p.SetChainError(err) + return New("").WithChainError(p.ChainError()) } for _, outboundPort := range pipes { //Fan-Out err = ForwardSignals(p, outboundPort) if err != nil { - return p.WithChainError(err) + p.SetChainError(err) + return New("").WithChainError(p.ChainError()) } } return p.Clear() diff --git a/signal/group.go b/signal/group.go index 782020e..e3fe2c3 100644 --- a/signal/group.go +++ b/signal/group.go @@ -33,7 +33,7 @@ func (g *Group) First() *Signal { if len(g.signals) == 0 { g.SetChainError(ErrNoSignalsInGroup) - return New(nil).WithChainError(ErrNoSignalsInGroup) + return New(nil).WithChainError(g.ChainError()) } return g.signals[0] @@ -76,11 +76,13 @@ func (g *Group) With(signals ...*Signal) *Group { copy(newSignals, g.signals) for i, sig := range signals { if sig == nil { - return g.WithChainError(ErrInvalidSignal) + g.SetChainError(ErrInvalidSignal) + return NewGroup().WithChainError(g.ChainError()) } if sig.HasChainError() { - return g.WithChainError(sig.ChainError()) + g.SetChainError(sig.ChainError()) + return NewGroup().WithChainError(g.ChainError()) } newSignals[len(g.signals)+i] = sig diff --git a/signal/group_test.go b/signal/group_test.go index bd24e12..585cf7b 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -189,7 +189,7 @@ func TestGroup_With(t *testing.T) { args: args{ signals: NewGroup(7, nil, 9).SignalsOrNil(), }, - want: NewGroup(1, 2, 3, "valid before invalid").WithChainError(errors.New("signal is invalid")), + want: NewGroup().WithChainError(errors.New("signal is invalid")), }, { name: "with error in signal", From e745e141371d0e098178c787d888bed31cf0a941 Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 4 Nov 2024 23:08:10 +0200 Subject: [PATCH 32/41] Rename activation result error to avoid confusion with chain error (once renamed) --- component/activation_result.go | 30 +++++++++++++++--------------- component/component_test.go | 24 ++++++++++++------------ cycle/cycle_test.go | 8 ++++---- export/dot/dot.go | 4 ++-- fmesh_test.go | 20 ++++++++++---------- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/component/activation_result.go b/component/activation_result.go index 8ec3619..2c78a6e 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -9,10 +9,10 @@ import ( // ActivationResult defines the result (possibly an error) of the activation of given component in given cycle type ActivationResult struct { *common.Chainable - componentName string - activated bool - code ActivationResultCode - err error + componentName string + activated bool + code ActivationResultCode + activationError error //Error returned from component activation function } // ActivationResultCode denotes a specific info about how a component been activated or why not activated at all @@ -86,9 +86,9 @@ func (ar *ActivationResult) Activated() bool { return ar.activated } -// Error getter -func (ar *ActivationResult) Error() error { - return ar.err +// ActivationError getter +func (ar *ActivationResult) ActivationError() error { + return ar.activationError } // Code getter @@ -98,12 +98,12 @@ func (ar *ActivationResult) Code() ActivationResultCode { // IsError returns true when activation result has an error func (ar *ActivationResult) IsError() bool { - return ar.code == ActivationCodeReturnedError && ar.Error() != nil + return ar.code == ActivationCodeReturnedError && ar.ActivationError() != nil } // IsPanic returns true when activation result is derived from panic func (ar *ActivationResult) IsPanic() bool { - return ar.code == ActivationCodePanicked && ar.Error() != nil + return ar.code == ActivationCodePanicked && ar.ActivationError() != nil } // SetActivated setter @@ -118,9 +118,9 @@ func (ar *ActivationResult) WithActivationCode(code ActivationResultCode) *Activ return ar } -// WithError setter -func (ar *ActivationResult) WithError(err error) *ActivationResult { - ar.err = err +// WithActivationError sets the activation result error +func (ar *ActivationResult) WithActivationError(activationError error) *ActivationResult { + ar.activationError = activationError return ar } @@ -151,7 +151,7 @@ func (c *Component) newActivationResultReturnedError(err error) *ActivationResul return NewActivationResult(c.Name()). SetActivated(true). WithActivationCode(ActivationCodeReturnedError). - WithError(fmt.Errorf("component returned an error: %w", err)) + WithActivationError(fmt.Errorf("component returned an error: %w", err)) } // newActivationResultPanicked builds a specific activation result @@ -159,7 +159,7 @@ func (c *Component) newActivationResultPanicked(err error) *ActivationResult { return NewActivationResult(c.Name()). SetActivated(true). WithActivationCode(ActivationCodePanicked). - WithError(err) + WithActivationError(err) } func (c *Component) newActivationResultWaitingForInputs(err error) *ActivationResult { @@ -170,7 +170,7 @@ func (c *Component) newActivationResultWaitingForInputs(err error) *ActivationRe return NewActivationResult(c.Name()). SetActivated(true). WithActivationCode(activationCode). - WithError(err) + WithActivationError(err) } func IsWaitingForInput(activationResult *ActivationResult) bool { diff --git a/component/component_test.go b/component/component_test.go index 1856dcf..26f781d 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -416,7 +416,7 @@ func TestComponent_MaybeActivate(t *testing.T) { wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodeReturnedError). - WithError(errors.New("component returned an error: test error")), + WithActivationError(errors.New("component returned an error: test error")), }, { name: "activated without error", @@ -452,7 +452,7 @@ func TestComponent_MaybeActivate(t *testing.T) { wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodePanicked). - WithError(errors.New("panicked with: oh shrimps")), + WithActivationError(errors.New("panicked with: oh shrimps")), }, { name: "component panicked with string", @@ -471,7 +471,7 @@ func TestComponent_MaybeActivate(t *testing.T) { wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodePanicked). - WithError(errors.New("panicked with: oh shrimps")), + WithActivationError(errors.New("panicked with: oh shrimps")), }, { name: "component is waiting for inputs", @@ -492,10 +492,10 @@ func TestComponent_MaybeActivate(t *testing.T) { return c1 }, wantActivationResult: &ActivationResult{ - componentName: "c1", - activated: true, - code: ActivationCodeWaitingForInputsClear, - err: NewErrWaitForInputs(false), + componentName: "c1", + activated: true, + code: ActivationCodeWaitingForInputsClear, + activationError: NewErrWaitForInputs(false), }, }, { @@ -517,10 +517,10 @@ func TestComponent_MaybeActivate(t *testing.T) { return c1 }, wantActivationResult: &ActivationResult{ - componentName: "c1", - activated: true, - code: ActivationCodeWaitingForInputsKeep, - err: NewErrWaitForInputs(true), + componentName: "c1", + activated: true, + code: ActivationCodeWaitingForInputsKeep, + activationError: NewErrWaitForInputs(true), }, }, { @@ -553,7 +553,7 @@ func TestComponent_MaybeActivate(t *testing.T) { assert.Equal(t, tt.wantActivationResult.ComponentName(), gotActivationResult.ComponentName()) assert.Equal(t, tt.wantActivationResult.Code(), gotActivationResult.Code()) if tt.wantActivationResult.IsError() { - assert.EqualError(t, gotActivationResult.Error(), tt.wantActivationResult.Error().Error()) + assert.EqualError(t, gotActivationResult.ActivationError(), tt.wantActivationResult.ActivationError().Error()) } else { assert.False(t, gotActivationResult.IsError()) } diff --git a/cycle/cycle_test.go b/cycle/cycle_test.go index bca2854..c2e052b 100644 --- a/cycle/cycle_test.go +++ b/cycle/cycle_test.go @@ -102,7 +102,7 @@ func TestCycle_HasErrors(t *testing.T) { name: "some components returned errors", cycleResult: New().WithActivationResults( component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithActivationError(errors.New("some error")), component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), ), want: true, @@ -132,7 +132,7 @@ func TestCycle_HasPanics(t *testing.T) { component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), component.NewActivationResult("c2").SetActivated(false).WithActivationCode(component.ActivationCodeNoFunction), component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithActivationError(errors.New("some error")), ), want: false, }, @@ -140,9 +140,9 @@ func TestCycle_HasPanics(t *testing.T) { name: "some components panicked", cycleResult: New().WithActivationResults( component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithActivationError(errors.New("some error")), component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), - component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked).WithError(errors.New("some panic")), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked).WithActivationError(errors.New("some panic")), ), want: true, }, diff --git a/export/dot/dot.go b/export/dot/dot.go index 8267a43..33335d9 100644 --- a/export/dot/dot.go +++ b/export/dot/dot.go @@ -239,12 +239,12 @@ func (d *dotExporter) getComponentNode(componentSubgraph *dot.Graph, component * } if activationResult != nil { - if activationResult.Error() != nil { + if activationResult.ActivationError() != nil { errorNode := componentSubgraph.Node(func(a *dot.AttributesMap) { setAttrMap(a, d.config.Component.ErrorNode) }) errorNode. - Attr("label", activationResult.Error().Error()) + Attr("label", activationResult.ActivationError().Error()) componentSubgraph.Edge(componentNode, errorNode) } } diff --git a/fmesh_test.go b/fmesh_test.go index 4457c10..0791d25 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -280,7 +280,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("component returned an error: boom")), + WithActivationError(errors.New("component returned an error: boom")), ), ).CyclesOrNil(), wantErr: true, @@ -346,7 +346,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c3"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("component returned an error: boom")), + WithActivationError(errors.New("component returned an error: boom")), component.NewActivationResult("c4"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -380,7 +380,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c4"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). - WithError(errors.New("panicked with: no way")), + WithActivationError(errors.New("panicked with: no way")), ), ).CyclesOrNil(), wantErr: true, @@ -460,7 +460,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c3"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("component returned an error: boom")), + WithActivationError(errors.New("component returned an error: boom")), component.NewActivationResult("c4"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -502,7 +502,7 @@ func TestFMesh_Run(t *testing.T) { component.NewActivationResult("c4"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). - WithError(errors.New("panicked with: no way")), + WithActivationError(errors.New("panicked with: no way")), component.NewActivationResult("c5"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -573,7 +573,7 @@ func TestFMesh_Run(t *testing.T) { assert.Equal(t, tt.want[i].ActivationResults()[componentName].Code(), gotActivationResult.Code()) if tt.want[i].ActivationResults()[componentName].IsError() { - assert.EqualError(t, tt.want[i].ActivationResults()[componentName].Error(), gotActivationResult.Error().Error()) + assert.EqualError(t, tt.want[i].ActivationResults()[componentName].ActivationError(), gotActivationResult.ActivationError().Error()) } else { assert.False(t, gotActivationResult.IsError()) } @@ -723,7 +723,7 @@ func TestFMesh_mustStop(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). - WithError(errors.New("c1 activation finished with error")), + WithActivationError(errors.New("c1 activation finished with error")), ).WithNumber(5), }, want: true, @@ -739,7 +739,7 @@ func TestFMesh_mustStop(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). - WithError(errors.New("c1 panicked")), + WithActivationError(errors.New("c1 panicked")), ).WithNumber(5), }, want: true, @@ -884,7 +884,7 @@ func TestFMesh_drainComponents(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeWaitingForInputsClear). - WithError(component.NewErrWaitForInputs(false)), + WithActivationError(component.NewErrWaitForInputs(false)), component.NewActivationResult("c2"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), @@ -936,7 +936,7 @@ func TestFMesh_drainComponents(t *testing.T) { component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeWaitingForInputsKeep). - WithError(component.NewErrWaitForInputs(true)), + WithActivationError(component.NewErrWaitForInputs(true)), component.NewActivationResult("c2"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), From c1475858653ac1a8ce16d4755936939eccdbc9d9 Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 4 Nov 2024 23:54:50 +0200 Subject: [PATCH 33/41] Update readme --- .../{math_test.go => basic_test.go} | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) rename integration_tests/computation/{math_test.go => basic_test.go} (56%) diff --git a/integration_tests/computation/math_test.go b/integration_tests/computation/basic_test.go similarity index 56% rename from integration_tests/computation/math_test.go rename to integration_tests/computation/basic_test.go index 5e56f5e..e675b1b 100644 --- a/integration_tests/computation/math_test.go +++ b/integration_tests/computation/basic_test.go @@ -1,12 +1,15 @@ package integration_tests import ( + "fmt" "github.com/hovsep/fmesh" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "github.com/hovsep/fmesh/port" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" + "os" + "strings" "testing" ) @@ -70,3 +73,55 @@ func Test_Math(t *testing.T) { }) } } + +func Test_Readme(t *testing.T) { + t.Run("readme test", func(t *testing.T) { + fm := fmesh.New("hello world"). + WithComponents( + component.New("concat"). + WithInputs("i1", "i2"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + word1 := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + word2 := inputs.ByName("i2").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(word1 + word2)) + return nil + }), + component.New("case"). + WithInputs("i1"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputString := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(strings.ToTitle(inputString))) + return nil + })). + WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) + + fm.Components().ByName("concat").Outputs().ByName("res").PipeTo( + fm.Components().ByName("case").Inputs().ByName("i1"), + ) + + // Init inputs + fm.Components().ByName("concat").Inputs().ByName("i1").PutSignals(signal.New("hello ")) + fm.Components().ByName("concat").Inputs().ByName("i2").PutSignals(signal.New("world !")) + + // Run the mesh + _, err := fm.Run() + + // Check for errors + if err != nil { + fmt.Println("F-Mesh returned an error") + os.Exit(1) + } + + //Extract results + results := fm.Components().ByName("case").Outputs().ByName("res").FirstSignalPayloadOrNil() + fmt.Printf("Result is :%v", results) + assert.Equal(t, "HELLO WORLD !", results) + }) +} From e53ebd0ac2de9445ce86407a211e89a6baa8a8d2 Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 4 Nov 2024 23:55:22 +0200 Subject: [PATCH 34/41] Rename chainable error to be more go idiomatic --- common/chainable.go | 11 +- component/activation_result.go | 6 +- component/collection.go | 22 ++-- component/component.go | 102 +++++++++--------- component/component_test.go | 10 +- cycle/cycle.go | 6 +- cycle/cycle_test.go | 2 +- cycle/group.go | 4 +- fmesh.go | 62 +++++------ fmesh_test.go | 8 +- .../error_handling/chainable_api_test.go | 22 ++-- port/collection.go | 72 ++++++------- port/collection_test.go | 10 +- port/group.go | 14 +-- port/group_test.go | 2 +- port/port.go | 60 +++++------ port/port_test.go | 44 ++++---- signal/group.go | 40 +++---- signal/group_test.go | 42 ++++---- signal/signal.go | 10 +- signal/signal_test.go | 4 +- 21 files changed, 278 insertions(+), 275 deletions(-) diff --git a/common/chainable.go b/common/chainable.go index 517cea9..756b468 100644 --- a/common/chainable.go +++ b/common/chainable.go @@ -4,19 +4,22 @@ type Chainable struct { err error } +// NewChainable initialises new chainable func NewChainable() *Chainable { return &Chainable{} } -func (c *Chainable) SetChainError(err error) { +// SetErr sets chainable error +func (c *Chainable) SetErr(err error) { c.err = err } -func (c *Chainable) HasChainError() bool { +// HasErr returns true when chainable has error +func (c *Chainable) HasErr() bool { return c.err != nil } -// @TODO: rename to Err() -func (c *Chainable) ChainError() error { +// Err returns chainable error +func (c *Chainable) Err() error { return c.err } diff --git a/component/activation_result.go b/component/activation_result.go index 2c78a6e..dbdb92a 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -182,8 +182,8 @@ func WantsToKeepInputs(activationResult *ActivationResult) bool { return activationResult.Code() == ActivationCodeWaitingForInputsKeep } -// WithChainError returns activation result with chain error -func (ar *ActivationResult) WithChainError(err error) *ActivationResult { - ar.SetChainError(err) +// WithErr returns activation result with chain error +func (ar *ActivationResult) WithErr(err error) *ActivationResult { + ar.SetErr(err) return ar } diff --git a/component/collection.go b/component/collection.go index c663d41..daef8dc 100644 --- a/component/collection.go +++ b/component/collection.go @@ -24,14 +24,14 @@ func NewCollection() *Collection { // ByName returns a component by its name func (c *Collection) ByName(name string) *Component { - if c.HasChainError() { - return New("").WithChainError(c.ChainError()) + if c.HasErr() { + return New("").WithErr(c.Err()) } component, ok := c.components[name] if !ok { - c.SetChainError(errors.New("component not found")) + c.SetErr(errors.New("component not found")) return nil } @@ -40,24 +40,24 @@ func (c *Collection) ByName(name string) *Component { // With adds components and returns the collection func (c *Collection) With(components ...*Component) *Collection { - if c.HasChainError() { + if c.HasErr() { return c } for _, component := range components { c.components[component.Name()] = component - if component.HasChainError() { - return c.WithChainError(component.ChainError()) + if component.HasErr() { + return c.WithErr(component.Err()) } } return c } -// WithChainError returns group with error -func (c *Collection) WithChainError(err error) *Collection { - c.SetChainError(err) +// WithErr returns group with error +func (c *Collection) WithErr(err error) *Collection { + c.SetErr(err) return c } @@ -67,8 +67,8 @@ func (c *Collection) Len() int { } func (c *Collection) Components() (ComponentsMap, error) { - if c.HasChainError() { - return nil, c.ChainError() + if c.HasErr() { + return nil, c.Err() } return c.components, nil } diff --git a/component/component.go b/component/component.go index ebacc34..7828040 100644 --- a/component/component.go +++ b/component/component.go @@ -38,7 +38,7 @@ func New(name string) *Component { // WithDescription sets a description func (c *Component) WithDescription(description string) *Component { - if c.HasChainError() { + if c.HasErr() { return c } @@ -48,11 +48,11 @@ func (c *Component) WithDescription(description string) *Component { // withInputPorts sets input ports collection func (c *Component) withInputPorts(collection *port.Collection) *Component { - if c.HasChainError() { + if c.HasErr() { return c } - if collection.HasChainError() { - return c.WithChainError(collection.ChainError()) + if collection.HasErr() { + return c.WithErr(collection.Err()) } c.inputs = collection return c @@ -60,11 +60,11 @@ func (c *Component) withInputPorts(collection *port.Collection) *Component { // withOutputPorts sets input ports collection func (c *Component) withOutputPorts(collection *port.Collection) *Component { - if c.HasChainError() { + if c.HasErr() { return c } - if collection.HasChainError() { - return c.WithChainError(collection.ChainError()) + if collection.HasErr() { + return c.WithErr(collection.Err()) } c.outputs = collection @@ -73,14 +73,14 @@ func (c *Component) withOutputPorts(collection *port.Collection) *Component { // WithInputs ads input ports func (c *Component) WithInputs(portNames ...string) *Component { - if c.HasChainError() { + if c.HasErr() { return c } ports, err := port.NewGroup(portNames...).Ports() if err != nil { - c.SetChainError(err) - return New("").WithChainError(c.ChainError()) + c.SetErr(err) + return New("").WithErr(c.Err()) } return c.withInputPorts(c.Inputs().With(ports...)) @@ -88,21 +88,21 @@ func (c *Component) WithInputs(portNames ...string) *Component { // WithOutputs adds output ports func (c *Component) WithOutputs(portNames ...string) *Component { - if c.HasChainError() { + if c.HasErr() { return c } ports, err := port.NewGroup(portNames...).Ports() if err != nil { - c.SetChainError(err) - return New("").WithChainError(c.ChainError()) + c.SetErr(err) + return New("").WithErr(c.Err()) } return c.withOutputPorts(c.Outputs().With(ports...)) } // WithInputsIndexed creates multiple prefixed ports func (c *Component) WithInputsIndexed(prefix string, startIndex int, endIndex int) *Component { - if c.HasChainError() { + if c.HasErr() { return c } @@ -111,7 +111,7 @@ func (c *Component) WithInputsIndexed(prefix string, startIndex int, endIndex in // WithOutputsIndexed creates multiple prefixed ports func (c *Component) WithOutputsIndexed(prefix string, startIndex int, endIndex int) *Component { - if c.HasChainError() { + if c.HasErr() { return c } @@ -120,7 +120,7 @@ func (c *Component) WithOutputsIndexed(prefix string, startIndex int, endIndex i // WithActivationFunc sets activation function func (c *Component) WithActivationFunc(f ActivationFunc) *Component { - if c.HasChainError() { + if c.HasErr() { return c } @@ -130,7 +130,7 @@ func (c *Component) WithActivationFunc(f ActivationFunc) *Component { // WithLabels sets labels and returns the component func (c *Component) WithLabels(labels common.LabelsCollection) *Component { - if c.HasChainError() { + if c.HasErr() { return c } c.LabeledEntity.SetLabels(labels) @@ -139,8 +139,8 @@ func (c *Component) WithLabels(labels common.LabelsCollection) *Component { // Inputs getter func (c *Component) Inputs() *port.Collection { - if c.HasChainError() { - return port.NewCollection().WithChainError(c.ChainError()) + if c.HasErr() { + return port.NewCollection().WithErr(c.Err()) } return c.inputs @@ -148,8 +148,8 @@ func (c *Component) Inputs() *port.Collection { // Outputs getter func (c *Component) Outputs() *port.Collection { - if c.HasChainError() { - return port.NewCollection().WithChainError(c.ChainError()) + if c.HasErr() { + return port.NewCollection().WithErr(c.Err()) } return c.outputs @@ -157,33 +157,33 @@ func (c *Component) Outputs() *port.Collection { // OutputByName is shortcut method func (c *Component) OutputByName(name string) *port.Port { - if c.HasChainError() { - return port.New("").WithChainError(c.ChainError()) + if c.HasErr() { + return port.New("").WithErr(c.Err()) } outputPort := c.Outputs().ByName(name) - if outputPort.HasChainError() { - c.SetChainError(outputPort.ChainError()) - return port.New("").WithChainError(c.ChainError()) + if outputPort.HasErr() { + c.SetErr(outputPort.Err()) + return port.New("").WithErr(c.Err()) } return outputPort } // InputByName is shortcut method func (c *Component) InputByName(name string) *port.Port { - if c.HasChainError() { - return port.New("").WithChainError(c.ChainError()) + if c.HasErr() { + return port.New("").WithErr(c.Err()) } inputPort := c.Inputs().ByName(name) - if inputPort.HasChainError() { - c.SetChainError(inputPort.ChainError()) - return port.New("").WithChainError(c.ChainError()) + if inputPort.HasErr() { + c.SetErr(inputPort.Err()) + return port.New("").WithErr(c.Err()) } return inputPort } // hasActivationFunction checks when activation function is set func (c *Component) hasActivationFunction() bool { - if c.HasChainError() { + if c.HasErr() { return false } @@ -192,26 +192,26 @@ func (c *Component) hasActivationFunction() bool { // propagateChainErrors propagates up all chain errors that might have not been propagated yet func (c *Component) propagateChainErrors() { - if c.Inputs().HasChainError() { - c.SetChainError(c.Inputs().ChainError()) + if c.Inputs().HasErr() { + c.SetErr(c.Inputs().Err()) return } - if c.Outputs().HasChainError() { - c.SetChainError(c.Outputs().ChainError()) + if c.Outputs().HasErr() { + c.SetErr(c.Outputs().Err()) return } for _, p := range c.Inputs().PortsOrNil() { - if p.HasChainError() { - c.SetChainError(p.ChainError()) + if p.HasErr() { + c.SetErr(p.Err()) return } } for _, p := range c.Outputs().PortsOrNil() { - if p.HasChainError() { - c.SetChainError(p.ChainError()) + if p.HasErr() { + c.SetErr(p.Err()) return } } @@ -223,8 +223,8 @@ func (c *Component) propagateChainErrors() { func (c *Component) MaybeActivate() (activationResult *ActivationResult) { c.propagateChainErrors() - if c.HasChainError() { - activationResult = NewActivationResult(c.Name()).WithChainError(c.ChainError()) + if c.HasErr() { + activationResult = NewActivationResult(c.Name()).WithErr(c.Err()) return } @@ -265,19 +265,19 @@ func (c *Component) MaybeActivate() (activationResult *ActivationResult) { // FlushOutputs pushed signals out of the component outputs to pipes and clears outputs func (c *Component) FlushOutputs() *Component { - if c.HasChainError() { + if c.HasErr() { return c } ports, err := c.Outputs().Ports() if err != nil { - c.SetChainError(err) - return New("").WithChainError(c.ChainError()) + c.SetErr(err) + return New("").WithErr(c.Err()) } for _, out := range ports { out = out.Flush() - if out.HasChainError() { - return c.WithChainError(out.ChainError()) + if out.HasErr() { + return c.WithErr(out.Err()) } } return c @@ -285,15 +285,15 @@ func (c *Component) FlushOutputs() *Component { // ClearInputs clears all input ports func (c *Component) ClearInputs() *Component { - if c.HasChainError() { + if c.HasErr() { return c } c.Inputs().Clear() return c } -// WithChainError returns component with error -func (c *Component) WithChainError(err error) *Component { - c.SetChainError(err) +// WithErr returns component with error +func (c *Component) WithErr(err error) *Component { + c.SetErr(err) return c } diff --git a/component/component_test.go b/component/component_test.go index 26f781d..b7c4e22 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -92,7 +92,7 @@ func TestComponent_FlushOutputs(t *testing.T) { name: "with chain error", getComponent: func() *Component { sink := port.New("sink") - c := New("c").WithOutputs("o1").WithChainError(errors.New("some error")) + c := New("c").WithOutputs("o1").WithErr(errors.New("some error")) //Lines below are ignored as error immediately propagates up to component level c.Outputs().ByName("o1").PipeTo(sink) c.Outputs().ByName("o1").PutSignals(signal.New("signal from component with chain error")) @@ -527,23 +527,23 @@ func TestComponent_MaybeActivate(t *testing.T) { name: "with chain error from input port", getComponent: func() *Component { c := New("c").WithInputs("i1").WithOutputs("o1") - c.Inputs().With(port.New("p").WithChainError(errors.New("some error"))) + c.Inputs().With(port.New("p").WithErr(errors.New("some error"))) return c }, wantActivationResult: NewActivationResult("c"). WithActivationCode(ActivationCodeUndefined). - WithChainError(errors.New("some error")), + WithErr(errors.New("some error")), }, { name: "with chain error from output port", getComponent: func() *Component { c := New("c").WithInputs("i1").WithOutputs("o1") - c.Outputs().With(port.New("p").WithChainError(errors.New("some error"))) + c.Outputs().With(port.New("p").WithErr(errors.New("some error"))) return c }, wantActivationResult: NewActivationResult("c"). WithActivationCode(ActivationCodeUndefined). - WithChainError(errors.New("some error")), + WithErr(errors.New("some error")), }, } for _, tt := range tests { diff --git a/cycle/cycle.go b/cycle/cycle.go index eaf75d2..7051b76 100644 --- a/cycle/cycle.go +++ b/cycle/cycle.go @@ -59,8 +59,8 @@ func (cycle *Cycle) WithNumber(number int) *Cycle { return cycle } -// WithChainError returns cycle with error -func (cycle *Cycle) WithChainError(err error) *Cycle { - cycle.SetChainError(err) +// WithErr returns cycle with error +func (cycle *Cycle) WithErr(err error) *Cycle { + cycle.SetErr(err) return cycle } diff --git a/cycle/cycle_test.go b/cycle/cycle_test.go index c2e052b..6d6d82f 100644 --- a/cycle/cycle_test.go +++ b/cycle/cycle_test.go @@ -11,7 +11,7 @@ func TestNew(t *testing.T) { t.Run("happy path", func(t *testing.T) { cycle := New() assert.NotNil(t, cycle) - assert.False(t, cycle.HasChainError()) + assert.False(t, cycle.HasErr()) }) } diff --git a/cycle/group.go b/cycle/group.go index 5aba75c..a668c4e 100644 --- a/cycle/group.go +++ b/cycle/group.go @@ -37,8 +37,8 @@ func (g *Group) withCycles(cycles Cycles) *Group { // Cycles getter func (g *Group) Cycles() (Cycles, error) { - if g.HasChainError() { - return nil, g.ChainError() + if g.HasErr() { + return nil, g.Err() } return g.cycles, nil } diff --git a/fmesh.go b/fmesh.go index ec4f9b8..1ba982f 100644 --- a/fmesh.go +++ b/fmesh.go @@ -45,15 +45,15 @@ func New(name string) *FMesh { // Components getter func (fm *FMesh) Components() *component.Collection { - if fm.HasChainError() { - return component.NewCollection().WithChainError(fm.ChainError()) + if fm.HasErr() { + return component.NewCollection().WithErr(fm.Err()) } return fm.components } // WithDescription sets a description func (fm *FMesh) WithDescription(description string) *FMesh { - if fm.HasChainError() { + if fm.HasErr() { return fm } @@ -63,14 +63,14 @@ func (fm *FMesh) WithDescription(description string) *FMesh { // WithComponents adds components to f-mesh func (fm *FMesh) WithComponents(components ...*component.Component) *FMesh { - if fm.HasChainError() { + if fm.HasErr() { return fm } for _, c := range components { fm.components = fm.components.With(c) - if c.HasChainError() { - return fm.WithChainError(c.ChainError()) + if c.HasErr() { + return fm.WithErr(c.Err()) } } return fm @@ -78,7 +78,7 @@ func (fm *FMesh) WithComponents(components ...*component.Component) *FMesh { // WithConfig sets the configuration and returns the f-mesh func (fm *FMesh) WithConfig(config Config) *FMesh { - if fm.HasChainError() { + if fm.HasErr() { return fm } @@ -90,26 +90,26 @@ func (fm *FMesh) WithConfig(config Config) *FMesh { func (fm *FMesh) runCycle() *cycle.Cycle { newCycle := cycle.New() - if fm.HasChainError() { - return newCycle.WithChainError(fm.ChainError()) + if fm.HasErr() { + return newCycle.WithErr(fm.Err()) } if fm.Components().Len() == 0 { - fm.SetChainError(errors.New("failed to run cycle: no components found")) - return newCycle.WithChainError(fm.ChainError()) + fm.SetErr(errors.New("failed to run cycle: no components found")) + return newCycle.WithErr(fm.Err()) } var wg sync.WaitGroup components, err := fm.Components().Components() if err != nil { - fm.SetChainError(fmt.Errorf("failed to run cycle: %w", err)) - return newCycle.WithChainError(fm.ChainError()) + fm.SetErr(fmt.Errorf("failed to run cycle: %w", err)) + return newCycle.WithErr(fm.Err()) } for _, c := range components { - if c.HasChainError() { - fm.SetChainError(c.ChainError()) + if c.HasErr() { + fm.SetErr(c.Err()) } wg.Add(1) @@ -126,8 +126,8 @@ func (fm *FMesh) runCycle() *cycle.Cycle { //Bubble up chain errors from activation results for _, ar := range newCycle.ActivationResults() { - if ar.HasChainError() { - newCycle.SetChainError(ar.ChainError()) + if ar.HasErr() { + newCycle.SetErr(ar.Err()) break } } @@ -137,8 +137,8 @@ func (fm *FMesh) runCycle() *cycle.Cycle { // DrainComponents drains the data from activated components func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { - if fm.HasChainError() { - return fm.ChainError() + if fm.HasErr() { + return fm.Err() } components, err := fm.Components().Components() @@ -149,8 +149,8 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { for _, c := range components { activationResult := cycle.ActivationResults().ByComponentName(c.Name()) - if activationResult.HasChainError() { - return activationResult.ChainError() + if activationResult.HasErr() { + return activationResult.Err() } if !activationResult.Activated() { @@ -183,8 +183,8 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) func (fm *FMesh) Run() (cycle.Cycles, error) { - if fm.HasChainError() { - return nil, fm.ChainError() + if fm.HasErr() { + return nil, fm.Err() } allCycles := cycle.NewGroup() @@ -192,9 +192,9 @@ func (fm *FMesh) Run() (cycle.Cycles, error) { for { cycleResult := fm.runCycle().WithNumber(cycleNumber) - if cycleResult.HasChainError() { - fm.SetChainError(cycleResult.ChainError()) - return nil, fmt.Errorf("chain error occurred in cycle #%d : %w", cycleResult.Number(), cycleResult.ChainError()) + if cycleResult.HasErr() { + fm.SetErr(cycleResult.Err()) + return nil, fmt.Errorf("chain error occurred in cycle #%d : %w", cycleResult.Number(), cycleResult.Err()) } allCycles = allCycles.With(cycleResult) @@ -222,8 +222,8 @@ func (fm *FMesh) Run() (cycle.Cycles, error) { // mustStop defines when f-mesh must stop after activation cycle func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error, error) { - if fm.HasChainError() { - return false, fm.ChainError(), nil + if fm.HasErr() { + return false, fm.Err(), nil } if (fm.config.CyclesLimit > 0) && (cycleResult.Number() > fm.config.CyclesLimit) { @@ -254,8 +254,8 @@ func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error, error) { } } -// WithChainError returns f-mesh with error -func (fm *FMesh) WithChainError(err error) *FMesh { - fm.SetChainError(err) +// WithErr returns f-mesh with error +func (fm *FMesh) WithErr(err error) *FMesh { + fm.SetErr(err) return fm } diff --git a/fmesh_test.go b/fmesh_test.go index 0791d25..d8f6224 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -651,11 +651,11 @@ func TestFMesh_runCycle(t *testing.T) { } cycleResult := tt.fm.runCycle() if tt.wantError { - assert.True(t, cycleResult.HasChainError()) - assert.Error(t, cycleResult.ChainError()) + assert.True(t, cycleResult.HasErr()) + assert.Error(t, cycleResult.Err()) } else { - assert.False(t, cycleResult.HasChainError()) - assert.NoError(t, cycleResult.ChainError()) + assert.False(t, cycleResult.HasErr()) + assert.NoError(t, cycleResult.Err()) assert.Equal(t, tt.want, cycleResult) } }) diff --git a/integration_tests/error_handling/chainable_api_test.go b/integration_tests/error_handling/chainable_api_test.go index 24880ba..36d60c2 100644 --- a/integration_tests/error_handling/chainable_api_test.go +++ b/integration_tests/error_handling/chainable_api_test.go @@ -20,14 +20,14 @@ func Test_Signal(t *testing.T) { test: func(t *testing.T) { sig := signal.New(123) _, err := sig.Payload() - assert.False(t, sig.HasChainError()) + assert.False(t, sig.HasErr()) assert.NoError(t, err) _ = sig.PayloadOrDefault(555) - assert.False(t, sig.HasChainError()) + assert.False(t, sig.HasErr()) _ = sig.PayloadOrNil() - assert.False(t, sig.HasChainError()) + assert.False(t, sig.HasErr()) }, }, { @@ -36,8 +36,8 @@ func Test_Signal(t *testing.T) { emptyGroup := signal.NewGroup() sig := emptyGroup.First() - assert.True(t, sig.HasChainError()) - assert.Error(t, sig.ChainError()) + assert.True(t, sig.HasErr()) + assert.Error(t, sig.Err()) _, err := sig.Payload() assert.Error(t, err) @@ -72,7 +72,7 @@ func Test_FMesh(t *testing.T) { fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) _, err := fm.Run() - assert.False(t, fm.HasChainError()) + assert.False(t, fm.HasErr()) assert.NoError(t, err) }, }, @@ -89,14 +89,14 @@ func Test_FMesh(t *testing.T) { outputs.ByName("sum").PutSignals(signal.New(num1 + num2)) return nil }). - WithChainError(errors.New("some error in component")), + WithErr(errors.New("some error in component")), ) fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10)) fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) _, err := fm.Run() - assert.True(t, fm.HasChainError()) + assert.True(t, fm.HasErr()) assert.Error(t, err) assert.EqualError(t, err, "some error in component") }, @@ -121,7 +121,7 @@ func Test_FMesh(t *testing.T) { fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) _, err := fm.Run() - assert.True(t, fm.HasChainError()) + assert.True(t, fm.HasErr()) assert.Error(t, err) assert.EqualError(t, err, "chain error occurred in cycle #0 : port not found") }, @@ -139,11 +139,11 @@ func Test_FMesh(t *testing.T) { }), ) - fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10).WithChainError(errors.New("some error in input signal"))) + fm.Components().ByName("c1").InputByName("num1").PutSignals(signal.New(10).WithErr(errors.New("some error in input signal"))) fm.Components().ByName("c1").InputByName("num2").PutSignals(signal.New(5)) _, err := fm.Run() - assert.True(t, fm.HasChainError()) + assert.True(t, fm.HasErr()) assert.Error(t, err) assert.EqualError(t, err, "chain error occurred in cycle #0 : some error in input signal") }, diff --git a/port/collection.go b/port/collection.go index 5541d82..02f0f2d 100644 --- a/port/collection.go +++ b/port/collection.go @@ -29,21 +29,21 @@ func NewCollection() *Collection { // ByName returns a port by its name func (collection *Collection) ByName(name string) *Port { - if collection.HasChainError() { - return New("").WithChainError(collection.ChainError()) + if collection.HasErr() { + return New("").WithErr(collection.Err()) } port, ok := collection.ports[name] if !ok { - collection.SetChainError(ErrPortNotFoundInCollection) - return New("").WithChainError(collection.ChainError()) + collection.SetErr(ErrPortNotFoundInCollection) + return New("").WithErr(collection.Err()) } return port } // ByNames returns multiple ports by their names func (collection *Collection) ByNames(names ...string) *Collection { - if collection.HasChainError() { - return NewCollection().WithChainError(collection.ChainError()) + if collection.HasErr() { + return NewCollection().WithErr(collection.Err()) } //Preserve collection config @@ -60,7 +60,7 @@ func (collection *Collection) ByNames(names ...string) *Collection { // AnyHasSignals returns true if at least one port in collection has signals func (collection *Collection) AnyHasSignals() bool { - if collection.HasChainError() { + if collection.HasErr() { return false } @@ -75,7 +75,7 @@ func (collection *Collection) AnyHasSignals() bool { // AllHaveSignals returns true when all ports in collection have signals func (collection *Collection) AllHaveSignals() bool { - if collection.HasChainError() { + if collection.HasErr() { return false } @@ -90,14 +90,14 @@ func (collection *Collection) AllHaveSignals() bool { // PutSignals adds buffer to every port in collection func (collection *Collection) PutSignals(signals ...*signal.Signal) *Collection { - if collection.HasChainError() { - return NewCollection().WithChainError(collection.ChainError()) + if collection.HasErr() { + return NewCollection().WithErr(collection.Err()) } for _, p := range collection.ports { p.PutSignals(signals...) - if p.HasChainError() { - return collection.WithChainError(p.ChainError()) + if p.HasErr() { + return collection.WithErr(p.Err()) } } @@ -109,8 +109,8 @@ func (collection *Collection) Clear() *Collection { for _, p := range collection.ports { p.Clear() - if p.HasChainError() { - return collection.WithChainError(p.ChainError()) + if p.HasErr() { + return collection.WithErr(p.Err()) } } return collection @@ -118,15 +118,15 @@ func (collection *Collection) Clear() *Collection { // Flush flushes all ports in collection func (collection *Collection) Flush() *Collection { - if collection.HasChainError() { - return NewCollection().WithChainError(collection.ChainError()) + if collection.HasErr() { + return NewCollection().WithErr(collection.Err()) } for _, p := range collection.ports { p = p.Flush() - if p.HasChainError() { - return collection.WithChainError(p.ChainError()) + if p.HasErr() { + return collection.WithErr(p.Err()) } } return collection @@ -137,8 +137,8 @@ func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { for _, p := range collection.ports { p = p.PipeTo(destPorts...) - if p.HasChainError() { - return collection.WithChainError(p.ChainError()) + if p.HasErr() { + return collection.WithErr(p.Err()) } } @@ -147,13 +147,13 @@ func (collection *Collection) PipeTo(destPorts ...*Port) *Collection { // With adds ports to collection and returns it func (collection *Collection) With(ports ...*Port) *Collection { - if collection.HasChainError() { + if collection.HasErr() { return collection } for _, port := range ports { - if port.HasChainError() { - return collection.WithChainError(port.ChainError()) + if port.HasErr() { + return collection.WithErr(port.Err()) } port.AddLabels(collection.defaultLabels) collection.ports[port.Name()] = port @@ -164,30 +164,30 @@ func (collection *Collection) With(ports ...*Port) *Collection { // WithIndexed creates ports with names like "o1","o2","o3" and so on func (collection *Collection) WithIndexed(prefix string, startIndex int, endIndex int) *Collection { - if collection.HasChainError() { + if collection.HasErr() { return collection } indexedPorts, err := NewIndexedGroup(prefix, startIndex, endIndex).Ports() if err != nil { - collection.SetChainError(err) - return NewCollection().WithChainError(collection.ChainError()) + collection.SetErr(err) + return NewCollection().WithErr(collection.Err()) } return collection.With(indexedPorts...) } // Signals returns all signals of all ports in the collection func (collection *Collection) Signals() *signal.Group { - if collection.HasChainError() { - return signal.NewGroup().WithChainError(collection.ChainError()) + if collection.HasErr() { + return signal.NewGroup().WithErr(collection.Err()) } group := signal.NewGroup() for _, p := range collection.ports { signals, err := p.Buffer().Signals() if err != nil { - collection.SetChainError(err) - return signal.NewGroup().WithChainError(collection.ChainError()) + collection.SetErr(err) + return signal.NewGroup().WithErr(collection.Err()) } group = group.With(signals...) } @@ -197,8 +197,8 @@ func (collection *Collection) Signals() *signal.Group { // Ports getter // @TODO:maybe better to hide all errors within chainable and ask user to check error ? func (collection *Collection) Ports() (PortMap, error) { - if collection.HasChainError() { - return nil, collection.ChainError() + if collection.HasErr() { + return nil, collection.Err() } return collection.ports, nil } @@ -210,7 +210,7 @@ func (collection *Collection) PortsOrNil() PortMap { // PortsOrDefault returns ports or default in case of any error func (collection *Collection) PortsOrDefault(defaultPorts PortMap) PortMap { - if collection.HasChainError() { + if collection.HasErr() { return defaultPorts } @@ -221,9 +221,9 @@ func (collection *Collection) PortsOrDefault(defaultPorts PortMap) PortMap { return ports } -// WithChainError returns group with error -func (collection *Collection) WithChainError(err error) *Collection { - collection.SetChainError(err) +// WithErr returns group with error +func (collection *Collection) WithErr(err error) *Collection { + collection.SetErr(err) return collection } diff --git a/port/collection_test.go b/port/collection_test.go index 63ffe0d..fd4ebdc 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -107,15 +107,15 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "p3", }, - want: New("").WithChainError(ErrPortNotFoundInCollection), + want: New("").WithErr(ErrPortNotFoundInCollection), }, { name: "with chain error", - collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithChainError(errors.New("some error")), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithErr(errors.New("some error")), args: args{ name: "p1", }, - want: New("").WithChainError(errors.New("some error")), + want: New("").WithErr(errors.New("some error")), }, } for _, tt := range tests { @@ -170,11 +170,11 @@ func TestCollection_ByNames(t *testing.T) { }, { name: "with chain error", - collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithChainError(errors.New("some error")), + collection: NewCollection().With(NewGroup("p1", "p2").PortsOrNil()...).WithErr(errors.New("some error")), args: args{ names: []string{"p1", "p2"}, }, - want: NewCollection().WithChainError(errors.New("some error")), + want: NewCollection().WithErr(errors.New("some error")), }, } for _, tt := range tests { diff --git a/port/group.go b/port/group.go index 3949126..58080f1 100644 --- a/port/group.go +++ b/port/group.go @@ -31,7 +31,7 @@ func NewGroup(names ...string) *Group { // NOTE: endIndex is inclusive, e.g. NewIndexedGroup("p", 0, 0) will create one port with name "p0" func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { if startIndex > endIndex { - return NewGroup().WithChainError(ErrInvalidRangeForIndexedGroup) + return NewGroup().WithErr(ErrInvalidRangeForIndexedGroup) } ports := make(Ports, endIndex-startIndex+1) @@ -45,7 +45,7 @@ func NewIndexedGroup(prefix string, startIndex int, endIndex int) *Group { // With adds ports to group func (g *Group) With(ports ...*Port) *Group { - if g.HasChainError() { + if g.HasErr() { return g } @@ -66,8 +66,8 @@ func (g *Group) withPorts(ports Ports) *Group { // Ports getter func (g *Group) Ports() (Ports, error) { - if g.HasChainError() { - return nil, g.ChainError() + if g.HasErr() { + return nil, g.Err() } return g.ports, nil } @@ -86,9 +86,9 @@ func (g *Group) PortsOrDefault(defaultPorts Ports) Ports { return ports } -// WithChainError returns group with error -func (g *Group) WithChainError(err error) *Group { - g.SetChainError(err) +// WithErr returns group with error +func (g *Group) WithErr(err error) *Group { + g.SetErr(err) return g } diff --git a/port/group_test.go b/port/group_test.go index 36026e2..a50ee82 100644 --- a/port/group_test.go +++ b/port/group_test.go @@ -80,7 +80,7 @@ func TestNewIndexedGroup(t *testing.T) { startIndex: 999, endIndex: 5, }, - want: NewGroup().WithChainError(ErrInvalidRangeForIndexedGroup), + want: NewGroup().WithErr(ErrInvalidRangeForIndexedGroup), }, } for _, tt := range tests { diff --git a/port/port.go b/port/port.go index b2f2aa5..8186d59 100644 --- a/port/port.go +++ b/port/port.go @@ -36,8 +36,8 @@ func New(name string) *Port { // Buffer getter // @TODO: maybe we can hide this and return signals to user code func (p *Port) Buffer() *signal.Group { - if p.HasChainError() { - return signal.NewGroup().WithChainError(p.ChainError()) + if p.HasErr() { + return signal.NewGroup().WithErr(p.Err()) } return p.buffer } @@ -45,21 +45,21 @@ func (p *Port) Buffer() *signal.Group { // Pipes getter // @TODO maybe better to return []*Port directly func (p *Port) Pipes() *Group { - if p.HasChainError() { - return NewGroup().WithChainError(p.ChainError()) + if p.HasErr() { + return NewGroup().WithErr(p.Err()) } return p.pipes } // withBuffer sets buffer field func (p *Port) withBuffer(buffer *signal.Group) *Port { - if p.HasChainError() { + if p.HasErr() { return p } - if buffer.HasChainError() { - p.SetChainError(buffer.ChainError()) - return New("").WithChainError(p.ChainError()) + if buffer.HasErr() { + p.SetErr(buffer.Err()) + return New("").WithErr(p.Err()) } p.buffer = buffer return p @@ -68,7 +68,7 @@ func (p *Port) withBuffer(buffer *signal.Group) *Port { // PutSignals adds signals to buffer // @TODO: rename func (p *Port) PutSignals(signals ...*signal.Signal) *Port { - if p.HasChainError() { + if p.HasErr() { return p } return p.withBuffer(p.Buffer().With(signals...)) @@ -76,7 +76,7 @@ func (p *Port) PutSignals(signals ...*signal.Signal) *Port { // WithSignals puts buffer and returns the port func (p *Port) WithSignals(signals ...*signal.Signal) *Port { - if p.HasChainError() { + if p.HasErr() { return p } @@ -85,18 +85,18 @@ func (p *Port) WithSignals(signals ...*signal.Signal) *Port { // WithSignalGroups puts groups of buffer and returns the port func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { - if p.HasChainError() { + if p.HasErr() { return p } for _, group := range signalGroups { signals, err := group.Signals() if err != nil { - p.SetChainError(err) - return New("").WithChainError(p.ChainError()) + p.SetErr(err) + return New("").WithErr(p.Err()) } p.PutSignals(signals...) - if p.HasChainError() { - return New("").WithChainError(p.ChainError()) + if p.HasErr() { + return New("").WithErr(p.Err()) } } @@ -105,7 +105,7 @@ func (p *Port) WithSignalGroups(signalGroups ...*signal.Group) *Port { // Clear removes all signals from the port buffer func (p *Port) Clear() *Port { - if p.HasChainError() { + if p.HasErr() { return p } return p.withBuffer(signal.NewGroup()) @@ -114,7 +114,7 @@ func (p *Port) Clear() *Port { // Flush pushes buffer to pipes and clears the port // @TODO: hide this method from user func (p *Port) Flush() *Port { - if p.HasChainError() { + if p.HasErr() { return p } @@ -126,16 +126,16 @@ func (p *Port) Flush() *Port { pipes, err := p.pipes.Ports() if err != nil { - p.SetChainError(err) - return New("").WithChainError(p.ChainError()) + p.SetErr(err) + return New("").WithErr(p.Err()) } for _, outboundPort := range pipes { //Fan-Out err = ForwardSignals(p, outboundPort) if err != nil { - p.SetChainError(err) - return New("").WithChainError(p.ChainError()) + p.SetErr(err) + return New("").WithErr(p.Err()) } } return p.Clear() @@ -154,14 +154,14 @@ func (p *Port) HasPipes() bool { // PipeTo creates one or multiple pipes to other port(s) // @TODO: hide this method from AF func (p *Port) PipeTo(destPorts ...*Port) *Port { - if p.HasChainError() { + if p.HasErr() { return p } for _, destPort := range destPorts { if err := validatePipe(p, destPort); err != nil { - p.SetChainError(fmt.Errorf("pipe validation failed: %w", err)) - return New("").WithChainError(p.ChainError()) + p.SetErr(fmt.Errorf("pipe validation failed: %w", err)) + return New("").WithErr(p.Err()) } p.pipes = p.pipes.With(destPort) } @@ -188,7 +188,7 @@ func validatePipe(srcPort *Port, dstPort *Port) error { // WithLabels sets labels and returns the port func (p *Port) WithLabels(labels common.LabelsCollection) *Port { - if p.HasChainError() { + if p.HasErr() { return p } @@ -203,15 +203,15 @@ func ForwardSignals(source *Port, dest *Port) error { return err } dest.PutSignals(signals...) - if dest.HasChainError() { - return dest.ChainError() + if dest.HasErr() { + return dest.Err() } return nil } -// WithChainError returns port with error -func (p *Port) WithChainError(err error) *Port { - p.SetChainError(err) +// WithErr returns port with error +func (p *Port) WithErr(err error) *Port { + p.SetErr(err) return p } diff --git a/port/port_test.go b/port/port_test.go index f503a4c..e11bd31 100644 --- a/port/port_test.go +++ b/port/port_test.go @@ -50,8 +50,8 @@ func TestPort_Buffer(t *testing.T) { }, { name: "with chain error", - port: New("p").WithChainError(errors.New("some error")), - want: signal.NewGroup().WithChainError(errors.New("some error")), + port: New("p").WithErr(errors.New("some error")), + want: signal.NewGroup().WithErr(errors.New("some error")), }, } for _, tt := range tests { @@ -118,8 +118,8 @@ func TestPort_PipeTo(t *testing.T) { toPorts: Ports{inputPorts.ByName("in2"), inputPorts.ByName("in3")}, }, assertions: func(t *testing.T, portAfter *Port) { - assert.False(t, portAfter.HasChainError()) - assert.NoError(t, portAfter.ChainError()) + assert.False(t, portAfter.HasErr()) + assert.NoError(t, portAfter.Err()) assert.Equal(t, 2, portAfter.Pipes().Len()) }, }, @@ -131,8 +131,8 @@ func TestPort_PipeTo(t *testing.T) { }, assertions: func(t *testing.T, portAfter *Port) { assert.Equal(t, "", portAfter.Name()) - assert.True(t, portAfter.HasChainError()) - assert.Error(t, portAfter.ChainError()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, }, { @@ -143,8 +143,8 @@ func TestPort_PipeTo(t *testing.T) { }, assertions: func(t *testing.T, portAfter *Port) { assert.Equal(t, "", portAfter.Name()) - assert.True(t, portAfter.HasChainError()) - assert.Error(t, portAfter.ChainError()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, }, { @@ -157,8 +157,8 @@ func TestPort_PipeTo(t *testing.T) { }, assertions: func(t *testing.T, portAfter *Port) { assert.Equal(t, "", portAfter.Name()) - assert.True(t, portAfter.HasChainError()) - assert.Error(t, portAfter.ChainError()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, }, { @@ -169,8 +169,8 @@ func TestPort_PipeTo(t *testing.T) { }, assertions: func(t *testing.T, portAfter *Port) { assert.Equal(t, "", portAfter.Name()) - assert.True(t, portAfter.HasChainError()) - assert.Error(t, portAfter.ChainError()) + assert.True(t, portAfter.HasErr()) + assert.Error(t, portAfter.Err()) }, }, } @@ -249,20 +249,20 @@ func TestPort_PutSignals(t *testing.T) { port: New("p"), assertions: func(t *testing.T, portAfter *Port) { assert.Zero(t, portAfter.Buffer().Len()) - assert.True(t, portAfter.Buffer().HasChainError()) + assert.True(t, portAfter.Buffer().HasErr()) }, args: args{ - signals: signal.Signals{signal.New(111).WithChainError(errors.New("some error in signal"))}, + signals: signal.Signals{signal.New(111).WithErr(errors.New("some error in signal"))}, }, }, { name: "with chain error", - port: New("p").WithChainError(errors.New("some error in port")), + port: New("p").WithErr(errors.New("some error in port")), args: args{ signals: signal.Signals{signal.New(123)}, }, assertions: func(t *testing.T, portAfter *Port) { - assert.True(t, portAfter.HasChainError()) + assert.True(t, portAfter.HasErr()) assert.Zero(t, portAfter.Buffer().Len()) }, }, @@ -501,8 +501,8 @@ func TestPort_Pipes(t *testing.T) { }, { name: "with chain error", - port: New("p").WithChainError(errors.New("some error")), - want: NewGroup().WithChainError(errors.New("some error")), + port: New("p").WithErr(errors.New("some error")), + want: NewGroup().WithErr(errors.New("some error")), }, } for _, tt := range tests { @@ -521,22 +521,22 @@ func TestPort_ShortcutGetters(t *testing.T) { }) t.Run("FirstSignalPayloadOrNil", func(t *testing.T) { - port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) assert.Nil(t, port.FirstSignalPayloadOrNil()) }) t.Run("FirstSignalPayloadOrDefault", func(t *testing.T) { - port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) assert.Equal(t, 888, port.FirstSignalPayloadOrDefault(888)) }) t.Run("AllSignalsOrNil", func(t *testing.T) { - port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) assert.Nil(t, port.AllSignalsOrNil()) }) t.Run("AllSignalsOrDefault", func(t *testing.T) { - port := New("p").WithSignals(signal.New(123).WithChainError(errors.New("some error"))) + port := New("p").WithSignals(signal.New(123).WithErr(errors.New("some error"))) assert.Equal(t, signal.NewGroup(999).SignalsOrNil(), port.AllSignalsOrDefault(signal.NewGroup(999).SignalsOrNil())) }) } diff --git a/signal/group.go b/signal/group.go index e3fe2c3..86e2f5e 100644 --- a/signal/group.go +++ b/signal/group.go @@ -27,13 +27,13 @@ func NewGroup(payloads ...any) *Group { // First returns the first signal in the group func (g *Group) First() *Signal { - if g.HasChainError() { - return New(nil).WithChainError(g.ChainError()) + if g.HasErr() { + return New(nil).WithErr(g.Err()) } if len(g.signals) == 0 { - g.SetChainError(ErrNoSignalsInGroup) - return New(nil).WithChainError(g.ChainError()) + g.SetErr(ErrNoSignalsInGroup) + return New(nil).WithErr(g.Err()) } return g.signals[0] @@ -41,8 +41,8 @@ func (g *Group) First() *Signal { // FirstPayload returns the first signal payload func (g *Group) FirstPayload() (any, error) { - if g.HasChainError() { - return nil, g.ChainError() + if g.HasErr() { + return nil, g.Err() } return g.First().Payload() @@ -50,8 +50,8 @@ func (g *Group) FirstPayload() (any, error) { // AllPayloads returns a slice with all payloads of the all signals in the group func (g *Group) AllPayloads() ([]any, error) { - if g.HasChainError() { - return nil, g.ChainError() + if g.HasErr() { + return nil, g.Err() } all := make([]any, len(g.signals)) @@ -67,7 +67,7 @@ func (g *Group) AllPayloads() ([]any, error) { // With returns the group with added signals func (g *Group) With(signals ...*Signal) *Group { - if g.HasChainError() { + if g.HasErr() { // Do nothing, but propagate error return g } @@ -76,13 +76,13 @@ func (g *Group) With(signals ...*Signal) *Group { copy(newSignals, g.signals) for i, sig := range signals { if sig == nil { - g.SetChainError(ErrInvalidSignal) - return NewGroup().WithChainError(g.ChainError()) + g.SetErr(ErrInvalidSignal) + return NewGroup().WithErr(g.Err()) } - if sig.HasChainError() { - g.SetChainError(sig.ChainError()) - return NewGroup().WithChainError(g.ChainError()) + if sig.HasErr() { + g.SetErr(sig.Err()) + return NewGroup().WithErr(g.Err()) } newSignals[len(g.signals)+i] = sig @@ -93,7 +93,7 @@ func (g *Group) With(signals ...*Signal) *Group { // WithPayloads returns a group with added signals created from provided payloads func (g *Group) WithPayloads(payloads ...any) *Group { - if g.HasChainError() { + if g.HasErr() { // Do nothing, but propagate error return g } @@ -114,8 +114,8 @@ func (g *Group) withSignals(signals Signals) *Group { // Signals getter func (g *Group) Signals() (Signals, error) { - if g.HasChainError() { - return nil, g.ChainError() + if g.HasErr() { + return nil, g.Err() } return g.signals, nil } @@ -134,9 +134,9 @@ func (g *Group) SignalsOrDefault(defaultSignals Signals) Signals { return signals } -// WithChainError returns group with error -func (g *Group) WithChainError(err error) *Group { - g.SetChainError(err) +// WithErr returns group with error +func (g *Group) WithErr(err error) *Group { + g.SetErr(err) return g } diff --git a/signal/group_test.go b/signal/group_test.go index 585cf7b..6d9ef5b 100644 --- a/signal/group_test.go +++ b/signal/group_test.go @@ -77,7 +77,7 @@ func TestGroup_FirstPayload(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(3, 4, 5).WithChainError(errors.New("some error in chain")), + group: NewGroup(3, 4, 5).WithErr(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, @@ -114,13 +114,13 @@ func TestGroup_AllPayloads(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, { name: "with error in signal", - group: NewGroup().withSignals(Signals{New(33).WithChainError(errors.New("some error in signal"))}), + group: NewGroup().withSignals(Signals{New(33).WithErr(errors.New("some error in signal"))}), want: nil, wantErrorString: "some error in signal", }, @@ -189,28 +189,28 @@ func TestGroup_With(t *testing.T) { args: args{ signals: NewGroup(7, nil, 9).SignalsOrNil(), }, - want: NewGroup().WithChainError(errors.New("signal is invalid")), + want: NewGroup().WithErr(errors.New("signal is invalid")), }, { name: "with error in signal", - group: NewGroup(1, 2, 3).With(New(44).WithChainError(errors.New("some error in signal"))), + group: NewGroup(1, 2, 3).With(New(44).WithErr(errors.New("some error in signal"))), args: args{ signals: Signals{New(456)}, }, want: NewGroup(1, 2, 3). With(New(44). - WithChainError(errors.New("some error in signal"))). - WithChainError(errors.New("some error in signal")), // error propagated from signal to group + WithErr(errors.New("some error in signal"))). + WithErr(errors.New("some error in signal")), // error propagated from signal to group }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.group.With(tt.args.signals...) - if tt.want.HasChainError() { - assert.Error(t, got.ChainError()) - assert.EqualError(t, got.ChainError(), tt.want.ChainError().Error()) + if tt.want.HasErr() { + assert.Error(t, got.Err()) + assert.EqualError(t, got.Err(), tt.want.Err().Error()) } else { - assert.NoError(t, got.ChainError()) + assert.NoError(t, got.Err()) } assert.Equal(t, tt.want, got) }) @@ -276,7 +276,7 @@ func TestGroup_First(t *testing.T) { { name: "empty group", group: NewGroup(), - want: New(nil).WithChainError(errors.New("group has no signals")), + want: New(nil).WithErr(errors.New("group has no signals")), }, { name: "happy path", @@ -285,17 +285,17 @@ func TestGroup_First(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), - want: New(nil).WithChainError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), + want: New(nil).WithErr(errors.New("some error in chain")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.group.First() - if tt.want.HasChainError() { - assert.True(t, got.HasChainError()) - assert.Error(t, got.ChainError()) - assert.EqualError(t, got.ChainError(), tt.want.ChainError().Error()) + if tt.want.HasErr() { + assert.True(t, got.HasErr()) + assert.Error(t, got.Err()) + assert.EqualError(t, got.Err(), tt.want.Err().Error()) } else { assert.Equal(t, tt.want, tt.group.First()) } @@ -324,7 +324,7 @@ func TestGroup_Signals(t *testing.T) { }, { name: "with error in chain", - group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, @@ -370,7 +370,7 @@ func TestGroup_SignalsOrDefault(t *testing.T) { }, { name: "with error in chain and nil default", - group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), args: args{ defaultSignals: nil, }, @@ -378,7 +378,7 @@ func TestGroup_SignalsOrDefault(t *testing.T) { }, { name: "with error in chain and default", - group: NewGroup(1, 2, 3).WithChainError(errors.New("some error in chain")), + group: NewGroup(1, 2, 3).WithErr(errors.New("some error in chain")), args: args{ defaultSignals: Signals{New(4), New(5)}, }, diff --git a/signal/signal.go b/signal/signal.go index 44bec3a..f33b475 100644 --- a/signal/signal.go +++ b/signal/signal.go @@ -18,8 +18,8 @@ func New(payload any) *Signal { // Payload getter func (s *Signal) Payload() (any, error) { - if s.HasChainError() { - return nil, s.ChainError() + if s.HasErr() { + return nil, s.Err() } return s.payload[0], nil } @@ -38,8 +38,8 @@ func (s *Signal) PayloadOrDefault(defaultPayload any) any { return payload } -// WithChainError returns signal with error -func (s *Signal) WithChainError(err error) *Signal { - s.SetChainError(err) +// WithErr returns signal with error +func (s *Signal) WithErr(err error) *Signal { + s.SetErr(err) return s } diff --git a/signal/signal_test.go b/signal/signal_test.go index 8d38ffb..0bccc01 100644 --- a/signal/signal_test.go +++ b/signal/signal_test.go @@ -63,7 +63,7 @@ func TestSignal_Payload(t *testing.T) { }, { name: "with error in chain", - signal: New(123).WithChainError(errors.New("some error in chain")), + signal: New(123).WithErr(errors.New("some error in chain")), want: nil, wantErrorString: "some error in chain", }, @@ -95,7 +95,7 @@ func TestSignal_PayloadOrNil(t *testing.T) { }, { name: "nil returned", - signal: New(123).WithChainError(errors.New("some error in chain")), + signal: New(123).WithErr(errors.New("some error in chain")), want: nil, }, } From 87cddc313c8b7392d830cb3e51037762f2c76dca Mon Sep 17 00:00:00 2001 From: hovsep Date: Tue, 5 Nov 2024 00:24:03 +0200 Subject: [PATCH 35/41] Bugfix: fix looped components --- fmesh.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fmesh.go b/fmesh.go index 1ba982f..cddc366 100644 --- a/fmesh.go +++ b/fmesh.go @@ -170,13 +170,14 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { shouldClearInputs = !component.WantsToKeepInputs(activationResult) } + if shouldClearInputs { + c.ClearInputs() + } + if shouldFlushOutputs { c.FlushOutputs() } - if shouldClearInputs { - c.ClearInputs() - } } return nil } From 9f9ecbe8157dc753b9436f16c82c4252be6e11ac Mon Sep 17 00:00:00 2001 From: hovsep Date: Tue, 5 Nov 2024 00:24:41 +0200 Subject: [PATCH 36/41] Add fibonacci example --- examples/fibonacci.go | 57 ++++++ experiments/1_function_composition/common.go | 9 - experiments/1_function_composition/fmesh.go | 75 -------- .../1_function_composition/fmesh_component.go | 40 ---- .../1_function_composition/fmesh_pipe.go | 11 -- experiments/1_function_composition/go.mod | 3 - experiments/1_function_composition/main.go | 8 - .../1_function_composition/nested_funcs.go | 9 - experiments/1_function_composition/pipes.go | 29 --- .../2_multiple_inputs_outputs/component.go | 22 --- .../2_multiple_inputs_outputs/fmesh.go | 34 ---- experiments/2_multiple_inputs_outputs/go.mod | 3 - experiments/2_multiple_inputs_outputs/main.go | 102 ---------- experiments/2_multiple_inputs_outputs/pipe.go | 13 -- experiments/2_multiple_inputs_outputs/port.go | 19 -- .../2_multiple_inputs_outputs/ports.go | 23 --- .../3_concurrent_computing/component.go | 25 --- experiments/3_concurrent_computing/fmesh.go | 48 ----- experiments/3_concurrent_computing/go.mod | 3 - experiments/3_concurrent_computing/main.go | 130 ------------- experiments/3_concurrent_computing/pipe.go | 13 -- experiments/3_concurrent_computing/port.go | 19 -- experiments/3_concurrent_computing/ports.go | 23 --- experiments/4_multiplexed_pipes/component.go | 42 ----- experiments/4_multiplexed_pipes/fmesh.go | 47 ----- experiments/4_multiplexed_pipes/go.mod | 3 - experiments/4_multiplexed_pipes/main.go | 139 -------------- experiments/4_multiplexed_pipes/pipe.go | 8 - experiments/4_multiplexed_pipes/port.go | 62 ------ experiments/4_multiplexed_pipes/ports.go | 29 --- experiments/4_multiplexed_pipes/signal.go | 34 ---- experiments/5_error_handling/component.go | 58 ------ experiments/5_error_handling/errors.go | 32 ---- experiments/5_error_handling/fmesh.go | 72 ------- experiments/5_error_handling/go.mod | 3 - experiments/5_error_handling/main.go | 161 ---------------- experiments/5_error_handling/pipe.go | 8 - experiments/5_error_handling/port.go | 62 ------ experiments/5_error_handling/ports.go | 29 --- experiments/5_error_handling/signal.go | 34 ---- experiments/6_waiting_for_input/component.go | 91 --------- experiments/6_waiting_for_input/errors.go | 44 ----- experiments/6_waiting_for_input/fmesh.go | 72 ------- experiments/6_waiting_for_input/go.mod | 3 - experiments/6_waiting_for_input/main.go | 176 ------------------ experiments/6_waiting_for_input/pipe.go | 8 - experiments/6_waiting_for_input/port.go | 62 ------ experiments/6_waiting_for_input/ports.go | 51 ----- experiments/6_waiting_for_input/signal.go | 34 ---- experiments/7_loop/component.go | 91 --------- experiments/7_loop/errors.go | 44 ----- experiments/7_loop/fmesh.go | 72 ------- experiments/7_loop/go.mod | 3 - experiments/7_loop/main.go | 98 ---------- experiments/7_loop/pipe.go | 8 - experiments/7_loop/port.go | 62 ------ experiments/7_loop/ports.go | 51 ----- experiments/7_loop/signal.go | 34 ---- experiments/8_fibonacci/component.go | 91 --------- experiments/8_fibonacci/errors.go | 44 ----- experiments/8_fibonacci/fmesh.go | 72 ------- experiments/8_fibonacci/go.mod | 3 - experiments/8_fibonacci/main.go | 75 -------- experiments/8_fibonacci/pipe.go | 8 - experiments/8_fibonacci/port.go | 62 ------ experiments/8_fibonacci/ports.go | 51 ----- experiments/8_fibonacci/signal.go | 34 ---- 67 files changed, 57 insertions(+), 2928 deletions(-) create mode 100644 examples/fibonacci.go delete mode 100644 experiments/1_function_composition/common.go delete mode 100644 experiments/1_function_composition/fmesh.go delete mode 100644 experiments/1_function_composition/fmesh_component.go delete mode 100644 experiments/1_function_composition/fmesh_pipe.go delete mode 100644 experiments/1_function_composition/go.mod delete mode 100644 experiments/1_function_composition/main.go delete mode 100644 experiments/1_function_composition/nested_funcs.go delete mode 100644 experiments/1_function_composition/pipes.go delete mode 100644 experiments/2_multiple_inputs_outputs/component.go delete mode 100644 experiments/2_multiple_inputs_outputs/fmesh.go delete mode 100644 experiments/2_multiple_inputs_outputs/go.mod delete mode 100644 experiments/2_multiple_inputs_outputs/main.go delete mode 100644 experiments/2_multiple_inputs_outputs/pipe.go delete mode 100644 experiments/2_multiple_inputs_outputs/port.go delete mode 100644 experiments/2_multiple_inputs_outputs/ports.go delete mode 100644 experiments/3_concurrent_computing/component.go delete mode 100644 experiments/3_concurrent_computing/fmesh.go delete mode 100644 experiments/3_concurrent_computing/go.mod delete mode 100644 experiments/3_concurrent_computing/main.go delete mode 100644 experiments/3_concurrent_computing/pipe.go delete mode 100644 experiments/3_concurrent_computing/port.go delete mode 100644 experiments/3_concurrent_computing/ports.go delete mode 100644 experiments/4_multiplexed_pipes/component.go delete mode 100644 experiments/4_multiplexed_pipes/fmesh.go delete mode 100644 experiments/4_multiplexed_pipes/go.mod delete mode 100644 experiments/4_multiplexed_pipes/main.go delete mode 100644 experiments/4_multiplexed_pipes/pipe.go delete mode 100644 experiments/4_multiplexed_pipes/port.go delete mode 100644 experiments/4_multiplexed_pipes/ports.go delete mode 100644 experiments/4_multiplexed_pipes/signal.go delete mode 100644 experiments/5_error_handling/component.go delete mode 100644 experiments/5_error_handling/errors.go delete mode 100644 experiments/5_error_handling/fmesh.go delete mode 100644 experiments/5_error_handling/go.mod delete mode 100644 experiments/5_error_handling/main.go delete mode 100644 experiments/5_error_handling/pipe.go delete mode 100644 experiments/5_error_handling/port.go delete mode 100644 experiments/5_error_handling/ports.go delete mode 100644 experiments/5_error_handling/signal.go delete mode 100644 experiments/6_waiting_for_input/component.go delete mode 100644 experiments/6_waiting_for_input/errors.go delete mode 100644 experiments/6_waiting_for_input/fmesh.go delete mode 100644 experiments/6_waiting_for_input/go.mod delete mode 100644 experiments/6_waiting_for_input/main.go delete mode 100644 experiments/6_waiting_for_input/pipe.go delete mode 100644 experiments/6_waiting_for_input/port.go delete mode 100644 experiments/6_waiting_for_input/ports.go delete mode 100644 experiments/6_waiting_for_input/signal.go delete mode 100644 experiments/7_loop/component.go delete mode 100644 experiments/7_loop/errors.go delete mode 100644 experiments/7_loop/fmesh.go delete mode 100644 experiments/7_loop/go.mod delete mode 100644 experiments/7_loop/main.go delete mode 100644 experiments/7_loop/pipe.go delete mode 100644 experiments/7_loop/port.go delete mode 100644 experiments/7_loop/ports.go delete mode 100644 experiments/7_loop/signal.go delete mode 100644 experiments/8_fibonacci/component.go delete mode 100644 experiments/8_fibonacci/errors.go delete mode 100644 experiments/8_fibonacci/fmesh.go delete mode 100644 experiments/8_fibonacci/go.mod delete mode 100644 experiments/8_fibonacci/main.go delete mode 100644 experiments/8_fibonacci/pipe.go delete mode 100644 experiments/8_fibonacci/port.go delete mode 100644 experiments/8_fibonacci/ports.go delete mode 100644 experiments/8_fibonacci/signal.go diff --git a/examples/fibonacci.go b/examples/fibonacci.go new file mode 100644 index 0000000..92aa2f0 --- /dev/null +++ b/examples/fibonacci.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" +) + +// This example shows how a component can have a pipe looped into it's input. +// This pattern allows to activate components multiple time using control plane (special output with looped-in pipe) +// For example we can calculate Fibonacci numbers without actually having a code for loop (the loop is implemented by ports and pipes) +func main() { + c1 := component.New("fibonacci number generator"). + WithInputs("i_cur", "i_prev"). + WithOutputs("o_cur", "o_prev"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + cur := inputs.ByName("i_cur").FirstSignalPayloadOrDefault(0).(int) + prev := inputs.ByName("i_prev").FirstSignalPayloadOrDefault(0).(int) + + next := cur + prev + + //Hardcoded limit + if next < 100 { + fmt.Println(next) + outputs.ByName("o_cur").PutSignals(signal.New(next)) + outputs.ByName("o_prev").PutSignals(signal.New(cur)) + } + + return nil + }) + + //Define pipes + c1.Outputs().ByName("o_cur").PipeTo(c1.Inputs().ByName("i_cur")) + c1.Outputs().ByName("o_prev").PipeTo(c1.Inputs().ByName("i_prev")) + + //Build mesh + fm := fmesh.New("fibonacci example").WithComponents(c1) + + //Set inputs (first 2 Fibonacci numbers) + f0, f1 := signal.New(0), signal.New(1) + + c1.Inputs().ByName("i_prev").PutSignals(f0) + c1.Inputs().ByName("i_cur").PutSignals(f1) + + fmt.Println(f0.PayloadOrNil()) + fmt.Println(f1.PayloadOrNil()) + + //Run the mesh + _, err := fm.Run() + + if err != nil { + fmt.Println(err) + } + +} diff --git a/experiments/1_function_composition/common.go b/experiments/1_function_composition/common.go deleted file mode 100644 index 93ac32e..0000000 --- a/experiments/1_function_composition/common.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -func Add(a int, b int) int { - return a + b -} - -func Mul(a int, b int) int { - return a * b -} diff --git a/experiments/1_function_composition/fmesh.go b/experiments/1_function_composition/fmesh.go deleted file mode 100644 index 4bab26b..0000000 --- a/experiments/1_function_composition/fmesh.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import "fmt" - -type FMesh struct { - Components []*Component - Pipes []*Pipe -} - -func (fm *FMesh) compute() { - for _, c := range fm.Components { - c.compute() - } -} - -func (fm *FMesh) flush() { - for _, p := range fm.Pipes { - p.flush() - } -} - -func (fm *FMesh) hasComponentToCompute() bool { - for _, c := range fm.Components { - if c.hasInput() { - return true - } - } - return false -} - -func RunAsFMesh(input int) { - c1 := Component{ - Name: "mul 2", - } - c1.h = func(input int) int { - return Mul(input, 2) - } - c1.setInput(10) - - c2 := Component{ - Name: "add 3", - } - c2.h = func(input int) int { - return Add(input, 3) - } - - c3 := Component{ - Name: "add 5", - } - c3.h = func(input int) int { - return Add(input, 5) - } - - fmesh := &FMesh{ - Components: []*Component{&c1, &c2, &c3}, - Pipes: []*Pipe{ - &Pipe{ - In: &c1, - Out: &c2, - }, - &Pipe{ - In: &c2, - Out: &c3, - }, - }, - } - - for fmesh.hasComponentToCompute() { - fmesh.compute() - fmesh.flush() - } - - res := c3.getOutput() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/1_function_composition/fmesh_component.go b/experiments/1_function_composition/fmesh_component.go deleted file mode 100644 index b01a673..0000000 --- a/experiments/1_function_composition/fmesh_component.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -type Component struct { - Name string - input int - h func(input int) int - output int -} - -func (c *Component) getOutput() int { - return c.output -} - -func (c *Component) hasInput() bool { - return c.input != 0 -} - -func (c *Component) setInput(input int) { - c.input = input -} - -func (c *Component) setOutput(output int) { - c.output = output -} - -func (c *Component) clearInput() { - c.input = 0 -} - -func (c *Component) clearOutput() { - c.output = 0 -} - -func (c *Component) compute() { - if !c.hasInput() { - return - } - c.output = c.h(c.input) - c.clearInput() -} diff --git a/experiments/1_function_composition/fmesh_pipe.go b/experiments/1_function_composition/fmesh_pipe.go deleted file mode 100644 index 1e9e11f..0000000 --- a/experiments/1_function_composition/fmesh_pipe.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -type Pipe struct { - In *Component - Out *Component -} - -func (p *Pipe) flush() { - p.Out.setInput(p.In.getOutput()) - p.In.clearOutput() -} diff --git a/experiments/1_function_composition/go.mod b/experiments/1_function_composition/go.mod deleted file mode 100644 index 64395aa..0000000 --- a/experiments/1_function_composition/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/1_function_composition - -go 1.23 diff --git a/experiments/1_function_composition/main.go b/experiments/1_function_composition/main.go deleted file mode 100644 index 1056afc..0000000 --- a/experiments/1_function_composition/main.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -// This demo shows different ways to compose 3 arithmetic operations: 10*2+3+5 -func main() { - //RunAsNestedFuncs(10) - //RunAsPipes(10) - RunAsFMesh(10) -} diff --git a/experiments/1_function_composition/nested_funcs.go b/experiments/1_function_composition/nested_funcs.go deleted file mode 100644 index d24332d..0000000 --- a/experiments/1_function_composition/nested_funcs.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import "fmt" - -func RunAsNestedFuncs(input int) { - res := Add(Add(Mul(input, 2), 3), 5) - - fmt.Printf("Result is %v", res) -} diff --git a/experiments/1_function_composition/pipes.go b/experiments/1_function_composition/pipes.go deleted file mode 100644 index d0fdb9e..0000000 --- a/experiments/1_function_composition/pipes.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "fmt" -) - -func RunAsPipes(input int) { - ch1 := make(chan int) - ch2 := make(chan int) - done := make(chan struct{}) - var res int - - go func() { - ch1 <- Mul(input, 2) - }() - - go func() { - ch2 <- Add(<-ch1, 3) - }() - - go func() { - res = Add(<-ch2, 5) - done <- struct{}{} - - }() - - <-done - fmt.Printf("Result is %v", res) -} diff --git a/experiments/2_multiple_inputs_outputs/component.go b/experiments/2_multiple_inputs_outputs/component.go deleted file mode 100644 index 67c6e51..0000000 --- a/experiments/2_multiple_inputs_outputs/component.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) -} - -type Components []*Component - -func (c *Component) compute() { - if !c.inputs.anyHasValue() { - //No inputs set, nothing to compute - return - } - //Run the computation - c.handler(c.inputs, c.outputs) - - //Clear inputs - c.inputs.setAll(nil) -} diff --git a/experiments/2_multiple_inputs_outputs/fmesh.go b/experiments/2_multiple_inputs_outputs/fmesh.go deleted file mode 100644 index 2210db0..0000000 --- a/experiments/2_multiple_inputs_outputs/fmesh.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type FMesh struct { - Components Components - Pipes Pipes -} - -func (fm *FMesh) compute() { - for _, c := range fm.Components { - c.compute() - } -} - -func (fm *FMesh) flush() { - for _, p := range fm.Pipes { - p.flush() - } -} - -func (fm *FMesh) run() { - for fm.hasComponentToCompute() { - fm.compute() - fm.flush() - } -} - -func (fm *FMesh) hasComponentToCompute() bool { - for _, c := range fm.Components { - if c.inputs.anyHasValue() { - return true - } - } - return false -} diff --git a/experiments/2_multiple_inputs_outputs/go.mod b/experiments/2_multiple_inputs_outputs/go.mod deleted file mode 100644 index 7b88dce..0000000 --- a/experiments/2_multiple_inputs_outputs/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/2_multiple_inputs_outputs - -go 1.23 diff --git a/experiments/2_multiple_inputs_outputs/main.go b/experiments/2_multiple_inputs_outputs/main.go deleted file mode 100644 index d889646..0000000 --- a/experiments/2_multiple_inputs_outputs/main.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -import "fmt" - -// This experiment aims to demonstrate components with multiple inputs and outputs -// In this case the component acts like a multivariable function -func main() { - - //Define components - c1 := &Component{ - name: "mul 2, mul 10", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - "o2": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) { - i1, i2 := inputs.byName("i1"), inputs.byName("i2") - if i1.hasValue() && i2.hasValue() { - //Merge 2 input signals and put it onto single output - c := *i1.getValue()*2 + *i2.getValue()*10 - outputs.byName("o1").setValue(&c) - } - - //We can generate output signal without any input - c4 := 4 - outputs.byName("o2").setValue(&c4) - }, - } - - c2 := &Component{ - name: "add 3 or input", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) { - c := 3 - if inputs.byName("i2").hasValue() { - c = *inputs.byName("i2").getValue() - } - t := *inputs.byName("i1").getValue() + c - outputs.byName("o1").setValue(&t) - }, - } - - c3 := &Component{ - name: "add 5", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) { - t := *inputs.byName("i1").getValue() + 5 - outputs.byName("o1").setValue(&t) - }, - } - - //Define pipes - pipes := Pipes{ - &Pipe{ - In: c1.outputs.byName("o1"), - Out: c2.inputs.byName("i1"), - }, - &Pipe{ - In: c1.outputs.byName("o2"), - Out: c2.inputs.byName("i2"), - }, - &Pipe{ - In: c2.outputs.byName("o1"), - Out: c3.inputs.byName("i1"), - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, c3, - }, - Pipes: pipes, - } - - //Set inputs - a, b := 10, 20 - c1.inputs.byName("i1").setValue(&a) - c1.inputs.byName("i2").setValue(&b) - - //Run the mesh - fm.run() - - //Read outputs - res := *c3.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/2_multiple_inputs_outputs/pipe.go b/experiments/2_multiple_inputs_outputs/pipe.go deleted file mode 100644 index 3b9ca7e..0000000 --- a/experiments/2_multiple_inputs_outputs/pipe.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -type Pipe struct { - In *Port - Out *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - p.Out.setValue(p.In.getValue()) - p.In.setValue(nil) -} diff --git a/experiments/2_multiple_inputs_outputs/port.go b/experiments/2_multiple_inputs_outputs/port.go deleted file mode 100644 index 7a2a2e2..0000000 --- a/experiments/2_multiple_inputs_outputs/port.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -type PortValue *int - -type Port struct { - val PortValue -} - -func (p *Port) getValue() PortValue { - return p.val -} - -func (p *Port) setValue(val PortValue) { - p.val = val -} - -func (p *Port) hasValue() bool { - return p.val != nil -} diff --git a/experiments/2_multiple_inputs_outputs/ports.go b/experiments/2_multiple_inputs_outputs/ports.go deleted file mode 100644 index 745f295..0000000 --- a/experiments/2_multiple_inputs_outputs/ports.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val PortValue) { - for _, p := range ports { - p.setValue(val) - } -} diff --git a/experiments/3_concurrent_computing/component.go b/experiments/3_concurrent_computing/component.go deleted file mode 100644 index d84e0a1..0000000 --- a/experiments/3_concurrent_computing/component.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "errors" - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) compute() error { - if !c.inputs.anyHasValue() { - //No inputs set, nothing to compute - return errors.New("no inputs set") - } - //Run the computation - c.handler(c.inputs, c.outputs) - - //Clear inputs - c.inputs.setAll(nil) - return nil -} diff --git a/experiments/3_concurrent_computing/fmesh.go b/experiments/3_concurrent_computing/fmesh.go deleted file mode 100644 index ba04dbf..0000000 --- a/experiments/3_concurrent_computing/fmesh.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "sync" - "sync/atomic" -) - -type FMesh struct { - Components Components - Pipes Pipes -} - -func (fm *FMesh) compute() int64 { - var wg sync.WaitGroup - var componentsTriggered int64 - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - err := c.compute() - - if err == nil { - atomic.AddInt64(&componentsTriggered, 1) - } - }() - } - - wg.Wait() - return componentsTriggered -} - -func (fm *FMesh) flush() { - for _, p := range fm.Pipes { - p.flush() - } -} - -func (fm *FMesh) run() { - for { - componentsTriggered := fm.compute() - - if componentsTriggered == 0 { - return - } - fm.flush() - } -} diff --git a/experiments/3_concurrent_computing/go.mod b/experiments/3_concurrent_computing/go.mod deleted file mode 100644 index 1f98efb..0000000 --- a/experiments/3_concurrent_computing/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/3_concurrent_computing - -go 1.23 diff --git a/experiments/3_concurrent_computing/main.go b/experiments/3_concurrent_computing/main.go deleted file mode 100644 index 2e76abf..0000000 --- a/experiments/3_concurrent_computing/main.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment demonstrates ability to run computations of N components (which are in the same "hop") concurrently -func main() { - //Define components - m1 := &Component{ - name: "m1", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 * 2 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - m2 := &Component{ - name: "m2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 * 3 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - a1 := &Component{ - name: "a1", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 + 50 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - a2 := &Component{ - name: "a2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1 := *inputs.byName("i1").getValue() - v1 = v1 + 100 - outputs.byName("o1").setValue(&v1) - return nil - }, - } - - comb := &Component{ - name: "combiner", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - v1, v2 := *inputs.byName("i1").getValue(), *inputs.byName("i2").getValue() - r := v1 + v2 - outputs.byName("o1").setValue(&r) - return nil - }, - } - - //Define pipes - pipes := Pipes{ - &Pipe{ - From: m1.outputs.byName("o1"), - To: a1.inputs.byName("i1"), - }, - &Pipe{ - From: m2.outputs.byName("o1"), - To: a2.inputs.byName("i1"), - }, - &Pipe{ - From: a1.outputs.byName("o1"), - To: comb.inputs.byName("i1"), - }, - &Pipe{ - From: a2.outputs.byName("o1"), - To: comb.inputs.byName("i2"), - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - m1, m2, a1, a2, comb, - }, - Pipes: pipes, - } - - //Set inputs - a, b := 10, 20 - m1.inputs.byName("i1").setValue(&a) - m2.inputs.byName("i1").setValue(&b) - - //Run the mesh - fm.run() - - //Read outputs - res := *comb.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/3_concurrent_computing/pipe.go b/experiments/3_concurrent_computing/pipe.go deleted file mode 100644 index 68451e1..0000000 --- a/experiments/3_concurrent_computing/pipe.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - p.To.setValue(p.From.getValue()) - p.From.setValue(nil) -} diff --git a/experiments/3_concurrent_computing/port.go b/experiments/3_concurrent_computing/port.go deleted file mode 100644 index 7a2a2e2..0000000 --- a/experiments/3_concurrent_computing/port.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -type PortValue *int - -type Port struct { - val PortValue -} - -func (p *Port) getValue() PortValue { - return p.val -} - -func (p *Port) setValue(val PortValue) { - p.val = val -} - -func (p *Port) hasValue() bool { - return p.val != nil -} diff --git a/experiments/3_concurrent_computing/ports.go b/experiments/3_concurrent_computing/ports.go deleted file mode 100644 index 745f295..0000000 --- a/experiments/3_concurrent_computing/ports.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val PortValue) { - for _, p := range ports { - p.setValue(val) - } -} diff --git a/experiments/4_multiplexed_pipes/component.go b/experiments/4_multiplexed_pipes/component.go deleted file mode 100644 index 656f9bb..0000000 --- a/experiments/4_multiplexed_pipes/component.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import "errors" - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() error { - if !c.inputs.anyHasValue() { - //No inputs set, nothing to activateComponents - return errors.New("no inputs set") - } - //Run the computation - err := c.handler(c.inputs, c.outputs) - if err != nil { - return err - } - - //Clear inputs - c.inputs.clearAll() - return nil -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/4_multiplexed_pipes/fmesh.go b/experiments/4_multiplexed_pipes/fmesh.go deleted file mode 100644 index bbfd32f..0000000 --- a/experiments/4_multiplexed_pipes/fmesh.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "sync" - "sync/atomic" -) - -type FMesh struct { - Components Components -} - -func (fm *FMesh) activateComponents() int64 { - var wg sync.WaitGroup - var componentsTriggered int64 - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - err := c.activate() - - if err == nil { - atomic.AddInt64(&componentsTriggered, 1) - } - }() - } - - wg.Wait() - return componentsTriggered -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() { - for { - componentsTriggered := fm.activateComponents() - - if componentsTriggered == 0 { - return - } - fm.flushPipes() - } -} diff --git a/experiments/4_multiplexed_pipes/go.mod b/experiments/4_multiplexed_pipes/go.mod deleted file mode 100644 index e7371e4..0000000 --- a/experiments/4_multiplexed_pipes/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/4_multiplexed_pipes - -go 1.23 diff --git a/experiments/4_multiplexed_pipes/main.go b/experiments/4_multiplexed_pipes/main.go deleted file mode 100644 index 985557f..0000000 --- a/experiments/4_multiplexed_pipes/main.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment add pipes multiplexing and signal aggregation -// - if N pipes are reading from the same port they will receive the same signal in next hop -// - if N pipes are writing to the same port in given hop the resulting signal will be an aggregation of all signals from all pipes -func main() { - //Define components - c1 := &Component{ - name: "add 3", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 3, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c2 := &Component{ - name: "mul 2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 * 2, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c3 := &Component{ - name: "add 5", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 5, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c4 := &Component{ - name: "agg sum", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsAggregate() { - a1 := (i1).(*AggregateSignal) - - sum := 0 - for _, v := range a1.val { - sum += v.GetInt() - } - - resSignal = &SingleSignal{ - val: sum, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, c3, c4, - }, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c1.outputs.byName("o1").CreatePipeTo(c3.inputs.byName("i1")) - c2.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - c3.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - - //Set inputs - a := &SingleSignal{ - val: 10, - } - c1.inputs.byName("i1").setValue(a) - - //Run the mesh - fm.run() - - //Read outputs - res := c4.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/4_multiplexed_pipes/pipe.go b/experiments/4_multiplexed_pipes/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/4_multiplexed_pipes/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/4_multiplexed_pipes/port.go b/experiments/4_multiplexed_pipes/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/4_multiplexed_pipes/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/4_multiplexed_pipes/ports.go b/experiments/4_multiplexed_pipes/ports.go deleted file mode 100644 index 0e4ea4f..0000000 --- a/experiments/4_multiplexed_pipes/ports.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/4_multiplexed_pipes/signal.go b/experiments/4_multiplexed_pipes/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/4_multiplexed_pipes/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/5_error_handling/component.go b/experiments/5_error_handling/component.go deleted file mode 100644 index 93bddc9..0000000 --- a/experiments/5_error_handling/component.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() ActivationResult { - if !c.inputs.anyHasValue() { - //No inputs set, stop here - return ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - } - //Run the computation - err := c.handler(c.inputs, c.outputs) - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - return ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - } - - return ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/5_error_handling/errors.go b/experiments/5_error_handling/errors.go deleted file mode 100644 index 2805f8e..0000000 --- a/experiments/5_error_handling/errors.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import "sync" - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} diff --git a/experiments/5_error_handling/fmesh.go b/experiments/5_error_handling/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/5_error_handling/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/5_error_handling/go.mod b/experiments/5_error_handling/go.mod deleted file mode 100644 index 4564f44..0000000 --- a/experiments/5_error_handling/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/5_error_handling - -go 1.23 diff --git a/experiments/5_error_handling/main.go b/experiments/5_error_handling/main.go deleted file mode 100644 index c1f3706..0000000 --- a/experiments/5_error_handling/main.go +++ /dev/null @@ -1,161 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -// This experiment enhances error handling capabilities -func main() { - //Define components - - c0 := &Component{ - name: "this component just brakes on start", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - return errors.New("i'm feelin baaad") - }, - } - - c1 := &Component{ - name: "add 3", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 3, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c2 := &Component{ - name: "mul 2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 * 2, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c3 := &Component{ - name: "add 5", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 5, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c4 := &Component{ - name: "agg sum", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsAggregate() { - a1 := (i1).(*AggregateSignal) - - sum := 0 - for _, v := range a1.val { - sum += v.GetInt() - } - - resSignal = &SingleSignal{ - val: sum, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Build mesh - fm := FMesh{ - ErrorHandlingStrategy: StopOnFirstError, - Components: Components{ - c0, c1, c2, c3, c4, - }, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c1.outputs.byName("o1").CreatePipeTo(c3.inputs.byName("i1")) - c2.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - c3.outputs.byName("o1").CreatePipeTo(c4.inputs.byName("i1")) - - //Set inputs - a := &SingleSignal{ - val: 10, - } - c1.inputs.byName("i1").setValue(a) - c0.inputs.byName("i1").setValue(&SingleSignal{ - val: 34, - }) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - - //Read outputs - res := c4.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/5_error_handling/pipe.go b/experiments/5_error_handling/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/5_error_handling/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/5_error_handling/port.go b/experiments/5_error_handling/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/5_error_handling/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/5_error_handling/ports.go b/experiments/5_error_handling/ports.go deleted file mode 100644 index 0e4ea4f..0000000 --- a/experiments/5_error_handling/ports.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/5_error_handling/signal.go b/experiments/5_error_handling/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/5_error_handling/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/6_waiting_for_input/component.go b/experiments/6_waiting_for_input/component.go deleted file mode 100644 index ee47c37..0000000 --- a/experiments/6_waiting_for_input/component.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: errors.New("component panicked"), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/6_waiting_for_input/errors.go b/experiments/6_waiting_for_input/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/6_waiting_for_input/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/6_waiting_for_input/fmesh.go b/experiments/6_waiting_for_input/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/6_waiting_for_input/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/6_waiting_for_input/go.mod b/experiments/6_waiting_for_input/go.mod deleted file mode 100644 index 0dba9cc..0000000 --- a/experiments/6_waiting_for_input/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/6_waiting_for_input - -go 1.23 diff --git a/experiments/6_waiting_for_input/main.go b/experiments/6_waiting_for_input/main.go deleted file mode 100644 index b85e1ea..0000000 --- a/experiments/6_waiting_for_input/main.go +++ /dev/null @@ -1,176 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment shows how components can "wait" for particular inputs. -// Here we have c5 component which just sums up the outcomes of c4 and the chain of c1->c2->c3 -// As c4 output is ready in second hop the c5 is triggered, but the problem is the other part coming from the chain is not ready yet, -// because in second hop only c2 is triggered. So c5 has to wait somehow while both inputs will be ready. -// By sending special sentinel errors we instruct mesh how to treat given component. -// So starting from this version component can tell the mesh if it's inputs must be reset or kept. -func main() { - //Define components - c1 := &Component{ - name: "c1", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 3, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c2 := &Component{ - name: "c2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 5, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c3 := &Component{ - name: "c3", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 * 2, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c4 := &Component{ - name: "c4", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: v1 + 10, - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - c5 := &Component{ - name: "c5", - inputs: Ports{ - "i1": &Port{}, - "i2": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - - // This component is a basic summator, it must trigger only when both inputs are set - if !inputs.manyByName("i1", "i2").allHaveValue() { - return errWaitingForInputKeepInputs - } - - i1 := inputs.byName("i1").getValue() - i2 := inputs.byName("i2").getValue() - - resSignal := &SingleSignal{ - val: (i1).(*SingleSignal).GetInt() + (i2).(*SingleSignal).GetInt(), - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, c3, c4, c5, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c2.outputs.byName("o1").CreatePipeTo(c3.inputs.byName("i1")) - c3.outputs.byName("o1").CreatePipeTo(c5.inputs.byName("i1")) - c4.outputs.byName("o1").CreatePipeTo(c5.inputs.byName("i2")) - - //Set inputs - a := &SingleSignal{ - val: 10, - } - - b := &SingleSignal{ - val: 2, - } - - c1.inputs.byName("i1").setValue(a) - c4.inputs.byName("i1").setValue(b) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - - //Read outputs - res := c5.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/6_waiting_for_input/pipe.go b/experiments/6_waiting_for_input/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/6_waiting_for_input/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/6_waiting_for_input/port.go b/experiments/6_waiting_for_input/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/6_waiting_for_input/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/6_waiting_for_input/ports.go b/experiments/6_waiting_for_input/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/6_waiting_for_input/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/6_waiting_for_input/signal.go b/experiments/6_waiting_for_input/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/6_waiting_for_input/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/7_loop/component.go b/experiments/7_loop/component.go deleted file mode 100644 index ee47c37..0000000 --- a/experiments/7_loop/component.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: errors.New("component panicked"), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/7_loop/errors.go b/experiments/7_loop/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/7_loop/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/7_loop/fmesh.go b/experiments/7_loop/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/7_loop/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/7_loop/go.mod b/experiments/7_loop/go.mod deleted file mode 100644 index 6d114d1..0000000 --- a/experiments/7_loop/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/7_loop - -go 1.23 diff --git a/experiments/7_loop/main.go b/experiments/7_loop/main.go deleted file mode 100644 index f6109e0..0000000 --- a/experiments/7_loop/main.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "fmt" - "strconv" -) - -// This experiment shows how a component can have a pipe looped in to it's input. -// This pattern allows to activate components multiple time using control plane (special output with looped in pipe) -func main() { - //Define components - c1 := &Component{ - name: "c1", - inputs: Ports{ - "i1": &Port{}, //Data plane - "i2": &Port{}, //Control plane - }, - outputs: Ports{ - "o1": &Port{}, //Data plane - "o2": &Port{}, //Control plane (loop) - }, - handler: func(inputs Ports, outputs Ports) error { - i1 := inputs.byName("i1").getValue() - - v1 := (i1).(*SingleSignal).GetInt() - - if v1 > 100 { - //Signal is ready to go to next component, breaking the loop - outputs.byName("o1").setValue(&SingleSignal{ - val: v1, - }) - } else { - //Loop in - outputs.byName("o2").setValue(&SingleSignal{ - val: v1 + 5, - }) - } - - return nil - }, - } - - c2 := &Component{ - name: "c2", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - var resSignal *SingleSignal - i1 := inputs.byName("i1").getValue() - - //Bypass i1->o1 - if i1.IsSingle() { - v1 := (i1).(*SingleSignal).GetInt() - resSignal = &SingleSignal{ - val: strconv.Itoa(v1) + " suffix added once", - } - } - - outputs.byName("o1").setValue(resSignal) - return nil - }, - } - - //Define pipes - c1.outputs.byName("o1").CreatePipeTo(c2.inputs.byName("i1")) - c1.outputs.byName("o2").CreatePipeTo(c1.inputs.byName("i1")) //Loop - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, c2, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - a := &SingleSignal{ - val: 10, - } - - c1.inputs.byName("i1").setValue(a) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - - //Read outputs - res := c2.outputs.byName("o1").getValue() - fmt.Printf("Result is %v", res) -} diff --git a/experiments/7_loop/pipe.go b/experiments/7_loop/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/7_loop/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/7_loop/port.go b/experiments/7_loop/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/7_loop/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/7_loop/ports.go b/experiments/7_loop/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/7_loop/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/7_loop/signal.go b/experiments/7_loop/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/7_loop/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} diff --git a/experiments/8_fibonacci/component.go b/experiments/8_fibonacci/component.go deleted file mode 100644 index ee47c37..0000000 --- a/experiments/8_fibonacci/component.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "errors" - "fmt" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: errors.New("component panicked"), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/8_fibonacci/errors.go b/experiments/8_fibonacci/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/8_fibonacci/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/8_fibonacci/fmesh.go b/experiments/8_fibonacci/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/8_fibonacci/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/8_fibonacci/go.mod b/experiments/8_fibonacci/go.mod deleted file mode 100644 index 1ace83f..0000000 --- a/experiments/8_fibonacci/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/8_fibonacci - -go 1.23 diff --git a/experiments/8_fibonacci/main.go b/experiments/8_fibonacci/main.go deleted file mode 100644 index ed89a30..0000000 --- a/experiments/8_fibonacci/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "fmt" -) - -// This experiment shows how a component can have a pipe looped in to it's input. -// This pattern allows to activate components multiple time using control plane (special output with looped in pipe) -func main() { - //Define components - c1 := &Component{ - name: "c1", - inputs: Ports{ - "i_cur": &Port{}, - "i_prev": &Port{}, - }, - outputs: Ports{ - "o_cur": &Port{}, - "o_prev": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - iCur := inputs.byName("i_cur").getValue() - iPrev := inputs.byName("i_prev").getValue() - - vCur := iCur.(*SingleSignal).GetInt() - vPrev := iPrev.(*SingleSignal).GetInt() - - vNext := vCur + vPrev - - if vNext < 150 { - fmt.Println(vNext) - outputs.byName("o_cur").setValue(&SingleSignal{val: vNext}) - outputs.byName("o_prev").setValue(&SingleSignal{val: vCur}) - } - - return nil - }, - } - - //Define pipes - c1.outputs.byName("o_cur").CreatePipeTo(c1.inputs.byName("i_cur")) - c1.outputs.byName("o_prev").CreatePipeTo(c1.inputs.byName("i_prev")) - - //Build mesh - fm := FMesh{ - Components: Components{ - c1, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - f0 := &SingleSignal{ - val: 0, - } - - f1 := &SingleSignal{ - val: 1, - } - - c1.inputs.byName("i_prev").setValue(f0) - c1.inputs.byName("i_cur").setValue(f1) - - fmt.Println(f0.val) - fmt.Println(f1.val) - - //Run the mesh - hops, err := fm.run() - _ = hops - - if err != nil { - fmt.Println(err) - } - -} diff --git a/experiments/8_fibonacci/pipe.go b/experiments/8_fibonacci/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/8_fibonacci/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/8_fibonacci/port.go b/experiments/8_fibonacci/port.go deleted file mode 100644 index 444f169..0000000 --- a/experiments/8_fibonacci/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/8_fibonacci/ports.go b/experiments/8_fibonacci/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/8_fibonacci/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/8_fibonacci/signal.go b/experiments/8_fibonacci/signal.go deleted file mode 100644 index 8d37159..0000000 --- a/experiments/8_fibonacci/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetInt() int { - return s.val.(int) -} From 457c75f555f92036de29cccc7eb48dc5a3284581 Mon Sep 17 00:00:00 2001 From: hovsep Date: Tue, 5 Nov 2024 00:28:30 +0200 Subject: [PATCH 37/41] Remove experiments which are not worth converting to examples --- experiments/10_shared_pointer/component.go | 93 ----- experiments/10_shared_pointer/errors.go | 44 --- experiments/10_shared_pointer/fmesh.go | 72 ---- experiments/10_shared_pointer/go.mod | 3 - experiments/10_shared_pointer/main.go | 76 ----- experiments/10_shared_pointer/pipe.go | 8 - experiments/10_shared_pointer/port.go | 65 ---- experiments/10_shared_pointer/ports.go | 51 --- experiments/10_shared_pointer/signal.go | 34 -- experiments/9_complex_signal/component.go | 92 ----- experiments/9_complex_signal/errors.go | 44 --- experiments/9_complex_signal/fmesh.go | 72 ---- experiments/9_complex_signal/go.mod | 3 - experiments/9_complex_signal/main.go | 376 --------------------- experiments/9_complex_signal/pipe.go | 8 - experiments/9_complex_signal/port.go | 62 ---- experiments/9_complex_signal/ports.go | 51 --- experiments/9_complex_signal/signal.go | 34 -- 18 files changed, 1188 deletions(-) delete mode 100644 experiments/10_shared_pointer/component.go delete mode 100644 experiments/10_shared_pointer/errors.go delete mode 100644 experiments/10_shared_pointer/fmesh.go delete mode 100644 experiments/10_shared_pointer/go.mod delete mode 100644 experiments/10_shared_pointer/main.go delete mode 100644 experiments/10_shared_pointer/pipe.go delete mode 100644 experiments/10_shared_pointer/port.go delete mode 100644 experiments/10_shared_pointer/ports.go delete mode 100644 experiments/10_shared_pointer/signal.go delete mode 100644 experiments/9_complex_signal/component.go delete mode 100644 experiments/9_complex_signal/errors.go delete mode 100644 experiments/9_complex_signal/fmesh.go delete mode 100644 experiments/9_complex_signal/go.mod delete mode 100644 experiments/9_complex_signal/main.go delete mode 100644 experiments/9_complex_signal/pipe.go delete mode 100644 experiments/9_complex_signal/port.go delete mode 100644 experiments/9_complex_signal/ports.go delete mode 100644 experiments/9_complex_signal/signal.go diff --git a/experiments/10_shared_pointer/component.go b/experiments/10_shared_pointer/component.go deleted file mode 100644 index 20ca1e8..0000000 --- a/experiments/10_shared_pointer/component.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/10_shared_pointer/errors.go b/experiments/10_shared_pointer/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/10_shared_pointer/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/10_shared_pointer/fmesh.go b/experiments/10_shared_pointer/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/10_shared_pointer/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/10_shared_pointer/go.mod b/experiments/10_shared_pointer/go.mod deleted file mode 100644 index 3a17a50..0000000 --- a/experiments/10_shared_pointer/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/10_shared_pointer - -go 1.23 diff --git a/experiments/10_shared_pointer/main.go b/experiments/10_shared_pointer/main.go deleted file mode 100644 index 686d94b..0000000 --- a/experiments/10_shared_pointer/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "fmt" -) - -// This example demonstrates a multiplexing problem in current implementation (signals are shared pointers when multiplexed) -func main() { - //Define components - gen := &Component{ - name: "number generator", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "num": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - outputs.byName("num").setValue(inputs.byName("i1").getValue()) - return nil - }, - } - - r1 := &Component{ - name: "modifies input signal ", - inputs: Ports{ - "i1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - sig := inputs.byName("i1").getValue() - sig.(*SingleSignal).val = 666 //This modifies the signals for all receivers, as signal is a shared pointer - return nil - }, - } - - r2 := &Component{ - name: "receives multiplexed input signal ", - inputs: Ports{ - "i1": &Port{}, - }, - outputs: Ports{ - "o1": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - //r2 expects 10 to come from "gen", but actually it is getting modified by r1 caused undesired behaviour - outputs.byName("o1").setValue(inputs.byName("i1").getValue()) - return nil - }, - } - - //Define pipes - gen.outputs.byName("num").CreatePipesTo(r1.inputs.byName("i1"), r2.inputs.byName("i1")) - - //Build mesh - fm := FMesh{ - Components: Components{gen, r1, r2}, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - - gen.inputs.byName("i1").setValue(&SingleSignal{val: 10}) - - //Run the mesh - hops, err := fm.run() - _ = hops - - res := r2.outputs.byName("o1").getValue() - - fmt.Printf("r2 received : %v", res.(*SingleSignal).GetVal()) - - if err != nil { - fmt.Println(err) - } - -} diff --git a/experiments/10_shared_pointer/pipe.go b/experiments/10_shared_pointer/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/10_shared_pointer/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/10_shared_pointer/port.go b/experiments/10_shared_pointer/port.go deleted file mode 100644 index f056109..0000000 --- a/experiments/10_shared_pointer/port.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(*AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipesTo(toPorts ...*Port) { - for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } - -} diff --git a/experiments/10_shared_pointer/ports.go b/experiments/10_shared_pointer/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/10_shared_pointer/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/10_shared_pointer/signal.go b/experiments/10_shared_pointer/signal.go deleted file mode 100644 index c4caeb4..0000000 --- a/experiments/10_shared_pointer/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetVal() any { - return s.val -} diff --git a/experiments/9_complex_signal/component.go b/experiments/9_complex_signal/component.go deleted file mode 100644 index 772edc3..0000000 --- a/experiments/9_complex_signal/component.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -type Component struct { - name string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -type Components []*Component - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -func (c *Component) flushOutputs() { - for _, out := range c.outputs { - if !out.hasValue() || len(out.pipes) == 0 { - continue - } - - for _, pipe := range out.pipes { - //Multiplexing - pipe.To.setValue(out.getValue()) - } - out.clearValue() - } -} diff --git a/experiments/9_complex_signal/errors.go b/experiments/9_complex_signal/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/9_complex_signal/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/9_complex_signal/fmesh.go b/experiments/9_complex_signal/fmesh.go deleted file mode 100644 index f5b4cff..0000000 --- a/experiments/9_complex_signal/fmesh.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/9_complex_signal/go.mod b/experiments/9_complex_signal/go.mod deleted file mode 100644 index c38204b..0000000 --- a/experiments/9_complex_signal/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/9_complex_signal - -go 1.23 diff --git a/experiments/9_complex_signal/main.go b/experiments/9_complex_signal/main.go deleted file mode 100644 index 91d2964..0000000 --- a/experiments/9_complex_signal/main.go +++ /dev/null @@ -1,376 +0,0 @@ -package main - -import ( - "fmt" - "math/rand" - "strings" -) - -type Request struct { - Method string - Headers map[string]string - Body []byte -} - -type Response struct { - Status string - StatusCode int - Headers map[string]string - Body []byte -} - -// This experiment shows how signal can carry arbitrary data (not only primitive types) -// We use a simple round-robin load balancer as an example -func main() { - //Define components - lb := &Component{ - name: "Load Balancer", - inputs: Ports{ - "req:localhost:80": &Port{}, //Incoming requests - - //Responses from each backend: - "resp:backend1:80": &Port{}, - "resp:backend2:80": &Port{}, - "resp:backend3:80": &Port{}, - }, - outputs: Ports{ - //Requests to each backend - "req:backend1:80": &Port{}, - "req:backend2:80": &Port{}, - "req:backend3:80": &Port{}, - - "resp:localhost:80": &Port{}, //Out coming responses - }, - handler: func(inputs Ports, outputs Ports) error { - //Process incoming requests - reqInput := inputs.byName("req:localhost:80") - if reqInput.hasValue() { - reqSignals := reqInput.getValue() - - if reqSignals.IsAggregate() { - for _, sig := range reqSignals.(*AggregateSignal).val { - - //Handle authorization - req := sig.val.(*Request) - authHeader, ok := req.Headers["Auth"] - - if !ok || len(authHeader) == 0 { - //Missing auth header - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("response from LB: request: %s is missing auth header", string(req.Body))), - }}) - continue - } - - if authHeader != "Bearer 123" { - //Auth error - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("response from LB: request: %s is unauthorized", string(req.Body))), - }}) - continue - } - - targetBackendIndex := 1 + rand.Intn(3) - outputs.byName(fmt.Sprintf("req:backend%d:80", targetBackendIndex)).setValue(sig) - - } - - } else { - doDispatch := true - - sig := reqSignals.(*SingleSignal) - - //Handle authorization - req := sig.val.(*Request) - authHeader, ok := req.Headers["Auth"] - - if !ok || len(authHeader) == 0 { - //Missing auth header - doDispatch = false - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("Request: %s is missing auth header", string(req.Body))), - }}) - } - - if authHeader != "Bearer 123" { - //Auth error - doDispatch = false - outputs.byName("resp:localhost:80").setValue(&SingleSignal{val: &Response{ - Status: "401 Unauthorized", - StatusCode: 401, - Headers: nil, - Body: []byte(fmt.Sprintf("Request: %s is unauthorized", string(req.Body))), - }}) - } - - if doDispatch { - targetBackendIndex := 1 + rand.Intn(3) - outputs.byName(fmt.Sprintf("req:backend%d:80", targetBackendIndex)).setValue(sig) - } - } - } - - //Read responses from backends and put them on main output for upstream consumer - for pname, port := range inputs { - if strings.Contains(pname, "resp:backend") && port.hasValue() { - outputs.byName("resp:localhost:80").setValue(port.getValue()) - } - } - - return nil - }, - } - - b1 := &Component{ - name: "Backend 1", - inputs: Ports{ - "req:localhost:80": &Port{}, - }, - outputs: Ports{ - "resp:localhost:80": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - reqInput := inputs.byName("req:localhost:80") - - if !reqInput.hasValue() { - return nil - } - - reqSignal := reqInput.getValue() - - if reqSignal.IsSingle() { - req := reqSignal.(*SingleSignal).GetVal().(*Request) - - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b1 for req:" + string(req.Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } else { - for _, sig := range reqSignal.(*AggregateSignal).val { - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b1 for req:" + string(sig.val.(*Request).Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } - } - return nil - }, - } - - b2 := &Component{ - name: "Backend 2", - inputs: Ports{ - "req:localhost:80": &Port{}, - }, - outputs: Ports{ - "resp:localhost:80": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - reqInput := inputs.byName("req:localhost:80") - - if !reqInput.hasValue() { - return nil - } - - reqSignal := reqInput.getValue() - - if reqSignal.IsSingle() { - req := reqSignal.(*SingleSignal).GetVal().(*Request) - - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b2 for req:" + string(req.Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } else { - for _, sig := range reqSignal.(*AggregateSignal).val { - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b2 for req:" + string(sig.val.(*Request).Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } - } - return nil - }, - } - - b3 := &Component{ - name: "Backend 3", - inputs: Ports{ - "req:localhost:80": &Port{}, - }, - outputs: Ports{ - "resp:localhost:80": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - reqInput := inputs.byName("req:localhost:80") - - if !reqInput.hasValue() { - return nil - } - - reqSignal := reqInput.getValue() - - if reqSignal.IsSingle() { - req := reqSignal.(*SingleSignal).GetVal().(*Request) - - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b3 for req:" + string(req.Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } else { - for _, sig := range reqSignal.(*AggregateSignal).val { - resp := &SingleSignal{ - val: &Response{ - Status: "200 OK", - StatusCode: 200, - Headers: nil, - Body: []byte("response from b3 for req:" + string(sig.val.(*Request).Body)), - }, - } - - outputs.byName("resp:localhost:80").setValue(resp) - } - } - return nil - }, - } - - //Define pipes - - //Request path - lb.outputs.byName("req:backend1:80").CreatePipeTo(b1.inputs.byName("req:localhost:80")) - lb.outputs.byName("req:backend2:80").CreatePipeTo(b2.inputs.byName("req:localhost:80")) - lb.outputs.byName("req:backend3:80").CreatePipeTo(b3.inputs.byName("req:localhost:80")) - - //Response path - b1.outputs.byName("resp:localhost:80").CreatePipeTo(lb.inputs.byName("resp:backend1:80")) - b2.outputs.byName("resp:localhost:80").CreatePipeTo(lb.inputs.byName("resp:backend2:80")) - b3.outputs.byName("resp:localhost:80").CreatePipeTo(lb.inputs.byName("resp:backend3:80")) - - //Build mesh - fm := FMesh{ - Components: Components{ - lb, b1, b2, b3, - }, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - reqs := []*Request{ - { - Method: "GET", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 1"), - }, - { - Method: "POST", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 2"), - }, - { - Method: "PATCH", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 3"), - }, - - { - Method: "GET", - Headers: map[string]string{ - "Auth": "Bearer 777", //Gonna fail auth on LB: - }, - Body: []byte("request 4"), - }, - { - Method: "GET", - Headers: map[string]string{ - "Auth": "Bearer 123", - }, - Body: []byte("request 5"), - }, - { - Method: "GET", - Headers: map[string]string{ - "Auth": "", //Empty auth header - }, - Body: []byte("request 6"), - }, - { - Method: "GET", - Headers: map[string]string{ - "Content-Type": "application/json", //Missing auth - }, - Body: []byte("request 7"), - }, - } - - //No need to create aggregated signal literal, - //we can perfectly set the value to same input, and it will be aggregated automatically - for _, req := range reqs { - lb.inputs.byName("req:localhost:80").setValue(&SingleSignal{val: req}) - //break //set just 1 request - } - - //Run the mesh - hops, err := fm.run() - _ = hops - - res := lb.outputs.byName("resp:localhost:80").getValue() - - if res.IsAggregate() { - for _, sig := range res.(*AggregateSignal).val { - resp := sig.val.(*Response) - - fmt.Println(string(resp.Body)) - } - } - - if err != nil { - fmt.Println(err) - } - -} diff --git a/experiments/9_complex_signal/pipe.go b/experiments/9_complex_signal/pipe.go deleted file mode 100644 index 488d01c..0000000 --- a/experiments/9_complex_signal/pipe.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe diff --git a/experiments/9_complex_signal/port.go b/experiments/9_complex_signal/port.go deleted file mode 100644 index 7ce0d4a..0000000 --- a/experiments/9_complex_signal/port.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -type Port struct { - val Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getValue() Signal { - return p.val -} - -func (p *Port) setValue(val Signal) { - if p.hasValue() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.val.IsSingle() { - resValues = append(resValues, p.val.(*SingleSignal)) - } else if p.val.IsAggregate() { - resValues = p.val.(*AggregateSignal).val - } - - //Add new signal(s) - if val.IsSingle() { - resValues = append(resValues, val.(*SingleSignal)) - } else if val.IsAggregate() { - resValues = append(resValues, val.(*AggregateSignal).val...) - } - - p.val = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.val = val -} - -func (p *Port) clearValue() { - p.val = nil -} - -func (p *Port) hasValue() bool { - return p.val != nil -} - -// Adds pipe reference to port, so all pipes of the port are easily iterable (no in\out semantics) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipeTo(toPort *Port) { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) -} diff --git a/experiments/9_complex_signal/ports.go b/experiments/9_complex_signal/ports.go deleted file mode 100644 index ecd43cd..0000000 --- a/experiments/9_complex_signal/ports.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -type Ports map[string]*Port - -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasValue() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasValue() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.setValue(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearValue() - } -} diff --git a/experiments/9_complex_signal/signal.go b/experiments/9_complex_signal/signal.go deleted file mode 100644 index c4caeb4..0000000 --- a/experiments/9_complex_signal/signal.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool -} - -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s SingleSignal) GetVal() any { - return s.val -} From 5ecbc2916558747200b6fd2a02a0f9c9cd46a53b Mon Sep 17 00:00:00 2001 From: hovsep Date: Sat, 9 Nov 2024 01:15:53 +0200 Subject: [PATCH 38/41] Refactor drain logic: clear all inputs and then flush all --- component/collection.go | 6 +- component/collection_test.go | 5 +- component/errors.go | 1 + cycle/errors.go | 7 + cycle/group.go | 9 + errors.go | 4 + examples/{fibonacci.go => fibonacci/main.go} | 11 +- examples/nesting/main.go | 212 ++++++++++++++++++ export/dot/dot.go | 4 +- fmesh.go | 163 ++++++++------ fmesh_test.go | 110 +++++---- .../error_handling/chainable_api_test.go | 4 +- port/collection.go | 3 +- port/collection_test.go | 3 +- 14 files changed, 412 insertions(+), 130 deletions(-) create mode 100644 cycle/errors.go rename examples/{fibonacci.go => fibonacci/main.go} (70%) create mode 100644 examples/nesting/main.go diff --git a/component/collection.go b/component/collection.go index daef8dc..0c52c32 100644 --- a/component/collection.go +++ b/component/collection.go @@ -1,7 +1,7 @@ package component import ( - "errors" + "fmt" "github.com/hovsep/fmesh/common" ) @@ -31,8 +31,8 @@ func (c *Collection) ByName(name string) *Component { component, ok := c.components[name] if !ok { - c.SetErr(errors.New("component not found")) - return nil + c.SetErr(fmt.Errorf("%w, component name: %s", errNotFound, name)) + return New("").WithErr(c.Err()) } return component diff --git a/component/collection_test.go b/component/collection_test.go index 0be13d3..ec52f9f 100644 --- a/component/collection_test.go +++ b/component/collection_test.go @@ -1,6 +1,7 @@ package component import ( + "fmt" "github.com/stretchr/testify/assert" "testing" ) @@ -29,7 +30,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "c3", }, - want: nil, + want: New("").WithErr(fmt.Errorf("%w, component name: %s", errNotFound, "c3")), }, } for _, tt := range tests { @@ -69,7 +70,6 @@ func TestCollection_With(t *testing.T) { assert.Equal(t, 2, collection.Len()) assert.NotNil(t, collection.ByName("c1")) assert.NotNil(t, collection.ByName("c2")) - assert.Nil(t, collection.ByName("c999")) }, }, { @@ -84,7 +84,6 @@ func TestCollection_With(t *testing.T) { assert.NotNil(t, collection.ByName("c2")) assert.NotNil(t, collection.ByName("c3")) assert.NotNil(t, collection.ByName("c4")) - assert.Nil(t, collection.ByName("c999")) }, }, } diff --git a/component/errors.go b/component/errors.go index 671df5f..9d2d808 100644 --- a/component/errors.go +++ b/component/errors.go @@ -6,6 +6,7 @@ import ( ) var ( + errNotFound = errors.New("component not found") errWaitingForInputs = errors.New("component is waiting for some inputs") errWaitingForInputsKeep = fmt.Errorf("%w: do not clear input ports", errWaitingForInputs) ) diff --git a/cycle/errors.go b/cycle/errors.go new file mode 100644 index 0000000..4559671 --- /dev/null +++ b/cycle/errors.go @@ -0,0 +1,7 @@ +package cycle + +import "errors" + +var ( + errNoCyclesInGroup = errors.New("group has no cycles") +) diff --git a/cycle/group.go b/cycle/group.go index a668c4e..4e529ed 100644 --- a/cycle/group.go +++ b/cycle/group.go @@ -61,3 +61,12 @@ func (g *Group) CyclesOrDefault(defaultCycles Cycles) Cycles { func (g *Group) Len() int { return len(g.cycles) } + +// Last returns the latest cycle added to the group +func (g *Group) Last() *Cycle { + if g.Len() == 0 { + return New().WithErr(errNoCyclesInGroup) + } + + return g.cycles[g.Len()-1] +} diff --git a/errors.go b/errors.go index 7f10ce8..dcb20ff 100644 --- a/errors.go +++ b/errors.go @@ -22,4 +22,8 @@ var ( ErrHitAPanic = errors.New("f-mesh hit a panic and will be stopped") ErrUnsupportedErrorHandlingStrategy = errors.New("unsupported error handling strategy") ErrReachedMaxAllowedCycles = errors.New("reached max allowed cycles") + errFailedToRunCycle = errors.New("failed to run cycle") + errNoComponents = errors.New("no components found") + errFailedToClearInputs = errors.New("failed to clear input ports") + ErrFailedToDrain = errors.New("failed to drain") ) diff --git a/examples/fibonacci.go b/examples/fibonacci/main.go similarity index 70% rename from examples/fibonacci.go rename to examples/fibonacci/main.go index 92aa2f0..08de21b 100644 --- a/examples/fibonacci.go +++ b/examples/fibonacci/main.go @@ -8,9 +8,14 @@ import ( "github.com/hovsep/fmesh/signal" ) -// This example shows how a component can have a pipe looped into it's input. -// This pattern allows to activate components multiple time using control plane (special output with looped-in pipe) -// For example we can calculate Fibonacci numbers without actually having a code for loop (the loop is implemented by ports and pipes) +// This example demonstrates how a component can have a pipe looped back into its own input, +// enabling a pattern that reactivates the component multiple times. +// By looping the output back into the input, the component can perform repeated calculations +// without explicit looping constructs in the code. +// +// For instance, this approach can be used to calculate Fibonacci numbers without needing +// traditional looping code. Instead, the loop is achieved by configuring ports and pipes, +// where each cycle processes a new Fibonacci term. func main() { c1 := component.New("fibonacci number generator"). WithInputs("i_cur", "i_prev"). diff --git a/examples/nesting/main.go b/examples/nesting/main.go new file mode 100644 index 0000000..2bd6788 --- /dev/null +++ b/examples/nesting/main.go @@ -0,0 +1,212 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" +) + +type FactorizedNumber struct { + Num int + Factors []any +} + +// This example demonstrates the ability to nest meshes, where a component within a mesh +// can itself be another mesh. This nesting is recursive, allowing for an unlimited depth +// of nested meshes. Each nested mesh behaves as an individual component within the larger +// mesh, enabling complex and hierarchical workflows. +// In this example we implement prime factorization (which is core part of RSA encryption algorithm) as a sub-mesh +func main() { + starter := component.New("starter"). + WithDescription("This component just holds numbers we want to factorize"). + WithInputs("in"). // Single port is enough, as it can hold any number of signals (as long as they fit into1 memory) + WithOutputs("out"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + // Pure bypass + return port.ForwardSignals(inputs.ByName("in"), outputs.ByName("out")) + }) + + filter := component.New("filter"). + WithDescription("In this component we can do some optional filtering"). + WithInputs("in"). + WithOutputs("out", "log"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + isValid := func(num int) bool { + return num < 1000 + } + + for _, sig := range inputs.ByName("in").AllSignalsOrNil() { + if isValid(sig.PayloadOrNil().(int)) { + outputs.ByName("out").PutSignals(sig) + } else { + outputs.ByName("log").PutSignals(sig) + } + } + return nil + }) + + logger := component.New("logger"). + WithDescription("Simple logger"). + WithInputs("in"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + log := func(line string) { + fmt.Printf("LOG: %s", line) + } + + for _, sig := range inputs.ByName("in").AllSignalsOrNil() { + if logLine := sig.PayloadOrNil(); logLine != nil { + log(logLine.(string)) + } + } + return nil + }) + + factorizer := component.New("factorizer"). + WithDescription("Prime factorization implemented as separate f-mesh"). + WithInputs("in"). + WithOutputs("out"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + //This activation function has no implementation of factorization algorithm, + //it only runs another f-mesh to get results + + //Get nested mesh or meshes + factorization := getPrimeFactorizationMesh() + + // As nested f-mesh processes 1 signal per run we run it in the loop per each number + for _, numSig := range inputs.ByName("in").AllSignalsOrNil() { + //Set init data to nested mesh (pass signals from outer mesh to inner one) + factorization.Components().ByName("starter").InputByName("in").PutSignals(numSig) + + //Run nested mesh + _, err := factorization.Run() + + if err != nil { + return fmt.Errorf("inner mesh failed: %w", err) + } + + // Get results from nested mesh + factors, err := factorization.Components().ByName("results").OutputByName("factors").AllSignalsPayloads() + if err != nil { + return fmt.Errorf("failed to get factors: %w", err) + } + + //Pass results to outer mesh + number := numSig.PayloadOrNil().(int) + outputs.ByName("out").PutSignals(signal.New(FactorizedNumber{ + Num: number, + Factors: factors, + })) + } + + return nil + }) + + //Setup pipes + starter.OutputByName("out").PipeTo(filter.InputByName("in")) + filter.OutputByName("log").PipeTo(logger.InputByName("in")) + filter.OutputByName("out").PipeTo(factorizer.InputByName("in")) + + // Build the mesh + outerMesh := fmesh.New("outer").WithComponents(starter, filter, logger, factorizer) + + //Set init data + outerMesh.Components(). + ByName("starter"). + InputByName("in"). + PutSignals(signal.NewGroup(315).SignalsOrNil()...) + + //Run outer mesh + _, err := outerMesh.Run() + + if err != nil { + fmt.Println(fmt.Errorf("outer mesh failed with error: %w", err)) + } + + //Read results + for _, resSig := range outerMesh.Components().ByName("factorizer").OutputByName("out").AllSignalsOrNil() { + result := resSig.PayloadOrNil().(FactorizedNumber) + fmt.Println(fmt.Sprintf("Factors of number %d : %v", result.Num, result.Factors)) + } +} + +func getPrimeFactorizationMesh() *fmesh.FMesh { + starter := component.New("starter"). + WithDescription("Load the number to be factorized"). + WithInputs("in"). + WithOutputs("out"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + //For simplicity this f-mesh processes only one signal per run, so ignore all except first + outputs.ByName("out").PutSignals(inputs.ByName("in").Buffer().First()) + return nil + }) + + d2 := component.New("d2"). + WithDescription("Divide by smallest prime (2) to handle even factors"). + WithInputs("in"). + WithOutputs("out", "factor"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + number := inputs.ByName("in").FirstSignalPayloadOrNil().(int) + + for number%2 == 0 { + outputs.ByName("factor").PutSignals(signal.New(2)) + number /= 2 + } + + outputs.ByName("out").PutSignals(signal.New(number)) + return nil + }) + + dodd := component.New("dodd"). + WithDescription("Divide by odd primes starting from 3"). + WithInputs("in"). + WithOutputs("out", "factor"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + number := inputs.ByName("in").FirstSignalPayloadOrNil().(int) + divisor := 3 + for number > 1 && divisor*divisor <= number { + for number%divisor == 0 { + outputs.ByName("factor").PutSignals(signal.New(divisor)) + number /= divisor + } + divisor += 2 + } + outputs.ByName("out").PutSignals(signal.New(number)) + return nil + }) + + finalPrime := component.New("final_prime"). + WithDescription("Store the last remaining prime factor, if any"). + WithInputs("in"). + WithOutputs("factor"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + number := inputs.ByName("in").FirstSignalPayloadOrNil().(int) + if number > 1 { + outputs.ByName("factor").PutSignals(signal.New(number)) + } + return nil + }) + + results := component.New("results"). + WithDescription("factors holder"). + WithInputs("factor"). + WithOutputs("factors"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + return port.ForwardSignals(inputs.ByName("factor"), outputs.ByName("factors")) + }) + + //Main pipeline starter->d2->dodd->finalPrime + starter.OutputByName("out").PipeTo(d2.InputByName("in")) + d2.OutputByName("out").PipeTo(dodd.InputByName("in")) + dodd.OutputByName("out").PipeTo(finalPrime.InputByName("in")) + + //All found factors are accumulated in results + d2.OutputByName("factor").PipeTo(results.InputByName("factor")) + dodd.OutputByName("factor").PipeTo(results.InputByName("factor")) + finalPrime.OutputByName("factor").PipeTo(results.InputByName("factor")) + + return fmesh.New("prime factors algo"). + WithDescription("Pass single signal to starter"). + WithComponents(starter, d2, dodd, finalPrime, results) +} diff --git a/export/dot/dot.go b/export/dot/dot.go index 33335d9..3df40a8 100644 --- a/export/dot/dot.go +++ b/export/dot/dot.go @@ -77,7 +77,7 @@ func (d *dotExporter) ExportWithCycles(fm *fmesh.FMesh, activationCycles cycle.C buf := new(bytes.Buffer) graphForCycle.Write(buf) - results[activationCycle.Number()] = buf.Bytes() + results[activationCycle.Number()-1] = buf.Bytes() } return results, nil @@ -143,7 +143,7 @@ func (d *dotExporter) addPipes(graph *dot.Graph, components fmeshcomponent.Compo return fmt.Errorf("failed to add pipe to port: %s : %w", destPort.Name(), err) } // Delete label, as it is not needed anymore - destPort.DeleteLabel(nodeIDLabel) + //destPort.DeleteLabel(nodeIDLabel) // Any source port in any pipe is always output port, so we can build its node ID srcPortNode := graph.FindNodeByID(getPortID(c.Name(), port.DirectionOut, srcPort.Name())) diff --git a/fmesh.go b/fmesh.go index cddc366..02345e0 100644 --- a/fmesh.go +++ b/fmesh.go @@ -29,6 +29,7 @@ type FMesh struct { common.DescribedEntity *common.Chainable components *component.Collection + cycles *cycle.Group config Config } @@ -39,6 +40,7 @@ func New(name string) *FMesh { DescribedEntity: common.NewDescribedEntity(""), Chainable: common.NewChainable(), components: component.NewCollection(), + cycles: cycle.NewGroup(), config: defaultConfig, } } @@ -51,6 +53,8 @@ func (fm *FMesh) Components() *component.Collection { return fm.components } +//@TODO: add shortcut method: ComponentByName() + // WithDescription sets a description func (fm *FMesh) WithDescription(description string) *FMesh { if fm.HasErr() { @@ -87,24 +91,22 @@ func (fm *FMesh) WithConfig(config Config) *FMesh { } // runCycle runs one activation cycle (tries to activate ready components) -func (fm *FMesh) runCycle() *cycle.Cycle { - newCycle := cycle.New() +func (fm *FMesh) runCycle() { + newCycle := cycle.New().WithNumber(fm.cycles.Len() + 1) if fm.HasErr() { - return newCycle.WithErr(fm.Err()) + newCycle.SetErr(fm.Err()) } if fm.Components().Len() == 0 { - fm.SetErr(errors.New("failed to run cycle: no components found")) - return newCycle.WithErr(fm.Err()) + newCycle.SetErr(errors.Join(errFailedToRunCycle, errNoComponents)) } var wg sync.WaitGroup components, err := fm.Components().Components() if err != nil { - fm.SetErr(fmt.Errorf("failed to run cycle: %w", err)) - return newCycle.WithErr(fm.Err()) + newCycle.SetErr(errors.Join(errFailedToRunCycle, err)) } for _, c := range components { @@ -132,25 +134,41 @@ func (fm *FMesh) runCycle() *cycle.Cycle { } } - return newCycle + if newCycle.HasErr() { + fm.SetErr(newCycle.Err()) + } + + fm.cycles = fm.cycles.With(newCycle) + + return } // DrainComponents drains the data from activated components -func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { +func (fm *FMesh) drainComponents() { if fm.HasErr() { - return fm.Err() + fm.SetErr(errors.Join(ErrFailedToDrain, fm.Err())) + return + } + + fm.clearInputs() + if fm.HasErr() { + return } components, err := fm.Components().Components() if err != nil { - return fmt.Errorf("failed to drain components: %w", err) + fm.SetErr(errors.Join(ErrFailedToDrain, err)) + return } + lastCycle := fm.cycles.Last() + for _, c := range components { - activationResult := cycle.ActivationResults().ByComponentName(c.Name()) + activationResult := lastCycle.ActivationResults().ByComponentName(c.Name()) if activationResult.HasErr() { - return activationResult.Err() + fm.SetErr(errors.Join(ErrFailedToDrain, activationResult.Err())) + return } if !activationResult.Activated() { @@ -158,28 +176,52 @@ func (fm *FMesh) drainComponents(cycle *cycle.Cycle) error { continue } - // By default, all outputs are flushed and all inputs are cleared - shouldFlushOutputs := true - shouldClearInputs := true - + // Components waiting for inputs are never drained if component.IsWaitingForInput(activationResult) { - // @TODO: maybe we should clear outputs - // in order to prevent leaking outputs from previous cycle - // (if outputs were set before returning errWaitingForInputs) - shouldFlushOutputs = false - shouldClearInputs = !component.WantsToKeepInputs(activationResult) + // @TODO: maybe we should additionally clear outputs + // because it is technically possible to set some output signals and then return errWaitingForInput in AF + continue + } + + c.FlushOutputs() + + } +} + +// clearInputs clears all the input ports of all components activated in latest cycle +func (fm *FMesh) clearInputs() { + if fm.HasErr() { + return + } + + components, err := fm.Components().Components() + if err != nil { + fm.SetErr(errors.Join(errFailedToClearInputs, err)) + return + } + + lastCycle := fm.cycles.Last() + + for _, c := range components { + activationResult := lastCycle.ActivationResults().ByComponentName(c.Name()) + + if activationResult.HasErr() { + fm.SetErr(errors.Join(errFailedToClearInputs, activationResult.Err())) } - if shouldClearInputs { - c.ClearInputs() + if !activationResult.Activated() { + // Component did not activate hence it's inputs must be clear + continue } - if shouldFlushOutputs { - c.FlushOutputs() + if component.IsWaitingForInput(activationResult) && component.WantsToKeepInputs(activationResult) { + // Component want to keep inputs for the next cycle + //@TODO: add fine grained control on which ports to keep + continue } + c.ClearInputs() } - return nil } // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) @@ -188,70 +230,55 @@ func (fm *FMesh) Run() (cycle.Cycles, error) { return nil, fm.Err() } - allCycles := cycle.NewGroup() - cycleNumber := 0 for { - cycleResult := fm.runCycle().WithNumber(cycleNumber) + fm.runCycle() - if cycleResult.HasErr() { - fm.SetErr(cycleResult.Err()) - return nil, fmt.Errorf("chain error occurred in cycle #%d : %w", cycleResult.Number(), cycleResult.Err()) + if mustStop, err := fm.mustStop(); mustStop { + return fm.cycles.CyclesOrNil(), err } - allCycles = allCycles.With(cycleResult) - - mustStop, chainError, stopError := fm.mustStop(cycleResult) - if chainError != nil { - return nil, chainError + fm.drainComponents() + if fm.HasErr() { + return nil, fm.Err() } - - if mustStop { - cycles, err := allCycles.Cycles() - if err != nil { - return nil, err - } - return cycles, stopError - } - - err := fm.drainComponents(cycleResult) - if err != nil { - return nil, err - } - cycleNumber++ } } -// mustStop defines when f-mesh must stop after activation cycle -func (fm *FMesh) mustStop(cycleResult *cycle.Cycle) (bool, error, error) { +// mustStop defines when f-mesh must stop (it always checks only last cycle) +func (fm *FMesh) mustStop() (bool, error) { if fm.HasErr() { - return false, fm.Err(), nil + return false, nil } - if (fm.config.CyclesLimit > 0) && (cycleResult.Number() > fm.config.CyclesLimit) { - return true, nil, ErrReachedMaxAllowedCycles + lastCycle := fm.cycles.Last() + + if (fm.config.CyclesLimit > 0) && (lastCycle.Number() > fm.config.CyclesLimit) { + return true, ErrReachedMaxAllowedCycles } - //Check if we are done (no components activated during the cycle => all inputs are processed) - if !cycleResult.HasActivatedComponents() { - return true, nil, nil + if !lastCycle.HasActivatedComponents() { + // Stop naturally (no components activated during the cycle => all inputs are processed) + return true, nil } //Check if mesh must stop because of configured error handling strategy switch fm.config.ErrorHandlingStrategy { case StopOnFirstErrorOrPanic: - if cycleResult.HasErrors() || cycleResult.HasPanics() { - return true, nil, ErrHitAnErrorOrPanic + if lastCycle.HasErrors() || lastCycle.HasPanics() { + //@TODO: add failing components names to error + return true, fmt.Errorf("%w, cycle # %d", ErrHitAnErrorOrPanic, lastCycle.Number()) } - return false, nil, nil + return false, nil case StopOnFirstPanic: - if cycleResult.HasPanics() { - return true, nil, ErrHitAPanic + // @TODO: add more context to error + if lastCycle.HasPanics() { + return true, ErrHitAPanic } - return false, nil, nil + return false, nil case IgnoreAll: - return false, nil, nil + return false, nil default: - return true, nil, ErrUnsupportedErrorHandlingStrategy + return true, ErrUnsupportedErrorHandlingStrategy } } diff --git a/fmesh_test.go b/fmesh_test.go index d8f6224..c97e21e 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -2,6 +2,7 @@ package fmesh import ( "errors" + "fmt" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" @@ -30,6 +31,7 @@ func TestNew(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), Chainable: common.NewChainable(), components: component.NewCollection(), + cycles: cycle.NewGroup(), config: defaultConfig, }, }, @@ -43,6 +45,7 @@ func TestNew(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), Chainable: common.NewChainable(), components: component.NewCollection(), + cycles: cycle.NewGroup(), config: defaultConfig, }, }, @@ -75,6 +78,7 @@ func TestFMesh_WithDescription(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), Chainable: common.NewChainable(), components: component.NewCollection(), + cycles: cycle.NewGroup(), config: defaultConfig, }, }, @@ -89,6 +93,7 @@ func TestFMesh_WithDescription(t *testing.T) { DescribedEntity: common.NewDescribedEntity("descr"), Chainable: common.NewChainable(), components: component.NewCollection(), + cycles: cycle.NewGroup(), config: defaultConfig, }, }, @@ -124,6 +129,7 @@ func TestFMesh_WithConfig(t *testing.T) { DescribedEntity: common.NewDescribedEntity(""), Chainable: common.NewChainable(), components: component.NewCollection(), + cycles: cycle.NewGroup(), config: Config{ ErrorHandlingStrategy: IgnoreAll, CyclesLimit: 9999, @@ -641,7 +647,7 @@ func TestFMesh_runCycle(t *testing.T) { component.NewActivationResult("c3"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ), + ).WithNumber(1), }, } for _, tt := range tests { @@ -649,98 +655,107 @@ func TestFMesh_runCycle(t *testing.T) { if tt.initFM != nil { tt.initFM(tt.fm) } - cycleResult := tt.fm.runCycle() + tt.fm.runCycle() + gotCycleResult := tt.fm.cycles.Last() if tt.wantError { - assert.True(t, cycleResult.HasErr()) - assert.Error(t, cycleResult.Err()) + assert.True(t, gotCycleResult.HasErr()) + assert.Error(t, gotCycleResult.Err()) } else { - assert.False(t, cycleResult.HasErr()) - assert.NoError(t, cycleResult.Err()) - assert.Equal(t, tt.want, cycleResult) + assert.False(t, gotCycleResult.HasErr()) + assert.NoError(t, gotCycleResult.Err()) + assert.Equal(t, tt.want, gotCycleResult) } }) } } func TestFMesh_mustStop(t *testing.T) { - type args struct { - cycleResult *cycle.Cycle - } tests := []struct { - name string - fmesh *FMesh - args args - want bool - wantErr error + name string + getFMesh func() *FMesh + want bool + wantErr error }{ { - name: "with default config, no time to stop", - fmesh: New("fm"), - args: args{ - cycleResult: cycle.New().WithActivationResults( + name: "with default config, no time to stop", + getFMesh: func() *FMesh { + fm := New("fm") + + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ).WithNumber(5), + ).WithNumber(5) + + fm.cycles = fm.cycles.With(c) + return fm }, want: false, wantErr: nil, }, { - name: "with default config, reached max cycles", - fmesh: New("fm"), - args: args{ - cycleResult: cycle.New().WithActivationResults( + name: "with default config, reached max cycles", + getFMesh: func() *FMesh { + fm := New("fm") + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeOK), - ).WithNumber(1001), + ).WithNumber(1001) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, wantErr: ErrReachedMaxAllowedCycles, }, { - name: "mesh finished naturally and must stop", - fmesh: New("fm"), - args: args{ - cycleResult: cycle.New().WithActivationResults( + name: "mesh finished naturally and must stop", + getFMesh: func() *FMesh { + fm := New("fm") + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(false). WithActivationCode(component.ActivationCodeNoInput), - ).WithNumber(5), + ).WithNumber(5) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, wantErr: nil, }, { name: "mesh hit an error", - fmesh: New("fm").WithConfig(Config{ - ErrorHandlingStrategy: StopOnFirstErrorOrPanic, - CyclesLimit: UnlimitedCycles, - }), - args: args{ - cycleResult: cycle.New().WithActivationResults( + getFMesh: func() *FMesh { + fm := New("fm").WithConfig(Config{ + ErrorHandlingStrategy: StopOnFirstErrorOrPanic, + CyclesLimit: UnlimitedCycles, + }) + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodeReturnedError). WithActivationError(errors.New("c1 activation finished with error")), - ).WithNumber(5), + ).WithNumber(5) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, - wantErr: ErrHitAnErrorOrPanic, + wantErr: fmt.Errorf("%w, cycle # %d", ErrHitAnErrorOrPanic, 5), }, { name: "mesh hit a panic", - fmesh: New("fm").WithConfig(Config{ - ErrorHandlingStrategy: StopOnFirstPanic, - }), - args: args{ - cycleResult: cycle.New().WithActivationResults( + getFMesh: func() *FMesh { + fm := New("fm").WithConfig(Config{ + ErrorHandlingStrategy: StopOnFirstPanic, + }) + c := cycle.New().WithActivationResults( component.NewActivationResult("c1"). SetActivated(true). WithActivationCode(component.ActivationCodePanicked). WithActivationError(errors.New("c1 panicked")), - ).WithNumber(5), + ).WithNumber(5) + fm.cycles = fm.cycles.With(c) + return fm }, want: true, wantErr: ErrHitAPanic, @@ -748,7 +763,8 @@ func TestFMesh_mustStop(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, _, stopErr := tt.fmesh.mustStop(tt.args.cycleResult) + fm := tt.getFMesh() + got, stopErr := fm.mustStop() if tt.wantErr != nil { assert.EqualError(t, stopErr, tt.wantErr.Error()) } else { @@ -760,7 +776,7 @@ func TestFMesh_mustStop(t *testing.T) { } } -func TestFMesh_drainComponents(t *testing.T) { +func TO_BE_REWRITTEN_FMesh_drainComponents(t *testing.T) { type args struct { cycle *cycle.Cycle } @@ -954,7 +970,7 @@ func TestFMesh_drainComponents(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fm := tt.getFM() - fm.drainComponents(tt.args.cycle) + fm.drainComponents() if tt.assertions != nil { tt.assertions(t, fm) } diff --git a/integration_tests/error_handling/chainable_api_test.go b/integration_tests/error_handling/chainable_api_test.go index 36d60c2..64b4179 100644 --- a/integration_tests/error_handling/chainable_api_test.go +++ b/integration_tests/error_handling/chainable_api_test.go @@ -123,7 +123,7 @@ func Test_FMesh(t *testing.T) { _, err := fm.Run() assert.True(t, fm.HasErr()) assert.Error(t, err) - assert.EqualError(t, err, "chain error occurred in cycle #0 : port not found") + assert.ErrorContains(t, err, "port not found, port name: num777") }, }, { @@ -145,7 +145,7 @@ func Test_FMesh(t *testing.T) { _, err := fm.Run() assert.True(t, fm.HasErr()) assert.Error(t, err) - assert.EqualError(t, err, "chain error occurred in cycle #0 : some error in input signal") + assert.ErrorContains(t, err, "some error in input signal") }, }, } diff --git a/port/collection.go b/port/collection.go index 02f0f2d..e8d17fc 100644 --- a/port/collection.go +++ b/port/collection.go @@ -1,6 +1,7 @@ package port import ( + "fmt" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" ) @@ -34,7 +35,7 @@ func (collection *Collection) ByName(name string) *Port { } port, ok := collection.ports[name] if !ok { - collection.SetErr(ErrPortNotFoundInCollection) + collection.SetErr(fmt.Errorf("%w, port name: %s", ErrPortNotFoundInCollection, name)) return New("").WithErr(collection.Err()) } return port diff --git a/port/collection_test.go b/port/collection_test.go index fd4ebdc..107fc3e 100644 --- a/port/collection_test.go +++ b/port/collection_test.go @@ -2,6 +2,7 @@ package port import ( "errors" + "fmt" "github.com/hovsep/fmesh/common" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -107,7 +108,7 @@ func TestCollection_ByName(t *testing.T) { args: args{ name: "p3", }, - want: New("").WithErr(ErrPortNotFoundInCollection), + want: New("").WithErr(fmt.Errorf("%w, port name: %s", ErrPortNotFoundInCollection, "p3")), }, { name: "with chain error", From 610b64bc6783fbbf5bf6d4cc38a50b1f436fdd11 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 10 Nov 2024 01:41:19 +0200 Subject: [PATCH 39/41] Fix panic in example --- examples/nesting/main.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/nesting/main.go b/examples/nesting/main.go index 2bd6788..7523fa5 100644 --- a/examples/nesting/main.go +++ b/examples/nesting/main.go @@ -51,14 +51,12 @@ func main() { WithDescription("Simple logger"). WithInputs("in"). WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { - log := func(line string) { - fmt.Printf("LOG: %s", line) + log := func(data any) { + fmt.Printf("LOG: %v", data) } for _, sig := range inputs.ByName("in").AllSignalsOrNil() { - if logLine := sig.PayloadOrNil(); logLine != nil { - log(logLine.(string)) - } + log(sig.PayloadOrNil()) } return nil }) From 8efe078e5d71823f838ecbec68d35ac12d58754f Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 10 Nov 2024 02:08:26 +0200 Subject: [PATCH 40/41] Add example --- examples/async_input/main.go | 155 +++++++++++++++++ experiments/11_nested_meshes/component.go | 93 ---------- experiments/11_nested_meshes/components.go | 12 -- experiments/11_nested_meshes/errors.go | 44 ----- experiments/11_nested_meshes/fmesh.go | 74 -------- experiments/11_nested_meshes/go.mod | 3 - experiments/11_nested_meshes/main.go | 153 ----------------- experiments/11_nested_meshes/pipe.go | 12 -- experiments/11_nested_meshes/port.go | 68 -------- experiments/11_nested_meshes/ports.go | 54 ------ experiments/11_nested_meshes/signal.go | 48 ------ experiments/12_async_input/component.go | 93 ---------- experiments/12_async_input/components.go | 12 -- experiments/12_async_input/errors.go | 44 ----- experiments/12_async_input/fmesh.go | 74 -------- experiments/12_async_input/go.mod | 3 - experiments/12_async_input/main.go | 187 --------------------- experiments/12_async_input/pipe.go | 12 -- experiments/12_async_input/port.go | 76 --------- experiments/12_async_input/ports.go | 54 ------ experiments/12_async_input/signal.go | 61 ------- experiments/README.md | 1 - 22 files changed, 155 insertions(+), 1178 deletions(-) create mode 100644 examples/async_input/main.go delete mode 100644 experiments/11_nested_meshes/component.go delete mode 100644 experiments/11_nested_meshes/components.go delete mode 100644 experiments/11_nested_meshes/errors.go delete mode 100644 experiments/11_nested_meshes/fmesh.go delete mode 100644 experiments/11_nested_meshes/go.mod delete mode 100644 experiments/11_nested_meshes/main.go delete mode 100644 experiments/11_nested_meshes/pipe.go delete mode 100644 experiments/11_nested_meshes/port.go delete mode 100644 experiments/11_nested_meshes/ports.go delete mode 100644 experiments/11_nested_meshes/signal.go delete mode 100644 experiments/12_async_input/component.go delete mode 100644 experiments/12_async_input/components.go delete mode 100644 experiments/12_async_input/errors.go delete mode 100644 experiments/12_async_input/fmesh.go delete mode 100644 experiments/12_async_input/go.mod delete mode 100644 experiments/12_async_input/main.go delete mode 100644 experiments/12_async_input/pipe.go delete mode 100644 experiments/12_async_input/port.go delete mode 100644 experiments/12_async_input/ports.go delete mode 100644 experiments/12_async_input/signal.go delete mode 100644 experiments/README.md diff --git a/examples/async_input/main.go b/examples/async_input/main.go new file mode 100644 index 0000000..c7743ec --- /dev/null +++ b/examples/async_input/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "net/http" + "time" +) + +// This example processes 1 url every 3 seconds +// NOTE: urls are not crawled concurrently, because fm has only 1 worker (crawler component) +func main() { + fm := getMesh() + + urls := []string{ + "http://fffff.com", + "https://google.com", + "http://habr.com", + "http://localhost:80", + "https://postman-echo.com/delay/1", + "https://postman-echo.com/delay/3", + "https://postman-echo.com/delay/5", + "https://postman-echo.com/delay/10", + } + + ticker := time.NewTicker(3 * time.Second) + resultsChan := make(chan []any) + doneChan := make(chan struct{}) // Signals when all urls are processed + + //Producer goroutine + go func() { + for { + select { + case <-ticker.C: + if len(urls) == 0 { + close(resultsChan) + return + } + //Pop an url + url := urls[0] + urls = urls[1:] + + fmt.Println("produce:", url) + + fm.Components().ByName("web crawler").InputByName("url").PutSignals(signal.New(url)) + _, err := fm.Run() + if err != nil { + fmt.Println("fmesh returned error ", err) + } + + if fm.Components().ByName("web crawler").OutputByName("headers").HasSignals() { + results, err := fm.Components().ByName("web crawler").OutputByName("headers").AllSignalsPayloads() + if err != nil { + fmt.Println("Failed to get results ", err) + } + fm.Components().ByName("web crawler").OutputByName("headers").Clear() //@TODO maybe we can add fm.Reset() for cases when FMesh is reused (instead of cleaning ports explicitly) + resultsChan <- results + } + } + } + }() + + //Consumer goroutine + go func() { + for { + select { + case r, ok := <-resultsChan: + if !ok { + fmt.Println("results chan is closed. shutting down the reader") + doneChan <- struct{}{} + return + } + fmt.Println(fmt.Sprintf("consume: %v", r)) + } + } + }() + + <-doneChan +} + +func getMesh() *fmesh.FMesh { + //Setup dependencies + client := &http.Client{} + + //Define components + crawler := component.New("web crawler"). + WithDescription("gets http headers from given url"). + WithInputs("url"). + WithOutputs("errors", "headers").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + if !inputs.ByName("url").HasSignals() { + return component.NewErrWaitForInputs(false) + } + + allUrls, err := inputs.ByName("url").AllSignalsPayloads() + if err != nil { + return err + } + + for _, urlVal := range allUrls { + + url := urlVal.(string) + //All urls will be crawled sequentially + // in order to call them concurrently we need run each request in separate goroutine and handle synchronization (e.g. waitgroup) + response, err := client.Get(url) + if err != nil { + outputs.ByName("errors").PutSignals(signal.New(fmt.Errorf("got error: %w from url: %s", err, url))) + continue + } + + if len(response.Header) == 0 { + outputs.ByName("errors").PutSignals(signal.New(fmt.Errorf("no headers for url %s", url))) + continue + } + + outputs.ByName("headers").PutSignals(signal.New(map[string]http.Header{ + url: response.Header, + })) + } + + return nil + }) + + logger := component.New("error logger"). + WithDescription("logs http errors"). + WithInputs("error").WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + if !inputs.ByName("error").HasSignals() { + return component.NewErrWaitForInputs(false) + } + + allErrors, err := inputs.ByName("error").AllSignalsPayloads() + if err != nil { + return err + } + + for _, errVal := range allErrors { + e := errVal.(error) + if e != nil { + fmt.Println("Error logger says:", e) + } + } + + return nil + }) + + //Define pipes + crawler.OutputByName("errors").PipeTo(logger.InputByName("error")) + + return fmesh.New("web scraper").WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + }).WithComponents(crawler, logger) + +} diff --git a/experiments/11_nested_meshes/component.go b/experiments/11_nested_meshes/component.go deleted file mode 100644 index 563c713..0000000 --- a/experiments/11_nested_meshes/component.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -// @TODO: add a builder pattern implementation -type Component struct { - name string - description string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -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() - } -} diff --git a/experiments/11_nested_meshes/components.go b/experiments/11_nested_meshes/components.go deleted file mode 100644 index 95a556e..0000000 --- a/experiments/11_nested_meshes/components.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Components []*Component - -func (components Components) byName(name string) *Component { - for _, c := range components { - if c.name == name { - return c - } - } - return nil -} diff --git a/experiments/11_nested_meshes/errors.go b/experiments/11_nested_meshes/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/11_nested_meshes/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/11_nested_meshes/fmesh.go b/experiments/11_nested_meshes/fmesh.go deleted file mode 100644 index 9439f9a..0000000 --- a/experiments/11_nested_meshes/fmesh.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Name string - Description string - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/11_nested_meshes/go.mod b/experiments/11_nested_meshes/go.mod deleted file mode 100644 index f8658fe..0000000 --- a/experiments/11_nested_meshes/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/11_nested_meshes - -go 1.23 diff --git a/experiments/11_nested_meshes/main.go b/experiments/11_nested_meshes/main.go deleted file mode 100644 index 1fc89fd..0000000 --- a/experiments/11_nested_meshes/main.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "fmt" -) - -// This example demonstrates the ability of meshes to be nested (a component of mesh can be a mesh itself and nesting depth is unlimited) -func main() { - //Define components - c1 := &Component{ - name: "math", - description: "a * b + c", - inputs: Ports{ - "a": &Port{}, - "b": &Port{}, - "c": &Port{}, - }, - outputs: Ports{ - "out": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - if !inputs.manyByName("a", "b", "c").allHaveValue() { - return errWaitingForInputKeepInputs - } - - //This component is using a nested mesh for multiplication - multiplierWithLogger := getSubMesh() - - //Pass inputs - forwardSignal(inputs.byName("a"), multiplierWithLogger.Components.byName("Multiplier").inputs.byName("a")) - forwardSignal(inputs.byName("b"), multiplierWithLogger.Components.byName("Multiplier").inputs.byName("b")) - - //Run submesh inside a component - multiplierWithLogger.run() - - //Read the multiplication result - multiplicationResult := multiplierWithLogger.Components.byName("Multiplier").outputs.byName("result").getSignal().GetValue().(int) - - //Do the rest of calculation - res := multiplicationResult + inputs.byName("c").getSignal().GetValue().(int) - - outputs.byName("out").putSignal(newSignal(res)) - return nil - }, - } - - c2 := &Component{ - name: "add constant", - description: "a + 35", - inputs: Ports{ - "a": &Port{}, - }, - outputs: Ports{ - "out": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - if inputs.byName("a").hasSignal() { - a := inputs.byName("a").getSignal().GetValue().(int) - outputs.byName("out").putSignal(newSignal(a + 35)) - } - return nil - }, - } - - //Define pipes - c1.outputs.byName("out").CreatePipesTo(c2.inputs.byName("a")) - - //Build mesh - fm := &FMesh{ - Components: Components{c1, c2}, - ErrorHandlingStrategy: StopOnFirstError, - } - - //Set inputs - - c1.inputs.byName("a").putSignal(newSignal(2)) - c1.inputs.byName("b").putSignal(newSignal(3)) - c1.inputs.byName("c").putSignal(newSignal(4)) - - //Run the mesh - hops, err := fm.run() - if err != nil { - fmt.Println(err) - } - _ = hops - - res := c2.outputs.byName("out").getSignal().GetValue() - - fmt.Printf("outter fmesh result %v", res) -} - -func getSubMesh() *FMesh { - multiplier := &Component{ - name: "Multiplier", - description: "This component multiplies numbers on it's inputs", - inputs: Ports{ - "a": &Port{}, - "b": &Port{}, - }, - outputs: Ports{ - "bypass_a": &Port{}, - "bypass_b": &Port{}, - - "result": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - //@TODO: simplify waiting API - if !inputs.manyByName("a", "b").allHaveValue() { - return errWaitingForInputKeepInputs - } - - //Bypass input signals, so logger can get them - forwardSignal(inputs.byName("a"), outputs.byName("bypass_a")) - forwardSignal(inputs.byName("b"), outputs.byName("bypass_b")) - - a, b := inputs.byName("a").getSignal().GetValue().(int), inputs.byName("b").getSignal().GetValue().(int) - - outputs.byName("result").putSignal(newSignal(a * b)) - return nil - }, - } - - logger := &Component{ - name: "Logger", - description: "This component logs inputs of multiplier", - inputs: Ports{ - "a": &Port{}, - "b": &Port{}, - }, - outputs: nil, //No output - handler: func(inputs Ports, outputs Ports) error { - if inputs.byName("a").hasSignal() { - fmt.Println(fmt.Sprintf("Inner logger says: a is %v", inputs.byName("a").getSignal().GetValue())) - } - - if inputs.byName("b").hasSignal() { - fmt.Println(fmt.Sprintf("Inner logger says: b is %v", inputs.byName("b").getSignal().GetValue())) - } - - return nil - }, - } - - multiplier.outputs.byName("bypass_a").CreatePipesTo(logger.inputs.byName("a")) - multiplier.outputs.byName("bypass_b").CreatePipesTo(logger.inputs.byName("b")) - - return &FMesh{ - Name: "Logged multiplicator", - Description: "multiply 2 numbers and log inputs into std out", - Components: Components{multiplier, logger}, - ErrorHandlingStrategy: StopOnFirstError, - } -} diff --git a/experiments/11_nested_meshes/pipe.go b/experiments/11_nested_meshes/pipe.go deleted file mode 100644 index 8737a62..0000000 --- a/experiments/11_nested_meshes/pipe.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - forwardSignal(p.From, p.To) -} diff --git a/experiments/11_nested_meshes/port.go b/experiments/11_nested_meshes/port.go deleted file mode 100644 index 7350a2c..0000000 --- a/experiments/11_nested_meshes/port.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -type Port struct { - signal Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getSignal() Signal { - if p.signal.IsAggregate() { - return p.signal.(*AggregateSignal) - } - return p.signal.(*SingleSignal) -} - -func (p *Port) putSignal(sig Signal) { - if p.hasSignal() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.signal.IsSingle() { - resValues = append(resValues, p.signal.(*SingleSignal)) - } else if p.signal.IsAggregate() { - resValues = p.signal.(*AggregateSignal).val - } - - //Add new signal(s) - if sig.IsSingle() { - resValues = append(resValues, sig.(*SingleSignal)) - } else if sig.IsAggregate() { - resValues = append(resValues, sig.(*AggregateSignal).val...) - } - - p.signal = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.signal = sig -} - -func (p *Port) clearSignal() { - p.signal = nil -} - -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) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipesTo(toPorts ...*Port) { - for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } - -} diff --git a/experiments/11_nested_meshes/ports.go b/experiments/11_nested_meshes/ports.go deleted file mode 100644 index b53e6d2..0000000 --- a/experiments/11_nested_meshes/ports.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -// @TODO: this type must have good tooling for working with collection -// like adding new ports, filtering and so on -type Ports map[string]*Port - -// @TODO: add error handling (e.g. when port does not exist) -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasSignal() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasSignal() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.putSignal(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearSignal() - } -} diff --git a/experiments/11_nested_meshes/signal.go b/experiments/11_nested_meshes/signal.go deleted file mode 100644 index 17d7a9c..0000000 --- a/experiments/11_nested_meshes/signal.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool - GetValue() any -} - -// @TODO: enhance naming -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) GetValue() any { - return s.val -} - -func (s SingleSignal) GetValue() any { - return s.val -} - -func newSignal(val any) *SingleSignal { - return &SingleSignal{val: val} -} - -func forwardSignal(source *Port, dest *Port) { - dest.putSignal(source.getSignal()) -} diff --git a/experiments/12_async_input/component.go b/experiments/12_async_input/component.go deleted file mode 100644 index 563c713..0000000 --- a/experiments/12_async_input/component.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "runtime/debug" -) - -// @TODO: add a builder pattern implementation -type Component struct { - name string - description string - inputs Ports - outputs Ports - handler func(inputs Ports, outputs Ports) error -} - -func (c *Component) activate() (aRes ActivationResult) { - defer func() { - if r := recover(); r != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("panicked with %w, stacktrace: %s", r, debug.Stack()), - } - } - }() - - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.anyHasValue() { - //No inputs set, stop here - - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - return - } - - //Run the computation - err := c.handler(c.inputs, c.outputs) - - if isWaitingForInput(err) { - aRes = ActivationResult{ - activated: false, - componentName: c.name, - err: nil, - } - - if !errors.Is(err, errWaitingForInputKeepInputs) { - c.inputs.clearAll() - } - - return - } - - //Clear inputs - c.inputs.clearAll() - - if err != nil { - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: fmt.Errorf("failed to activate component: %w", err), - } - - return - } - - aRes = ActivationResult{ - activated: true, - componentName: c.name, - err: nil, - } - - return -} - -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() - } -} diff --git a/experiments/12_async_input/components.go b/experiments/12_async_input/components.go deleted file mode 100644 index 95a556e..0000000 --- a/experiments/12_async_input/components.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Components []*Component - -func (components Components) byName(name string) *Component { - for _, c := range components { - if c.name == name { - return c - } - } - return nil -} diff --git a/experiments/12_async_input/errors.go b/experiments/12_async_input/errors.go deleted file mode 100644 index 50b99c0..0000000 --- a/experiments/12_async_input/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "sync" -) - -type ErrorHandlingStrategy int - -const ( - StopOnFirstError ErrorHandlingStrategy = iota - IgnoreAll -) - -var ( - errWaitingForInputResetInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be reset") - errWaitingForInputKeepInputs = errors.New("component is not ready (waiting for one or more inputs). All inputs will be kept") -) - -// ActivationResult defines the result (possibly an error) of the activation of given component -type ActivationResult struct { - activated bool - componentName string - err error -} - -// HopResult describes the outcome of every single component activation in single hop -type HopResult struct { - sync.Mutex - activationResults map[string]error -} - -func (r *HopResult) hasErrors() bool { - for _, err := range r.activationResults { - if err != nil { - return true - } - } - return false -} - -func isWaitingForInput(err error) bool { - return errors.Is(err, errWaitingForInputResetInputs) || errors.Is(err, errWaitingForInputKeepInputs) -} diff --git a/experiments/12_async_input/fmesh.go b/experiments/12_async_input/fmesh.go deleted file mode 100644 index 9439f9a..0000000 --- a/experiments/12_async_input/fmesh.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "sync" -) - -type FMesh struct { - Name string - Description string - Components Components - ErrorHandlingStrategy -} - -func (fm *FMesh) activateComponents() *HopResult { - hop := &HopResult{ - activationResults: make(map[string]error), - } - activationResultsChan := make(chan ActivationResult) - doneChan := make(chan struct{}) - - var wg sync.WaitGroup - - go func() { - for { - select { - case aRes := <-activationResultsChan: - if aRes.activated { - hop.Lock() - hop.activationResults[aRes.componentName] = aRes.err - hop.Unlock() - } - case <-doneChan: - return - } - } - }() - - for _, c := range fm.Components { - wg.Add(1) - c := c - go func() { - defer wg.Done() - activationResultsChan <- c.activate() - }() - } - - wg.Wait() - doneChan <- struct{}{} - return hop -} - -func (fm *FMesh) flushPipes() { - for _, c := range fm.Components { - c.flushOutputs() - } -} - -func (fm *FMesh) run() ([]*HopResult, error) { - hops := make([]*HopResult, 0) - for { - hopReport := fm.activateComponents() - hops = append(hops, hopReport) - - if fm.ErrorHandlingStrategy == StopOnFirstError && hopReport.hasErrors() { - return hops, fmt.Errorf("Hop #%d finished with errors. Stopping fmesh. Report: %v", len(hops), hopReport.activationResults) - } - - if len(hopReport.activationResults) == 0 { - return hops, nil - } - fm.flushPipes() - } -} diff --git a/experiments/12_async_input/go.mod b/experiments/12_async_input/go.mod deleted file mode 100644 index ba77271..0000000 --- a/experiments/12_async_input/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/hovsep/fmesh/experiments/12_async_input - -go 1.23 diff --git a/experiments/12_async_input/main.go b/experiments/12_async_input/main.go deleted file mode 100644 index ca108ec..0000000 --- a/experiments/12_async_input/main.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "time" -) - -// This example demonstrates how fmesh can be fed input asynchronously -func main() { - //exampleFeedBatches() - exampleFeedSequentially() -} - -// This example processes 1 url every 3 seconds -// NOTE: urls are not crawled concurrently, because fm has only 1 worker (crawler component) -func exampleFeedSequentially() { - fm := getMesh() - - urls := []string{ - "http://fffff.com", - "https://google.com", - "http://habr.com", - "http://localhost:80", - "https://postman-echo.com/delay/1", - "https://postman-echo.com/delay/3", - "https://postman-echo.com/delay/5", - "https://postman-echo.com/delay/10", - } - - ticker := time.NewTicker(3 * time.Second) - resultsChan := make(chan []any) - doneChan := make(chan struct{}) // Signals when all urls are processed - - //Feeder routine - go func() { - for { - select { - case <-ticker.C: - if len(urls) == 0 { - close(resultsChan) - return - } - //Pop an url - url := urls[0] - urls = urls[1:] - - fmt.Println("feed this url:", url) - - fm.Components.byName("web crawler").inputs.byName("url").putSignal(newSignal(url)) - _, err := fm.run() - if err != nil { - fmt.Println("fmesh returned error ", err) - } - - if fm.Components.byName("web crawler").outputs.byName("headers").hasSignal() { - results := fm.Components.byName("web crawler").outputs.byName("headers").getSignal().AllValues() - fm.Components.byName("web crawler").outputs.byName("headers").clearSignal() //@TODO maybe we can add fm.Reset() for cases when FMesh is reused (instead of cleaning ports explicitly) - resultsChan <- results - } - } - } - }() - - //Result reader routine - go func() { - for { - select { - case r, ok := <-resultsChan: - if !ok { - fmt.Println("results chan is closed. shutting down the reader") - doneChan <- struct{}{} - return - } - fmt.Println(fmt.Sprintf("got results from channel: %v", r)) - } - } - }() - - <-doneChan -} - -// This example leverages signal aggregation, so urls are pushed into fmesh all at once -// so we wait for all urls to be processed and only them we can read results -func exampleFeedBatches() { - batch := []string{ - "http://fffff.com", - "https://google.com", - "http://habr.com", - "http://localhost:80", - } - - fm := getMesh() - - for _, url := range batch { - fm.Components.byName("web crawler").inputs.byName("url").putSignal(newSignal(url)) - } - - _, err := fm.run() - if err != nil { - fmt.Println("fmesh returned error ", err) - } - - if fm.Components.byName("web crawler").outputs.byName("headers").hasSignal() { - results := fm.Components.byName("web crawler").outputs.byName("headers").getSignal().AllValues() - fmt.Printf("results: %v", results) - } -} - -func getMesh() *FMesh { - //Setup dependencies - client := &http.Client{} - - //Define components - crawler := &Component{ - name: "web crawler", - description: "gets http headers from given url", - inputs: Ports{ - "url": &Port{}, - }, - outputs: Ports{ - "errors": &Port{}, - "headers": &Port{}, - }, - handler: func(inputs Ports, outputs Ports) error { - if !inputs.byName("url").hasSignal() { - return errWaitingForInputResetInputs - } - - for _, sigVal := range inputs.byName("url").getSignal().AllValues() { - - url := sigVal.(string) - //All urls incoming as aggregatet signal will be crawled sequentially - // in order to call them concurrently we need run each request in separate goroutine and handle synchronization (e.g. waitgroup) - response, err := client.Get(url) - if err != nil { - outputs.byName("errors").putSignal(newSignal(fmt.Errorf("got error: %w from url: %s", err, url))) - continue - } - - if len(response.Header) == 0 { - outputs.byName("errors").putSignal(newSignal(fmt.Errorf("no headers for url %s", url))) - continue - } - - outputs.byName("headers").putSignal(newSignal(map[string]http.Header{ - url: response.Header, - })) - } - - return nil - }, - } - - logger := &Component{ - name: "error logger", - description: "logs http errors", - inputs: Ports{ - "error": &Port{}, - }, - outputs: nil, - handler: func(inputs Ports, outputs Ports) error { - if !inputs.byName("error").hasSignal() { - return errWaitingForInputResetInputs - } - - for _, sigVal := range inputs.byName("error").getSignal().AllValues() { - err := sigVal.(error) - if err != nil { - fmt.Println("Error logger says:", err) - } - } - - return nil - }, - } - - //Define pipes - crawler.outputs.byName("errors").CreatePipesTo(logger.inputs.byName("error")) - - //Build mesh - return &FMesh{ - Components: Components{crawler, logger}, - ErrorHandlingStrategy: StopOnFirstError, - } - -} diff --git a/experiments/12_async_input/pipe.go b/experiments/12_async_input/pipe.go deleted file mode 100644 index 8737a62..0000000 --- a/experiments/12_async_input/pipe.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type Pipe struct { - From *Port - To *Port -} - -type Pipes []*Pipe - -func (p *Pipe) flush() { - forwardSignal(p.From, p.To) -} diff --git a/experiments/12_async_input/port.go b/experiments/12_async_input/port.go deleted file mode 100644 index 97c1358..0000000 --- a/experiments/12_async_input/port.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -type Port struct { - signal Signal - pipes Pipes //Refs to pipes connected to that port (no in\out semantics) -} - -func (p *Port) getSignal() Signal { - if p == nil { - panic("invalid port") - } - - if !p.hasSignal() { - return nil - } - - if p.signal.IsAggregate() { - return p.signal.(*AggregateSignal) - } - return p.signal.(*SingleSignal) -} - -func (p *Port) putSignal(sig Signal) { - if p.hasSignal() { - //Aggregate signal - var resValues []*SingleSignal - - //Extract existing signal(s) - if p.signal.IsSingle() { - resValues = append(resValues, p.signal.(*SingleSignal)) - } else if p.signal.IsAggregate() { - resValues = p.signal.(*AggregateSignal).val - } - - //Add new signal(s) - if sig.IsSingle() { - resValues = append(resValues, sig.(*SingleSignal)) - } else if sig.IsAggregate() { - resValues = append(resValues, sig.(*AggregateSignal).val...) - } - - p.signal = &AggregateSignal{ - val: resValues, - } - return - } - - //Single signal - p.signal = sig -} - -func (p *Port) clearSignal() { - p.signal = nil -} - -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) -func (p *Port) addPipeRef(pipe *Pipe) { - p.pipes = append(p.pipes, pipe) -} - -// CreatePipeTo must be used to explicitly set pipe direction -func (p *Port) CreatePipesTo(toPorts ...*Port) { - for _, toPort := range toPorts { - newPipe := &Pipe{ - From: p, - To: toPort, - } - p.addPipeRef(newPipe) - toPort.addPipeRef(newPipe) - } - -} diff --git a/experiments/12_async_input/ports.go b/experiments/12_async_input/ports.go deleted file mode 100644 index b53e6d2..0000000 --- a/experiments/12_async_input/ports.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -// @TODO: this type must have good tooling for working with collection -// like adding new ports, filtering and so on -type Ports map[string]*Port - -// @TODO: add error handling (e.g. when port does not exist) -func (ports Ports) byName(name string) *Port { - return ports[name] -} - -func (ports Ports) manyByName(names ...string) Ports { - selectedPorts := make(Ports) - - for _, name := range names { - if p, ok := ports[name]; ok { - selectedPorts[name] = p - } - } - - return selectedPorts -} - -func (ports Ports) anyHasValue() bool { - for _, p := range ports { - if p.hasSignal() { - return true - } - } - - return false -} - -func (ports Ports) allHaveValue() bool { - for _, p := range ports { - if !p.hasSignal() { - return false - } - } - - return true -} - -func (ports Ports) setAll(val Signal) { - for _, p := range ports { - p.putSignal(val) - } -} - -func (ports Ports) clearAll() { - for _, p := range ports { - p.clearSignal() - } -} diff --git a/experiments/12_async_input/signal.go b/experiments/12_async_input/signal.go deleted file mode 100644 index 55346b9..0000000 --- a/experiments/12_async_input/signal.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -type Signal interface { - IsAggregate() bool - IsSingle() bool - GetValue() any - AllValues() []any //@TODO: refactor with true iterator -} - -// @TODO: enhance naming -type SingleSignal struct { - val any -} - -type AggregateSignal struct { - val []*SingleSignal -} - -func (s SingleSignal) IsAggregate() bool { - return false -} - -func (s SingleSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) IsAggregate() bool { - return true -} - -func (s AggregateSignal) IsSingle() bool { - return !s.IsAggregate() -} - -func (s AggregateSignal) GetValue() any { - return s.val -} - -func (s SingleSignal) GetValue() any { - return s.val -} - -func (s SingleSignal) AllValues() []any { - return []any{s.val} -} - -func (s AggregateSignal) AllValues() []any { - all := make([]any, 0) - for _, sig := range s.val { - all = append(all, sig.GetValue()) - } - return all -} - -func newSignal(val any) *SingleSignal { - return &SingleSignal{val: val} -} - -func forwardSignal(source *Port, dest *Port) { - dest.putSignal(source.getSignal()) -} diff --git a/experiments/README.md b/experiments/README.md deleted file mode 100644 index ee4f01e..0000000 --- a/experiments/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains experiments made to reach a proof of concept of FMesh. Each next experiment is started as a copy of previous one. \ No newline at end of file From a8ca31cf11be2c9dab855a3f9df6f6c8188da6f6 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 10 Nov 2024 02:23:34 +0200 Subject: [PATCH 41/41] Add readme example --- examples/strings_processing/main.go | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 examples/strings_processing/main.go diff --git a/examples/strings_processing/main.go b/examples/strings_processing/main.go new file mode 100644 index 0000000..539a112 --- /dev/null +++ b/examples/strings_processing/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "github.com/hovsep/fmesh" + "github.com/hovsep/fmesh/component" + "github.com/hovsep/fmesh/port" + "github.com/hovsep/fmesh/signal" + "os" + "strings" +) + +// This example is used in readme.md +func main() { + fm := fmesh.New("hello world"). + WithComponents( + component.New("concat"). + WithInputs("i1", "i2"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + word1 := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + word2 := inputs.ByName("i2").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(word1 + word2)) + return nil + }), + component.New("case"). + WithInputs("i1"). + WithOutputs("res"). + WithActivationFunc(func(inputs *port.Collection, outputs *port.Collection) error { + inputString := inputs.ByName("i1").FirstSignalPayloadOrDefault("").(string) + + outputs.ByName("res").PutSignals(signal.New(strings.ToTitle(inputString))) + return nil + })). + WithConfig(fmesh.Config{ + ErrorHandlingStrategy: fmesh.StopOnFirstErrorOrPanic, + CyclesLimit: 10, + }) + + fm.Components().ByName("concat").Outputs().ByName("res").PipeTo( + fm.Components().ByName("case").Inputs().ByName("i1"), + ) + + // Init inputs + fm.Components().ByName("concat").InputByName("i1").PutSignals(signal.New("hello ")) + fm.Components().ByName("concat").InputByName("i2").PutSignals(signal.New("world !")) + + // Run the mesh + _, err := fm.Run() + + // Check for errors + if err != nil { + fmt.Println("F-Mesh returned an error") + os.Exit(1) + } + + //Extract results + results := fm.Components().ByName("case").OutputByName("res").FirstSignalPayloadOrNil() + fmt.Printf("Result is : %v", results) +}