Skip to content

Commit bb7209c

Browse files
Merge branch 'lukebakken-fix/misc'
2 parents 8b5a1f0 + 55cb0b2 commit bb7209c

9 files changed

Lines changed: 330 additions & 30 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ cargo build
1313

1414
cargo fmt --all
1515

16-
cargo nextest run --all-features
17-
cargo clippy --all-features
16+
RUSTFLAGS="-D warnings" cargo nextest run --all-features
17+
RUSTFLAGS="-D warnings" cargo clippy --all-features
1818
```
1919

2020
To [filter](https://nexte.st/docs/filtersets/) tests with `cargo nextest`:
@@ -26,7 +26,7 @@ cargo nextest run -E "test(test_name)"
2626
### Test Node Configuration
2727

2828
Test suites require a RabbitMQ node running on `localhost:15672` with `rabbitmq_management` plugin enabled.
29-
`bin/ci/before_build.sh` is a script that demonstrates how the node should be configured.
29+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for step-by-step Docker setup instructions.
3030

3131

3232
## Key Files
@@ -78,7 +78,7 @@ Test suites require a RabbitMQ node running on `localhost:15672` with `rabbitmq_
7878
* Add integration tests to `tests/integration/`, unit tests to `tests/unit/`, property-based tests to `tests/proptests/`
7979
* Never add tests in the implementation files
8080
* At the end of each task, run `cargo fmt --all`
81-
* At the end of each task, run `cargo clippy --all` and fix any warnings it might emit
81+
* At the end of each task, run `RUSTFLAGS="-D warnings" cargo clippy --all-features` and fix any warnings it might emit
8282

8383
## Comments
8484

CONTRIBUTING.md

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,89 @@
22

33
## Running Tests
44

5-
While tests support the standard `cargo test` option, another option
6-
for running tests is [`cargo nextest`](https://nexte.st/).
5+
Most tests require a locally running RabbitMQ node with default ports.
6+
This can be a locally installed package, a [generic binary build](https://rabbitmq.com/docs/install-generic-unix)
7+
or a container that uses the [community OCI image](https://github.com/docker-library/rabbitmq).
78

8-
### Test Structure
9+
### Prerequisites
910

10-
Tests are organized into three directories under `tests/`:
11+
Install [cargo-nextest](https://nexte.st/):
1112

12-
* `tests/integration/`: integration tests that drive the CLI binary and require a running RabbitMQ node
13-
* `tests/unit/`: unit tests with no external dependencies
14-
* `tests/proptests/`: property-based tests
13+
```bash
14+
cargo install cargo-nextest
15+
```
16+
17+
### Step 1: Start RabbitMQ
18+
19+
```bash
20+
docker run -d --name rabbitmq \
21+
-p 15672:15672 \
22+
-p 5672:5672 \
23+
rabbitmq:4-management
24+
```
25+
26+
Wait for the node to boot:
27+
28+
```bash
29+
sleep 15
30+
```
31+
32+
### Step 2: Pre-configure the Node
33+
34+
Run the setup script using the Docker exec variant of `rabbitmqctl`:
35+
36+
```bash
37+
RUST_HTTP_API_CLIENT_RABBITMQCTL=DOCKER:rabbitmq bin/ci/before_build.sh
38+
```
39+
40+
This enables the required plugins (management, shovel, federation, stream), creates test users,
41+
sets up the `rust/rabbitmqadmin` virtual host, sets the cluster name,
42+
and enables all stable feature flags.
1543

16-
### Run All Tests
44+
Wait for the changes to apply:
1745

18-
``` bash
46+
```bash
47+
sleep 10
48+
```
49+
50+
### Step 3: Run All Tests
51+
52+
```bash
1953
NEXTEST_RETRIES=3 cargo nextest run --all-features
2054
```
2155

56+
`NEXTEST_RETRIES=3` retries each failing test up to 3 times.
57+
58+
### Stopping the Node
59+
60+
```bash
61+
docker stop rabbitmq && docker rm rabbitmq
62+
```
63+
64+
---
65+
66+
## Test Structure
67+
68+
Tests are organized into three directories under `tests/`:
69+
70+
* `tests/integration/`: integration tests that drive the CLI binary and require a running RabbitMQ node
71+
* `tests/unit/`: unit tests with no external dependencies
72+
* `tests/proptests/`: property-based tests
73+
2274
### Run Only Unit and Property-Based Tests (No Local RabbitMQ Node Needed)
2375

24-
``` bash
76+
```bash
2577
cargo nextest run -E 'binary(unit) or binary(proptests)'
2678
```
2779

2880
### Run Only Integration Tests
2981

30-
``` bash
82+
```bash
3183
NEXTEST_RETRIES=3 cargo nextest run -E 'binary(integration)'
3284
```
3385

3486
### Run a Specific Test
3587

36-
``` bash
88+
```bash
3789
NEXTEST_RETRIES=3 cargo nextest run -E "test(test_list_all_vhost_limits)"
3890
```

bin/ci/before_build_tls.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ if [ -n "$CONTAINER_ID" ]; then
8181
echo " -p 15671:15671 -p 15672:15672 -p 5672:5672 \\"
8282
echo " -v ${CERTS_DIR}:/certs:ro \\"
8383
echo " -v ${RABBITMQ_CONF}:/etc/rabbitmq/rabbitmq.conf:ro \\"
84-
echo " rabbitmq:4.0-management"
84+
echo " rabbitmq:4-management"
8585
fi
8686

8787
# Enable management plugin (should already be enabled in the management image)

src/errors.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,20 @@ pub struct HealthCheckInfo {
4040

4141
#[derive(thiserror::Error, Debug)]
4242
pub enum CommandRunError {
43-
#[error("Asked to run an unknown command '{command} {subcommand}")]
43+
#[error("Asked to run an unknown command '{command} {subcommand}'")]
4444
UnknownCommandTarget { command: String, subcommand: String },
4545
#[error("Missing required argument: {name}")]
4646
MissingRequiredArgument { name: String },
4747
#[error("Invalid value for argument '{name}': {message}")]
4848
InvalidArgumentValue { name: String, message: String },
4949
#[error(
50-
"Local TLS certificate file at {local_path} does not exist, cannot be read or passed as a PEM file: {cause}"
50+
"Local TLS certificate file at {local_path} could not be parsed as a PEM file: {cause}"
5151
)]
5252
CertificateFileCouldNotBeLoaded1 {
5353
local_path: String,
5454
cause: reqwest::Error,
5555
},
56-
#[error(
57-
"Local TLS certificate file at {local_path} does not exist, cannot be read or passed as a PEM file: {cause}"
58-
)]
56+
#[error("Local TLS certificate file at {local_path} could not be read: {cause}")]
5957
CertificateFileCouldNotBeLoaded2 {
6058
local_path: String,
6159
cause: rustls::pki_types::pem::Error,
@@ -72,7 +70,7 @@ pub enum CommandRunError {
7270
CertificateFileInvalidPem { local_path: String, details: String },
7371
#[error("TLS private key file at {local_path} contains an unsupported key type or format")]
7472
PrivateKeyFileUnsupported { local_path: String },
75-
#[error("TLS certificate and private key files do not match")]
73+
#[error("TLS certificate {cert_path} and private key {key_path} do not match")]
7674
CertificateKeyMismatch { cert_path: String, key_path: String },
7775
#[error("{}", format_client_error(&.0.status_code, &.0.error_details))]
7876
ClientError(Box<HttpErrorInfo>),
@@ -90,11 +88,11 @@ pub enum CommandRunError {
9088
MissingArgumentValue { property: String },
9189
#[error("Unsupported argument value for property (field) {property}")]
9290
UnsupportedArgumentValue { property: String },
93-
#[error("This request produces an invalid HTTP header value")]
91+
#[error("This request produces an invalid HTTP header value: {error}")]
9492
InvalidHeaderValue { error: InvalidHeaderValue },
95-
#[error("Response is incompatible with the target data type")]
93+
#[error("Response is incompatible with the target data type: {error}")]
9694
IncompatibleBody { error: ConversionError },
97-
#[error("Encountered an error when performing an HTTP request")]
95+
#[error("Encountered an error when performing an HTTP request: {error}")]
9896
RequestError { error: reqwest::Error },
9997
#[error("Failed to build HTTP client: {0}")]
10098
HttpClientBuildError(reqwest::Error),

src/output.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ impl<'a> ResultHandler<'a> {
308308
println!("{}", table);
309309
}
310310
Err(ref e) => {
311-
println!("Error: {:?}", e);
311+
eprintln!("{}", e);
312312
self.exit_code = Some(ExitCode::Unavailable);
313313
}
314314
}
@@ -318,6 +318,13 @@ impl<'a> ResultHandler<'a> {
318318
eprintln!("{}", error);
319319
let code = match error {
320320
CommandRunError::UnknownCommandTarget { .. } => ExitCode::Usage,
321+
CommandRunError::MissingRequiredArgument { .. } => ExitCode::Usage,
322+
CommandRunError::InvalidArgumentValue { .. } => ExitCode::Usage,
323+
CommandRunError::ConflictingOptions { .. } => ExitCode::Usage,
324+
CommandRunError::MissingOptions { .. } => ExitCode::Usage,
325+
CommandRunError::MissingArgumentValue { .. } => ExitCode::Usage,
326+
CommandRunError::UnsupportedArgumentValue { .. } => ExitCode::Usage,
327+
CommandRunError::InvalidBaseUri { .. } => ExitCode::Usage,
321328
CommandRunError::CertificateFileCouldNotBeLoaded1 { .. } => ExitCode::DataErr,
322329
CommandRunError::CertificateFileCouldNotBeLoaded2 { .. } => ExitCode::DataErr,
323330
CommandRunError::CertificateFileNotFound { .. } => ExitCode::DataErr,
@@ -328,7 +335,16 @@ impl<'a> ResultHandler<'a> {
328335
CommandRunError::IoError { .. } => ExitCode::DataErr,
329336
CommandRunError::FailureDuringExecution { .. } => ExitCode::DataErr,
330337
CommandRunError::HttpClientBuildError { .. } => ExitCode::DataErr,
331-
_ => ExitCode::Usage,
338+
CommandRunError::ClientError { .. } => ExitCode::DataErr,
339+
CommandRunError::ServerError { .. } => ExitCode::DataErr,
340+
CommandRunError::NotFound => ExitCode::DataErr,
341+
CommandRunError::InvalidHeaderValue { .. } => ExitCode::DataErr,
342+
CommandRunError::IncompatibleBody { .. } => ExitCode::DataErr,
343+
CommandRunError::RequestError { .. } => ExitCode::DataErr,
344+
CommandRunError::JsonParseError { .. } => ExitCode::DataErr,
345+
CommandRunError::Other => ExitCode::DataErr,
346+
// HealthCheckFailed is handled separately in health_check_result
347+
CommandRunError::HealthCheckFailed { .. } => ExitCode::Unavailable,
332348
};
333349
self.exit_code = Some(code);
334350
}
@@ -542,7 +558,7 @@ impl ProgressReporter for QuietProgressReporter {
542558
// Silent
543559
}
544560

545-
fn finish_operation(&mut self, total: usize) {
546-
println!("Completed: {} items processed", total);
561+
fn finish_operation(&mut self, _total: usize) {
562+
// Silent
547563
}
548564
}

tests/integration/health_check_tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use crate::test_helpers::output_includes;
1616
use crate::test_helpers::{run_fails, run_succeeds};
1717
use std::error::Error;
18+
1819
#[test]
1920
fn test_health_check_local_alarms() -> Result<(), Box<dyn Error>> {
2021
run_succeeds(["health_check", "local_alarms"]).stdout(output_includes("passed"));
@@ -72,3 +73,21 @@ fn test_health_check_protocol_listener_fails() -> Result<(), Box<dyn Error>> {
7273

7374
Ok(())
7475
}
76+
77+
#[test]
78+
fn test_health_check_unreachable_host_produces_useful_error() -> Result<(), Box<dyn Error>> {
79+
// Port 19999 is not expected to have anything listening on localhost
80+
run_fails([
81+
"--host",
82+
"localhost",
83+
"--port",
84+
"19999",
85+
"health_check",
86+
"cluster_wide_alarms",
87+
])
88+
.stderr(output_includes(
89+
"Encountered an error when performing an HTTP request:",
90+
));
91+
92+
Ok(())
93+
}

tests/integration/vhosts_delete_multiple_tests.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,27 @@ fn test_vhosts_delete_multiple_protects_deletion_protected_vhosts() -> Result<()
382382

383383
Ok(())
384384
}
385+
386+
#[test]
387+
fn test_vhosts_delete_multiple_quiet_produces_no_output() -> Result<(), Box<dyn Error>> {
388+
let prefix = "rabbitmqadmin.test-vhosts-delete-multiple-quiet";
389+
390+
delete_vhosts_with_prefix(prefix).ok();
391+
392+
for i in 1..=3 {
393+
let vh_name = format!("{}-{}", prefix, i);
394+
run_succeeds(["vhosts", "declare", "--name", &vh_name]);
395+
}
396+
397+
run_succeeds([
398+
"--quiet",
399+
"--non-interactive",
400+
"vhosts",
401+
"delete_multiple",
402+
"--name-pattern",
403+
prefix,
404+
])
405+
.stdout(predicates::str::is_empty());
406+
407+
Ok(())
408+
}

0 commit comments

Comments
 (0)