Skip to content

Commit

Permalink
Implement recorded::test macro for recorded tests (#1926)
Browse files Browse the repository at this point in the history
* Rename typespec_derive to typespec_macros

Will be more consistent with upcoming azure_core_macros, and may not contain *just* derive macros anyway. Will keep feature as "derive", though, for derive macros.

* Remove 128-bit number functions for Cosmos

* Implement `#[recorded]` attribute macro

* Allow live-only tests with no parameters

* Replace test_e2e feature with `recorded` attribute macro

* Refactor so that tests need only import azure_core_tests

Also makes the attribute `#[recorded::test]` which, IMO, looks a bit better.

* Fix build breaks

We may need to set env vars for our Windows agents to find OpenSSL, which does appear to be installed. See #1746. For now, I'm removing `--all-features` from this PR since that issue is already tracking adding them separately.
  • Loading branch information
heaths authored Nov 25, 2024
1 parent 341a094 commit 8518ea7
Show file tree
Hide file tree
Showing 43 changed files with 572 additions and 134 deletions.
3 changes: 2 additions & 1 deletion .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"datetime",
"devicecode",
"docsrs",
"doctest",
"downcasted",
"downcasting",
"entra",
Expand Down Expand Up @@ -128,4 +129,4 @@
]
}
]
}
}
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- [Rust toolchain](https://www.rust-lang.org/tools/install)

When you run `cargo build`, toolchain version [1.76](https://releases.rs/docs/1.76.0/) and necessary components will be installed automatically.
When you run `cargo build`, toolchain version [1.80](https://releases.rs/docs/1.80.0/) and necessary components will be installed automatically.

- (Recommended) If you use [Visual Studio Code](https://code.visualstudio.com), install recommended extensions to improve your development experience.

Expand Down
19 changes: 13 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ resolver = "2"
members = [
"sdk/typespec",
"sdk/typespec/typespec_client_core",
"sdk/typespec/typespec_derive",
"sdk/typespec/typespec_macros",
"sdk/core/azure_core",
"sdk/core/azure_core_amqp",
"sdk/core/azure_core_test",
"sdk/core/azure_core_test_macros",
"sdk/cosmos/azure_data_cosmos",
"sdk/identity/azure_identity",
"sdk/eventhubs/azure_messaging_eventhubs",
Expand All @@ -19,7 +21,7 @@ authors = ["Microsoft"]
edition = "2021"
license = "MIT"
repository = "https://github.com/azure/azure-sdk-for-rust"
rust-version = "1.76"
rust-version = "1.80"

[workspace.dependencies.typespec]
path = "sdk/typespec"
Expand All @@ -29,15 +31,20 @@ default-features = false
path = "sdk/typespec/typespec_client_core"
default-features = false

[workspace.dependencies.typespec_derive]
path = "sdk/typespec/typespec_derive"
[workspace.dependencies.typespec_macros]
path = "sdk/typespec/typespec_macros"

[workspace.dependencies.azure_core]
path = "sdk/core/azure_core"

[workspace.dependencies.azure_core_amqp]
path = "sdk/core/azure_core_amqp"

[workspace.dependencies.azure_core_test]
path = "sdk/core/azure_core_test"

[workspace.dependencies.azure_core_test_macros]
path = "sdk/core/azure_core_test_macros"

[workspace.dependencies.azure_identity]
path = "sdk/identity/azure_identity"
Expand Down Expand Up @@ -74,6 +81,7 @@ paste = "1.0"
pin-project = "1.0"
proc-macro2 = "1.0.86"
quick-xml = { version = "0.31", features = ["serialize", "serde-types"] }
quote = "1.0.37"
rand = "0.8"
reqwest = { version = "0.12", features = [
"json",
Expand All @@ -87,8 +95,7 @@ serde_json = "1.0"
serde_test = "1"
serial_test = "3.0"
sha2 = { version = "0.10" }
syn = { version = "2.0.74", features = ["full"] }
quote = "1.0.36"
syn = { version = "2.0.87", features = ["full"] }
thiserror = "1.0"
time = { version = "0.3.10", features = [
"serde-well-known",
Expand Down
1 change: 1 addition & 0 deletions eng/scripts/Test-Packages.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ foreach ($package in $packagesToTest) {
if ($FunctionalTests) {
$targets += "--bins"
$targets += "--examples"
$targets += "--tests"
$targets += "--benches"
}

Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[toolchain]
channel = "1.76.0"
channel = "1.80.0"
components = [
"rustc",
"rustfmt",
Expand Down
19 changes: 10 additions & 9 deletions sdk/core/azure_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,25 @@ thiserror.workspace = true

[features]
default = []
azurite_workaround = []
hmac_openssl = ["dep:openssl"]
hmac_rust = ["dep:sha2", "dep:hmac"]
reqwest = ["typespec_client_core/reqwest"]
reqwest_gzip = ["typespec_client_core/reqwest_gzip"]
reqwest_rustls = ["typespec_client_core/reqwest_rustls"]
hmac_rust = ["dep:sha2", "dep:hmac"]
hmac_openssl = ["dep:openssl"]
azurite_workaround = []
xml = ["typespec_client_core/xml"]
test = []
tokio_fs = ["typespec_client_core/tokio_fs"]
tokio_sleep = ["typespec_client_core/tokio_sleep"]
xml = ["typespec_client_core/xml"]

[package.metadata.docs.rs]
features = [
"xml",
"tokio_fs",
"reqwest",
"hmac_openssl",
"hmac_rust",
"reqwest_gzip",
"reqwest_rustls",
"hmac_rust",
"hmac_openssl",
"reqwest",
"test",
"tokio_fs",
"xml",
]
3 changes: 3 additions & 0 deletions sdk/core/azure_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub mod headers;
pub mod lro;
pub mod request_options;

#[cfg(feature = "test")]
pub mod test;

pub mod tokio;

pub use constants::*;
Expand Down
63 changes: 63 additions & 0 deletions sdk/core/azure_core/src/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//! Shared utilities for testing client libraries built on `azure_core`.
//!
//! For a comprehensive suite of utilities for testing client libraries built on `azure_core`,
//! read documentation for the `azure_core_test` crate.
use crate::error::{Error, ErrorKind};
use std::{fmt, str::FromStr};

/// Whether to test client methods by playing back recordings, recording live sessions, or executing live sessions without recording.
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum TestMode {
/// Test client methods by playing back recordings.
#[default]
Playback,

/// Record live sessions.
Record,

/// Execute live sessions without recording.
Live,
}

impl TestMode {
/// Gets the `TestMode` from the `AZURE_TEST_MODE` environment variable or returns the default if undefined.
pub fn current() -> typespec::Result<Self> {
std::env::var("AZURE_TEST_MODE").map_or_else(|_| Ok(TestMode::default()), |v| v.parse())
}
}

impl fmt::Debug for TestMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.into())
}
}

impl From<&TestMode> for &'static str {
fn from(mode: &TestMode) -> Self {
match mode {
TestMode::Playback => "playback",
TestMode::Record => "record",
TestMode::Live => "live",
}
}
}

impl FromStr for TestMode {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"playback" => Ok(Self::Playback),
"record" => Ok(Self::Record),
"live" => Ok(Self::Live),
_ => Err(Error::message(
ErrorKind::DataConversion,
"expected 'playback', 'record', or 'live'",
)),
}
}
}
21 changes: 21 additions & 0 deletions sdk/core/azure_core_test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "azure_core_test"
version = "0.1.0"
description = "Utilities for testing client libraries built on azure_core."
readme = "README.md"
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage = "https://github.com/azure/azure-sdk-for-rust"
documentation = "https://docs.rs/azure_core"
keywords = ["sdk", "azure", "rest", "iot", "cloud"]
categories = ["development-tools::testing"]
edition.workspace = true
rust-version.workspace = true

[dependencies]
azure_core = { workspace = true, features = ["test"] }
azure_core_test_macros.workspace = true

[dev-dependencies]
tokio.workspace = true
25 changes: 25 additions & 0 deletions sdk/core/azure_core_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Azure client library test utilities

The types and functions in this crate help test client libraries built on `azure_core`.

## Client methods

To test client methods using our [Test Proxy], you can attribute both synchronous and asynchronous (recommend) tests
using the `#[recorded::test]` attribute:

```rust
use azure_core_test::{recorded, TestContext};
use azure_core::Result;

#[recorded::test]
async fn get_secret(ctx: TestContext) -> Result<()> {
todo!()
}
```

The `TestContext` parameter is required unless your test function is attribute as `#[recorded::test(live)]` (live-only),
in which case it is optional. You can name the parameter whatever you want.
The `TestContext` parameter is used to initialize an HTTP client to play back or record tests
and provides other information to test functions that may be useful.

[Test Proxy]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md
54 changes: 54 additions & 0 deletions sdk/core/azure_core_test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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

/// Live recording and playing back of client library tests.
pub mod recorded {
pub use azure_core_test_macros::test;
}

pub use azure_core::test::TestMode;

/// Context information required by recorded client library tests.
///
/// This context is required for any recorded tests not attributed as `#[recorded::test(live)]`
/// to setup up the HTTP client to record or play back session records.
#[derive(Clone, Debug)]
pub struct TestContext {
test_mode: TestMode,
test_name: &'static str,
}

impl TestContext {
/// Not intended for use outside the `azure_core` crates.
#[doc(hidden)]
pub fn new(test_mode: TestMode, test_name: &'static str) -> Self {
Self {
test_mode,
test_name,
}
}

/// 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(test)]
mod tests {
use super::*;

#[test]
fn test_content_new() {
let ctx = TestContext::new(TestMode::default(), "test_content_new");
assert_eq!(ctx.test_mode(), TestMode::Playback);
assert_eq!(ctx.test_name(), "test_content_new");
}
}
27 changes: 27 additions & 0 deletions sdk/core/azure_core_test_macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "azure_core_test_macros"
version = "0.1.0"
description = "Procedural macros for testing client libraries built on azure_core."
readme = "README.md"
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage = "https://github.com/azure/azure-sdk-for-rust"
documentation = "https://docs.rs/azure_core"
keywords = ["azure", "cloud", "iot", "rest", "sdk"]
categories = ["development-tools"]
edition.workspace = true
rust-version.workspace = true

[lib]
proc-macro = true

[dependencies]
azure_core = { workspace = true, features = ["test"] }
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true

[dev-dependencies]
azure_core_test.workspace = true
tokio.workspace = true
25 changes: 25 additions & 0 deletions sdk/core/azure_core_test_macros/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Azure client library test macros

Macros for testing client libraries built on `azure_core`.

## Client methods

To test client methods using our [Test Proxy], you can attribute both synchronous and asynchronous (recommend) tests
using the `#[recorded::test]` attribute:

```rust
use azure_core_test::{recorded, TestContext};
use azure_core::Result;

#[recorded::test]
async fn get_secret(ctx: TestContext) -> Result<()> {
todo!()
}
```

The `TestContext` parameter is required unless your test function is attribute as `#[recorded::test(live)]` (live-only),
in which case it is optional. You can name the parameter whatever you want.
The `TestContext` parameter is used to initialize an HTTP client to play back or record tests
and provides other information to test functions that may be useful.

[Test Proxy]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md
17 changes: 17 additions & 0 deletions sdk/core/azure_core_test_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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

mod test;

use proc_macro::TokenStream;

/// Attribute client library tests to play back recordings, record sessions, or execute tests without recording.
///
/// Read documentation for `azure_core_test` for more information and examples.
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
test::parse_test(attr.into(), item.into())
.map_or_else(|e| e.into_compile_error().into(), |v| v.into())
}
Loading

0 comments on commit 8518ea7

Please sign in to comment.