hybridgroup.gobot/system/digitalpin_sysfs_test.go

419 lines
11 KiB
Go

package system
import (
"errors"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gobot.io/x/gobot/v2"
)
var (
_ gobot.DigitalPinner = (*digitalPinSysfs)(nil)
_ gobot.DigitalPinValuer = (*digitalPinSysfs)(nil)
_ gobot.DigitalPinOptioner = (*digitalPinSysfs)(nil)
_ gobot.DigitalPinOptionApplier = (*digitalPinSysfs)(nil)
)
func initTestDigitalPinSysfsWithMockedFilesystem(mockPaths []string) (*digitalPinSysfs, *MockFilesystem) {
fs := newMockFilesystem(mockPaths)
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
pin := newDigitalPinSysfs(&sfa, "10")
return pin, fs
}
func Test_newDigitalPinSysfs(t *testing.T) {
// arrange
m := &MockFilesystem{}
sfa := sysfsFileAccess{fs: m, readBufLen: 2}
const pinID = "1"
// act
pin := newDigitalPinSysfs(&sfa, pinID, WithPinOpenDrain())
// assert
assert.Equal(t, pinID, pin.pin)
assert.Equal(t, &sfa, pin.sfa)
assert.Equal(t, "gpio"+pinID, pin.label)
assert.Equal(t, "in", pin.direction)
assert.Equal(t, 1, pin.drive)
}
func TestApplyOptionsSysfs(t *testing.T) {
tests := map[string]struct {
changed []bool
simErr bool
wantExport string
wantErr string
}{
"both_changed": {
changed: []bool{true, true},
wantExport: "10",
},
"first_changed": {
changed: []bool{true, false},
wantExport: "10",
},
"second_changed": {
changed: []bool{false, true},
wantExport: "10",
},
"none_changed": {
changed: []bool{false, false},
wantExport: "",
},
"error_on_change": {
changed: []bool{false, true},
simErr: true,
wantExport: "10",
wantErr: "gpio10/direction: no such file",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
mockPaths := []string{
"/sys/class/gpio/export",
"/sys/class/gpio/gpio10/value",
}
if !tc.simErr {
mockPaths = append(mockPaths, "/sys/class/gpio/gpio10/direction")
}
pin, fs := initTestDigitalPinSysfsWithMockedFilesystem(mockPaths)
optionFunction1 := func(gobot.DigitalPinOptioner) bool {
pin.digitalPinConfig.direction = OUT
return tc.changed[0]
}
optionFunction2 := func(gobot.DigitalPinOptioner) bool {
pin.digitalPinConfig.drive = 15
return tc.changed[1]
}
// act
err := pin.ApplyOptions(optionFunction1, optionFunction2)
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, OUT, pin.digitalPinConfig.direction)
assert.Equal(t, 15, pin.digitalPinConfig.drive)
// marker for call of reconfigure, correct reconfigure is tested independently
assert.Equal(t, tc.wantExport, fs.Files["/sys/class/gpio/export"].Contents)
})
}
}
func TestDirectionBehaviorSysfs(t *testing.T) {
// arrange
pin := newDigitalPinSysfs(nil, "1")
require.Equal(t, "in", pin.direction)
pin.direction = "test"
// act && assert
assert.Equal(t, "test", pin.DirectionBehavior())
}
func TestDigitalPinExportSysfs(t *testing.T) {
// this tests mainly the function reconfigure()
const (
exportPath = "/sys/class/gpio/export"
dirPath = "/sys/class/gpio/gpio10/direction"
valuePath = "/sys/class/gpio/gpio10/value"
inversePath = "/sys/class/gpio/gpio10/active_low"
unexportPath = "/sys/class/gpio/unexport"
)
allMockPaths := []string{exportPath, dirPath, valuePath, inversePath, unexportPath}
tests := map[string]struct {
mockPaths []string
changeDirection string
changeOutInitialState int
changeActiveLow bool
changeBias int
changeDrive int
changeDebouncePeriod time.Duration
changeEdge int
changePollInterval time.Duration
simEbusyOnPath string
wantWrites int
wantExport string
wantUnexport string
wantDirection string
wantValue string
wantInverse string
wantErr string
}{
"ok_without_option": {
mockPaths: allMockPaths,
wantWrites: 2,
wantExport: "10",
wantDirection: "in",
},
"ok_input_bias_dropped": {
mockPaths: allMockPaths,
changeBias: 3,
wantWrites: 2,
wantExport: "10",
wantDirection: "in",
},
"ok_input_drive_dropped": {
mockPaths: allMockPaths,
changeDrive: 2,
wantWrites: 2,
wantExport: "10",
wantDirection: "in",
},
"ok_input_debounce_dropped": {
mockPaths: allMockPaths,
changeDebouncePeriod: 2 * time.Second,
wantWrites: 2,
wantExport: "10",
wantDirection: "in",
},
"ok_input_inverse": {
mockPaths: allMockPaths,
changeActiveLow: true,
wantWrites: 3,
wantExport: "10",
wantDirection: "in",
wantInverse: "1",
},
"ok_output": {
mockPaths: allMockPaths,
changeDirection: "out",
changeOutInitialState: 4,
wantWrites: 3,
wantExport: "10",
wantDirection: "out",
wantValue: "4",
},
"ok_output_bias_dropped": {
mockPaths: allMockPaths,
changeDirection: "out",
changeBias: 3,
wantWrites: 3,
wantExport: "10",
wantDirection: "out",
wantValue: "0",
},
"ok_output_drive_dropped": {
mockPaths: allMockPaths,
changeDirection: "out",
changeDrive: 2,
wantWrites: 3,
wantExport: "10",
wantDirection: "out",
wantValue: "0",
},
"ok_output_debounce_dropped": {
mockPaths: allMockPaths,
changeDirection: "out",
changeDebouncePeriod: 2 * time.Second,
wantWrites: 3,
wantExport: "10",
wantDirection: "out",
wantValue: "0",
},
"ok_output_inverse": {
mockPaths: allMockPaths,
changeDirection: "out",
changeActiveLow: true,
wantWrites: 4,
wantExport: "10",
wantDirection: "out",
wantInverse: "1",
wantValue: "0",
},
"ok_already_exported": {
mockPaths: allMockPaths,
wantWrites: 2,
wantExport: "10",
wantDirection: "in",
simEbusyOnPath: exportPath, // just means "already exported"
},
"error_no_eventhandler_for_polling": { // this only tests the call of function, all other is tested separately
mockPaths: allMockPaths,
changePollInterval: 3 * time.Second,
wantWrites: 3,
wantUnexport: "10",
wantDirection: "in",
wantErr: "event handler is mandatory",
},
"error_no_export_file": {
mockPaths: []string{unexportPath},
wantErr: "/export: no such file",
},
"error_no_direction_file": {
mockPaths: []string{exportPath, unexportPath},
wantWrites: 2,
wantUnexport: "10",
wantErr: "gpio10/direction: no such file",
},
"error_write_direction_file": {
mockPaths: allMockPaths,
wantWrites: 3,
wantUnexport: "10",
simEbusyOnPath: dirPath,
wantErr: "device or resource busy",
},
"error_no_value_file": {
mockPaths: []string{exportPath, dirPath, unexportPath},
wantWrites: 2,
wantUnexport: "10",
wantErr: "gpio10/value: no such file",
},
"error_no_inverse_file": {
mockPaths: []string{exportPath, dirPath, valuePath, unexportPath},
changeActiveLow: true,
wantWrites: 3,
wantUnexport: "10",
wantErr: "gpio10/active_low: no such file",
},
"error_input_edge_without_poll": {
mockPaths: allMockPaths,
changeEdge: 2,
wantWrites: 3,
wantUnexport: "10",
wantErr: "not implemented for sysfs without discrete polling",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
fs := newMockFilesystem(tc.mockPaths)
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
pin := newDigitalPinSysfs(&sfa, "10")
if tc.changeDirection != "" {
pin.direction = tc.changeDirection
}
if tc.changeOutInitialState != 0 {
pin.outInitialState = tc.changeOutInitialState
}
if tc.changeActiveLow {
pin.activeLow = tc.changeActiveLow
}
if tc.changeBias != 0 {
pin.bias = tc.changeBias
}
if tc.changeDrive != 0 {
pin.drive = tc.changeDrive
}
if tc.changeDebouncePeriod != 0 {
pin.debouncePeriod = tc.changeDebouncePeriod
}
if tc.changeEdge != 0 {
pin.edge = tc.changeEdge
}
if tc.changePollInterval != 0 {
pin.pollInterval = tc.changePollInterval
}
// arrange write function
if tc.simEbusyOnPath != "" {
fs.Files[tc.simEbusyOnPath].simulateWriteError = &os.PathError{Err: Syscall_EBUSY}
}
// act
err := pin.Export()
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
assert.NotNil(t, pin.valFile)
assert.NotNil(t, pin.dirFile)
assert.Equal(t, tc.wantDirection, fs.Files[dirPath].Contents)
assert.Equal(t, tc.wantExport, fs.Files[exportPath].Contents)
assert.Equal(t, tc.wantValue, fs.Files[valuePath].Contents)
assert.Equal(t, tc.wantInverse, fs.Files[inversePath].Contents)
}
assert.Equal(t, tc.wantUnexport, fs.Files[unexportPath].Contents)
assert.Equal(t, tc.wantWrites, fs.numCallsWrite)
})
}
}
func TestDigitalPinSysfs(t *testing.T) {
mockPaths := []string{
"/sys/class/gpio/export",
"/sys/class/gpio/unexport",
"/sys/class/gpio/gpio10/value",
"/sys/class/gpio/gpio10/direction",
}
pin, fs := initTestDigitalPinSysfsWithMockedFilesystem(mockPaths)
assert.Equal(t, "10", pin.pin)
assert.Equal(t, "gpio10", pin.label)
assert.Nil(t, pin.valFile)
err := pin.Unexport()
require.NoError(t, err)
assert.Equal(t, "10", fs.Files["/sys/class/gpio/unexport"].Contents)
require.NoError(t, pin.Export())
err = pin.Write(1)
require.NoError(t, err)
assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio10/value"].Contents)
err = pin.ApplyOptions(WithPinDirectionInput())
require.NoError(t, err)
assert.Equal(t, "in", fs.Files["/sys/class/gpio/gpio10/direction"].Contents)
data, _ := pin.Read()
assert.Equal(t, 1, data)
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
pin2 := newDigitalPinSysfs(&sfa, "30")
err = pin2.Write(1)
require.ErrorContains(t, err, "pin has not been exported")
data, err = pin2.Read()
require.ErrorContains(t, err, "pin has not been exported")
assert.Equal(t, 0, data)
// arrange: unexport general write error, the error is not suppressed
fs.Files["/sys/class/gpio/unexport"].simulateWriteError = &os.PathError{Err: errors.New("write error")}
// act: unexport
err = pin.Unexport()
// assert: the error is not suppressed
var pathError *os.PathError
require.ErrorAs(t, err, &pathError)
require.ErrorContains(t, err, "write error")
}
func TestDigitalPinUnexportErrorSysfs(t *testing.T) {
tests := map[string]struct {
simulateError error
wantErr string
}{
"reserved_pin": {
// simulation of reserved pin, the internal error is suppressed
simulateError: &os.PathError{Err: Syscall_EINVAL},
wantErr: "",
},
"error_busy": {
simulateError: &os.PathError{Err: Syscall_EBUSY},
wantErr: " : device or resource busy",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
mockPaths := []string{
"/sys/class/gpio/unexport",
}
pin, fs := initTestDigitalPinSysfsWithMockedFilesystem(mockPaths)
fs.Files["/sys/class/gpio/unexport"].simulateWriteError = tc.simulateError
// act
err := pin.Unexport()
// assert
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
})
}
}