Add OsSystem support to mdtests (#16518)

## Summary

This PR introduces a new mdtest option `system` that can either be
`in-memory` or `os`
where `in-memory` is the default.

The motivation for supporting `os` is so that we can write OS/system
specific tests
with mdtests. Specifically, I want to write mdtests for the module
resolver,
testing that module resolution is case sensitive. 

## Test Plan

I tested that the case-sensitive module resolver test start failing when
setting `system = "os"`
This commit is contained in:
Micha Reiser 2025-03-06 09:41:40 +00:00 committed by GitHub
parent 48f906e06c
commit ce0018c3cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 541 additions and 229 deletions

2
Cargo.lock generated
View file

@ -2541,6 +2541,7 @@ dependencies = [
"regex", "regex",
"ruff_db", "ruff_db",
"ruff_index", "ruff_index",
"ruff_notebook",
"ruff_python_ast", "ruff_python_ast",
"ruff_python_trivia", "ruff_python_trivia",
"ruff_source_file", "ruff_source_file",
@ -2549,6 +2550,7 @@ dependencies = [
"salsa", "salsa",
"serde", "serde",
"smallvec", "smallvec",
"tempfile",
"thiserror 2.0.11", "thiserror 2.0.11",
"toml", "toml",
] ]

View file

@ -255,7 +255,7 @@ mod tests {
use crate::files::Index; use crate::files::Index;
use crate::ProjectMetadata; use crate::ProjectMetadata;
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::{DbWithWritableSystem as _, SystemPathBuf};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
#[test] #[test]

View file

@ -528,7 +528,7 @@ mod tests {
use ruff_db::diagnostic::OldDiagnosticTrait; use ruff_db::diagnostic::OldDiagnosticTrait;
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::source::source_text; use ruff_db::source::source_text;
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf}; use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::assert_function_query_was_not_run; use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;

View file

@ -321,7 +321,7 @@ mod tests {
system system
.memory_file_system() .memory_file_system()
.write_files([(root.join("foo.py"), ""), (root.join("bar.py"), "")]) .write_files_all([(root.join("foo.py"), ""), (root.join("bar.py"), "")])
.context("Failed to write files")?; .context("Failed to write files")?;
let project = let project =
@ -349,7 +349,7 @@ mod tests {
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -393,7 +393,7 @@ mod tests {
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -432,7 +432,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -482,7 +482,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -532,7 +532,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -572,7 +572,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -623,7 +623,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_files([ .write_files_all([
( (
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
@ -673,7 +673,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -703,7 +703,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -735,7 +735,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -765,7 +765,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -795,7 +795,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -828,7 +828,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -861,7 +861,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -886,7 +886,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]
@ -911,7 +911,7 @@ expected `.`, `]`
system system
.memory_file_system() .memory_file_system()
.write_file( .write_file_all(
root.join("pyproject.toml"), root.join("pyproject.toml"),
r#" r#"
[project] [project]

View file

@ -117,7 +117,7 @@ fn run_corpus_tests(pattern: &str) -> anyhow::Result<()> {
let code = std::fs::read_to_string(source)?; let code = std::fs::read_to_string(source)?;
let mut check_with_file_name = |path: &SystemPath| { let mut check_with_file_name = |path: &SystemPath| {
memory_fs.write_file(path, &code).unwrap(); memory_fs.write_file_all(path, &code).unwrap();
File::sync_path(&mut db, path); File::sync_path(&mut db, path);
// this test is only asserting that we can pull every expression type without a panic // this test is only asserting that we can pull every expression type without a panic

View file

@ -1,6 +1,11 @@
# Case Sensitive Imports # Case Sensitive Imports
TODO: This test should use the real file system instead of the memory file system. ```toml
# TODO: This test should use the real file system instead of the memory file system.
# but we can't change the file system yet because the tests would then start failing for
# case-insensitive file systems.
#system = "os"
```
Python's import system is case-sensitive even on case-insensitive file system. This means, importing Python's import system is case-sensitive even on case-insensitive file system. This means, importing
a module `a` should fail if the file in the search paths is named `A.py`. See a module `a` should fail if the file in the search paths is named `A.py`. See

View file

@ -25,7 +25,9 @@ pub(crate) mod tests {
use crate::lint::{LintRegistry, RuleSelection}; use crate::lint::{LintRegistry, RuleSelection};
use anyhow::Context; use anyhow::Context;
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem}; use ruff_db::system::{
DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem,
};
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast}; use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;

View file

@ -720,7 +720,7 @@ impl<'db> ResolverContext<'db> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use ruff_db::files::{system_path_to_file, File, FilePath}; use ruff_db::files::{system_path_to_file, File, FilePath};
use ruff_db::system::DbWithTestSystem; use ruff_db::system::{DbWithTestSystem as _, DbWithWritableSystem as _};
use ruff_db::testing::{ use ruff_db::testing::{
assert_const_function_query_was_not_run, assert_function_query_was_not_run, assert_const_function_query_was_not_run, assert_function_query_was_not_run,
}; };

View file

@ -1,4 +1,6 @@
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf}; use ruff_db::system::{
DbWithTestSystem as _, DbWithWritableSystem as _, SystemPath, SystemPathBuf,
};
use ruff_db::vendored::VendoredPathBuf; use ruff_db::vendored::VendoredPathBuf;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;

View file

@ -409,7 +409,7 @@ impl FusedIterator for ChildrenIter<'_> {}
mod tests { mod tests {
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::system::DbWithTestSystem; use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -440,7 +440,7 @@ mod tests {
file: File, file: File,
} }
fn test_case(content: impl ToString) -> TestCase { fn test_case(content: impl AsRef<str>) -> TestCase {
let mut db = TestDb::new(); let mut db = TestDb::new();
db.write_file("test.py", content).unwrap(); db.write_file("test.py", content).unwrap();

View file

@ -545,7 +545,7 @@ mod tests {
system_install_sys_prefix.join(&unix_site_packages); system_install_sys_prefix.join(&unix_site_packages);
(system_home_path, system_exe_path, system_site_packages_path) (system_home_path, system_exe_path, system_site_packages_path)
}; };
memory_fs.write_file(system_exe_path, "").unwrap(); memory_fs.write_file_all(system_exe_path, "").unwrap();
memory_fs memory_fs
.create_directory_all(&system_site_packages_path) .create_directory_all(&system_site_packages_path)
.unwrap(); .unwrap();
@ -562,7 +562,7 @@ mod tests {
venv_sys_prefix.join(&unix_site_packages), venv_sys_prefix.join(&unix_site_packages),
) )
}; };
memory_fs.write_file(&venv_exe, "").unwrap(); memory_fs.write_file_all(&venv_exe, "").unwrap();
memory_fs.create_directory_all(&site_packages_path).unwrap(); memory_fs.create_directory_all(&site_packages_path).unwrap();
let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg"); let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg");
@ -576,7 +576,7 @@ mod tests {
pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n"); pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n");
} }
memory_fs memory_fs
.write_file(pyvenv_cfg_path, &pyvenv_cfg_contents) .write_file_all(pyvenv_cfg_path, &pyvenv_cfg_contents)
.unwrap(); .unwrap();
venv_sys_prefix venv_sys_prefix
@ -740,7 +740,7 @@ mod tests {
let system = TestSystem::default(); let system = TestSystem::default();
system system
.memory_file_system() .memory_file_system()
.write_file("/.venv", "") .write_file_all("/.venv", "")
.unwrap(); .unwrap();
assert!(matches!( assert!(matches!(
VirtualEnvironment::new("/.venv", &system), VirtualEnvironment::new("/.venv", &system),
@ -767,7 +767,7 @@ mod tests {
let memory_fs = system.memory_file_system(); let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs memory_fs
.write_file(&pyvenv_cfg_path, "home = bar = /.venv/bin") .write_file_all(&pyvenv_cfg_path, "home = bar = /.venv/bin")
.unwrap(); .unwrap();
let venv_result = VirtualEnvironment::new("/.venv", &system); let venv_result = VirtualEnvironment::new("/.venv", &system);
assert!(matches!( assert!(matches!(
@ -785,7 +785,9 @@ mod tests {
let system = TestSystem::default(); let system = TestSystem::default();
let memory_fs = system.memory_file_system(); let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs.write_file(&pyvenv_cfg_path, "home =").unwrap(); memory_fs
.write_file_all(&pyvenv_cfg_path, "home =")
.unwrap();
let venv_result = VirtualEnvironment::new("/.venv", &system); let venv_result = VirtualEnvironment::new("/.venv", &system);
assert!(matches!( assert!(matches!(
venv_result, venv_result,
@ -803,7 +805,7 @@ mod tests {
let memory_fs = system.memory_file_system(); let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs memory_fs
.write_file(&pyvenv_cfg_path, "= whatever") .write_file_all(&pyvenv_cfg_path, "= whatever")
.unwrap(); .unwrap();
let venv_result = VirtualEnvironment::new("/.venv", &system); let venv_result = VirtualEnvironment::new("/.venv", &system);
assert!(matches!( assert!(matches!(
@ -821,7 +823,7 @@ mod tests {
let system = TestSystem::default(); let system = TestSystem::default();
let memory_fs = system.memory_file_system(); let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs.write_file(&pyvenv_cfg_path, "").unwrap(); memory_fs.write_file_all(&pyvenv_cfg_path, "").unwrap();
let venv_result = VirtualEnvironment::new("/.venv", &system); let venv_result = VirtualEnvironment::new("/.venv", &system);
assert!(matches!( assert!(matches!(
venv_result, venv_result,
@ -839,7 +841,7 @@ mod tests {
let memory_fs = system.memory_file_system(); let memory_fs = system.memory_file_system();
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg"); let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
memory_fs memory_fs
.write_file(&pyvenv_cfg_path, "home = foo") .write_file_all(&pyvenv_cfg_path, "home = foo")
.unwrap(); .unwrap();
let venv_result = VirtualEnvironment::new("/.venv", &system); let venv_result = VirtualEnvironment::new("/.venv", &system);
assert!(matches!( assert!(matches!(

View file

@ -4350,7 +4350,7 @@ pub(crate) mod tests {
}; };
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::system::DbWithTestSystem; use ruff_db::system::DbWithWritableSystem as _;
use ruff_db::testing::assert_function_query_was_not_run; use ruff_db::testing::assert_function_query_was_not_run;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;

View file

@ -6551,7 +6551,7 @@ mod tests {
use crate::symbol::global_symbol; use crate::symbol::global_symbol;
use crate::types::check_types; use crate::types::check_types;
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::DbWithTestSystem; use ruff_db::system::DbWithWritableSystem as _;
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
use super::*; use super::*;

View file

@ -348,7 +348,7 @@ mod tests {
use crate::db::tests::{setup_db, TestDb}; use crate::db::tests::{setup_db, TestDb};
use crate::symbol::global_symbol; use crate::symbol::global_symbol;
use crate::types::{FunctionType, KnownClass}; use crate::types::{FunctionType, KnownClass};
use ruff_db::system::DbWithTestSystem; use ruff_db::system::DbWithWritableSystem as _;
#[track_caller] #[track_caller]
fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> { fn get_function_f<'db>(db: &'db TestDb, file: &'static str) -> FunctionType<'db> {

View file

@ -15,6 +15,7 @@ red_knot_python_semantic = { workspace = true, features = ["serde"] }
red_knot_vendored = { workspace = true } red_knot_vendored = { workspace = true }
ruff_db = { workspace = true, features = ["testing"] } ruff_db = { workspace = true, features = ["testing"] }
ruff_index = { workspace = true } ruff_index = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_trivia = { workspace = true } ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true } ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true } ruff_text_size = { workspace = true }
@ -30,6 +31,7 @@ rustc-hash = { workspace = true }
salsa = { workspace = true } salsa = { workspace = true }
smallvec = { workspace = true } smallvec = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View file

@ -490,12 +490,12 @@ pub(crate) enum ErrorAssertionParseError<'a> {
mod tests { mod tests {
use super::*; use super::*;
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
fn get_assertions(source: &str) -> InlineFileAssertions { fn get_assertions(source: &str) -> InlineFileAssertions {
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src")); let mut db = Db::setup();
db.write_file("/src/test.py", source).unwrap(); db.write_file("/src/test.py", source).unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap();
InlineFileAssertions::from_file(&db, file) InlineFileAssertions::from_file(&db, file)

View file

@ -12,7 +12,7 @@ use anyhow::Context;
use red_knot_python_semantic::PythonPlatform; use red_knot_python_semantic::PythonPlatform;
use ruff_db::system::{SystemPath, SystemPathBuf}; use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
use serde::Deserialize; use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug, Default, Clone)] #[derive(Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
@ -20,6 +20,11 @@ pub(crate) struct MarkdownTestConfig {
pub(crate) environment: Option<Environment>, pub(crate) environment: Option<Environment>,
pub(crate) log: Option<Log>, pub(crate) log: Option<Log>,
/// The [`ruff_db::system::System`] to use for tests.
///
/// Defaults to the case-sensitive [`ruff_db::system::InMemorySystem`].
pub(crate) system: Option<SystemKind>,
} }
impl MarkdownTestConfig { impl MarkdownTestConfig {
@ -74,3 +79,19 @@ pub(crate) enum Log {
/// Enable logging and only show filters that match the given [env-filter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) /// Enable logging and only show filters that match the given [env-filter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)
Filter(String), Filter(String),
} }
/// The system to use for tests.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum SystemKind {
/// Use an in-memory system with a case sensitive file system..
///
/// This is recommended for all tests because it's fast.
#[default]
InMemory,
/// Use the os system.
///
/// This system should only be used when testing system or OS specific behavior.
Os,
}

View file

@ -1,69 +1,53 @@
use std::sync::Arc; use camino::{Utf8Component, Utf8PathBuf};
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{ use red_knot_python_semantic::{default_lint_registry, Db as SemanticDb};
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonPlatform,
SearchPathSettings,
};
use ruff_db::files::{File, Files}; use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::system::{
DbWithWritableSystem, InMemorySystem, OsSystem, System, SystemPath, SystemPathBuf,
WritableSystem,
};
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast}; use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion; use ruff_notebook::{Notebook, NotebookError};
use std::borrow::Cow;
use std::sync::Arc;
use tempfile::TempDir;
#[salsa::db] #[salsa::db]
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Db { pub(crate) struct Db {
project_root: SystemPathBuf,
storage: salsa::Storage<Self>, storage: salsa::Storage<Self>,
files: Files, files: Files,
system: TestSystem, system: MdtestSystem,
vendored: VendoredFileSystem, vendored: VendoredFileSystem,
rule_selection: Arc<RuleSelection>, rule_selection: Arc<RuleSelection>,
} }
impl Db { impl Db {
pub(crate) fn setup(project_root: SystemPathBuf) -> Self { pub(crate) fn setup() -> Self {
let rule_selection = RuleSelection::from_registry(default_lint_registry()); let rule_selection = RuleSelection::from_registry(default_lint_registry());
let db = Self { Self {
project_root, system: MdtestSystem::in_memory(),
storage: salsa::Storage::default(), storage: salsa::Storage::default(),
system: TestSystem::default(),
vendored: red_knot_vendored::file_system().clone(), vendored: red_knot_vendored::file_system().clone(),
files: Files::default(), files: Files::default(),
rule_selection: Arc::new(rule_selection), rule_selection: Arc::new(rule_selection),
}; }
db.memory_file_system()
.create_directory_all(&db.project_root)
.unwrap();
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersion::default(),
python_platform: PythonPlatform::default(),
search_paths: SearchPathSettings::new(vec![db.project_root.clone()]),
},
)
.expect("Invalid search path settings");
db
} }
pub(crate) fn project_root(&self) -> &SystemPath { pub(crate) fn use_os_system_with_temp_dir(&mut self, cwd: SystemPathBuf, temp_dir: TempDir) {
&self.project_root self.system.with_os(cwd, temp_dir);
} Files::sync_all(self);
}
impl DbWithTestSystem for Db {
fn test_system(&self) -> &TestSystem {
&self.system
} }
fn test_system_mut(&mut self) -> &mut TestSystem { pub(crate) fn use_in_memory_system(&mut self) {
&mut self.system self.system.with_in_memory();
Files::sync_all(self);
}
pub(crate) fn create_directory_all(&self, path: &SystemPath) -> ruff_db::system::Result<()> {
self.system.create_directory_all(path)
} }
} }
@ -110,3 +94,175 @@ impl SemanticDb for Db {
impl salsa::Database for Db { impl salsa::Database for Db {
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {} fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
} }
impl DbWithWritableSystem for Db {
type System = MdtestSystem;
fn writable_system(&self) -> &Self::System {
&self.system
}
}
#[derive(Debug, Clone)]
pub(crate) struct MdtestSystem(Arc<MdtestSystemInner>);
#[derive(Debug)]
enum MdtestSystemInner {
InMemory(InMemorySystem),
Os {
os_system: OsSystem,
_temp_dir: TempDir,
},
}
impl MdtestSystem {
fn in_memory() -> Self {
Self(Arc::new(MdtestSystemInner::InMemory(
InMemorySystem::default(),
)))
}
fn as_system(&self) -> &dyn WritableSystem {
match &*self.0 {
MdtestSystemInner::InMemory(system) => system,
MdtestSystemInner::Os { os_system, .. } => os_system,
}
}
fn with_os(&mut self, cwd: SystemPathBuf, temp_dir: TempDir) {
self.0 = Arc::new(MdtestSystemInner::Os {
os_system: OsSystem::new(cwd),
_temp_dir: temp_dir,
});
}
fn with_in_memory(&mut self) {
if let MdtestSystemInner::InMemory(in_memory) = &*self.0 {
in_memory.fs().remove_all();
} else {
self.0 = Arc::new(MdtestSystemInner::InMemory(InMemorySystem::default()));
}
}
fn normalize_path<'a>(&self, path: &'a SystemPath) -> Cow<'a, SystemPath> {
match &*self.0 {
MdtestSystemInner::InMemory(_) => Cow::Borrowed(path),
MdtestSystemInner::Os { os_system, .. } => {
// Make all paths relative to the current directory
// to avoid writing or reading from outside the temp directory.
let without_root: Utf8PathBuf = path
.components()
.skip_while(|component| {
matches!(
component,
Utf8Component::RootDir | Utf8Component::Prefix(..)
)
})
.collect();
Cow::Owned(os_system.current_directory().join(&without_root))
}
}
}
}
impl System for MdtestSystem {
fn path_metadata(
&self,
path: &SystemPath,
) -> ruff_db::system::Result<ruff_db::system::Metadata> {
self.as_system().path_metadata(&self.normalize_path(path))
}
fn canonicalize_path(&self, path: &SystemPath) -> ruff_db::system::Result<SystemPathBuf> {
let canonicalized = self
.as_system()
.canonicalize_path(&self.normalize_path(path))?;
if let MdtestSystemInner::Os { os_system, .. } = &*self.0 {
// Make the path relative to the current directory
Ok(canonicalized
.strip_prefix(os_system.current_directory())
.unwrap()
.to_owned())
} else {
Ok(canonicalized)
}
}
fn read_to_string(&self, path: &SystemPath) -> ruff_db::system::Result<String> {
self.as_system().read_to_string(&self.normalize_path(path))
}
fn read_to_notebook(&self, path: &SystemPath) -> Result<Notebook, NotebookError> {
self.as_system()
.read_to_notebook(&self.normalize_path(path))
}
fn read_virtual_path_to_string(
&self,
path: &ruff_db::system::SystemVirtualPath,
) -> ruff_db::system::Result<String> {
self.as_system().read_virtual_path_to_string(path)
}
fn read_virtual_path_to_notebook(
&self,
path: &ruff_db::system::SystemVirtualPath,
) -> Result<Notebook, NotebookError> {
self.as_system().read_virtual_path_to_notebook(path)
}
fn current_directory(&self) -> &SystemPath {
self.as_system().current_directory()
}
fn user_config_directory(&self) -> Option<SystemPathBuf> {
self.as_system().user_config_directory()
}
fn read_directory<'a>(
&'a self,
path: &SystemPath,
) -> ruff_db::system::Result<
Box<dyn Iterator<Item = ruff_db::system::Result<ruff_db::system::DirectoryEntry>> + 'a>,
> {
self.as_system().read_directory(&self.normalize_path(path))
}
fn walk_directory(
&self,
path: &SystemPath,
) -> ruff_db::system::walk_directory::WalkDirectoryBuilder {
self.as_system().walk_directory(&self.normalize_path(path))
}
fn glob(
&self,
pattern: &str,
) -> Result<
Box<dyn Iterator<Item = Result<SystemPathBuf, ruff_db::system::GlobError>>>,
ruff_db::system::PatternError,
> {
self.as_system()
.glob(self.normalize_path(SystemPath::new(pattern)).as_str())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl WritableSystem for MdtestSystem {
fn write_file(&self, path: &SystemPath, content: &str) -> ruff_db::system::Result<()> {
self.as_system()
.write_file(&self.normalize_path(path), content)
}
fn create_directory_all(&self, path: &SystemPath) -> ruff_db::system::Result<()> {
self.as_system()
.create_directory_all(&self.normalize_path(path))
}
}

View file

@ -148,14 +148,14 @@ mod tests {
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity, Span}; use ruff_db::diagnostic::{DiagnosticId, LintName, Severity, Span};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::source::line_index; use ruff_db::source::line_index;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::DbWithWritableSystem as _;
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use std::borrow::Cow; use std::borrow::Cow;
#[test] #[test]
fn sort_and_group() { fn sort_and_group() {
let mut db = Db::setup(SystemPathBuf::from("/src")); let mut db = Db::setup();
db.write_file("/src/test.py", "one\ntwo\n").unwrap(); db.write_file("/src/test.py", "one\ntwo\n").unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap();
let lines = line_index(&db, file); let lines = line_index(&db, file);

View file

@ -2,14 +2,15 @@ use crate::config::Log;
use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap}; use crate::parser::{BacktickOffsets, EmbeddedFileSourceMap};
use camino::Utf8Path; use camino::Utf8Path;
use colored::Colorize; use colored::Colorize;
use config::SystemKind;
use parser as test_parser; use parser as test_parser;
use red_knot_python_semantic::types::check_types; use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{Program, ProgramSettings, PythonPath, SearchPathSettings}; use red_knot_python_semantic::{Program, ProgramSettings, PythonPath, SearchPathSettings};
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait, OldParseDiagnostic}; use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait, OldParseDiagnostic};
use ruff_db::files::{system_path_to_file, File, Files}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::panic::catch_unwind; use ruff_db::panic::catch_unwind;
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf}; use ruff_db::system::{DbWithWritableSystem as _, SystemPath, SystemPathBuf};
use ruff_db::testing::{setup_logging, setup_logging_with_filter}; use ruff_db::testing::{setup_logging, setup_logging_with_filter};
use ruff_source_file::{LineIndex, OneIndexed}; use ruff_source_file::{LineIndex, OneIndexed};
use std::fmt::Write; use std::fmt::Write;
@ -42,7 +43,7 @@ pub fn run(
} }
}; };
let mut db = db::Db::setup(SystemPathBuf::from("/src")); let mut db = db::Db::setup();
let filter = std::env::var(MDTEST_TEST_FILTER).ok(); let filter = std::env::var(MDTEST_TEST_FILTER).ok();
let mut any_failures = false; let mut any_failures = false;
@ -56,10 +57,6 @@ pub fn run(
Log::Filter(filter) => setup_logging_with_filter(filter), Log::Filter(filter) => setup_logging_with_filter(filter),
}); });
// Remove all files so that the db is in a "fresh" state.
db.memory_file_system().remove_all();
Files::sync_all(&mut db);
if let Err(failures) = run_test(&mut db, relative_fixture_path, snapshot_path, &test) { if let Err(failures) = run_test(&mut db, relative_fixture_path, snapshot_path, &test) {
any_failures = true; any_failures = true;
println!("\n{}\n", test.name().bold().underline()); println!("\n{}\n", test.name().bold().underline());
@ -104,9 +101,30 @@ fn run_test(
snapshot_path: &Utf8Path, snapshot_path: &Utf8Path,
test: &parser::MarkdownTest, test: &parser::MarkdownTest,
) -> Result<(), Failures> { ) -> Result<(), Failures> {
let project_root = db.project_root().to_path_buf(); // Initialize the system and remove all files and directories to reset the system to a clean state.
let src_path = SystemPathBuf::from("/src"); match test.configuration().system.unwrap_or_default() {
let custom_typeshed_path = test.configuration().typeshed().map(SystemPath::to_path_buf); SystemKind::InMemory => {
db.use_in_memory_system();
}
SystemKind::Os => {
let dir = tempfile::TempDir::new().expect("Creating a temporary directory to succeed");
let root_path = dir
.path()
.canonicalize()
.expect("Canonicalizing to succeed");
let root_path = SystemPathBuf::from_path_buf(root_path)
.expect("Temp directory to be a valid UTF8 path");
db.use_os_system_with_temp_dir(root_path, dir);
}
}
let project_root = SystemPathBuf::from("/src");
db.create_directory_all(&project_root)
.expect("Creating the project root to succeed");
let src_path = project_root.clone();
let custom_typeshed_path = test.configuration().typeshed();
let mut typeshed_files = vec![]; let mut typeshed_files = vec![];
let mut has_custom_versions_file = false; let mut has_custom_versions_file = false;
@ -124,7 +142,7 @@ fn run_test(
let full_path = embedded.full_path(&project_root); let full_path = embedded.full_path(&project_root);
if let Some(ref typeshed_path) = custom_typeshed_path { if let Some(typeshed_path) = custom_typeshed_path {
if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) { if let Ok(relative_path) = full_path.strip_prefix(typeshed_path.join("stdlib")) {
if relative_path.as_str() == "VERSIONS" { if relative_path.as_str() == "VERSIONS" {
has_custom_versions_file = true; has_custom_versions_file = true;
@ -151,7 +169,7 @@ fn run_test(
.collect(); .collect();
// Create a custom typeshed `VERSIONS` file if none was provided. // Create a custom typeshed `VERSIONS` file if none was provided.
if let Some(ref typeshed_path) = custom_typeshed_path { if let Some(typeshed_path) = custom_typeshed_path {
if !has_custom_versions_file { if !has_custom_versions_file {
let versions_file = typeshed_path.join("stdlib/VERSIONS"); let versions_file = typeshed_path.join("stdlib/VERSIONS");
let contents = typeshed_files let contents = typeshed_files
@ -170,25 +188,26 @@ fn run_test(
} }
} }
Program::get(db) let settings = ProgramSettings {
.update_from_settings( python_version: test.configuration().python_version().unwrap_or_default(),
db, python_platform: test.configuration().python_platform().unwrap_or_default(),
ProgramSettings { search_paths: SearchPathSettings {
python_version: test.configuration().python_version().unwrap_or_default(), src_roots: vec![src_path],
python_platform: test.configuration().python_platform().unwrap_or_default(), extra_paths: test
search_paths: SearchPathSettings { .configuration()
src_roots: vec![src_path], .extra_paths()
extra_paths: test .unwrap_or_default()
.configuration() .to_vec(),
.extra_paths() custom_typeshed: custom_typeshed_path.map(SystemPath::to_path_buf),
.unwrap_or_default() python_path: PythonPath::KnownSitePackages(vec![]),
.to_vec(), },
custom_typeshed: custom_typeshed_path, };
python_path: PythonPath::KnownSitePackages(vec![]),
}, match Program::try_get(db) {
}, Some(program) => program.update_from_settings(db, settings),
) None => Program::from_settings(db, settings).map(|_| ()),
.expect("Failed to update Program settings in TestDb"); }
.expect("Failed to update Program settings in TestDb");
// When snapshot testing is enabled, this is populated with // When snapshot testing is enabled, this is populated with
// all diagnostics. Otherwise it remains empty. // all diagnostics. Otherwise it remains empty.

View file

@ -349,7 +349,7 @@ mod tests {
use super::FailuresByLine; use super::FailuresByLine;
use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, Severity, Span}; use ruff_db::diagnostic::{DiagnosticId, OldDiagnosticTrait, Severity, Span};
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::DbWithWritableSystem as _;
use ruff_python_trivia::textwrap::dedent; use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
use ruff_text_size::TextRange; use ruff_text_size::TextRange;
@ -413,7 +413,7 @@ mod tests {
) -> Result<(), FailuresByLine> { ) -> Result<(), FailuresByLine> {
colored::control::set_override(false); colored::control::set_override(false);
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src")); let mut db = crate::db::Db::setup();
db.write_file("/src/test.py", source).unwrap(); db.write_file("/src/test.py", source).unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap(); let file = system_path_to_file(&db, "/src/test.py").unwrap();

View file

@ -64,7 +64,7 @@ impl Workspace {
pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> { pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> {
self.system self.system
.fs .fs
.write_file(path, contents) .write_file_all(path, contents)
.map_err(into_error)?; .map_err(into_error)?;
let file = system_path_to_file(&self.db, path).expect("File to exist"); let file = system_path_to_file(&self.db, path).expect("File to exist");

View file

@ -106,7 +106,7 @@ fn setup_case() -> Case {
let system = TestSystem::default(); let system = TestSystem::default();
let fs = system.memory_file_system().clone(); let fs = system.memory_file_system().clone();
fs.write_files( fs.write_files_all(
TOMLLIB_FILES TOMLLIB_FILES
.iter() .iter()
.map(|file| (tomllib_path(file), file.code().to_string())), .map(|file| (tomllib_path(file), file.code().to_string())),
@ -173,7 +173,7 @@ fn benchmark_incremental(criterion: &mut Criterion) {
assert_diagnostics(&case.db, &result); assert_diagnostics(&case.db, &result);
case.fs case.fs
.write_file( .write_file_all(
&case.re_path, &case.re_path,
format!("{}\n# A comment\n", source_text(&case.db, case.re).as_str()), format!("{}\n# A comment\n", source_text(&case.db, case.re).as_str()),
) )

View file

@ -496,7 +496,7 @@ impl std::error::Error for FileError {}
mod tests { mod tests {
use crate::file_revision::FileRevision; use crate::file_revision::FileRevision;
use crate::files::{system_path_to_file, vendored_path_to_file, FileError}; use crate::files::{system_path_to_file, vendored_path_to_file, FileError};
use crate::system::DbWithTestSystem; use crate::system::DbWithWritableSystem as _;
use crate::tests::TestDb; use crate::tests::TestDb;
use crate::vendored::VendoredFileSystemBuilder; use crate::vendored::VendoredFileSystemBuilder;
use zip::CompressionMethod; use zip::CompressionMethod;

View file

@ -85,7 +85,9 @@ impl Eq for ParsedModule {}
mod tests { mod tests {
use crate::files::{system_path_to_file, vendored_path_to_file}; use crate::files::{system_path_to_file, vendored_path_to_file};
use crate::parsed::parsed_module; use crate::parsed::parsed_module;
use crate::system::{DbWithTestSystem, SystemPath, SystemVirtualPath}; use crate::system::{
DbWithTestSystem, DbWithWritableSystem as _, SystemPath, SystemVirtualPath,
};
use crate::tests::TestDb; use crate::tests::TestDb;
use crate::vendored::{VendoredFileSystemBuilder, VendoredPath}; use crate::vendored::{VendoredFileSystemBuilder, VendoredPath};
use crate::Db; use crate::Db;
@ -96,7 +98,7 @@ mod tests {
let mut db = TestDb::new(); let mut db = TestDb::new();
let path = "test.py"; let path = "test.py";
db.write_file(path, "x = 10".to_string())?; db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap(); let file = system_path_to_file(&db, path).unwrap();
@ -112,7 +114,7 @@ mod tests {
let mut db = TestDb::new(); let mut db = TestDb::new();
let path = SystemPath::new("test.ipynb"); let path = SystemPath::new("test.ipynb");
db.write_file(path, "%timeit a = b".to_string())?; db.write_file(path, "%timeit a = b")?;
let file = system_path_to_file(&db, path).unwrap(); let file = system_path_to_file(&db, path).unwrap();

View file

@ -176,7 +176,7 @@ mod tests {
use crate::files::system_path_to_file; use crate::files::system_path_to_file;
use crate::source::{line_index, source_text}; use crate::source::{line_index, source_text};
use crate::system::{DbWithTestSystem, SystemPath}; use crate::system::{DbWithWritableSystem as _, SystemPath};
use crate::tests::TestDb; use crate::tests::TestDb;
#[test] #[test]
@ -184,13 +184,13 @@ mod tests {
let mut db = TestDb::new(); let mut db = TestDb::new();
let path = SystemPath::new("test.py"); let path = SystemPath::new("test.py");
db.write_file(path, "x = 10".to_string())?; db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap(); let file = system_path_to_file(&db, path).unwrap();
assert_eq!(source_text(&db, file).as_str(), "x = 10"); assert_eq!(source_text(&db, file).as_str(), "x = 10");
db.write_file(path, "x = 20".to_string()).unwrap(); db.write_file(path, "x = 20").unwrap();
assert_eq!(source_text(&db, file).as_str(), "x = 20"); assert_eq!(source_text(&db, file).as_str(), "x = 20");
@ -202,7 +202,7 @@ mod tests {
let mut db = TestDb::new(); let mut db = TestDb::new();
let path = SystemPath::new("test.py"); let path = SystemPath::new("test.py");
db.write_file(path, "x = 10".to_string())?; db.write_file(path, "x = 10")?;
let file = system_path_to_file(&db, path).unwrap(); let file = system_path_to_file(&db, path).unwrap();
@ -228,7 +228,7 @@ mod tests {
let mut db = TestDb::new(); let mut db = TestDb::new();
let path = SystemPath::new("test.py"); let path = SystemPath::new("test.py");
db.write_file(path, "x = 10\ny = 20".to_string())?; db.write_file(path, "x = 10\ny = 20")?;
let file = system_path_to_file(&db, path).unwrap(); let file = system_path_to_file(&db, path).unwrap();
let index = line_index(&db, file); let index = line_index(&db, file);

View file

@ -12,7 +12,7 @@ use std::error::Error;
use std::fmt::Debug; use std::fmt::Debug;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{fmt, io}; use std::{fmt, io};
pub use test::{DbWithTestSystem, InMemorySystem, TestSystem}; pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
use walk_directory::WalkDirectoryBuilder; use walk_directory::WalkDirectoryBuilder;
use crate::file_revision::FileRevision; use crate::file_revision::FileRevision;
@ -161,6 +161,15 @@ pub trait System: Debug {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any; fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
} }
/// System trait for non-readonly systems.
pub trait WritableSystem: System {
/// Writes the given content to the file at the given path.
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()>;
/// Creates a directory at `path` as well as any intermediate directories.
fn create_directory_all(&self, path: &SystemPath) -> Result<()>;
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct Metadata { pub struct Metadata {
revision: FileRevision, revision: FileRevision,

View file

@ -153,28 +153,9 @@ impl MemoryFileSystem {
virtual_files.contains_key(&path.to_path_buf()) virtual_files.contains_key(&path.to_path_buf())
} }
/// Writes the files to the file system.
///
/// The operation overrides existing files with the same normalized path.
///
/// Enclosing directories are automatically created if they don't exist.
pub fn write_files<P, C>(&self, files: impl IntoIterator<Item = (P, C)>) -> Result<()>
where
P: AsRef<SystemPath>,
C: ToString,
{
for (path, content) in files {
self.write_file(path.as_ref(), content.to_string())?;
}
Ok(())
}
/// Stores a new file in the file system. /// Stores a new file in the file system.
/// ///
/// The operation overrides the content for an existing file with the same normalized `path`. /// The operation overrides the content for an existing file with the same normalized `path`.
///
/// Enclosing directories are automatically created if they don't exist.
pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> { pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
let mut by_path = self.inner.by_path.write().unwrap(); let mut by_path = self.inner.by_path.write().unwrap();
@ -187,6 +168,42 @@ impl MemoryFileSystem {
Ok(()) Ok(())
} }
/// Writes the files to the file system.
///
/// The operation overrides existing files with the same normalized path.
///
/// Enclosing directories are automatically created if they don't exist.
pub fn write_files_all<P, C>(&self, files: impl IntoIterator<Item = (P, C)>) -> Result<()>
where
P: AsRef<SystemPath>,
C: ToString,
{
for (path, content) in files {
self.write_file_all(path.as_ref(), content.to_string())?;
}
Ok(())
}
/// Stores a new file in the file system.
///
/// The operation overrides the content for an existing file with the same normalized `path`.
///
/// Enclosing directories are automatically created if they don't exist.
pub fn write_file_all(
&self,
path: impl AsRef<SystemPath>,
content: impl ToString,
) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
self.create_directory_all(parent)?;
}
self.write_file(path, content)
}
/// Stores a new virtual file in the file system. /// Stores a new virtual file in the file system.
/// ///
/// The operation overrides the content for an existing virtual file with the same `path`. /// The operation overrides the content for an existing virtual file with the same `path`.
@ -486,7 +503,11 @@ fn get_or_create_file<'a>(
normalized: &Utf8Path, normalized: &Utf8Path,
) -> Result<&'a mut File> { ) -> Result<&'a mut File> {
if let Some(parent) = normalized.parent() { if let Some(parent) = normalized.parent() {
create_dir_all(paths, parent)?; let parent_entry = paths.get(parent).ok_or_else(not_found)?;
if parent_entry.is_file() {
return Err(not_a_directory());
}
} }
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| { let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
@ -719,7 +740,7 @@ mod tests {
P: AsRef<SystemPath>, P: AsRef<SystemPath>,
{ {
let fs = MemoryFileSystem::new(); let fs = MemoryFileSystem::new();
fs.write_files(files.into_iter().map(|path| (path, ""))) fs.write_files_all(files.into_iter().map(|path| (path, "")))
.unwrap(); .unwrap();
fs fs
@ -822,29 +843,25 @@ mod tests {
} }
#[test] #[test]
fn write_file_fails_if_a_component_is_a_file() { fn write_file_fails_if_a_parent_directory_is_missing() {
let fs = with_files(["a/b.py"]); let fs = with_files(["c.py"]);
let error = fs let error = fs
.write_file(SystemPath::new("a/b.py/c"), "content".to_string()) .write_file(SystemPath::new("a/b.py"), "content".to_string())
.unwrap_err(); .unwrap_err();
assert_eq!(error.kind(), ErrorKind::Other); assert_eq!(error.kind(), ErrorKind::NotFound);
} }
#[test] #[test]
fn write_file_fails_if_path_points_to_a_directory() -> Result<()> { fn write_file_all_fails_if_a_component_is_a_file() {
let fs = MemoryFileSystem::new(); let fs = with_files(["a/b.py"]);
fs.create_directory_all("a")?;
let error = fs let error = fs
.write_file(SystemPath::new("a"), "content".to_string()) .write_file_all(SystemPath::new("a/b.py/c"), "content".to_string())
.unwrap_err(); .unwrap_err();
assert_eq!(error.kind(), ErrorKind::Other); assert_eq!(error.kind(), ErrorKind::Other);
Ok(())
} }
#[test] #[test]
@ -864,7 +881,7 @@ mod tests {
let fs = MemoryFileSystem::new(); let fs = MemoryFileSystem::new();
let path = SystemPath::new("a.py"); let path = SystemPath::new("a.py");
fs.write_file(path, "Test content".to_string())?; fs.write_file_all(path, "Test content".to_string())?;
assert_eq!(fs.read_to_string(path)?, "Test content"); assert_eq!(fs.read_to_string(path)?, "Test content");
@ -895,6 +912,21 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn write_file_fails_if_path_points_to_a_directory() -> Result<()> {
let fs = MemoryFileSystem::new();
fs.create_directory_all("a")?;
let error = fs
.write_file(SystemPath::new("a"), "content".to_string())
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::Other);
Ok(())
}
#[test] #[test]
fn read_fails_if_virtual_path_doesnt_exit() { fn read_fails_if_virtual_path_doesnt_exit() {
let fs = MemoryFileSystem::new(); let fs = MemoryFileSystem::new();
@ -1046,7 +1078,7 @@ mod tests {
let root = SystemPath::new("/src"); let root = SystemPath::new("/src");
let system = MemoryFileSystem::with_current_directory(root); let system = MemoryFileSystem::with_current_directory(root);
system.write_files([ system.write_files_all([
(root.join("foo.py"), "print('foo')"), (root.join("foo.py"), "print('foo')"),
(root.join("a/bar.py"), "print('bar')"), (root.join("a/bar.py"), "print('bar')"),
(root.join("a/baz.py"), "print('baz')"), (root.join("a/baz.py"), "print('baz')"),
@ -1105,7 +1137,7 @@ mod tests {
let root = SystemPath::new("/src"); let root = SystemPath::new("/src");
let system = MemoryFileSystem::with_current_directory(root); let system = MemoryFileSystem::with_current_directory(root);
system.write_files([ system.write_files_all([
(root.join("foo.py"), "print('foo')"), (root.join("foo.py"), "print('foo')"),
(root.join("a/bar.py"), "print('bar')"), (root.join("a/bar.py"), "print('bar')"),
(root.join("a/.baz.py"), "print('baz')"), (root.join("a/.baz.py"), "print('baz')"),
@ -1151,7 +1183,7 @@ mod tests {
let root = SystemPath::new("/src"); let root = SystemPath::new("/src");
let system = MemoryFileSystem::with_current_directory(root); let system = MemoryFileSystem::with_current_directory(root);
system.write_file(root.join("foo.py"), "print('foo')")?; system.write_file_all(root.join("foo.py"), "print('foo')")?;
let writer = DirectoryEntryToString::new(root.to_path_buf()); let writer = DirectoryEntryToString::new(root.to_path_buf());
@ -1181,7 +1213,7 @@ mod tests {
let root = SystemPath::new("/src"); let root = SystemPath::new("/src");
let fs = MemoryFileSystem::with_current_directory(root); let fs = MemoryFileSystem::with_current_directory(root);
fs.write_files([ fs.write_files_all([
(root.join("foo.py"), "print('foo')"), (root.join("foo.py"), "print('foo')"),
(root.join("a/bar.py"), "print('bar')"), (root.join("a/bar.py"), "print('bar')"),
(root.join("a/.baz.py"), "print('baz')"), (root.join("a/.baz.py"), "print('baz')"),

View file

@ -7,7 +7,7 @@ use ruff_notebook::{Notebook, NotebookError};
use crate::system::{ use crate::system::{
DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, SystemPath, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result, System, SystemPath,
SystemPathBuf, SystemVirtualPath, SystemPathBuf, SystemVirtualPath, WritableSystem,
}; };
use super::walk_directory::{ use super::walk_directory::{
@ -191,6 +191,16 @@ impl System for OsSystem {
} }
} }
impl WritableSystem for OsSystem {
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
std::fs::write(path.as_std_path(), content)
}
fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
std::fs::create_dir_all(path.as_std_path())
}
}
#[derive(Debug)] #[derive(Debug)]
struct OsDirectoryWalker; struct OsDirectoryWalker;

View file

@ -1,6 +1,5 @@
use glob::PatternError; use glob::PatternError;
use ruff_notebook::{Notebook, NotebookError}; use ruff_notebook::{Notebook, NotebookError};
use ruff_python_trivia::textwrap;
use std::panic::RefUnwindSafe; use std::panic::RefUnwindSafe;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -12,6 +11,7 @@ use crate::system::{
use crate::Db; use crate::Db;
use super::walk_directory::WalkDirectoryBuilder; use super::walk_directory::WalkDirectoryBuilder;
use super::WritableSystem;
/// System implementation intended for testing. /// System implementation intended for testing.
/// ///
@ -22,10 +22,16 @@ use super::walk_directory::WalkDirectoryBuilder;
/// Don't use this system for production code. It's intended for testing only. /// Don't use this system for production code. It's intended for testing only.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TestSystem { pub struct TestSystem {
inner: Arc<dyn System + RefUnwindSafe + Send + Sync>, inner: Arc<dyn WritableSystem + RefUnwindSafe + Send + Sync>,
} }
impl TestSystem { impl TestSystem {
pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self {
Self {
inner: Arc::new(inner),
}
}
/// Returns the [`InMemorySystem`]. /// Returns the [`InMemorySystem`].
/// ///
/// ## Panics /// ## Panics
@ -50,12 +56,12 @@ impl TestSystem {
fn use_system<S>(&mut self, system: S) fn use_system<S>(&mut self, system: S)
where where
S: System + Send + Sync + RefUnwindSafe + 'static, S: WritableSystem + Send + Sync + RefUnwindSafe + 'static,
{ {
self.inner = Arc::new(system); self.inner = Arc::new(system);
} }
pub fn system(&self) -> &dyn System { pub fn system(&self) -> &dyn WritableSystem {
&*self.inner &*self.inner
} }
} }
@ -134,6 +140,73 @@ impl Default for TestSystem {
} }
} }
impl WritableSystem for TestSystem {
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
self.system().write_file(path, content)
}
fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
self.system().create_directory_all(path)
}
}
/// Extension trait for databases that use a [`WritableSystem`].
///
/// Provides various helper function that ease testing.
pub trait DbWithWritableSystem: Db + Sized {
type System: WritableSystem;
fn writable_system(&self) -> &Self::System;
/// Writes the content of the given file and notifies the Db about the change.
fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl AsRef<str>) -> Result<()> {
let path = path.as_ref();
match self.writable_system().write_file(path, content.as_ref()) {
Ok(()) => {
File::sync_path(self, path);
Ok(())
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
if let Some(parent) = path.parent() {
self.writable_system().create_directory_all(parent)?;
for ancestor in parent.ancestors() {
File::sync_path(self, ancestor);
}
self.writable_system().write_file(path, content.as_ref())?;
File::sync_path(self, path);
Ok(())
} else {
Err(error)
}
}
err => err,
}
}
/// Writes auto-dedented text to a file.
fn write_dedented(&mut self, path: &str, content: &str) -> Result<()> {
self.write_file(path, ruff_python_trivia::textwrap::dedent(content))?;
Ok(())
}
/// Writes the content of the given files and notifies the Db about the change.
fn write_files<P, C, I>(&mut self, files: I) -> Result<()>
where
I: IntoIterator<Item = (P, C)>,
P: AsRef<SystemPath>,
C: AsRef<str>,
{
for (path, content) in files {
self.write_file(path, content)?;
}
Ok(())
}
}
/// Extension trait for databases that use [`TestSystem`]. /// Extension trait for databases that use [`TestSystem`].
/// ///
/// Provides various helper function that ease testing. /// Provides various helper function that ease testing.
@ -142,35 +215,6 @@ pub trait DbWithTestSystem: Db + Sized {
fn test_system_mut(&mut self) -> &mut TestSystem; fn test_system_mut(&mut self) -> &mut TestSystem;
/// Writes the content of the given file and notifies the Db about the change.
///
/// ## Panics
/// If the db isn't using the [`InMemorySystem`].
fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
let path = path.as_ref();
let memory_fs = self.test_system().memory_file_system();
let sync_ancestors = path
.parent()
.is_some_and(|parent| !memory_fs.exists(parent));
let result = memory_fs.write_file(path, content);
if result.is_ok() {
File::sync_path(self, path);
// Sync the ancestor paths if the path's parent
// directory didn't exist before.
if sync_ancestors {
for ancestor in path.ancestors() {
File::sync_path(self, ancestor);
}
}
}
result
}
/// Writes the content of the given virtual file. /// Writes the content of the given virtual file.
/// ///
/// ## Panics /// ## Panics
@ -182,32 +226,6 @@ pub trait DbWithTestSystem: Db + Sized {
.write_virtual_file(path, content); .write_virtual_file(path, content);
} }
/// Writes auto-dedented text to a file.
///
/// ## Panics
/// If the db isn't using the [`InMemorySystem`].
fn write_dedented(&mut self, path: &str, content: &str) -> crate::system::Result<()> {
self.write_file(path, textwrap::dedent(content))?;
Ok(())
}
/// Writes the content of the given files and notifies the Db about the change.
///
/// ## Panics
/// If the db isn't using the [`InMemorySystem`].
fn write_files<P, C, I>(&mut self, files: I) -> crate::system::Result<()>
where
I: IntoIterator<Item = (P, C)>,
P: AsRef<SystemPath>,
C: ToString,
{
for (path, content) in files {
self.write_file(path, content)?;
}
Ok(())
}
/// Uses the given system instead of the testing system. /// Uses the given system instead of the testing system.
/// ///
/// This useful for testing advanced file system features like permissions, symlinks, etc. /// This useful for testing advanced file system features like permissions, symlinks, etc.
@ -215,7 +233,7 @@ pub trait DbWithTestSystem: Db + Sized {
/// Note that any files written to the memory file system won't be copied over. /// Note that any files written to the memory file system won't be copied over.
fn use_system<S>(&mut self, os: S) fn use_system<S>(&mut self, os: S)
where where
S: System + Send + Sync + RefUnwindSafe + 'static, S: WritableSystem + Send + Sync + RefUnwindSafe + 'static,
{ {
self.test_system_mut().use_system(os); self.test_system_mut().use_system(os);
} }
@ -229,6 +247,17 @@ pub trait DbWithTestSystem: Db + Sized {
} }
} }
impl<T> DbWithWritableSystem for T
where
T: DbWithTestSystem,
{
type System = TestSystem;
fn writable_system(&self) -> &Self::System {
self.test_system()
}
}
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct InMemorySystem { pub struct InMemorySystem {
user_config_directory: Mutex<Option<SystemPathBuf>>, user_config_directory: Mutex<Option<SystemPathBuf>>,
@ -236,6 +265,13 @@ pub struct InMemorySystem {
} }
impl InMemorySystem { impl InMemorySystem {
pub fn new(cwd: SystemPathBuf) -> Self {
Self {
user_config_directory: Mutex::new(None),
memory_fs: MemoryFileSystem::with_current_directory(cwd),
}
}
pub fn fs(&self) -> &MemoryFileSystem { pub fn fs(&self) -> &MemoryFileSystem {
&self.memory_fs &self.memory_fs
} }
@ -314,3 +350,13 @@ impl System for InMemorySystem {
self self
} }
} }
impl WritableSystem for InMemorySystem {
fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
self.memory_fs.write_file(path, content)
}
fn create_directory_all(&self, path: &SystemPath) -> Result<()> {
self.memory_fs.create_directory_all(path)
}
}

View file

@ -14,7 +14,9 @@ use red_knot_python_semantic::{
PythonPlatform, SearchPathSettings, PythonPlatform, SearchPathSettings,
}; };
use ruff_db::files::{system_path_to_file, File, Files}; use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem}; use ruff_db::system::{
DbWithTestSystem, DbWithWritableSystem as _, System, SystemPathBuf, TestSystem,
};
use ruff_db::vendored::VendoredFileSystem; use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast}; use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;