absolutely slayed testing with iced task

[?]
May 19, 2025, 6:55 AM
WGID4LS4EISIOXB5Y5SOFGEF5PLBJSCPFCETH2CGRTFN3NC4WGJQC

Dependencies

  • [2] 6YZAVBWU Initial commit
  • [3] KLR5FRIB add fs state read/write of repos
  • [4] WT3GA27P add cursor with selection
  • [5] UB2ITZJS refresh changed files on FS changes
  • [6] YBJRDOTC make all repo actions async
  • [7] 2VUX5BTD load identity
  • [8] A5YBC77V record!
  • [9] Z2CJPWZE focus record message text_editor on spawn
  • [10] AMPZ2BXK show changed files diffs (only Edit atm)
  • [11] V55EAIWQ add src file LRU cache
  • [12] 6SW7UVSH update iced version
  • [13] B4RMW5AE add syntax highlighter to untracked files contents
  • [14] ZVI4AWER woot contents_diff
  • [15] OQ6HSAWH show record log
  • [16] NWJD6VM6 mv libflowers libflorescence
  • [17] 4ELJZGRJ load and store all change diffs at once
  • [18] HC7ROIBC move main diffs state out of cursor
  • [19] CALXOZXA flatten crates dir
  • [20] L6KSEFQI move cursor related stuff into its module
  • [21] BFN2VHZS refactor file stuff into sub-mod
  • [22] 3SYSJKYL add app icon
  • [23] 23SFYK4Q big view refactor into a new crate
  • [24] MYGIBRRH wip custom theme
  • [25] IQDCHWCP load a pijul repo
  • [26] SWWE2R6M display basic repo stuff
  • [27] D7A7MSIH allow to defer or abandon record, add buttons
  • [28] HOJZI52Y rename flowers_ui to inflorescence
  • [29] QMAUTRB6 refactor diff
  • [30] XSZZB47U refactor stuff into lib
  • [31] JE44NYHM display log files diffs
  • [32] AHWWRC73 navigate log entries

Change contents

  • file addition: task.rs (----------)
    [19.364]
    //! A drop-in replacement for [`iced::Task`] that allows for easy evaluation for
    //! testing.
    mod wrappers;
    pub use wrappers::*;
    #[cfg(not(test))]
    pub use iced::Task;
    #[cfg(test)]
    pub struct Task<T> {
    stream: Option<iced::futures::stream::BoxStream<'static, T>>,
    }
    #[cfg(test)]
    pub async fn await_next_result<T>(tasks: &mut Task<T>) -> Option<T> {
    let Task { stream } = tasks;
    if let Some(stream) = stream.as_mut() {
    if let Some(msg) = iced::futures::stream::StreamExt::next(stream).await
    {
    return Some(msg);
    }
    }
    None
    }
    #[cfg(test)]
    impl<T> Task<T> {
    pub fn none() -> Self {
    Self { stream: None }
    }
    pub fn done(value: T) -> Task<T>
    where
    T: Send + 'static,
    {
    Self::future(iced::futures::future::ready(value))
    }
    pub fn perform<A>(
    future: impl Future<Output = A> + Send + 'static,
    f: impl FnOnce(A) -> T + Send + 'static,
    ) -> Self
    where
    T: Send + 'static,
    A: Send + 'static,
    {
    Self::future(iced::futures::FutureExt::map(future, f))
    }
    pub fn run<A>(
    stream: impl iced::futures::stream::Stream<Item = A> + Send + 'static,
    f: impl Fn(A) -> T + Send + 'static,
    ) -> Self
    where
    T: 'static,
    {
    Self::stream(iced::futures::stream::StreamExt::map(stream, f))
    }
    pub fn batch(tasks: impl IntoIterator<Item = Self>) -> Self
    where
    T: 'static,
    {
    let mut select_all = iced::futures::stream::SelectAll::new();
    for task in tasks.into_iter() {
    if let Some(stream) = task.stream {
    select_all.push(stream);
    }
    }
    Self {
    stream: Some(iced::futures::stream::StreamExt::boxed(select_all)),
    }
    }
    pub fn map<O>(self, mut f: impl FnMut(T) -> O + Send + 'static) -> Task<O>
    where
    T: Send + 'static,
    O: Send + 'static,
    {
    self.then(move |output| Task::done(f(output)))
    }
    pub fn then<O>(
    self,
    mut f: impl FnMut(T) -> Task<O> + Send + 'static,
    ) -> Task<O>
    where
    T: Send + 'static,
    O: Send + 'static,
    {
    Task {
    stream: match self.stream {
    None => None,
    Some(stream) => Some(iced::futures::stream::StreamExt::boxed(
    iced::futures::stream::StreamExt::flat_map(
    stream,
    move |output| {
    f(output).stream.unwrap_or_else(|| {
    iced::futures::stream::StreamExt::boxed(
    iced::futures::stream::empty(),
    )
    })
    },
    ),
    )),
    },
    }
    }
    pub fn future(future: impl Future<Output = T> + Send + 'static) -> Self
    where
    T: 'static,
    {
    Self::stream(iced::futures::stream::once(future))
    }
    pub fn stream(
    stream: impl iced::futures::stream::Stream<Item = T> + Send + 'static,
    ) -> Self
    where
    T: 'static,
    {
    Self {
    stream: Some(iced::futures::stream::StreamExt::boxed(stream)),
    }
    }
    }
    #[cfg(test)]
    mod test {
    use super::*;
    #[tokio::test]
    async fn test_run_task() {
    let mut task = Task::done(123_usize);
    let result = await_next_result(&mut task).await;
    assert_eq!(result, Some(123_usize))
    }
    }
  • file addition: task (d--r------)
    [19.364]
  • file addition: wrappers.rs (----------)
    [0.3625]
    //! Wrappers for iced tasks to allow testing.
    use std::path::PathBuf;
    use super::Task;
    use iced::window;
    /// Opens a new window with the given [`Settings`]; producing the [`Id`]
    /// of the new window on completion.
    pub fn open_window(
    settings: window::Settings,
    ) -> (window::Id, Task<window::Id>) {
    #[cfg(not(test))]
    let (window_id, task) = window::open(settings);
    #[cfg(test)]
    let (window_id, task) = {
    let _ = settings;
    let id = window::Id::unique();
    (id, Task::done(id))
    };
    (window_id, task)
    }
    /// Changes the icon of the window using an icon from the content of an image
    /// file at the given path.
    pub fn window_set_icon<T>(window_id: window::Id, path: PathBuf) -> Task<T>
    where
    T: Send + 'static,
    {
    #[cfg(not(test))]
    let set_icon_task = Task::perform(
    async move {
    use iced::advanced::graphics::image::image_rs::ImageFormat;
    let icon_bytes = tokio::fs::read(path).await.unwrap();
    window::icon::from_file_data(&icon_bytes, Some(ImageFormat::Png))
    .unwrap()
    },
    |msg| msg,
    )
    .then(move |icon| window::set_icon(window_id, icon));
    #[cfg(test)]
    let set_icon_task = {
    let _ = (window_id, path);
    Task::none()
    };
    set_icon_task
    }
    /// Focuses the next focusable widget.
    pub fn widget_focus_next<T>() -> Task<T> {
    #[cfg(not(test))]
    let task = iced::widget::focus_next();
    #[cfg(test)]
    let task = Task::none();
    task
    }
  • edit in inflorescence/src/main.rs at line 1
    [2.2763]
    [10.1207]
    pub mod task;
  • replacement in inflorescence/src/main.rs at line 7
    [10.1220][13.26:41](),[13.41][21.28:42](),[21.42][18.18:49](),[13.41][18.18:49](),[13.96][13.96:140]()
    use core::str;
    use std::cmp;
    use std::collections::HashMap;
    use std::path::PathBuf;
    use std::sync::Arc;
    [10.1220]
    [13.161]
    use task::Task;
  • replacement in inflorescence/src/main.rs at line 10
    [23.25389][24.8909:8954]()
    use inflorescence_view::{app, theme, Theme};
    [23.25389]
    [16.365]
    use inflorescence_view::{app, Theme};
  • replacement in inflorescence/src/main.rs at line 16
    [23.25459][23.25459:25516]()
    use iced::{widget, window, Element, Subscription, Task};
    [23.25459]
    [8.4438]
    use iced::{window, Element, Subscription};
  • edit in inflorescence/src/main.rs at line 21
    [15.4563][22.109:124]()
    use tokio::fs;
  • edit in inflorescence/src/main.rs at line 25
    [6.4810]
    [11.174]
    use std::cmp;
    use std::collections::HashMap;
    use std::path::PathBuf;
    use std::sync::Arc;
  • edit in inflorescence/src/main.rs at line 31
    [11.175]
    [2.2835]
    #[cfg(not(test))]
  • edit in inflorescence/src/main.rs at line 33
    [2.2867]
    [6.4811]
    use core::str;
  • edit in inflorescence/src/main.rs at line 36
    [6.4877]
    [6.4877]
  • replacement in inflorescence/src/main.rs at line 51
    [4.537][2.2922:2944](),[3.5905][2.2922:2944](),[2.2922][2.2922:2944]()
    .theme(theme)
    [4.537]
    [15.4564]
    .theme(inflorescence_view::theme)
  • replacement in inflorescence/src/main.rs at line 58
    [22.203][22.203:653]()
    window::open(window::Settings::default());
    let set_icon_task = Task::perform(
    async {
    use iced::advanced::graphics::image::image_rs::ImageFormat;
    let icon_bytes = fs::read("assets/icon.png").await.unwrap();
    window::icon::from_file_data(&icon_bytes, Some(ImageFormat::Png))
    .unwrap()
    },
    |msg| msg,
    )
    .then(move |icon| window::set_icon(window_id, icon));
    [22.203]
    [22.653]
    task::open_window(window::Settings::default());
    let set_icon_task =
    // TODO: `include_bytes!`?
    task::window_set_icon(window_id, PathBuf::from("assets/icon.png"));
  • replacement in inflorescence/src/main.rs at line 256
    [9.217][9.217:266]()
    return widget::focus_next();
    [9.217]
    [6.14353]
    return task::widget_focus_next();
  • edit in inflorescence/src/file.rs at line 7
    [21.1887]
    [21.1887]
    use crate::task::Task;
  • edit in inflorescence/src/file.rs at line 16
    [21.2044][21.2044:2060]()
    use iced::Task;
  • edit in inflorescence/src/cursor.rs at line 6
    [23.29310]
    [21.8494]
    use crate::task::Task;
  • edit in inflorescence/src/cursor.rs at line 10
    [17.11959][20.1988:2004](),[14.19114][14.19114:19115]()
    use iced::Task;
  • edit in inflorescence/Cargo.toml at line 36
    [6.18435]
    [dev-dependencies]
    [dev-dependencies.tokio]
    workspace = true
    features = ["macros"]
  • edit in Cargo.lock at line 5560
    [12.21799]
    [7.25173]
    "tokio-macros",
  • edit in Cargo.lock at line 5562
    [7.25196]
    [5.6236]
    ]
    [[package]]
    name = "tokio-macros"
    version = "2.5.0"
    source = "registry+https://github.com/rust-lang/crates.io-index"
    checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
    dependencies = [
    "proc-macro2",
    "quote",
    "syn 2.0.100",