Compare commits

...

2 Commits

Author SHA1 Message Date
overtrue 53378ad385 ci(lock): use portable test evidence filenames 2026-10-28 11:52:47 +08:00
overtrue f5c082de93 test(lock): cover forced guard generations 2026-10-28 11:41:28 +08:00
2 changed files with 381 additions and 204 deletions
+182 -204
View File
@@ -1,224 +1,202 @@
# Copyright 2024 RustFS Team
#
# 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.
name: Windows Filesystem Tests
name: FastLock stale guard regression
on:
push:
branches: [ main ]
paths:
- "crates/ecstore/src/disk/**"
- "crates/ecstore/src/layout/**"
- "crates/ecstore/src/store/init_format.rs"
- "crates/ecstore/src/set_disk/ops/locking.rs"
- "crates/ecstore/src/object_api/write_commit_context.rs"
- "crates/ecstore/src/bucket/utils.rs"
- "crates/ecstore/Cargo.toml"
- "crates/utils/src/dunce.rs"
- "crates/utils/src/os/**"
- "crates/utils/Cargo.toml"
- "crates/utils/src/path.rs"
- "rustfs/build.rs"
- "rustfs/Cargo.toml"
- "rustfs/src/main.rs"
- "rustfs/src/server/runtime.rs"
- "rustfs/src/startup_*.rs"
- "Cargo.toml"
- "Cargo.lock"
- ".github/actions/setup/**"
- ".github/workflows/windows-filesystem.yml"
pull_request:
branches: [ main ]
paths:
- "crates/ecstore/src/disk/**"
- "crates/ecstore/src/layout/**"
- "crates/ecstore/src/store/init_format.rs"
- "crates/ecstore/src/set_disk/ops/locking.rs"
- "crates/ecstore/src/object_api/write_commit_context.rs"
- "crates/ecstore/src/bucket/utils.rs"
- "crates/ecstore/Cargo.toml"
- "crates/utils/src/dunce.rs"
- "crates/utils/src/os/**"
- "crates/utils/Cargo.toml"
- "crates/utils/src/path.rs"
- "rustfs/build.rs"
- "rustfs/Cargo.toml"
- "rustfs/src/main.rs"
- "rustfs/src/server/runtime.rs"
- "rustfs/src/startup_*.rs"
- "Cargo.toml"
- "Cargo.lock"
- ".github/actions/setup/**"
- ".github/workflows/windows-filesystem.yml"
workflow_dispatch:
inputs:
expected_head:
description: Exact workflow commit SHA
required: true
type: string
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: fast-lock-stale-guard-${{ github.ref }}-${{ github.run_id }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: never
CARGO_INCREMENTAL: '0'
CARGO_BUILD_JOBS: '2'
RUST_BACKTRACE: '0'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
NO_PROXY: 127.0.0.1,localhost
jobs:
rename-safety:
name: Rename Safety
runs-on: windows-latest
timeout-minutes: 60
scoped-regressions:
runs-on: ubuntu-latest
timeout-minutes: 110
steps:
- name: Checkout repository
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
with:
persist-credentials: false
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: build-x86_64-pc-windows-msvc
cache-save-if: 'false'
install-build-packaging-tools: 'false'
install-test-tools: 'false'
- name: Check production Windows dependencies
shell: pwsh
run: cargo check -p rustfs-ecstore --lib
- name: Test guarded rename publication
shell: pwsh
run: cargo test -p rustfs-ecstore --lib rename_all_ -- --nocapture
- name: Test multipart parent durability barriers
shell: pwsh
run: cargo test -p rustfs-ecstore --lib multipart_owned_record_parent_barriers_ -- --nocapture
- name: Test Windows path conversion
shell: pwsh
run: "cargo test -p rustfs-utils --no-default-features --features path --lib dunce:: -- --nocapture"
- name: Test Windows volume and disk-space probes
shell: pwsh
- name: Prepare evidence
shell: bash
run: |
# Map a drive letter to the temp drive's administrative share so the
# remote-volume probe also covers mapped network drives.
$drive = (Split-Path -Qualifier ([System.IO.Path]::GetTempPath())).TrimEnd(':')
net use R: "\\localhost\$drive`$" /persistent:no
if ($LASTEXITCODE -eq 0) {
$env:RUSTFS_TEST_WINDOWS_MAPPED_DRIVE = 'R:'
} else {
Write-Output "::warning::could not map R: to \\localhost\$drive`$; mapped-drive test will skip"
}
cargo test -p rustfs-utils --features os --lib os:: -- --nocapture
$status = $LASTEXITCODE
if ($env:RUSTFS_TEST_WINDOWS_MAPPED_DRIVE) { net use R: /delete /y | Out-Null }
exit $status
set -euo pipefail
mkdir -p "$RUNNER_TEMP/fast-lock-stale-guard"
printf 'not-started\n' > "$RUNNER_TEMP/fast-lock-stale-guard/build-exit.txt"
- name: Test Windows handle guards
shell: pwsh
run: cargo test -p rustfs-ecstore --lib windows_ -- --nocapture
- name: Test endpoint path parsing
shell: pwsh
run: "cargo test -p rustfs-ecstore --lib layout::endpoint:: -- --nocapture"
- name: Test startup temporary-directory cleanup
shell: pwsh
run: cargo test -p rustfs-ecstore --lib cleanup_tmp_on_startup_ -- --nocapture
- name: Test object key Windows admission
shell: pwsh
run: "cargo test -p rustfs-ecstore --lib bucket::utils:: -- --nocapture"
- name: Test Windows file-name rules
shell: pwsh
run: "cargo test -p rustfs-utils --no-default-features --features path --lib dunce:: -- --nocapture"
- name: Test fresh format publication
shell: pwsh
run: cargo test -p rustfs-ecstore --lib fresh_format_load_initializes_all_disks -- --nocapture
# The native Windows main thread only gets the 1 MiB PE stack reserve, while
# Linux and macOS hand out 8 MiB. Startup runs the whole root future on the
# thread that calls `block_on`, so a debug server once overflowed before it
# listened on Windows only. Start a real server to keep that covered.
startup-smoke:
name: Startup Smoke
runs-on: windows-latest
timeout-minutes: 90
steps:
- name: Checkout repository
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
- name: Checkout
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a
with:
fetch-depth: 3
persist-credentials: false
clean: false
- name: Setup Rust environment
uses: ./.github/actions/setup
with:
rust-version: stable
cache-shared-key: build-x86_64-pc-windows-msvc
cache-save-if: 'false'
install-build-packaging-tools: 'false'
install-test-tools: 'false'
- name: Build debug server binary
shell: pwsh
run: cargo build --locked -p rustfs --bin rustfs
- name: Print version
shell: pwsh
run: |
& .\target\debug\rustfs.exe --version
if ($LASTEXITCODE -ne 0) { throw "rustfs --version exited with $LASTEXITCODE" }
- name: Start server and wait for readiness
shell: pwsh
- name: Verify exact source and workflow scope
shell: bash
env:
RUSTFS_ACCESS_KEY: rustfsadmin
RUSTFS_SECRET_KEY: rustfsadmin
EXPECTED_HEAD: ${{ inputs.expected_head }}
run: |
$volume = Join-Path $env:RUNNER_TEMP 'rustfs-startup-smoke'
$stdout = Join-Path $env:RUNNER_TEMP 'rustfs-startup-smoke.out.log'
$stderr = Join-Path $env:RUNNER_TEMP 'rustfs-startup-smoke.err.log'
New-Item -ItemType Directory -Force -Path $volume | Out-Null
$server = Start-Process -FilePath .\target\debug\rustfs.exe `
-ArgumentList @('--address', '127.0.0.1:9000', "`"$volume`"") `
-RedirectStandardOutput $stdout -RedirectStandardError $stderr `
-NoNewWindow -PassThru
# Cache the process handle now; otherwise ExitCode reads back empty.
$null = $server.Handle
$ready = $false
try {
for ($i = 0; $i -lt 180; $i++) {
if ($server.HasExited) { break }
try {
$response = Invoke-WebRequest -Uri 'http://127.0.0.1:9000/health/ready' -UseBasicParsing -NoProxy -TimeoutSec 2
if ($response.StatusCode -eq 200) { $ready = $true; break }
} catch {}
Start-Sleep -Seconds 1
}
} finally {
if (-not $server.HasExited) { Stop-Process -Id $server.Id -Force }
$server.WaitForExit()
Write-Host '--- server stdout (tail) ---'
Get-Content $stdout -Tail 100 -ErrorAction SilentlyContinue
Write-Host '--- server stderr (tail) ---'
Get-Content $stderr -Tail 100 -ErrorAction SilentlyContinue
}
if (-not $ready) {
throw ("rustfs did not report ready on /health/ready (exit code: {0})" -f $server.ExitCode)
}
set -euo pipefail
evidence="$RUNNER_TEMP/fast-lock-stale-guard"
workflow_sha=$(git rev-parse HEAD)
source_sha=$(git rev-parse HEAD^)
base_sha=$(git rev-parse HEAD^^)
source_tree=$(git rev-parse "$source_sha^{tree}")
source_diff=$(git diff --full-index --binary "$base_sha" "$source_sha" | sha256sum | cut -d' ' -f1)
source_files=$(git diff --name-only "$base_sha" "$source_sha")
workflow_files=$(git diff --name-only "$source_sha" "$workflow_sha")
printf 'expected_head=%s\nworkflow=%s\nsource=%s\nbase=%s\ntree=%s\ndiff=%s\nsource_files=%s\nworkflow_files=%s\n' \
"$EXPECTED_HEAD" "$workflow_sha" "$source_sha" "$base_sha" "$source_tree" "$source_diff" \
"$source_files" "$workflow_files" | tee "$evidence/source-proof.txt"
[[ "$EXPECTED_HEAD" =~ ^[0-9a-f]{40}$ ]]
[[ "$GITHUB_EVENT_NAME" == 'workflow_dispatch' ]]
[[ "$EXPECTED_HEAD" == "$GITHUB_SHA" ]]
[[ "$workflow_sha" == "$GITHUB_SHA" ]]
[[ "$source_sha" == 'f5c082de935e64f6cad64caf860021b0937ab213' ]]
[[ "$base_sha" == '7a3bf8cdbd781116a0970585b995ade668c0e662' ]]
[[ "$source_tree" == '0984e2b01c6d3ec7d162dd637d8f2af40fb41b59' ]]
[[ "$source_diff" == 'c78aea1d30af8be1d40d7bd5607168442cd13d3f61ea87af012d84164218f261' ]]
[[ "$source_files" == 'crates/lock/src/fast_lock/manager.rs' ]]
[[ "$workflow_files" == '.github/workflows/windows-filesystem.yml' ]]
[[ -z "$(git status --porcelain)" ]]
df -h . | tee "$evidence/disk.txt"
- name: Install system dependencies
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y build-essential pkg-config libssl-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
with:
toolchain: stable
- name: Compile only the lock library tests
shell: bash
timeout-minutes: 75
run: |
set -euo pipefail
evidence="$RUNNER_TEMP/fast-lock-stale-guard"
set +e
timeout --signal=TERM --kill-after=30s 70m \
cargo test --locked -p rustfs-lock --lib --no-run > "$evidence/build.log" 2>&1
result=$?
set -e
printf '%s\n' "$result" > "$evidence/build-exit.txt"
tail -n 100 "$evidence/build.log"
exit "$result"
- name: Run normal shared and exclusive controls
shell: bash
timeout-minutes: 12
run: |
set -euo pipefail
evidence="$RUNNER_TEMP/fast-lock-stale-guard"
tests=(
'fast_lock::tests::fast_lock_tests::test_basic_write_lock_acquire_release'
'fast_lock::tests::fast_lock_tests::test_basic_read_lock_acquire_release'
)
for test_name in "${tests[@]}"; do
file_name=${test_name//::/_}
log="$evidence/$file_name.log"
set +e
timeout --signal=TERM --kill-after=10s 5m \
cargo test --locked -p rustfs-lock --lib -- --exact "$test_name" > "$log" 2>&1
result=$?
set -e
printf '%s\n' "$result" > "$evidence/$file_name.exit"
tail -n 80 "$log"
[[ "$result" -eq 0 ]]
grep -Eq '^[[:space:]]*Finished .*test.* profile' "$log"
grep -Fq 'Running unittests src/lib.rs ' "$log"
grep -Fxq 'running 1 test' "$log"
grep -Fxq "test $test_name ... ok" "$log"
grep -Fq 'test result: ok. 1 passed; 0 failed;' "$log"
if grep -Eq '^error(\[E[0-9]+\])?: ' "$log"; then
printf 'Unexpected Cargo or compiler error in control %s\n' "$test_name"
exit 1
fi
done
- name: Run and classify each expected red regression
shell: bash
timeout-minutes: 27
run: |
set -euo pipefail
evidence="$RUNNER_TEMP/fast-lock-stale-guard"
tests=(
'fast_lock::manager::tests::force_unlocked_exclusive_guard_drop_preserves_same_owner_successor'
'fast_lock::manager::tests::force_unlocked_exclusive_guard_release_preserves_same_owner_successor'
'fast_lock::manager::tests::force_unlock_releases_all_shared_guards_of_same_owner'
'fast_lock::manager::tests::force_unlocked_shared_guards_drop_preserves_same_owner_successor'
'fast_lock::manager::tests::force_unlocked_shared_guards_release_preserves_same_owner_successor'
)
messages=(
'after stale guard drop: successor must remain the sole holder'
'after stale guard release: successor must remain the sole holder'
'force unlock must release every shared guard of the owner'
'after first stale shared drop: successor must remain the sole holder'
'after first stale shared release: successor must remain the sole holder'
)
lines=(670 670 781 670 670)
left=(0 0 1 0 0)
right=(1 1 0 1 1)
failures=0
for index in "${!tests[@]}"; do
test_name=${tests[$index]}
file_name=${test_name//::/_}
message=${messages[$index]}
panic_line=${lines[$index]}
expected_left=${left[$index]}
expected_right=${right[$index]}
log="$evidence/$file_name.log"
set +e
timeout --signal=TERM --kill-after=10s 5m \
cargo test --locked -p rustfs-lock --lib -- --exact "$test_name" > "$log" 2>&1
result=$?
set -e
printf '%s\n' "$result" > "$evidence/$file_name.exit"
tail -n 100 "$log"
unexpected_errors=$(grep -E '^error(\[E[0-9]+\])?: ' "$log" | grep -Fv 'error: test failed,' || true)
panic_count=$(grep -Ec "^thread '.*' \([0-9]+\) panicked at " "$log" || true)
if [[ "$result" -ne 101 ]] \
|| [[ -n "$unexpected_errors" ]] \
|| [[ "$panic_count" -ne 1 ]] \
|| ! grep -Eq '^[[:space:]]*Finished .*test.* profile' "$log" \
|| ! grep -Fq 'Running unittests src/lib.rs ' "$log" \
|| ! grep -Fxq 'running 1 test' "$log" \
|| ! grep -Fxq "test $test_name ... FAILED" "$log" \
|| ! grep -Eq "^thread '$test_name' \([0-9]+\) panicked at crates/lock/src/fast_lock/manager\.rs:$panic_line:[0-9]+:$" "$log" \
|| ! grep -Eq "^assertion .left == right. failed: $message$" "$log" \
|| ! grep -Eq "^[[:space:]]*left: $expected_left$" "$log" \
|| ! grep -Eq "^[[:space:]]*right: $expected_right$" "$log" \
|| ! grep -Fq 'test result: FAILED. 0 passed; 1 failed;' "$log"; then
printf 'Unexpected outcome for %s\n' "$test_name" | tee "$evidence/$file_name.classification"
failures=$((failures + 1))
else
printf 'Expected red assertion observed for %s\n' "$test_name" | tee "$evidence/$file_name.classification"
fi
done
[[ "$failures" -eq 0 ]]
- name: Preserve logs and exit codes
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: fast-lock-stale-guard-${{ github.run_id }}
path: ${{ runner.temp }}/fast-lock-stale-guard/
if-no-files-found: error
retention-days: 14
+199
View File
@@ -661,4 +661,203 @@ mod tests {
manager.shutdown().await;
}
fn assert_registered_holder_blocks_writer(manager: &FastObjectLockManager, key: &ObjectKey, guard_id: u64, stage: &str) {
assert!(
manager.get_shard(key).is_guard_active(guard_id),
"{stage}: successor guard must remain registered"
);
assert_eq!(manager.current_lock_counts().0, 1, "{stage}: successor must remain the sole holder");
let contender = ObjectLockRequest::new_write(key.clone(), "contender");
assert!(
!manager.get_shard(key).try_fast_path_only(&contender),
"{stage}: successor must block another writer"
);
}
fn assert_free_for_new_writer(manager: &FastObjectLockManager, key: &ObjectKey, stage: &str) {
assert_eq!(manager.current_lock_counts().0, 0, "{stage}: no holder should remain");
let writer = ObjectLockRequest::new_write(key.clone(), "contender");
let shard = manager.get_shard(key);
assert!(shard.try_fast_path_only(&writer), "{stage}: a new writer must acquire");
assert_eq!(manager.current_lock_counts().0, 1, "{stage}: new writer must hold the lock");
assert!(
shard.release_lock(key, &writer.owner, LockMode::Exclusive),
"{stage}: release the new writer"
);
assert_eq!(manager.current_lock_counts().0, 0, "{stage}: new writer must release the lock");
}
fn assert_no_registered_guards(manager: &FastObjectLockManager) {
let active: usize = manager.shards.iter().map(|shard| shard.active_guard_count()).sum();
assert_eq!(active, 0, "all guards in the isolated manager must be unregistered");
}
fn assert_successor_release_or_drop(
manager: &FastObjectLockManager,
key: &ObjectKey,
mut successor: FastLockGuard,
release_first: bool,
) {
let successor_id = successor.guard_id();
if release_first {
assert!(successor.release(), "successor must release its own physical lock");
assert!(successor.is_released(), "successor must report its explicit release");
assert!(
!manager.get_shard(key).is_guard_active(successor_id),
"released successor must no longer be registered"
);
assert_free_for_new_writer(manager, key, "after successor release and before drop");
}
drop(successor);
assert!(
!manager.get_shard(key).is_guard_active(successor_id),
"dropped successor must no longer be registered"
);
assert_free_for_new_writer(manager, key, "after successor drop");
}
async fn stale_exclusive_guard_does_not_release_successor(release_first: bool, release_successor: bool) {
let manager = FastObjectLockManager::new();
let key = ObjectKey::new("bucket", "force-unlock-exclusive-successor");
let mut stale = manager
.acquire_write_lock(key.clone(), "same-owner")
.await
.expect("acquire original exclusive guard");
let stale_id = stale.guard_id();
assert_eq!(manager.force_unlock(&key), 1);
let successor = manager
.acquire_write_lock(key.clone(), "same-owner")
.await
.expect("acquire exclusive successor after force unlock");
assert_ne!(stale.guard_id(), successor.guard_id());
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "before stale guard action");
if release_first {
let _ = stale.release();
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "after stale guard release");
}
drop(stale);
assert!(
!manager.get_shard(&key).is_guard_active(stale_id),
"dropped stale guard must be unregistered"
);
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "after stale guard drop");
assert_successor_release_or_drop(&manager, &key, successor, release_successor);
assert_no_registered_guards(&manager);
manager.shutdown().await;
}
#[tokio::test]
async fn force_unlocked_exclusive_guard_drop_preserves_same_owner_successor() {
stale_exclusive_guard_does_not_release_successor(false, true).await;
stale_exclusive_guard_does_not_release_successor(false, false).await;
}
#[tokio::test]
async fn force_unlocked_exclusive_guard_release_preserves_same_owner_successor() {
stale_exclusive_guard_does_not_release_successor(true, true).await;
stale_exclusive_guard_does_not_release_successor(true, false).await;
}
#[tokio::test]
async fn force_unlock_releases_all_shared_guards_of_same_owner() {
let manager = FastObjectLockManager::new();
let key = ObjectKey::new("bucket", "force-unlock-shared-owners");
let first = manager
.acquire_read_lock(key.clone(), "same-owner")
.await
.expect("acquire first shared guard");
let second = manager
.acquire_read_lock(key.clone(), "same-owner")
.await
.expect("acquire second shared guard");
let first_id = first.guard_id();
let second_id = second.guard_id();
assert_eq!(manager.current_lock_counts().0, 2);
assert_eq!(manager.force_unlock(&key), 1, "one distinct owner was force unlocked");
assert_eq!(
manager.current_lock_counts().0,
0,
"force unlock must release every shared guard of the owner"
);
assert_free_for_new_writer(&manager, &key, "after shared force unlock");
drop(first);
assert!(
!manager.get_shard(&key).is_guard_active(first_id),
"first dropped stale guard must be unregistered"
);
drop(second);
assert!(
!manager.get_shard(&key).is_guard_active(second_id),
"second dropped stale guard must be unregistered"
);
assert_free_for_new_writer(&manager, &key, "after stale shared guards drop");
assert_no_registered_guards(&manager);
manager.shutdown().await;
}
async fn stale_shared_guards_do_not_release_successor(release_first: bool, release_successor: bool) {
let manager = FastObjectLockManager::new();
let key = ObjectKey::new("bucket", "force-unlock-shared-successor");
let mut first = manager
.acquire_read_lock(key.clone(), "same-owner")
.await
.expect("acquire first shared guard");
let mut second = manager
.acquire_read_lock(key.clone(), "same-owner")
.await
.expect("acquire second shared guard");
let first_id = first.guard_id();
let second_id = second.guard_id();
assert_eq!(manager.force_unlock(&key), 1);
// Clear both old holders even on the baseline, so the following
// assertion isolates stale guard release from force-unlock completeness.
let _ = manager.force_unlock(&key);
assert_eq!(manager.current_lock_counts().0, 0);
let successor = manager
.acquire_read_lock(key.clone(), "same-owner")
.await
.expect("acquire shared successor after force unlock");
assert_ne!(first.guard_id(), successor.guard_id());
assert_ne!(second.guard_id(), successor.guard_id());
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "before stale shared guard action");
if release_first {
let _ = first.release();
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "after first stale shared release");
let _ = second.release();
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "after second stale shared release");
}
drop(first);
assert!(
!manager.get_shard(&key).is_guard_active(first_id),
"first dropped stale guard must be unregistered"
);
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "after first stale shared drop");
drop(second);
assert!(
!manager.get_shard(&key).is_guard_active(second_id),
"second dropped stale guard must be unregistered"
);
assert_registered_holder_blocks_writer(&manager, &key, successor.guard_id(), "after second stale shared drop");
assert_successor_release_or_drop(&manager, &key, successor, release_successor);
assert_no_registered_guards(&manager);
manager.shutdown().await;
}
#[tokio::test]
async fn force_unlocked_shared_guards_drop_preserves_same_owner_successor() {
stale_shared_guards_do_not_release_successor(false, true).await;
stale_shared_guards_do_not_release_successor(false, false).await;
}
#[tokio::test]
async fn force_unlocked_shared_guards_release_preserves_same_owner_successor() {
stale_shared_guards_do_not_release_successor(true, true).await;
stale_shared_guards_do_not_release_successor(true, false).await;
}
}