use rullst::feature::{
self, DbFeatureDriver, EnvFeatureDriver, FeatureDriver, FeatureManager, MemoryFeatureDriver,
TomlFeatureDriver,
};
use rullst_orm::Orm;
use std::fs;
use std::time::Duration;
#[tokio::test]
async fn test_memory_feature_driver() {
let memory_driver = MemoryFeatureDriver::new();
memory_driver.override_enabled("new-checkout", true);
memory_driver.override_enabled("legacy-sidebar", false);
assert_eq!(memory_driver.enabled("new-checkout").await, Some(true));
assert_eq!(memory_driver.enabled("legacy-sidebar").await, Some(false));
assert_eq!(memory_driver.enabled("non-existent").await, None);
memory_driver.override_rollout("progressive-rollout", 30);
let mut enabled_count = 0;
for i in 0..100 {
let user_id = format!("user_{}", i);
if memory_driver
.enabled_for("progressive-rollout", &user_id)
.await
.unwrap_or(false)
{
enabled_count += 1;
}
}
assert!(enabled_count > 10 && enabled_count < 50);
let variants = vec![("red".to_string(), 50), ("blue".to_string(), 50)];
memory_driver.override_variants("button-color", variants);
let mut red_count = 0;
let mut blue_count = 0;
for i in 0..100 {
let user_id = format!("user_{}", i);
let var = memory_driver.variant("button-color", &user_id).await;
if var == Some("red".to_string()) {
red_count += 1;
} else if var == Some("blue".to_string()) {
blue_count += 1;
}
}
assert!(red_count > 30 && blue_count > 30);
assert_eq!(red_count + blue_count, 100);
}
use tokio::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::const_new(());
#[tokio::test]
async fn test_env_feature_driver() {
let _guard = ENV_LOCK.lock().await;
let env_driver = EnvFeatureDriver::new();
unsafe {
std::env::set_var("FEATURE_BETA_FLAG", "true");
std::env::set_var("FEATURE_BETA_PCT", "30%");
std::env::set_var("FEATURE_THEME_SPLIT", "light:50,dark:50");
}
assert_eq!(env_driver.enabled("beta-flag").await, Some(true));
let bucket_user_4 = feature::calculate_hash_bucket("beta-pct", "user_4"); let user_4_enabled = env_driver
.enabled_for("beta-pct", "user_4")
.await
.unwrap_or(false);
assert_eq!(user_4_enabled, bucket_user_4 < 30);
let var_user_1 = env_driver.variant("theme-split", "user_1").await.unwrap();
assert!(var_user_1 == "light" || var_user_1 == "dark");
unsafe {
std::env::remove_var("FEATURE_BETA_FLAG");
std::env::remove_var("FEATURE_BETA_PCT");
std::env::remove_var("FEATURE_THEME_SPLIT");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_toml_feature_driver() {
let toml_mock = r#"
[server]
port = 3000
[features]
billing-v2 = true
admin-redesign = "50%"
home-ab = "control:40,treatment:60"
"#;
fs::write("Rullst.toml", toml_mock).unwrap();
let toml_driver = TomlFeatureDriver::new();
assert_eq!(toml_driver.enabled("billing-v2").await, Some(true));
let bucket = feature::calculate_hash_bucket("admin-redesign", "user_99");
assert_eq!(
toml_driver.enabled_for("admin-redesign", "user_99").await,
Some(bucket < 50)
);
let ab_var = toml_driver.variant("home-ab", "user_50").await.unwrap();
assert!(ab_var == "control" || ab_var == "treatment");
let _ = fs::remove_file("Rullst.toml");
}
#[tokio::test]
async fn test_database_feature_driver() {
Orm::init("sqlite:file:memdb1?mode=memory&cache=shared")
.await
.unwrap();
let pool = Orm::pool();
let _conn = pool.acquire().await.unwrap();
sqlx::query(
"CREATE TABLE rullst_feature_flags (
name TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 0,
rollout_percentage INTEGER DEFAULT NULL,
variants TEXT DEFAULT NULL
)",
)
.execute(pool)
.await
.unwrap();
sqlx::query(
"INSERT INTO rullst_feature_flags (name, enabled, rollout_percentage, variants)
VALUES
('db-dashboard', 1, NULL, NULL),
('db-rollout', 1, 40, NULL),
('db-ab-split', 1, NULL, 'variant-a:30,variant-b:70')",
)
.execute(pool)
.await
.unwrap();
let db_driver = DbFeatureDriver::with_ttl(Duration::from_millis(100));
assert_eq!(db_driver.enabled("db-dashboard").await, Some(true));
let rollout_bucket = feature::calculate_hash_bucket("db-rollout", "user_x");
assert_eq!(
db_driver.enabled_for("db-rollout", "user_x").await,
Some(rollout_bucket < 40)
);
let ab_var = db_driver.variant("db-ab-split", "user_y").await.unwrap();
assert!(ab_var == "variant-a" || ab_var == "variant-b");
sqlx::query("UPDATE rullst_feature_flags SET enabled = 0 WHERE name = 'db-dashboard'")
.execute(pool)
.await
.unwrap();
assert_eq!(db_driver.enabled("db-dashboard").await, Some(true));
tokio::time::sleep(Duration::from_millis(150)).await;
assert_eq!(db_driver.enabled("db-dashboard").await, Some(false));
}
#[tokio::test]
async fn test_global_feature_manager_facade() {
let memory_driver = Box::new(MemoryFeatureDriver::new());
memory_driver.override_enabled("global-toggle", true);
let manager = FeatureManager::new().add_driver(memory_driver);
feature::init(manager).unwrap_or(());
assert!(feature::enabled("global-toggle").await);
assert!(!feature::enabled("non-existent-global").await);
}