Skip to content

Commit 3661a4e

Browse files
Bug 2026043 - Require Option<T> for gecko-pref variables and require null defaults by default (#7296)
In bug 2020683 we made the default field mutually exclusive with gecko-pref. However, this lead to another error, namely that gecko-pref was only valid for string, integer, and boolean types and `null` is not a valid value for these types. This issue lead to a backout in bug 2025587. This patch re-adds the requirement of null defaults for gecko-pref variables but also adds the requirement that these variables must be `Option<T>`, where `T` is a string, boolean, or integer. These constraints *can* be overridden by supplying the `--lax-gecko-pref-validation` flag to the `fml single-file`, `fml generate-experimenter`, and `fml validate` commands. This will allow Experimenter to continue to ingest manifests without error. Once the invalid manifest is ingested into Experimenter, we can manually edit the manifest to remove the invalid feature and eventually disable this validation mode.
1 parent fe7d28f commit 3661a4e

12 files changed

Lines changed: 226 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# v151.0 (In progress)
22

33
## ⚠️ Breaking Changes ⚠️
4-
- `nimbus-fml validate` no longer enforces that variables do not use both `gecko-pref` and `default` due to causing CI failures in Experimenter.
4+
- It is now enforced by `nimbus-fml` that feature variables using gecko-pref must be have `type: Option<T>`, for `T` in `Boolean`, `Int`, and `String`.
5+
- `nimbus-fml` commands now all have a `--lax-gecko-pref-validation` flag to bypass the above restriction, as well as the restriction that `gecko-pref` and `default` are mutually exclusive.
6+
- The `FmlLoaderConfig` now has a `lax_gecko_pref_validation` field to allow `FmlClient` consumers (i.e., Experimenter) to opt-in to lax validation.
57

68
[Full Changelog](In progress)
79

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
about:
3+
description: This file codes for all features in the Fenix browser
4+
kotlin:
5+
class: .nimbus.FxNimbus
6+
package: com.example.app
7+
swift:
8+
class: MyNimbus
9+
module: Client
10+
channels:
11+
- release
12+
- beta
13+
- nightly
14+
- developer
15+
features:
16+
gecko-nimbus-validation:
17+
description: "Feature for validating Nimbus Gecko functionality"
18+
variables:
19+
test-preference-bool:
20+
description: "A test gecko preference"
21+
type: Option<Boolean>
22+
gecko-pref:
23+
pref: "gecko.nimbus.test.bool"
24+
branch: "default"
25+
test-preference-int:
26+
description: "A test gecko preference"
27+
type: Option<Int>
28+
gecko-pref:
29+
pref: "gecko.nimbus.test.int"
30+
branch: "default"
31+
test-preference-string:
32+
description: "A test gecko preference"
33+
type: Option<String>
34+
gecko-pref:
35+
pref: "gecko.nimbus.test.string"
36+
branch: "default"

components/support/nimbus-fml/src/client/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct FmlLoaderConfig {
1111
pub cache: Option<String>,
1212
pub refs: HashMap<String, String>,
1313
pub ref_files: Vec<String>,
14+
pub lax_gecko_pref_validation: bool,
1415
}
1516

1617
impl From<FmlLoaderConfig> for LoaderConfig {
@@ -22,6 +23,7 @@ impl From<FmlLoaderConfig> for LoaderConfig {
2223
refs: value.refs.into_iter().collect(),
2324
repo_files: value.ref_files,
2425
cache_dir: cache,
26+
lax_gecko_pref_validation: value.lax_gecko_pref_validation,
2527
}
2628
}
2729
}

components/support/nimbus-fml/src/client/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ impl FmlClient {
7979
let path = files.file_path(&manifest_path)?;
8080
let parser: Parser = Parser::new(files, path)?;
8181
let ir = parser.get_intermediate_representation(Some(&channel))?;
82-
ir.validate_manifest()?;
82+
ir.validate_manifest_with(config.lax_gecko_pref_validation)?;
8383

8484
Ok(FmlClient {
8585
default_json: get_default_json_for_manifest(&ir)?,

components/support/nimbus-fml/src/command_line/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ pub struct LoaderInfo {
174174
/// If INPUT is a remote file, then use this as the tag or branch name.
175175
#[arg(long = "ref")]
176176
pub ref_: Option<String>,
177+
178+
#[arg(long)]
179+
/// Enable lax gecko-pref validation.
180+
pub lax_gecko_pref_validation: bool,
177181
}
178182

179183
impl From<Language> for TargetLanguage {

components/support/nimbus-fml/src/command_line/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ fn create_loader(
166166
repo_files,
167167
cwd,
168168
refs,
169+
lax_gecko_pref_validation: loader_info.lax_gecko_pref_validation,
169170
})
170171
}
171172

components/support/nimbus-fml/src/command_line/workflows.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ fn generate_struct_single(
8787
manifest_path,
8888
cmd.load_from_ir,
8989
Some(&cmd.channel),
90+
false,
9091
)?;
9192
generate_struct_from_ir(&ir, cmd)
9293
}
@@ -112,15 +113,27 @@ fn generate_struct_from_ir(ir: &FeatureManifest, cmd: &GenerateStructCmd) -> Res
112113
pub(crate) fn generate_experimenter_manifest(cmd: &GenerateExperimenterManifestCmd) -> Result<()> {
113114
let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
114115
let path = files.file_path(&cmd.manifest)?;
115-
let ir = load_feature_manifest(files, path, cmd.load_from_ir, None)?;
116+
let ir = load_feature_manifest(
117+
files,
118+
path,
119+
cmd.load_from_ir,
120+
None,
121+
cmd.loader.lax_gecko_pref_validation,
122+
)?;
116123
backends::experimenter_manifest::generate_manifest(ir, cmd)?;
117124
Ok(())
118125
}
119126

120127
pub(crate) fn generate_single_file_manifest(cmd: &GenerateSingleFileManifestCmd) -> Result<()> {
121128
let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
122129
let path = files.file_path(&cmd.manifest)?;
123-
let fm = load_feature_manifest(files, path, false, Some(&cmd.channel))?;
130+
let fm = load_feature_manifest(
131+
files,
132+
path,
133+
false,
134+
Some(&cmd.channel),
135+
cmd.loader.lax_gecko_pref_validation,
136+
)?;
124137
let frontend: ManifestFrontEnd = fm.into();
125138
std::fs::write(&cmd.output, serde_yaml::to_string(&frontend)?)?;
126139
Ok(())
@@ -131,14 +144,15 @@ fn load_feature_manifest(
131144
path: FilePath,
132145
load_from_ir: bool,
133146
channel: Option<&str>,
147+
lax_gecko_pref_validation: bool,
134148
) -> Result<FeatureManifest> {
135149
let ir = if !load_from_ir {
136150
let parser: Parser = Parser::new(files, path)?;
137151
parser.get_intermediate_representation(channel)?
138152
} else {
139153
files.read_ir::<FeatureManifest>(&path)?
140154
};
141-
ir.validate_manifest()?;
155+
ir.validate_manifest_with(lax_gecko_pref_validation)?;
142156
Ok(ir)
143157
}
144158

@@ -302,7 +316,10 @@ pub(crate) fn validate(cmd: &ValidateCmd) -> Result<()> {
302316
.map(|c| {
303317
let intermediate_representation = parser.get_intermediate_representation(Some(c));
304318
match intermediate_representation {
305-
Ok(ir) => (c, ir.validate_manifest()),
319+
Ok(ir) => (
320+
c,
321+
ir.validate_manifest_with(cmd.loader.lax_gecko_pref_validation),
322+
),
306323
Err(e) => (c, Err(e)),
307324
}
308325
})
@@ -352,7 +369,7 @@ pub(crate) fn print_channels(cmd: &PrintChannelsCmd) -> Result<()> {
352369
pub(crate) fn print_info(cmd: &PrintInfoCmd) -> Result<()> {
353370
let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
354371
let path = files.file_path(&cmd.manifest)?;
355-
let fm = load_feature_manifest(files, path.clone(), false, cmd.channel.as_deref())?;
372+
let fm = load_feature_manifest(files, path.clone(), false, cmd.channel.as_deref(), false)?;
356373
let info = if let Some(feature_id) = &cmd.feature {
357374
ManifestInfo::from_feature(&path, &fm, feature_id)?
358375
} else {
@@ -384,6 +401,7 @@ mod test {
384401
"fixtures/ir/full_homescreen.ir.json",
385402
"fixtures/fe/importing/simple/app.yaml",
386403
"fixtures/fe/importing/diamond/00-app.yaml",
404+
"fixtures/fe/gecko-pref.yaml",
387405
];
388406

389407
#[allow(dead_code)]
@@ -407,7 +425,8 @@ mod test {
407425
fn generate_struct_cli_overrides(from_cli: AboutBlock, cmd: &GenerateStructCmd) -> Result<()> {
408426
let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
409427
let path = files.file_path(&cmd.manifest)?;
410-
let mut ir = load_feature_manifest(files, path, cmd.load_from_ir, Some(&cmd.channel))?;
428+
let mut ir =
429+
load_feature_manifest(files, path, cmd.load_from_ir, Some(&cmd.channel), false)?;
411430

412431
// We do a dance here to make sure that we can override class names and package names during tests,
413432
// and while we still have to support setting those options from the command line.
@@ -611,7 +630,7 @@ mod test {
611630
// Load the source file, and get the default_json()
612631
let files: FileLoader = TryFrom::try_from(&loader)?;
613632
let src = files.file_path(&manifest)?;
614-
let fm = load_feature_manifest(files, src, false, Some(channel))?;
633+
let fm = load_feature_manifest(files, src, false, Some(channel), false)?;
615634
let expected = fm.default_json();
616635

617636
let output = NamedTempFile::new()?;
@@ -628,7 +647,7 @@ mod test {
628647
// Reload the generated file, and get the default_json()
629648
let dest = FilePath::Local(output.path().into());
630649
let files: FileLoader = TryFrom::try_from(&loader)?;
631-
let fm = load_feature_manifest(files, dest, false, Some(channel))?;
650+
let fm = load_feature_manifest(files, dest, false, Some(channel), false)?;
632651
let observed = fm.default_json();
633652

634653
// They should be the same.

components/support/nimbus-fml/src/defaults/validator.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use std::collections::{BTreeMap, HashMap, HashSet};
1515
pub(crate) struct DefaultsValidator<'a> {
1616
enum_defs: &'a BTreeMap<String, EnumDef>,
1717
object_defs: &'a BTreeMap<String, ObjectDef>,
18+
19+
lax_gecko_pref_validation: bool,
1820
}
1921

2022
impl<'a> DefaultsValidator<'a> {
@@ -25,9 +27,15 @@ impl<'a> DefaultsValidator<'a> {
2527
Self {
2628
enum_defs,
2729
object_defs,
30+
lax_gecko_pref_validation: false,
2831
}
2932
}
3033

34+
pub(crate) fn with_lax_gecko_pref_validation(mut self, value: bool) -> Self {
35+
self.lax_gecko_pref_validation = value;
36+
self
37+
}
38+
3139
pub(crate) fn validate_object_def(&self, object_def: &ObjectDef) -> Result<(), FMLError> {
3240
let mut errors = Default::default();
3341
let path = ErrorPath::object(&object_def.name);
@@ -67,6 +75,10 @@ impl<'a> DefaultsValidator<'a> {
6775
// This is only checking if a Map with an Enum as key has a complete set of keys (i.e. all variants)
6876
self.validate_feature_enum_maps(feature_def)?;
6977

78+
if !self.lax_gecko_pref_validation {
79+
self.validate_no_defaults_for_gecko_prefs(feature_def)?;
80+
}
81+
7082
// Now check the examples for this feature.
7183
let path = ErrorPath::feature(&feature_def.name);
7284
for ex in &feature_def.examples {
@@ -154,6 +166,20 @@ impl<'a> DefaultsValidator<'a> {
154166
Ok(())
155167
}
156168

169+
fn validate_no_defaults_for_gecko_prefs(&self, feature_def: &FeatureDef) -> Result<()> {
170+
let path = ErrorPath::feature(&feature_def.name);
171+
for prop in &feature_def.props {
172+
if prop.gecko_pref.is_some() && !prop.default.is_null() {
173+
let path = path.property(&prop.name);
174+
return Err(FMLError::ValidationError(
175+
path.path,
176+
"gecko-pref and default are mutually exclusive".into(),
177+
));
178+
}
179+
}
180+
Ok(())
181+
}
182+
157183
/// Check enum maps (Map<Enum, T>) have all keys represented.
158184
///
159185
/// We split this out because if the FML has all keys, then any feature configs do as well.
@@ -1302,3 +1328,40 @@ mod string_alias {
13021328
Ok(())
13031329
}
13041330
}
1331+
1332+
#[cfg(test)]
1333+
mod test_gecko_prefs {
1334+
use serde_json::json;
1335+
1336+
use crate::error::FMLError;
1337+
use crate::intermediate_representation::{GeckoPrefDef, PrefBranch};
1338+
1339+
use super::*;
1340+
1341+
#[test]
1342+
fn test_validate_default_mutually_exclusive_with_gecko_pref() {
1343+
let mut prop = PropDef::new("var", &TypeRef::String, &json!("default-value"));
1344+
prop.gecko_pref = Some(GeckoPrefDef {
1345+
pref: "some.pref".into(),
1346+
branch: PrefBranch::User,
1347+
});
1348+
1349+
let feature = FeatureDef {
1350+
name: "Feature".to_string(),
1351+
props: vec![prop],
1352+
..Default::default()
1353+
};
1354+
1355+
let objs = Default::default();
1356+
let enums = Default::default();
1357+
let validator = DefaultsValidator::new(&enums, &objs);
1358+
1359+
assert!(matches!(
1360+
validator.validate_feature_def(&feature),
1361+
Err(FMLError::ValidationError(path, reason)) if path == "features/Feature.var" && reason == "gecko-pref and default are mutually exclusive",
1362+
));
1363+
1364+
let validator = DefaultsValidator::new(&enums, &objs).with_lax_gecko_pref_validation(true);
1365+
assert!(matches!(validator.validate_feature_def(&feature), Ok(())));
1366+
}
1367+
}

components/support/nimbus-fml/src/fml.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dictionary FmlLoaderConfig {
2323
string? cache;
2424
record<DOMString, string> refs;
2525
sequence<string> ref_files;
26+
boolean lax_gecko_pref_validation;
2627
};
2728

2829
interface FmlClient {

components/support/nimbus-fml/src/intermediate_representation.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ impl TypeRef {
129129
}
130130
}
131131

132+
pub(crate) fn supports_gecko_prefs(&self, lax_pref_validation: bool) -> bool {
133+
match self {
134+
Self::Option(boxed) => matches!(**boxed, Self::Boolean | Self::Int | Self::String),
135+
Self::Boolean | Self::Int | Self::String => lax_pref_validation,
136+
_ => false,
137+
}
138+
}
139+
132140
pub(crate) fn name(&self) -> Option<&str> {
133141
match self {
134142
Self::Enum(s) | Self::Object(s) | Self::StringAlias(s) => Some(s),
@@ -303,22 +311,33 @@ impl FeatureManifest {
303311
Ok(())
304312
}
305313

314+
#[allow(dead_code)]
306315
pub fn validate_manifest(&self) -> Result<()> {
316+
self.validate_manifest_with(false)
317+
}
318+
319+
pub fn validate_manifest_with(&self, lax_gecko_pref_validation: bool) -> Result<()> {
307320
// We then validate that each type_ref is valid
308-
self.validate_schema()?;
309-
self.validate_defaults()?;
321+
self.validate_schema_with(lax_gecko_pref_validation)?;
322+
self.validate_defaults_with(lax_gecko_pref_validation)?;
310323

311324
// Validating the imported manifests.
312325
// This is not only validating the well formed-ness of the imported manifests
313326
// but also the defaults that are sent into the child manifests.
314327
for child in self.all_imports.values() {
315-
child.validate_manifest()?;
328+
child.validate_manifest_with(lax_gecko_pref_validation)?;
316329
}
317330
Ok(())
318331
}
319332

333+
#[allow(dead_code)]
320334
fn validate_schema(&self) -> Result<(), FMLError> {
321-
let validator = SchemaValidator::new(&self.enum_defs, &self.obj_defs);
335+
self.validate_schema_with(false)
336+
}
337+
338+
fn validate_schema_with(&self, lax_gecko_pref_validation: bool) -> Result<(), FMLError> {
339+
let validator = SchemaValidator::new(&self.enum_defs, &self.obj_defs)
340+
.with_lax_gecko_pref_validation(lax_gecko_pref_validation);
322341
for object in self.iter_object_defs() {
323342
validator.validate_object_def(object)?;
324343
}
@@ -329,8 +348,14 @@ impl FeatureManifest {
329348
Ok(())
330349
}
331350

351+
#[allow(dead_code)]
332352
fn validate_defaults(&self) -> Result<()> {
333-
let validator = DefaultsValidator::new(&self.enum_defs, &self.obj_defs);
353+
self.validate_defaults_with(false)
354+
}
355+
356+
fn validate_defaults_with(&self, lax_gecko_pref_validation: bool) -> Result<()> {
357+
let validator = DefaultsValidator::new(&self.enum_defs, &self.obj_defs)
358+
.with_lax_gecko_pref_validation(lax_gecko_pref_validation);
334359
for object in self.iter_object_defs() {
335360
validator.validate_object_def(object)?;
336361
}
@@ -762,7 +787,7 @@ impl PropDef {
762787
self.pref_key.is_some() && self.typ.supports_prefs()
763788
}
764789
pub fn has_gecko_prefs(&self) -> bool {
765-
self.gecko_pref.is_some() && self.typ.supports_prefs()
790+
self.gecko_pref.is_some() && self.typ.supports_gecko_prefs(false)
766791
}
767792
pub fn pref_key(&self) -> Option<String> {
768793
self.pref_key.clone()

0 commit comments

Comments
 (0)