1use crate::rollout;
22use crate::scope::Scope;
23use crate::store::{FlagStore, MemoryStore};
24use rustlavel_core::{Config, Error, Result};
25use std::collections::{BTreeMap, BTreeSet, HashMap};
26use std::future::Future;
27use std::pin::Pin;
28use std::sync::{Arc, Mutex};
29
30type ResolverFuture = Pin<Box<dyn Future<Output = Result<bool>> + Send + 'static>>;
32
33type Resolver = Arc<dyn Fn(Scope) -> ResolverFuture + Send + Sync>;
35
36pub trait FlagAnswer {
44 fn into_answer(self) -> Result<bool>;
45}
46
47impl FlagAnswer for bool {
48 fn into_answer(self) -> Result<bool> {
49 Ok(self)
50 }
51}
52
53impl FlagAnswer for Result<bool> {
54 fn into_answer(self) -> Result<bool> {
55 self
56 }
57}
58
59#[derive(Clone)]
94pub struct Flags {
95 definitions: Arc<HashMap<String, Resolver>>,
96 store: Arc<dyn FlagStore>,
97 forced_on: Arc<BTreeSet<String>>,
98 forced_off: Arc<BTreeSet<String>>,
99}
100
101impl Default for Flags {
102 fn default() -> Self {
103 Flags::new()
104 }
105}
106
107impl Flags {
108 pub fn new() -> Self {
110 Flags {
111 definitions: Arc::new(HashMap::new()),
112 store: Arc::new(MemoryStore::new()),
113 forced_on: Arc::new(BTreeSet::new()),
114 forced_off: Arc::new(BTreeSet::new()),
115 }
116 }
117
118 pub fn from_config(config: &Config) -> Self {
122 Flags::new().configured(config)
123 }
124
125 pub fn configured(self, config: &Config) -> Self {
141 self.force_on(config.list("flags.on")).force_off(config.list("flags.off"))
142 }
143
144 pub fn store(mut self, store: impl FlagStore) -> Self {
147 self.store = Arc::new(store);
148 self
149 }
150
151 pub fn with_store(mut self, store: Arc<dyn FlagStore>) -> Self {
153 self.store = store;
154 self
155 }
156
157 pub fn define<F, Fut, A>(mut self, name: impl Into<String>, resolver: F) -> Self
176 where
177 F: Fn(Scope) -> Fut + Send + Sync + 'static,
178 Fut: Future<Output = A> + Send + 'static,
179 A: FlagAnswer,
180 {
181 let boxed: Resolver = Arc::new(move |scope| {
182 let future = resolver(scope);
183 Box::pin(async move { future.await.into_answer() })
184 });
185
186 Arc::make_mut(&mut self.definitions).insert(name.into(), boxed);
189 self
190 }
191
192 pub fn rollout(self, name: impl Into<String>, percent: u8) -> Self {
202 let name = name.into();
203 let flag = name.clone();
204
205 self.define(name, move |scope: Scope| {
206 let flag = flag.clone();
207 async move { rollout::in_rollout(&flag, &scope, percent) }
208 })
209 }
210
211 pub fn force_on<I, S>(mut self, names: I) -> Self
213 where
214 I: IntoIterator<Item = S>,
215 S: Into<String>,
216 {
217 Arc::make_mut(&mut self.forced_on).extend(names.into_iter().map(Into::into));
218 self
219 }
220
221 pub fn force_off<I, S>(mut self, names: I) -> Self
223 where
224 I: IntoIterator<Item = S>,
225 S: Into<String>,
226 {
227 Arc::make_mut(&mut self.forced_off).extend(names.into_iter().map(Into::into));
228 self
229 }
230
231 pub fn defined(&self) -> Vec<&str> {
234 let mut names: Vec<&str> = self.definitions.keys().map(String::as_str).collect();
235 names.sort_unstable();
236 names
237 }
238
239 pub fn is_defined(&self, flag: &str) -> bool {
244 self.definitions.contains_key(flag)
245 }
246
247 pub fn store_handle(&self) -> &Arc<dyn FlagStore> {
249 &self.store
250 }
251
252 pub async fn active(&self, flag: &str, scope: &Scope) -> Result<bool> {
263 self.resolve(flag, scope).await
264 }
265
266 pub async fn inactive(&self, flag: &str, scope: &Scope) -> Result<bool> {
268 Ok(!self.active(flag, scope).await?)
269 }
270
271 pub fn for_scope(&self, scope: Scope) -> ScopedFlags {
277 ScopedFlags {
278 flags: self.clone(),
279 scope,
280 resolved: Arc::new(Mutex::new(HashMap::new())),
281 }
282 }
283
284 pub async fn activate_for(&self, flag: &str, scope: &Scope) -> Result<()> {
286 self.store.set(flag, scope, true).await
287 }
288
289 pub async fn deactivate_for(&self, flag: &str, scope: &Scope) -> Result<()> {
291 self.store.set(flag, scope, false).await
292 }
293
294 pub async fn forget_for(&self, flag: &str, scope: &Scope) -> Result<()> {
296 self.store.forget(flag, scope).await
297 }
298
299 pub async fn activate(&self, flag: &str) -> Result<()> {
301 self.activate_for(flag, &Scope::none()).await
302 }
303
304 pub async fn deactivate(&self, flag: &str) -> Result<()> {
307 self.deactivate_for(flag, &Scope::none()).await
308 }
309
310 pub async fn forget(&self, flag: &str) -> Result<()> {
312 self.forget_for(flag, &Scope::none()).await
313 }
314
315 pub async fn purge(&self) -> Result<()> {
317 self.store.flush().await
318 }
319
320 async fn resolve(&self, flag: &str, scope: &Scope) -> Result<bool> {
322 if self.forced_off.contains(flag) {
325 return Ok(false);
326 }
327
328 let scoped = self.store.get(flag, scope).await?;
329 let global = if scope.is_none() {
332 scoped
333 } else {
334 self.store.get(flag, &Scope::none()).await?
335 };
336
337 if scoped == Some(false) || global == Some(false) {
339 return Ok(false);
340 }
341 if scoped == Some(true) || global == Some(true) {
342 return Ok(true);
343 }
344
345 if self.forced_on.contains(flag) {
347 return Ok(true);
348 }
349
350 match self.definitions.get(flag) {
352 Some(resolver) => resolver(scope.clone()).await,
353 None => Ok(false),
354 }
355 }
356}
357
358impl std::fmt::Debug for Flags {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 f.debug_struct("Flags")
364 .field("defined", &self.defined())
365 .field("forced_on", &self.forced_on)
366 .field("forced_off", &self.forced_off)
367 .field("store", &self.store.name())
368 .finish()
369 }
370}
371
372#[derive(Clone)]
390pub struct ScopedFlags {
391 flags: Flags,
392 scope: Scope,
393 resolved: Arc<Mutex<HashMap<String, bool>>>,
394}
395
396impl ScopedFlags {
397 pub fn scope(&self) -> &Scope {
399 &self.scope
400 }
401
402 pub fn flags(&self) -> &Flags {
404 &self.flags
405 }
406
407 pub async fn active(&self, flag: &str) -> Result<bool> {
409 if let Some(answer) = self.remembered(flag) {
410 return Ok(answer);
411 }
412
413 let answer = self.flags.resolve(flag, &self.scope).await?;
414 self.remember(flag, answer);
415 Ok(answer)
416 }
417
418 pub async fn inactive(&self, flag: &str) -> Result<bool> {
420 Ok(!self.active(flag).await?)
421 }
422
423 pub async fn values<I, S>(&self, flags: I) -> Result<BTreeMap<String, bool>>
444 where
445 I: IntoIterator<Item = S>,
446 S: Into<String>,
447 {
448 let names: Vec<String> = flags.into_iter().map(Into::into).collect();
449 let mut answers = BTreeMap::new();
450
451 for name in names {
452 let answer = self.active(&name).await?;
453 answers.insert(name, answer);
454 }
455
456 Ok(answers)
457 }
458
459 pub async fn all(&self) -> Result<BTreeMap<String, bool>> {
466 let names: Vec<String> = self.flags.defined().into_iter().map(str::to_string).collect();
467 self.values(names).await
468 }
469
470 pub async fn any<I, S>(&self, flags: I) -> Result<bool>
472 where
473 I: IntoIterator<Item = S>,
474 S: Into<String>,
475 {
476 let names: Vec<String> = flags.into_iter().map(Into::into).collect();
477
478 for name in names {
479 if self.active(&name).await? {
480 return Ok(true);
481 }
482 }
483
484 Ok(false)
485 }
486
487 pub fn refresh(&self) {
492 self.lock().clear();
493 }
494
495 pub fn resolved_count(&self) -> usize {
498 self.lock().len()
499 }
500
501 fn remembered(&self, flag: &str) -> Option<bool> {
502 self.lock().get(flag).copied()
503 }
504
505 fn remember(&self, flag: &str, answer: bool) {
506 self.lock().insert(flag.to_string(), answer);
507 }
508
509 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, bool>> {
513 self.resolved.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
514 }
515}
516
517impl std::fmt::Debug for ScopedFlags {
518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519 f.debug_struct("ScopedFlags")
520 .field("scope", &self.scope)
521 .field("resolved", &*self.lock())
522 .finish()
523 }
524}
525
526pub(crate) fn missing_registry() -> Error {
531 Error::msg(
532 "feature flags were checked, but there is no `Flags` in application state. Register them \
533 with `App::new().plugin(FeatureFlags::new(flags))`, or put one there yourself with \
534 `Context::builder().state(flags)`. Refusing to guess whether the flag is on.",
535 )
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use std::sync::atomic::{AtomicUsize, Ordering};
542
543 fn flags() -> Flags {
546 Flags::new()
547 .define("new-checkout", |scope: Scope| async move { scope.id().ends_with('7') })
548 .define("beta-search", |_| async { false })
549 }
550
551 fn config(on: &str, off: &str) -> Config {
552 let config = Config::new();
553 config.set("flags.on", on);
554 config.set("flags.off", off);
555 config
556 }
557
558 #[tokio::test]
559 async fn a_resolver_decides_when_nothing_else_has_an_opinion() {
560 let flags = flags();
561
562 assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
563 assert!(flags.inactive("new-checkout", &Scope::user_id(18)).await.unwrap());
564 assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
565 }
566
567 #[tokio::test]
568 async fn a_flag_nobody_defined_is_off() {
569 assert!(flags().inactive("never-existed", &Scope::none()).await.unwrap());
572 assert!(!flags().is_defined("never-existed"));
573 assert_eq!(flags().defined(), ["beta-search", "new-checkout"]);
574 }
575
576 #[tokio::test]
577 async fn a_resolver_may_fail_and_the_failure_is_not_a_no() {
578 let flags = Flags::new()
579 .define("flaky", |_| async { Result::<bool>::Err(Error::msg("the database is down")) });
580
581 let error = flags.active("flaky", &Scope::none()).await.unwrap_err();
582
583 assert!(error.to_string().contains("the database is down"), "{error}");
584 }
585
586 #[tokio::test]
589 async fn an_override_for_a_scope_beats_the_resolver() {
590 let flags = flags();
591 let seven = Scope::user_id(7);
592 let eight = Scope::user_id(8);
593
594 flags.deactivate_for("new-checkout", &seven).await.unwrap();
595 flags.activate_for("beta-search", &eight).await.unwrap();
596
597 assert!(flags.inactive("new-checkout", &seven).await.unwrap());
598 assert!(flags.active("beta-search", &eight).await.unwrap());
599
600 assert!(flags.active("new-checkout", &Scope::user_id(27)).await.unwrap());
602 assert!(flags.inactive("beta-search", &Scope::user_id(9)).await.unwrap());
603 }
604
605 #[tokio::test]
606 async fn forgetting_an_override_gives_the_flag_back_to_the_resolver() {
607 let flags = flags();
608 let seven = Scope::user_id(7);
609
610 flags.deactivate_for("new-checkout", &seven).await.unwrap();
611 assert!(flags.inactive("new-checkout", &seven).await.unwrap());
612
613 flags.forget_for("new-checkout", &seven).await.unwrap();
614 assert!(flags.active("new-checkout", &seven).await.unwrap());
615 }
616
617 #[tokio::test]
618 async fn a_global_override_reaches_every_scope() {
619 let flags = flags();
620
621 flags.activate("beta-search").await.unwrap();
622
623 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
624 assert!(flags.active("beta-search", &Scope::tenant("acme")).await.unwrap());
625 assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
626 }
627
628 #[tokio::test]
629 async fn a_scope_override_and_a_global_one_agreeing_is_uneventful() {
630 let flags = flags();
631
632 flags.activate("beta-search").await.unwrap();
634 flags.activate_for("beta-search", &Scope::user_id(1)).await.unwrap();
635 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
636
637 flags.deactivate("new-checkout").await.unwrap();
639 flags.deactivate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
640 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
641 }
642
643 #[tokio::test]
644 async fn a_global_off_beats_a_scope_on() {
645 let flags = flags();
649
650 flags.activate_for("beta-search", &Scope::user_id(1)).await.unwrap();
651 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
652
653 flags.deactivate("beta-search").await.unwrap();
654 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
655
656 flags.forget("beta-search").await.unwrap();
658 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
659 }
660
661 #[tokio::test]
662 async fn a_scope_off_beats_a_global_on() {
663 let flags = flags();
664
665 flags.activate("beta-search").await.unwrap();
666 flags.deactivate_for("beta-search", &Scope::user_id(1)).await.unwrap();
667
668 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
669 assert!(flags.active("beta-search", &Scope::user_id(2)).await.unwrap());
670 }
671
672 #[tokio::test]
673 async fn config_off_beats_every_override_there_is() {
674 let flags = flags().configured(&config("", "new-checkout,beta-search"));
675
676 flags.activate("new-checkout").await.unwrap();
677 flags.activate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
678 flags.activate_for("beta-search", &Scope::tenant("acme")).await.unwrap();
679
680 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
683 assert!(flags.inactive("new-checkout", &Scope::none()).await.unwrap());
684 assert!(flags.inactive("beta-search", &Scope::tenant("acme")).await.unwrap());
685 }
686
687 #[tokio::test]
688 async fn config_on_beats_the_resolver_but_not_an_override() {
689 let flags = flags().configured(&config("beta-search", ""));
690
691 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
692
693 flags.deactivate_for("beta-search", &Scope::user_id(1)).await.unwrap();
696 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
697 assert!(flags.active("beta-search", &Scope::user_id(2)).await.unwrap());
698 }
699
700 #[tokio::test]
701 async fn a_name_in_both_config_lists_is_off() {
702 let flags = flags().configured(&config("beta-search", "beta-search"));
703
704 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
705 }
706
707 #[tokio::test]
708 async fn config_can_force_a_flag_nobody_defined() {
709 let flags = flags().configured(&config("unshipped", ""));
712
713 assert!(flags.active("unshipped", &Scope::none()).await.unwrap());
714 }
715
716 #[tokio::test]
717 async fn config_lists_accept_a_json_array_as_well_as_a_comma_separated_string() {
718 let config = Config::new();
719 config.set(
720 "flags.off",
721 rustlavel_core::Json::Array(vec![
722 rustlavel_core::Json::from("new-checkout"),
723 rustlavel_core::Json::from("beta-search"),
724 ]),
725 );
726
727 let flags = flags().configured(&config);
728
729 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
730 assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
731 }
732
733 #[tokio::test]
734 async fn config_off_survives_a_store_that_cannot_answer() {
735 let flags = Flags::new().store(BrokenStore).configured(&config("", "new-checkout"));
738
739 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
740 assert!(flags.active("beta-search", &Scope::user_id(17)).await.is_err());
742 }
743
744 struct BrokenStore;
746
747 impl FlagStore for BrokenStore {
748 fn name(&self) -> &'static str {
749 "broken"
750 }
751
752 fn get<'a>(
753 &'a self,
754 _flag: &'a str,
755 _scope: &'a Scope,
756 ) -> crate::store::BoxFuture<'a, Result<Option<bool>>> {
757 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
758 }
759
760 fn set<'a>(
761 &'a self,
762 _flag: &'a str,
763 _scope: &'a Scope,
764 _value: bool,
765 ) -> crate::store::BoxFuture<'a, Result<()>> {
766 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
767 }
768
769 fn forget<'a>(
770 &'a self,
771 _flag: &'a str,
772 _scope: &'a Scope,
773 ) -> crate::store::BoxFuture<'a, Result<()>> {
774 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
775 }
776
777 fn flush(&self) -> crate::store::BoxFuture<'_, Result<()>> {
778 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
779 }
780 }
781
782 #[tokio::test]
785 async fn a_rollout_puts_the_same_scope_in_the_same_bucket_across_instances() {
786 let first: Vec<bool> = {
790 let flags = Flags::new().rollout("dark-mode", 25);
791 let mut answers = Vec::new();
792 for id in 0..500 {
793 answers.push(flags.active("dark-mode", &Scope::user_id(id)).await.unwrap());
794 }
795 answers
796 };
797
798 let second: Vec<bool> = {
799 let flags = Flags::new().rollout("dark-mode", 25);
800 let mut answers = Vec::new();
801 for id in 0..500 {
802 answers.push(flags.active("dark-mode", &Scope::user_id(id)).await.unwrap());
803 }
804 answers
805 };
806
807 assert_eq!(first, second);
808 assert!(first.iter().any(|on| *on), "a 25% rollout reached nobody at all");
809 assert!(first.iter().any(|on| !*on), "a 25% rollout reached everybody");
810 }
811
812 #[tokio::test]
813 async fn a_rollout_is_roughly_the_proportion_it_says_over_ten_thousand_scopes() {
814 let flags = Flags::new().rollout("dark-mode", 25);
815 let mut inside = 0;
816
817 for id in 0..10_000 {
818 if flags.active("dark-mode", &Scope::user_id(id)).await.unwrap() {
819 inside += 1;
820 }
821 }
822
823 assert!((2_300..=2_700).contains(&inside), "25% of 10,000 scopes came out as {inside}");
824 }
825
826 #[tokio::test]
827 async fn a_rollout_is_still_overridable() {
828 let flags = Flags::new().rollout("dark-mode", 0);
829
830 assert!(flags.inactive("dark-mode", &Scope::user_id(41)).await.unwrap());
831
832 flags.activate_for("dark-mode", &Scope::user_id(41)).await.unwrap();
833 assert!(flags.active("dark-mode", &Scope::user_id(41)).await.unwrap());
834 }
835
836 fn counted() -> (Flags, Arc<AtomicUsize>) {
840 let calls = Arc::new(AtomicUsize::new(0));
841 let counter = Arc::clone(&calls);
842
843 let flags = Flags::new().define("slow", move |_| {
844 let counter = Arc::clone(&counter);
845 async move {
846 counter.fetch_add(1, Ordering::SeqCst);
847 true
848 }
849 });
850
851 (flags, calls)
852 }
853
854 #[tokio::test]
855 async fn a_scoped_view_runs_a_resolver_once_however_often_it_is_asked() {
856 let (flags, calls) = counted();
857 let view = flags.for_scope(Scope::user_id(41));
858
859 for _ in 0..10 {
860 assert!(view.active("slow").await.unwrap());
861 }
862 assert!(!view.inactive("slow").await.unwrap());
863
864 assert_eq!(calls.load(Ordering::SeqCst), 1);
865 assert_eq!(view.resolved_count(), 1);
866 }
867
868 #[tokio::test]
869 async fn asking_the_registry_directly_resolves_every_time() {
870 let (flags, calls) = counted();
873
874 for _ in 0..10 {
875 flags.active("slow", &Scope::user_id(41)).await.unwrap();
876 }
877
878 assert_eq!(calls.load(Ordering::SeqCst), 10);
879 }
880
881 #[tokio::test]
882 async fn two_scopes_are_two_answers_and_two_calls() {
883 let (flags, calls) = counted();
884
885 flags.for_scope(Scope::user_id(1)).active("slow").await.unwrap();
886 flags.for_scope(Scope::user_id(2)).active("slow").await.unwrap();
887
888 assert_eq!(calls.load(Ordering::SeqCst), 2);
889 }
890
891 #[tokio::test]
892 async fn a_batch_of_flags_costs_one_call_each() {
893 let (flags, calls) = counted();
894 let flags = flags.define("other", |_| async { false });
895 let view = flags.for_scope(Scope::user_id(41));
896
897 let values = view.values(["slow", "other", "slow", "other"]).await.unwrap();
898
899 assert_eq!(values.len(), 2);
900 assert!(values["slow"]);
901 assert!(!values["other"]);
902 assert_eq!(calls.load(Ordering::SeqCst), 1);
903 }
904
905 #[tokio::test]
906 async fn refreshing_a_view_makes_it_ask_again() {
907 let (flags, calls) = counted();
908 let view = flags.for_scope(Scope::user_id(41));
909
910 view.active("slow").await.unwrap();
911 view.refresh();
912 view.active("slow").await.unwrap();
913
914 assert_eq!(calls.load(Ordering::SeqCst), 2);
915 assert_eq!(view.scope(), &Scope::user_id(41));
916 }
917
918 #[tokio::test]
919 async fn a_view_answers_for_every_defined_flag_at_once() {
920 let view = flags().for_scope(Scope::user_id(17));
921 let all = view.all().await.unwrap();
922
923 assert_eq!(all.len(), 2);
924 assert!(all["new-checkout"]);
925 assert!(!all["beta-search"]);
926 assert!(view.any(["beta-search", "new-checkout"]).await.unwrap());
927 assert!(!view.any(["beta-search", "never-existed"]).await.unwrap());
928 }
929
930 #[tokio::test]
931 async fn a_view_carries_the_failure_of_a_resolver_rather_than_swallowing_it() {
932 let flags =
933 Flags::new().define("flaky", |_| async { Result::<bool>::Err(Error::msg("nope")) });
934
935 assert!(flags.for_scope(Scope::none()).values(["flaky"]).await.is_err());
936 }
937
938 #[tokio::test]
941 async fn purge_drops_every_override() {
942 let flags = flags();
943 flags.deactivate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
944 flags.activate("beta-search").await.unwrap();
945
946 flags.purge().await.unwrap();
947
948 assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
949 assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
950 }
951
952 #[tokio::test]
953 async fn a_clone_shares_the_store_and_the_definitions() {
954 let flags = flags();
955 let handle = flags.clone();
956
957 handle.activate("beta-search").await.unwrap();
958
959 assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
960 }
961
962 #[tokio::test]
963 async fn defining_a_name_twice_replaces_it() {
964 let flags = flags().define("beta-search", |_| async { true });
965
966 assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
967 assert_eq!(flags.defined().len(), 2);
968 }
969
970 #[tokio::test]
971 async fn a_store_can_be_supplied_as_a_shared_handle() {
972 let store = MemoryStore::new();
973 let flags = Flags::new().with_store(Arc::new(store.clone()) as Arc<dyn FlagStore>);
974
975 flags.activate("beta-search").await.unwrap();
976
977 assert_eq!(store.get("beta-search", &Scope::none()).await.unwrap(), Some(true));
980 assert_eq!(flags.store_handle().name(), "memory");
981 }
982
983 #[test]
984 fn the_debug_output_names_the_flags() {
985 let rendered = format!("{:?}", flags().configured(&config("a", "b")));
986
987 assert!(rendered.contains("new-checkout"), "{rendered}");
988 assert!(rendered.contains("forced_off"), "{rendered}");
989 assert!(rendered.contains("memory"), "{rendered}");
990 }
991
992 #[test]
993 fn the_missing_registry_error_says_how_to_fix_it() {
994 let message = missing_registry().to_string();
995
996 assert!(message.contains("FeatureFlags::new"), "{message}");
997 assert!(message.contains("Refusing to guess"), "{message}");
998 }
999}