/ tests / integration_python.rs
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  }