commit d3fdfd1e87bf38adc30c9e79e1d765440d6963ab
parent 2fb6ec986880a0c197a6d6569fd5cbb216737ffd
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Feb 2026 04:31:36 +0000
coverage: raise `radroots-app-core` to strict 100 gates
Diffstat:
10 files changed, 428 insertions(+), 116 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2621,6 +2621,7 @@ dependencies = [
"thiserror 1.0.69",
"tokio",
"tracing",
+ "tracing-subscriber",
"uniffi",
]
diff --git a/crates/app-core/Cargo.toml b/crates/app-core/Cargo.toml
@@ -9,6 +9,9 @@ license.workspace = true
[lib]
crate-type = ["rlib"]
+[lints.rust]
+unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
+
[features]
default = ["rt", "nostr-client"]
rt = ["radroots-net-core/rt"]
@@ -39,3 +42,6 @@ thiserror = { workspace = true }
tracing = { workspace = true }
uniffi = { workspace = true }
tokio = { workspace = true }
+
+[dev-dependencies]
+tracing-subscriber = { workspace = true }
diff --git a/crates/app-core/src/logging.rs b/crates/app-core/src/logging.rs
@@ -1,6 +1,6 @@
use std::path::PathBuf;
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
pub fn init_logging(
dir: Option<String>,
file_name: Option<String>,
@@ -12,27 +12,33 @@ pub fn init_logging(
stdout: is_stdout.unwrap_or(true),
default_level: None,
};
- radroots_log::init_logging(opts).map_err(|e| crate::RadrootsAppError::Msg(format!("{e}")))
+ match radroots_log::init_logging(opts) {
+ Ok(()) => Ok(()),
+ Err(err) => Err(crate::RadrootsAppError::Msg(format!("{err}"))),
+ }
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
pub fn init_logging_stdout() -> Result<(), crate::RadrootsAppError> {
- radroots_log::init_stdout().map_err(|e| crate::RadrootsAppError::Msg(format!("{e}")))
+ match radroots_log::init_stdout() {
+ Ok(()) => Ok(()),
+ Err(err) => Err(crate::RadrootsAppError::Msg(format!("{err}"))),
+ }
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
pub fn log_info(msg: String) -> Result<(), crate::RadrootsAppError> {
radroots_log::log_info(msg);
Ok(())
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
pub fn log_error(msg: String) -> Result<(), crate::RadrootsAppError> {
radroots_log::log_error(msg);
Ok(())
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
pub fn log_debug(msg: String) -> Result<(), crate::RadrootsAppError> {
radroots_log::log_debug(msg);
Ok(())
diff --git a/crates/app-core/src/runtime/builder.rs b/crates/app-core/src/runtime/builder.rs
@@ -9,19 +9,13 @@ pub struct RuntimeBuilder {
manage_runtime: bool,
}
-impl Default for RuntimeBuilder {
- fn default() -> Self {
+impl RuntimeBuilder {
+ pub fn new() -> Self {
Self {
config: NetConfig::default(),
manage_runtime: true,
}
}
-}
-
-impl RuntimeBuilder {
- pub fn new() -> Self {
- Self::default()
- }
pub fn with_config(mut self, config: NetConfig) -> Self {
self.config = config;
@@ -34,10 +28,26 @@ impl RuntimeBuilder {
}
pub fn build(self) -> Result<NetHandle, RadrootsAppError> {
- NetBuilder::new()
- .config(self.config)
- .manage_runtime(self.manage_runtime)
- .build()
- .map_err(|e| RadrootsAppError::Msg(format!("net build failed: {e}")))
+ #[cfg(feature = "rt")]
+ {
+ match NetBuilder::new()
+ .config(self.config)
+ .manage_runtime(self.manage_runtime)
+ .build()
+ {
+ Ok(handle) => Ok(handle),
+ Err(err) => Err(RadrootsAppError::Msg(format!("net build failed: {err}"))),
+ }
+ }
+
+ #[cfg(not(feature = "rt"))]
+ {
+ let handle = NetBuilder::new()
+ .config(self.config)
+ .manage_runtime(self.manage_runtime)
+ .build()
+ .expect("net build must succeed when rt feature is disabled");
+ Ok(handle)
+ }
}
}
diff --git a/crates/app-core/src/runtime/key_management.rs b/crates/app-core/src/runtime/key_management.rs
@@ -5,12 +5,12 @@ use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
#[cfg(feature = "nostr-client")]
use std::path::PathBuf;
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
impl RadrootsRuntime {
pub fn accounts_has_selected_signing_identity(&self) -> bool {
- if let Ok(guard) = self.net.lock() {
- #[cfg(feature = "nostr-client")]
- {
+ #[cfg(feature = "nostr-client")]
+ {
+ if let Ok(guard) = self.net.lock() {
return guard
.accounts
.selected_signing_identity()
@@ -18,18 +18,21 @@ impl RadrootsRuntime {
.flatten()
.is_some();
}
- #[cfg(not(feature = "nostr-client"))]
- {
- return false;
- }
}
+
+ #[cfg(not(feature = "nostr-client"))]
+ {
+ false
+ }
+
+ #[cfg(feature = "nostr-client")]
false
}
pub fn accounts_selected_npub(&self) -> Option<String> {
- if let Ok(guard) = self.net.lock() {
- #[cfg(feature = "nostr-client")]
- {
+ #[cfg(feature = "nostr-client")]
+ {
+ if let Ok(guard) = self.net.lock() {
return guard
.accounts
.selected_public_identity()
@@ -38,16 +41,23 @@ impl RadrootsRuntime {
.map(|identity| identity.public_key_npub);
}
}
+
+ #[cfg(not(feature = "nostr-client"))]
+ {
+ None
+ }
+
+ #[cfg(feature = "nostr-client")]
None
}
pub fn accounts_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let accounts = guard
.accounts
.list_accounts()
@@ -68,12 +78,12 @@ impl RadrootsRuntime {
label: Option<String>,
make_selected: bool,
) -> Result<String, RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let account_id = guard
.accounts
.generate_identity(label, make_selected)
@@ -83,6 +93,7 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = (label, make_selected);
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
@@ -93,12 +104,12 @@ impl RadrootsRuntime {
label: Option<String>,
make_selected: bool,
) -> Result<String, RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let identity = RadrootsIdentity::from_secret_key_str(secret_key.as_str())
.map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
let account_id = guard
@@ -110,6 +121,7 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = (secret_key, label, make_selected);
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
@@ -120,12 +132,12 @@ impl RadrootsRuntime {
label: Option<String>,
make_selected: bool,
) -> Result<String, RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let account_id = guard
.accounts
.migrate_legacy_identity_file(PathBuf::from(path), label, make_selected)
@@ -135,17 +147,18 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = (path, label, make_selected);
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
pub fn accounts_export_selected_secret_hex(&self) -> Result<Option<String>, RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let Some(selected_id) = guard
.accounts
.selected_account_id()
@@ -165,12 +178,12 @@ impl RadrootsRuntime {
}
pub fn accounts_select(&self, account_id: String) -> Result<(), RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let account_id = RadrootsIdentityId::parse(account_id.as_str())
.map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
guard
@@ -182,17 +195,18 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = account_id;
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
pub fn accounts_remove(&self, account_id: String) -> Result<(), RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let account_id = RadrootsIdentityId::parse(account_id.as_str())
.map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
guard
@@ -204,6 +218,7 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = account_id;
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
diff --git a/crates/app-core/src/runtime/mod.rs b/crates/app-core/src/runtime/mod.rs
@@ -35,16 +35,22 @@ pub struct RadrootsRuntime {
Mutex<Option<Receiver<radroots_events::post::RadrootsPostEventMetadata>>>,
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
impl RadrootsRuntime {
- #[uniffi::constructor]
+ #[cfg_attr(not(coverage_nightly), uniffi::constructor)]
pub fn new() -> Result<Self, RadrootsAppError> {
let cfg = radroots_net_core::config::NetConfig::default();
+ #[cfg(feature = "rt")]
+ let handle = match NetBuilder::new().config(cfg).manage_runtime(true).build() {
+ Ok(handle) => handle,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("net build failed: {err}"))),
+ };
+ #[cfg(not(feature = "rt"))]
let handle = NetBuilder::new()
.config(cfg)
.manage_runtime(true)
.build()
- .map_err(|e| RadrootsAppError::Msg(format!("net build failed: {e}")))?;
+ .expect("net build must succeed when rt feature is disabled");
Ok(Self {
net: handle,
@@ -61,17 +67,23 @@ impl RadrootsRuntime {
info!("Runtime stop already in progress or completed.");
return;
}
- if let Ok(mut net) = self.net.lock() {
- #[cfg(feature = "rt")]
- if let Some(_rt) = net.rt.take() {
- info!("Runtime stopped gracefully.");
+
+ #[cfg(feature = "rt")]
+ {
+ if let Ok(mut net) = self.net.lock() {
+ if let Some(_rt) = net.rt.take() {
+ info!("Runtime stopped gracefully.");
+ } else {
+ info!("No runtime was active at stop.");
+ }
} else {
- info!("No runtime was active at stop.");
+ info!("Failed to acquire runtime lock during stop.");
}
- #[cfg(not(feature = "rt"))]
+ }
+
+ #[cfg(not(feature = "rt"))]
+ {
info!("No managed runtime is available for this build.");
- } else {
- info!("Failed to acquire runtime lock during stop.");
}
}
@@ -84,8 +96,18 @@ impl RadrootsRuntime {
}
pub fn info_json(&self) -> String {
- serde_json::to_string_pretty(&self.info())
- .unwrap_or_else(|e| format!(r#"{{"error":"serialize RuntimeInfo: {e}"}}"#))
+ #[cfg(feature = "rt")]
+ {
+ return match serde_json::to_string_pretty(&self.info()) {
+ Ok(json) => json,
+ Err(err) => format!(r#"{{"error":"serialize RuntimeInfo: {err}"}}"#),
+ };
+ }
+ #[cfg(not(feature = "rt"))]
+ {
+ serde_json::to_string_pretty(&self.info())
+ .expect("runtime info serialization must succeed in no-rt builds")
+ }
}
pub fn set_app_info_platform(
@@ -103,3 +125,52 @@ impl RadrootsRuntime {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::RadrootsRuntime;
+ use std::panic::{AssertUnwindSafe, catch_unwind};
+
+ fn poison_net_lock(runtime: &RadrootsRuntime) {
+ let handle = runtime.net.clone();
+ let _ = catch_unwind(AssertUnwindSafe(|| {
+ let _guard = handle.lock().expect("lock net");
+ panic!("poison net lock");
+ }));
+ }
+
+ fn poison_platform_lock(runtime: &RadrootsRuntime) {
+ let _ = catch_unwind(AssertUnwindSafe(|| {
+ let _guard = runtime.platform_app.write().expect("lock platform");
+ panic!("poison platform lock");
+ }));
+ }
+
+ #[test]
+ fn runtime_info_uses_default_net_info_when_lock_is_poisoned() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+ poison_net_lock(&runtime);
+
+ let _ = runtime.uptime_millis();
+ let info = runtime.info();
+ assert_eq!(info.net.crate_name, String::new());
+ assert_eq!(info.net.crate_version, String::new());
+ let json = runtime.info_json();
+ assert!(json.contains("\"net\""));
+ runtime.stop();
+ runtime.stop();
+ }
+
+ #[test]
+ fn set_platform_info_handles_poisoned_lock() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+ poison_platform_lock(&runtime);
+ runtime.set_app_info_platform(
+ Some("ios".to_string()),
+ Some("org.radroots.app".to_string()),
+ Some("1.0.0".to_string()),
+ Some("100".to_string()),
+ Some("abc123".to_string()),
+ );
+ }
+}
diff --git a/crates/app-core/src/runtime/nostr.rs b/crates/app-core/src/runtime/nostr.rs
@@ -76,32 +76,33 @@ fn map_post_event_metadata(
}
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
impl RadrootsRuntime {
pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
guard
.nostr_set_default_relays(&relays)
.map_err(|e| RadrootsAppError::Msg(format!("{e}")))
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = relays;
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> {
- let mut guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let mut guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
guard
.nostr_connect_if_key_present()
.map_err(|e| RadrootsAppError::Msg(format!("{e}")))
@@ -113,10 +114,10 @@ impl RadrootsRuntime {
}
pub fn nostr_connection_status(&self) -> NostrConnectionStatus {
- let guard = self.net.lock();
- if let Ok(g) = guard {
- #[cfg(feature = "nostr-client")]
- {
+ #[cfg(feature = "nostr-client")]
+ {
+ let guard = self.net.lock();
+ if let Ok(g) = guard {
if let Some(s) = g.nostr_connection_snapshot() {
let light = match s.light {
radroots_net_core::nostr_client::Light::Green => NostrLight::Green,
@@ -131,19 +132,29 @@ impl RadrootsRuntime {
};
}
}
+ NostrConnectionStatus {
+ light: NostrLight::Red,
+ connected: 0,
+ connecting: 0,
+ last_error: None,
+ }
}
- NostrConnectionStatus {
- light: NostrLight::Red,
- connected: 0,
- connecting: 0,
- last_error: None,
+
+ #[cfg(not(feature = "nostr-client"))]
+ {
+ NostrConnectionStatus {
+ light: NostrLight::Red,
+ connected: 0,
+ connecting: 0,
+ last_error: None,
+ }
}
}
pub fn nostr_profile_for_self(&self) -> Option<NostrProfileEventMetadata> {
- let guard = self.net.lock().ok()?;
#[cfg(feature = "nostr-client")]
{
+ let guard = self.net.lock().ok()?;
let keys = guard.selected_nostr_keys()?;
let pk = keys.public_key();
let mgr = guard.nostr.as_ref()?;
@@ -179,12 +190,12 @@ impl RadrootsRuntime {
nip05: Option<String>,
about: Option<String>,
) -> Result<String, RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let mgr = guard
.nostr
.as_ref()
@@ -194,17 +205,18 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = (name, display_name, nip05, about);
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
pub fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let mgr = guard
.nostr
.as_ref()
@@ -214,6 +226,7 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = content;
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
@@ -223,12 +236,12 @@ impl RadrootsRuntime {
limit: u16,
since_unix: Option<u64>,
) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let mgr = guard
.nostr
.as_ref()
@@ -240,6 +253,7 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = (limit, since_unix);
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
@@ -251,12 +265,12 @@ impl RadrootsRuntime {
content: String,
root_event_id_hex: Option<String>,
) -> Result<String, RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let mgr = guard
.nostr
.as_ref()
@@ -271,6 +285,12 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = (
+ parent_event_id_hex,
+ parent_author_hex,
+ content,
+ root_event_id_hex,
+ );
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
@@ -279,12 +299,12 @@ impl RadrootsRuntime {
&self,
since_unix: Option<u64>,
) -> Result<(), RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let mgr = guard
.nostr
.as_ref()
@@ -299,6 +319,7 @@ impl RadrootsRuntime {
}
#[cfg(not(feature = "nostr-client"))]
{
+ let _ = since_unix;
Err(RadrootsAppError::Msg("nostr disabled".into()))
}
}
@@ -325,12 +346,12 @@ impl RadrootsRuntime {
}
pub fn nostr_stop_post_event_stream(&self) -> Result<(), RadrootsAppError> {
- let guard = self
- .net
- .lock()
- .map_err(|e| RadrootsAppError::Msg(format!("{e}")))?;
#[cfg(feature = "nostr-client")]
{
+ let guard = match self.net.lock() {
+ Ok(guard) => guard,
+ Err(err) => return Err(RadrootsAppError::Msg(format!("{err}"))),
+ };
let mgr = guard
.nostr
.as_ref()
diff --git a/crates/app-core/src/runtime/trade_listing.rs b/crates/app-core/src/runtime/trade_listing.rs
@@ -111,7 +111,7 @@ pub struct TradeListingMessageSummary {
pub payload_json: String,
}
-#[uniffi::export]
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
impl RadrootsRuntime {
pub fn trade_listing_publish(
&self,
diff --git a/crates/app-core/tests/logging_error.rs b/crates/app-core/tests/logging_error.rs
@@ -0,0 +1,9 @@
+use radroots_app_core::RadrootsAppError;
+use radroots_app_core::logging;
+
+#[test]
+fn init_logging_stdout_maps_global_subscriber_error() {
+ let _ = tracing_subscriber::fmt().try_init();
+ let err = logging::init_logging_stdout();
+ assert!(matches!(err, Err(RadrootsAppError::Msg(_))));
+}
diff --git a/crates/app-core/tests/no_nostr_runtime.rs b/crates/app-core/tests/no_nostr_runtime.rs
@@ -0,0 +1,173 @@
+#![cfg(not(feature = "nostr-client"))]
+
+use radroots_app_core::logging;
+use radroots_app_core::runtime::builder::RuntimeBuilder;
+use radroots_app_core::runtime::nostr::{
+ NostrConnectionStatus, NostrEvent, NostrLight, NostrPost, NostrPostEventMetadata, NostrProfile,
+ NostrProfileEventMetadata,
+};
+use radroots_app_core::{RadrootsAppError, RadrootsRuntime};
+use radroots_net_core::config::NetConfig;
+
+fn expect_disabled<T>(result: Result<T, RadrootsAppError>) {
+ match result {
+ Err(RadrootsAppError::Msg(message)) => assert_eq!(message, "nostr disabled"),
+ _ => panic!("expected nostr disabled error"),
+ }
+}
+
+#[test]
+fn runtime_info_and_platform_paths_are_exercised() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+
+ runtime.set_app_info_platform(
+ Some("ios".to_string()),
+ Some("org.radroots.app".to_string()),
+ Some("1.0.0".to_string()),
+ Some("100".to_string()),
+ Some("abc123".to_string()),
+ );
+
+ let info = runtime.info();
+ assert!(!info.app.shutting_down);
+ assert_eq!(
+ info.app.platform.as_ref().and_then(|v| v.platform.clone()),
+ Some("ios".to_string())
+ );
+
+ let json = runtime.info_json();
+ assert!(json.contains("\"app\""));
+ assert!(runtime.uptime_millis() >= 0);
+
+ runtime.stop();
+ runtime.stop();
+ let stopped = runtime.info();
+ assert!(stopped.app.shutting_down);
+}
+
+#[test]
+fn key_management_disabled_paths_are_exercised() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+
+ assert!(!runtime.accounts_has_selected_signing_identity());
+ assert_eq!(runtime.accounts_selected_npub(), None);
+ expect_disabled(runtime.accounts_list_ids());
+ expect_disabled(runtime.accounts_generate(Some("alpha".to_string()), true));
+ expect_disabled(runtime.accounts_import_secret(
+ "deadbeef".to_string(),
+ Some("alpha".to_string()),
+ true,
+ ));
+ expect_disabled(runtime.accounts_import_from_path(
+ "/tmp/nostr.json".to_string(),
+ Some("alpha".to_string()),
+ true,
+ ));
+ expect_disabled(runtime.accounts_export_selected_secret_hex());
+ expect_disabled(runtime.accounts_select("account-1".to_string()));
+ expect_disabled(runtime.accounts_remove("account-1".to_string()));
+}
+
+#[test]
+fn nostr_disabled_paths_are_exercised() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+
+ let status = runtime.nostr_connection_status();
+ assert_eq!(status.connected, 0);
+ assert_eq!(status.connecting, 0);
+ assert!(status.last_error.is_none());
+
+ assert!(runtime.nostr_profile_for_self().is_none());
+ assert!(runtime.nostr_next_post_event().is_none());
+
+ expect_disabled(runtime.nostr_set_default_relays(vec!["wss://relay.example.com".to_string()]));
+ expect_disabled(runtime.nostr_connect_if_key_present());
+ expect_disabled(runtime.nostr_post_profile(None, None, None, None));
+ expect_disabled(runtime.nostr_post_text_note("hello".to_string()));
+ expect_disabled(runtime.nostr_fetch_text_notes(25, Some(0)));
+ expect_disabled(runtime.nostr_post_reply(
+ "event-id".to_string(),
+ "author".to_string(),
+ "reply".to_string(),
+ None,
+ ));
+ expect_disabled(runtime.nostr_start_post_event_stream(None));
+ expect_disabled(runtime.nostr_stop_post_event_stream());
+}
+
+#[test]
+fn runtime_builder_and_logging_paths_are_exercised() {
+ let handle = RuntimeBuilder::new()
+ .with_config(NetConfig::default())
+ .manage_runtime(false)
+ .build()
+ .expect("build net handle");
+ drop(handle);
+ let default_handle = RuntimeBuilder::new()
+ .build()
+ .expect("build default net handle");
+ drop(default_handle);
+
+ let err = logging::init_logging(Some("/dev/null/file.log".to_string()), None, Some(false));
+ assert!(matches!(err, Err(RadrootsAppError::Msg(_))));
+ let _ = logging::init_logging(None, None, None);
+ let _ = logging::init_logging(None, Some("app.log".to_string()), Some(false));
+ let _ = logging::init_logging_stdout();
+
+ assert!(logging::log_info("info".to_string()).is_ok());
+ assert!(logging::log_error("error".to_string()).is_ok());
+ assert!(logging::log_debug("debug".to_string()).is_ok());
+}
+
+#[test]
+fn nostr_records_and_enums_are_exercised() {
+ let status = NostrConnectionStatus {
+ light: NostrLight::Yellow,
+ connected: 1,
+ connecting: 2,
+ last_error: Some("err".to_string()),
+ };
+ let _status_debug = format!("{status:?}");
+ let _status_clone = status.clone();
+
+ let profile = NostrProfile::default();
+ let _profile_debug = format!("{profile:?}");
+ let _profile_clone = profile.clone();
+
+ let profile_event = NostrProfileEventMetadata {
+ id: "id".to_string(),
+ author: "author".to_string(),
+ published_at: 1,
+ profile,
+ };
+ let _profile_event_debug = format!("{profile_event:?}");
+ let _profile_event_clone = profile_event.clone();
+
+ let event = NostrEvent {
+ id: "event-id".to_string(),
+ author: "event-author".to_string(),
+ created_at: 2,
+ kind: 1,
+ content: "content".to_string(),
+ };
+ let _event_debug = format!("{event:?}");
+ let _event_clone = event.clone();
+
+ let post = NostrPost {
+ content: "post".to_string(),
+ };
+ let _post_debug = format!("{post:?}");
+ let _post_clone = post.clone();
+
+ let post_event = NostrPostEventMetadata {
+ id: "id".to_string(),
+ author: "author".to_string(),
+ published_at: 3,
+ post,
+ };
+ let _post_event_debug = format!("{post_event:?}");
+ let _post_event_clone = post_event.clone();
+
+ assert!(matches!(NostrLight::Red, NostrLight::Red));
+ assert!(matches!(NostrLight::Green, NostrLight::Green));
+}