refactor tests

This commit is contained in:
appflowy 2021-12-08 17:33:22 +08:00
parent 4450d4410b
commit 7ac55f29db
30 changed files with 467 additions and 454 deletions

1
backend/Cargo.lock generated
View File

@ -1272,6 +1272,7 @@ dependencies = [
"flowy-document-infra",
"futures",
"futures-core",
"futures-util",
"lazy_static",
"lib-dispatch",
"lib-infra",

View File

@ -3,7 +3,7 @@
use actix_web::web::Data;
use backend::services::doc::{crud::update_doc, manager::DocManager};
use flowy_document::services::doc::edit::ClientDocEditor as ClientEditDocContext;
use flowy_test::{workspace::ViewTest, FlowyTest};
use flowy_test::{helper::ViewTest, FlowySDKTest};
use flowy_user::services::user::UserSession;
use futures_util::{stream, stream::StreamExt};
use sqlx::PgPool;
@ -18,7 +18,7 @@ use lib_ot::core::Interval;
pub struct DocumentTest {
server: TestServer,
flowy_test: FlowyTest,
flowy_test: FlowySDKTest,
}
#[derive(Clone)]
pub enum DocScript {
@ -34,7 +34,7 @@ pub enum DocScript {
impl DocumentTest {
pub async fn new() -> Self {
let server = spawn_server().await;
let flowy_test = FlowyTest::setup_with(server.client_server_config.clone());
let flowy_test = FlowySDKTest::setup_with(server.client_server_config.clone());
Self { server, flowy_test }
}
@ -50,7 +50,7 @@ impl DocumentTest {
#[derive(Clone)]
struct ScriptContext {
client_edit_context: Option<Arc<ClientEditDocContext>>,
flowy_test: FlowyTest,
client_sdk: FlowySDKTest,
client_user_session: Arc<UserSession>,
server_doc_manager: Arc<DocManager>,
server_pg_pool: Data<PgPool>,
@ -58,13 +58,13 @@ struct ScriptContext {
}
impl ScriptContext {
async fn new(flowy_test: FlowyTest, server: TestServer) -> Self {
let user_session = flowy_test.sdk.user_session.clone();
let doc_id = create_doc(&flowy_test).await;
async fn new(client_sdk: FlowySDKTest, server: TestServer) -> Self {
let user_session = client_sdk.user_session.clone();
let doc_id = create_doc(&client_sdk).await;
Self {
client_edit_context: None,
flowy_test,
client_sdk,
client_user_session: user_session,
server_doc_manager: server.app_ctx.doc_biz.manager.clone(),
server_pg_pool: Data::new(server.pg_pool.clone()),
@ -73,7 +73,7 @@ impl ScriptContext {
}
async fn open_doc(&mut self) {
let flowy_document = self.flowy_test.sdk.flowy_document.clone();
let flowy_document = self.client_sdk.flowy_document.clone();
let doc_id = self.doc_id.clone();
let edit_context = flowy_document.open(DocIdentifier { doc_id }).await.unwrap();
@ -161,7 +161,7 @@ fn assert_eq(expect: &str, receive: &str) {
assert_eq!(target_delta, expected_delta);
}
async fn create_doc(flowy_test: &FlowyTest) -> String {
async fn create_doc(flowy_test: &FlowySDKTest) -> String {
let view_test = ViewTest::new(flowy_test).await;
view_test.view.id
}

View File

@ -3,7 +3,7 @@ use flowy_core::entities::{
trash::{TrashIdentifier, TrashType},
view::*,
};
use flowy_test::workspace::*;
use flowy_test::helper::*;
#[tokio::test]
#[should_panic]

View File

@ -3,12 +3,12 @@ use flowy_core::entities::{
trash::{TrashIdentifier, TrashType},
view::*,
};
use flowy_test::{workspace::*, FlowyTest};
use flowy_test::{helper::*, FlowySDKTest};
#[tokio::test]
#[should_panic]
async fn view_delete() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let _ = test.init_user().await;
let test = ViewTest::new(&test).await;
@ -21,7 +21,7 @@ async fn view_delete() {
#[tokio::test]
async fn view_delete_then_putback() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let _ = test.init_user().await;
let test = ViewTest::new(&test).await;
@ -44,7 +44,7 @@ async fn view_delete_then_putback() {
#[tokio::test]
async fn view_delete_all() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let _ = test.init_user().await;
let test = ViewTest::new(&test).await;
@ -66,7 +66,7 @@ async fn view_delete_all() {
#[tokio::test]
async fn view_delete_all_permanent() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let _ = test.init_user().await;
let test = ViewTest::new(&test).await;
@ -85,7 +85,7 @@ async fn view_delete_all_permanent() {
#[tokio::test]
async fn view_open_doc() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let _ = test.init_user().await;
let test = ViewTest::new(&test).await;

View File

@ -3,7 +3,7 @@ use flowy_core::{
event::WorkspaceEvent::*,
prelude::*,
};
use flowy_test::{builder::*, workspace::*, FlowyTest};
use flowy_test::{event_builder::*, helper::*, FlowySDKTest};
#[tokio::test]
async fn workspace_read_all() {
@ -42,13 +42,13 @@ async fn workspace_create_with_apps() {
#[tokio::test]
async fn workspace_create_with_invalid_name() {
for (name, code) in invalid_workspace_name_test_case() {
let sdk = FlowyTest::setup().sdk;
let sdk = FlowySDKTest::setup();
let request = CreateWorkspaceRequest {
name,
desc: "".to_owned(),
};
assert_eq!(
FlowyWorkspaceTest::new(sdk)
CoreModuleEventBuilder::new(sdk)
.event(CreateWorkspace)
.request(request)
.async_send()
@ -62,14 +62,14 @@ async fn workspace_create_with_invalid_name() {
#[tokio::test]
async fn workspace_update_with_invalid_name() {
let sdk = FlowyTest::setup().sdk;
let sdk = FlowySDKTest::setup();
for (name, code) in invalid_workspace_name_test_case() {
let request = CreateWorkspaceRequest {
name,
desc: "".to_owned(),
};
assert_eq!(
FlowyWorkspaceTest::new(sdk.clone())
CoreModuleEventBuilder::new(sdk.clone())
.event(CreateWorkspace)
.request(request)
.async_send()

View File

@ -39,6 +39,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = {version = "1.0"}
chrono = "0.4.19"
futures-core = { version = "0.3", default-features = false }
futures-util = "0.3.15"
byteorder = {version = "1.3.4"}
async-stream = "0.3.2"
futures = "0.3.15"

View File

@ -46,7 +46,7 @@ impl DocError {
static_doc_error!(ws, ErrorCode::WsConnectError);
static_doc_error!(internal, ErrorCode::InternalError);
static_doc_error!(unauthorized, ErrorCode::UserUnauthorized);
static_doc_error!(record_not_found, ErrorCode::DocNotfound);
static_doc_error!(doc_not_found, ErrorCode::DocNotfound);
static_doc_error!(duplicate_rev, ErrorCode::DuplicateRevision);
}
@ -82,7 +82,7 @@ impl std::default::Default for ErrorCode {
impl std::convert::From<flowy_database::Error> for DocError {
fn from(error: flowy_database::Error) -> Self {
match error {
flowy_database::Error::NotFound => DocError::record_not_found().context(error),
flowy_database::Error::NotFound => DocError::doc_not_found().context(error),
_ => DocError::internal().context(error),
}
}

View File

@ -1,7 +1,7 @@
use crate::{
errors::DocError,
services::{
doc::{doc_controller::DocController, edit::ClientDocEditor},
doc::{controller::DocController, edit::ClientDocEditor},
server::construct_doc_server,
ws::WsDocumentManager,
},

View File

@ -46,4 +46,4 @@ impl DocCache {
}
}
fn doc_not_found() -> DocError { DocError::record_not_found().context("Doc is close or you should call open first") }
fn doc_not_found() -> DocError { DocError::doc_not_found().context("Doc is close or you should call open first") }

View File

@ -122,7 +122,7 @@ struct RevisionServerImpl {
impl RevisionServer for RevisionServerImpl {
#[tracing::instrument(level = "debug", skip(self))]
fn fetch_document_from_remote(&self, doc_id: &str) -> ResultFuture<Doc, DocError> {
fn fetch_document(&self, doc_id: &str) -> ResultFuture<Doc, DocError> {
let params = DocIdentifier {
doc_id: doc_id.to_string(),
};
@ -131,7 +131,7 @@ impl RevisionServer for RevisionServerImpl {
ResultFuture::new(async move {
match server.read_doc(&token, params).await? {
None => Err(DocError::record_not_found().context("Remote doesn't have this document")),
None => Err(DocError::doc_not_found().context("Remote doesn't have this document")),
Some(doc) => Ok(doc),
}
})

View File

@ -209,7 +209,7 @@ impl ClientDocEditor {
async fn handle_push_rev(&self, bytes: Bytes) -> DocResult<()> {
// Transform the revision
let (ret, rx) = oneshot::channel::<DocumentResult<TransformDeltas>>();
let _ = self.edit_tx.send(EditCommand::RemoteRevision { bytes, ret });
let _ = self.edit_tx.send(EditCommand::ProcessRemoteRevision { bytes, ret });
let TransformDeltas {
client_prime,
server_prime,
@ -268,12 +268,12 @@ impl ClientDocEditor {
let revision = self.rev_manager.mk_revisions(range).await?;
let _ = self.ws.send(revision.into());
},
WsDataType::NewDocUser => {},
WsDataType::Acked => {
let rev_id = RevId::try_from(bytes)?;
let _ = self.rev_manager.ack_revision(rev_id).await?;
},
WsDataType::Conflict => {},
WsDataType::NewDocUser => {},
}
Ok(())
}

View File

@ -55,7 +55,7 @@ impl EditCommandQueue {
let result = self.composed_delta(delta).await;
let _ = ret.send(result);
},
EditCommand::RemoteRevision { bytes, ret } => {
EditCommand::ProcessRemoteRevision { bytes, ret } => {
let revision = Revision::try_from(bytes)?;
let delta = RichTextDelta::from_bytes(&revision.delta_data)?;
let rev_id: RevId = revision.rev_id.into();
@ -131,7 +131,7 @@ pub(crate) enum EditCommand {
delta: RichTextDelta,
ret: Ret<()>,
},
RemoteRevision {
ProcessRemoteRevision {
bytes: Bytes,
ret: Ret<TransformDeltas>,
},

View File

@ -1,4 +1,4 @@
pub mod edit;
pub mod revision;
pub(crate) mod doc_controller;
pub(crate) mod controller;

View File

@ -22,7 +22,7 @@ use lib_ot::{
};
use std::{sync::Arc, time::Duration};
use tokio::{
sync::RwLock,
sync::{mpsc, RwLock},
task::{spawn_blocking, JoinHandle},
};
@ -107,13 +107,15 @@ impl RevisionCache {
}
}
pub async fn fetch_document(&self) -> DocResult<Doc> {
let result = fetch_from_local(&self.doc_id, self.dish_cache.clone()).await;
pub async fn load_document(&self) -> DocResult<Doc> {
// Loading the document from disk and it will be sync with server.
let result = load_from_disk(&self.doc_id, self.memory_cache.clone(), self.dish_cache.clone()).await;
if result.is_ok() {
return result;
}
let doc = self.server.fetch_document_from_remote(&self.doc_id).await?;
// The document doesn't exist in local. Try load from server
let doc = self.server.fetch_document(&self.doc_id).await?;
let delta_data = doc.data.as_bytes();
let revision = Revision::new(
doc.base_rev_id,
@ -154,21 +156,30 @@ impl RevisionIterator for RevisionCache {
}
}
async fn fetch_from_local(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>) -> DocResult<Doc> {
async fn load_from_disk(
doc_id: &str,
memory_cache: Arc<RevisionMemoryCache>,
disk_cache: Arc<DocRevisionDeskCache>,
) -> DocResult<Doc> {
let doc_id = doc_id.to_owned();
spawn_blocking(move || {
let (tx, mut rx) = mpsc::channel(2);
let doc = spawn_blocking(move || {
let revisions = disk_cache.read_revisions(&doc_id)?;
if revisions.is_empty() {
return Err(DocError::record_not_found().context("Local doesn't have this document"));
return Err(DocError::doc_not_found().context("Local doesn't have this document"));
}
let base_rev_id: RevId = revisions.last().unwrap().base_rev_id.into();
let rev_id: RevId = revisions.last().unwrap().rev_id.into();
let (base_rev_id, rev_id) = revisions.last().unwrap().pair_rev_id();
let mut delta = RichTextDelta::new();
for (_, revision) in revisions.into_iter().enumerate() {
match RichTextDelta::from_bytes(revision.delta_data) {
// Opti: revision's clone may cause memory issues
match RichTextDelta::from_bytes(revision.clone().delta_data) {
Ok(local_delta) => {
delta = delta.compose(&local_delta)?;
match tx.blocking_send(revision) {
Ok(_) => {},
Err(e) => log::error!("Load document from disk error: {}", e),
}
},
Err(e) => {
log::error!("Deserialize delta from revision failed: {}", e);
@ -176,51 +187,36 @@ async fn fetch_from_local(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>) -
}
}
#[cfg(debug_assertions)]
validate_delta(&doc_id, disk_cache, &delta);
match delta.ops.last() {
None => {},
Some(op) => {
let data = op.get_data();
if !data.ends_with('\n') {
delta.ops.push(Operation::Insert("\n".into()))
}
},
}
correct_delta_if_need(&mut delta);
Result::<Doc, DocError>::Ok(Doc {
id: doc_id,
data: delta.to_json(),
rev_id: rev_id.into(),
base_rev_id: base_rev_id.into(),
rev_id,
base_rev_id,
})
})
.await
.map_err(internal_error)?
.map_err(internal_error)?;
while let Some(revision) = rx.recv().await {
match memory_cache.add_revision(revision).await {
Ok(_) => {},
Err(e) => log::error!("{:?}", e),
}
}
doc
}
#[cfg(debug_assertions)]
fn validate_delta(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>, delta: &RichTextDelta) {
fn correct_delta_if_need(delta: &mut RichTextDelta) {
if delta.ops.last().is_none() {
return;
}
let data = delta.ops.last().as_ref().unwrap().get_data();
if !data.ends_with('\n') {
log::error!("The op must end with newline");
let result = || {
let revisions = disk_cache.read_revisions(&doc_id)?;
for revision in revisions {
let delta = RichTextDelta::from_bytes(revision.delta_data)?;
log::error!("Invalid revision: {}:{}", revision.rev_id, delta.to_json());
}
Ok::<(), DocError>(())
};
match result() {
Ok(_) => {},
Err(e) => log::error!("{}", e),
}
log::error!("The op must end with newline. Correcting it by inserting newline op");
delta.ops.push(Operation::Insert("\n".into()));
}
}

View File

@ -14,7 +14,7 @@ use std::sync::Arc;
use tokio::sync::mpsc;
pub trait RevisionServer: Send + Sync {
fn fetch_document_from_remote(&self, doc_id: &str) -> ResultFuture<Doc, DocError>;
fn fetch_document(&self, doc_id: &str) -> ResultFuture<Doc, DocError>;
}
pub struct RevisionManager {
@ -41,7 +41,7 @@ impl RevisionManager {
}
pub async fn load_document(&mut self) -> DocResult<RichTextDelta> {
let doc = self.cache.fetch_document().await?;
let doc = self.cache.load_document().await?;
self.update_rev_id_counter_value(doc.rev_id);
Ok(doc.delta()?)
}

View File

@ -53,6 +53,7 @@ impl RevisionUploadStream {
}
async fn send_next_revision(&self) -> DocResult<()> {
log::debug!("😁Tick");
match self.revisions.next().await? {
None => Ok(()),
Some(record) => {

View File

@ -1,6 +1,7 @@
#![allow(clippy::module_inception)]
mod attribute_test;
mod op_test;
mod revision_test;
mod serde_test;
mod undo_redo_test;

View File

@ -0,0 +1,8 @@
use flowy_test::editor::*;
#[tokio::test]
async fn create_doc() {
let test = EditorTest::new().await;
let _editor = test.create_doc().await;
println!("123");
}

View File

@ -0,0 +1,22 @@
use crate::{helper::ViewTest, FlowySDKTest};
use flowy_document::services::doc::edit::ClientDocEditor;
use flowy_document_infra::entities::doc::DocIdentifier;
use std::sync::Arc;
pub struct EditorTest {
pub sdk: FlowySDKTest,
}
impl EditorTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::setup();
let _ = sdk.init_user().await;
Self { sdk }
}
pub async fn create_doc(&self) -> Arc<ClientDocEditor> {
let test = ViewTest::new(&self.sdk).await;
let doc_identifier: DocIdentifier = test.view.id.clone().into();
self.sdk.flowy_document.open(doc_identifier).await.unwrap()
}
}

View File

@ -5,37 +5,36 @@ use std::{
hash::Hash,
};
use crate::FlowyTestSDK;
use lib_dispatch::prelude::*;
use crate::FlowySDKTest;
use flowy_core::errors::WorkspaceError;
use flowy_sdk::*;
use flowy_user::errors::UserError;
use lib_dispatch::prelude::*;
use std::{convert::TryFrom, marker::PhantomData, sync::Arc};
pub type FlowyWorkspaceTest = Builder<WorkspaceError>;
impl FlowyWorkspaceTest {
pub fn new(sdk: FlowyTestSDK) -> Self { Builder::test(TestContext::new(sdk)) }
pub type CoreModuleEventBuilder = EventBuilder<WorkspaceError>;
impl CoreModuleEventBuilder {
pub fn new(sdk: FlowySDKTest) -> Self { EventBuilder::test(TestContext::new(sdk)) }
}
pub type UserTest = Builder<UserError>;
impl UserTest {
pub fn new(sdk: FlowyTestSDK) -> Self { Builder::test(TestContext::new(sdk)) }
pub type UserModuleEventBuilder = EventBuilder<UserError>;
impl UserModuleEventBuilder {
pub fn new(sdk: FlowySDKTest) -> Self { EventBuilder::test(TestContext::new(sdk)) }
pub fn user_profile(&self) -> &Option<UserProfile> { &self.user_profile }
}
#[derive(Clone)]
pub struct Builder<E> {
pub struct EventBuilder<E> {
context: TestContext,
user_profile: Option<UserProfile>,
err_phantom: PhantomData<E>,
}
impl<E> Builder<E>
impl<E> EventBuilder<E>
where
E: FromBytes + Debug,
{
pub(crate) fn test(context: TestContext) -> Self {
fn test(context: TestContext) -> Self {
Self {
context,
user_profile: None,
@ -111,8 +110,6 @@ where
self
}
pub fn sdk(&self) -> FlowySDK { self.context.sdk.clone() }
fn dispatch(&self) -> Arc<EventDispatcher> { self.context.sdk.dispatcher() }
fn get_response(&self) -> EventResponse {
@ -128,13 +125,13 @@ where
#[derive(Clone)]
pub struct TestContext {
sdk: FlowyTestSDK,
pub sdk: FlowySDKTest,
request: Option<ModuleRequest>,
response: Option<EventResponse>,
}
impl TestContext {
pub fn new(sdk: FlowyTestSDK) -> Self {
pub fn new(sdk: FlowySDKTest) -> Self {
Self {
sdk,
request: None,

View File

@ -1,19 +1,282 @@
use crate::prelude::*;
use bytes::Bytes;
use lib_dispatch::prelude::{EventDispatcher, ModuleRequest, ToBytes};
use lib_infra::{kv::KV, uuid};
use flowy_core::{
entities::workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, Workspace},
errors::WorkspaceError,
event::WorkspaceEvent::{CreateWorkspace, OpenWorkspace},
entities::{
app::*,
trash::{RepeatedTrash, TrashIdentifier},
view::*,
workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, Workspace, *},
},
errors::{ErrorCode, WorkspaceError},
event::WorkspaceEvent::{CreateWorkspace, OpenWorkspace, *},
};
use flowy_document_infra::entities::doc::Doc;
use flowy_user::{
entities::{SignInRequest, SignUpRequest, UserProfile},
errors::UserError,
event::UserEvent::{SignIn, SignOut, SignUp},
};
use lib_dispatch::prelude::{EventDispatcher, ModuleRequest, ToBytes};
use lib_infra::{kv::KV, uuid};
use std::{fs, path::PathBuf, sync::Arc};
pub struct WorkspaceTest {
pub sdk: FlowySDKTest,
pub workspace: Workspace,
}
impl WorkspaceTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::setup();
let _ = sdk.init_user().await;
let workspace = create_workspace(&sdk, "Workspace", "").await;
open_workspace(&sdk, &workspace.id).await;
Self { sdk, workspace }
}
}
pub struct AppTest {
pub sdk: FlowySDKTest,
pub workspace: Workspace,
pub app: App,
}
impl AppTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::setup();
let _ = sdk.init_user().await;
let workspace = create_workspace(&sdk, "Workspace", "").await;
open_workspace(&sdk, &workspace.id).await;
let app = create_app(&sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
Self { sdk, workspace, app }
}
pub async fn move_app_to_trash(&self) {
let request = UpdateAppRequest {
app_id: self.app.id.clone(),
name: None,
desc: None,
color_style: None,
is_trash: Some(true),
};
update_app(&self.sdk, request).await;
}
}
pub struct ViewTest {
pub sdk: FlowySDKTest,
pub workspace: Workspace,
pub app: App,
pub view: View,
}
impl ViewTest {
pub async fn new(sdk: &FlowySDKTest) -> Self {
let workspace = create_workspace(&sdk, "Workspace", "").await;
open_workspace(&sdk, &workspace.id).await;
let app = create_app(&sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
let view = create_view(&sdk, &app.id).await;
Self {
sdk: sdk.clone(),
workspace,
app,
view,
}
}
pub async fn delete_views(&self, view_ids: Vec<String>) {
let request = QueryViewRequest { view_ids };
delete_view(&self.sdk, request).await;
}
pub async fn delete_views_permanent(&self, view_ids: Vec<String>) {
let request = QueryViewRequest { view_ids };
delete_view(&self.sdk, request).await;
CoreModuleEventBuilder::new(self.sdk.clone())
.event(DeleteAll)
.async_send()
.await;
}
}
pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
vec![
("".to_owned(), ErrorCode::WorkspaceNameInvalid),
("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
]
}
pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
let request = CreateWorkspaceRequest {
name: name.to_owned(),
desc: desc.to_owned(),
};
let workspace = CoreModuleEventBuilder::new(sdk.clone())
.event(CreateWorkspace)
.request(request)
.async_send()
.await
.parse::<Workspace>();
workspace
}
async fn open_workspace(sdk: &FlowySDKTest, workspace_id: &str) {
let request = QueryWorkspaceRequest {
workspace_id: Some(workspace_id.to_owned()),
};
let _ = CoreModuleEventBuilder::new(sdk.clone())
.event(OpenWorkspace)
.request(request)
.async_send()
.await;
}
pub async fn read_workspace(sdk: &FlowySDKTest, request: QueryWorkspaceRequest) -> Vec<Workspace> {
let repeated_workspace = CoreModuleEventBuilder::new(sdk.clone())
.event(ReadWorkspaces)
.request(request.clone())
.async_send()
.await
.parse::<RepeatedWorkspace>();
let workspaces;
if let Some(workspace_id) = &request.workspace_id {
workspaces = repeated_workspace
.into_inner()
.into_iter()
.filter(|workspace| &workspace.id == workspace_id)
.collect::<Vec<Workspace>>();
debug_assert_eq!(workspaces.len(), 1);
} else {
workspaces = repeated_workspace.items;
}
workspaces
}
pub async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &str) -> App {
let create_app_request = CreateAppRequest {
workspace_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
color_style: Default::default(),
};
let app = CoreModuleEventBuilder::new(sdk.clone())
.event(CreateApp)
.request(create_app_request)
.async_send()
.await
.parse::<App>();
app
}
pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
let delete_app_request = AppIdentifier {
app_id: app_id.to_string(),
};
CoreModuleEventBuilder::new(sdk.clone())
.event(DeleteApp)
.request(delete_app_request)
.async_send()
.await;
}
pub async fn update_app(sdk: &FlowySDKTest, request: UpdateAppRequest) {
CoreModuleEventBuilder::new(sdk.clone())
.event(UpdateApp)
.request(request)
.async_send()
.await;
}
pub async fn read_app(sdk: &FlowySDKTest, request: QueryAppRequest) -> App {
let app = CoreModuleEventBuilder::new(sdk.clone())
.event(ReadApp)
.request(request)
.async_send()
.await
.parse::<App>();
app
}
pub async fn create_view_with_request(sdk: &FlowySDKTest, request: CreateViewRequest) -> View {
let view = CoreModuleEventBuilder::new(sdk.clone())
.event(CreateView)
.request(request)
.async_send()
.await
.parse::<View>();
view
}
pub async fn create_view(sdk: &FlowySDKTest, app_id: &str) -> View {
let request = CreateViewRequest {
belong_to_id: app_id.to_string(),
name: "View A".to_string(),
desc: "".to_string(),
thumbnail: Some("http://1.png".to_string()),
view_type: ViewType::Doc,
};
create_view_with_request(sdk, request).await
}
pub async fn update_view(sdk: &FlowySDKTest, request: UpdateViewRequest) {
CoreModuleEventBuilder::new(sdk.clone())
.event(UpdateView)
.request(request)
.async_send()
.await;
}
pub async fn read_view(sdk: &FlowySDKTest, request: QueryViewRequest) -> View {
CoreModuleEventBuilder::new(sdk.clone())
.event(ReadView)
.request(request)
.async_send()
.await
.parse::<View>()
}
pub async fn delete_view(sdk: &FlowySDKTest, request: QueryViewRequest) {
CoreModuleEventBuilder::new(sdk.clone())
.event(DeleteView)
.request(request)
.async_send()
.await;
}
pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash {
CoreModuleEventBuilder::new(sdk.clone())
.event(ReadTrash)
.async_send()
.await
.parse::<RepeatedTrash>()
}
pub async fn putback_trash(sdk: &FlowySDKTest, id: TrashIdentifier) {
CoreModuleEventBuilder::new(sdk.clone())
.event(PutbackTrash)
.request(id)
.async_send()
.await;
}
pub async fn open_view(sdk: &FlowySDKTest, request: QueryViewRequest) -> Doc {
CoreModuleEventBuilder::new(sdk.clone())
.event(OpenView)
.request(request)
.async_send()
.await
.parse::<Doc>()
}
pub fn root_dir() -> String {
// https://doc.rust-lang.org/cargo/reference/environment-variables.html
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "./".to_owned());

View File

@ -1,6 +1,6 @@
pub mod builder;
mod helper;
pub mod workspace;
pub mod editor;
pub mod event_builder;
pub mod helper;
use crate::helper::*;
use backend_service::configuration::{get_client_server_configuration, ClientServerConfiguration};
@ -9,40 +9,40 @@ use flowy_user::entities::UserProfile;
use lib_infra::uuid;
pub mod prelude {
pub use crate::{builder::*, helper::*, *};
pub use crate::{event_builder::*, helper::*, *};
pub use lib_dispatch::prelude::*;
}
pub type FlowyTestSDK = FlowySDK;
#[derive(Clone)]
pub struct FlowyTest {
pub sdk: FlowyTestSDK,
pub struct FlowySDKTest(pub FlowySDK);
impl std::ops::Deref for FlowySDKTest {
type Target = FlowySDK;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl FlowyTest {
impl FlowySDKTest {
pub fn setup() -> Self {
let server_config = get_client_server_configuration().unwrap();
let test = Self::setup_with(server_config);
std::mem::forget(test.sdk.dispatcher());
test
}
pub async fn sign_up(&self) -> SignUpContext {
let context = async_sign_up(self.sdk.dispatcher()).await;
context
}
pub async fn init_user(&self) -> UserProfile {
let context = async_sign_up(self.sdk.dispatcher()).await;
context.user_profile
let sdk = Self::setup_with(server_config);
std::mem::forget(sdk.dispatcher());
sdk
}
pub fn setup_with(server_config: ClientServerConfiguration) -> Self {
let config = FlowySDKConfig::new(&root_dir(), server_config, &uuid()).log_filter("debug");
let sdk = FlowySDK::new(config);
Self { sdk }
Self(sdk)
}
pub fn sdk(&self) -> FlowyTestSDK { self.sdk.clone() }
pub async fn sign_up(&self) -> SignUpContext {
let context = async_sign_up(self.0.dispatcher()).await;
context
}
pub async fn init_user(&self) -> UserProfile {
let context = async_sign_up(self.0.dispatcher()).await;
context.user_profile
}
}

View File

@ -1,276 +0,0 @@
use crate::prelude::*;
use flowy_core::{
entities::{
app::*,
trash::{RepeatedTrash, TrashIdentifier},
view::*,
workspace::*,
},
errors::ErrorCode,
event::WorkspaceEvent::*,
};
use flowy_document_infra::entities::doc::Doc;
pub struct WorkspaceTest {
pub sdk: FlowyTestSDK,
pub workspace: Workspace,
}
impl WorkspaceTest {
pub async fn new() -> Self {
let test = FlowyTest::setup();
let _ = test.init_user().await;
let workspace = create_workspace(&test.sdk, "Workspace", "").await;
open_workspace(&test.sdk, &workspace.id).await;
Self {
sdk: test.sdk,
workspace,
}
}
}
pub struct AppTest {
pub sdk: FlowyTestSDK,
pub workspace: Workspace,
pub app: App,
}
impl AppTest {
pub async fn new() -> Self {
let test = FlowyTest::setup();
let _ = test.init_user().await;
let workspace = create_workspace(&test.sdk, "Workspace", "").await;
open_workspace(&test.sdk, &workspace.id).await;
let app = create_app(&test.sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
Self {
sdk: test.sdk,
workspace,
app,
}
}
pub async fn move_app_to_trash(&self) {
let request = UpdateAppRequest {
app_id: self.app.id.clone(),
name: None,
desc: None,
color_style: None,
is_trash: Some(true),
};
update_app(&self.sdk, request).await;
}
}
pub struct ViewTest {
pub sdk: FlowyTestSDK,
pub workspace: Workspace,
pub app: App,
pub view: View,
}
impl ViewTest {
pub async fn new(test: &FlowyTest) -> Self {
let workspace = create_workspace(&test.sdk, "Workspace", "").await;
open_workspace(&test.sdk, &workspace.id).await;
let app = create_app(&test.sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
let view = create_view(&test.sdk, &app.id).await;
Self {
sdk: test.sdk.clone(),
workspace,
app,
view,
}
}
pub async fn delete_views(&self, view_ids: Vec<String>) {
let request = QueryViewRequest { view_ids };
delete_view(&self.sdk, request).await;
}
pub async fn delete_views_permanent(&self, view_ids: Vec<String>) {
let request = QueryViewRequest { view_ids };
delete_view(&self.sdk, request).await;
FlowyWorkspaceTest::new(self.sdk.clone())
.event(DeleteAll)
.async_send()
.await;
}
}
pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
vec![
("".to_owned(), ErrorCode::WorkspaceNameInvalid),
("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
]
}
pub async fn create_workspace(sdk: &FlowyTestSDK, name: &str, desc: &str) -> Workspace {
let request = CreateWorkspaceRequest {
name: name.to_owned(),
desc: desc.to_owned(),
};
let workspace = FlowyWorkspaceTest::new(sdk.clone())
.event(CreateWorkspace)
.request(request)
.async_send()
.await
.parse::<Workspace>();
workspace
}
async fn open_workspace(sdk: &FlowyTestSDK, workspace_id: &str) {
let request = QueryWorkspaceRequest {
workspace_id: Some(workspace_id.to_owned()),
};
let _ = FlowyWorkspaceTest::new(sdk.clone())
.event(OpenWorkspace)
.request(request)
.async_send()
.await;
}
pub async fn read_workspace(sdk: &FlowyTestSDK, request: QueryWorkspaceRequest) -> Vec<Workspace> {
let repeated_workspace = FlowyWorkspaceTest::new(sdk.clone())
.event(ReadWorkspaces)
.request(request.clone())
.async_send()
.await
.parse::<RepeatedWorkspace>();
let workspaces;
if let Some(workspace_id) = &request.workspace_id {
workspaces = repeated_workspace
.into_inner()
.into_iter()
.filter(|workspace| &workspace.id == workspace_id)
.collect::<Vec<Workspace>>();
debug_assert_eq!(workspaces.len(), 1);
} else {
workspaces = repeated_workspace.items;
}
workspaces
}
pub async fn create_app(sdk: &FlowyTestSDK, name: &str, desc: &str, workspace_id: &str) -> App {
let create_app_request = CreateAppRequest {
workspace_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
color_style: Default::default(),
};
let app = FlowyWorkspaceTest::new(sdk.clone())
.event(CreateApp)
.request(create_app_request)
.async_send()
.await
.parse::<App>();
app
}
pub async fn delete_app(sdk: &FlowyTestSDK, app_id: &str) {
let delete_app_request = AppIdentifier {
app_id: app_id.to_string(),
};
FlowyWorkspaceTest::new(sdk.clone())
.event(DeleteApp)
.request(delete_app_request)
.async_send()
.await;
}
pub async fn update_app(sdk: &FlowyTestSDK, request: UpdateAppRequest) {
FlowyWorkspaceTest::new(sdk.clone())
.event(UpdateApp)
.request(request)
.async_send()
.await;
}
pub async fn read_app(sdk: &FlowyTestSDK, request: QueryAppRequest) -> App {
let app = FlowyWorkspaceTest::new(sdk.clone())
.event(ReadApp)
.request(request)
.async_send()
.await
.parse::<App>();
app
}
pub async fn create_view_with_request(sdk: &FlowyTestSDK, request: CreateViewRequest) -> View {
let view = FlowyWorkspaceTest::new(sdk.clone())
.event(CreateView)
.request(request)
.async_send()
.await
.parse::<View>();
view
}
pub async fn create_view(sdk: &FlowyTestSDK, app_id: &str) -> View {
let request = CreateViewRequest {
belong_to_id: app_id.to_string(),
name: "View A".to_string(),
desc: "".to_string(),
thumbnail: Some("http://1.png".to_string()),
view_type: ViewType::Doc,
};
create_view_with_request(sdk, request).await
}
pub async fn update_view(sdk: &FlowyTestSDK, request: UpdateViewRequest) {
FlowyWorkspaceTest::new(sdk.clone())
.event(UpdateView)
.request(request)
.async_send()
.await;
}
pub async fn read_view(sdk: &FlowyTestSDK, request: QueryViewRequest) -> View {
FlowyWorkspaceTest::new(sdk.clone())
.event(ReadView)
.request(request)
.async_send()
.await
.parse::<View>()
}
pub async fn delete_view(sdk: &FlowyTestSDK, request: QueryViewRequest) {
FlowyWorkspaceTest::new(sdk.clone())
.event(DeleteView)
.request(request)
.async_send()
.await;
}
pub async fn read_trash(sdk: &FlowyTestSDK) -> RepeatedTrash {
FlowyWorkspaceTest::new(sdk.clone())
.event(ReadTrash)
.async_send()
.await
.parse::<RepeatedTrash>()
}
pub async fn putback_trash(sdk: &FlowyTestSDK, id: TrashIdentifier) {
FlowyWorkspaceTest::new(sdk.clone())
.event(PutbackTrash)
.request(id)
.async_send()
.await;
}
pub async fn open_view(sdk: &FlowyTestSDK, request: QueryViewRequest) -> Doc {
FlowyWorkspaceTest::new(sdk.clone())
.event(OpenView)
.request(request)
.async_send()
.await
.parse::<Doc>()
}

View File

@ -1,11 +1,11 @@
use crate::helper::*;
use flowy_test::{builder::UserTest, FlowyTest};
use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest};
use flowy_user::{errors::ErrorCode, event::UserEvent::*, prelude::*};
#[tokio::test]
async fn sign_up_with_invalid_email() {
for email in invalid_email_test_case() {
let test = FlowyTest::setup();
let sdk = FlowySDKTest::setup();
let request = SignUpRequest {
email: email.to_string(),
name: valid_name(),
@ -13,7 +13,7 @@ async fn sign_up_with_invalid_email() {
};
assert_eq!(
UserTest::new(test.sdk)
UserModuleEventBuilder::new(sdk)
.event(SignUp)
.request(request)
.async_send()
@ -27,14 +27,14 @@ async fn sign_up_with_invalid_email() {
#[tokio::test]
async fn sign_up_with_invalid_password() {
for password in invalid_password_test_case() {
let test = FlowyTest::setup();
let sdk = FlowySDKTest::setup();
let request = SignUpRequest {
email: random_email(),
name: valid_name(),
password,
};
UserTest::new(test.sdk)
UserModuleEventBuilder::new(sdk)
.event(SignUp)
.request(request)
.async_send()
@ -45,8 +45,8 @@ async fn sign_up_with_invalid_password() {
#[tokio::test]
async fn sign_in_success() {
let test = FlowyTest::setup();
let _ = UserTest::new(test.sdk()).event(SignOut).sync_send();
let test = FlowySDKTest::setup();
let _ = UserModuleEventBuilder::new(test.clone()).event(SignOut).sync_send();
let sign_up_context = test.sign_up().await;
let request = SignInRequest {
@ -55,7 +55,7 @@ async fn sign_in_success() {
name: "".to_string(),
};
let response = UserTest::new(test.sdk())
let response = UserModuleEventBuilder::new(test.clone())
.event(SignIn)
.request(request)
.async_send()
@ -67,7 +67,7 @@ async fn sign_in_success() {
#[tokio::test]
async fn sign_in_with_invalid_email() {
for email in invalid_email_test_case() {
let test = FlowyTest::setup();
let sdk = FlowySDKTest::setup();
let request = SignInRequest {
email: email.to_string(),
password: login_password(),
@ -75,7 +75,7 @@ async fn sign_in_with_invalid_email() {
};
assert_eq!(
UserTest::new(test.sdk)
UserModuleEventBuilder::new(sdk)
.event(SignIn)
.request(request)
.async_send()
@ -90,7 +90,7 @@ async fn sign_in_with_invalid_email() {
#[tokio::test]
async fn sign_in_with_invalid_password() {
for password in invalid_password_test_case() {
let test = FlowyTest::setup();
let sdk = FlowySDKTest::setup();
let request = SignInRequest {
email: random_email(),
@ -98,7 +98,7 @@ async fn sign_in_with_invalid_password() {
name: "".to_string(),
};
UserTest::new(test.sdk)
UserModuleEventBuilder::new(sdk)
.event(SignIn)
.request(request)
.async_send()

View File

@ -1,5 +1,5 @@
pub use flowy_test::{
builder::*,
event_builder::*,
prelude::{login_password, random_email},
};

View File

@ -1,13 +1,13 @@
use crate::helper::*;
use flowy_test::{builder::UserTest, FlowyTest};
use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest};
use flowy_user::{errors::ErrorCode, event::UserEvent::*, prelude::*};
use lib_infra::uuid;
use serial_test::*;
#[tokio::test]
async fn user_profile_get_failed() {
let test = FlowyTest::setup();
let result = UserTest::new(test.sdk)
let sdk = FlowySDKTest::setup();
let result = UserModuleEventBuilder::new(sdk)
.event(GetUserProfile)
.assert_error()
.async_send()
@ -18,9 +18,9 @@ async fn user_profile_get_failed() {
#[tokio::test]
#[serial]
async fn user_profile_get() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let user_profile = test.init_user().await;
let user = UserTest::new(test.sdk.clone())
let user = UserModuleEventBuilder::new(test.clone())
.event(GetUserProfile)
.sync_send()
.parse::<UserProfile>();
@ -30,13 +30,16 @@ async fn user_profile_get() {
#[tokio::test]
#[serial]
async fn user_update_with_name() {
let test = FlowyTest::setup();
let user = test.init_user().await;
let sdk = FlowySDKTest::setup();
let user = sdk.init_user().await;
let new_name = "hello_world".to_owned();
let request = UpdateUserRequest::new(&user.id).name(&new_name);
let _ = UserTest::new(test.sdk()).event(UpdateUser).request(request).sync_send();
let _ = UserModuleEventBuilder::new(sdk.clone())
.event(UpdateUser)
.request(request)
.sync_send();
let user_profile = UserTest::new(test.sdk())
let user_profile = UserModuleEventBuilder::new(sdk.clone())
.event(GetUserProfile)
.assert_error()
.sync_send()
@ -48,12 +51,15 @@ async fn user_update_with_name() {
#[tokio::test]
#[serial]
async fn user_update_with_email() {
let test = FlowyTest::setup();
let user = test.init_user().await;
let sdk = FlowySDKTest::setup();
let user = sdk.init_user().await;
let new_email = format!("{}@gmail.com", uuid());
let request = UpdateUserRequest::new(&user.id).email(&new_email);
let _ = UserTest::new(test.sdk()).event(UpdateUser).request(request).sync_send();
let user_profile = UserTest::new(test.sdk())
let _ = UserModuleEventBuilder::new(sdk.clone())
.event(UpdateUser)
.request(request)
.sync_send();
let user_profile = UserModuleEventBuilder::new(sdk.clone())
.event(GetUserProfile)
.assert_error()
.sync_send()
@ -65,12 +71,12 @@ async fn user_update_with_email() {
#[tokio::test]
#[serial]
async fn user_update_with_password() {
let test = FlowyTest::setup();
let user = test.init_user().await;
let sdk = FlowySDKTest::setup();
let user = sdk.init_user().await;
let new_password = "H123world!".to_owned();
let request = UpdateUserRequest::new(&user.id).password(&new_password);
let _ = UserTest::new(test.sdk())
let _ = UserModuleEventBuilder::new(sdk.clone())
.event(UpdateUser)
.request(request)
.sync_send()
@ -80,12 +86,12 @@ async fn user_update_with_password() {
#[tokio::test]
#[serial]
async fn user_update_with_invalid_email() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let user = test.init_user().await;
for email in invalid_email_test_case() {
let request = UpdateUserRequest::new(&user.id).email(&email);
assert_eq!(
UserTest::new(test.sdk())
UserModuleEventBuilder::new(test.clone())
.event(UpdateUser)
.request(request)
.sync_send()
@ -99,12 +105,12 @@ async fn user_update_with_invalid_email() {
#[tokio::test]
#[serial]
async fn user_update_with_invalid_password() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let user = test.init_user().await;
for password in invalid_password_test_case() {
let request = UpdateUserRequest::new(&user.id).password(&password);
UserTest::new(test.sdk())
UserModuleEventBuilder::new(test.clone())
.event(UpdateUser)
.request(request)
.sync_send()
@ -115,10 +121,10 @@ async fn user_update_with_invalid_password() {
#[tokio::test]
#[serial]
async fn user_update_with_invalid_name() {
let test = FlowyTest::setup();
let test = FlowySDKTest::setup();
let user = test.init_user().await;
let request = UpdateUserRequest::new(&user.id).name("");
UserTest::new(test.sdk())
UserModuleEventBuilder::new(test.clone())
.event(UpdateUser)
.request(request)
.sync_send()

View File

@ -6,8 +6,11 @@ use std::convert::{TryFrom, TryInto};
#[derive(Debug, Clone, ProtoBuf_Enum, Eq, PartialEq, Hash)]
pub enum WsDataType {
// The frontend receives the Acked means the backend has accepted the revision
Acked = 0,
// The frontend receives the PushRev event means the backend is pushing the new revision to frontend
PushRev = 1,
// The fronted receives the PullRev event means the backend try to pull the revision from frontend
PullRev = 2, // data should be Revision
Conflict = 3,
NewDocUser = 4,

View File

@ -36,6 +36,7 @@ impl OTError {
}
static_ot_error!(duplicate_revision, OTErrorCode::DuplicatedRevision);
static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict);
}
impl fmt::Display for OTError {
@ -66,6 +67,7 @@ pub enum OTErrorCode {
RedoFail,
SerdeError,
DuplicatedRevision,
RevisionIDConflict,
}
pub struct ErrorBuilder {

View File

@ -33,8 +33,12 @@ impl RevisionMemoryCache {
pub fn new() -> Self { RevisionMemoryCache::default() }
pub async fn add_revision(&self, revision: Revision) -> Result<(), OTError> {
if self.revs_map.contains_key(&revision.rev_id) {
return Ok(());
// The last revision's rev_id must be greater than the new one.
if let Some(rev_id) = self.pending_revs.read().await.back() {
if *rev_id >= revision.rev_id {
return Err(OTError::revision_id_conflict()
.context(format!("The new revision's id must be greater than {}", rev_id)));
}
}
self.pending_revs.write().await.push_back(revision.rev_id);
@ -121,21 +125,3 @@ impl RevisionRecord {
pub fn ack(&mut self) { self.state = RevState::Acked; }
}
pub struct PendingRevId {
pub rev_id: i64,
pub sender: RevIdSender,
}
impl PendingRevId {
pub fn new(rev_id: i64, sender: RevIdSender) -> Self { Self { rev_id, sender } }
pub fn finish(&self, rev_id: i64) -> bool {
if self.rev_id > rev_id {
false
} else {
let _ = self.sender.send(self.rev_id);
true
}
}
}

View File

@ -25,6 +25,8 @@ pub struct Revision {
impl Revision {
pub fn is_empty(&self) -> bool { self.base_rev_id == self.rev_id }
pub fn pair_rev_id(&self) -> (i64, i64) { (self.base_rev_id, self.rev_id) }
}
impl std::fmt::Debug for Revision {