From 3e6c052731ddd1a3ba61e8a9747580a3d8febd64 Mon Sep 17 00:00:00 2001 From: Dominik Nakamura Date: Thu, 2 Jan 2025 00:03:24 +0100 Subject: [PATCH] test: replace real OBS tests with mock server --- .github/workflows/ci.yml | 9 +- .github/workflows/pages.yml | 46 ++ CHANGELOG.md | 1 + Cargo.toml | 2 + Justfile | 26 +- src/client/mod.rs | 7 +- src/requests/mod.rs | 12 + src/requests/ui.rs | 8 + tests/README.md | 20 - tests/integration/client.rs | 7 +- tests/integration/common.rs | 572 +++++++++++++++++-------- tests/integration/config.rs | 87 +++- tests/integration/filters.rs | 121 +++++- tests/integration/general.rs | 80 +++- tests/integration/hotkeys.rs | 35 +- tests/integration/inputs.rs | 273 +++++++++++- tests/integration/main.rs | 2 - tests/integration/media_inputs.rs | 54 ++- tests/integration/outputs.rs | 71 ++- tests/integration/profiles.rs | 84 +++- tests/integration/recording.rs | 107 +++-- tests/integration/replay_buffer.rs | 65 ++- tests/integration/scene_collections.rs | 48 ++- tests/integration/scene_items.rs | 290 ++++++++++--- tests/integration/scenes.rs | 135 +++++- tests/integration/sources.rs | 45 +- tests/integration/streaming.rs | 45 +- tests/integration/transitions.rs | 93 +++- tests/integration/ui.rs | 101 ++++- tests/integration/virtual_cam.rs | 55 ++- 30 files changed, 2004 insertions(+), 497 deletions(-) create mode 100644 .github/workflows/pages.yml delete mode 100644 tests/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84135e3..73d782e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,18 +23,17 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - toolchain: [stable, "1.70"] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.toolchain }} + uses: dtolnay/rust-toolchain@stable - name: Configure cache uses: Swatinem/rust-cache@v2 + - name: Install cargo-nextest + uses: taiki-e/install-action@cargo-nextest - name: Test - run: cargo test + run: cargo nextest run --all-features lint: name: Lint runs-on: ubuntu-latest diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..9d69928 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,46 @@ +name: Deploy coverage report to Pages +on: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: "pages" + cancel-in-progress: false +jobs: + build: + name: Build coverage report + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + - name: Configure cache + uses: Swatinem/rust-cache@v2 + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Install just + uses: taiki-e/install-action@just + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + - name: Run tests with coverage + run: just coverage + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./target/llvm-cov/html + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index ba08e77..331485f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improve feature flag documentation and enable feature markers on items in docs.rs, that show under what conditions certain items are available. +- Revamp the integration tests to use a mocking server instead of running against a real OBS instance. This was long overdue as the tests didn't work anymore and it became harder and harder to make all tests work due to bugs or behavior in OBS. ## [0.14.0] - 2025-01-01 diff --git a/Cargo.toml b/Cargo.toml index ac10faf..37e7e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,8 +57,10 @@ uuid = { version = "1.11.0", features = ["serde"] } anyhow = "1.0.95" dotenvy = "0.15.7" serde_test = "1.0.177" +test-log = { version = "0.2.14", default-features = false, features = ["trace"] } tokio = { version = "1.38.1", features = ["fs", "macros", "rt-multi-thread", "time"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +uuid = { version = "1.11.0", features = ["v8"] } [features] default = [] diff --git a/Justfile b/Justfile index 38418be..3508a57 100644 --- a/Justfile +++ b/Justfile @@ -1,33 +1,21 @@ -set dotenv-load := true - _default: @just --list --unsorted +# format all Rust source code +fmt: + cargo +nightly fmt --all + # run unit and integration tests test: - cargo test - cargo test --all-features --test integration -- --test-threads 1 + cargo nextest run --all-features # run integration tests with coverage coverage: - cargo install cargo-llvm-cov - rustup component add llvm-tools-preview - - cargo llvm-cov --remap-path-prefix --html --all-features -- --test-threads 1 - cargo llvm-cov --remap-path-prefix --no-run --json --summary-only | \ + cargo llvm-cov --html --all-features + cargo llvm-cov --no-run --json --summary-only | \ jq -c '.data[0].totals.lines.percent | { \ schemaVersion: 1, \ label: "coverage", \ message: "\(.|round)%", \ color: (if . < 70 then "red" elif . < 80 then "yellow" else "green" end) \ }' > target/llvm-cov/html/coverage.json - -# upload coverage to GitHub Pages -upload-coverage: coverage - git checkout gh-pages - rm -rf coverage coverage.json index.html style.css - cp -R target/llvm-cov/html/ . - git add -A coverage coverage.json index.html style.css - git commit -m "Coverage for $(git rev-parse --short main)" - git push - git checkout main diff --git a/src/client/mod.rs b/src/client/mod.rs index 01e71c3..188dc92 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -291,10 +291,7 @@ impl Client { let (mut write, mut read) = socket.split(); let receivers = Arc::new(ReceiverList::default()); - let receivers2 = Arc::clone(&receivers); - let reidentify_receivers = Arc::new(ReidentifyReceiverList::default()); - let reidentify_receivers2 = Arc::clone(&reidentify_receivers); #[cfg(feature = "events")] let (event_sender, _) = broadcast::channel(config.broadcast_capacity); @@ -315,8 +312,8 @@ impl Client { read, #[cfg(feature = "events")] events_tx, - receivers2, - reidentify_receivers2, + Arc::clone(&receivers), + Arc::clone(&reidentify_receivers), )); let write = Mutex::new(write); diff --git a/src/requests/mod.rs b/src/requests/mod.rs index 768a0ad..186c1cf 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -138,6 +138,11 @@ pub(crate) struct RequestBatch<'a> { /// Bit flags for possible event subscriptions, that can be enabled when connecting to the OBS /// instance. #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +#[cfg_attr( + feature = "test-integration", + derive(serde::Deserialize), + serde(from = "u32") +)] #[serde(into = "u32")] pub struct EventSubscription(u32); @@ -211,6 +216,13 @@ impl From for u32 { } } +#[cfg(feature = "test-integration")] +impl From for EventSubscription { + fn from(value: u32) -> Self { + Self::from_bits_truncate(value) + } +} + #[allow(dead_code)] #[derive(Serialize_repr)] #[repr(i8)] diff --git a/src/requests/ui.rs b/src/requests/ui.rs index a1ceb76..72c768d 100644 --- a/src/requests/ui.rs +++ b/src/requests/ui.rs @@ -1,5 +1,7 @@ //! Requests related to the user interface. +use std::fmt::{self, Display}; + use bitflags::bitflags; use serde::Serialize; @@ -234,6 +236,12 @@ impl Default for QtGeometry { } } +impl Display for QtGeometry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.serialize()) + } +} + /// Request information for [`crate::client::Ui::open_video_mix_projector`] and /// [`crate::client::Ui::open_source_projector`] as part of [`QtGeometry`]. #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 2e450f8..0000000 --- a/tests/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Integration tests - -To run integration tests, obws will connect to your OBS instance and send several commands against -the obs-websocket API to make sure most of the API works as expected. - -For this to work, a few settings need to be set and some scene items created so that the tests have -items to work on. This has to be done manually as the API doesn't allow to create new sources and -scenes or modify specific settings. - -- Use at least OBS version `27.0.0`. -- Create a **source collection** called `OBWS-TEST`. -- Create a **profile** called `OBWS-TEST`. -- Create two **scene**s called `OBWS-TEST-Scene` and `OBWS-TEST-Scene2`. -- Create two **Freetype2 text source**s called `OBWS-TEST-Text` and `OBWS-TEST-Text2`. -- Create a **browser source** called `OBWS-TEST-Browser`. -- Create a **VLC media source** called `OBWS-TEST-Media` and add a folder with videos to the - playlist. -- Create two **transition**s called `OBWS-TEST-Transition` and `OBWS-TEST-Transition2`. -- Make sure a global **Desktop Audio** device is configured. -- Set any **hotkey** to `P` without any modifier keys (like _ctrl_ or _alt_). diff --git a/tests/integration/client.rs b/tests/integration/client.rs index 1a3fcbd..1c057d1 100644 --- a/tests/integration/client.rs +++ b/tests/integration/client.rs @@ -1,13 +1,14 @@ use anyhow::Result; use obws::requests::EventSubscription; +use test_log::test; use crate::common; -#[tokio::test] +#[test(tokio::test)] async fn client() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; client.reidentify(EventSubscription::ALL).await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/common.rs b/tests/integration/common.rs index 2ae51ef..0be1b90 100644 --- a/tests/integration/common.rs +++ b/tests/integration/common.rs @@ -1,19 +1,35 @@ -use std::{env, sync::Once}; +use std::net::Ipv4Addr; -use anyhow::{ensure, Result}; +use anyhow::{bail, ensure, Context, Result}; +use base64::{engine::general_purpose, Engine}; +use futures_util::{SinkExt, StreamExt}; use obws::{ - requests::{inputs::InputId, scenes::SceneId}, - responses::{filters::SourceFilter, inputs::Input, scenes::Scene}, + events::Event, + requests::{inputs::InputId, scenes::SceneId, EventSubscription}, + responses::StatusCode, Client, }; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::json; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use sha2::{Digest, Sha256}; +use tokio::{ + net::{TcpListener, TcpStream}, + select, + sync::{mpsc, oneshot}, + task::JoinHandle, +}; +use tokio_tungstenite::{ + tungstenite::{self, Message}, + WebSocketStream, +}; +use tracing::{debug, error, info}; -pub const TEST_PROFILE: &str = "OBWS-TEST"; pub const TEST_SCENE: SceneId<'_> = SceneId::Name("OBWS-TEST-Scene"); pub const TEST_SCENE_2: SceneId<'_> = SceneId::Name("OBWS-TEST-Scene2"); pub const TEST_SCENE_RENAME: SceneId<'_> = SceneId::Name("OBWS-TEST-Scene-Renamed"); pub const TEST_SCENE_CREATE: SceneId<'_> = SceneId::Name("OBWS-TEST-Scene-Created"); pub const TEST_TEXT: InputId<'_> = InputId::Name("OBWS-TEST-Text"); -pub const TEST_TEXT_2: InputId<'_> = InputId::Name("OBWS-TEST-Text2"); pub const TEST_BROWSER: InputId<'_> = InputId::Name("OBWS-TEST-Browser"); pub const TEST_BROWSER_RENAME: InputId<'_> = InputId::Name("OBWS-TEST-Browser-Renamed"); pub const TEST_MEDIA: InputId<'_> = InputId::Name("OBWS-TEST-Media"); @@ -22,220 +38,422 @@ pub const TEST_TRANSITION: &str = "OBWS-TEST-Transition"; pub const TEST_FILTER: &str = "OBWS-TEST-Filter"; pub const TEST_FILTER_2: &str = "OBWS-TEST-Filter2"; pub const TEST_FILTER_RENAME: &str = "OBWS-TEST-Filter-Renamed"; -pub const INPUT_KIND_TEXT_FT2: &str = "text_ft2_source_v2"; pub const INPUT_KIND_BROWSER: &str = "browser_source"; pub const INPUT_KIND_VLC: &str = "vlc_source"; pub const FILTER_COLOR: &str = "color_filter"; -static INIT: Once = Once::new(); +pub async fn new_client() -> Result<(Client, MockServer)> { + let (server, port) = MockServer::start().await?; + let client = Client::connect("localhost", port, Some("mock-password")).await?; -pub async fn new_client() -> Result { - INIT.call_once(|| { - dotenvy::dotenv().ok(); - tracing_subscriber::fmt::init(); - }); + Ok((client, server)) +} - let host = env::var("OBS_HOST").unwrap_or_else(|_| "localhost".to_owned()); - let port = env::var("OBS_PORT") - .map(|p| p.parse()) - .unwrap_or(Ok(4455))?; - let client = Client::connect(host, port, env::var("OBS_PASSWORD").ok()).await?; - - ensure_obs_setup(&client).await?; - - Ok(client) -} - -async fn ensure_obs_setup(client: &Client) -> Result<()> { - let scenes = client.scenes().list().await?; - ensure!( - scenes.scenes.iter().any(is_required_scene), - "scene `{}` not found, required for scenes tests", - TEST_SCENE - ); - ensure!( - scenes.scenes.iter().any(is_required_scene_2), - "scene `{}` not found, required for scenes tests", - TEST_SCENE - ); - ensure!( - !scenes.scenes.iter().any(is_renamed_scene), - "scene `{}` found, must NOT be present for scenes tests", - TEST_SCENE_RENAME - ); - ensure!( - !scenes.scenes.iter().any(is_created_scene), - "scene `{}` found, must NOT be present for scenes tests", - TEST_SCENE_CREATE - ); - - let groups = client.scenes().list_groups().await?; - ensure!( - groups.iter().map(String::as_str).any(is_required_group), - "group `{}` not found, required for scenes and scene items tests", - TEST_GROUP - ); - - let inputs = client.inputs().list(None).await?; - ensure!( - inputs.iter().any(is_required_text_input), - "text input `{}` not found, required for inputs tests", - TEST_TEXT - ); - ensure!( - inputs.iter().any(is_required_text_2_input), - "text input `{}` not found, required for inputs tests", - TEST_TEXT_2 - ); - ensure!( - inputs.iter().any(is_required_browser_input), - "media input `{}` not found, required for inputs tests", - TEST_BROWSER - ); - ensure!( - inputs.iter().any(is_required_media_input), - "media input `{}` not found, required for inputs tests", - TEST_MEDIA - ); - ensure!( - !inputs.iter().any(is_renamed_input), - "browser input `{}` found, must NOT be present for inputs tests", - TEST_BROWSER_RENAME - ); - - let filters = client.filters().list(TEST_TEXT.as_source()).await?; - ensure!( - filters.iter().any(is_required_filter), - "filter `{}` not found, required for filters tests", - TEST_FILTER - ); - ensure!( - !filters.iter().any(is_filter_2), - "filter `{}` found, must NOT be present for filters tests", - TEST_FILTER_2 - ); - ensure!( - !filters.iter().any(is_renamed_filter), - "filter `{}` found, must NOT be present for filters tests", - TEST_FILTER_RENAME - ); - - let profiles = client.profiles().list().await?.profiles; - ensure!( - profiles.iter().map(String::as_str).any(is_required_profile), - "profile `{}` not found, required for profiles tests", - TEST_PROFILE - ); - - let studio_mode_enabled = client.ui().studio_mode_enabled().await?; - ensure!( - !studio_mode_enabled, - "studio mode enabled, required to be disabled for studio mode tests" - ); - - let recording_active = client.recording().status().await?.active; - ensure!( - !recording_active, - "recording active, required to be stopped for recording tests" - ); - - let virtual_cam_active = client.virtual_cam().status().await?; - ensure!( - !virtual_cam_active, - "virtual cam active, required to be stopped for outputs tests" - ); - - let replay_buffer_active = client.replay_buffer().status().await?; - ensure!( - !replay_buffer_active, - "replay buffer active, required to be stopped for outputs tests" - ); - - client - .scenes() - .set_current_program_scene(TEST_SCENE) - .await?; +#[macro_export] +macro_rules! wait_for { + ($expression:expr, $pattern:pat) => {{ + use futures_util::stream::StreamExt; - Ok(()) + while let Some(event) = $expression.next().await { + if matches!(event, $pattern) { + break; + } + } + }}; } -fn is_required_scene(scene: &Scene) -> bool { - scene.id == TEST_SCENE +pub struct MockServer { + handle: JoinHandle>, + shutdown: Option>, + expectations: mpsc::UnboundedSender, + events: mpsc::UnboundedSender, } -fn is_required_scene_2(scene: &Scene) -> bool { - scene.id == TEST_SCENE_2 +impl MockServer { + pub async fn start() -> Result<(Self, u16)> { + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; + let port = listener.local_addr()?.port(); + debug!("server started"); + + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + let (expect_tx, mut expect_rx) = mpsc::unbounded_channel(); + let (event_tx, mut event_rx) = mpsc::unbounded_channel(); + + let handle = tokio::spawn(async move { + let (stream, _) = listener.accept().await?; + let mut stream = tokio_tungstenite::accept_async(stream).await?; + debug!("connected"); + + handshake(&mut stream).await?; + debug!("handshake done"); + version_check(&mut stream).await?; + debug!("version check done"); + + loop { + select! { + _ = &mut shutdown_rx => break, + Some(msg) = stream.next() => { + handle_ws_message(&mut stream, &mut expect_rx, msg).await?; + } + Some(event) = event_rx.recv() => { + handle_event(&mut stream, event).await?; + } + } + } + + anyhow::Ok(()) + }); + + Ok(( + Self { + handle, + shutdown: Some(shutdown_tx), + expectations: expect_tx, + events: event_tx, + }, + port, + )) + } + + pub async fn stop(mut self) -> Result<()> { + if let Some(tx) = self.shutdown.take() { + tx.send(()).ok(); + } + self.handle.await? + } + + pub fn expect(&self, name: &str, req: Req, rsp: Rsp) + where + Req: Serialize, + Rsp: Serialize, + { + self.expectations + .send(Expectation { + name: name.to_owned(), + req: serde_json::to_value(req).unwrap(), + rsp: serde_json::to_value(rsp).unwrap(), + }) + .unwrap(); + } + + pub fn send_event(&self, event: Event) { + self.events.send(event).unwrap(); + } } -fn is_renamed_scene(scene: &Scene) -> bool { - scene.id == TEST_SCENE_RENAME +struct Expectation { + name: String, + req: serde_json::Value, + rsp: serde_json::Value, } -fn is_created_scene(scene: &Scene) -> bool { - scene.id == TEST_SCENE_CREATE +async fn handshake(stream: &mut WebSocketStream) -> Result<()> { + let hello = ServerMessage::Hello(Hello { + obs_web_socket_version: semver::Version::new(5, 5, 0), + rpc_version: 1, + authentication: Some(Authentication { + challenge: "mock-challenge".to_owned(), + salt: "mock-salt".to_owned(), + }), + }); + + stream + .send(Message::text(serde_json::to_string(&hello)?)) + .await?; + + let identify = stream.next().await.context("no message from client")??; + let ClientMessage::Identify(identify) = + serde_json::from_str::(identify.to_text()?)? + else { + bail!("unexpected client message"); + }; + + ensure!(identify.rpc_version == 1); + ensure!(identify.event_subscriptions == None); + verify_auth(&identify)?; + + let identified = ServerMessage::Identified(Identified { + negotiated_rpc_version: 1, + }); + + stream + .send(Message::text(serde_json::to_string(&identified)?)) + .await?; + + Ok(()) } -fn is_required_group(group: &str) -> bool { - group == TEST_GROUP +fn verify_auth(identify: &Identify) -> Result<()> { + let mut hasher = Sha256::new(); + hasher.update(b"mock-password"); + hasher.update(b"mock-salt"); + + let intermediate = general_purpose::STANDARD.encode(hasher.finalize_reset()); + hasher.update(intermediate.as_bytes()); + hasher.update(b"mock-challenge"); + + let auth = general_purpose::STANDARD.encode(hasher.finalize()); + ensure!(Some(auth) == identify.authentication); + + Ok(()) } -fn is_required_text_input(input: &Input) -> bool { - input.id == TEST_TEXT && is_text_input(input) +async fn version_check(stream: &mut WebSocketStream) -> Result<()> { + let request = stream.next().await.context("no message from client")??; + let request = serde_json::from_str::(request.to_text()?)?; + + let ClientMessage::Request(request) = request else { + bail!("unexpected client message"); + }; + + ensure!(request.request_type == "GetVersion"); + + let response = ServerMessage::RequestResponse(RequestResponse { + request_type: request.request_type, + request_id: request.request_id, + request_status: Status::ok(), + response_data: json! {{ + "obsVersion": "31.0.0", + "obsWebSocketVersion": "5.5.0", + "rpcVersion": 1, + "availableRequests": [], + "supportedImageFormats": [], + "platform": "mock", + "platformDescription": "", + }}, + }); + + stream + .send(Message::text(serde_json::to_string(&response)?)) + .await?; + + Ok(()) } -fn is_required_text_2_input(input: &Input) -> bool { - input.id == TEST_TEXT_2 && is_text_input(input) +async fn handle_ws_message( + stream: &mut WebSocketStream, + expect_rx: &mut mpsc::UnboundedReceiver, + msg: tungstenite::Result, +) -> Result<()> { + match msg { + Ok(msg) => { + let msg = serde_json::from_str::(msg.to_text()?)?; + info!(message = ?msg); + + match msg { + ClientMessage::Identify(identify) => { + bail!("should never get a second `Identify` message: {identify:?}") + } + ClientMessage::Reidentify(reidentify) => { + debug!(?reidentify, "received reidentification request"); + ensure!(reidentify.event_subscriptions != None); + + let identified = ServerMessage::Identified(Identified { + negotiated_rpc_version: 1, + }); + + stream + .send(Message::text(serde_json::to_string(&identified)?)) + .await?; + } + ClientMessage::Request(request) => { + let expect = expect_rx + .recv() + .await + .context("no expectations for request")?; + + ensure!(expect.name == request.request_type); + ensure!(expect.req == request.request_data); + + stream + .send(Message::text(serde_json::to_string( + &ServerMessage::RequestResponse(RequestResponse { + request_type: request.request_type, + request_id: request.request_id, + request_status: Status::ok(), + response_data: expect.rsp, + }), + )?)) + .await?; + } + } + } + Err(err) => error!(?err), + } + + Ok(()) } -fn is_required_browser_input(input: &Input) -> bool { - input.id == TEST_BROWSER && is_browser_input(input) +async fn handle_event(stream: &mut WebSocketStream, event: Event) -> Result<()> { + let msg = ServerMessage::Event(event); + + stream + .send(Message::text(serde_json::to_string(&msg)?)) + .await + .map_err(Into::into) } -fn is_required_media_input(input: &Input) -> bool { - input.id == TEST_MEDIA && is_media_input(input) +enum ServerMessage { + Hello(Hello), + Identified(Identified), + Event(Event), + RequestResponse(RequestResponse), } -fn is_renamed_input(input: &Input) -> bool { - input.id == TEST_BROWSER_RENAME +impl Serialize for ServerMessage { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct RawMessage { + op: OpCode, + d: T, + } + + #[derive(Serialize_repr)] + #[repr(u8)] + enum OpCode { + Hello = 0, + Identified = 2, + Event = 5, + RequestResponse = 7, + } + + match self { + ServerMessage::Hello(d) => RawMessage { + op: OpCode::Hello, + d, + } + .serialize(serializer), + ServerMessage::Identified(d) => RawMessage { + op: OpCode::Identified, + d, + } + .serialize(serializer), + ServerMessage::Event(d) => RawMessage { + op: OpCode::Event, + d, + } + .serialize(serializer), + ServerMessage::RequestResponse(d) => RawMessage { + op: OpCode::RequestResponse, + d, + } + .serialize(serializer), + } + } } -fn is_text_input(input: &Input) -> bool { - input.kind == INPUT_KIND_TEXT_FT2 +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Hello { + obs_web_socket_version: semver::Version, + rpc_version: u32, + authentication: Option, } -fn is_browser_input(input: &Input) -> bool { - input.kind == INPUT_KIND_BROWSER +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Authentication { + challenge: String, + salt: String, } -fn is_media_input(input: &Input) -> bool { - input.kind == INPUT_KIND_VLC +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Identified { + pub negotiated_rpc_version: u32, } -fn is_required_filter(filter: &SourceFilter) -> bool { - filter.name == TEST_FILTER +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct RequestResponse { + request_type: String, + request_id: String, + request_status: Status, + response_data: serde_json::Value, } -fn is_filter_2(filter: &SourceFilter) -> bool { - filter.name == TEST_FILTER_2 +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Status { + result: bool, + code: StatusCode, + comment: Option, } -fn is_renamed_filter(filter: &SourceFilter) -> bool { - filter.name == TEST_FILTER_RENAME +impl Status { + const fn ok() -> Self { + Self { + result: true, + code: StatusCode::NoError, + comment: None, + } + } } -fn is_required_profile(profile: &str) -> bool { - profile == TEST_PROFILE +#[derive(Debug)] +enum ClientMessage { + Identify(Identify), + Reidentify(Reidentify), + Request(Request), } -#[macro_export] -macro_rules! wait_for { - ($expression:expr, $pattern:pat) => {{ - use futures_util::stream::StreamExt; +impl<'de> Deserialize<'de> for ClientMessage { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct RawMessage { + op: OpCode, + d: serde_json::Value, + } - while let Some(event) = $expression.next().await { - if matches!(event, $pattern) { - break; - } + #[derive(Deserialize_repr)] + #[repr(u8)] + enum OpCode { + Identify = 1, + Reidentify = 3, + Request = 6, } - }}; + + let raw = RawMessage::deserialize(deserializer)?; + + Ok(match raw.op { + OpCode::Identify => { + ClientMessage::Identify(serde_json::from_value(raw.d).map_err(de::Error::custom)?) + } + OpCode::Reidentify => { + ClientMessage::Reidentify(serde_json::from_value(raw.d).map_err(de::Error::custom)?) + } + OpCode::Request => { + ClientMessage::Request(serde_json::from_value(raw.d).map_err(de::Error::custom)?) + } + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Identify { + rpc_version: u32, + authentication: Option, + event_subscriptions: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Reidentify { + event_subscriptions: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Request { + request_id: String, + request_type: String, + #[serde(default)] + request_data: serde_json::Value, } diff --git a/tests/integration/config.rs b/tests/integration/config.rs index 47bd1bc..b5f97c6 100644 --- a/tests/integration/config.rs +++ b/tests/integration/config.rs @@ -1,13 +1,25 @@ use anyhow::Result; use obws::requests::config::{Realm, SetPersistentData}; +use serde_json::json; +use test_log::test; use crate::common; -#[tokio::test] +#[test(tokio::test)] async fn config() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.config(); + server.expect( + "SetPersistentData", + json!({ + "realm": "OBS_WEBSOCKET_DATA_REALM_PROFILE", + "slotName": "obws-test", + "slotValue": true, + }), + json!(null), + ); + client .set_persistent_data(SetPersistentData { realm: Realm::Profile, @@ -15,22 +27,91 @@ async fn config() -> Result<()> { slot_value: &true.into(), }) .await?; + + server.expect( + "GetPersistentData", + json!({ + "realm": "OBS_WEBSOCKET_DATA_REALM_PROFILE", + "slotName": "obws-test", + }), + json!({"slotValue": true}), + ); + client .get_persistent_data(Realm::Profile, "obws-test") .await?; + server.expect( + "GetVideoSettings", + json!(null), + json!({ + "fpsNumerator": 1, + "fpsDenominator": 60, + "baseWidth": 1920, + "baseHeight": 1080, + "outputWidth": 1280, + "outputHeight": 720, + }), + ); + let settings = client.video_settings().await?; + + server.expect( + "SetVideoSettings", + json!({ + "fpsNumerator": 1, + "fpsDenominator": 60, + "baseWidth": 1920, + "baseHeight": 1080, + "outputWidth": 1280, + "outputHeight": 720, + }), + json!(null), + ); + client.set_video_settings(settings.into()).await?; + server.expect( + "GetStreamServiceSettings", + json!(null), + json!({ + "streamServiceType": "rtmp_common", + "streamServiceSettings": {}, + }), + ); + let settings = client .stream_service_settings::() .await?; + + server.expect( + "SetStreamServiceSettings", + json!({ + "streamServiceType": "rtmp_common", + "streamServiceSettings": {}, + }), + json!(null), + ); + client .set_stream_service_settings(&settings.r#type, &settings.settings) .await?; + server.expect( + "GetRecordDirectory", + json!(null), + json!({"recordDirectory": "/tmp"}), + ); + let directory = client.record_directory().await?; + + server.expect( + "SetRecordDirectory", + json!({"recordDirectory": "/tmp"}), + json!(null), + ); + client.set_record_directory(&directory).await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/filters.rs b/tests/integration/filters.rs index 3e75802..2d67cce 100644 --- a/tests/integration/filters.rs +++ b/tests/integration/filters.rs @@ -1,20 +1,54 @@ use anyhow::Result; use obws::requests::filters::{Create, SetEnabled, SetIndex, SetName, SetSettings}; +use serde_json::json; +use test_log::test; use crate::common::{ self, FILTER_COLOR, TEST_FILTER, TEST_FILTER_2, TEST_FILTER_RENAME, TEST_TEXT, }; -#[tokio::test] +#[test(tokio::test)] async fn filters() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.filters(); + server.expect( + "GetSourceFilterKindList", + json!(null), + json!({"sourceFilterKinds": []}), + ); + + client.list_kinds().await?; + + server.expect( + "GetSourceFilterList", + json!({"sourceName": "OBWS-TEST-Text"}), + json!({"filters": []}), + ); + client.list(TEST_TEXT.as_source()).await?; + server.expect( + "GetSourceFilterDefaultSettings", + json!({"filterKind": "color_filter"}), + json!({"defaultFilterSettings": {}}), + ); + client .default_settings::(FILTER_COLOR) .await?; + + server.expect( + "CreateSourceFilter", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter2", + "filterKind": "color_filter", + "filterSettings": {}, + }), + json!(null), + ); + client .create(Create { source: TEST_TEXT.as_source(), @@ -23,8 +57,28 @@ async fn filters() -> Result<()> { settings: Some(serde_json::Map::new()), }) .await?; + + server.expect( + "RemoveSourceFilter", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter2", + }), + json!(null), + ); + client.remove(TEST_TEXT.as_source(), TEST_FILTER_2).await?; + server.expect( + "SetSourceFilterName", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter", + "newFilterName": "OBWS-TEST-Filter-Renamed", + }), + json!(null), + ); + client .set_name(SetName { source: TEST_TEXT.as_source(), @@ -32,16 +86,33 @@ async fn filters() -> Result<()> { new_name: TEST_FILTER_RENAME, }) .await?; - client - .set_name(SetName { - source: TEST_TEXT.as_source(), - filter: TEST_FILTER_RENAME, - new_name: TEST_FILTER, - }) - .await?; + + server.expect( + "GetSourceFilter", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter", + }), + json!({ + "filterEnabled": true, + "filterIndex": 1, + "filterKind": "color_filter", + "filterSettings": {}, + }), + ); client.get(TEST_TEXT.as_source(), TEST_FILTER).await?; + server.expect( + "SetSourceFilterIndex", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter", + "filterIndex": 0, + }), + json!(null), + ); + client .set_index(SetIndex { source: TEST_TEXT.as_source(), @@ -49,6 +120,18 @@ async fn filters() -> Result<()> { index: 0, }) .await?; + + server.expect( + "SetSourceFilterSettings", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter", + "filterSettings": {}, + "overlay": true, + }), + json!(null), + ); + client .set_settings(SetSettings { source: TEST_TEXT.as_source(), @@ -57,6 +140,17 @@ async fn filters() -> Result<()> { overlay: Some(true), }) .await?; + + server.expect( + "SetSourceFilterEnabled", + json!({ + "sourceName": "OBWS-TEST-Text", + "filterName": "OBWS-TEST-Filter", + "filterEnabled": false, + }), + json!(null), + ); + client .set_enabled(SetEnabled { source: TEST_TEXT.as_source(), @@ -64,13 +158,6 @@ async fn filters() -> Result<()> { enabled: false, }) .await?; - client - .set_enabled(SetEnabled { - source: TEST_TEXT.as_source(), - filter: TEST_FILTER, - enabled: true, - }) - .await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/general.rs b/tests/integration/general.rs index f4ad0c5..5142022 100644 --- a/tests/integration/general.rs +++ b/tests/integration/general.rs @@ -1,25 +1,95 @@ use anyhow::Result; -use obws::events::Event; +use obws::{events::Event, requests::general::CallVendorRequest}; use serde::Serialize; +use serde_json::json; +use test_log::test; use crate::{common, wait_for}; -#[tokio::test] +#[test(tokio::test)] async fn general() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let events = client.events()?; let client = client.general(); tokio::pin!(events); + server.expect( + "GetVersion", + json!(null), + json!({ + "obsVersion": "31.0.0", + "obsWebSocketVersion": "5.5.0", + "rpcVersion": 1, + "availableRequests": [], + "supportedImageFormats": [], + "platform": "mock", + "platformDescription": "", + }), + ); + client.version().await?; + + server.expect( + "GetStats", + json!(null), + json!({ + "cpuUsage": 0.5, + "memoryUsage": 200, + "availableDiskSpace": 30_000_000, + "activeFps": 59.99, + "averageFrameRenderTime": 5, + "renderSkippedFrames": 0, + "renderTotalFrames": 10_000, + "outputSkippedFrames": 0, + "outputTotalFrames": 8_000, + "webSocketSessionIncomingMessages": 10, + "webSocketSessionOutgoingMessages": 10, + }), + ); + + client.stats().await?; + + server.expect( + "BroadcastCustomEvent", + json!({ + "eventData": { + "hello": "world!", + }, + }), + json!(null), + ); + client .broadcast_custom_event(&CustomEvent { hello: "world!" }) .await?; + + server.send_event(Event::CustomEvent(json!({"hello": "world!"}))); wait_for!(events, Event::CustomEvent(_)); - client.stats().await?; - Ok(()) + server.expect( + "CallVendorRequest", + json!({ + "vendorName": "mock", + "requestType": "call", + "requestData": 1, + }), + json!({ + "vendorName": "mock", + "requestType": "call", + "responseData": true, + }), + ); + + client + .call_vendor_request::<_, bool>(CallVendorRequest { + vendor_name: "mock", + request_type: "call", + request_data: &1, + }) + .await?; + + server.stop().await } #[derive(Serialize)] diff --git a/tests/integration/hotkeys.rs b/tests/integration/hotkeys.rs index 1a3428b..01f40b1 100644 --- a/tests/integration/hotkeys.rs +++ b/tests/integration/hotkeys.rs @@ -1,18 +1,47 @@ use anyhow::Result; use obws::requests::hotkeys::KeyModifiers; +use serde_json::json; +use test_log::test; use crate::common; -#[tokio::test] +#[test(tokio::test)] async fn hotkeys() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.hotkeys(); + server.expect("GetHotkeyList", json!(null), json!({"hotkeys": []})); + client.list().await?; + + server.expect( + "TriggerHotkeyByName", + json!({ + "hotkeyName": "ReplayBuffer.Save", + "contextName": null, + }), + json!(null), + ); + client.trigger_by_name("ReplayBuffer.Save", None).await?; + + server.expect( + "TriggerHotkeyByKeySequence", + json!({ + "keyId": "OBS_KEY_P", + "keyModifiers": { + "shift": false, + "control": false, + "alt": false, + "command": false, + }, + }), + json!(null), + ); + client .trigger_by_sequence("OBS_KEY_P", KeyModifiers::default()) .await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/inputs.rs b/tests/integration/inputs.rs index cb80bec..511efeb 100644 --- a/tests/integration/inputs.rs +++ b/tests/integration/inputs.rs @@ -1,28 +1,83 @@ use anyhow::Result; use obws::{ common::MonitorType, - requests::inputs::{SetSettings, Volume}, + requests::inputs::{Create, SetSettings, Volume}, }; -use time::Duration; +use serde_json::json; +use test_log::test; +use uuid::Uuid; -use crate::common::{self, INPUT_KIND_BROWSER, TEST_BROWSER, TEST_BROWSER_RENAME, TEST_MEDIA}; +use crate::common::{ + self, INPUT_KIND_BROWSER, INPUT_KIND_VLC, TEST_BROWSER, TEST_BROWSER_RENAME, TEST_MEDIA, + TEST_SCENE, +}; -#[tokio::test] +#[test(tokio::test)] async fn inputs() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.inputs(); + server.expect("GetInputList", json!({}), json!({"inputs": []})); + client.list(None).await?; + + server.expect( + "GetInputKindList", + json!({"unversioned": false}), + json!({"inputKinds": []}), + ); + client.list_kinds(false).await?; + + server.expect( + "GetSpecialInputs", + json!(null), + json!({ + "desktop1": "audio1", + "desktop2": "audio2", + "mic1": "audio3", + "mic2": "audio4", + "mic3": "audio5", + "mic4": "audio6", + }), + ); + client.specials().await?; + + server.expect( + "GetInputDefaultSettings", + json!({"inputKind": "browser_source"}), + json!({"defaultInputSettings": {}}), + ); + client .default_settings::(INPUT_KIND_BROWSER) .await?; + server.expect( + "GetInputSettings", + json!({"inputName": "OBWS-TEST-Browser"}), + json!({ + "inputSettings": {}, + "inputKind": "browser_source", + }), + ); + let settings = client .settings::(TEST_BROWSER) .await? .settings; + + server.expect( + "SetInputSettings", + json!({ + "inputName": "OBWS-TEST-Browser", + "inputSettings": {}, + "overlay": false, + }), + json!(null), + ); + client .set_settings(SetSettings { input: TEST_BROWSER, @@ -31,49 +86,223 @@ async fn inputs() -> Result<()> { }) .await?; + server.expect( + "GetInputMute", + json!({"inputName": "OBWS-TEST-Media"}), + json!({"inputMuted": true}), + ); + let muted = client.muted(TEST_MEDIA).await?; + + server.expect( + "SetInputMute", + json!({ + "inputName": "OBWS-TEST-Media", + "inputMuted": false, + }), + json!(null), + ); + client.set_muted(TEST_MEDIA, !muted).await?; - client.set_muted(TEST_MEDIA, muted).await?; - client.toggle_mute(TEST_MEDIA).await?; + + server.expect( + "ToggleInputMute", + json!({"inputName": "OBWS-TEST-Media"}), + json!({"inputMuted": true}), + ); + client.toggle_mute(TEST_MEDIA).await?; + server.expect( + "GetInputVolume", + json!({"inputName": "OBWS-TEST-Media"}), + json!({ + "inputVolumeMul": 1.0, + "inputVolumeDb": 20.5, + }), + ); + let volume = client.volume(TEST_MEDIA).await?; + + server.expect( + "SetInputVolume", + json!({ + "inputName": "OBWS-TEST-Media", + "inputVolumeMul": 0.5, + }), + json!(null), + ); + client - .set_volume(TEST_MEDIA, Volume::Mul(volume.mul)) + .set_volume(TEST_MEDIA, Volume::Mul(volume.mul / 2.0)) .await?; - client - .set_name(TEST_BROWSER, TEST_BROWSER_RENAME.as_name().unwrap()) + server.expect( + "CreateInput", + json!({ + "sceneName": "OBWS-TEST-Scene", + "inputName": "new-input", + "inputKind": "vlc_source", + "inputSettings": {}, + "sceneItemEnabled": true, + }), + json!({ + "inputUuid": Uuid::nil(), + "sceneItemId": 1, + }), + ); + + let scene_item_id = client + .create(Create { + scene: TEST_SCENE, + input: "new-input", + kind: INPUT_KIND_VLC, + settings: Some(serde_json::Map::new()), + enabled: Some(true), + }) .await?; + + server.expect( + "RemoveInput", + json!({"inputUuid": Uuid::nil()}), + json!(null), + ); + + client.remove(scene_item_id.input_uuid.into()).await?; + + server.expect( + "SetInputName", + json!({ + "inputName": "OBWS-TEST-Browser", + "newInputName": "OBWS-TEST-Browser-Renamed", + }), + json!(null), + ); + client - .set_name(TEST_BROWSER_RENAME, TEST_BROWSER.as_name().unwrap()) + .set_name(TEST_BROWSER, TEST_BROWSER_RENAME.as_name().unwrap()) .await?; + server.expect( + "GetInputAudioBalance", + json!({"inputName": "OBWS-TEST-Media"}), + json!({"inputAudioBalance": 1.0}), + ); + let balance = client.audio_balance(TEST_MEDIA).await?; + + server.expect( + "SetInputAudioBalance", + json!({ + "inputName": "OBWS-TEST-Media", + "inputAudioBalance": 0.5, + }), + json!(null), + ); + client.set_audio_balance(TEST_MEDIA, balance / 2.0).await?; - client.set_audio_balance(TEST_MEDIA, balance).await?; + + server.expect( + "GetInputAudioSyncOffset", + json!({"inputName": "OBWS-TEST-Media"}), + json!({"inputAudioSyncOffset": 1000}), + ); let offset = client.audio_sync_offset(TEST_MEDIA).await?; - client - .set_audio_sync_offset(TEST_MEDIA, Duration::milliseconds(500)) - .await?; - client.set_audio_sync_offset(TEST_MEDIA, offset).await?; - let monitor_type = client.audio_monitor_type(TEST_MEDIA).await?; + server.expect( + "SetInputAudioSyncOffset", + json!({ + "inputName": "OBWS-TEST-Media", + "inputAudioSyncOffset": 500, + }), + json!(null), + ); + + client.set_audio_sync_offset(TEST_MEDIA, offset / 2).await?; + + server.expect( + "GetInputAudioMonitorType", + json!({"inputName": "OBWS-TEST-Media"}), + json!({"monitorType": "OBS_MONITORING_TYPE_NONE"}), + ); + + client.audio_monitor_type(TEST_MEDIA).await?; + + server.expect( + "SetInputAudioMonitorType", + json!({ + "inputName": "OBWS-TEST-Media", + "monitorType": "OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT", + }), + json!(null), + ); + client .set_audio_monitor_type(TEST_MEDIA, MonitorType::MonitorAndOutput) .await?; - client - .set_audio_monitor_type(TEST_MEDIA, monitor_type) - .await?; + + server.expect( + "GetInputAudioTracks", + json!({"inputName": "OBWS-TEST-Media"}), + json!({ + "inputAudioTracks": { + "1": true, + "2": false, + "3": false, + "4": false, + "5": false, + "6": false, + }, + }), + ); let tracks = client.audio_tracks(TEST_MEDIA).await?; + + server.expect( + "SetInputAudioTracks", + json!({ + "inputName": "OBWS-TEST-Media", + "inputAudioTracks": { + "1": false, + }, + }), + json!(null), + ); + client .set_audio_tracks(TEST_MEDIA, [Some(!tracks[0]), None, None, None, None, None]) .await?; + + server.expect( + "GetInputPropertiesListPropertyItems", + json!({ + "inputName": "OBWS-TEST-Media", + "propertyName": "prop", + }), + json!({ + "propertyItems": [{ + "itemName": "Option", + "itemEnabled": true, + "itemValue": "hello", + }], + }), + ); + client - .set_audio_tracks(TEST_MEDIA, [Some(tracks[0]), None, None, None, None, None]) + .properties_list_property_items(TEST_MEDIA, "prop") .await?; - Ok(()) + server.expect( + "PressInputPropertiesButton", + json!({ + "inputName": "OBWS-TEST-Media", + "propertyName": "prop", + }), + json!(null), + ); + + client.press_properties_button(TEST_MEDIA, "prop").await?; + + server.stop().await } diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 4bea0bc..b1a3479 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "test-integration")] - mod client; mod common; mod config; diff --git a/tests/integration/media_inputs.rs b/tests/integration/media_inputs.rs index bcb99a2..7578d02 100644 --- a/tests/integration/media_inputs.rs +++ b/tests/integration/media_inputs.rs @@ -1,20 +1,64 @@ use anyhow::Result; use obws::common::MediaAction; +use serde_json::json; +use test_log::test; use time::Duration; use crate::common::{self, TEST_MEDIA}; -#[tokio::test] +#[test(tokio::test)] async fn media_inputs() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.media_inputs(); - client.status(TEST_MEDIA).await?; - client.set_cursor(TEST_MEDIA, Duration::seconds(1)).await?; + server.expect( + "GetMediaInputStatus", + json!({"inputName": "OBWS-TEST-Media"}), + json!({ + "mediaState": "OBS_MEDIA_STATE_PLAYING", + "mediaDuration": 12_500, + "mediaCursor": 100, + }), + ); + + let status = client.status(TEST_MEDIA).await?; + + server.expect( + "SetMediaInputCursor", + json!({ + "inputName": "OBWS-TEST-Media", + "mediaCursor": 50, + }), + json!(null), + ); + + client + .set_cursor(TEST_MEDIA, status.cursor.unwrap() / 2) + .await?; + + server.expect( + "OffsetMediaInputCursor", + json!({ + "inputName": "OBWS-TEST-Media", + "mediaCursorOffset": 1000, + }), + json!(null), + ); + client .offset_cursor(TEST_MEDIA, Duration::seconds(1)) .await?; + + server.expect( + "TriggerMediaInputAction", + json!({ + "inputName": "OBWS-TEST-Media", + "mediaAction": "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT", + }), + json!(null), + ); + client.trigger_action(TEST_MEDIA, MediaAction::Next).await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/outputs.rs b/tests/integration/outputs.rs index 133ffec..6707f55 100644 --- a/tests/integration/outputs.rs +++ b/tests/integration/outputs.rs @@ -1,36 +1,81 @@ -#![cfg(feature = "test-integration")] - -use std::time::Duration; - use anyhow::Result; -use tokio::time; +use serde_json::json; +use test_log::test; use crate::common; const OUTPUT_VIRTUALCAM: &str = "virtualcam_output"; -#[tokio::test] +#[test(tokio::test)] async fn outputs() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.outputs(); - time::sleep(Duration::from_secs(1)).await; + server.expect("GetOutputList", json!(null), json!({"outputs": []})); + client.list().await?; + + server.expect( + "GetOutputStatus", + json!({"outputName": "virtualcam_output"}), + json!({ + "outputActive": true, + "outputReconnecting": false, + "outputTimecode": "12:30:45.678", + "outputDuration": 50_000, + "outputCongestion": 0, + "outputBytes": 1024, + "outputSkippedFrames": 0, + "outputTotalFrames": 250, + }), + ); + client.status(OUTPUT_VIRTUALCAM).await?; + server.expect( + "ToggleOutput", + json!({"outputName": "virtualcam_output"}), + json!({"outputActive": false}), + ); + client.toggle(OUTPUT_VIRTUALCAM).await?; - time::sleep(Duration::from_secs(1)).await; - client.toggle(OUTPUT_VIRTUALCAM).await?; - time::sleep(Duration::from_secs(1)).await; + + server.expect( + "StartOutput", + json!({"outputName": "virtualcam_output"}), + json!(null), + ); client.start(OUTPUT_VIRTUALCAM).await?; - time::sleep(Duration::from_secs(1)).await; + + server.expect( + "StopOutput", + json!({"outputName": "virtualcam_output"}), + json!(null), + ); + client.stop(OUTPUT_VIRTUALCAM).await?; + server.expect( + "GetOutputSettings", + json!({"outputName": "virtualcam_output"}), + json!({"outputSettings": {}}), + ); + let settings = client .settings::(OUTPUT_VIRTUALCAM) .await?; + + server.expect( + "SetOutputSettings", + json!({ + "outputName": "virtualcam_output", + "outputSettings": {}, + }), + json!(null), + ); + client.set_settings(OUTPUT_VIRTUALCAM, &settings).await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/profiles.rs b/tests/integration/profiles.rs index 6685784..7b0d506 100644 --- a/tests/integration/profiles.rs +++ b/tests/integration/profiles.rs @@ -1,27 +1,86 @@ -use std::time::Duration; - use anyhow::Result; use obws::{requests::profiles::SetParameter, responses::profiles::Profiles}; -use tokio::time; +use serde_json::json; +use test_log::test; use crate::common; -#[tokio::test] +#[test(tokio::test)] async fn profiles() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.profiles(); + server.expect( + "GetProfileList", + json!(null), + json!({ + "currentProfileName": "main", + "profiles": ["main", "other"], + }), + ); + let Profiles { current, profiles } = client.list().await?; + + server.expect( + "GetProfileList", + json!(null), + json!({ + "currentProfileName": "main", + "profiles": ["main", "other"], + }), + ); + client.current().await?; let other = profiles.iter().find(|p| *p != ¤t).unwrap(); + + server.expect( + "SetCurrentProfile", + json!({"profileName": "other"}), + json!(null), + ); + client.set_current(other).await?; - time::sleep(Duration::from_secs(1)).await; - client.set_current(¤t).await?; - time::sleep(Duration::from_secs(1)).await; + + server.expect( + "CreateProfile", + json!({"profileName": "OBWS-TEST-New-Profile"}), + json!(null), + ); + client.create("OBWS-TEST-New-Profile").await?; + + server.expect( + "RemoveProfile", + json!({"profileName": "OBWS-TEST-New-Profile"}), + json!(null), + ); + client.remove("OBWS-TEST-New-Profile").await?; + server.expect( + "GetProfileParameter", + json!({ + "parameterCategory": "General", + "parameterName": "Name", + }), + json!({ + "parameterValue": "Some", + "defaultParameterValue": null, + }), + ); + client.parameter("General", "Name").await?; + + server.expect( + "SetProfileParameter", + json!({ + "parameterCategory": "OBWS", + "parameterName": "Test", + "parameterValue": "Value", + }), + json!(null), + ); + client .set_parameter(SetParameter { category: "OBWS", @@ -29,13 +88,6 @@ async fn profiles() -> Result<()> { value: Some("Value"), }) .await?; - client - .set_parameter(SetParameter { - category: "OBWS", - name: "Test", - value: None, - }) - .await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/recording.rs b/tests/integration/recording.rs index 78223dc..7f01375 100644 --- a/tests/integration/recording.rs +++ b/tests/integration/recording.rs @@ -1,21 +1,39 @@ -use std::time::Duration; - use anyhow::Result; use obws::events::{Event, OutputState}; -use tokio::time; +use serde_json::json; +use test_log::test; use crate::{common, wait_for}; -#[tokio::test] +#[test(tokio::test)] async fn recording() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let events = client.events()?; let client = client.recording(); tokio::pin!(events); + server.expect( + "GetRecordStatus", + json!(null), + json!({ + "outputActive": false, + "outputPaused": false, + "outputTimecode": "00:00:00.500", + "outputDuration": 500, + "outputBytes": 2048, + }), + ); + client.status().await?; + server.expect("StartRecord", json!(null), json!(null)); + server.send_event(Event::RecordStateChanged { + active: true, + state: OutputState::Started, + path: None, + }); + client.start().await?; wait_for!( events, @@ -24,7 +42,14 @@ async fn recording() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("PauseRecord", json!(null), json!(null)); + server.send_event(Event::RecordStateChanged { + active: true, + state: OutputState::Paused, + path: None, + }); + client.pause().await?; wait_for!( events, @@ -33,7 +58,14 @@ async fn recording() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("ResumeRecord", json!(null), json!(null)); + server.send_event(Event::RecordStateChanged { + active: true, + state: OutputState::Resumed, + path: None, + }); + client.resume().await?; wait_for!( events, @@ -42,7 +74,14 @@ async fn recording() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("StopRecord", json!(null), json!({"outputPath": "/tmp"})); + server.send_event(Event::RecordStateChanged { + active: false, + state: OutputState::Stopped, + path: None, + }); + client.stop().await?; wait_for!( events, @@ -51,7 +90,13 @@ async fn recording() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("ToggleRecord", json!(null), json!({"outputActive": true})); + server.send_event(Event::RecordStateChanged { + active: true, + state: OutputState::Started, + path: None, + }); client.toggle().await?; wait_for!( @@ -61,34 +106,38 @@ async fn recording() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; - client.toggle_pause().await?; - wait_for!( - events, - Event::RecordStateChanged { - state: OutputState::Paused, - .. - } + + server.expect( + "ToggleRecordPause", + json!(null), + json!({"outputPaused": true}), ); - time::sleep(Duration::from_secs(1)).await; + server.send_event(Event::RecordStateChanged { + active: true, + state: OutputState::Paused, + path: None, + }); + client.toggle_pause().await?; wait_for!( events, Event::RecordStateChanged { - state: OutputState::Resumed, + state: OutputState::Paused, .. } ); - time::sleep(Duration::from_secs(1)).await; - client.toggle().await?; - wait_for!( - events, - Event::RecordStateChanged { - state: OutputState::Stopped, - .. - } + + server.expect("SplitRecordFile", json!(null), json!(null)); + + client.split_file().await?; + + server.expect( + "CreateRecordChapter", + json!({"chapterName": "one"}), + json!(null), ); - time::sleep(Duration::from_secs(1)).await; - Ok(()) + client.create_chapter(Some("one")).await?; + + server.stop().await } diff --git a/tests/integration/replay_buffer.rs b/tests/integration/replay_buffer.rs index f246eb8..7a7f453 100644 --- a/tests/integration/replay_buffer.rs +++ b/tests/integration/replay_buffer.rs @@ -1,41 +1,51 @@ -#![cfg(feature = "test-integration")] - -use std::time::Duration; - use anyhow::Result; use obws::events::{Event, OutputState}; -use tokio::time; +use serde_json::json; +use test_log::test; use crate::{common, wait_for}; -#[tokio::test] +#[test(tokio::test)] async fn replay_buffer() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let events = client.events()?; let client = client.replay_buffer(); tokio::pin!(events); + server.expect( + "GetReplayBufferStatus", + json!(null), + json!({"outputActive": false}), + ); + client.status().await?; - client.toggle().await?; - wait_for!( - events, - Event::ReplayBufferStateChanged { - state: OutputState::Started, - .. - } + server.expect( + "ToggleReplayBuffer", + json!(null), + json!({"outputActive": false}), ); - time::sleep(Duration::from_secs(1)).await; + server.send_event(Event::ReplayBufferStateChanged { + active: true, + state: OutputState::Started, + }); + client.toggle().await?; wait_for!( events, Event::ReplayBufferStateChanged { - state: OutputState::Stopped, + state: OutputState::Started, .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("StartReplayBuffer", json!(null), json!(null)); + server.send_event(Event::ReplayBufferStateChanged { + active: true, + state: OutputState::Started, + }); + client.start().await?; wait_for!( events, @@ -44,9 +54,25 @@ async fn replay_buffer() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("SaveReplayBuffer", json!(null), json!(null)); + client.save().await?; + + server.expect( + "GetLastReplayBufferReplay", + json!(null), + json!({"savedReplayPath": "/tmp"}), + ); + client.last_replay().await?; + + server.expect("StopReplayBuffer", json!(null), json!(null)); + server.send_event(Event::ReplayBufferStateChanged { + active: true, + state: OutputState::Stopped, + }); + client.stop().await?; wait_for!( events, @@ -55,7 +81,6 @@ async fn replay_buffer() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; - Ok(()) + server.stop().await } diff --git a/tests/integration/scene_collections.rs b/tests/integration/scene_collections.rs index 4b6e385..f2036d1 100644 --- a/tests/integration/scene_collections.rs +++ b/tests/integration/scene_collections.rs @@ -1,26 +1,56 @@ -use std::time::Duration; - use anyhow::Result; use obws::responses::scene_collections::SceneCollections; -use tokio::time; +use serde_json::json; +use test_log::test; use crate::common; -#[tokio::test] +#[test(tokio::test)] async fn scene_collections() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.scene_collections(); + server.expect( + "GetSceneCollectionList", + json!(null), + json!({ + "currentSceneCollectionName": "main", + "sceneCollections": ["main", "other"], + }), + ); + let SceneCollections { current, collections, } = client.list().await?; + + server.expect( + "GetSceneCollectionList", + json!(null), + json!({ + "currentSceneCollectionName": "main", + "sceneCollections": ["main", "other"], + }), + ); + client.current().await?; let other = collections.iter().find(|sc| *sc != ¤t).unwrap(); + + server.expect( + "SetCurrentSceneCollection", + json!({"sceneCollectionName": "other"}), + json!(null), + ); + client.set_current(other).await?; - time::sleep(Duration::from_secs(1)).await; - client.set_current(¤t).await?; - time::sleep(Duration::from_secs(1)).await; - Ok(()) + server.expect( + "CreateSceneCollection", + json!({"sceneCollectionName": "new"}), + json!(null), + ); + + client.create("new").await?; + + server.stop().await } diff --git a/tests/integration/scene_items.rs b/tests/integration/scene_items.rs index 2223faa..b183f38 100644 --- a/tests/integration/scene_items.rs +++ b/tests/integration/scene_items.rs @@ -3,20 +3,45 @@ use obws::{ common::{BlendMode, BoundsType}, requests::scene_items::{ Bounds, CreateSceneItem, Duplicate, Id, SceneItemTransform, SetBlendMode, SetEnabled, - SetIndex, SetLocked, SetTransform, + SetIndex, SetLocked, SetPrivateSettings, SetTransform, Source, }, }; +use serde_json::json; +use test_log::test; +use uuid::Uuid; use crate::common::{self, TEST_GROUP, TEST_SCENE, TEST_SCENE_2, TEST_TEXT}; -#[tokio::test] +#[test(tokio::test)] async fn scene_items() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.scene_items(); + server.expect( + "GetSceneItemList", + json!({"sceneName": "OBWS-TEST-Scene"}), + json!({"sceneItems": []}), + ); + client.list(TEST_SCENE).await?; + + server.expect( + "GetGroupSceneItemList", + json!({"sceneName": "OBWS-TEST-Group"}), + json!({"sceneItems": []}), + ); + client.list_group(TEST_GROUP).await?; + server.expect( + "GetSceneItemId", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sourceName": "OBWS-TEST-Text", + }), + json!({"sceneItemId": 1}), + ); + let test_text_id = client .id(Id { scene: TEST_SCENE, @@ -25,14 +50,52 @@ async fn scene_items() -> Result<()> { }) .await?; - let id = client + server.expect( + "GetSceneItemSource", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({ + "sourceName": "test-source", + "sourceUuid": Uuid::nil(), + }), + ); + + client + .source(Source { + scene: TEST_SCENE, + item_id: test_text_id, + }) + .await?; + + server.expect( + "DuplicateSceneItem", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "destinationSceneName": "OBWS-TEST-Scene2", + }), + json!({"sceneItemId": 2}), + ); + + client .duplicate(Duplicate { scene: TEST_SCENE, item_id: test_text_id, destination: Some(TEST_SCENE_2.into()), }) .await?; - client.remove(TEST_SCENE_2, id).await?; + + server.expect( + "CreateSceneItem", + json!({ + "sceneName": "OBWS-TEST-Scene2", + "sourceName": "OBWS-TEST-Text", + "sceneItemEnabled": true, + }), + json!({"sceneItemId": 3}), + ); let id = client .create(CreateSceneItem { @@ -41,29 +104,70 @@ async fn scene_items() -> Result<()> { enabled: Some(true), }) .await?; + + server.expect( + "RemoveSceneItem", + json!({ + "sceneName": "OBWS-TEST-Scene2", + "sceneItemId": 3, + }), + json!(null), + ); + client.remove(TEST_SCENE_2, id).await?; - let transform = client.transform(TEST_SCENE, test_text_id).await?; - client - .set_transform(SetTransform { - scene: TEST_SCENE, - item_id: test_text_id, - transform: SceneItemTransform { - bounds: Some(Bounds { - r#type: Some(BoundsType::Stretch), - ..Bounds::default() - }), - ..SceneItemTransform::default() + server.expect( + "GetSceneItemTransform", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({ + "sceneItemTransform": { + "sourceWidth": 1920, + "sourceHeight": 1080, + "positionX": 5, + "positionY": 10, + "rotation": 30.0, + "scaleX": 1.1, + "scaleY": 1.2, + "width": 800, + "height": 600, + "alignment": 0b1111, + "boundsType": "OBS_BOUNDS_SCALE_OUTER", + "boundsAlignment": 0, + "boundsWidth": 200, + "boundsHeight": 100, + "cropLeft": 1, + "cropRight": 2, + "cropTop": 3, + "cropBottom": 4, + "cropToBounds": false, }, - }) - .await?; + }), + ); + + client.transform(TEST_SCENE, test_text_id).await?; + + server.expect( + "SetSceneItemTransform", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "sceneItemTransform": { + "boundsType": "OBS_BOUNDS_STRETCH", + }, + }), + json!(null), + ); + client .set_transform(SetTransform { scene: TEST_SCENE, item_id: test_text_id, transform: SceneItemTransform { bounds: Some(Bounds { - r#type: Some(transform.bounds_type), + r#type: Some(BoundsType::Stretch), ..Bounds::default() }), ..SceneItemTransform::default() @@ -71,7 +175,27 @@ async fn scene_items() -> Result<()> { }) .await?; + server.expect( + "GetSceneItemEnabled", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({"sceneItemEnabled": true}), + ); + let enabled = client.enabled(TEST_SCENE, test_text_id).await?; + + server.expect( + "SetSceneItemEnabled", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "sceneItemEnabled": false, + }), + json!(null), + ); + client .set_enabled(SetEnabled { scene: TEST_SCENE, @@ -79,15 +203,28 @@ async fn scene_items() -> Result<()> { enabled: !enabled, }) .await?; - client - .set_enabled(SetEnabled { - scene: TEST_SCENE, - item_id: test_text_id, - enabled, - }) - .await?; + + server.expect( + "GetSceneItemLocked", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({"sceneItemLocked": false}), + ); let locked = client.locked(TEST_SCENE, test_text_id).await?; + + server.expect( + "SetSceneItemLocked", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "sceneItemLocked": true, + }), + json!(null), + ); + client .set_locked(SetLocked { scene: TEST_SCENE, @@ -95,15 +232,28 @@ async fn scene_items() -> Result<()> { locked: !locked, }) .await?; - client - .set_locked(SetLocked { - scene: TEST_SCENE, - item_id: test_text_id, - locked, - }) - .await?; - let index = client.index(TEST_SCENE, test_text_id).await?; + server.expect( + "GetSceneItemIndex", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({"sceneItemIndex": 1}), + ); + + client.index(TEST_SCENE, test_text_id).await?; + + server.expect( + "SetSceneItemIndex", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "sceneItemIndex": 0, + }), + json!(null), + ); + client .set_index(SetIndex { scene: TEST_SCENE, @@ -111,16 +261,29 @@ async fn scene_items() -> Result<()> { index: 0, }) .await?; - client - .set_index(SetIndex { - scene: TEST_SCENE, - item_id: test_text_id, - index, - }) - .await?; + + server.expect( + "GetSceneItemBlendMode", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({"sceneItemBlendMode": "OBS_BLEND_NORMAL"}), + ); let mode = client.blend_mode(TEST_SCENE, test_text_id).await?; assert_eq!(BlendMode::Normal, mode); + + server.expect( + "SetSceneItemBlendMode", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "sceneItemBlendMode": "OBS_BLEND_MULTIPLY", + }), + json!(null), + ); + client .set_blend_mode(SetBlendMode { scene: TEST_SCENE, @@ -128,28 +291,37 @@ async fn scene_items() -> Result<()> { mode: BlendMode::Multiply, }) .await?; + + server.expect( + "GetSceneItemPrivateSettings", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + }), + json!({"sceneItemSettings": {}}), + ); + + let settings = client + .private_settings::(TEST_SCENE, test_text_id) + .await?; + + server.expect( + "SetSceneItemPrivateSettings", + json!({ + "sceneName": "OBWS-TEST-Scene", + "sceneItemId": 1, + "sceneItemSettings": {}, + }), + json!(null), + ); + client - .set_blend_mode(SetBlendMode { + .set_private_settings(SetPrivateSettings { scene: TEST_SCENE, item_id: test_text_id, - mode, + settings: &settings, }) .await?; - let _settings = client - .private_settings::(TEST_SCENE, test_text_id) - .await?; - - // TODO: Currently obs-websocket doesn't accept empty objects `{}`, and this fails as our - // test scene item doesn't have any private settings. - // - // client - // .set_private_settings(SetPrivateSettings { - // scene: TEST_SCENE, - // item_id: test_text_id, - // settings: &settings, - // }) - // .await?; - - Ok(()) + server.stop().await } diff --git a/tests/integration/scenes.rs b/tests/integration/scenes.rs index c682204..24e508e 100644 --- a/tests/integration/scenes.rs +++ b/tests/integration/scenes.rs @@ -1,41 +1,145 @@ use anyhow::Result; use obws::requests::scenes::SetTransitionOverride; +use serde_json::json; +use test_log::test; use time::Duration; +use uuid::Uuid; use crate::common::{self, TEST_SCENE, TEST_SCENE_CREATE, TEST_SCENE_RENAME, TEST_TRANSITION}; -#[tokio::test] +#[test(tokio::test)] async fn scenes() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let ui = client.ui(); let client = client.scenes(); + server.expect( + "SetStudioModeEnabled", + json!({"studioModeEnabled": true}), + json!(null), + ); + ui.set_studio_mode_enabled(true).await?; + server.expect( + "GetSceneList", + json!(null), + json!({ + "currentProgramSceneName": "main", + "currentProgramSceneUuid": Uuid::new_v8([1; 16]), + "currentPreviewSceneName": "other", + "currentPreviewSceneUuid": Uuid::new_v8([2; 16]), + "scenes": [ + { + "sceneName": "main", + "sceneUuid": Uuid::new_v8([1; 16]), + "sceneIndex": 0, + }, + { + "sceneName": "other", + "sceneUuid": Uuid::new_v8([2; 16]), + "sceneIndex": 1, + }, + ], + }), + ); + let scenes = client.list().await?.scenes; + + server.expect("GetGroupList", json!(null), json!({"groups": ["one"]})); + client.list_groups().await?; + server.expect( + "GetCurrentProgramScene", + json!(null), + json!({ + "sceneName": "main", + "sceneUuid": Uuid::new_v8([1; 16]), + }), + ); + let current = client.current_program_scene().await?; + + server.expect( + "SetCurrentProgramScene", + json!({"sceneUuid": Uuid::new_v8([2; 16])}), + json!(null), + ); + let other = &scenes.iter().find(|s| s.id != current.id).unwrap().id; client.set_current_program_scene(other).await?; - client.set_current_program_scene(current.id).await?; + + server.expect( + "GetCurrentPreviewScene", + json!(null), + json!({ + "sceneName": "main", + "sceneUuid": Uuid::new_v8([2; 16]), + }), + ); let current = client.current_preview_scene().await?; + + server.expect( + "SetCurrentPreviewScene", + json!({"sceneUuid": Uuid::new_v8([1; 16])}), + json!(null), + ); + let other = &scenes.iter().find(|s| s.id != current.id).unwrap().id; client.set_current_preview_scene(other).await?; - client.set_current_preview_scene(current.id).await?; + + server.expect( + "SetSceneName", + json!({ + "sceneName": "OBWS-TEST-Scene", + "newSceneName": "OBWS-TEST-Scene-Renamed", + }), + json!(null), + ); client .set_name(TEST_SCENE, TEST_SCENE_RENAME.as_name().unwrap()) .await?; - client - .set_name(TEST_SCENE_RENAME, TEST_SCENE.as_name().unwrap()) - .await?; + + server.expect( + "CreateScene", + json!({"sceneName": "OBWS-TEST-Scene-Created"}), + json!({"sceneUuid": Uuid::new_v8([3; 16])}), + ); client.create(TEST_SCENE_CREATE.as_name().unwrap()).await?; + + server.expect( + "RemoveScene", + json!({"sceneName": "OBWS-TEST-Scene-Created"}), + json!(null), + ); + client.remove(TEST_SCENE_CREATE).await?; - let to = client.transition_override(TEST_SCENE).await?; + server.expect( + "GetSceneSceneTransitionOverride", + json!({"sceneName": "OBWS-TEST-Scene"}), + json!({ + "transitionName": "some-transition", + "transitionDuration": 500, + }), + ); + + client.transition_override(TEST_SCENE).await?; + + server.expect( + "SetSceneSceneTransitionOverride", + json!({ + "sceneName": "OBWS-TEST-Scene", + "transitionName": "OBWS-TEST-Transition", + "transitionDuration": 5000, + }), + json!(null), + ); + client .set_transition_override(SetTransitionOverride { scene: TEST_SCENE, @@ -43,15 +147,14 @@ async fn scenes() -> Result<()> { duration: Some(Duration::seconds(5)), }) .await?; - client - .set_transition_override(SetTransitionOverride { - scene: TEST_SCENE, - transition: to.name.as_deref(), - duration: to.duration, - }) - .await?; + + server.expect( + "SetStudioModeEnabled", + json!({"studioModeEnabled": false}), + json!(null), + ); ui.set_studio_mode_enabled(false).await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/sources.rs b/tests/integration/sources.rs index e776f90..859e5b1 100644 --- a/tests/integration/sources.rs +++ b/tests/integration/sources.rs @@ -1,16 +1,40 @@ -use std::env; +use std::path::Path; use anyhow::Result; use obws::requests::sources::{SaveScreenshot, TakeScreenshot}; +use serde_json::json; +use test_log::test; use crate::common::{self, TEST_TEXT}; -#[tokio::test] +#[test(tokio::test)] async fn sources() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.sources(); + server.expect( + "GetSourceActive", + json!({"sourceName": "OBWS-TEST-Text"}), + json!({ + "videoActive": true, + "videoShowing": true, + }), + ); + client.active(TEST_TEXT.as_source()).await?; + + server.expect( + "GetSourceScreenshot", + json!({ + "sourceName": "OBWS-TEST-Text", + "imageFormat": "jpg", + "imageWidth": 100, + "imageHeight": 100, + "imageCompressionQuality": 50, + }), + json!({"imageData": ""}), + ); + client .take_screenshot(TakeScreenshot { source: TEST_TEXT.as_source(), @@ -21,11 +45,20 @@ async fn sources() -> Result<()> { }) .await?; - let file = env::temp_dir().join("obws-test-image.png"); + server.expect( + "SaveSourceScreenshot", + json!({ + "sourceName": "OBWS-TEST-Text", + "imageFormat": "png", + "imageFilePath": "/tmp/file.png", + }), + json!(null), + ); + client .save_screenshot(SaveScreenshot { source: TEST_TEXT.as_source(), - file_path: &file, + file_path: Path::new("/tmp/file.png"), width: None, height: None, compression_quality: None, @@ -33,5 +66,5 @@ async fn sources() -> Result<()> { }) .await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/streaming.rs b/tests/integration/streaming.rs index e365acc..fbc55c2 100644 --- a/tests/integration/streaming.rs +++ b/tests/integration/streaming.rs @@ -1,17 +1,50 @@ use anyhow::Result; +use serde_json::json; +use test_log::test; use crate::common; -#[tokio::test] +#[test(tokio::test)] async fn streaming() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.streaming(); + server.expect( + "GetStreamStatus", + json!(null), + json!({ + "outputActive": false, + "outputReconnecting": false, + "outputTimecode": "00:00:00.000", + "outputDuration": 0, + "outputCongestion": 0, + "outputBytes": 0, + "outputSkippedFrames": 0, + "outputTotalFrames": 0, + }), + ); + client.status().await?; - // TODO: Dangerous to run as it would make us live stream. - // client.start_stream().await?; - // client.stop_stream().await?; + server.expect("StartStream", json!(null), json!(null)); + + client.start().await?; + + server.expect("StopStream", json!(null), json!(null)); + + client.stop().await?; + + server.expect("ToggleStream", json!(null), json!({"outputActive": true})); + + client.toggle().await?; + + server.expect( + "SendStreamCaption", + json!({"captionText": "test"}), + json!(null), + ); + + client.send_caption("test").await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/transitions.rs b/tests/integration/transitions.rs index d2c8839..2f7a4e9 100644 --- a/tests/integration/transitions.rs +++ b/tests/integration/transitions.rs @@ -1,33 +1,118 @@ use anyhow::Result; +use serde_json::json; +use test_log::test; +use uuid::Uuid; use crate::common::{self, TEST_TRANSITION}; -#[tokio::test] +#[test(tokio::test)] async fn transitions() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let ui = client.ui(); let client = client.transitions(); + server.expect( + "GetTransitionKindList", + json!(null), + json!({"transitionKinds": ["fade"]}), + ); + client.list_kinds().await?; + + server.expect( + "GetSceneTransitionList", + json!(null), + json!({ + "currentSceneTransitionName": "main", + "currentSceneTransitionUuid": Uuid::new_v8([1; 16]), + "currentSceneTransitionKind": "fade", + "transitions": [{ + "transitionName": "main", + "transitionUuid": Uuid::new_v8([1; 16]), + "transitionKind": "fade", + "transitionFixed": false, + "transitionConfigurable": false, + }], + }), + ); + client.list().await?; + server.expect( + "SetCurrentSceneTransition", + json!({"transitionName": "OBWS-TEST-Transition"}), + json!(null), + ); + client.set_current(TEST_TRANSITION).await?; + + server.expect( + "GetCurrentSceneTransition", + json!(null), + json!({ + "transitionName": "OBWS-TEST-Transition", + "transitionUuid": Uuid::new_v8([1; 16]), + "transitionKind": "fade", + "transitionFixed": false, + "transitionDuration": 1000, + "transitionConfigurable": true, + "transitionSettings": {}, + }), + ); + let transition = client.current().await?; + server.expect( + "SetCurrentSceneTransitionDuration", + json!({"transitionDuration": 500}), + json!(null), + ); + client - .set_current_duration(transition.duration.unwrap()) + .set_current_duration(transition.duration.unwrap() / 2) .await?; + + server.expect( + "SetCurrentSceneTransitionSettings", + json!({"transitionSettings": {}}), + json!(null), + ); + client .set_current_settings(transition.settings.unwrap(), None) .await?; + + server.expect( + "GetCurrentSceneTransitionCursor", + json!(null), + json!({"transitionCursor": 0.1}), + ); + client.current_cursor().await?; + server.expect( + "SetStudioModeEnabled", + json!({"studioModeEnabled": true}), + json!(null), + ); + ui.set_studio_mode_enabled(true).await?; + server.expect("TriggerStudioModeTransition", json!(null), json!(null)); + client.trigger().await?; + + server.expect("SetTBarPosition", json!({"position": 0.5}), json!(null)); + client.set_tbar_position(0.5, None).await?; + server.expect( + "SetStudioModeEnabled", + json!({"studioModeEnabled": false}), + json!(null), + ); + ui.set_studio_mode_enabled(false).await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/ui.rs b/tests/integration/ui.rs index 9cec5d9..68fddd5 100644 --- a/tests/integration/ui.rs +++ b/tests/integration/ui.rs @@ -2,33 +2,108 @@ use anyhow::Result; use obws::requests::ui::{ Location, OpenSourceProjector, OpenVideoMixProjector, QtGeometry, QtRect, VideoMixType, }; +use serde_json::json; +use test_log::test; use crate::common::{self, TEST_TEXT}; -#[tokio::test] +#[test(tokio::test)] async fn ui() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let client = client.ui(); + server.expect( + "GetStudioModeEnabled", + json!(null), + json!({"studioModeEnabled": false}), + ); + let enabled = client.studio_mode_enabled().await?; + + server.expect( + "SetStudioModeEnabled", + json!({"studioModeEnabled": true}), + json!(null), + ); + client.set_studio_mode_enabled(!enabled).await?; - client.set_studio_mode_enabled(enabled).await?; + + server.expect( + "OpenInputPropertiesDialog", + json!({"inputName": "OBWS-TEST-Text"}), + json!(null), + ); + + client.open_properties_dialog(TEST_TEXT).await?; + + server.expect( + "OpenInputFiltersDialog", + json!({"inputName": "OBWS-TEST-Text"}), + json!(null), + ); + + client.open_filters_dialog(TEST_TEXT).await?; + + server.expect( + "OpenInputInteractDialog", + json!({"inputName": "OBWS-TEST-Text"}), + json!(null), + ); + + client.open_interact_dialog(TEST_TEXT).await?; + + server.expect( + "GetMonitorList", + json!(null), + json!({ + "monitors": [{ + "monitorName": "sample", + "monitorIndex": 0, + "monitorWidth": 640, + "monitorHeight": 480, + "monitorPositionX": 5, + "monitorPositionY": 10, + }], + }), + ); client.list_monitors().await?; + + let geometry = QtGeometry { + rect: QtRect { + left: 50, + top: 150, + right: 250, + bottom: 350, + }, + ..QtGeometry::default() + }; + + server.expect( + "OpenVideoMixProjector", + json!({ + "videoMixType": "OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW", + "projectorGeometry": geometry.to_string(), + }), + json!(null), + ); + client .open_video_mix_projector(OpenVideoMixProjector { r#type: VideoMixType::Preview, - location: Some(Location::ProjectorGeometry(QtGeometry { - rect: QtRect { - left: 50, - top: 150, - right: 250, - bottom: 350, - }, - ..QtGeometry::default() - })), + location: Some(Location::ProjectorGeometry(geometry)), }) .await?; + + server.expect( + "OpenSourceProjector", + json!({ + "sourceName": "OBWS-TEST-Text", + "monitorIndex": -1, + }), + json!(null), + ); + client .open_source_projector(OpenSourceProjector { source: TEST_TEXT.as_source(), @@ -36,5 +111,5 @@ async fn ui() -> Result<()> { }) .await?; - Ok(()) + server.stop().await } diff --git a/tests/integration/virtual_cam.rs b/tests/integration/virtual_cam.rs index 581756e..ac5bc4f 100644 --- a/tests/integration/virtual_cam.rs +++ b/tests/integration/virtual_cam.rs @@ -1,41 +1,51 @@ -#![cfg(feature = "test-integration")] - -use std::time::Duration; - use anyhow::Result; use obws::events::{Event, OutputState}; -use tokio::time; +use serde_json::json; +use test_log::test; use crate::{common, wait_for}; -#[tokio::test] +#[test(tokio::test)] async fn virtual_cam() -> Result<()> { - let client = common::new_client().await?; + let (client, server) = common::new_client().await?; let events = client.events()?; let client = client.virtual_cam(); tokio::pin!(events); + server.expect( + "GetVirtualCamStatus", + json!(null), + json!({"outputActive": false}), + ); + client.status().await?; - client.toggle().await?; - wait_for!( - events, - Event::VirtualcamStateChanged { - state: OutputState::Started, - .. - } + server.expect( + "ToggleVirtualCam", + json!(null), + json!({"outputActive": true}), ); - time::sleep(Duration::from_secs(1)).await; + server.send_event(Event::VirtualcamStateChanged { + active: true, + state: OutputState::Started, + }); + client.toggle().await?; wait_for!( events, Event::VirtualcamStateChanged { - state: OutputState::Stopped, + state: OutputState::Started, .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("StartVirtualCam", json!(null), json!(null)); + server.send_event(Event::VirtualcamStateChanged { + active: true, + state: OutputState::Started, + }); + client.start().await?; wait_for!( events, @@ -44,7 +54,13 @@ async fn virtual_cam() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; + + server.expect("StopVirtualCam", json!(null), json!(null)); + server.send_event(Event::VirtualcamStateChanged { + active: false, + state: OutputState::Stopped, + }); + client.stop().await?; wait_for!( events, @@ -53,7 +69,6 @@ async fn virtual_cam() -> Result<()> { .. } ); - time::sleep(Duration::from_secs(1)).await; - Ok(()) + server.stop().await }