mod app_bundle;
mod app_manifest;
pub mod error;
use crate::{dna::DnaBundle, prelude::CoordinatorBundle};
pub use app_bundle::*;
pub use app_manifest::app_manifest_validated::*;
pub use app_manifest::*;
use derive_more::{Display, Into};
use holo_hash::{AgentPubKey, DnaHash};
use holochain_serialized_bytes::prelude::*;
use holochain_util::ffs;
use holochain_zome_types::cell::CloneId;
use holochain_zome_types::prelude::*;
use itertools::Itertools;
use std::{collections::HashMap, path::PathBuf};
use self::error::{AppError, AppResult};
pub type InstalledAppId = String;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DnaSource {
Path(PathBuf),
Bundle(Box<DnaBundle>),
Hash(DnaHash),
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CoordinatorSource {
Path(PathBuf),
Bundle(Box<CoordinatorBundle>),
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RegisterDnaPayload {
#[serde(default)]
pub modifiers: DnaModifiersOpt<YamlProperties>,
#[serde(flatten)]
pub source: DnaSource,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NetworkInfoRequestPayload {
pub agent_pub_key: AgentPubKey,
pub dnas: Vec<DnaHash>,
pub last_time_queried: Option<Timestamp>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct UpdateCoordinatorsPayload {
pub dna_hash: DnaHash,
#[serde(flatten)]
pub source: CoordinatorSource,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct CreateCloneCellPayload {
pub app_id: InstalledAppId,
pub role_name: RoleName,
pub modifiers: DnaModifiersOpt<YamlProperties>,
pub membrane_proof: Option<MembraneProof>,
pub name: Option<String>,
}
#[derive(Clone, Debug, Display, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum CloneCellId {
CloneId(CloneId),
CellId(CellId),
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DisableCloneCellPayload {
pub app_id: InstalledAppId,
pub clone_cell_id: CloneCellId,
}
pub type EnableCloneCellPayload = DisableCloneCellPayload;
pub type DeleteCloneCellPayload = DisableCloneCellPayload;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallAppPayload {
#[serde(flatten)]
pub source: AppBundleSource,
pub agent_key: AgentPubKey,
pub installed_app_id: Option<InstalledAppId>,
pub membrane_proofs: HashMap<RoleName, MembraneProof>,
pub network_seed: Option<NetworkSeed>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AppBundleSource {
Bundle(AppBundle),
Path(PathBuf),
}
impl AppBundleSource {
pub async fn resolve(self) -> Result<AppBundle, AppBundleError> {
Ok(match self {
Self::Bundle(bundle) => bundle,
Self::Path(path) => AppBundle::decode(&ffs::read(&path).await?)?,
})
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct InstallAppDnaPayload {
pub hash: DnaHash,
pub role_name: RoleName,
pub membrane_proof: Option<MembraneProof>,
}
impl InstallAppDnaPayload {
pub fn hash_only(hash: DnaHash, role_name: RoleName) -> Self {
Self {
hash,
role_name,
membrane_proof: None,
}
}
}
#[derive(Clone, Debug, Into, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct InstalledCell {
cell_id: CellId,
role_name: RoleName,
}
impl InstalledCell {
pub fn new(cell_id: CellId, role_name: RoleName) -> Self {
Self { cell_id, role_name }
}
pub fn into_id(self) -> CellId {
self.cell_id
}
pub fn into_role_name(self) -> RoleName {
self.role_name
}
pub fn into_inner(self) -> (CellId, RoleName) {
(self.cell_id, self.role_name)
}
pub fn as_id(&self) -> &CellId {
&self.cell_id
}
pub fn as_role_name(&self) -> &RoleName {
&self.role_name
}
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
derive_more::Constructor,
shrinkwraprs::Shrinkwrap,
)]
#[shrinkwrap(mutable, unsafe_ignore_visibility)]
pub struct InstalledApp {
#[shrinkwrap(main_field)]
app: InstalledAppCommon,
pub status: AppStatus,
}
impl InstalledApp {
pub fn new_fresh(app: InstalledAppCommon) -> Self {
Self {
app,
status: AppStatus::Disabled(DisabledAppReason::NeverStarted),
}
}
#[cfg(feature = "test_utils")]
pub fn new_running(app: InstalledAppCommon) -> Self {
Self {
app,
status: AppStatus::Running,
}
}
pub fn into_app_and_status(self) -> (InstalledAppCommon, AppStatus) {
(self.app, self.status)
}
pub fn status(&self) -> &AppStatus {
&self.status
}
pub fn id(&self) -> &InstalledAppId {
&self.app.installed_app_id
}
}
impl automap::AutoMapped for InstalledApp {
type Key = InstalledAppId;
fn key(&self) -> &Self::Key {
&self.app.installed_app_id
}
}
pub type InstalledAppMap = automap::AutoHashMap<InstalledApp>;
#[derive(
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
derive_more::From,
shrinkwraprs::Shrinkwrap,
)]
#[shrinkwrap(mutable, unsafe_ignore_visibility)]
pub struct RunningApp(InstalledAppCommon);
impl RunningApp {
pub fn into_stopped(self, reason: StoppedAppReason) -> StoppedApp {
StoppedApp {
app: self.0,
reason,
}
}
pub fn into_common(self) -> InstalledAppCommon {
self.0
}
}
impl From<RunningApp> for InstalledApp {
fn from(app: RunningApp) -> Self {
Self {
app: app.into_common(),
status: AppStatus::Running,
}
}
}
#[derive(
Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, shrinkwraprs::Shrinkwrap,
)]
#[shrinkwrap(mutable, unsafe_ignore_visibility)]
pub struct StoppedApp {
#[shrinkwrap(main_field)]
app: InstalledAppCommon,
reason: StoppedAppReason,
}
impl StoppedApp {
#[deprecated = "should only be constructable through conversions from other types"]
pub fn new(app: InstalledAppCommon, reason: StoppedAppReason) -> Self {
Self { app, reason }
}
pub fn new_fresh(app: InstalledAppCommon) -> Self {
Self {
app,
reason: StoppedAppReason::Disabled(DisabledAppReason::NeverStarted),
}
}
pub fn from_app(app: &InstalledApp) -> Option<Self> {
StoppedAppReason::from_status(app.status()).map(|reason| Self {
app: app.as_ref().clone(),
reason,
})
}
pub fn into_active(self) -> RunningApp {
RunningApp(self.app)
}
pub fn into_common(self) -> InstalledAppCommon {
self.app
}
}
impl From<StoppedApp> for InstalledAppCommon {
fn from(d: StoppedApp) -> Self {
d.app
}
}
impl From<StoppedApp> for InstalledApp {
fn from(d: StoppedApp) -> Self {
Self {
app: d.app,
status: d.reason.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct InstalledAppCommon {
installed_app_id: InstalledAppId,
agent_key: AgentPubKey,
role_assignments: HashMap<RoleName, AppRoleAssignment>,
manifest: AppManifest,
}
impl InstalledAppCommon {
pub fn new<S: ToString, I: IntoIterator<Item = (RoleName, AppRoleAssignment)>>(
installed_app_id: S,
agent_key: AgentPubKey,
role_assignments: I,
manifest: AppManifest,
) -> AppResult<Self> {
let role_assignments: HashMap<_, _> = role_assignments.into_iter().collect();
if let Some((illegal_role_name, _)) = role_assignments
.iter()
.find(|(role_name, _)| role_name.contains(CLONE_ID_DELIMITER))
{
return Err(AppError::IllegalRoleName(illegal_role_name.clone()));
}
Ok(InstalledAppCommon {
installed_app_id: installed_app_id.to_string(),
agent_key,
role_assignments,
manifest,
})
}
pub fn id(&self) -> &InstalledAppId {
&self.installed_app_id
}
pub fn provisioned_cells(&self) -> impl Iterator<Item = (&RoleName, &CellId)> {
self.role_assignments
.iter()
.filter_map(|(role_name, role)| role.provisioned_cell().map(|c| (role_name, c)))
}
pub fn into_provisioned_cells(self) -> impl Iterator<Item = (RoleName, CellId)> {
self.role_assignments
.into_iter()
.filter_map(|(role_name, role)| role.into_provisioned_cell().map(|c| (role_name, c)))
}
pub fn clone_cells(&self) -> impl Iterator<Item = (&CloneId, &CellId)> {
self.role_assignments
.iter()
.flat_map(|app_role_assignment| app_role_assignment.1.clones.iter())
}
pub fn disabled_clone_cells(&self) -> impl Iterator<Item = (&CloneId, &CellId)> {
self.role_assignments
.iter()
.flat_map(|app_role_assignment| app_role_assignment.1.disabled_clones.iter())
}
pub fn clone_cells_for_role_name(
&self,
role_name: &RoleName,
) -> Option<&HashMap<CloneId, CellId>> {
match self.role_assignments.get(role_name) {
None => None,
Some(role_assignments) => Some(&role_assignments.clones),
}
}
pub fn disabled_clone_cells_for_role_name(
&self,
role_name: &RoleName,
) -> Option<&HashMap<CloneId, CellId>> {
match self.role_assignments.get(role_name) {
None => None,
Some(role_assignment) => Some(&role_assignment.disabled_clones),
}
}
pub fn clone_cell_ids(&self) -> impl Iterator<Item = &CellId> {
self.clone_cells().map(|(_, cell_id)| cell_id)
}
pub fn disabled_clone_cell_ids(&self) -> impl Iterator<Item = &CellId> {
self.disabled_clone_cells().map(|(_, cell_id)| cell_id)
}
pub fn all_cells(&self) -> impl Iterator<Item = &CellId> {
self.provisioned_cells()
.map(|(_, c)| c)
.chain(self.clone_cell_ids())
.chain(self.disabled_clone_cell_ids())
}
pub fn all_enabled_cells(&self) -> impl Iterator<Item = &CellId> {
self.provisioned_cells()
.map(|(_, c)| c)
.chain(self.clone_cell_ids())
}
pub fn required_cells(&self) -> impl Iterator<Item = &CellId> {
self.provisioned_cells().map(|(_, c)| c)
}
pub fn role(&self, role_name: &RoleName) -> AppResult<&AppRoleAssignment> {
self.role_assignments
.get(role_name)
.ok_or_else(|| AppError::RoleNameMissing(role_name.clone()))
}
fn role_mut(&mut self, role_name: &RoleName) -> AppResult<&mut AppRoleAssignment> {
self.role_assignments
.get_mut(role_name)
.ok_or_else(|| AppError::RoleNameMissing(role_name.clone()))
}
pub fn roles(&self) -> &HashMap<RoleName, AppRoleAssignment> {
&self.role_assignments
}
pub fn add_clone(&mut self, role_name: &RoleName, cell_id: &CellId) -> AppResult<CloneId> {
let app_role_assignment = self.role_mut(role_name)?;
assert_eq!(
cell_id.agent_pubkey(),
app_role_assignment.agent_key(),
"A clone cell must use the same agent key as the role it is added to"
);
if app_role_assignment.is_clone_limit_reached() {
return Err(AppError::CloneLimitExceeded(
app_role_assignment.clone_limit,
app_role_assignment.clone(),
));
}
let clone_id = CloneId::new(role_name, app_role_assignment.next_clone_index);
if app_role_assignment.clones.contains_key(&clone_id) {
return Err(AppError::DuplicateCloneIds(clone_id));
}
app_role_assignment
.clones
.insert(clone_id.clone(), cell_id.clone());
app_role_assignment.next_clone_index += 1;
Ok(clone_id)
}
pub fn get_clone_cell_id(&self, clone_cell_id: &CloneCellId) -> AppResult<CellId> {
let cell_id = match clone_cell_id {
CloneCellId::CellId(cell_id) => cell_id,
CloneCellId::CloneId(clone_id) => self
.role(&clone_id.as_base_role_name())?
.clones
.get(clone_id)
.ok_or_else(|| {
AppError::CloneCellNotFound(CloneCellId::CloneId(clone_id.clone()))
})?,
};
Ok(cell_id.clone())
}
pub fn get_clone_id(&self, clone_cell_id: &CloneCellId) -> AppResult<CloneId> {
let clone_id = match clone_cell_id {
CloneCellId::CloneId(id) => id,
CloneCellId::CellId(id) => {
self.clone_cells()
.find(|(_, cell_id)| *cell_id == id)
.ok_or_else(|| AppError::CloneCellNotFound(CloneCellId::CellId(id.clone())))?
.0
}
};
Ok(clone_id.clone())
}
pub fn get_disabled_clone_id(&self, clone_cell_id: &CloneCellId) -> AppResult<CloneId> {
let clone_id = match clone_cell_id {
CloneCellId::CloneId(id) => id.clone(),
CloneCellId::CellId(id) => {
self.role_assignments
.iter()
.flat_map(|(_, role_assignment)| role_assignment.disabled_clones.clone())
.find(|(_, cell_id)| cell_id == id)
.ok_or_else(|| AppError::CloneCellNotFound(CloneCellId::CellId(id.clone())))?
.0
}
};
Ok(clone_id)
}
pub fn disable_clone_cell(&mut self, clone_id: &CloneId) -> AppResult<()> {
let app_role_assignment = self.role_mut(&clone_id.as_base_role_name())?;
match app_role_assignment.clones.remove(clone_id) {
None => {
if app_role_assignment.disabled_clones.contains_key(clone_id) {
Ok(())
} else {
Err(AppError::CloneCellNotFound(CloneCellId::CloneId(
clone_id.to_owned(),
)))
}
}
Some(cell_id) => {
let insert_result = app_role_assignment
.disabled_clones
.insert(clone_id.to_owned(), cell_id);
assert!(
insert_result.is_none(),
"disable: clone cell is already disabled"
);
Ok(())
}
}
}
pub fn enable_clone_cell(&mut self, clone_id: &CloneId) -> AppResult<InstalledCell> {
let app_role_assignment = self.role_mut(&clone_id.as_base_role_name())?;
match app_role_assignment.disabled_clones.remove(clone_id) {
None => app_role_assignment
.clones
.get(clone_id)
.cloned()
.map(|cell_id| {
Ok(InstalledCell {
role_name: clone_id.as_app_role_name().to_owned(),
cell_id,
})
})
.unwrap_or_else(|| {
Err(AppError::CloneCellNotFound(CloneCellId::CloneId(
clone_id.to_owned(),
)))
}),
Some(cell_id) => {
let insert_result = app_role_assignment
.clones
.insert(clone_id.to_owned(), cell_id.clone());
assert!(
insert_result.is_none(),
"enable: clone cell already enabled"
);
Ok(InstalledCell {
role_name: clone_id.as_app_role_name().to_owned(),
cell_id,
})
}
}
}
pub fn delete_clone_cell(&mut self, clone_id: &CloneId) -> AppResult<()> {
let app_role_assignment = self.role_mut(&clone_id.as_base_role_name())?;
app_role_assignment
.disabled_clones
.remove(clone_id)
.map(|_| ())
.ok_or_else(|| {
if app_role_assignment.clones.contains_key(clone_id) {
AppError::CloneCellMustBeDisabledBeforeDeleting(CloneCellId::CloneId(
clone_id.to_owned(),
))
} else {
AppError::CloneCellNotFound(CloneCellId::CloneId(clone_id.to_owned()))
}
})
}
pub fn agent_key(&self) -> &AgentPubKey {
&self.agent_key
}
pub fn new_legacy<S: ToString, I: IntoIterator<Item = InstalledCell>>(
installed_app_id: S,
installed_cells: I,
) -> AppResult<Self> {
let installed_app_id = installed_app_id.to_string();
let installed_cells: Vec<_> = installed_cells.into_iter().collect();
let _agent_key = installed_cells
.get(0)
.expect("Can't create app with 0 cells")
.cell_id
.agent_pubkey()
.to_owned();
if installed_cells
.iter()
.any(|c| *c.cell_id.agent_pubkey() != _agent_key)
{
tracing::warn!(
"It's kind of an informal convention that all cells in a legacy installation should use the same agent key. But, no big deal... Cell data: {:#?}",
installed_cells
);
}
let duplicates: Vec<RoleName> = installed_cells
.iter()
.map(|c| c.role_name.to_owned())
.counts()
.into_iter()
.filter_map(|(role_name, count)| if count > 1 { Some(role_name) } else { None })
.collect();
if !duplicates.is_empty() {
return Err(AppError::DuplicateRoleNames(installed_app_id, duplicates));
}
let manifest = AppManifest::from_legacy(installed_cells.clone().into_iter());
let role_assignments = installed_cells
.into_iter()
.map(|InstalledCell { role_name, cell_id }| {
let role = AppRoleAssignment {
base_cell_id: cell_id,
is_provisioned: true,
clones: HashMap::new(),
clone_limit: 256,
next_clone_index: 0,
disabled_clones: HashMap::new(),
};
(role_name, role)
})
.collect();
Ok(Self {
installed_app_id,
agent_key: _agent_key,
role_assignments,
manifest,
})
}
pub fn manifest(&self) -> &AppManifest {
&self.manifest
}
pub fn role_assignments(&self) -> &HashMap<RoleName, AppRoleAssignment> {
&self.role_assignments
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(rename_all = "snake_case")]
pub enum AppStatus {
Running,
Paused(PausedAppReason),
Disabled(DisabledAppReason),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum AppStatusKind {
Running,
Paused,
Disabled,
}
impl From<AppStatus> for AppStatusKind {
fn from(status: AppStatus) -> Self {
match status {
AppStatus::Running => Self::Running,
AppStatus::Paused(_) => Self::Paused,
AppStatus::Disabled(_) => Self::Disabled,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AppStatusTransition {
Start,
Pause(PausedAppReason),
Enable,
Disable(DisabledAppReason),
}
impl AppStatus {
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Running | Self::Paused(_))
}
pub fn is_running(&self) -> bool {
matches!(self, Self::Running)
}
pub fn is_paused(&self) -> bool {
matches!(self, Self::Paused(_))
}
pub fn transition(&mut self, transition: AppStatusTransition) -> AppStatusFx {
use AppStatus::*;
use AppStatusFx::*;
use AppStatusTransition::*;
match (&self, transition) {
(Running, Pause(reason)) => Some((Paused(reason), SpinDown)),
(Running, Disable(reason)) => Some((Disabled(reason), SpinDown)),
(Running, Start) | (Running, Enable) => None,
(Paused(_), Start) => Some((Running, SpinUp)),
(Paused(_), Enable) => Some((Running, SpinUp)),
(Paused(_), Disable(reason)) => Some((Disabled(reason), SpinDown)),
(Paused(_), Pause(_)) => None,
(Disabled(_), Enable) => Some((Running, SpinUp)),
(Disabled(_), Pause(_)) | (Disabled(_), Disable(_)) | (Disabled(_), Start) => None,
}
.map(|(new_status, delta)| {
*self = new_status;
delta
})
.unwrap_or(NoChange)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[must_use = "be sure to run this value through `process_app_status_fx` to handle any transition effects"]
pub enum AppStatusFx {
NoChange,
SpinDown,
SpinUp,
Both,
}
impl Default for AppStatusFx {
fn default() -> Self {
Self::NoChange
}
}
impl AppStatusFx {
pub fn combine(self, other: Self) -> Self {
use AppStatusFx::*;
match (self, other) {
(NoChange, a) | (a, NoChange) => a,
(SpinDown, SpinDown) => SpinDown,
(SpinUp, SpinUp) => SpinUp,
(Both, _) | (_, Both) => Both,
(SpinDown, SpinUp) | (SpinUp, SpinDown) => Both,
}
}
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
serde::Deserialize,
SerializedBytes,
derive_more::From,
)]
#[serde(rename_all = "snake_case")]
pub enum StoppedAppReason {
Paused(PausedAppReason),
Disabled(DisabledAppReason),
}
impl StoppedAppReason {
pub fn from_status(status: &AppStatus) -> Option<Self> {
match status {
AppStatus::Paused(reason) => Some(Self::Paused(reason.clone())),
AppStatus::Disabled(reason) => Some(Self::Disabled(reason.clone())),
AppStatus::Running => None,
}
}
}
impl From<StoppedAppReason> for AppStatus {
fn from(reason: StoppedAppReason) -> Self {
match reason {
StoppedAppReason::Paused(reason) => Self::Paused(reason),
StoppedAppReason::Disabled(reason) => Self::Disabled(reason),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(rename_all = "snake_case")]
pub enum PausedAppReason {
Error(String),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
#[serde(rename_all = "snake_case")]
pub enum DisabledAppReason {
NeverStarted,
User,
Error(String),
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AppRoleAssignment {
base_cell_id: CellId,
is_provisioned: bool,
clone_limit: u32,
next_clone_index: u32,
clones: HashMap<CloneId, CellId>,
disabled_clones: HashMap<CloneId, CellId>,
}
impl AppRoleAssignment {
pub fn new(base_cell_id: CellId, is_provisioned: bool, clone_limit: u32) -> Self {
Self {
base_cell_id,
is_provisioned,
clone_limit,
clones: HashMap::new(),
next_clone_index: 0,
disabled_clones: HashMap::new(),
}
}
pub fn cell_id(&self) -> &CellId {
&self.base_cell_id
}
pub fn dna_hash(&self) -> &DnaHash {
self.base_cell_id.dna_hash()
}
pub fn agent_key(&self) -> &AgentPubKey {
self.base_cell_id.agent_pubkey()
}
pub fn provisioned_cell(&self) -> Option<&CellId> {
if self.is_provisioned {
Some(&self.base_cell_id)
} else {
None
}
}
pub fn clone_ids(&self) -> impl Iterator<Item = &CloneId> {
self.clones.keys()
}
pub fn clone_limit(&self) -> u32 {
self.clone_limit
}
pub fn is_clone_limit_reached(&self) -> bool {
self.clones.len() as u32 == self.clone_limit
}
pub fn into_provisioned_cell(self) -> Option<CellId> {
if self.is_provisioned {
Some(self.base_cell_id)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::{AppRoleAssignment, RunningApp};
use crate::prelude::*;
use ::fixt::prelude::*;
use arbitrary::Arbitrary;
use std::collections::HashSet;
#[test]
fn illegal_role_name_is_rejected() {
let mut u = unstructured_noise();
let result = InstalledAppCommon::new(
"test_app",
fixt!(AgentPubKey),
vec![(
CLONE_ID_DELIMITER.into(),
AppRoleAssignment::new(fixt!(CellId), false, 0),
)],
AppManifest::arbitrary(&mut u).unwrap(),
);
assert!(result.is_err())
}
#[test]
fn clone_management() {
let base_cell_id = fixt!(CellId);
let agent = base_cell_id.agent_pubkey().clone();
let new_clone = || CellId::new(fixt!(DnaHash), agent.clone());
let clone_limit = 3;
let role1 = AppRoleAssignment::new(base_cell_id, false, clone_limit);
let agent = fixt!(AgentPubKey);
let role_name: RoleName = "role_name".into();
let manifest = AppManifest::arbitrary(&mut unstructured_noise()).unwrap();
let mut app: RunningApp = InstalledAppCommon::new(
"app",
agent.clone(),
vec![(role_name.clone(), role1)],
manifest,
)
.unwrap()
.into();
let clones: Vec<_> = vec![new_clone(), new_clone(), new_clone()];
let clone_id_0 = app.add_clone(&role_name, &clones[0]).unwrap();
let clone_id_1 = app.add_clone(&role_name, &clones[1]).unwrap();
let clone_id_2 = app.add_clone(&role_name, &clones[2]).unwrap();
assert_eq!(clone_id_0, CloneId::new(&role_name, 0));
assert_eq!(clone_id_1, CloneId::new(&role_name, 1));
assert_eq!(clone_id_2, CloneId::new(&role_name, 2));
assert_eq!(
app.clone_cell_ids().collect::<HashSet<_>>(),
maplit::hashset! { &clones[0], &clones[1], &clones[2] }
);
assert_eq!(app.clone_cells().count(), 3);
let result_add_clone_twice = app.add_clone(&role_name, &clones[0]);
assert!(result_add_clone_twice.is_err());
matches::assert_matches!(
app.add_clone(&role_name, &new_clone()),
Err(AppError::CloneLimitExceeded(3, _))
);
app.disable_clone_cell(&clone_id_0).unwrap();
assert!(!app
.clone_cells()
.any(|(clone_id, _)| *clone_id == clone_id_0));
assert_eq!(app.clone_cells().count(), 2);
assert!(app
.disabled_clone_cells()
.any(|(clone_id, _)| *clone_id == clone_id_0));
let enabled_cell = app.enable_clone_cell(&clone_id_0).unwrap();
assert_eq!(
enabled_cell.role_name,
clone_id_0.as_app_role_name().to_owned()
);
let enabled_cell_2 = app.enable_clone_cell(&clone_id_0).unwrap();
assert_eq!(enabled_cell_2, enabled_cell);
assert!(app
.clone_cells()
.find(|(clone_id, _)| **clone_id == clone_id_0)
.is_some());
assert_eq!(
app.clone_cell_ids().collect::<HashSet<_>>(),
maplit::hashset! { &clones[0], &clones[1], &clones[2] }
);
assert_eq!(app.clone_cells().count(), 3);
app.disable_clone_cell(&clone_id_0).unwrap();
app.disable_clone_cell(&clone_id_0).unwrap();
app.delete_clone_cell(&clone_id_0).unwrap();
assert!(app.enable_clone_cell(&clone_id_0).is_err());
}
}