Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"shuttle-schedulers",
"shuttle-std",
"shuttle",
"wrappers/shuttle_enabler",
"wrappers/shuttle_rand_0.8",
"wrappers/shuttle_sync",
"wrappers/collections",
Expand Down
4 changes: 3 additions & 1 deletion wrappers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ tokio = { package = "shuttle-tokio", version = "1" }
parking_lot = { package = "shuttle-parking_lot", version = "0.12" }
```

Note that depending on `shuttle_enabler` will cause all crates in `shuttle_enabler` to be compiled when the `shuttle` flag is enabled.
`shuttle_enabler`'s `shuttle` feature enables the `shuttle` feature of every wrapper in this directory. Because Cargo compiles each crate once with the union of the features requested of it, your own `tokio` dependency is the same `shuttle-tokio` crate and so gets swapped too, without being named in the list. Adding another wrapped dependency later needs no change to the feature list.

Note that depending on `shuttle_enabler` will cause all crates in `shuttle_enabler` to be compiled when the `shuttle` flag is enabled. Note also that this relies on your dependency on a wrapper and `shuttle_enabler`'s dependency on it resolving to semver-compatible versions; if they do not, Cargo builds two separate copies and only `shuttle_enabler`'s gets the `shuttle` feature, leaving your code on the real implementation. See [shuttle_enabler/README.md](shuttle_enabler/README.md) for details.

## A note on versioning

Expand Down
51 changes: 51 additions & 0 deletions wrappers/shuttle_enabler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[package]
name = "shuttle_enabler"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
description = "Enables the Shuttle-compatible implementation of every Shuttle wrapper crate with a single feature flag."
repository = "https://github.com/awslabs/shuttle"
readme = "README.md"

# This crate contains no code. Its only purpose is to own a single `shuttle` feature that turns on
# the `shuttle` feature of every wrapper crate, so that downstream crates do not have to enumerate
# them (and cannot forget to update the list when adding a dependency).
#
# Every dependency is optional and is only pulled in by the `shuttle` feature, so depending on this
# crate costs nothing when the feature is off.
#
# Dependencies deliberately use `default-features = false`. Cargo unifies features as a union, so
# this crate contributes *only* the `shuttle` feature and never silently turns on any other feature
# of a wrapper on a downstream crate's behalf.
[features]
default = []
shuttle = [
"determinizable_collections/deterministic",
"shuttle-async-stream/shuttle",
"shuttle-dashmap/shuttle",
"shuttle-lazy_static/shuttle",
"shuttle-parking_lot/shuttle",
"shuttle-rand/shuttle",
"shuttle-sync/shuttle",
"shuttle-tokio/shuttle",
"shuttle-tokio-retry/shuttle",
"shuttle-tokio-stream/shuttle",
"shuttle-tokio-util/shuttle",
]

# Version requirements are deliberately as permissive as semver allows. The whole mechanism relies on
# Cargo unifying this crate's dependency on a wrapper with the downstream crate's own dependency on
# that same wrapper; a tighter requirement here would risk resolving to a second, separate copy that
# does not get the `shuttle` feature.
[dependencies]
determinizable_collections = { path = "../collections", version = "0.1", optional = true, default-features = false }
shuttle-async-stream = { path = "../async_stream", version = "0.3", optional = true, default-features = false }
shuttle-dashmap = { path = "../dashmap", version = "6", optional = true, default-features = false }
shuttle-lazy_static = { path = "../lazy_static", version = "1", optional = true, default-features = false }
shuttle-parking_lot = { path = "../parking_lot", version = "0.12", optional = true, default-features = false }
shuttle-rand = { path = "../shuttle_rand_0.8", version = "0.8", optional = true, default-features = false }
shuttle-sync = { path = "../shuttle_sync", version = "0.1", optional = true, default-features = false }
shuttle-tokio = { path = "../tokio/wrappers/shuttle-tokio", version = "1", optional = true, default-features = false }
shuttle-tokio-retry = { path = "../tokio/wrappers/shuttle-tokio-retry", version = "0.3", optional = true, default-features = false }
shuttle-tokio-stream = { path = "../tokio/wrappers/shuttle-tokio-stream", version = "0.1", optional = true, default-features = false }
shuttle-tokio-util = { path = "../tokio/wrappers/shuttle-tokio-util", version = "0.7", optional = true, default-features = false }
83 changes: 83 additions & 0 deletions wrappers/shuttle_enabler/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# shuttle_enabler

This crate turns on the Shuttle-compatible implementation of every wrapper crate with a single
feature flag, so that crates depending on several wrappers do not have to enumerate them all.

It contains no code.

## How to use

Without it, every wrapped dependency needs its own entry in your `shuttle` feature:

```toml
[features]
shuttle = [
"tokio/shuttle",
"parking_lot/shuttle",
# ... and one line for every other wrapped dependency
]
```

That list is easy to get wrong, and getting it wrong is quiet: a missing entry does not fail the
build, it just leaves that dependency running its real implementation inside a Shuttle test, where
the scheduler has no control over it. With this crate, the list is one entry:

```toml
[features]
shuttle = [
"shuttle_enabler/shuttle",
]

[dependencies]
shuttle_enabler = "0.1.0"
tokio = { package = "shuttle-tokio", version = "1" }
parking_lot = { package = "shuttle-parking_lot", version = "0.12" }
```

Adding another wrapped dependency later needs no change to the feature list.

## How it works

Cargo compiles each crate in a dependency graph once, with the union of the features requested by
everything that depends on it. `shuttle_enabler`'s `shuttle` feature requests
`shuttle-tokio/shuttle`, and your `tokio` dependency is that same `shuttle-tokio` crate, so it gets
compiled with `shuttle` enabled and resolves to the Shuttle implementation.

Every dependency of this crate is optional and is activated only by the `shuttle` feature, so
depending on it costs nothing when the feature is off.

## What it covers

| Wrapper | Feature enabled |
| --- | --- |
| `shuttle-tokio` | `shuttle` |
| `shuttle-tokio-stream` | `shuttle` |
| `shuttle-tokio-util` | `shuttle` |
| `shuttle-tokio-retry` | `shuttle` |
| `shuttle-parking_lot` | `shuttle` |
| `shuttle-dashmap` | `shuttle` |
| `shuttle-async-stream` | `shuttle` |
| `shuttle-lazy_static` | `shuttle` |
| `shuttle-rand` | `shuttle` |
| `shuttle-sync` | `shuttle` |
| `determinizable_collections` | `deterministic` |

Enabling the `shuttle` feature compiles all of them, including the ones you do not use.

From each wrapper, this crate requests only the feature in the table above, and declares every
dependency with `default-features = false`, so it does not turn on anything else on your behalf.
Cargo still enables the union of what *everything* in your graph requests, so a wrapper may end up
with more features than that if another crate asks for them.

## Limitations

**Version skew silently disables the swap.** The mechanism relies on Cargo unifying this crate's
dependency on a wrapper with your own dependency on that wrapper. If the two resolve to
semver-incompatible versions, Cargo builds two separate copies and only this crate's copy gets the
`shuttle` feature. Your code keeps using the real implementation, and your Shuttle test passes
without having tested anything.

Requirements in this crate are as permissive as semver allows to make that unlikely. If you pin a
wrapper to a specific version, check that it is semver-compatible with the requirement in
[Cargo.toml](Cargo.toml), or enable that wrapper's `shuttle` feature directly rather than relying on
this crate for it.
62 changes: 62 additions & 0 deletions wrappers/shuttle_enabler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Turns on the Shuttle-compatible implementation of every [Shuttle] wrapper crate with a single
//! feature flag.
//!
//! This crate intentionally contains no code. Wrapper crates such as `shuttle-tokio` each expose a
//! `shuttle` feature that swaps the real crate for a Shuttle-compatible one, and a crate that
//! depends on several wrappers would otherwise have to enumerate every one of them:
//!
//! ```toml
//! [features]
//! shuttle = [
//! "tokio/shuttle",
//! "parking_lot/shuttle",
//! # ... and one line for every other wrapped dependency
//! ]
//! ```
//!
//! That list is easy to get wrong. Forgetting to add an entry does not fail the build; it silently
//! leaves that dependency running its real implementation under a Shuttle test, where it will not be
//! controlled by the scheduler. Depending on this crate instead reduces the list to one entry:
//!
//! ```toml
//! [features]
//! shuttle = [
//! "shuttle_enabler/shuttle",
//! ]
//!
//! [dependencies]
//! shuttle_enabler = "0.1.0"
//! tokio = { package = "shuttle-tokio", version = "1" }
//! parking_lot = { package = "shuttle-parking_lot", version = "0.12" }
//! ```
//!
//! # How it works
//!
//! Cargo compiles each crate in a dependency graph once, with the *union* of the features requested
//! by everything that depends on it. `shuttle_enabler`'s `shuttle` feature requests
//! `shuttle-tokio/shuttle`, and the downstream crate's `tokio` dependency is that same
//! `shuttle-tokio` crate, so it is compiled with the `shuttle` feature enabled and resolves to the
//! Shuttle implementation. Adding a new wrapped dependency needs no change to the feature list.
//!
//! Every dependency of this crate is optional and activated only by the `shuttle` feature, so
//! depending on it costs nothing when the feature is off. When the feature is on, all of the wrapper
//! crates are compiled, including ones the downstream crate does not use.
//!
//! Dependencies are declared with `default-features = false`, so this crate requests nothing from a
//! wrapper beyond its `shuttle` feature. Cargo enables the union of what every dependent requests,
//! so a wrapper may still end up with more features than that if something else asks for them.
//!
//! # Version skew
//!
//! The mechanism depends on Cargo unifying this crate's dependency on a wrapper with the downstream
//! crate's own dependency on that wrapper. If the two requirements resolve to semver-incompatible
//! versions, Cargo builds two separate copies, and only this crate's copy gets the `shuttle`
//! feature. The downstream crate would keep using the real implementation, and its Shuttle test
//! would pass without ever having tested anything.
//!
//! Requirements here are therefore as permissive as semver allows. If you pin a wrapper to a
//! specific version, check that it is semver-compatible with the requirement in this crate's
//! `Cargo.toml`, or enable that wrapper's `shuttle` feature directly instead of relying on this
//! crate for it.
//!
//! [Shuttle]: https://crates.io/crates/shuttle
Loading