use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::Path,
sync::Arc,
};
use gled_model::{
asset::{
Asset,
midi_controller::{MidiEffectParameter, MidiModeHighlight},
project::{Project, scene_instance_path::SceneInstanceUnion},
scene::grid::GridLocation,
},
asset_id::AssetId,
input::key_code::KeyCode,
patching::{color_orders::ColorOrders, definition::ColorOrder},
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, GridLine, ModeSwitch, OutputConfig, RenderStats,
ServerSettings, ServerStatus, SoundSnapshot, StorageCommand, TouchedControl,
},
};
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(Arc::new(fixture_project())),
project_id: Some(AssetId::default()),
selected_scene_instance: GridLocation::new(2, 3),
selected_scene_effect: 1,
controller_line: Some(GridLine::Column),
midi_modes: BTreeMap::from([("P1-X".to_owned(), 1)]),
last_mode_switch: Some(ModeSwitch {
number: 3,
highlight: Some(MidiModeHighlight::SceneSettings),
}),
touched_controls: vec![TouchedControl::EffectParameter {
scene: GridLocation::new(1, 0),
effect: 2,
parameter: MidiEffectParameter::Setting3,
}],
midi_inputs: vec!["iCON P1-X1:iCON P1-X1 MIDI 1 28:0".to_owned()],
blackout: false,
blackout_hold: true,
midi_output_active: true,
output_config: OutputConfig {
universes: Arc::new(BTreeSet::from([0, 1, 7])),
routings: Arc::default(),
color_orders: Arc::new({
let mut orders = ColorOrders::default();
orders.set(7, 3, ColorOrder::Grb);
orders
}),
},
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()),
storage_progress: Some(0.25),
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::Input(ClientInput::SavePicture {
path: "pictures/stage.png".into(),
bytes: SharedBytes(Arc::new(vec![0x89, b'P', b'N', b'G'])),
}),
ClientMessage::Input(ClientInput::RequestPicture("backgrounds/stage.svg".into())),
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_deref()),
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::Event(EngineEvent::Picture {
path: "backgrounds/stage.svg".into(),
picture: Ok(SharedBytes(Arc::new(b"<svg/>".to_vec()))),
}),
ServerMessage::Event(EngineEvent::Picture {
path: "pictures/gone.png".into(),
picture: Err("not found".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 {
Arc::make_mut(project).main_dimmer += 0.1;
}
assert!(!state.same_but_timing(&project));
let mut copied = state.clone();
copied.project = Some(Arc::new(fixture_project()));
assert!(state.same_but_timing(&copied));
}
#[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());
}