From 69fa3a8311583a3b475e7acb6baf189696beaff1 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 3 Apr 2025 17:03:26 -0700 Subject: [PATCH 1/4] Fix `Assertion` resource to fail when test fails --- dsc/assertion.dsc.resource.json | 6 +++++- dsc/examples/assertion.dsc.yaml | 4 ++-- dsc/locales/en-us.toml | 1 + dsc/src/args.rs | 2 ++ dsc/src/main.rs | 6 +++--- dsc/src/subcommand.rs | 24 ++++++++++++++++++++---- dsc/src/util.rs | 28 ++++++++++++++++++++++++++++ dsc/tests/dsc_assertion.tests.ps1 | 20 ++++++++++++++++++++ 8 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 dsc/tests/dsc_assertion.tests.ps1 diff --git a/dsc/assertion.dsc.resource.json b/dsc/assertion.dsc.resource.json index b3d69373d..0beab7662 100644 --- a/dsc/assertion.dsc.resource.json +++ b/dsc/assertion.dsc.resource.json @@ -11,6 +11,7 @@ "pass-through", "config", "--as-group", + "--as-assert", "test", "--as-get", { @@ -26,6 +27,7 @@ "pass-through", "config", "--as-group", + "--as-assert", "test", { "jsonInputArg": "--input", @@ -42,6 +44,7 @@ "pass-through", "config", "--as-group", + "--as-assert", "test", "--as-config", { @@ -59,7 +62,8 @@ "4": "Invalid input format", "5": "Resource instance failed schema validation", "6": "Command cancelled", - "7": "Resource not found" + "7": "Resource not found", + "8": "Assertion failed" }, "validate": { "executable": "dsc", diff --git a/dsc/examples/assertion.dsc.yaml b/dsc/examples/assertion.dsc.yaml index 41ccb23b0..341b60d65 100644 --- a/dsc/examples/assertion.dsc.yaml +++ b/dsc/examples/assertion.dsc.yaml @@ -23,5 +23,5 @@ resources: keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion valueName: SystemRoot valueData: - # this is deliberately set to z: drive so that the assertion fails - String: Z:\Windows + # this is deliberately set to L: drive so that the assertion fails + String: L:\Windows diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index 740ab9a1e..ab72129f2 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -73,6 +73,7 @@ testInputEmpty = "Expected input is required" [subcommand] actualStateNotObject = "actual_state is not an object" unexpectedTestResult = "Unexpected Group TestResult" +assertionFailed = "Assertion failed for resource '%{resource_type}'" message = "message" currentDirectory = "current directory" noParameters = "No parameters specified" diff --git a/dsc/src/args.rs b/dsc/src/args.rs index 622391ddb..410ba2f61 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -58,6 +58,8 @@ pub enum SubCommand { // Used to inform when DSC is used as a group resource to modify it's output #[clap(long, hide = true)] as_group: bool, + #[clap(long, hide = true)] + as_assert: bool, // Used to inform when DSC is used as a include group resource #[clap(long, hide = true)] as_include: bool, diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 34cd1d2ad..526c3e628 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -49,11 +49,11 @@ fn main() { let mut cmd = Args::command(); generate(shell, &mut cmd, "dsc", &mut io::stdout()); }, - SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_include } => { + SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_assert, as_include } => { if let Some(file_name) = parameters_file { info!("{}: {file_name}", t!("main.readingParametersFile")); match std::fs::read_to_string(&file_name) { - Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), system_root.as_ref(), &as_group, &as_include, progress_format), + Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format), Err(err) => { error!("{} '{file_name}': {err}", t!("main.failedReadingParametersFile")); exit(util::EXIT_INVALID_INPUT); @@ -61,7 +61,7 @@ fn main() { } } else { - subcommand::config(&subcommand, ¶meters, system_root.as_ref(), &as_group, &as_include, progress_format); + subcommand::config(&subcommand, ¶meters, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format); } }, SubCommand::Resource { subcommand } => { diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 5c9c36163..fd108640e 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand}; use crate::resolve::{get_contents, Include}; use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; -use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, get_schema, write_object, get_input, set_dscconfigroot, validate_json}; +use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, validate_json, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR}; use dsc_lib::{ configure::{ config_doc::{ @@ -106,7 +106,7 @@ pub fn config_set(configurator: &mut Configurator, format: Option<&OutputFormat> } } -pub fn config_test(configurator: &mut Configurator, format: Option<&OutputFormat>, as_group: &bool, as_get: &bool, as_config: &bool) +pub fn config_test(configurator: &mut Configurator, format: Option<&OutputFormat>, as_group: &bool, as_get: &bool, as_config: &bool, as_assert: &bool) { match configurator.invoke_test() { Ok(result) => { @@ -115,6 +115,10 @@ pub fn config_test(configurator: &mut Configurator, format: Option<&OutputFormat let mut result_configuration = Configuration::new(); result_configuration.resources = Vec::new(); for test_result in result.results { + if *as_assert && !in_desired_state(&test_result) { + error!("{}", t!("subcommand.assertionFailed", resource_type = test_result.resource_type)); + exit(EXIT_DSC_ASSERTION_FAILED); + } let properties = match test_result.result { TestResult::Resource(test_response) => { if test_response.actual_state.is_object() { @@ -150,6 +154,10 @@ pub fn config_test(configurator: &mut Configurator, format: Option<&OutputFormat else if *as_get { let mut group_result = Vec::::new(); for test_result in result.results { + if *as_assert && !in_desired_state(&test_result) { + error!("{}", t!("subcommand.assertionFailed", resource_type = test_result.resource_type)); + exit(EXIT_DSC_ASSERTION_FAILED); + } group_result.push(test_result.into()); } match serde_json::to_string(&group_result) { @@ -161,6 +169,14 @@ pub fn config_test(configurator: &mut Configurator, format: Option<&OutputFormat } } else { + if *as_assert { + for test_result in &result.results { + if !in_desired_state(test_result) { + error!("{}", t!("subcommand.assertionFailed", resource_type = test_result.resource_type)); + exit(EXIT_DSC_ASSERTION_FAILED); + } + } + } match serde_json::to_string(&(result.results)) { Ok(json) => json, Err(err) => { @@ -252,7 +268,7 @@ fn initialize_config_root(path: Option<&String>) -> Option { } #[allow(clippy::too_many_lines)] -pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounted_path: Option<&String>, as_group: &bool, as_include: &bool, progress_format: ProgressFormat) { +pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounted_path: Option<&String>, as_group: &bool, as_assert: &bool, as_include: &bool, progress_format: ProgressFormat) { let (new_parameters, json_string) = match subcommand { ConfigSubCommand::Get { input, file, .. } | ConfigSubCommand::Set { input, file, .. } | @@ -363,7 +379,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounte config_set(&mut configurator, output_format.as_ref(), as_group); }, ConfigSubCommand::Test { output_format, as_get, as_config, .. } => { - config_test(&mut configurator, output_format.as_ref(), as_group, as_get, as_config); + config_test(&mut configurator, output_format.as_ref(), as_group, as_get, as_config, as_assert); }, ConfigSubCommand::Validate { input, file, output_format} => { let mut result = ValidateResult { diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 3c1607faf..e6991bb9e 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -3,6 +3,7 @@ use crate::args::{DscType, OutputFormat, TraceFormat}; use crate::resolve::Include; +use dsc_lib::configure::config_result::ResourceTestResult; use dsc_lib::{ configure::{ config_doc::Configuration, @@ -54,6 +55,7 @@ pub const EXIT_INVALID_INPUT: i32 = 4; pub const EXIT_VALIDATION_FAILED: i32 = 5; pub const EXIT_CTRL_C: i32 = 6; pub const EXIT_DSC_RESOURCE_NOT_FOUND: i32 = 7; +pub const EXIT_DSC_ASSERTION_FAILED: i32 = 8; pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT"; pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL"; @@ -530,3 +532,29 @@ pub fn set_dscconfigroot(config_path: &str) -> String full_path.to_string_lossy().into_owned() } + + +/// Check if the test result is in the desired state. +/// +/// # Arguments +/// +/// * `test_result` - The test result to check +/// +/// # Returns +/// +/// * `bool` - True if the test result is in the desired state, false otherwise +pub fn in_desired_state(test_result: &ResourceTestResult) -> bool { + match &test_result.result { + TestResult::Resource(result) => { + return result.in_desired_state; + }, + TestResult::Group(results) => { + for result in results { + if !in_desired_state(&result) { + return false; + } + } + return true; + } + } +} diff --git a/dsc/tests/dsc_assertion.tests.ps1 b/dsc/tests/dsc_assertion.tests.ps1 new file mode 100644 index 000000000..d5d688a75 --- /dev/null +++ b/dsc/tests/dsc_assertion.tests.ps1 @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Assertion resource tests' { + It 'Example works for ' -TestCases @( + @{ operation = 'get' } + @{ operation = 'set' } + @{ operation = 'test' } +# TODO: Add export to test when https://github.com/PowerShell/DSC/issues/428 is fixed +# @{ operation = 'export' } + ) { + param($operation) + $jsonPath = Join-Path $PSScriptRoot '../examples/assertion.dsc.yaml' + $out = dsc config $operation -f $jsonPath 2> "$TestDrive/trace.log" + $LASTEXITCODE | Should -Be 2 + $out | Should -BeNullOrEmpty + $log = Get-Content "$TestDrive/trace.log" -Raw + $log | Should -Match '.*Assertion failed.*' + } +} From cfd2a021a4e5d09c44fe90b5925841e8459fd91b Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 3 Apr 2025 17:08:21 -0700 Subject: [PATCH 2/4] fix clippy --- dsc/src/util.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dsc/src/util.rs b/dsc/src/util.rs index e6991bb9e..564802c6f 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -543,18 +543,19 @@ pub fn set_dscconfigroot(config_path: &str) -> String /// # Returns /// /// * `bool` - True if the test result is in the desired state, false otherwise +#[must_use] pub fn in_desired_state(test_result: &ResourceTestResult) -> bool { match &test_result.result { TestResult::Resource(result) => { - return result.in_desired_state; + result.in_desired_state }, TestResult::Group(results) => { for result in results { - if !in_desired_state(&result) { + if !in_desired_state(result) { return false; } } - return true; + true } } } From 393258429b44c8a03ff071909a01effa18beedb1 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 3 Apr 2025 17:22:17 -0700 Subject: [PATCH 3/4] need to skip test if not windows --- dsc/tests/dsc_assertion.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/tests/dsc_assertion.tests.ps1 b/dsc/tests/dsc_assertion.tests.ps1 index d5d688a75..0e13b6de5 100644 --- a/dsc/tests/dsc_assertion.tests.ps1 +++ b/dsc/tests/dsc_assertion.tests.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'Assertion resource tests' { +Describe 'Assertion resource tests' -Skip:(!$IsWindows) { It 'Example works for ' -TestCases @( @{ operation = 'get' } @{ operation = 'set' } From fa5ecf5bd2214514a985674b19ed1263d427ba76 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Thu, 3 Apr 2025 17:40:25 -0700 Subject: [PATCH 4/4] fix test --- dsc/tests/dsc_config_test.tests.ps1 | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dsc/tests/dsc_config_test.tests.ps1 b/dsc/tests/dsc_config_test.tests.ps1 index 16e265822..5452ce3e0 100644 --- a/dsc/tests/dsc_config_test.tests.ps1 +++ b/dsc/tests/dsc_config_test.tests.ps1 @@ -21,15 +21,14 @@ Describe 'dsc config test tests' { family: Windows '@ - $out = dsc config test -i $configYaml | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - + $out = dsc config test -i $configYaml 2> "$TestDrive/trace.log" | ConvertFrom-Json if ($IsWindows) { + $LASTEXITCODE | Should -Be 0 $out.results[0].result.inDesiredState | Should -BeTrue - } - else { - $out.results[0].result.inDesiredState | Should -BeFalse - $out.results[0].result.differingProperties | Should -Contain 'resources' + } else { + $LASTEXITCODE | Should -Be 2 + $log = Get-Content "$TestDrive/trace.log" -Raw + $log | Should -Match '.*Assertion failed.*' } }