hybridgroup.gobot/drivers/gpio/stepper_driver_test.go

454 lines
11 KiB
Go

package gpio
import (
"fmt"
"log"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gobot.io/x/gobot/v2/drivers/aio"
)
func initTestStepperDriverWithStubbedAdaptor() (*StepperDriver, *gpioTestAdaptor) {
const stepsPerRev = 32
a := newGpioTestAdaptor()
d := NewStepperDriver(a, [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping, stepsPerRev)
return d, a
}
func TestNewStepperDriver(t *testing.T) {
// arrange
const stepsPerRev = 32
a := newGpioTestAdaptor()
// act
d := NewStepperDriver(a, [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping, stepsPerRev)
// assert
assert.IsType(t, &StepperDriver{}, d)
// assert: gpio.driver attributes
require.NotNil(t, d.driver)
assert.True(t, strings.HasPrefix(d.driverCfg.name, "Stepper"))
assert.Equal(t, a, d.connection)
require.NoError(t, d.afterStart())
require.NoError(t, d.beforeHalt())
assert.NotNil(t, d.Commander)
assert.NotNil(t, d.mutex)
// assert: driver specific attributes
assert.Equal(t, "forward", d.direction)
assert.Equal(t, StepperModes.DualPhaseStepping, d.phase)
assert.InDelta(t, float32(stepsPerRev), d.stepsPerRev, 0.0)
assert.Equal(t, 0, d.stepNum)
assert.Nil(t, d.stopAsynchRunFunc)
}
func TestNewStepperDriver_options(t *testing.T) {
// This is a general test, that options are applied in constructor by using the common WithName() option, least one
// option of this driver and one of another driver (which should lead to panic). Further tests for options can also
// be done by call of "WithOption(val).apply(cfg)".
// arrange
const (
myName = "left wheel"
)
panicFunc := func() {
NewStepperDriver(newGpioTestAdaptor(), [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping,
32, WithName("crazy"), aio.WithActuatorScaler(func(float64) int { return 0 }))
}
// act
d := NewStepperDriver(newGpioTestAdaptor(), [4]string{"7", "11", "13", "15"}, StepperModes.DualPhaseStepping,
32, WithName(myName))
// assert
assert.Equal(t, myName, d.Name())
assert.PanicsWithValue(t, "'scaler option for analog actuators' can not be applied on 'crazy'", panicFunc)
}
func TestStepperMove_IsMoving(t *testing.T) {
const stepsPerRev = 32
tests := map[string]struct {
inputSteps int
noAutoStopIfRunning bool
simulateAlreadyRunning bool
simulateWriteErr bool
wantWrites int
wantSteps int
wantMoving bool
wantErr string
}{
"move_forward": {
inputSteps: 2,
wantWrites: 8,
wantSteps: 2,
wantMoving: false,
},
"move_more_forward": {
inputSteps: 10,
wantWrites: 40,
wantSteps: 10,
wantMoving: false,
},
"move_forward_full_revolution": {
inputSteps: stepsPerRev,
wantWrites: 128,
wantSteps: 0, // will be reset after each revision
wantMoving: false,
},
"move_backward": {
inputSteps: -2,
wantWrites: 8,
wantSteps: stepsPerRev - 2,
wantMoving: false,
},
"move_more_backward": {
inputSteps: -10,
wantWrites: 40,
wantSteps: stepsPerRev - 10,
wantMoving: false,
},
"move_backward_full_revolution": {
inputSteps: -stepsPerRev,
wantWrites: 128,
wantSteps: 0, // will be reset after each revision
wantMoving: false,
},
"already_running_autostop": {
inputSteps: 3,
simulateAlreadyRunning: true,
wantWrites: 12,
wantSteps: 3,
wantMoving: false,
},
"error_already_running": {
noAutoStopIfRunning: true,
simulateAlreadyRunning: true,
wantMoving: true,
wantErr: "already running or moving",
},
"error_no_steps": {
inputSteps: 0,
wantWrites: 0,
wantSteps: 0,
wantMoving: false,
wantErr: "no steps to do",
},
"error_write": {
inputSteps: 1,
simulateWriteErr: true,
wantWrites: 0,
wantMoving: false,
wantErr: "write error",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d, a := initTestStepperDriverWithStubbedAdaptor()
defer func() {
// for cleanup dangling channels
if d.stopAsynchRunFunc != nil {
err := d.stopAsynchRunFunc(true)
require.NoError(t, err)
}
}()
// arrange: different behavior
d.haltIfRunning = !tc.noAutoStopIfRunning
if tc.simulateAlreadyRunning {
d.stopAsynchRunFunc = func(bool) error { log.Println("former run stopped"); return nil }
}
// arrange: writes
a.written = nil // reset writes of Start()
a.simulateWriteError = tc.simulateWriteErr
// act
err := d.Move(tc.inputSteps)
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantSteps, d.stepNum)
assert.Len(t, a.written, tc.wantWrites)
assert.Equal(t, tc.wantMoving, d.IsMoving())
})
}
}
func TestStepperRun_IsMoving(t *testing.T) {
tests := map[string]struct {
noAutoStopIfRunning bool
simulateAlreadyRunning bool
simulateWriteErr bool
wantMoving bool
wantErr string
}{
"run": {
wantMoving: true,
},
"error_write": {
simulateWriteErr: true,
wantMoving: true,
},
"error_already_running": {
noAutoStopIfRunning: true,
simulateAlreadyRunning: true,
wantMoving: true,
wantErr: "already running or moving",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d, a := initTestStepperDriverWithStubbedAdaptor()
defer func() {
// for cleanup dangling channels
if d.stopAsynchRunFunc != nil {
err := d.stopAsynchRunFunc(true)
require.NoError(t, err)
}
}()
// arrange: different behavior
writeChan := make(chan struct{})
if tc.noAutoStopIfRunning {
// in this case no write should be called
close(writeChan)
writeChan = nil
d.haltIfRunning = false
} else {
d.haltIfRunning = true
}
if tc.simulateAlreadyRunning {
d.stopAsynchRunFunc = func(bool) error { return nil }
}
// arrange: writes
simWriteErr := tc.simulateWriteErr // to prevent data race in write function (go-called)
var firstWriteDone bool
a.digitalWriteFunc = func(string, byte) error {
if firstWriteDone {
return nil // to prevent to much output and write to channel
}
writeChan <- struct{}{}
firstWriteDone = true
if simWriteErr {
return fmt.Errorf("write error")
}
return nil
}
// act
err := d.Run()
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantMoving, d.IsMoving())
if writeChan != nil {
// wait until the first write was called and a little bit longer
<-writeChan
time.Sleep(10 * time.Millisecond)
var asynchErr error
if d.stopAsynchRunFunc != nil {
asynchErr = d.stopAsynchRunFunc(false)
d.stopAsynchRunFunc = nil
}
if tc.simulateWriteErr {
require.Error(t, asynchErr)
} else {
require.NoError(t, asynchErr)
}
}
})
}
}
func TestStepperStop_IsMoving(t *testing.T) {
tests := map[string]struct {
running bool
wantErr string
}{
"stop_running": {
running: true,
},
"errro_not_started": {
running: false,
wantErr: "is not yet started",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d, _ := initTestStepperDriverWithStubbedAdaptor()
if tc.running {
require.NoError(t, d.Run())
require.True(t, d.IsMoving())
}
// act
err := d.Stop()
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.False(t, d.IsMoving())
})
}
}
func TestStepperHalt_IsMoving(t *testing.T) {
tests := map[string]struct {
running bool
}{
"halt_running": {
running: true,
},
"halt_not_started": {
running: false,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d, _ := initTestStepperDriverWithStubbedAdaptor()
if tc.running {
require.NoError(t, d.Run())
require.True(t, d.IsMoving())
}
// act
err := d.Halt()
// assert
require.NoError(t, err)
assert.False(t, d.IsMoving())
})
}
}
func TestStepperSetDirection(t *testing.T) {
tests := map[string]struct {
input string
wantVal string
wantErr string
}{
"direction_forward": {
input: "forward",
wantVal: "forward",
},
"direction_backward": {
input: "backward",
wantVal: "backward",
},
"error_invalid_direction": {
input: "reverse",
wantVal: "forward",
wantErr: "Invalid direction 'reverse'",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d, _ := initTestStepperDriverWithStubbedAdaptor()
require.Equal(t, "forward", d.direction)
// act
err := d.SetDirection(tc.input)
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantVal, d.direction)
})
}
}
func TestStepperMaxSpeed(t *testing.T) {
const delayForMaxSpeed = 1428 * time.Microsecond // 1/700Hz
tests := map[string]struct {
stepsPerRev float32
want uint
}{
"maxspeed_for_20spr": {
stepsPerRev: 20,
want: 2100,
},
"maxspeed_for_50spr": {
stepsPerRev: 50,
want: 840,
},
"maxspeed_for_100spr": {
stepsPerRev: 100,
want: 420,
},
"maxspeed_for_400spr": {
stepsPerRev: 400,
want: 105,
},
"maxspeed_for_1000spr": {
stepsPerRev: 1000,
want: 42,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d := StepperDriver{stepsPerRev: tc.stepsPerRev}
// act
got := d.MaxSpeed()
d.speedRpm = got
got2 := d.getDelayPerStep()
// assert
assert.Equal(t, tc.want, got)
assert.Equal(t, delayForMaxSpeed, got2)
})
}
}
func TestStepperSetSpeed(t *testing.T) {
const maxRpm = 1166
tests := map[string]struct {
input uint
want uint
wantErr string
}{
"below_minimum": {
input: 0,
want: 0,
wantErr: "RPM (0) cannot be a zero or negative value",
},
"minimum": {
input: 1,
want: 1,
},
"maximum": {
input: maxRpm,
want: maxRpm,
},
"above_maximum": {
input: maxRpm + 1,
want: maxRpm,
wantErr: "cannot be greater then maximal value 1166",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
d, _ := initTestStepperDriverWithStubbedAdaptor()
d.stepsPerRev = 36
// act
err := d.SetSpeed(tc.input)
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.want, d.speedRpm)
})
}
}