Skip to content

Commit b331361

Browse files
Bug 2026043 - Require Option<T> for gecko-pref variables and require null defaults by default
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 a24a87a commit b331361

File tree

9 files changed

+234
-18
lines changed

9 files changed

+234
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
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+
- The `nimbus-fml validate`, `nimbus-fml single-file`, and `nimbus-fml generate-experimenter` 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.
56

67
[Full Changelog](In progress)
78

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/command_line/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ pub struct GenerateExperimenter {
7979

8080
#[command(flatten)]
8181
pub loader_info: LoaderInfo,
82+
83+
#[arg(long)]
84+
/// Enable lax gecko-pref validation.
85+
pub lax_gecko_pref_validation: bool,
8286
}
8387

8488
#[derive(Args)]
@@ -107,6 +111,10 @@ pub struct SingleFile {
107111

108112
#[command(flatten)]
109113
pub loader_info: LoaderInfo,
114+
115+
#[arg(long)]
116+
/// Enable lax gecko-pref validation.
117+
pub lax_gecko_pref_validation: bool,
110118
}
111119

112120
#[derive(Args)]
@@ -117,6 +125,10 @@ pub struct Validate {
117125

118126
#[command(flatten)]
119127
pub loader_info: LoaderInfo,
128+
129+
#[arg(long)]
130+
/// Enable lax gecko-pref validation.
131+
pub lax_gecko_pref_validation: bool,
120132
}
121133

122134
#[derive(Args)]

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,21 @@ pub(crate) struct GenerateExperimenterManifestCmd {
3434
pub(crate) language: TargetLanguage,
3535
pub(crate) load_from_ir: bool,
3636
pub(crate) loader: LoaderConfig,
37+
pub(crate) lax_gecko_pref_validation: bool,
3738
}
3839

3940
pub(crate) struct GenerateSingleFileManifestCmd {
4041
pub(crate) manifest: String,
4142
pub(crate) output: PathBuf,
4243
pub(crate) channel: String,
4344
pub(crate) loader: LoaderConfig,
45+
pub(crate) lax_gecko_pref_validation: bool,
4446
}
4547

4648
pub(crate) struct ValidateCmd {
4749
pub(crate) manifest: String,
4850
pub(crate) loader: LoaderConfig,
51+
pub(crate) lax_gecko_pref_validation: bool,
4952
}
5053

5154
pub(crate) struct PrintChannelsCmd {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ fn create_single_file_from_cli(
9191
output,
9292
channel,
9393
loader,
94+
lax_gecko_pref_validation: cmd.lax_gecko_pref_validation,
9495
})
9596
}
9697

@@ -110,6 +111,7 @@ fn create_generate_command_experimenter_from_cli(
110111
language,
111112
load_from_ir,
112113
loader,
114+
lax_gecko_pref_validation: cmd.lax_gecko_pref_validation,
113115
};
114116
Ok(cmd)
115117
}
@@ -172,7 +174,11 @@ fn create_loader(
172174
fn create_validate_command_from_cli(cmd: &cli::Validate, cwd: &Path) -> Result<ValidateCmd> {
173175
let manifest = cmd.input.clone();
174176
let loader = create_loader(&cmd.input, &cmd.loader_info, cwd)?;
175-
Ok(ValidateCmd { manifest, loader })
177+
Ok(ValidateCmd {
178+
manifest,
179+
loader,
180+
lax_gecko_pref_validation: cmd.lax_gecko_pref_validation,
181+
})
176182
}
177183

178184
fn create_print_channels_from_cli(cmd: &cli::Channels, cwd: &Path) -> Result<PrintChannelsCmd> {

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

Lines changed: 28 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.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.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,7 @@ 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) => (c, ir.validate_manifest_with(cmd.lax_gecko_pref_validation)),
306320
Err(e) => (c, Err(e)),
307321
}
308322
})
@@ -352,7 +366,7 @@ pub(crate) fn print_channels(cmd: &PrintChannelsCmd) -> Result<()> {
352366
pub(crate) fn print_info(cmd: &PrintInfoCmd) -> Result<()> {
353367
let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
354368
let path = files.file_path(&cmd.manifest)?;
355-
let fm = load_feature_manifest(files, path.clone(), false, cmd.channel.as_deref())?;
369+
let fm = load_feature_manifest(files, path.clone(), false, cmd.channel.as_deref(), false)?;
356370
let info = if let Some(feature_id) = &cmd.feature {
357371
ManifestInfo::from_feature(&path, &fm, feature_id)?
358372
} else {
@@ -384,6 +398,7 @@ mod test {
384398
"fixtures/ir/full_homescreen.ir.json",
385399
"fixtures/fe/importing/simple/app.yaml",
386400
"fixtures/fe/importing/diamond/00-app.yaml",
401+
"fixtures/fe/gecko-pref.yaml",
387402
];
388403

389404
#[allow(dead_code)]
@@ -407,7 +422,8 @@ mod test {
407422
fn generate_struct_cli_overrides(from_cli: AboutBlock, cmd: &GenerateStructCmd) -> Result<()> {
408423
let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
409424
let path = files.file_path(&cmd.manifest)?;
410-
let mut ir = load_feature_manifest(files, path, cmd.load_from_ir, Some(&cmd.channel))?;
425+
let mut ir =
426+
load_feature_manifest(files, path, cmd.load_from_ir, Some(&cmd.channel), false)?;
411427

412428
// We do a dance here to make sure that we can override class names and package names during tests,
413429
// and while we still have to support setting those options from the command line.
@@ -560,6 +576,7 @@ mod test {
560576
let cmd = ValidateCmd {
561577
loader: Default::default(),
562578
manifest,
579+
lax_gecko_pref_validation: false,
563580
};
564581
validate(&cmd)?;
565582
}
@@ -573,6 +590,7 @@ mod test {
573590
let cmd = ValidateCmd {
574591
loader: Default::default(),
575592
manifest,
593+
lax_gecko_pref_validation: false,
576594
};
577595
let result = validate(&cmd);
578596

@@ -601,6 +619,7 @@ mod test {
601619
language: TargetLanguage::ExperimenterYAML,
602620
load_from_ir,
603621
loader,
622+
lax_gecko_pref_validation: false,
604623
})
605624
}
606625

@@ -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()?;
@@ -622,13 +641,14 @@ mod test {
622641
manifest,
623642
output: output.path().into(),
624643
channel: channel.to_string(),
644+
lax_gecko_pref_validation: false,
625645
};
626646
generate_single_file_manifest(&cmd)?;
627647

628648
// Reload the generated file, and get the default_json()
629649
let dest = FilePath::Local(output.path().into());
630650
let files: FileLoader = TryFrom::try_from(&loader)?;
631-
let fm = load_feature_manifest(files, dest, false, Some(channel))?;
651+
let fm = load_feature_manifest(files, dest, false, Some(channel), false)?;
632652
let observed = fm.default_json();
633653

634654
// 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+
}

0 commit comments

Comments
 (0)