exsys 0.5 → 1.0
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 +211 -19
- data/Rakefile +19 -0
- data/bin/exsys-usb +77 -20
- data/exsys.gemspec +4 -2
- data/lib/exsys/managed-usb.rb +399 -79
- data/lib/exsys/version.rb +1 -1
- data/test/helper.rb +60 -0
- data/test/support/fake_hub.rb +165 -0
- data/test/support/uart.rb +57 -0
- data/test/test_exsys_usb.rb +325 -0
- data/test/test_managed_usb.rb +541 -0
- data/test/test_readme.rb +61 -0
- metadata +24 -6
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# A model of the ExSYS managed hub's serial protocol, faithful enough
|
|
2
|
+
# to drive ExSYS::ManagedUSB end to end without hardware.
|
|
3
|
+
#
|
|
4
|
+
# The wire encoding here is written out independently of the library's
|
|
5
|
+
# own pack/unpack, so that a disagreement between the two shows up as a
|
|
6
|
+
# test failure rather than both sharing the same mistake.
|
|
7
|
+
class FakeHub
|
|
8
|
+
DEFAULT_PASSWORD = 'pass'.ljust(8)
|
|
9
|
+
|
|
10
|
+
attr_reader :log, :opens, :locks, :line, :speed, :mode, :lock_path
|
|
11
|
+
attr_accessor :password, :silent, :lockable, :garbage, :lock_error,
|
|
12
|
+
:ident
|
|
13
|
+
|
|
14
|
+
# @param state [Integer] initial port bitmap
|
|
15
|
+
# @param path [String] file backing the state, so that separate
|
|
16
|
+
# processes share one hub
|
|
17
|
+
# @param lock [String] file used for a real flock, so that the
|
|
18
|
+
# locking is genuinely exercised
|
|
19
|
+
# @param delay [Float] pause inside a write, widening the
|
|
20
|
+
# window a concurrent process could slip
|
|
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.
|
|
27
|
+
def initialize(password: DEFAULT_PASSWORD, state: 0x0000,
|
|
28
|
+
ports: 16, width: 8,
|
|
29
|
+
path: nil, lock: nil, delay: 0)
|
|
30
|
+
@password = password
|
|
31
|
+
@state = state
|
|
32
|
+
@flash = state
|
|
33
|
+
@path = path
|
|
34
|
+
@lock_path = lock
|
|
35
|
+
@delay = delay
|
|
36
|
+
@log = []
|
|
37
|
+
@opens = 0
|
|
38
|
+
@locks = 0
|
|
39
|
+
@silent = false # hub answers nothing at all
|
|
40
|
+
@lockable = true # platform allows locking the line
|
|
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
|
|
46
|
+
|
|
47
|
+
# Attach to the hub the file already describes, so that a test
|
|
48
|
+
# can inspect what its subprocesses did; seed it otherwise.
|
|
49
|
+
if @path && File.exist?(@path) then load
|
|
50
|
+
else save
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Build from the environment, for the executable's subprocesses.
|
|
55
|
+
def self.from_env(env = ENV)
|
|
56
|
+
new(path: env['EXSYS_TEST_HUB'],
|
|
57
|
+
lock: env['EXSYS_TEST_LOCK'],
|
|
58
|
+
delay: env['EXSYS_TEST_DELAY'].to_f).tap do |hub|
|
|
59
|
+
hub.silent = !env['EXSYS_TEST_SILENT'].nil?
|
|
60
|
+
hub.lockable = env['EXSYS_TEST_NOLOCK'].nil?
|
|
61
|
+
hub.garbage = env['EXSYS_TEST_GARBAGE']
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def state = (load; @state)
|
|
66
|
+
def flash = (load; @flash)
|
|
67
|
+
|
|
68
|
+
# Ports currently powered, as a sorted list -- the oracle the tests
|
|
69
|
+
# compare against.
|
|
70
|
+
def ports_on
|
|
71
|
+
1.upto(@ports).select {|p| state & (1 << (p-1)) != 0 }
|
|
72
|
+
end
|
|
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
|
+
|
|
85
|
+
# Record how the line was opened, so a test can check the library
|
|
86
|
+
# asks for the device and the speed the hub actually needs.
|
|
87
|
+
def opened(line, speed, mode)
|
|
88
|
+
@line, @speed, @mode = line, speed, mode
|
|
89
|
+
@opens += 1
|
|
90
|
+
end
|
|
91
|
+
|
|
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
|
|
98
|
+
@locks += 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Answer one command, as the hub would.
|
|
102
|
+
def command(cmd)
|
|
103
|
+
@log << cmd
|
|
104
|
+
return '' if @silent
|
|
105
|
+
return @garbage if @garbage
|
|
106
|
+
|
|
107
|
+
load
|
|
108
|
+
reply = dispatch(cmd)
|
|
109
|
+
save
|
|
110
|
+
reply
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def dispatch(cmd)
|
|
116
|
+
code = cmd[0, 2]
|
|
117
|
+
args = cmd[2..].to_s
|
|
118
|
+
|
|
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'
|
|
123
|
+
|
|
124
|
+
return 'E01' unless args.start_with?(@password)
|
|
125
|
+
rest = args[@password.size..]
|
|
126
|
+
|
|
127
|
+
sleep @delay if @delay.positive?
|
|
128
|
+
|
|
129
|
+
case code
|
|
130
|
+
when 'SP' then @state = decode(rest) & real ; 'G'
|
|
131
|
+
when 'FP' then @state = @flash = decode(rest) & real ; 'G'
|
|
132
|
+
when 'WP' then @flash = @state ; 'G'
|
|
133
|
+
when 'RD' then @state = @flash = 0
|
|
134
|
+
@password = DEFAULT_PASSWORD ; 'G'
|
|
135
|
+
when 'RH' then @state = @flash ; nil
|
|
136
|
+
when 'CP' then @password = rest ; 'G'
|
|
137
|
+
else 'E02'
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
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
|
|
153
|
+
|
|
154
|
+
def load
|
|
155
|
+
return if @path.nil? || !File.exist?(@path)
|
|
156
|
+
@state, @flash, @password = File.read(@path).split("\n", 3)
|
|
157
|
+
@state = @state.to_i(16)
|
|
158
|
+
@flash = @flash.to_i(16)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def save
|
|
162
|
+
return if @path.nil?
|
|
163
|
+
File.write(@path, "%04x\n%04x\n%s" % [ @state, @flash, @password ])
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Test double for the `uart` gem.
|
|
2
|
+
#
|
|
3
|
+
# test/helper.rb puts this directory on the load path ahead of the real
|
|
4
|
+
# gem, so `require "uart"` inside ExSYS::ManagedUSB picks this up and
|
|
5
|
+
# the suite runs against FakeHub. That is what lets the tests run with
|
|
6
|
+
# no hub attached and with neither the uart nor the termios gem
|
|
7
|
+
# installed.
|
|
8
|
+
require_relative 'fake_hub'
|
|
9
|
+
|
|
10
|
+
module UART
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :hub
|
|
13
|
+
|
|
14
|
+
# In-process tests set the hub directly; the subprocesses the
|
|
15
|
+
# executable's tests spawn build theirs from the environment.
|
|
16
|
+
def hub = @hub ||= FakeHub.from_env
|
|
17
|
+
|
|
18
|
+
def reset! = @hub = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
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.
|
|
24
|
+
class Serial
|
|
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
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.open(line, speed = 9600, mode = '8N1')
|
|
47
|
+
hub.opened(line, speed, mode)
|
|
48
|
+
serial = Serial.new(hub)
|
|
49
|
+
return serial unless block_given?
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
yield serial
|
|
53
|
+
ensure
|
|
54
|
+
serial.close
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
require_relative 'helper'
|
|
2
|
+
|
|
3
|
+
# Behaviour of the bin/exsys-usb executable, run as a subprocess so
|
|
4
|
+
# that its exit status -- what a calling script actually reads -- is
|
|
5
|
+
# part of what is asserted.
|
|
6
|
+
class TestExsysUsb < Minitest::Test
|
|
7
|
+
include CLI
|
|
8
|
+
|
|
9
|
+
## Switching #########################################################
|
|
10
|
+
|
|
11
|
+
def test_on_and_off_reach_the_hub
|
|
12
|
+
_, _, st = exsys_usb('on', '1', '2')
|
|
13
|
+
assert_predicate st, :success?
|
|
14
|
+
assert_equal [ 1, 2 ], hub.ports_on
|
|
15
|
+
|
|
16
|
+
exsys_usb('off', '1')
|
|
17
|
+
assert_equal [ 2 ], hub.ports_on
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_bare_on_and_off_cover_every_port
|
|
21
|
+
exsys_usb('on')
|
|
22
|
+
assert_equal ExSYS::ManagedUSB::PORTS, hub.ports_on
|
|
23
|
+
exsys_usb('off')
|
|
24
|
+
assert_empty hub.ports_on
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_toggle
|
|
28
|
+
exsys_usb('on', '1', '3')
|
|
29
|
+
exsys_usb('toggle', '3', '4')
|
|
30
|
+
assert_equal [ 1, 4 ], hub.ports_on
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_set_pairs
|
|
34
|
+
exsys_usb('on', '5')
|
|
35
|
+
_, _, st = exsys_usb('set', '3:on', '5:off')
|
|
36
|
+
assert_predicate st, :success?
|
|
37
|
+
assert_equal [ 3 ], hub.ports_on
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_set_accepts_every_documented_spelling
|
|
41
|
+
exsys_usb('set', '1:on', '2:ON', '3:true', '4:t', '5:1')
|
|
42
|
+
assert_equal [ 1, 2, 3, 4, 5 ], hub.ports_on
|
|
43
|
+
exsys_usb('set', '1:off', '2:OFF', '3:false', '4:f', '5:0')
|
|
44
|
+
assert_empty hub.ports_on
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_default_flag_forces_the_unlisted_ports
|
|
48
|
+
exsys_usb('on', '8')
|
|
49
|
+
exsys_usb('-D', 'false', 'set', '3:on')
|
|
50
|
+
assert_equal [ 3 ], hub.ports_on
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_commit_saves_the_power_on_state
|
|
54
|
+
exsys_usb('on', '1')
|
|
55
|
+
exsys_usb('commit')
|
|
56
|
+
exsys_usb('on', '2')
|
|
57
|
+
assert_equal [ 1, 2 ], hub.ports_on
|
|
58
|
+
assert_equal [ 1 ], hub.flash_ports
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_commit_flag_writes_through
|
|
62
|
+
exsys_usb('-c', 'on', '4')
|
|
63
|
+
exsys_usb('on', '5')
|
|
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
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
## Exit status -- regression. Every failure used to be printed and
|
|
154
|
+
## then exited 0, so that `exsys-usb off 3 || alert` never fired.
|
|
155
|
+
######################################################################
|
|
156
|
+
|
|
157
|
+
def test_a_refused_command_exits_non_zero
|
|
158
|
+
seed_hub(password: 'other')
|
|
159
|
+
_, err, st = exsys_usb('off', '3')
|
|
160
|
+
refute_predicate st, :success?
|
|
161
|
+
assert_equal 1, st.exitstatus
|
|
162
|
+
assert_match(/exsys-usb: 01/, err)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_a_silent_hub_exits_non_zero
|
|
166
|
+
_, err, st = exsys_usb('off', '3', env: { 'EXSYS_TEST_SILENT' => '1' })
|
|
167
|
+
assert_equal 1, st.exitstatus
|
|
168
|
+
assert_match(/exsys-usb:/, err)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_a_successful_command_exits_zero
|
|
172
|
+
_, _, st = exsys_usb('on', '1')
|
|
173
|
+
assert_equal 0, st.exitstatus
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
## Argument handling #################################################
|
|
177
|
+
|
|
178
|
+
# Regression: a port outside 1..16, or a word that to_i turned into
|
|
179
|
+
# 0, used to be accepted and quietly do nothing.
|
|
180
|
+
def test_an_out_of_range_port_is_refused
|
|
181
|
+
[ '0', '17', '99' ].each do |p|
|
|
182
|
+
_, err, st = exsys_usb('on', p)
|
|
183
|
+
assert_equal 1, st.exitstatus, "port #{p} should be refused"
|
|
184
|
+
assert_match(/invalid port: #{p}/, err)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_a_port_that_is_not_a_number_is_refused
|
|
189
|
+
_, err, st = exsys_usb('off', 'usb3')
|
|
190
|
+
assert_equal 1, st.exitstatus
|
|
191
|
+
assert_match(/invalid port/, err)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def test_a_refused_port_leaves_the_hub_alone
|
|
195
|
+
exsys_usb('on', '1')
|
|
196
|
+
exsys_usb('on', '17')
|
|
197
|
+
assert_equal [ 1 ], hub.ports_on
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Regression: an action the case did not know fell through it, so
|
|
201
|
+
# the tool exited 0 having done nothing at all.
|
|
202
|
+
def test_an_unknown_action_is_refused
|
|
203
|
+
_, err, st = exsys_usb('onn', '1')
|
|
204
|
+
assert_equal 1, st.exitstatus
|
|
205
|
+
assert_match(/unknown action: onn/, err)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_a_malformed_set_pair_is_refused
|
|
209
|
+
[ 'foo', '3:maybe', ':on', '3:' ].each do |a|
|
|
210
|
+
_, err, st = exsys_usb('set', a)
|
|
211
|
+
assert_equal 1, st.exitstatus, "#{a} should be refused"
|
|
212
|
+
assert_match(/invalid argument/, err)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
## Startup failures -- regression. Option parsing, the debug file
|
|
217
|
+
## and the hub were all built before the rescue, so their failures
|
|
218
|
+
## escaped as a Ruby backtrace.
|
|
219
|
+
######################################################################
|
|
220
|
+
|
|
221
|
+
def test_an_over_long_password_is_reported_not_dumped
|
|
222
|
+
_, err, st = exsys_usb('-p', 'verylongpassword', 'on', '1')
|
|
223
|
+
assert_equal 1, st.exitstatus
|
|
224
|
+
assert_equal "exsys-usb: password too long\n", err
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_an_unopenable_debug_file_is_reported_not_dumped
|
|
228
|
+
_, err, st = exsys_usb('--debug=/nonexistent-dir/x.log', 'on', '1')
|
|
229
|
+
assert_equal 1, st.exitstatus
|
|
230
|
+
assert_match(/\Aexsys-usb: /, err)
|
|
231
|
+
refute_match(/\.rb:\d+:in/, err)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def test_a_bad_option_value_is_reported_not_dumped
|
|
235
|
+
_, err, st = exsys_usb('-D', '0', 'set', '3:on')
|
|
236
|
+
assert_equal 1, st.exitstatus
|
|
237
|
+
assert_match(/\Aexsys-usb: /, err)
|
|
238
|
+
refute_match(/\.rb:\d+:in/, err)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
## Help ##############################################################
|
|
242
|
+
|
|
243
|
+
def test_no_action_prints_the_usage
|
|
244
|
+
out, _, st = exsys_usb
|
|
245
|
+
assert_equal 0, st.exitstatus
|
|
246
|
+
assert_match(/Usage: exsys-usb ACTION/, out)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def test_help_and_version
|
|
250
|
+
out, _, st = exsys_usb('-h')
|
|
251
|
+
assert_equal 0, st.exitstatus
|
|
252
|
+
assert_match(/--device/, out)
|
|
253
|
+
|
|
254
|
+
out, _, st = exsys_usb('-V')
|
|
255
|
+
assert_equal 0, st.exitstatus
|
|
256
|
+
assert_match(/#{ExSYS::VERSION}/, out)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
## Debug file ########################################################
|
|
260
|
+
|
|
261
|
+
# Regression: opened RDWR without truncating, so a shorter session
|
|
262
|
+
# left the tail of a longer previous one in place, reading as if the
|
|
263
|
+
# hub had sent it.
|
|
264
|
+
def test_the_debug_file_is_appended_to
|
|
265
|
+
log = File.join(@tmp, 'debug.log')
|
|
266
|
+
File.write(log, "PREVIOUS SESSION#{'.' * 200}\n")
|
|
267
|
+
exsys_usb("--debug=#{log}", 'on', '1')
|
|
268
|
+
assert_match(/\APREVIOUS SESSION/, File.read(log))
|
|
269
|
+
assert_match(/<-- GP/, File.read(log))
|
|
270
|
+
refute_match(/\.{20}\z/, File.read(log).lines.last)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def test_two_runs_both_survive_in_the_debug_file
|
|
274
|
+
log = File.join(@tmp, 'debug.log')
|
|
275
|
+
exsys_usb("--debug=#{log}", 'on', '1')
|
|
276
|
+
exsys_usb("--debug=#{log}", 'off', '1')
|
|
277
|
+
assert_equal 2, File.read(log).scan(/<-- GP/).size
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Regression: the trace carries the password, in a file that used to
|
|
281
|
+
# be created world-readable.
|
|
282
|
+
def test_the_debug_file_is_private_and_redacted
|
|
283
|
+
log = File.join(@tmp, 'debug.log')
|
|
284
|
+
seed_hub(password: 's3cret')
|
|
285
|
+
exsys_usb('-p', 's3cret', "--debug=#{log}", 'on', '1')
|
|
286
|
+
assert_equal 0o600, File.stat(log).mode & 0o777
|
|
287
|
+
refute_match(/s3cret/, File.read(log))
|
|
288
|
+
assert_match(/<-- SP\*{8}/, File.read(log))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
## Concurrency #######################################################
|
|
292
|
+
|
|
293
|
+
# Regression: the GP and the SP were separate openings of the line,
|
|
294
|
+
# so two processes could both read the old state and the second
|
|
295
|
+
# write would drop the first one's port.
|
|
296
|
+
def test_two_concurrent_invocations_do_not_lose_an_update
|
|
297
|
+
env = { 'EXSYS_TEST_LOCK' => File.join(@tmp, 'lock'),
|
|
298
|
+
'EXSYS_TEST_DELAY' => '0.2' }
|
|
299
|
+
|
|
300
|
+
results = [ '1', '2' ].map do |port|
|
|
301
|
+
Thread.new { exsys_usb('on', port, env: env) }
|
|
302
|
+
end.map(&:value)
|
|
303
|
+
|
|
304
|
+
results.each {|(_, err, st)| assert_equal 0, st.exitstatus, err }
|
|
305
|
+
assert_equal [ 1, 2 ], hub.ports_on
|
|
306
|
+
end
|
|
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
|
+
|
|
318
|
+
## Packaging #########################################################
|
|
319
|
+
|
|
320
|
+
# Regression: the file carried a shebang but no execute bit, so a
|
|
321
|
+
# fresh clone could not run the examples in the README.
|
|
322
|
+
def test_the_executable_is_executable
|
|
323
|
+
assert_predicate File.stat(EXE).mode & 0o111, :positive?
|
|
324
|
+
end
|
|
325
|
+
end
|