use std::any::Any; use std::cmp::Ordering; use std::panic::{RefUnwindSafe, UnwindSafe}; use color_eyre::eyre::eyre; use futures::future::BoxFuture; use futures::{Future, FutureExt}; use imbl_value::{InternedString, to_value}; use patch_db::json_ptr::ROOT; use crate::Error; use crate::context::RpcContext; use crate::db::model::Database; use crate::prelude::*; use crate::progress::PhaseProgressTrackerHandle; mod v0_3_5; mod v0_3_5_1; mod v0_3_5_2; mod v0_3_6_alpha_0; mod v0_3_6_alpha_1; mod v0_3_6_alpha_2; mod v0_3_6_alpha_3; mod v0_3_6_alpha_4; mod v0_3_6_alpha_5; mod v0_3_6_alpha_6; mod v0_3_6_alpha_7; mod v0_3_6_alpha_8; mod v0_3_6_alpha_9; mod v0_3_6_alpha_10; mod v0_3_6_alpha_11; mod v0_3_6_alpha_12; mod v0_3_6_alpha_13; mod v0_3_6_alpha_14; mod v0_3_6_alpha_15; mod v0_3_6_alpha_16; mod v0_3_6_alpha_17; mod v0_3_6_alpha_18; mod v0_4_0_alpha_0; mod v0_4_0_alpha_1; mod v0_4_0_alpha_2; mod v0_4_0_alpha_3; mod v0_4_0_alpha_4; mod v0_4_0_alpha_5; mod v0_4_0_alpha_6; mod v0_4_0_alpha_7; mod v0_4_0_alpha_8; mod v0_4_0_alpha_9; mod v0_4_0_alpha_10; mod v0_4_0_alpha_11; mod v0_4_0_alpha_12; mod v0_4_0_alpha_13; mod v0_4_0_alpha_14; mod v0_4_0_alpha_15; mod v0_4_0_alpha_16; mod v0_4_0_alpha_17; mod v0_4_0_alpha_18; mod v0_4_0_alpha_19; mod v0_4_0_alpha_20; mod v0_4_0_alpha_21; mod v0_4_0_alpha_22; mod v0_4_0_alpha_23; pub type Current = v0_4_0_alpha_23::Version; // VERSION_BUMP impl Current { #[instrument(skip(self, db))] pub async fn pre_init(self, db: &PatchDb) -> Result<(), Error> { let from = from_value::( version_accessor(&mut db.dump(&ROOT).await.value) .or_not_found("`version` in db")? .clone(), )? .as_version_t()?; match from.semver().cmp(&self.semver()) { Ordering::Greater => { db.apply_function(|mut db| { rollback_to_unchecked(&from, &self, &mut db)?; Ok::<_, Error>((db, ())) }) .await .result?; } Ordering::Less => { let pre_ups = PreUps::load(&from, &self).await?; db.apply_function(|mut db| { migrate_from_unchecked(&from, &self, pre_ups, &mut db)?; Ok::<_, Error>((to_value(&from_value::(db.clone())?)?, ())) }) .await .result?; } Ordering::Equal => { db.apply_function(|db| { Ok::<_, Error>((to_value(&from_value::(db.clone())?)?, ())) }) .await .result?; } } Ok(()) } } pub async fn post_init( ctx: &RpcContext, mut progress: PhaseProgressTrackerHandle, ) -> Result<(), Error> { let mut peek = ctx.db.peek().await; let todos = peek .as_public() .as_server_info() .as_post_init_migration_todos() .de()?; progress.start(); if !todos.is_empty() { while let Some((version, input)) = { peek = ctx.db.peek().await; peek.as_public() .as_server_info() .as_post_init_migration_todos() .de()? .pop_first() .map(|(version, input)| { Ok::<_, Error>((Version::from_exver_version(version).as_version_t()?, input)) }) .transpose()? } { version.0.post_up(ctx, input).await?; ctx.db .mutate(|db| { db.as_public_mut() .as_server_info_mut() .as_post_init_migration_todos_mut() .mutate(|m| Ok(m.remove(&version.0.semver()))) }) .await .result?; } } progress.complete(); Ok(()) } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] #[allow(non_camel_case_types)] enum Version { LT0_3_5(LTWrapper), V0_3_5(Wrapper), V0_3_5_1(Wrapper), V0_3_5_2(Wrapper), V0_3_6_alpha_0(Wrapper), V0_3_6_alpha_1(Wrapper), V0_3_6_alpha_2(Wrapper), V0_3_6_alpha_3(Wrapper), V0_3_6_alpha_4(Wrapper), V0_3_6_alpha_5(Wrapper), V0_3_6_alpha_6(Wrapper), V0_3_6_alpha_7(Wrapper), V0_3_6_alpha_8(Wrapper), V0_3_6_alpha_9(Wrapper), V0_3_6_alpha_10(Wrapper), V0_3_6_alpha_11(Wrapper), V0_3_6_alpha_12(Wrapper), V0_3_6_alpha_13(Wrapper), V0_3_6_alpha_14(Wrapper), V0_3_6_alpha_15(Wrapper), V0_3_6_alpha_16(Wrapper), V0_3_6_alpha_17(Wrapper), V0_3_6_alpha_18(Wrapper), V0_4_0_alpha_0(Wrapper), V0_4_0_alpha_1(Wrapper), V0_4_0_alpha_2(Wrapper), V0_4_0_alpha_3(Wrapper), V0_4_0_alpha_4(Wrapper), V0_4_0_alpha_5(Wrapper), V0_4_0_alpha_6(Wrapper), V0_4_0_alpha_7(Wrapper), V0_4_0_alpha_8(Wrapper), V0_4_0_alpha_9(Wrapper), V0_4_0_alpha_10(Wrapper), V0_4_0_alpha_11(Wrapper), V0_4_0_alpha_12(Wrapper), V0_4_0_alpha_13(Wrapper), V0_4_0_alpha_14(Wrapper), V0_4_0_alpha_15(Wrapper), V0_4_0_alpha_16(Wrapper), V0_4_0_alpha_17(Wrapper), V0_4_0_alpha_18(Wrapper), V0_4_0_alpha_19(Wrapper), V0_4_0_alpha_20(Wrapper), V0_4_0_alpha_21(Wrapper), V0_4_0_alpha_22(Wrapper), V0_4_0_alpha_23(Wrapper), // VERSION_BUMP Other(exver::Version), } impl Version { fn from_exver_version(version: exver::Version) -> Self { serde_json::to_value(version.clone()) .and_then(serde_json::from_value) .unwrap_or_else(|_e| { tracing::warn!("Can't deserialize: {:?} and falling back to other", version); Version::Other(version) }) } fn as_version_t(&self) -> Result { Ok(match self { Self::LT0_3_5(_) => { return Err(Error::new( eyre!("cannot migrate from versions before 0.3.5"), ErrorKind::MigrationFailed, )); } Self::V0_3_5(v) => DynVersion(Box::new(v.0)), Self::V0_3_5_1(v) => DynVersion(Box::new(v.0)), Self::V0_3_5_2(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_0(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_1(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_2(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_3(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_4(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_5(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_6(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_7(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_8(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_9(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_10(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_11(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_12(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_13(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_14(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_15(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_16(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_17(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_18(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_0(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_1(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_2(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_3(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_4(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_5(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_6(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_8(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_11(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_12(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_21(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_22(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_23(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( eyre!("unknown version {v}"), ErrorKind::MigrationFailed, )); } }) } #[cfg(test)] fn as_exver(&self) -> exver::Version { match self { Version::LT0_3_5(LTWrapper(_, x)) => x.clone(), Version::V0_3_5(Wrapper(x)) => x.semver(), Version::V0_3_5_1(Wrapper(x)) => x.semver(), Version::V0_3_5_2(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_0(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_1(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_2(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_3(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_4(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_5(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_6(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_7(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_8(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_9(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_10(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_11(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_12(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_13(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_14(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_15(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_16(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_17(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_18(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_0(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_1(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_2(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_3(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_4(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_5(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_6(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_8(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_11(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_12(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_21(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_22(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_23(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } } #[instrument(skip_all)] fn version_accessor(db: &mut Value) -> Option<&mut Value> { if db.get("public").is_some() { db.get_mut("public")? .get_mut("serverInfo")? .get_mut("version") } else { db.get_mut("server-info")?.get_mut("version") } } #[instrument(skip_all)] fn version_compat_accessor(db: &mut Value) -> Option<&mut Value> { if db.get("public").is_some() { let server_info = db.get_mut("public")?.get_mut("serverInfo")?; if server_info.get("packageVersionCompat").is_some() { server_info.get_mut("packageVersionCompat") } else { if let Some(prev) = server_info.get("eosVersionCompat").cloned() { server_info .as_object_mut()? .insert("packageVersionCompat".into(), prev); } else if let Some(prev) = server_info.get("versionCompat").cloned() { server_info .as_object_mut()? .insert("packageVersionCompat".into(), prev); } server_info.get_mut("packageVersionCompat") } } else { db.get_mut("server-info")?.get_mut("eos-version-compat") } } #[instrument(skip_all)] fn post_init_migration_todos_accessor(db: &mut Value) -> Option<&mut Value> { let server_info = if db.get("public").is_some() { db.get_mut("public")?.get_mut("serverInfo")? } else { db.get_mut("server-info")? }; if server_info.get("postInitMigrationTodos").is_none() { server_info.as_object_mut()?.insert( "postInitMigrationTodos".into(), Value::Object(Default::default()), ); } server_info.get_mut("postInitMigrationTodos") } struct PreUps { prev: Option>, value: Box, } impl PreUps { #[instrument(skip(from, to))] fn load<'a, VFrom: DynVersionT + ?Sized, VTo: DynVersionT + ?Sized>( from: &'a VFrom, to: &'a VTo, ) -> BoxFuture<'a, Result> { async { let previous = to.previous(); let prev = match from.semver().cmp(&previous.semver()) { Ordering::Less => Some(Box::new(PreUps::load(from, &previous).await?)), Ordering::Greater => { return Err(Error::new( eyre!( "NO PATH FROM {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", from.semver() ), crate::ErrorKind::MigrationFailed, )); } Ordering::Equal => None, }; Ok(Self { prev, value: to.pre_up().await?, }) } .boxed() } } fn migrate_from_unchecked( from: &VFrom, to: &VTo, pre_ups: PreUps, db: &mut Value, ) -> Result<(), Error> { let previous = to.previous(); match pre_ups.prev { Some(prev) if from.semver() < previous.semver() => { migrate_from_unchecked(from, &previous, *prev, db)? } _ if from.semver() > previous.semver() => { return Err(Error::new( eyre!( "NO PATH FROM {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", from.semver() ), crate::ErrorKind::MigrationFailed, )); } _ => (), }; let res = to.up(db, pre_ups.value)?; to.commit(db, res)?; Ok(()) } fn rollback_to_unchecked( from: &VFrom, to: &VTo, db: &mut Value, ) -> Result<(), Error> { let previous = from.previous(); from.down(db)?; previous.commit(db, Value::Null)?; if to.semver() < previous.semver() { rollback_to_unchecked(&previous, to, db)? } else if to.semver() > previous.semver() { return Err(Error::new( eyre!( "NO PATH TO {}, THIS IS LIKELY A MISTAKE IN THE VERSION DEFINITION", to.semver() ), crate::ErrorKind::MigrationFailed, )); } Ok(()) } #[allow(unused_variables)] pub trait VersionT where Self: Default + Copy + Sized + RefUnwindSafe + Send + Sync + 'static, { type Previous: VersionT; type PreUpRes: Send + UnwindSafe; fn semver(self) -> exver::Version; fn compat(self) -> &'static exver::VersionRange; /// MUST be idempotent, and is run before *all* db migrations fn pre_up(self) -> impl Future> + Send + 'static; fn up(self, db: &mut Value, input: Self::PreUpRes) -> Result { Ok(Value::Null) } /// MUST be idempotent, and is run after *all* db migrations fn post_up<'a>( self, ctx: &'a RpcContext, input: Value, ) -> impl Future> + Send + 'a { async { Ok(()) } } fn down(self, db: &mut Value) -> Result<(), Error> { Err(Error::new( eyre!("downgrades prohibited"), ErrorKind::InvalidRequest, )) } #[instrument(skip_all)] fn commit(self, db: &mut Value, res: Value) -> Result<(), Error> { *version_accessor(db).or_not_found("`public.serverInfo.version` in db")? = to_value(&self.semver())?; *version_compat_accessor(db).or_not_found("`public.serverInfo.versionCompat` in db")? = to_value(self.compat())?; if let Some(arr) = post_init_migration_todos_accessor(db) .or_not_found("`public.serverInfo.postInitMigrationTodos` in db")? .as_array_mut() { arr.push_back(to_value(&self.semver())?); } else if let Some(obj) = post_init_migration_todos_accessor(db) .or_not_found("`public.serverInfo.postInitMigrationTodos` in db")? .as_object_mut() { obj.insert(InternedString::from_display(&self.semver()), res); } else { return Err(Error::new( eyre!("postInitMigrationTodos is not an array or object"), ErrorKind::Database, )); } Ok(()) } } struct DynVersion(Box); unsafe impl Send for DynVersion {} trait DynVersionT: RefUnwindSafe + Send + Sync { fn previous(&self) -> DynVersion; fn semver(&self) -> exver::Version; fn compat(&self) -> &'static exver::VersionRange; fn pre_up(&self) -> BoxFuture<'static, Result, Error>>; fn up(&self, db: &mut Value, input: Box) -> Result; fn post_up<'a>(&self, ctx: &'a RpcContext, input: Value) -> BoxFuture<'a, Result<(), Error>>; fn down(&self, db: &mut Value) -> Result<(), Error>; fn commit(&self, db: &mut Value, res: Value) -> Result<(), Error>; } impl DynVersionT for T where T: VersionT, { fn previous(&self) -> DynVersion { DynVersion(Box::new(::Previous::default())) } fn semver(&self) -> exver::Version { VersionT::semver(*self) } fn compat(&self) -> &'static exver::VersionRange { VersionT::compat(*self) } fn pre_up(&self) -> BoxFuture<'static, Result, Error>> { let v = *self; async move { Ok(Box::new(VersionT::pre_up(v).await?) as Box) } .boxed() } fn up(&self, db: &mut Value, input: Box) -> Result { VersionT::up( *self, db, *input.downcast().map_err(|_| { Error::new( eyre!("pre_up returned unexpected type"), ErrorKind::Incoherent, ) })?, ) } fn post_up<'a>(&self, ctx: &'a RpcContext, input: Value) -> BoxFuture<'a, Result<(), Error>> { VersionT::post_up(*self, ctx, input).boxed() } fn down(&self, db: &mut Value) -> Result<(), Error> { VersionT::down(*self, db) } fn commit(&self, db: &mut Value, res: Value) -> Result<(), Error> { VersionT::commit(*self, db, res) } } impl DynVersionT for DynVersion { fn previous(&self) -> DynVersion { self.0.previous() } fn semver(&self) -> exver::Version { self.0.semver() } fn compat(&self) -> &'static exver::VersionRange { self.0.compat() } fn pre_up(&self) -> BoxFuture<'static, Result, Error>> { self.0.pre_up() } fn up(&self, db: &mut Value, input: Box) -> Result { self.0.up(db, input) } fn post_up<'a>(&self, ctx: &'a RpcContext, input: Value) -> BoxFuture<'a, Result<(), Error>> { self.0.post_up(ctx, input) } fn down(&self, db: &mut Value) -> Result<(), Error> { self.0.down(db) } fn commit(&self, db: &mut Value, res: Value) -> Result<(), Error> { self.0.commit(db, res) } } #[derive(Debug, Clone)] struct LTWrapper(T, exver::Version); impl serde::Serialize for LTWrapper where T: VersionT, { fn serialize(&self, serializer: S) -> Result { self.0.semver().serialize(serializer) } } impl<'de, T> serde::Deserialize<'de> for LTWrapper where T: VersionT, { fn deserialize>(deserializer: D) -> Result { let v = exver::Version::deserialize(deserializer)?; let version = T::default(); if v < version.semver() { Ok(Self(version, v)) } else { Err(serde::de::Error::custom("Mismatched Version")) } } } #[derive(Debug, Clone)] struct Wrapper(T); impl serde::Serialize for Wrapper where T: VersionT, { fn serialize(&self, serializer: S) -> Result { self.0.semver().serialize(serializer) } } impl<'de, T> serde::Deserialize<'de> for Wrapper where T: VersionT, { fn deserialize>(deserializer: D) -> Result { let v = exver::Version::deserialize(deserializer)?; let version = T::default(); if v == version.semver() { Ok(Wrapper(version)) } else { Err(serde::de::Error::custom("Mismatched Version")) } } } pub const COMMIT_HASH: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/../build/env/GIT_HASH.txt" )); pub fn git_info() -> Result { Ok(InternedString::intern(COMMIT_HASH)) } #[cfg(test)] mod tests { use proptest::prelude::*; use super::*; fn em_version() -> impl Strategy { any::<(usize, usize, usize, bool)>().prop_map(|(major, minor, patch, alpha)| { if alpha { exver::Version::new( [0, major, minor] .into_iter() .chain(Some(patch).filter(|n| *n != 0)), [], ) } else { exver::Version::new([major, minor, patch], []) } }) } fn versions() -> impl Strategy { prop_oneof![ Just(Version::V0_3_5(Wrapper(v0_3_5::Version::default()))), Just(Version::V0_3_5_1(Wrapper(v0_3_5_1::Version::default()))), Just(Version::V0_3_5_2(Wrapper(v0_3_5_2::Version::default()))), Just(Version::V0_3_6_alpha_0(Wrapper( v0_3_6_alpha_0::Version::default() ))), Just(Version::V0_3_6_alpha_1(Wrapper( v0_3_6_alpha_1::Version::default() ))), Just(Version::V0_3_6_alpha_2(Wrapper( v0_3_6_alpha_2::Version::default() ))), em_version().prop_map(Version::Other), ] } proptest! { #[test] fn exversion_isomorphic_version(original in em_version()) { let version = Version::from_exver_version(original.clone().into()); let back = version.as_exver(); prop_assert_eq!(original, back, "All versions should round trip"); } #[test] fn version_isomorphic_em_version(version in versions()) { let sem_ver = version.as_exver(); let back = Version::from_exver_version(sem_ver.into()); prop_assert_eq!(format!("{:?}",version), format!("{:?}", back), "All versions should round trip"); } } }