hybridgroup.gobot/platforms/bleclient/ble_client_adaptor_test.go

408 lines
9.9 KiB
Go

package bleclient
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gobot.io/x/gobot/v2"
)
var (
_ gobot.Adaptor = (*Adaptor)(nil)
_ gobot.BLEConnector = (*Adaptor)(nil)
)
func TestNewAdaptor(t *testing.T) {
a := NewAdaptor("D7:99:5A:26:EC:38")
assert.Equal(t, "D7:99:5A:26:EC:38", a.Address())
assert.True(t, strings.HasPrefix(a.Name(), "BLEClient"))
}
func TestName(t *testing.T) {
a := NewAdaptor("D7:99:5A:26:EC:38")
a.SetName("awesome")
assert.Equal(t, "awesome", a.Name())
}
func TestConnect(t *testing.T) {
const (
scanTimeout = 5 * time.Millisecond
deviceName = "hello"
deviceAddress = "11:22:44:AA:BB:CC"
rssi = 56
)
tests := map[string]struct {
identifier string
extAdapter *btTestAdapter
extDevice btTestDevice
wantAddress string
wantName string
wantErr string
}{
"connect_by_address": {
identifier: deviceAddress,
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
rssi: rssi,
payload: &btTestPayload{name: deviceName},
},
extDevice: btTestDevice{},
wantAddress: deviceAddress,
wantName: deviceName,
},
"connect_by_name": {
identifier: deviceName,
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
rssi: rssi,
payload: &btTestPayload{name: deviceName},
},
extDevice: btTestDevice{},
wantAddress: deviceAddress,
wantName: deviceName,
},
"error_enable": {
extAdapter: &btTestAdapter{
simulateEnableErr: true,
},
wantName: "BLEClient",
wantErr: "can't get adapter default: adapter enable error",
},
"error_scan": {
extAdapter: &btTestAdapter{
simulateScanErr: true,
},
wantName: "BLEClient",
wantErr: "scan error",
},
"error_stop_scan": {
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
payload: &btTestPayload{},
simulateStopScanErr: true,
},
wantName: "BLEClient",
wantErr: "stop scan error",
},
"error_timeout_long_delay": {
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
payload: &btTestPayload{},
scanDelay: 2 * scanTimeout,
},
wantName: "BLEClient",
wantErr: "scan timeout (5ms) elapsed",
},
"error_timeout_bad_identifier": {
identifier: "bad_identifier",
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
payload: &btTestPayload{},
},
wantAddress: "bad_identifier",
wantName: "BLEClient",
wantErr: "scan timeout (5ms) elapsed",
},
"error_connect": {
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
payload: &btTestPayload{},
simulateConnectErr: true,
},
wantName: "BLEClient",
wantErr: "adapter connect error",
},
"error_discovery_services": {
identifier: "disco_err",
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
payload: &btTestPayload{name: "disco_err"},
},
extDevice: btTestDevice{
simulateDiscoverServicesErr: true,
},
wantAddress: deviceAddress,
wantName: "disco_err",
wantErr: "device discover services error",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor(tc.identifier)
btdc := func(_ bluetoothExtDevicer, address, name string) *btDevice {
return &btDevice{extDevice: tc.extDevice, devAddress: address, devName: name}
}
btac := func(bluetoothExtAdapterer, bool) *btAdapter {
return &btAdapter{extAdapter: tc.extAdapter, btDeviceCreator: btdc}
}
a.btAdptCreator = btac
a.cfg.scanTimeout = scanTimeout // to speed up test
// act
err := a.Connect()
// assert
if tc.wantErr == "" {
require.NoError(t, err)
assert.Equal(t, tc.wantName, a.Name())
assert.Equal(t, tc.wantAddress, a.Address())
assert.Equal(t, rssi, a.RSSI())
assert.True(t, a.connected)
} else {
require.ErrorContains(t, err, tc.wantErr)
assert.Contains(t, a.Name(), tc.wantName)
assert.Equal(t, tc.wantAddress, a.Address())
assert.False(t, a.connected)
}
})
}
}
func TestReconnect(t *testing.T) {
const (
scanTimeout = 5 * time.Millisecond
deviceName = "hello"
deviceAddress = "11:22:44:AA:BB:CC"
rssi = 56
)
tests := map[string]struct {
extAdapter *btTestAdapter
extDevice *btTestDevice
wasConnected bool
wantErr string
}{
"reconnect_not_connected": {
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
rssi: rssi,
payload: &btTestPayload{name: deviceName},
},
extDevice: &btTestDevice{},
},
"reconnect_was_connected": {
extAdapter: &btTestAdapter{
deviceAddress: deviceAddress,
rssi: rssi,
payload: &btTestPayload{name: deviceName},
},
extDevice: &btTestDevice{},
wasConnected: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor(deviceAddress)
btdc := func(_ bluetoothExtDevicer, address, name string) *btDevice {
return &btDevice{extDevice: tc.extDevice, devAddress: address, devName: name}
}
a.btAdpt = &btAdapter{extAdapter: tc.extAdapter, btDeviceCreator: btdc}
a.cfg.scanTimeout = scanTimeout // to speed up test in case of errors
a.cfg.sleepAfterDisconnect = 0 // to speed up test
if tc.wasConnected {
a.btDevice = btdc(nil, "", "")
a.connected = tc.wasConnected
}
// act
err := a.Reconnect()
// assert
if tc.wantErr == "" {
require.NoError(t, err)
assert.Equal(t, rssi, a.RSSI())
} else {
require.ErrorContains(t, err, tc.wantErr)
}
assert.True(t, a.connected)
})
}
}
func TestFinalize(t *testing.T) {
// this also tests Disconnect()
tests := map[string]struct {
extDevice *btTestDevice
wantErr string
}{
"disconnect": {
extDevice: &btTestDevice{},
},
"error_disconnect": {
extDevice: &btTestDevice{
simulateDisconnectErr: true,
},
wantErr: "device disconnect error",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor("")
a.cfg.sleepAfterDisconnect = 0 // to speed up test
a.btDevice = &btDevice{extDevice: tc.extDevice}
// act
err := a.Finalize()
// assert
if tc.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.wantErr)
}
assert.False(t, a.connected)
})
}
}
func TestReadCharacteristic(t *testing.T) {
const uuid = "00001234-0000-1000-8000-00805f9b34fb"
tests := map[string]struct {
inUUID string
chara *btTestChara
notConnected bool
want []byte
wantErr string
}{
"read_ok": {
inUUID: uuid,
chara: &btTestChara{readData: []byte{1, 2, 3}},
want: []byte{1, 2, 3},
},
"error_not_connected": {
notConnected: true,
wantErr: "cannot read from BLE device until connected",
},
"error_bad_chara": {
inUUID: "gag1",
wantErr: "'gag1' is not a valid 16-bit Bluetooth UUID",
},
"error_unknown_chara": {
inUUID: uuid,
wantErr: "unknown characteristic: 00001234-0000-1000-8000-00805f9b34fb",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor("")
if tc.chara != nil {
a.characteristics[uuid] = tc.chara
}
a.connected = !tc.notConnected
// act
got, err := a.ReadCharacteristic(tc.inUUID)
// assert
if tc.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.wantErr)
}
assert.Equal(t, tc.want, got)
})
}
}
func TestWriteCharacteristic(t *testing.T) {
const uuid = "00004321-0000-1000-8000-00805f9b34fb"
tests := map[string]struct {
inUUID string
inData []byte
notConnected bool
chara *btTestChara
want []byte
wantErr string
}{
"write_ok": {
inUUID: uuid,
inData: []byte{3, 2, 1},
chara: &btTestChara{},
want: []byte{3, 2, 1},
},
"error_not_connected": {
notConnected: true,
wantErr: "cannot write to BLE device until connected",
},
"error_bad_chara": {
inUUID: "gag2",
wantErr: "'gag2' is not a valid 16-bit Bluetooth UUID",
},
"error_unknown_chara": {
inUUID: uuid,
wantErr: "unknown characteristic: 00004321-0000-1000-8000-00805f9b34fb",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor("")
if tc.chara != nil {
a.characteristics[uuid] = tc.chara
}
a.connected = !tc.notConnected
// act
err := a.WriteCharacteristic(tc.inUUID, tc.inData)
// assert
if tc.wantErr == "" {
require.NoError(t, err)
assert.Equal(t, tc.want, tc.chara.writtenData)
} else {
require.ErrorContains(t, err, tc.wantErr)
}
})
}
}
func TestSubscribe(t *testing.T) {
const uuid = "00004321-0000-1000-8000-00805f9b34fb"
tests := map[string]struct {
inUUID string
notConnected bool
chara *btTestChara
want []byte
wantErr string
}{
"subscribe_ok": {
inUUID: uuid,
chara: &btTestChara{},
want: []byte{3, 4, 5},
},
"error_not_connected": {
notConnected: true,
wantErr: "cannot subscribe to BLE device until connected",
},
"error_bad_chara": {
inUUID: "gag2",
wantErr: "'gag2' is not a valid 16-bit Bluetooth UUID",
},
"error_unknown_chara": {
inUUID: uuid,
wantErr: "unknown characteristic: 00004321-0000-1000-8000-00805f9b34fb",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a := NewAdaptor("")
if tc.chara != nil {
a.characteristics[uuid] = tc.chara
}
a.connected = !tc.notConnected
var got []byte
notificationFunc := func(data []byte) {
got = append(got, data...)
}
// act
err := a.Subscribe(tc.inUUID, notificationFunc)
// assert
if tc.wantErr == "" {
require.NoError(t, err)
tc.chara.notificationFunc([]byte{3, 4, 5})
} else {
require.ErrorContains(t, err, tc.wantErr)
}
assert.Equal(t, tc.want, got)
})
}
}