diff --git a/.cargo/config.toml b/.cargo/config.toml
index 537e721c..33cf3dc1 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -2,8 +2,6 @@
 lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
 lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
 
-ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
-
 # just check the library (without dev deps)
 ci-check-min = "hack --workspace check --no-default-features"
 ci-check-lib = "hack --workspace --feature-powerset --depth=2 --exclude-features=io-uring check"
@@ -19,8 +17,5 @@ ci-test-rustls-020 = "hack --feature-powerset --depth=2 --exclude-features=io-ur
 ci-test-rustls-021 = "hack --feature-powerset --depth=2 --exclude-features=io-uring,rustls-0_20,rustls-0_22 test --lib --tests --no-fail-fast -- --nocapture"
 ci-test-rustls-022 = "hack --feature-powerset --depth=2 --exclude-features=io-uring,rustls-0_20,rustls-0_21 test --lib --tests --no-fail-fast -- --nocapture"
 
-# tests avoiding io-uring feature on Windows
-ci-test-win = "hack --feature-powerset --depth=2 --exclude-features=io-uring test --lib --tests --no-fail-fast -- --nocapture"
-
 # test with io-uring feature
-ci-test-linux = "hack --feature-powerset --depth=2 --exclude-features=rustls-0_20 test --lib --tests --no-fail-fast -- --nocapture"
+ci-test-linux = "hack --feature-powerset --depth=2 --exclude-features=rustls-0_20,rustls-0_21 test --lib --tests --no-fail-fast -- --nocapture"
diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml
index 621ba81d..e27a2fe8 100644
--- a/.github/workflows/ci-post-merge.yml
+++ b/.github/workflows/ci-post-merge.yml
@@ -56,10 +56,10 @@ jobs:
         with:
           toolchain: ${{ matrix.version }}
 
-      - name: Install cargo-hack and cargo-ci-cache-clean
+      - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
         uses: taiki-e/install-action@v2.33.17
         with:
-          tool: cargo-hack,cargo-ci-cache-clean
+          tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
 
       - name: check lib
         if: >
@@ -84,10 +84,8 @@ jobs:
         run: cargo ci-check-linux
 
       - name: tests
-        if: >
-          matrix.target.os != 'ubuntu-latest'
-          && matrix.target.triple != 'x86_64-pc-windows-gnu'
-        run: cargo ci-test
+        if: matrix.target.os != 'ubuntu-latest'
+        run: just test-code
       - name: tests
         if: matrix.target.os == 'ubuntu-latest'
         run: >-
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f77d2cb9..412ed481 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
           - { name: Windows (MinGW), os: windows-latest, triple: x86_64-pc-windows-gnu }
           - { name: Windows (32-bit), os: windows-latest, triple: i686-pc-windows-msvc }
         version:
-          - { name: msrv, version: 1.65.0 }
+          - { name: msrv, version: 1.70.0 }
           - { name: stable, version: stable }
 
     name: ${{ matrix.target.name }} / ${{ matrix.version.name }}
@@ -58,17 +58,17 @@ jobs:
         with:
           toolchain: ${{ matrix.version.version }}
 
-      - name: Install just, cargo-hack, cargo-ci-cache-clean
+      - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
         uses: taiki-e/install-action@v2.33.17
         with:
-          tool: just,cargo-hack,cargo-ci-cache-clean
+          tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
 
       - name: Generate Cargo.lock
         run: cargo generate-lockfile
 
       - name: workaround MSRV issues
         if: matrix.version.name == 'msrv'
-        run: just downgrade-msrv
+        run: just downgrade-for-msrv
 
       - name: check lib
         if: >
@@ -79,7 +79,7 @@ jobs:
         if: matrix.target.os == 'ubuntu-latest'
         run: cargo ci-check-lib-linux
       - name: check lib
-        if: matrix.target.triple == 'x86_64-pc-windows-gnu'
+        if: matrix.target.triple != 'x86_64-pc-windows-gnu'
         run: cargo ci-check-min
 
       - name: check full
@@ -93,30 +93,15 @@ jobs:
         run: cargo ci-check-linux
 
       - name: tests
-        if: matrix.target.os == 'macos-latest'
-        run: cargo ci-test
-      - name: tests
-        if: >
-          matrix.target.os == 'windows-latest'
-          && matrix.target.triple != 'x86_64-pc-windows-gnu'
-        run: cargo ci-test-win
-      - name: tests
-        if: matrix.target.os == 'ubuntu-latest'
-        run: >-
-          sudo bash -c "
-          ulimit -Sl 512
-          && ulimit -Hl 512
-          && PATH=$PATH:/usr/share/rust/.cargo/bin
-          && RUSTUP_TOOLCHAIN=${{ matrix.version.version }} cargo ci-test-rustls-020
-          && RUSTUP_TOOLCHAIN=${{ matrix.version.version }} cargo ci-test-rustls-021
-          && RUSTUP_TOOLCHAIN=${{ matrix.version.version }} cargo ci-test-linux
-          "
+        run: just test
+
+      # TODO: re-instate some io-uring tests for PRs
 
       - name: CI cache clean
         run: cargo-ci-cache-clean
 
-  rustdoc:
-    name: rustdoc
+  docs:
+    name: Documentation
     runs-on: ubuntu-latest
 
     steps:
@@ -127,6 +112,10 @@ jobs:
         with:
           toolchain: nightly
 
-      - name: doc tests io-uring
-        run: |
-          sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=nightly cargo ci-doctest"
+      - name: Install just
+        uses: taiki-e/install-action@v2.33.17
+        with:
+          tool: just
+
+      - name: doc tests
+        run: just test-docs
diff --git a/actix-tls/src/connect/rustls_0_20.rs b/actix-tls/src/connect/rustls_0_20.rs
index 5a114b2a..fc65b686 100644
--- a/actix-tls/src/connect/rustls_0_20.rs
+++ b/actix-tls/src/connect/rustls_0_20.rs
@@ -34,6 +34,8 @@ pub mod reexports {
 /// Returns root certificates via `rustls-native-certs` crate as a rustls certificate store.
 ///
 /// See [`rustls_native_certs::load_native_certs()`] for more info on behavior and errors.
+///
+/// [`rustls_native_certs::load_native_certs()`]: rustls_native_certs_06::load_native_certs()
 #[cfg(feature = "rustls-0_20-native-roots")]
 pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
     let mut root_certs = RootCertStore::empty();
diff --git a/actix-tls/src/connect/rustls_0_21.rs b/actix-tls/src/connect/rustls_0_21.rs
index bf922ede..071cb98e 100644
--- a/actix-tls/src/connect/rustls_0_21.rs
+++ b/actix-tls/src/connect/rustls_0_21.rs
@@ -34,6 +34,8 @@ pub mod reexports {
 /// Returns root certificates via `rustls-native-certs` crate as a rustls certificate store.
 ///
 /// See [`rustls_native_certs::load_native_certs()`] for more info on behavior and errors.
+///
+/// [`rustls_native_certs::load_native_certs()`]: rustls_native_certs_06::load_native_certs()
 #[cfg(feature = "rustls-0_21-native-roots")]
 pub fn native_roots_cert_store() -> io::Result<RootCertStore> {
     let mut root_certs = RootCertStore::empty();
diff --git a/actix-tls/src/connect/rustls_0_22.rs b/actix-tls/src/connect/rustls_0_22.rs
index c3e8b35d..7db1c7d8 100644
--- a/actix-tls/src/connect/rustls_0_22.rs
+++ b/actix-tls/src/connect/rustls_0_22.rs
@@ -34,6 +34,8 @@ pub mod reexports {
 /// Returns root certificates via `rustls-native-certs` crate as a rustls certificate store.
 ///
 /// See [`rustls_native_certs::load_native_certs()`] for more info on behavior and errors.
+///
+/// [`rustls_native_certs::load_native_certs()`]: rustls_native_certs_07::load_native_certs()
 #[cfg(feature = "rustls-0_22-native-roots")]
 pub fn native_roots_cert_store() -> io::Result<tokio_rustls::rustls::RootCertStore> {
     let mut root_certs = tokio_rustls::rustls::RootCertStore::empty();
diff --git a/actix-tracing/Cargo.toml b/actix-tracing/Cargo.toml
index 21974a7d..6e06beed 100644
--- a/actix-tracing/Cargo.toml
+++ b/actix-tracing/Cargo.toml
@@ -27,6 +27,6 @@ actix-utils = "3"
 tracing = "0.1.35"
 tracing-futures = "0.2"
 
-[dev_dependencies]
+[dev-dependencies]
 actix-rt = "2"
 slab = "0.4"
diff --git a/justfile b/justfile
index 364e8be9..f17a6f67 100644
--- a/justfile
+++ b/justfile
@@ -3,7 +3,7 @@ _list:
 
 # Downgrade dev-dependencies necessary to run MSRV checks/tests.
 [private]
-downgrade-msrv:
+downgrade-for-msrv:
     cargo update -p=ciborium --precise=0.2.1
     cargo update -p=ciborium-ll --precise=0.2.1
     cargo update -p=time --precise=0.3.16
@@ -12,14 +12,51 @@ downgrade-msrv:
     cargo update -p=anstyle --precise=1.0.2
     cargo update -p=trybuild --precise=1.0.89
 
+msrv := ```
+    cargo metadata --format-version=1 \
+    | jq -r 'first(.packages[] | .name = "actix-tls") | .rust_version'
+```
+msrv_full := msrv + ".0" # comment out if the MSRV has a patch version specified
+msrv_rustup := "+" + msrv_full
+
+non_linux_all_features_list := ```
+    cargo metadata --format-version=1 \
+    | jq '.packages[] | select(.source == null) | .features | keys' \
+    | jq -r --slurp \
+        --arg exclusions "tokio-uring,io-uring" \
+        'add | unique | . - ($exclusions | split(",")) | join(",")'
+```
+
+all_crate_features := if os() == "linux" {
+    "--all-features"
+} else {
+    "--features='" + non_linux_all_features_list + "'"
+}
+
+# Test workspace code.
+test toolchain="":
+    cargo {{ toolchain }} test --lib --tests --package=actix-macros
+    cargo {{ toolchain }} nextest run --workspace --exclude=actix-macros --no-default-features
+    cargo {{ toolchain }} nextest run --workspace --exclude=actix-macros {{ all_crate_features }}
+
+# Test workspace using MSRV.
+test-msrv: downgrade-for-msrv (test msrv_rustup)
+
+# Test workspace docs.
+test-docs toolchain="": && doc
+    cargo {{ toolchain }} test --doc --workspace {{ all_crate_features }} --no-fail-fast -- --nocapture
+
+# Test workspace.
+test-all toolchain="": (test toolchain) (test-docs)
+
 # Document crates in workspace.
-doc:
-    RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --features=rustls,openssl
+doc *args:
+    RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --no-deps --workspace {{ all_crate_features }} {{ args }}
 
 # Document crates in workspace and watch for changes.
 doc-watch:
-    RUSTDOCFLAGS="--cfg=docsrs"                cargo +nightly doc --no-deps --workspace --features=rustls-0_20,rustls-0_21,rustls-0_20-native-roots,rustls-0_21-native-roots,openssl --open
-    cargo watch -- RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace --features=rustls-0_20,rustls-0_21,rustls-0_20-native-roots,rustls-0_21-native-roots,openssl
+    @just doc --open
+    cargo watch -- just doc
 
 # Check for unintentional external type exposure on all crates in workspace.
 check-external-types-all toolchain="+nightly":
@@ -27,7 +64,7 @@ check-external-types-all toolchain="+nightly":
     set -euo pipefail
     exit=0
     for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do
-        if ! just check-external-types-manifest "$f" {{toolchain}}; then exit=1; fi
+        if ! just check-external-types-manifest "$f" {{ toolchain }}; then exit=1; fi
         echo
         echo
     done
@@ -40,9 +77,9 @@ check-external-types-all-table toolchain="+nightly":
     for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do
         echo
         echo "Checking for $f"
-        just check-external-types-manifest "$f" {{toolchain}} --output-format=markdown-table
+        just check-external-types-manifest "$f" {{ toolchain }} --output-format=markdown-table
     done
 
 # Check for unintentional external type exposure on a crate.
 check-external-types-manifest manifest_path toolchain="+nightly" *extra_args="":
-    cargo {{toolchain}} check-external-types --manifest-path "{{manifest_path}}" {{extra_args}}
+    cargo {{ toolchain }} check-external-types --manifest-path "{{ manifest_path }}" {{ extra_args }}