Skip to main content

spin_templates/
template.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::PathBuf,
4};
5
6use anyhow::{Context, anyhow};
7use indexmap::IndexMap;
8use itertools::Itertools;
9use regex::Regex;
10
11use crate::{
12    constraints::StringConstraints,
13    reader::{
14        RawCondition, RawConditional, RawExtraOutput, RawParameter, RawTemplateManifest,
15        RawTemplateManifestV1, RawTemplateVariant,
16    },
17    run::{Run, RunOptions},
18    store::TemplateLayout,
19};
20
21/// A Spin template.
22#[derive(Debug)]
23pub struct Template {
24    id: String,
25    tags: HashSet<String>,
26    description: Option<String>,
27    installed_from: InstalledFrom,
28    variants: HashMap<TemplateVariantKind, TemplateVariant>,
29    parameters: Vec<TemplateParameter>,
30    extra_outputs: Vec<ExtraOutputAction>,
31    snippets_dir: Option<PathBuf>,
32    partials_dir: Option<PathBuf>,
33    content_dir: Option<PathBuf>, // TODO: maybe always need a spin.toml file in there?
34}
35
36#[derive(Debug)]
37enum InstalledFrom {
38    Git { url: String, branch: Option<String> },
39    Directory(String),
40    RemoteTar(String),
41    Unknown,
42}
43
44#[derive(Debug, Eq, PartialEq, Hash)]
45enum TemplateVariantKind {
46    NewApplication,
47    AddComponent,
48}
49
50/// The variant mode in which a template should be run.
51#[derive(Clone, Debug)]
52pub enum TemplateVariantInfo {
53    /// Create a new application from the template.
54    NewApplication,
55    /// Create a new component in an existing application from the template.
56    AddComponent {
57        /// The manifest to which the component will be added.
58        manifest_path: PathBuf,
59    },
60}
61
62impl TemplateVariantInfo {
63    fn kind(&self) -> TemplateVariantKind {
64        match self {
65            Self::NewApplication => TemplateVariantKind::NewApplication,
66            Self::AddComponent { .. } => TemplateVariantKind::AddComponent,
67        }
68    }
69
70    /// A human-readable description of the variant.
71    pub fn description(&self) -> &'static str {
72        match self {
73            Self::NewApplication => "new application",
74            Self::AddComponent { .. } => "add component",
75        }
76    }
77
78    /// The noun that should be used for the variant in a prompt
79    pub fn prompt_noun(&self) -> &'static str {
80        match self {
81            Self::NewApplication => "application",
82            Self::AddComponent { .. } => "component",
83        }
84    }
85
86    /// The noun that should be used for the variant in a prompt,
87    /// qualified with the appropriate a/an article for English
88    pub fn articled_noun(&self) -> &'static str {
89        match self {
90            Self::NewApplication => "an application",
91            Self::AddComponent { .. } => "a component",
92        }
93    }
94}
95
96#[derive(Clone, Debug, Default)]
97pub(crate) struct TemplateVariant {
98    skip_files: Vec<String>,
99    skip_parameters: Vec<String>,
100    snippets: HashMap<String, String>,
101    conditions: Vec<Conditional>,
102}
103
104#[derive(Clone, Debug)]
105pub(crate) struct Conditional {
106    condition: Condition,
107    skip_files: Vec<String>,
108    skip_parameters: Vec<String>,
109    skip_snippets: Vec<String>,
110}
111
112#[derive(Clone, Debug)]
113pub(crate) enum Condition {
114    ManifestEntryExists(Vec<String>),
115    #[cfg(test)]
116    Always(bool),
117}
118
119#[derive(Clone, Debug)]
120pub(crate) enum TemplateParameterDataType {
121    String(StringConstraints),
122}
123
124#[derive(Debug)]
125pub(crate) struct TemplateParameter {
126    id: String,
127    data_type: TemplateParameterDataType, // TODO: possibly abstract to a ValidationCriteria type?
128    prompt: String,
129    default_value: Option<String>,
130}
131
132pub(crate) enum ExtraOutputAction {
133    CreateDirectory(
134        String,
135        std::sync::Arc<liquid::Template>,
136        crate::reader::CreateLocation,
137    ),
138}
139
140impl std::fmt::Debug for ExtraOutputAction {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            Self::CreateDirectory(orig, ..) => {
144                f.debug_tuple("CreateDirectory").field(orig).finish()
145            }
146        }
147    }
148}
149
150impl Template {
151    pub(crate) fn load_from(layout: &TemplateLayout) -> anyhow::Result<Self> {
152        let manifest_path = layout.manifest_path();
153
154        let manifest_text = std::fs::read_to_string(&manifest_path).with_context(|| {
155            format!(
156                "Failed to read template manifest file {}",
157                manifest_path.display()
158            )
159        })?;
160        let raw = crate::reader::parse_manifest_toml(manifest_text).with_context(|| {
161            format!(
162                "Manifest file {} is not a valid manifest",
163                manifest_path.display()
164            )
165        })?;
166
167        validate_manifest(&raw)?;
168
169        let content_dir = if layout.content_dir().exists() {
170            Some(layout.content_dir())
171        } else {
172            None
173        };
174
175        let snippets_dir = if layout.snippets_dir().exists() {
176            Some(layout.snippets_dir())
177        } else {
178            None
179        };
180
181        let partials_dir = if layout.partials_dir().exists() {
182            Some(layout.partials_dir())
183        } else {
184            None
185        };
186
187        let installed_from = read_install_record(layout);
188
189        let template = match raw {
190            RawTemplateManifest::V1(raw) => Self {
191                id: raw.id.clone(),
192                tags: raw.tags.map(Self::normalize_tags).unwrap_or_default(),
193                description: raw.description.clone(),
194                installed_from,
195                variants: Self::parse_template_variants(raw.new_application, raw.add_component),
196                parameters: Self::parse_parameters(&raw.parameters)?,
197                extra_outputs: Self::parse_extra_outputs(&raw.outputs)?,
198                snippets_dir,
199                partials_dir,
200                content_dir,
201            },
202        };
203        Ok(template)
204    }
205
206    /// The ID of the template. This is used to identify the template
207    /// on the Spin command line.
208    pub fn id(&self) -> &str {
209        &self.id
210    }
211
212    /// Returns true if the templates matches the provided set of tags.
213    pub fn matches_all_tags(&self, match_set: &[String]) -> bool {
214        match_set
215            .iter()
216            .all(|tag| self.tags().contains(&tag.to_lowercase()))
217    }
218
219    /// The set of tags associated with the template, provided by the
220    /// template author.
221    pub fn tags(&self) -> &HashSet<String> {
222        &self.tags
223    }
224
225    /// A human-readable description of the template, provided by the
226    /// template author.
227    pub fn description(&self) -> &Option<String> {
228        &self.description
229    }
230
231    /// A human-readable description of the template, provided by the
232    /// template author, or an empty string if no description was
233    /// provided.
234    pub fn description_or_empty(&self) -> &str {
235        match &self.description {
236            Some(s) => s,
237            None => "",
238        }
239    }
240
241    /// The Git repository from which the template was installed, if
242    /// it was installed from Git; otherwise None.
243    pub fn source_repo(&self) -> Option<&str> {
244        // TODO: this is kind of specialised - should we do the discarding of
245        // non-Git sources at the application layer?
246        match &self.installed_from {
247            InstalledFrom::Git { url, .. } => Some(url),
248            _ => None,
249        }
250    }
251
252    pub(crate) fn is_from_source_repo(&self, source_repo: &url::Url) -> bool {
253        self.source_repo()
254            .is_some_and(|r| r == source_repo.as_str())
255    }
256
257    /// Determines if the template was installed from the expected repo and
258    /// branch. This does not prove that the template is up to date, as the
259    /// tag or branch may have moved in the interim.
260    pub fn is_installed_from_git_ref(
261        &self,
262        expected_repo: &str,
263        expected_tag: Option<&str>,
264    ) -> bool {
265        match &self.installed_from {
266            // If either the install record _or_ the env has None for the branch/tag, then
267            // we must assume the worst, because that is the inherently mutable "use the Spin
268            // CLI version."
269            InstalledFrom::Git {
270                url,
271                branch: Some(br),
272            } => url == expected_repo && expected_tag.is_some_and(|t| t == br),
273            _ => false,
274        }
275    }
276
277    /// A human-readable description of where the template was installed
278    /// from.
279    pub fn installed_from_or_empty(&self) -> &str {
280        match &self.installed_from {
281            InstalledFrom::Git { url, .. } => url,
282            InstalledFrom::Directory(path) => path,
283            InstalledFrom::RemoteTar(url) => url,
284            InstalledFrom::Unknown => "",
285        }
286    }
287
288    // TODO: we should resolve this once at the start of Run and then use that forever
289    fn variant(&self, variant_info: &TemplateVariantInfo) -> Option<TemplateVariant> {
290        let kind = variant_info.kind();
291        self.variants
292            .get(&kind)
293            .map(|vt| vt.resolve_conditions(variant_info))
294    }
295
296    pub(crate) fn parameters(
297        &self,
298        variant_kind: &TemplateVariantInfo,
299    ) -> impl Iterator<Item = &TemplateParameter> {
300        let variant = self.variant(variant_kind).unwrap(); // TODO: for now
301        self.parameters
302            .iter()
303            .filter(move |p| !variant.skip_parameter(p))
304    }
305
306    pub(crate) fn parameter(&self, name: impl AsRef<str>) -> Option<&TemplateParameter> {
307        self.parameters.iter().find(|p| p.id == name.as_ref())
308    }
309
310    pub(crate) fn extra_outputs(&self) -> &[ExtraOutputAction] {
311        &self.extra_outputs
312    }
313
314    pub(crate) fn content_dir(&self) -> &Option<PathBuf> {
315        &self.content_dir
316    }
317
318    pub(crate) fn snippets_dir(&self) -> &Option<PathBuf> {
319        &self.snippets_dir
320    }
321
322    pub(crate) fn partials_dir(&self) -> &Option<PathBuf> {
323        &self.partials_dir
324    }
325
326    /// Checks if the template supports the specified variant mode.
327    pub fn supports_variant(&self, variant: &TemplateVariantInfo) -> bool {
328        self.variants.contains_key(&variant.kind())
329    }
330
331    pub(crate) fn snippets(&self, variant_kind: &TemplateVariantInfo) -> HashMap<String, String> {
332        let variant = self.variant(variant_kind).unwrap(); // TODO: for now
333        variant.snippets
334    }
335
336    /// Creates a runner for the template, governed by the given options. Call
337    /// the relevant associated function of the `Run` to execute the template
338    /// as appropriate to your application (e.g. `interactive()` to prompt the user
339    /// for values and interact with the user at the console).
340    pub fn run(self, options: RunOptions) -> Run {
341        Run::new(self, options)
342    }
343
344    fn normalize_tags(tags: HashSet<String>) -> HashSet<String> {
345        tags.into_iter().map(|tag| tag.to_lowercase()).collect()
346    }
347
348    fn parse_template_variants(
349        new_application: Option<RawTemplateVariant>,
350        add_component: Option<RawTemplateVariant>,
351    ) -> HashMap<TemplateVariantKind, TemplateVariant> {
352        let mut variants = HashMap::default();
353        if let Some(vt) = Self::get_variant(new_application, true) {
354            variants.insert(TemplateVariantKind::NewApplication, vt);
355        }
356        if let Some(vt) = Self::get_variant(add_component, false) {
357            variants.insert(TemplateVariantKind::AddComponent, vt);
358        }
359        variants
360    }
361
362    fn get_variant(
363        raw: Option<RawTemplateVariant>,
364        default_supported: bool,
365    ) -> Option<TemplateVariant> {
366        match raw {
367            None => {
368                if default_supported {
369                    Some(Default::default())
370                } else {
371                    None
372                }
373            }
374            Some(rv) => {
375                if rv.supported.unwrap_or(true) {
376                    Some(Self::parse_template_variant(rv))
377                } else {
378                    None
379                }
380            }
381        }
382    }
383
384    fn parse_template_variant(raw: RawTemplateVariant) -> TemplateVariant {
385        TemplateVariant {
386            skip_files: raw.skip_files.unwrap_or_default(),
387            skip_parameters: raw.skip_parameters.unwrap_or_default(),
388            snippets: raw.snippets.unwrap_or_default(),
389            conditions: raw
390                .conditions
391                .unwrap_or_default()
392                .into_values()
393                .map(Self::parse_conditional)
394                .collect(),
395        }
396    }
397
398    fn parse_conditional(conditional: RawConditional) -> Conditional {
399        Conditional {
400            condition: Self::parse_condition(conditional.condition),
401            skip_files: conditional.skip_files.unwrap_or_default(),
402            skip_parameters: conditional.skip_parameters.unwrap_or_default(),
403            skip_snippets: conditional.skip_snippets.unwrap_or_default(),
404        }
405    }
406
407    fn parse_condition(condition: RawCondition) -> Condition {
408        match condition {
409            RawCondition::ManifestEntryExists(path) => {
410                Condition::ManifestEntryExists(path.split('.').map(|s| s.to_string()).collect_vec())
411            }
412        }
413    }
414
415    fn parse_parameters(
416        raw: &Option<IndexMap<String, RawParameter>>,
417    ) -> anyhow::Result<Vec<TemplateParameter>> {
418        match raw {
419            None => Ok(vec![]),
420            Some(parameters) => parameters
421                .iter()
422                .map(|(k, v)| TemplateParameter::from_raw(k, v))
423                .collect(),
424        }
425    }
426
427    fn parse_extra_outputs(
428        raw: &Option<IndexMap<String, RawExtraOutput>>,
429    ) -> anyhow::Result<Vec<ExtraOutputAction>> {
430        match raw {
431            None => Ok(vec![]),
432            Some(parameters) => parameters
433                .iter()
434                .map(|(k, v)| ExtraOutputAction::from_raw(k, v))
435                .collect(),
436        }
437    }
438
439    pub(crate) fn included_files(
440        &self,
441        base: &std::path::Path,
442        all_files: Vec<PathBuf>,
443        variant_kind: &TemplateVariantInfo,
444    ) -> Vec<PathBuf> {
445        let variant = self.variant(variant_kind).unwrap(); // TODO: for now
446        all_files
447            .into_iter()
448            .filter(|path| !variant.skip_file(base, path))
449            .collect()
450    }
451
452    pub(crate) fn check_compatible_manifest_format(
453        &self,
454        manifest_format: u32,
455    ) -> anyhow::Result<()> {
456        let Some(content_dir) = &self.content_dir else {
457            return Ok(());
458        };
459        let manifest_tpl = content_dir.join("spin.toml");
460        if !manifest_tpl.is_file() {
461            return Ok(());
462        }
463
464        // We can't load the manifest template because it's not valid TOML until
465        // substituted, so GO BIG or at least GO CRUDE.
466        let Ok(manifest_tpl_str) = std::fs::read_to_string(&manifest_tpl) else {
467            return Ok(());
468        };
469        let is_v1_tpl = manifest_tpl_str.contains("spin_manifest_version = \"1\"");
470        let is_v2_tpl = manifest_tpl_str.contains("spin_manifest_version = 2");
471
472        // If we have not positively identified a format, err on the side of forgiveness
473        let positively_identified = is_v1_tpl ^ is_v2_tpl; // exactly one should be true
474        if !positively_identified {
475            return Ok(());
476        }
477
478        let compatible = (is_v1_tpl && manifest_format == 1) || (is_v2_tpl && manifest_format == 2);
479
480        if compatible {
481            Ok(())
482        } else {
483            Err(anyhow!(
484                "This template is for a different version of the Spin manifest"
485            ))
486        }
487    }
488}
489
490impl TemplateParameter {
491    fn from_raw(id: &str, raw: &RawParameter) -> anyhow::Result<Self> {
492        let data_type = TemplateParameterDataType::parse(raw)?;
493
494        Ok(Self {
495            id: id.to_owned(),
496            data_type,
497            prompt: raw.prompt.clone(),
498            default_value: raw.default_value.clone(),
499        })
500    }
501
502    pub fn id(&self) -> &str {
503        &self.id
504    }
505
506    pub fn data_type(&self) -> &TemplateParameterDataType {
507        &self.data_type
508    }
509
510    pub fn prompt(&self) -> &str {
511        &self.prompt
512    }
513
514    pub fn default_value(&self) -> &Option<String> {
515        &self.default_value
516    }
517
518    pub fn validate_value(&self, value: impl AsRef<str>) -> anyhow::Result<String> {
519        self.data_type.validate_value(value.as_ref().to_owned())
520    }
521}
522
523impl TemplateParameterDataType {
524    fn parse(raw: &RawParameter) -> anyhow::Result<Self> {
525        match &raw.data_type[..] {
526            "string" => Ok(Self::String(parse_string_constraints(raw)?)),
527            _ => Err(anyhow!("Unrecognised data type '{}'", raw.data_type)),
528        }
529    }
530
531    fn validate_value(&self, value: String) -> anyhow::Result<String> {
532        match self {
533            TemplateParameterDataType::String(constraints) => constraints.validate(value),
534        }
535    }
536}
537
538impl ExtraOutputAction {
539    fn from_raw(id: &str, raw: &RawExtraOutput) -> anyhow::Result<Self> {
540        Ok(match raw {
541            RawExtraOutput::CreateDir(create) => {
542                let path_template =
543                    liquid::Parser::new().parse(&create.path).with_context(|| {
544                        format!("Template error: output {id} is not a valid template")
545                    })?;
546                Self::CreateDirectory(
547                    create.path.clone(),
548                    std::sync::Arc::new(path_template),
549                    create.at.unwrap_or_default(),
550                )
551            }
552        })
553    }
554}
555
556impl TemplateVariant {
557    pub(crate) fn skip_file(&self, base: &std::path::Path, path: &std::path::Path) -> bool {
558        self.skip_files
559            .iter()
560            .map(|s| base.join(s))
561            .any(|f| path == f)
562    }
563
564    pub(crate) fn skip_parameter(&self, parameter: &TemplateParameter) -> bool {
565        self.skip_parameters.iter().any(|p| &parameter.id == p)
566    }
567
568    fn resolve_conditions(&self, variant_info: &TemplateVariantInfo) -> Self {
569        let mut resolved = self.clone();
570        for condition in &self.conditions {
571            if condition.condition.is_true(variant_info) {
572                resolved
573                    .skip_files
574                    .append(&mut condition.skip_files.clone());
575                resolved
576                    .skip_parameters
577                    .append(&mut condition.skip_parameters.clone());
578                resolved
579                    .snippets
580                    .retain(|id, _| !condition.skip_snippets.contains(id));
581            }
582        }
583        resolved
584    }
585}
586
587impl Condition {
588    fn is_true(&self, variant_info: &TemplateVariantInfo) -> bool {
589        match self {
590            Self::ManifestEntryExists(path) => match variant_info {
591                TemplateVariantInfo::NewApplication => false,
592                TemplateVariantInfo::AddComponent { manifest_path } => {
593                    let Ok(toml_text) = std::fs::read_to_string(manifest_path) else {
594                        return false;
595                    };
596                    let Ok(table) = toml::from_str::<toml::Value>(&toml_text) else {
597                        return false;
598                    };
599                    crate::toml::get_at(table, path).is_some()
600                }
601            },
602            #[cfg(test)]
603            Self::Always(b) => *b,
604        }
605    }
606}
607
608fn parse_string_constraints(raw: &RawParameter) -> anyhow::Result<StringConstraints> {
609    let regex = raw.pattern.as_ref().map(|re| Regex::new(re)).transpose()?;
610
611    Ok(StringConstraints {
612        regex,
613        allowed_values: raw.allowed_values.clone(),
614    })
615}
616
617fn read_install_record(layout: &TemplateLayout) -> InstalledFrom {
618    use crate::reader::{RawInstalledFrom, parse_installed_from};
619
620    let installed_from_text = std::fs::read_to_string(layout.installation_record_file()).ok();
621    match installed_from_text.and_then(parse_installed_from) {
622        Some(RawInstalledFrom::Git { git, branch }) => InstalledFrom::Git { url: git, branch },
623        Some(RawInstalledFrom::File { dir }) => InstalledFrom::Directory(dir),
624        Some(RawInstalledFrom::RemoteTar { url }) => InstalledFrom::RemoteTar(url),
625        None => InstalledFrom::Unknown,
626    }
627}
628
629fn validate_manifest(raw: &RawTemplateManifest) -> anyhow::Result<()> {
630    match raw {
631        RawTemplateManifest::V1(raw) => validate_v1_manifest(raw),
632    }
633}
634
635fn validate_v1_manifest(raw: &RawTemplateManifestV1) -> anyhow::Result<()> {
636    if raw.custom_filters.is_some() {
637        anyhow::bail!(
638            "Custom filters are not supported in this version of Spin. Please update your template."
639        );
640    }
641    Ok(())
642}
643
644#[cfg(test)]
645mod test {
646    use super::*;
647
648    struct TempFile {
649        _temp_dir: tempfile::TempDir,
650        path: PathBuf,
651    }
652
653    impl TempFile {
654        fn path(&self) -> PathBuf {
655            self.path.clone()
656        }
657    }
658
659    fn make_temp_manifest(content: &str) -> TempFile {
660        let temp_dir = tempfile::tempdir().unwrap();
661        let temp_file = temp_dir.path().join("spin.toml");
662        std::fs::write(&temp_file, content).unwrap();
663        TempFile {
664            _temp_dir: temp_dir,
665            path: temp_file,
666        }
667    }
668
669    #[test]
670    fn manifest_entry_exists_condition_is_false_for_new_app() {
671        let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
672            "application.trigger.redis".to_owned(),
673        ));
674        assert!(!condition.is_true(&TemplateVariantInfo::NewApplication));
675    }
676
677    #[test]
678    fn manifest_entry_exists_condition_is_false_if_not_present_in_existing_manifest() {
679        let temp_file =
680            make_temp_manifest("name = \"hello\"\n[application.trigger.http]\nbase = \"/\"");
681        let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
682            "application.trigger.redis".to_owned(),
683        ));
684        assert!(!condition.is_true(&TemplateVariantInfo::AddComponent {
685            manifest_path: temp_file.path()
686        }));
687    }
688
689    #[test]
690    fn manifest_entry_exists_condition_is_true_if_present_in_existing_manifest() {
691        let temp_file = make_temp_manifest(
692            "name = \"hello\"\n[application.trigger.redis]\nchannel = \"HELLO\"",
693        );
694        let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
695            "application.trigger.redis".to_owned(),
696        ));
697        assert!(condition.is_true(&TemplateVariantInfo::AddComponent {
698            manifest_path: temp_file.path()
699        }));
700    }
701
702    #[test]
703    fn manifest_entry_exists_condition_is_false_if_path_does_not_exist() {
704        let condition = Template::parse_condition(RawCondition::ManifestEntryExists(
705            "application.trigger.redis".to_owned(),
706        ));
707        assert!(!condition.is_true(&TemplateVariantInfo::AddComponent {
708            manifest_path: PathBuf::from("this/file/does/not.exist")
709        }));
710    }
711
712    #[test]
713    fn selected_variant_respects_target() {
714        let add_component_vt = TemplateVariant {
715            conditions: vec![Conditional {
716                condition: Condition::Always(true),
717                skip_files: vec!["test2".to_owned()],
718                skip_parameters: vec!["p1".to_owned()],
719                skip_snippets: vec!["s1".to_owned()],
720            }],
721            skip_files: vec!["test1".to_owned()],
722            snippets: [
723                ("s1".to_owned(), "s1val".to_owned()),
724                ("s2".to_owned(), "s2val".to_owned()),
725            ]
726            .into_iter()
727            .collect(),
728            ..Default::default()
729        };
730        let variants = [
731            (
732                TemplateVariantKind::NewApplication,
733                TemplateVariant::default(),
734            ),
735            (TemplateVariantKind::AddComponent, add_component_vt),
736        ]
737        .into_iter()
738        .collect();
739        let template = Template {
740            id: "test".to_owned(),
741            tags: HashSet::new(),
742            description: None,
743            installed_from: InstalledFrom::Unknown,
744            variants,
745            parameters: vec![],
746            extra_outputs: vec![],
747            snippets_dir: None,
748            partials_dir: None,
749            content_dir: None,
750        };
751
752        let variant_info = TemplateVariantInfo::NewApplication;
753        let variant = template.variant(&variant_info).unwrap();
754        assert!(variant.skip_files.is_empty());
755        assert!(variant.skip_parameters.is_empty());
756        assert!(variant.snippets.is_empty());
757
758        let add_variant_info = TemplateVariantInfo::AddComponent {
759            manifest_path: PathBuf::from("dummy"),
760        };
761        let add_variant = template.variant(&add_variant_info).unwrap();
762        // the conditional skip_files and skip_parameters are added to the variant's skip lists
763        assert_eq!(2, add_variant.skip_files.len());
764        assert!(add_variant.skip_files.contains(&"test1".to_owned()));
765        assert!(add_variant.skip_files.contains(&"test2".to_owned()));
766        assert_eq!(1, add_variant.skip_parameters.len());
767        assert!(add_variant.skip_parameters.contains(&"p1".to_owned()));
768        // the conditional skip_snippets are *removed from* the variant's snippets list
769        assert_eq!(1, add_variant.snippets.len());
770        assert!(!add_variant.snippets.contains_key("s1"));
771        assert!(add_variant.snippets.contains_key("s2"));
772    }
773}