Remove projects from contact updates

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2022-09-29 19:40:36 +02:00
parent 1898e813f5
commit b35e8f0164
7 changed files with 45 additions and 1339 deletions

View file

@ -8,7 +8,7 @@ use anyhow::anyhow;
use call::Room;
use client::{
self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT,
Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
};
use collections::{BTreeMap, HashMap, HashSet};
use editor::{
@ -731,294 +731,6 @@ async fn test_cancel_join_request(
);
}
#[gpui::test(iterations = 10)]
async fn test_offline_projects(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let user_a = UserId::from_proto(client_a.user_id().unwrap());
server
.make_contacts(vec![
(&client_a, cx_a),
(&client_b, cx_b),
(&client_c, cx_c),
])
.await;
// Set up observers of the project and user stores. Any time either of
// these models update, they should be in a consistent state with each
// other. There should not be an observable moment where the current
// user's contact entry contains a project that does not match one of
// the current open projects. That would cause a duplicate entry to be
// shown in the contacts panel.
let mut subscriptions = vec![];
let (window_id, view) = cx_a.add_window(|cx| {
subscriptions.push(cx.observe(&client_a.user_store, {
let project_store = client_a.project_store.clone();
let user_store = client_a.user_store.clone();
move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx)
}));
subscriptions.push(cx.observe(&client_a.project_store, {
let project_store = client_a.project_store.clone();
let user_store = client_a.user_store.clone();
move |_, _, cx| check_project_list(project_store.clone(), user_store.clone(), cx)
}));
fn check_project_list(
project_store: ModelHandle<ProjectStore>,
user_store: ModelHandle<UserStore>,
cx: &mut gpui::MutableAppContext,
) {
let user_store = user_store.read(cx);
for contact in user_store.contacts() {
if contact.user.id == user_store.current_user().unwrap().id {
for project in &contact.projects {
let store_contains_project = project_store
.read(cx)
.projects(cx)
.filter_map(|project| project.read(cx).remote_id())
.any(|x| x == project.id);
if !store_contains_project {
panic!(
concat!(
"current user's contact data has a project",
"that doesn't match any open project {:?}",
),
project
);
}
}
}
}
}
EmptyView
});
// Build an offline project with two worktrees.
client_a
.fs
.insert_tree(
"/code",
json!({
"crate1": { "a.rs": "" },
"crate2": { "b.rs": "" },
}),
)
.await;
let project = cx_a.update(|cx| {
Project::local(
false,
client_a.client.clone(),
client_a.user_store.clone(),
client_a.project_store.clone(),
client_a.language_registry.clone(),
client_a.fs.clone(),
cx,
)
});
project
.update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/code/crate1", true, cx)
})
.await
.unwrap();
project
.update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/code/crate2", true, cx)
})
.await
.unwrap();
project
.update(cx_a, |p, cx| p.restore_state(cx))
.await
.unwrap();
// When a project is offline, we still create it on the server but is invisible
// to other users.
deterministic.run_until_parked();
assert!(server
.store
.lock()
.await
.project_metadata_for_user(user_a)
.is_empty());
project.read_with(cx_a, |project, _| {
assert!(project.remote_id().is_some());
assert!(!project.is_online());
});
assert!(client_b
.user_store
.read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
// When the project is taken online, its metadata is sent to the server
// and broadcasted to other users.
project.update(cx_a, |p, cx| p.set_online(true, cx));
deterministic.run_until_parked();
let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap();
client_b.user_store.read_with(cx_b, |store, _| {
assert_eq!(
store.contacts()[0].projects,
&[ProjectMetadata {
id: project_id,
visible_worktree_root_names: vec!["crate1".into(), "crate2".into()],
guests: Default::default(),
}]
);
});
// The project is registered again when the host loses and regains connection.
server.disconnect_client(user_a);
server.forbid_connections();
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
assert!(server
.store
.lock()
.await
.project_metadata_for_user(user_a)
.is_empty());
assert!(project.read_with(cx_a, |p, _| p.remote_id().is_none()));
assert!(client_b
.user_store
.read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() }));
server.allow_connections();
cx_b.foreground().advance_clock(Duration::from_secs(10));
let project_id = project.read_with(cx_a, |p, _| p.remote_id()).unwrap();
client_b.user_store.read_with(cx_b, |store, _| {
assert_eq!(
store.contacts()[0].projects,
&[ProjectMetadata {
id: project_id,
visible_worktree_root_names: vec!["crate1".into(), "crate2".into()],
guests: Default::default(),
}]
);
});
project
.update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/code/crate3", true, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
client_b.user_store.read_with(cx_b, |store, _| {
assert_eq!(
store.contacts()[0].projects,
&[ProjectMetadata {
id: project_id,
visible_worktree_root_names: vec![
"crate1".into(),
"crate2".into(),
"crate3".into()
],
guests: Default::default(),
}]
);
});
// Build another project using a directory which was previously part of
// an online project. Restore the project's state from the host's database.
let project2_a = cx_a.update(|cx| {
Project::local(
false,
client_a.client.clone(),
client_a.user_store.clone(),
client_a.project_store.clone(),
client_a.language_registry.clone(),
client_a.fs.clone(),
cx,
)
});
project2_a
.update(cx_a, |p, cx| {
p.find_or_create_local_worktree("/code/crate3", true, cx)
})
.await
.unwrap();
project2_a
.update(cx_a, |project, cx| project.restore_state(cx))
.await
.unwrap();
// This project is now online, because its directory was previously online.
project2_a.read_with(cx_a, |project, _| assert!(project.is_online()));
deterministic.run_until_parked();
let project2_id = project2_a.read_with(cx_a, |p, _| p.remote_id()).unwrap();
client_b.user_store.read_with(cx_b, |store, _| {
assert_eq!(
store.contacts()[0].projects,
&[
ProjectMetadata {
id: project_id,
visible_worktree_root_names: vec![
"crate1".into(),
"crate2".into(),
"crate3".into()
],
guests: Default::default(),
},
ProjectMetadata {
id: project2_id,
visible_worktree_root_names: vec!["crate3".into()],
guests: Default::default(),
}
]
);
});
let project2_b = client_b.build_remote_project(&project2_a, cx_a, cx_b).await;
let project2_c = cx_c.foreground().spawn(Project::remote(
project2_id,
client_c.client.clone(),
client_c.user_store.clone(),
client_c.project_store.clone(),
client_c.language_registry.clone(),
FakeFs::new(cx_c.background()),
cx_c.to_async(),
));
deterministic.run_until_parked();
// Taking a project offline unshares the project, rejects any pending join request and
// disconnects existing guests.
project2_a.update(cx_a, |project, cx| project.set_online(false, cx));
deterministic.run_until_parked();
project2_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project2_b.read_with(cx_b, |project, _| assert!(project.is_read_only()));
project2_c.await.unwrap_err();
client_b.user_store.read_with(cx_b, |store, _| {
assert_eq!(
store.contacts()[0].projects,
&[ProjectMetadata {
id: project_id,
visible_worktree_root_names: vec![
"crate1".into(),
"crate2".into(),
"crate3".into()
],
guests: Default::default(),
},]
);
});
cx_a.update(|cx| {
drop(subscriptions);
drop(view);
cx.remove_window(window_id);
});
}
#[gpui::test(iterations = 10)]
async fn test_propagate_saves_and_fs_changes(
cx_a: &mut TestAppContext,
@ -3911,24 +3623,15 @@ async fn test_contacts(
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![]),
("user_c".to_string(), true, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), true, vec![]),
("user_c".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_c, cx_c),
[
("user_a".to_string(), true, vec![]),
("user_b".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_b".to_string(), true)]
);
// Share a project as client A.
@ -3938,24 +3641,15 @@ async fn test_contacts(
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![]),
("user_c".to_string(), true, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), true, vec![("a".to_string(), vec![])]),
("user_c".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_c, cx_c),
[
("user_a".to_string(), true, vec![("a".to_string(), vec![])]),
("user_b".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_b".to_string(), true)]
);
let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
@ -3963,32 +3657,15 @@ async fn test_contacts(
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![]),
("user_c".to_string(), true, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
(
"user_a".to_string(),
true,
vec![("a".to_string(), vec!["user_b".to_string()])]
),
("user_c".to_string(), true, vec![])
]
[("user_a".to_string(), true,), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_c, cx_c),
[
(
"user_a".to_string(),
true,
vec![("a".to_string(), vec!["user_b".to_string()])]
),
("user_b".to_string(), true, vec![])
]
[("user_a".to_string(), true,), ("user_b".to_string(), true)]
);
// Add a local project as client B
@ -3998,32 +3675,15 @@ async fn test_contacts(
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
("user_c".to_string(), true, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
(
"user_a".to_string(),
true,
vec![("a".to_string(), vec!["user_b".to_string()])]
),
("user_c".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_c, cx_c),
[
(
"user_a".to_string(),
true,
vec![("a".to_string(), vec!["user_b".to_string()])]
),
("user_b".to_string(), true, vec![("b".to_string(), vec![])])
]
[("user_a".to_string(), true,), ("user_b".to_string(), true)]
);
project_a
@ -4036,24 +3696,15 @@ async fn test_contacts(
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
("user_c".to_string(), true, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), true, vec![]),
("user_c".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_c, cx_c),
[
("user_a".to_string(), true, vec![]),
("user_b".to_string(), true, vec![("b".to_string(), vec![])])
]
[("user_a".to_string(), true), ("user_b".to_string(), true)]
);
server.disconnect_client(client_c.current_user_id(cx_c));
@ -4061,17 +3712,11 @@ async fn test_contacts(
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
("user_c".to_string(), false, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), false)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), true, vec![]),
("user_c".to_string(), false, vec![])
]
[("user_a".to_string(), true), ("user_c".to_string(), false)]
);
assert_eq!(contacts(&client_c, cx_c), []);
@ -4084,48 +3729,24 @@ async fn test_contacts(
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), true, vec![("b".to_string(), vec![])]),
("user_c".to_string(), true, vec![])
]
[("user_b".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), true, vec![]),
("user_c".to_string(), true, vec![])
]
[("user_a".to_string(), true), ("user_c".to_string(), true)]
);
assert_eq!(
contacts(&client_c, cx_c),
[
("user_a".to_string(), true, vec![]),
("user_b".to_string(), true, vec![("b".to_string(), vec![])])
]
[("user_a".to_string(), true), ("user_b".to_string(), true)]
);
#[allow(clippy::type_complexity)]
fn contacts(
client: &TestClient,
cx: &TestAppContext,
) -> Vec<(String, bool, Vec<(String, Vec<String>)>)> {
fn contacts(client: &TestClient, cx: &TestAppContext) -> Vec<(String, bool)> {
client.user_store.read_with(cx, |store, _| {
store
.contacts()
.iter()
.map(|contact| {
let projects = contact
.projects
.iter()
.map(|p| {
(
p.visible_worktree_root_names[0].clone(),
p.guests.iter().map(|p| p.github_login.clone()).collect(),
)
})
.collect();
(contact.user.github_login.clone(), contact.online, projects)
})
.map(|contact| (contact.user.github_login.clone(), contact.online))
.collect()
})
}
@ -5155,22 +4776,6 @@ async fn test_random_collaboration(
log::error!("{} error - {:?}", guest.username, guest_err);
}
let contacts = server
.app_state
.db
.get_contacts(guest.current_user_id(&guest_cx))
.await
.unwrap();
let contacts = server
.store
.lock()
.await
.build_initial_contacts_update(contacts)
.contacts;
assert!(!contacts
.iter()
.flat_map(|contact| &contact.projects)
.any(|project| project.id == host_project_id));
guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
guest_cx.update(|_| drop((guest, guest_project)));
}
@ -5259,14 +4864,6 @@ async fn test_random_collaboration(
"removed guest is still a contact of another peer"
);
}
for project in contact.projects {
for project_guest_id in project.guests {
assert_ne!(
project_guest_id, removed_guest_id.0 as u64,
"removed guest appears as still participating on a project"
);
}
}
}
}

View file

@ -345,47 +345,11 @@ impl Store {
pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact {
proto::Contact {
user_id: user_id.to_proto(),
projects: self.project_metadata_for_user(user_id),
online: self.is_user_online(user_id),
should_notify,
}
}
pub fn project_metadata_for_user(&self, user_id: UserId) -> Vec<proto::ProjectMetadata> {
let user_connection_state = self.connected_users.get(&user_id);
let project_ids = user_connection_state.iter().flat_map(|state| {
state
.connection_ids
.iter()
.filter_map(|connection_id| self.connections.get(connection_id))
.flat_map(|connection| connection.projects.iter().copied())
});
let mut metadata = Vec::new();
for project_id in project_ids {
if let Some(project) = self.projects.get(&project_id) {
if project.host.user_id == user_id && project.online {
metadata.push(proto::ProjectMetadata {
id: project_id.to_proto(),
visible_worktree_root_names: project
.worktrees
.values()
.filter(|worktree| worktree.visible)
.map(|worktree| worktree.root_name.clone())
.collect(),
guests: project
.guests
.values()
.map(|guest| guest.user_id.to_proto())
.collect(),
});
}
}
}
metadata
}
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<RoomId> {
let connection = self
.connections