Skip to content

Commit f3a2662

Browse files
AlexStocksOmX
andcommitted
test(ci): normalize contract fixtures across platforms
Keep CI contract mutants independent of checkout line endings. Normalize CRLF fixture text to LF at the shared include_str boundary so Windows and Linux exercise the same workflow, audit, runner, Makefile, and Python source mutations. TDD: the exact Windows ci_contract suite failed 16/19 on the parent tree because three LF-based mutants were not constructed from CRLF fixtures. The normalized final tree passes Windows 19/19 and WSL/Linux 23/23. Constraint: Test-only fixture handling in tools/compat/tests/ci_contract.rs. No workflow, runner, Oracle, cluster, storage, Raft, or Vector runtime behavior changes. Tested: Windows and WSL full ci_contract; cargo fmt; clippy all targets/features with warnings denied; validate_sdd self-test and real; git diff checks; test-guard self-review. Related: REQ-STABILITY-003, REQ-VECTOR-003, REQ-VECTOR-005, #421 Co-authored-by: OmX <omx@oh-my-codex.dev> Signed-off-by: Xin.Zh <alexstocks@foxmail.com>
1 parent 9106ec5 commit f3a2662

1 file changed

Lines changed: 64 additions & 41 deletions

File tree

tools/compat/tests/ci_contract.rs

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// See the License for the specific language governing permissions and
1616
// limitations under the License.
1717

18+
use std::borrow::Cow;
1819
use std::collections::BTreeMap;
1920
#[cfg(target_os = "linux")]
2021
use std::fs;
@@ -87,6 +88,14 @@ const GRPCURL_CHECKSUM_VERIFY: &str = "| (cd \"$RUNNER_TEMP\" && sha256sum -c -)
8788
const GRPCURL_EXTRACT: &str =
8889
"tar -xzf \"$RUNNER_TEMP/grpcurl.tar.gz\" -C \"$RUNNER_TEMP\" grpcurl";
8990

91+
fn normalized_fixture(source: &str) -> Cow<'_, str> {
92+
if source.contains("\r\n") {
93+
Cow::Owned(source.replace("\r\n", "\n"))
94+
} else {
95+
Cow::Borrowed(source)
96+
}
97+
}
98+
9099
fn make_logical_lines(source: &str) -> Vec<String> {
91100
let mut lines = Vec::new();
92101
let mut logical_line = String::new();
@@ -637,7 +646,7 @@ fn validate_rkyv_audit_governance(source: &str) -> Result<(), String> {
637646

638647
#[test]
639648
fn vector_cluster_required_job_is_unique_and_fail_closed() {
640-
let workflow_source = include_str!("../../../.github/workflows/ci.yml");
649+
let workflow_source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
641650
assert_eq!(
642651
workflow_source
643652
.lines()
@@ -652,7 +661,8 @@ fn vector_cluster_required_job_is_unique_and_fail_closed() {
652661
1
653662
);
654663

655-
let workflow: Workflow = yaml_serde::from_str(workflow_source).expect("CI workflow must parse");
664+
let workflow: Workflow =
665+
yaml_serde::from_str(&workflow_source).expect("CI workflow must parse");
656666
validate_vector_cluster_workflow(&workflow).expect("required Vector cluster job must be exact");
657667

658668
for (name, from, to) in [
@@ -668,7 +678,11 @@ fn vector_cluster_required_job_is_unique_and_fail_closed() {
668678
),
669679
] {
670680
let mutant = workflow_source.replacen(from, to, 1);
671-
assert_ne!(mutant, workflow_source, "failed to construct {name} mutant");
681+
assert_ne!(
682+
mutant,
683+
workflow_source.as_ref(),
684+
"failed to construct {name} mutant"
685+
);
672686
let workflow: Workflow =
673687
yaml_serde::from_str(&mutant).expect("cluster condition mutant must parse");
674688
assert!(
@@ -680,23 +694,24 @@ fn vector_cluster_required_job_is_unique_and_fail_closed() {
680694

681695
#[test]
682696
fn rkyv_static_analysis_gate_is_pr_blocking_and_ordered() {
683-
let workflow_source = include_str!("../../../.github/workflows/ci.yml");
697+
let workflow_source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
684698
assert_eq!(
685699
workflow_source
686700
.lines()
687701
.filter(|line| *line == " static-analysis:")
688702
.count(),
689703
1
690704
);
691-
let workflow: Workflow = yaml_serde::from_str(workflow_source).expect("CI workflow must parse");
705+
let workflow: Workflow =
706+
yaml_serde::from_str(&workflow_source).expect("CI workflow must parse");
692707
validate_rkyv_static_analysis_workflow(&workflow)
693708
.expect("static analysis must contain the required fail-closed rkyv gate");
694709
}
695710

696711
#[test]
697712
fn rkyv_static_analysis_contract_rejects_removal_reordering_and_continue_on_error() {
698-
let source = include_str!("../../../.github/workflows/ci.yml");
699-
let workflow: Workflow = yaml_serde::from_str(source).expect("CI workflow must parse");
713+
let source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
714+
let workflow: Workflow = yaml_serde::from_str(&source).expect("CI workflow must parse");
700715

701716
let mut no_pull_request = workflow.clone();
702717
no_pull_request.triggers.remove("pull_request");
@@ -778,7 +793,7 @@ fn rkyv_static_analysis_contract_rejects_removal_reordering_and_continue_on_erro
778793

779794
#[test]
780795
fn rkyv_static_analysis_contract_rejects_conditional_job_and_critical_steps() {
781-
let source = include_str!("../../../.github/workflows/ci.yml").replace("\r\n", "\n");
796+
let source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
782797
let mut accepted = Vec::new();
783798
for (name, needle, replacement) in [
784799
(
@@ -835,8 +850,10 @@ fn rkyv_static_analysis_contract_rejects_conditional_job_and_critical_steps() {
835850

836851
#[test]
837852
fn rkyv_sentinel_contract_rejects_ignored_stdout() {
838-
let source = include_str!("../../../scripts/ci/check-rkyv-reachability.sh");
839-
validate_rkyv_sentinel_source(source).expect("rkyv sentinel source must be fail closed");
853+
let source = normalized_fixture(include_str!(
854+
"../../../scripts/ci/check-rkyv-reachability.sh"
855+
));
856+
validate_rkyv_sentinel_source(&source).expect("rkyv sentinel source must be fail closed");
840857

841858
let ignored_stdout = source.replacen(
842859
"if [[ -s \"$stdout_file\" ]]; then",
@@ -848,8 +865,8 @@ fn rkyv_sentinel_contract_rejects_ignored_stdout() {
848865

849866
#[test]
850867
fn rkyv_audit_ignore_has_owner_path_status_and_removal_condition() {
851-
let source = include_str!("../../../.cargo/audit.toml");
852-
validate_rkyv_audit_governance(source)
868+
let source = normalized_fixture(include_str!("../../../.cargo/audit.toml"));
869+
validate_rkyv_audit_governance(&source)
853870
.expect("rkyv advisory ignore must carry accurate governance");
854871

855872
let removed_advisory = source.replacen(" \"RUSTSEC-2026-0235\",\n", "", 1);
@@ -861,9 +878,9 @@ fn rkyv_audit_ignore_has_owner_path_status_and_removal_condition() {
861878

862879
#[test]
863880
fn rkyv_security_workflow_remains_scheduled_visibility() {
864-
let source = include_str!("../../../.github/workflows/security.yml");
881+
let source = normalized_fixture(include_str!("../../../.github/workflows/security.yml"));
865882
assert!(source.contains(" schedule:"));
866-
let workflow: Workflow = yaml_serde::from_str(source).expect("security workflow must parse");
883+
let workflow: Workflow = yaml_serde::from_str(&source).expect("security workflow must parse");
867884
let job = workflow
868885
.jobs
869886
.get("cargo-audit")
@@ -878,9 +895,8 @@ fn rkyv_security_workflow_remains_scheduled_visibility() {
878895

879896
#[test]
880897
fn vector_cluster_workflow_rejects_grpcurl_moved_to_unrelated_job() {
881-
let mut workflow: Workflow =
882-
yaml_serde::from_str(include_str!("../../../.github/workflows/ci.yml"))
883-
.expect("CI workflow must parse");
898+
let source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
899+
let mut workflow: Workflow = yaml_serde::from_str(&source).expect("CI workflow must parse");
884900
let grpcurl_step = {
885901
let required_job = workflow
886902
.jobs
@@ -907,9 +923,8 @@ fn vector_cluster_workflow_rejects_grpcurl_moved_to_unrelated_job() {
907923

908924
#[test]
909925
fn vector_cluster_workflow_rejects_grpcurl_extraction_before_checksum() {
910-
let mut workflow: Workflow =
911-
yaml_serde::from_str(include_str!("../../../.github/workflows/ci.yml"))
912-
.expect("CI workflow must parse");
926+
let source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
927+
let mut workflow: Workflow = yaml_serde::from_str(&source).expect("CI workflow must parse");
913928
mutate_grpcurl_command(&mut workflow, |command| {
914929
let without_extract = command.replacen(&format!("{GRPCURL_EXTRACT}\n"), "", 1);
915930
without_extract.replacen(
@@ -926,9 +941,8 @@ fn vector_cluster_workflow_rejects_grpcurl_extraction_before_checksum() {
926941

927942
#[test]
928943
fn vector_cluster_workflow_rejects_checksum_literal_without_verification() {
929-
let mut workflow: Workflow =
930-
yaml_serde::from_str(include_str!("../../../.github/workflows/ci.yml"))
931-
.expect("CI workflow must parse");
944+
let source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
945+
let mut workflow: Workflow = yaml_serde::from_str(&source).expect("CI workflow must parse");
932946
mutate_grpcurl_command(&mut workflow, |command| {
933947
command.replacen(
934948
GRPCURL_CHECKSUM_VERIFY,
@@ -971,7 +985,8 @@ fn vector_cluster_runner_and_collection_are_fail_closed() {
971985
}
972986
assert!(!runner.contains("command -v grpcurl"));
973987

974-
let cluster_tests = include_str!("../../../tests/python/test_vector_cluster.py");
988+
let cluster_tests =
989+
normalized_fixture(include_str!("../../../tests/python/test_vector_cluster.py"));
975990
assert!(!cluster_tests.contains("pytest.mark.skipif"));
976991
assert!(cluster_tests.contains("@pytest.mark.parametrize"));
977992
assert!(cluster_tests.contains("signal.SIGTERM"));
@@ -1141,7 +1156,7 @@ fn vector_cluster_validators_reject_collection_totals_and_cleanup_drift() {
11411156

11421157
#[test]
11431158
fn vector_differential_required_job_is_unique_and_fail_closed() {
1144-
let workflow_source = include_str!("../../../.github/workflows/ci.yml");
1159+
let workflow_source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
11451160
assert_eq!(
11461161
workflow_source
11471162
.lines()
@@ -1155,7 +1170,8 @@ fn vector_differential_required_job_is_unique_and_fail_closed() {
11551170
.count(),
11561171
1
11571172
);
1158-
let workflow: Workflow = yaml_serde::from_str(workflow_source).expect("CI workflow must parse");
1173+
let workflow: Workflow =
1174+
yaml_serde::from_str(&workflow_source).expect("CI workflow must parse");
11591175
let matching = workflow
11601176
.jobs
11611177
.iter()
@@ -1191,8 +1207,9 @@ fn vector_differential_required_job_is_unique_and_fail_closed() {
11911207

11921208
#[test]
11931209
fn required_jobs_reject_unversioned_actions() {
1194-
let workflow_source = include_str!("../../../.github/workflows/ci.yml");
1195-
let workflow: Workflow = yaml_serde::from_str(workflow_source).expect("CI workflow must parse");
1210+
let workflow_source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
1211+
let workflow: Workflow =
1212+
yaml_serde::from_str(&workflow_source).expect("CI workflow must parse");
11961213
validate_required_job_action_versions(&workflow)
11971214
.expect("required jobs must use versioned action runners");
11981215

@@ -1216,15 +1233,16 @@ fn required_jobs_reject_unversioned_actions() {
12161233

12171234
#[test]
12181235
fn vector_differential_upload_requires_explicit_missing_file_error() {
1219-
let workflow_source = include_str!("../../../.github/workflows/ci.yml");
1236+
let workflow_source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
12201237
let workflow: Workflow =
1221-
yaml_serde::from_str(workflow_source).expect("CI workflow must remain valid YAML");
1238+
yaml_serde::from_str(&workflow_source).expect("CI workflow must remain valid YAML");
12221239
validate_vector_differential_workflow(&workflow)
12231240
.expect("explicit error-on-missing provenance must remain accepted");
12241241

12251242
let missing_policy = workflow_source.replacen("\n if-no-files-found: error", "", 1);
12261243
assert_ne!(
1227-
missing_policy, workflow_source,
1244+
missing_policy,
1245+
workflow_source.as_ref(),
12281246
"failed to construct missing upload policy mutant"
12291247
);
12301248
let workflow: Workflow = yaml_serde::from_str(&missing_policy)
@@ -1237,7 +1255,7 @@ fn vector_differential_upload_requires_explicit_missing_file_error() {
12371255

12381256
#[test]
12391257
fn vector_differential_make_undefined_variable_cannot_split_path_ignore() {
1240-
let makefile = include_str!("../../../tests/Makefile");
1258+
let makefile = normalized_fixture(include_str!("../../../tests/Makefile"));
12411259
let undetected = [":=", "=", "?=", "+="]
12421260
.into_iter()
12431261
.filter(|operator| {
@@ -1255,7 +1273,7 @@ fn vector_differential_make_undefined_variable_cannot_split_path_ignore() {
12551273

12561274
#[test]
12571275
fn vector_differential_make_simple_assignment_freezes_earlier_value() {
1258-
let makefile = include_str!("../../../tests/Makefile");
1276+
let makefile = normalized_fixture(include_str!("../../../tests/Makefile"));
12591277
let mutant = format!(
12601278
"{makefile}\nDIFF_TEST := python/test_vector_set_differential.py\nDIFF_IGNORE := --ignore=$(DIFF_TEST)\nDIFF_TEST := python/test_other.py\ntest-immediate:\n\tpytest $(DIFF_IGNORE)"
12611279
);
@@ -1267,7 +1285,7 @@ fn vector_differential_make_simple_assignment_freezes_earlier_value() {
12671285

12681286
#[test]
12691287
fn vector_differential_make_tab_recipe_scans_env_prefixed_command() {
1270-
let makefile = include_str!("../../../tests/Makefile");
1288+
let makefile = normalized_fixture(include_str!("../../../tests/Makefile"));
12711289
let mutant = format!(
12721290
"{makefile}\ntest-env-ignore:\n\tPYTEST_ADDOPTS=--ignore=python/test_vector_set_differential.py pytest python/"
12731291
);
@@ -1279,13 +1297,16 @@ fn vector_differential_make_tab_recipe_scans_env_prefixed_command() {
12791297

12801298
#[test]
12811299
fn vector_differential_rejects_supervisor_bypass_and_unsafe_uploads() {
1282-
let workflow_source = include_str!("../../../.github/workflows/ci.yml");
1283-
let workflow: Workflow = yaml_serde::from_str(workflow_source).expect("CI workflow must parse");
1300+
let workflow_source = normalized_fixture(include_str!("../../../.github/workflows/ci.yml"));
1301+
let workflow: Workflow =
1302+
yaml_serde::from_str(&workflow_source).expect("CI workflow must parse");
12841303
validate_vector_differential_workflow(&workflow)
12851304
.expect("trusted differential workflow must be fail closed");
12861305

1287-
let runner_source = include_str!("../../../scripts/compat/run-vector-differential.sh");
1288-
validate_vector_differential_runner_source(runner_source)
1306+
let runner_source = normalized_fixture(include_str!(
1307+
"../../../scripts/compat/run-vector-differential.sh"
1308+
));
1309+
validate_vector_differential_runner_source(&runner_source)
12891310
.expect("trusted differential runner must obtain its runtime from the verifier");
12901311

12911312
let runner_bypass = workflow_source.replacen(
@@ -1400,8 +1421,8 @@ fn vector_differential_rejects_supervisor_bypass_and_unsafe_uploads() {
14001421

14011422
#[test]
14021423
fn vector_differential_fast_job_uses_marker_ownership_not_path_ignore() {
1403-
let makefile = include_str!("../../../tests/Makefile");
1404-
assert!(!has_vector_differential_path_ignore(makefile));
1424+
let makefile = normalized_fixture(include_str!("../../../tests/Makefile"));
1425+
assert!(!has_vector_differential_path_ignore(&makefile));
14051426
assert!(makefile.contains("-m \"not raw_vector_protocol\""));
14061427
for mutant in [
14071428
format!("{makefile}\npytest --ignore=python/test_vector_set_differential.py"),
@@ -1426,7 +1447,9 @@ fn vector_differential_fast_job_uses_marker_ownership_not_path_ignore() {
14261447
"marker-only ownership must not be treated as a path ignore"
14271448
);
14281449

1429-
let runner = include_str!("../../../scripts/compat/run-vector-differential.sh");
1450+
let runner = normalized_fixture(include_str!(
1451+
"../../../scripts/compat/run-vector-differential.sh"
1452+
));
14301453
for required in [
14311454
"KIWI_COMPAT_REQUIRE_ORACLE",
14321455
"kiwi-required-vector-jobs",

0 commit comments

Comments
 (0)