hybridgroup.gobot/platforms/adaptors/pwmpinsadaptor_test.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()
}
}