Files
openidea/crates/ide-run/tests/import_vscode.rs
Andrea Basileandandreabasile 5c642819e2 feat(run): import run configurations from .vscode/launch.json (#1030)
Run/Debug Configurations offers Import from .vscode/launch.json when the project has one (also a Find Action command). debugpy/python launches become Python, Django test, module or pytest configurations with their interpreter; debugpy attach becomes Python Remote Debug; node/pwa-node launches become node or npm configurations, attaches become Attach to Node.js, and node-terminal commands map to npm, a program or a shell line. cwd, env, envFile and the per-OS blocks are applied. Imported configurations join the dialog like new ones and are saved on OK/Apply; names already present are skipped, and a notification lists skipped entries with the reason and keys with no equivalent.

Refs #991

Co-authored-by: andreabasile <7715018+andreabasile@users.noreply.github.com>
2026-11-04 00:09:38 +08:00

522 lines
23 KiB
Rust

//! `.vscode/launch.json` import, from launch files written the way the VS Code docs and
//! framework guides (Django, Flask, FastAPI, Next.js) suggest.
use ide_run::RunConfig;
use ide_run::import::{RunImport, RunImportSource, SkipReason, import};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
const VENV_PYTHON: &str = if cfg!(windows) { ".venv/Scripts/python.exe" } else { ".venv/bin/python" };
fn env() -> BTreeMap<String, String> {
BTreeMap::from([("HOME".into(), "/home/me".into()), ("API_TOKEN".into(), "s3cret".into())])
}
fn project(launch: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join(".vscode")).unwrap();
fs::write(dir.path().join(".vscode/launch.json"), launch).unwrap();
dir
}
fn import_from(root: &Path, existing: &[RunConfig]) -> RunImport {
import(root, RunImportSource::Vscode, existing, &env()).unwrap()
}
fn run(launch: &str) -> (tempfile::TempDir, RunImport) {
let dir = project(launch);
let result = import_from(dir.path(), &[]);
(dir, result)
}
fn get<'a>(r: &'a RunImport, name: &str) -> &'a RunConfig {
&r.imported.iter().find(|c| c.config.name == name).unwrap_or_else(|| panic!("{name} not imported: {r:#?}")).config
}
fn ignored<'a>(r: &'a RunImport, name: &str) -> &'a [String] {
&r.imported.iter().find(|c| c.config.name == name).unwrap().ignored
}
fn reason<'a>(r: &'a RunImport, name: &str) -> &'a SkipReason {
&r.skipped.iter().find(|s| s.name == name).unwrap_or_else(|| panic!("{name} not skipped: {r:#?}")).reason
}
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|a| (*a).to_owned()).collect()
}
fn cwd(c: &RunConfig) -> Option<PathBuf> {
c.cwd.as_ref().map(|p| p.as_ref().to_path_buf())
}
const DJANGO: &str = r#"{
// Use IntelliSense to learn about possible attributes.
"version": "0.2.0",
"configurations": [
{
"name": "Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver", "0.0.0.0:8000", "--noreload"],
"django": true,
"justMyCode": false,
"env": { "DJANGO_SETTINGS_MODULE": "mysite.settings.dev", "DEBUG": true, "PATH": "${env:PATH}" },
},
/* the test runner */
{
"name": "Django tests",
"type": "python",
"request": "launch",
"python": "${workspaceFolder}/.venv/bin/python",
"program": "${workspaceFolder}/manage.py",
"args": ["test", "polls", "--keepdb"],
"django": true,
},
],
}"#;
#[test]
fn django_runserver_and_tests_with_comments_and_trailing_commas() {
let dir = project(DJANGO);
let venv = dir.path().join(VENV_PYTHON);
fs::create_dir_all(venv.parent().unwrap()).unwrap();
fs::write(&venv, "").unwrap();
let r = import_from(dir.path(), &[]);
assert!(r.skipped.is_empty(), "{r:#?}");
assert_eq!(r.file, ".vscode/launch.json");
let server = get(&r, "Django");
assert_eq!(server.id, "user:django");
assert_eq!(server.kind, "python");
// No `python`: the project's virtualenv, which the debugger launches directly.
assert_eq!(server.command, VENV_PYTHON);
assert_eq!(server.args, s(&["manage.py", "runserver", "0.0.0.0:8000", "--noreload"]));
assert_eq!(server.debug_adapter.as_deref(), Some("debugpy"));
assert_eq!(cwd(server), None);
// A variable set to its own value is inherited anyway; scalars become strings.
assert_eq!(
server.env,
BTreeMap::from([
("DEBUG".into(), "true".into()),
("DJANGO_SETTINGS_MODULE".into(), "mysite.settings.dev".into())
])
);
assert!(!server.extras.local && !server.detected && !server.is_test);
assert_eq!(ignored(&r, "Django"), ["justMyCode"]);
let tests = get(&r, "Django tests");
assert_eq!(tests.kind, "django-test");
assert!(tests.is_test);
assert_eq!(tests.command, ".venv/bin/python");
assert_eq!(tests.args, s(&["manage.py", "test", "polls", "--keepdb"]));
assert!(ignored(&r, "Django tests").is_empty());
}
#[test]
fn flask_and_fastapi_run_as_modules() {
let (_dir, r) = run(r#"{
"configurations": [
{
"name": "Python Debugger: Flask",
"type": "debugpy",
"request": "launch",
"module": "flask",
"python": "/usr/bin/python3.12",
"env": { "FLASK_APP": "app.py", "FLASK_DEBUG": "1" },
"args": ["run", "--no-debugger", "--no-reload"],
"jinja": true,
"autoStartBrowser": false
},
{
"name": "FastAPI",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"python": "${workspaceFolder}/.venv/bin/python",
"args": "app.main:app --reload --port 8001",
"envFile": "${workspaceFolder}/.env.local"
}
]
}"#);
let flask = get(&r, "Python Debugger: Flask");
assert_eq!(flask.kind, "python");
assert_eq!(flask.command, "/usr/bin/python3.12");
assert_eq!(flask.args, s(&["-m", "flask", "run", "--no-debugger", "--no-reload"]));
assert_eq!(flask.env["FLASK_APP"], "app.py");
assert_eq!(ignored(&r, "Python Debugger: Flask"), ["autoStartBrowser", "jinja"]);
let api = get(&r, "FastAPI");
// A string `args` is split like a command line.
assert_eq!(api.args, s(&["-m", "uvicorn", "app.main:app", "--reload", "--port", "8001"]));
assert_eq!(api.extras.env_files, s(&[".env.local"]));
}
#[test]
fn pytest_module_in_a_subdirectory_is_a_test_configuration() {
let (dir, r) = run(r#"{
"configurations": [{
"name": "Backend tests",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"cwd": "${workspaceFolder}/backend",
"python": "${command:python.interpreterPath}",
"pythonArgs": ["-X", "dev"],
"args": ["-x", "${workspaceFolder}/backend/tests", "--ds=${workspaceFolderBasename}.settings"]
}]
}"#);
let c = get(&r, "Backend tests");
assert_eq!(c.kind, "pytest");
assert!(c.is_test);
assert_eq!(cwd(c), Some(PathBuf::from("backend")));
// The interpreter VS Code has selected is the project's here: no virtualenv, so python3.
assert_eq!(c.command, if cfg!(windows) { "python" } else { "python3" });
let base = dir.path().file_name().unwrap().to_string_lossy().into_owned();
// Paths inside the project are written relative to the working directory.
assert_eq!(c.args, s(&["-X", "dev", "-m", "pytest", "-x", "tests", &format!("--ds={base}.settings")]));
}
#[test]
fn interpreter_from_the_root_virtualenv_is_relative_to_the_working_directory() {
let dir = project(
r#"{ "configurations": [{ "name": "Worker", "type": "debugpy", "request": "launch",
"program": "${workspaceFolder}/api/worker.py", "cwd": "${workspaceFolder}/api" }] }"#,
);
let venv = dir.path().join(VENV_PYTHON);
fs::create_dir_all(venv.parent().unwrap()).unwrap();
fs::write(&venv, "").unwrap();
let r = import_from(dir.path(), &[]);
let c = get(&r, "Worker");
assert_eq!(c.command, format!("../{VENV_PYTHON}"));
assert_eq!(c.args, s(&["worker.py"]));
assert_eq!(cwd(c), Some(PathBuf::from("api")));
}
const NEXT: &str = r#"{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/next/dist/bin/next",
"runtimeArgs": ["--inspect"],
"skipFiles": ["<node_internals>/**"],
"serverReadyAction": { "action": "debugWithEdge", "killOnServerStop": true, "pattern": "- Local:.+(https?://.+)" }
}
]
}"#;
#[test]
fn nextjs_recipes() {
let (_dir, r) = run(NEXT);
let server = get(&r, "Next.js: debug server-side");
assert_eq!((server.kind.as_str(), server.command.as_str()), ("npm", "npm"));
assert_eq!(server.args, s(&["run", "dev"]));
assert_eq!(server.debug_adapter.as_deref(), Some("js-debug"));
assert_eq!(reason(&r, "Next.js: debug client-side"), &SkipReason::UnsupportedType { type_: "chrome".into() });
let full = get(&r, "Next.js: debug full stack");
assert_eq!((full.kind.as_str(), full.command.as_str()), ("node", "node"));
assert_eq!(full.args, s(&["--inspect", "node_modules/next/dist/bin/next"]));
assert_eq!(ignored(&r, "Next.js: debug full stack"), ["serverReadyAction", "skipFiles"]);
}
#[test]
fn node_runtimes_npm_scripts_and_terminal_commands() {
let (_dir, r) = run(r#"{
"configurations": [
{ "name": "Start", "type": "node", "request": "launch", "runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "start"], "cwd": "${workspaceFolder}/web", "env": { "PORT": 3001 } },
{ "name": "Vite", "type": "pwa-node", "request": "launch", "cwd": "${workspaceFolder}/web",
"runtimeExecutable": "${workspaceFolder}/web/node_modules/.bin/vite", "runtimeArgs": ["--port", "5173"] },
{ "name": "Script", "type": "node", "request": "launch", "program": "${workspaceFolder}/scripts/seed.js",
"args": ["--count", 10], "envFile": "${workspaceFolder}/.env", "preLaunchTask": "tsc: build" },
{ "name": "Storybook", "type": "node-terminal", "request": "launch", "command": "pnpm exec storybook dev -p 6006" },
{ "name": "Migrate and serve", "type": "node-terminal", "request": "launch", "command": "npm run migrate && npm start" },
{ "name": "Nothing", "type": "node", "request": "launch" }
]
}"#);
let start = get(&r, "Start");
assert_eq!((start.kind.as_str(), start.command.as_str()), ("npm", "npm"));
assert_eq!(start.args, s(&["run", "start"]));
assert_eq!(cwd(start), Some(PathBuf::from("web")));
assert_eq!(start.env["PORT"], "3001");
let vite = get(&r, "Vite");
assert_eq!((vite.kind.as_str(), vite.command.as_str()), ("node", "node_modules/.bin/vite"));
assert_eq!(vite.args, s(&["--port", "5173"]));
let script = get(&r, "Script");
assert_eq!(script.args, s(&["scripts/seed.js", "--count", "10"]));
assert_eq!(script.extras.env_files, s(&[".env"]));
assert_eq!(ignored(&r, "Script"), ["preLaunchTask"]);
let storybook = get(&r, "Storybook");
assert_eq!((storybook.kind.as_str(), storybook.command.as_str()), ("shell", "pnpm"));
assert_eq!(storybook.args, s(&["exec", "storybook", "dev", "-p", "6006"]));
assert_eq!(storybook.debug_adapter.as_deref(), Some("js-debug"));
// A shell line runs through the shell, without the debugger.
let line = get(&r, "Migrate and serve");
assert_eq!((line.kind.as_str(), line.command.as_str()), ("shell", "npm run migrate && npm start"));
assert!(line.args.is_empty() && line.debug_adapter.is_none());
assert_eq!(reason(&r, "Nothing"), &SkipReason::NothingToRun);
}
#[test]
fn compounds_resolve_members_by_name() {
let launch = r#"{
"configurations": [
{ "name": "Django", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/manage.py", "args": ["runserver"] },
{ "name": "Next.js: debug server-side", "type": "node-terminal", "request": "launch", "command": "npm run dev" },
],
"compounds": [
{ "name": "Full stack", "configurations": ["Django", "Next.js: debug server-side"], "stopAll": true },
{ "name": "With the worker", "configurations": ["Django", { "name": "Celery", "folder": "api" }] },
],
}"#;
let (dir, r) = run(launch);
let full = get(&r, "Full stack");
assert_eq!(full.kind, "compound");
assert_eq!(full.extras.compound, s(&["user:django", "user:next-js-debug-server-side"]));
assert_eq!(ignored(&r, "Full stack"), ["stopAll"]);
assert_eq!(reason(&r, "With the worker"), &SkipReason::MissingMember { member: "Celery".into() });
// Importing again adds nothing; a compound still finds members that already exist.
let mut existing: Vec<RunConfig> = r.imported.iter().map(|c| c.config.clone()).collect();
existing.retain(|c| c.name != "Full stack");
let again = import_from(dir.path(), &existing);
assert_eq!(again.imported.iter().map(|c| c.config.name.as_str()).collect::<Vec<_>>(), ["Full stack"]);
assert_eq!(again.imported[0].config.extras.compound, s(&["user:django", "user:next-js-debug-server-side"]));
assert_eq!(reason(&again, "Django"), &SkipReason::NameTaken);
assert_eq!(reason(&again, "Next.js: debug server-side"), &SkipReason::NameTaken);
}
#[test]
fn existing_names_are_skipped_and_ids_stay_unique() {
let dir = project(
r#"{ "configurations": [
{ "name": "manage.py runserver", "type": "debugpy", "request": "launch", "program": "manage.py", "args": ["runserver"] },
{ "name": "Server", "type": "debugpy", "request": "launch", "program": "server.py" },
{ "name": "Server", "type": "debugpy", "request": "launch", "program": "other.py" }
] }"#,
);
let detected = RunConfig {
id: "django:runserver".into(),
name: "manage.py runserver".into(),
kind: "python".into(),
command: "python3".into(),
args: s(&["manage.py", "runserver"]),
cwd: None,
env: BTreeMap::new(),
detected: true,
is_test: false,
debug_adapter: Some("debugpy".into()),
allow_parallel: false,
extras: Default::default(),
};
let user = RunConfig { id: "user:server".into(), name: "Old server".into(), detected: false, ..detected.clone() };
let r = import_from(dir.path(), &[detected, user]);
assert_eq!(reason(&r, "manage.py runserver"), &SkipReason::NameTaken);
// Two entries with the same name: the first wins.
assert_eq!(r.imported.len(), 1);
assert_eq!(r.imported[0].config.id, "user:server-2");
assert_eq!(r.imported[0].config.args, s(&["server.py"]));
assert_eq!(r.skipped.len(), 2);
}
#[test]
fn variables_known_only_at_launch_skip_the_entry() {
let (_dir, r) = run(r#"{
"configurations": [
{ "name": "Current file", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal" },
{ "name": "Port", "type": "debugpy", "request": "launch", "module": "app", "args": ["--port", "${input:port}"] },
{ "name": "Home", "type": "debugpy", "request": "launch", "program": "${userHome}/bin/tool.py" },
{ "name": "Token", "type": "debugpy", "request": "launch", "program": "app.py",
"env": { "TOKEN": "${env:API_TOKEN}", "MISSING": "${env:NOT_SET}x" } }
],
"inputs": [{ "id": "port", "type": "promptString", "description": "Port" }]
}"#);
assert_eq!(reason(&r, "Current file"), &SkipReason::Variable { variable: "${file}".into() });
assert_eq!(reason(&r, "Port"), &SkipReason::Variable { variable: "${input:port}".into() });
assert_eq!(get(&r, "Home").args, s(&["/home/me/bin/tool.py"]));
// The home directory is this machine's too: the configuration stays out of run.toml.
assert!(get(&r, "Home").extras.local);
let token = get(&r, "Token");
assert_eq!(token.env["TOKEN"], "s3cret");
assert_eq!(token.env["MISSING"], "x");
// Values from this machine's environment go to the workspace-local file, not run.toml.
assert!(token.extras.local);
}
#[test]
fn attach_requests_become_remote_debug_configurations() {
let (_dir, r) = run(r#"{
"configurations": [
{ "name": "Attach (container)", "type": "debugpy", "request": "attach",
"connect": { "host": "localhost", "port": 5678 }, "justMyCode": true,
"pathMappings": [{ "localRoot": "${workspaceFolder}", "remoteRoot": "/app" }] },
{ "name": "Attach to Node", "type": "node", "request": "attach", "port": 9230,
"localRoot": "${workspaceFolder}/web", "remoteRoot": "/usr/src/app", "restart": true },
{ "name": "Attach to PID", "type": "debugpy", "request": "attach", "processId": "${command:pickProcess}" },
{ "name": "Odd", "type": "node", "request": "test" }
]
}"#);
let py = get(&r, "Attach (container)");
assert_eq!((py.kind.as_str(), py.command.as_str()), ("python-remote", ""));
assert_eq!(py.debug_adapter.as_deref(), Some("debugpy"));
assert_eq!(
py.extras.params,
BTreeMap::from([
("host".into(), "localhost".into()),
("port".into(), "5678".into()),
("remoteRoot".into(), "/app".into())
])
);
assert_eq!(ignored(&r, "Attach (container)"), ["justMyCode"]);
let node = get(&r, "Attach to Node");
assert_eq!(node.kind, "node-remote");
assert_eq!(node.extras.params["port"], "9230");
assert_eq!(node.extras.params["localRoot"], "web");
assert_eq!(node.extras.params["remoteRoot"], "/usr/src/app");
assert_eq!(ignored(&r, "Attach to Node"), ["restart"]);
assert_eq!(reason(&r, "Attach to PID"), &SkipReason::UnsupportedAttach);
assert_eq!(reason(&r, "Odd"), &SkipReason::UnsupportedRequest { request: "test".into() });
}
#[test]
fn the_block_for_this_os_overrides_the_entry() {
let os = if cfg!(windows) {
"windows"
} else if cfg!(target_os = "macos") {
"osx"
} else {
"linux"
};
let (_dir, r) = run(&format!(
r#"{{ "configurations": [{{ "name": "App", "type": "debugpy", "request": "launch", "program": "app.py",
"python": "python3", "{os}": {{ "python": "/opt/py/bin/python" }} }}] }}"#
));
assert_eq!(get(&r, "App").command, "/opt/py/bin/python");
assert!(ignored(&r, "App").is_empty());
}
#[test]
fn unnamed_entries_are_invalid_and_bad_json_is_an_error() {
let (_dir, r) = run(r#"{ "configurations": [{ "type": "debugpy", "request": "launch", "program": "a.py" }, 3] }"#);
assert!(r.imported.is_empty());
assert!(r.skipped.iter().all(|s| s.reason == SkipReason::Invalid && s.name.is_empty()));
let dir = project("{ \"configurations\": [ ");
assert!(import(dir.path(), RunImportSource::Vscode, &[], &env()).is_err());
let empty = tempfile::tempdir().unwrap();
assert!(import(empty.path(), RunImportSource::Vscode, &[], &env()).is_err());
assert!(ide_run::import::available(empty.path()).is_empty());
assert_eq!(ide_run::import::available(dir.path()), [RunImportSource::Vscode]);
}
#[test]
fn imported_configurations_round_trip_through_run_toml() {
let (dir, r) = run(DJANGO);
let configs: Vec<RunConfig> = r.imported.iter().map(|c| c.config.clone()).collect();
ide_run::save_user_configs(dir.path(), &configs).unwrap();
assert_eq!(ide_run::load_user_configs(dir.path()).unwrap(), configs);
}
/// Run configurations pass `env` values to the process as written (only `.env` files expand
/// `${VAR}`), so `${env:…}` is resolved when importing; the value stays out of the shared run.toml.
#[test]
fn resolved_environment_values_are_saved_in_the_local_file_only() {
let (dir, r) = run(r#"{ "configurations": [
{ "name": "Token", "type": "debugpy", "request": "launch", "program": "app.py", "env": { "TOKEN": "${env:API_TOKEN}" } },
{ "name": "Plain", "type": "debugpy", "request": "launch", "program": "app.py" }
] }"#);
let configs: Vec<RunConfig> = r.imported.iter().map(|c| c.config.clone()).collect();
ide_run::save_user_configs(dir.path(), &configs).unwrap();
let shared = fs::read_to_string(ide_run::user_configs_path(dir.path())).unwrap();
assert!(shared.contains("Plain") && !shared.contains("Token") && !shared.contains("s3cret"));
let local = fs::read_to_string(dir.path().join(".openidea/run.local.toml")).unwrap();
assert!(local.contains("TOKEN = \"s3cret\""), "{local}");
let loaded = ide_run::load_user_configs(dir.path()).unwrap();
assert!(loaded.iter().any(|c| c.name == "Token" && c.extras.local && c.env["TOKEN"] == "s3cret"));
}
#[test]
fn environment_variables_come_from_what_runs_inherit_login_shell_included() {
// An app started from the Dock lacks what ~/.zshrc exports; runs get it from the login shell.
ide_core::process::set_shell_env(vec![("OPENIDEA_IMPORT_SHELL_ONLY".into(), "from-shell".into())]);
let dir = project(
r#"{ "configurations": [{ "name": "App", "type": "debugpy", "request": "launch", "program": "app.py",
"env": { "DATABASE_URL": "${env:OPENIDEA_IMPORT_SHELL_ONLY}" } }] }"#,
);
let r = ide_run::import_configs(dir.path(), RunImportSource::Vscode, &[]).unwrap();
assert_eq!(get(&r, "App").env["DATABASE_URL"], "from-shell");
}
#[test]
fn selected_interpreter_keeps_its_options_and_subdirectory_manage_py_stays_python() {
let (_dir, r) = run(r#"{ "configurations": [
{ "name": "Dev mode", "type": "debugpy", "request": "launch", "program": "app.py",
"python": ["${command:python.interpreterPath}", "-X", "dev"] },
{ "name": "Default", "type": "debugpy", "request": "launch", "program": "app.py",
"python": "${config:python.defaultInterpreterPath}" },
{ "name": "Spaces", "type": "debugpy", "request": "launch", "program": "app.py",
"python": "/opt/My Python/bin/python" },
{ "name": "Dot tests", "type": "debugpy", "request": "launch", "program": "./manage.py", "args": ["test", "polls"] },
{ "name": "Nested tests", "type": "debugpy", "request": "launch",
"program": "${workspaceFolder}/backend/manage.py", "args": ["test"] }
] }"#);
let py = if cfg!(windows) { "python" } else { "python3" };
assert_eq!(get(&r, "Dev mode").command, py);
assert_eq!(get(&r, "Dev mode").args, s(&["-X", "dev", "app.py"]));
assert_eq!(get(&r, "Default").command, py);
assert_eq!(get(&r, "Spaces").command, "/opt/My Python/bin/python");
let dot = get(&r, "Dot tests");
assert_eq!((dot.kind.as_str(), dot.args.clone()), ("django-test", s(&["manage.py", "test", "polls"])));
// The Django tests form runs `manage.py` from the working directory: a nested one stays a Python run.
let nested = get(&r, "Nested tests");
assert_eq!((nested.kind.as_str(), nested.args.clone()), ("python", s(&["backend/manage.py", "test"])));
}
#[test]
fn existing_names_match_without_surrounding_spaces() {
let dir = project(
r#"{ "configurations": [{ "name": " Django ", "type": "debugpy", "request": "launch", "program": "manage.py" }] }"#,
);
let existing = RunConfig {
id: "user:django".into(),
name: "Django ".into(),
kind: "python".into(),
command: "python3".into(),
args: s(&["manage.py"]),
cwd: None,
env: BTreeMap::new(),
detected: false,
is_test: false,
debug_adapter: None,
allow_parallel: false,
extras: Default::default(),
};
let r = import_from(dir.path(), &[existing]);
assert_eq!(reason(&r, "Django"), &SkipReason::NameTaken);
}