1use std::borrow::Cow;
4use std::fmt;
5use std::fs;
6use std::marker::PhantomData;
7use std::path::Path;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::thread::available_parallelism;
11
12use anyhow::Context;
13use anyhow::Result;
14use anyhow::anyhow;
15use anyhow::bail;
16use anyhow::ensure;
17use bytesize::ByteSize;
18use codespan_reporting::files::SimpleFile;
19use codespan_reporting::term;
20use codespan_reporting::term::termcolor::Buffer;
21use indexmap::IndexMap;
22use rowan::GreenNode;
23use schemars::JsonSchema;
24use secrecy::ExposeSecret;
25use tokio::process::Command;
26use toml_spanner::Arena;
27use toml_spanner::ErrorKind;
28use toml_spanner::Failed;
29use toml_spanner::FromToml;
30use toml_spanner::Item;
31use toml_spanner::Table;
32use toml_spanner::ToToml;
33use toml_spanner::ToTomlError;
34use toml_spanner::Toml;
35use toml_spanner::helper::display;
36use toml_spanner::helper::flatten_any;
37use toml_spanner::helper::parse_string;
38use tracing::error;
39use tracing::warn;
40use url::Url;
41use wdl_analysis::Diagnostics;
42use wdl_analysis::DiagnosticsConfig;
43use wdl_analysis::Exceptable;
44use wdl_analysis::diagnostics::unknown_name;
45use wdl_analysis::diagnostics::unknown_type;
46use wdl_analysis::document::Task;
47use wdl_analysis::types::PrimitiveType;
48use wdl_analysis::types::Type;
49use wdl_analysis::types::v1::ExprTypeEvaluator;
50use wdl_ast::AstNode;
51use wdl_ast::Diagnostic;
52use wdl_ast::Span;
53use wdl_ast::SupportedVersion;
54use wdl_ast::TreeNode;
55use wdl_ast::lexer::Lexer;
56use wdl_ast::v1::Expr;
57use wdl_grammar::SyntaxKind;
58use wdl_grammar::construct_tree;
59use wdl_grammar::grammar::v1;
60use wdl_grammar::grammar::v1::Parser;
61
62use crate::CancellationContext;
63use crate::EvaluationContext;
64use crate::EvaluationPath;
65use crate::Events;
66use crate::NoneValue;
67use crate::Object;
68use crate::SYSTEM;
69use crate::Value;
70use crate::backend::ExecuteTaskRequest;
71use crate::backend::TaskExecutionBackend;
72use crate::convert_unit_string;
73use crate::diagnostics::unknown_enum_choice;
74use crate::http::Transferer;
75use crate::path::is_supported_url;
76use crate::tree::SyntaxNode;
77use crate::v1::DEFAULT_TASK_REQUIREMENT_MAX_RETRIES;
78use crate::v1::ExprEvaluator;
79
80pub(crate) const MAX_RETRIES: u64 = 100;
82
83pub(crate) const DEFAULT_TASK_SHELL: &str = "bash";
85
86pub(crate) const fn default_task_shell() -> &'static str {
88 DEFAULT_TASK_SHELL
89}
90
91pub(crate) const DEFAULT_TASK_CONTAINER: &str = "ubuntu:latest";
93
94pub(crate) const fn default_task_container() -> &'static str {
96 DEFAULT_TASK_CONTAINER
97}
98
99const fn default_backend_name() -> &'static str {
101 "default"
102}
103
104const MAX_LSF_JOB_NAME_PREFIX: usize = 100;
106
107const REDACTED: &str = "<REDACTED>";
109
110const fn cache_dir_sentinel() -> &'static str {
112 "system"
113}
114
115const fn default_http_retries() -> u32 {
119 5
120}
121
122const fn default_apptainer_executable() -> &'static str {
124 "apptainer"
125}
126
127const fn default_scatter_concurrency() -> u64 {
130 1000
131}
132
133pub(crate) fn cache_dir() -> Result<PathBuf> {
135 const CACHE_DIR_ROOT: &str = "sprocket";
137
138 Ok(dirs::cache_dir()
139 .context("failed to determine user cache directory")?
140 .join(CACHE_DIR_ROOT))
141}
142
143fn escape_mapping(toml: &str) -> Vec<(usize, usize)> {
174 let mut iter = toml.char_indices();
175 let mut mapping = Vec::new();
176 let mut new = 0;
177 while let Some((old, c)) = iter.next() {
178 if c != '\\' {
179 new += c.len_utf8();
180 continue;
181 }
182
183 match iter.next() {
184 Some((_, 'u')) => {
185 let c = u32::from_str_radix(&toml[old + 2 ..old + 6 ], 16)
186 .map(char::from_u32)
187 .expect("invalid TOML escape sequence")
188 .expect("invalid TOML escape character");
189 new += c.len_utf8();
190
191 iter.nth(3);
193 mapping.push((new, old + 6 ));
194 }
195 Some((_, 'U')) => {
196 let c = u32::from_str_radix(&toml[old + 2 ..old + 10 ], 16)
197 .map(char::from_u32)
198 .expect("invalid TOML escape sequence")
199 .expect("invalid TOML escape character");
200 new += c.len_utf8();
201
202 iter.nth(7);
204 mapping.push((new, old + 10 ));
205 }
206 Some(_) => {
207 new += 1;
209 mapping.push((new, old + 2 ));
210 }
211 None => break,
212 }
213 }
214
215 mapping
216}
217
218#[derive(Default, Debug, Clone, JsonSchema)]
222#[schemars(with = "String")]
223pub struct SecretString {
224 inner: secrecy::SecretString,
228 redacted: bool,
237}
238
239impl SecretString {
240 pub fn redact(mut self) -> Self {
245 self.redacted = true;
246 self
247 }
248
249 pub fn inner(&self) -> &secrecy::SecretString {
251 &self.inner
252 }
253}
254
255impl From<String> for SecretString {
256 fn from(s: String) -> Self {
257 Self {
258 inner: s.into(),
259 redacted: false,
260 }
261 }
262}
263
264impl From<&str> for SecretString {
265 fn from(s: &str) -> Self {
266 Self {
267 inner: s.into(),
268 redacted: false,
269 }
270 }
271}
272
273impl<'de> FromToml<'de> for SecretString {
274 fn from_toml(ctx: &mut toml_spanner::Context<'de>, item: &Item<'de>) -> Result<Self, Failed> {
275 Ok(String::from_toml(ctx, item)?.into())
276 }
277}
278
279impl ToToml for SecretString {
280 fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
281 use secrecy::ExposeSecret;
282
283 if self.redacted {
284 REDACTED.to_toml(arena)
285 } else {
286 self.inner.expose_secret().to_toml(arena)
287 }
288 }
289}
290
291impl PartialEq for SecretString {
292 fn eq(&self, other: &Self) -> bool {
293 use secrecy::ExposeSecret;
294
295 self.inner.expose_secret() == other.inner.expose_secret()
297 }
298}
299
300impl Eq for SecretString {}
301
302#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Toml, JsonSchema)]
305#[toml(Toml, rename_all = "snake_case")]
306#[schemars(rename_all = "snake_case")]
307pub enum FailureMode {
308 #[default]
311 Slow,
312 Fast,
316}
317
318mod index_map {
320 use indexmap::IndexMap;
321 use toml_spanner::Arena;
322 use toml_spanner::Context;
323 use toml_spanner::Failed;
324 use toml_spanner::FromToml;
325 use toml_spanner::Item;
326 use toml_spanner::Key;
327 use toml_spanner::Table;
328 use toml_spanner::TableStyle;
329 use toml_spanner::ToToml;
330 use toml_spanner::ToTomlError;
331
332 pub fn from_toml<'de, V>(
334 ctx: &mut Context<'de>,
335 item: &Item<'de>,
336 ) -> Result<IndexMap<String, V>, Failed>
337 where
338 V: FromToml<'de>,
339 {
340 let table = item.require_table(ctx)?;
341 let mut map = IndexMap::default();
342 let mut had_error = false;
343 for (key, item) in table {
344 match V::from_toml(ctx, item) {
345 Ok(v) => {
346 map.insert(key.name.into(), v);
347 }
348 Err(_) => had_error = true,
349 }
350 }
351
352 if had_error { Err(Failed) } else { Ok(map) }
353 }
354
355 pub fn to_toml<'a, V>(
357 value: &'a IndexMap<String, V>,
358 arena: &'a Arena,
359 ) -> Result<Item<'a>, ToTomlError>
360 where
361 V: ToToml,
362 {
363 let Some(mut table) = Table::try_with_capacity(value.len(), arena) else {
364 return Err(ToTomlError::from(
365 "length of table exceeded maximum capacity",
366 ));
367 };
368
369 table.set_style(TableStyle::Implicit);
370
371 for (k, v) in value {
372 table.insert_unique(Key::new(k), v.to_toml(arena)?, arena);
373 }
374
375 Ok(table.into_item())
376 }
377}
378
379#[derive(Debug, Clone, Toml, PartialEq, Eq, JsonSchema)]
390#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
391#[schemars(
392 rename = "WdlEngineConfig",
393 rename_all = "snake_case",
394 deny_unknown_fields
395)]
396pub struct Config {
397 #[toml(default, style = Header)]
399 #[schemars(default)]
400 pub http: HttpConfig,
401 #[toml(default, style = Header)]
403 #[schemars(default)]
404 pub workflow: WorkflowConfig,
405 #[toml(default, style = Header)]
407 #[schemars(default)]
408 pub task: TaskConfig,
409 #[toml(default = String::from(default_backend_name()))]
411 #[schemars(default = "default_backend_name")]
412 pub backend: String,
413 #[toml(default, with = index_map)]
418 #[schemars(default)]
419 pub backends: IndexMap<String, BackendConfig>,
420 #[toml(default, style = Header)]
422 #[schemars(default)]
423 pub storage: StorageConfig,
424 #[toml(default)]
438 #[schemars(default)]
439 pub suppress_env_specific_output: bool,
440 #[toml(default)]
447 #[schemars(default)]
448 pub experimental_features_enabled: bool,
449 #[toml(default, rename = "fail")]
457 #[schemars(default, rename = "fail")]
458 pub failure_mode: FailureMode,
459}
460
461impl Default for Config {
462 fn default() -> Self {
463 Self {
464 http: Default::default(),
465 workflow: Default::default(),
466 task: Default::default(),
467 backend: default_backend_name().into(),
468 backends: Default::default(),
469 storage: Default::default(),
470 suppress_env_specific_output: Default::default(),
471 experimental_features_enabled: Default::default(),
472 failure_mode: Default::default(),
473 }
474 }
475}
476
477impl Config {
478 pub fn builder() -> ConfigBuilder<Self> {
480 ConfigBuilder::default()
481 }
482
483 pub async fn validate(&self) -> Result<()> {
485 self.http.validate()?;
486 self.workflow.validate()?;
487 self.task.validate()?;
488
489 if self.backends.is_empty() && self.backend == default_backend_name() {
490 } else {
492 let backend = &self.backend;
493 if !self.backends.contains_key(backend) {
494 bail!("a backend named `{backend}` is not present in the configuration");
495 }
496 }
497
498 for backend in self.backends.values() {
499 backend.validate().await?;
500 }
501
502 self.storage.validate()?;
503
504 if self.suppress_env_specific_output && !self.experimental_features_enabled {
505 bail!("`suppress_env_specific_output` requires enabling experimental features");
506 }
507
508 Ok(())
509 }
510
511 pub fn redact(mut self) -> Self {
515 for backend in self.backends.values_mut() {
516 *backend = std::mem::take(backend).redact();
517 }
518
519 if let Some(auth) = self.storage.azure.auth.take() {
520 self.storage.azure.auth = Some(auth.redact());
521 }
522
523 if let Some(auth) = self.storage.s3.auth.take() {
524 self.storage.s3.auth = Some(auth.redact());
525 }
526
527 if let Some(auth) = self.storage.google.auth.take() {
528 self.storage.google.auth = Some(auth.redact());
529 }
530
531 self
532 }
533
534 pub fn backend(&self) -> Result<Cow<'_, BackendConfig>> {
539 if !self.backends.is_empty() {
540 let backend = &self.backend;
541 return Ok(Cow::Borrowed(self.backends.get(backend).ok_or_else(
542 || anyhow!("a backend named `{backend}` is not present in the configuration"),
543 )?));
544 }
545 Ok(Cow::Owned(BackendConfig::default()))
547 }
548
549 pub(crate) async fn create_backend(
551 self: &Arc<Self>,
552 run_root_dir: &Path,
553 events: Events,
554 cancellation: CancellationContext,
555 ) -> Result<Arc<dyn TaskExecutionBackend>> {
556 use crate::backend::*;
557
558 match self.backend()?.as_ref() {
559 BackendConfig::Local { .. } => {
560 warn!(
561 "the engine is configured to use the local backend: tasks will not be run \
562 inside of a container"
563 );
564 Ok(Arc::new(LocalBackend::new(
565 self.clone(),
566 events,
567 cancellation,
568 )?))
569 }
570 BackendConfig::Docker { .. } => Ok(Arc::new(
571 DockerBackend::new(self.clone(), events, cancellation).await?,
572 )),
573 BackendConfig::Tes { .. } => Ok(Arc::new(
574 TesBackend::new(self.clone(), events, cancellation).await?,
575 )),
576 BackendConfig::LsfApptainer { .. } => Ok(Arc::new(LsfApptainerBackend::new(
577 self.clone(),
578 run_root_dir,
579 events,
580 cancellation,
581 )?)),
582 BackendConfig::SlurmApptainer { .. } => Ok(Arc::new(SlurmApptainerBackend::new(
583 self.clone(),
584 run_root_dir,
585 events,
586 cancellation,
587 )?)),
588 }
589 }
590}
591
592#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, JsonSchema)]
594#[schemars(rename_all = "lowercase")]
595pub enum Parallelism {
596 #[default]
598 Available,
599 #[schemars(untagged)]
601 Use(usize),
602}
603
604impl From<usize> for Parallelism {
605 fn from(value: usize) -> Self {
606 Self::Use(value)
607 }
608}
609
610impl From<Parallelism> for usize {
611 fn from(value: Parallelism) -> Self {
612 match value {
613 Parallelism::Available => available_parallelism().map(Into::into).unwrap_or(1),
614 Parallelism::Use(value) => value,
615 }
616 }
617}
618
619impl<'de> FromToml<'de> for Parallelism {
620 fn from_toml(ctx: &mut toml_spanner::Context<'de>, item: &Item<'de>) -> Result<Self, Failed> {
621 if let Some("available") = item.as_str() {
622 return Ok(Self::Available);
623 }
624
625 if let Some(n) = item.as_u64().and_then(|n| usize::try_from(n).ok())
626 && n > 0
627 {
628 return Ok(Self::Use(n));
629 }
630
631 Err(ctx.report_custom_error(
632 "expected a positive integer or `available` for parallelism",
633 item,
634 ))
635 }
636}
637
638impl ToToml for Parallelism {
639 fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
640 match self {
641 Self::Available => Ok(Item::string("available")),
642 Self::Use(n) => Ok(i64::try_from(*n)
643 .map_err(|e| ToTomlError {
644 message: format!("invalid parallelism: {e}").into(),
645 })?
646 .into()),
647 }
648 }
649}
650
651#[derive(Debug, Clone, Toml, PartialEq, Eq, JsonSchema)]
653#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
654#[schemars(rename_all = "snake_case", deny_unknown_fields)]
655pub struct HttpConfig {
656 #[toml(default = String::from(cache_dir_sentinel()))]
660 #[schemars(default = "cache_dir_sentinel")]
661 pub cache_dir: String,
662 #[toml(default = default_http_retries())]
664 #[schemars(default = "default_http_retries")]
665 pub retries: u32,
666 #[toml(default)]
670 #[schemars(default)]
671 pub parallelism: Parallelism,
672 #[toml(default, FromToml with = parse_string, ToToml with = display)]
677 #[schemars(default, with = "String")]
678 pub hash_algorithm: cloud_copy::HashAlgorithm,
679}
680
681impl Default for HttpConfig {
682 fn default() -> Self {
683 Self {
684 cache_dir: cache_dir_sentinel().into(),
685 retries: default_http_retries(),
686 parallelism: Default::default(),
687 hash_algorithm: Default::default(),
688 }
689 }
690}
691
692impl HttpConfig {
693 pub fn validate(&self) -> Result<()> {
695 if let Parallelism::Use(parallelism) = self.parallelism
696 && parallelism == 0
697 {
698 bail!("configuration value `http.parallelism` cannot be zero");
699 }
700 Ok(())
701 }
702
703 pub fn cache_dir(&self) -> Result<PathBuf> {
705 const DOWNLOADS_CACHE_SUBDIR: &str = "downloads";
706
707 if self.using_system_cache_dir() {
708 cache_dir().map(|d| d.join(DOWNLOADS_CACHE_SUBDIR))
709 } else {
710 Ok(PathBuf::from(&self.cache_dir))
711 }
712 }
713
714 pub fn using_system_cache_dir(&self) -> bool {
716 self.cache_dir == cache_dir_sentinel()
717 }
718}
719
720#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
722#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
723#[schemars(rename_all = "snake_case", deny_unknown_fields)]
724pub struct StorageConfig {
725 #[toml(default, style = Header)]
727 #[schemars(default)]
728 pub azure: AzureStorageConfig,
729 #[toml(default, style = Header)]
731 #[schemars(default)]
732 pub s3: S3StorageConfig,
733 #[toml(default, style = Header)]
735 #[schemars(default)]
736 pub google: GoogleStorageConfig,
737}
738
739impl StorageConfig {
740 pub fn validate(&self) -> Result<()> {
742 self.azure.validate()?;
743 self.s3.validate()?;
744 self.google.validate()?;
745 Ok(())
746 }
747}
748
749#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
751#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
752#[schemars(rename_all = "snake_case", deny_unknown_fields)]
753pub struct AzureStorageAuthConfig {
754 pub account_name: String,
756 pub access_key: SecretString,
758}
759
760impl AzureStorageAuthConfig {
761 pub fn validate(&self) -> Result<()> {
763 if self.account_name.is_empty() {
764 bail!("configuration value `storage.azure.auth.account_name` is required");
765 }
766
767 if self.access_key.inner.expose_secret().is_empty() {
768 bail!("configuration value `storage.azure.auth.access_key` is required");
769 }
770
771 Ok(())
772 }
773
774 pub fn redact(mut self) -> Self {
777 self.access_key = self.access_key.redact();
778 self
779 }
780}
781
782#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
784#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
785#[schemars(rename_all = "snake_case", deny_unknown_fields)]
786pub struct AzureStorageConfig {
787 #[toml(style = Header)]
789 pub auth: Option<AzureStorageAuthConfig>,
790}
791
792impl AzureStorageConfig {
793 pub fn validate(&self) -> Result<()> {
795 if let Some(auth) = &self.auth {
796 auth.validate()?;
797 }
798
799 Ok(())
800 }
801}
802
803#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
805#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
806#[schemars(rename_all = "snake_case", deny_unknown_fields)]
807pub struct S3StorageAuthConfig {
808 pub access_key_id: String,
810 pub secret_access_key: SecretString,
812}
813
814impl S3StorageAuthConfig {
815 pub fn validate(&self) -> Result<()> {
817 if self.access_key_id.is_empty() {
818 bail!("configuration value `storage.s3.auth.access_key_id` is required");
819 }
820
821 if self.secret_access_key.inner.expose_secret().is_empty() {
822 bail!("configuration value `storage.s3.auth.secret_access_key` is required");
823 }
824
825 Ok(())
826 }
827
828 pub fn redact(mut self) -> Self {
831 self.secret_access_key = self.secret_access_key.redact();
832 self
833 }
834}
835
836#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
838#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
839#[schemars(rename_all = "snake_case", deny_unknown_fields)]
840pub struct S3StorageConfig {
841 pub region: Option<String>,
846
847 #[toml(style = Header)]
849 pub auth: Option<S3StorageAuthConfig>,
850}
851
852impl S3StorageConfig {
853 pub fn validate(&self) -> Result<()> {
855 if let Some(auth) = &self.auth {
856 auth.validate()?;
857 }
858
859 Ok(())
860 }
861}
862
863#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
865#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
866#[schemars(rename_all = "snake_case", deny_unknown_fields)]
867pub struct GoogleStorageAuthConfig {
868 pub access_key: String,
870 pub secret: SecretString,
872}
873
874impl GoogleStorageAuthConfig {
875 pub fn validate(&self) -> Result<()> {
877 if self.access_key.is_empty() {
878 bail!("configuration value `storage.google.auth.access_key` is required");
879 }
880
881 if self.secret.inner.expose_secret().is_empty() {
882 bail!("configuration value `storage.google.auth.secret` is required");
883 }
884
885 Ok(())
886 }
887
888 pub fn redact(mut self) -> Self {
891 self.secret = self.secret.redact();
892 self
893 }
894}
895
896#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
898#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
899#[schemars(rename_all = "snake_case", deny_unknown_fields)]
900pub struct GoogleStorageConfig {
901 #[toml(style = Header)]
903 pub auth: Option<GoogleStorageAuthConfig>,
904}
905
906impl GoogleStorageConfig {
907 pub fn validate(&self) -> Result<()> {
909 if let Some(auth) = &self.auth {
910 auth.validate()?;
911 }
912
913 Ok(())
914 }
915}
916
917#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
919#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
920#[schemars(rename_all = "snake_case", deny_unknown_fields)]
921pub struct WorkflowConfig {
922 #[toml(default, style = Header)]
924 #[schemars(default)]
925 pub scatter: ScatterConfig,
926}
927
928impl WorkflowConfig {
929 pub fn validate(&self) -> Result<()> {
931 self.scatter.validate()?;
932 Ok(())
933 }
934}
935
936#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
938#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
939#[schemars(rename_all = "snake_case", deny_unknown_fields)]
940pub struct ScatterConfig {
941 #[toml(default = default_scatter_concurrency())]
994 #[schemars(default = "default_scatter_concurrency")]
995 pub concurrency: u64,
996}
997
998impl Default for ScatterConfig {
999 fn default() -> Self {
1000 Self {
1001 concurrency: default_scatter_concurrency(),
1002 }
1003 }
1004}
1005
1006impl ScatterConfig {
1007 pub fn validate(&self) -> Result<()> {
1009 if self.concurrency == 0 {
1010 bail!("configuration value `workflow.scatter.concurrency` cannot be zero");
1011 }
1012
1013 Ok(())
1014 }
1015}
1016
1017#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Toml, JsonSchema)]
1019#[toml(Toml, rename_all = "snake_case")]
1020#[schemars(rename_all = "snake_case")]
1021pub enum CallCachingMode {
1022 #[default]
1029 Off,
1030 On,
1036 Explicit,
1044}
1045
1046#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Toml, JsonSchema)]
1048#[toml(Toml, rename_all = "snake_case")]
1049#[schemars(rename_all = "snake_case")]
1050pub enum ContentDigestMode {
1051 Strong,
1058 Strongish,
1070 #[default]
1081 Weak,
1082}
1083
1084#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, JsonSchema)]
1086#[schemars(rename_all = "lowercase")]
1087pub enum Retries {
1088 #[default]
1090 Default,
1091 #[schemars(untagged)]
1093 Use(u64),
1094}
1095
1096impl From<u64> for Retries {
1097 fn from(value: u64) -> Self {
1098 Self::Use(value)
1099 }
1100}
1101
1102impl From<Retries> for u64 {
1103 fn from(value: Retries) -> Self {
1104 match value {
1105 Retries::Default => DEFAULT_TASK_REQUIREMENT_MAX_RETRIES,
1106 Retries::Use(value) => value,
1107 }
1108 }
1109}
1110
1111impl<'de> FromToml<'de> for Retries {
1112 fn from_toml(ctx: &mut toml_spanner::Context<'de>, item: &Item<'de>) -> Result<Self, Failed> {
1113 if let Some("default") = item.as_str() {
1114 return Ok(Self::Default);
1115 }
1116
1117 if let Some(n) = item.as_u64()
1118 && n < MAX_RETRIES
1119 {
1120 return Ok(Self::Use(n));
1121 }
1122
1123 Err(ctx.report_custom_error(
1124 format!("expected an integer less than {MAX_RETRIES} or `default` for retries"),
1125 item,
1126 ))
1127 }
1128}
1129
1130impl ToToml for Retries {
1131 fn to_toml<'a>(&'a self, _: &'a Arena) -> Result<Item<'a>, ToTomlError> {
1132 match self {
1133 Self::Default => Ok(Item::string("default")),
1134 Self::Use(n) => Ok(i64::try_from(*n)
1135 .map_err(|e| ToTomlError {
1136 message: format!("invalid retries: {e}").into(),
1137 })?
1138 .into()),
1139 }
1140 }
1141}
1142
1143#[derive(Debug, Clone, Toml, PartialEq, Eq, JsonSchema)]
1145#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1146#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1147pub struct TaskConfig {
1148 #[toml(default)]
1152 #[schemars(default)]
1153 pub retries: Retries,
1154 #[toml(default = String::from(default_task_container()))]
1157 #[schemars(default = "default_task_container")]
1158 pub container: String,
1159 #[toml(default = String::from(default_task_shell()))]
1174 #[schemars(default = "default_task_shell")]
1175 pub shell: String,
1176 #[toml(default)]
1178 #[schemars(default)]
1179 pub cpu_limit_behavior: TaskResourceLimitBehavior,
1180 #[toml(default)]
1182 #[schemars(default)]
1183 pub memory_limit_behavior: TaskResourceLimitBehavior,
1184 #[toml(default = String::from(cache_dir_sentinel()))]
1188 #[schemars(default = "cache_dir_sentinel")]
1189 pub cache_dir: String,
1190 #[toml(default)]
1192 #[schemars(default)]
1193 pub cache: CallCachingMode,
1194 #[toml(default)]
1198 #[schemars(default)]
1199 pub digests: ContentDigestMode,
1200 #[toml(default)]
1209 #[schemars(default)]
1210 pub excluded_cache_requirements: Vec<String>,
1211 #[toml(default)]
1219 #[schemars(default)]
1220 pub excluded_cache_hints: Vec<String>,
1221 #[toml(default)]
1229 #[schemars(default)]
1230 pub excluded_cache_inputs: Vec<String>,
1231}
1232
1233impl Default for TaskConfig {
1234 fn default() -> Self {
1235 Self {
1236 retries: Default::default(),
1237 container: default_task_container().into(),
1238 shell: default_task_shell().into(),
1239 cpu_limit_behavior: Default::default(),
1240 memory_limit_behavior: Default::default(),
1241 cache_dir: cache_dir_sentinel().into(),
1242 cache: Default::default(),
1243 digests: Default::default(),
1244 excluded_cache_requirements: Default::default(),
1245 excluded_cache_hints: Default::default(),
1246 excluded_cache_inputs: Default::default(),
1247 }
1248 }
1249}
1250
1251impl TaskConfig {
1252 pub fn validate(&self) -> Result<()> {
1254 if let Retries::Use(value) = self.retries
1255 && value >= MAX_RETRIES
1256 {
1257 bail!("configuration value `task.retries` cannot exceed {MAX_RETRIES}");
1258 }
1259
1260 Ok(())
1261 }
1262
1263 pub fn cache_dir(&self) -> Option<PathBuf> {
1265 if self.cache_dir == cache_dir_sentinel() {
1266 None
1267 } else {
1268 Some(PathBuf::from(&self.cache_dir))
1269 }
1270 }
1271}
1272
1273#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Toml, JsonSchema)]
1276#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1277#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1278pub enum TaskResourceLimitBehavior {
1279 TryWithMax,
1282 #[default]
1286 Deny,
1287}
1288
1289#[derive(Debug, Clone, Toml, PartialEq, Eq, JsonSchema)]
1291#[toml(Toml, rename_all = "snake_case", tag = "type")]
1292#[schemars(rename_all = "snake_case", tag = "type")]
1293pub enum BackendConfig {
1294 Local {
1296 #[toml(default, style = Header, flatten, with = flatten_any)]
1298 #[schemars(default, flatten)]
1299 config: LocalBackendConfig,
1300 },
1301 Docker {
1303 #[toml(default, style = Header, flatten, with = flatten_any)]
1305 #[schemars(default, flatten)]
1306 config: DockerBackendConfig,
1307 },
1308 Tes {
1310 #[toml(default, style = Header, flatten, with = flatten_any)]
1312 #[schemars(default, flatten)]
1313 config: TesBackendConfig,
1314 },
1315 LsfApptainer {
1319 #[toml(default, style = Header, flatten, with = flatten_any)]
1321 #[schemars(default, flatten)]
1322 config: LsfApptainerBackendConfig,
1323 },
1324 SlurmApptainer {
1328 #[toml(default, style = Header, flatten, with = flatten_any)]
1330 #[schemars(default, flatten)]
1331 config: SlurmApptainerBackendConfig,
1332 },
1333}
1334
1335impl Default for BackendConfig {
1336 fn default() -> Self {
1337 Self::Docker {
1338 config: Default::default(),
1339 }
1340 }
1341}
1342
1343impl BackendConfig {
1344 pub async fn validate(&self) -> Result<()> {
1346 match self {
1347 Self::Local { config } => config.validate(),
1348 Self::Docker { config } => config.validate(),
1349 Self::Tes { config } => config.validate(),
1350 Self::LsfApptainer { config } => config.validate().await,
1351 Self::SlurmApptainer { config } => config.validate().await,
1352 }
1353 }
1354
1355 pub fn as_local(&self) -> Option<&LocalBackendConfig> {
1359 match self {
1360 Self::Local { config } => Some(config),
1361 _ => None,
1362 }
1363 }
1364
1365 pub fn as_docker(&self) -> Option<&DockerBackendConfig> {
1369 match self {
1370 Self::Docker { config } => Some(config),
1371 _ => None,
1372 }
1373 }
1374
1375 pub fn as_tes(&self) -> Option<&TesBackendConfig> {
1379 match self {
1380 Self::Tes { config } => Some(config),
1381 _ => None,
1382 }
1383 }
1384
1385 pub fn as_lsf_apptainer(&self) -> Option<&LsfApptainerBackendConfig> {
1390 match self {
1391 Self::LsfApptainer { config } => Some(config),
1392 _ => None,
1393 }
1394 }
1395
1396 pub fn as_slurm_apptainer(&self) -> Option<&SlurmApptainerBackendConfig> {
1401 match self {
1402 Self::SlurmApptainer { config } => Some(config),
1403 _ => None,
1404 }
1405 }
1406
1407 pub fn redact(self) -> Self {
1409 match self {
1410 Self::Local { .. }
1411 | Self::Docker { .. }
1412 | Self::LsfApptainer { .. }
1413 | Self::SlurmApptainer { .. } => self,
1414 Self::Tes { config } => Self::Tes {
1415 config: config.redact(),
1416 },
1417 }
1418 }
1419}
1420
1421impl From<LocalBackendConfig> for BackendConfig {
1422 fn from(config: LocalBackendConfig) -> Self {
1423 Self::Local { config }
1424 }
1425}
1426
1427impl From<DockerBackendConfig> for BackendConfig {
1428 fn from(config: DockerBackendConfig) -> Self {
1429 Self::Docker { config }
1430 }
1431}
1432
1433impl From<TesBackendConfig> for BackendConfig {
1434 fn from(config: TesBackendConfig) -> Self {
1435 Self::Tes { config }
1436 }
1437}
1438
1439impl From<LsfApptainerBackendConfig> for BackendConfig {
1440 fn from(config: LsfApptainerBackendConfig) -> Self {
1441 Self::LsfApptainer { config }
1442 }
1443}
1444
1445impl From<SlurmApptainerBackendConfig> for BackendConfig {
1446 fn from(config: SlurmApptainerBackendConfig) -> Self {
1447 Self::SlurmApptainer { config }
1448 }
1449}
1450
1451#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
1458#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1459#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1460pub struct LocalBackendConfig {
1461 pub cpu: Option<u64>,
1467
1468 pub memory: Option<String>,
1475}
1476
1477impl LocalBackendConfig {
1478 pub fn validate(&self) -> Result<()> {
1480 if let Some(cpu) = self.cpu {
1481 if cpu == 0 {
1482 bail!("local backend configuration value `cpu` cannot be zero");
1483 }
1484
1485 let total = SYSTEM.cpus().len() as u64;
1486 if cpu > total {
1487 bail!(
1488 "local backend configuration value `cpu` cannot exceed the virtual CPUs \
1489 available to the host ({total})"
1490 );
1491 }
1492 }
1493
1494 if let Some(memory) = &self.memory {
1495 let memory = convert_unit_string(memory).with_context(|| {
1496 format!("local backend configuration value `memory` has invalid value `{memory}`")
1497 })?;
1498
1499 if memory == 0 {
1500 bail!("local backend configuration value `memory` cannot be zero");
1501 }
1502
1503 let total = SYSTEM.total_memory();
1504 if memory > total {
1505 bail!(
1506 "local backend configuration value `memory` cannot exceed the total memory of \
1507 the host ({total} bytes)"
1508 );
1509 }
1510 }
1511
1512 Ok(())
1513 }
1514}
1515
1516fn default_docker_cleanup() -> bool {
1518 true
1519}
1520
1521#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
1523#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1524#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1525pub struct DockerBackendConfig {
1526 #[toml(default = default_docker_cleanup())]
1530 #[schemars(default = "default_docker_cleanup")]
1531 pub cleanup: bool,
1532}
1533
1534impl DockerBackendConfig {
1535 pub fn validate(&self) -> Result<()> {
1537 Ok(())
1538 }
1539}
1540
1541impl Default for DockerBackendConfig {
1542 fn default() -> Self {
1543 Self {
1544 cleanup: default_docker_cleanup(),
1545 }
1546 }
1547}
1548
1549#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
1551#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1552#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1553pub struct BasicAuthConfig {
1554 pub username: String,
1556 pub password: SecretString,
1558}
1559
1560impl BasicAuthConfig {
1561 pub fn validate(&self) -> Result<()> {
1563 Ok(())
1564 }
1565
1566 pub fn redact(mut self) -> Self {
1568 self.password = self.password.redact();
1569 self
1570 }
1571}
1572
1573#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
1575#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1576#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1577pub struct BearerAuthConfig {
1578 pub token: SecretString,
1580}
1581
1582impl BearerAuthConfig {
1583 pub fn validate(&self) -> Result<()> {
1585 Ok(())
1586 }
1587
1588 pub fn redact(mut self) -> Self {
1590 self.token = self.token.redact();
1591 self
1592 }
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
1597#[toml(Toml, rename_all = "snake_case", tag = "type")]
1598#[schemars(rename_all = "snake_case", tag = "type")]
1599pub enum TesBackendAuthConfig {
1600 Basic {
1602 #[toml(default, style = Header, flatten, with = flatten_any)]
1604 #[schemars(default, flatten)]
1605 config: BasicAuthConfig,
1606 },
1607 Bearer {
1609 #[toml(default, style = Header, flatten, with = flatten_any)]
1611 #[schemars(default, flatten)]
1612 config: BearerAuthConfig,
1613 },
1614}
1615
1616impl TesBackendAuthConfig {
1617 pub fn validate(&self) -> Result<()> {
1619 match self {
1620 Self::Basic { config } => config.validate(),
1621 Self::Bearer { config } => config.validate(),
1622 }
1623 }
1624
1625 pub fn redact(self) -> Self {
1628 match self {
1629 Self::Basic { config } => Self::Basic {
1630 config: config.redact(),
1631 },
1632 Self::Bearer { config } => Self::Bearer {
1633 config: config.redact(),
1634 },
1635 }
1636 }
1637}
1638
1639impl From<BasicAuthConfig> for TesBackendAuthConfig {
1640 fn from(config: BasicAuthConfig) -> Self {
1641 Self::Basic { config }
1642 }
1643}
1644
1645impl From<BearerAuthConfig> for TesBackendAuthConfig {
1646 fn from(config: BearerAuthConfig) -> Self {
1647 Self::Bearer { config }
1648 }
1649}
1650
1651#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
1653#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1654#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1655pub struct TesBackendConfig {
1656 #[toml(FromToml with = parse_string, ToToml with = display)]
1658 pub url: Option<Url>,
1659
1660 #[toml(style = Header)]
1662 pub auth: Option<TesBackendAuthConfig>,
1663
1664 #[toml(FromToml with = parse_string, ToToml with = display)]
1666 pub inputs: Option<Url>,
1667
1668 #[toml(FromToml with = parse_string, ToToml with = display)]
1670 pub outputs: Option<Url>,
1671
1672 pub interval: Option<u64>,
1676
1677 pub retries: Option<u32>,
1682
1683 pub max_concurrency: Option<u32>,
1688
1689 #[toml(default)]
1692 #[schemars(default)]
1693 pub insecure: bool,
1694}
1695
1696impl TesBackendConfig {
1697 pub fn validate(&self) -> Result<()> {
1699 match &self.url {
1700 Some(url) => {
1701 if !self.insecure && url.scheme() != "https" {
1702 bail!(
1703 "TES backend configuration value `url` has invalid value `{url}`: URL \
1704 must use a HTTPS scheme"
1705 );
1706 }
1707 }
1708 None => bail!("TES backend configuration value `url` is required"),
1709 }
1710
1711 if let Some(auth) = &self.auth {
1712 auth.validate()?;
1713 }
1714
1715 if let Some(max_concurrency) = self.max_concurrency
1716 && max_concurrency == 0
1717 {
1718 bail!("TES backend configuration value `max_concurrency` cannot be zero");
1719 }
1720
1721 match &self.inputs {
1722 Some(url) => {
1723 if !is_supported_url(url.as_str()) {
1724 bail!(
1725 "TES backend storage configuration value `inputs` has invalid value \
1726 `{url}`: URL scheme is not supported"
1727 );
1728 }
1729
1730 if !url.path().ends_with('/') {
1731 bail!(
1732 "TES backend storage configuration value `inputs` has invalid value \
1733 `{url}`: URL path must end with a slash"
1734 );
1735 }
1736 }
1737 None => bail!("TES backend configuration value `inputs` is required"),
1738 }
1739
1740 match &self.outputs {
1741 Some(url) => {
1742 if !is_supported_url(url.as_str()) {
1743 bail!(
1744 "TES backend storage configuration value `outputs` has invalid value \
1745 `{url}`: URL scheme is not supported"
1746 );
1747 }
1748
1749 if !url.path().ends_with('/') {
1750 bail!(
1751 "TES backend storage configuration value `outputs` has invalid value \
1752 `{url}`: URL path must end with a slash"
1753 );
1754 }
1755 }
1756 None => bail!("TES backend storage configuration value `outputs` is required"),
1757 }
1758
1759 Ok(())
1760 }
1761
1762 pub fn redact(mut self) -> Self {
1764 if let Some(auth) = self.auth.take() {
1765 self.auth = Some(auth.redact());
1766 }
1767
1768 self
1769 }
1770}
1771
1772#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
1774#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
1775#[schemars(rename_all = "snake_case", deny_unknown_fields)]
1776pub struct ApptainerConfig {
1777 #[toml(default = String::from(default_apptainer_executable()))]
1783 #[schemars(default = "default_apptainer_executable")]
1784 pub executable: String,
1785
1786 pub image_cache_dir: Option<PathBuf>,
1792
1793 #[toml(default)]
1796 #[schemars(default)]
1797 pub extra_args: Vec<String>,
1798}
1799
1800impl Default for ApptainerConfig {
1801 fn default() -> Self {
1802 Self {
1803 executable: default_apptainer_executable().into(),
1804 image_cache_dir: None,
1805 extra_args: Default::default(),
1806 }
1807 }
1808}
1809
1810impl ApptainerConfig {
1811 pub async fn validate(&self) -> Result<(), anyhow::Error> {
1813 Ok(())
1814 }
1815}
1816
1817#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
1826#[schemars(with = "String")]
1827pub struct Condition {
1828 pub raw: String,
1830 expr: GreenNode,
1832}
1833
1834impl Condition {
1835 pub fn new(raw: impl Into<String>) -> Result<Self, Vec<Diagnostic>> {
1837 #[derive(Default)]
1840 struct Context(Diagnostics);
1841
1842 impl wdl_analysis::types::v1::EvaluationContext for Context {
1843 fn version(&self) -> SupportedVersion {
1844 Default::default()
1845 }
1846
1847 fn resolve_name(&mut self, name: &str, span: Span) -> Option<Type> {
1848 match name {
1849 "cpu" => Some(PrimitiveType::Float.into()),
1850 "memory" => Some(PrimitiveType::Integer.into()),
1851 "gpu" | "fpga" => Some(PrimitiveType::Boolean.into()),
1852 "disks" => Some(PrimitiveType::Integer.into()),
1853 "hint" => Some(Type::Object),
1854 _ => {
1855 self.add_diagnostic(unknown_name(name, span));
1856 None
1857 }
1858 }
1859 }
1860
1861 fn resolve_type_name(&mut self, name: &str, span: Span) -> Result<Type, Diagnostic> {
1862 Err(unknown_type(name, span))
1863 }
1864
1865 fn task(&self) -> Option<&Task> {
1866 None
1867 }
1868
1869 fn diagnostics_config(&self) -> DiagnosticsConfig {
1870 Default::default()
1871 }
1872
1873 fn add_diagnostic(&mut self, diagnostic: Diagnostic) {
1874 self.0.add(diagnostic);
1875 }
1876
1877 fn exceptable_add_diagnostic<N: TreeNode + Exceptable>(
1878 &mut self,
1879 diagnostic: Diagnostic,
1880 element: &N,
1881 exceptable_nodes: &Option<&'static [SyntaxKind]>,
1882 ) {
1883 self.0.exceptable_add(diagnostic, element, exceptable_nodes);
1884 }
1885 }
1886
1887 let raw = raw.into();
1888 let mut parser = Parser::new(Lexer::new(&raw));
1889 let marker = parser.start();
1890 match v1::expr(&mut parser, marker) {
1891 Ok(()) => {
1892 if let Some((_, span)) = parser.next() {
1893 return Err(vec![
1894 Diagnostic::error("expected a single WDL expression")
1895 .with_label("extraneous WDL source starts here", span),
1896 ]);
1897 }
1898
1899 let output = parser.finish();
1900 if !output.diagnostics.is_empty() {
1901 return Err(output.diagnostics);
1902 }
1903
1904 let expr = Expr::cast(construct_tree(raw.as_ref(), output.events))
1905 .expect("node should cast");
1906
1907 let mut context = Context::default();
1909 let ty = ExprTypeEvaluator::new(&mut context)
1910 .evaluate_expr(&expr)
1911 .unwrap_or(Type::Union);
1912
1913 if !context.0.is_empty() {
1914 return Err(context.0.into());
1915 }
1916
1917 match ty {
1918 Type::Primitive(PrimitiveType::Boolean, false) | Type::Union => {}
1919 _ => {
1920 return Err(vec![
1921 Diagnostic::error(format!(
1922 "conditional expression is expected to be type `Boolean`, but \
1923 found type `{ty}`",
1924 ))
1925 .with_highlight(expr.span()),
1926 ]);
1927 }
1928 }
1929
1930 Ok(Self {
1931 raw,
1932 expr: expr.inner().green().into_owned(),
1933 })
1934 }
1935 Err((marker, diagnostic)) => {
1936 marker.abandon(&mut parser);
1937 Err(vec![diagnostic.into()])
1938 }
1939 }
1940 }
1941
1942 pub(crate) async fn evaluate(
1947 &self,
1948 request: &ExecuteTaskRequest<'_>,
1949 transferer: &dyn Transferer,
1950 ) -> Result<bool> {
1951 struct Context<'a> {
1953 request: &'a ExecuteTaskRequest<'a>,
1955 transferer: &'a dyn Transferer,
1957 }
1958
1959 impl EvaluationContext for Context<'_> {
1960 fn version(&self) -> SupportedVersion {
1961 Default::default()
1962 }
1963
1964 fn resolve_name(&self, name: &str, span: Span) -> Result<Value, Diagnostic> {
1965 match name {
1966 "cpu" => Ok(self.request.constraints.cpu.into()),
1967 "memory" => Ok((self.request.constraints.memory as i64).into()),
1968 "gpu" => Ok((!self.request.constraints.gpu.is_empty()).into()),
1969 "fpga" => Ok((!self.request.constraints.fpga.is_empty()).into()),
1970 "disks" => Ok(self
1971 .request
1972 .constraints
1973 .disks
1974 .iter()
1975 .map(|(_, s)| *s)
1976 .sum::<i64>()
1977 .into()),
1978 "hint" => Ok(self.request.hints.clone().into()),
1979 _ => Err(unknown_name(name, span)),
1980 }
1981 }
1982
1983 fn resolve_type_name(&self, name: &str, span: Span) -> Result<Type, Diagnostic> {
1984 Err(unknown_type(name, span))
1985 }
1986
1987 fn enum_choice_value(
1988 &self,
1989 enum_name: &str,
1990 choice_name: &str,
1991 ) -> Result<Value, Diagnostic> {
1992 Err(unknown_enum_choice(enum_name, choice_name))
1993 }
1994
1995 fn base_dir(&self) -> &EvaluationPath {
1996 self.request.base_dir
1997 }
1998
1999 fn temp_dir(&self) -> &Path {
2000 self.request.temp_dir
2001 }
2002
2003 fn transferer(&self) -> &dyn Transferer {
2004 self.transferer
2005 }
2006
2007 fn object_access(&self, object: &Object, name: &str) -> Option<Value> {
2008 if !Arc::ptr_eq(&object.members, &self.request.hints.members) {
2011 return None;
2012 }
2013
2014 Some(
2018 self.request
2019 .inputs
2020 .hint(name)
2021 .or_else(|| object.get(name))
2022 .cloned()
2023 .unwrap_or_else(|| NoneValue::untyped().into()),
2024 )
2025 }
2026 }
2027
2028 async fn eval(context: Context<'_>, expr: &Expr<SyntaxNode>) -> Result<bool, Diagnostic> {
2033 let mut evaluator = ExprEvaluator::new(context);
2034 let value = evaluator.evaluate_expr(expr).await?;
2035 match value.as_boolean() {
2036 Some(res) => Ok(res),
2037 None => Err(Diagnostic::error(format!(
2038 "conditional expression is expected to be type `Boolean`, but found type \
2039 `{ty}`",
2040 ty = value.ty()
2041 ))
2042 .with_highlight(expr.span())),
2043 }
2044 }
2045
2046 let expr = Expr::cast(self.expr.clone().into()).expect("should be an expression node");
2047 match eval(
2048 Context {
2049 request,
2050 transferer,
2051 },
2052 &expr,
2053 )
2054 .await
2055 {
2056 Ok(res) => Ok(res),
2057 Err(diagnostic) => {
2058 let file: SimpleFile<_, _> = SimpleFile::new("<condition>", &self.raw);
2059 let mut buffer = Buffer::no_color();
2060 term::emit_to_write_style(
2061 &mut buffer,
2062 &Default::default(),
2063 &file,
2064 &diagnostic.to_codespan(()),
2065 )
2066 .context("failed to write diagnostic to buffer")?;
2067 let diagnostic = String::from_utf8(buffer.into_inner())
2068 .context("diagnostic buffer contents are not UTF-8")?;
2069 bail!(
2070 "failed to evaluate backend dynamic arguments condition: {diagnostic}",
2071 diagnostic = diagnostic
2072 .strip_prefix("error: ")
2073 .unwrap_or(&diagnostic)
2074 .trim()
2075 );
2076 }
2077 }
2078 }
2079}
2080
2081impl ToToml for Condition {
2082 fn to_toml<'a>(&'a self, arena: &'a Arena) -> Result<Item<'a>, ToTomlError> {
2083 self.raw.to_toml(arena)
2084 }
2085}
2086
2087impl<'de> FromToml<'de> for Condition {
2088 fn from_toml(ctx: &mut toml_spanner::Context<'de>, item: &Item<'de>) -> Result<Self, Failed> {
2089 fn remap(mapping: &[(usize, usize)], unescaped: usize) -> usize {
2092 match mapping.binary_search_by_key(&unescaped, |x| x.0) {
2093 Ok(i) => {
2094 mapping[i].1
2096 }
2097 Err(i) => {
2098 unescaped
2101 + if i == 0 {
2102 0
2105 } else {
2106 mapping[i - 1].1 - mapping[i - 1].0
2108 }
2109 }
2110 }
2111 }
2112
2113 fn push_errors(
2116 ctx: &mut toml_spanner::Context<'_>,
2117 item: &Item<'_>,
2118 diagnostics: Vec<Diagnostic>,
2119 ) -> Failed {
2120 for diagnostic in diagnostics {
2121 let span = if let Some(label) = diagnostic.labels().next() {
2122 let label_span = label.span();
2123 let span = item.span();
2124
2125 let source = &ctx.source()[span.start as usize..span.end as usize];
2126 let offset = if source.starts_with(r#"""""#) | source.starts_with("'''") {
2127 3
2128 } else {
2129 1
2130 };
2131
2132 let mapping = escape_mapping(
2133 source
2134 .get(offset..(source.len() - offset))
2135 .expect("invalid TOML string"),
2136 );
2137
2138 let label_start = remap(&mapping, label_span.start());
2140 let label_end = remap(&mapping, label_span.end());
2141
2142 toml_spanner::Span::new(
2143 span.start + offset as u32 + label_start as u32,
2144 span.start + offset as u32 + label_end as u32,
2145 )
2146 } else {
2147 item.span()
2148 };
2149
2150 ctx.errors
2151 .push(toml_spanner::Error::custom(diagnostic.message(), span));
2152 }
2153
2154 Failed
2155 }
2156
2157 Self::new(String::from_toml(ctx, item)?).map_err(|diags| push_errors(ctx, item, diags))
2158 }
2159}
2160
2161#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
2166#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
2167#[schemars(rename_all = "snake_case", deny_unknown_fields)]
2168pub struct ConditionalArgs {
2169 pub condition: Condition,
2171 #[toml(default)]
2173 #[schemars(default)]
2174 pub args: Vec<String>,
2175}
2176
2177impl ConditionalArgs {
2178 pub fn validate(&self) -> Result<()> {
2183 if self.args.is_empty() {
2184 bail!("backend conditional arguments must have at least one argument specified");
2185 }
2186
2187 Ok(())
2188 }
2189}
2190
2191#[derive(Debug, Clone, Default, PartialEq, Eq, Toml, JsonSchema)]
2195#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
2196#[schemars(rename_all = "snake_case", deny_unknown_fields)]
2197pub struct AdditionalArgs {
2198 #[toml(default)]
2200 #[schemars(default)]
2201 pub args: Vec<String>,
2202 #[toml(default)]
2207 #[schemars(default)]
2208 pub conditional: Vec<ConditionalArgs>,
2209}
2210
2211impl AdditionalArgs {
2212 pub fn validate(&self) -> Result<()> {
2214 for arg in &self.conditional {
2215 arg.validate()?;
2216 }
2217
2218 Ok(())
2219 }
2220}
2221
2222mod byte_size {
2224 use bytesize::ByteSize;
2225 use toml_spanner::Context;
2226 use toml_spanner::Failed;
2227 use toml_spanner::Item;
2228
2229 pub fn from_toml<'de>(ctx: &mut Context<'de>, item: &Item<'de>) -> Result<ByteSize, Failed> {
2231 if let Some(s) = item.as_u64() {
2232 return Ok(ByteSize(s));
2233 }
2234
2235 if let Some(s) = item.as_str() {
2236 return s
2237 .parse()
2238 .map_err(|e| ctx.report_custom_error(format!("invalid byte size: {e}"), item));
2239 }
2240
2241 Err(ctx.report_expected_but_found(&"integer or string", item))
2242 }
2243}
2244
2245#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
2254#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
2255#[schemars(rename_all = "snake_case", deny_unknown_fields)]
2256pub struct LsfQueueConfig {
2257 pub name: String,
2260 pub max_cpu_per_task: Option<u64>,
2262 #[toml(FromToml with = byte_size, ToToml with = display)]
2264 #[schemars(with = "Option<u64>")]
2265 pub max_memory_per_task: Option<ByteSize>,
2266}
2267
2268impl LsfQueueConfig {
2269 pub async fn validate(&self, name: &str) -> Result<(), anyhow::Error> {
2271 let queue = &self.name;
2272 ensure!(!queue.is_empty(), "{name}_lsf_queue name cannot be empty");
2273 if let Some(max_cpu_per_task) = self.max_cpu_per_task {
2274 ensure!(
2275 max_cpu_per_task > 0,
2276 "{name}_lsf_queue `{queue}` must allow at least 1 CPU to be provisioned"
2277 );
2278 }
2279 if let Some(max_memory_per_task) = self.max_memory_per_task {
2280 ensure!(
2281 max_memory_per_task.as_u64() > 0,
2282 "{name}_lsf_queue `{queue}` must allow at least some memory to be provisioned"
2283 );
2284 }
2285 match tokio::time::timeout(
2286 std::time::Duration::from_secs(10),
2289 Command::new("bqueues").arg(queue).output(),
2290 )
2291 .await
2292 {
2293 Ok(output) => {
2294 let output = output.context("validating LSF queue")?;
2295 if !output.status.success() {
2296 let stdout = String::from_utf8_lossy(&output.stdout);
2297 let stderr = String::from_utf8_lossy(&output.stderr);
2298 error!(%stdout, %stderr, %queue, "failed to validate {name}_lsf_queue");
2299 Err(anyhow!("failed to validate {name}_lsf_queue `{queue}`"))
2300 } else {
2301 Ok(())
2302 }
2303 }
2304 Err(_) => Err(anyhow!(
2305 "timed out trying to validate {name}_lsf_queue `{queue}`"
2306 )),
2307 }
2308 }
2309}
2310
2311#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
2315#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
2316#[schemars(rename_all = "snake_case", deny_unknown_fields)]
2317pub struct LsfApptainerBackendConfig {
2318 pub interval: Option<u64>,
2322 pub max_concurrency: Option<u32>,
2330 #[toml(style = Header)]
2337 pub default_lsf_queue: Option<LsfQueueConfig>,
2338 #[toml(style = Header)]
2345 pub short_task_lsf_queue: Option<LsfQueueConfig>,
2346 #[toml(style = Header)]
2350 pub gpu_lsf_queue: Option<LsfQueueConfig>,
2351 #[toml(style = Header)]
2355 pub fpga_lsf_queue: Option<LsfQueueConfig>,
2356 pub job_name_prefix: Option<String>,
2359 #[toml(default)]
2361 #[schemars(default)]
2362 pub bsub: AdditionalArgs,
2363 #[toml(default)]
2370 #[schemars(default)]
2371 pub apptainer: ApptainerConfig,
2372}
2373
2374impl LsfApptainerBackendConfig {
2375 pub async fn validate(&self) -> Result<(), anyhow::Error> {
2377 if cfg!(not(unix)) {
2378 bail!("LSF + Apptainer backend is not supported on non-unix platforms");
2379 }
2380
2381 if let Some(queue) = &self.default_lsf_queue {
2387 queue.validate("default").await?;
2388 }
2389
2390 if let Some(queue) = &self.short_task_lsf_queue {
2391 queue.validate("short_task").await?;
2392 }
2393
2394 if let Some(queue) = &self.gpu_lsf_queue {
2395 queue.validate("gpu").await?;
2396 }
2397
2398 if let Some(queue) = &self.fpga_lsf_queue {
2399 queue.validate("fpga").await?;
2400 }
2401
2402 if let Some(prefix) = &self.job_name_prefix
2403 && prefix.len() > MAX_LSF_JOB_NAME_PREFIX
2404 {
2405 bail!(
2406 "LSF job name prefix `{prefix}` exceeds the maximum {MAX_LSF_JOB_NAME_PREFIX} \
2407 bytes"
2408 );
2409 }
2410
2411 self.bsub.validate()?;
2413
2414 self.apptainer.validate().await?;
2416
2417 Ok(())
2418 }
2419
2420 pub(crate) fn lsf_queue_for_task(
2425 &self,
2426 requirements: &Object,
2427 hints: &Object,
2428 ) -> Option<&LsfQueueConfig> {
2429 if let Some(queue) = self.fpga_lsf_queue.as_ref()
2431 && let Some(true) = requirements
2432 .get(wdl_ast::v1::TASK_REQUIREMENT_FPGA)
2433 .and_then(Value::as_boolean)
2434 {
2435 return Some(queue);
2436 }
2437
2438 if let Some(queue) = self.gpu_lsf_queue.as_ref()
2439 && let Some(true) = requirements
2440 .get(wdl_ast::v1::TASK_REQUIREMENT_GPU)
2441 .and_then(Value::as_boolean)
2442 {
2443 return Some(queue);
2444 }
2445
2446 if let Some(queue) = self.short_task_lsf_queue.as_ref()
2448 && let Some(true) = hints
2449 .get(wdl_ast::v1::TASK_HINT_SHORT_TASK)
2450 .and_then(Value::as_boolean)
2451 {
2452 return Some(queue);
2453 }
2454
2455 self.default_lsf_queue.as_ref()
2458 }
2459}
2460
2461#[derive(Debug, Clone, PartialEq, Eq, Toml, JsonSchema)]
2470#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
2471#[schemars(rename_all = "snake_case", deny_unknown_fields)]
2472pub struct SlurmPartitionConfig {
2473 pub name: String,
2476 pub max_cpu_per_task: Option<u64>,
2479 #[toml(FromToml with = byte_size, ToToml with = display)]
2481 #[schemars(with = "Option<u64>")]
2482 pub max_memory_per_task: Option<ByteSize>,
2483}
2484
2485impl SlurmPartitionConfig {
2486 pub async fn validate(&self, name: &str) -> Result<(), anyhow::Error> {
2489 let partition = &self.name;
2490 ensure!(
2491 !partition.is_empty(),
2492 "{name}_slurm_partition name cannot be empty"
2493 );
2494 if let Some(max_cpu_per_task) = self.max_cpu_per_task {
2495 ensure!(
2496 max_cpu_per_task > 0,
2497 "{name}_slurm_partition `{partition}` must allow at least 1 CPU to be provisioned"
2498 );
2499 }
2500 if let Some(max_memory_per_task) = self.max_memory_per_task {
2501 ensure!(
2502 max_memory_per_task.as_u64() > 0,
2503 "{name}_slurm_partition `{partition}` must allow at least some memory to be \
2504 provisioned"
2505 );
2506 }
2507 match tokio::time::timeout(
2508 std::time::Duration::from_secs(10),
2511 Command::new("scontrol")
2512 .arg("show")
2513 .arg("partition")
2514 .arg(partition)
2515 .output(),
2516 )
2517 .await
2518 {
2519 Ok(output) => {
2520 let output = output.context("validating Slurm partition")?;
2521 if !output.status.success() {
2522 let stdout = String::from_utf8_lossy(&output.stdout);
2523 let stderr = String::from_utf8_lossy(&output.stderr);
2524 error!(%stdout, %stderr, %partition, "failed to validate {name}_slurm_partition");
2525 Err(anyhow!(
2526 "failed to validate {name}_slurm_partition `{partition}`"
2527 ))
2528 } else {
2529 Ok(())
2530 }
2531 }
2532 Err(_) => Err(anyhow!(
2533 "timed out trying to validate {name}_slurm_partition `{partition}`"
2534 )),
2535 }
2536 }
2537}
2538
2539#[derive(Debug, Default, Clone, PartialEq, Eq, Toml, JsonSchema)]
2543#[toml(Toml, rename_all = "snake_case", deny_unknown_fields)]
2544#[schemars(rename_all = "snake_case", deny_unknown_fields)]
2545pub struct SlurmApptainerBackendConfig {
2546 pub interval: Option<u64>,
2550 pub max_concurrency: Option<u32>,
2558 #[toml(style = Header)]
2567 pub default_slurm_partition: Option<SlurmPartitionConfig>,
2568 #[toml(style = Header)]
2576 pub short_task_slurm_partition: Option<SlurmPartitionConfig>,
2577 #[toml(style = Header)]
2581 pub gpu_slurm_partition: Option<SlurmPartitionConfig>,
2582 #[toml(style = Header)]
2586 pub fpga_slurm_partition: Option<SlurmPartitionConfig>,
2587 #[toml(default)]
2589 #[schemars(default)]
2590 pub sbatch: AdditionalArgs,
2591 pub job_name_prefix: Option<String>,
2593 #[toml(default)]
2600 #[schemars(default)]
2601 pub apptainer: ApptainerConfig,
2602}
2603
2604impl SlurmApptainerBackendConfig {
2605 pub async fn validate(&self) -> Result<(), anyhow::Error> {
2607 if cfg!(not(unix)) {
2608 bail!("Slurm + Apptainer backend is not supported on non-unix platforms");
2609 }
2610
2611 if let Some(partition) = &self.default_slurm_partition {
2617 partition.validate("default").await?;
2618 }
2619 if let Some(partition) = &self.short_task_slurm_partition {
2620 partition.validate("short_task").await?;
2621 }
2622 if let Some(partition) = &self.gpu_slurm_partition {
2623 partition.validate("gpu").await?;
2624 }
2625 if let Some(partition) = &self.fpga_slurm_partition {
2626 partition.validate("fpga").await?;
2627 }
2628
2629 self.sbatch.validate()?;
2631
2632 self.apptainer.validate().await?;
2634
2635 Ok(())
2636 }
2637
2638 pub(crate) fn slurm_partition_for_task(
2643 &self,
2644 requirements: &Object,
2645 hints: &Object,
2646 ) -> Option<&SlurmPartitionConfig> {
2647 if let Some(partition) = self.fpga_slurm_partition.as_ref()
2653 && let Some(true) = requirements
2654 .get(wdl_ast::v1::TASK_REQUIREMENT_FPGA)
2655 .and_then(Value::as_boolean)
2656 {
2657 return Some(partition);
2658 }
2659
2660 if let Some(partition) = self.gpu_slurm_partition.as_ref()
2661 && let Some(true) = requirements
2662 .get(wdl_ast::v1::TASK_REQUIREMENT_GPU)
2663 .and_then(Value::as_boolean)
2664 {
2665 return Some(partition);
2666 }
2667
2668 if let Some(partition) = self.short_task_slurm_partition.as_ref()
2670 && let Some(true) = hints
2671 .get(wdl_ast::v1::TASK_HINT_SHORT_TASK)
2672 .and_then(Value::as_boolean)
2673 {
2674 return Some(partition);
2675 }
2676
2677 self.default_slurm_partition.as_ref()
2680 }
2681}
2682
2683#[derive(Debug, thiserror::Error)]
2685pub enum BuilderMergeError {
2686 #[error("failed to serialize after merging configuration")]
2688 Serialize(#[from] ToTomlError),
2689 #[error("failed to parse after merging configuration")]
2691 Parse {
2692 source: String,
2694 #[source]
2696 error: toml_spanner::Error,
2697 },
2698 #[error("failed to deserialize after merging configuration")]
2700 Deserialize {
2701 source: String,
2703 #[source]
2705 error: toml_spanner::FromTomlError,
2706 },
2707}
2708
2709struct BuilderErrorDisplay<'a>(&'static str, &'a Option<PathBuf>);
2711
2712impl fmt::Display for BuilderErrorDisplay<'_> {
2713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2714 match self.1 {
2715 Some(path) => write!(
2716 f,
2717 "failed to {op} configuration file `{path}`",
2718 op = self.0,
2719 path = path.display()
2720 ),
2721 None => write!(
2722 f,
2723 "failed to {op} in-memory configuration string",
2724 op = self.0
2725 ),
2726 }
2727 }
2728}
2729
2730#[derive(Debug, thiserror::Error)]
2732pub enum BuilderError {
2733 #[error("failed to read configuration file `{path}`")]
2735 Io {
2736 path: PathBuf,
2738 #[source]
2740 error: std::io::Error,
2741 },
2742 #[error("{}", BuilderErrorDisplay("parse", .path))]
2744 Parse {
2745 path: Option<PathBuf>,
2749 source: String,
2751 #[source]
2753 error: toml_spanner::Error,
2754 },
2755 #[error("{}", BuilderErrorDisplay("deserialize", .path))]
2757 Deserialize {
2758 path: Option<PathBuf>,
2762 source: String,
2764 #[source]
2766 error: toml_spanner::FromTomlError,
2767 },
2768 #[error(transparent)]
2770 Merge(#[from] BuilderMergeError),
2771}
2772
2773impl BuilderError {
2774 pub fn path(&self) -> impl fmt::Display + Clone {
2782 #[derive(Clone)]
2783 struct Helper<'a>(&'a BuilderError);
2784
2785 impl fmt::Display for Helper<'_> {
2786 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2787 match self.0 {
2788 BuilderError::Io { path, .. }
2789 | BuilderError::Parse {
2790 path: Some(path), ..
2791 }
2792 | BuilderError::Deserialize {
2793 path: Some(path), ..
2794 } => path.display().fmt(f),
2795 BuilderError::Parse { path: None, .. }
2796 | BuilderError::Deserialize { path: None, .. } => write!(f, "<string>"),
2797 BuilderError::Merge(_) => write!(f, "<merged>"),
2798 }
2799 }
2800 }
2801
2802 Helper(self)
2803 }
2804
2805 pub fn toml_error(&self) -> Option<&toml_spanner::Error> {
2810 match &self {
2811 Self::Parse { error, .. } | Self::Merge(BuilderMergeError::Parse { error, .. }) => {
2812 Some(error)
2813 }
2814 Self::Deserialize { error, .. }
2815 | Self::Merge(BuilderMergeError::Deserialize { error, .. }) => error.errors.first(),
2816 _ => None,
2817 }
2818 }
2819
2820 pub fn source(&self) -> Option<&str> {
2825 match &self {
2826 Self::Parse { source, .. }
2827 | Self::Deserialize { source, .. }
2828 | Self::Merge(BuilderMergeError::Parse { source, .. })
2829 | Self::Merge(BuilderMergeError::Deserialize { source, .. }) => Some(source),
2830 _ => None,
2831 }
2832 }
2833
2834 pub fn to_diagnostic(&self) -> Diagnostic {
2836 let mut diagnostic = Diagnostic::error(self.to_string());
2837
2838 if let Some(e) = self.toml_error() {
2839 for (span, text) in [e.primary_label(), e.secondary_label()]
2840 .into_iter()
2841 .flatten()
2842 {
2843 let span = Span::new(span.start as usize, (span.end - span.start) as usize);
2844 let text: &str = text.trim();
2845
2846 let text = if text.is_empty() {
2848 match e.kind() {
2849 ErrorKind::UnexpectedEof => "unexpected end of file",
2850 ErrorKind::RedefineAsArray { .. } => {
2851 "a previously defined table was redefined as an array"
2852 }
2853 ErrorKind::FileTooLarge => "file is too large",
2854 ErrorKind::Custom(message) => message,
2855 _ => text,
2856 }
2857 } else {
2858 text
2859 };
2860
2861 if text.is_empty() {
2862 diagnostic = diagnostic.with_highlight(span);
2863 } else {
2864 diagnostic = diagnostic.with_label(text, span);
2865 }
2866 }
2867 }
2868
2869 if matches!(self, Self::Merge(_)) {
2870 diagnostic = diagnostic.with_help("reported line numbers reflect merged TOML source");
2871 }
2872
2873 diagnostic
2874 }
2875}
2876
2877#[derive(Debug)]
2879enum Source {
2880 Path(PathBuf),
2882 String(String),
2884}
2885
2886#[derive(Default, Debug)]
2900pub struct ConfigBuilder<T> {
2901 sources: Vec<Source>,
2903 _phantom: PhantomData<T>,
2905}
2906
2907impl<T> ConfigBuilder<T> {
2908 pub fn with_string_source(mut self, toml: impl Into<String>) -> Self {
2910 self.sources.push(Source::String(toml.into()));
2911 self
2912 }
2913
2914 pub fn with_file_source(mut self, path: impl Into<PathBuf>) -> Self {
2916 self.sources.push(Source::Path(path.into()));
2917 self
2918 }
2919
2920 pub fn try_build(self) -> Result<T, BuilderError>
2925 where
2926 T: ToToml + for<'de> FromToml<'de>,
2927 {
2928 let sources: Vec<(Option<PathBuf>, String)> = self
2930 .sources
2931 .into_iter()
2932 .map(|s| match s {
2933 Source::Path(path) => {
2934 let source = fs::read_to_string(&path).map_err(|e| BuilderError::Io {
2935 path: path.clone(),
2936 error: e,
2937 })?;
2938
2939 Ok((Some(path), source))
2940 }
2941 Source::String(source) => Ok((None, source)),
2942 })
2943 .collect::<Result<_, BuilderError>>()?;
2944
2945 let arena = Arena::new();
2947 let documents = sources
2948 .iter()
2949 .map(|(path, source)| {
2950 toml_spanner::parse(source, &arena).map_err(|e| BuilderError::Parse {
2951 path: path.clone(),
2952 source: source.clone(),
2953 error: e,
2954 })
2955 })
2956 .collect::<Result<Vec<_>, _>>()?;
2957
2958 let mut merged_table: Table<'_> = Table::new();
2960 for (index, mut document) in documents.into_iter().enumerate() {
2961 document.to::<T>().map_err(|e| {
2964 let (path, source) = &sources[index];
2965 BuilderError::Deserialize {
2966 path: path.clone(),
2967 source: source.clone(),
2968 error: e,
2969 }
2970 })?;
2971
2972 Self::merge_tables(document.into_table(), &mut merged_table, &arena);
2974 }
2975
2976 let source =
2980 toml_spanner::to_string(&merged_table).map_err(BuilderMergeError::Serialize)?;
2981
2982 Ok(toml_spanner::parse(&source, &arena)
2984 .map_err(|e| BuilderMergeError::Parse {
2985 source: source.clone(),
2986 error: e,
2987 })?
2988 .to()
2989 .map_err(|e| BuilderMergeError::Deserialize {
2990 source: source.clone(),
2991 error: e,
2992 })?)
2993 }
2994
2995 fn merge_tables<'de>(src: Table<'de>, dest: &mut Table<'de>, arena: &'de Arena) {
3006 for (key, src_item) in src {
3007 let Some(dest_item) = dest.get_mut(key.name) else {
3008 dest.insert(key, src_item, arena);
3009 continue;
3010 };
3011
3012 if let Some(src_array) = src_item.as_array()
3014 && let Some(dest_array) = dest_item.as_array_mut()
3015 {
3016 for element in src_array {
3017 dest_array.push(element.clone_in(arena), arena);
3018 }
3019 continue;
3020 }
3021
3022 if let Some(_) = src_item.as_table()
3024 && let Some(dest_table) = dest_item.as_table_mut()
3025 {
3026 Self::merge_tables(src_item.into_table().unwrap(), dest_table, arena);
3027 continue;
3028 }
3029
3030 dest.insert(key, src_item, arena);
3032 }
3033 }
3034}
3035
3036#[cfg(test)]
3037mod test {
3038 use std::collections::HashMap;
3039 use std::io::Write;
3040
3041 use codespan_reporting::files::SimpleFile;
3042 use codespan_reporting::term::DisplayStyle;
3043 use codespan_reporting::term::emit_into_string;
3044 use codespan_reporting::term::{self};
3045 use futures::future::BoxFuture;
3046 use pretty_assertions::assert_eq;
3047 use tempfile::TempPath;
3048 use tempfile::tempdir;
3049
3050 use super::*;
3051 use crate::ONE_GIBIBYTE;
3052 use crate::TaskInputs;
3053 use crate::backend::TaskExecutionConstraints;
3054 use crate::http::Location;
3055 use crate::v1::DEFAULT_TASK_REQUIREMENT_CPU;
3056 use crate::v1::DEFAULT_TASK_REQUIREMENT_DISKS;
3057 use crate::v1::DEFAULT_TASK_REQUIREMENT_MEMORY;
3058
3059 #[test]
3060 fn redacted_secret() {
3061 let mut map: HashMap<_, SecretString> = HashMap::new();
3062 map.insert(
3063 "foo",
3064 SecretString {
3065 inner: "secret".into(),
3066 redacted: false,
3067 },
3068 );
3069
3070 assert_eq!(
3071 toml_spanner::to_string(&map).unwrap().trim(),
3072 format!(r#"foo = "secret""#)
3073 );
3074
3075 map.insert(
3076 "foo",
3077 SecretString {
3078 inner: "secret".into(),
3079 redacted: true,
3080 },
3081 );
3082 assert_eq!(
3083 toml_spanner::to_string(&map).unwrap().trim(),
3084 format!(r#"foo = "{REDACTED}""#)
3085 );
3086 }
3087
3088 #[test]
3089 fn redacted_config() {
3090 let config = Config {
3091 backends: [
3092 (
3093 "first".to_string(),
3094 TesBackendConfig {
3095 auth: Some(TesBackendAuthConfig::Basic {
3096 config: BasicAuthConfig {
3097 username: "foo".into(),
3098 password: "secret".into(),
3099 },
3100 }),
3101 ..Default::default()
3102 }
3103 .into(),
3104 ),
3105 (
3106 "second".to_string(),
3107 TesBackendConfig {
3108 auth: Some(
3109 BearerAuthConfig {
3110 token: "secret".into(),
3111 }
3112 .into(),
3113 ),
3114 ..Default::default()
3115 }
3116 .into(),
3117 ),
3118 ]
3119 .into(),
3120 storage: StorageConfig {
3121 azure: AzureStorageConfig {
3122 auth: Some(AzureStorageAuthConfig {
3123 account_name: "foo".into(),
3124 access_key: "secret".into(),
3125 }),
3126 },
3127 s3: S3StorageConfig {
3128 auth: Some(S3StorageAuthConfig {
3129 access_key_id: "foo".into(),
3130 secret_access_key: "secret".into(),
3131 }),
3132 ..Default::default()
3133 },
3134 google: GoogleStorageConfig {
3135 auth: Some(GoogleStorageAuthConfig {
3136 access_key: "foo".into(),
3137 secret: "secret".into(),
3138 }),
3139 },
3140 },
3141 ..Default::default()
3142 };
3143
3144 let toml = toml_spanner::to_string(&config).unwrap();
3145 assert!(toml.contains("secret"), "`{toml}` contains a secret");
3146 }
3147
3148 #[tokio::test]
3149 async fn test_config_validate() {
3150 let mut config = Config::default();
3152 config.task.retries = 255.into();
3153 assert_eq!(
3154 config.validate().await.unwrap_err().to_string(),
3155 "configuration value `task.retries` cannot exceed 100"
3156 );
3157
3158 let mut config = Config::default();
3160 config.workflow.scatter.concurrency = 0;
3161 assert_eq!(
3162 config.validate().await.unwrap_err().to_string(),
3163 "configuration value `workflow.scatter.concurrency` cannot be zero"
3164 );
3165
3166 let config = Config {
3168 backend: "foo".into(),
3169 ..Default::default()
3170 };
3171 assert_eq!(
3172 config.validate().await.unwrap_err().to_string(),
3173 "a backend named `foo` is not present in the configuration"
3174 );
3175 let config = Config {
3176 backend: "bar".into(),
3177 backends: [("foo".to_string(), BackendConfig::default())].into(),
3178 ..Default::default()
3179 };
3180 assert_eq!(
3181 config.validate().await.unwrap_err().to_string(),
3182 "a backend named `bar` is not present in the configuration"
3183 );
3184
3185 let config = Config {
3187 backend: "foo".to_string(),
3188 backends: [("foo".to_string(), BackendConfig::default())].into(),
3189 ..Default::default()
3190 };
3191 config.validate().await.expect("config should validate");
3192
3193 let config = Config {
3195 backends: [(
3196 "default".to_string(),
3197 LocalBackendConfig {
3198 cpu: Some(0),
3199 ..Default::default()
3200 }
3201 .into(),
3202 )]
3203 .into(),
3204 ..Default::default()
3205 };
3206 assert_eq!(
3207 config.validate().await.unwrap_err().to_string(),
3208 "local backend configuration value `cpu` cannot be zero"
3209 );
3210 let config = Config {
3211 backends: [(
3212 "default".to_string(),
3213 LocalBackendConfig {
3214 cpu: Some(10000000),
3215 ..Default::default()
3216 }
3217 .into(),
3218 )]
3219 .into(),
3220 ..Default::default()
3221 };
3222 assert!(
3223 config
3224 .validate()
3225 .await
3226 .unwrap_err()
3227 .to_string()
3228 .starts_with(
3229 "local backend configuration value `cpu` cannot exceed the virtual CPUs \
3230 available to the host"
3231 )
3232 );
3233
3234 let config = Config {
3236 backends: [(
3237 "default".to_string(),
3238 LocalBackendConfig {
3239 memory: Some("0 GiB".to_string()),
3240 ..Default::default()
3241 }
3242 .into(),
3243 )]
3244 .into(),
3245 ..Default::default()
3246 };
3247 assert_eq!(
3248 config.validate().await.unwrap_err().to_string(),
3249 "local backend configuration value `memory` cannot be zero"
3250 );
3251 let config = Config {
3252 backends: [(
3253 "default".to_string(),
3254 LocalBackendConfig {
3255 memory: Some("100 meows".to_string()),
3256 ..Default::default()
3257 }
3258 .into(),
3259 )]
3260 .into(),
3261 ..Default::default()
3262 };
3263 assert_eq!(
3264 config.validate().await.unwrap_err().to_string(),
3265 "local backend configuration value `memory` has invalid value `100 meows`"
3266 );
3267
3268 let config = Config {
3269 backends: [(
3270 "default".to_string(),
3271 LocalBackendConfig {
3272 memory: Some("1000 TiB".to_string()),
3273 ..Default::default()
3274 }
3275 .into(),
3276 )]
3277 .into(),
3278 ..Default::default()
3279 };
3280 assert!(
3281 config
3282 .validate()
3283 .await
3284 .unwrap_err()
3285 .to_string()
3286 .starts_with(
3287 "local backend configuration value `memory` cannot exceed the total memory of \
3288 the host"
3289 )
3290 );
3291
3292 let config = Config {
3294 backends: [("default".to_string(), TesBackendConfig::default().into())].into(),
3295 ..Default::default()
3296 };
3297 assert_eq!(
3298 config.validate().await.unwrap_err().to_string(),
3299 "TES backend configuration value `url` is required"
3300 );
3301
3302 let config = Config {
3304 backends: [(
3305 "default".to_string(),
3306 TesBackendConfig {
3307 url: Some("https://example.com".parse().unwrap()),
3308 max_concurrency: Some(0),
3309 ..Default::default()
3310 }
3311 .into(),
3312 )]
3313 .into(),
3314 ..Default::default()
3315 };
3316 assert_eq!(
3317 config.validate().await.unwrap_err().to_string(),
3318 "TES backend configuration value `max_concurrency` cannot be zero"
3319 );
3320
3321 let config = Config {
3323 backends: [(
3324 "default".to_string(),
3325 TesBackendConfig {
3326 url: Some("http://example.com".parse().unwrap()),
3327 inputs: Some("http://example.com".parse().unwrap()),
3328 outputs: Some("http://example.com".parse().unwrap()),
3329 ..Default::default()
3330 }
3331 .into(),
3332 )]
3333 .into(),
3334 ..Default::default()
3335 };
3336 assert_eq!(
3337 config.validate().await.unwrap_err().to_string(),
3338 "TES backend configuration value `url` has invalid value `http://example.com/`: URL \
3339 must use a HTTPS scheme"
3340 );
3341
3342 let config = Config {
3344 backends: [(
3345 "default".to_string(),
3346 TesBackendConfig {
3347 url: Some("http://example.com".parse().unwrap()),
3348 inputs: Some("http://example.com".parse().unwrap()),
3349 outputs: Some("http://example.com".parse().unwrap()),
3350 insecure: true,
3351 ..Default::default()
3352 }
3353 .into(),
3354 )]
3355 .into(),
3356 ..Default::default()
3357 };
3358 config
3359 .validate()
3360 .await
3361 .expect("configuration should validate");
3362
3363 let mut config = Config::default();
3365 config.http.parallelism = 0.into();
3366 assert_eq!(
3367 config.validate().await.unwrap_err().to_string(),
3368 "configuration value `http.parallelism` cannot be zero"
3369 );
3370
3371 let mut config = Config::default();
3373 config.http.parallelism = 5.into();
3374 assert!(
3375 config.validate().await.is_ok(),
3376 "should pass for valid configuration"
3377 );
3378 let mut config = Config::default();
3379 config.http.parallelism = Parallelism::default();
3380 assert!(config.validate().await.is_ok(), "should pass for default");
3381
3382 #[cfg(unix)]
3384 {
3385 let job_name_prefix = "A".repeat(MAX_LSF_JOB_NAME_PREFIX * 2);
3386 let mut config = Config {
3387 experimental_features_enabled: true,
3388 ..Default::default()
3389 };
3390 config.backends.insert(
3391 "default".to_string(),
3392 LsfApptainerBackendConfig {
3393 job_name_prefix: Some(job_name_prefix.clone()),
3394 ..Default::default()
3395 }
3396 .into(),
3397 );
3398 assert_eq!(
3399 config.validate().await.unwrap_err().to_string(),
3400 format!("LSF job name prefix `{job_name_prefix}` exceeds the maximum 100 bytes")
3401 );
3402 }
3403 }
3404
3405 fn create_temp_file(contents: &str) -> TempPath {
3406 let mut file: tempfile::NamedTempFile =
3407 tempfile::NamedTempFile::new().expect("failed to create temporary file");
3408 file.write_all(contents.as_bytes())
3409 .expect("failed to write temporary file");
3410 file.into_temp_path()
3411 }
3412
3413 #[test]
3414 fn it_builds_with_no_sources() {
3415 let config = Config::builder().try_build().expect("should build");
3416 assert_eq!(config, Config::default(), "should be equal");
3417 }
3418
3419 #[test]
3420 fn it_builds_with_one_source() {
3421 let path = create_temp_file("backend = 'foo'");
3422
3423 let config = Config::builder()
3424 .with_file_source(&path)
3425 .try_build()
3426 .expect("should build");
3427 assert_eq!(
3428 config,
3429 Config {
3430 backend: "foo".into(),
3431 ..Default::default()
3432 },
3433 "should be equal"
3434 );
3435 }
3436
3437 #[test]
3438 fn it_errors_on_invalid_parse() {
3439 let path = create_temp_file("invalid");
3440
3441 let e = Config::builder()
3442 .with_file_source(&path)
3443 .try_build()
3444 .expect_err("should fail");
3445
3446 let source = e.source().expect("should have source");
3447
3448 let diagnostic = e.to_diagnostic();
3449 let error = emit_into_string(
3450 &term::Config {
3451 display_style: DisplayStyle::Rich,
3452 ..Default::default()
3453 },
3454 &SimpleFile::new(e.path(), source),
3455 &diagnostic.to_codespan(()),
3456 )
3457 .expect("should emit");
3458
3459 assert!(
3460 error.contains("expected an equals"),
3461 "the error `{error}` does not contain the expected message"
3462 );
3463 }
3464
3465 #[test]
3466 fn it_errors_on_invalid_deserialization() {
3467 let path = create_temp_file("backend = 42");
3468
3469 let e = Config::builder()
3470 .with_file_source(&path)
3471 .try_build()
3472 .expect_err("should fail");
3473
3474 let source = e.source().expect("should have source");
3475
3476 let diagnostic = e.to_diagnostic();
3477 let error = emit_into_string(
3478 &term::Config {
3479 display_style: DisplayStyle::Rich,
3480 ..Default::default()
3481 },
3482 &SimpleFile::new(e.path(), source),
3483 &diagnostic.to_codespan(()),
3484 )
3485 .expect("should emit");
3486
3487 assert!(
3488 error.contains("expected a string"),
3489 "the error `{error}` does not contain the expected message"
3490 );
3491 }
3492
3493 #[test]
3494 fn it_merges_sources() {
3495 let first = create_temp_file(
3496 r#"
3497backend = 'foo'
3498
3499[task]
3500excluded_cache_inputs = ['1', '2', '3']
3501
3502[backends.foo]
3503type = 'local'
3504"#,
3505 );
3506
3507 let second = create_temp_file(
3508 r#"
3509[task]
3510excluded_cache_inputs = ['4', '5']
3511
3512[backends.bar]
3513type = 'docker'
3514"#,
3515 );
3516
3517 let third: TempPath = create_temp_file(
3518 r#"
3519backend = 'baz'
3520
3521[task]
3522excluded_cache_inputs = ['6', '7', '8']
3523
3524[backends.baz]
3525type = 'tes'
3526"#,
3527 );
3528
3529 let fourth = r#"
3530backend = 'qux'
3531
3532[task]
3533excluded_cache_inputs = ['9', '10']
3534
3535[backends.qux]
3536type = 'lsf_apptainer'
3537"#;
3538
3539 let config = Config::builder()
3540 .with_file_source(&first)
3541 .with_file_source(&second)
3542 .with_file_source(&third)
3543 .with_string_source(fourth)
3544 .try_build()
3545 .expect("should build");
3546
3547 assert_eq!(
3548 config,
3549 Config {
3550 backend: "qux".into(),
3551 task: TaskConfig {
3552 excluded_cache_inputs: vec![
3553 "1".to_string(),
3554 "2".to_string(),
3555 "3".to_string(),
3556 "4".to_string(),
3557 "5".to_string(),
3558 "6".to_string(),
3559 "7".to_string(),
3560 "8".to_string(),
3561 "9".to_string(),
3562 "10".to_string(),
3563 ],
3564 ..Default::default()
3565 },
3566 backends: IndexMap::from_iter([
3567 ("foo".to_string(), LocalBackendConfig::default().into()),
3568 ("bar".to_string(), DockerBackendConfig::default().into()),
3569 ("baz".to_string(), TesBackendConfig::default().into()),
3570 (
3571 "qux".to_string(),
3572 LsfApptainerBackendConfig::default().into()
3573 ),
3574 ]),
3575 ..Default::default()
3576 },
3577 "should be equal"
3578 );
3579 }
3580
3581 #[test]
3582 fn parallelism_serialization() {
3583 let map: HashMap<&str, Parallelism> =
3584 HashMap::from_iter([("value", Parallelism::Available)]);
3585 assert_eq!(
3586 toml_spanner::to_string(&map).unwrap(),
3587 format!("value = \"available\"\n")
3588 );
3589
3590 let map: HashMap<&str, Parallelism> =
3591 HashMap::from_iter([("value", Parallelism::Use(123))]);
3592 assert_eq!(toml_spanner::to_string(&map).unwrap(), "value = 123\n");
3593 }
3594
3595 #[test]
3596 fn parallelism_deserialization() {
3597 let map: HashMap<String, Parallelism> =
3598 toml_spanner::from_str("value = 'available'").unwrap();
3599 assert_eq!(map["value"], Parallelism::Available);
3600
3601 let map: HashMap<String, Parallelism> = toml_spanner::from_str("value = 123").unwrap();
3602 assert_eq!(map["value"], Parallelism::Use(123));
3603
3604 let expected_error =
3605 "expected a positive integer or `available` for parallelism at `value`";
3606
3607 let error =
3608 toml_spanner::from_str::<HashMap<String, Parallelism>>("value = 'wrong'").unwrap_err();
3609 assert_eq!(error.to_string(), expected_error);
3610
3611 let error =
3612 toml_spanner::from_str::<HashMap<String, Parallelism>>("value = 0").unwrap_err();
3613 assert_eq!(error.to_string(), expected_error);
3614
3615 let error =
3616 toml_spanner::from_str::<HashMap<String, Parallelism>>("value = -10").unwrap_err();
3617 assert_eq!(error.to_string(), expected_error);
3618 }
3619
3620 #[test]
3621 fn retries_serialization() {
3622 let map: HashMap<&str, Retries> = HashMap::from_iter([("value", Retries::Default)]);
3623 assert_eq!(
3624 toml_spanner::to_string(&map).unwrap(),
3625 format!("value = \"default\"\n")
3626 );
3627
3628 let map: HashMap<&str, Retries> = HashMap::from_iter([("value", Retries::Use(123))]);
3629 assert_eq!(toml_spanner::to_string(&map).unwrap(), "value = 123\n");
3630 }
3631
3632 #[test]
3633 fn retries_deserialization() {
3634 let map: HashMap<String, Retries> = toml_spanner::from_str("value = 'default'").unwrap();
3635 assert_eq!(map["value"], Retries::Default);
3636
3637 let map: HashMap<String, Retries> = toml_spanner::from_str("value = 12").unwrap();
3638 assert_eq!(map["value"], Retries::Use(12));
3639
3640 let map: HashMap<String, Retries> = toml_spanner::from_str("value = 0").unwrap();
3641 assert_eq!(map["value"], Retries::Use(0));
3642
3643 let expected_error = format!(
3644 "expected an integer less than {MAX_RETRIES} or `default` for retries at `value`"
3645 );
3646
3647 let error =
3648 toml_spanner::from_str::<HashMap<String, Retries>>("value = 'wrong'").unwrap_err();
3649 assert_eq!(error.to_string(), expected_error);
3650
3651 let error = toml_spanner::from_str::<HashMap<String, Retries>>("value = 101").unwrap_err();
3652 assert_eq!(error.to_string(), expected_error);
3653
3654 let error = toml_spanner::from_str::<HashMap<String, Retries>>("value = -10").unwrap_err();
3655 assert_eq!(error.to_string(), expected_error);
3656 }
3657
3658 #[test]
3659 fn mapping_escape_indexes() {
3660 assert!(escape_mapping("").is_empty());
3662
3663 assert!(escape_mapping("hello world!").is_empty());
3665
3666 assert_eq!(escape_mapping(r#"\"\""#), &[(1, 2), (2, 4)]);
3669 assert_eq!(escape_mapping(r#"\u0022\u0022"#), &[(1, 6), (2, 12)]);
3670 assert_eq!(
3671 escape_mapping(r#"\U00000022\U00000022"#),
3672 &[(1, 10), (2, 20)]
3673 );
3674
3675 assert_eq!(
3677 escape_mapping(r#"\"foo\u0022 == \U00000022bar\" && \"\" == \"\n\""#),
3678 &[
3679 (1, 2), (5, 11), (10, 25), (14, 30), (19, 36), (20, 38), (25, 44), (26, 46), (27, 48) ]
3689 );
3690 }
3691
3692 #[test]
3693 fn conditional_args_serialization() {
3694 let error = toml_spanner::from_str::<ConditionalArgs>("condition = 1").unwrap_err();
3696 assert_eq!(
3697 error.to_string(),
3698 "expected a string, found integer at `condition`"
3699 );
3700
3701 let error =
3703 toml_spanner::from_str::<ConditionalArgs>(r#"condition = "foo bar""#).unwrap_err();
3704 assert_eq!(error.to_string(), "expected a single WDL expression");
3705
3706 let error =
3708 toml_spanner::from_str::<ConditionalArgs>(r#"condition = "{ foo: }""#).unwrap_err();
3709 assert_eq!(error.to_string(), "expected expression, but found `}`");
3710
3711 let error = toml_spanner::from_str::<ConditionalArgs>(r#"condition = "1""#).unwrap_err();
3713 assert_eq!(
3714 error.to_string(),
3715 "conditional expression is expected to be type `Boolean`, but found type `Int`"
3716 );
3717 let error = toml_spanner::from_str::<ConditionalArgs>(r#"condition = "hint""#).unwrap_err();
3718 assert_eq!(
3719 error.to_string(),
3720 "conditional expression is expected to be type `Boolean`, but found type `Object`"
3721 );
3722
3723 let error = toml_spanner::from_str::<ConditionalArgs>(r#"condition = "foo""#).unwrap_err();
3725 assert_eq!(error.to_string(), "unknown name `foo`");
3726
3727 let error =
3729 toml_spanner::from_str::<ConditionalArgs>(r#"condition = "Foo {}""#).unwrap_err();
3730 assert_eq!(error.to_string(), "unknown type name `Foo`");
3731
3732 let args: ConditionalArgs = toml_spanner::from_str(r#"condition = "true""#).unwrap();
3734 assert_eq!(args.condition.raw, "true");
3735 assert_eq!(
3736 toml_spanner::to_string(&args).unwrap(),
3737 "condition = \"true\"\nargs = []\n"
3738 );
3739
3740 let args: ConditionalArgs =
3741 toml_spanner::from_str(r#"condition = "cpu == 1 && hint.bar == \"foo\"""#).unwrap();
3742 assert_eq!(args.condition.raw, r#"cpu == 1 && hint.bar == "foo""#);
3743 assert_eq!(
3744 toml_spanner::to_string(&args).unwrap(),
3745 "condition = 'cpu == 1 && hint.bar == \"foo\"'\nargs = []\n"
3746 );
3747 }
3748
3749 #[test]
3750 fn validate_conditional_args() {
3751 let args: ConditionalArgs = toml_spanner::from_str(r#"condition = "true""#).unwrap();
3753 assert_eq!(
3754 args.validate().unwrap_err().to_string(),
3755 "backend conditional arguments must have at least one argument specified"
3756 );
3757 }
3758
3759 #[tokio::test]
3760 async fn evaluate_conditions() {
3761 struct Context {
3763 cpu: f64,
3764 memory: u64,
3765 gpu: bool,
3766 fpga: bool,
3767 disks: i64,
3768 inputs: TaskInputs,
3769 hints: Object,
3770 }
3771
3772 impl Default for Context {
3773 fn default() -> Self {
3774 Self {
3775 cpu: DEFAULT_TASK_REQUIREMENT_CPU,
3776 memory: DEFAULT_TASK_REQUIREMENT_MEMORY as u64,
3777 gpu: false,
3778 fpga: false,
3779 disks: (DEFAULT_TASK_REQUIREMENT_DISKS * ONE_GIBIBYTE) as i64,
3780 inputs: Default::default(),
3781 hints: Default::default(),
3782 }
3783 }
3784 }
3785
3786 struct Transferer;
3787
3788 impl crate::http::Transferer for Transferer {
3789 fn download<'a>(&'a self, _: &'a Url) -> BoxFuture<'a, Result<Location>> {
3790 unimplemented!()
3791 }
3792
3793 fn upload<'a>(&'a self, _: &'a Path, _: &'a Url) -> BoxFuture<'a, Result<()>> {
3794 unimplemented!()
3795 }
3796
3797 fn size<'a>(&'a self, _: &'a Url) -> BoxFuture<'a, anyhow::Result<Option<u64>>> {
3798 unimplemented!()
3799 }
3800
3801 fn walk<'a>(&'a self, _: &'a Url) -> BoxFuture<'a, Result<Arc<[String]>>> {
3802 unimplemented!()
3803 }
3804
3805 fn exists<'a>(&'a self, _: &'a Url) -> BoxFuture<'a, Result<bool>> {
3806 unimplemented!()
3807 }
3808
3809 fn digest<'a>(
3810 &'a self,
3811 _: &'a Url,
3812 ) -> BoxFuture<'a, Result<Option<Arc<cloud_copy::ContentDigest>>>> {
3813 unimplemented!()
3814 }
3815 }
3816
3817 async fn eval(context: Context, expression: &str) -> Result<bool> {
3821 let dir = tempdir().context("failed to create temporary directory")?;
3822 let condition = Condition::new(expression).expect("invalid expression");
3823 condition
3824 .evaluate(
3825 &ExecuteTaskRequest {
3826 id: "test",
3827 command: "",
3828 inputs: &context.inputs,
3829 backend_inputs: &[],
3830 requirements: &Object::empty(),
3831 hints: &context.hints,
3832 env: &Default::default(),
3833 constraints: &TaskExecutionConstraints {
3834 container: None,
3835 cpu: context.cpu,
3836 memory: context.memory,
3837 gpu: if context.gpu {
3838 vec![String::new()]
3839 } else {
3840 Default::default()
3841 },
3842 fpga: if context.fpga {
3843 vec![String::new()]
3844 } else {
3845 Default::default()
3846 },
3847 disks: IndexMap::from_iter([("".into(), context.disks)]),
3848 },
3849 base_dir: &EvaluationPath::from_local_path(dir.path().into()),
3850 attempt_dir: &dir.path().join("0"),
3851 temp_dir: &dir.path().join("tmp"),
3852 },
3853 &Transferer,
3854 )
3855 .await
3856 }
3857
3858 assert_eq!(eval(Context::default(), "true").await.unwrap(), true);
3860 assert_eq!(eval(Context::default(), "false").await.unwrap(), false);
3861 assert_eq!(eval(Context::default(), "cpu == 1").await.unwrap(), true);
3862 assert_eq!(
3863 eval(Context::default(), "memory == 2147483648")
3864 .await
3865 .unwrap(),
3866 true
3867 );
3868 assert_eq!(eval(Context::default(), "gpu").await.unwrap(), false);
3869 assert_eq!(eval(Context::default(), "fpga").await.unwrap(), false);
3870 assert_eq!(
3871 eval(Context::default(), "disks == 1073741824")
3872 .await
3873 .unwrap(),
3874 true
3875 );
3876 assert_eq!(
3877 eval(Context::default(), "defined(hint.foo)").await.unwrap(),
3878 false
3879 );
3880
3881 assert_eq!(
3883 eval(
3884 Context {
3885 cpu: 10.,
3886 memory: 10 * 1024 * 1024,
3887 gpu: true,
3888 fpga: true,
3889 disks: 1024 * 1024,
3890 inputs: Default::default(),
3891 hints: Object::new(IndexMap::from_iter([(
3892 "foo".into(),
3893 "hi".to_string().into()
3894 )]))
3895 },
3896 r#"cpu == 10 && memory == 10*1024*1024 && gpu && fpga && disks == 1024 * 1024 && hint.foo == "hi""#
3897 )
3898 .await
3899 .unwrap(),
3900 true
3901 );
3902
3903 let mut context = Context {
3905 hints: Object::new(IndexMap::from_iter([(
3906 "foo".into(),
3907 "hi".to_string().into(),
3908 )])),
3909 ..Default::default()
3910 };
3911 context
3912 .inputs
3913 .override_hint("foo", "overridden!".to_string());
3914 assert_eq!(
3915 eval(context, r#"hint.foo == "overridden!""#).await.unwrap(),
3916 true
3917 );
3918 }
3919}