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());
}