Files
galaxy/app/src/notebooks/link_tests.rs
T

401 lines
13 KiB
Rust

use std::{
io::ErrorKind,
path::{Path, PathBuf},
sync::Arc,
};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use tempfile::tempdir;
use url::Url;
use warp_util::path::LineAndColumnArg;
use warpui::{App, ModelHandle, WindowId};
use crate::{
notebooks::{file::is_markdown_file, link::LinkEvent},
terminal::{model::session::Session, shell::ShellType},
util::openable_file_type::FileTarget,
workspace::ActiveSession,
};
use super::{LinkTarget, NotebookLinks, ResolveError, SessionSource};
fn url(s: &str) -> LinkTarget {
LinkTarget::Url(Url::parse(s).expect("Invalid URL"))
}
fn local_directory(path: impl Into<PathBuf>) -> LinkTarget {
LinkTarget::LocalDirectory { path: path.into() }
}
fn local_file(path: impl Into<PathBuf>) -> LinkTarget {
let path = path.into();
LinkTarget::LocalFile {
is_markdown: is_markdown_file(&path),
path,
line_and_column: None,
session: TEST_SESSION.clone(),
}
}
fn local_file_location(path: impl Into<PathBuf>, line: usize, column: Option<usize>) -> LinkTarget {
let path = path.into();
LinkTarget::LocalFile {
is_markdown: is_markdown_file(&path),
path,
line_and_column: Some(LineAndColumnArg {
line_num: line,
column_num: column,
}),
session: TEST_SESSION.clone(),
}
}
lazy_static! {
// ActiveSession holds a weak reference to the session, so we need this strong one to keep it
// alive.
static ref TEST_SESSION: Arc<Session> = Arc::new(Session::test().with_shell_launch_data(crate::terminal::ShellLaunchData::Executable { executable_path: PathBuf::from("/bin/bash"), shell_type: ShellType::Bash }));
}
/// Initialize the app and link resolver. For test purposes, we only care about the base
/// directory's value, not how it was obtained.
fn init_link_model(app: &mut App, base_directory: Option<&Path>) -> ModelHandle<NotebookLinks> {
let window_id = WindowId::new();
let source = match base_directory {
Some(dir) => SessionSource::Target {
session: TEST_SESSION.clone(),
base_directory: dir.to_owned(),
},
// File links can't be resolved without a session, even if there's no working directory.
None => SessionSource::Active(window_id),
};
app.add_singleton_model(|ctx| {
let mut session = ActiveSession::default();
session.set_session_for_test(window_id, TEST_SESSION.clone(), base_directory, None, ctx);
session
});
app.add_model(|ctx| NotebookLinks::new(source, ctx))
}
async fn resolve(app: &App, links: &ModelHandle<NotebookLinks>, link: &str) -> LinkTarget {
match links.read(app, |links, ctx| links.resolve(link, ctx)).await {
Ok(target) => target,
Err(err) => panic!("Error resolving {link}: {err}"),
}
}
/// Ensure a file exists, creating its parents if necessary.
async fn touch(path: impl AsRef<Path>) {
let path = path.as_ref();
if let Some(parent) = path.parent() {
if let Err(err) = async_fs::create_dir_all(parent).await {
if err.kind() != ErrorKind::AlreadyExists {
panic!("Creating parent {} failed: {}", parent.display(), err);
}
}
}
async_fs::File::create(path)
.await
.expect("Creating test file failed")
.sync_all()
.await
.expect("Syncing test file failed");
}
fn next_link_event(events: &Arc<Mutex<Vec<LinkEvent>>>) -> LinkEvent {
events.lock().remove(0)
}
#[test]
fn test_resolve_bare_url() {
App::test((), |mut app| async move {
let base = tempdir().unwrap();
let base_path = base.path();
touch(base_path.join("nodot/slash")).await;
touch(base_path.join(".vscode/settings.json")).await;
touch(base_path.join("myfile.swift")).await;
touch(base_path.join("license.txt")).await;
touch(base_path.join("app/src/main.rs")).await;
let links = init_link_model(&mut app, Some(base_path));
assert_eq!(
resolve(&app, &links, "example.com/some-path").await,
url("http://example.com/some-path")
);
// These should not be considered URLs.
assert_eq!(
resolve(&app, &links, "nodot/slash").await,
local_file(base_path.join("nodot/slash"))
);
assert_eq!(
resolve(&app, &links, ".vscode/settings.json").await,
local_file(base_path.join(".vscode/settings.json"))
);
// These rely on domain name validation.
assert_eq!(
resolve(&app, &links, "google.com").await,
url("http://google.com")
);
assert_eq!(
resolve(&app, &links, "warp.dev").await,
url("http://warp.dev")
);
assert_eq!(
resolve(&app, &links, "bbc.co.uk").await,
url("http://bbc.co.uk")
);
assert_eq!(
resolve(&app, &links, "192.168.0.1/admin").await,
url("http://192.168.0.1/admin")
);
assert_eq!(
resolve(&app, &links, "myfile.swift").await,
local_file(base_path.join("myfile.swift"))
);
assert_eq!(
resolve(&app, &links, "license.txt").await,
local_file(base_path.join("license.txt"))
);
// `app` is a valid TLD, so this tests that we need both a TLD and a root domain to link as an
// URL.
assert_eq!(
resolve(&app, &links, "app/src/main.rs").await,
local_file(base_path.join("app/src/main.rs"))
);
});
}
#[test]
fn test_open_local_image_uses_system_generic_target() {
App::test((), |mut app| async move {
let base = tempdir().unwrap();
let base_path = base.path();
let image_path = base_path.join("images/example.png");
touch(&image_path).await;
let links = init_link_model(&mut app, Some(base_path));
let events = Arc::new(Mutex::new(vec![]));
{
let events = events.clone();
app.update(|ctx| {
ctx.subscribe_to_model(&links, move |_, event, _| {
events.lock().push(event.clone());
})
});
}
links.update(&mut app, |links, ctx| {
links.open(local_file(&image_path), ctx);
});
match next_link_event(&events) {
LinkEvent::OpenFileWithTarget {
path,
target,
line_col,
} => {
assert_eq!(path, image_path);
assert_eq!(target, FileTarget::SystemGeneric);
assert_eq!(line_col, None);
}
other => panic!("Expected OpenFileWithTarget event, got {other:?}"),
}
});
}
#[test]
fn test_resolve_valid_url() {
App::test((), |mut app| async move {
let links = init_link_model(&mut app, None);
assert_eq!(
resolve(&app, &links, "https://warp.dev").await,
url("https://warp.dev")
);
assert_eq!(
resolve(&app, &links, "mailto:test@warp.dev").await,
url("mailto:test@warp.dev")
);
});
}
#[cfg_attr(windows, ignore = "TODO(CORE-3626)")]
#[test]
fn test_resolve_file_url() {
App::test((), |mut app| async move {
let base = tempdir().unwrap();
let base_path = base.path();
let test_file = base_path.join("some/path.txt");
touch(&test_file).await;
let links = init_link_model(&mut app, Some(base_path));
assert_eq!(
resolve(&app, &links, &format!("file://{}", test_file.display())).await,
local_file(&test_file)
);
assert_eq!(
resolve(
&app,
&links,
&format!("file://localhost{}", test_file.display())
)
.await,
local_file(&test_file)
);
// file:// URLs can have non-local hosts on Windows. If we encounter one, it should be kept a
// URL for the system to handle.
assert_eq!(
resolve(&app, &links, "file://remote/some/path.txt").await,
url("file://remote/some/path.txt")
);
});
}
#[test]
fn test_resolve_relative_file_no_base() {
App::test((), |mut app| async move {
let links = init_link_model(&mut app, None);
let absolute_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("Cargo.toml")
.canonicalize()
.expect("Path exists");
assert_eq!(
resolve(&app, &links, absolute_path.to_str().unwrap()).await,
local_file(absolute_path)
);
let absolute_directory = Path::new(env!("CARGO_MANIFEST_DIR"))
.canonicalize()
.expect("Path exists");
assert_eq!(
resolve(&app, &links, absolute_directory.to_str().unwrap()).await,
local_directory(absolute_directory)
);
assert_eq!(
links
.read(&app, |links, ctx| links.resolve("relative/path.txt", ctx))
.await,
Err(ResolveError::MissingContext)
);
});
}
#[test]
fn test_resolve_relative_file_base() {
// This absolute path is specifically not within the base directory.
let absolute_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("Cargo.toml")
.canonicalize()
.expect("Path exists");
App::test((), |mut app| async move {
let base = tempdir().unwrap();
let base_path = base.path();
touch(base_path.join("relative/path.txt")).await;
touch(base_path.join("dotted.txt")).await;
let links = init_link_model(&mut app, Some(base_path));
assert_eq!(
resolve(&app, &links, absolute_path.to_str().unwrap()).await,
local_file(&absolute_path)
);
assert_eq!(
resolve(&app, &links, "relative/path.txt").await,
local_file(base_path.join("relative/path.txt"))
);
assert_eq!(
resolve(&app, &links, "./dotted.txt").await,
local_file(base_path.join("dotted.txt"))
);
assert_eq!(
resolve(&app, &links, "./relative/../dotted.txt").await,
local_file(base_path.join("relative/../dotted.txt"))
);
assert_eq!(
resolve(&app, &links, "./relative").await,
local_directory(base_path.join("relative"))
);
assert_eq!(
links
.read(&app, |links, ctx| links.resolve("missing.txt", ctx))
.await,
Err(ResolveError::FileNotFound)
);
});
}
#[test]
fn test_resolve_file_with_line() {
App::test((), |mut app| async move {
let base = tempdir().unwrap();
let base_path = base.path();
touch(base_path.join("src/main.rs")).await;
touch(base_path.join("path/to/index.html")).await;
let links = init_link_model(&mut app, Some(base_path));
assert_eq!(
resolve(&app, &links, "./src/main.rs:123").await,
local_file_location(base_path.join("src/main.rs"), 123, None)
);
assert_eq!(
resolve(&app, &links, "path/to/index.html:99:51").await,
local_file_location(base_path.join("path/to/index.html"), 99, Some(51))
);
});
}
#[test]
fn test_open_markdown_file() {
let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
if !root.join("README.md").exists() {
root = root.parent().unwrap().to_path_buf();
}
App::test((), |mut app| async move {
let links = init_link_model(&mut app, Some(&root));
let events = Arc::new(Mutex::new(vec![]));
{
let events = events.clone();
app.update(|ctx| {
ctx.subscribe_to_model(&links, move |_, event, _| {
events.lock().push(event.clone());
})
});
}
links
.update(&mut app, |links, ctx| {
// The `./` in the link is important: `.md` is the TLD for Moldova, so this will be
// resolved as a web link otherwise.
let future = links.resolve_and_open("./README.md", ctx);
ctx.await_spawned_future(future.future_id())
})
.await;
let events = events.lock();
assert_eq!(events.len(), 1);
match events.first() {
Some(LinkEvent::OpenFileNotebook { path, session }) => {
assert_eq!(path, &root.join("README.md"));
assert!(Arc::ptr_eq(&TEST_SESSION, session));
}
other => panic!("Expected OpenFileNotebook event, got {other:?}"),
}
});
}