1mod cli;
6mod cmd;
7mod config;
8mod feature_utils;
9mod output;
10mod protocol;
11mod sources;
12mod updater;
13mod value_utils;
14mod version_utils;
15
16use anyhow::{bail, Result};
17use clap::Parser;
18use cli::{Cli, CliCommand, ExperimentArgs, OpenArgs};
19use sources::{ExperimentListSource, ExperimentSource, ManifestSource};
20use std::{ffi::OsString, path::PathBuf};
21
22pub(crate) static USER_AGENT: &str =
23 concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
24
25fn main() -> Result<()> {
26 let cmds = get_commands_from_cli(std::env::args_os())?;
27 for c in cmds {
28 let success = cmd::process_cmd(&c)?;
29 if !success {
30 bail!("Failed");
31 }
32 }
33 updater::check_for_update();
34 Ok(())
35}
36
37fn get_commands_from_cli<I, T>(args: I) -> Result<Vec<AppCommand>>
38where
39 I: IntoIterator<Item = T>,
40 T: Into<OsString> + Clone,
41{
42 let cli = Cli::try_parse_from(args)?;
43
44 let mut commands: Vec<AppCommand> = Default::default();
45
46 let main_command = AppCommand::try_from(&cli)?;
52
53 cli.command.check_valid()?;
56
57 commands.push(AppCommand::try_validate(&cli)?);
59
60 if cli.command.should_kill() {
61 let app = LaunchableApp::try_from(&cli)?;
62 commands.push(AppCommand::Kill { app });
63 }
64 if cli.command.should_reset() {
65 let app = LaunchableApp::try_from(&cli)?;
66 commands.push(AppCommand::Reset { app });
67 }
68 commands.push(main_command);
69
70 Ok(commands)
71}
72
73#[derive(Clone, Debug, PartialEq)]
74enum LaunchableApp {
75 Android {
76 package_name: String,
77 activity_name: String,
78 device_id: Option<String>,
79 scheme: Option<String>,
80 open_deeplink: Option<String>,
81 },
82 Ios {
83 device_id: String,
84 app_id: String,
85 scheme: Option<String>,
86 },
87}
88
89#[derive(Clone, Debug, PartialEq)]
90pub(crate) struct NimbusApp {
91 app_name: Option<String>,
92 channel: Option<String>,
93}
94
95impl NimbusApp {
96 #[cfg(test)]
97 fn new(app: &str, channel: &str) -> Self {
98 Self {
99 app_name: Some(app.to_string()),
100 channel: Some(channel.to_string()),
101 }
102 }
103
104 fn channel(&self) -> Option<String> {
105 self.channel.clone()
106 }
107 fn app_name(&self) -> Option<String> {
108 self.app_name.clone()
109 }
110}
111
112impl From<&Cli> for NimbusApp {
113 fn from(value: &Cli) -> Self {
114 Self {
115 channel: value.channel.clone(),
116 app_name: value.app.clone(),
117 }
118 }
119}
120
121#[derive(Debug, PartialEq)]
122enum AppCommand {
123 ApplyFile {
124 app: LaunchableApp,
125 open: AppOpenArgs,
126 list: ExperimentListSource,
127 preserve_nimbus_db: bool,
128 },
129
130 CaptureLogs {
131 app: LaunchableApp,
132 file: PathBuf,
133 },
134
135 Defaults {
136 manifest: ManifestSource,
137 feature_id: Option<String>,
138 output: Option<PathBuf>,
139 },
140
141 Enroll {
142 app: LaunchableApp,
143 params: NimbusApp,
144 experiment: ExperimentSource,
145 rollouts: Vec<ExperimentSource>,
146 branch: String,
147 preserve_targeting: bool,
148 preserve_bucketing: bool,
149 preserve_nimbus_db: bool,
150 open: AppOpenArgs,
151 },
152
153 ExtractFeatures {
154 experiment: ExperimentSource,
155 branch: String,
156 manifest: ManifestSource,
157
158 feature_id: Option<String>,
159 validate: bool,
160 multi: bool,
161
162 output: Option<PathBuf>,
163 },
164
165 FetchList {
166 list: ExperimentListSource,
167 file: Option<PathBuf>,
168 },
169
170 FmlPassthrough {
171 args: Vec<OsString>,
172 cwd: PathBuf,
173 },
174
175 Info {
176 experiment: ExperimentSource,
177 output: Option<PathBuf>,
178 },
179
180 Kill {
181 app: LaunchableApp,
182 },
183
184 List {
185 list: ExperimentListSource,
186 },
187
188 LogState {
189 app: LaunchableApp,
190 open: AppOpenArgs,
191 },
192
193 NoOp,
195
196 Open {
197 app: LaunchableApp,
198 open: AppOpenArgs,
199 },
200
201 Reset {
202 app: LaunchableApp,
203 },
204
205 #[cfg(feature = "server")]
206 StartServer,
207
208 TailLogs {
209 app: LaunchableApp,
210 },
211
212 Unenroll {
213 app: LaunchableApp,
214 open: AppOpenArgs,
215 },
216
217 ValidateExperiment {
218 params: NimbusApp,
219 manifest: ManifestSource,
220 experiment: ExperimentSource,
221 },
222}
223
224impl AppCommand {
225 fn try_validate(cli: &Cli) -> Result<Self> {
226 let params = cli.into();
227 Ok(match &cli.command {
228 CliCommand::Enroll {
229 no_validate,
230 manifest,
231 ..
232 }
233 | CliCommand::TestFeature {
234 no_validate,
235 manifest,
236 ..
237 } if !no_validate => {
238 let experiment = ExperimentSource::try_from(cli)?;
239 let manifest = ManifestSource::try_from(¶ms, manifest)?;
240 AppCommand::ValidateExperiment {
241 params,
242 experiment,
243 manifest,
244 }
245 }
246 CliCommand::Validate { manifest, .. } => {
247 let experiment = ExperimentSource::try_from(cli)?;
248 let manifest = ManifestSource::try_from(¶ms, manifest)?;
249 AppCommand::ValidateExperiment {
250 params,
251 experiment,
252 manifest,
253 }
254 }
255 _ => Self::NoOp,
256 })
257 }
258}
259
260impl TryFrom<&Cli> for AppCommand {
261 type Error = anyhow::Error;
262
263 fn try_from(cli: &Cli) -> Result<Self> {
264 let params = NimbusApp::from(cli);
265 Ok(match cli.command.clone() {
266 CliCommand::ApplyFile {
267 file,
268 preserve_nimbus_db,
269 open,
270 } => {
271 let app = LaunchableApp::try_from(cli)?;
272 let list = ExperimentListSource::try_from(file.as_path())?;
273 AppCommand::ApplyFile {
274 app,
275 open: open.into(),
276 list,
277 preserve_nimbus_db,
278 }
279 }
280 CliCommand::CaptureLogs { file } => {
281 let app = LaunchableApp::try_from(cli)?;
282 AppCommand::CaptureLogs { app, file }
283 }
284 CliCommand::Defaults {
285 feature_id,
286 output,
287 manifest,
288 } => {
289 let manifest = ManifestSource::try_from(¶ms, &manifest)?;
290 AppCommand::Defaults {
291 manifest,
292 feature_id,
293 output,
294 }
295 }
296 CliCommand::Enroll {
297 branch,
298 rollouts,
299 preserve_targeting,
300 preserve_bucketing,
301 preserve_nimbus_db,
302 experiment,
303 open,
304 ..
305 } => {
306 let app = LaunchableApp::try_from(cli)?;
307 let mut recipes: Vec<ExperimentSource> = Vec::new();
309 for r in rollouts {
310 let rollout = ExperimentArgs {
311 experiment: r,
312 ..experiment.clone()
313 };
314 recipes.push(ExperimentSource::try_from(&rollout)?);
315 }
316
317 let experiment = ExperimentSource::try_from(cli)?;
318
319 Self::Enroll {
320 app,
321 params,
322 experiment,
323 branch,
324 rollouts: recipes,
325 preserve_targeting,
326 preserve_bucketing,
327 preserve_nimbus_db,
328 open: open.into(),
329 }
330 }
331 CliCommand::Features {
332 manifest,
333 branch,
334 feature_id,
335 output,
336 validate,
337 multi,
338 ..
339 } => {
340 let manifest = ManifestSource::try_from(¶ms, &manifest)?;
341 let experiment = ExperimentSource::try_from(cli)?;
342 AppCommand::ExtractFeatures {
343 experiment,
344 branch,
345 manifest,
346 feature_id,
347 validate,
348 multi,
349 output,
350 }
351 }
352 CliCommand::Fetch { output, .. } | CliCommand::FetchList { output, .. } => {
353 let list = ExperimentListSource::try_from(cli)?;
354
355 AppCommand::FetchList { list, file: output }
356 }
357 CliCommand::Fml { args } => {
358 let cwd = std::env::current_dir().expect("Current Working Directory is not set");
359 AppCommand::FmlPassthrough { args, cwd }
360 }
361 CliCommand::Info { experiment, output } => AppCommand::Info {
362 experiment: ExperimentSource::try_from(&experiment)?,
363 output,
364 },
365 CliCommand::List { .. } => {
366 let list = ExperimentListSource::try_from(cli)?;
367 AppCommand::List { list }
368 }
369 CliCommand::LogState { open } => {
370 let app = LaunchableApp::try_from(cli)?;
371 AppCommand::LogState {
372 app,
373 open: open.into(),
374 }
375 }
376 CliCommand::Open { open, .. } => {
377 let app = LaunchableApp::try_from(cli)?;
378 AppCommand::Open {
379 app,
380 open: open.into(),
381 }
382 }
383 #[cfg(feature = "server")]
384 CliCommand::StartServer => AppCommand::StartServer,
385 CliCommand::TailLogs => {
386 let app = LaunchableApp::try_from(cli)?;
387 AppCommand::TailLogs { app }
388 }
389 CliCommand::TestFeature { files, open, .. } => {
390 let app = LaunchableApp::try_from(cli)?;
391 let experiment = ExperimentSource::try_from(cli)?;
392 let first = files
393 .first()
394 .ok_or_else(|| anyhow::Error::msg("Need at least one file to make a branch"))?;
395 let branch = feature_utils::slug(first)?;
396
397 Self::Enroll {
398 app,
399 params,
400 experiment,
401 branch,
402 rollouts: Default::default(),
403 open: open.into(),
404 preserve_targeting: false,
405 preserve_bucketing: false,
406 preserve_nimbus_db: false,
407 }
408 }
409 CliCommand::Unenroll { open } => {
410 let app = LaunchableApp::try_from(cli)?;
411 AppCommand::Unenroll {
412 app,
413 open: open.into(),
414 }
415 }
416 _ => Self::NoOp,
417 })
418 }
419}
420
421impl CliCommand {
422 fn check_valid(&self) -> Result<()> {
423 if let Some(open) = self.open_args() {
425 if open.reset_app || !open.passthrough.is_empty() {
426 const ERR: &str = "does not work with --reset-app or passthrough args";
427 if open.pbcopy {
428 bail!(format!("{} {}", "--pbcopy", ERR));
429 }
430 if open.pbpaste {
431 bail!(format!("{} {}", "--pbpaste", ERR));
432 }
433 if open.output.is_some() {
434 bail!(format!("{} {}", "--output", ERR));
435 }
436 }
437 if open.deeplink.is_some() {
438 const ERR: &str = "does not work with --deeplink";
439 if open.output.is_some() {
440 bail!(format!("{} {}", "--output", ERR));
441 }
442 }
443 }
444 Ok(())
445 }
446
447 fn open_args(&self) -> Option<&OpenArgs> {
448 if let Self::ApplyFile { open, .. }
449 | Self::Open { open, .. }
450 | Self::Enroll { open, .. }
451 | Self::LogState { open, .. }
452 | Self::TestFeature { open, .. }
453 | Self::Unenroll { open, .. } = self
454 {
455 Some(open)
456 } else {
457 None
458 }
459 }
460
461 fn should_kill(&self) -> bool {
462 if let Some(open) = self.open_args() {
463 let using_links = open.pbcopy || open.pbpaste;
464 let output_to_file = open.output.is_some();
465 let no_clobber = if let Self::Open { no_clobber, .. } = self {
466 *no_clobber
467 } else {
468 false
469 };
470 !using_links && !no_clobber && !output_to_file
471 } else {
472 matches!(self, Self::ResetApp)
473 }
474 }
475
476 fn should_reset(&self) -> bool {
477 if let Some(open) = self.open_args() {
478 open.reset_app
479 } else {
480 matches!(self, Self::ResetApp)
481 }
482 }
483}
484
485#[derive(Debug, Default, PartialEq)]
486pub(crate) struct AppOpenArgs {
487 deeplink: Option<String>,
488 passthrough: Vec<String>,
489 pbcopy: bool,
490 pbpaste: bool,
491
492 output: Option<PathBuf>,
493}
494
495impl From<OpenArgs> for AppOpenArgs {
496 fn from(value: OpenArgs) -> Self {
497 Self {
498 deeplink: value.deeplink,
499 passthrough: value.passthrough,
500 pbcopy: value.pbcopy,
501 pbpaste: value.pbpaste,
502 output: value.output,
503 }
504 }
505}
506
507impl AppOpenArgs {
508 fn args(&self) -> (&[String], &[String]) {
509 let splits = &mut self.passthrough.splitn(2, |item| item == "{}");
510 match (splits.next(), splits.next()) {
511 (Some(first), Some(last)) => (first, last),
512 (None, Some(last)) | (Some(last), None) => (&[], last),
513 _ => (&[], &[]),
514 }
515 }
516}
517
518#[cfg(test)]
519mod unit_tests {
520 use crate::sources::ExperimentListFilter;
521
522 use super::*;
523
524 #[test]
525 fn test_launchable_app() -> Result<()> {
526 fn cli(app: &str, channel: &str) -> Cli {
527 Cli {
528 app: Some(app.to_string()),
529 channel: Some(channel.to_string()),
530 device_id: None,
531 command: CliCommand::ResetApp,
532 }
533 }
534 fn android(
535 package: &str,
536 activity: &str,
537 scheme: Option<&str>,
538 open_deeplink: Option<&str>,
539 ) -> LaunchableApp {
540 LaunchableApp::Android {
541 package_name: package.to_string(),
542 activity_name: activity.to_string(),
543 device_id: None,
544 scheme: scheme.map(str::to_string),
545 open_deeplink: open_deeplink.map(str::to_string),
546 }
547 }
548 fn ios(id: &str, scheme: Option<&str>) -> LaunchableApp {
549 LaunchableApp::Ios {
550 app_id: id.to_string(),
551 device_id: "booted".to_string(),
552 scheme: scheme.map(str::to_string),
553 }
554 }
555
556 assert_eq!(
558 LaunchableApp::try_from(&cli("fenix", "developer"))?,
559 android(
560 "org.mozilla.fenix.debug",
561 ".App",
562 Some("fenix-dev"),
563 Some("open")
564 )
565 );
566 assert_eq!(
567 LaunchableApp::try_from(&cli("fenix", "nightly"))?,
568 android(
569 "org.mozilla.fenix",
570 ".App",
571 Some("fenix-nightly"),
572 Some("open")
573 )
574 );
575 assert_eq!(
576 LaunchableApp::try_from(&cli("fenix", "beta"))?,
577 android(
578 "org.mozilla.firefox_beta",
579 ".App",
580 Some("fenix-beta"),
581 Some("open")
582 )
583 );
584 assert_eq!(
585 LaunchableApp::try_from(&cli("fenix", "release"))?,
586 android("org.mozilla.firefox", ".App", Some("fenix"), Some("open"))
587 );
588
589 assert_eq!(
591 LaunchableApp::try_from(&cli("firefox_ios", "developer"))?,
592 ios("org.mozilla.ios.Fennec", Some("fennec"))
593 );
594 assert_eq!(
595 LaunchableApp::try_from(&cli("firefox_ios", "beta"))?,
596 ios("org.mozilla.ios.FirefoxBeta", Some("firefox-beta"))
597 );
598 assert_eq!(
599 LaunchableApp::try_from(&cli("firefox_ios", "release"))?,
600 ios("org.mozilla.ios.Firefox", Some("firefox-internal"))
601 );
602
603 assert_eq!(
605 LaunchableApp::try_from(&cli("focus_android", "developer"))?,
606 android(
607 "org.mozilla.focus.debug",
608 "org.mozilla.focus.activity.MainActivity",
609 None,
610 None,
611 )
612 );
613 assert_eq!(
614 LaunchableApp::try_from(&cli("focus_android", "nightly"))?,
615 android(
616 "org.mozilla.focus.nightly",
617 "org.mozilla.focus.activity.MainActivity",
618 None,
619 None,
620 )
621 );
622 assert_eq!(
623 LaunchableApp::try_from(&cli("focus_android", "beta"))?,
624 android(
625 "org.mozilla.focus.beta",
626 "org.mozilla.focus.activity.MainActivity",
627 None,
628 None,
629 )
630 );
631 assert_eq!(
632 LaunchableApp::try_from(&cli("focus_android", "release"))?,
633 android(
634 "org.mozilla.focus",
635 "org.mozilla.focus.activity.MainActivity",
636 None,
637 None,
638 )
639 );
640
641 Ok(())
642 }
643
644 #[test]
645 fn test_split_args() -> Result<()> {
646 let mut open = AppOpenArgs {
647 passthrough: vec![],
648 ..Default::default()
649 };
650 let empty: &[String] = &[];
651 let expected = (empty, empty);
652 let observed = open.args();
653 assert_eq!(observed.0, expected.0);
654 assert_eq!(observed.1, expected.1);
655
656 open.passthrough = vec!["{}".to_string()];
657 let expected = (empty, empty);
658 let observed = open.args();
659 assert_eq!(observed.0, expected.0);
660 assert_eq!(observed.1, expected.1);
661
662 open.passthrough = vec!["foo".to_string(), "bar".to_string()];
663 let expected: (&[String], &[String]) = (empty, &["foo".to_string(), "bar".to_string()]);
664 let observed = open.args();
665 assert_eq!(observed.0, expected.0);
666 assert_eq!(observed.1, expected.1);
667
668 open.passthrough = vec!["foo".to_string(), "bar".to_string(), "{}".to_string()];
669 let expected: (&[String], &[String]) = (&["foo".to_string(), "bar".to_string()], empty);
670 let observed = open.args();
671 assert_eq!(observed.0, expected.0);
672 assert_eq!(observed.1, expected.1);
673
674 open.passthrough = vec!["foo".to_string(), "{}".to_string(), "bar".to_string()];
675 let expected: (&[String], &[String]) = (&["foo".to_string()], &["bar".to_string()]);
676 let observed = open.args();
677 assert_eq!(observed.0, expected.0);
678 assert_eq!(observed.1, expected.1);
679
680 open.passthrough = vec!["{}".to_string(), "foo".to_string(), "bar".to_string()];
681 let expected: (&[String], &[String]) = (empty, &["foo".to_string(), "bar".to_string()]);
682 let observed = open.args();
683 assert_eq!(observed.0, expected.0);
684 assert_eq!(observed.1, expected.1);
685
686 Ok(())
687 }
688
689 fn fenix() -> LaunchableApp {
690 LaunchableApp::Android {
691 package_name: "org.mozilla.fenix.debug".to_string(),
692 activity_name: ".App".to_string(),
693 device_id: None,
694 scheme: Some("fenix-dev".to_string()),
695 open_deeplink: Some("open".to_string()),
696 }
697 }
698
699 fn fenix_params() -> NimbusApp {
700 NimbusApp::new("fenix", "developer")
701 }
702
703 fn fenix_old_manifest_with_ref(ref_: &str) -> ManifestSource {
704 ManifestSource::FromGithub {
705 channel: "developer".into(),
706 github_repo: "mozilla-mobile/firefox-android".into(),
707 ref_: ref_.into(),
708 manifest_file: "@mozilla-mobile/firefox-android/fenix/app/nimbus.fml.yaml".into(),
709 }
710 }
711
712 fn fenix_manifest() -> ManifestSource {
713 fenix_manifest_with_ref("master")
714 }
715
716 fn fenix_manifest_with_ref(ref_: &str) -> ManifestSource {
717 ManifestSource::FromGithub {
718 github_repo: "mozilla/gecko-dev".to_string(),
719 ref_: ref_.to_string(),
720 manifest_file: "@mozilla/gecko-dev/mobile/android/fenix/app/nimbus.fml.yaml".into(),
721 channel: "developer".to_string(),
722 }
723 }
724
725 fn manifest_from_file(file: &str) -> ManifestSource {
726 ManifestSource::FromFile {
727 channel: "developer".to_string(),
728 manifest_file: file.to_string(),
729 }
730 }
731
732 fn experiment(slug: &str) -> ExperimentSource {
733 let endpoint = config::api_v6_production_server();
734 ExperimentSource::FromApiV6 {
735 slug: slug.to_string(),
736 endpoint,
737 }
738 }
739
740 fn feature_experiment(feature_id: &str, files: &[&str]) -> ExperimentSource {
741 ExperimentSource::FromFeatureFiles {
742 app: fenix_params(),
743 feature_id: feature_id.to_string(),
744 files: files.iter().map(|f| f.into()).collect(),
745 }
746 }
747
748 fn with_deeplink(link: &str) -> AppOpenArgs {
749 AppOpenArgs {
750 deeplink: Some(link.to_string()),
751 ..Default::default()
752 }
753 }
754
755 fn with_pbcopy() -> AppOpenArgs {
756 AppOpenArgs {
757 pbcopy: true,
758 ..Default::default()
759 }
760 }
761
762 fn with_passthrough(params: &[&str]) -> AppOpenArgs {
763 AppOpenArgs {
764 passthrough: params.iter().map(|s| s.to_string()).collect(),
765 ..Default::default()
766 }
767 }
768
769 fn with_output(filename: &str) -> AppOpenArgs {
770 AppOpenArgs {
771 output: Some(PathBuf::from(filename)),
772 ..Default::default()
773 }
774 }
775
776 fn for_app(app: &str, list: ExperimentListSource) -> ExperimentListSource {
777 ExperimentListSource::Filtered {
778 filter: ExperimentListFilter::for_app(app),
779 inner: Box::new(list),
780 }
781 }
782
783 fn for_feature(feature: &str, list: ExperimentListSource) -> ExperimentListSource {
784 ExperimentListSource::Filtered {
785 filter: ExperimentListFilter::for_feature(feature),
786 inner: Box::new(list),
787 }
788 }
789
790 fn for_active_on_date(date: &str, list: ExperimentListSource) -> ExperimentListSource {
791 ExperimentListSource::Filtered {
792 filter: ExperimentListFilter::for_active_on(date),
793 inner: Box::new(list),
794 }
795 }
796
797 fn for_enrolling_on_date(date: &str, list: ExperimentListSource) -> ExperimentListSource {
798 ExperimentListSource::Filtered {
799 filter: ExperimentListFilter::for_enrolling_on(date),
800 inner: Box::new(list),
801 }
802 }
803
804 #[test]
805 fn test_enroll() -> Result<()> {
806 let observed = get_commands_from_cli([
807 "nimbus-cli",
808 "--app",
809 "fenix",
810 "--channel",
811 "developer",
812 "enroll",
813 "my-experiment",
814 "--branch",
815 "my-branch",
816 "--no-validate",
817 ])?;
818
819 let expected = vec![
820 AppCommand::NoOp,
821 AppCommand::Kill { app: fenix() },
822 AppCommand::Enroll {
823 app: fenix(),
824 params: fenix_params(),
825 experiment: experiment("my-experiment"),
826 rollouts: Default::default(),
827 branch: "my-branch".to_string(),
828 preserve_targeting: false,
829 preserve_bucketing: false,
830 preserve_nimbus_db: false,
831 open: Default::default(),
832 },
833 ];
834 assert_eq!(expected, observed);
835 Ok(())
836 }
837
838 #[test]
839 fn test_enroll_with_reset_app() -> Result<()> {
840 let observed = get_commands_from_cli([
841 "nimbus-cli",
842 "--app",
843 "fenix",
844 "--channel",
845 "developer",
846 "enroll",
847 "my-experiment",
848 "--branch",
849 "my-branch",
850 "--reset-app",
851 "--no-validate",
852 ])?;
853
854 let expected = vec![
855 AppCommand::NoOp,
856 AppCommand::Kill { app: fenix() },
857 AppCommand::Reset { app: fenix() },
858 AppCommand::Enroll {
859 app: fenix(),
860 params: fenix_params(),
861 experiment: experiment("my-experiment"),
862 rollouts: Default::default(),
863 branch: "my-branch".to_string(),
864 preserve_targeting: false,
865 preserve_bucketing: false,
866 preserve_nimbus_db: false,
867 open: Default::default(),
868 },
869 ];
870 assert_eq!(expected, observed);
871 Ok(())
872 }
873
874 #[test]
875 fn test_enroll_with_validate() -> Result<()> {
876 let observed = get_commands_from_cli([
877 "nimbus-cli",
878 "--app",
879 "fenix",
880 "--channel",
881 "developer",
882 "enroll",
883 "my-experiment",
884 "--branch",
885 "my-branch",
886 "--reset-app",
887 ])?;
888
889 let expected = vec![
890 AppCommand::ValidateExperiment {
891 params: fenix_params(),
892 manifest: fenix_manifest(),
893 experiment: experiment("my-experiment"),
894 },
895 AppCommand::Kill { app: fenix() },
896 AppCommand::Reset { app: fenix() },
897 AppCommand::Enroll {
898 app: fenix(),
899 params: fenix_params(),
900 experiment: experiment("my-experiment"),
901 rollouts: Default::default(),
902 branch: "my-branch".to_string(),
903 preserve_targeting: false,
904 preserve_bucketing: false,
905 preserve_nimbus_db: false,
906 open: Default::default(),
907 },
908 ];
909 assert_eq!(expected, observed);
910 Ok(())
911 }
912
913 #[test]
914 fn test_enroll_with_deeplink() -> Result<()> {
915 let observed = get_commands_from_cli([
916 "nimbus-cli",
917 "--app",
918 "fenix",
919 "--channel",
920 "developer",
921 "enroll",
922 "my-experiment",
923 "--branch",
924 "my-branch",
925 "--no-validate",
926 "--deeplink",
927 "host/path?key=value",
928 ])?;
929
930 let expected = vec![
931 AppCommand::NoOp,
932 AppCommand::Kill { app: fenix() },
933 AppCommand::Enroll {
934 app: fenix(),
935 params: fenix_params(),
936 experiment: experiment("my-experiment"),
937 rollouts: Default::default(),
938 branch: "my-branch".to_string(),
939 preserve_targeting: false,
940 preserve_bucketing: false,
941 preserve_nimbus_db: false,
942 open: with_deeplink("host/path?key=value"),
943 },
944 ];
945 assert_eq!(expected, observed);
946 Ok(())
947 }
948
949 #[test]
950 fn test_enroll_with_passthrough() -> Result<()> {
951 let observed = get_commands_from_cli([
952 "nimbus-cli",
953 "--app",
954 "fenix",
955 "--channel",
956 "developer",
957 "enroll",
958 "my-experiment",
959 "--branch",
960 "my-branch",
961 "--no-validate",
962 "--",
963 "--start-profiler",
964 "./profile.file",
965 "{}",
966 "--esn",
967 "TEST_FLAG",
968 ])?;
969
970 let expected = vec![
971 AppCommand::NoOp,
972 AppCommand::Kill { app: fenix() },
973 AppCommand::Enroll {
974 app: fenix(),
975 params: fenix_params(),
976 experiment: experiment("my-experiment"),
977 rollouts: Default::default(),
978 branch: "my-branch".to_string(),
979 preserve_targeting: false,
980 preserve_bucketing: false,
981 preserve_nimbus_db: false,
982 open: with_passthrough(&[
983 "--start-profiler",
984 "./profile.file",
985 "{}",
986 "--esn",
987 "TEST_FLAG",
988 ]),
989 },
990 ];
991 assert_eq!(expected, observed);
992 Ok(())
993 }
994
995 #[test]
996 fn test_enroll_with_pbcopy() -> Result<()> {
997 let observed = get_commands_from_cli([
998 "nimbus-cli",
999 "--app",
1000 "fenix",
1001 "--channel",
1002 "developer",
1003 "enroll",
1004 "my-experiment",
1005 "--branch",
1006 "my-branch",
1007 "--no-validate",
1008 "--pbcopy",
1009 ])?;
1010
1011 let expected = vec![
1012 AppCommand::NoOp,
1013 AppCommand::Enroll {
1014 app: fenix(),
1015 params: fenix_params(),
1016 experiment: experiment("my-experiment"),
1017 rollouts: Default::default(),
1018 branch: "my-branch".to_string(),
1019 preserve_targeting: false,
1020 preserve_bucketing: false,
1021 preserve_nimbus_db: false,
1022 open: with_pbcopy(),
1023 },
1024 ];
1025 assert_eq!(expected, observed);
1026 Ok(())
1027 }
1028
1029 #[test]
1030 fn test_enroll_with_output() -> Result<()> {
1031 let observed = get_commands_from_cli([
1032 "nimbus-cli",
1033 "--app",
1034 "fenix",
1035 "--channel",
1036 "developer",
1037 "enroll",
1038 "my-experiment",
1039 "--branch",
1040 "my-branch",
1041 "--no-validate",
1042 "--output",
1043 "./file.json",
1044 ])?;
1045
1046 let expected = vec![
1047 AppCommand::NoOp,
1048 AppCommand::Enroll {
1049 app: fenix(),
1050 params: fenix_params(),
1051 experiment: experiment("my-experiment"),
1052 rollouts: Default::default(),
1053 branch: "my-branch".to_string(),
1054 preserve_targeting: false,
1055 preserve_bucketing: false,
1056 preserve_nimbus_db: false,
1057 open: with_output("./file.json"),
1058 },
1059 ];
1060 assert_eq!(expected, observed);
1061 Ok(())
1062 }
1063
1064 #[test]
1065 fn test_validate() -> Result<()> {
1066 let observed = get_commands_from_cli([
1067 "nimbus-cli",
1068 "--app",
1069 "fenix",
1070 "--channel",
1071 "developer",
1072 "validate",
1073 "my-experiment",
1074 ])?;
1075
1076 let expected = vec![
1077 AppCommand::ValidateExperiment {
1078 params: fenix_params(),
1079 manifest: fenix_manifest(),
1080 experiment: experiment("my-experiment"),
1081 },
1082 AppCommand::NoOp,
1083 ];
1084 assert_eq!(expected, observed);
1085
1086 let observed = get_commands_from_cli([
1088 "nimbus-cli",
1089 "--app",
1090 "fenix",
1091 "--channel",
1092 "developer",
1093 "validate",
1094 "my-experiment",
1095 "--version",
1096 "114",
1097 ])?;
1098
1099 let expected = vec![
1100 AppCommand::ValidateExperiment {
1101 params: fenix_params(),
1102 manifest: fenix_old_manifest_with_ref("releases_v114"),
1103 experiment: experiment("my-experiment"),
1104 },
1105 AppCommand::NoOp,
1106 ];
1107 assert_eq!(expected, observed);
1108
1109 let observed = get_commands_from_cli([
1111 "nimbus-cli",
1112 "--app",
1113 "fenix",
1114 "--channel",
1115 "developer",
1116 "validate",
1117 "my-experiment",
1118 "--ref",
1119 "my-tag",
1120 ])?;
1121
1122 let expected = vec![
1123 AppCommand::ValidateExperiment {
1124 params: fenix_params(),
1125 manifest: fenix_manifest_with_ref("my-tag"),
1126 experiment: experiment("my-experiment"),
1127 },
1128 AppCommand::NoOp,
1129 ];
1130 assert_eq!(expected, observed);
1131
1132 let observed = get_commands_from_cli([
1134 "nimbus-cli",
1135 "--channel",
1136 "developer",
1137 "validate",
1138 "my-experiment",
1139 "--manifest",
1140 "./manifest.fml.yaml",
1141 ])?;
1142
1143 let expected = vec![
1144 AppCommand::ValidateExperiment {
1145 params: NimbusApp {
1146 channel: Some("developer".to_string()),
1147 app_name: None,
1148 },
1149 manifest: manifest_from_file("./manifest.fml.yaml"),
1150 experiment: experiment("my-experiment"),
1151 },
1152 AppCommand::NoOp,
1153 ];
1154 assert_eq!(expected, observed);
1155
1156 Ok(())
1157 }
1158
1159 #[test]
1160 fn test_test_feature() -> Result<()> {
1161 let observed = get_commands_from_cli([
1162 "nimbus-cli",
1163 "--app",
1164 "fenix",
1165 "--channel",
1166 "developer",
1167 "test-feature",
1168 "my-feature",
1169 "./my-branch.json",
1170 "./my-treatment.json",
1171 ])?;
1172
1173 let expected = vec![
1174 AppCommand::ValidateExperiment {
1175 params: fenix_params(),
1176 manifest: fenix_manifest(),
1177 experiment: feature_experiment(
1178 "my-feature",
1179 &["./my-branch.json", "./my-treatment.json"],
1180 ),
1181 },
1182 AppCommand::Kill { app: fenix() },
1183 AppCommand::Enroll {
1184 app: fenix(),
1185 params: fenix_params(),
1186 experiment: feature_experiment(
1187 "my-feature",
1188 &["./my-branch.json", "./my-treatment.json"],
1189 ),
1190 rollouts: Default::default(),
1191 branch: "my-branch".to_string(),
1192 preserve_targeting: false,
1193 preserve_bucketing: false,
1194 preserve_nimbus_db: false,
1195 open: Default::default(),
1196 },
1197 ];
1198 assert_eq!(expected, observed);
1199
1200 let observed = get_commands_from_cli([
1202 "nimbus-cli",
1203 "--app",
1204 "fenix",
1205 "--channel",
1206 "developer",
1207 "test-feature",
1208 "my-feature",
1209 "./my-branch.json",
1210 "./my-treatment.json",
1211 "--version",
1212 "114",
1213 ])?;
1214
1215 let expected = vec![
1216 AppCommand::ValidateExperiment {
1217 params: fenix_params(),
1218 manifest: fenix_old_manifest_with_ref("releases_v114"),
1219 experiment: feature_experiment(
1220 "my-feature",
1221 &["./my-branch.json", "./my-treatment.json"],
1222 ),
1223 },
1224 AppCommand::Kill { app: fenix() },
1225 AppCommand::Enroll {
1226 app: fenix(),
1227 params: fenix_params(),
1228 experiment: feature_experiment(
1229 "my-feature",
1230 &["./my-branch.json", "./my-treatment.json"],
1231 ),
1232 rollouts: Default::default(),
1233 branch: "my-branch".to_string(),
1234 preserve_targeting: false,
1235 preserve_bucketing: false,
1236 preserve_nimbus_db: false,
1237 open: Default::default(),
1238 },
1239 ];
1240 assert_eq!(expected, observed);
1241
1242 let observed = get_commands_from_cli([
1244 "nimbus-cli",
1245 "--app",
1246 "fenix",
1247 "--channel",
1248 "developer",
1249 "test-feature",
1250 "my-feature",
1251 "./my-branch.json",
1252 "./my-treatment.json",
1253 "--ref",
1254 "my-tag",
1255 ])?;
1256
1257 let expected = vec![
1258 AppCommand::ValidateExperiment {
1259 params: fenix_params(),
1260 manifest: fenix_manifest_with_ref("my-tag"),
1261 experiment: feature_experiment(
1262 "my-feature",
1263 &["./my-branch.json", "./my-treatment.json"],
1264 ),
1265 },
1266 AppCommand::Kill { app: fenix() },
1267 AppCommand::Enroll {
1268 app: fenix(),
1269 params: fenix_params(),
1270 experiment: feature_experiment(
1271 "my-feature",
1272 &["./my-branch.json", "./my-treatment.json"],
1273 ),
1274 rollouts: Default::default(),
1275 branch: "my-branch".to_string(),
1276 preserve_targeting: false,
1277 preserve_bucketing: false,
1278 preserve_nimbus_db: false,
1279 open: Default::default(),
1280 },
1281 ];
1282 assert_eq!(expected, observed);
1283
1284 let observed = get_commands_from_cli([
1286 "nimbus-cli",
1287 "--app",
1288 "fenix",
1289 "--channel",
1290 "developer",
1291 "test-feature",
1292 "my-feature",
1293 "./my-branch.json",
1294 "./my-treatment.json",
1295 "--manifest",
1296 "./manifest.fml.yaml",
1297 ])?;
1298
1299 let expected = vec![
1300 AppCommand::ValidateExperiment {
1301 params: fenix_params(),
1302 manifest: manifest_from_file("./manifest.fml.yaml"),
1303 experiment: feature_experiment(
1304 "my-feature",
1305 &["./my-branch.json", "./my-treatment.json"],
1306 ),
1307 },
1308 AppCommand::Kill { app: fenix() },
1309 AppCommand::Enroll {
1310 app: fenix(),
1311 params: fenix_params(),
1312 experiment: feature_experiment(
1313 "my-feature",
1314 &["./my-branch.json", "./my-treatment.json"],
1315 ),
1316 rollouts: Default::default(),
1317 branch: "my-branch".to_string(),
1318 preserve_targeting: false,
1319 preserve_bucketing: false,
1320 preserve_nimbus_db: false,
1321 open: Default::default(),
1322 },
1323 ];
1324 assert_eq!(expected, observed);
1325
1326 let observed = get_commands_from_cli([
1327 "nimbus-cli",
1328 "--app",
1329 "fenix",
1330 "--channel",
1331 "developer",
1332 "test-feature",
1333 "my-feature",
1334 "./my-branch.json",
1335 "./my-treatment.json",
1336 "--no-validate",
1337 "--deeplink",
1338 "host/path?key=value",
1339 ])?;
1340
1341 let expected = vec![
1342 AppCommand::NoOp,
1343 AppCommand::Kill { app: fenix() },
1344 AppCommand::Enroll {
1345 app: fenix(),
1346 params: fenix_params(),
1347 experiment: feature_experiment(
1348 "my-feature",
1349 &["./my-branch.json", "./my-treatment.json"],
1350 ),
1351 rollouts: Default::default(),
1352 branch: "my-branch".to_string(),
1353 preserve_targeting: false,
1354 preserve_bucketing: false,
1355 preserve_nimbus_db: false,
1356 open: with_deeplink("host/path?key=value"),
1357 },
1358 ];
1359 assert_eq!(expected, observed);
1360
1361 Ok(())
1362 }
1363
1364 #[test]
1365 fn test_open() -> Result<()> {
1366 let observed = get_commands_from_cli([
1367 "nimbus-cli",
1368 "--app",
1369 "fenix",
1370 "--channel",
1371 "developer",
1372 "open",
1373 ])?;
1374
1375 let expected = vec![
1376 AppCommand::NoOp,
1377 AppCommand::Kill { app: fenix() },
1378 AppCommand::Open {
1379 app: fenix(),
1380 open: Default::default(),
1381 },
1382 ];
1383 assert_eq!(expected, observed);
1384 Ok(())
1385 }
1386
1387 #[test]
1388 fn test_open_with_reset() -> Result<()> {
1389 let observed = get_commands_from_cli([
1390 "nimbus-cli",
1391 "--app",
1392 "fenix",
1393 "--channel",
1394 "developer",
1395 "open",
1396 "--reset-app",
1397 ])?;
1398
1399 let expected = vec![
1400 AppCommand::NoOp,
1401 AppCommand::Kill { app: fenix() },
1402 AppCommand::Reset { app: fenix() },
1403 AppCommand::Open {
1404 app: fenix(),
1405 open: Default::default(),
1406 },
1407 ];
1408 assert_eq!(expected, observed);
1409 Ok(())
1410 }
1411
1412 #[test]
1413 fn test_open_with_deeplink() -> Result<()> {
1414 let observed = get_commands_from_cli([
1415 "nimbus-cli",
1416 "--app",
1417 "fenix",
1418 "--channel",
1419 "developer",
1420 "open",
1421 "--deeplink",
1422 "host/path",
1423 ])?;
1424
1425 let expected = vec![
1426 AppCommand::NoOp,
1427 AppCommand::Kill { app: fenix() },
1428 AppCommand::Open {
1429 app: fenix(),
1430 open: with_deeplink("host/path"),
1431 },
1432 ];
1433 assert_eq!(expected, observed);
1434
1435 Ok(())
1436 }
1437
1438 #[test]
1439 fn test_open_with_passthrough_params() -> Result<()> {
1440 let observed = get_commands_from_cli([
1441 "nimbus-cli",
1442 "--app",
1443 "fenix",
1444 "--channel",
1445 "developer",
1446 "open",
1447 "--",
1448 "--start-profiler",
1449 "./profile.file",
1450 "{}",
1451 "--esn",
1452 "TEST_FLAG",
1453 ])?;
1454
1455 let expected = vec![
1456 AppCommand::NoOp,
1457 AppCommand::Kill { app: fenix() },
1458 AppCommand::Open {
1459 app: fenix(),
1460 open: with_passthrough(&[
1461 "--start-profiler",
1462 "./profile.file",
1463 "{}",
1464 "--esn",
1465 "TEST_FLAG",
1466 ]),
1467 },
1468 ];
1469 assert_eq!(expected, observed);
1470 Ok(())
1471 }
1472
1473 #[test]
1474 fn test_open_with_noclobber() -> Result<()> {
1475 let observed = get_commands_from_cli([
1476 "nimbus-cli",
1477 "--app",
1478 "fenix",
1479 "--channel",
1480 "developer",
1481 "open",
1482 "--no-clobber",
1483 ])?;
1484
1485 let expected = vec![
1486 AppCommand::NoOp,
1487 AppCommand::Open {
1488 app: fenix(),
1489 open: Default::default(),
1490 },
1491 ];
1492 assert_eq!(expected, observed);
1493 Ok(())
1494 }
1495
1496 #[test]
1497 fn test_open_with_pbcopy() -> Result<()> {
1498 let observed = get_commands_from_cli([
1499 "nimbus-cli",
1500 "--app",
1501 "fenix",
1502 "--channel",
1503 "developer",
1504 "open",
1505 "--pbcopy",
1506 ])?;
1507
1508 let expected = vec![
1509 AppCommand::NoOp,
1510 AppCommand::Open {
1511 app: fenix(),
1512 open: with_pbcopy(),
1513 },
1514 ];
1515 assert_eq!(expected, observed);
1516 Ok(())
1517 }
1518
1519 #[test]
1520 fn test_fetch() -> Result<()> {
1521 let file = Some(PathBuf::from("./archived.json"));
1522 let observed = get_commands_from_cli([
1523 "nimbus-cli",
1524 "--app",
1525 "fenix",
1526 "--channel",
1527 "developer",
1528 "fetch",
1529 "--output",
1530 "./archived.json",
1531 "my-experiment",
1532 ])?;
1533
1534 let expected = vec![
1535 AppCommand::NoOp,
1536 AppCommand::FetchList {
1537 list: for_app(
1538 "fenix",
1539 ExperimentListSource::FromRecipes {
1540 recipes: vec![experiment("my-experiment")],
1541 },
1542 ),
1543 file: file.clone(),
1544 },
1545 ];
1546 assert_eq!(expected, observed);
1547
1548 let observed = get_commands_from_cli([
1549 "nimbus-cli",
1550 "--app",
1551 "fenix",
1552 "--channel",
1553 "developer",
1554 "fetch",
1555 "--output",
1556 "./archived.json",
1557 "my-experiment-1",
1558 "my-experiment-2",
1559 ])?;
1560
1561 let expected = vec![
1562 AppCommand::NoOp,
1563 AppCommand::FetchList {
1564 list: for_app(
1565 "fenix",
1566 ExperimentListSource::FromRecipes {
1567 recipes: vec![experiment("my-experiment-1"), experiment("my-experiment-2")],
1568 },
1569 ),
1570 file,
1571 },
1572 ];
1573 assert_eq!(expected, observed);
1574 Ok(())
1575 }
1576
1577 #[test]
1578 fn test_fetch_list() -> Result<()> {
1579 let file = Some(PathBuf::from("./archived.json"));
1580 let observed =
1581 get_commands_from_cli(["nimbus-cli", "fetch-list", "--output", "./archived.json"])?;
1582
1583 let expected = vec![
1584 AppCommand::NoOp,
1585 AppCommand::FetchList {
1586 list: ExperimentListSource::FromRemoteSettings {
1587 endpoint: config::rs_production_server(),
1588 is_preview: false,
1589 },
1590 file: file.clone(),
1591 },
1592 ];
1593 assert_eq!(expected, observed);
1594
1595 let observed = get_commands_from_cli([
1596 "nimbus-cli",
1597 "--app",
1598 "fenix",
1599 "fetch-list",
1600 "--output",
1601 "./archived.json",
1602 ])?;
1603
1604 let expected = vec![
1605 AppCommand::NoOp,
1606 AppCommand::FetchList {
1607 list: for_app(
1608 "fenix",
1609 ExperimentListSource::FromRemoteSettings {
1610 endpoint: config::rs_production_server(),
1611 is_preview: false,
1612 },
1613 ),
1614 file: file.clone(),
1615 },
1616 ];
1617 assert_eq!(expected, observed);
1618
1619 let observed = get_commands_from_cli([
1620 "nimbus-cli",
1621 "fetch-list",
1622 "--output",
1623 "./archived.json",
1624 "stage",
1625 ])?;
1626
1627 let expected = vec![
1628 AppCommand::NoOp,
1629 AppCommand::FetchList {
1630 list: ExperimentListSource::FromRemoteSettings {
1631 endpoint: config::rs_stage_server(),
1632 is_preview: false,
1633 },
1634 file: file.clone(),
1635 },
1636 ];
1637 assert_eq!(expected, observed);
1638
1639 let observed = get_commands_from_cli([
1640 "nimbus-cli",
1641 "fetch-list",
1642 "--output",
1643 "./archived.json",
1644 "preview",
1645 ])?;
1646
1647 let expected = vec![
1648 AppCommand::NoOp,
1649 AppCommand::FetchList {
1650 list: ExperimentListSource::FromRemoteSettings {
1651 endpoint: config::rs_production_server(),
1652 is_preview: true,
1653 },
1654 file: file.clone(),
1655 },
1656 ];
1657 assert_eq!(expected, observed);
1658
1659 let observed = get_commands_from_cli([
1660 "nimbus-cli",
1661 "fetch-list",
1662 "--output",
1663 "./archived.json",
1664 "--use-api",
1665 ])?;
1666
1667 let expected = vec![
1668 AppCommand::NoOp,
1669 AppCommand::FetchList {
1670 list: ExperimentListSource::FromApiV6 {
1671 endpoint: config::api_v6_production_server(),
1672 },
1673 file: file.clone(),
1674 },
1675 ];
1676 assert_eq!(expected, observed);
1677
1678 let observed = get_commands_from_cli([
1679 "nimbus-cli",
1680 "fetch-list",
1681 "--use-api",
1682 "--output",
1683 "./archived.json",
1684 "stage",
1685 ])?;
1686
1687 let expected = vec![
1688 AppCommand::NoOp,
1689 AppCommand::FetchList {
1690 list: ExperimentListSource::FromApiV6 {
1691 endpoint: config::api_v6_stage_server(),
1692 },
1693 file,
1694 },
1695 ];
1696 assert_eq!(expected, observed);
1697
1698 Ok(())
1699 }
1700
1701 #[test]
1702 fn test_list() -> Result<()> {
1703 let observed = get_commands_from_cli(["nimbus-cli", "list"])?;
1704 let expected = vec![
1705 AppCommand::NoOp,
1706 AppCommand::List {
1707 list: ExperimentListSource::FromRemoteSettings {
1708 endpoint: config::rs_production_server(),
1709 is_preview: false,
1710 },
1711 },
1712 ];
1713 assert_eq!(expected, observed);
1714
1715 let observed = get_commands_from_cli(["nimbus-cli", "list", "preview"])?;
1716 let expected = vec![
1717 AppCommand::NoOp,
1718 AppCommand::List {
1719 list: ExperimentListSource::FromRemoteSettings {
1720 endpoint: config::rs_production_server(),
1721 is_preview: true,
1722 },
1723 },
1724 ];
1725 assert_eq!(expected, observed);
1726
1727 let observed = get_commands_from_cli(["nimbus-cli", "list", "stage"])?;
1728 let expected = vec![
1729 AppCommand::NoOp,
1730 AppCommand::List {
1731 list: ExperimentListSource::FromRemoteSettings {
1732 endpoint: config::rs_stage_server(),
1733 is_preview: false,
1734 },
1735 },
1736 ];
1737 assert_eq!(expected, observed);
1738
1739 let observed = get_commands_from_cli(["nimbus-cli", "list", "--use-api", "stage"])?;
1740 let expected = vec![
1741 AppCommand::NoOp,
1742 AppCommand::List {
1743 list: ExperimentListSource::FromApiV6 {
1744 endpoint: config::api_v6_stage_server(),
1745 },
1746 },
1747 ];
1748 assert_eq!(expected, observed);
1749
1750 let observed = get_commands_from_cli(["nimbus-cli", "list", "--use-api"])?;
1751 let expected = vec![
1752 AppCommand::NoOp,
1753 AppCommand::List {
1754 list: ExperimentListSource::FromApiV6 {
1755 endpoint: config::api_v6_production_server(),
1756 },
1757 },
1758 ];
1759 assert_eq!(expected, observed);
1760 Ok(())
1761 }
1762
1763 #[test]
1764 fn test_list_filter() -> Result<()> {
1765 let observed = get_commands_from_cli(["nimbus-cli", "--app", "my-app", "list"])?;
1766 let expected = vec![
1767 AppCommand::NoOp,
1768 AppCommand::List {
1769 list: for_app(
1770 "my-app",
1771 ExperimentListSource::FromRemoteSettings {
1772 endpoint: config::rs_production_server(),
1773 is_preview: false,
1774 },
1775 ),
1776 },
1777 ];
1778 assert_eq!(expected, observed);
1779
1780 let observed = get_commands_from_cli(["nimbus-cli", "list", "--feature", "messaging"])?;
1781 let expected = vec![
1782 AppCommand::NoOp,
1783 AppCommand::List {
1784 list: for_feature(
1785 "messaging",
1786 ExperimentListSource::FromRemoteSettings {
1787 endpoint: config::rs_production_server(),
1788 is_preview: false,
1789 },
1790 ),
1791 },
1792 ];
1793 assert_eq!(expected, observed);
1794
1795 let observed = get_commands_from_cli([
1796 "nimbus-cli",
1797 "--app",
1798 "my-app",
1799 "list",
1800 "--feature",
1801 "messaging",
1802 ])?;
1803 let expected = vec![
1804 AppCommand::NoOp,
1805 AppCommand::List {
1806 list: for_app(
1807 "my-app",
1808 for_feature(
1809 "messaging",
1810 ExperimentListSource::FromRemoteSettings {
1811 endpoint: config::rs_production_server(),
1812 is_preview: false,
1813 },
1814 ),
1815 ),
1816 },
1817 ];
1818 assert_eq!(expected, observed);
1819
1820 Ok(())
1821 }
1822
1823 #[test]
1824 fn test_list_filter_by_date_with_error() -> Result<()> {
1825 let observed = get_commands_from_cli(["nimbus-cli", "list", "--active-on", "FOO"]);
1826 assert!(observed.is_err());
1827 let err = observed.unwrap_err();
1828 assert!(err.to_string().contains("Date string must be yyyy-mm-dd"));
1829
1830 let observed = get_commands_from_cli(["nimbus-cli", "list", "--enrolling-on", "FOO"]);
1831 assert!(observed.is_err());
1832 let err = observed.unwrap_err();
1833 assert!(err.to_string().contains("Date string must be yyyy-mm-dd"));
1834 Ok(())
1835 }
1836
1837 #[test]
1838 fn test_list_filter_by_dates() -> Result<()> {
1839 let today = "1970-01-01";
1840
1841 let observed = get_commands_from_cli(["nimbus-cli", "list", "--active-on", today])?;
1842 let expected = vec![
1843 AppCommand::NoOp,
1844 AppCommand::List {
1845 list: for_active_on_date(
1846 today,
1847 ExperimentListSource::FromRemoteSettings {
1848 endpoint: config::rs_production_server(),
1849 is_preview: false,
1850 },
1851 ),
1852 },
1853 ];
1854 assert_eq!(expected, observed);
1855
1856 let observed = get_commands_from_cli(["nimbus-cli", "list", "--enrolling-on", today])?;
1857 let expected = vec![
1858 AppCommand::NoOp,
1859 AppCommand::List {
1860 list: for_enrolling_on_date(
1861 today,
1862 ExperimentListSource::FromRemoteSettings {
1863 endpoint: config::rs_production_server(),
1864 is_preview: false,
1865 },
1866 ),
1867 },
1868 ];
1869 assert_eq!(expected, observed);
1870
1871 Ok(())
1872 }
1873}