mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 12:05:57 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1879 lines
		
	
	
	
		
			59 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			1879 lines
		
	
	
	
		
			59 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use std::collections::HashSet;
 | |
| use std::io::Write;
 | |
| use std::time::{Duration, Instant};
 | |
| 
 | |
| use anyhow::{Context, anyhow};
 | |
| use ruff_db::Db as _;
 | |
| use ruff_db::files::{File, FileError, system_path_to_file};
 | |
| use ruff_db::source::source_text;
 | |
| use ruff_db::system::{
 | |
|     OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard, file_time_now,
 | |
| };
 | |
| use ruff_python_ast::PythonVersion;
 | |
| use ty_project::metadata::options::{EnvironmentOptions, Options, ProjectOptionsOverrides};
 | |
| use ty_project::metadata::pyproject::{PyProject, Tool};
 | |
| use ty_project::metadata::value::{RangedValue, RelativePathBuf};
 | |
| use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
 | |
| use ty_project::{Db, ProjectDatabase, ProjectMetadata};
 | |
| use ty_python_semantic::{ModuleName, PythonPlatform, resolve_module};
 | |
| 
 | |
| struct TestCase {
 | |
|     db: ProjectDatabase,
 | |
|     watcher: Option<ProjectWatcher>,
 | |
|     changes_receiver: crossbeam::channel::Receiver<Vec<ChangeEvent>>,
 | |
|     /// The temporary directory that contains the test files.
 | |
|     /// We need to hold on to it in the test case or the temp files get deleted.
 | |
|     _temp_dir: tempfile::TempDir,
 | |
|     root_dir: SystemPathBuf,
 | |
| }
 | |
| 
 | |
| impl TestCase {
 | |
|     fn project_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
 | |
|         SystemPath::absolute(relative, self.db.project().root(&self.db))
 | |
|     }
 | |
| 
 | |
|     fn root_path(&self) -> &SystemPath {
 | |
|         &self.root_dir
 | |
|     }
 | |
| 
 | |
|     fn db(&self) -> &ProjectDatabase {
 | |
|         &self.db
 | |
|     }
 | |
| 
 | |
|     #[track_caller]
 | |
|     fn stop_watch<M>(&mut self, matcher: M) -> Vec<ChangeEvent>
 | |
|     where
 | |
|         M: MatchEvent,
 | |
|     {
 | |
|         // track_caller is unstable for lambdas -> That's why this is a fn
 | |
|         #[track_caller]
 | |
|         fn panic_with_formatted_events(events: Vec<ChangeEvent>) -> Vec<ChangeEvent> {
 | |
|             panic!(
 | |
|                 "Didn't observe the expected event. The following events occurred:\n{}",
 | |
|                 events
 | |
|                     .into_iter()
 | |
|                     .map(|event| format!("  - {event:?}"))
 | |
|                     .collect::<Vec<_>>()
 | |
|                     .join("\n")
 | |
|             )
 | |
|         }
 | |
| 
 | |
|         self.try_stop_watch(matcher, Duration::from_secs(10))
 | |
|             .unwrap_or_else(panic_with_formatted_events)
 | |
|     }
 | |
| 
 | |
|     fn try_stop_watch<M>(
 | |
|         &mut self,
 | |
|         mut matcher: M,
 | |
|         timeout: Duration,
 | |
|     ) -> Result<Vec<ChangeEvent>, Vec<ChangeEvent>>
 | |
|     where
 | |
|         M: MatchEvent,
 | |
|     {
 | |
|         tracing::debug!("Try stopping watch with timeout {:?}", timeout);
 | |
| 
 | |
|         let watcher = self
 | |
|             .watcher
 | |
|             .take()
 | |
|             .expect("Cannot call `stop_watch` more than once");
 | |
| 
 | |
|         let start = Instant::now();
 | |
|         let mut all_events = Vec::new();
 | |
| 
 | |
|         loop {
 | |
|             let events = self
 | |
|                 .changes_receiver
 | |
|                 .recv_timeout(Duration::from_millis(100))
 | |
|                 .unwrap_or_default();
 | |
| 
 | |
|             if events
 | |
|                 .iter()
 | |
|                 .any(|event| matcher.match_event(event) || event.is_rescan())
 | |
|             {
 | |
|                 all_events.extend(events);
 | |
|                 break;
 | |
|             }
 | |
| 
 | |
|             all_events.extend(events);
 | |
| 
 | |
|             if start.elapsed() > timeout {
 | |
|                 return Err(all_events);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         watcher.flush();
 | |
|         tracing::debug!("Flushed file watcher");
 | |
|         watcher.stop();
 | |
|         tracing::debug!("Stopping file watcher");
 | |
| 
 | |
|         // Consume remaining events
 | |
|         for event in &self.changes_receiver {
 | |
|             all_events.extend(event);
 | |
|         }
 | |
| 
 | |
|         Ok(all_events)
 | |
|     }
 | |
| 
 | |
|     fn take_watch_changes<M: MatchEvent>(&self, matcher: M) -> Vec<ChangeEvent> {
 | |
|         self.try_take_watch_changes(matcher, Duration::from_secs(10))
 | |
|             .expect("Expected watch changes but observed none")
 | |
|     }
 | |
| 
 | |
|     fn try_take_watch_changes<M: MatchEvent>(
 | |
|         &self,
 | |
|         mut matcher: M,
 | |
|         timeout: Duration,
 | |
|     ) -> Result<Vec<ChangeEvent>, Vec<ChangeEvent>> {
 | |
|         let watcher = self
 | |
|             .watcher
 | |
|             .as_ref()
 | |
|             .expect("Cannot call `try_take_watch_changes` after `stop_watch`");
 | |
| 
 | |
|         let start = Instant::now();
 | |
|         let mut all_events = Vec::new();
 | |
| 
 | |
|         loop {
 | |
|             let events = self
 | |
|                 .changes_receiver
 | |
|                 .recv_timeout(Duration::from_millis(100))
 | |
|                 .unwrap_or_default();
 | |
| 
 | |
|             if events
 | |
|                 .iter()
 | |
|                 .any(|event| matcher.match_event(event) || event.is_rescan())
 | |
|             {
 | |
|                 all_events.extend(events);
 | |
|                 break;
 | |
|             }
 | |
| 
 | |
|             all_events.extend(events);
 | |
| 
 | |
|             if start.elapsed() > timeout {
 | |
|                 return Err(all_events);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         while let Ok(event) = self
 | |
|             .changes_receiver
 | |
|             .recv_timeout(Duration::from_millis(10))
 | |
|         {
 | |
|             all_events.extend(event);
 | |
|             watcher.flush();
 | |
|         }
 | |
| 
 | |
|         Ok(all_events)
 | |
|     }
 | |
| 
 | |
|     fn apply_changes(
 | |
|         &mut self,
 | |
|         changes: Vec<ChangeEvent>,
 | |
|         project_options_overrides: Option<&ProjectOptionsOverrides>,
 | |
|     ) {
 | |
|         self.db.apply_changes(changes, project_options_overrides);
 | |
|     }
 | |
| 
 | |
|     fn update_options(&mut self, options: Options) -> anyhow::Result<()> {
 | |
|         std::fs::write(
 | |
|             self.project_path("pyproject.toml").as_std_path(),
 | |
|             toml::to_string(&PyProject {
 | |
|                 project: None,
 | |
|                 tool: Some(Tool { ty: Some(options) }),
 | |
|             })
 | |
|             .context("Failed to serialize options")?,
 | |
|         )
 | |
|         .context("Failed to write configuration")?;
 | |
| 
 | |
|         let changes = self.take_watch_changes(event_for_file("pyproject.toml"));
 | |
|         self.apply_changes(changes, None);
 | |
| 
 | |
|         if let Some(watcher) = &mut self.watcher {
 | |
|             watcher.update(&self.db);
 | |
|             assert!(!watcher.has_errored_paths());
 | |
|         }
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     #[track_caller]
 | |
|     fn assert_indexed_project_files(&self, expected: impl IntoIterator<Item = File>) {
 | |
|         let mut expected: HashSet<_> = expected.into_iter().collect();
 | |
| 
 | |
|         let actual = self.db().project().files(self.db());
 | |
|         for file in &actual {
 | |
|             assert!(
 | |
|                 expected.remove(&file),
 | |
|                 "Indexed project files contains '{}' which was not expected.",
 | |
|                 file.path(self.db())
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         if !expected.is_empty() {
 | |
|             let paths: Vec<_> = expected
 | |
|                 .iter()
 | |
|                 .map(|file| file.path(self.db()).as_str())
 | |
|                 .collect();
 | |
|             panic!(
 | |
|                 "Indexed project files are missing the following files: {:?}",
 | |
|                 paths.join(", ")
 | |
|             );
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn system_file(&self, path: impl AsRef<SystemPath>) -> Result<File, FileError> {
 | |
|         system_path_to_file(self.db(), path.as_ref())
 | |
|     }
 | |
| }
 | |
| 
 | |
| trait MatchEvent {
 | |
|     fn match_event(&mut self, event: &ChangeEvent) -> bool;
 | |
| }
 | |
| 
 | |
| fn event_for_file(name: &str) -> impl MatchEvent + '_ {
 | |
|     |event: &ChangeEvent| event.file_name() == Some(name)
 | |
| }
 | |
| 
 | |
| impl<F> MatchEvent for F
 | |
| where
 | |
|     F: FnMut(&ChangeEvent) -> bool,
 | |
| {
 | |
|     fn match_event(&mut self, event: &ChangeEvent) -> bool {
 | |
|         (*self)(event)
 | |
|     }
 | |
| }
 | |
| 
 | |
| trait Setup {
 | |
|     fn setup(self, context: &mut SetupContext) -> anyhow::Result<()>;
 | |
| }
 | |
| 
 | |
| struct SetupContext<'a> {
 | |
|     system: &'a OsSystem,
 | |
|     root_path: &'a SystemPath,
 | |
|     options: Option<Options>,
 | |
|     included_paths: Option<Vec<SystemPathBuf>>,
 | |
| }
 | |
| 
 | |
| impl<'a> SetupContext<'a> {
 | |
|     fn system(&self) -> &'a OsSystem {
 | |
|         self.system
 | |
|     }
 | |
| 
 | |
|     fn join_project_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
 | |
|         self.project_path().join(relative)
 | |
|     }
 | |
| 
 | |
|     fn project_path(&self) -> &SystemPath {
 | |
|         self.system.current_directory()
 | |
|     }
 | |
| 
 | |
|     fn root_path(&self) -> &'a SystemPath {
 | |
|         self.root_path
 | |
|     }
 | |
| 
 | |
|     fn join_root_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
 | |
|         self.root_path().join(relative)
 | |
|     }
 | |
| 
 | |
|     fn write_project_file(
 | |
|         &self,
 | |
|         relative_path: impl AsRef<SystemPath>,
 | |
|         content: &str,
 | |
|     ) -> anyhow::Result<()> {
 | |
|         let relative_path = relative_path.as_ref();
 | |
|         let absolute_path = self.join_project_path(relative_path);
 | |
|         Self::write_file_impl(absolute_path, content)
 | |
|     }
 | |
| 
 | |
|     fn write_file(
 | |
|         &self,
 | |
|         relative_path: impl AsRef<SystemPath>,
 | |
|         content: &str,
 | |
|     ) -> anyhow::Result<()> {
 | |
|         let relative_path = relative_path.as_ref();
 | |
|         let absolute_path = self.join_root_path(relative_path);
 | |
|         Self::write_file_impl(absolute_path, content)
 | |
|     }
 | |
| 
 | |
|     fn write_file_impl(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()> {
 | |
|         let path = path.as_ref();
 | |
|         if let Some(parent) = path.parent() {
 | |
|             std::fs::create_dir_all(parent)
 | |
|                 .with_context(|| format!("Failed to create parent directory for file `{path}`"))?;
 | |
|         }
 | |
| 
 | |
|         let mut file = std::fs::File::create(path.as_std_path())
 | |
|             .with_context(|| format!("Failed to open file `{path}`"))?;
 | |
|         file.write_all(content.as_bytes())
 | |
|             .with_context(|| format!("Failed to write to file `{path}`"))?;
 | |
|         file.sync_data()?;
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn set_options(&mut self, options: Options) {
 | |
|         self.options = Some(options);
 | |
|     }
 | |
| 
 | |
|     fn set_included_paths(&mut self, paths: Vec<SystemPathBuf>) {
 | |
|         self.included_paths = Some(paths);
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<const N: usize, P> Setup for [(P, &'static str); N]
 | |
| where
 | |
|     P: AsRef<SystemPath>,
 | |
| {
 | |
|     fn setup(self, context: &mut SetupContext) -> anyhow::Result<()> {
 | |
|         for (relative_path, content) in self {
 | |
|             context.write_project_file(relative_path, content)?;
 | |
|         }
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<F> Setup for F
 | |
| where
 | |
|     F: FnOnce(&mut SetupContext) -> anyhow::Result<()>,
 | |
| {
 | |
|     fn setup(self, context: &mut SetupContext) -> anyhow::Result<()> {
 | |
|         self(context)
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
 | |
| where
 | |
|     F: Setup,
 | |
| {
 | |
|     let temp_dir = tempfile::tempdir()?;
 | |
| 
 | |
|     let root_path = SystemPath::from_std_path(temp_dir.path()).ok_or_else(|| {
 | |
|         anyhow!(
 | |
|             "Temporary directory `{}` is not a valid UTF-8 path.",
 | |
|             temp_dir.path().display()
 | |
|         )
 | |
|     })?;
 | |
| 
 | |
|     let root_path = SystemPathBuf::from_utf8_path_buf(
 | |
|         root_path
 | |
|             .as_utf8_path()
 | |
|             .canonicalize_utf8()
 | |
|             .with_context(|| "Failed to canonicalize root path.")?,
 | |
|     )
 | |
|     .simplified()
 | |
|     .to_path_buf();
 | |
| 
 | |
|     let project_path = root_path.join("project");
 | |
| 
 | |
|     std::fs::create_dir_all(project_path.as_std_path())
 | |
|         .with_context(|| format!("Failed to create project directory `{project_path}`"))?;
 | |
| 
 | |
|     let system = OsSystem::new(&project_path);
 | |
|     let mut setup_context = SetupContext {
 | |
|         system: &system,
 | |
|         root_path: &root_path,
 | |
|         options: None,
 | |
|         included_paths: None,
 | |
|     };
 | |
| 
 | |
|     setup_files
 | |
|         .setup(&mut setup_context)
 | |
|         .context("Failed to setup test files")?;
 | |
| 
 | |
|     if let Some(options) = setup_context.options {
 | |
|         std::fs::write(
 | |
|             project_path.join("pyproject.toml").as_std_path(),
 | |
|             toml::to_string(&PyProject {
 | |
|                 project: None,
 | |
|                 tool: Some(Tool { ty: Some(options) }),
 | |
|             })
 | |
|             .context("Failed to serialize options")?,
 | |
|         )
 | |
|         .context("Failed to write configuration")?;
 | |
|     }
 | |
| 
 | |
|     let included_paths = setup_context.included_paths;
 | |
| 
 | |
|     let mut project = ProjectMetadata::discover(&project_path, &system)?;
 | |
|     project.apply_configuration_files(&system)?;
 | |
| 
 | |
|     // We need a chance to create the directories here.
 | |
|     if let Some(environment) = project.options().environment.as_ref() {
 | |
|         for path in environment
 | |
|             .extra_paths
 | |
|             .as_deref()
 | |
|             .unwrap_or_default()
 | |
|             .iter()
 | |
|             .chain(environment.typeshed.as_ref())
 | |
|         {
 | |
|             std::fs::create_dir_all(path.absolute(&project_path, &system).as_std_path())
 | |
|                 .with_context(|| format!("Failed to create search path `{path}`"))?;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     let mut db = ProjectDatabase::new(project, system)?;
 | |
| 
 | |
|     if let Some(included_paths) = included_paths {
 | |
|         db.project().set_included_paths(&mut db, included_paths);
 | |
|     }
 | |
| 
 | |
|     let (sender, receiver) = crossbeam::channel::unbounded();
 | |
|     let watcher = directory_watcher(move |events| sender.send(events).unwrap())
 | |
|         .with_context(|| "Failed to create directory watcher")?;
 | |
| 
 | |
|     let watcher = ProjectWatcher::new(watcher, &db);
 | |
|     assert!(!watcher.has_errored_paths());
 | |
| 
 | |
|     let test_case = TestCase {
 | |
|         db,
 | |
|         changes_receiver: receiver,
 | |
|         watcher: Some(watcher),
 | |
|         _temp_dir: temp_dir,
 | |
|         root_dir: root_path,
 | |
|     };
 | |
| 
 | |
|     // Sometimes the file watcher reports changes for events that happened before the watcher was started.
 | |
|     // Do a best effort at dropping these events.
 | |
|     let _ =
 | |
|         test_case.try_take_watch_changes(|_event: &ChangeEvent| true, Duration::from_millis(100));
 | |
| 
 | |
|     Ok(test_case)
 | |
| }
 | |
| 
 | |
| /// Updates the content of a file and ensures that the last modified file time is updated.
 | |
| fn update_file(path: impl AsRef<SystemPath>, content: &str) -> anyhow::Result<()> {
 | |
|     let path = path.as_ref().as_std_path();
 | |
| 
 | |
|     let metadata = path.metadata()?;
 | |
|     let last_modified_time = filetime::FileTime::from_last_modification_time(&metadata);
 | |
| 
 | |
|     let mut file = std::fs::OpenOptions::new()
 | |
|         .create(false)
 | |
|         .write(true)
 | |
|         .truncate(true)
 | |
|         .open(path)?;
 | |
|     file.write_all(content.as_bytes())?;
 | |
| 
 | |
|     loop {
 | |
|         file.sync_all()?;
 | |
| 
 | |
|         let modified_time = filetime::FileTime::from_last_modification_time(&path.metadata()?);
 | |
| 
 | |
|         if modified_time != last_modified_time {
 | |
|             break Ok(());
 | |
|         }
 | |
| 
 | |
|         std::thread::sleep(Duration::from_nanos(10));
 | |
| 
 | |
|         filetime::set_file_handle_times(&file, None, Some(file_time_now()))?;
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn new_file() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("bar.py", "")])?;
 | |
|     let bar_path = case.project_path("bar.py");
 | |
|     let bar_file = case.system_file(&bar_path).unwrap();
 | |
|     let foo_path = case.project_path("foo.py");
 | |
| 
 | |
|     assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
 | |
|     case.assert_indexed_project_files([bar_file]);
 | |
| 
 | |
|     std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     let foo = case.system_file(&foo_path).expect("foo.py to exist.");
 | |
| 
 | |
|     case.assert_indexed_project_files([bar_file, foo]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn new_ignored_file() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("bar.py", ""), (".ignore", "foo.py")])?;
 | |
|     let bar_path = case.project_path("bar.py");
 | |
|     let bar_file = case.system_file(&bar_path).unwrap();
 | |
|     let foo_path = case.project_path("foo.py");
 | |
| 
 | |
|     assert_eq!(case.system_file(&foo_path), Err(FileError::NotFound));
 | |
|     case.assert_indexed_project_files([bar_file]);
 | |
| 
 | |
|     std::fs::write(foo_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(case.system_file(&foo_path).is_ok());
 | |
|     case.assert_indexed_project_files([bar_file]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn new_non_project_file() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         context.write_project_file("bar.py", "")?;
 | |
|         context.set_options(Options {
 | |
|             environment: Some(EnvironmentOptions {
 | |
|                 extra_paths: Some(vec![RelativePathBuf::cli(
 | |
|                     context.join_root_path("site_packages"),
 | |
|                 )]),
 | |
|                 ..EnvironmentOptions::default()
 | |
|             }),
 | |
|             ..Options::default()
 | |
|         });
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let bar_path = case.project_path("bar.py");
 | |
|     let bar_file = case.system_file(&bar_path).unwrap();
 | |
| 
 | |
|     case.assert_indexed_project_files([bar_file]);
 | |
| 
 | |
|     // Add a file to site packages
 | |
|     let black_path = case.root_path().join("site_packages/black.py");
 | |
|     std::fs::write(black_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("black.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(case.system_file(&black_path).is_ok());
 | |
| 
 | |
|     // The file should not have been added to the project files
 | |
|     case.assert_indexed_project_files([bar_file]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn new_files_with_explicit_included_paths() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         context.write_project_file("src/main.py", "")?;
 | |
|         context.write_project_file("src/sub/__init__.py", "")?;
 | |
|         context.write_project_file("src/test.py", "")?;
 | |
|         context.set_included_paths(vec![
 | |
|             context.join_project_path("src/main.py"),
 | |
|             context.join_project_path("src/sub"),
 | |
|         ]);
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let main_path = case.project_path("src/main.py");
 | |
|     let main_file = case.system_file(&main_path).unwrap();
 | |
| 
 | |
|     let sub_init_path = case.project_path("src/sub/__init__.py");
 | |
|     let sub_init = case.system_file(&sub_init_path).unwrap();
 | |
| 
 | |
|     case.assert_indexed_project_files([main_file, sub_init]);
 | |
| 
 | |
|     // Write a new file to `sub` which is an included path
 | |
|     let sub_a_path = case.project_path("src/sub/a.py");
 | |
|     std::fs::write(sub_a_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     // and write a second file in the root directory -- this should not be included
 | |
|     let test2_path = case.project_path("src/test2.py");
 | |
|     std::fs::write(test2_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("test2.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     let sub_a_file = case.system_file(&sub_a_path).expect("sub/a.py to exist");
 | |
| 
 | |
|     case.assert_indexed_project_files([main_file, sub_init, sub_a_file]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn new_file_in_included_out_of_project_directory() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         context.write_project_file("src/main.py", "")?;
 | |
|         context.write_project_file("script.py", "")?;
 | |
|         context.write_file("outside_project/a.py", "")?;
 | |
| 
 | |
|         context.set_included_paths(vec![
 | |
|             context.join_root_path("outside_project"),
 | |
|             context.join_project_path("src"),
 | |
|         ]);
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let main_path = case.project_path("src/main.py");
 | |
|     let main_file = case.system_file(&main_path).unwrap();
 | |
| 
 | |
|     let outside_a_path = case.root_path().join("outside_project/a.py");
 | |
|     let outside_a = case.system_file(&outside_a_path).unwrap();
 | |
| 
 | |
|     case.assert_indexed_project_files([outside_a, main_file]);
 | |
| 
 | |
|     // Write a new file to `src` which should be watched
 | |
|     let src_a = case.project_path("src/a.py");
 | |
|     std::fs::write(src_a.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     // and write a second file to `outside_project` which should be watched too
 | |
|     let outside_b_path = case.root_path().join("outside_project/b.py");
 | |
|     std::fs::write(outside_b_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     // and a third file in the project's root that should not be included
 | |
|     let script2_path = case.project_path("script2.py");
 | |
|     std::fs::write(script2_path.as_std_path(), "print('Hello')")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("script2.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     let src_a_file = case.system_file(&src_a).unwrap();
 | |
|     let outside_b_file = case.system_file(&outside_b_path).unwrap();
 | |
| 
 | |
|     // The file should not have been added to the project files
 | |
|     case.assert_indexed_project_files([main_file, outside_a, outside_b_file, src_a_file]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn changed_file() -> anyhow::Result<()> {
 | |
|     let foo_source = "print('Hello, world!')";
 | |
|     let mut case = setup([("foo.py", foo_source)])?;
 | |
|     let foo_path = case.project_path("foo.py");
 | |
| 
 | |
|     let foo = case.system_file(&foo_path)?;
 | |
|     assert_eq!(source_text(case.db(), foo).as_str(), foo_source);
 | |
|     case.assert_indexed_project_files([foo]);
 | |
| 
 | |
|     update_file(&foo_path, "print('Version 2')")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     assert!(!changes.is_empty());
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
 | |
|     case.assert_indexed_project_files([foo]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn deleted_file() -> anyhow::Result<()> {
 | |
|     let foo_source = "print('Hello, world!')";
 | |
|     let mut case = setup([("foo.py", foo_source)])?;
 | |
|     let foo_path = case.project_path("foo.py");
 | |
| 
 | |
|     let foo = case.system_file(&foo_path)?;
 | |
| 
 | |
|     assert!(foo.exists(case.db()));
 | |
|     case.assert_indexed_project_files([foo]);
 | |
| 
 | |
|     std::fs::remove_file(foo_path.as_std_path())?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(!foo.exists(case.db()));
 | |
|     case.assert_indexed_project_files([]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// Tests the case where a file is moved from inside a watched directory to a directory that is not watched.
 | |
| ///
 | |
| /// This matches the behavior of deleting a file in VS code.
 | |
| #[test]
 | |
| fn move_file_to_trash() -> anyhow::Result<()> {
 | |
|     let foo_source = "print('Hello, world!')";
 | |
|     let mut case = setup([("foo.py", foo_source)])?;
 | |
|     let foo_path = case.project_path("foo.py");
 | |
| 
 | |
|     let trash_path = case.root_path().join(".trash");
 | |
|     std::fs::create_dir_all(trash_path.as_std_path())?;
 | |
| 
 | |
|     let foo = case.system_file(&foo_path)?;
 | |
| 
 | |
|     assert!(foo.exists(case.db()));
 | |
|     case.assert_indexed_project_files([foo]);
 | |
| 
 | |
|     std::fs::rename(
 | |
|         foo_path.as_std_path(),
 | |
|         trash_path.join("foo.py").as_std_path(),
 | |
|     )?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(!foo.exists(case.db()));
 | |
|     case.assert_indexed_project_files([]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// Move a file from a non-project (non-watched) location into the project.
 | |
| #[test]
 | |
| fn move_file_to_project() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("bar.py", "")])?;
 | |
|     let bar_path = case.project_path("bar.py");
 | |
|     let bar = case.system_file(&bar_path).unwrap();
 | |
| 
 | |
|     let foo_path = case.root_path().join("foo.py");
 | |
|     std::fs::write(foo_path.as_std_path(), "")?;
 | |
| 
 | |
|     let foo_in_project = case.project_path("foo.py");
 | |
| 
 | |
|     assert!(case.system_file(&foo_path).is_ok());
 | |
|     case.assert_indexed_project_files([bar]);
 | |
| 
 | |
|     std::fs::rename(foo_path.as_std_path(), foo_in_project.as_std_path())?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     let foo_in_project = case.system_file(&foo_in_project)?;
 | |
| 
 | |
|     assert!(foo_in_project.exists(case.db()));
 | |
|     case.assert_indexed_project_files([bar, foo_in_project]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// Rename a project file.
 | |
| #[test]
 | |
| fn rename_file() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("foo.py", "")])?;
 | |
|     let foo_path = case.project_path("foo.py");
 | |
|     let bar_path = case.project_path("bar.py");
 | |
| 
 | |
|     let foo = case.system_file(&foo_path)?;
 | |
| 
 | |
|     case.assert_indexed_project_files([foo]);
 | |
| 
 | |
|     std::fs::rename(foo_path.as_std_path(), bar_path.as_std_path())?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("bar.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(!foo.exists(case.db()));
 | |
| 
 | |
|     let bar = case.system_file(&bar_path)?;
 | |
| 
 | |
|     assert!(bar.exists(case.db()));
 | |
|     case.assert_indexed_project_files([bar]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn directory_moved_to_project() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("bar.py", "import sub.a")])?;
 | |
|     let bar = case.system_file(case.project_path("bar.py")).unwrap();
 | |
| 
 | |
|     let sub_original_path = case.root_path().join("sub");
 | |
|     let init_original_path = sub_original_path.join("__init__.py");
 | |
|     let a_original_path = sub_original_path.join("a.py");
 | |
| 
 | |
|     std::fs::create_dir(sub_original_path.as_std_path())
 | |
|         .with_context(|| "Failed to create sub directory")?;
 | |
|     std::fs::write(init_original_path.as_std_path(), "")
 | |
|         .with_context(|| "Failed to create __init__.py")?;
 | |
|     std::fs::write(a_original_path.as_std_path(), "").with_context(|| "Failed to create a.py")?;
 | |
| 
 | |
|     let sub_a_module = resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap());
 | |
| 
 | |
|     assert_eq!(sub_a_module, None);
 | |
|     case.assert_indexed_project_files([bar]);
 | |
| 
 | |
|     let sub_new_path = case.project_path("sub");
 | |
|     std::fs::rename(sub_original_path.as_std_path(), sub_new_path.as_std_path())
 | |
|         .with_context(|| "Failed to move sub directory")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("sub"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     let init_file = case
 | |
|         .system_file(sub_new_path.join("__init__.py"))
 | |
|         .expect("__init__.py to exist");
 | |
|     let a_file = case
 | |
|         .system_file(sub_new_path.join("a.py"))
 | |
|         .expect("a.py to exist");
 | |
| 
 | |
|     // `import sub.a` should now resolve
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
 | |
| 
 | |
|     case.assert_indexed_project_files([bar, init_file, a_file]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn directory_moved_to_trash() -> anyhow::Result<()> {
 | |
|     let mut case = setup([
 | |
|         ("bar.py", "import sub.a"),
 | |
|         ("sub/__init__.py", ""),
 | |
|         ("sub/a.py", ""),
 | |
|     ])?;
 | |
|     let bar = case.system_file(case.project_path("bar.py")).unwrap();
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
 | |
| 
 | |
|     let sub_path = case.project_path("sub");
 | |
|     let init_file = case
 | |
|         .system_file(sub_path.join("__init__.py"))
 | |
|         .expect("__init__.py to exist");
 | |
|     let a_file = case
 | |
|         .system_file(sub_path.join("a.py"))
 | |
|         .expect("a.py to exist");
 | |
| 
 | |
|     case.assert_indexed_project_files([bar, init_file, a_file]);
 | |
| 
 | |
|     std::fs::create_dir(case.root_path().join(".trash").as_std_path())?;
 | |
|     let trashed_sub = case.root_path().join(".trash/sub");
 | |
|     std::fs::rename(sub_path.as_std_path(), trashed_sub.as_std_path())
 | |
|         .with_context(|| "Failed to move the sub directory to the trash")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("sub"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     // `import sub.a` should no longer resolve
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
 | |
| 
 | |
|     assert!(!init_file.exists(case.db()));
 | |
|     assert!(!a_file.exists(case.db()));
 | |
| 
 | |
|     case.assert_indexed_project_files([bar]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn directory_renamed() -> anyhow::Result<()> {
 | |
|     let mut case = setup([
 | |
|         ("bar.py", "import sub.a"),
 | |
|         ("sub/__init__.py", ""),
 | |
|         ("sub/a.py", ""),
 | |
|     ])?;
 | |
| 
 | |
|     let bar = case.system_file(case.project_path("bar.py")).unwrap();
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_none());
 | |
| 
 | |
|     let sub_path = case.project_path("sub");
 | |
|     let sub_init = case
 | |
|         .system_file(sub_path.join("__init__.py"))
 | |
|         .expect("__init__.py to exist");
 | |
|     let sub_a = case
 | |
|         .system_file(sub_path.join("a.py"))
 | |
|         .expect("a.py to exist");
 | |
| 
 | |
|     case.assert_indexed_project_files([bar, sub_init, sub_a]);
 | |
| 
 | |
|     let foo_baz = case.project_path("foo/baz");
 | |
| 
 | |
|     std::fs::create_dir(case.project_path("foo").as_std_path())?;
 | |
|     std::fs::rename(sub_path.as_std_path(), foo_baz.as_std_path())
 | |
|         .with_context(|| "Failed to move the sub directory")?;
 | |
| 
 | |
|     // Linux and windows only emit an event for the newly created root directory, but not for every new component.
 | |
|     let changes = case.stop_watch(event_for_file("sub"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     // `import sub.a` should no longer resolve
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
 | |
|     // `import foo.baz` should now resolve
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("foo.baz").unwrap()).is_some());
 | |
| 
 | |
|     // The old paths are no longer tracked
 | |
|     assert!(!sub_init.exists(case.db()));
 | |
|     assert!(!sub_a.exists(case.db()));
 | |
| 
 | |
|     let foo_baz_init = case
 | |
|         .system_file(foo_baz.join("__init__.py"))
 | |
|         .expect("__init__.py to exist");
 | |
|     let foo_baz_a = case
 | |
|         .system_file(foo_baz.join("a.py"))
 | |
|         .expect("a.py to exist");
 | |
| 
 | |
|     // The new paths are synced
 | |
| 
 | |
|     assert!(foo_baz_init.exists(case.db()));
 | |
|     assert!(foo_baz_a.exists(case.db()));
 | |
| 
 | |
|     case.assert_indexed_project_files([bar, foo_baz_init, foo_baz_a]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn directory_deleted() -> anyhow::Result<()> {
 | |
|     let mut case = setup([
 | |
|         ("bar.py", "import sub.a"),
 | |
|         ("sub/__init__.py", ""),
 | |
|         ("sub/a.py", ""),
 | |
|     ])?;
 | |
| 
 | |
|     let bar = case.system_file(case.project_path("bar.py")).unwrap();
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_some());
 | |
| 
 | |
|     let sub_path = case.project_path("sub");
 | |
| 
 | |
|     let init_file = case
 | |
|         .system_file(sub_path.join("__init__.py"))
 | |
|         .expect("__init__.py to exist");
 | |
|     let a_file = case
 | |
|         .system_file(sub_path.join("a.py"))
 | |
|         .expect("a.py to exist");
 | |
|     case.assert_indexed_project_files([bar, init_file, a_file]);
 | |
| 
 | |
|     std::fs::remove_dir_all(sub_path.as_std_path())
 | |
|         .with_context(|| "Failed to remove the sub directory")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("sub"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     // `import sub.a` should no longer resolve
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("sub.a").unwrap()).is_none());
 | |
| 
 | |
|     assert!(!init_file.exists(case.db()));
 | |
|     assert!(!a_file.exists(case.db()));
 | |
|     case.assert_indexed_project_files([bar]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn search_path() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         context.write_project_file("bar.py", "import sub.a")?;
 | |
| 
 | |
|         context.set_options(Options {
 | |
|             environment: Some(EnvironmentOptions {
 | |
|                 extra_paths: Some(vec![RelativePathBuf::cli(
 | |
|                     context.join_root_path("site_packages"),
 | |
|                 )]),
 | |
|                 ..EnvironmentOptions::default()
 | |
|             }),
 | |
|             ..Options::default()
 | |
|         });
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let site_packages = case.root_path().join("site_packages");
 | |
| 
 | |
|     assert_eq!(
 | |
|         resolve_module(case.db(), &ModuleName::new("a").unwrap()),
 | |
|         None
 | |
|     );
 | |
| 
 | |
|     std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("a.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
 | |
|     case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn add_search_path() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("bar.py", "import sub.a")])?;
 | |
| 
 | |
|     let site_packages = case.project_path("site_packages");
 | |
|     std::fs::create_dir_all(site_packages.as_std_path())?;
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_none());
 | |
| 
 | |
|     // Register site-packages as a search path.
 | |
|     case.update_options(Options {
 | |
|         environment: Some(EnvironmentOptions {
 | |
|             extra_paths: Some(vec![RelativePathBuf::cli("site_packages")]),
 | |
|             ..EnvironmentOptions::default()
 | |
|         }),
 | |
|         ..Options::default()
 | |
|     })
 | |
|     .expect("Search path settings to be valid");
 | |
| 
 | |
|     std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("a.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new_static("a").unwrap()).is_some());
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn remove_search_path() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         context.write_project_file("bar.py", "import sub.a")?;
 | |
|         context.set_options(Options {
 | |
|             environment: Some(EnvironmentOptions {
 | |
|                 extra_paths: Some(vec![RelativePathBuf::cli(
 | |
|                     context.join_root_path("site_packages"),
 | |
|                 )]),
 | |
|                 ..EnvironmentOptions::default()
 | |
|             }),
 | |
|             ..Options::default()
 | |
|         });
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     // Remove site packages from the search path settings.
 | |
|     let site_packages = case.root_path().join("site_packages");
 | |
| 
 | |
|     case.update_options(Options {
 | |
|         environment: None,
 | |
|         ..Options::default()
 | |
|     })
 | |
|     .expect("Search path settings to be valid");
 | |
| 
 | |
|     std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
 | |
| 
 | |
|     let changes = case.try_stop_watch(|_: &ChangeEvent| true, Duration::from_millis(100));
 | |
| 
 | |
|     assert_eq!(changes, Err(vec![]));
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn change_python_version_and_platform() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         // `sys.last_exc` is a Python 3.12 only feature
 | |
|         // `os.getegid()` is Unix only
 | |
|         context.write_project_file(
 | |
|             "bar.py",
 | |
|             r#"
 | |
| import sys
 | |
| import os
 | |
| print(sys.last_exc, os.getegid())
 | |
| "#,
 | |
|         )?;
 | |
|         context.set_options(Options {
 | |
|             environment: Some(EnvironmentOptions {
 | |
|                 python_version: Some(RangedValue::cli(PythonVersion::PY311)),
 | |
|                 python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
 | |
|                     "win32".to_string(),
 | |
|                 ))),
 | |
|                 ..EnvironmentOptions::default()
 | |
|             }),
 | |
|             ..Options::default()
 | |
|         });
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let diagnostics = case.db.check();
 | |
| 
 | |
|     assert_eq!(diagnostics.len(), 2);
 | |
|     assert_eq!(
 | |
|         diagnostics[0].primary_message(),
 | |
|         "Type `<module 'sys'>` has no attribute `last_exc`"
 | |
|     );
 | |
|     assert_eq!(
 | |
|         diagnostics[1].primary_message(),
 | |
|         "Type `<module 'os'>` has no attribute `getegid`"
 | |
|     );
 | |
| 
 | |
|     // Change the python version
 | |
|     case.update_options(Options {
 | |
|         environment: Some(EnvironmentOptions {
 | |
|             python_version: Some(RangedValue::cli(PythonVersion::PY312)),
 | |
|             python_platform: Some(RangedValue::cli(PythonPlatform::Identifier(
 | |
|                 "linux".to_string(),
 | |
|             ))),
 | |
|             ..EnvironmentOptions::default()
 | |
|         }),
 | |
|         ..Options::default()
 | |
|     })
 | |
|     .expect("Search path settings to be valid");
 | |
| 
 | |
|     let diagnostics = case.db.check();
 | |
|     assert!(diagnostics.is_empty());
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn changed_versions_file() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         std::fs::write(
 | |
|             context.join_project_path("bar.py").as_std_path(),
 | |
|             "import sub.a",
 | |
|         )?;
 | |
|         std::fs::create_dir_all(context.join_root_path("typeshed/stdlib").as_std_path())?;
 | |
|         std::fs::write(
 | |
|             context
 | |
|                 .join_root_path("typeshed/stdlib/VERSIONS")
 | |
|                 .as_std_path(),
 | |
|             "",
 | |
|         )?;
 | |
|         std::fs::write(
 | |
|             context
 | |
|                 .join_root_path("typeshed/stdlib/os.pyi")
 | |
|                 .as_std_path(),
 | |
|             "# not important",
 | |
|         )?;
 | |
| 
 | |
|         context.set_options(Options {
 | |
|             environment: Some(EnvironmentOptions {
 | |
|                 typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
 | |
|                 ..EnvironmentOptions::default()
 | |
|             }),
 | |
|             ..Options::default()
 | |
|         });
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     // Unset the custom typeshed directory.
 | |
|     assert_eq!(
 | |
|         resolve_module(case.db(), &ModuleName::new("os").unwrap()),
 | |
|         None
 | |
|     );
 | |
| 
 | |
|     std::fs::write(
 | |
|         case.root_path()
 | |
|             .join("typeshed/stdlib/VERSIONS")
 | |
|             .as_std_path(),
 | |
|         "os: 3.0-",
 | |
|     )?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("VERSIONS"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// Watch a project that contains two files where one file is a hardlink to another.
 | |
| ///
 | |
| /// Setup:
 | |
| /// ```text
 | |
| /// - project
 | |
| ///   |- foo.py
 | |
| ///   |- bar.py (hard link to foo.py)
 | |
| /// ```
 | |
| ///
 | |
| /// # Linux
 | |
| /// `inotify` only emits a single change event for the file that was changed.
 | |
| /// Other files that point to the same inode (hardlinks) won't get updated.
 | |
| ///
 | |
| /// For reference: VS Code and PyCharm have the same behavior where the results for one of the
 | |
| /// files are stale.
 | |
| ///
 | |
| /// # Windows
 | |
| /// I haven't found any documentation that states the notification behavior on Windows but what
 | |
| /// we're seeing is that Windows only emits a single event, similar to Linux.
 | |
| #[test]
 | |
| fn hard_links_in_project() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         let foo_path = context.join_project_path("foo.py");
 | |
|         std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
 | |
| 
 | |
|         // Create a hardlink to `foo`
 | |
|         let bar_path = context.join_project_path("bar.py");
 | |
|         std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
 | |
|             .context("Failed to create hard link from foo.py -> bar.py")?;
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let foo_path = case.project_path("foo.py");
 | |
|     let foo = case.system_file(&foo_path).unwrap();
 | |
|     let bar_path = case.project_path("bar.py");
 | |
|     let bar = case.system_file(&bar_path).unwrap();
 | |
| 
 | |
|     assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
 | |
|     assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
 | |
|     case.assert_indexed_project_files([bar, foo]);
 | |
| 
 | |
|     // Write to the hard link target.
 | |
|     update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
 | |
| 
 | |
|     // macOS is the only platform that emits events for every hardlink.
 | |
|     if cfg!(target_os = "macos") {
 | |
|         assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
 | |
|     }
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// Watch a project that contains one file that is a hardlink to a file outside the project.
 | |
| ///
 | |
| /// Setup:
 | |
| /// ```text
 | |
| /// - foo.py
 | |
| /// - project
 | |
| ///   |- bar.py (hard link to /foo.py)
 | |
| /// ```
 | |
| ///
 | |
| /// # Linux
 | |
| /// inotiyf doesn't support observing changes to hard linked files.
 | |
| ///
 | |
| /// > Note: when monitoring a directory, events are not generated for
 | |
| /// > the files inside the directory when the events are performed via
 | |
| /// > a pathname (i.e., a link) that lies outside the monitored
 | |
| /// > directory. [source](https://man7.org/linux/man-pages/man7/inotify.7.html)
 | |
| ///
 | |
| /// # Windows
 | |
| /// > Retrieves information that describes the changes within the specified directory.
 | |
| ///
 | |
| /// [source](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw)
 | |
| ///
 | |
| /// My interpretation of this is that Windows doesn't support observing changes made to
 | |
| /// hard linked files outside the project.
 | |
| #[test]
 | |
| #[cfg_attr(
 | |
|     target_os = "linux",
 | |
|     ignore = "inotify doesn't support observing changes to hard linked files."
 | |
| )]
 | |
| #[cfg_attr(
 | |
|     target_os = "windows",
 | |
|     ignore = "windows doesn't support observing changes to hard linked files."
 | |
| )]
 | |
| fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         let foo_path = context.join_root_path("foo.py");
 | |
|         std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
 | |
| 
 | |
|         // Create a hardlink to `foo`
 | |
|         let bar_path = context.join_project_path("bar.py");
 | |
|         std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
 | |
|             .context("Failed to create hard link from foo.py -> bar.py")?;
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let foo_path = case.root_path().join("foo.py");
 | |
|     let foo = case.system_file(&foo_path).unwrap();
 | |
|     let bar_path = case.project_path("bar.py");
 | |
|     let bar = case.system_file(&bar_path).unwrap();
 | |
| 
 | |
|     assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 1')");
 | |
|     assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 1')");
 | |
| 
 | |
|     // Write to the hard link target.
 | |
|     update_file(foo_path, "print('Version 2')").context("Failed to update foo.py")?;
 | |
| 
 | |
|     let changes = case.stop_watch(ChangeEvent::is_changed);
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[cfg(unix)]
 | |
| mod unix {
 | |
|     //! Tests that make use of unix specific file-system features.
 | |
|     use super::*;
 | |
| 
 | |
|     /// Changes the metadata of the only file in the project.
 | |
|     #[test]
 | |
|     fn changed_metadata() -> anyhow::Result<()> {
 | |
|         use std::os::unix::fs::PermissionsExt;
 | |
| 
 | |
|         let mut case = setup([("foo.py", "")])?;
 | |
|         let foo_path = case.project_path("foo.py");
 | |
| 
 | |
|         let foo = case.system_file(&foo_path)?;
 | |
|         assert_eq!(
 | |
|             foo.permissions(case.db()),
 | |
|             Some(
 | |
|                 std::fs::metadata(foo_path.as_std_path())
 | |
|                     .unwrap()
 | |
|                     .permissions()
 | |
|                     .mode()
 | |
|             )
 | |
|         );
 | |
| 
 | |
|         std::fs::set_permissions(
 | |
|             foo_path.as_std_path(),
 | |
|             std::fs::Permissions::from_mode(0o777),
 | |
|         )
 | |
|         .with_context(|| "Failed to set file permissions.")?;
 | |
| 
 | |
|         let changes = case.stop_watch(event_for_file("foo.py"));
 | |
| 
 | |
|         case.apply_changes(changes, None);
 | |
| 
 | |
|         assert_eq!(
 | |
|             foo.permissions(case.db()),
 | |
|             Some(
 | |
|                 std::fs::metadata(foo_path.as_std_path())
 | |
|                     .unwrap()
 | |
|                     .permissions()
 | |
|                     .mode()
 | |
|             )
 | |
|         );
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// A project path is a symlink to a file outside the project.
 | |
|     ///
 | |
|     /// Setup:
 | |
|     /// ```text
 | |
|     /// - bar
 | |
|     ///   |- baz.py
 | |
|     ///
 | |
|     /// - project
 | |
|     ///   |- bar -> /bar
 | |
|     /// ```
 | |
|     ///
 | |
|     /// # macOS
 | |
|     /// This test case isn't supported on macOS.
 | |
|     /// macOS uses `FSEvents` and `FSEvents` doesn't emit an event if a file in a symlinked directory is changed.
 | |
|     ///
 | |
|     /// > Generally speaking, when working with file system event notifications, you will probably want to use lstat,
 | |
|     /// > because changes to the underlying file will not result in a change notification for the directory containing
 | |
|     /// > the symbolic link to that file. However, if you are working with a controlled file structure in
 | |
|     /// > which symbolic links always point within your watched tree, you might have reason to use stat.
 | |
|     ///
 | |
|     /// [source](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html#//apple_ref/doc/uid/TP40005289-CH4-SW4)
 | |
|     ///
 | |
|     /// Pyright also does not support this case.
 | |
|     #[test]
 | |
|     #[cfg_attr(
 | |
|         target_os = "macos",
 | |
|         ignore = "FSEvents doesn't emit change events for symlinked directories outside of the watched paths."
 | |
|     )]
 | |
|     fn symlink_target_outside_watched_paths() -> anyhow::Result<()> {
 | |
|         let mut case = setup(|context: &mut SetupContext| {
 | |
|             // Set up the symlink target.
 | |
|             let link_target = context.join_root_path("bar");
 | |
|             std::fs::create_dir_all(link_target.as_std_path())
 | |
|                 .context("Failed to create link target directory")?;
 | |
|             let baz_original = link_target.join("baz.py");
 | |
|             std::fs::write(baz_original.as_std_path(), "def baz(): ...")
 | |
|                 .context("Failed to write link target file")?;
 | |
| 
 | |
|             // Create a symlink inside the project
 | |
|             let bar = context.join_project_path("bar");
 | |
|             std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
 | |
|                 .context("Failed to create symlink to bar package")?;
 | |
| 
 | |
|             Ok(())
 | |
|         })?;
 | |
| 
 | |
|         let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
 | |
|             .expect("Expected bar.baz to exist in site-packages.");
 | |
|         let baz_project = case.project_path("bar/baz.py");
 | |
|         let baz_file = baz.file().unwrap();
 | |
| 
 | |
|         assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
 | |
|         assert_eq!(
 | |
|             baz_file.path(case.db()).as_system_path(),
 | |
|             Some(&*baz_project)
 | |
|         );
 | |
| 
 | |
|         let baz_original = case.root_path().join("bar/baz.py");
 | |
| 
 | |
|         // Write to the symlink target.
 | |
|         update_file(baz_original, "def baz(): print('Version 2')")
 | |
|             .context("Failed to update bar/baz.py")?;
 | |
| 
 | |
|         let changes = case.take_watch_changes(event_for_file("baz.py"));
 | |
| 
 | |
|         case.apply_changes(changes, None);
 | |
| 
 | |
|         assert_eq!(
 | |
|             source_text(case.db(), baz_file).as_str(),
 | |
|             "def baz(): print('Version 2')"
 | |
|         );
 | |
| 
 | |
|         // Write to the symlink source.
 | |
|         update_file(baz_project, "def baz(): print('Version 3')")
 | |
|             .context("Failed to update bar/baz.py")?;
 | |
| 
 | |
|         let changes = case.stop_watch(event_for_file("baz.py"));
 | |
| 
 | |
|         case.apply_changes(changes, None);
 | |
| 
 | |
|         assert_eq!(
 | |
|             source_text(case.db(), baz_file).as_str(),
 | |
|             "def baz(): print('Version 3')"
 | |
|         );
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// Project contains a symlink to another directory inside the project.
 | |
|     /// Changes to files in the symlinked directory should be reflected
 | |
|     /// to all files.
 | |
|     ///
 | |
|     /// Setup:
 | |
|     /// ```text
 | |
|     /// - project
 | |
|     ///   | - bar -> /project/patched/bar
 | |
|     ///   |
 | |
|     ///   | - patched
 | |
|     ///   |   |-- bar
 | |
|     ///   |   |  |- baz.py
 | |
|     ///   |
 | |
|     ///   |-- foo.py
 | |
|     /// ```
 | |
|     #[test]
 | |
|     fn symlink_inside_project() -> anyhow::Result<()> {
 | |
|         let mut case = setup(|context: &mut SetupContext| {
 | |
|             // Set up the symlink target.
 | |
|             let link_target = context.join_project_path("patched/bar");
 | |
|             std::fs::create_dir_all(link_target.as_std_path())
 | |
|                 .context("Failed to create link target directory")?;
 | |
|             let baz_original = link_target.join("baz.py");
 | |
|             std::fs::write(baz_original.as_std_path(), "def baz(): ...")
 | |
|                 .context("Failed to write link target file")?;
 | |
| 
 | |
|             // Create a symlink inside site-packages
 | |
|             let bar_in_project = context.join_project_path("bar");
 | |
|             std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_project.as_std_path())
 | |
|                 .context("Failed to create symlink to bar package")?;
 | |
| 
 | |
|             Ok(())
 | |
|         })?;
 | |
| 
 | |
|         let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
 | |
|             .expect("Expected bar.baz to exist in site-packages.");
 | |
|         let baz_file = baz.file().unwrap();
 | |
|         let bar_baz = case.project_path("bar/baz.py");
 | |
| 
 | |
|         let patched_bar_baz = case.project_path("patched/bar/baz.py");
 | |
|         let patched_bar_baz_file = case.system_file(&patched_bar_baz).unwrap();
 | |
| 
 | |
|         assert_eq!(
 | |
|             source_text(case.db(), patched_bar_baz_file).as_str(),
 | |
|             "def baz(): ..."
 | |
|         );
 | |
| 
 | |
|         assert_eq!(source_text(case.db(), baz_file).as_str(), "def baz(): ...");
 | |
|         assert_eq!(baz_file.path(case.db()).as_system_path(), Some(&*bar_baz));
 | |
| 
 | |
|         case.assert_indexed_project_files([patched_bar_baz_file]);
 | |
| 
 | |
|         // Write to the symlink target.
 | |
|         update_file(&patched_bar_baz, "def baz(): print('Version 2')")
 | |
|             .context("Failed to update bar/baz.py")?;
 | |
| 
 | |
|         let changes = case.stop_watch(event_for_file("baz.py"));
 | |
| 
 | |
|         case.apply_changes(changes, None);
 | |
| 
 | |
|         // The file watcher is guaranteed to emit one event for the changed file, but it isn't specified
 | |
|         // if the event is emitted for the "original" or linked path because both paths are watched.
 | |
|         // The best we can assert here is that one of the files should have been updated.
 | |
|         //
 | |
|         // In a perfect world, the file watcher would emit two events, one for the original file and
 | |
|         // one for the symlink. I tried parcel/watcher, node's `fs.watch` and `chokidar` and
 | |
|         // only `chokidar seems to support it (used by Pyright).
 | |
|         //
 | |
|         // I further tested how good editor support is for symlinked files and it is not good ;)
 | |
|         // * VS Code doesn't update the file content if a file gets changed through a symlink
 | |
|         // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as ty).
 | |
|         //
 | |
|         // That's why I think it's fine to not support this case for now.
 | |
| 
 | |
|         let patched_baz_text = source_text(case.db(), patched_bar_baz_file);
 | |
|         let did_update_patched_baz = patched_baz_text.as_str() == "def baz(): print('Version 2')";
 | |
| 
 | |
|         let bar_baz_text = source_text(case.db(), baz_file);
 | |
|         let did_update_bar_baz = bar_baz_text.as_str() == "def baz(): print('Version 2')";
 | |
| 
 | |
|         assert!(
 | |
|             did_update_patched_baz || did_update_bar_baz,
 | |
|             "Expected one of the files to be updated but neither file was updated.\nOriginal: {patched_baz_text}\nSymlinked: {bar_baz_text}",
 | |
|             patched_baz_text = patched_baz_text.as_str(),
 | |
|             bar_baz_text = bar_baz_text.as_str()
 | |
|         );
 | |
| 
 | |
|         case.assert_indexed_project_files([patched_bar_baz_file]);
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     /// A module search path is a symlink.
 | |
|     ///
 | |
|     /// Setup:
 | |
|     /// ```text
 | |
|     /// - site-packages
 | |
|     ///   | - bar/baz.py
 | |
|     ///
 | |
|     /// - project
 | |
|     ///   |-- .venv/lib/python3.12/site-packages -> /site-packages
 | |
|     ///   |
 | |
|     ///   |-- foo.py
 | |
|     /// ```
 | |
|     #[test]
 | |
|     fn symlinked_module_search_path() -> anyhow::Result<()> {
 | |
|         let mut case = setup(|context: &mut SetupContext| {
 | |
|             // Set up the symlink target.
 | |
|             let site_packages = context.join_root_path("site-packages");
 | |
|             let bar = site_packages.join("bar");
 | |
|             std::fs::create_dir_all(bar.as_std_path()).context("Failed to create bar directory")?;
 | |
|             let baz_original = bar.join("baz.py");
 | |
|             std::fs::write(baz_original.as_std_path(), "def baz(): ...")
 | |
|                 .context("Failed to write baz.py")?;
 | |
| 
 | |
|             // Symlink the site packages in the venv to the global site packages
 | |
|             let venv_site_packages =
 | |
|                 context.join_project_path(".venv/lib/python3.12/site-packages");
 | |
|             std::fs::create_dir_all(venv_site_packages.parent().unwrap())
 | |
|                 .context("Failed to create .venv directory")?;
 | |
|             std::os::unix::fs::symlink(
 | |
|                 site_packages.as_std_path(),
 | |
|                 venv_site_packages.as_std_path(),
 | |
|             )
 | |
|             .context("Failed to create symlink to site-packages")?;
 | |
| 
 | |
|             context.set_options(Options {
 | |
|                 environment: Some(EnvironmentOptions {
 | |
|                     extra_paths: Some(vec![RelativePathBuf::cli(
 | |
|                         ".venv/lib/python3.12/site-packages",
 | |
|                     )]),
 | |
|                     python_version: Some(RangedValue::cli(PythonVersion::PY312)),
 | |
|                     ..EnvironmentOptions::default()
 | |
|                 }),
 | |
|                 ..Options::default()
 | |
|             });
 | |
| 
 | |
|             Ok(())
 | |
|         })?;
 | |
| 
 | |
|         let baz = resolve_module(case.db(), &ModuleName::new_static("bar.baz").unwrap())
 | |
|             .expect("Expected bar.baz to exist in site-packages.");
 | |
|         let baz_site_packages_path =
 | |
|             case.project_path(".venv/lib/python3.12/site-packages/bar/baz.py");
 | |
|         let baz_site_packages = case.system_file(&baz_site_packages_path).unwrap();
 | |
|         let baz_original = case.root_path().join("site-packages/bar/baz.py");
 | |
|         let baz_original_file = case.system_file(&baz_original).unwrap();
 | |
| 
 | |
|         assert_eq!(
 | |
|             source_text(case.db(), baz_original_file).as_str(),
 | |
|             "def baz(): ..."
 | |
|         );
 | |
| 
 | |
|         assert_eq!(
 | |
|             source_text(case.db(), baz_site_packages).as_str(),
 | |
|             "def baz(): ..."
 | |
|         );
 | |
|         assert_eq!(
 | |
|             baz.file().unwrap().path(case.db()).as_system_path(),
 | |
|             Some(&*baz_original)
 | |
|         );
 | |
| 
 | |
|         case.assert_indexed_project_files([]);
 | |
| 
 | |
|         // Write to the symlink target.
 | |
|         update_file(&baz_original, "def baz(): print('Version 2')")
 | |
|             .context("Failed to update bar/baz.py")?;
 | |
| 
 | |
|         let changes = case.stop_watch(event_for_file("baz.py"));
 | |
| 
 | |
|         case.apply_changes(changes, None);
 | |
| 
 | |
|         assert_eq!(
 | |
|             source_text(case.db(), baz_original_file).as_str(),
 | |
|             "def baz(): print('Version 2')"
 | |
|         );
 | |
| 
 | |
|         // It would be nice if this is supported but the underlying file system watchers
 | |
|         // only emit a single event. For reference
 | |
|         // * VS Code doesn't update the file content if a file gets changed through a symlink
 | |
|         // * PyCharm doesn't update diagnostics if a symlinked module is changed (same as ty).
 | |
|         // We could add support for it by keeping a reverse map from `real_path` to symlinked path but
 | |
|         // it doesn't seem worth doing considering that as prominent tools like PyCharm don't support it.
 | |
|         // Pyright does support it, thanks to chokidar.
 | |
|         assert_ne!(
 | |
|             source_text(case.db(), baz_site_packages).as_str(),
 | |
|             "def baz(): print('Version 2')"
 | |
|         );
 | |
| 
 | |
|         case.assert_indexed_project_files([]);
 | |
| 
 | |
|         Ok(())
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn nested_projects_delete_root() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         std::fs::write(
 | |
|             context.join_project_path("pyproject.toml").as_std_path(),
 | |
|             r#"
 | |
|             [project]
 | |
|             name = "inner"
 | |
| 
 | |
|             [tool.ty]
 | |
|             "#,
 | |
|         )?;
 | |
| 
 | |
|         std::fs::write(
 | |
|             context.join_root_path("pyproject.toml").as_std_path(),
 | |
|             r#"
 | |
|             [project]
 | |
|             name = "outer"
 | |
| 
 | |
|             [tool.ty]
 | |
|             "#,
 | |
|         )?;
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     assert_eq!(case.db().project().root(case.db()), &*case.project_path(""));
 | |
| 
 | |
|     std::fs::remove_file(case.project_path("pyproject.toml").as_std_path())?;
 | |
| 
 | |
|     let changes = case.stop_watch(ChangeEvent::is_deleted);
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     // It should now pick up the outer project.
 | |
|     assert_eq!(case.db().project().root(case.db()), case.root_path());
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn changes_to_user_configuration() -> anyhow::Result<()> {
 | |
|     let mut _config_dir_override: Option<UserConfigDirectoryOverrideGuard> = None;
 | |
| 
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         std::fs::write(
 | |
|             context.join_project_path("pyproject.toml").as_std_path(),
 | |
|             r#"
 | |
|             [project]
 | |
|             name = "test"
 | |
|             "#,
 | |
|         )?;
 | |
| 
 | |
|         std::fs::write(
 | |
|             context.join_project_path("foo.py").as_std_path(),
 | |
|             "a = 10 / 0",
 | |
|         )?;
 | |
| 
 | |
|         let config_directory = context.join_root_path("home/.config");
 | |
|         std::fs::create_dir_all(config_directory.join("ty").as_std_path())?;
 | |
|         std::fs::write(
 | |
|             config_directory.join("ty/ty.toml").as_std_path(),
 | |
|             r#"
 | |
|             [rules]
 | |
|             division-by-zero = "ignore"
 | |
|             "#,
 | |
|         )?;
 | |
| 
 | |
|         _config_dir_override = Some(
 | |
|             context
 | |
|                 .system()
 | |
|                 .with_user_config_directory(Some(config_directory)),
 | |
|         );
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let foo = case
 | |
|         .system_file(case.project_path("foo.py"))
 | |
|         .expect("foo.py to exist");
 | |
|     let diagnostics = case.db().check_file(foo);
 | |
| 
 | |
|     assert!(
 | |
|         diagnostics.is_empty(),
 | |
|         "Expected no diagnostics but got: {diagnostics:#?}"
 | |
|     );
 | |
| 
 | |
|     // Enable division-by-zero in the user configuration with warning severity
 | |
|     update_file(
 | |
|         case.root_path().join("home/.config/ty/ty.toml"),
 | |
|         r#"
 | |
|         [rules]
 | |
|         division-by-zero = "warn"
 | |
|         "#,
 | |
|     )?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("ty.toml"));
 | |
| 
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     let diagnostics = case.db().check_file(foo);
 | |
| 
 | |
|     assert!(
 | |
|         diagnostics.len() == 1,
 | |
|         "Expected exactly one diagnostic but got: {diagnostics:#?}"
 | |
|     );
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn changes_to_config_file_override() -> anyhow::Result<()> {
 | |
|     let mut case = setup(|context: &mut SetupContext| {
 | |
|         std::fs::write(
 | |
|             context.join_project_path("pyproject.toml").as_std_path(),
 | |
|             r#"
 | |
|             [project]
 | |
|             name = "test"
 | |
|             "#,
 | |
|         )?;
 | |
| 
 | |
|         std::fs::write(
 | |
|             context.join_project_path("foo.py").as_std_path(),
 | |
|             "a = 10 / 0",
 | |
|         )?;
 | |
| 
 | |
|         std::fs::write(
 | |
|             context.join_project_path("ty-override.toml").as_std_path(),
 | |
|             r#"
 | |
|             [rules]
 | |
|             division-by-zero = "ignore"
 | |
|             "#,
 | |
|         )?;
 | |
| 
 | |
|         Ok(())
 | |
|     })?;
 | |
| 
 | |
|     let foo = case
 | |
|         .system_file(case.project_path("foo.py"))
 | |
|         .expect("foo.py to exist");
 | |
|     let diagnostics = case.db().check_file(foo);
 | |
| 
 | |
|     assert!(
 | |
|         diagnostics.is_empty(),
 | |
|         "Expected no diagnostics but got: {diagnostics:#?}"
 | |
|     );
 | |
| 
 | |
|     // Enable division-by-zero in the explicitly specified configuration with warning severity
 | |
|     update_file(
 | |
|         case.project_path("ty-override.toml"),
 | |
|         r#"
 | |
|         [rules]
 | |
|         division-by-zero = "warn"
 | |
|         "#,
 | |
|     )?;
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("ty-override.toml"));
 | |
| 
 | |
|     case.apply_changes(
 | |
|         changes,
 | |
|         Some(&ProjectOptionsOverrides::new(
 | |
|             Some(case.project_path("ty-override.toml")),
 | |
|             Options::default(),
 | |
|         )),
 | |
|     );
 | |
| 
 | |
|     let diagnostics = case.db().check_file(foo);
 | |
| 
 | |
|     assert!(
 | |
|         diagnostics.len() == 1,
 | |
|         "Expected exactly one diagnostic but got: {diagnostics:#?}"
 | |
|     );
 | |
| 
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// Tests that renaming a file from `lib.py` to `Lib.py` is correctly reflected.
 | |
| ///
 | |
| /// This test currently fails on case-insensitive systems because `Files` is case-sensitive
 | |
| /// but the `System::metadata` call isn't. This means that
 | |
| /// ty considers both `Lib.py` and `lib.py` to exist when only `lib.py` does
 | |
| ///
 | |
| /// The incoming change events then are no-ops because they don't change either file's
 | |
| /// status nor does it update their last modified time (renaming a file doesn't bump it's
 | |
| /// last modified timestamp).
 | |
| ///
 | |
| /// Fixing this requires to either make `Files` case-insensitive and store the
 | |
| /// real-case path (if it differs) on `File` or make `Files` use a
 | |
| /// case-sensitive `System::metadata` call. This does open the question if all
 | |
| /// `System` calls should be case sensitive. This would be the most consistent
 | |
| /// but might be hard to pull off.
 | |
| ///
 | |
| /// What the right solution is also depends on if ty itself should be case
 | |
| /// sensitive or not. E.g. should `include="src"` be case sensitive on all systems
 | |
| /// or only on case-sensitive systems?
 | |
| ///
 | |
| /// Lastly, whatever solution we pick must also work well with VS Code which,
 | |
| /// unfortunately ,doesn't propagate casing-only renames.
 | |
| /// <https://github.com/rust-lang/rust-analyzer/issues/9581>
 | |
| #[ignore]
 | |
| #[test]
 | |
| fn rename_files_casing_only() -> anyhow::Result<()> {
 | |
|     let mut case = setup([("lib.py", "class Foo: ...")])?;
 | |
| 
 | |
|     assert!(
 | |
|         resolve_module(case.db(), &ModuleName::new("lib").unwrap()).is_some(),
 | |
|         "Expected `lib` module to exist."
 | |
|     );
 | |
|     assert_eq!(
 | |
|         resolve_module(case.db(), &ModuleName::new("Lib").unwrap()),
 | |
|         None,
 | |
|         "Expected `Lib` module not to exist"
 | |
|     );
 | |
| 
 | |
|     // Now rename `lib.py` to `Lib.py`
 | |
|     if case.db().system().case_sensitivity().is_case_sensitive() {
 | |
|         std::fs::rename(
 | |
|             case.project_path("lib.py").as_std_path(),
 | |
|             case.project_path("Lib.py").as_std_path(),
 | |
|         )
 | |
|         .context("Failed to rename `lib.py` to `Lib.py`")?;
 | |
|     } else {
 | |
|         // On case-insensitive file systems, renaming a file to a different casing is a no-op.
 | |
|         // Rename to a different name first
 | |
|         std::fs::rename(
 | |
|             case.project_path("lib.py").as_std_path(),
 | |
|             case.project_path("temp.py").as_std_path(),
 | |
|         )
 | |
|         .context("Failed to rename `lib.py` to `temp.py`")?;
 | |
| 
 | |
|         std::fs::rename(
 | |
|             case.project_path("temp.py").as_std_path(),
 | |
|             case.project_path("Lib.py").as_std_path(),
 | |
|         )
 | |
|         .context("Failed to rename `temp.py` to `Lib.py`")?;
 | |
|     }
 | |
| 
 | |
|     let changes = case.stop_watch(event_for_file("Lib.py"));
 | |
|     case.apply_changes(changes, None);
 | |
| 
 | |
|     // Resolving `lib` should now fail but `Lib` should now succeed
 | |
|     assert_eq!(
 | |
|         resolve_module(case.db(), &ModuleName::new("lib").unwrap()),
 | |
|         None,
 | |
|         "Expected `lib` module to no longer exist."
 | |
|     );
 | |
| 
 | |
|     assert!(
 | |
|         resolve_module(case.db(), &ModuleName::new("Lib").unwrap()).is_some(),
 | |
|         "Expected `Lib` module to exist"
 | |
|     );
 | |
| 
 | |
|     Ok(())
 | |
| }
 | 
