1
0
mirror of https://github.com/hybridgroup/gobot.git synced 2025-04-27 13:48:56 +08:00
hybridgroup.gobot/drivers/gpio/easy_driver_test.go
2023-10-31 18:12:07 +01:00

634 lines
14 KiB
Go

package gpio
import (
"fmt"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
stepAngle = 0.5 // use non int step angle to check int math
stepsPerRev = 720
)
func initTestEasyDriverWithStubbedAdaptor() (*EasyDriver, *gpioTestAdaptor) {
a := newGpioTestAdaptor()
d := NewEasyDriver(a, stepAngle, "1", "2", "3", "4")
return d, a
}
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) {
// 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) {
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_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_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) {
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) {
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) {
const (
angle = 10
max = 36 // 360/angle
)
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 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 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 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 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 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())
})
}
}