1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Tests for the --connect-timeout CLI option
//!
//! This feature allows users to configure the SSH connection timeout
//! separately from the command execution timeout.
use std::process::Command;
/// Helper to get bssh binary path
fn bssh_binary() -> String {
// Prefer debug binary since `cargo test` builds debug binaries
// This avoids using stale release binaries in CI
let debug = std::env::current_dir().unwrap().join("target/debug/bssh");
let release = std::env::current_dir().unwrap().join("target/release/bssh");
if debug.exists() {
debug.to_string_lossy().to_string()
} else if release.exists() {
release.to_string_lossy().to_string()
} else {
panic!("bssh binary not found. Run 'cargo build' first.");
}
}
#[test]
fn test_connect_timeout_default_value() {
// The default connect timeout should be 30 seconds
let output = Command::new(bssh_binary())
.args(["--help"])
.output()
.expect("Failed to execute bssh");
let help_text = String::from_utf8_lossy(&output.stdout);
// Verify --connect-timeout is documented in help
assert!(
help_text.contains("--connect-timeout"),
"Help should mention --connect-timeout option"
);
// Verify default value is mentioned
assert!(
help_text.contains("SSH connection timeout"),
"Help should describe connection timeout"
);
}
#[test]
fn test_connect_timeout_rejects_zero() {
// Connect timeout of 0 should be rejected (minimum is 1)
let output = Command::new(bssh_binary())
.args([
"--connect-timeout",
"0",
"-H",
"test@localhost",
"echo",
"test",
])
.output()
.expect("Failed to execute bssh");
let stderr = String::from_utf8_lossy(&output.stderr);
// Should fail with validation error
assert!(
!output.status.success(),
"Should fail when connect_timeout is 0"
);
assert!(
stderr.contains("invalid value '0'") || stderr.contains("not in"),
"Error should mention invalid value: {}",
stderr
);
}
#[test]
fn test_connect_timeout_accepts_valid_values() {
// Valid connect timeout values should be accepted
// We test with --help to verify parsing without actually connecting
for value in &["1", "10", "30", "60", "300"] {
let output = Command::new(bssh_binary())
.args(["--connect-timeout", value, "--help"])
.output()
.expect("Failed to execute bssh");
assert!(
output.status.success(),
"Should accept valid connect_timeout value: {}",
value
);
}
}
#[test]
fn test_connect_timeout_rejects_negative() {
// Negative values should be rejected by clap (u64 type)
let output = Command::new(bssh_binary())
.args([
"--connect-timeout",
"-1",
"-H",
"test@localhost",
"echo",
"test",
])
.output()
.expect("Failed to execute bssh");
// Should fail with parsing error (negative number for u64)
assert!(
!output.status.success(),
"Should fail when connect_timeout is negative"
);
}
#[test]
fn test_connect_timeout_independent_of_command_timeout() {
// Verify both --connect-timeout and --timeout can be used together
let output = Command::new(bssh_binary())
.args(["--connect-timeout", "10", "--timeout", "300", "--help"])
.output()
.expect("Failed to execute bssh");
assert!(
output.status.success(),
"Should accept both --connect-timeout and --timeout together"
);
}
#[test]
fn test_connect_timeout_with_cluster() {
// Verify --connect-timeout works with cluster operations
let output = Command::new(bssh_binary())
.args([
"--connect-timeout",
"5",
"-C",
"nonexistent_cluster",
"echo",
"test",
])
.output()
.expect("Failed to execute bssh");
// Should fail because cluster doesn't exist, not because of timeout option
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("invalid value") && !stderr.contains("connect-timeout"),
"Error should be about missing cluster, not timeout option"
);
}
#[test]
fn test_connect_timeout_help_text_format() {
let output = Command::new(bssh_binary())
.args(["--help"])
.output()
.expect("Failed to execute bssh");
let help_text = String::from_utf8_lossy(&output.stdout);
// Verify help text mentions the minimum value
assert!(
help_text.contains("minimum: 1") || help_text.contains("minimum"),
"Help text should mention minimum value for connect-timeout"
);
}
/// Test that connect_timeout is properly passed through executor
/// This is a unit test for the ExecutionConfig struct
#[test]
fn test_execution_config_connect_timeout_field() {
// This test verifies the ExecutionConfig struct has connect_timeout field
// by checking the module compiles correctly with the field
use bssh::executor::ParallelExecutor;
use bssh::node::Node;
let nodes = vec![Node::new("test".to_string(), 22, "user".to_string())];
// Create executor with connect_timeout
let executor = ParallelExecutor::new(nodes.clone(), 1, None).with_connect_timeout(Some(10));
// Executor should be created successfully with connect_timeout
// We verify by checking the nodes count matches
assert_eq!(nodes.len(), 1);
// The executor creation itself verifies the API works
drop(executor);
}
/// Test that connect_timeout works with file transfer operations
#[test]
fn test_connect_timeout_with_upload_command() {
// Test that connect_timeout is accepted with upload subcommand
let output = Command::new(bssh_binary())
.args([
"--connect-timeout",
"15",
"-H",
"test@localhost",
"upload",
"/nonexistent/source",
"/tmp/dest",
])
.output()
.expect("Failed to execute bssh");
// Should fail due to missing source file, not timeout option
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("connect-timeout"),
"Error should not be about connect-timeout option"
);
}
/// Test that connect_timeout works with download command
#[test]
fn test_connect_timeout_with_download_command() {
// Test that connect_timeout is accepted with download subcommand
let output = Command::new(bssh_binary())
.args([
"--connect-timeout",
"15",
"-H",
"test@localhost",
"download",
"/etc/hosts",
"/tmp/",
])
.output()
.expect("Failed to execute bssh");
// Should fail due to connection (can't connect to localhost:22), not timeout option
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("invalid") || !stderr.contains("connect-timeout"),
"Error should not be about invalid connect-timeout option"
);
}
/// Test that connect_timeout works with ping command
#[test]
fn test_connect_timeout_with_ping_command() {
// Test that connect_timeout is accepted with ping subcommand
let output = Command::new(bssh_binary())
.args([
"--connect-timeout",
"2",
"-H",
"test@192.0.2.1", // TEST-NET-1 (unroutable)
"ping",
])
.output()
.expect("Failed to execute bssh");
// Should timeout trying to connect, not fail on option parsing
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}{}", stdout, stderr);
assert!(
!combined.contains("invalid value '2'"),
"Should accept connect_timeout value of 2"
);
}