embers-server 0.1.0

Headless session, layout, persistence, and PTY runtime server for Embers.
use embers_core::{ActivityState, MuxError, PtySize};
use embers_server::{BufferAttachment, BufferState, ServerState};

#[test]
fn running_buffers_transition_to_exited_and_track_metadata() {
    let mut state = ServerState::new();
    let buffer_id = state.create_buffer("shell", vec!["/bin/sh".to_owned()], None);

    state
        .mark_buffer_running(buffer_id, Some(42))
        .expect("mark running");
    let sequence = state.note_buffer_output(buffer_id).expect("note output");
    assert_eq!(sequence, 1);
    state
        .set_buffer_size(buffer_id, PtySize::new(120, 40))
        .expect("resize buffer");
    state
        .mark_buffer_exited(buffer_id, Some(0))
        .expect("mark exited");

    let buffer = state.buffer(buffer_id).expect("buffer exists");
    assert_eq!(buffer.pty_size, PtySize::new(120, 40));
    assert_eq!(buffer.activity, ActivityState::Activity);
    assert_eq!(buffer.last_snapshot_seq, 1);
    assert!(matches!(
        buffer.state,
        BufferState::Exited(ref exited) if exited.exit_code == Some(0)
    ));
}

#[test]
fn single_attachment_requires_detach_before_reattach() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");
    let first_buffer = state.create_buffer("first", vec!["first".to_owned()], None);
    let second_buffer = state.create_buffer("second", vec!["second".to_owned()], None);
    let first_view = state
        .create_buffer_view(session_id, first_buffer)
        .expect("create first view");
    let second_view = state
        .create_buffer_view(session_id, second_buffer)
        .expect("create second view");
    state
        .add_root_tab(session_id, "first", first_view)
        .expect("attach first view to root");
    state
        .add_root_tab(session_id, "second", second_view)
        .expect("attach second view to root");

    let error = state
        .attach_buffer(first_buffer, second_view)
        .expect_err("reattach without detach should fail");
    assert!(matches!(error, MuxError::Conflict(_)));

    state.close_node(first_view).expect("close original view");
    state
        .attach_buffer(first_buffer, second_view)
        .expect("reattach detached buffer");

    assert!(matches!(
        state.buffer(first_buffer).expect("first buffer").attachment,
        BufferAttachment::Attached(node_id) if node_id == second_view
    ));
    assert!(matches!(
        state
            .buffer(second_buffer)
            .expect("second buffer")
            .attachment,
        BufferAttachment::Detached
    ));
    assert!(matches!(state.node(first_view), Err(MuxError::NotFound(_))));
    assert_eq!(
        state
            .node(second_view)
            .expect("second view still exists")
            .as_buffer_view()
            .expect("second node is a buffer view")
            .buffer_id,
        first_buffer
    );
    state.validate().expect("state stays valid after reattach");
}

#[test]
fn closing_a_view_detaches_but_preserves_running_buffer() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");
    let buffer_id = state.create_buffer("shell", vec!["/bin/sh".to_owned()], None);
    state
        .mark_buffer_running(buffer_id, Some(7))
        .expect("mark running");
    let view_id = state
        .create_buffer_view(session_id, buffer_id)
        .expect("create buffer view");
    state
        .add_root_tab(session_id, "shell", view_id)
        .expect("attach view to root tabs");

    state.close_node(view_id).expect("close buffer view");

    let buffer = state.buffer(buffer_id).expect("buffer still exists");
    assert!(matches!(buffer.attachment, BufferAttachment::Detached));
    assert!(matches!(
        buffer.state,
        BufferState::Running(ref running) if running.pid == Some(7)
    ));
}

#[test]
fn focusing_a_leaf_clears_recorded_activity() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");
    let first_buffer = state.create_buffer("first", vec!["/bin/sh".to_owned()], None);
    let first_view = state
        .create_buffer_view(session_id, first_buffer)
        .expect("create first view");
    state
        .add_root_tab(session_id, "first", first_view)
        .expect("attach first view");

    let second_buffer = state.create_buffer("second", vec!["/bin/sh".to_owned()], None);
    let second_view = state
        .create_buffer_view(session_id, second_buffer)
        .expect("create second view");
    state
        .add_root_tab(session_id, "second", second_view)
        .expect("attach second view");
    state
        .focus_leaf(session_id, second_view)
        .expect("focus second leaf");

    state
        .set_buffer_activity(first_buffer, ActivityState::Bell)
        .expect("mark first buffer active");

    state
        .focus_leaf(session_id, first_view)
        .expect("focus hidden first leaf");

    assert_eq!(
        state
            .buffer(first_buffer)
            .expect("first buffer exists")
            .activity,
        ActivityState::Idle
    );
}

#[test]
fn resize_updates_buffer_size_for_attached_and_detached_buffers() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");
    let buffer_id = state.create_buffer("shell", vec!["/bin/sh".to_owned()], None);
    let view_id = state
        .create_buffer_view(session_id, buffer_id)
        .expect("create buffer view");
    state
        .add_root_tab(session_id, "shell", view_id)
        .expect("attach view to root tabs");

    state
        .set_buffer_size(buffer_id, PtySize::new(100, 30))
        .expect("resize attached buffer");
    assert_eq!(
        state.buffer(buffer_id).expect("buffer exists").pty_size,
        PtySize::new(100, 30)
    );

    state.close_node(view_id).expect("close buffer view");
    state
        .set_buffer_size(buffer_id, PtySize::new(90, 20))
        .expect("resize detached buffer");
    assert_eq!(
        state.buffer(buffer_id).expect("buffer exists").pty_size,
        PtySize::new(90, 20)
    );
}

#[test]
fn exited_detached_buffers_can_be_removed_cleanly() {
    let mut state = ServerState::new();
    let buffer_id = state.create_buffer("shell", vec!["/bin/sh".to_owned()], None);

    state
        .mark_buffer_running(buffer_id, Some(88))
        .expect("mark running");
    state
        .mark_buffer_exited(buffer_id, Some(0))
        .expect("mark exited");

    let removed = state
        .remove_buffer(buffer_id)
        .expect("remove detached exited buffer");
    assert!(matches!(
        removed.state,
        BufferState::Exited(ref exited) if exited.exit_code == Some(0)
    ));
    assert!(matches!(
        state.buffer(buffer_id),
        Err(MuxError::NotFound(_))
    ));
}