Skip to content

Commit 1a0285e

Browse files
committed
Tests for template conditions feature
Signed-off-by: itowlson <[email protected]>
1 parent 18071ef commit 1a0285e

File tree

3 files changed

+201
-15
lines changed

3 files changed

+201
-15
lines changed

crates/templates/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod run;
1717
mod source;
1818
mod store;
1919
mod template;
20+
mod toml;
2021
mod writer;
2122

2223
pub use manager::*;

crates/templates/src/template.rs

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ enum TemplateVariantKind {
4747
}
4848

4949
/// The variant mode in which a template should be run.
50-
#[derive(Clone, Debug, Eq, PartialEq)]
50+
#[derive(Clone, Debug)]
5151
pub enum TemplateVariantInfo {
5252
/// Create a new application from the template.
5353
NewApplication,
@@ -111,6 +111,8 @@ pub(crate) struct Conditional {
111111
#[derive(Clone, Debug)]
112112
pub(crate) enum Condition {
113113
ManifestEntryExists(Vec<String>),
114+
#[cfg(test)]
115+
Always(bool),
114116
}
115117

116118
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
@@ -601,9 +603,11 @@ impl Condition {
601603
let Ok(table) = toml::from_str::<toml::Value>(&toml_text) else {
602604
return false;
603605
};
604-
get_at(table, path).is_some()
606+
crate::toml::get_at(table, path).is_some()
605607
}
606608
},
609+
#[cfg(test)]
610+
Self::Always(b) => *b,
607611
}
608612
}
609613
}
@@ -638,19 +642,127 @@ fn validate_v1_manifest(raw: &RawTemplateManifestV1) -> anyhow::Result<()> {
638642
Ok(())
639643
}
640644

641-
fn get_at(value: toml::Value, path: &[String]) -> Option<toml::Value> {
642-
match path.split_first() {
643-
None => Some(value), // we are at the end of the path and we have a thing
644-
Some((first, rest)) => {
645-
match value.as_table() {
646-
None => None, // we need to key into it and we can't
647-
Some(t) => {
648-
match t.get(first) {
649-
None => None, // we tried to key into it and no match
650-
Some(v) => get_at(v.clone(), rest), // we pathed into it! keep pathing
651-
}
652-
}
653-
}
645+
#[cfg(test)]
646+
mod test {
647+
use super::*;
648+
649+
struct TempFile(tempfile::TempDir, PathBuf);
650+
651+
impl TempFile {
652+
fn path(&self) -> PathBuf {
653+
self.1.clone()
654654
}
655655
}
656+
657+
fn make_temp_manifest(content: &str) -> TempFile {
658+
let temp_dir = tempfile::tempdir().unwrap();
659+
let temp_file = temp_dir.path().join("spin.toml");
660+
std::fs::write(&temp_file, content).unwrap();
661+
TempFile(temp_dir, temp_file)
662+
}
663+
664+
#[test]
665+
fn manifest_entry_exists_condition_is_false_for_new_app() {
666+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
667+
"application.trigger.redis".to_owned(),
668+
));
669+
assert!(!condition.is_true(&TemplateVariantInfo::NewApplication));
670+
}
671+
672+
#[test]
673+
fn manifest_entry_exists_condition_is_false_if_not_present_in_existing_manifest() {
674+
let temp_file =
675+
make_temp_manifest("name = \"hello\"\n[application.trigger.http]\nbase = \"/\"");
676+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
677+
"application.trigger.redis".to_owned(),
678+
));
679+
assert!(!condition.is_true(&TemplateVariantInfo::AddComponent {
680+
manifest_path: temp_file.path()
681+
}));
682+
}
683+
684+
#[test]
685+
fn manifest_entry_exists_condition_is_true_if_present_in_existing_manifest() {
686+
let temp_file = make_temp_manifest(
687+
"name = \"hello\"\n[application.trigger.redis]\nchannel = \"HELLO\"",
688+
);
689+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
690+
"application.trigger.redis".to_owned(),
691+
));
692+
assert!(condition.is_true(&TemplateVariantInfo::AddComponent {
693+
manifest_path: temp_file.path()
694+
}));
695+
}
696+
697+
#[test]
698+
fn manifest_entry_exists_condition_is_false_if_path_does_not_exist() {
699+
let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
700+
"application.trigger.redis".to_owned(),
701+
));
702+
assert!(!condition.is_true(&TemplateVariantInfo::AddComponent {
703+
manifest_path: PathBuf::from("this/file/does/not.exist")
704+
}));
705+
}
706+
707+
#[test]
708+
fn selected_variant_respects_target() {
709+
let add_component_vt = TemplateVariant {
710+
conditions: vec![Conditional {
711+
condition: Condition::Always(true),
712+
skip_files: vec!["test2".to_owned()],
713+
skip_parameters: vec!["p1".to_owned()],
714+
skip_snippets: vec!["s1".to_owned()],
715+
}],
716+
skip_files: vec!["test1".to_owned()],
717+
snippets: [
718+
("s1".to_owned(), "s1val".to_owned()),
719+
("s2".to_owned(), "s2val".to_owned()),
720+
]
721+
.into_iter()
722+
.collect(),
723+
..Default::default()
724+
};
725+
let variants = [
726+
(
727+
TemplateVariantKind::NewApplication,
728+
TemplateVariant::default(),
729+
),
730+
(TemplateVariantKind::AddComponent, add_component_vt),
731+
]
732+
.into_iter()
733+
.collect();
734+
let template = Template {
735+
id: "test".to_owned(),
736+
tags: HashSet::new(),
737+
description: None,
738+
installed_from: InstalledFrom::Unknown,
739+
trigger: TemplateTriggerCompatibility::Any,
740+
variants,
741+
parameters: vec![],
742+
extra_outputs: vec![],
743+
snippets_dir: None,
744+
content_dir: None,
745+
};
746+
747+
let variant_info = TemplateVariantInfo::NewApplication;
748+
let variant = template.variant(&variant_info).unwrap();
749+
assert!(variant.skip_files.is_empty());
750+
assert!(variant.skip_parameters.is_empty());
751+
assert!(variant.snippets.is_empty());
752+
753+
let add_variant_info = TemplateVariantInfo::AddComponent {
754+
manifest_path: PathBuf::from("dummy"),
755+
};
756+
let add_variant = template.variant(&add_variant_info).unwrap();
757+
// the conditional skip_files and skip_parameters are added to the variant's skip lists
758+
assert_eq!(2, add_variant.skip_files.len());
759+
assert!(add_variant.skip_files.contains(&"test1".to_owned()));
760+
assert!(add_variant.skip_files.contains(&"test2".to_owned()));
761+
assert_eq!(1, add_variant.skip_parameters.len());
762+
assert!(add_variant.skip_parameters.contains(&"p1".to_owned()));
763+
// the conditional skip_snippets are *removed from* the variant's snippets list
764+
assert_eq!(1, add_variant.snippets.len());
765+
assert!(!add_variant.snippets.contains_key("s1"));
766+
assert!(add_variant.snippets.contains_key("s2"));
767+
}
656768
}

crates/templates/src/toml.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
pub(crate) fn get_at<S: AsRef<str>>(value: toml::Value, path: &[S]) -> Option<toml::Value> {
2+
match path.split_first() {
3+
None => Some(value), // we are at the end of the path and we have a thing
4+
Some((first, rest)) => {
5+
match value.as_table() {
6+
None => None, // we need to key into it and we can't
7+
Some(t) => {
8+
match t.get(first.as_ref()) {
9+
None => None, // we tried to key into it and no match
10+
Some(v) => get_at(v.clone(), rest), // we pathed into it! keep pathing
11+
}
12+
}
13+
}
14+
}
15+
}
16+
}
17+
18+
#[cfg(test)]
19+
mod test {
20+
use super::*;
21+
22+
#[test]
23+
fn if_path_does_not_exist_then_get_at_is_none() {
24+
let document = toml::toml! {
25+
name = "test"
26+
27+
[application.redis.trigger]
28+
address = "test-address"
29+
30+
[[trigger.redis]]
31+
channel = "messages"
32+
};
33+
34+
assert!(get_at(document.clone(), &["name", "snort"]).is_none());
35+
assert!(get_at(document.clone(), &["snort", "fie"]).is_none());
36+
assert!(get_at(document.clone(), &["application", "snort"]).is_none());
37+
assert!(get_at(document.clone(), &["application", "redis", "snort"]).is_none());
38+
assert!(get_at(document.clone(), &["trigger", "redis", "snort"]).is_none());
39+
40+
// We have not yet needed to define a behaviour for seeking into table arrays, but
41+
// presumably it will need some sort of disambiguation for array element.
42+
// For now, we assume that eithout disambiguation it will report no result.
43+
assert!(get_at(document.clone(), &["trigger", "redis", "channel"]).is_none());
44+
}
45+
46+
#[test]
47+
fn if_path_does_exist_then_get_at_finds_it() {
48+
let document = toml::toml! {
49+
name = "test"
50+
51+
[application.redis.trigger]
52+
address = "test-address"
53+
54+
[[trigger.redis]]
55+
channel = "messages"
56+
};
57+
58+
assert!(get_at(document.clone(), &["name"])
59+
.expect("should find name")
60+
.is_str());
61+
assert!(get_at(document.clone(), &["application", "redis"])
62+
.expect("should find application.redis")
63+
.is_table());
64+
assert!(
65+
get_at(document.clone(), &["application", "redis", "trigger"])
66+
.expect("should find application.redis.trigger")
67+
.is_table()
68+
);
69+
assert!(get_at(document.clone(), &["trigger", "redis"])
70+
.expect("should find trigger.redis.channel")
71+
.is_array());
72+
}
73+
}

0 commit comments

Comments
 (0)