use std::{ io::{Read, Seek, Write}, ops::RangeInclusive, }; use helix_core::diagnostic::Severity; use helix_stdx::path; use helix_view::doc; use super::*; #[tokio::test(flavor = "multi_thread")] async fn test_write_quit_fail() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; test_key_sequence( &mut app, Some("ihello:wq"), Some(&|app| { let mut docs: Vec<_> = app.editor.documents().collect(); assert_eq!(1, docs.len()); let doc = docs.pop().unwrap(); assert_eq!(Some(&path::normalize(file.path())), doc.path()); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), false, ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_buffer_close_concurrent() -> anyhow::Result<()> { test_key_sequences( &mut helpers::AppBuilder::new().build()?, vec![ ( None, Some(&|app| { assert_eq!(1, app.editor.documents().count()); assert!(!app.editor.is_err()); }), ), ( Some("ihello:new"), Some(&|app| { assert_eq!(2, app.editor.documents().count()); assert!(!app.editor.is_err()); }), ), ( Some(":bufferclose"), Some(&|app| { assert_eq!(1, app.editor.documents().count()); assert!(!app.editor.is_err()); }), ), ], false, ) .await?; // verify if writes are queued up, it finishes them before closing the buffer let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); const RANGE: RangeInclusive = 1..=1000; for i in RANGE { let cmd = format!("%c{}:w!", i); command.push_str(&cmd); } command.push_str(":bufferclose"); let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; test_key_sequence( &mut app, Some(&command), Some(&|app| { assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); let doc = app.editor.document_by_path(file.path()); assert!(doc.is_none(), "found doc: {:?}", doc); }), false, ) .await?; helpers::assert_file_has_content( &mut file, &LineFeedHandling::Native.apply(&RANGE.end().to_string()), )?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; test_key_sequence( &mut app, Some("ithe gostak distims the doshes:w"), None, false, ) .await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_overwrite_protection() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; helpers::run_event_loop_until_idle(&mut app).await; file.as_file_mut() .write_all("extremely important content".as_bytes())?; file.as_file_mut().flush()?; file.as_file_mut().sync_all()?; test_key_sequence(&mut app, Some(":x"), None, false).await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.read_to_string(&mut file_content)?; assert_eq!("extremely important content", file_content); Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_quit() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; test_key_sequence( &mut app, Some("ithe gostak distims the doshes:wq"), None, true, ) .await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.read_to_string(&mut file_content)?; assert_eq!( LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_concurrent() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); const RANGE: RangeInclusive = 1..=1000; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; for i in RANGE { let cmd = format!("%c{}:w!", i); command.push_str(&cmd); } test_key_sequence(&mut app, Some(&command), None, false).await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.read_to_string(&mut file_content)?; assert_eq!( LineFeedHandling::Native.apply(&RANGE.end().to_string()), file_content ); Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; test_key_sequences( &mut app, vec![ ( None, Some(&|app| { let doc = doc!(app.editor); assert!(!doc.is_modified()); }), ), ( Some("ihello"), Some(&|app| { let doc = doc!(app.editor); assert!(doc.is_modified()); }), ), ( Some(":w"), Some(&|app| { assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); let doc = doc!(app.editor); assert!(doc.is_modified()); }), ), ], false, ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_scratch_to_new_path() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; test_key_sequence( &mut AppBuilder::new().build()?, Some(format!("ihello:w {}", file.path().to_string_lossy()).as_ref()), Some(&|app| { assert!(!app.editor.is_err()); let mut docs: Vec<_> = app.editor.documents().collect(); assert_eq!(1, docs.len()); let doc = docs.pop().unwrap(); assert_eq!(Some(&path::normalize(file.path())), doc.path()); }), false, ) .await?; helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("hello"))?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_scratch_no_path_fails() -> anyhow::Result<()> { helpers::test_key_sequence_with_input_text( None, ("#[\n|]#", "ihello:w", "hello#[\n|]#"), &|app| { assert!(app.editor.is_err()); let mut docs: Vec<_> = app.editor.documents().collect(); assert_eq!(1, docs.len()); let doc = docs.pop().unwrap(); assert_eq!(None, doc.path()); }, false, ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> { let mut file = tempfile::Builder::new().suffix(".rs").tempfile()?; let lang_conf = indoc! {r#" [[language]] name = "rust" formatter = { command = "bash", args = [ "-c", "exit 1" ] } "#}; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .with_input_text("#[l|]#et foo = 0;\n") .with_lang_loader(helpers::test_syntax_loader(Some(lang_conf.into()))) .build()?; test_key_sequences(&mut app, vec![(Some(":w"), None)], false).await?; // file still saves helpers::assert_file_has_content(&mut file, "let foo = 0;\n")?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_new_path() -> anyhow::Result<()> { let mut file1 = tempfile::NamedTempFile::new().unwrap(); let mut file2 = tempfile::NamedTempFile::new().unwrap(); let mut app = helpers::AppBuilder::new() .with_file(file1.path(), None) .build()?; test_key_sequences( &mut app, vec![ ( Some("ii can eat glass, it will not hurt me:w"), Some(&|app| { let doc = doc!(app.editor); assert!(!app.editor.is_err()); assert_eq!(&path::normalize(file1.path()), doc.path().unwrap()); }), ), ( Some(&format!(":w {}", file2.path().to_string_lossy())), Some(&|app| { let doc = doc!(app.editor); assert!(!app.editor.is_err()); assert_eq!(&path::normalize(file2.path()), doc.path().unwrap()); assert!(app.editor.document_by_path(file1.path()).is_none()); }), ), ], false, ) .await?; helpers::assert_file_has_content( &mut file1, &LineFeedHandling::Native.apply("i can eat glass, it will not hurt me\n"), )?; helpers::assert_file_has_content( &mut file2, &LineFeedHandling::Native.apply("i can eat glass, it will not hurt me\n"), )?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_fail_new_path() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; test_key_sequences( &mut AppBuilder::new().build()?, vec![ ( None, Some(&|app| { let doc = doc!(app.editor); assert_ne!( Some(&Severity::Error), app.editor.get_status().map(|status| status.1) ); assert_eq!(None, doc.path()); }), ), ( Some(&format!(":w {}", file.path().to_string_lossy())), Some(&|app| { let doc = doc!(app.editor); assert_eq!( Some(&Severity::Error), app.editor.get_status().map(|status| status.1) ); assert_eq!(None, doc.path()); }), ), ], false, ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_utf_bom_file() -> anyhow::Result<()> { // "ABC" with utf8 bom const UTF8_FILE: [u8; 6] = [0xef, 0xbb, 0xbf, b'A', b'B', b'C']; // "ABC" in UTF16 with bom const UTF16LE_FILE: [u8; 8] = [0xff, 0xfe, b'A', 0x00, b'B', 0x00, b'C', 0x00]; const UTF16BE_FILE: [u8; 8] = [0xfe, 0xff, 0x00, b'A', 0x00, b'B', 0x00, b'C']; edit_file_with_content(&UTF8_FILE).await?; edit_file_with_content(&UTF16LE_FILE).await?; edit_file_with_content(&UTF16BE_FILE).await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_insert_final_newline_added_if_missing() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .with_input_text("#[h|]#ave you tried chamomile tea?") .build()?; test_key_sequence(&mut app, Some(":w"), None, false).await?; helpers::assert_file_has_content( &mut file, &LineFeedHandling::Native.apply("have you tried chamomile tea?\n"), )?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_insert_final_newline_unchanged_if_not_missing() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .with_input_text(LineFeedHandling::Native.apply("#[t|]#en minutes, please\n")) .build()?; test_key_sequence(&mut app, Some(":w"), None, false).await?; helpers::assert_file_has_content( &mut file, &LineFeedHandling::Native.apply("ten minutes, please\n"), )?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_insert_final_newline_unchanged_if_missing_and_false() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_config(Config { editor: helix_view::editor::Config { insert_final_newline: false, ..Default::default() }, ..Default::default() }) .with_file(file.path(), None) .with_input_text("#[t|]#he quiet rain continued through the night") .build()?; test_key_sequence(&mut app, Some(":w"), None, false).await?; reload_file(&mut file).unwrap(); helpers::assert_file_has_content(&mut file, "the quiet rain continued through the night")?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_all_insert_final_newline_add_if_missing_and_modified() -> anyhow::Result<()> { let mut file1 = tempfile::NamedTempFile::new()?; let mut file2 = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file1.path(), None) .with_input_text("#[w|]#e don't serve time travelers here") .build()?; test_key_sequence( &mut app, Some(&format!( ":o {}ia time traveler walks into a bar:wa", file2.path().to_string_lossy() )), None, false, ) .await?; helpers::assert_file_has_content( &mut file1, &LineFeedHandling::Native.apply("we don't serve time travelers here\n"), )?; helpers::assert_file_has_content( &mut file2, &LineFeedHandling::Native.apply("a time traveler walks into a bar\n"), )?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_write_all_insert_final_newline_do_not_add_if_unmodified() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; file.write_all(b"i lost on Jeopardy!")?; file.rewind()?; test_key_sequence(&mut app, Some(":wa"), None, false).await?; helpers::assert_file_has_content(&mut file, "i lost on Jeopardy!")?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_symlink_write() -> anyhow::Result<()> { #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(not(unix))] use std::os::windows::fs::symlink_file as symlink; let dir = tempfile::tempdir()?; let mut file = tempfile::NamedTempFile::new_in(&dir)?; // NOTE: This will fail on Windows unless ran in administrator let symlink_path = dir.path().join("linked"); symlink(file.path(), &symlink_path)?; let mut app = helpers::AppBuilder::new() .with_file(&symlink_path, None) .build()?; test_key_sequence( &mut app, Some("ithe gostak distims the doshes:w"), None, false, ) .await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); assert!(symlink_path.is_symlink()); Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_symlink_write_fail() -> anyhow::Result<()> { #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(not(unix))] use std::os::windows::fs::symlink_file as symlink; let dir = tempfile::tempdir()?; let file = helpers::new_readonly_tempfile_in_dir(&dir)?; let symlink_path = dir.path().join("linked"); // NOTE: This will fail on Windows unless ran in administrator symlink(file.path(), &symlink_path)?; let mut app = helpers::AppBuilder::new() .with_file(&symlink_path, None) .build()?; test_key_sequence( &mut app, Some("ihello:wq"), Some(&|app| { let mut docs: Vec<_> = app.editor.documents().collect(); assert_eq!(1, docs.len()); let doc = docs.pop().unwrap(); assert_eq!(Some(&path::normalize(&symlink_path)), doc.path()); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), false, ) .await?; assert!(symlink_path.is_symlink()); Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_symlink_write_relative() -> anyhow::Result<()> { #[cfg(unix)] use std::os::unix::fs::symlink; #[cfg(not(unix))] use std::os::windows::fs::symlink_file as symlink; // tempdir // |- - b // | |- file // |- linked (symlink to file) let dir = tempfile::tempdir()?; let inner_dir = dir.path().join("b"); std::fs::create_dir(&inner_dir)?; let mut file = tempfile::NamedTempFile::new_in(&inner_dir)?; let symlink_path = dir.path().join("linked"); let relative_path = std::path::PathBuf::from("b").join(file.path().file_name().unwrap()); // NOTE: This will fail on Windows unless ran in administrator symlink(relative_path, &symlink_path)?; let mut app = helpers::AppBuilder::new() .with_file(&symlink_path, None) .build()?; test_key_sequence( &mut app, Some("ithe gostak distims the doshes:w"), None, false, ) .await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); assert!(symlink_path.is_symlink()); Ok(()) } #[tokio::test(flavor = "multi_thread")] #[cfg(not(target_os = "android"))] async fn test_hardlink_write() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let mut file = tempfile::NamedTempFile::new_in(&dir)?; let hardlink_path = dir.path().join("linked"); std::fs::hard_link(file.path(), &hardlink_path)?; let mut app = helpers::AppBuilder::new() .with_file(&hardlink_path, None) .build()?; test_key_sequence( &mut app, Some("ithe gostak distims the doshes:w"), None, false, ) .await?; reload_file(&mut file).unwrap(); let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( LineFeedHandling::Native.apply("the gostak distims the doshes"), file_content ); assert!(helix_stdx::faccess::hardlink_count(&hardlink_path)? > 1); assert!(same_file::is_same_file(file.path(), &hardlink_path)?); Ok(()) } #[tokio::test(flavor = "multi_thread")] #[cfg(unix)] async fn test_write_ownership() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut app = helpers::AppBuilder::new() .with_file(file.path(), None) .build()?; let old_meta = file.as_file().metadata()?; test_key_sequence(&mut app, Some("hello:w"), None, false).await?; reload_file(&mut file).unwrap(); let new_meta = file.as_file().metadata()?; assert!(old_meta.uid() == new_meta.uid() && old_meta.gid() == new_meta.gid()); Ok(()) } async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.as_file_mut().write_all(&file_content)?; helpers::test_key_sequence( &mut helpers::AppBuilder::new() .with_config(Config { editor: helix_view::editor::Config { insert_final_newline: false, ..Default::default() }, ..Default::default() }) .build()?, Some(&format!(":o {}:x", file.path().to_string_lossy())), None, true, ) .await?; reload_file(&mut file).unwrap(); let mut new_file_content: Vec = Vec::new(); file.read_to_end(&mut new_file_content)?; assert_eq!(file_content, new_file_content); Ok(()) }