DeviceWatcher events are more strongly typed and added higher-level methods.
This commit is contained in:
parent
0aa31c0548
commit
af4b3ddcf2
|
@ -18,10 +18,42 @@ type DeviceWatcher struct {
|
||||||
*deviceWatcherImpl
|
*deviceWatcherImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeviceStateChangedEvent represents a device state transition.
|
||||||
type DeviceStateChangedEvent struct {
|
type DeviceStateChangedEvent struct {
|
||||||
Serial string
|
Serial string
|
||||||
OldState string
|
OldState DeviceState
|
||||||
NewState string
|
NewState DeviceState
|
||||||
|
}
|
||||||
|
|
||||||
|
// CameOnline returns true if this event represents a device coming online.
|
||||||
|
func (s DeviceStateChangedEvent) CameOnline() bool {
|
||||||
|
return s.OldState != StateOnline && s.NewState == StateOnline
|
||||||
|
}
|
||||||
|
|
||||||
|
// WentOffline returns true if this event represents a device going offline.
|
||||||
|
func (s DeviceStateChangedEvent) WentOffline() bool {
|
||||||
|
return s.OldState == StateOnline && s.NewState != StateOnline
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceState represents one of the 3 possible states adb will report devices.
|
||||||
|
// A device can be communicated with when it's in StateOnline.
|
||||||
|
// A USB device will transition from StateDisconnected->StateOffline->StateOnline when
|
||||||
|
// plugged in, and then StateOnline->StateDisconnected when unplugged.
|
||||||
|
// If code doesn't care about specific states, DeviceStateChangedEvent provides methods
|
||||||
|
// to query at a higher level.
|
||||||
|
//go:generate stringer -type=DeviceState
|
||||||
|
type DeviceState int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateDisconnected DeviceState = iota
|
||||||
|
StateOffline
|
||||||
|
StateOnline
|
||||||
|
)
|
||||||
|
|
||||||
|
var deviceStateStrings = map[string]DeviceState{
|
||||||
|
"": StateDisconnected,
|
||||||
|
"offline": StateOffline,
|
||||||
|
"device": StateOnline,
|
||||||
}
|
}
|
||||||
|
|
||||||
type deviceWatcherImpl struct {
|
type deviceWatcherImpl struct {
|
||||||
|
@ -96,7 +128,7 @@ and abort. If true, report no error and stop.
|
||||||
func publishDevices(watcher *deviceWatcherImpl) {
|
func publishDevices(watcher *deviceWatcherImpl) {
|
||||||
defer close(watcher.eventChan)
|
defer close(watcher.eventChan)
|
||||||
|
|
||||||
var lastKnownStates map[string]string
|
var lastKnownStates map[string]DeviceState
|
||||||
finished := false
|
finished := false
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -148,7 +180,7 @@ func connectToTrackDevices(dialer Dialer) (wire.Scanner, error) {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func publishDevicesUntilError(scanner wire.Scanner, eventChan chan<- DeviceStateChangedEvent, lastKnownStates *map[string]string) (finished bool, err error) {
|
func publishDevicesUntilError(scanner wire.Scanner, eventChan chan<- DeviceStateChangedEvent, lastKnownStates *map[string]DeviceState) (finished bool, err error) {
|
||||||
for {
|
for {
|
||||||
msg, err := scanner.ReadMessage()
|
msg, err := scanner.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -167,8 +199,8 @@ func publishDevicesUntilError(scanner wire.Scanner, eventChan chan<- DeviceState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDeviceStates(msg string) (states map[string]string, err error) {
|
func parseDeviceStates(msg string) (states map[string]DeviceState, err error) {
|
||||||
states = make(map[string]string)
|
states = make(map[string]DeviceState)
|
||||||
|
|
||||||
for lineNum, line := range strings.Split(msg, "\n") {
|
for lineNum, line := range strings.Split(msg, "\n") {
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
|
@ -181,14 +213,18 @@ func parseDeviceStates(msg string) (states map[string]string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serial, state := fields[0], fields[1]
|
serial, stateString := fields[0], fields[1]
|
||||||
|
state, ok := deviceStateStrings[stateString]
|
||||||
|
if !ok {
|
||||||
|
err = util.Errorf(util.ParseError, "invalid device state: %s", state)
|
||||||
|
}
|
||||||
states[serial] = state
|
states[serial] = state
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateStateDiffs(oldStates, newStates map[string]string) (events []DeviceStateChangedEvent) {
|
func calculateStateDiffs(oldStates, newStates map[string]DeviceState) (events []DeviceStateChangedEvent) {
|
||||||
for serial, oldState := range oldStates {
|
for serial, oldState := range oldStates {
|
||||||
newState, ok := newStates[serial]
|
newState, ok := newStates[serial]
|
||||||
|
|
||||||
|
@ -198,7 +234,7 @@ func calculateStateDiffs(oldStates, newStates map[string]string) (events []Devic
|
||||||
events = append(events, DeviceStateChangedEvent{serial, oldState, newState})
|
events = append(events, DeviceStateChangedEvent{serial, oldState, newState})
|
||||||
} else {
|
} else {
|
||||||
// Device only present in old list: device removed.
|
// Device only present in old list: device removed.
|
||||||
events = append(events, DeviceStateChangedEvent{serial, oldState, ""})
|
events = append(events, DeviceStateChangedEvent{serial, oldState, StateDisconnected})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,7 +242,7 @@ func calculateStateDiffs(oldStates, newStates map[string]string) (events []Devic
|
||||||
for serial, newState := range newStates {
|
for serial, newState := range newStates {
|
||||||
if _, ok := oldStates[serial]; !ok {
|
if _, ok := oldStates[serial]; !ok {
|
||||||
// Device only present in new list: device added.
|
// Device only present in new list: device added.
|
||||||
events = append(events, DeviceStateChangedEvent{serial, "", newState})
|
events = append(events, DeviceStateChangedEvent{serial, StateDisconnected, newState})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package goadb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -11,27 +10,27 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseDeviceStatesSingle(t *testing.T) {
|
func TestParseDeviceStatesSingle(t *testing.T) {
|
||||||
states, err := parseDeviceStates(`192.168.56.101:5555 emulator-state
|
states, err := parseDeviceStates(`192.168.56.101:5555 offline
|
||||||
`)
|
`)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, states, 1)
|
assert.Len(t, states, 1)
|
||||||
assert.Equal(t, "emulator-state", states["192.168.56.101:5555"])
|
assert.Equal(t, StateOffline, states["192.168.56.101:5555"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDeviceStatesMultiple(t *testing.T) {
|
func TestParseDeviceStatesMultiple(t *testing.T) {
|
||||||
states, err := parseDeviceStates(`192.168.56.101:5555 emulator-state
|
states, err := parseDeviceStates(`192.168.56.101:5555 offline
|
||||||
0x0x0x0x usb-state
|
0x0x0x0x device
|
||||||
`)
|
`)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, states, 2)
|
assert.Len(t, states, 2)
|
||||||
assert.Equal(t, "emulator-state", states["192.168.56.101:5555"])
|
assert.Equal(t, StateOffline, states["192.168.56.101:5555"])
|
||||||
assert.Equal(t, "usb-state", states["0x0x0x0x"])
|
assert.Equal(t, StateOnline, states["0x0x0x0x"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDeviceStatesMalformed(t *testing.T) {
|
func TestParseDeviceStatesMalformed(t *testing.T) {
|
||||||
_, err := parseDeviceStates(`192.168.56.101:5555 emulator-state
|
_, err := parseDeviceStates(`192.168.56.101:5555 offline
|
||||||
0x0x0x0x
|
0x0x0x0x
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -40,8 +39,8 @@ func TestParseDeviceStatesMalformed(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsUnchangedEmpty(t *testing.T) {
|
func TestCalculateStateDiffsUnchangedEmpty(t *testing.T) {
|
||||||
oldStates := map[string]string{}
|
oldStates := map[string]DeviceState{}
|
||||||
newStates := map[string]string{}
|
newStates := map[string]DeviceState{}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
|
@ -49,13 +48,13 @@ func TestCalculateStateDiffsUnchangedEmpty(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsUnchangedNonEmpty(t *testing.T) {
|
func TestCalculateStateDiffsUnchangedNonEmpty(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "device",
|
"1": StateOnline,
|
||||||
"2": "device",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"1": "device",
|
"1": StateOnline,
|
||||||
"2": "device",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
@ -64,131 +63,147 @@ func TestCalculateStateDiffsUnchangedNonEmpty(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneAdded(t *testing.T) {
|
func TestCalculateStateDiffsOneAdded(t *testing.T) {
|
||||||
oldStates := map[string]string{}
|
oldStates := map[string]DeviceState{}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"serial": "added",
|
"serial": StateOffline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.Equal(t, []DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"serial", "", "added"},
|
DeviceStateChangedEvent{"serial", StateDisconnected, StateOffline},
|
||||||
}, diffs)
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneRemoved(t *testing.T) {
|
func TestCalculateStateDiffsOneRemoved(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"serial": "removed",
|
"serial": StateOffline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{}
|
newStates := map[string]DeviceState{}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.Equal(t, []DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"serial", "removed", ""},
|
DeviceStateChangedEvent{"serial", StateOffline, StateDisconnected},
|
||||||
}, diffs)
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneAddedOneUnchanged(t *testing.T) {
|
func TestCalculateStateDiffsOneAddedOneUnchanged(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "device",
|
"1": StateOnline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"1": "device",
|
"1": StateOnline,
|
||||||
"2": "added",
|
"2": StateOffline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.Equal(t, []DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"2", "", "added"},
|
DeviceStateChangedEvent{"2", StateDisconnected, StateOffline},
|
||||||
}, diffs)
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneRemovedOneUnchanged(t *testing.T) {
|
func TestCalculateStateDiffsOneRemovedOneUnchanged(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "removed",
|
"1": StateOffline,
|
||||||
"2": "device",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"2": "device",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.Equal(t, []DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"1", "removed", ""},
|
DeviceStateChangedEvent{"1", StateOffline, StateDisconnected},
|
||||||
}, diffs)
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneAddedOneRemoved(t *testing.T) {
|
func TestCalculateStateDiffsOneAddedOneRemoved(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "removed",
|
"1": StateOffline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"2": "added",
|
"2": StateOffline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.Equal(t, []DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"1", "removed", ""},
|
DeviceStateChangedEvent{"1", StateOffline, StateDisconnected},
|
||||||
DeviceStateChangedEvent{"2", "", "added"},
|
DeviceStateChangedEvent{"2", StateDisconnected, StateOffline},
|
||||||
}, diffs)
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneChangedOneUnchanged(t *testing.T) {
|
func TestCalculateStateDiffsOneChangedOneUnchanged(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "oldState",
|
"1": StateOffline,
|
||||||
"2": "device",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"1": "newState",
|
"1": StateOnline,
|
||||||
"2": "device",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.Equal(t, []DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"1", "oldState", "newState"},
|
DeviceStateChangedEvent{"1", StateOffline, StateOnline},
|
||||||
}, diffs)
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsMultipleChangedMultipleUnchanged(t *testing.T) {
|
func TestCalculateStateDiffsMultipleChanged(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "oldState",
|
"1": StateOffline,
|
||||||
"2": "oldState",
|
"2": StateOnline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"1": "newState",
|
"1": StateOnline,
|
||||||
"2": "newState",
|
"2": StateOffline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.True(t, reflect.DeepEqual([]DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"1", "oldState", "newState"},
|
DeviceStateChangedEvent{"1", StateOffline, StateOnline},
|
||||||
DeviceStateChangedEvent{"2", "oldState", "newState"},
|
DeviceStateChangedEvent{"2", StateOnline, StateOffline},
|
||||||
}, diffs))
|
}, diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculateStateDiffsOneAddedOneRemovedOneChanged(t *testing.T) {
|
func TestCalculateStateDiffsOneAddedOneRemovedOneChanged(t *testing.T) {
|
||||||
oldStates := map[string]string{
|
oldStates := map[string]DeviceState{
|
||||||
"1": "oldState",
|
"1": StateOffline,
|
||||||
"2": "removed",
|
"2": StateOffline,
|
||||||
}
|
}
|
||||||
newStates := map[string]string{
|
newStates := map[string]DeviceState{
|
||||||
"1": "newState",
|
"1": StateOnline,
|
||||||
"3": "added",
|
"3": StateOffline,
|
||||||
}
|
}
|
||||||
|
|
||||||
diffs := calculateStateDiffs(oldStates, newStates)
|
diffs := calculateStateDiffs(oldStates, newStates)
|
||||||
|
|
||||||
assert.True(t, reflect.DeepEqual([]DeviceStateChangedEvent{
|
assertContainsOnly(t, []DeviceStateChangedEvent{
|
||||||
DeviceStateChangedEvent{"1", "oldState", "newState"},
|
DeviceStateChangedEvent{"1", StateOffline, StateOnline},
|
||||||
DeviceStateChangedEvent{"2", "removed", ""},
|
DeviceStateChangedEvent{"2", StateOffline, StateDisconnected},
|
||||||
DeviceStateChangedEvent{"3", "", "added"},
|
DeviceStateChangedEvent{"3", StateDisconnected, StateOffline},
|
||||||
}, diffs))
|
}, diffs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCameOnline(t *testing.T) {
|
||||||
|
assert.True(t, DeviceStateChangedEvent{"", StateDisconnected, StateOnline}.CameOnline())
|
||||||
|
assert.True(t, DeviceStateChangedEvent{"", StateOffline, StateOnline}.CameOnline())
|
||||||
|
assert.False(t, DeviceStateChangedEvent{"", StateOnline, StateOffline}.CameOnline())
|
||||||
|
assert.False(t, DeviceStateChangedEvent{"", StateOnline, StateDisconnected}.CameOnline())
|
||||||
|
assert.False(t, DeviceStateChangedEvent{"", StateOffline, StateDisconnected}.CameOnline())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWentOffline(t *testing.T) {
|
||||||
|
assert.True(t, DeviceStateChangedEvent{"", StateOnline, StateDisconnected}.WentOffline())
|
||||||
|
assert.True(t, DeviceStateChangedEvent{"", StateOnline, StateOffline}.WentOffline())
|
||||||
|
assert.False(t, DeviceStateChangedEvent{"", StateOffline, StateOnline}.WentOffline())
|
||||||
|
assert.False(t, DeviceStateChangedEvent{"", StateDisconnected, StateOnline}.WentOffline())
|
||||||
|
assert.False(t, DeviceStateChangedEvent{"", StateOffline, StateDisconnected}.WentOffline())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublishDevicesRestartsServer(t *testing.T) {
|
func TestPublishDevicesRestartsServer(t *testing.T) {
|
||||||
|
@ -231,3 +246,19 @@ func (s *MockServerStarter) StartServer() error {
|
||||||
return s.err
|
return s.err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertContainsOnly(t *testing.T, expected, actual []DeviceStateChangedEvent) {
|
||||||
|
assert.Len(t, actual, len(expected))
|
||||||
|
for _, expectedEntry := range expected {
|
||||||
|
assertContains(t, expectedEntry, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertContains(t *testing.T, expectedEntry DeviceStateChangedEvent, actual []DeviceStateChangedEvent) {
|
||||||
|
for _, actualEntry := range actual {
|
||||||
|
if expectedEntry == actualEntry {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Fail(t, "expected to find %+v in %+v", expectedEntry, actual)
|
||||||
|
}
|
||||||
|
|
16
devicestate_string.go
Normal file
16
devicestate_string.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// generated by stringer -type=DeviceState; DO NOT EDIT
|
||||||
|
|
||||||
|
package goadb
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const _DeviceState_name = "StateDisconnectedStateOfflineStateOnline"
|
||||||
|
|
||||||
|
var _DeviceState_index = [...]uint8{0, 17, 29, 40}
|
||||||
|
|
||||||
|
func (i DeviceState) String() string {
|
||||||
|
if i < 0 || i+1 >= DeviceState(len(_DeviceState_index)) {
|
||||||
|
return fmt.Sprintf("DeviceState(%d)", i)
|
||||||
|
}
|
||||||
|
return _DeviceState_name[_DeviceState_index[i]:_DeviceState_index[i+1]]
|
||||||
|
}
|
Loading…
Reference in a new issue