Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix when resource test returns '_inDesiredState', that takes precedence #676

Merged
merged 3 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions dsc/tests/dsc_config_test.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,34 @@ Describe 'dsc config test tests' {
$out.results[0].result.differingProperties | Should -Contain 'resources'
}
}

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, $valueOne, $valueTwo)

$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Test
type: Test/InDesiredState
properties:
_inDesiredState: $inDesiredState
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 ($same) {
$out.results[0].result.differingProperties | Should -BeNullOrEmpty
}
else {
$out.results[0].result.differingProperties | Should -Be @('valueOne', 'valueTwo')
}
}
}
3 changes: 2 additions & 1 deletion dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}'"
Expand Down
19 changes: 17 additions & 2 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,12 @@ 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 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,
in_desired_state: diff_properties.is_empty(),
in_desired_state: in_desired_state.unwrap_or(diff_properties.is_empty()),
diff_properties,
}))
},
Expand All @@ -302,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,
}))
},
Expand Down Expand Up @@ -335,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 {
Expand Down
26 changes: 26 additions & 0 deletions tools/dsctest/dscindesiredstate.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
7 changes: 7 additions & 0 deletions tools/dsctest/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum Schemas {
Delete,
Exist,
ExitCode,
InDesiredState,
Sleep,
Trace,
WhatIf,
Expand Down Expand Up @@ -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")]
Expand Down
17 changes: 17 additions & 0 deletions tools/dsctest/src/in_desired_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

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>,
#[serde(rename = "valueOne")]
pub value_one: i32,
#[serde(rename = "valueTwo")]
pub value_two: i32,
}
17 changes: 17 additions & 0 deletions tools/dsctest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod args;
mod delete;
mod exist;
mod exit_code;
mod in_desired_state;
mod sleep;
mod trace;
mod whatif;
Expand All @@ -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;
Expand Down Expand Up @@ -65,6 +67,18 @@ 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_one = 1;
in_desired_state.value_two = 2;
serde_json::to_string(&in_desired_state).unwrap()
},
SubCommand::Schema { subcommand } => {
let schema = match subcommand {
Schemas::Delete => {
Expand All @@ -76,6 +90,9 @@ fn main() {
Schemas::ExitCode => {
schema_for!(ExitCode)
},
Schemas::InDesiredState => {
schema_for!(InDesiredState)
},
Schemas::Sleep => {
schema_for!(Sleep)
},
Expand Down