diff --git a/RFCs/0079-qtest-tap-output.md b/RFCs/0079-qtest-tap-output.md index af86be9..b7b8244 100644 --- a/RFCs/0079-qtest-tap-output.md +++ b/RFCs/0079-qtest-tap-output.md @@ -9,7 +9,7 @@ ## CLI 契约 -`qtest --tap FILE_OR_DIRECTORY...` 输出 TAP version 13。输入文件先按现有递归路径排序,然后每个文件恰输出一条编号记录:成功为 `ok N - path`,失败为 `not ok N - path`。失败详情紧随其后按 `# ` 注释行输出;详情中的换行分割为多条注释,避免伪造新的测试记录。末尾输出 `1..N`,其中 `N` 是发现的测试文件数。 +`qtest --tap FILE_OR_DIRECTORY...` 输出 TAP version 13。所有输入递归发现的文件合并后按路径全局排序并去重,然后每个文件恰输出一条编号记录:成功为 `ok N - path`,失败为 `not ok N - path`。失败详情紧随其后按 `# ` 注释行输出;详情中的换行分割为多条注释,避免伪造新的测试记录。末尾输出 `1..N`,其中 `N` 是去重后发现的测试文件数。 `--tap` 与 `--json` 互斥,冲突以用法错误退出码 2 结束。读取错误或找不到测试文件时,在已输出 TAP 版本后输出 `Bail out!` 并以非零状态退出。测试失败仍返回非零状态;`--stats` 继续只写标准错误,不污染 TAP 标准输出。 diff --git a/src/bin/qtest.rs b/src/bin/qtest.rs index 2fcd7e0..af60be0 100644 --- a/src/bin/qtest.rs +++ b/src/bin/qtest.rs @@ -112,6 +112,8 @@ fn main() -> ExitCode { return ExitCode::from(1); } } + files.sort(); + files.dedup(); if files.is_empty() { if tap { println!("TAP version 13"); diff --git a/src/vm.rs b/src/vm.rs index 106598a..52fa71a 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -870,23 +870,21 @@ impl Vm { }); } Value::String(value) => { - let values: Rc> = Rc::new( - value - .chars() - .map(|character| { - Value::String(Rc::from(character.to_string())) - }) - .collect(), - ); - let position = if step < 0 { - values.len().saturating_sub(1) - } else { - 0 - }; frame.iterators.push(Iteration { kind: IterationKind::String { - values, - position, + values: Rc::new( + value + .chars() + .map(|character| { + Value::String(Rc::from(character.to_string())) + }) + .collect(), + ), + position: if step < 0 { + value.chars().count().saturating_sub(1) + } else { + 0 + }, step, }, }); diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index 72cfc90..e788cb3 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -189,9 +189,54 @@ fn qtest_tap_output_is_deterministic_and_describes_failures() { assert_eq!( String::from_utf8_lossy(&output.stdout), format!( - "TAP version 13\nnot ok 1 - {}/a-fail.qc\n# final value was 1, expected true\nok 2 - {}/b-pass.qc\n1..2\n", - temp.display(), - temp.display() + "TAP version 13\nnot ok 1 - {}\n# final value was 1, expected true\nok 2 - {}\n1..2\n", + temp.join("a-fail.qc").display(), + temp.join("b-pass.qc").display() + ) + ); + let reversed = Command::new(bin("qtest")) + .args([ + "--tap", + temp.join("b-pass.qc").to_str().unwrap(), + temp.join("a-fail.qc").to_str().unwrap(), + temp.join("a-fail.qc").to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(!reversed.status.success()); + assert_eq!( + String::from_utf8_lossy(&reversed.stdout), + format!( + "TAP version 13\nnot ok 1 - {}\n# final value was 1, expected true\nok 2 - {}\n1..2\n", + temp.join("a-fail.qc").display(), + temp.join("b-pass.qc").display() + ) + ); + let reversed_json = Command::new(bin("qtest")) + .args([ + "--json", + temp.join("b-pass.qc").to_str().unwrap(), + temp.join("a-fail.qc").to_str().unwrap(), + temp.join("a-fail.qc").to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(!reversed_json.status.success()); + let failed_path = temp + .join("a-fail.qc") + .display() + .to_string() + .replace('\\', "\\\\"); + let passed_path = temp + .join("b-pass.qc") + .display() + .to_string() + .replace('\\', "\\\\"); + assert_eq!( + String::from_utf8_lossy(&reversed_json.stdout), + format!( + "{{\"ok\":false,\"file\":\"{}\",\"error\":\"final value was 1, expected true\"}}\n{{\"ok\":true,\"file\":\"{}\"}}\n", + failed_path, passed_path ) ); let conflict = Command::new(bin("qtest"))