1use std::{cmp::min, collections::HashMap, sync::Arc};
8
9use anyhow::Result;
10use async_trait::async_trait;
11use maplit::hashmap;
12use rand::{prelude::SliceRandom, thread_rng};
13use shrub_rs::models::{
14 commands::{fn_call, fn_call_with_params, EvgCommand},
15 params::ParamValue,
16 task::{EvgTask, TaskDependency},
17};
18use tokio::sync::Mutex;
19use tracing::{event, warn, Level};
20
21use crate::{
22 evergreen::evg_task_history::{
23 get_test_name, TaskHistoryService, TaskRuntimeHistory, TestRuntimeHistory,
24 },
25 evergreen_names::{
26 ADD_GIT_TAG, CONFIGURE_EVG_API_CREDS, DO_MULTIVERSION_SETUP, DO_SETUP,
27 GEN_TASK_CONFIG_LOCATION, GET_PROJECT_WITH_NO_MODULES, MULTIVERSION_EXCLUDE_TAG,
28 MULTIVERSION_EXCLUDE_TAGS_FILE, REQUIRE_MULTIVERSION_SETUP, RESMOKE_ARGS, RESMOKE_JOBS_MAX,
29 RUN_GENERATED_TESTS, SUITE_NAME,
30 },
31 resmoke::resmoke_proxy::TestDiscovery,
32 utils::{fs_service::FsService, task_name::name_generated_task},
33};
34
35use super::{
36 generated_suite::GeneratedSuite, multiversion::MultiversionService,
37 resmoke_config_writer::ResmokeConfigActor,
38};
39
40#[derive(Clone, Debug, Default)]
42pub struct ResmokeGenParams {
43 pub task_name: String,
45 pub suite_name: String,
47 pub use_large_distro: bool,
49 pub require_multiversion_setup: bool,
51 pub generate_multiversion_combos: bool,
53 pub repeat_suites: Option<u64>,
55 pub resmoke_args: String,
57 pub resmoke_jobs_max: Option<u64>,
59 pub config_location: String,
61 pub dependencies: Vec<String>,
63 pub is_enterprise: bool,
65 pub pass_through_vars: Option<HashMap<String, ParamValue>>,
67 pub platform: Option<String>,
69}
70
71impl ResmokeGenParams {
72 fn build_run_test_vars(
82 &self,
83 suite_file: &str,
84 sub_suite: &SubSuite,
85 exclude_tags: &str,
86 suite_override: Option<String>,
87 ) -> HashMap<String, ParamValue> {
88 let mut run_test_vars: HashMap<String, ParamValue> = hashmap! {};
89 if let Some(pass_through_vars) = &self.pass_through_vars {
90 run_test_vars.extend(pass_through_vars.clone());
91 }
92
93 let resmoke_args = self.build_resmoke_args(exclude_tags, &sub_suite.origin_suite);
94 let suite = if let Some(suite_override) = suite_override {
95 suite_override
96 } else {
97 format!("generated_resmoke_config/{}.yml", suite_file)
98 };
99
100 run_test_vars.extend(hashmap! {
101 REQUIRE_MULTIVERSION_SETUP.to_string() => ParamValue::from(self.require_multiversion_setup),
102 RESMOKE_ARGS.to_string() => ParamValue::from(resmoke_args.as_str()),
103 SUITE_NAME.to_string() => ParamValue::from(suite.as_str()),
104 GEN_TASK_CONFIG_LOCATION.to_string() => ParamValue::from(self.config_location.as_str()),
105 });
106
107 if let Some(mv_exclude_tags) = &sub_suite.mv_exclude_tags {
108 run_test_vars.insert(
109 MULTIVERSION_EXCLUDE_TAG.to_string(),
110 ParamValue::from(mv_exclude_tags.as_str()),
111 );
112 }
113
114 if let Some(resmoke_jobs_max) = self.resmoke_jobs_max {
115 run_test_vars.insert(
116 RESMOKE_JOBS_MAX.to_string(),
117 ParamValue::from(resmoke_jobs_max),
118 );
119 }
120
121 run_test_vars
122 }
123
124 fn build_resmoke_args(&self, exclude_tags: &str, origin_suite: &str) -> String {
135 let suffix = if self.require_multiversion_setup {
136 format!(
137 "--tagFile=generated_resmoke_config/{} --excludeWithAnyTags={}",
138 MULTIVERSION_EXCLUDE_TAGS_FILE, exclude_tags
139 )
140 } else {
141 "".to_string()
142 };
143
144 let repeat_arg = if let Some(repeat) = self.repeat_suites {
145 format!("--repeatSuites={}", repeat)
146 } else {
147 "".to_string()
148 };
149
150 format!(
151 "--originSuite={} {} {} {}",
152 origin_suite, repeat_arg, suffix, self.resmoke_args
153 )
154 }
155
156 fn get_dependencies(&self) -> Option<Vec<TaskDependency>> {
162 if self.dependencies.is_empty() {
163 None
164 } else {
165 Some(
166 self.dependencies
167 .iter()
168 .map(|d| TaskDependency {
169 name: d.to_string(),
170 variant: None,
171 })
172 .collect(),
173 )
174 }
175 }
176}
177
178#[derive(Clone, Debug, Default)]
180pub struct SubSuite {
181 pub index: usize,
183
184 pub name: String,
186
187 pub test_list: Vec<String>,
189
190 pub origin_suite: String,
192
193 pub exclude_test_list: Option<Vec<String>>,
195
196 pub mv_exclude_tags: Option<String>,
198
199 pub is_enterprise: bool,
201
202 pub platform: Option<String>,
204}
205
206#[derive(Clone, Debug)]
208pub struct ResmokeSuiteGenerationInfo {
209 pub task_name: String,
211
212 pub origin_suite: String,
214
215 pub sub_suites: Vec<SubSuite>,
217
218 pub generate_multiversion_combos: bool,
220}
221
222#[derive(Clone, Debug)]
224pub struct GeneratedResmokeSuite {
225 pub task_name: String,
227
228 pub sub_suites: Vec<EvgTask>,
230
231 pub use_large_distro: bool,
233}
234
235impl GeneratedSuite for GeneratedResmokeSuite {
236 fn display_name(&self) -> String {
238 self.task_name.clone()
239 }
240
241 fn sub_tasks(&self) -> Vec<EvgTask> {
243 self.sub_suites.clone()
244 }
245
246 fn use_large_distro(&self) -> bool {
248 self.use_large_distro
249 }
250}
251
252#[async_trait]
254pub trait GenResmokeTaskService: Sync + Send {
255 async fn generate_resmoke_task(
266 &self,
267 params: &ResmokeGenParams,
268 build_variant: &str,
269 ) -> Result<Box<dyn GeneratedSuite>>;
270
271 fn build_resmoke_sub_task(
282 &self,
283 sub_suite: &SubSuite,
284 total_sub_suites: usize,
285 params: &ResmokeGenParams,
286 suite_override: Option<String>,
287 ) -> EvgTask;
288}
289
290#[derive(Debug, Clone)]
291pub struct GenResmokeConfig {
292 n_suites: usize,
294
295 use_task_split_fallback: bool,
297
298 enterprise_dir: Option<String>,
300}
301
302impl GenResmokeConfig {
303 pub fn new(
316 n_suites: usize,
317 use_task_split_fallback: bool,
318 enterprise_dir: Option<String>,
319 ) -> Self {
320 Self {
321 n_suites,
322 use_task_split_fallback,
323 enterprise_dir,
324 }
325 }
326}
327
328#[derive(Clone)]
330pub struct GenResmokeTaskServiceImpl {
331 task_history_service: Arc<dyn TaskHistoryService>,
333
334 test_discovery: Arc<dyn TestDiscovery>,
336
337 resmoke_config_actor: Arc<Mutex<dyn ResmokeConfigActor>>,
339
340 multiversion_service: Arc<dyn MultiversionService>,
342
343 fs_service: Arc<dyn FsService>,
345
346 config: GenResmokeConfig,
348}
349
350impl GenResmokeTaskServiceImpl {
351 pub fn new(
364 task_history_service: Arc<dyn TaskHistoryService>,
365 test_discovery: Arc<dyn TestDiscovery>,
366 resmoke_config_actor: Arc<Mutex<dyn ResmokeConfigActor>>,
367 multiversion_service: Arc<dyn MultiversionService>,
368 fs_service: Arc<dyn FsService>,
369 config: GenResmokeConfig,
370 ) -> Self {
371 Self {
372 task_history_service,
373 test_discovery,
374 resmoke_config_actor,
375 multiversion_service,
376 fs_service,
377 config,
378 }
379 }
380}
381
382impl GenResmokeTaskServiceImpl {
383 fn split_task(
396 &self,
397 params: &ResmokeGenParams,
398 task_stats: &TaskRuntimeHistory,
399 multiversion_name: Option<&str>,
400 multiversion_tags: Option<String>,
401 ) -> Result<Vec<SubSuite>> {
402 let origin_suite = multiversion_name.unwrap_or(¶ms.suite_name);
403 let test_list = self.get_test_list(params, multiversion_name)?;
404 let total_runtime = task_stats
405 .test_map
406 .iter()
407 .fold(0.0, |init, (_, item)| init + item.average_runtime);
408
409 let max_tasks = min(self.config.n_suites, test_list.len());
410 let runtime_per_subtask = total_runtime / max_tasks as f64;
411 event!(
412 Level::INFO,
413 "Splitting task: {}, runtime: {}, tests: {}",
414 ¶ms.suite_name,
415 runtime_per_subtask,
416 test_list.len()
417 );
418
419 let sorted_test_list = sort_tests_by_runtime(test_list, task_stats);
420 let mut running_tests = vec![vec![]; max_tasks];
421 let mut running_runtimes = vec![0.0; max_tasks];
422 let mut left_tests = vec![];
423
424 for test in sorted_test_list {
425 let min_idx = get_min_index(&running_runtimes);
426 let test_name = get_test_name(&test);
427 if let Some(test_stats) = task_stats.test_map.get(&test_name) {
428 running_runtimes[min_idx] += test_stats.average_runtime;
429 running_tests[min_idx].push(test.clone());
430 } else {
431 left_tests.push(test.clone());
432 }
433 }
434
435 let min_idx = get_min_index(&running_runtimes);
436 for (i, test) in left_tests.iter().enumerate() {
437 running_tests[(min_idx + i) % max_tasks].push(test.clone());
438 }
439
440 let mut sub_suites = vec![];
441 for (i, slice) in running_tests.iter().enumerate() {
442 sub_suites.push(SubSuite {
443 index: i,
444 name: multiversion_name.unwrap_or(¶ms.task_name).to_string(),
445 test_list: slice.clone(),
446 origin_suite: origin_suite.to_string(),
447 exclude_test_list: None,
448 mv_exclude_tags: multiversion_tags.clone(),
449 is_enterprise: params.is_enterprise,
450 platform: params.platform.clone(),
451 });
452 }
453
454 Ok(sub_suites)
455 }
456
457 fn get_test_list(
467 &self,
468 params: &ResmokeGenParams,
469 multiversion_name: Option<&str>,
470 ) -> Result<Vec<String>> {
471 let suite_name = multiversion_name.unwrap_or(¶ms.suite_name);
472 let mut test_list: Vec<String> = self
473 .test_discovery
474 .discover_tests(suite_name)?
475 .into_iter()
476 .filter(|s| self.fs_service.file_exists(s))
477 .collect();
478
479 if !params.is_enterprise {
480 if let Some(enterprise_dir) = &self.config.enterprise_dir {
481 test_list.retain(|s| !s.starts_with(enterprise_dir));
482 }
483 }
484
485 test_list.shuffle(&mut thread_rng());
486
487 Ok(test_list)
488 }
489
490 fn split_task_fallback(
505 &self,
506 params: &ResmokeGenParams,
507 multiversion_name: Option<&str>,
508 multiversion_tags: Option<String>,
509 ) -> Result<Vec<SubSuite>> {
510 let origin_suite = multiversion_name.unwrap_or(¶ms.suite_name);
511 let test_list = self.get_test_list(params, multiversion_name)?;
512 let n_suites = min(test_list.len(), self.config.n_suites);
513 let tasks_per_suite = test_list.len() / n_suites;
514
515 let mut sub_suites = vec![];
516 let mut current_tests = vec![];
517 let mut i = 0;
518 for test in test_list {
519 current_tests.push(test);
520 if current_tests.len() >= tasks_per_suite {
521 sub_suites.push(SubSuite {
522 index: i,
523 name: multiversion_name.unwrap_or(¶ms.task_name).to_string(),
524 test_list: current_tests,
525 origin_suite: origin_suite.to_string(),
526 exclude_test_list: None,
527 mv_exclude_tags: multiversion_tags.clone(),
528 is_enterprise: params.is_enterprise,
529 platform: params.platform.clone(),
530 });
531 current_tests = vec![];
532 i += 1;
533 }
534 }
535
536 if !current_tests.is_empty() {
537 sub_suites.push(SubSuite {
538 index: i,
539 name: multiversion_name.unwrap_or(¶ms.task_name).to_string(),
540 test_list: current_tests,
541 origin_suite: origin_suite.to_string(),
542 exclude_test_list: None,
543 mv_exclude_tags: multiversion_tags,
544 is_enterprise: params.is_enterprise,
545 platform: params.platform.clone(),
546 });
547 }
548
549 Ok(sub_suites)
550 }
551
552 async fn create_multiversion_combinations(
563 &self,
564 params: &ResmokeGenParams,
565 build_variant: &str,
566 ) -> Result<Vec<SubSuite>> {
567 let mut mv_sub_suites = vec![];
568 for (old_version, version_combination) in self
569 .multiversion_service
570 .multiversion_iter(¶ms.suite_name)?
571 {
572 let multiversion_name = self.multiversion_service.name_multiversion_suite(
573 ¶ms.suite_name,
574 &old_version,
575 &version_combination,
576 );
577 let multiversion_tags = Some(old_version.clone());
578 let suites = self
579 .create_tasks(
580 params,
581 build_variant,
582 Some(&multiversion_name),
583 multiversion_tags,
584 )
585 .await?;
586 mv_sub_suites.extend_from_slice(&suites);
587 }
588
589 Ok(mv_sub_suites)
590 }
591
592 async fn create_tasks(
605 &self,
606 params: &ResmokeGenParams,
607 build_variant: &str,
608 multiversion_name: Option<&str>,
609 multiversion_tags: Option<String>,
610 ) -> Result<Vec<SubSuite>> {
611 let sub_suites = if self.config.use_task_split_fallback {
612 self.split_task_fallback(params, multiversion_name, multiversion_tags.clone())?
613 } else {
614 let task_history = self
615 .task_history_service
616 .get_task_history(¶ms.task_name, build_variant)
617 .await;
618
619 match task_history {
620 Ok(task_history) => self.split_task(
621 params,
622 &task_history,
623 multiversion_name,
624 multiversion_tags.clone(),
625 )?,
626 Err(err) => {
627 warn!(
628 task_name = params.task_name.as_str(),
629 error = err.to_string().as_str(),
630 "Could not get task history from evergreen",
631 );
632 self.split_task_fallback(params, multiversion_name, multiversion_tags.clone())?
635 }
636 }
637 };
638
639 Ok(sub_suites)
640 }
641}
642
643fn sort_tests_by_runtime(
656 test_list: Vec<String>,
657 task_stats: &TaskRuntimeHistory,
658) -> Vec<std::string::String> {
659 let mut sorted_test_list = test_list;
660 sorted_test_list.sort_by(|test_file_a, test_file_b| {
661 let test_name_a = get_test_name(test_file_a);
662 let test_name_b = get_test_name(test_file_b);
663 let default_runtime = TestRuntimeHistory {
664 test_name: "default".to_string(),
665 average_runtime: 0.0,
666 hooks: vec![],
667 };
668 let runtime_history_a = task_stats
669 .test_map
670 .get(&test_name_a)
671 .unwrap_or(&default_runtime);
672 let runtime_history_b = task_stats
673 .test_map
674 .get(&test_name_b)
675 .unwrap_or(&default_runtime);
676 runtime_history_b
677 .average_runtime
678 .partial_cmp(&runtime_history_a.average_runtime)
679 .unwrap()
680 });
681 sorted_test_list
682}
683
684fn get_min_index(running_runtimes: &[f64]) -> usize {
694 let mut min_idx = 0;
695 for (i, value) in running_runtimes.iter().enumerate() {
696 if value < &running_runtimes[min_idx] {
697 min_idx = i;
698 }
699 }
700 min_idx
701}
702
703#[async_trait]
704impl GenResmokeTaskService for GenResmokeTaskServiceImpl {
705 async fn generate_resmoke_task(
716 &self,
717 params: &ResmokeGenParams,
718 build_variant: &str,
719 ) -> Result<Box<dyn GeneratedSuite>> {
720 let sub_suites = if params.generate_multiversion_combos {
721 self.create_multiversion_combinations(params, build_variant)
722 .await?
723 } else {
724 self.create_tasks(params, build_variant, None, None).await?
725 };
726
727 let sub_task_total = sub_suites.len();
728 let suite_info = ResmokeSuiteGenerationInfo {
729 task_name: params.task_name.to_string(),
730 origin_suite: params.suite_name.to_string(),
731 sub_suites: sub_suites.clone(),
732 generate_multiversion_combos: params.generate_multiversion_combos,
733 };
734 let mut resmoke_config_actor = self.resmoke_config_actor.lock().await;
735 resmoke_config_actor.write_sub_suite(&suite_info).await;
736
737 Ok(Box::new(GeneratedResmokeSuite {
738 task_name: params.task_name.clone(),
739 sub_suites: sub_suites
740 .into_iter()
741 .map(|s| self.build_resmoke_sub_task(&s, sub_task_total, params, None))
742 .collect(),
743 use_large_distro: params.use_large_distro,
744 }))
745 }
746
747 fn build_resmoke_sub_task(
758 &self,
759 sub_suite: &SubSuite,
760 total_sub_suites: usize,
761 params: &ResmokeGenParams,
762 suite_override: Option<String>,
763 ) -> EvgTask {
764 let exclude_tags = self
765 .multiversion_service
766 .exclude_tags_for_task(¶ms.task_name, sub_suite.mv_exclude_tags.clone());
767 let suite_file = name_generated_task(
768 &sub_suite.name,
769 sub_suite.index,
770 total_sub_suites,
771 params.is_enterprise,
772 params.platform.as_deref(),
773 );
774
775 let run_test_vars =
776 params.build_run_test_vars(&suite_file, sub_suite, &exclude_tags, suite_override);
777
778 EvgTask {
779 name: suite_file,
780 commands: Some(resmoke_commands(
781 RUN_GENERATED_TESTS,
782 run_test_vars,
783 params.require_multiversion_setup,
784 )),
785 depends_on: params.get_dependencies(),
786 ..Default::default()
787 }
788 }
789}
790
791fn resmoke_commands(
803 run_test_fn_name: &str,
804 run_test_vars: HashMap<String, ParamValue>,
805 requires_multiversion_setup: bool,
806) -> Vec<EvgCommand> {
807 let mut commands = vec![];
808
809 if requires_multiversion_setup {
810 commands.push(fn_call(GET_PROJECT_WITH_NO_MODULES));
811 commands.push(fn_call(ADD_GIT_TAG));
812 }
813
814 commands.push(fn_call(DO_SETUP));
815 commands.push(fn_call(CONFIGURE_EVG_API_CREDS));
816
817 if requires_multiversion_setup {
818 commands.push(fn_call(DO_MULTIVERSION_SETUP));
819 }
820
821 commands.push(fn_call_with_params(run_test_fn_name, run_test_vars));
822 commands
823}
824
825#[cfg(test)]
826mod tests {
827 use rstest::rstest;
828
829 use crate::{
830 evergreen::evg_task_history::TestRuntimeHistory,
831 resmoke::{resmoke_proxy::MultiversionConfig, resmoke_suite::ResmokeSuiteConfig},
832 task_types::multiversion::MultiversionIterator,
833 };
834
835 use super::*;
836
837 const MOCK_ENTERPRISE_DIR: &str = "src/enterprise";
838
839 #[test]
841 fn test_build_run_test_vars() {
842 let params = ResmokeGenParams {
843 suite_name: "my_suite".to_string(),
844 resmoke_args: "resmoke args".to_string(),
845 pass_through_vars: Some(hashmap! {
846 "suite".to_string() => ParamValue::from("my_suite"),
847 "resmoke_args".to_string() => ParamValue::from("resmoke args"),
848 }),
849 ..Default::default()
850 };
851 let sub_suite = SubSuite {
852 ..Default::default()
853 };
854
855 let test_vars = params.build_run_test_vars("my_suite_0", &sub_suite, "", None);
856
857 assert_eq!(test_vars.len(), 4);
858 assert!(!test_vars.contains_key("resmoke_jobs_max"));
859 assert_eq!(
860 test_vars.get("suite").unwrap(),
861 &ParamValue::from("generated_resmoke_config/my_suite_0.yml")
862 );
863 }
864
865 #[test]
866 fn test_build_run_test_vars_with_resmoke_jobs() {
867 let params = ResmokeGenParams {
868 suite_name: "my_suite".to_string(),
869 resmoke_args: "resmoke args".to_string(),
870 resmoke_jobs_max: Some(5),
871 pass_through_vars: Some(hashmap! {
872 "suite".to_string() => ParamValue::from("my_suite"),
873 "resmoke_args".to_string() => ParamValue::from("resmoke args"),
874 "resmoke_jobs_max".to_string() => ParamValue::from(5),
875 }),
876 ..Default::default()
877 };
878 let sub_suite = SubSuite {
879 ..Default::default()
880 };
881
882 let test_vars = params.build_run_test_vars("my_suite_0", &sub_suite, "", None);
883
884 assert_eq!(test_vars.len(), 5);
885 assert_eq!(
886 test_vars.get("resmoke_jobs_max").unwrap(),
887 &ParamValue::from(5)
888 );
889 assert_eq!(
890 test_vars.get("suite").unwrap(),
891 &ParamValue::from("generated_resmoke_config/my_suite_0.yml")
892 );
893 }
894
895 #[test]
896 fn test_build_run_test_vars_for_multiversion() {
897 let params = ResmokeGenParams {
898 suite_name: "my_suite".to_string(),
899 resmoke_args: "resmoke args".to_string(),
900 require_multiversion_setup: true,
901 pass_through_vars: Some(hashmap! {
902 "suite".to_string() => ParamValue::from("my_suite"),
903 "resmoke_args".to_string() => ParamValue::from("resmoke args"),
904 }),
905 ..Default::default()
906 };
907 let sub_suite = SubSuite {
908 mv_exclude_tags: Some("last_lts".to_string()),
909 origin_suite: "my_origin_suite".to_string(),
910 ..Default::default()
911 };
912
913 let test_vars =
914 params.build_run_test_vars("my_suite_0", &sub_suite, "tag_0,tag_1,tag_2", None);
915
916 assert_eq!(test_vars.len(), 5);
917 assert_eq!(
918 test_vars.get("multiversion_exclude_tags_version").unwrap(),
919 &ParamValue::from("last_lts")
920 );
921 assert_eq!(
922 test_vars.get("resmoke_args").unwrap(),
923 &ParamValue::from("--originSuite=my_origin_suite --tagFile=generated_resmoke_config/multiversion_exclude_tags.yml --excludeWithAnyTags=tag_0,tag_1,tag_2 resmoke args")
924 );
925 }
926
927 #[test]
928 fn test_build_run_test_vars_with_pass_through_params() {
929 let params = ResmokeGenParams {
930 suite_name: "my_suite".to_string(),
931 resmoke_args: "resmoke args".to_string(),
932 pass_through_vars: Some(hashmap! {
933 "suite".to_string() => ParamValue::from("my_suite"),
934 "resmoke_args".to_string() => ParamValue::from("resmoke args"),
935 "multiversion_exclude_tags_version".to_string() => ParamValue::from("last_lts"),
936 }),
937 ..Default::default()
938 };
939 let sub_suite = SubSuite {
940 ..Default::default()
941 };
942
943 let test_vars = params.build_run_test_vars("my_suite_0", &sub_suite, "", None);
944
945 assert_eq!(test_vars.len(), 5);
946 assert_eq!(
947 test_vars.get("multiversion_exclude_tags_version").unwrap(),
948 &ParamValue::from("last_lts")
949 );
950 assert_eq!(
951 test_vars.get("suite").unwrap(),
952 &ParamValue::from("generated_resmoke_config/my_suite_0.yml")
953 );
954 }
955
956 #[test]
957 fn test_build_run_test_vars_pass_through_params_does_are_overridden() {
958 let params = ResmokeGenParams {
959 suite_name: "my_suite".to_string(),
960 resmoke_args: "resmoke args".to_string(),
961 pass_through_vars: Some(hashmap! {
962 "suite".to_string() => ParamValue::from("my_suite"),
963 "resmoke_args".to_string() => ParamValue::from("resmoke args"),
964 "multiversion_exclude_tags_version".to_string() => ParamValue::from("last_continuous"),
965 }),
966 ..Default::default()
967 };
968 let sub_suite = SubSuite {
969 mv_exclude_tags: Some("last_lts".to_string()),
970 origin_suite: "my_origin_suite".to_string(),
971 ..Default::default()
972 };
973
974 let test_vars = params.build_run_test_vars("my_suite_0", &sub_suite, "", None);
975
976 assert_eq!(test_vars.len(), 5);
977 assert_eq!(
978 test_vars.get("multiversion_exclude_tags_version").unwrap(),
979 &ParamValue::from("last_lts")
980 );
981 assert_eq!(
982 test_vars.get("suite").unwrap(),
983 &ParamValue::from("generated_resmoke_config/my_suite_0.yml")
984 );
985 }
986
987 #[test]
988 fn test_build_resmoke_args() {
989 let params = ResmokeGenParams {
990 suite_name: "my_suite".to_string(),
991 resmoke_args: "--args to --pass to resmoke".to_string(),
992 repeat_suites: Some(3),
993 ..Default::default()
994 };
995
996 let resmoke_args = params.build_resmoke_args("", "my_origin_suite");
997
998 assert!(resmoke_args.contains("--originSuite=my_origin_suite"));
999 assert!(resmoke_args.contains("--args to --pass to resmoke"));
1000 assert!(resmoke_args.contains("--repeatSuites=3"));
1001 }
1002
1003 struct MockTaskHistoryService {
1005 task_history: TaskRuntimeHistory,
1006 }
1007
1008 #[async_trait]
1009 impl TaskHistoryService for MockTaskHistoryService {
1010 async fn get_task_history(
1011 &self,
1012 _task: &str,
1013 _variant: &str,
1014 ) -> Result<TaskRuntimeHistory> {
1015 Ok(self.task_history.clone())
1016 }
1017 }
1018
1019 struct MockTestDiscovery {
1020 test_list: Vec<String>,
1021 }
1022
1023 impl TestDiscovery for MockTestDiscovery {
1024 fn discover_tests(&self, _suite_name: &str) -> Result<Vec<String>> {
1025 Ok(self.test_list.clone())
1026 }
1027
1028 fn get_suite_config(&self, _suite_name: &str) -> Result<ResmokeSuiteConfig> {
1029 todo!()
1030 }
1031
1032 fn get_multiversion_config(&self) -> Result<MultiversionConfig> {
1033 todo!()
1034 }
1035 }
1036
1037 struct MockFsService {}
1038 impl FsService for MockFsService {
1039 fn file_exists(&self, _path: &str) -> bool {
1040 true
1041 }
1042
1043 fn write_file(&self, _path: &std::path::Path, _contents: &str) -> Result<()> {
1044 Ok(())
1045 }
1046 }
1047
1048 struct MockResmokeConfigActor {}
1049 #[async_trait]
1050 impl ResmokeConfigActor for MockResmokeConfigActor {
1051 async fn write_sub_suite(&mut self, _gen_suite: &ResmokeSuiteGenerationInfo) {}
1052
1053 async fn flush(&mut self) -> Result<Vec<String>> {
1054 Ok(vec![])
1055 }
1056 }
1057
1058 struct MockMultiversionService {
1059 old_version: Vec<String>,
1060 version_combos: Vec<String>,
1061 }
1062 impl MultiversionService for MockMultiversionService {
1063 fn get_version_combinations(&self, _suite_name: &str) -> Result<Vec<String>> {
1064 todo!()
1065 }
1066
1067 fn multiversion_iter(
1068 &self,
1069 _version_combinations: &str,
1070 ) -> Result<crate::task_types::multiversion::MultiversionIterator> {
1071 Ok(MultiversionIterator::new(
1072 &self.old_version,
1073 &self.version_combos,
1074 ))
1075 }
1076
1077 fn name_multiversion_suite(
1078 &self,
1079 base_name: &str,
1080 old_version: &str,
1081 version_combination: &str,
1082 ) -> String {
1083 format!("{}_{}_{}", base_name, old_version, version_combination)
1084 }
1085
1086 fn exclude_tags_for_task(&self, _task_name: &str, _mv_mode: Option<String>) -> String {
1087 "tag_0,tag_1".to_string()
1088 }
1089 }
1090
1091 fn build_mocked_service(
1092 test_list: Vec<String>,
1093 task_history: TaskRuntimeHistory,
1094 n_suites: usize,
1095 old_version: Vec<String>,
1096 version_combos: Vec<String>,
1097 ) -> GenResmokeTaskServiceImpl {
1098 let test_discovery = MockTestDiscovery { test_list };
1099 let multiversion_service = MockMultiversionService {
1100 old_version,
1101 version_combos,
1102 };
1103 let task_history_service = MockTaskHistoryService {
1104 task_history: task_history.clone(),
1105 };
1106 let fs_service = MockFsService {};
1107 let resmoke_config_actor = MockResmokeConfigActor {};
1108
1109 let config = GenResmokeConfig::new(n_suites, false, Some(MOCK_ENTERPRISE_DIR.to_string()));
1110
1111 GenResmokeTaskServiceImpl::new(
1112 Arc::new(task_history_service),
1113 Arc::new(test_discovery),
1114 Arc::new(Mutex::new(resmoke_config_actor)),
1115 Arc::new(multiversion_service),
1116 Arc::new(fs_service),
1117 config,
1118 )
1119 }
1120
1121 fn build_mock_test_runtime(test_name: &str, runtime: f64) -> TestRuntimeHistory {
1122 TestRuntimeHistory {
1123 test_name: test_name.to_string(),
1124 average_runtime: runtime,
1125 hooks: vec![],
1126 }
1127 }
1128
1129 #[test]
1130 fn test_split_task_should_split_tasks_by_runtime() {
1131 let n_suites = 3;
1135 let test_list: Vec<String> = (0..6)
1136 .into_iter()
1137 .map(|i| format!("test_{}.js", i))
1138 .collect();
1139 let task_history = TaskRuntimeHistory {
1140 task_name: "my task".to_string(),
1141 test_map: hashmap! {
1142 "test_0".to_string() => build_mock_test_runtime("test_0.js", 100.0),
1143 "test_1".to_string() => build_mock_test_runtime("test_1.js", 56.0),
1144 "test_2".to_string() => build_mock_test_runtime("test_2.js", 50.0),
1145 "test_3".to_string() => build_mock_test_runtime("test_3.js", 35.0),
1146 "test_4".to_string() => build_mock_test_runtime("test_4.js", 34.0),
1147 "test_5".to_string() => build_mock_test_runtime("test_5.js", 30.0),
1148 },
1149 };
1150 let gen_resmoke_service = build_mocked_service(
1151 test_list.clone(),
1152 task_history.clone(),
1153 n_suites,
1154 vec![],
1155 vec![],
1156 );
1157
1158 let params = ResmokeGenParams {
1159 ..Default::default()
1160 };
1161
1162 let sub_suites = gen_resmoke_service
1163 .split_task(¶ms, &task_history, None, None)
1164 .unwrap();
1165
1166 assert_eq!(sub_suites.len(), n_suites);
1167 let suite_0 = &sub_suites[0];
1168 assert!(suite_0.test_list.contains(&"test_0.js".to_string()));
1169 let suite_1 = &sub_suites[1];
1170 assert!(suite_1.test_list.contains(&"test_1.js".to_string()));
1171 assert!(suite_1.test_list.contains(&"test_4.js".to_string()));
1172 let suite_2 = &sub_suites[2];
1173 assert!(suite_2.test_list.contains(&"test_2.js".to_string()));
1174 assert!(suite_2.test_list.contains(&"test_3.js".to_string()));
1175 assert!(suite_2.test_list.contains(&"test_5.js".to_string()));
1176 }
1177
1178 #[test]
1179 fn test_split_task_with_missing_history_should_split_tasks_equally() {
1180 let n_suites = 3;
1181 let test_list: Vec<String> = (0..12)
1182 .into_iter()
1183 .map(|i| format!("test_{}.js", i))
1184 .collect();
1185 let task_history = TaskRuntimeHistory {
1186 task_name: "my task".to_string(),
1187 test_map: hashmap! {
1188 "test_0".to_string() => build_mock_test_runtime("test_0.js", 100.0),
1189 "test_1".to_string() => build_mock_test_runtime("test_1.js", 50.0),
1190 "test_2".to_string() => build_mock_test_runtime("test_2.js", 50.0),
1191 },
1192 };
1193 let gen_resmoke_service =
1194 build_mocked_service(test_list, task_history.clone(), n_suites, vec![], vec![]);
1195
1196 let params = ResmokeGenParams {
1197 ..Default::default()
1198 };
1199
1200 let sub_suites = gen_resmoke_service
1201 .split_task(¶ms, &task_history, None, None)
1202 .unwrap();
1203
1204 assert_eq!(sub_suites.len(), n_suites);
1205 let suite_0 = &sub_suites[0];
1206 assert_eq!(suite_0.test_list.len(), 4);
1207 let suite_1 = &sub_suites[1];
1208 assert_eq!(suite_1.test_list.len(), 4);
1209 let suite_2 = &sub_suites[2];
1210 assert_eq!(suite_2.test_list.len(), 4);
1211 }
1212
1213 #[test]
1214 fn test_split_tasks_should_include_multiversion_information() {
1215 let n_suites = 3;
1216 let test_list: Vec<String> = (0..3)
1217 .into_iter()
1218 .map(|i| format!("test_{}.js", i))
1219 .collect();
1220 let task_history = TaskRuntimeHistory {
1221 task_name: "my task".to_string(),
1222 test_map: hashmap! {
1223 "test_0".to_string() => build_mock_test_runtime("test_0.js", 100.0),
1224 "test_1".to_string() => build_mock_test_runtime("test_1.js", 50.0),
1225 "test_2".to_string() => build_mock_test_runtime("test_2.js", 50.0),
1226 },
1227 };
1228 let gen_resmoke_service =
1229 build_mocked_service(test_list, task_history.clone(), n_suites, vec![], vec![]);
1230
1231 let params = ResmokeGenParams {
1232 ..Default::default()
1233 };
1234
1235 let sub_suites = gen_resmoke_service
1236 .split_task(
1237 ¶ms,
1238 &task_history,
1239 Some("multiversion_test"),
1240 Some("multiversion_tag".to_string()),
1241 )
1242 .unwrap();
1243
1244 assert_eq!(sub_suites.len(), n_suites);
1245 for sub_suite in sub_suites {
1246 assert_eq!(sub_suite.name, "multiversion_test");
1247 assert_eq!(
1248 sub_suite.mv_exclude_tags,
1249 Some("multiversion_tag".to_string())
1250 );
1251 }
1252 }
1253
1254 #[test]
1257 fn test_split_task_fallback_should_split_tasks_count() {
1258 let n_suites = 3;
1259 let n_tests = 6;
1260 let test_list: Vec<String> = (0..n_tests)
1261 .into_iter()
1262 .map(|i| format!("test_{}.js", i))
1263 .collect();
1264 let task_history = TaskRuntimeHistory {
1265 task_name: "my task".to_string(),
1266 test_map: hashmap! {},
1267 };
1268 let gen_resmoke_service = build_mocked_service(
1269 test_list.clone(),
1270 task_history.clone(),
1271 n_suites,
1272 vec![],
1273 vec![],
1274 );
1275
1276 let params = ResmokeGenParams {
1277 ..Default::default()
1278 };
1279
1280 let sub_suites = gen_resmoke_service
1281 .split_task_fallback(¶ms, None, None)
1282 .unwrap();
1283
1284 assert_eq!(sub_suites.len(), n_suites);
1285 for sub_suite in &sub_suites {
1286 assert_eq!(sub_suite.test_list.len(), n_tests / n_suites);
1287 }
1288
1289 let all_tests: Vec<String> = sub_suites
1290 .iter()
1291 .flat_map(|s| s.test_list.clone())
1292 .collect();
1293 assert_eq!(all_tests.len(), n_tests);
1294 for test_name in test_list {
1295 assert!(all_tests.contains(&test_name.to_string()));
1296 }
1297 }
1298
1299 #[rstest]
1301 #[case(true, 12)]
1302 #[case(false, 6)]
1303 fn test_get_test_list_should_filter_enterprise_tests(
1304 #[case] is_enterprise: bool,
1305 #[case] expected_tests: usize,
1306 ) {
1307 let n_suites = 3;
1308 let mut test_list: Vec<String> = (0..6)
1309 .into_iter()
1310 .map(|i| format!("test_{}.js", i))
1311 .collect();
1312 test_list.extend::<Vec<String>>(
1313 (6..12)
1314 .into_iter()
1315 .map(|i| format!("{}/test_{}.js", MOCK_ENTERPRISE_DIR, i))
1316 .collect(),
1317 );
1318 let task_history = TaskRuntimeHistory {
1319 task_name: "my task".to_string(),
1320 test_map: hashmap! {},
1321 };
1322 let gen_resmoke_service =
1323 build_mocked_service(test_list, task_history.clone(), n_suites, vec![], vec![]);
1324
1325 let params = ResmokeGenParams {
1326 is_enterprise,
1327 ..Default::default()
1328 };
1329
1330 let sub_suites = gen_resmoke_service
1331 .split_task_fallback(¶ms, None, None)
1332 .unwrap();
1333 let all_tests: Vec<String> = sub_suites
1334 .iter()
1335 .flat_map(|s| s.test_list.clone())
1336 .collect();
1337 assert_eq!(expected_tests, all_tests.len());
1338 }
1339
1340 #[rstest]
1341 #[case(true, 12)]
1342 #[case(false, 12)]
1343 fn test_get_test_list_should_work_with_missing_enterprise_details(
1344 #[case] is_enterprise: bool,
1345 #[case] expected_tests: usize,
1346 ) {
1347 let n_suites = 3;
1348 let mut test_list: Vec<String> = (0..6)
1349 .into_iter()
1350 .map(|i| format!("test_{}.js", i))
1351 .collect();
1352 test_list.extend::<Vec<String>>(
1353 (6..12)
1354 .into_iter()
1355 .map(|i| format!("{}/test_{}.js", MOCK_ENTERPRISE_DIR, i))
1356 .collect(),
1357 );
1358 let task_history = TaskRuntimeHistory {
1359 task_name: "my task".to_string(),
1360 test_map: hashmap! {},
1361 };
1362 let mut gen_resmoke_service =
1363 build_mocked_service(test_list, task_history.clone(), n_suites, vec![], vec![]);
1364 gen_resmoke_service.config.enterprise_dir = None;
1365
1366 let params = ResmokeGenParams {
1367 is_enterprise,
1368 ..Default::default()
1369 };
1370
1371 let sub_suites = gen_resmoke_service
1372 .split_task_fallback(¶ms, None, None)
1373 .unwrap();
1374 let all_tests: Vec<String> = sub_suites
1375 .iter()
1376 .flat_map(|s| s.test_list.clone())
1377 .collect();
1378 assert_eq!(expected_tests, all_tests.len());
1379 }
1380
1381 #[tokio::test]
1383 async fn test_create_multiversion_combinations() {
1384 let old_version = vec!["last_lts".to_string(), "continuous".to_string()];
1385 let version_combos = vec!["new_new_new".to_string(), "old_new_old".to_string()];
1386 let sub_suites = vec![
1387 SubSuite {
1388 index: 0,
1389 name: "suite".to_string(),
1390 origin_suite: "suite".to_string(),
1391 test_list: vec!["test_0.js".to_string(), "test_1.js".to_string()],
1392 ..Default::default()
1393 },
1394 SubSuite {
1395 index: 1,
1396 name: "suite".to_string(),
1397 origin_suite: "suite".to_string(),
1398 test_list: vec!["test_2.js".to_string(), "test_3.js".to_string()],
1399 ..Default::default()
1400 },
1401 ];
1402 let params = ResmokeGenParams {
1403 suite_name: "suite".to_string(),
1404 ..Default::default()
1405 };
1406 let task_history = TaskRuntimeHistory {
1407 task_name: "my task".to_string(),
1408 test_map: hashmap! {},
1409 };
1410 let gen_resmoke_service = build_mocked_service(
1411 vec![
1412 "test_0.js".to_string(),
1413 "test_1.js".to_string(),
1414 "test_2.js".to_string(),
1415 "test_3.js".to_string(),
1416 ],
1417 task_history,
1418 1,
1419 old_version.clone(),
1420 version_combos.clone(),
1421 );
1422
1423 let suite_list = gen_resmoke_service
1424 .create_multiversion_combinations(¶ms, "build_variant")
1425 .await
1426 .unwrap();
1427
1428 for version in old_version {
1429 for combo in &version_combos {
1430 for sub_suite in &sub_suites {
1431 let sub_task_name = format!("{}_{}_{}", &sub_suite.name, version, combo);
1432 let suite = suite_list.iter().find(|s| s.name == sub_task_name);
1433 assert!(suite.is_some());
1434 }
1435 }
1436 }
1437 }
1438
1439 #[tokio::test]
1441 async fn test_generate_resmoke_tasks() {
1442 let n_suites = 3;
1446 let test_list: Vec<String> = (0..6)
1447 .into_iter()
1448 .map(|i| format!("test_{}.js", i))
1449 .collect();
1450 let task_history = TaskRuntimeHistory {
1451 task_name: "my_task".to_string(),
1452 test_map: hashmap! {
1453 "test_0".to_string() => build_mock_test_runtime("test_0.js", 100.0),
1454 "test_1".to_string() => build_mock_test_runtime("test_1.js", 50.0),
1455 "test_2".to_string() => build_mock_test_runtime("test_2.js", 50.0),
1456 "test_3".to_string() => build_mock_test_runtime("test_3.js", 34.0),
1457 "test_4".to_string() => build_mock_test_runtime("test_4.js", 34.0),
1458 "test_5".to_string() => build_mock_test_runtime("test_5.js", 34.0),
1459 },
1460 };
1461 let gen_resmoke_service =
1462 build_mocked_service(test_list, task_history.clone(), n_suites, vec![], vec![]);
1463
1464 let params = ResmokeGenParams {
1465 task_name: "my_task".to_string(),
1466 ..Default::default()
1467 };
1468
1469 let suite = gen_resmoke_service
1470 .generate_resmoke_task(¶ms, "build-variant")
1471 .await
1472 .unwrap();
1473
1474 assert_eq!(suite.display_name(), "my_task".to_string());
1475 assert_eq!(suite.sub_tasks().len(), n_suites);
1476 }
1477
1478 fn get_evg_fn_name(evg_command: &EvgCommand) -> Option<&str> {
1480 if let EvgCommand::Function(func) = evg_command {
1481 Some(&func.func)
1482 } else {
1483 None
1484 }
1485 }
1486
1487 #[test]
1488 fn test_resmoke_commands() {
1489 let commands = resmoke_commands("run test", hashmap! {}, false);
1490
1491 assert_eq!(commands.len(), 3);
1492 assert_eq!(get_evg_fn_name(&commands[0]), Some("do setup"));
1493 assert_eq!(get_evg_fn_name(&commands[2]), Some("run test"));
1494 }
1495
1496 #[test]
1497 fn test_resmoke_commands_should_include_multiversion() {
1498 let commands = resmoke_commands("run test", hashmap! {}, true);
1499
1500 assert_eq!(commands.len(), 6);
1501 assert_eq!(get_evg_fn_name(&commands[2]), Some("do setup"));
1502 assert_eq!(get_evg_fn_name(&commands[4]), Some("do multiversion setup"));
1503 assert_eq!(get_evg_fn_name(&commands[5]), Some("run test"));
1504 }
1505
1506 #[rstest]
1508 #[case(vec![100.0, 50.0, 30.0, 25.0, 20.0, 15.0], vec![0, 1, 2, 3, 4, 5])]
1509 #[case(vec![15.0, 20.0, 25.0, 30.0, 50.0, 100.0], vec![5, 4, 3, 2, 1, 0])]
1510 #[case(vec![15.0, 50.0, 25.0, 30.0, 20.0, 100.0], vec![5, 1, 3, 2, 4, 0])]
1511 #[case(vec![100.0, 50.0, 30.0], vec![0, 1, 2, 3, 4, 5])]
1512 #[case(vec![30.0, 50.0, 100.0], vec![2, 1, 0, 3, 4, 5])]
1513 #[case(vec![30.0, 100.0, 50.0], vec![1, 2, 0, 3, 4, 5])]
1514 #[case(vec![], vec![0, 1, 2, 3, 4, 5])]
1515 fn test_sort_tests_by_runtime(
1516 #[case] historic_runtimes: Vec<f64>,
1517 #[case] sorted_indexes: Vec<i32>,
1518 ) {
1519 let test_list: Vec<String> = (0..sorted_indexes.len())
1520 .into_iter()
1521 .map(|i| format!("test_{}.js", i))
1522 .collect();
1523 let task_stats = TaskRuntimeHistory {
1524 task_name: "my_task".to_string(),
1525 test_map: (0..historic_runtimes.len())
1526 .into_iter()
1527 .map(|i| {
1528 (
1529 format!("test_{}", i),
1530 build_mock_test_runtime(
1531 format!("test_{}.js", i).as_ref(),
1532 historic_runtimes[i],
1533 ),
1534 )
1535 })
1536 .collect::<HashMap<_, _>>(),
1537 };
1538 let expected_result: Vec<String> = (0..sorted_indexes.len())
1539 .into_iter()
1540 .map(|i| format!("test_{}.js", sorted_indexes[i]))
1541 .collect();
1542
1543 let result = sort_tests_by_runtime(test_list, &task_stats);
1544
1545 assert_eq!(result, expected_result);
1546 }
1547
1548 #[rstest]
1550 #[case(vec![100.0, 50.0, 30.0, 25.0, 20.0, 15.0], 5)]
1551 #[case(vec![15.0, 20.0, 25.0, 30.0, 50.0, 100.0], 0)]
1552 #[case(vec![25.0, 50.0, 15.0, 30.0, 100.0, 20.0], 2)]
1553 fn test_get_min_index(#[case] running_runtimes: Vec<f64>, #[case] expected_min_idx: usize) {
1554 let min_idx = get_min_index(&running_runtimes);
1555
1556 assert_eq!(min_idx, expected_min_idx);
1557 }
1558}