nimbus_fml/command_line/
workflows.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use glob::MatchOptions;
6use std::collections::HashSet;
7
8use super::commands::{
9    GenerateExperimenterManifestCmd, GenerateSingleFileManifestCmd, GenerateStructCmd,
10    PrintChannelsCmd, PrintInfoCmd, ValidateCmd,
11};
12use crate::backends::info::ManifestInfo;
13use crate::error::FMLError::CliError;
14use crate::frontend::ManifestFrontEnd;
15use crate::{
16    backends,
17    error::{FMLError, Result},
18    intermediate_representation::{FeatureManifest, TargetLanguage},
19    parser::Parser,
20    util::loaders::{FileLoader, FilePath, LoaderConfig},
21};
22use std::io::Write;
23use std::path::Path;
24use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
25
26/// Use this when recursively looking for files.
27const MATCHING_FML_EXTENSION: &str = ".fml.yaml";
28
29pub(crate) fn generate_struct(cmd: &GenerateStructCmd) -> Result<()> {
30    let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
31
32    let filename = &cmd.manifest;
33    let input = files.file_path(filename)?;
34
35    match (&input, &cmd.output.is_dir()) {
36        (FilePath::Remote(_), _) => generate_struct_single(&files, input, cmd),
37        (FilePath::Local(file), _) if file.is_file() => generate_struct_single(&files, input, cmd),
38        (FilePath::Local(dir), true) if dir.is_dir() => generate_struct_from_dir(&files, cmd, dir),
39        (_, true) => generate_struct_from_glob(&files, cmd, filename),
40        _ => Err(FMLError::CliError(
41            "Cannot generate a single output file from an input directory".to_string(),
42        )),
43    }
44}
45
46fn generate_struct_from_dir(files: &FileLoader, cmd: &GenerateStructCmd, cwd: &Path) -> Result<()> {
47    let entries = cwd.read_dir()?;
48    for entry in entries.filter_map(Result::ok) {
49        let pb = entry.path();
50        if pb.is_dir() {
51            generate_struct_from_dir(files, cmd, &pb)?;
52        } else if let Some(nm) = pb.file_name().map(|s| s.to_str().unwrap_or_default()) {
53            if nm.ends_with(MATCHING_FML_EXTENSION) {
54                let path = pb.as_path().into();
55                generate_struct_single(files, path, cmd)?;
56            }
57        }
58    }
59    Ok(())
60}
61
62fn generate_struct_from_glob(
63    files: &FileLoader,
64    cmd: &GenerateStructCmd,
65    pattern: &str,
66) -> Result<()> {
67    use glob::glob_with;
68    let entries = glob_with(pattern, MatchOptions::new()).unwrap();
69    for entry in entries.filter_map(Result::ok) {
70        let path = entry.as_path().into();
71        generate_struct_single(files, path, cmd)?;
72    }
73    Ok(())
74}
75
76fn generate_struct_single(
77    files: &FileLoader,
78    manifest_path: FilePath,
79    cmd: &GenerateStructCmd,
80) -> Result<()> {
81    let ir = load_feature_manifest(
82        files.clone(),
83        manifest_path,
84        cmd.load_from_ir,
85        Some(&cmd.channel),
86    )?;
87    generate_struct_from_ir(&ir, cmd)
88}
89
90fn generate_struct_from_ir(ir: &FeatureManifest, cmd: &GenerateStructCmd) -> Result<()> {
91    let language = &cmd.language;
92    ir.validate_manifest_for_lang(language)?;
93    match language {
94        TargetLanguage::IR => {
95            let contents = serde_json::to_string_pretty(&ir)?;
96            std::fs::write(&cmd.output, contents)?;
97        }
98        TargetLanguage::Kotlin => backends::kotlin::generate_struct(ir, cmd)?,
99        TargetLanguage::Swift => backends::swift::generate_struct(ir, cmd)?,
100        _ => unimplemented!(
101            "Unsupported output language for structs: {}",
102            language.extension()
103        ),
104    };
105    Ok(())
106}
107
108pub(crate) fn generate_experimenter_manifest(cmd: &GenerateExperimenterManifestCmd) -> Result<()> {
109    let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
110    let path = files.file_path(&cmd.manifest)?;
111    let ir = load_feature_manifest(files, path, cmd.load_from_ir, None)?;
112    backends::experimenter_manifest::generate_manifest(ir, cmd)?;
113    Ok(())
114}
115
116pub(crate) fn generate_single_file_manifest(cmd: &GenerateSingleFileManifestCmd) -> Result<()> {
117    let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
118    let path = files.file_path(&cmd.manifest)?;
119    let fm = load_feature_manifest(files, path, false, Some(&cmd.channel))?;
120    let frontend: ManifestFrontEnd = fm.into();
121    std::fs::write(&cmd.output, serde_yaml::to_string(&frontend)?)?;
122    Ok(())
123}
124
125fn load_feature_manifest(
126    files: FileLoader,
127    path: FilePath,
128    load_from_ir: bool,
129    channel: Option<&str>,
130) -> Result<FeatureManifest> {
131    let ir = if !load_from_ir {
132        let parser: Parser = Parser::new(files, path)?;
133        parser.get_intermediate_representation(channel)?
134    } else {
135        files.read::<FeatureManifest>(&path)?
136    };
137    ir.validate_manifest()?;
138    Ok(ir)
139}
140
141pub(crate) fn fetch_file(files: &LoaderConfig, nm: &str) -> Result<()> {
142    let files: FileLoader = files.try_into()?;
143    let file = files.file_path(nm)?;
144
145    let string = files.read_to_string(&file)?;
146
147    println!("{}", string);
148    Ok(())
149}
150
151fn output_ok(stream: &mut StandardStream, title: &str) -> Result<()> {
152    write!(stream, "✅ ")?;
153    stream.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
154    writeln!(stream, "{title}")?;
155    stream.reset()?;
156
157    Ok(())
158}
159
160fn output_note(stream: &mut StandardStream, title: &str) -> Result<()> {
161    write!(stream, "ℹ️ ")?;
162    stream.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
163    writeln!(stream, "{title}")?;
164    stream.reset()?;
165
166    Ok(())
167}
168
169fn output_warn(stream: &mut StandardStream, title: &str, detail: &str) -> Result<()> {
170    write!(stream, "⚠️ ")?;
171    stream.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
172    write!(stream, "{title}")?;
173    stream.reset()?;
174    writeln!(stream, ": {detail}")?;
175    Ok(())
176}
177
178fn output_err(stream: &mut StandardStream, title: &str, detail: &str) -> Result<()> {
179    writeln!(stream, "❎ ")?;
180    stream.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
181    writeln!(stream, "{title}")?;
182    stream.reset()?;
183    writeln!(stream, ": {detail}")?;
184
185    Ok(())
186}
187
188pub(crate) fn validate(cmd: &ValidateCmd) -> Result<()> {
189    let mut stdout = StandardStream::stdout(ColorChoice::Auto);
190
191    let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
192
193    let filename = &cmd.manifest;
194    let file_path = files.file_path(filename)?;
195    let parser: Parser = Parser::new(files, file_path.clone())?;
196    let mut loading = HashSet::new();
197    let manifest_front_end = parser.load_manifest(&file_path, &mut loading)?;
198
199    let iter_includes = loading.iter().map(|id| id.to_string());
200
201    let channels = manifest_front_end.channels();
202    if channels.is_empty() {
203        output_note(
204            &mut stdout,
205            &format!(
206                "Loaded modules:\n- {}\n",
207                iter_includes.collect::<Vec<String>>().join("\n- ")
208            ),
209        )?;
210        output_ok(&mut stdout, &format!(
211            "{}\n{}\n{}",
212            "The manifest is valid for including in other files. To be imported, or used as an app manifest, it requires the following:",
213            "- A `channels` list",
214            "- An `about` block",
215        ))?;
216        return Ok(());
217    }
218    let intermediate_representation =
219        parser
220            .get_intermediate_representation(None)
221            .inspect_err(|e| {
222                output_err(&mut stdout, "Manifest is invalid", &e.to_string()).unwrap();
223            })?;
224
225    output_note(
226        &mut stdout,
227        &format!(
228            "Loaded modules:\n- {}\n",
229            iter_includes
230                .chain(
231                    intermediate_representation
232                        .all_imports
233                        .keys()
234                        .map(|m| m.to_string())
235                )
236                .collect::<Vec<String>>()
237                .join("\n- ")
238        ),
239    )?;
240
241    writeln!(stdout, "Validating feature metadata:")?;
242    let mut features_with_warnings = 0;
243    for (_, f) in intermediate_representation.iter_all_feature_defs() {
244        let fm = &f.metadata;
245        let mut missing = vec![];
246        if fm.meta_bug.is_none() {
247            missing.push("'meta-bug'");
248        }
249        if fm.documentation.is_empty() {
250            missing.push("'documentation'");
251        }
252        if fm.contacts.is_empty() {
253            missing.push("'contacts'");
254        }
255        if !missing.is_empty() {
256            output_warn(
257                &mut stdout,
258                &format!("'{}' missing metadata", &f.name),
259                &missing.join(", "),
260            )?;
261            features_with_warnings += 1;
262        }
263    }
264
265    if features_with_warnings == 0 {
266        output_ok(&mut stdout, "All feature metadata ok\n")?;
267    } else {
268        let features = if features_with_warnings == 1 {
269            "feature"
270        } else {
271            "features"
272        };
273        writeln!(
274            &mut stdout,
275            "Each feature should have entries for at least:"
276        )?;
277        writeln!(&mut stdout, "  - meta-bug: a URL where to file bugs")?;
278        writeln!(
279            &mut stdout,
280            "  - documentation: a list of one or more URLs documenting the feature"
281        )?;
282        writeln!(&mut stdout, "      e.g. QA docs, user docs")?;
283        writeln!(
284            &mut stdout,
285            "  - contacts: a list of one or more email addresses"
286        )?;
287        writeln!(&mut stdout, "      (with Mozilla Jira accounts)")?;
288        writeln!(
289            &mut stdout,
290            "Metadata warnings detected in {features_with_warnings} {features}\n"
291        )?;
292    }
293
294    writeln!(&mut stdout, "Validating manifest for different channels:")?;
295
296    let results = channels
297        .iter()
298        .map(|c| {
299            let intermediate_representation = parser.get_intermediate_representation(Some(c));
300            match intermediate_representation {
301                Ok(ir) => (c, ir.validate_manifest()),
302                Err(e) => (c, Err(e)),
303            }
304        })
305        .collect::<Vec<(&String, Result<_>)>>();
306
307    let mut error_count = 0;
308    for (channel, result) in results {
309        match result {
310            Ok(_) => {
311                output_ok(&mut stdout, &format!("{channel:.<20}valid"))?;
312            }
313            Err(e) => {
314                error_count += 1;
315                output_err(
316                    &mut stdout,
317                    &format!("{channel:.<20}invalid"),
318                    &e.to_string(),
319                )?;
320            }
321        };
322    }
323
324    if error_count > 0 {
325        return Err(CliError(format!(
326            "Manifest contains error(s) in {} channel{}",
327            error_count,
328            if error_count > 1 { "s" } else { "" }
329        )));
330    }
331
332    Ok(())
333}
334
335pub(crate) fn print_channels(cmd: &PrintChannelsCmd) -> Result<()> {
336    let files = TryFrom::try_from(&cmd.loader)?;
337    let manifest = Parser::load_frontend(files, &cmd.manifest)?;
338    let channels = manifest.channels();
339    if cmd.as_json {
340        let json = serde_json::Value::from(channels);
341        println!("{}", json);
342    } else {
343        println!("{}", channels.join("\n"));
344    }
345    Ok(())
346}
347
348pub(crate) fn print_info(cmd: &PrintInfoCmd) -> Result<()> {
349    let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
350    let path = files.file_path(&cmd.manifest)?;
351    let fm = load_feature_manifest(files, path.clone(), false, cmd.channel.as_deref())?;
352    let info = if let Some(feature_id) = &cmd.feature {
353        ManifestInfo::from_feature(&path, &fm, feature_id)?
354    } else {
355        ManifestInfo::from(&path, &fm)
356    };
357    if cmd.as_json {
358        println!("{}", info.to_json()?);
359    } else {
360        println!("{}", info.to_yaml()?);
361    }
362    Ok(())
363}
364
365#[cfg(all(
366    test,
367    any(feature = "swift-tests", feature = "kotlin-tests"),
368    not(feature = "all-features-workaround")
369))]
370mod test {
371    use std::fs;
372    use std::path::PathBuf;
373
374    use anyhow::anyhow;
375    use jsonschema::JSONSchema;
376
377    use super::*;
378    use crate::backends::experimenter_manifest::ExperimenterManifest;
379    use crate::backends::{kotlin, swift};
380    use crate::frontend::AboutBlock;
381    use crate::util::{generated_src_dir, join, pkg_dir};
382
383    const MANIFEST_PATHS: &[&str] = &[
384        "fixtures/ir/simple_nimbus_validation.json",
385        "fixtures/ir/simple_nimbus_validation.json",
386        "fixtures/ir/with_objects.json",
387        "fixtures/ir/full_homescreen.json",
388        "fixtures/fe/importing/simple/app.yaml",
389        "fixtures/fe/importing/diamond/00-app.yaml",
390    ];
391
392    pub(crate) fn generate_and_assert(
393        test_script: &str,
394        manifest: &str,
395        channel: &str,
396        is_ir: bool,
397    ) -> Result<()> {
398        let cmd = create_command_from_test(test_script, manifest, channel, is_ir)?;
399        generate_struct(&cmd)?;
400        run_script_with_generated_code(
401            &cmd.language,
402            &[cmd.output.as_path().display().to_string()],
403            test_script,
404        )?;
405        Ok(())
406    }
407
408    fn generate_struct_cli_overrides(from_cli: AboutBlock, cmd: &GenerateStructCmd) -> Result<()> {
409        let files: FileLoader = TryFrom::try_from(&cmd.loader)?;
410        let path = files.file_path(&cmd.manifest)?;
411        let mut ir = load_feature_manifest(files, path, cmd.load_from_ir, Some(&cmd.channel))?;
412
413        // We do a dance here to make sure that we can override class names and package names during tests,
414        // and while we still have to support setting those options from the command line.
415        // We will deprecate setting classnames, package names etc, then we can simplify.
416        let from_file = ir.about;
417        let kotlin_about = from_cli.kotlin_about.or(from_file.kotlin_about);
418        let swift_about = from_cli.swift_about.or(from_file.swift_about);
419        let about = AboutBlock {
420            kotlin_about,
421            swift_about,
422            ..Default::default()
423        };
424        ir.about = about;
425
426        generate_struct_from_ir(&ir, cmd)
427    }
428
429    // Given a manifest.fml and script.kts in the tests directory generate
430    // a manifest.kt and run the script against it.
431    pub(crate) fn generate_and_assert_with_config(
432        test_script: &str,
433        manifest: &str,
434        channel: &str,
435        is_ir: bool,
436        config_about: AboutBlock,
437    ) -> Result<()> {
438        let cmd = create_command_from_test(test_script, manifest, channel, is_ir)?;
439        generate_struct_cli_overrides(config_about, &cmd)?;
440        run_script_with_generated_code(
441            &cmd.language,
442            &[cmd.output.as_path().display().to_string()],
443            test_script,
444        )?;
445        Ok(())
446    }
447
448    pub(crate) fn create_command_from_test(
449        test_script: &str,
450        manifest: &str,
451        channel: &str,
452        is_ir: bool,
453    ) -> Result<GenerateStructCmd, crate::error::FMLError> {
454        let test_script = join(pkg_dir(), test_script);
455        let pbuf = PathBuf::from(&test_script);
456        let ext = pbuf
457            .extension()
458            .ok_or_else(|| anyhow!("Require a test_script with an extension: {}", test_script))?;
459        let language: TargetLanguage = ext.try_into()?;
460        let manifest_fml = join(pkg_dir(), manifest);
461        let file = PathBuf::from(&manifest_fml);
462        let file = file
463            .file_stem()
464            .ok_or_else(|| anyhow!("Manifest file path isn't a file"))?
465            .to_str()
466            .ok_or_else(|| anyhow!("Manifest file path isn't a file with a sensible name"))?;
467        let stem = pbuf.file_stem().unwrap().to_str().unwrap();
468        fs::create_dir_all(join(generated_src_dir(), stem))?;
469        let manifest_out = format!(
470            "{}/{stem}/{file}_{channel}.{}",
471            generated_src_dir(),
472            language.extension()
473        );
474        let loader = Default::default();
475        Ok(GenerateStructCmd {
476            manifest: manifest_fml,
477            output: manifest_out.into(),
478            load_from_ir: is_ir,
479            language,
480            channel: channel.into(),
481            loader,
482        })
483    }
484
485    pub(crate) fn generate_multiple_and_assert(
486        test_script: &str,
487        manifests: &[(&str, &str)],
488    ) -> Result<()> {
489        let cmds = manifests
490            .iter()
491            .map(|(manifest, channel)| {
492                let cmd = create_command_from_test(test_script, manifest, channel, false)?;
493                generate_struct(&cmd)?;
494                Ok(cmd)
495            })
496            .collect::<Result<Vec<_>>>()?;
497
498        let first = cmds
499            .first()
500            .expect("At least one manifests are always used");
501        let language = &first.language;
502
503        let manifests_out = cmds
504            .iter()
505            .map(|cmd| cmd.output.display().to_string())
506            .collect::<Vec<_>>();
507
508        run_script_with_generated_code(language, &manifests_out, test_script)?;
509        Ok(())
510    }
511
512    fn run_script_with_generated_code(
513        language: &TargetLanguage,
514        manifests_out: &[String],
515        test_script: &str,
516    ) -> Result<()> {
517        match language {
518            TargetLanguage::Kotlin => {
519                kotlin::test::run_script_with_generated_code(manifests_out, test_script)?
520            }
521            TargetLanguage::Swift => {
522                swift::test::run_script_with_generated_code(manifests_out, test_script.as_ref())?
523            }
524            _ => unimplemented!(),
525        }
526        Ok(())
527    }
528
529    #[test]
530    fn test_importing_simple_experimenter_manifest() -> Result<()> {
531        // Both the app and lib files declare features, so we should have an experimenter manifest file with two features.
532        let cmd = create_experimenter_manifest_cmd("fixtures/fe/importing/simple/app.yaml")?;
533        let files = FileLoader::default()?;
534        let path = files.file_path(&cmd.manifest)?;
535        let fm = load_feature_manifest(files, path, cmd.load_from_ir, None)?;
536        let m: ExperimenterManifest = fm.try_into()?;
537
538        assert!(m.contains_key("homescreen"));
539        assert!(m.contains_key("search"));
540
541        Ok(())
542    }
543
544    fn validate_against_experimenter_schema<P: AsRef<Path>>(
545        schema_path: P,
546        generated_yaml: &serde_yaml::Value,
547    ) -> Result<()> {
548        let generated_manifest: ExperimenterManifest =
549            serde_yaml::from_value(generated_yaml.to_owned())?;
550        let generated_json = serde_json::to_value(generated_manifest)?;
551
552        let schema = fs::read_to_string(&schema_path)?;
553        let schema: serde_json::Value = serde_json::from_str(&schema)?;
554        let compiled = JSONSchema::compile(&schema).expect("The schema is invalid");
555        let res = compiled.validate(&generated_json);
556        if let Err(e) = res {
557            panic!(
558                "Validation errors: \n{}",
559                e.map(|e| e.to_string()).collect::<Vec<String>>().join("\n")
560            );
561        }
562        Ok(())
563    }
564
565    #[test]
566    fn test_schema_validation() -> Result<()> {
567        for path in MANIFEST_PATHS {
568            let cmd = create_experimenter_manifest_cmd(path)?;
569            generate_experimenter_manifest(&cmd)?;
570
571            let generated = fs::read_to_string(&cmd.output)?;
572            let generated_yaml = serde_yaml::from_str(&generated)?;
573            validate_against_experimenter_schema(
574                join(pkg_dir(), "ExperimentFeatureManifest.schema.json"),
575                &generated_yaml,
576            )?;
577        }
578        Ok(())
579    }
580
581    #[test]
582    fn test_validate_command() -> Result<()> {
583        let paths = MANIFEST_PATHS
584            .iter()
585            .filter(|p| p.ends_with(".yaml"))
586            .chain([&"fixtures/fe/no_about_no_channels.yaml"])
587            .collect::<Vec<&&str>>();
588        for path in paths {
589            let manifest = join(pkg_dir(), path);
590            let cmd = ValidateCmd {
591                loader: Default::default(),
592                manifest,
593            };
594            validate(&cmd)?;
595        }
596        Ok(())
597    }
598
599    #[test]
600    fn test_validate_command_fails_on_bad_default_value_for_one_channel() -> Result<()> {
601        let path = "fixtures/fe/invalid/invalid_default_value_for_one_channel.fml.yaml";
602        let manifest = join(pkg_dir(), path);
603        let cmd = ValidateCmd {
604            loader: Default::default(),
605            manifest,
606        };
607        let result = validate(&cmd);
608
609        assert!(result.is_err());
610
611        match result.err().unwrap() {
612            CliError(error) => {
613                assert_eq!(error, "Manifest contains error(s) in 1 channel");
614            }
615            _ => panic!("Error is not a ValidationError"),
616        };
617
618        Ok(())
619    }
620
621    fn create_experimenter_manifest_cmd(path: &str) -> Result<GenerateExperimenterManifestCmd> {
622        let manifest = join(pkg_dir(), path);
623        let file = Path::new(&manifest);
624        let filestem = file
625            .file_stem()
626            .ok_or_else(|| anyhow!("Manifest file path isn't a file"))?
627            .to_str()
628            .ok_or_else(|| anyhow!("Manifest file path isn't a file with a sensible name"))?;
629
630        fs::create_dir_all(generated_src_dir())?;
631
632        let output = join(generated_src_dir(), &format!("{filestem}.yaml")).into();
633        let load_from_ir = if let Some(ext) = file.extension() {
634            TargetLanguage::ExperimenterJSON == ext.try_into()?
635        } else {
636            false
637        };
638        let loader = Default::default();
639        Ok(GenerateExperimenterManifestCmd {
640            manifest,
641            output,
642            language: TargetLanguage::ExperimenterYAML,
643            load_from_ir,
644            loader,
645        })
646    }
647
648    fn test_single_merged_manifest_file(path: &str, channel: &str) -> Result<()> {
649        let manifest = join(pkg_dir(), path);
650        let file = Path::new(&manifest);
651        let filestem = file
652            .file_stem()
653            .ok_or_else(|| anyhow!("Manifest file path isn't a file"))?
654            .to_str()
655            .ok_or_else(|| anyhow!("Manifest file path isn't a file with a sensible name"))?;
656
657        fs::create_dir_all(generated_src_dir())?;
658
659        let output: PathBuf =
660            join(generated_src_dir(), &format!("single-file-{filestem}.yaml")).into();
661        let loader = Default::default();
662
663        // Load the source file, and get the default_json()
664        let files: FileLoader = TryFrom::try_from(&loader)?;
665        let src = files.file_path(&manifest)?;
666        let fm = load_feature_manifest(files, src, false, Some(channel))?;
667        let expected = fm.default_json();
668
669        // Generate the merged file
670        let cmd = GenerateSingleFileManifestCmd {
671            loader: Default::default(),
672            manifest,
673            output: output.clone(),
674            channel: channel.to_string(),
675        };
676        generate_single_file_manifest(&cmd)?;
677
678        // Reload the generated file, and get the default_json()
679        let dest = FilePath::Local(output);
680        let files: FileLoader = TryFrom::try_from(&loader)?;
681        let fm = load_feature_manifest(files, dest, false, Some(channel))?;
682        let observed = fm.default_json();
683
684        // They should be the same.
685        assert_eq!(expected, observed);
686
687        Ok(())
688    }
689
690    #[test]
691    fn test_single_file_command() -> Result<()> {
692        test_single_merged_manifest_file("fixtures/fe/browser.yaml", "release")?;
693        test_single_merged_manifest_file(
694            "fixtures/fe/importing/including-imports/ui.fml.yaml",
695            "none",
696        )?;
697        test_single_merged_manifest_file(
698            "fixtures/fe/importing/including-imports/app.fml.yaml",
699            "release",
700        )?;
701        test_single_merged_manifest_file("fixtures/fe/importing/overrides/app.fml.yaml", "debug")?;
702        test_single_merged_manifest_file("fixtures/fe/importing/overrides/lib.fml.yaml", "debug")?;
703        test_single_merged_manifest_file("fixtures/fe/importing/diamond/00-app.yaml", "debug")?;
704        test_single_merged_manifest_file("fixtures/fe/importing/diamond/01-lib.yaml", "debug")?;
705        test_single_merged_manifest_file("fixtures/fe/importing/diamond/02-sublib.yaml", "debug")?;
706
707        test_single_merged_manifest_file("fixtures/fe/misc-features.yaml", "debug")?;
708        Ok(())
709    }
710}
711
712#[cfg(all(
713    test,
714    feature = "kotlin-tests",
715    not(feature = "all-features-workaround")
716))]
717mod kts_tests {
718    use crate::frontend::{AboutBlock, KotlinAboutBlock};
719
720    use super::{
721        test::{
722            generate_and_assert, generate_and_assert_with_config, generate_multiple_and_assert,
723        },
724        *,
725    };
726
727    #[test]
728    fn test_simple_validation_code_from_ir() -> Result<()> {
729        generate_and_assert(
730            "test/simple_nimbus_validation.kts",
731            "fixtures/ir/simple_nimbus_validation.json",
732            "release",
733            true,
734        )?;
735        Ok(())
736    }
737
738    #[test]
739    fn test_with_objects_code_from_ir() -> Result<()> {
740        generate_and_assert(
741            "test/with_objects.kts",
742            "fixtures/ir/with_objects.json",
743            "release",
744            true,
745        )?;
746        Ok(())
747    }
748
749    #[test]
750    fn test_with_full_homescreen_from_ir() -> Result<()> {
751        generate_and_assert(
752            "test/full_homescreen.kts",
753            "fixtures/ir/full_homescreen.json",
754            "release",
755            true,
756        )?;
757        Ok(())
758    }
759
760    #[test]
761    fn test_with_full_fenix_release() -> Result<()> {
762        generate_and_assert_with_config(
763            "test/fenix_release.kts",
764            "fixtures/fe/browser.yaml",
765            "release",
766            false,
767            AboutBlock {
768                kotlin_about: Some(KotlinAboutBlock {
769                    package: "com.example.app".to_string(),
770                    class: "com.example.release.FxNimbus".to_string(),
771                }),
772                swift_about: None,
773                ..Default::default()
774            },
775        )?;
776        Ok(())
777    }
778
779    #[test]
780    fn test_with_full_fenix_nightly() -> Result<()> {
781        generate_and_assert_with_config(
782            "test/fenix_nightly.kts",
783            "fixtures/fe/browser.yaml",
784            "nightly",
785            false,
786            AboutBlock {
787                kotlin_about: Some(KotlinAboutBlock {
788                    package: "com.example.app".to_string(),
789                    class: "com.example.nightly.FxNimbus".to_string(),
790                }),
791                swift_about: None,
792                ..Default::default()
793            },
794        )?;
795        Ok(())
796    }
797
798    #[test]
799    fn test_with_full_fenix_nightly_with_prefs() -> Result<()> {
800        generate_and_assert_with_config(
801            "test/fenix_nightly.kts",
802            "fixtures/fe/browser-with-prefs.yaml",
803            "nightly",
804            false,
805            AboutBlock {
806                kotlin_about: Some(KotlinAboutBlock {
807                    package: "com.example.app".to_string(),
808                    class: "com.example.nightly.FxNimbus".to_string(),
809                }),
810                swift_about: None,
811                ..Default::default()
812            },
813        )?;
814        Ok(())
815    }
816
817    #[test]
818    fn test_with_dx_improvements() -> Result<()> {
819        generate_and_assert(
820            "test/dx_improvements_testing.kts",
821            "fixtures/fe/dx_improvements.yaml",
822            "testing",
823            false,
824        )?;
825        Ok(())
826    }
827
828    #[test]
829    fn test_with_app_menu_from_ir() -> Result<()> {
830        generate_and_assert(
831            "test/app_menu.kts",
832            "fixtures/ir/app_menu.json",
833            "release",
834            true,
835        )?;
836        Ok(())
837    }
838
839    #[test]
840    fn test_with_bundled_resources_kts() -> Result<()> {
841        generate_and_assert(
842            "test/bundled_resources.kts",
843            "fixtures/fe/bundled_resouces.yaml",
844            "testing",
845            false,
846        )?;
847        Ok(())
848    }
849
850    #[test]
851    fn test_importing_simple_kts() -> Result<()> {
852        generate_multiple_and_assert(
853            "test/importing/simple/app_debug.kts",
854            &[
855                ("fixtures/fe/importing/simple/lib.yaml", "debug"),
856                ("fixtures/fe/importing/simple/app.yaml", "debug"),
857            ],
858        )?;
859        Ok(())
860    }
861
862    #[test]
863    fn test_importing_channel_mismatching_kts() -> Result<()> {
864        generate_multiple_and_assert(
865            "test/importing/channels/app_debug.kts",
866            &[
867                ("fixtures/fe/importing/channels/app.fml.yaml", "app-debug"),
868                ("fixtures/fe/importing/channels/lib.fml.yaml", "debug"),
869            ],
870        )?;
871        Ok(())
872    }
873
874    #[test]
875    fn test_importing_override_defaults_kts() -> Result<()> {
876        generate_multiple_and_assert(
877            "test/importing/overrides/app_debug.kts",
878            &[
879                ("fixtures/fe/importing/overrides/app.fml.yaml", "debug"),
880                ("fixtures/fe/importing/overrides/lib.fml.yaml", "debug"),
881            ],
882        )?;
883        Ok(())
884    }
885
886    #[test]
887    fn test_importing_override_defaults_coverall_kts() -> Result<()> {
888        generate_multiple_and_assert(
889            "test/importing/overrides-coverall/app_debug.kts",
890            &[
891                (
892                    "fixtures/fe/importing/overrides-coverall/app.fml.yaml",
893                    "debug",
894                ),
895                (
896                    "fixtures/fe/importing/overrides-coverall/lib.fml.yaml",
897                    "debug",
898                ),
899            ],
900        )?;
901        Ok(())
902    }
903
904    #[test]
905    fn test_importing_diamond_overrides_kts() -> Result<()> {
906        // In this test, sublib implements a feature.
907        // Both lib and app offer some configuration, and both app and lib
908        // need to import sublib.
909        generate_multiple_and_assert(
910            "test/importing/diamond/00-app.kts",
911            &[
912                ("fixtures/fe/importing/diamond/00-app.yaml", "debug"),
913                ("fixtures/fe/importing/diamond/01-lib.yaml", "debug"),
914                ("fixtures/fe/importing/diamond/02-sublib.yaml", "debug"),
915            ],
916        )?;
917        Ok(())
918    }
919
920    #[test]
921    #[ignore]
922    fn test_importing_reexporting_features() -> Result<()> {
923        // In this test, sublib implements a feature.
924        // Both lib and app offer some configuration, but app doesn't need to know
925        // that the feature is provided by sublib– where the feature lives
926        // is an implementation detail, and should be encapsulated by lib.
927        // This is currently not possible, but filed as EXP-2540.
928        generate_multiple_and_assert(
929            "test/importing/reexporting/00-app.kts",
930            &[
931                ("fixtures/fe/importing/reexporting/00-app.yaml", "debug"),
932                ("fixtures/fe/importing/reexporting/01-lib.yaml", "debug"),
933                ("fixtures/fe/importing/reexporting/02-sublib.yaml", "debug"),
934            ],
935        )?;
936        Ok(())
937    }
938
939    #[test]
940    fn test_importing_including_imports_kts() -> Result<()> {
941        generate_multiple_and_assert(
942            "test/importing/including-imports/app_release.kts",
943            &[
944                (
945                    "fixtures/fe/importing/including-imports/ui.fml.yaml",
946                    "none",
947                ),
948                (
949                    "fixtures/fe/importing/including-imports/app.fml.yaml",
950                    "release",
951                ),
952            ],
953        )?;
954        Ok(())
955    }
956
957    #[test]
958    fn regression_test_concurrent_access_of_feature_holder_kts() -> Result<()> {
959        generate_and_assert(
960            "test/threadsafe_feature_holder.kts",
961            "fixtures/fe/browser.yaml",
962            "release",
963            false,
964        )?;
965        Ok(())
966    }
967
968    #[test]
969    fn test_with_coenrolled_features_and_imports_kts() -> Result<()> {
970        generate_multiple_and_assert(
971            "test/allow_coenrolling.kts",
972            &[
973                ("fixtures/fe/importing/coenrolling/app.fml.yaml", "release"),
974                ("fixtures/fe/importing/coenrolling/ui.fml.yaml", "release"),
975            ],
976        )?;
977        Ok(())
978    }
979
980    #[test]
981    fn test_with_preference_overrides_kt() -> Result<()> {
982        generate_multiple_and_assert(
983            "test/pref_overrides.kts",
984            &[("fixtures/fe/pref_overrides.fml.yaml", "debug")],
985        )?;
986        Ok(())
987    }
988}
989
990#[cfg(all(
991    test,
992    feature = "swift-tests",
993    not(feature = "all-features-workaround")
994))]
995mod swift_tests {
996    use super::{
997        test::{generate_and_assert, generate_multiple_and_assert},
998        *,
999    };
1000
1001    #[test]
1002    fn test_with_app_menu_swift_from_ir() -> Result<()> {
1003        generate_and_assert(
1004            "test/app_menu.swift",
1005            "fixtures/ir/app_menu.json",
1006            "release",
1007            true,
1008        )?;
1009        Ok(())
1010    }
1011
1012    #[test]
1013    fn test_with_objects_swift_from_ir() -> Result<()> {
1014        generate_and_assert(
1015            "test/with_objects.swift",
1016            "fixtures/ir/with_objects.json",
1017            "release",
1018            true,
1019        )?;
1020        Ok(())
1021    }
1022
1023    #[test]
1024    fn test_with_bundled_resources_swift() -> Result<()> {
1025        generate_and_assert(
1026            "test/bundled_resources.swift",
1027            "fixtures/fe/bundled_resouces.yaml",
1028            "testing",
1029            false,
1030        )?;
1031        Ok(())
1032    }
1033
1034    #[test]
1035    fn test_with_full_fenix_release_swift() -> Result<()> {
1036        generate_and_assert(
1037            "test/fenix_release.swift",
1038            "fixtures/fe/browser.yaml",
1039            "release",
1040            false,
1041        )?;
1042        Ok(())
1043    }
1044
1045    #[test]
1046    fn test_with_full_fenix_nightly_swift() -> Result<()> {
1047        generate_and_assert(
1048            "test/fenix_nightly.swift",
1049            "fixtures/fe/browser.yaml",
1050            "nightly",
1051            false,
1052        )?;
1053        Ok(())
1054    }
1055
1056    #[test]
1057    fn test_with_full_firefox_swift() -> Result<()> {
1058        generate_and_assert(
1059            "test/firefox_ios_release.swift",
1060            "fixtures/fe/including/ios.yaml",
1061            "release",
1062            false,
1063        )?;
1064        Ok(())
1065    }
1066
1067    #[test]
1068    fn test_importing_simple_swift() -> Result<()> {
1069        generate_multiple_and_assert(
1070            "test/importing/simple/app_debug.swift",
1071            &[
1072                ("fixtures/fe/importing/simple/app.yaml", "debug"),
1073                ("fixtures/fe/importing/simple/lib.yaml", "debug"),
1074            ],
1075        )?;
1076        Ok(())
1077    }
1078
1079    #[test]
1080    fn test_importing_override_defaults_swift() -> Result<()> {
1081        generate_multiple_and_assert(
1082            "test/importing/overrides/app_debug.swift",
1083            &[
1084                ("fixtures/fe/importing/overrides/app.fml.yaml", "debug"),
1085                ("fixtures/fe/importing/overrides/lib.fml.yaml", "debug"),
1086            ],
1087        )?;
1088        Ok(())
1089    }
1090    #[test]
1091    fn test_importing_diamond_overrides_swift() -> Result<()> {
1092        // In this test, sublib implements a feature.
1093        // Both lib and app offer some configuration, and both app and lib
1094        // need to import sublib.
1095        generate_multiple_and_assert(
1096            "test/importing/diamond/00-app.swift",
1097            &[
1098                ("fixtures/fe/importing/diamond/00-app.yaml", "debug"),
1099                ("fixtures/fe/importing/diamond/01-lib.yaml", "debug"),
1100                ("fixtures/fe/importing/diamond/02-sublib.yaml", "debug"),
1101            ],
1102        )?;
1103        Ok(())
1104    }
1105
1106    #[test]
1107    fn test_importing_including_imports_swift() -> Result<()> {
1108        generate_multiple_and_assert(
1109            "test/importing/including-imports/app_release.swift",
1110            &[
1111                (
1112                    "fixtures/fe/importing/including-imports/ui.fml.yaml",
1113                    "none",
1114                ),
1115                (
1116                    "fixtures/fe/importing/including-imports/app.fml.yaml",
1117                    "release",
1118                ),
1119            ],
1120        )?;
1121        Ok(())
1122    }
1123    #[test]
1124    fn regression_test_concurrent_access_of_feature_holder_swift() -> Result<()> {
1125        generate_and_assert(
1126            "test/threadsafe_feature_holder.swift",
1127            "fixtures/fe/browser.yaml",
1128            "release",
1129            false,
1130        )?;
1131        Ok(())
1132    }
1133
1134    #[test]
1135    fn test_with_coenrolled_features_and_imports_swift() -> Result<()> {
1136        generate_multiple_and_assert(
1137            "test/allow_coenrolling.swift",
1138            &[
1139                ("fixtures/fe/importing/coenrolling/app.fml.yaml", "release"),
1140                ("fixtures/fe/importing/coenrolling/ui.fml.yaml", "release"),
1141            ],
1142        )?;
1143        Ok(())
1144    }
1145
1146    #[test]
1147    fn test_with_preference_overrides_swift() -> Result<()> {
1148        generate_multiple_and_assert(
1149            "test/pref_overrides.swift",
1150            &[("fixtures/fe/pref_overrides.fml.yaml", "debug")],
1151        )?;
1152        Ok(())
1153    }
1154}