gled-protocol 2.30.1

What gled's user interfaces and its engine send each other
Documentation
//! Every message survives the wire unchanged, including a real project.

use std::{collections::BTreeSet, fs, path::Path, sync::Arc};

use gled_model::{
    asset::{
        Asset,
        project::{Project, scene_instance_path::SceneInstanceUnion},
        scene::grid::GridLocation,
    },
    asset_id::AssetId,
    input::key_code::KeyCode,
    timing::BeatState,
};
use gled_protocol::{
    action::UiAction,
    bytes::SharedBytes,
    codec::{decode, encode},
    message::{ClientMessage, PROTOCOL_VERSION, ServerMessage, Subscriptions},
    project_update::ProjectUpdate,
    state::{
        ClientInput, EngineEvent, EngineState, OutputConfig, RenderStats, ServerSettings,
        ServerStatus, SoundSnapshot, StorageCommand,
    },
};

fn fixture_project() -> Project {
    let path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../gled-model/tests/fixtures/projects/0bb3f5cd-f47f-4453-bd89-8bc53d9e1eb3.json");
    let text = fs::read(&path).expect("fixture project is readable");
    Asset::<Project>::read(text.as_slice(), AssetId::default())
        .expect("fixture project loads")
        .data
        .clone()
}

fn fixture_state() -> EngineState {
    EngineState {
        project: Some(fixture_project()),
        project_id: Some(AssetId::default()),
        selected_scene_instance: GridLocation::new(2, 3),
        blackout: false,
        blackout_hold: true,
        midi_output_active: true,
        output_config: OutputConfig {
            universes: Arc::new(BTreeSet::from([0, 1, 7])),
            routings: Arc::default(),
        },
        beat: BeatState {
            beat_progression: 12.5,
            beats_per_minute: 128.0,
            framerate: 240.0,
        },
        tap_count: 3,
        layout_generation: 9,
        render: RenderStats {
            fps: 240.0,
            max_frame_ms: 6.5,
        },
        status: ServerStatus {
            storage_task: Some("Loading projects".to_owned()),
            staged_files: 2,
            branches: vec!["main".to_owned()],
            current_branch: Some("main".to_owned()),
            network_out: 13.5,
            ..ServerStatus::default()
        },
    }
}

fn round_trip<T>(message: &T)
where
    T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug + PartialEq,
{
    let bytes = encode(message).expect("encodes");
    let decoded: T = decode(&bytes).expect("decodes");
    assert_eq!(message, &decoded);
}

#[test]
fn state_with_a_real_project_round_trips() {
    let state = fixture_state();
    let bytes = encode(&state).expect("encodes");
    let decoded: EngineState = decode(&bytes).expect("decodes");
    assert_eq!(state, decoded);
    println!("fixture state: {} bytes", bytes.len());
}

#[test]
fn client_messages_round_trip() {
    let messages = [
        ClientMessage::Hello {
            protocol_version: PROTOCOL_VERSION,
            app_version: "2.30.0".to_owned(),
        },
        ClientMessage::Input(ClientInput::Action {
            sequence: 7,
            action: UiAction::SetSceneOpacity(
                SceneInstanceUnion::Grid(GridLocation::new(1, 0)),
                0.5,
            ),
        }),
        ClientMessage::Input(ClientInput::Action {
            sequence: 8,
            action: UiAction::SetProjectTriggerInputs(Box::new(
                gled_protocol::action::TriggerInputs::of(&fixture_project()),
            )),
        }),
        ClientMessage::Input(ClientInput::Keys(BTreeSet::from([
            KeyCode::A,
            KeyCode::Space,
        ]))),
        ClientMessage::Input(ClientInput::SetBeatsPerMinute(123.0)),
        ClientMessage::Subscribe(Subscriptions {
            preview: true,
            sound: false,
            input_events: false,
            midi: false,
        }),
        ClientMessage::Input(ClientInput::Storage(StorageCommand::Commit {
            message: "evening show".to_owned(),
        })),
        ClientMessage::Input(ClientInput::Storage(StorageCommand::Reclone)),
        ClientMessage::Input(ClientInput::SetServerSettings(ServerSettings {
            fps_limit: 333.0,
            ableton_link_read_only: true,
            git_url: "git@example.org:show.git".to_owned(),
            git_private_key: Some("/var/lib/gled/ssh/id_ed25519".to_owned()),
            git_use_passphrase: false,
        })),
        ClientMessage::Ping(42),
    ];
    for message in &messages {
        round_trip(message);
    }
}

#[test]
fn server_messages_round_trip() {
    let messages = [
        ServerMessage::Welcome {
            client_id: 3,
            app_version: "2.30.0".to_owned(),
        },
        ServerMessage::Rejected {
            reason: "protocol 0 is not supported".to_owned(),
        },
        ServerMessage::State {
            state: Arc::new(fixture_state()),
            applied_sequence: 11,
        },
        ServerMessage::Update {
            state: Arc::new(fixture_state().without_project()),
            project: ProjectUpdate::between(None, fixture_state().project.as_ref()),
            applied_sequence: 12,
        },
        ServerMessage::Timing {
            beat: fixture_state().beat,
            render: RenderStats {
                fps: 240.0,
                max_frame_ms: 6.5,
            },
        },
        ServerMessage::Event(EngineEvent::ShaderReloaded(None)),
        ServerMessage::Event(EngineEvent::Error("broken".to_owned())),
        ServerMessage::Preview(SharedBytes(Arc::new(vec![1, 2, 3, 255]))),
        ServerMessage::Sound(Arc::new(SoundSnapshot {
            levels: [(uuid::Uuid::nil(), 0.5)].into(),
            spectrum: SharedBytes(Arc::new(vec![0; 1024])),
            ..SoundSnapshot::default()
        })),
        ServerMessage::Pong(42),
    ];
    for message in &messages {
        round_trip(message);
    }
}

#[test]
fn same_but_timing_ignores_only_the_timing() {
    let state = fixture_state();
    let mut beat_moved = state.clone();
    beat_moved.beat.beat_progression += 1.0;
    beat_moved.render.fps = 99.0;
    assert!(state.same_but_timing(&beat_moved));

    let mut blackout = state.clone();
    blackout.blackout = true;
    assert!(!state.same_but_timing(&blackout));

    let mut project = state.clone();
    if let Some(project) = &mut project.project {
        project.main_dimmer += 0.1;
    }
    assert!(!state.same_but_timing(&project));
}

#[test]
fn a_preview_frame_costs_its_bytes_and_a_few_more() {
    let frame = vec![7_u8; 24 * 512];
    let bytes = encode(&ServerMessage::Preview(SharedBytes(Arc::new(
        frame.clone(),
    ))))
    .expect("encodes");
    assert!(bytes.len() < frame.len() + 32, "{} bytes", bytes.len());
}