integration_python.rs
1 mod common; 2 use common::TestFixture; 3 use ecolog_lsp::server::handlers::compute_diagnostics; 4 use ecolog_lsp::server::handlers::handle_completion; 5 use ecolog_lsp::server::handlers::handle_hover; 6 use tower_lsp::lsp_types::{ 7 CompletionContext, CompletionParams, CompletionTriggerKind, HoverParams, Position, 8 TextDocumentIdentifier, TextDocumentPositionParams, 9 }; 10 11 #[tokio::test] 12 async fn test_py_hover_environ_getitem() { 13 let fixture = TestFixture::new().await; 14 let uri = fixture.create_file("test.py", "import os\nval = os.environ['DB_URL']"); 15 16 fixture 17 .state 18 .document_manager 19 .open( 20 uri.clone(), 21 "python".to_string(), 22 "import os\nval = os.environ['DB_URL']".to_string(), 23 0, 24 ) 25 .await; 26 27 let hover = handle_hover( 28 HoverParams { 29 text_document_position_params: TextDocumentPositionParams { 30 text_document: TextDocumentIdentifier { uri }, 31 position: Position::new(1, 20), 32 }, 33 work_done_progress_params: Default::default(), 34 }, 35 &fixture.state, 36 ) 37 .await; 38 39 assert!(hover.is_some()); 40 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); 41 } 42 43 #[tokio::test] 44 async fn test_py_hover_environ_get() { 45 let fixture = TestFixture::new().await; 46 let uri = fixture.create_file("test.py", "import os\nval = os.environ.get('API_KEY')"); 47 48 fixture 49 .state 50 .document_manager 51 .open( 52 uri.clone(), 53 "python".to_string(), 54 "import os\nval = os.environ.get('API_KEY')".to_string(), 55 0, 56 ) 57 .await; 58 59 let hover = handle_hover( 60 HoverParams { 61 text_document_position_params: TextDocumentPositionParams { 62 text_document: TextDocumentIdentifier { uri }, 63 position: Position::new(1, 25), 64 }, 65 work_done_progress_params: Default::default(), 66 }, 67 &fixture.state, 68 ) 69 .await; 70 71 assert!(hover.is_some()); 72 assert!(format!("{:?}", hover.unwrap()).contains("secret_key")); 73 } 74 75 #[tokio::test] 76 async fn test_py_hover_os_getenv() { 77 let fixture = TestFixture::new().await; 78 let uri = fixture.create_file("test.py", "import os\nval = os.getenv('PORT')"); 79 80 fixture 81 .state 82 .document_manager 83 .open( 84 uri.clone(), 85 "python".to_string(), 86 "import os\nval = os.getenv('PORT')".to_string(), 87 0, 88 ) 89 .await; 90 91 let hover = handle_hover( 92 HoverParams { 93 text_document_position_params: TextDocumentPositionParams { 94 text_document: TextDocumentIdentifier { uri }, 95 position: Position::new(1, 18), 96 }, 97 work_done_progress_params: Default::default(), 98 }, 99 &fixture.state, 100 ) 101 .await; 102 103 assert!(hover.is_some()); 104 assert!(format!("{:?}", hover.unwrap()).contains("8080")); 105 } 106 107 #[tokio::test] 108 async fn test_py_completion_environ() { 109 let fixture = TestFixture::new().await; 110 let uri = fixture.create_file("test.py", "import os\nos.environ['"); 111 112 fixture 113 .state 114 .document_manager 115 .open( 116 uri.clone(), 117 "python".to_string(), 118 "import os\nos.environ['".to_string(), 119 0, 120 ) 121 .await; 122 123 let completion = handle_completion( 124 CompletionParams { 125 text_document_position: TextDocumentPositionParams { 126 text_document: TextDocumentIdentifier { uri }, 127 position: Position::new(1, 12), 128 }, 129 work_done_progress_params: Default::default(), 130 partial_result_params: Default::default(), 131 context: Some(CompletionContext { 132 trigger_kind: CompletionTriggerKind::INVOKED, 133 trigger_character: None, 134 }), 135 }, 136 &fixture.state, 137 ) 138 .await; 139 140 assert!(completion.is_some()); 141 let items = completion.unwrap(); 142 assert!(items.iter().any(|i| i.label == "DB_URL")); 143 } 144 145 #[tokio::test] 146 async fn test_py_hover_from_import() { 147 let fixture = TestFixture::new().await; 148 let uri = fixture.create_file("test.py", "from os import environ\nx = environ['DEBUG']"); 149 150 fixture 151 .state 152 .document_manager 153 .open( 154 uri.clone(), 155 "python".to_string(), 156 "from os import environ\nx = environ['DEBUG']".to_string(), 157 0, 158 ) 159 .await; 160 161 let hover = handle_hover( 162 HoverParams { 163 text_document_position_params: TextDocumentPositionParams { 164 text_document: TextDocumentIdentifier { uri }, 165 position: Position::new(1, 15), 166 }, 167 work_done_progress_params: Default::default(), 168 }, 169 &fixture.state, 170 ) 171 .await; 172 173 if hover.is_some() { 174 assert!(format!("{:?}", hover.unwrap()).contains("true")); 175 } 176 } 177 178 #[tokio::test] 179 async fn test_py_hover_walrus_operator_environ_get() { 180 let fixture = TestFixture::new().await; 181 let uri = fixture.create_file( 182 "test.py", 183 "import os\nif (db_url := os.environ.get('DB_URL')):\n print(db_url)", 184 ); 185 fixture 186 .state 187 .document_manager 188 .open( 189 uri.clone(), 190 "python".to_string(), 191 "import os\nif (db_url := os.environ.get('DB_URL')):\n print(db_url)".to_string(), 192 0, 193 ) 194 .await; 195 196 let hover = handle_hover( 197 HoverParams { 198 text_document_position_params: TextDocumentPositionParams { 199 text_document: TextDocumentIdentifier { uri }, 200 position: Position::new(1, 6), 201 }, 202 work_done_progress_params: Default::default(), 203 }, 204 &fixture.state, 205 ) 206 .await; 207 208 assert!(hover.is_some()); 209 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); 210 } 211 212 #[tokio::test] 213 async fn test_py_hover_walrus_operator_getenv() { 214 let fixture = TestFixture::new().await; 215 let uri = fixture.create_file( 216 "test.py", 217 "import os\nif (api_key := os.getenv('API_KEY')):\n print(api_key)", 218 ); 219 fixture 220 .state 221 .document_manager 222 .open( 223 uri.clone(), 224 "python".to_string(), 225 "import os\nif (api_key := os.getenv('API_KEY')):\n print(api_key)".to_string(), 226 0, 227 ) 228 .await; 229 230 let hover = handle_hover( 231 HoverParams { 232 text_document_position_params: TextDocumentPositionParams { 233 text_document: TextDocumentIdentifier { uri }, 234 position: Position::new(1, 6), 235 }, 236 work_done_progress_params: Default::default(), 237 }, 238 &fixture.state, 239 ) 240 .await; 241 242 assert!(hover.is_some()); 243 assert!(format!("{:?}", hover.unwrap()).contains("secret_key")); 244 } 245 246 #[tokio::test] 247 async fn test_py_hover_walrus_operator_subscript() { 248 let fixture = TestFixture::new().await; 249 let uri = fixture.create_file( 250 "test.py", 251 "import os\nif (port := os.environ['PORT']):\n print(port)", 252 ); 253 fixture 254 .state 255 .document_manager 256 .open( 257 uri.clone(), 258 "python".to_string(), 259 "import os\nif (port := os.environ['PORT']):\n print(port)".to_string(), 260 0, 261 ) 262 .await; 263 264 let hover = handle_hover( 265 HoverParams { 266 text_document_position_params: TextDocumentPositionParams { 267 text_document: TextDocumentIdentifier { uri }, 268 position: Position::new(1, 6), 269 }, 270 work_done_progress_params: Default::default(), 271 }, 272 &fixture.state, 273 ) 274 .await; 275 276 assert!(hover.is_some()); 277 assert!(format!("{:?}", hover.unwrap()).contains("8080")); 278 } 279 280 #[tokio::test] 281 async fn test_py_hover_walrus_operator_while_loop() { 282 let fixture = TestFixture::new().await; 283 let uri = fixture.create_file( 284 "test.py", 285 "import os\nwhile (val := os.getenv('PORT')):\n print(val)\n break", 286 ); 287 fixture 288 .state 289 .document_manager 290 .open( 291 uri.clone(), 292 "python".to_string(), 293 "import os\nwhile (val := os.getenv('PORT')):\n print(val)\n break".to_string(), 294 0, 295 ) 296 .await; 297 298 let hover = handle_hover( 299 HoverParams { 300 text_document_position_params: TextDocumentPositionParams { 301 text_document: TextDocumentIdentifier { uri }, 302 position: Position::new(1, 8), 303 }, 304 work_done_progress_params: Default::default(), 305 }, 306 &fixture.state, 307 ) 308 .await; 309 310 assert!(hover.is_some()); 311 assert!(format!("{:?}", hover.unwrap()).contains("8080")); 312 } 313 314 #[tokio::test] 315 async fn test_py_diagnostics_walrus_undefined() { 316 let fixture = TestFixture::new().await; 317 let uri = fixture.create_file( 318 "test.py", 319 "import os\nif (missing := os.environ.get('MISSING_VAR')):\n print(missing)", 320 ); 321 fixture 322 .state 323 .document_manager 324 .open( 325 uri.clone(), 326 "python".to_string(), 327 "import os\nif (missing := os.environ.get('MISSING_VAR')):\n print(missing)" 328 .to_string(), 329 0, 330 ) 331 .await; 332 333 let diags = compute_diagnostics(&uri, &fixture.state).await; 334 335 assert!(!diags.is_empty()); 336 assert!(diags.iter().any(|d| d.message.contains("not defined"))); 337 } 338 339 #[tokio::test] 340 async fn test_py_diagnostics_walrus_getenv_undefined() { 341 let fixture = TestFixture::new().await; 342 let uri = fixture.create_file( 343 "test.py", 344 "import os\nif (missing := os.getenv('MISSING_VAR')):\n print(missing)", 345 ); 346 fixture 347 .state 348 .document_manager 349 .open( 350 uri.clone(), 351 "python".to_string(), 352 "import os\nif (missing := os.getenv('MISSING_VAR')):\n print(missing)".to_string(), 353 0, 354 ) 355 .await; 356 357 let diags = compute_diagnostics(&uri, &fixture.state).await; 358 359 assert!(!diags.is_empty()); 360 assert!(diags.iter().any(|d| d.message.contains("not defined"))); 361 } 362 363 #[tokio::test] 364 async fn test_py_walrus_operator_multiple_in_if() { 365 let fixture = TestFixture::new().await; 366 let uri = fixture.create_file("test.py", "import os\nif (db_url := os.environ.get('DB_URL')) and (api_key := os.environ.get('API_KEY')):\n print(db_url, api_key)"); 367 fixture.state.document_manager.open(uri.clone(), "python".to_string(), 368 "import os\nif (db_url := os.environ.get('DB_URL')) and (api_key := os.environ.get('API_KEY')):\n print(db_url, api_key)".to_string(), 0).await; 369 370 let hover = handle_hover( 371 HoverParams { 372 text_document_position_params: TextDocumentPositionParams { 373 text_document: TextDocumentIdentifier { uri: uri.clone() }, 374 position: Position::new(1, 6), 375 }, 376 work_done_progress_params: Default::default(), 377 }, 378 &fixture.state, 379 ) 380 .await; 381 382 assert!(hover.is_some()); 383 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); 384 385 let hover = handle_hover( 386 HoverParams { 387 text_document_position_params: TextDocumentPositionParams { 388 text_document: TextDocumentIdentifier { uri: uri.clone() }, 389 position: Position::new(1, 45), 390 }, 391 work_done_progress_params: Default::default(), 392 }, 393 &fixture.state, 394 ) 395 .await; 396 397 assert!(hover.is_some()); 398 assert!(format!("{:?}", hover.unwrap()).contains("secret_key")); 399 } 400 401 #[tokio::test] 402 async fn test_py_walrus_operator_undefined_and_defined_mix() { 403 let fixture = TestFixture::new().await; 404 let uri = fixture.create_file("test.py", "import os\nif (db_url := os.environ.get('DB_URL')) and (missing := os.environ.get('MISSING_VAR')):\n print(db_url, missing)"); 405 fixture.state.document_manager.open(uri.clone(), "python".to_string(), 406 "import os\nif (db_url := os.environ.get('DB_URL')) and (missing := os.environ.get('MISSING_VAR')):\n print(db_url, missing)".to_string(), 0).await; 407 408 let diags = compute_diagnostics(&uri, &fixture.state).await; 409 410 assert!(!diags.is_empty()); 411 assert!(diags.iter().any(|d| d.message.contains("MISSING_VAR"))); 412 } 413 414 // ═══════════════════════════════════════════════════════════════════════════ 415 // New Pattern Tests: decouple, function defaults, dict literals, alias subscript 416 // ═══════════════════════════════════════════════════════════════════════════ 417 418 #[tokio::test] 419 async fn test_py_hover_decouple_config() { 420 let fixture = TestFixture::new().await; 421 let content = "from decouple import config\ndb = config('DB_URL')"; 422 let uri = fixture.create_file("test.py", content); 423 424 fixture 425 .state 426 .document_manager 427 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 428 .await; 429 430 let hover = handle_hover( 431 HoverParams { 432 text_document_position_params: TextDocumentPositionParams { 433 text_document: TextDocumentIdentifier { uri }, 434 position: Position::new(1, 14), 435 }, 436 work_done_progress_params: Default::default(), 437 }, 438 &fixture.state, 439 ) 440 .await; 441 442 assert!(hover.is_some(), "Expected hover for decouple config()"); 443 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); //")); 444 } 445 446 #[tokio::test] 447 async fn test_py_hover_function_default_getenv() { 448 let fixture = TestFixture::new().await; 449 let content = "import os\ndef connect(db=os.getenv('DB_URL')):\n pass"; 450 let uri = fixture.create_file("test.py", content); 451 452 fixture 453 .state 454 .document_manager 455 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 456 .await; 457 458 let hover = handle_hover( 459 HoverParams { 460 text_document_position_params: TextDocumentPositionParams { 461 text_document: TextDocumentIdentifier { uri }, 462 position: Position::new(1, 27), 463 }, 464 work_done_progress_params: Default::default(), 465 }, 466 &fixture.state, 467 ) 468 .await; 469 470 assert!( 471 hover.is_some(), 472 "Expected hover for env var in function default" 473 ); 474 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); //")); 475 } 476 477 #[tokio::test] 478 async fn test_py_hover_function_default_environ_subscript() { 479 let fixture = TestFixture::new().await; 480 let content = "import os\ndef connect(port=os.environ['PORT']):\n pass"; 481 let uri = fixture.create_file("test.py", content); 482 483 fixture 484 .state 485 .document_manager 486 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 487 .await; 488 489 let hover = handle_hover( 490 HoverParams { 491 text_document_position_params: TextDocumentPositionParams { 492 text_document: TextDocumentIdentifier { uri }, 493 position: Position::new(1, 30), 494 }, 495 work_done_progress_params: Default::default(), 496 }, 497 &fixture.state, 498 ) 499 .await; 500 501 assert!( 502 hover.is_some(), 503 "Expected hover for environ subscript in function default" 504 ); 505 assert!(format!("{:?}", hover.unwrap()).contains("8080")); 506 } 507 508 #[tokio::test] 509 async fn test_py_hover_dict_literal_environ() { 510 let fixture = TestFixture::new().await; 511 let content = "import os\nconfig = {'db': os.environ['DB_URL']}"; 512 let uri = fixture.create_file("test.py", content); 513 514 fixture 515 .state 516 .document_manager 517 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 518 .await; 519 520 let hover = handle_hover( 521 HoverParams { 522 text_document_position_params: TextDocumentPositionParams { 523 text_document: TextDocumentIdentifier { uri }, 524 position: Position::new(1, 30), 525 }, 526 work_done_progress_params: Default::default(), 527 }, 528 &fixture.state, 529 ) 530 .await; 531 532 assert!( 533 hover.is_some(), 534 "Expected hover for environ in dict literal" 535 ); 536 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); //")); 537 } 538 539 #[tokio::test] 540 async fn test_py_hover_dict_literal_getenv() { 541 let fixture = TestFixture::new().await; 542 let content = "import os\nconfig = {'port': os.getenv('PORT')}"; 543 let uri = fixture.create_file("test.py", content); 544 545 fixture 546 .state 547 .document_manager 548 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 549 .await; 550 551 let hover = handle_hover( 552 HoverParams { 553 text_document_position_params: TextDocumentPositionParams { 554 text_document: TextDocumentIdentifier { uri }, 555 position: Position::new(1, 30), 556 }, 557 work_done_progress_params: Default::default(), 558 }, 559 &fixture.state, 560 ) 561 .await; 562 563 assert!(hover.is_some(), "Expected hover for getenv in dict literal"); 564 assert!(format!("{:?}", hover.unwrap()).contains("8080")); 565 } 566 567 #[tokio::test] 568 async fn test_py_hover_config_class_environ() { 569 let fixture = TestFixture::new().await; 570 let content = "import os\nclass Config:\n DB = os.environ['DB_URL']"; 571 let uri = fixture.create_file("test.py", content); 572 573 fixture 574 .state 575 .document_manager 576 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 577 .await; 578 579 let hover = handle_hover( 580 HoverParams { 581 text_document_position_params: TextDocumentPositionParams { 582 text_document: TextDocumentIdentifier { uri }, 583 position: Position::new(2, 22), 584 }, 585 work_done_progress_params: Default::default(), 586 }, 587 &fixture.state, 588 ) 589 .await; 590 591 assert!( 592 hover.is_some(), 593 "Expected hover for environ in config class" 594 ); 595 assert!(format!("{:?}", hover.unwrap()).contains("postgres://")); //")); 596 } 597 598 #[tokio::test] 599 async fn test_py_hover_config_class_getenv() { 600 let fixture = TestFixture::new().await; 601 let content = "import os\nclass Config:\n PORT = os.getenv('PORT')"; 602 let uri = fixture.create_file("test.py", content); 603 604 fixture 605 .state 606 .document_manager 607 .open(uri.clone(), "python".to_string(), content.to_string(), 0) 608 .await; 609 610 let hover = handle_hover( 611 HoverParams { 612 text_document_position_params: TextDocumentPositionParams { 613 text_document: TextDocumentIdentifier { uri }, 614 position: Position::new(2, 23), 615 }, 616 work_done_progress_params: Default::default(), 617 }, 618 &fixture.state, 619 ) 620 .await; 621 622 assert!(hover.is_some(), "Expected hover for getenv in config class"); 623 assert!(format!("{:?}", hover.unwrap()).contains("8080")); 624 }