Skip to content
Draft
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions adapters/csp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ serde_json = { workspace = true }
windows = { workspace = true }
wmi = { workspace = true }

[dev-dependencies]
test-case = { workspace = true }

[target.'cfg(windows)'.dev-dependencies]
is_elevated = { workspace = true }

[package.metadata.i18n]
available-locales = ["en-us"]
default-locale = "en-us"
Expand Down
87 changes: 49 additions & 38 deletions adapters/csp/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

use std::process::Command;

use test_case::test_case;

#[cfg(windows)]
use is_elevated::is_elevated;

#[test]
fn help_lists_all_operations() {
let output = Command::new(env!("CARGO_BIN_EXE_csp"))
Expand All @@ -16,18 +21,18 @@ fn help_lists_all_operations() {
}
}

#[test]
fn invalid_arguments_return_json_diagnostics() {
for args in [vec!["invalid"], vec!["get"], vec!["export", "--unexpected"]] {
let output = Command::new(env!("CARGO_BIN_EXE_csp"))
.args(args)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert!(error["error"].as_str().is_some_and(|s| !s.is_empty()));
}
#[test_case(vec!["invalid"]; "invalid command")]
#[test_case(vec!["get"]; "missing arguments for get")]
#[test_case(vec!["export", "--unexpected"]; "unexpected argument for export")]
fn invalid_arguments_return_json_diagnostics(args: Vec<&str>) {
let output = Command::new(env!("CARGO_BIN_EXE_csp"))
.args(&args)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert!(error["error"].as_str().is_some_and(|s| !s.is_empty()));
}

#[cfg(windows)]
Expand All @@ -53,49 +58,55 @@ fn invoke_with_stdin(operation: &str, input: &[u8]) -> std::process::Output {
}

#[cfg(windows)]
#[test]
fn accepts_large_json_payloads_over_stdin_for_all_operations() {
let input = serde_json::json!({"Payload": "x".repeat(70_000)}).to_string();
assert!(input.encode_utf16().count() > 32_767);
for operation in ["get", "set", "test", "export"] {
#[test_case("get"; "for get operation")]
#[test_case("set"; "for set operation")]
#[test_case("test"; "for test operation")]
#[test_case("export"; "for export operation")]
fn accepts_large_json_payloads_over_stdin(operation: &str) {
if is_elevated() {
let input = serde_json::json!({"Payload": "x".repeat(70_000)}).to_string();
assert!(input.encode_utf16().count() > 32_767);
let output = invoke_with_stdin(operation, input.as_bytes());
assert_eq!(output.status.code(), Some(2), "{operation}: {output:?}");
assert!(output.stdout.is_empty());
let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert_eq!(error["error"], "Unknown or output-only property 'Payload'.");
} else {
eprintln!("Skipping test - current context isn't elevated.");
}
}

#[cfg(windows)]
#[test]
fn reports_invalid_or_unreadable_stdin_as_json_errors() {
for (input, expected) in [
(b"{".as_slice(), "Invalid JSON input:"),
(b"".as_slice(), "Invalid JSON input:"),
(b"\xff".as_slice(), "Could not read JSON input from stdin:"),
] {
#[test_case(b"{".as_slice(), "Invalid JSON input:"; "invalid JSON input")]
#[test_case(b"".as_slice(), "Invalid JSON input:"; "empty JSON input")]
#[test_case(b"\xff".as_slice(), "Could not read JSON input from stdin:"; "unreadable JSON input")]
fn reports_invalid_or_unreadable_stdin_as_json_errors(input: &[u8], expected: &str) {
if is_elevated() {
let output = invoke_with_stdin("get", input);
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert!(error["error"].as_str().unwrap().starts_with(expected));
} else {
eprintln!("Skipping test - current context isn't elevated.");
}
}

#[test]
fn manifest_sends_operation_payloads_over_stdin() {
#[test_case("get"; "get operation")]
#[test_case("set"; "set operation")]
#[test_case("test"; "test operation")]
#[test_case("export"; "export operation")]
fn manifest_sends_operation_payloads_over_stdin(operation: &str) {
let manifest: serde_json::Value =
serde_json::from_str(include_str!("../csp.dsc.resource.json")).unwrap();
for operation in ["get", "set", "test", "export"] {
assert_eq!(manifest[operation]["input"], "stdin");
assert_eq!(
manifest[operation]["args"],
serde_json::json!([
operation,
{"resourceTypeArg": "--resource-type"},
"--input",
"-"
])
);
}
assert_eq!(manifest[operation]["input"], "stdin");
assert_eq!(
manifest[operation]["args"],
serde_json::json!([
operation,
{"resourceTypeArg": "--resource-type"},
"--input",
"-"
])
);
}
26 changes: 24 additions & 2 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ pub enum TraceFormat {
PassThrough,
}

impl Into<dsc_lib::settings::fields::TracingFormatField> for TraceFormat {
fn into(self) -> dsc_lib::settings::fields::TracingFormatField {
match self {
TraceFormat::Default => dsc_lib::settings::fields::TracingFormatField::Default,
TraceFormat::Plaintext => dsc_lib::settings::fields::TracingFormatField::Plaintext,
TraceFormat::Json => dsc_lib::settings::fields::TracingFormatField::Json,
// PassThrough is treated as Json for settings purposes
TraceFormat::PassThrough => dsc_lib::settings::fields::TracingFormatField::Json,
}
}
}

#[derive(Debug, Parser)]
#[clap(name = "dsc", version = env!("CARGO_PKG_VERSION"), about = t!("args.about").to_string(), long_about = None)]
pub struct Args {
Expand All @@ -56,8 +68,18 @@ pub struct Args {
pub trace_format: Option<TraceFormat>,
#[clap(short = 'p', long, help = t!("args.progressFormat").to_string(), value_enum)]
pub progress_format: Option<ProgressFormat>,
#[clap(long, help = t!("args.ignoreSettingsFile").to_string())]
pub ignore_settings_file: bool,
#[clap(long, help = t!("args.ignoreSettingsFile").to_string(), num_args=0..=1, default_missing_value="true", require_equals = true)]
pub ignore_settings_file: Option<bool>,
}

impl From<&Args> for dsc_lib::settings::sources::CommandLineData {
fn from(value: &Args) -> Self {
Self {
trace_level: value.trace_level.clone().map(|v| v.into()),
trace_format: value.trace_format.clone().map(|v| v.into()),
ignore_settings_file: value.ignore_settings_file.map(|v| v.into()),
}
}
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
Expand Down
31 changes: 24 additions & 7 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use args::{Args, SubCommand};
use clap::{CommandFactory, Parser};
use clap_complete::generate;
use dsc_lib::{progress::ProgressFormat, util::DSC_IGNORE_SETTINGS_FILE};
use dsc_lib::{progress::ProgressFormat, settings::{DscSettings, DscSettingsScope, global as global_settings, sources::EnvironmentData}};
use server::start_server;
use rust_i18n::{i18n, t};
use std::{env::set_var, io, process::ExitCode};
Expand Down Expand Up @@ -78,14 +78,31 @@ fn dsc_main() -> Result<(), ExitCode> {

let args = Args::parse();

// if args.ignore_settings_file is set, we create an env var called `DSC_IGNORE_SETTINGS_FILE` set to value 1
if args.ignore_settings_file {
unsafe {
set_var(DSC_IGNORE_SETTINGS_FILE, "1");
// Initialize with local settings; reload for global after tracing init
let mut settings = DscSettings::new_with_command_line((&args).into());
settings.load();

util::enable_tracing(settings.resolved());

let mut global_settings = global_settings::get_or_init_with_command_line((&args).into());
global_settings.load();
global_settings.resolve_all();
// if ignoring settings files is set from CLI, we create an env var called
// `DSC_IGNORE_SETTINGS_FILE` set to value 1 for recursive invocations
if global_settings.ignoring_settings_files()
&& global_settings.resolved().ignore_settings_file.scope == DscSettingsScope::CommandLine {
unsafe {
set_var(EnvironmentData::DSC_IGNORE_SETTINGS_FILE_ENV_VAR, "1");
}
}
// Set the DSC_TRACE_LEVEL environment variable for child processes
unsafe {
env::set_var(
EnvironmentData::DSC_TRACE_LEVEL_ENV_VAR,
global_settings.resolved().tracing.level.value.clone().to_string()
);
}

util::enable_tracing(args.trace_level.as_ref(), args.trace_format.as_ref());
drop(global_settings);

debug!("{}: {}", t!("main.usingDscVersion"), env!("CARGO_PKG_VERSION"));

Expand Down
Loading
Loading