use super::*;
use crate::macro_analyzer::{
AutoConfigMacro, HttpMethod, InjectMacro, InjectType, JobMacro, RouteMacro, ServiceMacro,
SummerMacro,
};
use crate::schema::SchemaProvider;
use crate::toml_analyzer::TomlAnalyzer;
use lsp_types::{Position, Range, Url};
fn test_range() -> Range {
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
}
}
#[allow(dead_code)]
fn test_url() -> Url {
Url::parse("file:///test.rs").unwrap()
}
fn test_engine() -> CompletionEngine {
let schema_provider = SchemaProvider::default();
CompletionEngine::new(schema_provider)
}
#[test]
fn test_complete_service_macro() {
let engine = test_engine();
let service_macro = ServiceMacro {
struct_name: "TestService".to_string(),
fields: vec![],
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::DeriveService(service_macro), None);
assert_eq!(completions.len(), 3);
assert_eq!(completions[0].label, "inject(component)");
assert_eq!(completions[0].kind, Some(CompletionItemKind::PROPERTY));
assert_eq!(completions[0].detail, Some("注入组件".to_string()));
assert!(completions[0].documentation.is_some());
assert_eq!(
completions[0].insert_text,
Some("inject(component)".to_string())
);
assert_eq!(completions[1].label, "inject(component = \"name\")");
assert_eq!(completions[1].kind, Some(CompletionItemKind::PROPERTY));
assert_eq!(
completions[1].detail,
Some("注入指定名称的组件".to_string())
);
assert!(completions[1].documentation.is_some());
assert_eq!(
completions[1].insert_text,
Some("inject(component = \"$1\")".to_string())
);
assert_eq!(
completions[1].insert_text_format,
Some(lsp_types::InsertTextFormat::SNIPPET)
);
assert_eq!(completions[2].label, "inject(config)");
assert_eq!(completions[2].kind, Some(CompletionItemKind::PROPERTY));
assert_eq!(completions[2].detail, Some("注入配置".to_string()));
assert!(completions[2].documentation.is_some());
assert_eq!(
completions[2].insert_text,
Some("inject(config)".to_string())
);
}
#[test]
fn test_complete_inject_macro() {
let engine = test_engine();
let inject_macro = InjectMacro {
inject_type: InjectType::Component,
component_name: None,
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::Inject(inject_macro), None);
assert_eq!(completions.len(), 2);
assert_eq!(completions[0].label, "component");
assert_eq!(completions[0].kind, Some(CompletionItemKind::KEYWORD));
assert_eq!(completions[0].detail, Some("注入组件".to_string()));
assert!(completions[0].documentation.is_some());
assert_eq!(completions[0].insert_text, Some("component".to_string()));
assert_eq!(completions[1].label, "config");
assert_eq!(completions[1].kind, Some(CompletionItemKind::KEYWORD));
assert_eq!(completions[1].detail, Some("注入配置".to_string()));
assert!(completions[1].documentation.is_some());
assert_eq!(completions[1].insert_text, Some("config".to_string()));
}
#[test]
fn test_complete_auto_config_macro() {
let engine = test_engine();
let auto_config_macro = AutoConfigMacro {
configurator_type: "".to_string(),
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::AutoConfig(auto_config_macro), None);
assert_eq!(completions.len(), 3);
assert_eq!(completions[0].label, "WebConfigurator");
assert_eq!(completions[0].kind, Some(CompletionItemKind::CLASS));
assert_eq!(completions[0].detail, Some("Web 路由配置器".to_string()));
assert!(completions[0].documentation.is_some());
assert_eq!(completions[1].label, "JobConfigurator");
assert_eq!(completions[1].kind, Some(CompletionItemKind::CLASS));
assert_eq!(completions[1].detail, Some("任务调度配置器".to_string()));
assert!(completions[1].documentation.is_some());
assert_eq!(completions[2].label, "StreamConfigurator");
assert_eq!(completions[2].kind, Some(CompletionItemKind::CLASS));
assert_eq!(completions[2].detail, Some("流处理配置器".to_string()));
assert!(completions[2].documentation.is_some());
}
#[test]
fn test_complete_route_macro() {
let engine = test_engine();
let route_macro = RouteMacro {
path: "/test".to_string(),
methods: vec![HttpMethod::Get],
middlewares: vec![],
handler_name: "test_handler".to_string(),
range: test_range(),
is_openapi: false,
};
let completions = engine.complete_macro(&SummerMacro::Route(route_macro), None);
assert!(completions.len() >= 7);
let get_completion = completions.iter().find(|c| c.label == "GET");
assert!(get_completion.is_some());
let get_completion = get_completion.unwrap();
assert_eq!(get_completion.kind, Some(CompletionItemKind::CONSTANT));
assert_eq!(get_completion.detail, Some("获取资源".to_string()));
assert!(get_completion.documentation.is_some());
let post_completion = completions.iter().find(|c| c.label == "POST");
assert!(post_completion.is_some());
let post_completion = post_completion.unwrap();
assert_eq!(post_completion.kind, Some(CompletionItemKind::CONSTANT));
assert_eq!(post_completion.detail, Some("创建资源".to_string()));
let path_param_completion = completions.iter().find(|c| c.label == "{id}");
assert!(path_param_completion.is_some());
let path_param_completion = path_param_completion.unwrap();
assert_eq!(
path_param_completion.kind,
Some(CompletionItemKind::SNIPPET)
);
assert_eq!(path_param_completion.detail, Some("路径参数".to_string()));
assert_eq!(
path_param_completion.insert_text,
Some("{${1:id}}".to_string())
);
assert_eq!(
path_param_completion.insert_text_format,
Some(lsp_types::InsertTextFormat::SNIPPET)
);
}
#[test]
fn test_complete_job_macro_cron() {
let engine = test_engine();
let job_macro = JobMacro::Cron {
expression: "".to_string(),
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::Job(job_macro), None);
assert!(completions.len() >= 6);
let hourly_cron = completions.iter().find(|c| c.label == "0 0 * * * *");
assert!(hourly_cron.is_some());
let hourly_cron = hourly_cron.unwrap();
assert_eq!(hourly_cron.kind, Some(CompletionItemKind::SNIPPET));
assert_eq!(hourly_cron.detail, Some("每小时执行".to_string()));
assert!(hourly_cron.documentation.is_some());
assert_eq!(hourly_cron.insert_text, Some("\"0 0 * * * *\"".to_string()));
let daily_cron = completions.iter().find(|c| c.label == "0 0 0 * * *");
assert!(daily_cron.is_some());
let daily_cron = daily_cron.unwrap();
assert_eq!(daily_cron.detail, Some("每天午夜执行".to_string()));
let delay_5 = completions.iter().find(|c| c.label == "5");
assert!(delay_5.is_some());
let delay_5 = delay_5.unwrap();
assert_eq!(delay_5.kind, Some(CompletionItemKind::VALUE));
assert_eq!(delay_5.detail, Some("延迟 5 秒".to_string()));
}
#[test]
fn test_complete_job_macro_fix_delay() {
let engine = test_engine();
let job_macro = JobMacro::FixDelay {
seconds: 0,
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::Job(job_macro), None);
assert!(completions.len() >= 3);
let delay_10 = completions.iter().find(|c| c.label == "10");
assert!(delay_10.is_some());
let delay_10 = delay_10.unwrap();
assert_eq!(delay_10.kind, Some(CompletionItemKind::VALUE));
assert!(delay_10.detail.is_some());
assert!(delay_10.documentation.is_some());
}
#[test]
fn test_complete_job_macro_fix_rate() {
let engine = test_engine();
let job_macro = JobMacro::FixRate {
seconds: 0,
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::Job(job_macro), None);
assert!(completions.len() >= 3);
let rate_60 = completions.iter().find(|c| c.label == "60");
assert!(rate_60.is_some());
let rate_60 = rate_60.unwrap();
assert_eq!(rate_60.kind, Some(CompletionItemKind::VALUE));
assert!(rate_60.detail.is_some());
assert!(rate_60.documentation.is_some());
}
#[test]
fn test_completion_items_have_documentation() {
let engine = test_engine();
let test_cases = vec![
SummerMacro::DeriveService(ServiceMacro {
struct_name: "Test".to_string(),
fields: vec![],
range: test_range(),
}),
SummerMacro::Inject(InjectMacro {
inject_type: InjectType::Component,
component_name: None,
range: test_range(),
}),
SummerMacro::AutoConfig(AutoConfigMacro {
configurator_type: "".to_string(),
range: test_range(),
}),
SummerMacro::Route(RouteMacro {
path: "/test".to_string(),
methods: vec![HttpMethod::Get],
middlewares: vec![],
handler_name: "handler".to_string(),
range: test_range(),
is_openapi: false,
}),
SummerMacro::Job(JobMacro::Cron {
expression: "".to_string(),
range: test_range(),
}),
];
for macro_info in test_cases {
let completions = engine.complete_macro(¯o_info, None);
assert!(!completions.is_empty(), "补全列表不应为空");
for completion in completions {
assert!(
completion.documentation.is_some(),
"补全项 '{}' 应该有文档说明",
completion.label
);
assert!(
completion.detail.is_some(),
"补全项 '{}' 应该有详细信息",
completion.label
);
assert!(
completion.insert_text.is_some(),
"补全项 '{}' 应该有插入文本",
completion.label
);
}
}
}
#[test]
fn test_completion_items_have_correct_kind() {
let engine = test_engine();
let service_completions = engine.complete_macro(
&SummerMacro::DeriveService(ServiceMacro {
struct_name: "Test".to_string(),
fields: vec![],
range: test_range(),
}),
None,
);
for completion in service_completions {
assert_eq!(
completion.kind,
Some(CompletionItemKind::PROPERTY),
"Service 宏的补全项应该是 PROPERTY 类型"
);
}
let inject_completions = engine.complete_macro(
&SummerMacro::Inject(InjectMacro {
inject_type: InjectType::Component,
component_name: None,
range: test_range(),
}),
None,
);
for completion in inject_completions {
assert_eq!(
completion.kind,
Some(CompletionItemKind::KEYWORD),
"Inject 宏的补全项应该是 KEYWORD 类型"
);
}
let auto_config_completions = engine.complete_macro(
&SummerMacro::AutoConfig(AutoConfigMacro {
configurator_type: "".to_string(),
range: test_range(),
}),
None,
);
for completion in auto_config_completions {
assert_eq!(
completion.kind,
Some(CompletionItemKind::CLASS),
"AutoConfig 宏的补全项应该是 CLASS 类型"
);
}
}
#[test]
fn test_complete_with_toml_context() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 10,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert!(!completions.is_empty());
}
#[test]
fn test_complete_with_macro_context() {
let engine = test_engine();
let service_macro = ServiceMacro {
struct_name: "TestService".to_string(),
fields: vec![],
range: test_range(),
};
let position = Position {
line: 0,
character: 0,
};
let completions = engine.complete(
CompletionContext::Macro,
position,
None,
Some(&SummerMacro::DeriveService(service_macro)),
);
assert_eq!(completions.len(), 3);
assert_eq!(completions[0].label, "inject(component)");
}
#[test]
fn test_complete_with_unknown_context() {
let engine = test_engine();
let position = Position {
line: 0,
character: 0,
};
let completions = engine.complete(CompletionContext::Unknown, position, None, None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_toml_without_document() {
let engine = test_engine();
let position = Position {
line: 0,
character: 0,
};
let completions = engine.complete(CompletionContext::Toml, position, None, None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_macro_without_macro_info() {
let engine = test_engine();
let position = Position {
line: 0,
character: 0,
};
let completions = engine.complete(CompletionContext::Macro, position, None, None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_dispatches_to_correct_handler() {
let engine = test_engine();
let test_cases = vec![
(
SummerMacro::DeriveService(ServiceMacro {
struct_name: "Test".to_string(),
fields: vec![],
range: test_range(),
}),
3, ),
(
SummerMacro::Inject(InjectMacro {
inject_type: InjectType::Component,
component_name: None,
range: test_range(),
}),
2, ),
(
SummerMacro::AutoConfig(AutoConfigMacro {
configurator_type: "".to_string(),
range: test_range(),
}),
3, ),
];
let position = Position {
line: 0,
character: 0,
};
for (macro_info, expected_count) in test_cases {
let completions =
engine.complete(CompletionContext::Macro, position, None, Some(¯o_info));
assert_eq!(
completions.len(),
expected_count,
"宏类型 {:?} 的补全项数量不正确",
macro_info
);
}
}
#[test]
fn test_completion_context_clone() {
let context = CompletionContext::Toml;
let cloned = context.clone();
match (context, cloned) {
(CompletionContext::Toml, CompletionContext::Toml) => {}
_ => panic!("克隆的上下文类型不匹配"),
}
}
#[test]
fn test_completion_context_debug() {
let context = CompletionContext::Macro;
let debug_str = format!("{:?}", context);
assert!(debug_str.contains("Macro"));
}
#[test]
fn test_complete_config_properties_in_section() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 1,
character: 5, };
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert!(!completions.is_empty());
let port_completion = completions.iter().find(|c| c.label == "port");
assert!(port_completion.is_some(), "应该包含 port 补全");
let port_completion = port_completion.unwrap();
assert_eq!(port_completion.kind, Some(CompletionItemKind::PROPERTY));
assert!(port_completion.detail.is_some());
assert!(port_completion.documentation.is_some());
assert!(port_completion.insert_text.is_some());
let insert_text = port_completion.insert_text.as_ref().unwrap();
assert!(insert_text.contains("port"));
assert!(insert_text.contains("#")); }
#[test]
fn test_complete_config_properties_deduplication() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"\nport = 8080";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 1,
character: 5,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
let host_completion = completions.iter().find(|c| c.label == "host");
assert!(host_completion.is_none(), "host 已存在,不应该再补全");
let port_completion = completions.iter().find(|c| c.label == "port");
assert!(port_completion.is_none(), "port 已存在,不应该再补全");
}
#[test]
fn test_complete_config_properties_empty_section() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 5,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert!(completions.len() >= 2);
let host_completion = completions.iter().find(|c| c.label == "host");
assert!(host_completion.is_some(), "应该包含 host 补全");
let port_completion = completions.iter().find(|c| c.label == "port");
assert!(port_completion.is_some(), "应该包含 port 补全");
}
#[test]
fn test_complete_config_properties_with_documentation() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 5,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
for completion in completions {
assert!(
completion.documentation.is_some(),
"补全项 '{}' 应该有文档说明",
completion.label
);
assert!(
completion.detail.is_some(),
"补全项 '{}' 应该有详细信息",
completion.label
);
assert!(
completion.insert_text.is_some(),
"补全项 '{}' 应该有插入文本",
completion.label
);
}
}
#[test]
fn test_complete_config_properties_correct_kind() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 5,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
for completion in completions {
assert_eq!(
completion.kind,
Some(CompletionItemKind::PROPERTY),
"配置项补全应该是 PROPERTY 类型"
);
}
}
#[test]
fn test_complete_outside_section() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 10,
character: 0,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_redis_section() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[redis]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 7,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert!(!completions.is_empty());
let url_completion = completions.iter().find(|c| c.label == "url");
assert!(url_completion.is_some(), "应该包含 url 补全");
}
#[test]
fn test_complete_unknown_section() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[unknown]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 9,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_type_info_to_hint() {
let engine = test_engine();
let string_type = crate::schema::TypeInfo::String {
enum_values: None,
min_length: None,
max_length: None,
};
let hint = engine.type_info_to_hint(&string_type);
assert_eq!(hint, "string");
let enum_type = crate::schema::TypeInfo::String {
enum_values: Some(vec!["a".to_string(), "b".to_string()]),
min_length: None,
max_length: None,
};
let hint = engine.type_info_to_hint(&enum_type);
assert!(hint.contains("enum"));
let int_type = crate::schema::TypeInfo::Integer {
min: Some(1),
max: Some(100),
};
let hint = engine.type_info_to_hint(&int_type);
assert!(hint.contains("integer"));
assert!(hint.contains("1"));
assert!(hint.contains("100"));
let bool_type = crate::schema::TypeInfo::Boolean;
let hint = engine.type_info_to_hint(&bool_type);
assert_eq!(hint, "boolean");
}
#[test]
fn test_type_info_to_default() {
let engine = test_engine();
let string_type = crate::schema::TypeInfo::String {
enum_values: None,
min_length: None,
max_length: None,
};
let default = engine.type_info_to_default(&string_type);
assert_eq!(default, "\"\"");
let enum_type = crate::schema::TypeInfo::String {
enum_values: Some(vec!["first".to_string(), "second".to_string()]),
min_length: None,
max_length: None,
};
let default = engine.type_info_to_default(&enum_type);
assert_eq!(default, "\"first\"");
let int_type = crate::schema::TypeInfo::Integer {
min: None,
max: None,
};
let default = engine.type_info_to_default(&int_type);
assert_eq!(default, "0");
let float_type = crate::schema::TypeInfo::Float {
min: None,
max: None,
};
let default = engine.type_info_to_default(&float_type);
assert_eq!(default, "0.0");
let bool_type = crate::schema::TypeInfo::Boolean;
let default = engine.type_info_to_default(&bool_type);
assert_eq!(default, "false");
let array_type = crate::schema::TypeInfo::Array {
item_type: Box::new(crate::schema::TypeInfo::String {
enum_values: None,
min_length: None,
max_length: None,
}),
};
let default = engine.type_info_to_default(&array_type);
assert_eq!(default, "[]");
}
#[test]
fn test_value_to_string() {
let engine = test_engine();
let string_val = crate::schema::Value::String("test".to_string());
assert_eq!(engine.value_to_string(&string_val), "\"test\"");
let int_val = crate::schema::Value::Integer(42);
assert_eq!(engine.value_to_string(&int_val), "42");
let float_val = crate::schema::Value::Float(3.5);
assert_eq!(engine.value_to_string(&float_val), "3.5");
let bool_val = crate::schema::Value::Boolean(true);
assert_eq!(engine.value_to_string(&bool_val), "true");
let array_val = crate::schema::Value::Array(vec![]);
assert_eq!(engine.value_to_string(&array_val), "[]");
use std::collections::HashMap;
let table_val = crate::schema::Value::Table(HashMap::new());
assert_eq!(engine.value_to_string(&table_val), "{}");
}
#[test]
fn test_position_in_range() {
let engine = test_engine();
let range = Range {
start: Position {
line: 1,
character: 5,
},
end: Position {
line: 3,
character: 10,
},
};
let pos_inside = Position {
line: 2,
character: 7,
};
assert!(engine.position_in_range(pos_inside, range));
let pos_start = Position {
line: 1,
character: 5,
};
assert!(engine.position_in_range(pos_start, range));
let pos_end = Position {
line: 3,
character: 10,
};
assert!(engine.position_in_range(pos_end, range));
let pos_before = Position {
line: 0,
character: 5,
};
assert!(!engine.position_in_range(pos_before, range));
let pos_after = Position {
line: 4,
character: 5,
};
assert!(!engine.position_in_range(pos_after, range));
let pos_before_char = Position {
line: 1,
character: 3,
};
assert!(!engine.position_in_range(pos_before_char, range));
let pos_after_char = Position {
line: 3,
character: 15,
};
assert!(!engine.position_in_range(pos_after_char, range));
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::toml_analyzer::ConfigProperty;
use proptest::prelude::*;
use std::collections::HashMap;
fn valid_prefix() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-z][a-z0-9-]*").unwrap()
}
fn valid_key() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-z][a-z0-9_]*").unwrap()
}
#[allow(dead_code)]
fn env_var_name() -> impl Strategy<Value = String> {
prop::string::string_regex("[A-Z][A-Z0-9_]*").unwrap()
}
fn create_config_section(
prefix: &str,
properties: HashMap<String, ConfigProperty>,
) -> crate::toml_analyzer::ConfigSection {
crate::toml_analyzer::ConfigSection {
prefix: prefix.to_string(),
properties,
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 100,
},
},
}
}
proptest! {
#[test]
fn prop_complete_config_prefix_returns_all_prefixes(
prefixes in prop::collection::hash_set(valid_prefix(), 1..10)
) {
let mut schema = crate::schema::ConfigSchema {
plugins: HashMap::new(),
};
for prefix in &prefixes {
schema.plugins.insert(
prefix.clone(),
serde_json::json!({
"type": "object",
"properties": {}
}),
);
}
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let engine = CompletionEngine::new(schema_provider);
let completions = engine.complete_config_prefix();
for prefix in &prefixes {
prop_assert!(
completions.iter().any(|c| c.label == *prefix),
"配置前缀 '{}' 应该出现在补全列表中",
prefix
);
}
prop_assert_eq!(
completions.len(),
prefixes.len(),
"补全项数量应该等于 Schema 中的插件数量"
);
}
}
proptest! {
#[test]
fn prop_complete_config_properties_returns_unused_properties(
prefix in valid_prefix(),
all_keys in prop::collection::hash_set(valid_key(), 2..10),
used_keys_count in 0usize..5usize,
) {
let all_keys: Vec<String> = all_keys.into_iter().collect();
let used_keys_count = used_keys_count.min(all_keys.len());
let used_keys: Vec<String> = all_keys.iter().take(used_keys_count).cloned().collect();
let mut properties_json = serde_json::Map::new();
for key in &all_keys {
properties_json.insert(
key.clone(),
serde_json::json!({
"type": "string",
"description": format!("Property {}", key)
}),
);
}
let mut schema = crate::schema::ConfigSchema {
plugins: HashMap::new(),
};
schema.plugins.insert(
prefix.clone(),
serde_json::json!({
"type": "object",
"properties": properties_json
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let engine = CompletionEngine::new(schema_provider);
let mut section_properties = HashMap::new();
for key in &used_keys {
section_properties.insert(
key.clone(),
ConfigProperty {
key: key.clone(),
value: crate::toml_analyzer::ConfigValue::String("test".to_string()),
range: Range {
start: Position { line: 0, character: 0 },
end: Position { line: 0, character: 10 },
},
},
);
}
let section = create_config_section(&prefix, section_properties);
let completions = engine.complete_config_properties(§ion);
for used_key in &used_keys {
prop_assert!(
!completions.iter().any(|c| c.label == *used_key),
"已使用的配置项 '{}' 不应该出现在补全列表中",
used_key
);
}
let unused_keys: Vec<&String> = all_keys.iter()
.filter(|k| !used_keys.contains(k))
.collect();
for unused_key in &unused_keys {
prop_assert!(
completions.iter().any(|c| c.label == **unused_key),
"未使用的配置项 '{}' 应该出现在补全列表中",
unused_key
);
}
}
}
proptest! {
#[test]
fn prop_complete_enum_values_returns_all_enum_values(
enum_values in prop::collection::vec(valid_key(), 1..10)
) {
let engine = test_engine();
let completions = engine.complete_enum_values(&enum_values);
for value in &enum_values {
prop_assert!(
completions.iter().any(|c| c.label == *value),
"枚举值 '{}' 应该出现在补全列表中",
value
);
}
prop_assert_eq!(
completions.len(),
enum_values.len(),
"补全项数量应该等于枚举值数量"
);
for completion in &completions {
prop_assert_eq!(
completion.kind,
Some(CompletionItemKind::ENUM_MEMBER),
"枚举值补全项的类型应该是 ENUM_MEMBER"
);
}
}
}
proptest! {
#[test]
fn prop_complete_env_var_returns_common_vars(
_dummy in any::<u8>() ) {
let engine = test_engine();
let completions = engine.complete_env_var();
prop_assert!(
!completions.is_empty(),
"环境变量补全列表不应该为空"
);
for completion in &completions {
prop_assert_eq!(
completion.kind,
Some(CompletionItemKind::VARIABLE),
"环境变量补全项的类型应该是 VARIABLE"
);
}
for completion in &completions {
prop_assert!(
completion.documentation.is_some(),
"环境变量补全项 '{}' 应该有文档说明",
completion.label
);
}
let common_vars = vec!["HOST", "PORT", "DATABASE_URL", "REDIS_URL"];
for var in common_vars {
prop_assert!(
completions.iter().any(|c| c.label == var),
"常见环境变量 '{}' 应该出现在补全列表中",
var
);
}
}
}
proptest! {
#[test]
fn prop_completion_deduplication_no_duplicates(
prefix in valid_prefix(),
all_keys in prop::collection::hash_set(valid_key(), 3..10),
existing_keys_count in 1usize..5usize,
) {
let all_keys: Vec<String> = all_keys.into_iter().collect();
let existing_keys_count = existing_keys_count.min(all_keys.len());
let existing_keys: Vec<String> = all_keys.iter()
.take(existing_keys_count)
.cloned()
.collect();
let mut properties_json = serde_json::Map::new();
for key in &all_keys {
properties_json.insert(
key.clone(),
serde_json::json!({
"type": "string",
"description": format!("Property {}", key)
}),
);
}
let mut schema = crate::schema::ConfigSchema {
plugins: HashMap::new(),
};
schema.plugins.insert(
prefix.clone(),
serde_json::json!({
"type": "object",
"properties": properties_json
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let engine = CompletionEngine::new(schema_provider);
let mut section_properties = HashMap::new();
for key in &existing_keys {
section_properties.insert(
key.clone(),
ConfigProperty {
key: key.clone(),
value: crate::toml_analyzer::ConfigValue::String("test".to_string()),
range: Range {
start: Position { line: 0, character: 0 },
end: Position { line: 0, character: 10 },
},
},
);
}
let section = create_config_section(&prefix, section_properties);
let completions = engine.complete_config_properties(§ion);
for existing_key in &existing_keys {
prop_assert!(
!completions.iter().any(|c| c.label == *existing_key),
"已存在的配置项 '{}' 不应该出现在补全列表中(应该被去重)",
existing_key
);
}
let mut seen = std::collections::HashSet::new();
for completion in &completions {
prop_assert!(
seen.insert(&completion.label),
"补全列表中不应该有重复的配置项 '{}'",
completion.label
);
}
}
}
proptest! {
#[test]
fn prop_completion_items_have_required_fields(
prefix in valid_prefix(),
keys in prop::collection::hash_set(valid_key(), 1..5),
) {
let keys: Vec<String> = keys.into_iter().collect();
let mut properties_json = serde_json::Map::new();
for key in &keys {
properties_json.insert(
key.clone(),
serde_json::json!({
"type": "string",
"description": format!("Property {}", key)
}),
);
}
let mut schema = crate::schema::ConfigSchema {
plugins: HashMap::new(),
};
schema.plugins.insert(
prefix.clone(),
serde_json::json!({
"type": "object",
"properties": properties_json
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let engine = CompletionEngine::new(schema_provider);
let section = create_config_section(&prefix, HashMap::new());
let completions = engine.complete_config_properties(§ion);
for completion in &completions {
prop_assert!(
completion.detail.is_some(),
"补全项 '{}' 应该有 detail 字段",
completion.label
);
prop_assert!(
completion.documentation.is_some(),
"补全项 '{}' 应该有 documentation 字段",
completion.label
);
prop_assert!(
completion.insert_text.is_some(),
"补全项 '{}' 应该有 insert_text 字段",
completion.label
);
prop_assert_eq!(
completion.kind,
Some(CompletionItemKind::PROPERTY),
"配置项补全的 kind 应该是 PROPERTY"
);
}
}
}
proptest! {
#[test]
fn prop_type_info_to_hint_is_consistent(
type_info in prop_oneof![
Just(crate::schema::TypeInfo::String {
enum_values: None,
min_length: None,
max_length: None,
}),
Just(crate::schema::TypeInfo::Integer {
min: None,
max: None,
}),
Just(crate::schema::TypeInfo::Float {
min: None,
max: None,
}),
Just(crate::schema::TypeInfo::Boolean),
]
) {
let engine = test_engine();
let hint = engine.type_info_to_hint(&type_info);
prop_assert!(
!hint.is_empty(),
"类型提示不应该为空"
);
match type_info {
crate::schema::TypeInfo::String { .. } => {
prop_assert!(
hint.contains("string") || hint.contains("enum"),
"字符串类型的提示应该包含 'string' 或 'enum'"
);
}
crate::schema::TypeInfo::Integer { .. } => {
prop_assert!(
hint.contains("integer"),
"整数类型的提示应该包含 'integer'"
);
}
crate::schema::TypeInfo::Float { .. } => {
prop_assert!(
hint.contains("float"),
"浮点数类型的提示应该包含 'float'"
);
}
crate::schema::TypeInfo::Boolean => {
prop_assert!(
hint.contains("boolean"),
"布尔类型的提示应该包含 'boolean'"
);
}
_ => {}
}
}
}
}
#[test]
fn test_config_property_insertion_has_complete_format() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 5,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
for completion in completions {
let insert_text = completion
.insert_text
.as_ref()
.unwrap_or_else(|| panic!("补全项 '{}' 应该有 insert_text", completion.label));
assert!(
insert_text.contains(&completion.label),
"插入文本应该包含配置项名称 '{}'",
completion.label
);
assert!(
insert_text.contains("="),
"插入文本应该包含等号: {}",
insert_text
);
assert!(
insert_text.contains("#"),
"插入文本应该包含类型提示注释: {}",
insert_text
);
let parts: Vec<&str> = insert_text.split("=").collect();
assert_eq!(parts.len(), 2, "插入文本应该包含一个等号: {}", insert_text);
let value_and_comment: Vec<&str> = parts[1].split("#").collect();
assert_eq!(
value_and_comment.len(),
2,
"插入文本应该包含一个注释符号: {}",
insert_text
);
}
}
#[test]
fn test_config_property_insertion_has_correct_default_values() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 5,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
let host_completion = completions
.iter()
.find(|c| c.label == "host")
.expect("应该有 host 补全项");
let insert_text = host_completion.insert_text.as_ref().unwrap();
assert!(
insert_text.contains("\"") && insert_text.matches("\"").count() >= 2,
"字符串类型的默认值应该是字符串格式(带引号): {}",
insert_text
);
let port_completion = completions
.iter()
.find(|c| c.label == "port")
.expect("应该有 port 补全项");
let insert_text = port_completion.insert_text.as_ref().unwrap();
assert!(
insert_text.contains("= 0") || insert_text.contains("= 8080"),
"整数类型应该有合理的默认值: {}",
insert_text
);
}
#[test]
fn test_enum_value_insertion_has_quotes() {
let engine = test_engine();
let enum_values = vec!["debug".to_string(), "info".to_string(), "warn".to_string()];
let completions = engine.complete_enum_values(&enum_values);
for completion in completions {
let insert_text = completion
.insert_text
.as_ref()
.unwrap_or_else(|| panic!("枚举值 '{}' 应该有 insert_text", completion.label));
assert!(
insert_text.starts_with("\"") && insert_text.ends_with("\""),
"枚举值的插入文本应该有引号: {}",
insert_text
);
let value = insert_text.trim_matches('"');
assert_eq!(value, completion.label, "引号内的值应该与标签匹配");
}
}
#[test]
fn test_env_var_insertion_has_snippet_format() {
let engine = test_engine();
let completions = engine.complete_env_var();
for completion in completions {
let insert_text = completion
.insert_text
.as_ref()
.unwrap_or_else(|| panic!("环境变量 '{}' 应该有 insert_text", completion.label));
assert!(
insert_text.contains(&completion.label),
"插入文本应该包含变量名 '{}': {}",
completion.label,
insert_text
);
assert!(
insert_text.contains("${1:") || insert_text.contains("$1"),
"插入文本应该包含 snippet 占位符: {}",
insert_text
);
assert!(
insert_text.contains(":") && insert_text.contains("}"),
"插入文本应该包含完整的环境变量格式: {}",
insert_text
);
assert_eq!(
completion.insert_text_format,
Some(lsp_types::InsertTextFormat::SNIPPET),
"环境变量补全应该使用 SNIPPET 格式"
);
}
}
#[test]
fn test_macro_parameter_insertion_completeness() {
let engine = test_engine();
let service_macro = ServiceMacro {
struct_name: "Test".to_string(),
fields: vec![],
range: test_range(),
};
let completions = engine.complete_macro(&SummerMacro::DeriveService(service_macro), None);
let named_inject = completions
.iter()
.find(|c| c.label.contains("component = \"name\""))
.expect("应该有带名称的组件注入补全");
assert_eq!(
named_inject.insert_text_format,
Some(lsp_types::InsertTextFormat::SNIPPET),
"带名称的组件注入应该使用 SNIPPET 格式"
);
let insert_text = named_inject.insert_text.as_ref().unwrap();
assert!(
insert_text.contains("$1"),
"带名称的组件注入应该包含 snippet 占位符: {}",
insert_text
);
}
#[test]
fn test_route_macro_path_parameter_snippet() {
let engine = test_engine();
let route_macro = RouteMacro {
path: "/test".to_string(),
methods: vec![HttpMethod::Get],
middlewares: vec![],
handler_name: "handler".to_string(),
range: test_range(),
is_openapi: false,
};
let completions = engine.complete_macro(&SummerMacro::Route(route_macro), None);
let path_param = completions
.iter()
.find(|c| c.label == "{id}")
.expect("应该有路径参数补全");
assert_eq!(
path_param.insert_text_format,
Some(lsp_types::InsertTextFormat::SNIPPET),
"路径参数应该使用 SNIPPET 格式"
);
let insert_text = path_param.insert_text.as_ref().unwrap();
assert!(
insert_text.contains("${1:") && insert_text.contains("}"),
"路径参数应该包含 snippet 占位符: {}",
insert_text
);
assert!(
insert_text.starts_with("{") && insert_text.ends_with("}"),
"路径参数应该包含大括号: {}",
insert_text
);
}
#[test]
fn test_complete_with_empty_toml_document() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 0,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_at_document_boundary() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position_start = Position {
line: 0,
character: 0,
};
let completions = engine.complete(CompletionContext::Toml, position_start, Some(&doc), None);
let _ = completions.len();
let position_end = Position {
line: 100,
character: 100,
};
let completions = engine.complete(CompletionContext::Toml, position_end, Some(&doc), None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_with_invalid_position() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: u32::MAX,
character: u32::MAX,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_with_all_properties_used() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"\nport = 8080";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 20,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
let has_host = completions.iter().any(|c| c.label == "host");
let has_port = completions.iter().any(|c| c.label == "port");
assert!(!has_host, "host 已存在,不应该再补全");
assert!(!has_port, "port 已存在,不应该再补全");
}
#[test]
fn test_complete_with_unknown_section() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[unknown_plugin_12345]";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position = Position {
line: 0,
character: 10,
};
let completions = engine.complete(CompletionContext::Toml, position, Some(&doc), None);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_enum_with_empty_values() {
let engine = test_engine();
let empty_values: Vec<String> = vec![];
let completions = engine.complete_enum_values(&empty_values);
assert_eq!(completions.len(), 0);
}
#[test]
fn test_complete_enum_with_single_value() {
let engine = test_engine();
let single_value = vec!["only_one".to_string()];
let completions = engine.complete_enum_values(&single_value);
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].label, "only_one");
let insert_text = completions[0].insert_text.as_ref().unwrap();
assert_eq!(insert_text, "\"only_one\"");
}
#[test]
fn test_complete_with_special_characters_in_enum() {
let engine = test_engine();
let special_values = vec![
"value-with-dash".to_string(),
"value_with_underscore".to_string(),
"value.with.dot".to_string(),
];
let completions = engine.complete_enum_values(&special_values);
assert_eq!(completions.len(), 3);
for (i, value) in special_values.iter().enumerate() {
assert_eq!(completions[i].label, *value);
let insert_text = completions[i].insert_text.as_ref().unwrap();
assert_eq!(insert_text, &format!("\"{}\"", value));
}
}
#[test]
fn test_position_in_range_edge_cases() {
let engine = test_engine();
let single_point_range = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 10,
},
};
let pos_exact = Position {
line: 5,
character: 10,
};
assert!(engine.position_in_range(pos_exact, single_point_range));
let pos_before = Position {
line: 5,
character: 9,
};
assert!(!engine.position_in_range(pos_before, single_point_range));
let pos_after = Position {
line: 5,
character: 11,
};
assert!(!engine.position_in_range(pos_after, single_point_range));
let multi_line_range = Range {
start: Position {
line: 1,
character: 5,
},
end: Position {
line: 3,
character: 10,
},
};
let pos_middle_line = Position {
line: 2,
character: 0,
};
assert!(engine.position_in_range(pos_middle_line, multi_line_range));
let pos_middle_line_end = Position {
line: 2,
character: 999,
};
assert!(engine.position_in_range(pos_middle_line_end, multi_line_range));
}
#[test]
fn test_type_info_to_hint_with_ranges() {
let engine = test_engine();
let int_with_range = crate::schema::TypeInfo::Integer {
min: Some(1),
max: Some(100),
};
let hint = engine.type_info_to_hint(&int_with_range);
assert!(hint.contains("1"));
assert!(hint.contains("100"));
assert!(hint.contains("integer"));
let int_with_min = crate::schema::TypeInfo::Integer {
min: Some(0),
max: None,
};
let hint = engine.type_info_to_hint(&int_with_min);
assert_eq!(hint, "integer");
let int_with_max = crate::schema::TypeInfo::Integer {
min: None,
max: Some(255),
};
let hint = engine.type_info_to_hint(&int_with_max);
assert_eq!(hint, "integer");
}
#[test]
fn test_type_info_to_default_for_all_types() {
let engine = test_engine();
let test_cases = vec![
(
crate::schema::TypeInfo::String {
enum_values: None,
min_length: None,
max_length: None,
},
"\"\"",
),
(
crate::schema::TypeInfo::Integer {
min: None,
max: None,
},
"0",
),
(
crate::schema::TypeInfo::Float {
min: None,
max: None,
},
"0.0",
),
(crate::schema::TypeInfo::Boolean, "false"),
(
crate::schema::TypeInfo::Array {
item_type: Box::new(crate::schema::TypeInfo::String {
enum_values: None,
min_length: None,
max_length: None,
}),
},
"[]",
),
(
crate::schema::TypeInfo::Object {
properties: std::collections::HashMap::new(),
},
"{}",
),
];
for (type_info, expected_default) in test_cases {
let default = engine.type_info_to_default(&type_info);
assert_eq!(
default, expected_default,
"类型 {:?} 的默认值不正确",
type_info
);
}
}
#[test]
fn test_value_to_string_for_all_value_types() {
let engine = test_engine();
let test_cases = vec![
(crate::schema::Value::String("test".to_string()), "\"test\""),
(crate::schema::Value::Integer(42), "42"),
(crate::schema::Value::Integer(-10), "-10"),
(crate::schema::Value::Float(3.5), "3.5"),
(crate::schema::Value::Float(-2.5), "-2.5"),
(crate::schema::Value::Boolean(true), "true"),
(crate::schema::Value::Boolean(false), "false"),
(crate::schema::Value::Array(vec![]), "[]"),
(
crate::schema::Value::Table(std::collections::HashMap::new()),
"{}",
),
];
for (value, expected_string) in test_cases {
let string = engine.value_to_string(&value);
assert_eq!(string, expected_string, "值 {:?} 的字符串表示不正确", value);
}
}
#[test]
fn test_complete_with_nested_config_sections() {
let engine = test_engine();
let schema_provider = SchemaProvider::default();
let toml_analyzer = TomlAnalyzer::new(schema_provider);
let toml_content = "[web]\nhost = \"localhost\"\n\n[redis]\nurl = \"redis://localhost\"";
let doc = toml_analyzer.parse(toml_content).unwrap();
let position_web = Position {
line: 0,
character: 20,
};
let completions_web = engine.complete(CompletionContext::Toml, position_web, Some(&doc), None);
let has_url = completions_web.iter().any(|c| c.label == "url");
assert!(!has_url, "在 web 配置节内不应该补全 redis 的属性");
}
#[test]
fn test_completion_context_equality() {
let toml1 = CompletionContext::Toml;
let toml2 = CompletionContext::Toml;
let macro1 = CompletionContext::Macro;
let unknown = CompletionContext::Unknown;
match (toml1, toml2) {
(CompletionContext::Toml, CompletionContext::Toml) => {}
_ => panic!("相同的 Toml 上下文应该匹配"),
}
match (macro1, unknown) {
(CompletionContext::Macro, CompletionContext::Unknown) => {}
_ => panic!("不同的上下文应该不匹配"),
}
}