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 {
147 let listed = |key: &str, variable: &str| {
148 let from_config = config.list(key);
149 if !from_config.is_empty() {
150 return from_config;
151 }
152 rustlavel_core::env::env_or(variable, "")
153 .split(',')
154 .map(str::trim)
155 .filter(|name| !name.is_empty())
156 .map(str::to_string)
157 .collect()
158 };
159
160 self.force_on(listed("flags.on", "FLAGS_ON"))
161 .force_off(listed("flags.off", "FLAGS_OFF"))
162 }
163
164 pub fn store(mut self, store: impl FlagStore) -> Self {
167 self.store = Arc::new(store);
168 self
169 }
170
171 pub fn with_store(mut self, store: Arc<dyn FlagStore>) -> Self {
173 self.store = store;
174 self
175 }
176
177 pub fn define<F, Fut, A>(mut self, name: impl Into<String>, resolver: F) -> Self
196 where
197 F: Fn(Scope) -> Fut + Send + Sync + 'static,
198 Fut: Future<Output = A> + Send + 'static,
199 A: FlagAnswer,
200 {
201 let boxed: Resolver = Arc::new(move |scope| {
202 let future = resolver(scope);
203 Box::pin(async move { future.await.into_answer() })
204 });
205
206 Arc::make_mut(&mut self.definitions).insert(name.into(), boxed);
209 self
210 }
211
212 pub fn rollout(self, name: impl Into<String>, percent: u8) -> Self {
222 let name = name.into();
223 let flag = name.clone();
224
225 self.define(name, move |scope: Scope| {
226 let flag = flag.clone();
227 async move { rollout::in_rollout(&flag, &scope, percent) }
228 })
229 }
230
231 pub fn force_on<I, S>(mut self, names: I) -> Self
233 where
234 I: IntoIterator<Item = S>,
235 S: Into<String>,
236 {
237 Arc::make_mut(&mut self.forced_on).extend(names.into_iter().map(Into::into));
238 self
239 }
240
241 pub fn force_off<I, S>(mut self, names: I) -> Self
243 where
244 I: IntoIterator<Item = S>,
245 S: Into<String>,
246 {
247 Arc::make_mut(&mut self.forced_off).extend(names.into_iter().map(Into::into));
248 self
249 }
250
251 pub fn defined(&self) -> Vec<&str> {
254 let mut names: Vec<&str> = self.definitions.keys().map(String::as_str).collect();
255 names.sort_unstable();
256 names
257 }
258
259 pub fn is_defined(&self, flag: &str) -> bool {
264 self.definitions.contains_key(flag)
265 }
266
267 pub fn store_handle(&self) -> &Arc<dyn FlagStore> {
269 &self.store
270 }
271
272 pub async fn active(&self, flag: &str, scope: &Scope) -> Result<bool> {
283 self.resolve(flag, scope).await
284 }
285
286 pub async fn inactive(&self, flag: &str, scope: &Scope) -> Result<bool> {
288 Ok(!self.active(flag, scope).await?)
289 }
290
291 pub fn for_scope(&self, scope: Scope) -> ScopedFlags {
297 ScopedFlags {
298 flags: self.clone(),
299 scope,
300 resolved: Arc::new(Mutex::new(HashMap::new())),
301 }
302 }
303
304 pub async fn activate_for(&self, flag: &str, scope: &Scope) -> Result<()> {
306 self.store.set(flag, scope, true).await
307 }
308
309 pub async fn deactivate_for(&self, flag: &str, scope: &Scope) -> Result<()> {
311 self.store.set(flag, scope, false).await
312 }
313
314 pub async fn forget_for(&self, flag: &str, scope: &Scope) -> Result<()> {
316 self.store.forget(flag, scope).await
317 }
318
319 pub async fn activate(&self, flag: &str) -> Result<()> {
321 self.activate_for(flag, &Scope::none()).await
322 }
323
324 pub async fn deactivate(&self, flag: &str) -> Result<()> {
327 self.deactivate_for(flag, &Scope::none()).await
328 }
329
330 pub async fn forget(&self, flag: &str) -> Result<()> {
332 self.forget_for(flag, &Scope::none()).await
333 }
334
335 pub async fn purge(&self) -> Result<()> {
337 self.store.flush().await
338 }
339
340 async fn resolve(&self, flag: &str, scope: &Scope) -> Result<bool> {
342 if self.forced_off.contains(flag) {
345 return Ok(false);
346 }
347
348 let scoped = self.store.get(flag, scope).await?;
349 let global = if scope.is_none() {
352 scoped
353 } else {
354 self.store.get(flag, &Scope::none()).await?
355 };
356
357 if scoped == Some(false) || global == Some(false) {
359 return Ok(false);
360 }
361 if scoped == Some(true) || global == Some(true) {
362 return Ok(true);
363 }
364
365 if self.forced_on.contains(flag) {
367 return Ok(true);
368 }
369
370 match self.definitions.get(flag) {
372 Some(resolver) => resolver(scope.clone()).await,
373 None => Ok(false),
374 }
375 }
376}
377
378impl std::fmt::Debug for Flags {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 f.debug_struct("Flags")
384 .field("defined", &self.defined())
385 .field("forced_on", &self.forced_on)
386 .field("forced_off", &self.forced_off)
387 .field("store", &self.store.name())
388 .finish()
389 }
390}
391
392#[derive(Clone)]
410pub struct ScopedFlags {
411 flags: Flags,
412 scope: Scope,
413 resolved: Arc<Mutex<HashMap<String, bool>>>,
414}
415
416impl ScopedFlags {
417 pub fn scope(&self) -> &Scope {
419 &self.scope
420 }
421
422 pub fn flags(&self) -> &Flags {
424 &self.flags
425 }
426
427 pub async fn active(&self, flag: &str) -> Result<bool> {
429 if let Some(answer) = self.remembered(flag) {
430 return Ok(answer);
431 }
432
433 let answer = self.flags.resolve(flag, &self.scope).await?;
434 self.remember(flag, answer);
435 Ok(answer)
436 }
437
438 pub async fn inactive(&self, flag: &str) -> Result<bool> {
440 Ok(!self.active(flag).await?)
441 }
442
443 pub async fn values<I, S>(&self, flags: I) -> Result<BTreeMap<String, bool>>
464 where
465 I: IntoIterator<Item = S>,
466 S: Into<String>,
467 {
468 let names: Vec<String> = flags.into_iter().map(Into::into).collect();
469 let mut answers = BTreeMap::new();
470
471 for name in names {
472 let answer = self.active(&name).await?;
473 answers.insert(name, answer);
474 }
475
476 Ok(answers)
477 }
478
479 pub async fn all(&self) -> Result<BTreeMap<String, bool>> {
486 let names: Vec<String> = self.flags.defined().into_iter().map(str::to_string).collect();
487 self.values(names).await
488 }
489
490 pub async fn any<I, S>(&self, flags: I) -> Result<bool>
492 where
493 I: IntoIterator<Item = S>,
494 S: Into<String>,
495 {
496 let names: Vec<String> = flags.into_iter().map(Into::into).collect();
497
498 for name in names {
499 if self.active(&name).await? {
500 return Ok(true);
501 }
502 }
503
504 Ok(false)
505 }
506
507 pub fn refresh(&self) {
512 self.lock().clear();
513 }
514
515 pub fn resolved_count(&self) -> usize {
518 self.lock().len()
519 }
520
521 fn remembered(&self, flag: &str) -> Option<bool> {
522 self.lock().get(flag).copied()
523 }
524
525 fn remember(&self, flag: &str, answer: bool) {
526 self.lock().insert(flag.to_string(), answer);
527 }
528
529 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, bool>> {
533 self.resolved.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
534 }
535}
536
537impl std::fmt::Debug for ScopedFlags {
538 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539 f.debug_struct("ScopedFlags")
540 .field("scope", &self.scope)
541 .field("resolved", &*self.lock())
542 .finish()
543 }
544}
545
546pub(crate) fn missing_registry() -> Error {
551 Error::msg(
552 "feature flags were checked, but there is no `Flags` in application state. Register them \
553 with `App::new().plugin(FeatureFlags::new(flags))`, or put one there yourself with \
554 `Context::builder().state(flags)`. Refusing to guess whether the flag is on.",
555 )
556}
557
558#[cfg(test)]
559mod tests {
560 static ENVIRONMENT: std::sync::Mutex<()> = std::sync::Mutex::new(());
565
566 #[test]
567 fn the_environment_is_read_when_no_config_file_maps_it() {
568 let _guard = ENVIRONMENT.lock().unwrap_or_else(|e| e.into_inner());
572 unsafe {
573 std::env::set_var("FLAGS_ON", "beta, later");
574 std::env::set_var("FLAGS_OFF", "broken");
575 }
576
577 let flags = Flags::from_config(&Config::new()).define("broken", |_| async { true });
578
579 assert!(flags.forced_on.contains("beta"), "FLAGS_ON was not read");
580 assert!(flags.forced_on.contains("later"), "the comma-separated spelling was not split");
581 assert!(flags.forced_off.contains("broken"), "FLAGS_OFF was not read");
582
583 unsafe {
584 std::env::remove_var("FLAGS_ON");
585 std::env::remove_var("FLAGS_OFF");
586 }
587 }
588
589 #[test]
590 fn a_configuration_file_wins_over_the_environment() {
591 let _guard = ENVIRONMENT.lock().unwrap_or_else(|e| e.into_inner());
595 unsafe {
596 std::env::set_var("FLAGS_ON", "from-environment");
597 }
598
599 let config = Config::new();
600 config.set("flags.on", "from-config");
601 let flags = Flags::from_config(&config);
602
603 assert!(flags.forced_on.contains("from-config"));
604 assert!(!flags.forced_on.contains("from-environment"));
605
606 unsafe {
607 std::env::remove_var("FLAGS_ON");
608 }
609 }
610
611 use super::*;
612 use std::sync::atomic::{AtomicUsize, Ordering};
613
614 fn flags() -> Flags {
617 Flags::new()
618 .define("new-checkout", |scope: Scope| async move { scope.id().ends_with('7') })
619 .define("beta-search", |_| async { false })
620 }
621
622 fn config(on: &str, off: &str) -> Config {
623 let config = Config::new();
624 config.set("flags.on", on);
625 config.set("flags.off", off);
626 config
627 }
628
629 #[tokio::test]
630 async fn a_resolver_decides_when_nothing_else_has_an_opinion() {
631 let flags = flags();
632
633 assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
634 assert!(flags.inactive("new-checkout", &Scope::user_id(18)).await.unwrap());
635 assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
636 }
637
638 #[tokio::test]
639 async fn a_flag_nobody_defined_is_off() {
640 assert!(flags().inactive("never-existed", &Scope::none()).await.unwrap());
643 assert!(!flags().is_defined("never-existed"));
644 assert_eq!(flags().defined(), ["beta-search", "new-checkout"]);
645 }
646
647 #[tokio::test]
648 async fn a_resolver_may_fail_and_the_failure_is_not_a_no() {
649 let flags = Flags::new()
650 .define("flaky", |_| async { Result::<bool>::Err(Error::msg("the database is down")) });
651
652 let error = flags.active("flaky", &Scope::none()).await.unwrap_err();
653
654 assert!(error.to_string().contains("the database is down"), "{error}");
655 }
656
657 #[tokio::test]
660 async fn an_override_for_a_scope_beats_the_resolver() {
661 let flags = flags();
662 let seven = Scope::user_id(7);
663 let eight = Scope::user_id(8);
664
665 flags.deactivate_for("new-checkout", &seven).await.unwrap();
666 flags.activate_for("beta-search", &eight).await.unwrap();
667
668 assert!(flags.inactive("new-checkout", &seven).await.unwrap());
669 assert!(flags.active("beta-search", &eight).await.unwrap());
670
671 assert!(flags.active("new-checkout", &Scope::user_id(27)).await.unwrap());
673 assert!(flags.inactive("beta-search", &Scope::user_id(9)).await.unwrap());
674 }
675
676 #[tokio::test]
677 async fn forgetting_an_override_gives_the_flag_back_to_the_resolver() {
678 let flags = flags();
679 let seven = Scope::user_id(7);
680
681 flags.deactivate_for("new-checkout", &seven).await.unwrap();
682 assert!(flags.inactive("new-checkout", &seven).await.unwrap());
683
684 flags.forget_for("new-checkout", &seven).await.unwrap();
685 assert!(flags.active("new-checkout", &seven).await.unwrap());
686 }
687
688 #[tokio::test]
689 async fn a_global_override_reaches_every_scope() {
690 let flags = flags();
691
692 flags.activate("beta-search").await.unwrap();
693
694 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
695 assert!(flags.active("beta-search", &Scope::tenant("acme")).await.unwrap());
696 assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
697 }
698
699 #[tokio::test]
700 async fn a_scope_override_and_a_global_one_agreeing_is_uneventful() {
701 let flags = flags();
702
703 flags.activate("beta-search").await.unwrap();
705 flags.activate_for("beta-search", &Scope::user_id(1)).await.unwrap();
706 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
707
708 flags.deactivate("new-checkout").await.unwrap();
710 flags.deactivate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
711 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
712 }
713
714 #[tokio::test]
715 async fn a_global_off_beats_a_scope_on() {
716 let flags = flags();
720
721 flags.activate_for("beta-search", &Scope::user_id(1)).await.unwrap();
722 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
723
724 flags.deactivate("beta-search").await.unwrap();
725 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
726
727 flags.forget("beta-search").await.unwrap();
729 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
730 }
731
732 #[tokio::test]
733 async fn a_scope_off_beats_a_global_on() {
734 let flags = flags();
735
736 flags.activate("beta-search").await.unwrap();
737 flags.deactivate_for("beta-search", &Scope::user_id(1)).await.unwrap();
738
739 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
740 assert!(flags.active("beta-search", &Scope::user_id(2)).await.unwrap());
741 }
742
743 #[tokio::test]
744 async fn config_off_beats_every_override_there_is() {
745 let flags = flags().configured(&config("", "new-checkout,beta-search"));
746
747 flags.activate("new-checkout").await.unwrap();
748 flags.activate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
749 flags.activate_for("beta-search", &Scope::tenant("acme")).await.unwrap();
750
751 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
754 assert!(flags.inactive("new-checkout", &Scope::none()).await.unwrap());
755 assert!(flags.inactive("beta-search", &Scope::tenant("acme")).await.unwrap());
756 }
757
758 #[tokio::test]
759 async fn config_on_beats_the_resolver_but_not_an_override() {
760 let flags = flags().configured(&config("beta-search", ""));
761
762 assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
763
764 flags.deactivate_for("beta-search", &Scope::user_id(1)).await.unwrap();
767 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
768 assert!(flags.active("beta-search", &Scope::user_id(2)).await.unwrap());
769 }
770
771 #[tokio::test]
772 async fn a_name_in_both_config_lists_is_off() {
773 let flags = flags().configured(&config("beta-search", "beta-search"));
774
775 assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
776 }
777
778 #[tokio::test]
779 async fn config_can_force_a_flag_nobody_defined() {
780 let flags = flags().configured(&config("unshipped", ""));
783
784 assert!(flags.active("unshipped", &Scope::none()).await.unwrap());
785 }
786
787 #[tokio::test]
788 async fn config_lists_accept_a_json_array_as_well_as_a_comma_separated_string() {
789 let config = Config::new();
790 config.set(
791 "flags.off",
792 rustlavel_core::Json::Array(vec![
793 rustlavel_core::Json::from("new-checkout"),
794 rustlavel_core::Json::from("beta-search"),
795 ]),
796 );
797
798 let flags = flags().configured(&config);
799
800 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
801 assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
802 }
803
804 #[tokio::test]
805 async fn config_off_survives_a_store_that_cannot_answer() {
806 let flags = Flags::new().store(BrokenStore).configured(&config("", "new-checkout"));
809
810 assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
811 assert!(flags.active("beta-search", &Scope::user_id(17)).await.is_err());
813 }
814
815 struct BrokenStore;
817
818 impl FlagStore for BrokenStore {
819 fn name(&self) -> &'static str {
820 "broken"
821 }
822
823 fn get<'a>(
824 &'a self,
825 _flag: &'a str,
826 _scope: &'a Scope,
827 ) -> crate::store::BoxFuture<'a, Result<Option<bool>>> {
828 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
829 }
830
831 fn set<'a>(
832 &'a self,
833 _flag: &'a str,
834 _scope: &'a Scope,
835 _value: bool,
836 ) -> crate::store::BoxFuture<'a, Result<()>> {
837 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
838 }
839
840 fn forget<'a>(
841 &'a self,
842 _flag: &'a str,
843 _scope: &'a Scope,
844 ) -> crate::store::BoxFuture<'a, Result<()>> {
845 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
846 }
847
848 fn flush(&self) -> crate::store::BoxFuture<'_, Result<()>> {
849 Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
850 }
851 }
852
853 #[tokio::test]
856 async fn a_rollout_puts_the_same_scope_in_the_same_bucket_across_instances() {
857 let first: Vec<bool> = {
861 let flags = Flags::new().rollout("dark-mode", 25);
862 let mut answers = Vec::new();
863 for id in 0..500 {
864 answers.push(flags.active("dark-mode", &Scope::user_id(id)).await.unwrap());
865 }
866 answers
867 };
868
869 let second: Vec<bool> = {
870 let flags = Flags::new().rollout("dark-mode", 25);
871 let mut answers = Vec::new();
872 for id in 0..500 {
873 answers.push(flags.active("dark-mode", &Scope::user_id(id)).await.unwrap());
874 }
875 answers
876 };
877
878 assert_eq!(first, second);
879 assert!(first.iter().any(|on| *on), "a 25% rollout reached nobody at all");
880 assert!(first.iter().any(|on| !*on), "a 25% rollout reached everybody");
881 }
882
883 #[tokio::test]
884 async fn a_rollout_is_roughly_the_proportion_it_says_over_ten_thousand_scopes() {
885 let flags = Flags::new().rollout("dark-mode", 25);
886 let mut inside = 0;
887
888 for id in 0..10_000 {
889 if flags.active("dark-mode", &Scope::user_id(id)).await.unwrap() {
890 inside += 1;
891 }
892 }
893
894 assert!((2_300..=2_700).contains(&inside), "25% of 10,000 scopes came out as {inside}");
895 }
896
897 #[tokio::test]
898 async fn a_rollout_is_still_overridable() {
899 let flags = Flags::new().rollout("dark-mode", 0);
900
901 assert!(flags.inactive("dark-mode", &Scope::user_id(41)).await.unwrap());
902
903 flags.activate_for("dark-mode", &Scope::user_id(41)).await.unwrap();
904 assert!(flags.active("dark-mode", &Scope::user_id(41)).await.unwrap());
905 }
906
907 fn counted() -> (Flags, Arc<AtomicUsize>) {
911 let calls = Arc::new(AtomicUsize::new(0));
912 let counter = Arc::clone(&calls);
913
914 let flags = Flags::new().define("slow", move |_| {
915 let counter = Arc::clone(&counter);
916 async move {
917 counter.fetch_add(1, Ordering::SeqCst);
918 true
919 }
920 });
921
922 (flags, calls)
923 }
924
925 #[tokio::test]
926 async fn a_scoped_view_runs_a_resolver_once_however_often_it_is_asked() {
927 let (flags, calls) = counted();
928 let view = flags.for_scope(Scope::user_id(41));
929
930 for _ in 0..10 {
931 assert!(view.active("slow").await.unwrap());
932 }
933 assert!(!view.inactive("slow").await.unwrap());
934
935 assert_eq!(calls.load(Ordering::SeqCst), 1);
936 assert_eq!(view.resolved_count(), 1);
937 }
938
939 #[tokio::test]
940 async fn asking_the_registry_directly_resolves_every_time() {
941 let (flags, calls) = counted();
944
945 for _ in 0..10 {
946 flags.active("slow", &Scope::user_id(41)).await.unwrap();
947 }
948
949 assert_eq!(calls.load(Ordering::SeqCst), 10);
950 }
951
952 #[tokio::test]
953 async fn two_scopes_are_two_answers_and_two_calls() {
954 let (flags, calls) = counted();
955
956 flags.for_scope(Scope::user_id(1)).active("slow").await.unwrap();
957 flags.for_scope(Scope::user_id(2)).active("slow").await.unwrap();
958
959 assert_eq!(calls.load(Ordering::SeqCst), 2);
960 }
961
962 #[tokio::test]
963 async fn a_batch_of_flags_costs_one_call_each() {
964 let (flags, calls) = counted();
965 let flags = flags.define("other", |_| async { false });
966 let view = flags.for_scope(Scope::user_id(41));
967
968 let values = view.values(["slow", "other", "slow", "other"]).await.unwrap();
969
970 assert_eq!(values.len(), 2);
971 assert!(values["slow"]);
972 assert!(!values["other"]);
973 assert_eq!(calls.load(Ordering::SeqCst), 1);
974 }
975
976 #[tokio::test]
977 async fn refreshing_a_view_makes_it_ask_again() {
978 let (flags, calls) = counted();
979 let view = flags.for_scope(Scope::user_id(41));
980
981 view.active("slow").await.unwrap();
982 view.refresh();
983 view.active("slow").await.unwrap();
984
985 assert_eq!(calls.load(Ordering::SeqCst), 2);
986 assert_eq!(view.scope(), &Scope::user_id(41));
987 }
988
989 #[tokio::test]
990 async fn a_view_answers_for_every_defined_flag_at_once() {
991 let view = flags().for_scope(Scope::user_id(17));
992 let all = view.all().await.unwrap();
993
994 assert_eq!(all.len(), 2);
995 assert!(all["new-checkout"]);
996 assert!(!all["beta-search"]);
997 assert!(view.any(["beta-search", "new-checkout"]).await.unwrap());
998 assert!(!view.any(["beta-search", "never-existed"]).await.unwrap());
999 }
1000
1001 #[tokio::test]
1002 async fn a_view_carries_the_failure_of_a_resolver_rather_than_swallowing_it() {
1003 let flags =
1004 Flags::new().define("flaky", |_| async { Result::<bool>::Err(Error::msg("nope")) });
1005
1006 assert!(flags.for_scope(Scope::none()).values(["flaky"]).await.is_err());
1007 }
1008
1009 #[tokio::test]
1012 async fn purge_drops_every_override() {
1013 let flags = flags();
1014 flags.deactivate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
1015 flags.activate("beta-search").await.unwrap();
1016
1017 flags.purge().await.unwrap();
1018
1019 assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
1020 assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
1021 }
1022
1023 #[tokio::test]
1024 async fn a_clone_shares_the_store_and_the_definitions() {
1025 let flags = flags();
1026 let handle = flags.clone();
1027
1028 handle.activate("beta-search").await.unwrap();
1029
1030 assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
1031 }
1032
1033 #[tokio::test]
1034 async fn defining_a_name_twice_replaces_it() {
1035 let flags = flags().define("beta-search", |_| async { true });
1036
1037 assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
1038 assert_eq!(flags.defined().len(), 2);
1039 }
1040
1041 #[tokio::test]
1042 async fn a_store_can_be_supplied_as_a_shared_handle() {
1043 let store = MemoryStore::new();
1044 let flags = Flags::new().with_store(Arc::new(store.clone()) as Arc<dyn FlagStore>);
1045
1046 flags.activate("beta-search").await.unwrap();
1047
1048 assert_eq!(store.get("beta-search", &Scope::none()).await.unwrap(), Some(true));
1051 assert_eq!(flags.store_handle().name(), "memory");
1052 }
1053
1054 #[test]
1055 fn the_debug_output_names_the_flags() {
1056 let rendered = format!("{:?}", flags().configured(&config("a", "b")));
1057
1058 assert!(rendered.contains("new-checkout"), "{rendered}");
1059 assert!(rendered.contains("forced_off"), "{rendered}");
1060 assert!(rendered.contains("memory"), "{rendered}");
1061 }
1062
1063 #[test]
1064 fn the_missing_registry_error_says_how_to_fix_it() {
1065 let message = missing_registry().to_string();
1066
1067 assert!(message.contains("FeatureFlags::new"), "{message}");
1068 assert!(message.contains("Refusing to guess"), "{message}");
1069 }
1070}