action_core/
lib.rs

1// #![allow(warnings)]
2#![allow(warnings)]
3
4use std::collections::HashMap;
5use std::path::Path;
6
7#[cfg(feature = "derive")]
8pub use action_derive::Action;
9
10#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
11pub struct Input<'a> {
12    pub description: Option<&'a str>,
13    pub deprecation_message: Option<&'a str>,
14    pub default: Option<&'a str>,
15    pub required: Option<bool>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
19pub enum LogLevel {
20    Debug,
21    Error,
22    Warning,
23    Notice,
24}
25
26impl std::fmt::Display for LogLevel {
27    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
28        match self {
29            LogLevel::Debug => write!(f, "debug"),
30            LogLevel::Error => write!(f, "error"),
31            LogLevel::Warning => write!(f, "warning"),
32            LogLevel::Notice => write!(f, "notice"),
33        }
34    }
35}
36
37pub fn input_env_var(name: impl Into<String>) -> String {
38    let mut var: String = name.into();
39    if !var.starts_with("INPUT_") {
40        var = format!("INPUT_{var}");
41    }
42    var = var.replace(' ', "_").to_uppercase();
43    var
44}
45
46pub mod env {
47    use std::collections::HashMap;
48
49    #[derive(Debug, Default)]
50    pub struct Env(pub HashMap<String, String>);
51
52    impl FromIterator<(String, String)> for Env {
53        fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
54            Self::new(HashMap::from_iter(iter))
55        }
56    }
57
58    impl Env {
59        #[must_use]
60        pub fn new(values: HashMap<String, String>) -> Self {
61            let inner = values
62                .into_iter()
63                .map(|(k, v)| (super::input_env_var(k), v))
64                .collect();
65            Self(inner)
66        }
67
68        /// Parses environment from reader.
69        ///
70        /// # Errors
71        /// If the input cannot be parsed as a `HashMap<String, String>`.
72        #[cfg(feature = "serde")]
73        pub fn from_reader(reader: impl std::io::Read) -> Result<Self, serde_yaml::Error> {
74            Ok(Self::new(serde_yaml::from_reader(reader)?))
75        }
76    }
77
78    #[cfg(feature = "serde")]
79    impl std::str::FromStr for Env {
80        type Err = serde_yaml::Error;
81
82        fn from_str(env: &str) -> Result<Self, Self::Err> {
83            Ok(Self::new(serde_yaml::from_str(env)?))
84        }
85    }
86
87    impl std::borrow::Borrow<HashMap<String, String>> for Env {
88        fn borrow(&self) -> &HashMap<String, String> {
89            &self.0
90        }
91    }
92
93    impl std::ops::Deref for Env {
94        type Target = HashMap<String, String>;
95
96        fn deref(&self) -> &Self::Target {
97            &self.0
98        }
99    }
100
101    impl std::ops::DerefMut for Env {
102        fn deref_mut(&mut self) -> &mut Self::Target {
103            &mut self.0
104        }
105    }
106
107    pub trait Read {
108        /// Get value from environment.
109        ///
110        /// # Errors
111        /// When the environment variable is not present.
112        fn get(&self, key: &str) -> Result<String, std::env::VarError>;
113    }
114
115    pub trait Write {
116        /// Set value for environment.
117        fn set(&mut self, key: String, value: String);
118    }
119
120    impl<T> Read for T
121    where
122        T: std::borrow::Borrow<HashMap<String, String>>,
123    {
124        fn get(&self, key: &str) -> Result<String, std::env::VarError> {
125            self.borrow()
126                .get(key)
127                .ok_or(std::env::VarError::NotPresent)
128                .cloned()
129        }
130    }
131
132    impl<T> Write for T
133    where
134        T: std::borrow::BorrowMut<HashMap<String, String>>,
135    {
136        fn set(&mut self, key: String, value: String) {
137            self.borrow_mut().insert(key, value);
138        }
139    }
140
141    pub struct Std;
142
143    pub static ENV: Std = Std {};
144
145    impl Read for Std {
146        fn get(&self, key: &str) -> Result<String, std::env::VarError> {
147            std::env::var(key)
148        }
149    }
150
151    impl Write for Std {
152        fn set(&mut self, key: String, value: String) {
153            std::env::set_var(key, value);
154        }
155    }
156
157    pub trait Parse {
158        type Error: std::error::Error;
159
160        /// Parses environment from a string.
161        ///
162        /// # Errors
163        /// If the input cannot be parsed as a `HashMap<String, String>`.
164        fn from_str(config: &str) -> Result<HashMap<String, String>, Self::Error>;
165
166        /// Parses environment from a reader.
167        ///
168        /// # Errors
169        /// If the input cannot be parsed as a `HashMap<String, String>`.
170        fn from_reader(reader: impl std::io::Read) -> Result<HashMap<String, String>, Self::Error>;
171    }
172}
173
174pub mod utils {
175    /// `toPosixPath` converts the given path to the posix form.
176    ///
177    /// On Windows, \\ will be replaced with /.
178    pub fn to_posix_path(path: impl AsRef<str>) -> String {
179        path.as_ref().replace('\\', "/")
180    }
181
182    /// `toWin32Path` converts the given path to the win32 form.
183    ///
184    /// On Linux, / will be replaced with \\.
185    pub fn to_win32_path(path: impl AsRef<str>) -> String {
186        path.as_ref().replace('/', "\\")
187    }
188
189    /// `toPlatformPath` converts the given path to a platform-specific path.
190    ///
191    /// It does this by replacing instances of / and \ with
192    /// the platform-specific path separator.
193    pub fn to_platform_path(path: impl AsRef<str>) -> String {
194        path.as_ref()
195            .replace(['/', '\\'], std::path::MAIN_SEPARATOR_STR)
196    }
197
198    pub fn escape_data(data: impl AsRef<str>) -> String {
199        data.as_ref()
200            .replace('%', "%25")
201            .replace('\r', "%0D")
202            .replace('\n', "%0A")
203    }
204
205    pub fn escape_property(prop: impl AsRef<str>) -> String {
206        prop.as_ref()
207            .replace('%', "%25")
208            .replace('\r', "%0D")
209            .replace('\n', "%0A")
210            .replace(':', "%3A")
211            .replace(',', "%2C")
212    }
213}
214
215pub mod summary {
216    pub const ENV_VAR: &str = "GITHUB_STEP_SUMMARY";
217    pub const DOCS_URL: &str = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
218
219    #[derive(Debug, PartialEq, Eq, Hash, Clone)]
220    pub struct TableCell {
221        /// Cell content
222        pub data: String,
223        /// Render cell as header
224        pub header: bool,
225        /// Number of columns the cell extends
226        pub colspan: usize,
227        /// Number of rows the cell extends
228        pub rowspan: usize,
229    }
230
231    impl TableCell {
232        #[must_use]
233        pub fn new(data: String) -> Self {
234            Self {
235                data,
236                ..Self::default()
237            }
238        }
239
240        #[must_use]
241        pub fn header(data: String) -> Self {
242            Self {
243                data,
244                header: true,
245                ..Self::default()
246            }
247        }
248    }
249
250    impl Default for TableCell {
251        fn default() -> Self {
252            Self {
253                data: String::new(),
254                header: false,
255                colspan: 1,
256                rowspan: 1,
257            }
258        }
259    }
260
261    #[derive(Default, Debug, PartialEq, Eq, Hash, Clone)]
262    pub struct ImageOptions {
263        /// The width of the image in pixels.
264        width: Option<usize>,
265
266        /// The height of the image in pixels.
267        height: Option<usize>,
268    }
269
270    // todo: finish porting the summary stuff
271    // finish the proc macro, and test it!
272    // continue with the cache stuff?
273}
274
275/// Prepare key value message.
276///
277/// # Errors
278/// If the value contains the randomly generated delimiter.
279pub fn prepare_kv_message(key: &str, value: &str) -> Result<String, ValueError> {
280    use uuid::Uuid;
281    let delimiter = format!("ghadelimiter_{}", Uuid::new_v4());
282
283    // These should realistically never happen,
284    // but just in case someone finds a way to exploit
285    // uuid generation let's not allow keys or values that
286    // contain the delimiter.
287    if key.contains(&delimiter) {
288        return Err(ValueError::ContainsDelimiter { delimiter });
289    }
290
291    if value.contains(&delimiter) {
292        return Err(ValueError::ContainsDelimiter { delimiter });
293    }
294    Ok(format!("{key}<<{delimiter}\n{value}\n{delimiter}"))
295}
296
297/// Sets env variable for this action and future actions in the job.
298///
299/// # Errors
300/// If the file command fails.
301pub fn export_var(name: impl AsRef<str>, value: impl Into<String>) -> Result<(), CommandError> {
302    let value = value.into();
303    std::env::set_var(name.as_ref(), &value);
304
305    if std::env::var("GITHUB_ENV").and_then(not_empty).is_ok() {
306        let message = prepare_kv_message(name.as_ref(), &value)?;
307        issue_file_command("ENV", message)?;
308        return Ok(());
309    }
310
311    issue(
312        &CommandBuilder::new("set-env", value)
313            .property("name", name.as_ref())
314            .build(),
315    );
316    Ok(())
317}
318
319/// Registers a secret which will get masked from logs.
320pub fn set_secret(secret: impl Into<String>) {
321    issue(&CommandBuilder::new("add-mask", secret).build());
322}
323
324/// Prepends a path to the `PATH` environment variable.
325///
326/// # Errors
327/// If the paths can not be joined.
328fn prepend_to_path(path: impl AsRef<Path>) -> Result<(), std::env::JoinPathsError> {
329    if let Some(old_path) = std::env::var_os("PATH") {
330        let paths = [path.as_ref().to_path_buf()]
331            .into_iter()
332            .chain(std::env::split_paths(&old_path));
333        let new_path = std::env::join_paths(paths)?;
334        std::env::set_var("PATH", new_path);
335    }
336    Ok(())
337}
338
339#[derive(thiserror::Error, Debug)]
340pub enum AddPathError {
341    #[error(transparent)]
342    File(#[from] FileCommandError),
343
344    #[error(transparent)]
345    Join(#[from] std::env::JoinPathsError),
346}
347
348/// Prepends a path to the `PATH` environment variable.
349///
350/// Persisted for this action and future actions.
351///
352/// # Errors
353/// If the file command
354pub fn add_path(path: impl AsRef<Path>) -> Result<(), AddPathError> {
355    let path_string = path.as_ref().to_string_lossy();
356    prepend_to_path(path.as_ref())?;
357
358    if std::env::var("GITHUB_PATH").and_then(not_empty).is_ok() {
359        issue_file_command("PATH", &path_string)?;
360    } else {
361        issue(&CommandBuilder::new("add-path", path_string).build());
362    }
363    Ok(())
364}
365
366pub trait Parse {
367    type Input;
368
369    #[must_use]
370    fn parse() -> HashMap<Self::Input, Option<String>> {
371        Self::parse_from(&env::Std)
372    }
373
374    #[must_use]
375    fn parse_from<E: env::Read>(env: &E) -> HashMap<Self::Input, Option<String>>;
376}
377
378pub trait ParseInput: Sized {
379    type Error: std::error::Error;
380
381    /// Parse input string to type T.
382    ///
383    /// # Errors
384    /// When the string value cannot be parsed as `Self`.
385    fn parse(value: String) -> Result<Self, Self::Error>;
386}
387
388#[derive(thiserror::Error, Debug, PartialEq, Eq, Hash, Clone)]
389pub enum ParseError {
390    #[error("invalid boolean value \"{0}\"")]
391    Bool(String),
392}
393
394impl ParseInput for String {
395    type Error = std::convert::Infallible;
396    fn parse(value: String) -> Result<Self, Self::Error> {
397        Ok(value)
398    }
399}
400
401impl ParseInput for bool {
402    type Error = ParseError;
403    fn parse(value: String) -> Result<Self, Self::Error> {
404        match value.to_ascii_lowercase().as_str() {
405            "yes" | "true" | "t" => Ok(true),
406            "no" | "false" | "f" => Ok(false),
407            _ => Err(ParseError::Bool(value)),
408        }
409    }
410}
411
412/// Gets the value of an input.
413///
414/// Attempts to parse as T if a value is present, other returns `Ok(None)`.
415///
416/// # Errors
417/// If the variable cannot be parsed.
418pub fn get_input<T>(name: impl AsRef<str>) -> Result<Option<T>, <T as ParseInput>::Error>
419where
420    T: ParseInput,
421{
422    match get_raw_input(&env::ENV, name) {
423        Ok(input) => Some(T::parse(input)).transpose(),
424        Err(_) => Ok(None),
425    }
426}
427
428/// Filters empty values.
429///
430/// # Errors
431/// If the value is empty.
432pub fn not_empty(value: String) -> Result<String, std::env::VarError> {
433    if value.is_empty() {
434        Err(std::env::VarError::NotPresent)
435    } else {
436        Ok(value)
437    }
438}
439
440/// Gets the raw value of an input.
441///
442/// # Errors
443/// If the environment variable is not present.
444pub fn get_raw_input(
445    env: &impl env::Read,
446    name: impl AsRef<str>,
447) -> Result<String, std::env::VarError> {
448    env.get(&input_env_var(name.as_ref())).and_then(not_empty)
449}
450
451/// Gets the value of an input from an environment.
452///
453/// Attempts to parse as T if a value is present, other returns `Ok(None)`.
454///
455/// # Errors
456/// If the variable cannot be parsed.
457pub fn get_input_from<T>(
458    env: &impl env::Read,
459    name: impl AsRef<str>,
460) -> Result<Option<T>, <T as ParseInput>::Error>
461where
462    T: ParseInput,
463{
464    match get_raw_input(env, name) {
465        Ok(input) => Some(T::parse(input)).transpose(),
466        Err(_) => Ok(None),
467    }
468}
469
470/// Gets the values of an multiline input.
471///
472/// # Errors
473/// If the environment variable is not present.
474pub fn get_multiline_input(name: impl AsRef<str>) -> Result<Vec<String>, std::env::VarError> {
475    let value = get_raw_input(&env::ENV, name)?;
476    Ok(value.lines().map(ToOwned::to_owned).collect())
477}
478
479/// Enables or disables the echoing of commands into stdout for the rest of the step.
480///
481/// Echoing is disabled by default if `ACTIONS_STEP_DEBUG` is not set.
482pub fn set_command_echo(enabled: bool) {
483    issue(&CommandBuilder::new("echo", if enabled { "on" } else { "off" }).build());
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
487pub enum ExitCode {
488    /// A code indicating that the action was successful
489    Success = 0,
490    /// A code indicating that the action was a failure
491    Failure = 1,
492}
493
494/// Sets the action status to failed.
495///
496/// When the action exits it will be with an exit code of 1.
497pub fn fail(message: impl std::fmt::Display) {
498    error!("{}", message);
499    std::process::exit(ExitCode::Failure as i32);
500}
501
502/// Gets whether Actions Step Debug is on or not.
503#[must_use]
504pub fn is_debug() -> bool {
505    std::env::var("RUNNER_DEBUG")
506        .map(|v| v.trim() == "1")
507        .unwrap_or(false)
508}
509
510#[derive(Debug)]
511pub struct CommandBuilder {
512    command: String,
513    message: String,
514    props: HashMap<String, String>,
515}
516
517impl CommandBuilder {
518    #[must_use]
519    pub fn new(command: impl Into<String>, message: impl Into<String>) -> Self {
520        Self {
521            command: command.into(),
522            message: message.into(),
523            props: HashMap::new(),
524        }
525    }
526
527    #[must_use]
528    pub fn property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
529        self.props.insert(key.into(), value.into());
530        self
531    }
532
533    #[must_use]
534    pub fn properties(mut self, props: HashMap<String, String>) -> Self {
535        self.props.extend(props.into_iter());
536        self
537    }
538
539    #[must_use]
540    pub fn build(self) -> Command {
541        let Self {
542            command,
543            message,
544            props,
545        } = self;
546        Command {
547            command,
548            message,
549            props,
550        }
551    }
552}
553
554#[derive(Debug, PartialEq, Eq, Clone)]
555pub struct Command {
556    command: String,
557    message: String,
558    props: HashMap<String, String>,
559}
560
561impl Command {
562    #[must_use]
563    pub fn new(command: String, message: String, props: HashMap<String, String>) -> Self {
564        Self {
565            command,
566            message,
567            props,
568        }
569    }
570}
571
572impl std::fmt::Display for Command {
573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574        const CMD_STRING: &str = "::";
575        write!(f, "{}{}", CMD_STRING, self.command)?;
576        if !self.props.is_empty() {
577            write!(f, " ")?;
578        }
579        for (i, (k, v)) in self.props.iter().enumerate() {
580            if i > 0 {
581                write!(f, ",")?;
582            }
583            if v.is_empty() {
584                continue;
585            }
586            write!(f, "{k}={}", utils::escape_property(v))?;
587        }
588        write!(f, "{}{}", CMD_STRING, self.message)
589    }
590}
591
592pub fn issue(cmd: &Command) {
593    println!("{cmd}");
594}
595
596#[derive(thiserror::Error, Debug)]
597pub enum ValueError {
598    #[error("should not contain delimiter `{delimiter}`")]
599    ContainsDelimiter { delimiter: String },
600}
601
602#[derive(thiserror::Error, Debug)]
603pub enum FileCommandError {
604    #[error("missing env variable for file command {cmd}")]
605    Missing {
606        source: std::env::VarError,
607        cmd: String,
608    },
609    #[error(transparent)]
610    Io(#[from] std::io::Error),
611
612    #[error(transparent)]
613    Value(#[from] ValueError),
614}
615
616#[derive(thiserror::Error, Debug)]
617pub enum CommandError {
618    #[error(transparent)]
619    File(#[from] FileCommandError),
620
621    #[error(transparent)]
622    Value(#[from] ValueError),
623}
624
625/// Issue a file command.
626///
627/// # Errors
628/// When no env variable for the file command exists or writing fails.
629pub fn issue_file_command(
630    command: impl AsRef<str>,
631    message: impl AsRef<str>,
632) -> Result<(), FileCommandError> {
633    use std::io::Write;
634    let key = format!("GITHUB_{}", command.as_ref());
635    let file_path = std::env::var(key).map_err(|source| FileCommandError::Missing {
636        source,
637        cmd: command.as_ref().to_string(),
638    })?;
639    let file = std::fs::OpenOptions::new()
640        .append(true)
641        .write(true)
642        .open(file_path)?;
643    let mut file = std::io::BufWriter::new(file);
644    writeln!(file, "{}", message.as_ref())?;
645    Ok(())
646}
647
648// pub fn issue_command(
649//     command: impl AsRef<str>,
650//     message: impl std::fmt::Display,
651//     props: HashMap<String, String>,
652// ) {
653//     let cmd= Command::new(command.as_ref(), message.to_string(), props);
654//     issue();
655// }
656
657#[derive(Default, Debug, Hash, PartialEq, Eq)]
658pub struct AnnotationProperties {
659    pub title: Option<String>,
660    pub file: Option<String>,
661    pub start_line: Option<usize>,
662    pub end_line: Option<usize>,
663    pub start_column: Option<usize>,
664    pub end_column: Option<usize>,
665}
666
667impl<H> From<AnnotationProperties> for HashMap<String, String, H>
668where
669    H: std::hash::BuildHasher + Default,
670{
671    fn from(props: AnnotationProperties) -> Self {
672        [
673            ("title".to_string(), props.title),
674            ("file".to_string(), props.file),
675            (
676                "line".to_string(),
677                props.start_line.map(|line| line.to_string()),
678            ),
679            (
680                "endLine".to_string(),
681                props.end_line.map(|line| line.to_string()),
682            ),
683            (
684                "col".to_string(),
685                props.start_column.map(|col| col.to_string()),
686            ),
687            (
688                "endColumn".to_string(),
689                props.end_column.map(|col| col.to_string()),
690            ),
691        ]
692        .into_iter()
693        .filter_map(|(k, v)| v.map(|v| (k, v)))
694        .collect()
695    }
696}
697
698/// Adds an error issue.
699pub fn issue_level(
700    level: LogLevel,
701    message: impl Into<String>,
702    props: Option<AnnotationProperties>,
703) {
704    let props = props.unwrap_or_default();
705    issue(
706        &CommandBuilder::new(level.to_string(), message)
707            .properties(props.into())
708            .build(),
709    );
710}
711
712// /// Writes debug message to user log.
713// pub fn debug(message: impl std::fmt::Display) {
714//     issue_command("debug", message, HashMap::new())
715// }
716
717/// Adds an error issue.
718// pub fn error(message: impl ToString, props: AnnotationProperties) {
719//     issue_level(LogLevel::Error, message, props);
720// }
721
722#[macro_export]
723macro_rules! debug {
724        ($($arg:tt)*) => {{
725            $crate::issue_level(
726                $crate::LogLevel::Debug,
727                format!($($arg)*),
728                None,
729            );
730        }};
731    }
732
733#[macro_export]
734macro_rules! warning {
735    ($($arg:tt)*) => {{
736        $crate::issue_level(
737            $crate::LogLevel::Warning,
738            format!($($arg)*),
739            None,
740        );
741    }};
742}
743
744#[macro_export]
745macro_rules! error {
746    ($($arg:tt)*) => {{
747        $crate::issue_level(
748            $crate::LogLevel::Error,
749            format!($($arg)*),
750            None,
751        );
752    }};
753}
754
755#[macro_export]
756macro_rules! notice {
757    ($($arg:tt)*) => {{
758        $crate::issue_level(
759            $crate::LogLevel::Notice,
760            format!($($arg)*),
761            None,
762        );
763    }};
764}
765
766#[macro_export]
767macro_rules! info {
768    ($($arg:tt)*) => { println!($($arg)*); };
769}
770
771// /// Adds a warning issue.
772// pub fn issue_warning(message: impl ToString, props: AnnotationProperties) {
773//     issue_level(LogLevel::Warning, message, props);
774// }
775//
776// /// Adds a notice issue
777// pub fn notice(message: impl std::fmt::Display, props: AnnotationProperties) {
778//     issue_level(LogLevel::Notice, message, props);
779// }
780
781/// Begin an output group.
782///
783/// Output until the next `group_end` will be foldable in this group.
784pub fn start_group(name: impl Into<String>) {
785    issue(&CommandBuilder::new("group", name).build());
786}
787
788/// End an output group.
789pub fn end_group() {
790    issue(&CommandBuilder::new("endgroup", "").build());
791}
792
793/// Saves state for current action, the state can only be retrieved by this action's post job execution.
794///
795/// # Errors
796/// If the file command fails.
797pub fn save_state(name: impl AsRef<str>, value: impl Into<String>) -> Result<(), CommandError> {
798    if std::env::var("GITHUB_STATE").and_then(not_empty).is_ok() {
799        let message = prepare_kv_message(name.as_ref(), &value.into())?;
800        issue_file_command("STATE", message)?;
801        return Ok(());
802    }
803
804    issue(
805        &CommandBuilder::new("save-state", value)
806            .property("name", name.as_ref())
807            .build(),
808    );
809    Ok(())
810}
811
812/// Gets the value of an state set by this action's main execution.
813#[must_use]
814pub fn get_state(name: impl AsRef<str>) -> Option<String> {
815    std::env::var(format!("STATE_{}", name.as_ref())).ok()
816}
817
818/// Wrap an asynchronous function call in a group.
819///
820/// Returns the same type as the function itself.
821pub async fn group<T>(name: impl Into<String>, fut: impl std::future::Future<Output = T>) -> T {
822    start_group(name);
823    let res: T = fut.await;
824
825    end_group();
826    res
827}
828
829#[cfg(test)]
830mod tests {
831    use super::env::Env;
832
833    #[test]
834    fn test_env() {
835        let input_name = "SOME_NAME";
836        let env = Env::from_iter([(input_name.to_string(), "SET".to_string())]);
837        dbg!(&env);
838        assert_eq!(env.get("INPUT_SOME_NAME"), Some(&"SET".to_string()));
839    }
840
841    #[test]
842    fn test_get_non_empty_input() {
843        let input_name = "SOME_NAME";
844        let env = Env::from_iter([(input_name.to_string(), "SET".to_string())]);
845        dbg!(&env);
846        assert_eq!(
847            super::get_input_from::<String>(&env, input_name),
848            Ok(Some("SET".to_string()))
849        );
850    }
851
852    #[test]
853    fn test_get_empty_input() {
854        let input_name = "SOME_NAME";
855        let mut env = Env::from_iter([]);
856        assert_eq!(super::get_input_from::<String>(&env, input_name), Ok(None),);
857
858        env.insert(input_name.to_string(), String::new());
859        assert_eq!(super::get_input_from::<String>(&env, input_name), Ok(None),);
860    }
861}