diff --git a/crux_core/src/capability/mod.rs b/crux_core/src/capability/mod.rs index 621c1786..93f2b327 100644 --- a/crux_core/src/capability/mod.rs +++ b/crux_core/src/capability/mod.rs @@ -213,7 +213,7 @@ use channel::Sender; /// type Output = HttpResponse; /// } /// ``` -pub trait Operation: serde::Serialize + PartialEq + Send + 'static { +pub trait Operation: serde::Serialize + Clone + PartialEq + Send + 'static { /// `Output` assigns the type this request results in. type Output: serde::de::DeserializeOwned + Send + 'static; } @@ -238,7 +238,7 @@ pub trait Operation: serde::Serialize + PartialEq + Send + 'static { /// # } /// /// ``` -#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum Never {} /// Implement `Operation` for `Never` to allow using it as a capability operation. @@ -258,7 +258,7 @@ impl Operation for Never { /// # pub struct Http { /// # context: CapabilityContext, /// # } -/// # #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct HttpRequest; +/// # #[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct HttpRequest; /// # impl Operation for HttpRequest { /// # type Output = (); /// # } @@ -372,8 +372,8 @@ pub trait WithContext { /// For example (from `crux_time`) /// /// ```rust -/// # #[derive(PartialEq,serde::Serialize)]pub struct TimeRequest; -/// # #[derive(serde::Deserialize)]pub struct TimeResponse(pub String); +/// # #[derive(Clone, PartialEq, serde::Serialize)] pub struct TimeRequest; +/// # #[derive(Clone, serde::Deserialize)] pub struct TimeResponse(pub String); /// # impl crux_core::capability::Operation for TimeRequest { /// # type Output = TimeResponse; /// # } @@ -625,7 +625,7 @@ mod tests { #[allow(dead_code)] enum Event {} - #[derive(PartialEq, Serialize)] + #[derive(PartialEq, Clone, Serialize)] struct Op {} impl Operation for Op { diff --git a/crux_core/src/capability/shell_request.rs b/crux_core/src/capability/shell_request.rs index a6f55f08..776acfa0 100644 --- a/crux_core/src/capability/shell_request.rs +++ b/crux_core/src/capability/shell_request.rs @@ -119,7 +119,7 @@ mod tests { use crate::capability::{channel, executor_and_spawner, CapabilityContext, Operation}; - #[derive(serde::Serialize, PartialEq, Eq, Debug)] + #[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)] struct TestOperation; impl Operation for TestOperation { diff --git a/crux_core/src/capability/shell_stream.rs b/crux_core/src/capability/shell_stream.rs index 87faa2e6..c17c46d5 100644 --- a/crux_core/src/capability/shell_stream.rs +++ b/crux_core/src/capability/shell_stream.rs @@ -92,7 +92,7 @@ mod tests { use crate::capability::{channel, executor_and_spawner, CapabilityContext, Operation}; - #[derive(serde::Serialize, PartialEq, Eq, Debug)] + #[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)] struct TestOperation; impl Operation for TestOperation { diff --git a/crux_core/src/testing.rs b/crux_core/src/testing.rs index 7260d97b..608cf306 100644 --- a/crux_core/src/testing.rs +++ b/crux_core/src/testing.rs @@ -1,6 +1,6 @@ //! Testing support for unit testing Crux apps. use anyhow::Result; -use std::sync::Arc; +use std::{collections::VecDeque, sync::Arc}; use crate::{ capability::{ @@ -203,6 +203,30 @@ impl Update { self.events.len() ); } + + /// Take effects matching the `predicate` out of the [`Update`] + /// and return them, mutating the `Update` + pub fn take_effects

(&mut self, predicate: P) -> VecDeque + where + P: FnMut(&Ef) -> bool, + { + let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate); + + self.effects = other_effects.into_iter().collect(); + + matching_effects + } + + /// Take all of the effects out of the [`Update`] + /// and split them into those matching `predicate` and the rest + pub fn take_effects_partitioned_by

(&mut self, predicate: P) -> (VecDeque, VecDeque) + where + P: FnMut(&Ef) -> bool, + { + std::mem::take(&mut self.effects) + .into_iter() + .partition(predicate) + } } /// Panics if the pattern doesn't match an `Effect` from the specified `Update` diff --git a/crux_core/tests/capability_orchestration.rs b/crux_core/tests/capability_orchestration.rs index 937666a0..cd82d4a0 100644 --- a/crux_core/tests/capability_orchestration.rs +++ b/crux_core/tests/capability_orchestration.rs @@ -62,7 +62,7 @@ pub mod capabilities { use crux_core::macros::Capability; use serde::{Deserialize, Serialize}; - #[derive(PartialEq, Serialize, Deserialize, Debug)] + #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] pub struct OpOne { number: usize, } @@ -123,7 +123,7 @@ pub mod capabilities { use crux_core::macros::Capability; use serde::{Deserialize, Serialize}; - #[derive(PartialEq, Serialize, Deserialize, Debug)] + #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] pub struct OpTwo { number: usize, } diff --git a/crux_http/tests/with_tester.rs b/crux_http/tests/with_tester.rs index 5e2d523a..ab9757c5 100644 --- a/crux_http/tests/with_tester.rs +++ b/crux_http/tests/with_tester.rs @@ -143,19 +143,19 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::Get, &mut model); - - let Effect::Http(request) = update.effects_mut().next().unwrap(); - let http_request = &request.operation; + let request = &mut app + .update(Event::Get, &mut model) + .expect_one_effect() + .expect_http(); assert_eq!( - *http_request, + request.operation, HttpRequest::get("http://example.com/") .header("authorization", "secret-token") .build() ); - let update = app + let actual = app .resolve( request, HttpResult::Ok( @@ -166,19 +166,17 @@ mod tests { .build(), ), ) - .expect("Resolves successfully"); + .expect("Resolves successfully") + .expect_one_event(); - let actual = update.events.clone(); - assert_matches!(&actual[..], [Event::Set(Ok(response))] => { - assert_eq!(*response.body().unwrap(), "\"hello\"".to_string()); - assert_eq!(*response.header("my_header").unwrap().iter() + assert_matches!(actual.clone(), Event::Set(Ok(response)) => { + assert_eq!(response.body().unwrap(), "\"hello\""); + assert_eq!(response.header("my_header").unwrap().iter() .map(|v| v.to_string()) .collect::>(), vec!["my_value1", "my_value2"]); }); - for event in update.events { - app.update(event, &mut model).assert_empty(); - } + app.update(actual, &mut model).assert_empty(); assert_eq!(model.body, "\"hello\""); assert_eq!(model.values, vec!["my_value1", "my_value2"]); } @@ -188,9 +186,10 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::Post, &mut model); - - let Effect::Http(request) = update.effects_mut().next().unwrap(); + let request = &mut app + .update(Event::Post, &mut model) + .expect_one_effect() + .expect_http(); assert_eq!( request.operation, @@ -200,16 +199,16 @@ mod tests { .build() ); - let update = app + let actual = app .resolve( request, HttpResult::Ok(HttpResponse::ok().json("The Body").build()), ) - .expect("Resolves successfully"); + .expect("Resolves successfully") + .expect_one_event(); - let actual = update.events; - assert_matches!(&actual[..], [Event::Set(Ok(response))] => { - assert_eq!(*response.body().unwrap(), "\"The Body\"".to_string()); + assert_matches!(actual, Event::Set(Ok(response)) => { + assert_eq!(response.body().unwrap(), "\"The Body\""); }); } @@ -218,40 +217,37 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::GetPostChain, &mut model); - - let Effect::Http(request) = update.effects_mut().next().unwrap(); - let http_request = &request.operation; + let request = &mut app + .update(Event::GetPostChain, &mut model) + .expect_one_effect() + .expect_http(); assert_eq!( - *http_request, + request.operation, HttpRequest::get("http://example.com/").build() ); - let mut update = app + let request = &mut app .resolve( request, HttpResult::Ok(HttpResponse::ok().body("secret_place").build()), ) - .expect("Resolves successfully"); - - assert!(update.events.is_empty()); - - let Effect::Http(request) = update.effects_mut().next().unwrap(); - let http_request = &request.operation; + .expect("Resolves successfully") + .expect_one_effect() + .expect_http(); assert_eq!( - *http_request, + request.operation, HttpRequest::post("http://example.com/secret_place").build() ); - let update = app + let actual = app .resolve(request, HttpResult::Ok(HttpResponse::status(201).build())) - .expect("Resolves successfully"); + .expect("Resolves successfully") + .expect_one_event(); - let actual = update.events.clone(); - assert_matches!(&actual[..], [Event::ComposeComplete(status)] => { - assert_eq!(*status, 201); + assert_matches!(actual, Event::ComposeComplete(status) => { + assert_eq!(status, 201); }); } @@ -260,22 +256,21 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::ConcurrentGets, &mut model); - let mut effects = update.effects_mut(); + let mut requests = app + .update(Event::ConcurrentGets, &mut model) + .take_effects(Effect::is_http); - let Effect::Http(request_one) = effects.next().unwrap(); - let http_request = &request_one.operation; + let request_one = &mut requests.pop_front().unwrap().expect_http(); assert_eq!( - *http_request, + request_one.operation, HttpRequest::get("http://example.com/one").build() ); - let Effect::Http(request_two) = effects.next().unwrap(); - let http_request = &request_two.operation; + let request_two = &mut requests.pop_front().unwrap().expect_http(); assert_eq!( - *http_request, + request_two.operation, HttpRequest::get("http://example.com/two").build() ); @@ -291,16 +286,16 @@ mod tests { assert!(update.effects.is_empty()); assert!(update.events.is_empty()); - let update = app + let actual = app .resolve( request_one, HttpResult::Ok(HttpResponse::ok().body("one").build()), ) - .expect("Resolves successfully"); + .expect("Resolves successfully") + .expect_one_event(); - let actual = update.events.clone(); - assert_matches!(&actual[..], [Event::ComposeComplete(status)] => { - assert_eq!(*status, 200); + assert_matches!(actual, Event::ComposeComplete(status) => { + assert_eq!(status, 200); }); } @@ -309,31 +304,29 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::Get, &mut model); - - let mut effects = update.effects_mut(); - let Effect::Http(request) = effects.next().unwrap(); - let http_request = &request.operation; + let request = &mut app + .update(Event::Get, &mut model) + .expect_one_effect() + .expect_http(); assert_eq!( - *http_request, + request.operation, HttpRequest::get("http://example.com/") .header("authorization", "secret-token") .build() ); - let update = app + let actual = app .resolve( request, HttpResult::Err(crux_http::HttpError::Io( "Socket shenanigans prevented the request".to_string(), )), ) - .expect("Resolves successfully"); - - let actual = update.events.clone(); + .expect("Resolves successfully") + .expect_one_event(); - let Event::Set(Err(crux_http::HttpError::Io(error))) = &actual[0] else { + let Event::Set(Err(crux_http::HttpError::Io(error))) = actual else { panic!("Expected original error back") }; diff --git a/crux_kv/src/tests.rs b/crux_kv/src/tests.rs index 080a4e76..06b75343 100644 --- a/crux_kv/src/tests.rs +++ b/crux_kv/src/tests.rs @@ -137,32 +137,27 @@ fn test_get() { let app = AppTester::::default(); let mut model = Model::default(); - let updated = app.update(Event::Get, &mut model); - - let effect = updated.into_effects().next().unwrap(); - let Effect::KeyValue(mut request) = effect else { - panic!("Expected KeyValue effect"); - }; - - let KeyValueOperation::Get { key } = request.operation.clone() else { - panic!("Expected get operation"); - }; + let request = &mut app + .update(Event::Get, &mut model) + .expect_one_effect() + .expect_key_value(); - assert_eq!(key, "test"); + assert_eq!( + request.operation, + KeyValueOperation::Get { + key: "test".to_string() + } + ); - let updated = app - .resolve( - &mut request, - KeyValueResult::Ok { - response: KeyValueResponse::Get { - value: 42i32.to_ne_bytes().to_vec().into(), - }, + let _updated = app.resolve_to_event_then_update( + request, + KeyValueResult::Ok { + response: KeyValueResponse::Get { + value: 42i32.to_ne_bytes().to_vec().into(), }, - ) - .unwrap(); - - let event = updated.events.into_iter().next().unwrap(); - let _update = app.update(event, &mut model); + }, + &mut model, + ); assert_eq!(model.value, 42); } @@ -172,33 +167,28 @@ fn test_set() { let app = AppTester::::default(); let mut model = Model::default(); - let updated = app.update(Event::Set, &mut model); - - let effect = updated.into_effects().next().unwrap(); - let Effect::KeyValue(mut request) = effect else { - panic!("Expected KeyValue effect"); - }; - - let KeyValueOperation::Set { key, value } = request.operation.clone() else { - panic!("Expected set operation"); - }; + let request = &mut app + .update(Event::Set, &mut model) + .expect_one_effect() + .expect_key_value(); - assert_eq!(key, "test"); - assert_eq!(value, 42i32.to_ne_bytes().to_vec()); + assert_eq!( + request.operation, + KeyValueOperation::Set { + key: "test".to_string(), + value: 42i32.to_ne_bytes().to_vec().into(), + } + ); - let updated = app - .resolve( - &mut request, - KeyValueResult::Ok { - response: KeyValueResponse::Set { - previous: Value::None, - }, + let _updated = app.resolve_to_event_then_update( + request, + KeyValueResult::Ok { + response: KeyValueResponse::Set { + previous: Value::None, }, - ) - .unwrap(); - - let event = updated.events.into_iter().next().unwrap(); - let _update = app.update(event, &mut model); + }, + &mut model, + ); assert!(model.successful); } @@ -208,19 +198,20 @@ fn test_delete() { let app = AppTester::::default(); let mut model = Model::default(); - let mut request = app + let request = &mut app .update(Event::Delete, &mut model) .expect_one_effect() .expect_key_value(); - let KeyValueOperation::Delete { key } = request.operation.clone() else { - panic!("Expected delete operation"); - }; - - assert_eq!(key, "test"); + assert_eq!( + request.operation, + KeyValueOperation::Delete { + key: "test".to_string() + } + ); let _updated = app.resolve_to_event_then_update( - &mut request, + request, KeyValueResult::Ok { response: KeyValueResponse::Delete { previous: Value::None, @@ -237,19 +228,20 @@ fn test_exists() { let app = AppTester::::default(); let mut model = Model::default(); - let mut request = app + let request = &mut app .update(Event::Exists, &mut model) .expect_one_effect() .expect_key_value(); - let KeyValueOperation::Exists { key } = request.operation.clone() else { - panic!("Expected exists operation"); - }; - - assert_eq!(key, "test"); + assert_eq!( + request.operation, + KeyValueOperation::Exists { + key: "test".to_string() + } + ); let _updated = app.resolve_to_event_then_update( - &mut request, + request, KeyValueResult::Ok { response: KeyValueResponse::Exists { is_present: true }, }, @@ -264,20 +256,21 @@ fn test_list_keys() { let app = AppTester::::default(); let mut model = Model::default(); - let mut request = app + let request = &mut app .update(Event::ListKeys, &mut model) .expect_one_effect() .expect_key_value(); - let KeyValueOperation::ListKeys { prefix, cursor } = request.operation.clone() else { - panic!("Expected list keys operation"); - }; - - assert_eq!(prefix, "test:"); - assert_eq!(cursor, 0); + assert_eq!( + request.operation, + KeyValueOperation::ListKeys { + prefix: "test:".to_string(), + cursor: 0, + } + ); - let _update = app.resolve_to_event_then_update( - &mut request, + let _updated = app.resolve_to_event_then_update( + request, KeyValueResult::Ok { response: KeyValueResponse::ListKeys { keys: vec!["test:1".to_string(), "test:2".to_string()], @@ -296,55 +289,48 @@ pub fn test_kv_async() -> Result<()> { let app = AppTester::::default(); let mut model = Model::default(); - let update = app.update(Event::GetThenSet, &mut model); - - let effect = update.into_effects().next().unwrap(); - let Effect::KeyValue(mut request) = effect else { - panic!("Expected KeyValue effect"); - }; - - let KeyValueOperation::Get { key } = request.operation.clone() else { - panic!("Expected get operation"); - }; + let request = &mut app + .update(Event::GetThenSet, &mut model) + .expect_one_effect() + .expect_key_value(); - assert_eq!(key, "test_num"); + assert_eq!( + request.operation, + KeyValueOperation::Get { + key: "test_num".to_string() + } + ); - let update = app + let request = &mut app .resolve( - &mut request, + request, KeyValueResult::Ok { response: KeyValueResponse::Get { value: 17u32.to_ne_bytes().to_vec().into(), }, }, ) - .unwrap(); - - let effect = update.into_effects().next().unwrap(); - let Effect::KeyValue(mut request) = effect else { - panic!("Expected KeyValue effect"); - }; - - let KeyValueOperation::Set { key, value } = request.operation.clone() else { - panic!("Expected get operation"); - }; + .unwrap() + .expect_one_effect() + .expect_key_value(); - assert_eq!(key, "test_num".to_string()); - assert_eq!(value, 18u32.to_ne_bytes().to_vec()); + assert_eq!( + request.operation, + KeyValueOperation::Set { + key: "test_num".to_string(), + value: 18u32.to_ne_bytes().to_vec().into(), + } + ); - let update = app - .resolve( - &mut request, - KeyValueResult::Ok { - response: KeyValueResponse::Set { - previous: Value::None, - }, + let _updated = app.resolve_to_event_then_update( + request, + KeyValueResult::Ok { + response: KeyValueResponse::Set { + previous: Value::None, }, - ) - .unwrap(); - - let event = update.events.into_iter().next().unwrap(); - let _update = app.update(event, &mut model); + }, + &mut model, + ); assert!(model.successful); diff --git a/crux_platform/src/lib.rs b/crux_platform/src/lib.rs index 5502b912..b431f156 100644 --- a/crux_platform/src/lib.rs +++ b/crux_platform/src/lib.rs @@ -4,7 +4,7 @@ use crux_core::capability::{CapabilityContext, Operation}; use crux_core::macros::Capability; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct PlatformRequest; // TODO revisit this diff --git a/crux_time/tests/time_test.rs b/crux_time/tests/time_test.rs index ea77f14d..6d1dd577 100644 --- a/crux_time/tests/time_test.rs +++ b/crux_time/tests/time_test.rs @@ -195,14 +195,14 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut request = app + let request = &mut app .update(Event::GetAsync, &mut model) .expect_one_effect() .expect_time(); let now: DateTime = "2022-12-01T01:47:12.746202562+00:00".parse().unwrap(); let response = TimeResponse::Now(now.try_into().unwrap()); - let _update = app.resolve_to_event_then_update(&mut request, response, &mut model); + let _update = app.resolve_to_event_then_update(request, response, &mut model); assert_eq!(app.view(&model).time, "2022-12-01T01:47:12.746202562+00:00"); } @@ -212,18 +212,18 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut request1 = app + let request1 = &mut app .update(Event::StartDebounce, &mut model) .expect_one_effect() .expect_time(); - let mut request2 = app + let request2 = &mut app .update(Event::StartDebounce, &mut model) .expect_one_effect() .expect_time(); // resolve and update app.resolve_to_event_then_update( - &mut request1, + request1, TimeResponse::DurationElapsed { id: model.debounce_time_id.unwrap(), }, @@ -236,7 +236,7 @@ mod tests { // resolve and update app.resolve_to_event_then_update( - &mut request2, + request2, TimeResponse::DurationElapsed { id: model.debounce_time_id.unwrap(), }, @@ -253,7 +253,7 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut request1 = app + let request1 = &mut app .update(Event::StartDebounce, &mut model) .expect_one_effect() .expect_time(); @@ -261,7 +261,7 @@ mod tests { assert!(model.debounce_time_id.is_some()); app.resolve_to_event_then_update( - &mut request1, + request1, TimeResponse::Cleared { id: model.debounce_time_id.unwrap(), }, diff --git a/docs/src/guide/testing.md b/docs/src/guide/testing.md index 7770c903..c5db0887 100644 --- a/docs/src/guide/testing.md +++ b/docs/src/guide/testing.md @@ -113,7 +113,7 @@ let app = AppTester::::default(); The `Model` is normally private to the app (`NoteEditor`), but `AppTester` allows us to set it up for our test. In this case the document contains the -string `"hello"` with the last three characters selected. +string `"hello"` with the last two characters selected. ```rust,ignore,no_run let mut model = Model { @@ -123,7 +123,7 @@ let mut model = Model { }; ``` -Let's insert the text under the selection range. We simply create an `Event` +Let's insert some text under the selection range. We simply create an `Event` that captures the user's action and pass it into the app's `update()` method, along with the Model we just created (which we will be able to inspect afterwards). @@ -244,7 +244,8 @@ which we'll use later. Finally, we don't expect any more timer requests to have been generated. ```rust,ignore,no_run -let mut request = requests.next().unwrap(); // this is mutable so we can resolve it later +// this is mutable so we can resolve it later +let request = &mut requests.next().unwrap(); assert_let!( TimerOperation::Start { id: first_id, @@ -255,6 +256,39 @@ assert_let!( assert!(requests.next().is_none()); ``` +````admonish Note +There are other ways to analyze effects from the update. + +You can take all the effects that match a predicate out of the update: + + ```rust,ignore,no_run + let requests = update.take_effects(|effect| effect.is_timer()); + // or + let requests = update.take_effects(Effect::is_timer); + ``` + +Or you can partition the effects into those that match the predicate and those +that don't: + + ```rust,ignore,no_run + // split the effects into HTTP requests and renders + let (timer_requests, other_requests) = update.take_effects_partitioned_by(Effect::is_timer); + ``` + + There are also `expect_*` methods that allow you to assert and return a certain + type of effect: + + ```rust,ignore,no_run + // this is mutable so we can resolve it later + let request = &mut timer_requests.pop_front().unwrap().expect_timer(); + assert_eq!( + request.operation, + TimerOperation::Start { id: 1, millis: 1000 } + ); + assert!(timer_requests.is_empty()); + ``` +```` + At this point the shell would start the timer (this is something the core can't do as it is a side effect) and so we need to tell the app that it was created. We do this by "resolving" the request. @@ -270,11 +304,17 @@ Note that resolving a request could call the app's `update()` method resulting in more `Event`s being generated, which we need to feed back into the app. ```rust,ignore,no_run -let update = app - .resolve(&mut request, TimerOutput::Created { id: first_id }).unwrap(); +let update = app.resolve(request, TimerOutput::Created { id: first_id }).unwrap(); for event in update.events { - app.update(event, &mut model); + let _ = app.update(event, &mut model); } + +// or, if this event "settles" the app +let _updated = app.resolve_to_event_then_update( + request, + TimerOutput::Created { id: first_id }, + &mut model +); ``` Before the timer fires, we'll insert another character, which should cancel the @@ -333,16 +373,16 @@ Another edit should result in another timer, but not in a cancellation: ```rust,ignore,no_run let update = app.update(Event::Backspace, &mut model); -let mut timer_requests = update.into_effects().filter_map(Effect::into_timer); +let mut requests = update.into_effects().filter_map(Effect::into_timer); assert_let!( TimerOperation::Start { id: third_id, millis: 1000 }, - timer_requests.next().unwrap().operation + requests.next().unwrap().operation ); -assert!(timer_requests.next().is_none()); // no cancellation +assert!(requests.next().is_none()); // no cancellation assert_ne!(third_id, second_id); ``` diff --git a/doctest_support/src/lib.rs b/doctest_support/src/lib.rs index 1e2db50a..b7590fae 100644 --- a/doctest_support/src/lib.rs +++ b/doctest_support/src/lib.rs @@ -5,7 +5,7 @@ pub mod compose { use crux_core::capability::{CapabilityContext, Operation}; use serde::{Deserialize, Serialize}; - #[derive(PartialEq, Serialize, Deserialize, Debug)] + #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] pub struct OpOne { number: usize, } @@ -79,7 +79,7 @@ pub mod compose { use crux_core::capability::{CapabilityContext, Operation}; use serde::{Deserialize, Serialize}; - #[derive(PartialEq, Serialize, Deserialize, Debug)] + #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] pub struct OpTwo { number: usize, } diff --git a/examples/bridge_echo/Cargo.toml b/examples/bridge_echo/Cargo.toml index 7a7f8edd..055bcf82 100644 --- a/examples/bridge_echo/Cargo.toml +++ b/examples/bridge_echo/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0.90" -crux_core = "0.9.0" +crux_core = "0.9.1" serde = "1.0.210" [workspace.metadata.bin] diff --git a/examples/bridge_echo/shared/src/app.rs b/examples/bridge_echo/shared/src/app.rs index 588faae7..779e0e51 100644 --- a/examples/bridge_echo/shared/src/app.rs +++ b/examples/bridge_echo/shared/src/app.rs @@ -58,7 +58,7 @@ impl crux_core::App for App { #[cfg(test)] mod test { use super::*; - use crux_core::{assert_effect, testing::AppTester}; + use crux_core::testing::AppTester; #[test] fn shows_initial_count() { @@ -112,9 +112,9 @@ mod test { let app = AppTester::::default(); let mut model = Model::default(); - let update = app.update(Event::Tick, &mut model); - - assert_effect!(update, Effect::Render(_)); + app.update(Event::Tick, &mut model) + .expect_one_effect() + .expect_render(); } #[test] @@ -122,9 +122,9 @@ mod test { let app = AppTester::::default(); let mut model = Model::default(); - let update = app.update(Event::NewPeriod, &mut model); - - assert_effect!(update, Effect::Render(_)); + app.update(Event::NewPeriod, &mut model) + .expect_one_effect() + .expect_render(); } } // ANCHOR_END: test diff --git a/examples/cat_facts/Cargo.lock b/examples/cat_facts/Cargo.lock index 5e3c9122..c4c0b589 100644 --- a/examples/cat_facts/Cargo.lock +++ b/examples/cat_facts/Cargo.lock @@ -143,12 +143,6 @@ dependencies = [ "nom", ] -[[package]] -name = "assert_let_bind" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26da9bc03539eda841c2042fd584ee3982b652606f40dc9e7a828817eae79275" - [[package]] name = "async-channel" version = "1.9.0" @@ -254,9 +248,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "camino" @@ -434,8 +428,6 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crux_core" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7254d2fca79de45e1e595fa7d32a0d3d8bfec85ee02b250cc216af6b7f01162" dependencies = [ "anyhow", "bincode", @@ -454,8 +446,6 @@ dependencies = [ [[package]] name = "crux_http" version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0c2c59aee43cfd5a02ddf3eea264e59bc8317b731fa57905bfd62c4eb84c80" dependencies = [ "anyhow", "async-trait", @@ -476,8 +466,6 @@ dependencies = [ [[package]] name = "crux_kv" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5b90cf04e92216c2a5c4a878bd832b220ca70238228f271323be95585d7c1d" dependencies = [ "anyhow", "crux_core", @@ -489,8 +477,6 @@ dependencies = [ [[package]] name = "crux_macros" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6628a214f3754bdea8f05bf9ebbe62cd6bff48f7a4bea52ecb14cbd1743484ad" dependencies = [ "darling", "proc-macro-error", @@ -502,8 +488,6 @@ dependencies = [ [[package]] name = "crux_platform" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea670b8e6fa544ea8f9cd435624eea4d763d867298a8e7575ee71bcdd0859452" dependencies = [ "crux_core", "serde", @@ -512,8 +496,6 @@ dependencies = [ [[package]] name = "crux_time" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705d7974afd4343ae6f52dd588ff74b5a383c212232324be7449b6b7342ce08f" dependencies = [ "chrono", "crux_core", @@ -2247,9 +2229,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.211" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793" dependencies = [ "serde_derive", ] @@ -2312,9 +2294,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.211" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff" dependencies = [ "proc-macro2", "quote", @@ -2369,7 +2351,6 @@ dependencies = [ name = "shared" version = "0.1.0" dependencies = [ - "assert_let_bind", "chrono", "crux_core", "crux_http", diff --git a/examples/cat_facts/Cargo.toml b/examples/cat_facts/Cargo.toml index 7c470c84..3c992b6d 100644 --- a/examples/cat_facts/Cargo.toml +++ b/examples/cat_facts/Cargo.toml @@ -12,11 +12,16 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0.90" -crux_core = "0.9.0" -crux_http = "0.10.1" -crux_kv = "0.5.0" -crux_platform = "0.2.0" -crux_time = { version = "0.5.0", features = ["chrono"] } +crux_core = { path = "../../crux_core" } +crux_http = { path = "../../crux_http" } +crux_kv = { path = "../../crux_kv" } +crux_platform = { path = "../../crux_platform" } +crux_time = { path = "../../crux_time", features = ["chrono"] } +# crux_core = "0.9.1" +# crux_http = "0.10.2" +# crux_kv = "0.5.1" +# crux_platform = "0.2.1" +# crux_time = { version = "0.5.1", features = ["chrono"] } serde = "1.0.210" [workspace.metadata.bin] diff --git a/examples/cat_facts/shared/Cargo.toml b/examples/cat_facts/shared/Cargo.toml index 65ce90ef..e521b942 100644 --- a/examples/cat_facts/shared/Cargo.toml +++ b/examples/cat_facts/shared/Cargo.toml @@ -24,9 +24,6 @@ serde_json = "1.0.132" uniffi = "0.28.2" wasm-bindgen = "0.2.95" -[dev-dependencies] -assert_let_bind = "0.1.1" - [target.uniffi-bindgen.dependencies] uniffi = { version = "0.28.2", features = ["cli"] } diff --git a/examples/cat_facts/shared/src/app.rs b/examples/cat_facts/shared/src/app.rs index 47aba7b7..a1385427 100644 --- a/examples/cat_facts/shared/src/app.rs +++ b/examples/cat_facts/shared/src/app.rs @@ -151,9 +151,6 @@ impl App for CatFacts { Event::SetFact(Ok(mut response)) => { model.cat_fact = Some(response.take_body().unwrap()); - let bytes = serde_json::to_vec(&model).unwrap(); - caps.key_value.set(KEY.to_string(), bytes, |_| Event::None); - caps.time.now(Event::CurrentTime); } Event::SetImage(Ok(mut response)) => { @@ -170,6 +167,7 @@ impl App for CatFacts { Event::CurrentTime(TimeResponse::Now(instant)) => { let time: DateTime = instant.try_into().unwrap(); model.time = Some(time.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)); + let bytes = serde_json::to_vec(&model).unwrap(); caps.key_value.set(KEY.to_string(), bytes, |_| Event::None); @@ -215,83 +213,109 @@ impl App for CatFacts { #[cfg(test)] mod tests { - use assert_let_bind::assert_let; use crux_core::testing::AppTester; use crux_http::{ protocol::{HttpRequest, HttpResponse, HttpResult}, testing::ResponseBuilder, }; - - use crate::Effect; + use crux_kv::{value::Value, KeyValueOperation, KeyValueResponse, KeyValueResult}; + use crux_time::Instant; use super::*; - #[test] - fn fetch_sends_some_requests() { - let app = AppTester::::default(); - let mut model = Model::default(); - - let update = app.update(Event::Fetch, &mut model); - - assert_let!(Effect::Http(request), update.effects().next().unwrap()); - let actual = &request.operation; - let expected = &HttpRequest::get(FACT_API_URL).build(); - - assert_eq!(actual, expected); - } - #[test] fn fetch_results_in_set_fact_and_set_image() { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::Fetch, &mut model); - let mut effects = update.effects_mut(); + // send fetch event to app + let (mut http_effects, mut render_effects) = app + .update(Event::Fetch, &mut model) + .take_effects_partitioned_by(Effect::is_http); + + // receive render effect + render_effects.pop_front().unwrap().expect_render(); - assert_let!(Effect::Http(request), effects.next().unwrap()); - let actual = &request.operation; - let expected = &HttpRequest::get(FACT_API_URL).build(); - assert_eq!(actual, expected); + // receive two HTTP effects, one to fetch the fact and one to fetch the image + // we'll handle the fact request first + let request = &mut http_effects.pop_front().unwrap().expect_http(); + assert_eq!(request.operation, HttpRequest::get(FACT_API_URL).build()); let a_fact = CatFact { fact: "cats are good".to_string(), length: 13, }; - let response = HttpResult::Ok( - HttpResponse::ok() - .body(r#"{ "fact": "cats are good", "length": 13 }"#) - .build(), + // resolve the request with a simulated response from the web API + let event = app + .resolve( + request, + HttpResult::Ok( + HttpResponse::ok() + .body(r#"{ "fact": "cats are good", "length": 13 }"#) + .build(), + ), + ) + .expect("should resolve successfully") + .expect_one_event(); + + // check that the app emitted an (internal) event to update the model + assert_eq!( + event, + Event::SetFact(Ok(ResponseBuilder::ok().body(a_fact.clone()).build())) ); - let update = app - .resolve(request, response) - .expect("should resolve successfully"); - let expected_response = ResponseBuilder::ok().body(a_fact.clone()).build(); - assert_eq!(update.events, vec![Event::SetFact(Ok(expected_response))]); + // Setting the fact should trigger a time event + let mut time_events = app.update(event, &mut model).take_effects(|e| e.is_time()); - for event in update.events { - let _ = app.update(event, &mut model); - } + let request = &mut time_events.pop_front().unwrap().expect_time(); + + let response = TimeResponse::Now(Instant::new(0, 0).unwrap()); + let event = app + .resolve(request, response) + .expect("should resolve successfully") + .expect_one_event(); + + assert_eq!(event, Event::CurrentTime(response)); + + // update the app with the current time event + // and check that we get a key value set event and a render event + let (mut key_value_effects, mut render_effects) = app + .update(event, &mut model) + .take_effects_partitioned_by(Effect::is_key_value); + render_effects.pop_front().unwrap().expect_render(); + + let request = &mut key_value_effects.pop_front().unwrap().expect_key_value(); + assert_eq!( + request.operation, + KeyValueOperation::Set { + key: KEY.to_string(), + value: serde_json::to_vec(&model).unwrap() + } + ); + + let _updated = app.resolve_to_event_then_update( + request, + KeyValueResult::Ok { + response: KeyValueResponse::Set { + previous: Value::None, + }, + }, + &mut model, + ); - assert_let!(Effect::Http(request), effects.next().unwrap()); - let actual = &request.operation; - let expected = &HttpRequest::get(IMAGE_API_URL).build(); - assert_eq!(actual, expected); + // Now we'll handle the image + let request = &mut http_effects.pop_front().unwrap().expect_http(); + assert_eq!(request.operation, HttpRequest::get(IMAGE_API_URL).build()); - let a_image = CatImage { + let an_image = CatImage { href: "image_url".to_string(), }; let response = HttpResult::Ok(HttpResponse::ok().body(r#"{"href":"image_url"}"#).build()); - let update = app - .resolve(request, response) - .expect("should resolve successfully"); - for event in update.events { - let _ = app.update(event, &mut model); - } + let _updated = app.resolve_to_event_then_update(request, response, &mut model); assert_eq!(model.cat_fact, Some(a_fact)); - assert_eq!(model.cat_image, Some(a_image)); + assert_eq!(model.cat_image, Some(an_image)); } } diff --git a/examples/cat_facts/shared/src/app/platform.rs b/examples/cat_facts/shared/src/app/platform.rs index 3b6e3126..a83af205 100644 --- a/examples/cat_facts/shared/src/app/platform.rs +++ b/examples/cat_facts/shared/src/app/platform.rs @@ -47,7 +47,6 @@ impl crux_core::App for App { #[cfg(test)] mod tests { - use assert_let_bind::assert_let; use crux_core::testing::AppTester; use crux_platform::PlatformResponse; @@ -58,17 +57,15 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let mut update = app.update(Event::Get, &mut model); - - assert_let!(Effect::Platform(request), &mut update.effects[0]); + let request = &mut app + .update(Event::Get, &mut model) + .expect_one_effect() + .expect_platform(); let response = PlatformResponse("platform".to_string()); - let update = app - .resolve(request, response) - .expect("should resolve successfully"); - for event in update.events { - let _ = app.update(event, &mut model); - } + app.resolve_to_event_then_update(request, response, &mut model) + .expect_one_effect() + .expect_render(); assert_eq!(model.platform, "platform"); assert_eq!(app.view(&model).platform, "Hello platform"); diff --git a/examples/counter/Cargo.lock b/examples/counter/Cargo.lock index bdeb66ba..eacbeef8 100644 --- a/examples/counter/Cargo.lock +++ b/examples/counter/Cargo.lock @@ -158,12 +158,6 @@ dependencies = [ "nom", ] -[[package]] -name = "assert_let_bind" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26da9bc03539eda841c2042fd584ee3982b652606f40dc9e7a828817eae79275" - [[package]] name = "async-channel" version = "1.9.0" @@ -1019,8 +1013,6 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crux_core" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7254d2fca79de45e1e595fa7d32a0d3d8bfec85ee02b250cc216af6b7f01162" dependencies = [ "anyhow", "bincode", @@ -1039,8 +1031,6 @@ dependencies = [ [[package]] name = "crux_http" version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0c2c59aee43cfd5a02ddf3eea264e59bc8317b731fa57905bfd62c4eb84c80" dependencies = [ "anyhow", "async-trait", @@ -1061,8 +1051,6 @@ dependencies = [ [[package]] name = "crux_macros" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6628a214f3754bdea8f05bf9ebbe62cd6bff48f7a4bea52ecb14cbd1743484ad" dependencies = [ "darling", "proc-macro-error", @@ -5584,7 +5572,6 @@ dependencies = [ name = "shared" version = "0.1.0" dependencies = [ - "assert_let_bind", "async-sse", "async-std", "chrono", diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index e9d712d5..8ff0e89a 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -20,8 +20,10 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0.90" -crux_core = "0.9.0" -crux_http = "0.10.1" +crux_core = { path = "../../crux_core" } +crux_http = { path = "../../crux_http" } +# crux_core = "0.9.1" +# crux_http = "0.10.2" serde = "1.0.210" [workspace.metadata.bin] diff --git a/examples/counter/shared/Cargo.toml b/examples/counter/shared/Cargo.toml index c5526023..9c03887c 100644 --- a/examples/counter/shared/Cargo.toml +++ b/examples/counter/shared/Cargo.toml @@ -26,7 +26,6 @@ url = "2.5.2" wasm-bindgen = "0.2.95" [dev-dependencies] -assert_let_bind = "0.1.1" insta = { version = "1.40.0", features = ["yaml"] } [target.uniffi-bindgen.dependencies] diff --git a/examples/counter/shared/src/app.rs b/examples/counter/shared/src/app.rs index e6cad7d3..c4352fb4 100644 --- a/examples/counter/shared/src/app.rs +++ b/examples/counter/shared/src/app.rs @@ -127,7 +127,6 @@ mod tests { use super::{App, Event, Model}; use crate::capabilities::sse::SseRequest; use crate::{Count, Effect}; - use assert_let_bind::assert_let; use chrono::{TimeZone, Utc}; use crux_core::{assert_effect, testing::AppTester}; use crux_http::protocol::HttpResult; @@ -148,15 +147,15 @@ mod tests { let mut model = Model::default(); // send a `Get` event to the app - let mut update = app.update(Event::Get, &mut model); + let update = app.update(Event::Get, &mut model); // check that the app emitted an HTTP request, // capturing the request in the process - assert_let!(Effect::Http(request), &mut update.effects[0]); + let request = &mut update.expect_one_effect().expect_http(); // check that the request is a GET to the correct URL - let actual = &request.operation; - let expected = &HttpRequest::get("https://crux-counter.fly.dev/").build(); + let actual = request.operation.clone(); + let expected = HttpRequest::get("https://crux-counter.fly.dev/").build(); assert_eq!(actual, expected); // resolve the request with a simulated response from the web API @@ -227,8 +226,11 @@ mod tests { // send an `Increment` event to the app let mut update = app.update(Event::Increment, &mut model); + // split the effects into render and HTTP requests + let (mut render, mut http) = update.take_effects_partitioned_by(Effect::is_render); + // check that the app asked the shell to render - assert_effect!(update, Effect::Render(_)); + render.pop_front().unwrap().expect_render(); // we are expecting our model to be updated "optimistically" before the // HTTP request completes, so the value should have been updated @@ -242,7 +244,7 @@ mod tests { // check that the app also emitted an HTTP request, // capturing the request in the process - assert_let!(Effect::Http(request), &mut update.effects[1]); + let request = &mut http.pop_front().unwrap().expect_http(); // check that the request is a POST to the correct URL let actual = &request.operation; @@ -253,12 +255,8 @@ mod tests { let response = HttpResponse::ok() .body(r#"{ "value": 2, "updated_at": 1672531200000 }"#) .build(); - let update = app - .resolve(request, HttpResult::Ok(response)) - .expect("Update to succeed"); - - // send the generated (internal) `Set` event back into the app - let _ = app.update(update.events[0].clone(), &mut model); + let _updated = + app.resolve_to_event_then_update(request, HttpResult::Ok(response), &mut model); // check that the model has been updated correctly insta::assert_yaml_snapshot!(model, @r###" @@ -286,8 +284,11 @@ mod tests { // send a `Decrement` event to the app let mut update = app.update(Event::Decrement, &mut model); + // split the effects into render and HTTP requests + let (mut render, mut http) = update.take_effects_partitioned_by(Effect::is_render); + // check that the app asked the shell to render - assert_effect!(update, Effect::Render(_)); + render.pop_front().unwrap().expect_render(); // we are expecting our model to be updated "optimistically" before the // HTTP request completes, so the value should have been updated @@ -301,7 +302,7 @@ mod tests { // check that the app also emitted an HTTP request, // capturing the request in the process - assert_let!(Effect::Http(request), &mut update.effects[1]); + let request = &mut http.pop_front().unwrap().expect_http(); // check that the request is a POST to the correct URL let actual = &request.operation; @@ -312,15 +313,8 @@ mod tests { let response = HttpResponse::ok() .body(r#"{ "value": -1, "updated_at": 1672531200000 }"#) .build(); - let update = app - .resolve(request, HttpResult::Ok(response)) - .expect("a successful update"); - - // run the event loop in order to send the (internal) `Set` event - // back into the app - for event in update.events { - let _ = app.update(event, &mut model); - } + let _updated = + app.resolve_to_event_then_update(request, HttpResult::Ok(response), &mut model); // check that the model has been updated correctly insta::assert_yaml_snapshot!(model, @r###" @@ -336,15 +330,17 @@ mod tests { let app = AppTester::::default(); let mut model = Model::default(); - let update = app.update(Event::StartWatch, &mut model); - - assert_let!(Effect::ServerSentEvents(request), &update.effects[0]); + let request = app + .update(Event::StartWatch, &mut model) + .expect_one_effect() + .expect_sse(); - let actual = &request.operation; - let expected = &SseRequest { - url: "https://crux-counter.fly.dev/sse".to_string(), - }; - assert_eq!(actual, expected); + assert_eq!( + request.operation, + SseRequest { + url: "https://crux-counter.fly.dev/sse".to_string(), + } + ); } #[test] @@ -358,9 +354,9 @@ mod tests { }; let event = Event::Update(count); - let update = app.update(event, &mut model); - - assert_effect!(update, Effect::Render(_)); + app.update(event, &mut model) + .expect_one_effect() + .expect_render(); // check that the model has been updated correctly insta::assert_yaml_snapshot!(model, @r###" diff --git a/examples/hello_world/Cargo.lock b/examples/hello_world/Cargo.lock index aa0aafda..82aed45a 100644 --- a/examples/hello_world/Cargo.lock +++ b/examples/hello_world/Cargo.lock @@ -242,8 +242,6 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crux_core" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7254d2fca79de45e1e595fa7d32a0d3d8bfec85ee02b250cc216af6b7f01162" dependencies = [ "anyhow", "bincode", @@ -262,8 +260,6 @@ dependencies = [ [[package]] name = "crux_macros" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6628a214f3754bdea8f05bf9ebbe62cd6bff48f7a4bea52ecb14cbd1743484ad" dependencies = [ "darling", "proc-macro-error", diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index 1d17d073..113cacf4 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -12,7 +12,8 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0.90" -crux_core = "0.9.0" +crux_core = { path = "../../crux_core" } +# crux_core = "0.9.1" serde = "1.0.210" [workspace.metadata.bin] diff --git a/examples/hello_world/shared/src/app.rs b/examples/hello_world/shared/src/app.rs index c4999313..220e3cb7 100644 --- a/examples/hello_world/shared/src/app.rs +++ b/examples/hello_world/shared/src/app.rs @@ -47,7 +47,7 @@ impl App for Hello { #[cfg(test)] mod tests { use super::*; - use crux_core::{assert_effect, testing::AppTester}; + use crux_core::testing::AppTester; #[test] fn hello_says_hello_world() { @@ -58,7 +58,7 @@ mod tests { let update = hello.update(Event::None, &mut model); // Check update asked us to `Render` - assert_effect!(update, Effect::Render(_)); + update.expect_one_effect().expect_render(); // Make sure the view matches our expectations let actual_view = &hello.view(&model).data; diff --git a/examples/notes/Cargo.lock b/examples/notes/Cargo.lock index 7831e03e..058e978f 100644 --- a/examples/notes/Cargo.lock +++ b/examples/notes/Cargo.lock @@ -301,8 +301,6 @@ checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crux_core" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7254d2fca79de45e1e595fa7d32a0d3d8bfec85ee02b250cc216af6b7f01162" dependencies = [ "anyhow", "bincode", @@ -321,8 +319,6 @@ dependencies = [ [[package]] name = "crux_kv" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5b90cf04e92216c2a5c4a878bd832b220ca70238228f271323be95585d7c1d" dependencies = [ "anyhow", "crux_core", @@ -334,8 +330,6 @@ dependencies = [ [[package]] name = "crux_macros" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6628a214f3754bdea8f05bf9ebbe62cd6bff48f7a4bea52ecb14cbd1743484ad" dependencies = [ "darling", "proc-macro-error", diff --git a/examples/notes/Cargo.toml b/examples/notes/Cargo.toml index bcee3ad0..64b99285 100644 --- a/examples/notes/Cargo.toml +++ b/examples/notes/Cargo.toml @@ -12,8 +12,10 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0" -crux_core = "0.9.0" -crux_kv = "0.5.0" +crux_core = { path = "../../crux_core" } +crux_kv = { path = "../../crux_kv" } +# crux_core = "0.9.1" +# crux_kv = "0.5.1" serde = "1.0" [workspace.metadata.bin] diff --git a/examples/notes/shared/src/app.rs b/examples/notes/shared/src/app.rs index 79db70db..252c0921 100644 --- a/examples/notes/shared/src/app.rs +++ b/examples/notes/shared/src/app.rs @@ -330,8 +330,9 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::MoveCursor(5), &mut model); - assert_effect!(update, Effect::Render(_)); + app.update(Event::MoveCursor(5), &mut model) + .expect_one_effect() + .expect_render(); let view = app.view(&model); @@ -349,8 +350,9 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Select(2, 5), &mut model); - assert_effect!(update, Effect::Render(_)); + app.update(Event::Select(2, 5), &mut model) + .expect_one_effect() + .expect_render(); let view = app.view(&model); @@ -557,14 +559,19 @@ mod save_load_tests { }; // this will eventually take a document ID - let update = app.update(Event::Open, &mut model); - let requests = &mut update.into_effects().filter_map(Effect::into_key_value); + let mut requests = app + .update(Event::Open, &mut model) + .take_effects(Effect::is_key_value); - let mut request = requests.next().unwrap(); - assert_let!(KeyValueOperation::Get { key }, &request.operation); - assert_eq!(key, "note"); + let request = &mut requests.pop_front().unwrap().expect_key_value(); + assert_eq!( + request.operation, + KeyValueOperation::Get { + key: "note".to_string() + } + ); - assert!(requests.next().is_none()); + assert!(requests.is_empty()); // Read was successful let response = KeyValueResult::Ok { @@ -572,13 +579,8 @@ mod save_load_tests { value: note.save().into(), }, }; - let update = app.resolve(&mut request, response).unwrap(); - assert_eq!(update.events.len(), 1); - - for e in update.events { - let update = app.update(e, &mut model); - assert_effect!(update, Effect::Render(_)); - } + let update = app.resolve_to_event_then_update(request, response, &mut model); + assert_effect!(update, Effect::Render(_)); assert_eq!(app.view(&model).text, "LOADED"); } @@ -596,36 +598,42 @@ mod save_load_tests { // this will eventually take a document ID let requests = &mut app .update(Event::Open, &mut model) - .into_effects() - .filter_map(Effect::into_key_value); + .take_effects(Effect::is_key_value); - let mut request = requests.next().unwrap(); - assert_let!(KeyValueOperation::Get { key }, &request.operation); - assert_eq!(key, "note"); + let request = &mut requests.pop_front().unwrap().expect_key_value(); + assert_eq!( + request.operation, + KeyValueOperation::Get { + key: "note".to_string() + } + ); - assert!(requests.next().is_none()); + assert!(requests.is_empty()); // Read was unsuccessful - let update = app + let event = app .resolve( - &mut request, + request, KeyValueResult::Ok { response: KeyValueResponse::Get { value: Value::None }, }, ) - .unwrap(); - assert_eq!(update.events.len(), 1); + .unwrap() + .expect_one_event(); - for e in update.events { - let save = app - .update(e, &mut model) - .into_effects() - .find_map(Effect::into_key_value) - .unwrap(); + let save = app + .update(event, &mut model) + .into_effects() + .find_map(Effect::into_key_value) + .unwrap(); - assert_let!(KeyValueOperation::Set { key, value: _ }, &save.operation); - assert_eq!(key, "note"); - } + assert_eq!( + save.operation, + KeyValueOperation::Set { + key: "note".to_string(), + value: model.note.save().into(), + } + ); } #[test] @@ -644,7 +652,7 @@ mod save_load_tests { .into_effects() .filter_map(Effect::into_timer); - let mut request = requests.next().unwrap(); + let request = &mut requests.next().unwrap(); assert_let!( TimerOperation::Start { id: first_id, @@ -657,7 +665,7 @@ mod save_load_tests { // Tells app the timer was created let update = app - .resolve(&mut request, TimerOutput::Created { id: first_id }) + .resolve(request, TimerOutput::Created { id: first_id }) .unwrap(); for event in update.events { println!("Event: {event:?}"); @@ -691,24 +699,20 @@ mod save_load_tests { assert!(requests.next().is_none()); // Tell app the second timer was created - let update = app - .resolve(start_request, TimerOutput::Created { id: second_id }) - .unwrap(); - for event in update.events { - println!("Event: {event:?}"); - let _ = app.update(event, &mut model); - } + let _updated = app.resolve_to_event_then_update( + start_request, + TimerOutput::Created { id: second_id }, + &mut model, + ); // Time passes // Fire the timer - let update = app - .resolve(start_request, TimerOutput::Finished { id: second_id }) - .unwrap(); - for event in update.events { - println!("Event: {event:?}"); - let _ = app.update(event, &mut model); - } + let _updated = app.resolve_to_event_then_update( + start_request, + TimerOutput::Finished { id: second_id }, + &mut model, + ); // One more edit. Should result in a timer, but not in cancellation let update = app.update(Event::Backspace, &mut model); @@ -748,13 +752,13 @@ mod save_load_tests { .find_map(Effect::into_key_value) .unwrap(); - assert_let!( - KeyValueOperation::Set { key, value }, - &write_request.operation + assert_eq!( + write_request.operation, + KeyValueOperation::Set { + key: "note".to_string(), + value: model.note.save().into() + } ); - - assert_eq!(key, "note"); - assert_eq!(value, &model.note.save()); } } diff --git a/examples/tap_to_pay/Cargo.toml b/examples/tap_to_pay/Cargo.toml index d1e5b3ee..e97ba793 100644 --- a/examples/tap_to_pay/Cargo.toml +++ b/examples/tap_to_pay/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.68" [workspace.dependencies] anyhow = "1.0.90" -crux_core = "0.9.0" +crux_core = "0.9.1" serde = "1.0.210" [workspace.metadata.bin]