From 74f4b6972b7adbb8230a4ec998b4f3b7fda185f5 Mon Sep 17 00:00:00 2001 From: Steve Lee <slee@microsoft.com> Date: Wed, 5 Mar 2025 21:01:11 -0800 Subject: [PATCH 1/3] Fix when resource test returns '_inDesiredState', that takes precedence --- dsc/tests/dsc_config_test.tests.ps1 | 27 +++++++++++++++++++ dsc_lib/locales/en-us.toml | 3 ++- dsc_lib/src/dscresources/command_resource.rs | 22 +++++++++++++-- .../dscindesiredstate.dsc.resource.json | 26 ++++++++++++++++++ tools/dsctest/src/args.rs | 7 +++++ tools/dsctest/src/in_desired_state.rs | 13 +++++++++ tools/dsctest/src/main.rs | 16 +++++++++++ 7 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 tools/dsctest/dscindesiredstate.dsc.resource.json create mode 100644 tools/dsctest/src/in_desired_state.rs diff --git a/dsc/tests/dsc_config_test.tests.ps1 b/dsc/tests/dsc_config_test.tests.ps1 index 6b725467..be2db81d 100644 --- a/dsc/tests/dsc_config_test.tests.ps1 +++ b/dsc/tests/dsc_config_test.tests.ps1 @@ -32,4 +32,31 @@ Describe 'dsc config test tests' { $out.results[0].result.differingProperties | Should -Contain 'resources' } } + + It '_inDesiredState returned is used when: <inDesiredState>' -TestCases @( + @{ inDesiredState = $true } + @{ inDesiredState = $false } + ) { + param($inDesiredState) + + $configYaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test + type: Test/InDesiredState + properties: + _inDesiredState: $inDesiredState + value: Hello +"@ + + $out = dsc config test -i $configYaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.inDesiredState | Should -Be $inDesiredState + if ($inDesiredState) { + $out.results[0].result.differingProperties | Should -BeNullOrEmpty + } + else { + $out.results[0].result.differingProperties | Should -Contain 'value' + } + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 17ce4540..6393e0fd 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -99,7 +99,7 @@ setUnexpectedOutput = "Command did not return expected actual output" setUnexpectedDiff = "Command did not return expected diff output" invokeTest = "Invoking test for '%{resource}'" testSyntheticTest = "Resource '%{resource}' does not implement test, performing synthetic test" -invokeTestUsing = "Invoking test on '%{resource}' using '{executable}'" +invokeTestUsing = "Invoking test on '%{resource}' using '%{executable}'" testVerifyOutput = "Verifying output of test on '%{resource}' using '%{executable}'" testGroupTestResponse = "Import resource kind, returning group test response" testNoActualState = "No actual state returned" @@ -129,6 +129,7 @@ validateJson = "Validating against JSON: %{json}" resourceInvalidJson = "Resource reported input JSON is not valid" invalidArrayKey = "Unsupported array value for key '%{key}'. Only string and number is supported." invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, and array is supported." +inDesiredStateNotBool = "'_inDesiredState' is not a boolean" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index bd3442c0..9957e11d 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -283,11 +283,29 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re return Err(DscError::Operation(t!("dscresources.commandResource.failedParseJson", executable = &test.executable, stdout = stdout, stderr = stderr, err = err).to_string())) } }; - let diff_properties = get_diff(&expected_value, &actual_value); + // if actual state contains _inDesiredState, we use that to determine if the resource is in desired state + let mut in_desired_state: Option<bool> = None; + if let Some(in_desired_state_value) = actual_value.get("_inDesiredState") { + if let Some(desired_state) = in_desired_state_value.as_bool() { + in_desired_state = Some(desired_state); + } else { + return Err(DscError::Operation(t!("dscresources.commandResource.inDesiredStateNotBool").to_string())); + } + } + + let mut diff_properties: Vec<String> = Vec::new(); + match in_desired_state { + Some(true) => { + // if _inDesiredState is true, we don't need to check for diff properties + }, + Some(false) | None => { + diff_properties = get_diff(&expected_value, &actual_value); + } + } Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state: actual_value, - in_desired_state: diff_properties.is_empty(), + in_desired_state: in_desired_state.unwrap_or(diff_properties.is_empty()), diff_properties, })) }, diff --git a/tools/dsctest/dscindesiredstate.dsc.resource.json b/tools/dsctest/dscindesiredstate.dsc.resource.json new file mode 100644 index 00000000..be077603 --- /dev/null +++ b/tools/dsctest/dscindesiredstate.dsc.resource.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/InDesiredState", + "version": "0.1.0", + "test": { + "executable": "dsctest", + "args": [ + "in-desired-state", + { + "jsonInputArg": "--input", + "mandatory": true + } + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "in-desired-state" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 17127461..7b4e3f26 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -8,6 +8,7 @@ pub enum Schemas { Delete, Exist, ExitCode, + InDesiredState, Sleep, Trace, WhatIf, @@ -41,6 +42,12 @@ pub enum SubCommand { input: String, }, + #[clap(name = "in-desired-state", about = "Specify if the resource is in the desired state")] + InDesiredState { + #[clap(name = "input", short, long, help = "The input to the in desired state command as JSON")] + input: String, + }, + #[clap(name = "schema", about = "Get the JSON schema for a subcommand")] Schema { #[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")] diff --git a/tools/dsctest/src/in_desired_state.rs b/tools/dsctest/src/in_desired_state.rs new file mode 100644 index 00000000..2fb2c655 --- /dev/null +++ b/tools/dsctest/src/in_desired_state.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct InDesiredState { + #[serde(rename = "_inDesiredState", skip_serializing_if = "Option::is_none")] + pub in_desired_state: Option<bool>, + pub value: String, +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 400fa2ad..7f4ce788 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -5,6 +5,7 @@ mod args; mod delete; mod exist; mod exit_code; +mod in_desired_state; mod sleep; mod trace; mod whatif; @@ -15,6 +16,7 @@ use schemars::schema_for; use crate::delete::Delete; use crate::exist::{Exist, State}; use crate::exit_code::ExitCode; +use crate::in_desired_state::InDesiredState; use crate::sleep::Sleep; use crate::trace::Trace; use crate::whatif::WhatIf; @@ -65,6 +67,17 @@ fn main() { } input }, + SubCommand::InDesiredState { input } => { + let mut in_desired_state = match serde_json::from_str::<in_desired_state::InDesiredState>(&input) { + Ok(in_desired_state) => in_desired_state, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + in_desired_state.value = "SomethingElse".to_string(); + serde_json::to_string(&in_desired_state).unwrap() + }, SubCommand::Schema { subcommand } => { let schema = match subcommand { Schemas::Delete => { @@ -76,6 +89,9 @@ fn main() { Schemas::ExitCode => { schema_for!(ExitCode) }, + Schemas::InDesiredState => { + schema_for!(InDesiredState) + }, Schemas::Sleep => { schema_for!(Sleep) }, From 618d5ed783fc337ec39bf65d9415a24e8a486c60 Mon Sep 17 00:00:00 2001 From: Steve Lee <slee@microsoft.com> Date: Thu, 6 Mar 2025 13:52:55 -0800 Subject: [PATCH 2/3] make stateAndDiff also respect resource, enhance tests --- dsc/tests/dsc_config_test.tests.ps1 | 17 +++++---- dsc_lib/src/dscresources/command_resource.rs | 37 +++++++++----------- tools/dsctest/src/in_desired_state.rs | 6 +++- tools/dsctest/src/main.rs | 3 +- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/dsc/tests/dsc_config_test.tests.ps1 b/dsc/tests/dsc_config_test.tests.ps1 index be2db81d..16e26582 100644 --- a/dsc/tests/dsc_config_test.tests.ps1 +++ b/dsc/tests/dsc_config_test.tests.ps1 @@ -33,11 +33,13 @@ Describe 'dsc config test tests' { } } - It '_inDesiredState returned is used when: <inDesiredState>' -TestCases @( - @{ inDesiredState = $true } - @{ inDesiredState = $false } + It '_inDesiredState returned is used when: inDesiredState = <inDesiredState> and same = <same>' -TestCases @( + @{ inDesiredState = $true; valueOne = 1; valueTwo = 2; same = $true } + @{ inDesiredState = $true; valueOne = 3; valueTwo = 4; same = $false } + @{ inDesiredState = $false; valueOne = 1; valueTwo = 2; same = $true } + @{ inDesiredState = $false; valueOne = 3; valueTwo = 4; same = $false } ) { - param($inDesiredState) + param($inDesiredState, $valueOne, $valueTwo) $configYaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json @@ -46,17 +48,18 @@ Describe 'dsc config test tests' { type: Test/InDesiredState properties: _inDesiredState: $inDesiredState - value: Hello + valueOne: $valueOne + valueTwo: $valueTwo "@ $out = dsc config test -i $configYaml | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.results[0].result.inDesiredState | Should -Be $inDesiredState - if ($inDesiredState) { + if ($same) { $out.results[0].result.differingProperties | Should -BeNullOrEmpty } else { - $out.results[0].result.differingProperties | Should -Contain 'value' + $out.results[0].result.differingProperties | Should -Be @('valueOne', 'valueTwo') } } } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 9957e11d..7b9c6061 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -283,25 +283,8 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re return Err(DscError::Operation(t!("dscresources.commandResource.failedParseJson", executable = &test.executable, stdout = stdout, stderr = stderr, err = err).to_string())) } }; - // if actual state contains _inDesiredState, we use that to determine if the resource is in desired state - let mut in_desired_state: Option<bool> = None; - if let Some(in_desired_state_value) = actual_value.get("_inDesiredState") { - if let Some(desired_state) = in_desired_state_value.as_bool() { - in_desired_state = Some(desired_state); - } else { - return Err(DscError::Operation(t!("dscresources.commandResource.inDesiredStateNotBool").to_string())); - } - } - - let mut diff_properties: Vec<String> = Vec::new(); - match in_desired_state { - Some(true) => { - // if _inDesiredState is true, we don't need to check for diff properties - }, - Some(false) | None => { - diff_properties = get_diff(&expected_value, &actual_value); - } - } + let in_desired_state = get_desired_state(&actual_value)?; + let diff_properties = get_diff(&expected_value, &actual_value); Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state: actual_value, @@ -320,10 +303,11 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string())); }; let diff_properties: Vec<String> = serde_json::from_str(diff_properties)?; + let in_desired_state = get_desired_state(&actual_value)?; Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state: actual_value, - in_desired_state: diff_properties.is_empty(), + in_desired_state: in_desired_state.unwrap_or(diff_properties.is_empty()), diff_properties, })) }, @@ -353,6 +337,19 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re } } +fn get_desired_state(actual: &Value) -> Result<Option<bool>, DscError> { + // if actual state contains _inDesiredState, we use that to determine if the resource is in desired state + let mut in_desired_state: Option<bool> = None; + if let Some(in_desired_state_value) = actual.get("_inDesiredState") { + if let Some(desired_state) = in_desired_state_value.as_bool() { + in_desired_state = Some(desired_state); + } else { + return Err(DscError::Operation(t!("dscresources.commandResource.inDesiredStateNotBool").to_string())); + } + } + Ok(in_desired_state) +} + fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result<TestResult, DscError> { let get_result = invoke_get(resource, cwd, expected)?; let actual_state = match get_result { diff --git a/tools/dsctest/src/in_desired_state.rs b/tools/dsctest/src/in_desired_state.rs index 2fb2c655..868ecd48 100644 --- a/tools/dsctest/src/in_desired_state.rs +++ b/tools/dsctest/src/in_desired_state.rs @@ -4,10 +4,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +#[allow(clippy::struct_field_names)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct InDesiredState { #[serde(rename = "_inDesiredState", skip_serializing_if = "Option::is_none")] pub in_desired_state: Option<bool>, - pub value: String, + #[serde(rename = "valueOne")] + pub value_one: i32, + #[serde(rename = "valueTwo")] + pub value_two: i32, } diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 7f4ce788..696308c4 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -75,7 +75,8 @@ fn main() { std::process::exit(1); } }; - in_desired_state.value = "SomethingElse".to_string(); + in_desired_state.value_one = 1; + in_desired_state.value_two= 2; serde_json::to_string(&in_desired_state).unwrap() }, SubCommand::Schema { subcommand } => { From ddc3f4ad0767cd59f274afb84d799c5771c2ff35 Mon Sep 17 00:00:00 2001 From: Steve Lee <slee@microsoft.com> Date: Thu, 6 Mar 2025 13:55:14 -0800 Subject: [PATCH 3/3] fix whitespace --- tools/dsctest/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 696308c4..d9f85a0b 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -76,7 +76,7 @@ fn main() { } }; in_desired_state.value_one = 1; - in_desired_state.value_two= 2; + in_desired_state.value_two = 2; serde_json::to_string(&in_desired_state).unwrap() }, SubCommand::Schema { subcommand } => {