From 32312459b513cd916c1d913b2d2bfac3ec4c0eb7 Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Mon, 30 Oct 2023 20:13:02 +0100 Subject: [PATCH] gpio: fix data race in EasyDriver --- drivers/gpio/easy_driver.go | 292 ++++++++----- drivers/gpio/easy_driver_test.go | 719 +++++++++++++++++++++++++------ drivers/gpio/gpio_driver_test.go | 7 +- 3 files changed, 761 insertions(+), 257 deletions(-) diff --git a/drivers/gpio/easy_driver.go b/drivers/gpio/easy_driver.go index fdde14f2a..26687d71b 100644 --- a/drivers/gpio/easy_driver.go +++ b/drivers/gpio/easy_driver.go @@ -1,8 +1,10 @@ package gpio import ( - "errors" + "fmt" + "log" "strconv" + "sync" "time" "github.com/hashicorp/go-multierror" @@ -11,25 +13,24 @@ import ( // EasyDriver object type EasyDriver struct { - gobot.Commander - - name string - connection DigitalWriter - stepPin string - dirPin string - enPin string - sleepPin string - - angle float32 - rpm uint - dir int8 - moving bool - stepNum int - enabled bool - sleeping bool + *Driver + + stepPin string + dirPin string + enPin string + sleepPin string + + angle float32 + rpm uint + dir int8 + stepNum int + enabled bool + sleeping bool + runStopChan chan struct{} + runStopWaitGroup *sync.WaitGroup } -// NewEasyDriver returns a new EasyDriver from SparkFun (https://www.sparkfun.com/products/12779) +// NewEasyDriver returns a new driver for EasyDriver from SparkFun (https://www.sparkfun.com/products/12779) // TODO: Support selecting phase input instead of hard-wiring MS1 and MS2 to board truth table // This should also work for the BigEasyDriver (untested) // A - DigitalWriter @@ -38,22 +39,36 @@ type EasyDriver struct { // enPin - Pin corresponding to enabled input on EasyDriver. Optional // sleepPin - Pin corresponding to sleep input on EasyDriver. Optional // angle - Step angle of motor -func NewEasyDriver(a DigitalWriter, angle float32, stepPin string, dirPin string, enPin string, sleepPin string) *EasyDriver { +func NewEasyDriver( + a DigitalWriter, + angle float32, + stepPin string, + dirPin string, + enPin string, + sleepPin string, +) *EasyDriver { + if angle <= 0 { + panic("angle needs to be greater than zero") + } d := &EasyDriver{ - Commander: gobot.NewCommander(), - name: gobot.DefaultName("EasyDriver"), - connection: a, - stepPin: stepPin, - dirPin: dirPin, - enPin: enPin, - sleepPin: sleepPin, - + Driver: NewDriver(a.(gobot.Connection), "EasyDriver"), + stepPin: stepPin, + dirPin: dirPin, + enPin: enPin, + sleepPin: sleepPin, angle: angle, rpm: 1, dir: 1, enabled: true, sleeping: false, } + d.beforeHalt = func() error { + if err := d.Stop(); err != nil { + fmt.Printf("no need to stop motion: %v\n", err) + } + + return nil + } // panic if step pin isn't set if stepPin == "" { @@ -61,7 +76,7 @@ func NewEasyDriver(a DigitalWriter, angle float32, stepPin string, dirPin string } // 1/4 of max speed. Not too fast, not too slow - d.rpm = d.GetMaxSpeed() / 4 + d.rpm = d.MaxSpeed() / 4 d.AddCommand("Move", func(params map[string]interface{}) interface{} { degs, _ := strconv.Atoi(params["degs"].(string)) @@ -80,121 +95,142 @@ func NewEasyDriver(a DigitalWriter, angle float32, stepPin string, dirPin string return d } -// Name of EasyDriver -func (d *EasyDriver) Name() string { return d.name } +// Move the motor given number of degrees at current speed. The move can be stopped asynchronously. +func (d *EasyDriver) Move(degs int) error { + // ensure that move and run can not interfere + d.mutex.Lock() + defer d.mutex.Unlock() -// SetName sets name for EasyDriver -func (d *EasyDriver) SetName(n string) { d.name = n } + if !d.enabled { + return fmt.Errorf("motor '%s' is disabled and can not be running", d.name) + } -// Connection returns EasyDriver's connection -func (d *EasyDriver) Connection() gobot.Connection { return d.connection.(gobot.Connection) } + if d.runStopChan != nil { + return fmt.Errorf("motor '%s' already running or moving", d.name) + } -// Start implements the Driver interface -func (d *EasyDriver) Start() error { return nil } + d.runStopChan = make(chan struct{}) + d.runStopWaitGroup = &sync.WaitGroup{} + d.runStopWaitGroup.Add(1) -// Halt implements the Driver interface; stops running the stepper -func (d *EasyDriver) Halt() error { - return d.Stop() -} + defer func() { + close(d.runStopChan) + d.runStopChan = nil + d.runStopWaitGroup.Done() + }() -// Move the motor given number of degrees at current speed. -func (d *EasyDriver) Move(degs int) error { - if d.moving { - // don't do anything if already moving - return nil + steps := int(float32(degs) / d.angle) + if steps <= 0 { + fmt.Printf("steps are smaller than zero, no move for '%s'\n", d.name) } - d.moving = true - - steps := int(float32(degs) / d.angle) for i := 0; i < steps; i++ { - if !d.moving { + select { + case <-d.runStopChan: // don't continue to step if driver is stopped - break - } - - if err := d.Step(); err != nil { - return err + log.Println("stop happen") + return nil + default: + if err := d.step(); err != nil { + return err + } } } - d.moving = false - return nil } -// Step the stepper 1 step -func (d *EasyDriver) Step() error { - stepsPerRev := d.GetMaxSpeed() +// Run the stepper continuously. +func (d *EasyDriver) Run() error { + // ensure that run, can not interfere with step or move + d.mutex.Lock() + defer d.mutex.Unlock() - // a valid steps occurs for a low to high transition - if err := d.connection.DigitalWrite(d.stepPin, 0); err != nil { - return err - } - // 1 minute / steps per revolution / revolutions per minute - // let's keep it as Microseconds so we only have to do integer math - time.Sleep(time.Duration(60*1000*1000/stepsPerRev/d.rpm) * time.Microsecond) - if err := d.connection.DigitalWrite(d.stepPin, 1); err != nil { - return err + if !d.enabled { + return fmt.Errorf("motor '%s' is disabled and can not be moving", d.name) } - // increment or decrement the number of steps by 1 - d.stepNum += int(d.dir) - - return nil -} - -// Run the stepper continuously -func (d *EasyDriver) Run() error { - if d.moving { - // don't do anything if already moving - return nil + if d.runStopChan != nil { + return fmt.Errorf("motor '%s' already running or moving", d.name) } - d.moving = true - - go func() { - for d.moving { - if err := d.Step(); err != nil { - panic(err) + d.runStopChan = make(chan struct{}) + d.runStopWaitGroup = &sync.WaitGroup{} + d.runStopWaitGroup.Add(1) + + go func(name string) { + defer d.runStopWaitGroup.Done() + for { + select { + case <-d.runStopChan: + d.runStopChan = nil + return + default: + if err := d.step(); err != nil { + fmt.Printf("motor step skipped for '%s': %v\n", name, err) + } } } - }() + }(d.name) return nil } +// IsMoving returns a bool stating whether motor is currently in motion +func (d *EasyDriver) IsMoving() bool { + return d.runStopChan != nil +} + // Stop running the stepper func (d *EasyDriver) Stop() error { - d.moving = false + if !d.IsMoving() { + return fmt.Errorf("motor '%s' is not yet started", d.name) + } + + d.runStopChan <- struct{}{} + d.runStopWaitGroup.Wait() + return nil } +// Step the stepper 1 step +func (d *EasyDriver) Step() error { + // ensure that move and step can not interfere + d.mutex.Lock() + defer d.mutex.Unlock() + + if d.IsMoving() { + return fmt.Errorf("motor '%s' already running or moving", d.name) + } + + return d.step() +} + // SetDirection sets the direction to be moving. Valid directions are "cw" or "ccw" func (d *EasyDriver) SetDirection(dir string) error { // can't change direct if dirPin isn't set if d.dirPin == "" { - return errors.New("dirPin is not set") + return fmt.Errorf("dirPin is not set for '%s'", d.name) } if dir == "ccw" { d.dir = -1 // high is ccw - return d.connection.DigitalWrite(d.dirPin, 1) + return d.connection.(DigitalWriter).DigitalWrite(d.dirPin, 1) } // default to cw, even if user specified wrong value d.dir = 1 // low is cw - return d.connection.DigitalWrite(d.dirPin, 0) + return d.connection.(DigitalWriter).DigitalWrite(d.dirPin, 0) } -// SetSpeed sets the speed of the motor in RPMs. 1 is the lowest and GetMaxSpeed is the highest +// SetSpeed sets the speed of the motor in RPMs. 1 is the lowest and GetMaxSpeed is the highest func (d *EasyDriver) SetSpeed(rpm uint) error { if rpm < 1 { d.rpm = 1 - } else if rpm > d.GetMaxSpeed() { - d.rpm = d.GetMaxSpeed() + } else if rpm > d.MaxSpeed() { + d.rpm = d.MaxSpeed() } else { d.rpm = rpm } @@ -202,30 +238,26 @@ func (d *EasyDriver) SetSpeed(rpm uint) error { return nil } -// GetMaxSpeed returns the max speed of the stepper -func (d *EasyDriver) GetMaxSpeed() uint { +// MaxSpeed returns the max speed of the stepper +func (d *EasyDriver) MaxSpeed() uint { return uint(360 / d.angle) } -// GetCurrentStep returns current step number -func (d *EasyDriver) GetCurrentStep() int { +// CurrentStep returns current step number +func (d *EasyDriver) CurrentStep() int { return d.stepNum } -// IsMoving returns a bool stating whether motor is currently in motion -func (d *EasyDriver) IsMoving() bool { - return d.moving -} - // Enable enables all motor output func (d *EasyDriver) Enable() error { // can't enable if enPin isn't set. This is fine normally since it will be enabled by default if d.enPin == "" { - return errors.New("enPin is not set. Board is enabled by default") + d.enabled = true + return fmt.Errorf("enPin is not set - board '%s' is enabled by default", d.name) } // enPin is active low - if err := d.connection.DigitalWrite(d.enPin, 0); err != nil { + if err := d.connection.(DigitalWriter).DigitalWrite(d.enPin, 0); err != nil { return err } @@ -237,14 +269,14 @@ func (d *EasyDriver) Enable() error { func (d *EasyDriver) Disable() error { // can't disable if enPin isn't set if d.enPin == "" { - return errors.New("enPin is not set") + return fmt.Errorf("enPin is not set for '%s'", d.name) } - // let's stop the motor first, but do not return on error - err := d.Stop() + // stop the motor if running + err := d.tryStop() // enPin is active low - if e := d.connection.DigitalWrite(d.enPin, 1); e != nil { + if e := d.connection.(DigitalWriter).DigitalWrite(d.enPin, 1); e != nil { err = multierror.Append(err, e) } else { d.enabled = false @@ -262,14 +294,14 @@ func (d *EasyDriver) IsEnabled() bool { func (d *EasyDriver) Sleep() error { // can't sleep if sleepPin isn't set if d.sleepPin == "" { - return errors.New("sleepPin is not set") + return fmt.Errorf("sleepPin is not set for '%s'", d.name) } - // let's stop the motor first - err := d.Stop() + // stop the motor if running + err := d.tryStop() // sleepPin is active low - if e := d.connection.DigitalWrite(d.sleepPin, 0); e != nil { + if e := d.connection.(DigitalWriter).DigitalWrite(d.sleepPin, 0); e != nil { err = multierror.Append(err, e) } else { d.sleeping = true @@ -282,24 +314,52 @@ func (d *EasyDriver) Sleep() error { func (d *EasyDriver) Wake() error { // can't wake if sleepPin isn't set if d.sleepPin == "" { - return errors.New("sleepPin is not set") + return fmt.Errorf("sleepPin is not set for '%s'", d.name) } // sleepPin is active low - if err := d.connection.DigitalWrite(d.sleepPin, 1); err != nil { + if err := d.connection.(DigitalWriter).DigitalWrite(d.sleepPin, 1); err != nil { return err } d.sleeping = false // we need to wait 1ms after sleeping before doing a step to charge the step pump (according to data sheet) - // this will ensure that happens time.Sleep(1 * time.Millisecond) return nil } -// IsSleeping returns a bool stating whether motor is enabled +// IsSleeping returns a bool stating whether motor is sleeping func (d *EasyDriver) IsSleeping() bool { return d.sleeping } + +func (d *EasyDriver) step() error { + stepsPerRev := d.MaxSpeed() + + // a valid steps occurs for a low to high transition + if err := d.connection.(DigitalWriter).DigitalWrite(d.stepPin, 0); err != nil { + return err + } + // 1 minute / steps per revolution / revolutions per minute + // let's keep it as Microseconds so we only have to do integer math + time.Sleep(time.Duration(60*1000*1000/stepsPerRev/d.rpm) * time.Microsecond) + if err := d.connection.(DigitalWriter).DigitalWrite(d.stepPin, 1); err != nil { + return err + } + + // increment or decrement the number of steps by 1 + d.stepNum += int(d.dir) + + return nil +} + +// tryStop stop the stepper if moving or running +func (d *EasyDriver) tryStop() error { + if !d.IsMoving() { + return nil + } + + return d.Stop() +} diff --git a/drivers/gpio/easy_driver_test.go b/drivers/gpio/easy_driver_test.go index 669aeeb7b..087848d8d 100644 --- a/drivers/gpio/easy_driver_test.go +++ b/drivers/gpio/easy_driver_test.go @@ -1,11 +1,14 @@ package gpio import ( + "fmt" "strings" + "sync" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -13,174 +16,618 @@ const ( stepsPerRev = 720 ) -var adapter *gpioTestAdaptor - -func initEasyDriver() *EasyDriver { - adapter = newGpioTestAdaptor() - return NewEasyDriver(adapter, stepAngle, "1", "2", "3", "4") -} - -func TestEasyDriver_Connection(t *testing.T) { - d := initEasyDriver() - assert.Equal(t, adapter, d.Connection()) -} - -func TestEasyDriverDefaultName(t *testing.T) { - d := initEasyDriver() - assert.True(t, strings.HasPrefix(d.Name(), "EasyDriver")) +func initTestEasyDriverWithStubbedAdaptor() (*EasyDriver, *gpioTestAdaptor) { + a := newGpioTestAdaptor() + d := NewEasyDriver(a, stepAngle, "1", "2", "3", "4") + return d, a } -func TestEasyDriverSetName(t *testing.T) { - d := initEasyDriver() - d.SetName("OtherDriver") - assert.True(t, strings.HasPrefix(d.Name(), "OtherDriver")) -} - -func TestEasyDriverStart(t *testing.T) { - d := initEasyDriver() - _ = d.Start() - // noop - no error occurred +func TestNewEasyDriver(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + // act + d := NewEasyDriver(a, stepAngle, "1", "2", "3", "4") + // assert + assert.IsType(t, &EasyDriver{}, d) + assert.True(t, strings.HasPrefix(d.name, "EasyDriver")) + assert.Equal(t, a, d.connection) + assert.NoError(t, d.afterStart()) + assert.NoError(t, d.beforeHalt()) + assert.NotNil(t, d.Commander) + assert.NotNil(t, d.mutex) + assert.Equal(t, "1", d.stepPin) + assert.Equal(t, "2", d.dirPin) + assert.Equal(t, "3", d.enPin) + assert.Equal(t, "4", d.sleepPin) + assert.Equal(t, float32(stepAngle), d.angle) + assert.Equal(t, uint(180), d.rpm) + assert.Equal(t, int8(1), d.dir) + assert.Equal(t, 0, d.stepNum) + assert.Equal(t, true, d.enabled) + assert.Equal(t, false, d.sleeping) + assert.Nil(t, d.runStopChan) } func TestEasyDriverHalt(t *testing.T) { - d := initEasyDriver() - _ = d.Run() - assert.True(t, d.IsMoving()) - _ = d.Halt() + // arrange + d, _ := initTestEasyDriverWithStubbedAdaptor() + require.NoError(t, d.Run()) + require.True(t, d.IsMoving()) + // act + err := d.Halt() + // assert + assert.NoError(t, err) assert.False(t, d.IsMoving()) } func TestEasyDriverMove(t *testing.T) { - d := initEasyDriver() - _ = d.Move(2) - time.Sleep(2 * time.Millisecond) - assert.Equal(t, 4, d.GetCurrentStep()) - assert.False(t, d.IsMoving()) + tests := map[string]struct { + inputSteps int + simulateDisabled bool + simulateAlreadyRunning bool + simulateWriteErr bool + wantWrites int + wantSteps int + wantMoving bool + wantErr string + }{ + "move_one": { + inputSteps: 1, + wantWrites: 4, + wantSteps: 2, + wantMoving: false, + }, + "move_more": { + inputSteps: 20, + wantWrites: 80, + wantSteps: 40, + wantMoving: false, + }, + "error_disabled": { + simulateDisabled: true, + wantMoving: false, + wantErr: "is disabled", + }, + "error_already_running": { + simulateAlreadyRunning: true, + wantMoving: true, + wantErr: "already running or moving", + }, + "error_write": { + inputSteps: 1, + simulateWriteErr: true, + wantWrites: 1, + wantMoving: false, + wantErr: "write error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, a := initTestEasyDriverWithStubbedAdaptor() + d.enabled = !tc.simulateDisabled + if tc.simulateAlreadyRunning { + d.runStopChan = make(chan struct{}) + defer func() { close(d.runStopChan); d.runStopChan = nil }() + } + var numCallsWrite int + a.digitalWriteFunc = func(string, byte) error { + numCallsWrite++ + if tc.simulateWriteErr { + return fmt.Errorf("write error") + } + return nil + } + // act + err := d.Move(tc.inputSteps) + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.wantSteps, d.stepNum) + assert.Equal(t, tc.wantWrites, numCallsWrite) + assert.Equal(t, tc.wantMoving, d.IsMoving()) + }) + } } -func TestEasyDriverRun(t *testing.T) { - d := initEasyDriver() - _ = d.Run() - assert.True(t, d.IsMoving()) - _ = d.Run() - assert.True(t, d.IsMoving()) +func TestEasyDriverRun_IsMoving(t *testing.T) { + tests := map[string]struct { + simulateDisabled bool + simulateAlreadyRunning bool + simulateWriteErr bool + wantMoving bool + wantErr string + }{ + "run": { + wantMoving: true, + }, + "error_disabled": { + simulateDisabled: true, + wantMoving: false, + wantErr: "is disabled", + }, + "write_error_skipped": { + simulateWriteErr: true, + wantMoving: true, + }, + "error_already_running": { + simulateAlreadyRunning: true, + wantMoving: true, + wantErr: "already running or moving", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, a := initTestEasyDriverWithStubbedAdaptor() + d.enabled = !tc.simulateDisabled + if tc.simulateAlreadyRunning { + d.runStopChan = make(chan struct{}) + defer func() { close(d.runStopChan); d.runStopChan = nil }() + } + simWriteErr := tc.simulateWriteErr // to prevent data race in write function (go-called) + a.digitalWriteFunc = func(string, byte) error { + if simWriteErr { + simWriteErr = false // to prevent to much output + return fmt.Errorf("write error") + } + return nil + } + // act + err := d.Run() + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.wantMoving, d.IsMoving()) + }) + } } -func TestEasyDriverStop(t *testing.T) { - d := initEasyDriver() - _ = d.Run() - assert.True(t, d.IsMoving()) - _ = d.Stop() +func TestEasyDriverStop_IsMoving(t *testing.T) { + // arrange + d, _ := initTestEasyDriverWithStubbedAdaptor() + require.NoError(t, d.Run()) + require.True(t, d.IsMoving()) + // act + err := d.Stop() + // assert + assert.NoError(t, err) assert.False(t, d.IsMoving()) } func TestEasyDriverStep(t *testing.T) { - d := initEasyDriver() - _ = d.Step() - assert.Equal(t, 1, d.GetCurrentStep()) - _ = d.Step() - _ = d.Step() - _ = d.Step() - assert.Equal(t, 4, d.GetCurrentStep()) - _ = d.SetDirection("ccw") - _ = d.Step() - assert.Equal(t, 3, d.GetCurrentStep()) + tests := map[string]struct { + countCallsForth int + countCallsBack int + simulateAlreadyRunning bool + simulateWriteErr bool + wantSteps int + wantWritten []byte + wantErr string + }{ + "single": { + countCallsForth: 1, + wantSteps: 1, + wantWritten: []byte{0x00, 0x01}, + }, + "many": { + countCallsForth: 4, + wantSteps: 4, + wantWritten: []byte{0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1}, + }, + "forth_and_back": { + countCallsForth: 5, + countCallsBack: 3, + wantSteps: 2, + wantWritten: []byte{0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x1}, + }, + "reverse": { + countCallsBack: 3, + wantSteps: -3, + wantWritten: []byte{0x0, 0x1, 0x0, 0x1, 0x0, 0x1}, + }, + "error_already_running": { + countCallsForth: 1, + simulateAlreadyRunning: true, + wantErr: "already running or moving", + }, + "error_write": { + simulateWriteErr: true, + wantWritten: []byte{0x00, 0x00}, + countCallsBack: 2, + wantErr: "write error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, a := initTestEasyDriverWithStubbedAdaptor() + if tc.simulateAlreadyRunning { + d.runStopChan = make(chan struct{}) + defer func() { close(d.runStopChan); d.runStopChan = nil }() + } + var writtenValues []byte + a.digitalWriteFunc = func(pin string, val byte) error { + assert.Equal(t, d.stepPin, pin) + writtenValues = append(writtenValues, val) + if tc.simulateWriteErr { + return fmt.Errorf("write error") + } + return nil + } + var errs []string + // act + for i := 0; i < tc.countCallsForth; i++ { + if err := d.Step(); err != nil { + errs = append(errs, err.Error()) + } + } + d.dir = -1 + for i := 0; i < tc.countCallsBack; i++ { + if err := d.Step(); err != nil { + errs = append(errs, err.Error()) + } + } + // assert + if tc.wantErr != "" { + assert.Contains(t, strings.Join(errs, ","), tc.wantErr) + } else { + assert.Nil(t, errs) + } + assert.Equal(t, tc.wantSteps, d.stepNum) + assert.Equal(t, tc.wantSteps, d.CurrentStep()) + assert.Equal(t, tc.wantWritten, writtenValues) + }) + } } func TestEasyDriverSetDirection(t *testing.T) { - d := initEasyDriver() - assert.Equal(t, int8(1), d.dir) - _ = d.SetDirection("cw") - assert.Equal(t, int8(1), d.dir) - _ = d.SetDirection("ccw") - assert.Equal(t, int8(-1), d.dir) - _ = d.SetDirection("nothing") - assert.Equal(t, int8(1), d.dir) -} - -func TestEasyDriverSetDirectionNoPin(t *testing.T) { - d := initEasyDriver() - d.dirPin = "" - err := d.SetDirection("cw") - assert.NotNil(t, err) + tests := map[string]struct { + dirPin string + input string + wantVal int8 + wantErr string + }{ + "cw": { + input: "cw", + dirPin: "10", + wantVal: 1, + }, + "ccw": { + input: "ccw", + dirPin: "11", + wantVal: -1, + }, + "unknown": { + input: "unknown", + dirPin: "12", + wantVal: 1, + }, + "error_no_pin": { + dirPin: "", + wantVal: 1, + wantErr: "dirPin is not set", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + d := NewEasyDriver(a, stepAngle, "1", tc.dirPin, "3", "4") + require.Equal(t, int8(1), d.dir) + // act + err := d.SetDirection(tc.input) + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.wantVal, d.dir) + }) + } } func TestEasyDriverSetSpeed(t *testing.T) { - d := initEasyDriver() - assert.Equal(t, uint(stepsPerRev/4), d.rpm) // default speed of 720/4 - _ = d.SetSpeed(0) - assert.Equal(t, uint(1), d.rpm) - _ = d.SetSpeed(200) - assert.Equal(t, uint(200), d.rpm) - _ = d.SetSpeed(1000) - assert.Equal(t, uint(stepsPerRev), d.rpm) -} + const ( + angle = 10 + max = 36 // 360/angle + ) -func TestEasyDriverGetMaxSpeed(t *testing.T) { - d := initEasyDriver() - assert.Equal(t, uint(stepsPerRev), d.GetMaxSpeed()) + tests := map[string]struct { + input uint + want uint + }{ + "below_minimum": { + input: 0, + want: 1, + }, + "minimum": { + input: 1, + want: 1, + }, + "maximum": { + input: max, + want: max, + }, + "above_maximum": { + input: max + 1, + want: max, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d := EasyDriver{angle: angle} + // act + err := d.SetSpeed(tc.input) + // assert + assert.NoError(t, err) + assert.Equal(t, tc.want, d.rpm) + }) + } } -func TestEasyDriverSleep(t *testing.T) { - // let's test basic functionality - d := initEasyDriver() - _ = d.Sleep() - assert.True(t, d.IsSleeping()) - - // let's make sure it stops first - d = initEasyDriver() - _ = d.Run() - _ = d.Sleep() - assert.True(t, d.IsSleeping()) - assert.False(t, d.IsMoving()) -} - -func TestEasyDriverSleepNoPin(t *testing.T) { - d := initEasyDriver() - d.sleepPin = "" - err := d.Sleep() - assert.NotNil(t, err) - err = d.Wake() - assert.NotNil(t, err) +func TestEasyDriverMaxSpeed(t *testing.T) { + tests := map[string]struct { + angle float32 + want uint + }{ + "180": { + angle: 2.0, + want: 180, + }, + "360": { + angle: 1.0, + want: 360, + }, + "720": { + angle: 0.5, + want: 720, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d := EasyDriver{angle: tc.angle} + // act & assert + assert.Equal(t, tc.want, d.MaxSpeed()) + }) + } } -func TestEasyDriverWake(t *testing.T) { - // let's test basic functionality - d := initEasyDriver() - _ = d.Sleep() - assert.True(t, d.IsSleeping()) - _ = d.Wake() - assert.False(t, d.IsSleeping()) +func TestEasyDriverEnable_IsEnabled(t *testing.T) { + tests := map[string]struct { + enPin string + simulateWriteErr bool + wantWrites int + wantEnabled bool + wantErr string + }{ + "basic": { + enPin: "10", + wantWrites: 1, + wantEnabled: true, + }, + "with_run": { + enPin: "11", + wantWrites: 1, + wantEnabled: true, + }, + "error_no_pin": { + enPin: "", + wantWrites: 0, + wantEnabled: true, // is enabled by default + wantErr: "enPin is not set", + }, + "error_write": { + enPin: "12", + simulateWriteErr: true, + wantWrites: 1, + wantEnabled: false, + wantErr: "write error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + d := NewEasyDriver(a, stepAngle, "1", "2", tc.enPin, "4") + var numCallsWrite int + var writtenPin string + writtenValue := byte(0xFF) + a.digitalWriteFunc = func(pin string, val byte) error { + numCallsWrite++ + writtenPin = pin + writtenValue = val + if tc.simulateWriteErr { + return fmt.Errorf("write error") + } + return nil + } + d.enabled = false + require.False(t, d.IsEnabled()) + // act + err := d.Enable() + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + assert.Equal(t, byte(0), writtenValue) // enable pin is active low + } + assert.Equal(t, tc.wantEnabled, d.IsEnabled()) + assert.Equal(t, tc.wantWrites, numCallsWrite) + assert.Equal(t, tc.enPin, writtenPin) + }) + } } -func TestEasyDriverEnable(t *testing.T) { - // let's test basic functionality - d := initEasyDriver() - _ = d.Disable() - assert.False(t, d.IsEnabled()) - _ = d.Enable() - assert.True(t, d.IsEnabled()) +func TestEasyDriverDisable_IsEnabled(t *testing.T) { + tests := map[string]struct { + enPin string + runBefore bool + simulateWriteErr string + wantWrites int + wantEnabled bool + wantErr string + }{ + "basic": { + enPin: "10", + wantWrites: 1, + wantEnabled: false, + }, + "with_run": { + enPin: "10", + runBefore: true, + wantWrites: 1, + wantEnabled: false, + }, + "error_no_pin": { + enPin: "", + wantWrites: 0, + wantEnabled: true, // is enabled by default + wantErr: "enPin is not set", + }, + "error_write": { + enPin: "12", + simulateWriteErr: "write error", + wantWrites: 1, + wantEnabled: true, + wantErr: "write error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + d := NewEasyDriver(a, stepAngle, "1", "2", tc.enPin, "4") + writeMutex := sync.Mutex{} + var numCallsWrite int + var writtenPin string + writtenValue := byte(0xFF) + a.digitalWriteFunc = func(pin string, val byte) error { + writeMutex.Lock() + defer writeMutex.Unlock() + if pin == d.stepPin { + // we do not consider call of step() + return nil + } + numCallsWrite++ + writtenPin = pin + writtenValue = val + if tc.simulateWriteErr != "" { + return fmt.Errorf(tc.simulateWriteErr) + } + return nil + } + if tc.runBefore { + require.NoError(t, d.Run()) + require.True(t, d.IsMoving()) + time.Sleep(time.Millisecond) + } + d.enabled = true + require.True(t, d.IsEnabled()) + // act + err := d.Disable() + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + assert.Equal(t, byte(1), writtenValue) // enable pin is active low + } + assert.Equal(t, tc.wantEnabled, d.IsEnabled()) + assert.False(t, d.IsMoving()) + assert.Equal(t, tc.wantWrites, numCallsWrite) + assert.Equal(t, tc.enPin, writtenPin) + }) + } } -func TestEasyDriverEnableNoPin(t *testing.T) { - d := initEasyDriver() - d.enPin = "" - err := d.Disable() - assert.NotNil(t, err) - err = d.Enable() - assert.NotNil(t, err) +func TestEasyDriverSleep_IsSleeping(t *testing.T) { + tests := map[string]struct { + sleepPin string + runBefore bool + wantSleep bool + wantErr string + }{ + "basic": { + sleepPin: "10", + wantSleep: true, + }, + "with_run": { + sleepPin: "11", + runBefore: true, + wantSleep: true, + }, + "error_no_pin": { + sleepPin: "", + wantSleep: false, + wantErr: "sleepPin is not set", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + d := NewEasyDriver(a, stepAngle, "1", "2", "3", tc.sleepPin) + if tc.runBefore { + require.NoError(t, d.Run()) + } + d.sleeping = false + require.False(t, d.IsSleeping()) + // act + err := d.Sleep() + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.wantSleep, d.IsSleeping()) + }) + } } -func TestEasyDriverDisable(t *testing.T) { - // let's test basic functionality - d := initEasyDriver() - _ = d.Disable() - assert.False(t, d.IsEnabled()) - - // let's make sure it stops first - d = initEasyDriver() - _ = d.Run() - _ = d.Disable() - assert.False(t, d.IsEnabled()) - assert.False(t, d.IsMoving()) +func TestEasyDriverWake_IsSleeping(t *testing.T) { + tests := map[string]struct { + sleepPin string + wantSleep bool + wantErr string + }{ + "basic": { + sleepPin: "10", + wantSleep: false, + }, + "error_no_pin": { + sleepPin: "", + wantSleep: true, + wantErr: "sleepPin is not set", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newGpioTestAdaptor() + d := NewEasyDriver(a, stepAngle, "1", "2", "3", tc.sleepPin) + d.sleeping = true + require.True(t, d.IsSleeping()) + // act + err := d.Wake() + // assert + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.wantSleep, d.IsSleeping()) + }) + } } diff --git a/drivers/gpio/gpio_driver_test.go b/drivers/gpio/gpio_driver_test.go index 30ae59bbc..06fe1bcba 100644 --- a/drivers/gpio/gpio_driver_test.go +++ b/drivers/gpio/gpio_driver_test.go @@ -25,12 +25,9 @@ func TestNewDriver(t *testing.T) { // arrange a := newGpioTestAdaptor() // act - var di interface{} = NewDriver(a, "GPIO_BASIC") + d := NewDriver(a, "GPIO_BASIC") // assert - d, ok := di.(*Driver) - if !ok { - t.Errorf("NewDriver() should have returned a *Driver") - } + assert.IsType(t, Driver{}, d) assert.Contains(t, d.name, "GPIO_BASIC") assert.Equal(t, a, d.connection) assert.NoError(t, d.afterStart())