Compare commits

..

1 Commits

Author SHA1 Message Date
overtrue ba613b3b88 test: align transition fixtures and KMS context expectations 2026-09-10 03:13:47 +08:00
5 changed files with 123 additions and 45 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
sha256-darwin=874c881d7b45f12378a5817c7f42c95c4981960a2ec9ce12dcf4af239ae1f9d5
sha256-linux=9351e25b45bf7dfce18b951a5e3740225f457cacc53b8bf9f500f6947763ec0e
sha256-linux=9515861be899ceb10e2e0ef93c34208bb7a7a8a7f8067a02db4cfba23270ebd6
+97 -8
View File
@@ -13807,7 +13807,17 @@ mod transition_commit_failure_tests {
#[tokio::test]
#[serial_test::serial]
async fn restore_failure_after_snapshot_cleans_exact_generation_and_returns_primary_error() {
let (_temp_dirs, disk_stores, set_disks) = hermetic_set_disks(4).await;
assert_restore_failure_cleanup_boundary(true).await;
}
#[tokio::test]
#[serial_test::serial]
async fn restore_failure_after_snapshot_preserves_corrupt_known_transition_metadata() {
assert_restore_failure_cleanup_boundary(false).await;
}
async fn assert_restore_failure_cleanup_boundary(legacy_unknown: bool) {
let (temp_dirs, disk_stores, set_disks) = hermetic_set_disks(4).await;
let bucket = "restore-post-snapshot-cleanup-bucket";
let object = "object.bin";
for disk in &disk_stores {
@@ -13816,7 +13826,15 @@ mod transition_commit_failure_tests {
let mut reader = PutObjReader::from_vec(b"post-snapshot cleanup source".repeat(1024));
let original = set_disks
.put_object(bucket, object, &mut reader, &ObjectOptions::default())
.put_object(
bucket,
object,
&mut reader,
&ObjectOptions {
write_completion: WriteCompletion::TailDrained,
..Default::default()
},
)
.await
.expect("source object should be written");
let tier_name = format!("COLDTIER{}", &Uuid::new_v4().simple().to_string()[..8]).to_uppercase();
@@ -13857,16 +13875,70 @@ mod transition_commit_failure_tests {
.await
.expect("transitioned metadata should be readable")
.into_owned();
let known_state = source_fi.transition_version_state;
assert_ne!(known_state, rustfs_filemeta::TransitionVersionState::Unknown);
source_fi.metadata.extend(restore_metadata(operation_id, true));
rustfs_utils::http::insert_str(
&mut source_fi.metadata,
rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
"invalid".to_string(),
);
set_disks
.update_object_meta(bucket, object, source_fi, &online_disks)
.await
.expect("invalid backend identity fixture should be persisted");
.expect("restore markers should be persisted");
// Normal writes reject damage to a reconciled binding. Model on-disk
// corruption directly, with and without the legacy missing-state field.
let mut corrupted_metadata = Vec::new();
for temp_dir in &temp_dirs {
let metadata_path = temp_dir.path().join(bucket).join(object).join(STORAGE_FORMAT_FILE);
let encoded = tokio::fs::read(&metadata_path)
.await
.expect("transition metadata should be readable");
let mut metadata = FileMeta::load(&encoded).expect("transition metadata should decode");
let (version_index, mut version) = metadata
.find_version(original.version_id)
.expect("transitioned version should exist");
let object_meta = version.object.as_mut().expect("transitioned version should be an object");
rustfs_utils::http::insert_bytes(
&mut object_meta.meta_sys,
rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID,
b"invalid".to_vec(),
);
if legacy_unknown {
rustfs_utils::http::remove_bytes(
&mut object_meta.meta_sys,
rustfs_utils::http::SUFFIX_TRANSITIONED_VERSION_STATE,
);
}
metadata.versions[version_index] =
rustfs_filemeta::FileMetaShallowVersion::try_from(version).expect("corrupt fixture should re-encode");
tokio::fs::write(&metadata_path, metadata.marshal_msg().expect("corrupt fixture should encode"))
.await
.expect("corrupt fixture should be written");
let persisted = tokio::fs::read(&metadata_path)
.await
.expect("corrupt fixture should be readable");
let fixture = FileMeta::load(&persisted)
.expect("corrupt fixture should decode")
.find_version(original.version_id)
.expect("corrupt version should exist")
.1
.into_fileinfo(bucket, object, true)
.expect("corrupt version should decode");
assert_eq!(
fixture.transition_version_state,
if legacy_unknown {
rustfs_filemeta::TransitionVersionState::Unknown
} else {
known_state
}
);
assert_eq!(
rustfs_utils::http::get_str(&fixture.metadata, rustfs_utils::http::SUFFIX_TRANSITION_TIER_DESTINATION_ID),
Some("invalid".to_string())
);
for (key, value) in restore_metadata(operation_id, true) {
assert_eq!(fixture.metadata.get(&key), Some(&value), "fixture must retain restore marker {key}");
}
corrupted_metadata.push((metadata_path, persisted));
}
set_disks.invalidate_get_object_metadata_cache(bucket, object).await;
let mut opts = ObjectOptions::default();
@@ -13889,6 +13961,23 @@ mod transition_commit_failure_tests {
.await
.expect("cleanup should leave the transitioned object readable");
assert_eq!(cleaned.transitioned_object.status, TRANSITION_COMPLETE);
if !legacy_unknown {
// Known bindings with corrupt identities must be repaired before
// cleanup; rejection must preserve both the binding and markers.
for (key, value) in restore_metadata(operation_id, true) {
assert_eq!(cleaned.user_defined.get(&key), Some(&value), "cleanup must preserve restore marker {key}");
}
for (metadata_path, before) in corrupted_metadata {
assert_eq!(
tokio::fs::read(metadata_path)
.await
.expect("rejected cleanup metadata should remain readable"),
before,
"rejected cleanup must leave corrupt known metadata unchanged"
);
}
return;
}
assert!(!cleaned.user_defined.contains_key(s3s::header::X_AMZ_RESTORE.as_str()));
assert!(
rustfs_utils::http::get_str(cleaned.user_defined.as_ref(), rustfs_utils::http::SUFFIX_RESTORE_OPERATION_ID,)
+19 -12
View File
@@ -19057,27 +19057,34 @@ mod tests {
.await
.expect("transition metadata should be readable");
let mut metadata = FileMeta::load(&encoded).expect("transition metadata should decode");
let mut transitioned = metadata
.get_all_file_info_versions(bucket, object, true)
.expect("transitioned versions should decode")
.versions
.into_iter()
.find(|version| version.version_id == history.version_id)
let (version_index, mut transitioned) = metadata
.find_version(history.version_id)
.expect("transitioned history should exist");
transitioned.transition_version_state = rustfs_filemeta::TransitionVersionState::Unknown;
rustfs_utils::http::metadata_compat::remove_str(
&mut transitioned.metadata,
// Rewrite the serialized record to model legacy metadata;
// ordinary writes preserve an already reconciled state.
rustfs_utils::http::metadata_compat::remove_bytes(
&mut transitioned.object.as_mut().expect("history should be an object").meta_sys,
rustfs_utils::http::metadata_compat::SUFFIX_TRANSITIONED_VERSION_STATE,
);
metadata
.add_version(transitioned)
.expect("unknown state should replace the transitioned version");
metadata.versions[version_index] = rustfs_filemeta::FileMetaShallowVersion::try_from(transitioned)
.expect("legacy history should re-encode");
tokio::fs::write(
&metadata_path,
metadata.marshal_msg().expect("unknown transition metadata should encode"),
)
.await
.expect("unknown transition metadata should be written");
let encoded = tokio::fs::read(&metadata_path)
.await
.expect("legacy transition metadata should be readable");
let legacy = FileMeta::load(&encoded)
.expect("legacy transition metadata should decode")
.find_version(history.version_id)
.expect("legacy history should exist")
.1
.into_fileinfo(bucket, object, true)
.expect("legacy history should decode");
assert_eq!(legacy.transition_version_state, rustfs_filemeta::TransitionVersionState::Unknown);
}
let lifecycle_event = crate::bucket::lifecycle::lifecycle::Event {
action: rustfs_scanner_metrics::metrics::IlmAction::DeleteAllVersionsAction,
+5 -2
View File
@@ -5939,8 +5939,11 @@ mod tests {
})
.await
.expect_err("mismatched kms context should fail");
assert_eq!(err.code, S3ErrorCode::InternalError);
assert_eq!(err.message, ApiError::error_code_to_message(&S3ErrorCode::InternalError));
assert_eq!(err.code, S3ErrorCode::InvalidRequest);
assert_eq!(
err.message,
"Encryption context mismatch: Context mismatch for key 'tenant': expected 'alpha', got 'beta'"
);
assert_eq!(super::kms_data_plane_error_class(&err), "context_mismatch");
manager.stop().await.expect("kms service should stop cleanly");
+1 -22
View File
@@ -59,7 +59,7 @@ def expected_results(mode: str, event: str, ref: str) -> dict[str, str]:
expected.update({job: "success" if mode == "full" else "skipped" for job in CODE_JOBS})
rio = mode == "full" and event in ("schedule", "workflow_dispatch")
expected.update({job: "success" if rio else "skipped" for job in OPTIONAL_JOBS[:2]})
full = mode == "full" and (event in ("merge_group", "workflow_dispatch") or (event == "push" and ref in ("refs/heads/main", "refs/heads/release")))
full = mode == "full" and (event in ("merge_group", "workflow_dispatch") or (event == "push" and ref == "refs/heads/main"))
expected["e2e-full"] = "success" if full else "skipped"
return expected
@@ -219,27 +219,6 @@ class SelfTests(unittest.TestCase):
bad = {**good, "classify-changes": {"result": "success", "outputs": selection}}
self.assertTrue(verify_results(bad, event, "refs/heads/main"))
def test_full_e2e_gate_preserves_workflow_branch_and_event_scope(self):
for event, ref, required in (
("push", "refs/heads/main", "success"),
("push", "refs/heads/release", "success"),
("push", "refs/heads/feature", "skipped"),
("push", "refs/heads/release-candidate", "skipped"),
("push", "refs/tags/release", "skipped"),
("pull_request", "refs/pull/1/merge", "skipped"),
("schedule", "refs/heads/release", "skipped"),
("workflow_dispatch", "refs/heads/feature", "success"),
("merge_group", "refs/heads/gh-readonly-queue/release/pr-1", "success"),
):
with self.subTest(event=event, ref=ref):
expected = expected_results("full", event, ref)
self.assertEqual(expected["e2e-full"], required)
needs = {job: {"result": result} for job, result in expected.items()}
needs["classify-changes"]["outputs"] = {"mode": "full"}
for result in ("success", "skipped", "failure", "cancelled"):
needs["e2e-full"]["result"] = result
self.assertEqual(verify_results(needs, event, ref) == [], result == required)
def test_repository_wiring_and_missing_dependency_regression(self):
self.assertEqual(check_workflow(ROOT), [])
with tempfile.TemporaryDirectory() as directory: