478 lines
14 KiB
Go
478 lines
14 KiB
Go
//nolint:nonamedreturns // ok for tests
|
|
package adaptors
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"gobot.io/x/gobot/v2"
|
|
"gobot.io/x/gobot/v2/drivers/gpio"
|
|
"gobot.io/x/gobot/v2/system"
|
|
)
|
|
|
|
const (
|
|
pwmDir = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/" //nolint:gosec // false positive
|
|
pwmPwm44Dir = pwmDir + "pwm44/"
|
|
pwmPwm47Dir = pwmDir + "pwm47/"
|
|
pwmExportPath = pwmDir + "export"
|
|
pwmUnexportPath = pwmDir + "unexport"
|
|
pwm44EnablePath = pwmPwm44Dir + "enable"
|
|
pwm44PeriodPath = pwmPwm44Dir + "period"
|
|
pwm44DutyCyclePath = pwmPwm44Dir + "duty_cycle"
|
|
pwm44PolarityPath = pwmPwm44Dir + "polarity"
|
|
pwm47EnablePath = pwmPwm47Dir + "enable"
|
|
pwm47PeriodPath = pwmPwm47Dir + "period"
|
|
pwm47DutyCyclePath = pwmPwm47Dir + "duty_cycle"
|
|
pwm47PolarityPath = pwmPwm47Dir + "polarity"
|
|
)
|
|
|
|
var pwmMockPaths = []string{
|
|
pwmExportPath,
|
|
pwmUnexportPath,
|
|
pwm44EnablePath,
|
|
pwm44PeriodPath,
|
|
pwm44DutyCyclePath,
|
|
pwm44PolarityPath,
|
|
pwm47EnablePath,
|
|
pwm47PeriodPath,
|
|
pwm47DutyCyclePath,
|
|
pwm47PolarityPath,
|
|
}
|
|
|
|
// make sure that this PWMPinsAdaptor fulfills all the required interfaces
|
|
var (
|
|
_ gobot.PWMPinnerProvider = (*PWMPinsAdaptor)(nil)
|
|
_ gpio.PwmWriter = (*PWMPinsAdaptor)(nil)
|
|
_ gpio.ServoWriter = (*PWMPinsAdaptor)(nil)
|
|
)
|
|
|
|
func initTestPWMPinsAdaptorWithMockedFilesystem(mockPaths []string) (*PWMPinsAdaptor, *system.MockFilesystem) {
|
|
sys := system.NewAccesser()
|
|
fs := sys.UseMockFilesystem(mockPaths)
|
|
a := NewPWMPinsAdaptor(sys, testPWMPinTranslator)
|
|
fs.Files[pwm44EnablePath].Contents = "0"
|
|
fs.Files[pwm44PeriodPath].Contents = "0"
|
|
fs.Files[pwm44DutyCyclePath].Contents = "0"
|
|
fs.Files[pwm44PolarityPath].Contents = a.pwmPinsCfg.polarityInvertedIdentifier
|
|
if err := a.Connect(); err != nil {
|
|
panic(err)
|
|
}
|
|
return a, fs
|
|
}
|
|
|
|
func testPWMPinTranslator(id string) (string, int, error) {
|
|
channel, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
return "", -1, fmt.Errorf("'%s' is not a valid id of a PWM pin", id)
|
|
}
|
|
channel = channel + 11 // just for tests, 33=>pwm0, 36=>pwm3
|
|
return pwmDir, channel, err
|
|
}
|
|
|
|
func TestNewPWMPinsAdaptor(t *testing.T) {
|
|
// arrange
|
|
translate := func(pin string) (chip string, line int, err error) { return }
|
|
// act
|
|
a := NewPWMPinsAdaptor(system.NewAccesser(), translate)
|
|
// assert
|
|
assert.Equal(t, uint32(pwmPeriodDefault), a.pwmPinsCfg.periodDefault)
|
|
assert.Equal(t, "normal", a.pwmPinsCfg.polarityNormalIdentifier)
|
|
assert.Equal(t, "inversed", a.pwmPinsCfg.polarityInvertedIdentifier)
|
|
assert.True(t, a.pwmPinsCfg.adjustDutyOnSetPeriod)
|
|
}
|
|
|
|
func TestPWMPinsConnect(t *testing.T) {
|
|
translate := func(pin string) (chip string, line int, err error) { return }
|
|
a := NewPWMPinsAdaptor(system.NewAccesser(), translate)
|
|
assert.Equal(t, (map[string]gobot.PWMPinner)(nil), a.pins)
|
|
|
|
err := a.PwmWrite("33", 1)
|
|
require.ErrorContains(t, err, "not connected")
|
|
|
|
err = a.Connect()
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, (map[string]gobot.PWMPinner)(nil), a.pins)
|
|
assert.Empty(t, a.pins)
|
|
}
|
|
|
|
func TestPWMPinsFinalize(t *testing.T) {
|
|
// arrange
|
|
sys := system.NewAccesser()
|
|
fs := sys.UseMockFilesystem(pwmMockPaths)
|
|
a := NewPWMPinsAdaptor(sys, testPWMPinTranslator)
|
|
fs.Files[pwm44PeriodPath].Contents = "0"
|
|
fs.Files[pwm44DutyCyclePath].Contents = "0"
|
|
// assert that finalize before connect is working
|
|
require.NoError(t, a.Finalize())
|
|
// arrange
|
|
require.NoError(t, a.Connect())
|
|
require.NoError(t, a.PwmWrite("33", 1))
|
|
assert.Len(t, a.pins, 1)
|
|
// act
|
|
err := a.Finalize()
|
|
// assert
|
|
require.NoError(t, err)
|
|
assert.Empty(t, a.pins)
|
|
// assert that finalize after finalize is working
|
|
require.NoError(t, a.Finalize())
|
|
// arrange missing sysfs file
|
|
require.NoError(t, a.Connect())
|
|
require.NoError(t, a.PwmWrite("33", 2))
|
|
delete(fs.Files, pwmUnexportPath)
|
|
err = a.Finalize()
|
|
require.ErrorContains(t, err, pwmUnexportPath+": no such file")
|
|
// arrange write error
|
|
require.NoError(t, a.Connect())
|
|
require.NoError(t, a.PwmWrite("33", 2))
|
|
fs.WithWriteError = true
|
|
err = a.Finalize()
|
|
require.ErrorContains(t, err, "write error")
|
|
}
|
|
|
|
func TestPWMPinsReConnect(t *testing.T) {
|
|
// arrange
|
|
a, _ := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
|
|
require.NoError(t, a.PwmWrite("33", 1))
|
|
assert.Len(t, a.pins, 1)
|
|
require.NoError(t, a.Finalize())
|
|
// act
|
|
err := a.Connect()
|
|
// assert
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, a.pins)
|
|
assert.Empty(t, a.pins)
|
|
}
|
|
|
|
func TestPWMPinsCache(t *testing.T) {
|
|
// arrange
|
|
a, _ := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
|
|
// act
|
|
firstSysPin, err := a.PWMPin("33")
|
|
require.NoError(t, err)
|
|
secondSysPin, err := a.PWMPin("33")
|
|
require.NoError(t, err)
|
|
otherSysPin, err := a.PWMPin("36")
|
|
require.NoError(t, err)
|
|
// assert
|
|
assert.Equal(t, secondSysPin, firstSysPin)
|
|
assert.NotEqual(t, otherSysPin, firstSysPin)
|
|
}
|
|
|
|
func TestPwmWrite(t *testing.T) {
|
|
tests := map[string]struct {
|
|
pin string
|
|
value byte
|
|
minimumRate float64
|
|
simulateWriteErr bool
|
|
simulateReadErr bool
|
|
wantExport string
|
|
wantEnable string
|
|
wantPeriod string
|
|
wantDutyCycle string
|
|
wantErr string
|
|
}{
|
|
"write_max": {
|
|
pin: "33",
|
|
value: 255,
|
|
wantExport: "44",
|
|
wantEnable: "1",
|
|
wantPeriod: "10000000",
|
|
wantDutyCycle: "10000000",
|
|
},
|
|
"write_nearmax": {
|
|
pin: "33",
|
|
value: 254,
|
|
wantExport: "44",
|
|
wantEnable: "1",
|
|
wantPeriod: "10000000",
|
|
wantDutyCycle: "9960784",
|
|
},
|
|
"write_mid": {
|
|
pin: "33",
|
|
value: 100,
|
|
wantExport: "44",
|
|
wantEnable: "1",
|
|
wantPeriod: "10000000",
|
|
wantDutyCycle: "3921568",
|
|
},
|
|
"write_near min": {
|
|
pin: "33",
|
|
value: 1,
|
|
wantExport: "44",
|
|
wantEnable: "1",
|
|
wantPeriod: "10000000",
|
|
wantDutyCycle: "39215",
|
|
},
|
|
"write_min": {
|
|
pin: "33",
|
|
value: 0,
|
|
minimumRate: 0.05,
|
|
wantExport: "44",
|
|
wantEnable: "1",
|
|
wantPeriod: "10000000",
|
|
wantDutyCycle: "0",
|
|
},
|
|
"error_min_rate": {
|
|
pin: "33",
|
|
value: 1,
|
|
minimumRate: 0.05,
|
|
wantExport: "44",
|
|
wantEnable: "1",
|
|
wantPeriod: "10000000",
|
|
wantDutyCycle: "0",
|
|
wantErr: "is lower than allowed (0.05",
|
|
},
|
|
"error_non_existent_pin": {
|
|
pin: "notexist",
|
|
wantEnable: "0",
|
|
wantPeriod: "0",
|
|
wantDutyCycle: "0",
|
|
wantErr: "'notexist' is not a valid id of a PWM pin",
|
|
},
|
|
"error_write_error": {
|
|
pin: "33",
|
|
value: 10,
|
|
simulateWriteErr: true,
|
|
wantEnable: "0",
|
|
wantPeriod: "0",
|
|
wantDutyCycle: "0",
|
|
wantErr: "write error",
|
|
},
|
|
"error_read_error": {
|
|
pin: "33",
|
|
value: 11,
|
|
simulateReadErr: true,
|
|
wantExport: "44",
|
|
wantEnable: "0",
|
|
wantPeriod: "0",
|
|
wantDutyCycle: "0",
|
|
wantErr: "read error",
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
// arrange
|
|
a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
|
|
if tc.minimumRate > 0 {
|
|
a.pwmPinsCfg.dutyRateMinimum = tc.minimumRate
|
|
}
|
|
fs.WithWriteError = tc.simulateWriteErr
|
|
fs.WithReadError = tc.simulateReadErr
|
|
// act
|
|
err := a.PwmWrite(tc.pin, tc.value)
|
|
// assert
|
|
assert.Equal(t, tc.wantExport, fs.Files[pwmExportPath].Contents)
|
|
assert.Equal(t, tc.wantEnable, fs.Files[pwm44EnablePath].Contents)
|
|
assert.Equal(t, tc.wantPeriod, fs.Files[pwm44PeriodPath].Contents)
|
|
assert.Equal(t, tc.wantDutyCycle, fs.Files[pwm44DutyCyclePath].Contents)
|
|
if tc.wantErr != "" {
|
|
require.ErrorContains(t, err, tc.wantErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServoWrite(t *testing.T) {
|
|
a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
|
|
|
|
err := a.ServoWrite("33", 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "44", fs.Files[pwmExportPath].Contents)
|
|
assert.Equal(t, "1", fs.Files[pwm44EnablePath].Contents)
|
|
//nolint:perfsprint // ok here
|
|
assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwm44PeriodPath].Contents)
|
|
assert.Equal(t, "250000", fs.Files[pwm44DutyCyclePath].Contents)
|
|
assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents)
|
|
|
|
err = a.ServoWrite("33", 180)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "1250000", fs.Files[pwm44DutyCyclePath].Contents)
|
|
|
|
err = a.ServoWrite("notexist", 42)
|
|
require.ErrorContains(t, err, "'notexist' is not a valid id of a PWM pin")
|
|
|
|
fs.WithWriteError = true
|
|
err = a.ServoWrite("33", 100)
|
|
require.ErrorContains(t, err, "write error")
|
|
fs.WithWriteError = false
|
|
|
|
fs.WithReadError = true
|
|
err = a.ServoWrite("33", 100)
|
|
require.ErrorContains(t, err, "read error")
|
|
fs.WithReadError = false
|
|
|
|
delete(a.pwmPinsCfg.pinsServoScale, "33")
|
|
err = a.ServoWrite("33", 42)
|
|
require.EqualError(t, err, "no scaler found for servo pin '33'")
|
|
}
|
|
|
|
func TestSetPeriod(t *testing.T) {
|
|
// arrange
|
|
a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths)
|
|
newPeriod := uint32(2550000)
|
|
// act
|
|
err := a.SetPeriod("33", newPeriod)
|
|
// assert
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "44", fs.Files[pwmExportPath].Contents)
|
|
assert.Equal(t, "1", fs.Files[pwm44EnablePath].Contents)
|
|
assert.Equal(t, fmt.Sprintf("%d", newPeriod), fs.Files[pwm44PeriodPath].Contents) //nolint:perfsprint // ok here
|
|
assert.Equal(t, "0", fs.Files[pwm44DutyCyclePath].Contents)
|
|
assert.Equal(t, "normal", fs.Files[pwm44PolarityPath].Contents)
|
|
|
|
// arrange test for automatic adjustment of duty cycle to lower value
|
|
err = a.PwmWrite("33", 127) // 127 is a little bit smaller than 50% of period
|
|
require.NoError(t, err)
|
|
assert.Equal(t, strconv.Itoa(1270000), fs.Files[pwm44DutyCyclePath].Contents)
|
|
newPeriod = newPeriod / 10
|
|
|
|
// act
|
|
err = a.SetPeriod("33", newPeriod)
|
|
|
|
// assert
|
|
require.NoError(t, err)
|
|
assert.Equal(t, strconv.Itoa(127000), fs.Files[pwm44DutyCyclePath].Contents)
|
|
|
|
// arrange test for automatic adjustment of duty cycle to higher value
|
|
newPeriod = newPeriod * 20
|
|
|
|
// act
|
|
err = a.SetPeriod("33", newPeriod)
|
|
|
|
// assert
|
|
require.NoError(t, err)
|
|
assert.Equal(t, strconv.Itoa(2540000), fs.Files[pwm44DutyCyclePath].Contents)
|
|
|
|
// act
|
|
err = a.SetPeriod("not_exist", newPeriod)
|
|
// assert
|
|
require.ErrorContains(t, err, "'not_exist' is not a valid id of a PWM pin")
|
|
}
|
|
|
|
func Test_PWMPin(t *testing.T) {
|
|
translateErr := "translator_error"
|
|
translator := func(string) (string, int, error) { return pwmDir, 44, nil }
|
|
tests := map[string]struct {
|
|
mockPaths []string
|
|
period string
|
|
dutyCycle string
|
|
translate func(string) (string, int, error)
|
|
pin string
|
|
wantErr string
|
|
}{
|
|
"pin_ok": {
|
|
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath, pwm44PolarityPath},
|
|
period: "0",
|
|
dutyCycle: "0",
|
|
translate: translator,
|
|
pin: "33",
|
|
},
|
|
"init_export_error": {
|
|
mockPaths: []string{},
|
|
translate: translator,
|
|
pin: "33",
|
|
wantErr: "Export() failed for id 44 with : " +
|
|
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/export: no such file",
|
|
},
|
|
"init_setenabled_error": {
|
|
mockPaths: []string{pwmExportPath, pwm44PeriodPath},
|
|
period: "1000",
|
|
translate: translator,
|
|
pin: "33",
|
|
wantErr: "SetEnabled(false) failed for id 44 with : " +
|
|
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/enable: no such file",
|
|
},
|
|
"init_setperiod_dutycycle_no_error": {
|
|
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath, pwm44PolarityPath},
|
|
period: "0",
|
|
dutyCycle: "0",
|
|
translate: translator,
|
|
pin: "33",
|
|
},
|
|
"init_setperiod_error": {
|
|
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44DutyCyclePath},
|
|
dutyCycle: "0",
|
|
translate: translator,
|
|
pin: "33",
|
|
wantErr: "SetPeriod(10000000) failed for id 44 with : " +
|
|
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/period: no such file",
|
|
},
|
|
"init_setpolarity_error": {
|
|
mockPaths: []string{pwmExportPath, pwm44EnablePath, pwm44PeriodPath, pwm44DutyCyclePath},
|
|
period: "0",
|
|
dutyCycle: "0",
|
|
translate: translator,
|
|
pin: "33",
|
|
wantErr: "SetPolarity(normal) failed for id 44 with : " +
|
|
"/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/polarity: no such file",
|
|
},
|
|
"translate_error": {
|
|
translate: func(string) (string, int, error) { return "", -1, fmt.Errorf(translateErr) },
|
|
wantErr: translateErr,
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
// arrange
|
|
sys := system.NewAccesser()
|
|
fs := sys.UseMockFilesystem(tc.mockPaths)
|
|
if tc.period != "" {
|
|
fs.Files[pwm44PeriodPath].Contents = tc.period
|
|
}
|
|
if tc.dutyCycle != "" {
|
|
fs.Files[pwm44DutyCyclePath].Contents = tc.dutyCycle
|
|
}
|
|
a := NewPWMPinsAdaptor(sys, tc.translate)
|
|
if err := a.Connect(); err != nil {
|
|
panic(err)
|
|
}
|
|
// act
|
|
got, err := a.PWMPin(tc.pin)
|
|
// assert
|
|
if tc.wantErr == "" {
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, got)
|
|
} else {
|
|
require.ErrorContains(t, err, tc.wantErr)
|
|
assert.Nil(t, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPWMPinConcurrency(t *testing.T) {
|
|
oldProcs := runtime.GOMAXPROCS(0)
|
|
runtime.GOMAXPROCS(8)
|
|
defer runtime.GOMAXPROCS(oldProcs)
|
|
|
|
translate := func(pin string) (string, int, error) { line, err := strconv.Atoi(pin); return "", line, err }
|
|
sys := system.NewAccesser()
|
|
|
|
for retry := 0; retry < 20; retry++ {
|
|
|
|
a := NewPWMPinsAdaptor(sys, translate)
|
|
_ = a.Connect()
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 20; i++ {
|
|
wg.Add(1)
|
|
pinAsString := strconv.Itoa(i)
|
|
go func(pin string) {
|
|
defer wg.Done()
|
|
_, _ = a.PWMPin(pin)
|
|
}(pinAsString)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
}
|