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