diff --git a/components/nimbus/android/src/test/java/org/mozilla/experiments/nimbus/NimbusTests.kt b/components/nimbus/android/src/test/java/org/mozilla/experiments/nimbus/NimbusTests.kt index 8fc91ecba7..4ca1555107 100644 --- a/components/nimbus/android/src/test/java/org/mozilla/experiments/nimbus/NimbusTests.kt +++ b/components/nimbus/android/src/test/java/org/mozilla/experiments/nimbus/NimbusTests.kt @@ -43,6 +43,7 @@ import org.mozilla.experiments.nimbus.internal.GeckoPrefHandler import org.mozilla.experiments.nimbus.internal.GeckoPrefState import org.mozilla.experiments.nimbus.internal.JsonObject import org.mozilla.experiments.nimbus.internal.NimbusException +import org.mozilla.experiments.nimbus.internal.OriginalGeckoPref import org.mozilla.experiments.nimbus.internal.PrefBranch import org.mozilla.experiments.nimbus.internal.PrefEnrollmentData import org.mozilla.experiments.nimbus.internal.PrefUnenrollReason @@ -860,6 +861,7 @@ class NimbusTests { ), ), var setValues: List? = null, + var originalGeckoPrefValues: List? = null, ) : GeckoPrefHandler { override fun getPrefsWithState(): Map> { return internalMap @@ -868,6 +870,10 @@ class NimbusTests { override fun setGeckoPrefsState(newPrefsState: List) { setValues = newPrefsState } + + override fun setGeckoPrefsOriginalValues(originalGeckoPrefs: List) { + originalGeckoPrefValues = originalGeckoPrefs + } } @Test @@ -889,6 +895,21 @@ class NimbusTests { assertEquals("42", handler.setValues?.get(0)?.enrollmentValue?.prefValue) } + @Test + fun `GeckoPrefHandler setGeckoPrefsOriginalValues function`() { + val handler = TestGeckoPrefHandler() + val originalValues = listOf( + OriginalGeckoPref( + pref = "pref.number", + branch = PrefBranch.DEFAULT, + value = "1", + ), + ) + handler.setGeckoPrefsOriginalValues(originalValues) + assertEquals(1, handler.originalGeckoPrefValues?.size) + assertEquals("pref.number", handler.originalGeckoPrefValues?.get(0)?.pref) + } + @Test fun `unenroll for gecko pref functions`() { val handler = TestGeckoPrefHandler() diff --git a/components/nimbus/src/enrollment.rs b/components/nimbus/src/enrollment.rs index 75cbeaee82..e0af097a54 100644 --- a/components/nimbus/src/enrollment.rs +++ b/components/nimbus/src/enrollment.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. #[cfg(feature = "stateful")] -use crate::stateful::gecko_prefs::{OriginalGeckoPref, PrefUnenrollReason}; +use crate::stateful::gecko_prefs::{GeckoPrefStore, OriginalGeckoPref, PrefUnenrollReason}; use crate::{ defaults::Defaults, error::{debug, warn, NimbusError, Result}, @@ -11,6 +11,8 @@ use crate::{ SLUG_REPLACEMENT_PATTERN, }; use serde_derive::*; +#[cfg(feature = "stateful")] +use std::sync::Arc; use std::{ collections::{HashMap, HashSet}, fmt::{Display, Formatter, Result as FmtResult}, @@ -145,6 +147,24 @@ pub struct PreviousGeckoPrefState { pub variable: String, } +#[cfg(feature = "stateful")] +impl PreviousGeckoPrefState { + pub(crate) fn on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states: &[Self], + gecko_pref_store: &Option>, + ) { + if let Some(store) = gecko_pref_store { + let original_values: Vec<_> = prev_gecko_pref_states + .iter() + .map(|state| state.original_value.clone()) + .collect(); + store + .handler + .set_gecko_prefs_original_values(original_values); + } + } +} + // Every experiment has an ExperimentEnrollment, even when we aren't enrolled. // ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️ // ⚠️ in `src/stateful/tests/test_enrollment_bw_compat.rs` below, and may require a DB migration. ⚠️ @@ -232,6 +252,7 @@ impl ExperimentEnrollment { available_randomization_units: &AvailableRandomizationUnits, updated_experiment: &Experiment, targeting_helper: &NimbusTargetingHelper, + #[cfg(feature = "stateful")] gecko_pref_store: &Option>, out_enrollment_events: &mut Vec, ) -> Result { Ok(match &self.status { @@ -265,12 +286,34 @@ impl ExperimentEnrollment { "Existing experiment enrollment '{}' is now disqualified (global opt-out)", &self.slug ); + #[cfg(feature = "stateful")] + if let EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + .. + } = &self.status + { + PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states, + gecko_pref_store, + ); + } let updated_enrollment = self.disqualify_from_enrolled(DisqualifiedReason::OptOut); out_enrollment_events.push(updated_enrollment.get_change_event()); updated_enrollment } else if !updated_experiment.has_branch(branch) { // The branch we were in disappeared! + #[cfg(feature = "stateful")] + if let EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + .. + } = &self.status + { + PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states, + gecko_pref_store, + ); + } let updated_enrollment = self.disqualify_from_enrolled(DisqualifiedReason::Error); out_enrollment_events.push(updated_enrollment.get_change_event()); @@ -285,6 +328,20 @@ impl ExperimentEnrollment { updated_experiment, targeting_helper, )?; + + #[cfg(feature = "stateful")] + if self.will_pref_experiment_change(updated_experiment, &evaluated_enrollment) { + if let EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + .. + } = &self.status + { + PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states, + gecko_pref_store, + ); + } + } match evaluated_enrollment.status { EnrollmentStatus::Error { .. } => { let updated_enrollment = @@ -369,6 +426,7 @@ impl ExperimentEnrollment { /// from the database after `PREVIOUS_ENROLLMENTS_GC_TIME`. fn on_experiment_ended( &self, + #[cfg(feature = "stateful")] gecko_pref_store: &Option>, out_enrollment_events: &mut Vec, ) -> Option { debug!( @@ -382,6 +440,17 @@ impl ExperimentEnrollment { | EnrollmentStatus::WasEnrolled { .. } | EnrollmentStatus::Error { .. } => return None, // We were never enrolled anyway, simply delete the enrollment record from the DB. }; + #[cfg(feature = "stateful")] + if let EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + .. + } = &self.status + { + PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states, + gecko_pref_store, + ); + } let enrollment = Self { slug: self.slug.clone(), status: EnrollmentStatus::WasEnrolled { @@ -399,9 +468,22 @@ impl ExperimentEnrollment { pub(crate) fn on_explicit_opt_out( &self, out_enrollment_events: &mut Vec, + #[cfg(feature = "stateful")] gecko_pref_store: &Option>, ) -> ExperimentEnrollment { match self.status { EnrollmentStatus::Enrolled { .. } => { + #[cfg(feature = "stateful")] + if let EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + .. + } = &self.status + { + PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states, + gecko_pref_store, + ); + } + let enrollment = self.disqualify_from_enrolled(DisqualifiedReason::OptOut); out_enrollment_events.push(enrollment.get_change_event()); enrollment @@ -555,6 +637,71 @@ impl ExperimentEnrollment { | EnrollmentStatus::Error { .. } => self.clone(), } } + + #[cfg(feature = "stateful")] + pub(crate) fn will_pref_experiment_change( + &self, + updated_experiment: &Experiment, + updated_enrollment: &ExperimentEnrollment, + ) -> bool { + let (original_prev_gecko_pref_states, original_branch_slug) = match &self.status { + EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + branch, + .. + } => (prev_gecko_pref_states, branch), + // Can't change if it isn't a pref experiment + _ => { + return false; + } + }; + + let updated_branch_slug = match &updated_enrollment.status { + EnrollmentStatus::Enrolled { branch, .. } => branch, + // If we are no longer going to be enrolled, then a change happened + _ => { + return true; + } + }; + + // Branch changed + if updated_branch_slug != original_branch_slug { + return true; + } + + // Couldn't get a branch, something changed + let Some(updated_branch) = updated_experiment.get_branch(updated_branch_slug) else { + return true; + }; + + let updated_features = updated_branch.get_feature_configs(); + let original_feature_ids: HashSet<&String> = original_prev_gecko_pref_states + .iter() + .map(|state| &state.feature_id) + .collect(); + + // Amount of features should be the same + if updated_features.len() != original_feature_ids.len() { + return true; + } + + for original_state in original_prev_gecko_pref_states { + let matching_feature = updated_features + .iter() + .find(|config| config.feature_id == original_state.feature_id); + + // If original feature isn't present, then something changed + let Some(updated_feature) = matching_feature else { + return true; + }; + + // Property key should still exist in the feature's value map + if !updated_feature.value.contains_key(&original_state.variable) { + return true; + } + } + false + } } // ⚠️ Attention : Changes to this type should be accompanied by a new test ⚠️ @@ -650,6 +797,7 @@ impl<'a> EnrollmentsEvolver<'a> { prev_experiments: &[E], next_experiments: &[Experiment], prev_enrollments: &[ExperimentEnrollment], + #[cfg(feature = "stateful")] gecko_pref_store: &Option>, ) -> Result<(Vec, Vec)> where E: ExperimentMetadata + Clone, @@ -671,6 +819,8 @@ impl<'a> EnrollmentsEvolver<'a> { &prev_rollouts, &next_rollouts, &ro_enrollments, + #[cfg(feature = "stateful")] + gecko_pref_store, )?; enrollments.extend(next_ro_enrollments); @@ -695,6 +845,8 @@ impl<'a> EnrollmentsEvolver<'a> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + gecko_pref_store, )?; enrollments.extend(next_exp_enrollments); @@ -711,6 +863,7 @@ impl<'a> EnrollmentsEvolver<'a> { prev_experiments: &[E], next_experiments: &[Experiment], prev_enrollments: &[ExperimentEnrollment], + #[cfg(feature = "stateful")] gecko_pref_store: &Option>, ) -> Result<(Vec, Vec)> where E: ExperimentMetadata + Clone, @@ -750,6 +903,8 @@ impl<'a> EnrollmentsEvolver<'a> { next_experiments_map.get(slug).copied(), Some(prev_enrollment), &mut enrollment_events, + #[cfg(feature = "stateful")] + gecko_pref_store, ) { Ok(enrollment) => enrollment, Err(e) => { @@ -851,6 +1006,8 @@ impl<'a> EnrollmentsEvolver<'a> { Some(next_experiment), prev_enrollment, &mut enrollment_events, + #[cfg(feature = "stateful")] + gecko_pref_store, ) { Ok(enrollment) => enrollment, Err(e) => { @@ -944,6 +1101,7 @@ impl<'a> EnrollmentsEvolver<'a> { next_experiment: Option<&Experiment>, prev_enrollment: Option<&ExperimentEnrollment>, out_enrollment_events: &mut Vec, // out param containing the events we'd like to emit to glean. + #[cfg(feature = "stateful")] gecko_pref_store: &Option>, ) -> Result> where E: ExperimentMetadata + Clone, @@ -972,9 +1130,11 @@ impl<'a> EnrollmentsEvolver<'a> { out_enrollment_events, )?), // Experiment deleted remotely. - (Some(_), None, Some(enrollment)) => { - enrollment.on_experiment_ended(out_enrollment_events) - } + (Some(_), None, Some(enrollment)) => enrollment.on_experiment_ended( + #[cfg(feature = "stateful")] + gecko_pref_store, + out_enrollment_events, + ), // Known experiment. (Some(_), Some(experiment), Some(enrollment)) => { Some(enrollment.on_experiment_updated( @@ -982,6 +1142,8 @@ impl<'a> EnrollmentsEvolver<'a> { self.available_randomization_units, experiment, &targeting_helper, + #[cfg(feature = "stateful")] + gecko_pref_store, out_enrollment_events, )?) } diff --git a/components/nimbus/src/nimbus.udl b/components/nimbus/src/nimbus.udl index a91588b3d9..107badd640 100644 --- a/components/nimbus/src/nimbus.udl +++ b/components/nimbus/src/nimbus.udl @@ -128,6 +128,9 @@ callback interface GeckoPrefHandler { record> get_prefs_with_state(); void set_gecko_prefs_state(sequence new_prefs_state); + + void set_gecko_prefs_original_values(sequence original_gecko_prefs); + }; dictionary GeckoPref { diff --git a/components/nimbus/src/stateful/enrollment.rs b/components/nimbus/src/stateful/enrollment.rs index 566f1cc74e..839a3ca14c 100644 --- a/components/nimbus/src/stateful/enrollment.rs +++ b/components/nimbus/src/stateful/enrollment.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::enrollment::Participation; +use crate::stateful::gecko_prefs::GeckoPrefStore; use crate::stateful::persistence::{ DB_KEY_EXPERIMENT_PARTICIPATION, DB_KEY_ROLLOUT_PARTICIPATION, DEFAULT_EXPERIMENT_PARTICIPATION, DEFAULT_ROLLOUT_PARTICIPATION, @@ -18,6 +19,7 @@ use crate::{ }, EnrolledExperiment, EnrollmentStatus, Experiment, }; +use std::sync::Arc; impl EnrollmentsEvolver<'_> { /// Convenient wrapper around `evolve_enrollments` that fetches the current state of experiments, @@ -27,6 +29,7 @@ impl EnrollmentsEvolver<'_> { db: &Database, writer: &mut Writer, next_experiments: &[Experiment], + gecko_pref_store: &Option>, ) -> Result> { // Get separate participation states from the db let is_participating_in_experiments = get_experiment_participation(db, writer)?; @@ -47,6 +50,7 @@ impl EnrollmentsEvolver<'_> { &prev_experiments, next_experiments, &prev_enrollments, + gecko_pref_store, )?; let next_enrollments = map_enrollments(&next_enrollments); // Write the changes to the Database. @@ -134,13 +138,14 @@ pub fn opt_out( db: &Database, writer: &mut Writer, experiment_slug: &str, + gecko_prefs: &Option>, ) -> Result> { let mut events = vec![]; let enr_store = db.get_store(StoreId::Enrollments); if let Ok(Some(existing_enrollment)) = enr_store.get::(writer, experiment_slug) { - let updated_enrollment = &existing_enrollment.on_explicit_opt_out(&mut events); + let updated_enrollment = &existing_enrollment.on_explicit_opt_out(&mut events, gecko_prefs); enr_store.put(writer, experiment_slug, updated_enrollment)?; } else { events.push(EnrollmentChangeEvent { diff --git a/components/nimbus/src/stateful/gecko_prefs.rs b/components/nimbus/src/stateful/gecko_prefs.rs index c69b7623f7..122d8cc617 100644 --- a/components/nimbus/src/stateful/gecko_prefs.rs +++ b/components/nimbus/src/stateful/gecko_prefs.rs @@ -131,6 +131,9 @@ pub trait GeckoPrefHandler: Send + Sync { /// Used to set the state for each pref based on enrollments fn set_gecko_prefs_state(&self, new_prefs_state: Vec); + + /// Used to set back to the original state for each pref based on the original Gecko value + fn set_gecko_prefs_original_values(&self, original_gecko_prefs: Vec); } #[derive(Default)] diff --git a/components/nimbus/src/stateful/nimbus_client.rs b/components/nimbus/src/stateful/nimbus_client.rs index 97767823d9..1665ba910f 100644 --- a/components/nimbus/src/stateful/nimbus_client.rs +++ b/components/nimbus/src/stateful/nimbus_client.rs @@ -347,7 +347,7 @@ impl NimbusClient { pub fn opt_out(&self, experiment_slug: String) -> Result> { let db = self.db()?; let mut writer = db.write()?; - let result = opt_out(db, &mut writer, &experiment_slug)?; + let result = opt_out(db, &mut writer, &experiment_slug, &self.gecko_prefs)?; let mut state = self.mutable_state.lock().unwrap(); self.end_initialize(db, writer, &mut state)?; Ok(result) @@ -456,7 +456,7 @@ impl NimbusClient { &mut targeting_helper, &coenrolling_feature_ids, ); - evolver.evolve_enrollments_in_db(db, writer, experiments) + evolver.evolve_enrollments_in_db(db, writer, experiments, &self.gecko_prefs) } pub fn apply_pending_experiments(&self) -> Result> { diff --git a/components/nimbus/src/tests/helpers.rs b/components/nimbus/src/tests/helpers.rs index e31a5d689b..742aaea815 100644 --- a/components/nimbus/src/tests/helpers.rs +++ b/components/nimbus/src/tests/helpers.rs @@ -4,6 +4,8 @@ #![allow(unexpected_cfgs)] +#[cfg(feature = "stateful")] +use crate::stateful::gecko_prefs::OriginalGeckoPref; use crate::{ enrollment::{EnrolledFeatureConfig, EnrolledReason, ExperimentEnrollment, NotEnrolledReason}, metrics::{EnrollmentStatusExtraDef, MetricsHandler}, @@ -223,6 +225,7 @@ impl MetricsHandler for TestMetrics { #[cfg(feature = "stateful")] pub struct TestGeckoPrefHandlerState { pub prefs_set: Option>, + pub original_prefs_state: Option>, } #[cfg(feature = "stateful")] @@ -236,7 +239,10 @@ impl TestGeckoPrefHandler { pub(crate) fn new(prefs: MapOfFeatureIdToPropertyNameToGeckoPrefState) -> Self { Self { prefs, - state: Mutex::new(TestGeckoPrefHandlerState { prefs_set: None }), + state: Mutex::new(TestGeckoPrefHandlerState { + prefs_set: None, + original_prefs_state: None, + }), } } } @@ -253,6 +259,13 @@ impl GeckoPrefHandler for TestGeckoPrefHandler { .expect("Unable to lock TestGeckoPrefHandler state") .prefs_set = Some(new_prefs_state); } + + fn set_gecko_prefs_original_values(&self, original_prefs_state: Vec) { + self.state + .lock() + .expect("Unable to lock TestGeckoPrefHandler state") + .original_prefs_state = Some(original_prefs_state); + } } pub(crate) fn get_test_experiments() -> Vec { diff --git a/components/nimbus/src/tests/stateful/test_enrollment.rs b/components/nimbus/src/tests/stateful/test_enrollment.rs index d314a39b30..2b771873f3 100644 --- a/components/nimbus/src/tests/stateful/test_enrollment.rs +++ b/components/nimbus/src/tests/stateful/test_enrollment.rs @@ -59,7 +59,7 @@ fn test_enrollments() -> Result<()> { let ids = no_coenrolling_features(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut targeting_attributes, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &[exp1])?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &[exp1], &None)?; let enrollments = get_enrollments(&db, &writer)?; assert_eq!(enrollments.len(), 1); @@ -95,7 +95,7 @@ fn test_enrollments() -> Result<()> { )); // Now opt-out. - opt_out(&db, &mut writer, "secure-gold")?; + opt_out(&db, &mut writer, "secure-gold", &None)?; assert_eq!(get_enrollments(&db, &writer)?.len(), 0); // check we recorded the "why" correctly. let ee: ExperimentEnrollment = db @@ -142,7 +142,7 @@ fn test_updates() -> Result<()> { let ids = no_coenrolling_features(); let mut targeting_helper = th.clone(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut targeting_helper, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps)?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps, &None)?; let enrollments = get_enrollments(&db, &writer)?; assert_eq!(enrollments.len(), 2); @@ -151,7 +151,7 @@ fn test_updates() -> Result<()> { // pretend we just updated from the server and one of the 2 is missing. let exps = &[exps[1].clone()]; let mut evolver = EnrollmentsEvolver::new(&aru, &mut th, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, exps)?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, exps, &None)?; // should only have 1 now. let enrollments = get_enrollments(&db, &writer)?; @@ -192,7 +192,7 @@ fn test_global_opt_out() -> Result<()> { let ids = no_coenrolling_features(); let mut targeting_helper = th.clone(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut targeting_helper, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps)?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps, &None)?; let enrollments = get_enrollments(&db, &writer)?; assert_eq!(enrollments.len(), 0); @@ -217,7 +217,7 @@ fn test_global_opt_out() -> Result<()> { let mut targeting_helper = th.clone(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut targeting_helper, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps)?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps, &None)?; let enrollments = get_enrollments(&db, &writer)?; assert_eq!(enrollments.len(), 2); @@ -235,7 +235,7 @@ fn test_global_opt_out() -> Result<()> { let mut targeting_helper = th.clone(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut targeting_helper, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps)?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps, &None)?; let enrollments = get_enrollments(&db, &writer)?; assert_eq!(enrollments.len(), 0); @@ -263,7 +263,7 @@ fn test_global_opt_out() -> Result<()> { set_experiment_participation(&db, &mut writer, true)?; let mut evolver = EnrollmentsEvolver::new(&aru, &mut th, &ids); - let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps)?; + let events = evolver.evolve_enrollments_in_db(&db, &mut writer, &exps, &None)?; let enrollments = get_enrollments(&db, &writer)?; assert_eq!(enrollments.len(), 0); @@ -424,7 +424,7 @@ fn test_experiments_opt_out_with_rollouts_opt_in() -> Result<()> { let ids = no_coenrolling_features(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut th, &ids); - let _ = evolver.evolve_enrollments_in_db(&db, &mut writer, &[experiment, rollout])?; + let _ = evolver.evolve_enrollments_in_db(&db, &mut writer, &[experiment, rollout], &None)?; let enrollments = get_experiment_enrollments(&db, &writer)?; println!("Total enrollments: {}", enrollments.len()); @@ -500,7 +500,8 @@ fn test_rollouts_opt_out_with_experiments_opt_in() -> Result<()> { let ids = no_coenrolling_features(); let mut evolver = EnrollmentsEvolver::new(&aru, &mut th, &ids); - let _events = evolver.evolve_enrollments_in_db(&db, &mut writer, &[experiment, rollout])?; + let _events = + evolver.evolve_enrollments_in_db(&db, &mut writer, &[experiment, rollout], &None)?; // Use the same helper function as the working test let enrollments = get_experiment_enrollments(&db, &writer)?; diff --git a/components/nimbus/src/tests/stateful/test_gecko_prefs.rs b/components/nimbus/src/tests/stateful/test_gecko_prefs.rs index eea7680c9f..706e0575b1 100644 --- a/components/nimbus/src/tests/stateful/test_gecko_prefs.rs +++ b/components/nimbus/src/tests/stateful/test_gecko_prefs.rs @@ -8,7 +8,7 @@ use crate::{ json::PrefValue, stateful::gecko_prefs::{ create_feature_prop_pref_map, GeckoPrefHandler, GeckoPrefState, GeckoPrefStore, - GeckoPrefStoreState, PrefBranch, PrefEnrollmentData, + GeckoPrefStoreState, OriginalGeckoPref, PrefBranch, PrefEnrollmentData, }, tests::helpers::{get_multi_feature_experiment, TestGeckoPrefHandler}, EnrolledExperiment, @@ -362,3 +362,35 @@ fn test_build_prev_gecko_pref_states() -> Result<()> { Ok(()) } + +#[test] +fn test_set_gecko_prefs_original_values() { + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_gecko_prefs = vec![OriginalGeckoPref::from(&pref_state_1)]; + let handler = TestGeckoPrefHandler::new(create_feature_prop_pref_map(vec![( + "feature-id", + "test_prop", + pref_state_1.clone(), + )])); + let handler: Arc> = Arc::new(Box::new(handler)); + let store = Arc::new(GeckoPrefStore::new(handler.clone())); + let _ = store.initialize(); + + handler.set_gecko_prefs_original_values(original_gecko_prefs.clone()); + let test_handler = unsafe { + std::mem::transmute::>, Arc>>( + handler, + ) + }; + let test_handler_state = test_handler + .state + .lock() + .expect("Unable to lock transmuted handler state"); + + let original_prefs_stored = test_handler_state.original_prefs_state.clone().unwrap(); + assert_eq!(1, original_prefs_stored.len()); + assert_eq!(&original_gecko_prefs, &original_prefs_stored); +} diff --git a/components/nimbus/src/tests/test_enrollment.rs b/components/nimbus/src/tests/test_enrollment.rs index 37b1cbe73c..93640808e8 100644 --- a/components/nimbus/src/tests/test_enrollment.rs +++ b/components/nimbus/src/tests/test_enrollment.rs @@ -17,6 +17,15 @@ use crate::{ AppContext, AvailableRandomizationUnits, Branch, BucketConfig, Experiment, FeatureConfig, NimbusTargetingHelper, TargetingAttributes, }; +#[cfg(feature = "stateful")] +use crate::{ + stateful::gecko_prefs::{ + create_feature_prop_pref_map, GeckoPrefHandler, GeckoPrefState, GeckoPrefStore, + }, + tests::helpers::TestGeckoPrefHandler, +}; +#[cfg(feature = "stateful")] +use std::sync::Arc; cfg_if::cfg_if! { if #[cfg(feature = "stateful")] { @@ -435,7 +444,15 @@ fn test_ios_rollout_experiment() -> Result<()> { let mut evolver = enrollment_evolver(&mut th, &aru, &ids); let mut events = vec![]; let enrollment = evolver - .evolve_enrollment::(true, None, Some(exp), None, &mut events)? + .evolve_enrollment::( + true, + None, + Some(exp), + None, + &mut events, + #[cfg(feature = "stateful")] + &None, + )? .unwrap(); println!("Enrollment: {:?}", &enrollment.status); assert!(matches!( @@ -454,7 +471,15 @@ fn test_evolver_new_experiment_enrolled() -> Result<()> { let mut evolver = enrollment_evolver(&mut th, &aru, &ids); let mut events = vec![]; let enrollment = evolver - .evolve_enrollment::(true, None, Some(exp), None, &mut events)? + .evolve_enrollment::( + true, + None, + Some(exp), + None, + &mut events, + #[cfg(feature = "stateful")] + &None, + )? .unwrap(); assert!(matches!( enrollment.status, @@ -476,7 +501,15 @@ fn test_evolver_new_experiment_not_enrolled() -> Result<()> { let mut evolver = enrollment_evolver(&mut th, &aru, &ids); let mut events = vec![]; let enrollment = evolver - .evolve_enrollment::(true, None, Some(&exp), None, &mut events)? + .evolve_enrollment::( + true, + None, + Some(&exp), + None, + &mut events, + #[cfg(feature = "stateful")] + &None, + )? .unwrap(); assert!(matches!( enrollment.status, @@ -497,7 +530,15 @@ fn test_evolver_new_experiment_globally_opted_out() -> Result<()> { let mut evolver = enrollment_evolver(&mut th, &aru, &ids); let mut events = vec![]; let enrollment = evolver - .evolve_enrollment::(false, None, Some(&exp), None, &mut events)? + .evolve_enrollment::( + false, + None, + Some(&exp), + None, + &mut events, + #[cfg(feature = "stateful")] + &None, + )? .unwrap(); assert!(matches!( enrollment.status, @@ -519,7 +560,15 @@ fn test_evolver_new_experiment_enrollment_paused() -> Result<()> { let mut evolver = enrollment_evolver(&mut th, &aru, &ids); let mut events = vec![]; let enrollment = evolver - .evolve_enrollment::(true, None, Some(&exp), None, &mut events)? + .evolve_enrollment::( + true, + None, + Some(&exp), + None, + &mut events, + #[cfg(feature = "stateful")] + &None, + )? .unwrap(); assert!(matches!( enrollment.status, @@ -552,6 +601,8 @@ fn test_evolver_experiment_update_not_enrolled_opted_out() -> Result<()> { Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert_eq!(enrollment.status, existing_enrollment.status); @@ -581,6 +632,8 @@ fn test_evolver_experiment_update_not_enrolled_enrollment_paused() -> Result<()> Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert_eq!(enrollment.status, existing_enrollment.status); @@ -610,6 +663,8 @@ fn test_evolver_experiment_update_not_enrolled_resuming_not_selected() -> Result Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -643,6 +698,8 @@ fn test_evolver_experiment_update_not_enrolled_resuming_selected() -> Result<()> Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -682,6 +739,8 @@ fn test_evolver_experiment_update_enrolled_then_opted_out() -> Result<()> { Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -727,6 +786,8 @@ fn test_evolver_experiment_update_enrolled_then_experiment_paused() -> Result<() Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); dbg!(&enrollment.status); @@ -771,6 +832,8 @@ fn test_evolver_experiment_update_enrolled_then_targeting_changed() -> Result<() Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); @@ -819,6 +882,8 @@ fn test_evolver_experiment_update_enrolled_then_bucketing_changed() -> Result<() Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -848,8 +913,14 @@ fn test_rollout_unenrolls_when_bucketing_changes() -> Result<()> { let ro = get_bucketed_rollout(slug, 0); let recipes = [ro]; - let (enrollments, _) = - evolver.evolve_enrollments::(Participation::default(), &[], &recipes, &[])?; + let (enrollments, _) = evolver.evolve_enrollments::( + Participation::default(), + &[], + &recipes, + &[], + #[cfg(feature = "stateful")] + &None, + )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -866,6 +937,8 @@ fn test_rollout_unenrolls_when_bucketing_changes() -> Result<()> { &prev_recipes, &recipes, enrollments.as_slice(), + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -882,6 +955,8 @@ fn test_rollout_unenrolls_when_bucketing_changes() -> Result<()> { &prev_recipes, &recipes, enrollments.as_slice(), + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -910,8 +985,14 @@ fn test_rollout_unenrolls_then_reenrolls_when_bucketing_changes() -> Result<()> let ro = get_bucketed_rollout(slug, 0); let recipes = [ro]; - let (enrollments, _) = - evolver.evolve_enrollments::(Participation::default(), &[], &recipes, &[])?; + let (enrollments, _) = evolver.evolve_enrollments::( + Participation::default(), + &[], + &recipes, + &[], + #[cfg(feature = "stateful")] + &None, + )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -928,6 +1009,8 @@ fn test_rollout_unenrolls_then_reenrolls_when_bucketing_changes() -> Result<()> &prev_recipes, &recipes, enrollments.as_slice(), + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -944,6 +1027,8 @@ fn test_rollout_unenrolls_then_reenrolls_when_bucketing_changes() -> Result<()> &prev_recipes, &recipes, enrollments.as_slice(), + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -966,6 +1051,8 @@ fn test_rollout_unenrolls_then_reenrolls_when_bucketing_changes() -> Result<()> &prev_recipes, &recipes, enrollments.as_slice(), + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollments.len(), 1); let enr = enrollments.first().unwrap(); @@ -1012,6 +1099,8 @@ fn test_experiment_does_not_reenroll_from_disqualified_not_selected_or_not_targe }, }, ], + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollments.len(), 2); @@ -1065,6 +1154,8 @@ fn test_evolver_experiment_update_enrolled_then_branches_changed() -> Result<()> Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert_eq!(enrollment, existing_enrollment); @@ -1102,6 +1193,8 @@ fn test_evolver_experiment_update_enrolled_then_branch_disappears() -> Result<() Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -1143,6 +1236,8 @@ fn test_evolver_experiment_update_disqualified_then_opted_out() -> Result<()> { Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -1178,6 +1273,8 @@ fn test_evolver_experiment_update_disqualified_then_bucketing_ok() -> Result<()> Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert_eq!(enrollment, existing_enrollment); @@ -1202,6 +1299,8 @@ fn test_evolver_feature_can_have_only_one_experiment() -> Result<()> { &existing_experiments, &updated_experiments, &existing_enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(2, enrollments.len()); @@ -1242,6 +1341,8 @@ fn test_evolver_feature_can_have_only_one_experiment() -> Result<()> { &existing_experiments, &updated_experiments, &existing_enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(2, enrollments.len()); @@ -1269,6 +1370,8 @@ fn test_evolver_feature_can_have_only_one_experiment() -> Result<()> { &existing_experiments, &updated_experiments, &existing_enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(2, enrollments.len()); @@ -1292,6 +1395,8 @@ fn test_evolver_feature_can_have_only_one_experiment() -> Result<()> { &existing_experiments, &updated_experiments, &existing_enrollments, + #[cfg(feature = "stateful")] + &None, )?; // There should be one WasEnrolled; the NotEnrolled will have been @@ -1355,6 +1460,8 @@ fn test_evolver_experiment_not_enrolled_feature_conflict() -> Result<()> { &[], &test_experiments, &[], + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( @@ -1421,6 +1528,8 @@ fn test_multi_feature_per_branch_conflict() -> Result<()> { &[], &test_experiments, &[], + #[cfg(feature = "stateful")] + &None, )?; let enrolled_count = enrollments @@ -1465,6 +1574,8 @@ fn test_evolver_feature_id_reuse() -> Result<()> { &[], &test_experiments, &[], + #[cfg(feature = "stateful")] + &None, )?; let enrolled_count = enrollments @@ -1482,6 +1593,8 @@ fn test_evolver_feature_id_reuse() -> Result<()> { &test_experiments, &[test_experiments[1].clone(), conflicting_experiment.clone()], &enrollments, + #[cfg(feature = "stateful")] + &None, )?; debug!("events = {:?}", events); @@ -1532,6 +1645,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &[], &next_experiments, &[], + #[cfg(feature = "stateful")] + &None, )?; let feature_map = @@ -1574,6 +1689,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( @@ -1618,6 +1735,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; let feature_map = @@ -1650,6 +1769,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; let feature_map = @@ -1686,6 +1807,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; // 4b. Add the single feature experiments. @@ -1701,6 +1824,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( @@ -1742,6 +1867,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( @@ -1782,6 +1909,8 @@ fn test_evolver_multi_feature_experiments() -> Result<()> { &prev_experiments, &next_experiments, &prev_enrollments, + #[cfg(feature = "stateful")] + &None, )?; let feature_map = @@ -1838,6 +1967,8 @@ fn test_evolver_experiment_update_was_enrolled() -> Result<()> { Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert_eq!(enrollment, existing_enrollment); @@ -2053,8 +2184,14 @@ fn test_evolve_enrollments_with_coenrolling_features() -> Result<()> { let all_experiments = [exp1, exp2, exp3.clone(), exp4.clone()]; let no_experiments: [Experiment; 0] = []; - let (enrollments, _) = - evolver.evolve_enrollment_recipes(true, &no_experiments, &all_experiments, &[])?; + let (enrollments, _) = evolver.evolve_enrollment_recipes( + true, + &no_experiments, + &all_experiments, + &[], + #[cfg(feature = "stateful")] + &None, + )?; let observed = map_features_by_feature_id(&enrollments, &all_experiments, &ids); let expected = HashMap::from([ @@ -2079,8 +2216,14 @@ fn test_evolve_enrollments_with_coenrolling_features() -> Result<()> { assert_eq!(observed, expected); let experiments = [exp3, exp4]; - let (enrollments, _) = - evolver.evolve_enrollment_recipes(true, &all_experiments, &experiments, &enrollments)?; + let (enrollments, _) = evolver.evolve_enrollment_recipes( + true, + &all_experiments, + &experiments, + &enrollments, + #[cfg(feature = "stateful")] + &None, + )?; let observed = map_features_by_feature_id(&enrollments, &all_experiments, &ids); let expected = HashMap::from([ @@ -2139,8 +2282,14 @@ fn test_evolve_enrollments_with_coenrolling_multi_features() -> Result<()> { let all_experiments = [exp1, exp2, exp3.clone(), exp4.clone()]; let no_experiments: [Experiment; 0] = []; - let (enrollments, _) = - evolver.evolve_enrollment_recipes(true, &no_experiments, &all_experiments, &[])?; + let (enrollments, _) = evolver.evolve_enrollment_recipes( + true, + &no_experiments, + &all_experiments, + &[], + #[cfg(feature = "stateful")] + &None, + )?; let observed = map_features_by_feature_id(&enrollments, &all_experiments, &ids); let expected = HashMap::from([ @@ -2171,8 +2320,14 @@ fn test_evolve_enrollments_with_coenrolling_multi_features() -> Result<()> { assert_eq!(observed, expected); let experiments = [exp3, exp4]; - let (enrollments, _) = - evolver.evolve_enrollment_recipes(true, &all_experiments, &experiments, &enrollments)?; + let (enrollments, _) = evolver.evolve_enrollment_recipes( + true, + &all_experiments, + &experiments, + &enrollments, + #[cfg(feature = "stateful")] + &None, + )?; let observed = map_features_by_feature_id(&enrollments, &all_experiments, &ids); let expected = HashMap::from([ @@ -2312,6 +2467,8 @@ fn test_evolve_enrollments_error_handling() -> Result<()> { &test_experiments, &test_experiments, &[], + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( @@ -2333,6 +2490,8 @@ fn test_evolve_enrollments_error_handling() -> Result<()> { &[], &test_experiments, &existing_enrollments[..], + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( @@ -2369,6 +2528,8 @@ fn test_evolve_enrollments_is_already_enrolled_targeting() -> Result<()> { &[], test_experiments, &[], + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( enrollments.len(), @@ -2392,6 +2553,8 @@ fn test_evolve_enrollments_is_already_enrolled_targeting() -> Result<()> { test_experiments, test_experiments, &enrollments, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!( enrollments.len(), @@ -2429,6 +2592,8 @@ fn test_evolver_experiment_update_error() -> Result<()> { Some(&exp), Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); assert!(matches!( @@ -2463,6 +2628,8 @@ fn test_evolver_experiment_ended_was_enrolled() -> Result<()> { None, Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); if let EnrollmentStatus::WasEnrolled { branch, .. } = enrollment.status { @@ -2499,6 +2666,8 @@ fn test_evolver_experiment_ended_was_disqualified() -> Result<()> { None, Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )? .unwrap(); if let EnrollmentStatus::WasEnrolled { branch, .. } = enrollment.status { @@ -2533,6 +2702,8 @@ fn test_evolver_experiment_ended_was_not_enrolled() -> Result<()> { None, Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )?; assert!(enrollment.is_none()); assert!(events.is_empty()); @@ -2559,6 +2730,8 @@ fn test_evolver_garbage_collection_before_threshold() -> Result<()> { None, Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )?; assert_eq!(enrollment.unwrap(), existing_enrollment); assert!(events.is_empty()); @@ -2585,6 +2758,8 @@ fn test_evolver_garbage_collection_after_threshold() -> Result<()> { None, Some(&existing_enrollment), &mut events, + #[cfg(feature = "stateful")] + &None, )?; assert!(enrollment.is_none()); assert!(events.is_empty()); @@ -2611,6 +2786,8 @@ fn test_evolver_new_experiment_enrollment_already_exists() { Some(&exp), Some(&existing_enrollment), &mut vec![], + #[cfg(feature = "stateful")] + &None, ); assert!(res.is_err()); } @@ -2622,7 +2799,15 @@ fn test_evolver_existing_experiment_has_no_enrollment() { let mut th = app_ctx.into(); let ids = no_coenrolling_features(); let mut evolver = enrollment_evolver(&mut th, &aru, &ids); - let res = evolver.evolve_enrollment(true, Some(&exp), Some(&exp), None, &mut vec![]); + let res = evolver.evolve_enrollment( + true, + Some(&exp), + Some(&exp), + None, + &mut vec![], + #[cfg(feature = "stateful")] + &None, + ); assert!(res.is_err()); } @@ -2634,7 +2819,15 @@ fn test_evolver_no_experiments_no_enrollment() { let ids = no_coenrolling_features(); let mut evolver = enrollment_evolver(&mut th, &aru, &ids); evolver - .evolve_enrollment::(true, None, None, None, &mut vec![]) + .evolve_enrollment::( + true, + None, + None, + None, + &mut vec![], + #[cfg(feature = "stateful")] + &None, + ) .unwrap(); } @@ -2675,8 +2868,14 @@ fn test_evolver_rollouts_do_not_conflict_with_experiments() -> Result<()> { let mut th = app_ctx.into(); let ids = no_coenrolling_features(); let mut evolver = enrollment_evolver(&mut th, &aru, &ids); - let (enrollments, events) = - evolver.evolve_enrollments::(Participation::default(), &[], recipes, &[])?; + let (enrollments, events) = evolver.evolve_enrollments::( + Participation::default(), + &[], + recipes, + &[], + #[cfg(feature = "stateful")] + &None, + )?; assert_eq!(enrollments.len(), 2); assert_eq!(events.len(), 2); @@ -2736,8 +2935,14 @@ fn test_evolver_rollouts_do_not_conflict_with_rollouts() -> Result<()> { let mut th = app_ctx.into(); let ids = no_coenrolling_features(); let mut evolver = enrollment_evolver(&mut th, &aru, &ids); - let (enrollments, events) = - evolver.evolve_enrollments::(Participation::default(), &[], recipes, &[])?; + let (enrollments, events) = evolver.evolve_enrollments::( + Participation::default(), + &[], + recipes, + &[], + #[cfg(feature = "stateful")] + &None, + )?; assert_eq!(enrollments.len(), 3); assert_eq!(events.len(), 3); @@ -2966,8 +3171,14 @@ fn test_rollouts_end_to_end() -> Result<()> { let ids = no_coenrolling_features(); let mut evolver = enrollment_evolver(&mut th, &aru, &ids); - let (enrollments, _events) = - evolver.evolve_enrollments::(Participation::default(), &[], recipes, &[])?; + let (enrollments, _events) = evolver.evolve_enrollments::( + Participation::default(), + &[], + recipes, + &[], + #[cfg(feature = "stateful")] + &None, + )?; let features = map_features_by_feature_id(&enrollments, recipes, &no_coenrolling_features()); @@ -3019,7 +3230,11 @@ fn test_enrollment_enrolled_explicit_opt_out() { prev_gecko_pref_states: None, }, }; - let enrollment = existing_enrollment.on_explicit_opt_out(&mut events); + let enrollment = existing_enrollment.on_explicit_opt_out( + &mut events, + #[cfg(feature = "stateful")] + &None, + ); if let EnrollmentStatus::Disqualified { branch, .. } = enrollment.status { assert_eq!(branch, "control"); } else { @@ -3042,7 +3257,11 @@ fn test_enrollment_not_enrolled_explicit_opt_out() { reason: NotEnrolledReason::NotTargeted, }, }; - let enrollment = existing_enrollment.on_explicit_opt_out(&mut events); + let enrollment = existing_enrollment.on_explicit_opt_out( + &mut events, + #[cfg(feature = "stateful")] + &None, + ); assert!(matches!( enrollment.status, EnrollmentStatus::NotEnrolled { @@ -3064,7 +3283,11 @@ fn test_enrollment_disqualified_explicit_opt_out() { reason: DisqualifiedReason::NotTargeted, }, }; - let enrollment = existing_enrollment.on_explicit_opt_out(&mut events); + let enrollment = existing_enrollment.on_explicit_opt_out( + &mut events, + #[cfg(feature = "stateful")] + &None, + ); assert_eq!(enrollment, existing_enrollment); assert!(events.is_empty()); } @@ -3237,8 +3460,14 @@ fn test_evolve_enrollments_ordering() -> Result<()> { let all_experiments = [exp1, exp2]; let no_experiments: [Experiment; 0] = []; - let (enrollments, _) = - evolver.evolve_enrollment_recipes(true, &no_experiments, &all_experiments, &[])?; + let (enrollments, _) = evolver.evolve_enrollment_recipes( + true, + &no_experiments, + &all_experiments, + &[], + #[cfg(feature = "stateful")] + &None, + )?; let observed = map_features_by_feature_id(&enrollments, &all_experiments, &ids); let expected = HashMap::from([( @@ -3325,3 +3554,259 @@ fn test_on_add_states() { } } } + +#[cfg(feature = "stateful")] +#[test] +fn test_on_revert_to_previous_state_with_gecko_prefs() { + let exp = get_test_experiments()[0].clone(); + let existing_enrollment = ExperimentEnrollment { + slug: exp.slug, + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: None, + }, + }; + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_previous_state = PreviousGeckoPrefState { + original_value: OriginalGeckoPref::from(&pref_state_1), + feature_id: "feature-id".to_string(), + variable: "variable".to_string(), + }; + + let original_previous_states = vec![original_previous_state]; + let enrollment = existing_enrollment.on_add_gecko_pref_states(original_previous_states.clone()); + + let handler = TestGeckoPrefHandler::new(create_feature_prop_pref_map(vec![( + "feature-id", + "test_prop", + pref_state_1.clone(), + )])); + let handler: Arc> = Arc::new(Box::new(handler)); + let store = Arc::new(GeckoPrefStore::new(handler.clone())); + let _ = store.initialize(); + let gecko_pref_store = Some(store); + + if let EnrollmentStatus::Enrolled { + prev_gecko_pref_states: Some(prev_gecko_pref_states), + .. + } = &enrollment.status + { + PreviousGeckoPrefState::on_revert_to_prev_gecko_pref_states( + prev_gecko_pref_states, + &gecko_pref_store, + ); + let test_handler = unsafe { + std::mem::transmute::>, Arc>>( + handler, + ) + }; + let test_handler_state = test_handler + .state + .lock() + .expect("Unable to lock transmuted handler state"); + + let original_prefs_stored = test_handler_state.original_prefs_state.clone().unwrap(); + + assert_eq!(1, original_prefs_stored.len()); + assert_eq!( + original_previous_states.clone()[0].original_value, + original_prefs_stored[0] + ); + } +} + +#[cfg(feature = "stateful")] +#[test] +fn test_will_pref_experiment_change_no_change() { + let original_experiment = get_test_experiments()[0].clone(); + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_previous_state = PreviousGeckoPrefState { + original_value: OriginalGeckoPref::from(&pref_state_1), + feature_id: "some_control".to_string(), + variable: "text".to_string(), + }; + let original_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: Some(vec![original_previous_state]), + }, + }; + + let updated_experiment = get_test_experiments()[0].clone(); + let updated_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: None, + }, + }; + + assert!( + !original_enrollment.will_pref_experiment_change(&updated_experiment, &updated_enrollment) + ); +} + +#[cfg(feature = "stateful")] +#[test] +fn test_will_pref_experiment_change_branch_changed() { + let original_experiment = get_test_experiments()[0].clone(); + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_previous_state = PreviousGeckoPrefState { + original_value: OriginalGeckoPref::from(&pref_state_1), + feature_id: "some_control".to_string(), + variable: "text".to_string(), + }; + let original_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: Some(vec![original_previous_state]), + }, + }; + + let updated_experiment = get_test_experiments()[0].clone(); + let updated_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "changed_branch".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: None, + }, + }; + + assert!( + original_enrollment.will_pref_experiment_change(&updated_experiment, &updated_enrollment) + ); +} + +#[cfg(feature = "stateful")] +#[test] +fn test_will_pref_experiment_change_feature_id_changed() { + let original_experiment = get_test_experiments()[0].clone(); + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_previous_state = PreviousGeckoPrefState { + original_value: OriginalGeckoPref::from(&pref_state_1), + feature_id: "changed_feature_id".to_string(), + variable: "variable".to_string(), + }; + let original_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: Some(vec![original_previous_state]), + }, + }; + + let updated_experiment = get_test_experiments()[0].clone(); + let updated_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: None, + }, + }; + + assert!( + original_enrollment.will_pref_experiment_change(&updated_experiment, &updated_enrollment) + ); +} +#[cfg(feature = "stateful")] +#[test] +fn test_will_pref_experiment_change_variable_changed() { + let original_experiment = get_test_experiments()[0].clone(); + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_previous_state = PreviousGeckoPrefState { + original_value: OriginalGeckoPref::from(&pref_state_1), + feature_id: "some_control".to_string(), + variable: "changed_variable".to_string(), + }; + let original_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: Some(vec![original_previous_state]), + }, + }; + + let updated_experiment = get_test_experiments()[0].clone(); + let updated_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: None, + }, + }; + + assert!( + original_enrollment.will_pref_experiment_change(&updated_experiment, &updated_enrollment) + ); +} + +#[cfg(feature = "stateful")] +#[test] +fn test_will_pref_experiment_change_not_enrolled() { + let original_experiment = get_test_experiments()[0].clone(); + let pref_state_1 = GeckoPrefState::new("test.some.pref.1", Some(PrefBranch::Default)) + .with_gecko_value(serde_json::Value::String(String::from( + "some-gecko-value-1", + ))); + let original_previous_state = PreviousGeckoPrefState { + original_value: OriginalGeckoPref::from(&pref_state_1), + feature_id: "some_control".to_string(), + variable: "text".to_string(), + }; + let original_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Enrolled { + branch: "control".to_owned(), + reason: EnrolledReason::Qualified, + #[cfg(feature = "stateful")] + prev_gecko_pref_states: Some(vec![original_previous_state]), + }, + }; + + let updated_experiment = get_test_experiments()[0].clone(); + let updated_enrollment = ExperimentEnrollment { + slug: original_experiment.slug.clone(), + status: EnrollmentStatus::Error { + reason: "Something went wrong".to_string(), + }, + }; + + assert!( + original_enrollment.will_pref_experiment_change(&updated_experiment, &updated_enrollment) + ); +}