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