mongo_task_generator/task_types/
resmoke_tasks.rs

1//! Service for generating resmoke tasks.
2//!
3//! This service will query the historic runtime of tests in the given task and then
4//! use that information to divide the tests into sub-suites that can be run in parallel.
5//!
6//! Each task will contain the generated sub-suites.
7use 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/// Parameters describing how a specific resmoke suite should be generated.
41#[derive(Clone, Debug, Default)]
42pub struct ResmokeGenParams {
43    /// Name of task being generated.
44    pub task_name: String,
45    /// Name of suite being generated.
46    pub suite_name: String,
47    /// Should the generated tasks run on a 'large' distro.
48    pub use_large_distro: bool,
49    /// Does this task require multiversion setup.
50    pub require_multiversion_setup: bool,
51    /// Should multiversion combinations be used for this task.
52    pub generate_multiversion_combos: bool,
53    /// Specify how many times resmoke should repeat the suite being tested.
54    pub repeat_suites: Option<u64>,
55    /// Arguments that should be passed to resmoke.
56    pub resmoke_args: String,
57    /// Number of jobs to limit resmoke to.
58    pub resmoke_jobs_max: Option<u64>,
59    /// Location where generated task configuration will be stored in S3.
60    pub config_location: String,
61    /// List of tasks generated sub-tasks should depend on.
62    pub dependencies: Vec<String>,
63    /// Is this task for enterprise builds.
64    pub is_enterprise: bool,
65    /// Arguments to pass to 'run tests' function.
66    pub pass_through_vars: Option<HashMap<String, ParamValue>>,
67    /// Name of platform the task will run on.
68    pub platform: Option<String>,
69}
70
71impl ResmokeGenParams {
72    /// Build the vars to send to the tasks in the 'run tests' function.
73    ///
74    /// # Arguments
75    ///
76    /// * `suite_file` - Name of suite file to run.
77    ///
78    /// # Returns
79    ///
80    /// Map of arguments to pass to 'run tests' function.
81    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    /// Build the resmoke arguments to use for a generate sub-task.
125    ///
126    /// # Arguments
127    ///
128    /// * `exclude_tags` - Resmoke tags to exclude.
129    /// * `origin_suite` - Suite the generated suite is based on.
130    ///
131    /// # Returns
132    ///
133    /// String of arguments to pass to resmoke.
134    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    /// Build the dependency structure to use the the generated sub-tasks.
157    ///
158    /// # Returns
159    ///
160    /// List of `TaskDependency`s for generated tasks.
161    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/// Representation of generated sub-suite.
179#[derive(Clone, Debug, Default)]
180pub struct SubSuite {
181    /// Index value of generated suite.
182    pub index: usize,
183
184    /// Name of generated sub-suite.
185    pub name: String,
186
187    /// List of tests belonging to sub-suite.
188    pub test_list: Vec<String>,
189
190    /// Suite the generated suite is based off.
191    pub origin_suite: String,
192
193    /// List of tests that should be excluded from sub-suite.
194    pub exclude_test_list: Option<Vec<String>>,
195
196    /// Multiversion exclude tags.
197    pub mv_exclude_tags: Option<String>,
198
199    /// Is sub-suite for a enterprise build_variant.
200    pub is_enterprise: bool,
201
202    /// Platform of build_variant the sub-suite is for.
203    pub platform: Option<String>,
204}
205
206/// Information needed to generate resmoke configuration files for the generated task.
207#[derive(Clone, Debug)]
208pub struct ResmokeSuiteGenerationInfo {
209    /// Name of task being generated.
210    pub task_name: String,
211
212    /// Name of resmoke suite generated task is based on.
213    pub origin_suite: String,
214
215    /// List of generated sub-suites comprising task.
216    pub sub_suites: Vec<SubSuite>,
217
218    /// If true, sub-tasks should be generated for multiversion combinations.
219    pub generate_multiversion_combos: bool,
220}
221
222/// Representation of a generated resmoke suite.
223#[derive(Clone, Debug)]
224pub struct GeneratedResmokeSuite {
225    /// Name of display task to create.
226    pub task_name: String,
227
228    /// Sub suites that comprise generated task.
229    pub sub_suites: Vec<EvgTask>,
230
231    /// If true, run generated task on a large distro.
232    pub use_large_distro: bool,
233}
234
235impl GeneratedSuite for GeneratedResmokeSuite {
236    /// Get the display name to use for the generated task.
237    fn display_name(&self) -> String {
238        self.task_name.clone()
239    }
240
241    /// Get the list of sub-tasks that comprise the generated task.
242    fn sub_tasks(&self) -> Vec<EvgTask> {
243        self.sub_suites.clone()
244    }
245
246    // If true, run generated task on a large distro.
247    fn use_large_distro(&self) -> bool {
248        self.use_large_distro
249    }
250}
251
252/// A service for generating resmoke tasks.
253#[async_trait]
254pub trait GenResmokeTaskService: Sync + Send {
255    /// Generate a task for running the given task in parallel.
256    ///
257    /// # Arguments
258    ///
259    /// * `param` - Parameters for how task should be generated.
260    /// * `build_variant` - Build variant to base task splitting on.
261    ///
262    /// # Returns
263    ///
264    /// A generated suite representing the split task.
265    async fn generate_resmoke_task(
266        &self,
267        params: &ResmokeGenParams,
268        build_variant: &str,
269    ) -> Result<Box<dyn GeneratedSuite>>;
270
271    /// Build a shrub task to execute a split sub-task.
272    ///
273    /// # Arguments
274    ///
275    /// * `sub_suite` - Sub task to create task for.
276    /// * `params` - Parameters for how task should be generated.
277    ///
278    /// # Returns
279    ///
280    /// A shrub task to execute the given sub-suite.
281    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    /// Max number of suites to split tasks into.
293    n_suites: usize,
294
295    /// Disable evergreen task-history queries and use task splitting fallback.
296    use_task_split_fallback: bool,
297
298    /// Enterprise directory.
299    enterprise_dir: Option<String>,
300}
301
302impl GenResmokeConfig {
303    /// Create a new GenResmokeConfig.
304    ///
305    /// # Arguments
306    ///
307    /// * `n_suite` - Number of sub-suites to split tasks into.
308    /// * `use_task_split_fallback` - Disable evergreen task-history queries and use task
309    ///    splitting fallback.
310    /// * `enterprise_dir` - Directory enterprise files are stored in.
311    ///
312    /// # Returns
313    ///
314    /// New instance of `GenResmokeConfig`.
315    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/// Implementation of service to generate resmoke tasks.
329#[derive(Clone)]
330pub struct GenResmokeTaskServiceImpl {
331    /// Service to query task runtime history.
332    task_history_service: Arc<dyn TaskHistoryService>,
333
334    /// Test discovery service.
335    test_discovery: Arc<dyn TestDiscovery>,
336
337    /// Actor to create resmoke configuration files.
338    resmoke_config_actor: Arc<Mutex<dyn ResmokeConfigActor>>,
339
340    /// Service for generating multiversion configurations.
341    multiversion_service: Arc<dyn MultiversionService>,
342
343    /// Service to interact with file system.
344    fs_service: Arc<dyn FsService>,
345
346    /// Configuration for generating resmoke tasks.
347    config: GenResmokeConfig,
348}
349
350impl GenResmokeTaskServiceImpl {
351    /// Create a new instance of the service implementation.
352    ///
353    /// # Arguments
354    ///
355    /// * `task_history_service` - An instance of the service to query task history.
356    /// * `test_discovery` - An instance of the service to query tests belonging to a task.
357    /// * `fs_service` - An instance of the service too work with the file system.
358    /// * `gen_resmoke_config` - Configuration for how resmoke tasks should be generated.
359    ///
360    /// # Returns
361    ///
362    /// New instance of GenResmokeTaskService.
363    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    /// Split the given task into a number of sub-tasks for parallel execution.
384    ///
385    /// # Arguments
386    ///
387    /// * `params` - Parameters for how tasks should be generated.
388    /// * `task_stats` - Statistics on the historic runtimes of tests in the task.
389    /// * `multiversion_name` - Name of task if performing multiversion generation.
390    /// * `multiversion_tags` - Tag to include when performing multiversion generation.
391    ///
392    /// # Returns
393    ///
394    /// A list of sub-suites to run the tests is the given task.
395    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(&params.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            &params.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(&params.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    /// Get the list of tests belonging to the suite being generated.
458    ///
459    /// # Arguments
460    ///
461    /// * `params` - Parameters about the suite being split.
462    ///
463    /// # Returns
464    ///
465    /// List of tests belonging to suite being split.
466    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(&params.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    /// Split a task with no historic runtime information.
491    ///
492    /// Since we don't have any runtime information, we will just split the tests evenly among
493    /// the number of suites we want to create.
494    ///
495    /// # Arguments
496    ///
497    /// * `params` - Parameters for how tasks should be generated.
498    /// * `multiversion_name` - Name of task if performing multiversion generation.
499    /// * `multiversion_tags` - Tag to include when performing multiversion generation.
500    ///
501    /// # Returns
502    ///
503    /// A list of sub-suites to run the tests is the given task.
504    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(&params.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(&params.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(&params.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    /// Create version of the generated sub-tasks for all the multiversion combinations.
553    ///
554    /// # Arguments
555    ///
556    /// * `params` - Parameters for how task should be generated.
557    /// * `build_variant` - Build variant to base generation off.
558    ///
559    /// # Returns
560    ///
561    /// List of sub-suites that includes versions fall all multiversion combinations.
562    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(&params.suite_name)?
571        {
572            let multiversion_name = self.multiversion_service.name_multiversion_suite(
573                &params.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    /// Create sub-suites based on the given information.
593    ///
594    /// # Arguments
595    ///
596    /// * `params` - Parameters for how tasks should be generated.
597    /// * `build_variant` - Name of build variant to base generation off.
598    /// * `multiversion_name` - Name of task if performing multiversion generation.
599    /// * `multiversion_tags` - Tag to include when performing multiversion generation.
600    ///
601    /// # Returns
602    ///
603    /// List of sub-suites that were generated.
604    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(&params.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                    // If we couldn't get the task history, then fallback to splitting the tests evenly
633                    // among the desired number of sub-suites.
634                    self.split_task_fallback(params, multiversion_name, multiversion_tags.clone())?
635                }
636            }
637        };
638
639        Ok(sub_suites)
640    }
641}
642
643/// Sort tests by historic runtime descending.
644///
645/// Tests without historic runtime data will be placed at the end of the list.
646///
647/// # Arguments
648///
649/// * `test_list` - List of tests.
650/// * `task_stats` - Historic task stats.
651///
652/// # Returns
653///
654/// List of sorted tests by historic runtime.
655fn 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
684/// Get the index of sub suite with the least total runtime of tests.
685///
686/// # Arguments
687///
688/// * `running_runtimes` - Total runtimes of tests of sub suites.
689///
690/// # Returns
691///
692/// Index of sub suite with the least total runtime.
693fn 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    /// Generate a task for running the given task in parallel.
706    ///
707    /// # Arguments
708    ///
709    /// * `params` - Parameters for how task should be generated.
710    /// * `build_variant` - Build variant to base task splitting on.
711    ///
712    /// # Returns
713    ///
714    /// A generated suite representing the split task.
715    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    /// Build a shrub task to execute a split sub-task.
748    ///
749    /// # Arguments
750    ///
751    /// * `sub_suite` - Sub task to create task for.
752    /// * `params` - Parameters for how task should be generated.
753    ///
754    /// # Returns
755    ///
756    /// A shrub task to execute the given sub-suite.
757    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(&params.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
791/// Create a list of commands to run a resmoke task in evergreen.
792///
793/// # Arguments
794///
795/// * `run_test_fn_name` - Name of function to run tests.
796/// * `run_test_vars` - Variable to pass to the run tests function.
797/// * `requires_multiversion` - Does this task require multiversion setup.
798///
799/// # Returns
800///
801/// A list of evergreen commands comprising the task.
802fn 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    // ResmokeGenParams tests.
840    #[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    // split_task tests
1004    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        // In this test we will create 3 subtasks with 6 tests. The first sub task should contain
1132        // a single test. The second 2 tests and the third 3 tests. We will set the test runtimes
1133        // to make this happen.
1134        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(&params, &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(&params, &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                &params,
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    // split_task_fallback tests
1255
1256    #[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(&params, 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    // tests for get_test_list.
1300    #[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(&params, 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(&params, 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    // create_multiversion_combinations tests.
1382    #[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(&params, "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    // generate_resmoke_task tests.
1440    #[tokio::test]
1441    async fn test_generate_resmoke_tasks() {
1442        // In this test we will create 3 subtasks with 6 tests. The first sub task should contain
1443        // a single test. The second 2 tests and the third 3 tests. We will set the test runtimes
1444        // to make this happen.
1445        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(&params, "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    // resmoke_commands tests.
1479    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    // sort_tests_by_runtime tests.
1507    #[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    // get_min_index tests.
1549    #[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}