дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(run): read JetBrains path options written as file: URLs (#2287)
IDEA writes path options as URLs (`WORKING_DIRECTORY = file://$PROJECT_DIR$`). The run configuration importer expanded the macros but never stripped the `file:` scheme, so on Windows `file://C:/project` became the drive-relative `C:project` and was joined onto the project directory, and imported configurations failed to launch. Every path-valued option the importer reads (working directories, SDK home, script and settings files, pytest targets, env files including EnvFile entries, package.json, package manager, node interpreter, config file, test directory/file, JS file, interpreter path, path arguments) now goes through one conversion: drive letters in the authority position, `localhost` as a local authority, UNC for a real host, and IDEA's unencoded paths kept verbatim. Env-file lists no longer split at a URL's scheme colon on Unix. Co-authored-by: Clarence404 <[email protected]>
Цей коміт міститься в:
1 джерело
14aa612038
коміт
5ab5c8c677
2 files changed
+371
-18
No files matched your search
@@ -271,6 +271,11 @@ impl Cx<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A filesystem path: expand IDEA's macros before converting a file URL.
|
||||
fn path(&self, value: Option<&str>) -> Res<Option<String>> {
|
||||
Ok(self.text(value)?.map(|v| file_url_path(&v)))
|
||||
}
|
||||
|
||||
/// Words of a parameters field, split like IDEA's `ParametersListUtil.parse`.
|
||||
fn words(&self, value: Option<&str>) -> Res<Vec<String>> {
|
||||
self.words_quoted(value, Quotes::Double)
|
||||
@@ -286,12 +291,16 @@ impl Cx<'_> {
|
||||
}
|
||||
|
||||
fn args_quoted(&self, value: Option<&str>, quotes: Quotes, dir: &Path) -> Res<Vec<String>> {
|
||||
Ok(self.words_quoted(value, quotes)?.into_iter().map(|a| path_from(self.root, a, dir)).collect())
|
||||
Ok(self
|
||||
.words_quoted(value, quotes)?
|
||||
.into_iter()
|
||||
.map(|a| path_from(self.root, file_url_path(&a), dir))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The working directory (absolute) and the configuration's `cwd`.
|
||||
fn cwd(&self, value: Option<&str>) -> Res<(PathBuf, Option<PathBuf>)> {
|
||||
Ok(match self.text(value)? {
|
||||
Ok(match self.path(value)? {
|
||||
Some(dir) => working_dir(self.root, &dir),
|
||||
None => (self.root.to_path_buf(), None),
|
||||
})
|
||||
@@ -350,7 +359,7 @@ impl Cx<'_> {
|
||||
/// options, and the working directory.
|
||||
fn interpreter(&self, c: &El, ignored: &mut Vec<String>) -> Res<(String, Vec<String>, PathBuf, Option<PathBuf>)> {
|
||||
let (dir, cwd) = self.cwd(c.option("WORKING_DIRECTORY"))?;
|
||||
let sdk = if c.flag("IS_MODULE_SDK") { None } else { self.text(c.option("SDK_HOME"))? };
|
||||
let sdk = if c.flag("IS_MODULE_SDK") { None } else { self.path(c.option("SDK_HOME"))? };
|
||||
let (exe, mut args) = match sdk {
|
||||
Some(sdk) => (path_from(self.root, sdk, &dir), Vec::new()),
|
||||
None => project_python(self.root, &dir),
|
||||
@@ -369,7 +378,7 @@ impl Cx<'_> {
|
||||
rc.extras.include_system_env = c.option("PARENT_ENVS") != Some("false");
|
||||
// PyCharm 2024+: "Paths to .env files", joined with `File.pathSeparator`.
|
||||
for file in split_env_files(c.option("ENV_FILES").unwrap_or_default(), PATH_SEPARATOR) {
|
||||
if let Some(file) = self.text(Some(file))? {
|
||||
if let Some(file) = self.path(Some(file))? {
|
||||
rc.extras.env_files.push(path_from(self.root, file, self.root));
|
||||
}
|
||||
}
|
||||
@@ -403,7 +412,7 @@ impl Cx<'_> {
|
||||
ignored.push(format!("EnvFile {path}"));
|
||||
continue;
|
||||
}
|
||||
if let Some(file) = self.text(Some(path))? {
|
||||
if let Some(file) = self.path(Some(path))? {
|
||||
rc.extras.env_files.push(path_from(self.root, file, self.root));
|
||||
}
|
||||
}
|
||||
@@ -418,7 +427,7 @@ impl Cx<'_> {
|
||||
if c.flag("MODULE_MODE") {
|
||||
args.extend(["-m".to_owned(), script]);
|
||||
} else {
|
||||
args.push(path_from(self.root, script, &dir));
|
||||
args.push(path_from(self.root, file_url_path(&script), &dir));
|
||||
}
|
||||
args.extend(self.args(c.option("PARAMETERS"), &dir)?);
|
||||
let mut rc = config(name, "python", exe, args, Some("debugpy"));
|
||||
@@ -480,7 +489,7 @@ impl Cx<'_> {
|
||||
args.extend([manage, "test".to_owned()]);
|
||||
args.extend(self.words(c.option("TARGET"))?);
|
||||
if c.flag("CUSTOM_SETTINGS")
|
||||
&& let Some(settings) = self.text(c.option("SETTINGS_FILE"))?
|
||||
&& let Some(settings) = self.path(c.option("SETTINGS_FILE"))?
|
||||
{
|
||||
args.extend(["--settings".to_owned(), settings_module(&settings, self.root, &dir)]);
|
||||
}
|
||||
@@ -519,7 +528,7 @@ impl Cx<'_> {
|
||||
}
|
||||
}
|
||||
Some("CUSTOM") => {}
|
||||
_ => args.push(path_from(self.root, target, &dir)),
|
||||
_ => args.push(path_from(self.root, file_url_path(&target), &dir)),
|
||||
}
|
||||
}
|
||||
if target_type.as_deref() != Some("CUSTOM")
|
||||
@@ -574,7 +583,7 @@ impl Cx<'_> {
|
||||
fn node_interpreter(&self, c: &El) -> Res<Option<String>> {
|
||||
match c.value("node-interpreter").or_else(|| c.attr("node-interpreter")).map(str::trim) {
|
||||
None | Some("" | "project") => Ok(None),
|
||||
interpreter => self.text(interpreter),
|
||||
interpreter => self.path(interpreter),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,12 +596,12 @@ impl Cx<'_> {
|
||||
}
|
||||
|
||||
fn npm(&self, name: &str, c: &El, ignored: &mut Vec<String>) -> Res<RunConfig> {
|
||||
let package_json = self.text(c.value("package-json"))?;
|
||||
let package_json = self.path(c.value("package-json"))?;
|
||||
let dir = match &package_json {
|
||||
Some(p) => normalize(&self.root.join(p)).parent().map(Path::to_path_buf).unwrap_or(self.root.to_path_buf()),
|
||||
None => self.root.to_path_buf(),
|
||||
};
|
||||
let manager = match self.text(c.value("package-manager"))? {
|
||||
let manager = match self.path(c.value("package-manager"))? {
|
||||
Some(pm) if ["npm", "pnpm", "yarn", "bun"].contains(&command_name(&pm)) => command_name(&pm).to_owned(),
|
||||
_ => {
|
||||
let pkg = std::fs::read_to_string(dir.join("package.json"))
|
||||
@@ -650,14 +659,14 @@ impl Cx<'_> {
|
||||
fn js_tests(&self, name: &str, c: &El, vitest: bool, ignored: &mut Vec<String>) -> Res<RunConfig> {
|
||||
let (dir, cwd) = self.cwd(c.value("working-dir"))?;
|
||||
let mut args: Vec<String> = if vitest { vec!["vitest".into(), "run".into()] } else { vec!["jest".into()] };
|
||||
if !vitest && let Some(config_file) = self.text(c.value("config-file"))? {
|
||||
if !vitest && let Some(config_file) = self.path(c.value("config-file"))? {
|
||||
args.extend(["--config".to_owned(), path_from(self.root, config_file, &dir)]);
|
||||
}
|
||||
args.extend(self.args(c.value(if vitest { "vitest-options" } else { "jest-options" }), &dir)?);
|
||||
match c.value("scope-kind").unwrap_or("ALL") {
|
||||
"DIRECTORY" => args.extend(self.text(c.value("test-directory"))?.map(|d| path_from(self.root, d, &dir))),
|
||||
"DIRECTORY" => args.extend(self.path(c.value("test-directory"))?.map(|d| path_from(self.root, d, &dir))),
|
||||
"TEST_FILE" | "SUITE" | "TEST" => {
|
||||
args.extend(self.text(c.value("test-file"))?.map(|f| path_from(self.root, f, &dir)));
|
||||
args.extend(self.path(c.value("test-file"))?.map(|f| path_from(self.root, f, &dir)));
|
||||
let names: Vec<&str> = c
|
||||
.child("test-names")
|
||||
.into_iter()
|
||||
@@ -686,7 +695,7 @@ impl Cx<'_> {
|
||||
|
||||
fn node(&self, name: &str, c: &El, ignored: &mut Vec<String>) -> Res<RunConfig> {
|
||||
let (dir, cwd) = self.cwd(c.attr("working-dir"))?;
|
||||
let Some(file) = self.text(c.attr("path-to-js-file"))? else {
|
||||
let Some(file) = self.path(c.attr("path-to-js-file"))? else {
|
||||
return Err(SkipReason::NothingToRun);
|
||||
};
|
||||
let mut args = self.words(c.attr("node-parameters"))?;
|
||||
@@ -756,11 +765,11 @@ impl Cx<'_> {
|
||||
let mut params = BTreeMap::new();
|
||||
// Both default to true when missing (`ShRunConfiguration.java:148-149`).
|
||||
let rc = if c.option("EXECUTE_SCRIPT_FILE") != Some("false") {
|
||||
let Some(path) = self.text(c.option("SCRIPT_PATH"))? else { return Err(SkipReason::NothingToRun) };
|
||||
let Some(path) = self.path(c.option("SCRIPT_PATH"))? else { return Err(SkipReason::NothingToRun) };
|
||||
let path = path_from(self.root, path, &dir);
|
||||
let options = self.text(c.option("SCRIPT_OPTIONS"))?.unwrap_or_default();
|
||||
let interpreter =
|
||||
self.text(c.option("INTERPRETER_PATH"))?.map(|i| path_from(self.root, i, &dir)).unwrap_or_default();
|
||||
self.path(c.option("INTERPRETER_PATH"))?.map(|i| path_from(self.root, i, &dir)).unwrap_or_default();
|
||||
let interpreter_options = self.text(c.option("INTERPRETER_OPTIONS"))?.unwrap_or_default();
|
||||
// Run directly, the script is the program: `./run.sh`, so it isn't looked up on the PATH.
|
||||
let path = if interpreter.is_empty() { dot_slash(path) } else { path };
|
||||
@@ -861,7 +870,67 @@ const PATH_SEPARATOR: char = if cfg!(windows) { ';' } else { ':' };
|
||||
/// The `ENV_FILES` paths, joined with `separator` (intellij-community
|
||||
/// `AbstractPythonRunConfiguration.java:303-304`, `:337`).
|
||||
fn split_env_files(value: &str, separator: char) -> impl Iterator<Item = &str> {
|
||||
value.split(separator).filter(|f| !f.trim().is_empty())
|
||||
let mut rest = value;
|
||||
std::iter::from_fn(move || {
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let end = rest.match_indices(separator).find_map(|(end, _)| {
|
||||
let field = rest[..end].trim_start();
|
||||
if separator == ':' {
|
||||
// A URL's scheme and optional drive letter aren't list separators.
|
||||
let scheme = field.eq_ignore_ascii_case("file");
|
||||
let drive = field.strip_prefix("file:").is_some_and(|body| {
|
||||
let body = body.trim_start_matches('/');
|
||||
let body = body
|
||||
.split_once('/')
|
||||
.filter(|(host, _)| host.eq_ignore_ascii_case("localhost"))
|
||||
.map_or(body, |(_, path)| path);
|
||||
body.len() == 1 && body.as_bytes()[0].is_ascii_alphabetic()
|
||||
});
|
||||
if scheme || drive {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(end)
|
||||
});
|
||||
let field = match end {
|
||||
Some(end) => {
|
||||
let field = &rest[..end];
|
||||
rest = &rest[end + separator.len_utf8()..];
|
||||
field
|
||||
}
|
||||
None => std::mem::take(&mut rest),
|
||||
};
|
||||
Some(field)
|
||||
})
|
||||
.filter(|f| !f.trim().is_empty())
|
||||
}
|
||||
|
||||
/// The path a `file:` URL names, and `value` unchanged when it is not one.
|
||||
///
|
||||
/// IDEA writes path options as URLs (`file://$PROJECT_DIR$`), which expand to
|
||||
/// `file://C:/project` on Windows: the drive letter lands where a URL's authority
|
||||
/// belongs. Parsing that as a path yields `C:project`, a drive-relative path that
|
||||
/// then resolves against the project directory instead of replacing it.
|
||||
fn file_url_path(value: &str) -> String {
|
||||
let Some(rest) = value.strip_prefix("file:") else {
|
||||
return value.to_owned();
|
||||
};
|
||||
let rest = match rest.strip_prefix("//").and_then(|r| r.split_once('/')) {
|
||||
Some((host, path)) if host.eq_ignore_ascii_case("localhost") => path,
|
||||
_ => rest,
|
||||
};
|
||||
let body = rest.trim_start_matches('/');
|
||||
let drive = matches!(body.as_bytes(), [d, b':', ..] if d.is_ascii_alphabetic());
|
||||
if drive {
|
||||
body.to_owned()
|
||||
} else if rest.starts_with("//") && !rest.starts_with("///") {
|
||||
// A real authority (`file://server/share`): a UNC path.
|
||||
format!("//{body}")
|
||||
} else {
|
||||
format!("/{body}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Keywords without the quotes around them (`"not slow"` → `not slow`).
|
||||
@@ -965,6 +1034,57 @@ mod tests {
|
||||
assert!(parse("<a><b></a>").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_urls_become_paths_whatever_the_drive_letter_looks_like() {
|
||||
// IDEA on Windows: the drive letter sits where the authority belongs.
|
||||
assert_eq!(file_url_path("file://C:/p"), "C:/p");
|
||||
assert_eq!(file_url_path(r"file://C:\p"), r"C:\p");
|
||||
assert_eq!(file_url_path("file:///C:/p"), "C:/p");
|
||||
assert_eq!(file_url_path("file:///p/a"), "/p/a");
|
||||
assert_eq!(file_url_path("file://server/share"), "//server/share");
|
||||
// Not a URL: left alone, including a bare Windows path.
|
||||
assert_eq!(file_url_path(r"C:\p"), r"C:\p");
|
||||
assert_eq!(file_url_path("/p/a"), "/p/a");
|
||||
assert_eq!(file_url_path(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_urls_preserve_paths_and_treat_localhost_as_local() {
|
||||
for (url, expected) in [
|
||||
("file:///p/100%20", "/p/100%20"),
|
||||
("file:///p/a%E4", "/p/a%E4"),
|
||||
("file:///p/a b/中.py", "/p/a b/中.py"),
|
||||
("file://C:/a b/é", "C:/a b/é"),
|
||||
("file:///C:/a%20b", "C:/a%20b"),
|
||||
("file://localhost/p/a%20b", "/p/a%20b"),
|
||||
("file://localhost/C:/a%20b", "C:/a%20b"),
|
||||
("file://LOCALHOST/p", "/p"),
|
||||
("file://server/share/a%20b", "//server/share/a%20b"),
|
||||
("file:///p/%2520", "/p/%2520"),
|
||||
("/p/a%20b", "/p/a%20b"),
|
||||
("relative%20path", "relative%20path"),
|
||||
("https://localhost/a%20b", "https://localhost/a%20b"),
|
||||
] {
|
||||
assert_eq!(file_url_path(url), expected, "{url}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The property the bug broke: `working_dir` joins its `cwd` onto the project
|
||||
/// directory, so a URL that reads as a relative path resolves inside it instead
|
||||
/// of replacing it.
|
||||
#[test]
|
||||
fn file_urls_name_absolute_paths() {
|
||||
let absolute = if cfg!(windows) {
|
||||
vec!["file://C:/p", "file:///C:/p", r"file://C:\p", "file://server/share"]
|
||||
} else {
|
||||
vec!["file:///p/a", "file://server/share"]
|
||||
};
|
||||
for url in absolute {
|
||||
let path = file_url_path(url);
|
||||
assert!(Path::new(&path).is_absolute(), "{url} became {path:?}, which is not absolute");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_files_split_on_the_path_separator() {
|
||||
assert_eq!(
|
||||
@@ -975,6 +1095,19 @@ mod tests {
|
||||
split_env_files(r"C:\p\.env;C:\p\.env.local", ';').collect::<Vec<_>>(),
|
||||
[r"C:\p\.env", r"C:\p\.env.local"]
|
||||
);
|
||||
assert_eq!(
|
||||
split_env_files("file://$PROJECT_DIR$/.env:file://localhost/p/a%20b:.env.local", ':').collect::<Vec<_>>(),
|
||||
["file://$PROJECT_DIR$/.env", "file://localhost/p/a%20b", ".env.local"]
|
||||
);
|
||||
assert_eq!(
|
||||
split_env_files("file://C:/p/.env:file:///D:/p/.env:file://localhost/E:/p/.env%3Adev", ':')
|
||||
.collect::<Vec<_>>(),
|
||||
["file://C:/p/.env", "file:///D:/p/.env", "file://localhost/E:/p/.env%3Adev"]
|
||||
);
|
||||
assert_eq!(
|
||||
split_env_files("file://C:/p/.env;file://localhost/C:/p/.env.local", ';').collect::<Vec<_>>(),
|
||||
["file://C:/p/.env", "file://localhost/C:/p/.env.local"]
|
||||
);
|
||||
assert_eq!(split_env_files("", ':').count(), 0);
|
||||
assert_eq!(PATH_SEPARATOR, if cfg!(windows) { ';' } else { ':' });
|
||||
}
|
||||
|
||||
@@ -787,3 +787,223 @@ fn java_applications_and_spring_boot_keep_their_module() {
|
||||
|
||||
assert_eq!(reason(&r, "NoMain"), &SkipReason::NothingToRun);
|
||||
}
|
||||
|
||||
/// IDEA writes the working directory as a URL. On Windows `file://$PROJECT_DIR$`
|
||||
/// expands to `file://C:/project`, where the drive letter sits where the authority
|
||||
/// belongs; read as a path that is `C:project`, which resolves against the project
|
||||
/// directory instead of replacing it and the launch fails on the doubled path.
|
||||
#[test]
|
||||
fn file_url_working_directories_stay_inside_the_project() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(dir.path()).unwrap();
|
||||
write(&root, "pom.xml", "<project><artifactId>shop</artifactId><packaging>pom</packaging></project>");
|
||||
write(
|
||||
&root,
|
||||
"gateway/pom.xml",
|
||||
"<project><parent><artifactId>shop</artifactId></parent><artifactId>gateway</artifactId></project>",
|
||||
);
|
||||
write(
|
||||
&root,
|
||||
".run/Gateway.run.xml",
|
||||
&run_file(
|
||||
"Gateway",
|
||||
r#"<configuration name="NAME" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
|
||||
<module name="gateway" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.example.GatewayApplication" />
|
||||
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />
|
||||
<option name="ACTIVE_PROFILES" value="dev,local" />
|
||||
<option name="VM_PARAMETERS" value="-Xmx512m" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--server.port=8081" />
|
||||
<envs><env name="TZ" value="UTC" /></envs>
|
||||
<method v="2"><option name="Make" enabled="true" /></method>
|
||||
</configuration>"#,
|
||||
),
|
||||
);
|
||||
write(
|
||||
&root,
|
||||
".run/Sub.run.xml",
|
||||
&run_file(
|
||||
"Sub",
|
||||
r#"<configuration name="NAME" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
|
||||
<module name="gateway" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.example.SubApplication" />
|
||||
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$/gateway" />
|
||||
<method v="2" />
|
||||
</configuration>"#,
|
||||
),
|
||||
);
|
||||
|
||||
let local_root = root.to_string_lossy().replace('\\', "/");
|
||||
for (name, working_directory) in [
|
||||
("Literal", "file://$PROJECT_DIR$/gateway é".to_owned()),
|
||||
("Localhost", format!("file://localhost/{}", local_root.trim_start_matches('/'))),
|
||||
] {
|
||||
write(
|
||||
&root,
|
||||
&format!(".run/{name}.run.xml"),
|
||||
&run_file(
|
||||
name,
|
||||
&format!(
|
||||
r#"
|
||||
<configuration name="NAME" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
|
||||
<module name="gateway" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.example.GatewayApplication" />
|
||||
<option name="WORKING_DIRECTORY" value="{working_directory}" />
|
||||
</configuration>"#
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
let r = import_from(&root, &[]);
|
||||
let boot = get(&r, "Gateway");
|
||||
assert_eq!(boot.kind, "spring-boot");
|
||||
assert_eq!(boot.extras.module.as_deref(), Some("gateway"));
|
||||
assert_eq!(
|
||||
boot.args,
|
||||
s(&[
|
||||
"-Xmx512m",
|
||||
"-cp",
|
||||
"${classpath}",
|
||||
"com.example.GatewayApplication",
|
||||
"--spring.profiles.active=dev,local",
|
||||
"--server.port=8081"
|
||||
])
|
||||
);
|
||||
assert_eq!(boot.env["TZ"], "UTC");
|
||||
assert_eq!(cwd(get(&r, "Literal")), Some(PathBuf::from("gateway é")));
|
||||
assert_eq!(cwd(get(&r, "Localhost")), None);
|
||||
|
||||
// The project directory itself: no `cwd` to store.
|
||||
assert_eq!(cwd(get(&r, "Gateway")), None);
|
||||
assert_eq!(cwd(get(&r, "Sub")), Some(PathBuf::from("gateway")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_urls_work_in_all_imported_path_fields() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(dir.path()).unwrap();
|
||||
let cases = [
|
||||
(
|
||||
"Python",
|
||||
r#"<configuration name="NAME" type="PythonConfigurationType">
|
||||
<option name="WORKING_DIRECTORY" value="{base}/jobs 中" />
|
||||
<option name="SDK_HOME" value="{base}/bin/python" />
|
||||
<option name="SCRIPT_NAME" value="{base}/jobs 中/run me.py" />
|
||||
<option name="PARAMETERS" value=""{base}/data file" unchanged%20arg" />
|
||||
<option name="ENV_FILES" value="{base}/.env dev{sep}{base}/.env local" />
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="true" />
|
||||
<ENTRIES><ENTRY PATH="{base}/.env plugin" PARSER="env" /></ENTRIES>
|
||||
</extension>
|
||||
</configuration>"#,
|
||||
),
|
||||
(
|
||||
"Django",
|
||||
r#"<configuration name="NAME" type="DjangoTestsConfigurationType">
|
||||
<option name="CUSTOM_SETTINGS" value="true" />
|
||||
<option name="SETTINGS_FILE" value="{base}/settings/dev.py" />
|
||||
</configuration>"#,
|
||||
),
|
||||
(
|
||||
"Pytest",
|
||||
r#"<configuration name="NAME" type="tests" factoryName="pytest">
|
||||
<option name="_new_targetType" value=""PATH"" />
|
||||
<option name="_new_target" value=""{base}/test file.py"" />
|
||||
<option name="_new_additionalArguments" value=""'{base}/data file'"" />
|
||||
</configuration>"#,
|
||||
),
|
||||
(
|
||||
"Npm",
|
||||
r#"<configuration name="NAME" type="js.build_tools.npm">
|
||||
<package-json value="{base}/web app/package.json" />
|
||||
<package-manager value="{base}/bin/pnpm" />
|
||||
<node-interpreter value="{base}/bin/node" />
|
||||
<command value="run" /><scripts><script value="start" /></scripts>
|
||||
</configuration>"#,
|
||||
),
|
||||
(
|
||||
"Jest",
|
||||
r#"<configuration name="NAME" type="JavaScriptTestRunnerJest">
|
||||
<working-dir value="{base}/web app" />
|
||||
<config-file value="{base}/web app/jest config.js" />
|
||||
<scope-kind value="TEST_FILE" /><test-file value="{base}/web app/test file.js" />
|
||||
</configuration>"#,
|
||||
),
|
||||
(
|
||||
"Vitest",
|
||||
r#"<configuration name="NAME" type="JavaScriptTestRunnerVitest">
|
||||
<scope-kind value="DIRECTORY" /><test-directory value="{base}/test dir" />
|
||||
</configuration>"#,
|
||||
),
|
||||
(
|
||||
"Node",
|
||||
r#"<configuration name="NAME" type="NodeJSConfigurationType"
|
||||
working-dir="{base}/web app" path-to-js-file="{base}/web app/main file.js"
|
||||
application-parameters=""{base}/data file"" />"#,
|
||||
),
|
||||
(
|
||||
"Shell",
|
||||
r#"<configuration name="NAME" type="ShConfigurationType">
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="{base}/jobs 中" />
|
||||
<option name="SCRIPT_PATH" value="{base}/jobs 中/run me.sh" />
|
||||
<option name="INTERPRETER_PATH" value="{base}/bin/sh" />
|
||||
</configuration>"#,
|
||||
),
|
||||
];
|
||||
write(&root, "manage.py", "");
|
||||
for (name, body) in cases {
|
||||
write(
|
||||
&root,
|
||||
&format!(".run/{name}.run.xml"),
|
||||
&run_file(name, &body.replace("{base}", "file://$PROJECT_DIR$").replace("{sep}", &SEP.to_string())),
|
||||
);
|
||||
}
|
||||
let r = import_from(&root, &[]);
|
||||
assert!(r.skipped.is_empty(), "{r:#?}");
|
||||
let python = get(&r, "Python");
|
||||
assert_eq!(cwd(python), Some(PathBuf::from("jobs 中")));
|
||||
assert_eq!(python.command, "../bin/python");
|
||||
assert_eq!(python.args, s(&["run me.py", "../data file", "unchanged%20arg"]));
|
||||
assert_eq!(python.extras.env_files, s(&[".env dev", ".env local", ".env plugin"]));
|
||||
assert_eq!(get(&r, "Django").args, s(&["manage.py", "test", "--settings", "settings.dev"]));
|
||||
assert_eq!(get(&r, "Pytest").args, s(&["-m", "pytest", "test file.py", "data file"]));
|
||||
let npm = get(&r, "Npm");
|
||||
assert_eq!(cwd(npm), Some(PathBuf::from("web app")));
|
||||
assert_eq!(npm.command, "pnpm");
|
||||
assert_eq!(Path::new(&npm.extras.params["nodeInterpreter"]), root.join("bin/node"));
|
||||
assert_eq!(get(&r, "Jest").args, s(&["jest", "--config", "jest config.js", "test file.js"]));
|
||||
assert_eq!(get(&r, "Vitest").args, s(&["vitest", "run", "test dir"]));
|
||||
assert_eq!(get(&r, "Node").args, s(&["main file.js", "../data file"]));
|
||||
let shell = get(&r, "Shell");
|
||||
assert_eq!(shell.command, "../bin/sh");
|
||||
assert_eq!(shell.args, s(&["run me.sh"]));
|
||||
assert_eq!(shell.extras.params["path"], "run me.sh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_urls_expand_module_and_home_macros() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(dir.path()).unwrap();
|
||||
write(
|
||||
&root,
|
||||
".run/Home.run.xml",
|
||||
&run_file(
|
||||
"Home",
|
||||
r#"<configuration name="NAME" type="PythonConfigurationType">
|
||||
<option name="WORKING_DIRECTORY" value="file://$MODULE_DIR$/jobs dir" />
|
||||
<option name="SCRIPT_NAME" value="file://$MODULE_WORKING_DIR$/jobs dir/main.py" />
|
||||
<option name="SDK_HOME" value="file://$USER_HOME$/bin/python" />
|
||||
</configuration>"#,
|
||||
),
|
||||
);
|
||||
let mut environment = env();
|
||||
let home = root.join("home").to_string_lossy().into_owned();
|
||||
environment.insert("HOME".into(), home.clone());
|
||||
environment.insert("USERPROFILE".into(), home);
|
||||
let r = import(&root, RunImportSource::Jetbrains, &[], &environment).unwrap();
|
||||
let home = get(&r, "Home");
|
||||
assert_eq!(home.command, "../home/bin/python");
|
||||
assert_eq!(home.args, s(&["main.py"]));
|
||||
assert_eq!(cwd(home), Some(PathBuf::from("jobs dir")));
|
||||
assert!(home.extras.local);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача