diff --git a/sgl-router/src/routers/grpc/pd_router.rs b/sgl-router/src/routers/grpc/pd_router.rs index 1cadd834b..da3c2d7f0 100644 --- a/sgl-router/src/routers/grpc/pd_router.rs +++ b/sgl-router/src/routers/grpc/pd_router.rs @@ -1859,7 +1859,7 @@ impl GrpcPDRouter { // Check format detection first let can_parse = { let parser = pooled_parser.lock().await; - parser.detect_format(processed_text) + parser.has_tool_markers(processed_text) // Lock is dropped here }; diff --git a/sgl-router/src/routers/grpc/router.rs b/sgl-router/src/routers/grpc/router.rs index 553e3519e..06832c764 100644 --- a/sgl-router/src/routers/grpc/router.rs +++ b/sgl-router/src/routers/grpc/router.rs @@ -306,7 +306,7 @@ impl GrpcRouter { // Check format detection first let can_parse = { let parser = pooled_parser.lock().await; - parser.detect_format(processed_text) + parser.has_tool_markers(processed_text) // Lock is dropped here }; diff --git a/sgl-router/src/tool_parser/parsers/deepseek_parser.rs b/sgl-router/src/tool_parser/parsers/deepseek_parser.rs index 2f28aae06..32774797c 100644 --- a/sgl-router/src/tool_parser/parsers/deepseek_parser.rs +++ b/sgl-router/src/tool_parser/parsers/deepseek_parser.rs @@ -77,11 +77,6 @@ impl DeepSeekParser { } } - /// Check if text contains DeepSeek tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains("<|tool▁calls▁begin|>") - } - /// Parse a single tool call block - throws error if parsing fails fn parse_tool_call(&self, block: &str) -> ToolParserResult { let captures = self.func_detail_extractor.captures(block).ok_or_else(|| { @@ -312,8 +307,8 @@ impl ToolParser for DeepSeekParser { }) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) + fn has_tool_markers(&self, text: &str) -> bool { + text.contains("<|tool▁calls▁begin|>") } fn get_unstreamed_tool_args(&self) -> Option> { diff --git a/sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs b/sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs index 7d134f4eb..3980709ea 100644 --- a/sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs +++ b/sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs @@ -71,11 +71,6 @@ impl Glm4MoeParser { } } - /// Check if text contains GLM-4 MoE tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains(self.bot_token) - } - /// Parse arguments from key-value pairs fn parse_arguments(&self, args_text: &str) -> ToolParserResult> { let mut arguments = serde_json::Map::new(); @@ -313,8 +308,8 @@ impl ToolParser for Glm4MoeParser { }) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) + fn has_tool_markers(&self, text: &str) -> bool { + text.contains(self.bot_token) } fn get_unstreamed_tool_args(&self) -> Option> { diff --git a/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs b/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs index 5cbc71554..e7ca5179b 100644 --- a/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs +++ b/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs @@ -38,7 +38,7 @@ impl ToolParser for GptOssHarmonyParser { Ok(StreamingParseResult::default()) } - fn detect_format(&self, text: &str) -> bool { + fn has_tool_markers(&self, text: &str) -> bool { // Reuse the legacy heuristics for now; this will be replaced with Harmony-specific // start-token detection when the parser is fully implemented. text.contains("<|channel|>commentary") diff --git a/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs b/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs index 5769315a3..ddca0d32b 100644 --- a/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs +++ b/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs @@ -58,11 +58,6 @@ impl GptOssParser { } } - /// Check if text contains GPT-OSS tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains("<|channel|>commentary to=") - } - /// Extract function name from full namespace (e.g., "functions.get_weather" -> "get_weather") fn extract_function_name(&self, full_name: &str) -> String { if let Some(dot_pos) = full_name.rfind('.') { @@ -242,7 +237,7 @@ impl ToolParser for GptOssParser { Ok(StreamingParseResult::default()) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) || text.contains("<|channel|>commentary") + fn has_tool_markers(&self, text: &str) -> bool { + text.contains("<|channel|>commentary") } } diff --git a/sgl-router/src/tool_parser/parsers/json_parser.rs b/sgl-router/src/tool_parser/parsers/json_parser.rs index 3a005e2cd..cca506e9d 100644 --- a/sgl-router/src/tool_parser/parsers/json_parser.rs +++ b/sgl-router/src/tool_parser/parsers/json_parser.rs @@ -261,7 +261,7 @@ impl ToolParser for JsonParser { ) } - fn detect_format(&self, text: &str) -> bool { + fn has_tool_markers(&self, text: &str) -> bool { let trimmed = text.trim(); (trimmed.starts_with('[') || trimmed.starts_with('{')) && trimmed.contains(r#""name""#) } diff --git a/sgl-router/src/tool_parser/parsers/kimik2_parser.rs b/sgl-router/src/tool_parser/parsers/kimik2_parser.rs index 123c4d0f5..9642713e9 100644 --- a/sgl-router/src/tool_parser/parsers/kimik2_parser.rs +++ b/sgl-router/src/tool_parser/parsers/kimik2_parser.rs @@ -82,11 +82,6 @@ impl KimiK2Parser { } } - /// Check if text contains Kimi K2 tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains("<|tool_calls_section_begin|>") - } - /// Parse function ID to extract name and index fn parse_function_id(&self, id: &str) -> Option<(String, usize)> { if let Some(captures) = self.tool_call_id_regex.captures(id) { @@ -331,8 +326,8 @@ impl ToolParser for KimiK2Parser { }) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) || text.contains("<|tool_call_begin|>") + fn has_tool_markers(&self, text: &str) -> bool { + text.contains("<|tool_calls_section_begin|>") } fn get_unstreamed_tool_args(&self) -> Option> { diff --git a/sgl-router/src/tool_parser/parsers/llama_parser.rs b/sgl-router/src/tool_parser/parsers/llama_parser.rs index 5634aa81e..5836e9b55 100644 --- a/sgl-router/src/tool_parser/parsers/llama_parser.rs +++ b/sgl-router/src/tool_parser/parsers/llama_parser.rs @@ -228,7 +228,7 @@ impl ToolParser for LlamaParser { ) } - fn detect_format(&self, text: &str) -> bool { + fn has_tool_markers(&self, text: &str) -> bool { // Llama format if contains python_tag or starts with JSON object text.contains("<|python_tag|>") || (text.trim_start().starts_with('{') && text.contains(r#""name""#)) diff --git a/sgl-router/src/tool_parser/parsers/mistral_parser.rs b/sgl-router/src/tool_parser/parsers/mistral_parser.rs index 2148a966f..a7cf79aba 100644 --- a/sgl-router/src/tool_parser/parsers/mistral_parser.rs +++ b/sgl-router/src/tool_parser/parsers/mistral_parser.rs @@ -156,11 +156,6 @@ impl MistralParser { Ok(None) } } - - /// Check if text contains Mistral tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains("[TOOL_CALLS]") - } } impl Default for MistralParser { @@ -254,8 +249,8 @@ impl ToolParser for MistralParser { ) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) + fn has_tool_markers(&self, text: &str) -> bool { + text.contains("[TOOL_CALLS]") } fn get_unstreamed_tool_args(&self) -> Option> { diff --git a/sgl-router/src/tool_parser/parsers/pythonic_parser.rs b/sgl-router/src/tool_parser/parsers/pythonic_parser.rs index 8eeaecd41..2b5ad8bad 100644 --- a/sgl-router/src/tool_parser/parsers/pythonic_parser.rs +++ b/sgl-router/src/tool_parser/parsers/pythonic_parser.rs @@ -203,7 +203,7 @@ impl ToolParser for PythonicParser { }) } - fn detect_format(&self, text: &str) -> bool { + fn has_tool_markers(&self, text: &str) -> bool { let cleaned = Self::strip_special_tokens(text); if pythonic_block_regex().is_match(&cleaned) { return true; diff --git a/sgl-router/src/tool_parser/parsers/qwen_parser.rs b/sgl-router/src/tool_parser/parsers/qwen_parser.rs index f6de474f4..62eac4f64 100644 --- a/sgl-router/src/tool_parser/parsers/qwen_parser.rs +++ b/sgl-router/src/tool_parser/parsers/qwen_parser.rs @@ -98,16 +98,6 @@ impl QwenParser { Ok(None) } } - - /// Check if text contains Qwen tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains("") - } - - /// Check if text has tool call - fn has_tool_call(&self, text: &str) -> bool { - text.contains("") - } } impl Default for QwenParser { @@ -165,7 +155,7 @@ impl ToolParser for QwenParser { let current_text = &self.buffer.clone(); // Check if current_text has tool_call - let has_tool_start = self.has_tool_call(current_text) + let has_tool_start = self.has_tool_markers(current_text) || (self.current_tool_id >= 0 && current_text.starts_with(self.tool_call_separator)); if !has_tool_start { @@ -243,8 +233,8 @@ impl ToolParser for QwenParser { Ok(result) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) + fn has_tool_markers(&self, text: &str) -> bool { + text.contains("") } fn get_unstreamed_tool_args(&self) -> Option> { diff --git a/sgl-router/src/tool_parser/parsers/step3_parser.rs b/sgl-router/src/tool_parser/parsers/step3_parser.rs index 319b243ac..622843c0c 100644 --- a/sgl-router/src/tool_parser/parsers/step3_parser.rs +++ b/sgl-router/src/tool_parser/parsers/step3_parser.rs @@ -96,11 +96,6 @@ impl Step3Parser { } } - /// Check if text contains Step3 tool markers - fn has_tool_markers(&self, text: &str) -> bool { - text.contains(self.bot_token) - } - /// Reset streaming state for the next tool call fn reset_streaming_state(&mut self) { self.in_tool_call = false; @@ -553,8 +548,8 @@ impl ToolParser for Step3Parser { Ok(StreamingParseResult::default()) } - fn detect_format(&self, text: &str) -> bool { - self.has_tool_markers(text) + fn has_tool_markers(&self, text: &str) -> bool { + text.contains(self.bot_token) } fn get_unstreamed_tool_args(&self) -> Option> { diff --git a/sgl-router/src/tool_parser/tests.rs b/sgl-router/src/tool_parser/tests.rs index d5171abdf..cd10b23ee 100644 --- a/sgl-router/src/tool_parser/tests.rs +++ b/sgl-router/src/tool_parser/tests.rs @@ -12,7 +12,7 @@ async fn test_tool_parser_factory() { // Test that we can get a pooled parser let pooled_parser = factory.get_pooled("gpt-4"); let parser = pooled_parser.lock().await; - assert!(parser.detect_format(r#"{"name": "test", "arguments": {}}"#)); + assert!(parser.has_tool_markers(r#"{"name": "test", "arguments": {}}"#)); } #[tokio::test] @@ -25,7 +25,7 @@ async fn test_tool_parser_factory_model_mapping() { // Get parser for the test model let pooled_parser = factory.get_pooled("test-model"); let parser = pooled_parser.lock().await; - assert!(parser.detect_format(r#"{"name": "test", "arguments": {}}"#)); + assert!(parser.has_tool_markers(r#"{"name": "test", "arguments": {}}"#)); } #[test] @@ -234,12 +234,12 @@ fn test_json_parser_format_detection() { let parser = JsonParser::new(); // Should detect valid tool call formats - assert!(parser.detect_format(r#"{"name": "test", "arguments": {}}"#)); - assert!(parser.detect_format(r#"{"name": "test", "parameters": {"x": 1}}"#)); - assert!(parser.detect_format(r#"[{"name": "test"}]"#)); + assert!(parser.has_tool_markers(r#"{"name": "test", "arguments": {}}"#)); + assert!(parser.has_tool_markers(r#"{"name": "test", "parameters": {"x": 1}}"#)); + assert!(parser.has_tool_markers(r#"[{"name": "test"}]"#)); // Should not detect non-tool formats - assert!(!parser.detect_format("plain text")); + assert!(!parser.has_tool_markers("plain text")); } #[tokio::test] diff --git a/sgl-router/src/tool_parser/traits.rs b/sgl-router/src/tool_parser/traits.rs index ee6d00c87..f9f23216f 100644 --- a/sgl-router/src/tool_parser/traits.rs +++ b/sgl-router/src/tool_parser/traits.rs @@ -25,7 +25,7 @@ pub trait ToolParser: Send + Sync { ) -> ToolParserResult; /// Check if text contains tool calls in this parser's format - fn detect_format(&self, text: &str) -> bool; + fn has_tool_markers(&self, text: &str) -> bool; /// Optionally expose a token-aware parser implementation. /// Default returns `None`, meaning the parser only supports text input. diff --git a/sgl-router/tests/tool_parser_deepseek.rs b/sgl-router/tests/tool_parser_deepseek.rs index 8c4d34ca4..d3db93145 100644 --- a/sgl-router/tests/tool_parser_deepseek.rs +++ b/sgl-router/tests/tool_parser_deepseek.rs @@ -108,13 +108,13 @@ fn test_deepseek_format_detection() { let parser = DeepSeekParser::new(); // Should detect DeepSeek format - assert!(parser.detect_format("<|tool▁calls▁begin|>")); - assert!(parser.detect_format("text with <|tool▁calls▁begin|> marker")); + assert!(parser.has_tool_markers("<|tool▁calls▁begin|>")); + assert!(parser.has_tool_markers("text with <|tool▁calls▁begin|> marker")); // Should not detect other formats - assert!(!parser.detect_format("[TOOL_CALLS]")); - assert!(!parser.detect_format("")); - assert!(!parser.detect_format("plain text")); + assert!(!parser.has_tool_markers("[TOOL_CALLS]")); + assert!(!parser.has_tool_markers("")); + assert!(!parser.has_tool_markers("plain text")); } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_glm4_moe.rs b/sgl-router/tests/tool_parser_glm4_moe.rs index d2b3e54e7..e92848cf4 100644 --- a/sgl-router/tests/tool_parser_glm4_moe.rs +++ b/sgl-router/tests/tool_parser_glm4_moe.rs @@ -117,13 +117,13 @@ fn test_glm4_format_detection() { let parser = Glm4MoeParser::new(); // Should detect GLM-4 format - assert!(parser.detect_format("")); - assert!(parser.detect_format("text with marker")); + assert!(parser.has_tool_markers("")); + assert!(parser.has_tool_markers("text with marker")); // Should not detect other formats - assert!(!parser.detect_format("[TOOL_CALLS]")); - assert!(!parser.detect_format("<|tool▁calls▁begin|>")); - assert!(!parser.detect_format("plain text")); + assert!(!parser.has_tool_markers("[TOOL_CALLS]")); + assert!(!parser.has_tool_markers("<|tool▁calls▁begin|>")); + assert!(!parser.has_tool_markers("plain text")); } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_gpt_oss.rs b/sgl-router/tests/tool_parser_gpt_oss.rs index 8af554f20..98b197252 100644 --- a/sgl-router/tests/tool_parser_gpt_oss.rs +++ b/sgl-router/tests/tool_parser_gpt_oss.rs @@ -109,14 +109,14 @@ fn test_gpt_oss_format_detection() { let parser = GptOssParser::new(); // Should detect GPT-OSS format - assert!(parser.detect_format("<|channel|>commentary to=")); - assert!(parser.detect_format("<|channel|>commentary")); - assert!(parser.detect_format("text with <|channel|>commentary to= marker")); + assert!(parser.has_tool_markers("<|channel|>commentary to=")); + assert!(parser.has_tool_markers("<|channel|>commentary")); + assert!(parser.has_tool_markers("text with <|channel|>commentary to= marker")); // Should not detect other formats - assert!(!parser.detect_format("[TOOL_CALLS]")); - assert!(!parser.detect_format("")); - assert!(!parser.detect_format("plain text")); + assert!(!parser.has_tool_markers("[TOOL_CALLS]")); + assert!(!parser.has_tool_markers("")); + assert!(!parser.has_tool_markers("plain text")); } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_json.rs b/sgl-router/tests/tool_parser_json.rs index cd079e948..3bcea88ae 100644 --- a/sgl-router/tests/tool_parser_json.rs +++ b/sgl-router/tests/tool_parser_json.rs @@ -155,7 +155,7 @@ async fn test_json_invalid_format() { async fn test_json_format_detection() { let parser = JsonParser::new(); - assert!(parser.detect_format(r#"{"name": "test", "arguments": {}}"#)); - assert!(parser.detect_format(r#"[{"name": "test"}]"#)); - assert!(!parser.detect_format("plain text")); + assert!(parser.has_tool_markers(r#"{"name": "test", "arguments": {}}"#)); + assert!(parser.has_tool_markers(r#"[{"name": "test"}]"#)); + assert!(!parser.has_tool_markers("plain text")); } diff --git a/sgl-router/tests/tool_parser_kimik2.rs b/sgl-router/tests/tool_parser_kimik2.rs index e4d867166..f7f0a6c96 100644 --- a/sgl-router/tests/tool_parser_kimik2.rs +++ b/sgl-router/tests/tool_parser_kimik2.rs @@ -98,14 +98,13 @@ fn test_kimik2_format_detection() { let parser = KimiK2Parser::new(); // Should detect Kimi K2 format - assert!(parser.detect_format("<|tool_calls_section_begin|>")); - assert!(parser.detect_format("<|tool_call_begin|>")); - assert!(parser.detect_format("text with <|tool_calls_section_begin|> marker")); + assert!(parser.has_tool_markers("<|tool_calls_section_begin|>")); + assert!(parser.has_tool_markers("text with <|tool_calls_section_begin|> marker")); // Should not detect other formats - assert!(!parser.detect_format("[TOOL_CALLS]")); - assert!(!parser.detect_format("")); - assert!(!parser.detect_format("plain text")); + assert!(!parser.has_tool_markers("[TOOL_CALLS]")); + assert!(!parser.has_tool_markers("")); + assert!(!parser.has_tool_markers("plain text")); } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_llama.rs b/sgl-router/tests/tool_parser_llama.rs index e598efbc7..087ecac54 100644 --- a/sgl-router/tests/tool_parser_llama.rs +++ b/sgl-router/tests/tool_parser_llama.rs @@ -116,10 +116,10 @@ async fn test_llama_empty_arguments() { async fn test_llama_format_detection() { let parser = LlamaParser::new(); - assert!(parser.detect_format(r#"<|python_tag|>{"name": "test"}"#)); - assert!(parser.detect_format(r#"{"name": "test", "parameters": {}}"#)); - assert!(!parser.detect_format("plain text")); - assert!(!parser.detect_format(r#"{"key": "value"}"#)); // No name field + assert!(parser.has_tool_markers(r#"<|python_tag|>{"name": "test"}"#)); + assert!(parser.has_tool_markers(r#"{"name": "test", "parameters": {}}"#)); + assert!(!parser.has_tool_markers("plain text")); + assert!(!parser.has_tool_markers(r#"{"key": "value"}"#)); // No name field } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_mistral.rs b/sgl-router/tests/tool_parser_mistral.rs index d630b15dd..8ff45df99 100644 --- a/sgl-router/tests/tool_parser_mistral.rs +++ b/sgl-router/tests/tool_parser_mistral.rs @@ -96,10 +96,10 @@ async fn test_mistral_with_brackets_in_strings() { async fn test_mistral_format_detection() { let parser = MistralParser::new(); - assert!(parser.detect_format("[TOOL_CALLS] [")); - assert!(parser.detect_format("Some text [TOOL_CALLS] [")); - assert!(!parser.detect_format("Just plain text")); - assert!(!parser.detect_format("[{\"name\": \"test\"}]")); // JSON array without TOOL_CALLS + assert!(parser.has_tool_markers("[TOOL_CALLS] [")); + assert!(parser.has_tool_markers("Some text [TOOL_CALLS] [")); + assert!(!parser.has_tool_markers("Just plain text")); + assert!(!parser.has_tool_markers("[{\"name\": \"test\"}]")); // JSON array without TOOL_CALLS } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_pythonic.rs b/sgl-router/tests/tool_parser_pythonic.rs index af0d9c0e8..1215bbe4c 100644 --- a/sgl-router/tests/tool_parser_pythonic.rs +++ b/sgl-router/tests/tool_parser_pythonic.rs @@ -125,10 +125,10 @@ async fn test_pythonic_empty_arguments() { async fn test_pythonic_format_detection() { let parser = PythonicParser::new(); - assert!(!parser.detect_format("[function_name(")); // Incomplete - assert!(parser.detect_format("[get_weather(city=\"NYC\")]")); - assert!(!parser.detect_format("Just plain text")); - assert!(!parser.detect_format("{\"name\": \"test\"}")); // JSON + assert!(!parser.has_tool_markers("[function_name(")); // Incomplete + assert!(parser.has_tool_markers("[get_weather(city=\"NYC\")]")); + assert!(!parser.has_tool_markers("Just plain text")); + assert!(!parser.has_tool_markers("{\"name\": \"test\"}")); // JSON } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_qwen.rs b/sgl-router/tests/tool_parser_qwen.rs index 2a88e08ea..c6a447361 100644 --- a/sgl-router/tests/tool_parser_qwen.rs +++ b/sgl-router/tests/tool_parser_qwen.rs @@ -120,10 +120,10 @@ async fn test_qwen_with_newlines_in_strings() { async fn test_qwen_format_detection() { let parser = QwenParser::new(); - assert!(parser.detect_format("")); - assert!(parser.detect_format("Some text \n{")); - assert!(!parser.detect_format("Just plain text")); - assert!(!parser.detect_format("{\"name\": \"test\"}")); // Plain JSON + assert!(parser.has_tool_markers("")); + assert!(parser.has_tool_markers("Some text \n{")); + assert!(!parser.has_tool_markers("Just plain text")); + assert!(!parser.has_tool_markers("{\"name\": \"test\"}")); // Plain JSON } #[tokio::test] diff --git a/sgl-router/tests/tool_parser_step3.rs b/sgl-router/tests/tool_parser_step3.rs index 40257b2c2..85cbacfae 100644 --- a/sgl-router/tests/tool_parser_step3.rs +++ b/sgl-router/tests/tool_parser_step3.rs @@ -111,13 +111,13 @@ fn test_step3_format_detection() { let parser = Step3Parser::new(); // Should detect Step3 format - assert!(parser.detect_format("<|tool_calls_begin|>")); - assert!(parser.detect_format("text with <|tool_calls_begin|> marker")); + assert!(parser.has_tool_markers("<|tool_calls_begin|>")); + assert!(parser.has_tool_markers("text with <|tool_calls_begin|> marker")); // Should not detect other formats - assert!(!parser.detect_format("[TOOL_CALLS]")); - assert!(!parser.detect_format("")); - assert!(!parser.detect_format("plain text")); + assert!(!parser.has_tool_markers("[TOOL_CALLS]")); + assert!(!parser.has_tool_markers("")); + assert!(!parser.has_tool_markers("plain text")); } #[tokio::test]