Skip to content
Merged
48 changes: 48 additions & 0 deletions dsc/tests/dsc_expressions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,32 @@ resources:
@{ expression = "[equals('a', 'a')]"; expected = $true }
@{ expression = "[equals('a', 'b')]"; expected = $false }
@{ expression = "[not(equals('a', 'b'))]"; expected = $true }
@{ expression = "[greater(5, 3)]"; expected = $true }
@{ expression = "[greater(3, 5)]"; expected = $false }
@{ expression = "[greater(5, 5)]"; expected = $false }
@{ expression = "[greaterOrEquals(5, 3)]"; expected = $true }
@{ expression = "[greaterOrEquals(3, 5)]"; expected = $false }
@{ expression = "[greaterOrEquals(5, 5)]"; expected = $true }
@{ expression = "[less(3, 5)]"; expected = $true }
@{ expression = "[less(5, 3)]"; expected = $false }
@{ expression = "[less(5, 5)]"; expected = $false }
@{ expression = "[lessOrEquals(3, 5)]"; expected = $true }
@{ expression = "[lessOrEquals(5, 3)]"; expected = $false }
@{ expression = "[lessOrEquals(5, 5)]"; expected = $true }
@{ expression = "[greater('b', 'a')]"; expected = $true }
@{ expression = "[greater('a', 'b')]"; expected = $false }
@{ expression = "[greater('A', 'a')]"; expected = $false }
@{ expression = "[greaterOrEquals('b', 'a')]"; expected = $true }
@{ expression = "[greaterOrEquals('a', 'b')]"; expected = $false }
@{ expression = "[greaterOrEquals('a', 'a')]"; expected = $true }
@{ expression = "[greaterOrEquals('Aa', 'aa')]"; expected = $false }
@{ expression = "[less('a', 'b')]"; expected = $true }
@{ expression = "[less('b', 'a')]"; expected = $false }
@{ expression = "[less('A', 'a')]"; expected = $true }
@{ expression = "[lessOrEquals('a', 'b')]"; expected = $true }
@{ expression = "[lessOrEquals('b', 'a')]"; expected = $false }
@{ expression = "[lessOrEquals('a', 'a')]"; expected = $true }
@{ expression = "[lessOrEquals('aa', 'Aa')]"; expected = $false }
@{ expression = "[and(true, true)]"; expected = $true }
@{ expression = "[and(true, false)]"; expected = $false }
@{ expression = "[or(false, true)]"; expected = $true }
Expand All @@ -138,4 +164,26 @@ resources:
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
$out.results[0].result.actualState.output | Should -Be $expected -Because ($out | ConvertTo-Json -Depth 10| Out-String)
}

It 'Comparison functions handle type mismatches: <expression>' -TestCases @(
@{ expression = "[greater('a', 1)]" }
@{ expression = "[greaterOrEquals('5', 3)]" }
@{ expression = "[less(1, 'b')]" }
@{ expression = "[lessOrEquals(5, 'a')]" }
) {
param($expression)
$yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "$expression"
"@
$out = dsc config get -i $yaml 2>$TestDrive/error.log
$LASTEXITCODE | Should -Be 2
$log = Get-Content -Path $TestDrive/error.log -Raw
$log | Should -BeLike "*ERROR* Arguments must be of the same type*"

}
}
17 changes: 17 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ extensionManifestSchemaDescription = "Defines the JSON Schema the extension mani
[functions]
invalidArgType = "Invalid argument type"
invalidArguments = "Invalid argument(s)"
typeMismatch = "Arguments must be of the same type (both numbers or both strings)"
unknownFunction = "Unknown function '%{name}'"
noArgsAccepted = "Function '%{name}' does not accept arguments"
invalidArgCount = "Function '%{name}' requires exactly %{count} arguments"
Expand Down Expand Up @@ -248,6 +249,14 @@ description = "Evaluates if the two values are the same"
description = "Returns the boolean value false"
invoked = "false function"

[functions.greater]
description = "Evaluates if the first value is greater than the second value"
invoked = "greater function"

[functions.greaterOrEquals]
description = "Evaluates if the first value is greater than or equal to the second value"
invoked = "greaterOrEquals function"

[functions.format]
description = "Formats a string using the given arguments"
experimental = "`format()` function is experimental"
Expand All @@ -267,6 +276,14 @@ parseStringError = "unable to parse string to int"
castError = "unable to cast to int"
parseNumError = "unable to parse number to int"

[functions.less]
description = "Evaluates if the first value is less than the second value"
invoked = "less function"

[functions.lessOrEquals]
description = "Evaluates if the first value is less than or equal to the second value"
invoked = "lessOrEquals function"

[functions.max]
description = "Returns the largest number from a list of numbers"
emptyArray = "Array cannot be empty"
Expand Down
93 changes: 93 additions & 0 deletions dsc_lib/src/functions/greater.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{AcceptedArgKind, Function, FunctionCategory};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct Greater {}

impl Function for Greater {
fn description(&self) -> String {
t!("functions.greater.description").to_string()
}

fn category(&self) -> FunctionCategory {
FunctionCategory::Comparison
}

fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
vec![AcceptedArgKind::Number, AcceptedArgKind::String]
}

fn min_args(&self) -> usize {
2
}

fn max_args(&self) -> usize {
2
}

fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.greater.invoked"));

let first = &args[0];
let second = &args[1];

if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) {
return Ok(Value::Bool(num1 > num2));
}

if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) {
return Ok(Value::Bool(str1 > str2));
}

Err(DscError::Parser(t!("functions.typeMismatch").to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;

#[test]
fn number_greater() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greater(2,1)]", &Context::new()).unwrap();
assert_eq!(result, true);
}

#[test]
fn number_not_greater() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greater(1,2)]", &Context::new()).unwrap();
assert_eq!(result, false);
}

#[test]
fn number_equal() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greater(1,1)]", &Context::new()).unwrap();
assert_eq!(result, false);
}

#[test]
fn string_greater() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greater('b','a')]", &Context::new()).unwrap();
assert_eq!(result, true);
}

#[test]
fn type_mismatch_string_number() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greater('5', 3)]", &Context::new());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type"));
}
}
93 changes: 93 additions & 0 deletions dsc_lib/src/functions/greater_or_equals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{AcceptedArgKind, Function, FunctionCategory};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct GreaterOrEquals {}

impl Function for GreaterOrEquals {
fn description(&self) -> String {
t!("functions.greaterOrEquals.description").to_string()
}

fn category(&self) -> FunctionCategory {
FunctionCategory::Comparison
}

fn min_args(&self) -> usize {
2
}

fn max_args(&self) -> usize {
2
}

fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
vec![AcceptedArgKind::Number, AcceptedArgKind::String]
}

fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.greaterOrEquals.invoked"));

let first = &args[0];
let second = &args[1];

if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) {
return Ok(Value::Bool(num1 >= num2));
}

if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) {
return Ok(Value::Bool(str1 >= str2));
}

Err(DscError::Parser(t!("functions.typeMismatch").to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;

#[test]
fn number_greater_or_equals() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greaterOrEquals(5,3)]", &Context::new()).unwrap();
assert_eq!(result, true);
}

#[test]
fn number_not_greater_or_equals() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greaterOrEquals(3,5)]", &Context::new()).unwrap();
assert_eq!(result, false);
}

#[test]
fn number_equal() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greaterOrEquals(5,5)]", &Context::new()).unwrap();
assert_eq!(result, true);
}

#[test]
fn string_greater_or_equals() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greaterOrEquals('b','a')]", &Context::new()).unwrap();
assert_eq!(result, true);
}

#[test]
fn type_mismatch_string_number() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[greaterOrEquals('5', 3)]", &Context::new());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type"));
}
}
92 changes: 92 additions & 0 deletions dsc_lib/src/functions/less.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{AcceptedArgKind, Function, FunctionCategory};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct Less {}

impl Function for Less {
fn description(&self) -> String {
t!("functions.less.description").to_string()
}

fn category(&self) -> FunctionCategory {
FunctionCategory::Comparison
}

fn min_args(&self) -> usize {
2
}

fn max_args(&self) -> usize {
2
}

fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
vec![AcceptedArgKind::Number, AcceptedArgKind::String]
}

fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.less.invoked"));

let first = &args[0];
let second = &args[1];

if let (Some(num1), Some(num2)) = (first.as_i64(), second.as_i64()) {
return Ok(Value::Bool(num1 < num2));
}

if let (Some(str1), Some(str2)) = (first.as_str(), second.as_str()) {
return Ok(Value::Bool(str1 < str2));
}

Err(DscError::Parser(t!("functions.typeMismatch").to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;

#[test]
fn number_less() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[less(3,5)]", &Context::new()).unwrap();
assert_eq!(result, true);
}

#[test]
fn number_not_less() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[less(5,3)]", &Context::new()).unwrap();
assert_eq!(result, false);
}

#[test]
fn number_equal() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[less(5,5)]", &Context::new()).unwrap();
assert_eq!(result, false);
}

#[test]
fn string_less() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[less('a','b')]", &Context::new()).unwrap();
assert_eq!(result, true);
}

fn type_mismatch_string_number() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[lessOrEquals('5', 3)]", &Context::new());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Arguments must be of the same type"));
}
}
Loading
Loading