evobench_tools/run/
config.rs

1use std::{
2    borrow::Cow,
3    collections::{BTreeMap, BTreeSet},
4    fmt::{Debug, Display},
5    path::{Path, PathBuf},
6    str::FromStr,
7    sync::Arc,
8};
9
10use anyhow::{Context, Result, anyhow, bail};
11use chj_unix_util::daemon::DaemonPaths;
12use chrono::{DateTime, Local};
13use cj_path_util::path_util::AppendToPath;
14use kstring::KString;
15
16use crate::{
17    config_file::{ConfigFile, DefaultConfigPath, ron_to_string_pretty},
18    date_and_time::time_ranges::{DateTimeRange, LocalNaiveTimeRange},
19    info,
20    io_utils::{bash::bash_string_from_cmd, div::create_dir_if_not_exists},
21    run::{env_vars::AllowableCustomEnvVar, key::CustomParameters},
22    run_with_pre_exec::{BashSettings, BashSettingsLevel, RunWithPreExec, join_pre_exec_bash_code},
23    serde_types::{
24        allowed_env_var::AllowedEnvVar,
25        date_and_time::LocalNaiveTime,
26        git_branch_name::GitBranchName,
27        git_url::GitUrl,
28        priority::Priority,
29        proper_dirname::ProperDirname,
30        proper_filename::ProperFilename,
31        regex::SerializableRegex,
32        tilde_path::TildePath,
33        val_or_ref::{ValOrRef, ValOrRefTarget},
34    },
35    util::grep_diff::LogExtract,
36    utillib::arc::CloneArc,
37};
38
39use super::{
40    benchmarking_job::BenchmarkingJobSettingsOpts, custom_parameter::AllowedCustomParameter,
41    global_app_state_dir::GlobalAppStateDir, working_directory_pool::WorkingDirectoryPoolOpts,
42};
43
44#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
45#[serde(deny_unknown_fields)]
46pub enum ScheduleCondition {
47    /// Run jobs in this queue once right away
48    Immediately {
49        /// A description of the situation during which jobs in this
50        /// queue are executed; all jobs of the same context (and same
51        /// key) are grouped together and evaluated to "summary-" file
52        /// names with this string appended. Meant to reflect
53        /// conditions that might influence the results;
54        /// e.g. "immediate" or "night".
55        situation: ProperFilename,
56    },
57
58    /// Run jobs in this queue between the given times on every day
59    /// (except when one of the times is not valid or ambiguous on a
60    /// given day due to DST changes). Jobs started before the end of
61    /// the window are finished, though.
62    LocalNaiveTimeWindow {
63        /// The priority of this queue--it is added to the priority of
64        /// jobs in this queue. By default, 1.5 is used.
65        priority: Option<Priority>,
66        /// A description of the situation during which jobs in this
67        /// queue are executed; all jobs of the same context (and same
68        /// key) are grouped together and evaluated to "summary-" file
69        /// names with this string appended. Meant to reflect
70        /// conditions that might influence the results;
71        /// e.g. "immediate" or "night".
72        situation: ProperFilename,
73        /// A command and arguments, run with "stop" at the `from`
74        /// time and with "start" when done / at the `to` time.
75        stop_start: Option<Vec<String>>,
76        /// If true, run the `BenchmarkingJob`s in this queue until
77        /// their own `count` reaches zero or the time window runs out
78        /// (each job is rescheduled to the end of the queue after a
79        /// run, meaning the jobs are alternating). If false, each job
80        /// is run once and then moved to the next queue.
81        repeatedly: bool,
82        /// If true, when time runs out, move all remaining jobs to
83        /// the next queue; if false, the jobs remain and are
84        /// scheduled again in the same time window on the next day.
85        move_when_time_window_ends: bool,
86        /// Times in the time zone that the daemon is running with (to
87        /// change that, set `TZ` env var to area/city, or the default
88        /// time zone via dpkg-reconfigure).
89        from: LocalNaiveTime,
90        to: LocalNaiveTime,
91    },
92
93    /// A queue that is never run and never emptied, to add to the end
94    /// of the queue pipeline to take up jobs that have been expelled
95    /// from the second last queue, for informational purposes.
96    Inactive,
97}
98
99impl Display for ScheduleCondition {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            ScheduleCondition::Immediately { situation } => {
103                write!(f, "Immediately {:?}", situation.as_str())
104            }
105            ScheduleCondition::LocalNaiveTimeWindow {
106                priority: _,
107                situation,
108                stop_start,
109                repeatedly,
110                move_when_time_window_ends,
111                from,
112                to,
113            } => {
114                let rep = if *repeatedly { "repeatedly" } else { "once" };
115                let mov = if *move_when_time_window_ends {
116                    "move"
117                } else {
118                    "stay"
119                };
120                let cmd = if let Some(st) = stop_start {
121                    bash_string_from_cmd(st)
122                } else {
123                    "-".into()
124                };
125                let pri: f64 = self
126                    .priority()
127                    .expect("LocalNaiveTimeWindow *does* have priority field")
128                    .into();
129                write!(
130                    f,
131                    "LocalNaiveTimeWindow {:?} {from} - {to} pri={pri}: {rep}, {mov}, \"{cmd}\"",
132                    situation.as_str()
133                )
134            }
135            ScheduleCondition::Inactive => f.write_str("Inactive"),
136        }
137    }
138}
139
140impl ScheduleCondition {
141    pub const TIMED_QUEUE_DEFAULT_PRIORITY: Priority = Priority::new_unchecked(1.5);
142
143    /// Whether this queue will never run its jobs
144    pub fn is_inactive(&self) -> bool {
145        match self {
146            ScheduleCondition::Inactive => true,
147            _ => false,
148        }
149    }
150
151    pub fn time_range(&self) -> Option<(LocalNaiveTime, LocalNaiveTime)> {
152        match self {
153            ScheduleCondition::Immediately { situation: _ } => None,
154            ScheduleCondition::LocalNaiveTimeWindow {
155                priority: _,
156                situation: _,
157                stop_start: _,
158                repeatedly: _,
159                move_when_time_window_ends: _,
160                from,
161                to,
162            } => Some((from.clone(), to.clone())),
163            ScheduleCondition::Inactive => None,
164        }
165    }
166
167    pub fn stop_start(&self) -> Option<&[String]> {
168        match self {
169            ScheduleCondition::Immediately { situation: _ } => None,
170            ScheduleCondition::LocalNaiveTimeWindow {
171                priority: _,
172                situation: _,
173                stop_start,
174                repeatedly: _,
175                move_when_time_window_ends: _,
176                from: _,
177                to: _,
178            } => stop_start.as_deref(),
179            ScheduleCondition::Inactive => None,
180        }
181    }
182
183    /// Returns true if the condition offers that flag *and* it is true
184    pub fn move_when_time_window_ends(&self) -> bool {
185        match self {
186            ScheduleCondition::Immediately { situation: _ } => false,
187            ScheduleCondition::LocalNaiveTimeWindow {
188                priority: _,
189                situation: _,
190                stop_start: _,
191                repeatedly: _,
192                move_when_time_window_ends,
193                from: _,
194                to: _,
195            } => *move_when_time_window_ends,
196            ScheduleCondition::Inactive => false,
197        }
198    }
199
200    pub fn situation(&self) -> Option<&ProperFilename> {
201        match self {
202            ScheduleCondition::Immediately { situation } => Some(situation),
203            ScheduleCondition::LocalNaiveTimeWindow {
204                priority: _,
205                situation,
206                stop_start: _,
207                repeatedly: _,
208                move_when_time_window_ends: _,
209                from: _,
210                to: _,
211            } => Some(situation),
212            ScheduleCondition::Inactive => None,
213        }
214    }
215
216    pub fn priority(&self) -> Option<Priority> {
217        match self {
218            ScheduleCondition::Immediately { situation: _ } => Some(Priority::default()),
219            ScheduleCondition::LocalNaiveTimeWindow {
220                priority,
221                situation: _,
222                stop_start: _,
223                repeatedly: _,
224                move_when_time_window_ends: _,
225                from: _,
226                to: _,
227            } => Some(priority.unwrap_or(Self::TIMED_QUEUE_DEFAULT_PRIORITY)),
228            ScheduleCondition::Inactive => None,
229        }
230    }
231
232    /// Returns an optional time window (given if runnable due to
233    /// being in this time window) if runnable
234    pub fn is_runnable_at(
235        &self,
236        reference_time: DateTime<Local>,
237    ) -> Option<Option<DateTimeRange<Local>>> {
238        match self {
239            ScheduleCondition::Immediately { situation: _ } => Some(None),
240            ScheduleCondition::LocalNaiveTimeWindow {
241                priority: _,
242                situation: _,
243                stop_start: _,
244                repeatedly: _,
245                move_when_time_window_ends: _,
246                from,
247                to,
248            } => {
249                let ltr = LocalNaiveTimeRange {
250                    from: *from,
251                    to: *to,
252                };
253                let dtr: Option<DateTimeRange<Local>> = ltr.after_datetime(&reference_time, true);
254                if let Some(dtr) = dtr {
255                    if dtr.contains(&reference_time) {
256                        Some(Some(dtr))
257                    } else {
258                        None
259                    }
260                } else {
261                    info!("times in {ltr} do not resolve for {reference_time}");
262                    None
263                }
264            }
265            ScheduleCondition::Inactive => None,
266        }
267    }
268}
269
270#[derive(Debug, serde::Serialize, serde::Deserialize)]
271#[serde(deny_unknown_fields)]
272pub struct QueuesConfig {
273    /// If not given, `~/.evobench/queues/` is used. Also used for
274    /// locking the `run` action of evobench, to ensure only one
275    /// benchmarking job is executed at the same time--if you
276    /// configure multiple such directories then you don't have this
277    /// guarantee any more. Supports `~/`
278    /// for specifying the home directory.
279    pub run_queues_basedir: Option<TildePath<PathBuf>>,
280
281    /// The queues to use (file names, without '/'), and their
282    /// scheduled execution condition
283    pub pipeline: Vec<(ProperFilename, ScheduleCondition)>,
284
285    /// The queue where to put jobs when they run out of
286    /// `error_budget` (if `None` is given, the jobs will be dropped--
287    /// silently unless verbose flag is given). Should be of
288    /// scheduling type Inactive (or perhaps a future messaging
289    /// queue).
290    pub erroneous_jobs_queue: Option<(ProperFilename, ScheduleCondition)>,
291
292    /// The queue where to put jobs when they are finished
293    /// successfully (if `None` is given, the jobs will be dropped--
294    /// silently unless verbose flag is given).
295    pub done_jobs_queue: Option<(ProperFilename, ScheduleCondition)>,
296
297    /// How many jobs to show in the extra queues
298    /// (`erroneous_jobs_queue` and `done_jobs_queue`) when no `--all`
299    /// option is given
300    pub view_jobs_max_len: usize,
301}
302
303impl QueuesConfig {
304    pub fn run_queues_basedir(
305        &self,
306        create_if_not_exists: bool,
307        global_app_state_dir: &GlobalAppStateDir,
308    ) -> Result<PathBuf> {
309        if let Some(base_dir) = &self.run_queues_basedir {
310            let base_dir = base_dir.resolve()?;
311            if create_if_not_exists {
312                create_dir_if_not_exists(&base_dir, "queues base directory")?;
313            }
314            Ok(base_dir)
315        } else {
316            global_app_state_dir.run_queues_basedir()
317        }
318    }
319}
320
321#[derive(serde::Serialize, serde::Deserialize, Debug)]
322#[serde(deny_unknown_fields)]
323#[serde(rename = "RemoteRepository")]
324pub struct RemoteRepositoryOpts {
325    /// The Git repository to clone the target project from
326    pub url: GitUrl,
327
328    /// The remote branches to track
329    pub remote_branch_names_for_poll:
330        BTreeMap<GitBranchName, ValOrRef<JobTemplateListsField, Vec<JobTemplateOpts>>>,
331}
332
333pub struct RemoteRepository {
334    pub url: GitUrl,
335    pub remote_branch_names_for_poll: BTreeMap<GitBranchName, Arc<[JobTemplate]>>,
336}
337
338impl RemoteRepositoryOpts {
339    fn check(
340        &self,
341        job_template_lists: &BTreeMap<KString, Arc<[JobTemplate]>>,
342        targets: &BTreeMap<ProperDirname, Arc<BenchmarkingTarget>>,
343    ) -> Result<RemoteRepository> {
344        let Self {
345            url,
346            remote_branch_names_for_poll,
347        } = self;
348
349        let remote_branch_names_for_poll = remote_branch_names_for_poll
350            .iter()
351            .map(|(branch_name, job_template_optss)| -> Result<_> {
352                let job_templates: ValOrRef<JobTemplateListsField, Arc<[JobTemplate]>> =
353                    job_template_optss.try_map(
354                        |job_template_optss: &Vec<JobTemplateOpts>| -> Result<Arc<[JobTemplate]>> {
355                            job_template_optss
356                                .iter()
357                                .map(|job_template_opts| job_template_opts.check(targets))
358                                .collect()
359                        },
360                    )?;
361                let job_templates = job_templates.value_with_backing(job_template_lists)?;
362                Ok((branch_name.clone(), job_templates.clone_arc()))
363            })
364            .collect::<Result<_>>()?;
365
366        Ok(RemoteRepository {
367            url: url.clone(),
368            remote_branch_names_for_poll,
369        })
370    }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
374#[serde(from = "Option<Arc<str>>", into = "Option<Arc<str>>")]
375pub struct PreExecLevel2(Option<Arc<str>>);
376
377impl From<Option<Arc<str>>> for PreExecLevel2 {
378    fn from(value: Option<Arc<str>>) -> Self {
379        Self(value)
380    }
381}
382
383impl From<PreExecLevel2> for Option<Arc<str>> {
384    fn from(value: PreExecLevel2) -> Self {
385        value.0
386    }
387}
388
389impl PreExecLevel2 {
390    pub fn new(arg: Option<Arc<str>>) -> Self {
391        Self(arg)
392    }
393
394    pub fn to_run_with_pre_exec(&self, conf: &RunConfig) -> RunWithPreExec<'static> {
395        let code = join_pre_exec_bash_code(
396            conf.target_pre_exec_bash_code.as_deref().unwrap_or(""),
397            self.0.as_deref().unwrap_or(""),
398        );
399        RunWithPreExec {
400            pre_exec_bash_code: code.into(),
401            bash_settings: BashSettings {
402                level: BashSettingsLevel::SetMEUPipefail,
403                set_ifs: true,
404            },
405            bash_path: None,
406        }
407    }
408}
409
410#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
411#[serde(deny_unknown_fields)]
412/// What command to run on the target project to execute a
413/// benchmarking run; the env variables configured in CustomParameters
414/// are set when running this command.
415pub struct BenchmarkingCommand {
416    /// The name is matched on the `target_name` field in
417    /// `JobTemplate`, and it is used as the first path segment below
418    /// `output_base_dir` for storing the results. It will also be
419    /// shown by `evobench list`.
420    pub target_name: ProperDirname,
421
422    /// Relative path to the subdirectory (provide "." for the top
423    /// level of the working directory) where to run the command
424    pub subdir: PathBuf,
425
426    /// Name or path to the command to run, e.g. "make"
427    pub command: String,
428
429    /// Arguments to the command, e.g. "bench"
430    pub arguments: Vec<String>,
431
432    /// Bash shell code to run before executing this target. E.g. to
433    /// source a Python virtual env or defining env variables. `set
434    /// -meuo pipefail` and `IFS=` are enabled. Execution flow must
435    /// leave at the end of the string. This is executed after code
436    /// declared via the global `target_pre_exec_bash_code` field, if
437    /// any. Note that this value (together with the other values in
438    /// BenchmarkingCommand) is copied into the job, and
439    /// `evobench wd enter` will use the copy from the time the
440    /// job was started, not the current value here!
441    pub pre_exec_bash_code: PreExecLevel2,
442}
443
444#[derive(Debug, serde::Serialize, serde::Deserialize)]
445#[serde(deny_unknown_fields)]
446pub struct BenchmarkingTarget {
447    pub benchmarking_command: Arc<BenchmarkingCommand>,
448
449    /// Which custom environment variables are allowed, required, and
450    /// of what type (format) they must be.
451    pub allowed_custom_parameters:
452        BTreeMap<AllowedEnvVar<AllowableCustomEnvVar>, AllowedCustomParameter>,
453
454    /// Optional list of `LogExtract` declarations, to extract time
455    /// spans from the stdout/stderr of the benchmark run. (Note: this
456    /// is not and does not include the file optionally written by the
457    /// target application to the path in the `BENCH_OUTPUT_LOG` env
458    /// var!--Possible todo: offer something separate for that file?)
459    pub log_extracts: Option<Vec<LogExtract>>,
460}
461
462#[derive(Debug, serde::Serialize, serde::Deserialize)]
463#[serde(deny_unknown_fields)]
464#[serde(rename = "JobTemplate")]
465pub struct JobTemplateOpts {
466    priority: Priority,
467    initial_boost: Priority,
468    target_name: ProperDirname,
469    // Using `String` for values--type checking is done in conversion
470    // to `JobTemplate` (don't want to use another enum here that
471    // would be required, and `allowed_custom_parameters` already have
472    // the type, no *need* to specify it again, OK?)
473    custom_parameters: BTreeMap<AllowedEnvVar<AllowableCustomEnvVar>, KString>,
474}
475
476pub struct JobTemplate {
477    pub priority: Priority,
478    pub initial_boost: Priority,
479    pub command: Arc<BenchmarkingCommand>,
480    pub custom_parameters: Arc<CustomParameters>,
481}
482
483impl JobTemplateOpts {
484    pub fn check(
485        &self,
486        targets: &BTreeMap<ProperDirname, Arc<BenchmarkingTarget>>,
487    ) -> Result<JobTemplate> {
488        let Self {
489            priority,
490            initial_boost,
491            target_name,
492            custom_parameters,
493        } = self;
494
495        let target = targets
496            .get(target_name)
497            .ok_or_else(|| anyhow!("unknown target name {:?}", target_name.as_str()))?;
498
499        let custom_parameters =
500            CustomParameters::checked_from(custom_parameters, &target.allowed_custom_parameters)
501                .with_context(|| {
502                    let context = ron_to_string_pretty(self).expect("no serialisation errors");
503                    anyhow!("processing {context}")
504                })?;
505
506        Ok(JobTemplate {
507            priority: *priority,
508            initial_boost: *initial_boost,
509            command: target.benchmarking_command.clone_arc(),
510            custom_parameters: custom_parameters.into(),
511        })
512    }
513}
514
515/// Settings for calling `evobench-eval`
516#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
517#[serde(deny_unknown_fields)]
518pub struct EvalSettings {
519    /// Pass the --show-thread-number option to evobench-eval
520    /// ("Include the internally-allocated thread number in call
521    /// path strings in the output"). Only use if the application
522    /// has a limited number of threads (i.e. uses fixed thread
523    /// pools); if it allocates new threads all the time then this
524    /// will blow up the resulting Excel files.
525    pub show_thread_number: bool,
526}
527
528#[derive(Debug, serde::Serialize, serde::Deserialize)]
529#[serde(deny_unknown_fields)]
530#[serde(rename = "DaemonPaths")]
531pub struct DaemonPathsOpts {
532    /// Where the lock/pid files should be written to (is created if missing).
533    pub state_dir: Option<TildePath<PathBuf>>,
534    /// Where the log files should be written to (is created if missing).
535    pub log_dir: Option<TildePath<PathBuf>>,
536}
537
538impl DaemonPathsOpts {
539    fn check(
540        &self,
541        global_app_state_dir: &GlobalAppStateDir,
542        default_state_subdir: &str,
543    ) -> Result<DaemonPaths> {
544        let DaemonPathsOpts { state_dir, log_dir } = self;
545
546        let state_dir: Arc<Path> = if let Some(path) = state_dir {
547            path.resolve()?.into()
548        } else {
549            global_app_state_dir.subdir(default_state_subdir)?.into()
550        };
551        let log_dir = if let Some(path) = log_dir {
552            path.resolve()?.into()
553        } else {
554            (&state_dir).append("logs").into()
555        };
556        Ok(DaemonPaths { state_dir, log_dir })
557    }
558}
559
560#[derive(Debug, serde::Serialize, serde::Deserialize)]
561#[serde(deny_unknown_fields)]
562#[serde(rename = "OutputDir")]
563pub struct OutputDirOpts {
564    /// The base of the directory hierarchy where the output files
565    /// should be placed. Supports `~/` for specifying the home
566    /// directory.
567    pub path: Arc<TildePath<PathBuf>>,
568
569    /// URL where the same path is served (optional). Used for
570    /// `evobench list path url`.
571    pub url: Option<Arc<str>>,
572}
573
574impl OutputDirOpts {
575    fn resolve(&self) -> Result<OutputDir> {
576        let Self { path, url } = self;
577        let path = path.resolve()?.into();
578        let url = url.clone();
579        Ok(OutputDir { path, url })
580    }
581}
582
583pub struct OutputDir {
584    pub path: Arc<Path>,
585    pub url: Option<Arc<str>>,
586}
587
588/// Direct representation of the evobench config file
589// For why `Arc` is used, see `docs/hacking.md`
590#[derive(Debug, serde::Serialize, serde::Deserialize)]
591#[serde(deny_unknown_fields)]
592#[serde(rename = "RunConfig")]
593pub struct RunConfigOpts {
594    pub queues: Arc<QueuesConfig>,
595
596    pub working_directory_pool: Arc<WorkingDirectoryPoolOpts>,
597
598    /// Bash shell code to run before executing any of the
599    /// targets. E.g. to source a Python virtual env or defining env
600    /// variables. `set -meuo pipefail` and `IFS=` are
601    /// enabled. Execution flow must leave at the end of the string.
602    pub target_pre_exec_bash_code: Option<Arc<str>>,
603
604    /// What command to run on the target project to execute a
605    /// benchmarking run; the env variables configured in
606    /// CustomParameters are set when running this command.
607    pub targets: Vec<Arc<BenchmarkingTarget>>,
608
609    /// A set of named job template lists, referred to by name from
610    /// `remote_branch_names_for_poll` or from the command line
611    /// (`evobench insert templates`).  Each job template in a
612    /// list generates a separate benchmark run for each commit that
613    /// is inserted. The order defines in which order the jobs are
614    /// inserted (which means the job generated from the first
615    /// template is scheduled first, at least if priorities are the
616    /// same). `priority` is added to whatever priority the inserter
617    /// asks for, and `initial_boost` is added to the job for its
618    /// first run only.
619    pub job_template_lists: BTreeMap<KString, Vec<JobTemplateOpts>>,
620
621    /// Each job receives a copy of these settings after expansion
622    pub benchmarking_job_settings: Arc<BenchmarkingJobSettingsOpts>,
623
624    /// Settings for calling `evobench-eval`
625    pub eval_settings: Arc<EvalSettings>,
626
627    /// Information on the remote repository of the target project
628    pub remote_repository: RemoteRepositoryOpts,
629
630    /// Where the output files should be placed.
631    pub output_dir: OutputDirOpts,
632
633    /// The paths for the `evobench run daemon`. The defaults are
634    /// `~/.evobench/run_jobs_daemon` for the `state_dir` and
635    /// the `logs` subdir below that for `logs_dir`.  The paths
636    /// support `~/` notation.
637    run_jobs_daemon: DaemonPathsOpts,
638
639    /// The same as above for the `evobench poll daemon`, just
640    /// with the `polling_daemon` subdir as the default.
641    polling_daemon: DaemonPathsOpts,
642
643    /// Optional directory holding directories whose name is taken
644    /// from the (optional, depending on the configuration) `DATASET`
645    /// custom variable (hacky to mis-use a custom variable for
646    /// this?), inside which are directories named after git revision
647    /// names (tags or commit ids), the latest which is an ancestor or
648    /// the commit itself to be benchmarked,
649    /// i.e. `$versioned_datasets_base_dir/$DATASET/$best_rev_name`. The
650    /// resolved path (only when both this option and `DATASET` are
651    /// provided) is stored in the `DATASET_DIR` env var when calling
652    /// the benchmarking entry point of the client app. Supports `~/`
653    /// for specifying the home directory.
654    pub versioned_datasets_base_dir: Option<Arc<TildePath<PathBuf>>>,
655
656    /// A regular expression matching those Git tags that should be
657    /// passed to the target in the `COMMIT_TAGS` env variable (as
658    /// comma-separated strings). By default, all tags are passed.
659    pub commit_tags_regex: Option<SerializableRegex>,
660}
661
662#[derive(Debug)]
663pub struct JobTemplateListsField;
664impl ValOrRefTarget for JobTemplateListsField {
665    fn target_desc() -> Cow<'static, str> {
666        "`RunConfig.job_template_lists` field".into()
667    }
668}
669
670impl DefaultConfigPath for RunConfigOpts {
671    fn default_config_file_name_without_suffix() -> Result<Option<ProperFilename>> {
672        Ok(Some("evobench".parse().map_err(|e| anyhow!("{e:#}"))?))
673    }
674}
675
676/// Checked, produced from `RunConfigOpts`, for docs see there.
677pub struct RunConfig {
678    pub queues: Arc<QueuesConfig>,
679    pub run_jobs_daemon: DaemonPaths,
680    pub polling_daemon: DaemonPaths,
681    pub working_directory_pool: Arc<WorkingDirectoryPoolOpts>,
682    pub target_pre_exec_bash_code: Option<Arc<str>>,
683    // targets: BTreeMap<ProperDirname, Arc<BenchmarkingTarget>>,
684    pub job_template_lists: BTreeMap<KString, Arc<[JobTemplate]>>,
685    pub benchmarking_job_settings: Arc<BenchmarkingJobSettingsOpts>,
686    pub eval_settings: Arc<EvalSettings>,
687    pub remote_repository: RemoteRepository,
688    pub output_dir: OutputDir,
689    pub versioned_datasets_base_dir: Option<Arc<Path>>,
690    pub targets: BTreeMap<ProperDirname, Arc<BenchmarkingTarget>>,
691    pub commit_tags_regex: SerializableRegex,
692}
693
694impl RunConfig {
695    pub fn working_directory_change_signals_path(&self) -> PathBuf {
696        (&self.run_jobs_daemon.state_dir).append("working_directory_change.signals")
697    }
698}
699
700impl RunConfigOpts {
701    /// Don't take ownership since RunConfigWithReload can't give it
702    pub fn check(&self, global_app_state_dir: &GlobalAppStateDir) -> Result<RunConfig> {
703        let RunConfigOpts {
704            queues,
705            working_directory_pool,
706            target_pre_exec_bash_code,
707            targets,
708            job_template_lists,
709            benchmarking_job_settings,
710            eval_settings,
711            remote_repository,
712            output_dir,
713            run_jobs_daemon,
714            polling_daemon,
715            versioned_datasets_base_dir,
716            commit_tags_regex,
717        } = self;
718
719        let targets: BTreeMap<ProperDirname, Arc<BenchmarkingTarget>> = {
720            let mut seen = BTreeSet::new();
721            targets
722                .iter()
723                .map(|benchmarking_target| {
724                    let name = &benchmarking_target.benchmarking_command.target_name;
725                    if seen.contains(&name) {
726                        bail!("duplicate `target_name` value {:?}", name.as_str())
727                    }
728                    seen.insert(name);
729                    Ok((name.clone(), benchmarking_target.clone_arc()))
730                })
731                .collect::<Result<_>>()?
732        };
733
734        let job_template_lists: BTreeMap<KString, Arc<[JobTemplate]>> = job_template_lists
735            .iter()
736            .map(
737                |(template_list_name, template_list)| -> Result<(KString, Arc<[JobTemplate]>)> {
738                    Ok((
739                        template_list_name.clone(),
740                        template_list
741                            .iter()
742                            .map(|job_template_opts| job_template_opts.check(&targets))
743                            .collect::<Result<_>>()?,
744                    ))
745                },
746            )
747            .collect::<Result<_>>()?;
748
749        let remote_repository = remote_repository.check(&job_template_lists, &targets)?;
750
751        let commit_tags_regex: SerializableRegex =
752            if let Some(commit_tags_regex) = commit_tags_regex {
753                (*commit_tags_regex).clone()
754            } else {
755                SerializableRegex::from_str(".*")?
756            };
757
758        Ok(RunConfig {
759            queues: queues.clone_arc(),
760            working_directory_pool: working_directory_pool.clone_arc(),
761            target_pre_exec_bash_code: target_pre_exec_bash_code.clone(),
762            job_template_lists,
763            benchmarking_job_settings: benchmarking_job_settings.clone_arc(),
764            eval_settings: eval_settings.clone_arc(),
765            remote_repository,
766            output_dir: output_dir.resolve()?,
767            targets,
768            versioned_datasets_base_dir: versioned_datasets_base_dir
769                .as_ref()
770                .map(|d| d.resolve())
771                .transpose()?
772                .map(Arc::<Path>::from),
773            commit_tags_regex,
774            run_jobs_daemon: run_jobs_daemon.check(global_app_state_dir, "run_jobs_daemon")?,
775            polling_daemon: polling_daemon.check(global_app_state_dir, "polling_daemon")?,
776        })
777    }
778}
779
780/// Shareable across threads
781#[derive(Clone)]
782pub struct ShareableConfig {
783    pub run_config: Arc<RunConfig>,
784    pub global_app_state_dir: Arc<GlobalAppStateDir>,
785}
786
787/// Keep the original ConfigFile around so that we can check whether
788/// it needs reloading (in `Daemon`, passed to daemon run procedures
789/// via `DaemonCheckExit`). Also, need the `GlobalAppStateDir` for
790/// further path operations, too.
791pub struct RunConfigBundle {
792    pub config_file: Arc<ConfigFile<RunConfigOpts>>,
793    pub shareable: ShareableConfig,
794}
795
796impl RunConfigBundle {
797    pub fn load(
798        provided_path: Option<Arc<Path>>,
799        or_else: impl FnOnce(&str) -> Result<RunConfigOpts>,
800        global_app_state_dir: GlobalAppStateDir,
801    ) -> Result<Self> {
802        let config_file = Arc::new(ConfigFile::<RunConfigOpts>::load_config(
803            provided_path,
804            or_else,
805        )?);
806        let run_config = config_file.check(&global_app_state_dir)?.into();
807        Ok(Self {
808            config_file,
809            shareable: ShareableConfig {
810                run_config,
811                global_app_state_dir: global_app_state_dir.into(),
812            },
813        })
814    }
815}