Skip to content

Commit b52b5cb

Browse files
authored
Merge pull request #595 from SteveL-MSFT/assert-array
fix output of Assertion resource for test to resemble config and add support for array comparison
2 parents 51ebbe3 + 4036a5d commit b52b5cb

File tree

7 files changed

+269
-25
lines changed

7 files changed

+269
-25
lines changed

dsc/assertion.dsc.resource.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"config",
3838
"--as-group",
3939
"test",
40-
"--as-get"
40+
"--as-config"
4141
],
4242
"input": "stdin",
4343
"return": "state"

dsc/src/args.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,12 @@ pub enum ConfigSubCommand {
102102
path: Option<String>,
103103
#[clap(short = 'f', long, help = "The output format to use")]
104104
format: Option<OutputFormat>,
105+
// Used by Assertion resource to return `test` result as a `get` result
105106
#[clap(long, hide = true)]
106107
as_get: bool,
108+
// Used by Assertion resource to return `test` result as a configuration `test` result
109+
#[clap(long, hide = true)]
110+
as_config: bool,
107111
},
108112
#[clap(name = "validate", about = "Validate the current configuration", hide = true)]
109113
Validate {

dsc/src/subcommand.rs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ use crate::resolve::{get_contents, Include};
66
use crate::resource_command::{get_resource, self};
77
use crate::tablewriter::Table;
88
use crate::util::{DSC_CONFIG_ROOT, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR, get_schema, write_output, get_input, set_dscconfigroot, validate_json};
9-
use dsc_lib::configure::{Configurator, config_doc::{Configuration, ExecutionKind}, config_result::ResourceGetResult};
10-
use dsc_lib::dscerror::DscError;
11-
use dsc_lib::dscresources::invoke_result::ResolveResult;
129
use dsc_lib::{
10+
configure::{
11+
config_doc::{
12+
Configuration,
13+
ExecutionKind,
14+
Resource,
15+
},
16+
config_result::ResourceGetResult,
17+
Configurator,
18+
},
19+
dscerror::DscError,
1320
DscManager,
14-
dscresources::invoke_result::ValidateResult,
21+
dscresources::invoke_result::{
22+
ResolveResult,
23+
TestResult,
24+
ValidateResult,
25+
},
1526
dscresources::dscresource::{Capability, ImplementedAs, Invoke},
1627
dscresources::resource_manifest::{import_manifest, ResourceManifest},
1728
};
@@ -93,12 +104,48 @@ pub fn config_set(configurator: &mut Configurator, format: &Option<OutputFormat>
93104
}
94105
}
95106

96-
pub fn config_test(configurator: &mut Configurator, format: &Option<OutputFormat>, as_group: &bool, as_get: &bool)
107+
pub fn config_test(configurator: &mut Configurator, format: &Option<OutputFormat>, as_group: &bool, as_get: &bool, as_config: &bool)
97108
{
98109
match configurator.invoke_test() {
99110
Ok(result) => {
100111
if *as_group {
101-
let json = if *as_get {
112+
let json = if *as_config {
113+
let mut result_configuration = Configuration::new();
114+
result_configuration.resources = Vec::new();
115+
for test_result in result.results {
116+
let properties = match test_result.result {
117+
TestResult::Resource(test_response) => {
118+
if test_response.actual_state.is_object() {
119+
test_response.actual_state.as_object().cloned()
120+
} else {
121+
debug!("actual_state is not an object");
122+
None
123+
}
124+
},
125+
TestResult::Group(_) => {
126+
// not expected
127+
debug!("Unexpected Group TestResult");
128+
None
129+
}
130+
};
131+
let resource = Resource {
132+
name: test_result.name,
133+
resource_type: test_result.resource_type,
134+
properties,
135+
depends_on: None,
136+
metadata: None,
137+
};
138+
result_configuration.resources.push(resource);
139+
}
140+
match serde_json::to_string(&result_configuration) {
141+
Ok(json) => json,
142+
Err(err) => {
143+
error!("JSON Error: {err}");
144+
exit(EXIT_JSON_ERROR);
145+
}
146+
}
147+
}
148+
else if *as_get {
102149
let mut group_result = Vec::<ResourceGetResult>::new();
103150
for test_result in result.results {
104151
group_result.push(test_result.into());
@@ -294,8 +341,8 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, mounte
294341
ConfigSubCommand::Set { format, .. } => {
295342
config_set(&mut configurator, format, as_group);
296343
},
297-
ConfigSubCommand::Test { format, as_get, .. } => {
298-
config_test(&mut configurator, format, as_group, as_get);
344+
ConfigSubCommand::Test { format, as_get, as_config, .. } => {
345+
config_test(&mut configurator, format, as_group, as_get, as_config);
299346
},
300347
ConfigSubCommand::Validate { document, path, format} => {
301348
let mut result = ValidateResult {

dsc/tests/dsc_config_test.tests.ps1

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'dsc config test tests' {
5+
It 'Assertion works correctly' {
6+
$configYaml = @'
7+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
8+
resources:
9+
- name: Operating System Assertion
10+
type: Microsoft.DSC/Assertion
11+
properties:
12+
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json
13+
resources:
14+
- name: Is64BitOS
15+
type: Microsoft/OSInfo
16+
properties:
17+
bitness: '64'
18+
- name: 64bit test 2
19+
type: Microsoft/OSInfo
20+
properties:
21+
family: Windows
22+
'@
23+
24+
$out = dsc config test -d $configYaml | ConvertFrom-Json
25+
$LASTEXITCODE | Should -Be 0
26+
27+
if ($IsWindows) {
28+
$out.results[0].result.inDesiredState | Should -BeTrue
29+
}
30+
else {
31+
$out.results[0].result.inDesiredState | Should -BeFalse
32+
$out.results[0].result.differingProperties | Should -Contain 'resources'
33+
}
34+
}
35+
}

dsc_lib/src/discovery/command_discovery.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ impl CommandDiscovery {
9595
Ok(v) => {
9696
resource_path_setting = v;
9797
},
98-
Err(e) => {
98+
Err(e) => {
9999
debug!("{e}");
100100
}
101101
}
@@ -144,7 +144,7 @@ impl CommandDiscovery {
144144
paths.push(exe_home_pb);
145145

146146
if let Ok(new_path) = env::join_paths(paths.clone()) {
147-
env::set_var("PATH", &new_path);
147+
env::set_var("PATH", new_path);
148148
}
149149
}
150150
}
@@ -369,7 +369,7 @@ impl ResourceDiscovery for CommandDiscovery {
369369
} else {
370370
self.discover_resources("*")?;
371371
self.discover_adapted_resources(type_name_filter, adapter_name_filter)?;
372-
372+
373373
// add/update found adapted resources to the lookup_table
374374
add_resources_to_lookup_table(&self.adapted_resources);
375375

@@ -652,7 +652,7 @@ fn save_adapted_resources_lookup_table(lookup_table: &HashMap<String, String>)
652652
fn load_adapted_resources_lookup_table() -> HashMap<String, String>
653653
{
654654
let file_path = get_lookup_table_file_path();
655-
655+
656656
let lookup_table: HashMap<String, String> = match fs::read(file_path.clone()){
657657
Ok(data) => { serde_json::from_slice(&data).unwrap_or_default() },
658658
Err(_) => { HashMap::new() }

dsc_lib/src/dscresources/dscresource.rs

Lines changed: 168 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
77
use serde::{Deserialize, Serialize};
88
use serde_json::Value;
99
use std::collections::HashMap;
10-
use tracing::debug;
10+
use tracing::{debug, info};
1111

1212
use super::{command_resource, dscerror, invoke_result::{ExportResult, GetResult, ResolveResult, ResourceTestResponse, SetResult, TestResult, ValidateResult}, resource_manifest::import_manifest};
1313

@@ -338,6 +338,16 @@ pub fn get_well_known_properties() -> HashMap<String, Value> {
338338
}
339339

340340
#[must_use]
341+
/// Performs a comparison of two JSON Values if the expected is a strict subset of the actual
342+
///
343+
/// # Arguments
344+
///
345+
/// * `expected` - The expected value
346+
/// * `actual` - The actual value
347+
///
348+
/// # Returns
349+
///
350+
/// An array of top level properties that differ, if any
341351
pub fn get_diff(expected: &Value, actual: &Value) -> Vec<String> {
342352
let mut diff_properties: Vec<String> = Vec::new();
343353
if expected.is_null() {
@@ -363,28 +373,176 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec<String> {
363373
if value.is_object() {
364374
let sub_diff = get_diff(value, &actual[key]);
365375
if !sub_diff.is_empty() {
376+
debug!("diff: sub diff for {key}");
366377
diff_properties.push(key.to_string());
367378
}
368379
}
369380
else {
370-
match actual.as_object() {
371-
Some(actual_object) => {
372-
if actual_object.contains_key(key) {
373-
if value != &actual[key] {
381+
// skip `$schema` key as that is provided as input, but not output typically
382+
if key == "$schema" {
383+
continue;
384+
}
385+
386+
if let Some(actual_object) = actual.as_object() {
387+
if actual_object.contains_key(key) {
388+
if let Some(value_array) = value.as_array() {
389+
if let Some(actual_array) = actual[key].as_array() {
390+
if !is_same_array(value_array, actual_array) {
391+
info!("diff: arrays differ for {key}");
392+
diff_properties.push(key.to_string());
393+
}
394+
} else {
395+
info!("diff: {} is not an array", actual[key]);
374396
diff_properties.push(key.to_string());
375397
}
376-
}
377-
else {
398+
} else if value != &actual[key] {
378399
diff_properties.push(key.to_string());
379400
}
380-
},
381-
None => {
401+
} else {
402+
info!("diff: {key} missing");
382403
diff_properties.push(key.to_string());
383-
},
404+
}
405+
} else {
406+
info!("diff: {key} not object");
407+
diff_properties.push(key.to_string());
384408
}
385409
}
386410
}
387411
}
388412

389413
diff_properties
390414
}
415+
416+
/// Compares two arrays independent of order
417+
fn is_same_array(expected: &Vec<Value>, actual: &Vec<Value>) -> bool {
418+
if expected.len() != actual.len() {
419+
info!("diff: arrays are different lengths");
420+
return false;
421+
}
422+
423+
for item in expected {
424+
if !array_contains(actual, item) {
425+
info!("diff: actual array missing expected element");
426+
return false;
427+
}
428+
}
429+
430+
true
431+
}
432+
433+
fn array_contains(array: &Vec<Value>, find: &Value) -> bool {
434+
for item in array {
435+
if find.is_boolean() && item.is_boolean() && find.as_bool().unwrap() == item.as_bool().unwrap() {
436+
return true;
437+
}
438+
439+
if find.is_f64() && item.is_f64() && (find.as_f64().unwrap() - item.as_f64().unwrap()).abs() < 0.1 {
440+
return true;
441+
}
442+
443+
if find.is_i64() && item.is_i64() && find.as_i64().unwrap() == item.as_i64().unwrap() {
444+
return true;
445+
}
446+
447+
if find.is_null() && item.is_null() {
448+
return true;
449+
}
450+
451+
if find.is_number() && item.is_number() && find.as_number().unwrap() == item.as_number().unwrap() {
452+
return true;
453+
}
454+
455+
if find.is_string() && item.is_string() && find.as_str().unwrap() == item.as_str().unwrap() {
456+
return true;
457+
}
458+
459+
if find.is_u64() && item.is_u64() && find.as_u64().unwrap() == item.as_u64().unwrap() {
460+
return true;
461+
}
462+
463+
if find.is_object() && item.is_object() {
464+
let obj_diff = get_diff(find, item);
465+
if obj_diff.is_empty() {
466+
return true;
467+
}
468+
}
469+
470+
if find.is_array() && item.is_array() && is_same_array(item.as_array().unwrap(), find.as_array().unwrap()) {
471+
return true;
472+
}
473+
}
474+
475+
false
476+
}
477+
478+
#[test]
479+
fn same_array() {
480+
use serde_json::json;
481+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"}), json!(null)];
482+
let array_two = vec![json!("a"), json!(1), json!({"a":"b"}), json!(null)];
483+
assert_eq!(is_same_array(&array_one, &array_two), true);
484+
}
485+
486+
#[test]
487+
fn same_array_out_of_order() {
488+
use serde_json::json;
489+
let array_one = vec![json!("a"), json!(true), json!({"a":"b"})];
490+
let array_two = vec![json!({"a":"b"}), json!("a"), json!(true)];
491+
assert_eq!(is_same_array(&array_one, &array_two), true);
492+
}
493+
494+
#[test]
495+
fn different_array() {
496+
use serde_json::json;
497+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"})];
498+
let array_two = vec![json!({"a":"b"}), json!("a"), json!(2)];
499+
assert_eq!(is_same_array(&array_one, &array_two), false);
500+
}
501+
502+
#[test]
503+
fn different_array_sizes() {
504+
use serde_json::json;
505+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"})];
506+
let array_two = vec![json!({"a":"b"}), json!("a")];
507+
assert_eq!(is_same_array(&array_one, &array_two), false);
508+
}
509+
510+
#[test]
511+
fn array_with_multiple_objects_with_actual_superset() {
512+
use serde_json::json;
513+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"}), json!({"c":"d"})];
514+
let array_two = vec![json!("a"), json!(1), json!({"c":"d", "a":"b"}), json!({"c":"d"})];
515+
assert_eq!(is_same_array(&array_one, &array_two), true);
516+
}
517+
518+
#[test]
519+
fn array_with_multiple_objects_with_expected_superset() {
520+
use serde_json::json;
521+
let array_one = vec![json!("a"), json!(1), json!({"a":"b", "c":"d"}), json!({"c":"d"})];
522+
let array_two = vec![json!("a"), json!(1), json!({"a":"b"}), json!({"c":"d"})];
523+
assert_eq!(is_same_array(&array_one, &array_two), false);
524+
}
525+
526+
#[test]
527+
fn array_with_duplicates_out_of_order() {
528+
use serde_json::json;
529+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"}), json!({"a":"b"})];
530+
let array_two = vec![json!({"a":"b"}), json!("a"), json!(1), json!({"a":"b"})];
531+
assert_eq!(is_same_array(&array_one, &array_two), true);
532+
}
533+
534+
#[test]
535+
fn same_array_with_nested_array() {
536+
use serde_json::json;
537+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"}), json!(vec![json!("a"), json!(1)])];
538+
let array_two = vec![json!("a"), json!(1), json!({"a":"b"}), json!(vec![json!("a"), json!(1)])];
539+
assert_eq!(is_same_array(&array_one, &array_two), true);
540+
}
541+
542+
#[test]
543+
fn different_array_with_nested_array() {
544+
use serde_json::json;
545+
let array_one = vec![json!("a"), json!(1), json!({"a":"b"}), json!(vec![json!("a"), json!(1)])];
546+
let array_two = vec![json!("a"), json!(1), json!({"a":"b"}), json!(vec![json!("a"), json!(2)])];
547+
assert_eq!(is_same_array(&array_one, &array_two), false);
548+
}

0 commit comments

Comments
 (0)