Skip to content

Commit

Permalink
Bootstrap test-proxy service (#1956)
Browse files Browse the repository at this point in the history
* Bootstrap test-proxy service

Relates to #1874 but does not complete it.

* Elide recorded tests and supporting types for wasm32

* Resolve PR feedback
  • Loading branch information
heaths authored Dec 12, 2024
1 parent 6cf4cc9 commit b6bbc74
Show file tree
Hide file tree
Showing 10 changed files with 556 additions and 12 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# Build output.
target/

# Track only workspace Cargo.lock
Cargo.lock
!/Cargo.lock

# User secrets.
.env

# Test artifacts.
.assets/

# Editor user customizations.
.vscode/launch.json
.idea/
Expand Down
6 changes: 6 additions & 0 deletions Cargo.lock

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

12 changes: 11 additions & 1 deletion sdk/core/azure_core_test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ edition.workspace = true
rust-version.workspace = true

[dependencies]
async-trait.workspace = true
azure_core = { workspace = true, features = ["test"] }
azure_core_test_macros.workspace = true
serde.workspace = true
tracing.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = ["io-util", "process", "sync"] }

[dev-dependencies]
tokio.workspace = true
clap.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["signal"] }
83 changes: 83 additions & 0 deletions sdk/core/azure_core_test/examples/test_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use clap::Parser;

#[cfg(not(target_arch = "wasm32"))]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use azure_core_test::proxy;

// cspell:ignore ECANCELED ECHILD
const ECANCELED: i32 = 4;
const ECHILD: i32 = 5;

let args = Args::parse();

tracing_subscriber::fmt()
// Default trace level based on command line arguments.
.with_max_level(args.trace_level())
// RUST_LOG environment variable can override trace level.
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();

let mut proxy = proxy::start(env!("CARGO_MANIFEST_DIR"), Some(args.into())).await?;

let code = tokio::select! {
_ = tokio::signal::ctrl_c() => {
// Try to shutdown the test-proxy.
proxy.stop().await?;

ECANCELED
},
v = proxy.wait() => {
let code = v.map_or_else(|_| ECHILD, |v| v.code().unwrap_or_default());
println!("test-proxy exited with status code {code}");

code
},
};

if code != 0 {
std::process::exit(code);
}

Ok(())
}

#[derive(Debug, Parser)]
#[command(about = "Starts the Test-Proxy service", version)]
struct Args {
/// Allow insecure upstream SSL certs.
#[arg(long)]
insecure: bool,

/// Enable verbose logging.
#[arg(short, long)]
verbose: bool,
}

#[cfg(not(target_arch = "wasm32"))]
impl Args {
fn trace_level(&self) -> tracing::level_filters::LevelFilter {
if self.verbose {
return tracing::level_filters::LevelFilter::DEBUG;
}
tracing::level_filters::LevelFilter::INFO
}
}

#[cfg(not(target_arch = "wasm32"))]
impl From<Args> for azure_core_test::proxy::ProxyOptions {
fn from(args: Args) -> Self {
Self {
insecure: args.insecure,
}
}
}

#[cfg(target_arch = "wasm32")]
fn main() {
let _ = Args::parse();
println!("wasm32 target architecture not supported");
}
84 changes: 74 additions & 10 deletions sdk/core/azure_core_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@

#![doc = include_str!("../README.md")]

/// Live recording and playing back of client library tests.
pub mod recorded {
pub use azure_core_test_macros::test;
}
#[cfg(not(target_arch = "wasm32"))]
pub mod proxy;
#[cfg(not(target_arch = "wasm32"))]
pub mod recorded;
mod sanitizers;
mod transport;

pub use azure_core::test::TestMode;
use azure_core::{ClientOptions, TransportOptions};
pub use sanitizers::*;
use std::{
path::{Path, PathBuf},
sync::Arc,
};

const SPAN_TARGET: &str = "test-proxy";

/// Context information required by recorded client library tests.
///
Expand All @@ -17,7 +27,7 @@ pub use azure_core::test::TestMode;
#[derive(Clone, Debug)]
pub struct TestContext {
test_mode: TestMode,
crate_dir: &'static str,
crate_dir: &'static Path,
test_name: &'static str,
}

Expand All @@ -27,27 +37,79 @@ impl TestContext {
pub fn new(test_mode: TestMode, crate_dir: &'static str, test_name: &'static str) -> Self {
Self {
test_mode,
crate_dir,
crate_dir: Path::new(crate_dir),
test_name,
}
}

/// Gets the current [`TestMode`].
pub fn test_mode(&self) -> TestMode {
self.test_mode
/// Instruments the [`ClientOptions`] to support recording and playing back of session records.
///
/// # Examples
///
/// ```no_run
/// use azure_core_test::{recorded, TestContext};
///
/// # struct MyClient;
/// # #[derive(Default)]
/// # struct MyClientOptions { client_options: azure_core::ClientOptions };
/// # impl MyClient {
/// # fn new(endpoint: impl AsRef<str>, options: Option<MyClientOptions>) -> Self { todo!() }
/// # async fn invoke(&self) -> azure_core::Result<()> { todo!() }
/// # }
/// #[recorded::test]
/// async fn test_invoke(ctx: TestContext) -> azure_core::Result<()> {
/// let mut options = MyClientOptions::default();
/// ctx.instrument(&mut options.client_options);
///
/// let client = MyClient::new("https://azure.net", Some(options));
/// client.invoke().await
/// }
/// ```
pub fn instrument(&self, options: &mut ClientOptions) {
let transport = options.transport.clone().unwrap_or_default();
options.transport = Some(TransportOptions::new_custom_policy(Arc::new(
transport::ProxyTransportPolicy {
inner: transport,
mode: self.test_mode,
},
)));
}

/// Gets the root directory of the crate under test.
pub fn crate_dir(&self) -> &'static str {
pub fn crate_dir(&self) -> &'static Path {
self.crate_dir
}

/// Gets the test data directory under [`Self::crate_dir`].
pub fn test_data_dir(&self) -> PathBuf {
self.crate_dir.join("tests/data")
}

/// Gets the current [`TestMode`].
pub fn test_mode(&self) -> TestMode {
self.test_mode
}

/// Gets the current test function name.
pub fn test_name(&self) -> &'static str {
self.test_name
}
}

#[cfg(not(target_arch = "wasm32"))]
fn find_ancestor(dir: impl AsRef<Path>, name: &str) -> azure_core::Result<PathBuf> {
for dir in dir.as_ref().ancestors() {
let path = dir.join(name);
if path.exists() {
return Ok(path);
}
}
Err(azure_core::Error::new::<std::io::Error>(
azure_core::error::ErrorKind::Io,
std::io::ErrorKind::NotFound.into(),
))
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -62,6 +124,8 @@ mod tests {
assert_eq!(ctx.test_mode(), TestMode::Playback);
assert!(ctx
.crate_dir()
.to_str()
.unwrap()
.replace("\\", "/")
.ends_with("sdk/core/azure_core_test"));
assert_eq!(ctx.test_name(), "test_content_new");
Expand Down
Loading

0 comments on commit b6bbc74

Please sign in to comment.