exsys 0.6 → 1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +161 -13
- data/bin/exsys-usb +78 -7
- data/lib/exsys/discovery.rb +314 -0
- data/lib/exsys/managed-usb.rb +328 -104
- data/lib/exsys/version.rb +1 -1
- data/lib/exsys.rb +1 -0
- data/test/support/fake_hub.rb +52 -26
- data/test/support/uart.rb +23 -6
- data/test/test_discovery.rb +241 -0
- data/test/test_exsys_usb.rb +100 -5
- data/test/test_managed_usb.rb +268 -18
- metadata +3 -1
data/test/support/fake_hub.rb
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
class FakeHub
|
|
8
8
|
DEFAULT_PASSWORD = 'pass'.ljust(8)
|
|
9
9
|
|
|
10
|
-
attr_reader :log, :opens, :locks, :line, :speed, :mode
|
|
11
|
-
attr_accessor :password, :silent, :lockable, :garbage
|
|
10
|
+
attr_reader :log, :opens, :locks, :line, :speed, :mode, :lock_path
|
|
11
|
+
attr_accessor :password, :silent, :lockable, :garbage, :lock_error,
|
|
12
|
+
:ident
|
|
12
13
|
|
|
13
14
|
# @param state [Integer] initial port bitmap
|
|
14
15
|
# @param path [String] file backing the state, so that separate
|
|
@@ -18,13 +19,19 @@ class FakeHub
|
|
|
18
19
|
# @param delay [Float] pause inside a write, widening the
|
|
19
20
|
# window a concurrent process could slip
|
|
20
21
|
# into
|
|
22
|
+
# @param ports [Integer] how many ports this hub has
|
|
23
|
+
# @param width [Integer] hex digits in its state word. Real hubs
|
|
24
|
+
# answer wider than their port count: a
|
|
25
|
+
# 16-port unit answers eight, and the ports
|
|
26
|
+
# it does not have read as 1.
|
|
21
27
|
def initialize(password: DEFAULT_PASSWORD, state: 0x0000,
|
|
28
|
+
ports: 16, width: 8,
|
|
22
29
|
path: nil, lock: nil, delay: 0)
|
|
23
30
|
@password = password
|
|
24
31
|
@state = state
|
|
25
32
|
@flash = state
|
|
26
33
|
@path = path
|
|
27
|
-
@
|
|
34
|
+
@lock_path = lock
|
|
28
35
|
@delay = delay
|
|
29
36
|
@log = []
|
|
30
37
|
@opens = 0
|
|
@@ -32,6 +39,10 @@ class FakeHub
|
|
|
32
39
|
@silent = false # hub answers nothing at all
|
|
33
40
|
@lockable = true # platform allows locking the line
|
|
34
41
|
@garbage = nil # hub answers this instead, when set
|
|
42
|
+
@lock_error = nil # raised by flock; for the propagation test
|
|
43
|
+
@ports = ports
|
|
44
|
+
@width = width
|
|
45
|
+
@ident = format('CENTOS0005%02dv02', ports) # nil = refuse ?Q
|
|
35
46
|
|
|
36
47
|
# Attach to the hub the file already describes, so that a test
|
|
37
48
|
# can inspect what its subprocesses did; seed it otherwise.
|
|
@@ -57,9 +68,20 @@ class FakeHub
|
|
|
57
68
|
# Ports currently powered, as a sorted list -- the oracle the tests
|
|
58
69
|
# compare against.
|
|
59
70
|
def ports_on
|
|
60
|
-
1.upto(
|
|
71
|
+
1.upto(@ports).select {|p| state & (1 << (p-1)) != 0 }
|
|
61
72
|
end
|
|
62
73
|
|
|
74
|
+
# The power-on state the hub would come back to.
|
|
75
|
+
def flash_ports
|
|
76
|
+
1.upto(@ports).select {|p| flash & (1 << (p-1)) != 0 }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Bits for ports this hub does not have. A real one reports them
|
|
80
|
+
# set, which is why a client must write back what it read.
|
|
81
|
+
def absent = ((1 << (@width * 4)) - 1) & ~real
|
|
82
|
+
|
|
83
|
+
def real = (1 << @ports) - 1
|
|
84
|
+
|
|
63
85
|
# Record how the line was opened, so a test can check the library
|
|
64
86
|
# asks for the device and the speed the hub actually needs.
|
|
65
87
|
def opened(line, speed, mode)
|
|
@@ -67,18 +89,13 @@ class FakeHub
|
|
|
67
89
|
@opens += 1
|
|
68
90
|
end
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
# Called by each opened line before it locks. The lock handle
|
|
93
|
+
# itself belongs to the line, not to the hub, so that two threads
|
|
94
|
+
# hold two of them and genuinely contend.
|
|
95
|
+
def lock_attempted
|
|
96
|
+
raise @lock_error if @lock_error # must NOT be swallowed
|
|
97
|
+
raise Errno::ENOTSUP unless @lockable # platform refusal
|
|
72
98
|
@locks += 1
|
|
73
|
-
return 0 if @lock.nil?
|
|
74
|
-
|
|
75
|
-
@lockfh = File.open(@lock, File::RDWR|File::CREAT, 0o600)
|
|
76
|
-
@lockfh.flock(mode)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def unlock
|
|
80
|
-
@lockfh&.close
|
|
81
|
-
@lockfh = nil
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
# Answer one command, as the hub would.
|
|
@@ -99,9 +116,10 @@ class FakeHub
|
|
|
99
116
|
code = cmd[0, 2]
|
|
100
117
|
args = cmd[2..].to_s
|
|
101
118
|
|
|
102
|
-
# GP
|
|
103
|
-
# bare
|
|
104
|
-
return encode(@state
|
|
119
|
+
# GP and ?Q are the two commands the hub answers without a
|
|
120
|
+
# password, both with a bare payload rather than a G/E status.
|
|
121
|
+
return encode(@state | absent) if code == 'GP'
|
|
122
|
+
return @ident || 'E01' if code == '?Q'
|
|
105
123
|
|
|
106
124
|
return 'E01' unless args.start_with?(@password)
|
|
107
125
|
rest = args[@password.size..]
|
|
@@ -109,21 +127,29 @@ class FakeHub
|
|
|
109
127
|
sleep @delay if @delay.positive?
|
|
110
128
|
|
|
111
129
|
case code
|
|
112
|
-
when 'SP' then @state = decode(rest)
|
|
113
|
-
when 'FP' then @state = @flash = decode(rest)
|
|
130
|
+
when 'SP' then @state = decode(rest) & real ; 'G'
|
|
131
|
+
when 'FP' then @state = @flash = decode(rest) & real ; 'G'
|
|
114
132
|
when 'WP' then @flash = @state ; 'G'
|
|
115
|
-
when 'RD' then @state = @flash
|
|
133
|
+
when 'RD' then @state = @flash = 0
|
|
134
|
+
@password = DEFAULT_PASSWORD ; 'G'
|
|
116
135
|
when 'RH' then @state = @flash ; nil
|
|
117
136
|
when 'CP' then @password = rest ; 'G'
|
|
118
137
|
else 'E02'
|
|
119
138
|
end
|
|
120
139
|
end
|
|
121
140
|
|
|
122
|
-
# Little-endian
|
|
123
|
-
# because that is what a real hub answers -- observed on an
|
|
124
|
-
# 16-port unit, which replies to GP with
|
|
125
|
-
def encode(v)
|
|
126
|
-
|
|
141
|
+
# Little-endian, any width, spelled out rather than packed. Upper
|
|
142
|
+
# case because that is what a real hub answers -- observed on an
|
|
143
|
+
# ExSYS 16-port unit, which replies to GP with "C4FFFFFF".
|
|
144
|
+
def encode(v)
|
|
145
|
+
(@width / 2).times.map {|i| format('%02X', (v >> (8 * i)) & 0xff) }
|
|
146
|
+
.join
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def decode(str)
|
|
150
|
+
str.scan(/\h\h/).each_with_index
|
|
151
|
+
.sum {|byte, i| byte.to_i(16) << (8 * i) }
|
|
152
|
+
end
|
|
127
153
|
|
|
128
154
|
def load
|
|
129
155
|
return if @path.nil? || !File.exist?(@path)
|
data/test/support/uart.rb
CHANGED
|
@@ -18,12 +18,29 @@ module UART
|
|
|
18
18
|
def reset! = @hub = nil
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Stands in for the File that UART.open normally yields.
|
|
21
|
+
# Stands in for the File that UART.open normally yields. Each one
|
|
22
|
+
# carries its own lock handle, as a real open would, so that two of
|
|
23
|
+
# them contend instead of sharing one.
|
|
22
24
|
class Serial
|
|
23
|
-
def initialize(hub)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
def initialize(hub)
|
|
26
|
+
@hub = hub
|
|
27
|
+
@lock = hub.lock_path &&
|
|
28
|
+
File.open(hub.lock_path, File::RDWR|File::CREAT, 0o600)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def flock(mode)
|
|
32
|
+
@hub.lock_attempted
|
|
33
|
+
@lock ? @lock.flock(mode) : 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def write(cmd) = @reply = @hub.command(cmd.chomp("\r"))
|
|
37
|
+
|
|
38
|
+
# A real hub ends every reply CR LF -- seen in the hex off the
|
|
39
|
+
# wire -- so a read to the terminator takes the whole line.
|
|
40
|
+
def gets(_sep = "\n")
|
|
41
|
+
@reply.nil? ? nil : "#{@reply}\r\n"
|
|
42
|
+
end
|
|
43
|
+
def close = @lock&.close
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def self.open(line, speed = 9600, mode = '8N1')
|
|
@@ -34,7 +51,7 @@ module UART
|
|
|
34
51
|
begin
|
|
35
52
|
yield serial
|
|
36
53
|
ensure
|
|
37
|
-
|
|
54
|
+
serial.close
|
|
38
55
|
end
|
|
39
56
|
end
|
|
40
57
|
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
require_relative 'helper'
|
|
2
|
+
|
|
3
|
+
# Finding the hub's serial line, per platform.
|
|
4
|
+
#
|
|
5
|
+
# The reading and the parsing are kept apart in Discovery precisely so
|
|
6
|
+
# that this runs on a machine with nothing attached: what is asserted
|
|
7
|
+
# here is the parsing, against the output the two tools produce, and
|
|
8
|
+
# not that this host has a hub on it.
|
|
9
|
+
class TestDiscovery < Minitest::Test
|
|
10
|
+
D = ExSYS::ManagedUSB::Discovery
|
|
11
|
+
|
|
12
|
+
# sysctl -e dev.uftdi dev.uhub.
|
|
13
|
+
#
|
|
14
|
+
# The uhub half is captured verbatim from a real FreeBSD host --
|
|
15
|
+
# two controllers, a Genesys hub on one and a chain of two TI hubs
|
|
16
|
+
# on the other -- because the walk that builds a USB path out of it
|
|
17
|
+
# is the part worth testing against something nobody invented.
|
|
18
|
+
#
|
|
19
|
+
# The uftdi half is written: three adapters, one hanging off the
|
|
20
|
+
# deepest hub, one whose EEPROM carries no serial, and an FT230X,
|
|
21
|
+
# which uftdi drives too and which is not what is wanted.
|
|
22
|
+
UHUB = <<~'SYSCTL'
|
|
23
|
+
dev.uhub.%parent=
|
|
24
|
+
dev.uhub.0.%location=
|
|
25
|
+
dev.uhub.0.%parent=usbus1
|
|
26
|
+
dev.uhub.1.%location=
|
|
27
|
+
dev.uhub.1.%parent=usbus0
|
|
28
|
+
dev.uhub.2.%location=bus=1 hubaddr=1 port=1 devaddr=3 interface=0 ugen=ugen1.3
|
|
29
|
+
dev.uhub.2.%parent=uhub0
|
|
30
|
+
dev.uhub.3.%location=bus=0 hubaddr=1 port=2 devaddr=2 interface=0 ugen=ugen0.2
|
|
31
|
+
dev.uhub.3.%parent=uhub1
|
|
32
|
+
dev.uhub.4.%location=bus=1 hubaddr=3 port=1 devaddr=4 interface=0 ugen=ugen1.4
|
|
33
|
+
dev.uhub.4.%parent=uhub2
|
|
34
|
+
dev.uhub.5.%location=bus=1 hubaddr=4 port=4 devaddr=5 interface=0 ugen=ugen1.5
|
|
35
|
+
dev.uhub.5.%parent=uhub4
|
|
36
|
+
SYSCTL
|
|
37
|
+
|
|
38
|
+
UFTDI = <<~'SYSCTL'
|
|
39
|
+
dev.uftdi.0.%desc=FTDI FT232R USB UART
|
|
40
|
+
dev.uftdi.0.%driver=uftdi
|
|
41
|
+
dev.uftdi.0.%location=bus=1 hubaddr=5 port=4 devaddr=9 interface=0 ugen=ugen1.9
|
|
42
|
+
dev.uftdi.0.%pnpinfo=vendor=0x0403 product=0x6001 devclass=0x00 devsubclass=0x00 devproto=0x00 sernum="AL03GD7X" release=0x0600 mode=host intclass=0xff intsubclass=0xff intprotocol=0xff
|
|
43
|
+
dev.uftdi.0.%parent=uhub5
|
|
44
|
+
dev.uftdi.0.ttyname=U0
|
|
45
|
+
dev.uftdi.0.ttyports=1
|
|
46
|
+
dev.uftdi.1.%desc=FTDI FT232R USB UART
|
|
47
|
+
dev.uftdi.1.%driver=uftdi
|
|
48
|
+
dev.uftdi.1.%location=bus=0 hubaddr=2 port=3 devaddr=7 interface=0 ugen=ugen0.7
|
|
49
|
+
dev.uftdi.1.%pnpinfo=vendor=0x0403 product=0x6001 devclass=0x00 devsubclass=0x00 devproto=0x00 sernum="" release=0x0600 mode=host intclass=0xff intsubclass=0xff intprotocol=0xff
|
|
50
|
+
dev.uftdi.1.%parent=uhub3
|
|
51
|
+
dev.uftdi.1.ttyname=U1
|
|
52
|
+
dev.uftdi.1.ttyports=1
|
|
53
|
+
dev.uftdi.2.%desc=FTDI FT230X Basic UART
|
|
54
|
+
dev.uftdi.2.%driver=uftdi
|
|
55
|
+
dev.uftdi.2.%pnpinfo=vendor=0x0403 product=0x6015 devclass=0x00 devsubclass=0x00 devproto=0x00 sernum="DT04H6789" release=0x1000 mode=host intclass=0xff intsubclass=0xff intprotocol=0xff
|
|
56
|
+
dev.uftdi.2.%parent=uhub3
|
|
57
|
+
dev.uftdi.2.ttyname=U2
|
|
58
|
+
dev.uftdi.2.ttyports=1
|
|
59
|
+
SYSCTL
|
|
60
|
+
|
|
61
|
+
# udevadm info -q property --export, for one FT232R.
|
|
62
|
+
UDEVADM = <<~'UDEV'
|
|
63
|
+
DEVNAME='/dev/ttyUSB0'
|
|
64
|
+
DEVPATH='/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.2/1-1.2.4/1-1.2.4.4/1-1.2.4.4:1.0/ttyUSB0/tty/ttyUSB0'
|
|
65
|
+
ID_BUS='usb'
|
|
66
|
+
ID_MODEL='FT232R_USB_UART'
|
|
67
|
+
ID_MODEL_ID='6001'
|
|
68
|
+
ID_SERIAL_SHORT='A50285BI'
|
|
69
|
+
ID_VENDOR_ID='0403'
|
|
70
|
+
MAJOR='188'
|
|
71
|
+
SUBSYSTEM='tty'
|
|
72
|
+
UDEV
|
|
73
|
+
|
|
74
|
+
def freebsd(text = UFTDI + UHUB)
|
|
75
|
+
tree = D::FreeBSD.parse(text)
|
|
76
|
+
tree.filter_map {|name, dev|
|
|
77
|
+
next unless name.start_with?('uftdi')
|
|
78
|
+
D::FreeBSD.candidate(dev, tree)
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_freebsd_reports_the_line_the_serial_and_the_path
|
|
83
|
+
assert_equal({ :device => '/dev/ttyU0',
|
|
84
|
+
:serial => 'AL03GD7X',
|
|
85
|
+
:usb_path => '1-1.1.4.4' }, freebsd.first)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Nothing on FreeBSD states a path: it is walked, %parent by
|
|
89
|
+
# %parent, and the ports collected on the way up. uftdi0 sits on
|
|
90
|
+
# port 4 of uhub5, which is on port 4 of uhub4, which is on port 1
|
|
91
|
+
# of uhub2, which is on port 1 of the root hub of bus 1.
|
|
92
|
+
def test_the_path_is_walked_up_to_the_root_hub
|
|
93
|
+
tree = D::FreeBSD.parse(UFTDI + UHUB)
|
|
94
|
+
assert_equal '1-1.1.4', D::FreeBSD.usb_path(tree['uhub5'], tree)
|
|
95
|
+
assert_equal '1-1.1', D::FreeBSD.usb_path(tree['uhub4'], tree)
|
|
96
|
+
assert_equal '0-2', D::FreeBSD.usb_path(tree['uhub3'], tree)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A root hub occupies no port on anything: its %location is empty.
|
|
100
|
+
def test_a_root_hub_has_no_path_of_its_own
|
|
101
|
+
tree = D::FreeBSD.parse(UFTDI + UHUB)
|
|
102
|
+
assert_nil D::FreeBSD.usb_path(tree['uhub0'], tree)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Without the hubs there is nothing to walk. The half-built
|
|
106
|
+
# answer is the dangerous one: the adapter's own port alone reads
|
|
107
|
+
# as a whole path -- 1-4 rather than 1-1.1.4.4 -- and names a real
|
|
108
|
+
# socket that is not this one.
|
|
109
|
+
def test_a_walk_that_never_reaches_the_root_is_nil
|
|
110
|
+
assert_nil freebsd(UFTDI).first[:usb_path]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# An unprogrammed EEPROM answers sernum="", and nothing may be
|
|
114
|
+
# identified by an empty string.
|
|
115
|
+
# The case the path exists for: no serial to be named by, and the
|
|
116
|
+
# socket is then the only stable name it has.
|
|
117
|
+
def test_an_empty_serial_is_nil_and_the_path_is_still_there
|
|
118
|
+
assert_equal({ :device => '/dev/ttyU1',
|
|
119
|
+
:serial => nil,
|
|
120
|
+
:usb_path => '0-2.3' }, freebsd[1])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# uftdi drives every FTDI part, not only the one the hub uses.
|
|
124
|
+
def test_another_ftdi_part_is_not_a_candidate
|
|
125
|
+
refute_includes freebsd.map {|c| c[:device] }, '/dev/ttyU2'
|
|
126
|
+
assert_equal 2, freebsd.size
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# dev.uhub.%parent has no unit number in it and must not become a
|
|
130
|
+
# device. Two branches are read at once, so the key is the name.
|
|
131
|
+
def test_the_driver_wide_key_is_not_taken_for_a_device
|
|
132
|
+
keys = D::FreeBSD.parse(UFTDI + UHUB).keys
|
|
133
|
+
assert_includes keys, 'uftdi0'
|
|
134
|
+
assert_includes keys, 'uhub5'
|
|
135
|
+
refute_includes keys, 'uhub'
|
|
136
|
+
assert_equal 9, keys.size
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Not every token in a %pnpinfo is a key=value pair --
|
|
140
|
+
# dev.acpi_timer.0.%pnpinfo is the bare word 'unknown' -- and a
|
|
141
|
+
# parser that died on one driver's wording would take the hub
|
|
142
|
+
# with it.
|
|
143
|
+
def test_a_field_that_is_not_a_pair_does_not_stop_the_parse
|
|
144
|
+
tree = D::FreeBSD.parse(<<~'SYSCTL')
|
|
145
|
+
dev.uftdi.0.%pnpinfo=unknown vendor=0x0403 product=0x6001 sernum="AL03GD7X"
|
|
146
|
+
dev.uftdi.0.ttyname=U0
|
|
147
|
+
SYSCTL
|
|
148
|
+
assert_equal '0x0403', tree['uftdi0'][:'%pnpinfo'][:vendor]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_nothing_attached_is_an_empty_list_not_an_error
|
|
152
|
+
assert_empty freebsd('')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_linux_reports_the_line_the_serial_and_the_path
|
|
156
|
+
assert_equal({ :device => '/dev/ttyUSB0',
|
|
157
|
+
:serial => 'A50285BI',
|
|
158
|
+
:usb_path => '1-1.2.4.4' },
|
|
159
|
+
D::Linux.candidate(D::Linux.parse(UDEVADM)))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Every hub on the way to the device matches the shape too, so it
|
|
163
|
+
# is the last match that is the adapter -- not 1-1, not 1-1.2.
|
|
164
|
+
def test_the_usb_path_is_the_device_not_a_hub_above_it
|
|
165
|
+
assert_equal '1-1.2.4.4', D::Linux.usb_path(
|
|
166
|
+
'/devices/pci0000:00/usb1/1-1/1-1.2/1-1.2.4/1-1.2.4.4/' \
|
|
167
|
+
'1-1.2.4.4:1.0/ttyUSB0/tty/ttyUSB0')
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_a_devpath_with_no_usb_component_has_no_path
|
|
171
|
+
assert_nil D::Linux.usb_path('/devices/platform/serial8250/ttyS0')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The published shape, so that a caller can tell a path typed by a
|
|
175
|
+
# human from a serial number.
|
|
176
|
+
def test_what_a_usb_path_looks_like
|
|
177
|
+
assert_match ExSYS::ManagedUSB::USB_PATH, '1-1.2.4.4'
|
|
178
|
+
assert_match ExSYS::ManagedUSB::USB_PATH, '2-3'
|
|
179
|
+
refute_match ExSYS::ManagedUSB::USB_PATH, 'AL03GD7X'
|
|
180
|
+
refute_match ExSYS::ManagedUSB::USB_PATH, '1-1.2.4.4:1.0'
|
|
181
|
+
refute_match ExSYS::ManagedUSB::USB_PATH, '/dev/ttyUSB0'
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def test_linux_skips_an_adapter_that_is_not_the_right_part
|
|
185
|
+
props = D::Linux.parse(UDEVADM.sub("'6001'", "'6015'"))
|
|
186
|
+
assert_nil D::Linux.candidate(props)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_linux_unquotes_the_export_format
|
|
190
|
+
assert_equal '/dev/ttyUSB0', D::Linux.parse(UDEVADM)[:DEVNAME]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The public name delegates, so that a caller has one thing to call.
|
|
194
|
+
def test_available_is_what_the_platform_answered
|
|
195
|
+
found = [ { :device => '/dev/ttyU0', :serial => 'AL03GD7X',
|
|
196
|
+
:usb_path => nil } ]
|
|
197
|
+
D.stub(:available, found) do
|
|
198
|
+
assert_equal found, ExSYS::ManagedUSB.available
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A platform with no way to look says so. An empty list would read
|
|
203
|
+
# as "no hub attached", which is a different thing and a lie.
|
|
204
|
+
def test_a_platform_we_cannot_look_on_is_an_error
|
|
205
|
+
RbConfig::CONFIG.stub(:[], 'solaris2.11') do
|
|
206
|
+
e = assert_raises(ExSYS::ManagedUSB::Error) { D.available }
|
|
207
|
+
assert_match(/no hub discovery for this platform/, e.message)
|
|
208
|
+
assert_match(/solaris2\.11/, e.message)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_a_missing_tool_is_an_error_too
|
|
213
|
+
e = assert_raises(ExSYS::ManagedUSB::Error) {
|
|
214
|
+
D.missing('/sbin/sysctl', Errno::ENOENT.new('/sbin/sysctl'))
|
|
215
|
+
}
|
|
216
|
+
assert_match(%r{cannot look for a hub: /sbin/sysctl}, e.message)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Regression, and the reason Discovery.run exists. This reader
|
|
220
|
+
# once ran `sysctl ... 2>/dev/null` in a backtick; the redirection
|
|
221
|
+
# made Ruby hand the string to /bin/sh, which reports a missing
|
|
222
|
+
# binary itself as exit 127 with no output, so Errno::ENOENT never
|
|
223
|
+
# reached the rescue and a host with no sysctl answered "no hub
|
|
224
|
+
# attached" -- the exact lie the comment there warns against.
|
|
225
|
+
#
|
|
226
|
+
# The status cannot stand in for it either: sysctl exits 1 for an
|
|
227
|
+
# unknown oid, which is the legitimate empty answer.
|
|
228
|
+
def test_a_reader_that_is_not_installed_raises_rather_than_answering_none
|
|
229
|
+
e = assert_raises(ExSYS::ManagedUSB::Error) {
|
|
230
|
+
D.run('/nonexistent/sysctl', '-e', 'dev.uftdi')
|
|
231
|
+
}
|
|
232
|
+
assert_match(%r{cannot look for a hub: /nonexistent/sysctl},
|
|
233
|
+
e.message)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# ... and a reader that IS installed, asked for an oid this host
|
|
237
|
+
# has not got, answers nothing at all rather than raising.
|
|
238
|
+
def test_an_oid_that_does_not_exist_is_an_empty_answer
|
|
239
|
+
assert_empty D.run('/usr/bin/env', 'true')
|
|
240
|
+
end
|
|
241
|
+
end
|
data/test/test_exsys_usb.rb
CHANGED
|
@@ -50,19 +50,104 @@ class TestExsysUsb < Minitest::Test
|
|
|
50
50
|
assert_equal [ 3 ], hub.ports_on
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def
|
|
53
|
+
def test_commit_saves_the_power_on_state
|
|
54
54
|
exsys_usb('on', '1')
|
|
55
55
|
exsys_usb('commit')
|
|
56
56
|
exsys_usb('on', '2')
|
|
57
|
-
|
|
58
|
-
assert_equal [ 1 ],
|
|
57
|
+
assert_equal [ 1, 2 ], hub.ports_on
|
|
58
|
+
assert_equal [ 1 ], hub.flash_ports
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def test_commit_flag_writes_through
|
|
62
62
|
exsys_usb('-c', 'on', '4')
|
|
63
63
|
exsys_usb('on', '5')
|
|
64
|
-
|
|
65
|
-
assert_equal [ 4 ],
|
|
64
|
+
assert_equal [ 4, 5 ], hub.ports_on
|
|
65
|
+
assert_equal [ 4 ], hub.flash_ports
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# restore issues RD, the hub's factory reset -- not the inverse of
|
|
69
|
+
# commit. Nothing in the protocol reloads the flashed state.
|
|
70
|
+
def test_restore_is_a_factory_reset
|
|
71
|
+
exsys_usb('on', '1')
|
|
72
|
+
exsys_usb('commit')
|
|
73
|
+
_, err, st = exsys_usb('--yes', 'factory-reset')
|
|
74
|
+
assert_equal 0, st.exitstatus, err
|
|
75
|
+
assert_empty hub.ports_on
|
|
76
|
+
assert_empty hub.flash_ports
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
## Reading the hub ##################################################
|
|
80
|
+
|
|
81
|
+
def test_status_lists_every_port
|
|
82
|
+
exsys_usb('on', '1', '3')
|
|
83
|
+
out, _, st = exsys_usb('status')
|
|
84
|
+
assert_equal 0, st.exitstatus
|
|
85
|
+
assert_equal 16, out.lines.size
|
|
86
|
+
assert_includes out.lines, "1 on\n"
|
|
87
|
+
assert_includes out.lines, "2 off\n"
|
|
88
|
+
assert_includes out.lines, "3 on\n"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_status_can_be_asked_about_named_ports
|
|
92
|
+
exsys_usb('on', '3')
|
|
93
|
+
out, _, = exsys_usb('status', '3', '4')
|
|
94
|
+
assert_equal "3 on\n4 off\n", out
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_status_refuses_a_port_the_hub_does_not_have
|
|
98
|
+
_, err, st = exsys_usb('status', '99')
|
|
99
|
+
assert_equal 1, st.exitstatus
|
|
100
|
+
assert_match(/invalid port: 99/, err)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_query_reports_what_the_hub_says_it_is
|
|
104
|
+
out, _, st = exsys_usb('query')
|
|
105
|
+
assert_equal 0, st.exitstatus
|
|
106
|
+
assert_match(/^id:\s+CENTOS$/, out)
|
|
107
|
+
assert_match(/^ports:\s+16$/, out)
|
|
108
|
+
assert_match(/^firmware:\s+v02$/, out)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
## Verbose ##########################################################
|
|
112
|
+
|
|
113
|
+
def test_verbose_reports_the_state_after_a_change
|
|
114
|
+
out, _, = exsys_usb('-v', 'on', '2')
|
|
115
|
+
assert_includes out.lines, "2 on\n"
|
|
116
|
+
assert_equal 16, out.lines.size
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_without_verbose_a_change_says_nothing
|
|
120
|
+
out, _, st = exsys_usb('on', '2')
|
|
121
|
+
assert_equal 0, st.exitstatus
|
|
122
|
+
assert_empty out
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
## Confirming the irreversible ######################################
|
|
126
|
+
|
|
127
|
+
def test_reset_refuses_without_yes
|
|
128
|
+
_, err, st = exsys_usb('reset')
|
|
129
|
+
assert_equal 1, st.exitstatus
|
|
130
|
+
assert_match(/--yes/, err)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_reset_runs_when_meant
|
|
134
|
+
_, err, st = exsys_usb('--yes', 'reset')
|
|
135
|
+
assert_equal 0, st.exitstatus, err
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_factory_reset_refuses_without_yes
|
|
139
|
+
exsys_usb('on', '1')
|
|
140
|
+
_, err, st = exsys_usb('factory-reset')
|
|
141
|
+
assert_equal 1, st.exitstatus
|
|
142
|
+
assert_match(/--yes/, err)
|
|
143
|
+
assert_equal [ 1 ], hub.ports_on, 'the hub must not have been touched'
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_factory_reset_runs_when_meant
|
|
147
|
+
exsys_usb('on', '1')
|
|
148
|
+
_, err, st = exsys_usb('--yes', 'factory-reset')
|
|
149
|
+
assert_equal 0, st.exitstatus, err
|
|
150
|
+
assert_empty hub.ports_on
|
|
66
151
|
end
|
|
67
152
|
|
|
68
153
|
## Exit status -- regression. Every failure used to be printed and
|
|
@@ -220,6 +305,16 @@ class TestExsysUsb < Minitest::Test
|
|
|
220
305
|
assert_equal [ 1, 2 ], hub.ports_on
|
|
221
306
|
end
|
|
222
307
|
|
|
308
|
+
# Typing the old name must not silently do nothing, nor quietly do
|
|
309
|
+
# the reset: it must say what the command actually is.
|
|
310
|
+
def test_the_old_restore_action_explains_itself
|
|
311
|
+
_, err, st = exsys_usb('restore')
|
|
312
|
+
assert_equal 1, st.exitstatus
|
|
313
|
+
assert_match(/renamed factory-reset/, err)
|
|
314
|
+
assert_match(/factory defaults/, err)
|
|
315
|
+
assert_equal [], hub.ports_on
|
|
316
|
+
end
|
|
317
|
+
|
|
223
318
|
## Packaging #########################################################
|
|
224
319
|
|
|
225
320
|
# Regression: the file carried a shebang but no execute bit, so a
|