From 57e157694356aaf45c2cca7dd17f5efac210ab1d Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 15 Sep 2024 17:28:44 +0300 Subject: [PATCH 1/8] Add Qodana --- .github/workflows/qodana_code_quality.yml | 20 ++++++++++++++++++++ .gitignore | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/qodana_code_quality.yml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000..f4305a4 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,20 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + - v.0.1.0 + +jobs: + qodana: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.3 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 723ef36..1d31e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea \ No newline at end of file +.idea +qodana.* \ No newline at end of file From 017e844a1f6dc6565992c2054a53893c44fe6a3b Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 15 Sep 2024 19:51:36 +0300 Subject: [PATCH 2/8] FMesh: add new error handling strategy --- component/activation_result.go | 93 +++++++++++++++++----------------- component/component.go | 3 +- cycle/result.go | 30 ++++++++--- errors.go | 1 + fmesh.go | 28 ++++++---- 5 files changed, 90 insertions(+), 65 deletions(-) diff --git a/component/activation_result.go b/component/activation_result.go index fb4ec0f..9fc0afe 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -2,8 +2,8 @@ package component // ActivationResult defines the result (possibly an error) of the activation of given component in given cycle type ActivationResult struct { - activated bool componentName string + activated bool code ActivationCode err error } @@ -31,68 +31,67 @@ const ( ActivationCodePanicked ) -func (ar ActivationResult) HasError() bool { - return ar.Error() != nil +func New(componentName string) *ActivationResult { + return &ActivationResult{ + componentName: componentName, + } } -func (ar ActivationResult) ComponentName() string { - return ar.componentName +func (ar *ActivationResult) SetActivated(activated bool) *ActivationResult { + ar.activated = activated + return ar } -func (ar ActivationResult) Activated() bool { - return ar.activated +func (ar *ActivationResult) WithActivationCode(code ActivationCode) *ActivationResult { + ar.code = code + return ar } -func (ar ActivationResult) Error() error { - return ar.err +func (ar *ActivationResult) WithError(err error) *ActivationResult { + ar.err = err + return ar } -func (c *Component) newActivationResultOK() ActivationResult { - return ActivationResult{ - activated: true, - componentName: c.Name(), - code: ActivationCodeOK, - } +func (c *Component) newActivationResultOK() *ActivationResult { + return New(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeOK) } -func (c *Component) newActivationCodeNoInput() ActivationResult { - return ActivationResult{ - activated: false, - componentName: c.Name(), - code: ActivationCodeNoInput, - } +func (c *Component) newActivationCodeNoInput() *ActivationResult { + return New(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeNoInput) } -func (c *Component) newActivationCodeNoFunction() ActivationResult { - return ActivationResult{ - activated: false, - componentName: c.Name(), - code: ActivationCodeNoFunction, - } +func (c *Component) newActivationCodeNoFunction() *ActivationResult { + return New(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeNoFunction) } -func (c *Component) newActivationCodeWaitingForInput() ActivationResult { - return ActivationResult{ - activated: false, - componentName: c.Name(), - code: ActivationCodeWaitingForInput, - } +func (c *Component) newActivationCodeWaitingForInput() *ActivationResult { + return New(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeWaitingForInput) } -func (c *Component) newActivationCodeReturnedError(err error) ActivationResult { - return ActivationResult{ - activated: true, - componentName: c.Name(), - code: ActivationCodeReturnedError, - err: err, - } +func (c *Component) newActivationCodeReturnedError(err error) *ActivationResult { + return New(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeReturnedError).WithError(err) } -func (c *Component) newActivationCodePanicked(err error) ActivationResult { - return ActivationResult{ - activated: true, - componentName: c.Name(), - code: ActivationCodePanicked, - err: err, - } +func (c *Component) newActivationCodePanicked(err error) *ActivationResult { + return New(c.Name()).SetActivated(true).WithActivationCode(ActivationCodePanicked).WithError(err) +} + +func (ar *ActivationResult) ComponentName() string { + return ar.componentName +} + +func (ar *ActivationResult) Activated() bool { + return ar.activated +} + +func (ar *ActivationResult) Error() error { + return ar.err +} + +func (ar *ActivationResult) HasError() bool { + return ar.code == ActivationCodeReturnedError && ar.Error() != nil +} + +func (ar *ActivationResult) HasPanic() bool { + return ar.code == ActivationCodePanicked && ar.Error() != nil } diff --git a/component/component.go b/component/component.go index 41b5cdc..e265cce 100644 --- a/component/component.go +++ b/component/component.go @@ -86,13 +86,14 @@ func (c *Component) hasActivationFunction() bool { } // MaybeActivate tries to run the activation function if all required conditions are met -func (c *Component) MaybeActivate() (activationResult ActivationResult) { +func (c *Component) MaybeActivate() (activationResult *ActivationResult) { defer func() { if r := recover(); r != nil { errorFormat := "panicked with: %v, stacktrace: %s" if _, ok := r.(error); ok { errorFormat = "panicked with: %w, stacktrace: %s" } + //TODO: add custom error activationResult = c.newActivationCodePanicked(fmt.Errorf(errorFormat, r, debug.Stack())) } }() diff --git a/cycle/result.go b/cycle/result.go index ac24153..f132c05 100644 --- a/cycle/result.go +++ b/cycle/result.go @@ -8,7 +8,7 @@ import ( // Result contains the information about activation cycle type Result struct { sync.Mutex - ActivationResults map[string]component.ActivationResult + ActivationResults map[string]*component.ActivationResult } // Results contains the results of several activation cycles @@ -16,17 +16,23 @@ type Results []*Result func NewResult() *Result { return &Result{ - ActivationResults: make(map[string]component.ActivationResult), + ActivationResults: make(map[string]*component.ActivationResult), } } +// WithActivationResult adds an activation result of particular component to cycle result +func (result *Result) WithActivationResult(ar *component.ActivationResult) *Result { + result.ActivationResults[ar.ComponentName()] = ar + return result +} + func NewResults() Results { return make(Results, 0) } -// HasErrors tells whether the cycle is ended wih activation errors -func (r *Result) HasErrors() bool { - for _, ar := range r.ActivationResults { +// HasErrors tells whether the cycle is ended wih activation errors (at lease one component returned an error) +func (result *Result) HasErrors() bool { + for _, ar := range result.ActivationResults { if ar.HasError() { return true } @@ -34,8 +40,18 @@ func (r *Result) HasErrors() bool { return false } -func (r *Result) HasActivatedComponents() bool { - for _, ar := range r.ActivationResults { +// HasPanics tells whether the cycle is ended wih panic(at lease one component panicked) +func (result *Result) HasPanics() bool { + for _, ar := range result.ActivationResults { + if ar.HasPanic() { + return true + } + } + return false +} + +func (result *Result) HasActivatedComponents() bool { + for _, ar := range result.ActivationResults { if ar.Activated() { return true } diff --git a/errors.go b/errors.go index 2d5f6d8..a899d51 100644 --- a/errors.go +++ b/errors.go @@ -4,5 +4,6 @@ type ErrorHandlingStrategy int const ( StopOnFirstError ErrorHandlingStrategy = iota + StopOnFirstPanic IgnoreAll ) diff --git a/fmesh.go b/fmesh.go index 1d032f2..8025177 100644 --- a/fmesh.go +++ b/fmesh.go @@ -43,12 +43,16 @@ func (fm *FMesh) WithErrorHandlingStrategy(strategy ErrorHandlingStrategy) *FMes return fm } -// ActivateComponents runs one activation cycle (tries to activate all components) -func (fm *FMesh) activateComponents() *cycle.Result { +// runCycle runs one activation cycle (tries to activate all components) +func (fm *FMesh) runCycle() *cycle.Result { cycleResult := cycle.NewResult() - activationResultsChan := make(chan component.ActivationResult) //@TODO: close the channel - doneChan := make(chan struct{}) + if len(fm.components) == 0 { + return cycleResult + } + + activationResultsChan := make(chan *component.ActivationResult) //@TODO: close the channel + doneChan := make(chan struct{}) //@TODO: close the channel var wg sync.WaitGroup @@ -56,8 +60,9 @@ func (fm *FMesh) activateComponents() *cycle.Result { for { select { case aRes := <-activationResultsChan: + //@TODO :check for closed channel cycleResult.Lock() - cycleResult.ActivationResults[aRes.ComponentName()] = aRes + cycleResult = cycleResult.WithActivationResult(aRes) cycleResult.Unlock() case <-doneChan: return @@ -75,7 +80,7 @@ func (fm *FMesh) activateComponents() *cycle.Result { } wg.Wait() - doneChan <- struct{}{} + doneChan <- struct{}{} //@TODO: no need to send close signal, just close the channel return cycleResult } @@ -90,12 +95,14 @@ func (fm *FMesh) drainComponents() { func (fm *FMesh) Run() (cycle.Results, error) { allCycles := cycle.NewResults() for { - cycleResult := fm.activateComponents() + cycleResult := fm.runCycle() if fm.shouldStop(cycleResult) { + //TODO: add custom error return allCycles, fmt.Errorf("cycle #%d finished with errors. Stopping fmesh. Report: %v", len(allCycles), cycleResult.ActivationResults) } + //@TODO: maybe better move this to shouldStop if !cycleResult.HasActivatedComponents() { //No component activated in this cycle. FMesh is ready to stop return allCycles, nil @@ -109,12 +116,13 @@ func (fm *FMesh) Run() (cycle.Results, error) { func (fm *FMesh) shouldStop(cycleResult *cycle.Result) bool { switch fm.errorHandlingStrategy { case StopOnFirstError: - if cycleResult.HasErrors() { - return true - } + return cycleResult.HasErrors() + case StopOnFirstPanic: + return cycleResult.HasPanics() case IgnoreAll: return false default: + //@TODO: maybe better to return error panic("unsupported error handling strategy") } return false From 63353767869edd056b93bd16b065fea9ff048aa2 Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 15 Sep 2024 22:33:09 +0300 Subject: [PATCH 3/8] Component: fix tests --- component/component.go | 16 ++++---- component/component_test.go | 75 +++++++++++++++---------------------- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/component/component.go b/component/component.go index e265cce..af2ce7e 100644 --- a/component/component.go +++ b/component/component.go @@ -22,12 +22,14 @@ type Component struct { type Components map[string]*Component // NewComponent creates a new empty component +// TODO: rename all constructors to New func NewComponent(name string) *Component { return &Component{name: name} } // NewComponents creates a collection of components // names are optional and can be used to create multiple empty components in one call +// @TODO: rename all such constructors to NewCollection func NewComponents(names ...string) Components { components := make(Components, len(names)) for _, name := range names { @@ -98,17 +100,17 @@ func (c *Component) MaybeActivate() (activationResult *ActivationResult) { } }() - //@TODO:: https://github.com/hovsep/fmesh/issues/15 - if !c.inputs.AnyHasSignal() { - //No inputs set, stop here - activationResult = c.newActivationCodeNoInput() + if !c.hasActivationFunction() { + //Activation function is not set (maybe useful while the mesh is under development) + activationResult = c.newActivationCodeNoFunction() return } - if !c.hasActivationFunction() { - //Activation function is not set (maybe useful while the mesh is under development) - activationResult = c.newActivationCodeNoFunction() + //@TODO:: https://github.com/hovsep/fmesh/issues/15 + if !c.inputs.AnyHasSignal() { + //No inputs set, stop here + activationResult = c.newActivationCodeNoInput() return } diff --git a/component/component_test.go b/component/component_test.go index 2787fac..a9efa80 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -2,7 +2,6 @@ package component import ( "errors" - "github.com/hovsep/fmesh/cycle" "github.com/hovsep/fmesh/port" "github.com/hovsep/fmesh/signal" "github.com/stretchr/testify/assert" @@ -432,20 +431,16 @@ func TestNew(t *testing.T) { func TestComponent_Activate(t *testing.T) { tests := []struct { - name string - getComponent func() *Component - wantARes cycle.ActivationResult + name string + getComponent func() *Component + wantActivationResult *ActivationResult }{ { name: "empty component is not activated", getComponent: func() *Component { return NewComponent("c1") }, - wantARes: cycle.ActivationResult{ - Activated: false, - ComponentName: "c1", - ActivationError: nil, - }, + wantActivationResult: NewActivationResult("c1").SetActivated(false).WithActivationCode(ActivationCodeNoFunction), }, { name: "component with inputs set, but no activation func", @@ -454,11 +449,9 @@ func TestComponent_Activate(t *testing.T) { c.Inputs().ByName("i1").PutSignal(signal.New(123)) return c }, - wantARes: cycle.ActivationResult{ - Activated: false, - ComponentName: "c1", - ActivationError: nil, - }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(ActivationCodeNoFunction), }, { name: "component is waiting for input", @@ -477,11 +470,9 @@ func TestComponent_Activate(t *testing.T) { c.Inputs().ByName("i1").PutSignal(signal.New(123)) return c }, - wantARes: cycle.ActivationResult{ - Activated: false, - ComponentName: "c1", - ActivationError: nil, - }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(ActivationCodeWaitingForInput), }, { name: "activated with error", @@ -495,11 +486,10 @@ func TestComponent_Activate(t *testing.T) { c.Inputs().ByName("i1").PutSignal(signal.New(123)) return c }, - wantARes: cycle.ActivationResult{ - Activated: true, - ComponentName: "c1", - ActivationError: errors.New("failed to activate component: test error"), - }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(ActivationCodeReturnedError). + WithError(errors.New("failed to activate component: test error")), }, { name: "activated without error", @@ -515,11 +505,9 @@ func TestComponent_Activate(t *testing.T) { c.Inputs().ByName("i1").PutSignal(signal.New(123)) return c }, - wantARes: cycle.ActivationResult{ - Activated: true, - ComponentName: "c1", - ActivationError: nil, - }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(ActivationCodeOK), }, { name: "component panicked with error", @@ -536,11 +524,10 @@ func TestComponent_Activate(t *testing.T) { c.Inputs().ByName("i1").PutSignal(signal.New(123)) return c }, - wantARes: cycle.ActivationResult{ - Activated: true, - ComponentName: "c1", - ActivationError: errors.New("panicked with: oh shrimps"), - }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(ActivationCodePanicked). + WithError(errors.New("panicked with: oh shrimps")), }, { name: "component panicked with string", @@ -557,22 +544,22 @@ func TestComponent_Activate(t *testing.T) { c.Inputs().ByName("i1").PutSignal(signal.New(123)) return c }, - wantARes: cycle.ActivationResult{ - Activated: true, - ComponentName: "c1", - ActivationError: errors.New("panicked with: oh shrimps"), - }, + wantActivationResult: NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(ActivationCodePanicked). + WithError(errors.New("panicked with: oh shrimps")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.getComponent().MaybeActivate() - assert.Equal(t, got.Activated, tt.wantARes.Activated) - assert.Equal(t, got.ComponentName, tt.wantARes.ComponentName) - if tt.wantARes.ActivationError == nil { - assert.Nil(t, got.ActivationError) + assert.Equal(t, got.Activated(), tt.wantActivationResult.Activated()) + assert.Equal(t, got.ComponentName(), tt.wantActivationResult.ComponentName()) + assert.Equal(t, got.Code(), tt.wantActivationResult.Code()) + if !tt.wantActivationResult.HasError() { + assert.False(t, got.HasError()) } else { - assert.ErrorContains(t, got.ActivationError, tt.wantARes.ActivationError.Error()) + assert.ErrorContains(t, got.Error(), tt.wantActivationResult.Error().Error()) } }) From 582b589d8e3d998a356d216d4bdcd72f239066dd Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 15 Sep 2024 22:33:24 +0300 Subject: [PATCH 4/8] Various refactorings --- component/activation_result.go | 44 ++++++++++++++++++++++++---------- cycle/result.go | 12 ++++++++-- port/port.go | 5 +--- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/component/activation_result.go b/component/activation_result.go index 9fc0afe..15f9c58 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -4,16 +4,16 @@ package component type ActivationResult struct { componentName string activated bool - code ActivationCode + code ActivationResultCode err error } -// ActivationCode denotes a specific info about how a component been activated or why not activated at all -type ActivationCode int +// ActivationResultCode denotes a specific info about how a component been activated or why not activated at all +type ActivationResultCode int const ( // ActivationCodeOK ...: component is activated and did not return any errors - ActivationCodeOK ActivationCode = iota + ActivationCodeOK ActivationResultCode = iota // ActivationCodeNoInput ...: component is not activated because it has no input set ActivationCodeNoInput @@ -31,67 +31,87 @@ const ( ActivationCodePanicked ) -func New(componentName string) *ActivationResult { +// NewActivationResult creates a new activation result for given component +func NewActivationResult(componentName string) *ActivationResult { return &ActivationResult{ componentName: componentName, } } +// SetActivated setter func (ar *ActivationResult) SetActivated(activated bool) *ActivationResult { ar.activated = activated return ar } -func (ar *ActivationResult) WithActivationCode(code ActivationCode) *ActivationResult { +// WithActivationCode setter +func (ar *ActivationResult) WithActivationCode(code ActivationResultCode) *ActivationResult { ar.code = code return ar } +// WithError setter func (ar *ActivationResult) WithError(err error) *ActivationResult { ar.err = err return ar } +// newActivationResultOK builds a specific activation result func (c *Component) newActivationResultOK() *ActivationResult { - return New(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeOK) + return NewActivationResult(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeOK) } +// newActivationCodeNoInput builds a specific activation result func (c *Component) newActivationCodeNoInput() *ActivationResult { - return New(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeNoInput) + return NewActivationResult(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeNoInput) } +// newActivationCodeNoFunction builds a specific activation result func (c *Component) newActivationCodeNoFunction() *ActivationResult { - return New(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeNoFunction) + return NewActivationResult(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeNoFunction) } +// newActivationCodeWaitingForInput builds a specific activation result func (c *Component) newActivationCodeWaitingForInput() *ActivationResult { - return New(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeWaitingForInput) + return NewActivationResult(c.Name()).SetActivated(false).WithActivationCode(ActivationCodeWaitingForInput) } +// newActivationCodeReturnedError builds a specific activation result func (c *Component) newActivationCodeReturnedError(err error) *ActivationResult { - return New(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeReturnedError).WithError(err) + return NewActivationResult(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeReturnedError).WithError(err) } +// newActivationCodePanicked builds a specific activation result func (c *Component) newActivationCodePanicked(err error) *ActivationResult { - return New(c.Name()).SetActivated(true).WithActivationCode(ActivationCodePanicked).WithError(err) + return NewActivationResult(c.Name()).SetActivated(true).WithActivationCode(ActivationCodePanicked).WithError(err) } +// ComponentName getter func (ar *ActivationResult) ComponentName() string { return ar.componentName } +// Activated getter func (ar *ActivationResult) Activated() bool { return ar.activated } +// Error getter func (ar *ActivationResult) Error() error { return ar.err } +// Code getter +func (ar *ActivationResult) Code() ActivationResultCode { + return ar.code +} + +// HasError returns true when activation result has an error func (ar *ActivationResult) HasError() bool { return ar.code == ActivationCodeReturnedError && ar.Error() != nil } +// HasPanic returns true when activation result is derived from panic func (ar *ActivationResult) HasPanic() bool { return ar.code == ActivationCodePanicked && ar.Error() != nil } diff --git a/cycle/result.go b/cycle/result.go index f132c05..81b6cc1 100644 --- a/cycle/result.go +++ b/cycle/result.go @@ -21,8 +21,16 @@ func NewResult() *Result { } // WithActivationResult adds an activation result of particular component to cycle result -func (result *Result) WithActivationResult(ar *component.ActivationResult) *Result { - result.ActivationResults[ar.ComponentName()] = ar +func (result *Result) WithActivationResult(activationResult *component.ActivationResult) *Result { + result.ActivationResults[activationResult.ComponentName()] = activationResult + return result +} + +// WithActivationResults adds multiple activation results +func (result *Result) WithActivationResults(activationResults ...*component.ActivationResult) *Result { + for _, activationResult := range activationResults { + result.WithActivationResult(activationResult) + } return result } diff --git a/port/port.go b/port/port.go index c958f5c..061760e 100644 --- a/port/port.go +++ b/port/port.go @@ -8,8 +8,7 @@ import ( type Port struct { name string signal *signal.Signal //Current signal set on the port - //@TODO:think of replacing pipe abstraction with list of "To" ports, as all pipes always point to this port as their "From" - pipes Pipes //Refs to pipes connected to this port (without in\out semantics) + pipes Pipes //Refs to all outbound pipes connected to this port } // Ports is just useful collection type @@ -50,7 +49,6 @@ func (p *Port) PutSignal(sig *signal.Signal) { } // ClearSignal removes current signal from the port -// @TODO: check if this affects the signal itself, as it is a pointer func (p *Port) ClearSignal() { p.signal = nil } @@ -75,7 +73,6 @@ func (p *Port) PipeTo(toPorts ...*Port) { p.addPipeRef(newPipe) toPort.addPipeRef(newPipe) } - } // ByName returns a port by its name From 04b01fc3e9f5a4413b4e0cd099114be32688e90e Mon Sep 17 00:00:00 2001 From: hovsep Date: Sun, 15 Sep 2024 23:53:23 +0300 Subject: [PATCH 5/8] FMesh: add tests --- component/activation_result.go | 3 + component/component.go | 8 +- cycle/result.go | 60 +++++--- errors.go | 16 +++ fmesh.go | 52 ++++--- fmesh_test.go | 246 +++++++++++++++++++++++++++++---- 6 files changed, 317 insertions(+), 68 deletions(-) diff --git a/component/activation_result.go b/component/activation_result.go index 15f9c58..b4269ee 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -8,6 +8,9 @@ type ActivationResult struct { err error } +// ActivationResults is a collection +type ActivationResults map[string]*ActivationResult + // ActivationResultCode denotes a specific info about how a component been activated or why not activated at all type ActivationResultCode int diff --git a/component/component.go b/component/component.go index af2ce7e..51bdcf6 100644 --- a/component/component.go +++ b/component/component.go @@ -162,8 +162,10 @@ func (components Components) ByName(name string) *Component { return components[name] } -// Add adds a component to collection -func (components Components) Add(component *Component) Components { - components[component.Name()] = component +// Add adds new components to existing collection +func (components Components) Add(newComponents ...*Component) Components { + for _, component := range newComponents { + components[component.Name()] = component + } return components } diff --git a/cycle/result.go b/cycle/result.go index 81b6cc1..174617e 100644 --- a/cycle/result.go +++ b/cycle/result.go @@ -8,39 +8,57 @@ import ( // Result contains the information about activation cycle type Result struct { sync.Mutex - ActivationResults map[string]*component.ActivationResult + cycleNumber int + activationResults component.ActivationResults } // Results contains the results of several activation cycles type Results []*Result +// NewResult creates a new cycle result func NewResult() *Result { return &Result{ - ActivationResults: make(map[string]*component.ActivationResult), + activationResults: make(map[string]*component.ActivationResult), } } +// NewResults creates a collection +func NewResults() Results { + return make(Results, 0) +} + +func (cycleResult *Result) SetCycleNumber(n int) *Result { + cycleResult.cycleNumber = n + return cycleResult +} + +// CycleNumber getter +func (cycleResult *Result) CycleNumber() int { + return cycleResult.cycleNumber +} + +// ActivationResults getter +func (cycleResult *Result) ActivationResults() component.ActivationResults { + return cycleResult.activationResults +} + // WithActivationResult adds an activation result of particular component to cycle result -func (result *Result) WithActivationResult(activationResult *component.ActivationResult) *Result { - result.ActivationResults[activationResult.ComponentName()] = activationResult - return result +func (cycleResult *Result) WithActivationResult(activationResult *component.ActivationResult) *Result { + cycleResult.activationResults[activationResult.ComponentName()] = activationResult + return cycleResult } // WithActivationResults adds multiple activation results -func (result *Result) WithActivationResults(activationResults ...*component.ActivationResult) *Result { +func (cycleResult *Result) WithActivationResults(activationResults ...*component.ActivationResult) *Result { for _, activationResult := range activationResults { - result.WithActivationResult(activationResult) + cycleResult.WithActivationResult(activationResult) } - return result -} - -func NewResults() Results { - return make(Results, 0) + return cycleResult } // HasErrors tells whether the cycle is ended wih activation errors (at lease one component returned an error) -func (result *Result) HasErrors() bool { - for _, ar := range result.ActivationResults { +func (cycleResult *Result) HasErrors() bool { + for _, ar := range cycleResult.activationResults { if ar.HasError() { return true } @@ -49,8 +67,8 @@ func (result *Result) HasErrors() bool { } // HasPanics tells whether the cycle is ended wih panic(at lease one component panicked) -func (result *Result) HasPanics() bool { - for _, ar := range result.ActivationResults { +func (cycleResult *Result) HasPanics() bool { + for _, ar := range cycleResult.activationResults { if ar.HasPanic() { return true } @@ -58,11 +76,17 @@ func (result *Result) HasPanics() bool { return false } -func (result *Result) HasActivatedComponents() bool { - for _, ar := range result.ActivationResults { +func (cycleResult *Result) HasActivatedComponents() bool { + for _, ar := range cycleResult.activationResults { if ar.Activated() { return true } } return false } + +// Add adds a cycle result to existing collection +func (cycleResults Results) Add(cycleResult *Result) Results { + cycleResults = append(cycleResults, cycleResult) + return cycleResults +} diff --git a/errors.go b/errors.go index a899d51..9eb155f 100644 --- a/errors.go +++ b/errors.go @@ -1,5 +1,11 @@ package fmesh +import ( + "errors" + "fmt" + "github.com/hovsep/fmesh/cycle" +) + type ErrorHandlingStrategy int const ( @@ -7,3 +13,13 @@ const ( StopOnFirstPanic IgnoreAll ) + +var ( + ErrHitAnError = errors.New("f-mesh hit an error and will be stopped") + ErrHitAPanic = errors.New("f-mesh hit a panic and will be stopped") + ErrUnsupportedErrorHandlingStrategy = errors.New("unsupported error handling strategy") +) + +func newFMeshStopError(err error, cycleResult *cycle.Result) error { + return fmt.Errorf("%w (cycle #%d activation results: %v)", err, cycleResult.CycleNumber(), cycleResult.ActivationResults()) +} diff --git a/fmesh.go b/fmesh.go index 8025177..39272f7 100644 --- a/fmesh.go +++ b/fmesh.go @@ -1,7 +1,6 @@ package fmesh import ( - "fmt" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "sync" @@ -17,7 +16,21 @@ type FMesh struct { // New creates a new f-mesh func New(name string) *FMesh { - return &FMesh{name: name} + return &FMesh{name: name, components: component.NewComponents()} +} + +// Name getter +func (fm *FMesh) Name() string { + return fm.name +} + +// Description getter +func (fm *FMesh) Description() string { + return fm.description +} + +func (fm *FMesh) Components() component.Components { + return fm.components } // WithDescription sets a description @@ -28,9 +41,6 @@ func (fm *FMesh) WithDescription(description string) *FMesh { // WithComponents adds components to f-mesh func (fm *FMesh) WithComponents(components ...*component.Component) *FMesh { - if fm.components == nil { - fm.components = component.NewComponents() - } for _, c := range components { fm.components.Add(c) } @@ -97,33 +107,33 @@ func (fm *FMesh) Run() (cycle.Results, error) { for { cycleResult := fm.runCycle() - if fm.shouldStop(cycleResult) { - //TODO: add custom error - return allCycles, fmt.Errorf("cycle #%d finished with errors. Stopping fmesh. Report: %v", len(allCycles), cycleResult.ActivationResults) - } - - //@TODO: maybe better move this to shouldStop - if !cycleResult.HasActivatedComponents() { - //No component activated in this cycle. FMesh is ready to stop - return allCycles, nil + mustStop, err := fm.mustStop(cycleResult) + if mustStop { + return allCycles, err } - allCycles = append(allCycles, cycleResult) + allCycles.Add(cycleResult) fm.drainComponents() } } -func (fm *FMesh) shouldStop(cycleResult *cycle.Result) bool { +func (fm *FMesh) mustStop(cycleResult *cycle.Result) (bool, error) { + //Check if we are done (no components activated during the cycle => all inputs are processed) + if !cycleResult.HasActivatedComponents() { + return true, nil + } + + //Check if mesh must stop because of configured error handling strategy switch fm.errorHandlingStrategy { case StopOnFirstError: - return cycleResult.HasErrors() + return cycleResult.HasErrors(), newFMeshStopError(ErrHitAnError, cycleResult) case StopOnFirstPanic: - return cycleResult.HasPanics() + return cycleResult.HasPanics(), newFMeshStopError(ErrHitAPanic, cycleResult) case IgnoreAll: - return false + return false, nil default: //@TODO: maybe better to return error - panic("unsupported error handling strategy") + + return true, ErrUnsupportedErrorHandlingStrategy } - return false } diff --git a/fmesh_test.go b/fmesh_test.go index a8b26d9..3564753 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -3,6 +3,9 @@ package fmesh import ( "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" "reflect" "testing" ) @@ -21,14 +24,19 @@ func TestNew(t *testing.T) { args: args{ name: "", }, - want: &FMesh{}, + want: &FMesh{ + components: component.Components{}, + }, }, { name: "with name", args: args{ name: "fm1", }, - want: &FMesh{name: "fm1"}, + want: &FMesh{ + name: "fm1", + components: component.Components{}, + }, }, } for _, tt := range tests { @@ -59,7 +67,7 @@ func TestFMesh_WithDescription(t *testing.T) { want: &FMesh{ name: "fm1", description: "", - components: nil, + components: component.Components{}, errorHandlingStrategy: 0, }, }, @@ -72,7 +80,7 @@ func TestFMesh_WithDescription(t *testing.T) { want: &FMesh{ name: "fm1", description: "descr", - components: nil, + components: component.Components{}, errorHandlingStrategy: 0, }, }, @@ -104,6 +112,7 @@ func TestFMesh_WithErrorHandlingStrategy(t *testing.T) { }, want: &FMesh{ name: "fm1", + components: component.Components{}, errorHandlingStrategy: StopOnFirstError, }, }, @@ -115,6 +124,7 @@ func TestFMesh_WithErrorHandlingStrategy(t *testing.T) { }, want: &FMesh{ name: "fm1", + components: component.Components{}, errorHandlingStrategy: IgnoreAll, }, }, @@ -213,6 +223,58 @@ 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) { + if got := tt.fm.Name(); got != tt.want { + t.Errorf("Name() = %v, want %v", got, tt.want) + } + }) + } +} + +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) { + if got := tt.fm.Description(); got != tt.want { + t.Errorf("Description() = %v, want %v", got, tt.want) + } + }) + } +} + func TestFMesh_Run(t *testing.T) { type fields struct { name string @@ -248,45 +310,177 @@ func TestFMesh_Run(t *testing.T) { } } -func TestFMesh_activateComponents(t *testing.T) { +func TestFMesh_runCycle(t *testing.T) { tests := []struct { - name string - fm *FMesh - want *cycle.Result + name string + fm *FMesh + initFM func(fm *FMesh) + want *cycle.Result }{ - //@TODO + { + name: "empty mesh", + fm: New("empty mesh"), + want: cycle.NewResult(), + }, + { + name: "mesh has components, but no one is activated", + fm: New("test").WithComponents( + component.NewComponent("c1"). + WithDescription("I do not have any input signal set, hence I will never be activated"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(signal.New("this signal will never be sent")) + return nil + }), + + component.NewComponent("c2"). + WithDescription("I do not have activation func set"). + WithInputs("i1"). + WithOutputs("o1"), + + component.NewComponent("c3"). + WithDescription("I'm waiting for specific input"). + WithInputs("i1", "i2"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + if !inputs.ByNames("i1", "i2").AllHaveSignal() { + return component.ErrWaitingForInputKeepInputs + } + return nil + }), + ), + initFM: func(fm *FMesh) { + //Only i1 is set, while component is waiting for both i1 and i2 to be set + fm.Components().ByName("c3").Inputs().ByName("i1").PutSignal(signal.New(123)) + }, + want: cycle.NewResult(). + WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(false).WithActivationCode(component.ActivationCodeNoFunction), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput)), + }, + { + name: "all components activated in one cycle (concurrently)", + fm: New("test").WithComponents( + component.NewComponent("c1").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + return nil + }), + component.NewComponent("c2").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + return nil + }), + component.NewComponent("c3").WithDescription("").WithInputs("i1").WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + return nil + }), + ), + initFM: func(fm *FMesh) { + fm.Components().ByName("c1").Inputs().ByName("i1").PutSignal(signal.New(1)) + fm.Components().ByName("c2").Inputs().ByName("i1").PutSignal(signal.New(2)) + fm.Components().ByName("c3").Inputs().ByName("i1").PutSignal(signal.New(3)) + }, + want: cycle.NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c3").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + ), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.fm.activateComponents(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("activateComponents() = %v, want %v", got, tt.want) + if tt.initFM != nil { + tt.initFM(tt.fm) + } + if got := tt.fm.runCycle(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("runCycle() = %v, want %v", got, tt.want) } }) } } func TestFMesh_drainComponents(t *testing.T) { - type fields struct { - name string - description string - components component.Components - errorHandlingStrategy ErrorHandlingStrategy - } tests := []struct { - name string - fields fields + name string + fm *FMesh + initFM func(fm *FMesh) + assertionsAfterDrain func(t *testing.T, fm *FMesh) }{ - // TODO: Add test cases. + { + name: "no components", + fm: New("empty_fm"), + assertionsAfterDrain: func(t *testing.T, fm *FMesh) { + assert.Empty(t, fm.Components()) + }, + }, + { + name: "no signals to be drained", + fm: New("fm").WithComponents( + component.NewComponent("c1").WithInputs("i1").WithOutputs("o1"), + component.NewComponent("c2").WithInputs("i1").WithOutputs("o1"), + ), + initFM: func(fm *FMesh) { + //Create a pipe + c1, c2 := fm.Components().ByName("c1"), fm.Components().ByName("c2") + c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + }, + assertionsAfterDrain: func(t *testing.T, fm *FMesh) { + //All ports in all components are empty + assert.False(t, fm.Components().ByName("c1").Inputs().AnyHasSignal()) + assert.False(t, fm.Components().ByName("c1").Outputs().AnyHasSignal()) + assert.False(t, fm.Components().ByName("c2").Inputs().AnyHasSignal()) + assert.False(t, fm.Components().ByName("c2").Outputs().AnyHasSignal()) + }, + }, + { + name: "there are signals on output, but no pipes", + fm: New("fm").WithComponents( + component.NewComponent("c1").WithInputs("i1").WithOutputs("o1"), + component.NewComponent("c2").WithInputs("i1").WithOutputs("o1"), + ), + initFM: func(fm *FMesh) { + //Both components have signals on their outputs + fm.Components().ByName("c1").Outputs().ByName("o1").PutSignal(signal.New(1)) + fm.Components().ByName("c2").Outputs().ByName("o1").PutSignal(signal.New(1)) + }, + assertionsAfterDrain: func(t *testing.T, fm *FMesh) { + //Output signals are still there + assert.True(t, fm.Components().ByName("c1").Outputs().ByName("o1").HasSignal()) + assert.True(t, fm.Components().ByName("c2").Outputs().ByName("o1").HasSignal()) + + //Inputs are clear + assert.False(t, fm.Components().ByName("c1").Inputs().ByName("i1").HasSignal()) + assert.False(t, fm.Components().ByName("c2").Inputs().ByName("i1").HasSignal()) + }, + }, + { + name: "happy path", + fm: New("fm").WithComponents( + component.NewComponent("c1").WithInputs("i1").WithOutputs("o1"), + component.NewComponent("c2").WithInputs("i1").WithOutputs("o1"), + ), + initFM: func(fm *FMesh) { + //Create a pipe + c1, c2 := fm.Components().ByName("c1"), fm.Components().ByName("c2") + c1.Outputs().ByName("o1").PipeTo(c2.Inputs().ByName("i1")) + + //c1 has a signal which must go to c2.i1 after drain + c1.Outputs().ByName("o1").PutSignal(signal.New(123)) + }, + assertionsAfterDrain: func(t *testing.T, fm *FMesh) { + c1, c2 := fm.Components().ByName("c1"), fm.Components().ByName("c2") + + assert.True(t, c2.Inputs().ByName("i1").HasSignal()) //Signal is transferred to destination port + assert.False(t, c1.Outputs().ByName("o1").HasSignal()) //Source port is cleaned up + assert.Equal(t, c2.Inputs().ByName("i1").Signal().Payload().(int), 123) //The signal is correct + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - fm := &FMesh{ - name: tt.fields.name, - description: tt.fields.description, - components: tt.fields.components, - errorHandlingStrategy: tt.fields.errorHandlingStrategy, + if tt.initFM != nil { + tt.initFM(tt.fm) } - fm.drainComponents() + tt.fm.drainComponents() + tt.assertionsAfterDrain(t, tt.fm) }) } } From 8524428a7e134f386ee8e730bef967a61c0657dd Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 16 Sep 2024 01:46:19 +0300 Subject: [PATCH 6/8] Cycle: add tests --- cycle/result.go | 19 +-- cycle/result_test.go | 359 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+), 13 deletions(-) create mode 100644 cycle/result_test.go diff --git a/cycle/result.go b/cycle/result.go index 174617e..6d608b7 100644 --- a/cycle/result.go +++ b/cycle/result.go @@ -8,7 +8,7 @@ import ( // Result contains the information about activation cycle type Result struct { sync.Mutex - cycleNumber int + cycleNumber uint activationResults component.ActivationResults } @@ -18,7 +18,7 @@ type Results []*Result // NewResult creates a new cycle result func NewResult() *Result { return &Result{ - activationResults: make(map[string]*component.ActivationResult), + activationResults: make(component.ActivationResults), } } @@ -27,13 +27,13 @@ func NewResults() Results { return make(Results, 0) } -func (cycleResult *Result) SetCycleNumber(n int) *Result { +func (cycleResult *Result) SetCycleNumber(n uint) *Result { cycleResult.cycleNumber = n return cycleResult } // CycleNumber getter -func (cycleResult *Result) CycleNumber() int { +func (cycleResult *Result) CycleNumber() uint { return cycleResult.cycleNumber } @@ -42,16 +42,10 @@ func (cycleResult *Result) ActivationResults() component.ActivationResults { return cycleResult.activationResults } -// WithActivationResult adds an activation result of particular component to cycle result -func (cycleResult *Result) WithActivationResult(activationResult *component.ActivationResult) *Result { - cycleResult.activationResults[activationResult.ComponentName()] = activationResult - return cycleResult -} - // WithActivationResults adds multiple activation results func (cycleResult *Result) WithActivationResults(activationResults ...*component.ActivationResult) *Result { for _, activationResult := range activationResults { - cycleResult.WithActivationResult(activationResult) + cycleResult.activationResults[activationResult.ComponentName()] = activationResult } return cycleResult } @@ -87,6 +81,5 @@ func (cycleResult *Result) HasActivatedComponents() bool { // Add adds a cycle result to existing collection func (cycleResults Results) Add(cycleResult *Result) Results { - cycleResults = append(cycleResults, cycleResult) - return cycleResults + return append(cycleResults, cycleResult) } diff --git a/cycle/result_test.go b/cycle/result_test.go new file mode 100644 index 0000000..273818b --- /dev/null +++ b/cycle/result_test.go @@ -0,0 +1,359 @@ +package cycle + +import ( + "errors" + "github.com/hovsep/fmesh/component" + "reflect" + "testing" +) + +func TestResults_Add(t *testing.T) { + type args struct { + cycleResult *Result + } + tests := []struct { + name string + cycleResults Results + args args + want Results + }{ + { + name: "happy path", + cycleResults: NewResults(), + args: args{ + cycleResult: NewResult().SetCycleNumber(1).WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), + }, + want: Results{ + { + cycleNumber: 1, + activationResults: component.ActivationResults{ + "c1": component.NewActivationResult("c1").SetActivated(true), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResults.Add(tt.args.cycleResult); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Add() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewResult(t *testing.T) { + tests := []struct { + name string + want *Result + }{ + { + name: "happy path", + want: &Result{ + cycleNumber: 0, + activationResults: component.ActivationResults{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewResult(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewResult() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewResults(t *testing.T) { + tests := []struct { + name string + want Results + }{ + { + name: "happy path", + want: Results{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewResults(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewResults() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_ActivationResults(t *testing.T) { + tests := []struct { + name string + cycleResult *Result + want component.ActivationResults + }{ + { + name: "no activation results", + cycleResult: NewResult(), + want: component.ActivationResults{}, + }, + { + name: "happy path", + cycleResult: NewResult().WithActivationResults(component.NewActivationResult("c1").SetActivated(true).WithActivationCode(component.ActivationCodeOK)), + want: component.ActivationResults{ + "c1": component.NewActivationResult("c1").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResult.ActivationResults(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ActivationResults() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_CycleNumber(t *testing.T) { + tests := []struct { + name string + cycleResult *Result + want uint + }{ + { + name: "default number", + cycleResult: NewResult(), + want: 0, + }, + { + name: "mutated number", + cycleResult: NewResult().SetCycleNumber(777), + want: 777, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResult.CycleNumber(); got != tt.want { + t.Errorf("CycleNumber() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_HasActivatedComponents(t *testing.T) { + tests := []struct { + name string + cycleResult *Result + want bool + }{ + { + name: "no activation results at all", + cycleResult: NewResult(), + want: false, + }, + { + name: "has activation results, but no component activated", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(false).WithActivationCode(component.ActivationCodeNoFunction), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput), + ), + want: false, + }, + { + name: "some components did activate", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput), + ), + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResult.HasActivatedComponents(); got != tt.want { + t.Errorf("HasActivatedComponents() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_HasErrors(t *testing.T) { + tests := []struct { + name string + cycleResult *Result + want bool + }{ + { + name: "no activation results at all", + cycleResult: NewResult(), + want: false, + }, + { + name: "has activation results, but no one is error", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(false).WithActivationCode(component.ActivationCodeNoFunction), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput), + ), + want: false, + }, + { + name: "some components returned errors", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput), + ), + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResult.HasErrors(); got != tt.want { + t.Errorf("HasErrors() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_HasPanics(t *testing.T) { + tests := []struct { + name string + cycleResult *Result + want bool + }{ + { + name: "no activation results at all", + cycleResult: NewResult(), + want: false, + }, + { + name: "has activation results, but no one is panic", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(false).WithActivationCode(component.ActivationCodeNoFunction), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + ), + want: false, + }, + { + name: "some components panicked", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError).WithError(errors.New("some error")), + component.NewActivationResult("c3").SetActivated(false).WithActivationCode(component.ActivationCodeWaitingForInput), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked).WithError(errors.New("some panic")), + ), + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResult.HasPanics(); got != tt.want { + t.Errorf("HasPanics() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_SetCycleNumber(t *testing.T) { + type args struct { + n uint + } + tests := []struct { + name string + cycleResult *Result + args args + want *Result + }{ + { + name: "happy path", + cycleResult: NewResult(), + args: args{ + n: 23, + }, + want: &Result{ + cycleNumber: 23, + activationResults: component.ActivationResults{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cycleResult.SetCycleNumber(tt.args.n); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetCycleNumber() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestResult_WithActivationResults(t *testing.T) { + type args struct { + activationResults []*component.ActivationResult + } + tests := []struct { + name string + cycleResult *Result + args args + want *Result + }{ + { + name: "nothing added", + cycleResult: NewResult(), + args: args{ + activationResults: nil, + }, + want: NewResult(), + }, + { + name: "adding to empty collection", + cycleResult: NewResult(), + args: args{ + activationResults: []*component.ActivationResult{ + component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + }, + }, + want: &Result{ + cycleNumber: 0, + activationResults: component.ActivationResults{ + "c1": component.NewActivationResult("c1").SetActivated(false).WithActivationCode(component.ActivationCodeNoInput), + "c2": component.NewActivationResult("c2").SetActivated(true).WithActivationCode(component.ActivationCodeOK), + }, + }, + }, + { + name: "adding to existing collection", + cycleResult: NewResult().WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + ), + args: args{ + activationResults: []*component.ActivationResult{ + component.NewActivationResult("c3").SetActivated(true).WithActivationCode(component.ActivationCodeReturnedError), + component.NewActivationResult("c4").SetActivated(true).WithActivationCode(component.ActivationCodePanicked), + }, + }, + want: &Result{ + cycleNumber: 0, + activationResults: component.ActivationResults{ + "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) { + if got := tt.cycleResult.WithActivationResults(tt.args.activationResults...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithActivationResults() = %v, want %v", got, tt.want) + } + }) + } +} From 0814d219f27fae3d261bc99dacf8d46154b91ace Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 16 Sep 2024 01:48:04 +0300 Subject: [PATCH 7/8] Component: error messages changed --- component/errors.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component/errors.go b/component/errors.go index 5ace255..4708a4e 100644 --- a/component/errors.go +++ b/component/errors.go @@ -4,8 +4,8 @@ import "errors" var ( //@TODO: provide wrapper methods so exact input can be specified within error - 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") + ErrWaitingForInputResetInputs = errors.New("component is waiting for one or more inputs. All inputs will be reset") + ErrWaitingForInputKeepInputs = errors.New("component is waiting for one or more inputs. All inputs will be kept") ) func IsWaitingForInputError(err error) bool { From 9ea0e7df64c6d9d63edb4c05ebc4e59132425dd7 Mon Sep 17 00:00:00 2001 From: hovsep Date: Mon, 16 Sep 2024 04:11:48 +0300 Subject: [PATCH 8/8] FMesh: add tests --- component/activation_result.go | 7 +- component/component.go | 10 +- component/component_test.go | 8 +- cycle/result.go | 9 +- cycle/result_test.go | 19 +- errors.go | 6 - fmesh.go | 14 +- fmesh_test.go | 374 +++++++++++++++++++++++++++++++-- port/port.go | 6 +- 9 files changed, 399 insertions(+), 54 deletions(-) diff --git a/component/activation_result.go b/component/activation_result.go index b4269ee..10cc6a5 100644 --- a/component/activation_result.go +++ b/component/activation_result.go @@ -1,5 +1,7 @@ package component +import "fmt" + // ActivationResult defines the result (possibly an error) of the activation of given component in given cycle type ActivationResult struct { componentName string @@ -81,7 +83,10 @@ func (c *Component) newActivationCodeWaitingForInput() *ActivationResult { // newActivationCodeReturnedError builds a specific activation result func (c *Component) newActivationCodeReturnedError(err error) *ActivationResult { - return NewActivationResult(c.Name()).SetActivated(true).WithActivationCode(ActivationCodeReturnedError).WithError(err) + return NewActivationResult(c.Name()). + SetActivated(true). + WithActivationCode(ActivationCodeReturnedError). + WithError(fmt.Errorf("component returned an error: %w", err)) } // newActivationCodePanicked builds a specific activation result diff --git a/component/component.go b/component/component.go index 51bdcf6..e5a86e8 100644 --- a/component/component.go +++ b/component/component.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "github.com/hovsep/fmesh/port" - "runtime/debug" ) type ActivationFunc func(inputs port.Ports, outputs port.Ports) error @@ -91,12 +90,9 @@ func (c *Component) hasActivationFunction() bool { func (c *Component) MaybeActivate() (activationResult *ActivationResult) { defer func() { if r := recover(); r != nil { - errorFormat := "panicked with: %v, stacktrace: %s" - if _, ok := r.(error); ok { - errorFormat = "panicked with: %w, stacktrace: %s" - } - //TODO: add custom error - activationResult = c.newActivationCodePanicked(fmt.Errorf(errorFormat, r, debug.Stack())) + //Clear inputs and exit + c.inputs.ClearSignal() + activationResult = c.newActivationCodePanicked(fmt.Errorf("panicked with: %v", r)) } }() diff --git a/component/component_test.go b/component/component_test.go index a9efa80..af7e4c2 100644 --- a/component/component_test.go +++ b/component/component_test.go @@ -489,7 +489,7 @@ func TestComponent_Activate(t *testing.T) { wantActivationResult: NewActivationResult("c1"). SetActivated(true). WithActivationCode(ActivationCodeReturnedError). - WithError(errors.New("failed to activate component: test error")), + WithError(errors.New("component returned an error: test error")), }, { name: "activated without error", @@ -556,10 +556,10 @@ func TestComponent_Activate(t *testing.T) { assert.Equal(t, got.Activated(), tt.wantActivationResult.Activated()) assert.Equal(t, got.ComponentName(), tt.wantActivationResult.ComponentName()) assert.Equal(t, got.Code(), tt.wantActivationResult.Code()) - if !tt.wantActivationResult.HasError() { - assert.False(t, got.HasError()) + if tt.wantActivationResult.HasError() { + assert.EqualError(t, got.Error(), tt.wantActivationResult.Error().Error()) } else { - assert.ErrorContains(t, got.Error(), tt.wantActivationResult.Error().Error()) + assert.False(t, got.HasError()) } }) diff --git a/cycle/result.go b/cycle/result.go index 6d608b7..a9e14ee 100644 --- a/cycle/result.go +++ b/cycle/result.go @@ -79,7 +79,10 @@ func (cycleResult *Result) HasActivatedComponents() bool { return false } -// Add adds a cycle result to existing collection -func (cycleResults Results) Add(cycleResult *Result) Results { - return append(cycleResults, cycleResult) +// Add adds cycle results to existing collection +func (cycleResults Results) Add(newCycleResults ...*Result) Results { + for _, cycleResult := range newCycleResults { + cycleResults = append(cycleResults, cycleResult) + } + return cycleResults } diff --git a/cycle/result_test.go b/cycle/result_test.go index 273818b..03c2e10 100644 --- a/cycle/result_test.go +++ b/cycle/result_test.go @@ -9,7 +9,7 @@ import ( func TestResults_Add(t *testing.T) { type args struct { - cycleResult *Result + cycleResults []*Result } tests := []struct { name string @@ -21,11 +21,24 @@ func TestResults_Add(t *testing.T) { name: "happy path", cycleResults: NewResults(), args: args{ - cycleResult: NewResult().SetCycleNumber(1).WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), + cycleResults: []*Result{ + NewResult(). + SetCycleNumber(1). + WithActivationResults(component.NewActivationResult("c1").SetActivated(false)), + NewResult(). + SetCycleNumber(2). + WithActivationResults(component.NewActivationResult("c1").SetActivated(true)), + }, }, want: Results{ { cycleNumber: 1, + activationResults: component.ActivationResults{ + "c1": component.NewActivationResult("c1").SetActivated(false), + }, + }, + { + cycleNumber: 2, activationResults: component.ActivationResults{ "c1": component.NewActivationResult("c1").SetActivated(true), }, @@ -35,7 +48,7 @@ func TestResults_Add(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.cycleResults.Add(tt.args.cycleResult); !reflect.DeepEqual(got, tt.want) { + if got := tt.cycleResults.Add(tt.args.cycleResults...); !reflect.DeepEqual(got, tt.want) { t.Errorf("Add() = %v, want %v", got, tt.want) } }) diff --git a/errors.go b/errors.go index 9eb155f..d134b35 100644 --- a/errors.go +++ b/errors.go @@ -2,8 +2,6 @@ package fmesh import ( "errors" - "fmt" - "github.com/hovsep/fmesh/cycle" ) type ErrorHandlingStrategy int @@ -19,7 +17,3 @@ var ( ErrHitAPanic = errors.New("f-mesh hit a panic and will be stopped") ErrUnsupportedErrorHandlingStrategy = errors.New("unsupported error handling strategy") ) - -func newFMeshStopError(err error, cycleResult *cycle.Result) error { - return fmt.Errorf("%w (cycle #%d activation results: %v)", err, cycleResult.CycleNumber(), cycleResult.ActivationResults()) -} diff --git a/fmesh.go b/fmesh.go index 39272f7..d3c05b6 100644 --- a/fmesh.go +++ b/fmesh.go @@ -72,7 +72,7 @@ func (fm *FMesh) runCycle() *cycle.Result { case aRes := <-activationResultsChan: //@TODO :check for closed channel cycleResult.Lock() - cycleResult = cycleResult.WithActivationResult(aRes) + cycleResult = cycleResult.WithActivationResults(aRes) cycleResult.Unlock() case <-doneChan: return @@ -104,15 +104,17 @@ func (fm *FMesh) drainComponents() { // Run starts the computation until there is no component which activates (mesh has no unprocessed inputs) func (fm *FMesh) Run() (cycle.Results, error) { allCycles := cycle.NewResults() + cycleNumber := uint(0) for { - cycleResult := fm.runCycle() + cycleNumber++ + cycleResult := fm.runCycle().SetCycleNumber(cycleNumber) + allCycles = allCycles.Add(cycleResult) mustStop, err := fm.mustStop(cycleResult) if mustStop { return allCycles, err } - allCycles.Add(cycleResult) fm.drainComponents() } } @@ -126,14 +128,12 @@ func (fm *FMesh) mustStop(cycleResult *cycle.Result) (bool, error) { //Check if mesh must stop because of configured error handling strategy switch fm.errorHandlingStrategy { case StopOnFirstError: - return cycleResult.HasErrors(), newFMeshStopError(ErrHitAnError, cycleResult) + return cycleResult.HasErrors(), ErrHitAnError case StopOnFirstPanic: - return cycleResult.HasPanics(), newFMeshStopError(ErrHitAPanic, cycleResult) + return cycleResult.HasPanics(), ErrHitAPanic case IgnoreAll: return false, nil default: - //@TODO: maybe better to return error - return true, ErrUnsupportedErrorHandlingStrategy } } diff --git a/fmesh_test.go b/fmesh_test.go index 3564753..cc979e8 100644 --- a/fmesh_test.go +++ b/fmesh_test.go @@ -1,6 +1,7 @@ package fmesh import ( + "errors" "github.com/hovsep/fmesh/component" "github.com/hovsep/fmesh/cycle" "github.com/hovsep/fmesh/port" @@ -276,35 +277,368 @@ func TestFMesh_Description(t *testing.T) { } func TestFMesh_Run(t *testing.T) { - type fields struct { - name string - description string - components component.Components - errorHandlingStrategy ErrorHandlingStrategy - } tests := []struct { name string - fields fields - want []*cycle.Result + fm *FMesh + initFM func(fm *FMesh) + want cycle.Results wantErr bool }{ - // TODO: Add test cases. + { + name: "empty mesh stops after first cycle", + fm: New("fm"), + want: cycle.NewResults().Add(cycle.NewResult().SetCycleNumber(1)), + wantErr: false, + }, + { + name: "unsupported error handling strategy", + fm: New("fm").WithErrorHandlingStrategy(100). + WithComponents( + component.NewComponent("c1"). + WithDescription("This component simply puts a constant on o1"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(signal.New(77)) + return nil + }), + ), + initFM: func(fm *FMesh) { + //Fire the mesh + fm.Components().ByName("c1").Inputs().ByName("i1").PutSignal(signal.New("start c1")) + }, + want: cycle.NewResults().Add(cycle.NewResult(). + SetCycleNumber(1). + WithActivationResults(component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK)), + ), + wantErr: true, + }, + { + name: "stop on first error on first cycle", + fm: New("fm"). + WithErrorHandlingStrategy(StopOnFirstError). + WithComponents( + component.NewComponent("c1"). + WithDescription("This component just returns an unexpected error"). + WithInputs("i1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + return errors.New("boom") + })), + initFM: func(fm *FMesh) { + fm.Components().ByName("c1").Inputs().ByName("i1").PutSignal(signal.New("start")) + }, + want: cycle.NewResults().Add( + cycle.NewResult(). + SetCycleNumber(1). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeReturnedError). + WithError(errors.New("component returned an error: boom")), + ), + ), + wantErr: true, + }, + { + name: "stop on first panic on cycle 3", + fm: New("fm"). + WithErrorHandlingStrategy(StopOnFirstPanic). + WithComponents( + component.NewComponent("c1"). + WithDescription("This component just sends a number to c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(signal.New(10)) + return nil + }), + component.NewComponent("c2"). + WithDescription("This component receives a number from c1 and passes it to c4"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) + return nil + }), + component.NewComponent("c3"). + WithDescription("This component returns an error, but the mesh is configured to ignore errors"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + return errors.New("boom") + }), + component.NewComponent("c4"). + WithDescription("This component receives a number from c2 and panics"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + panic("no way") + return nil + }), + ), + 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")) + + //Input data + c1.Inputs().ByName("i1").PutSignal(signal.New("start c1")) + c3.Inputs().ByName("i1").PutSignal(signal.New("start c3")) + }, + want: cycle.NewResults().Add( + cycle.NewResult(). + SetCycleNumber(1). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c3"). + SetActivated(true). + WithActivationCode(component.ActivationCodeReturnedError). + WithError(errors.New("component returned an error: boom")), + component.NewActivationResult("c4"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + cycle.NewResult(). + SetCycleNumber(2). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c3"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c4"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + cycle.NewResult(). + SetCycleNumber(3). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c3"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c4"). + SetActivated(true). + WithActivationCode(component.ActivationCodePanicked). + WithError(errors.New("panicked with: no way")), + ), + ), + wantErr: true, + }, + { + name: "all errors and panics are ignored", + fm: New("fm"). + WithErrorHandlingStrategy(IgnoreAll). + WithComponents( + component.NewComponent("c1"). + WithDescription("This component just sends a number to c2"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(signal.New(10)) + return nil + }), + component.NewComponent("c2"). + WithDescription("This component receives a number from c1 and passes it to c4"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) + return nil + }), + component.NewComponent("c3"). + WithDescription("This component returns an error, but the mesh is configured to ignore errors"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + return errors.New("boom") + }), + component.NewComponent("c4"). + WithDescription("This component receives a number from c2 and panics, but the mesh is configured to ignore even panics"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) + + // Even component panicked, it managed to set some data on output "o1" + // so that data will be available in next cycle + panic("no way") + return nil + }), + component.NewComponent("c5"). + WithDescription("This component receives a number from c4"). + WithInputs("i1"). + WithOutputs("o1"). + WithActivationFunc(func(inputs port.Ports, outputs port.Ports) error { + outputs.ByName("o1").PutSignal(inputs.ByName("i1").Signal()) + return nil + }), + ), + 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")) + + //Input data + c1.Inputs().ByName("i1").PutSignal(signal.New("start c1")) + c3.Inputs().ByName("i1").PutSignal(signal.New("start c3")) + }, + want: cycle.NewResults().Add( + //c1 and c3 activated, c3 finishes with error + cycle.NewResult(). + SetCycleNumber(1). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c3"). + SetActivated(true). + WithActivationCode(component.ActivationCodeReturnedError). + WithError(errors.New("component returned an error: boom")), + component.NewActivationResult("c4"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c5"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + // Only c2 is activated + cycle.NewResult(). + SetCycleNumber(2). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + component.NewActivationResult("c3"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c4"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c5"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + //Only c4 is activated and panicked + cycle.NewResult(). + SetCycleNumber(3). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c3"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c4"). + SetActivated(true). + WithActivationCode(component.ActivationCodePanicked). + WithError(errors.New("panicked with: no way")), + component.NewActivationResult("c5"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + //Only c5 is activated (after c4 panicked in previous cycle) + cycle.NewResult(). + SetCycleNumber(4). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c3"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c4"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c5"). + SetActivated(true). + WithActivationCode(component.ActivationCodeOK), + ), + //Last (control) cycle, no component activated, so f-mesh stops naturally + cycle.NewResult(). + SetCycleNumber(5). + WithActivationResults( + component.NewActivationResult("c1"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c2"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c3"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c4"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + component.NewActivationResult("c5"). + SetActivated(false). + WithActivationCode(component.ActivationCodeNoInput), + ), + ), + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - fm := &FMesh{ - name: tt.fields.name, - description: tt.fields.description, - components: tt.fields.components, - errorHandlingStrategy: tt.fields.errorHandlingStrategy, + if tt.initFM != nil { + tt.initFM(tt.fm) } - got, err := fm.Run() - if (err != nil) != tt.wantErr { - t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr) - return + got, err := tt.fm.Run() + assert.Equal(t, len(tt.want), len(got)) + if tt.wantErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Run() got = %v, want %v", got, tt.want) + + //Compare cycle results one by one + for i := 0; i < len(got); i++ { + assert.Equal(t, tt.want[i].CycleNumber(), got[i].CycleNumber()) + assert.Equal(t, len(tt.want[i].ActivationResults()), len(got[i].ActivationResults()), "ActivationResults len mismatch") + + //Compare activation results + for componentName, gotActivationResult := range got[i].ActivationResults() { + assert.Equal(t, tt.want[i].ActivationResults()[componentName].Activated(), gotActivationResult.Activated()) + assert.Equal(t, tt.want[i].ActivationResults()[componentName].ComponentName(), gotActivationResult.ComponentName()) + assert.Equal(t, tt.want[i].ActivationResults()[componentName].Code(), gotActivationResult.Code()) + + if tt.want[i].ActivationResults()[componentName].HasError() { + assert.EqualError(t, tt.want[i].ActivationResults()[componentName].Error(), gotActivationResult.Error().Error()) + } else { + assert.False(t, gotActivationResult.HasError()) + } + } } }) } diff --git a/port/port.go b/port/port.go index 061760e..e2799c8 100644 --- a/port/port.go +++ b/port/port.go @@ -60,15 +60,15 @@ func (p *Port) HasSignal() bool { // Adds pipe reference to the port, so all pipes of the port are easily accessible func (p *Port) addPipeRef(pipe *Pipe) { - if pipe.From == nil || pipe.To == nil { - return - } p.pipes = append(p.pipes, pipe) } // PipeTo creates one or multiple pipes to other port(s) func (p *Port) PipeTo(toPorts ...*Port) { for _, toPort := range toPorts { + if toPort == nil { + continue + } newPipe := NewPipe(p, toPort) p.addPipeRef(newPipe) toPort.addPipeRef(newPipe)