Skip to content

Commit

Permalink
More coverage
Browse files Browse the repository at this point in the history
- Covered most of Client. Integration tests using Anthropic's service.
- Covered more of `message.rs`. Found an image Display bug.
- Switch coverage generation to `llvm_cov`. Tarpaulin is shit and can't run all tests at once.
  • Loading branch information
mdegans committed Sep 13, 2024
1 parent bc6f6e2 commit 4ae9de8
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 31 deletions.
25 changes: 15 additions & 10 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable, beta, nightly]

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
profile: minimal
override: true
components: llvm-tools-preview

- name: Cache cargo registry
uses: actions/cache@v2
Expand Down Expand Up @@ -57,19 +54,27 @@ jobs:
run: cargo test --all-features --verbose

# This should only happen on push to main. PRs should not upload coverage.
- name: Install tarpaulin
- name: Install llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
run: cargo install cargo-tarpaulin

- name: Run tarpaulin
- name: Install nextest
uses: taiki-e/install-action@nextest
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
run: cargo tarpaulin --out Xml --all-features

- name: Write API key to api.key
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
run: echo ${{ secrets.ANTHROPIC_API_KEY }} > api.key

- name: Collect coverage data (including ignored tests)
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
run: cargo llvm-cov nextest --all-features --run-ignored all --lcov --output-path lcov.info

- name: Upload coverage to Codecov
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'
uses: codecov/codecov-action@v2
with:
files: ./cobertura.xml
files: lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/target
Cargo.lock
.vscode
cobertura.xml
cobertura.xml
api.key
lcov.info
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ println!("{}", message);
- [x] Tool use,
- [x] Streaming responses
- [x] Message responses
- [x] Zero-copy where possible
- [x] Image support with or without the `image` crate
- [x] Markdown formatting of messages, including images
- [x] Prompt caching support
- [x] Custom request and endpoint support
- [ ] Zero-copy serde - Coming soon!
- [ ] Amazon Bedrock support
- [ ] Vertex AI support

Expand Down
83 changes: 83 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,12 @@ pub(crate) struct AnthropicErrorWrapper {

#[cfg(test)]
mod tests {
use futures::TryStreamExt;

use super::*;

// Test error deserialization.

#[test]
fn test_anthropic_error_deserialize() {
const INVALID_REQUEST: &str =
Expand Down Expand Up @@ -455,4 +459,83 @@ mod tests {
}
);
}

// Test the Client

use crate::{request::message::Role, Request};

const CRATE_ROOT: &str = env!("CARGO_MANIFEST_DIR");

// Note: This is a real key but it's been disabled. As is warned in the
// docs above, do not use a string literal for a real key. There is no
// TryFrom<&'static str> for Key for this reason.
const FAKE_API_KEY: &str = "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";

// Error message for when the API key is not found.
const NO_API_KEY: &str = "API key not found. Create a file named `api.key` in the crate root with your API key.";

// Load the API key from the `api.key` file in the crate root.
fn load_api_key() -> Option<String> {
use std::fs::File;
use std::io::Read;
use std::path::Path;

let mut file =
File::open(Path::new(CRATE_ROOT).join("api.key")).ok()?;
let mut key = String::new();
file.read_to_string(&mut key).unwrap();
Some(key.trim().to_string())
}

#[test]
fn test_client_new() {
let client = Client::new(FAKE_API_KEY.to_string()).unwrap();
assert_eq!(client.key.to_string(), FAKE_API_KEY);

// Apparently there isn't a way to check if the headers have been set
// on the client. Making a request returns a builder but the headers
// are not exposed.
}

#[tokio::test]
#[ignore = "This test requires a real API key."]
async fn test_client_message() {
let key = load_api_key().expect(NO_API_KEY);
let client = Client::new(key).unwrap();

let message = client
.message(Request::default().messages([(
Role::User,
"Emit just the \"🙏\" emoji, please.",
)]))
.await
.unwrap();

assert_eq!(message.message.role, Role::Assistant);
assert!(message.to_string().contains("🙏"));
}

#[tokio::test]
#[ignore = "This test requires a real API key."]
async fn test_client_stream() {
let key = load_api_key().expect(NO_API_KEY);
let client = Client::new(key).unwrap();

let stream = client
.stream(Request::default().messages([(
Role::User,
"Emit just the \"🙏\" emoji, please.",
)]))
.await
.unwrap();

let msg: String = stream
.filter_rate_limit()
.text()
.try_collect()
.await
.unwrap();

assert!(msg.contains("🙏"));
}
}
34 changes: 31 additions & 3 deletions src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ pub type Arr = [u8; LEN];
///
/// [`key::LEN`]: LEN
#[derive(Debug, thiserror::Error)]
#[error("Invalid key length: {0} (expected {LEN})")]
pub struct InvalidKeyLength(usize);
#[error("Invalid key length: {actual} (expected {LEN})")]
pub struct InvalidKeyLength {
/// The incorrect actual length of the key.
pub actual: usize,
}

/// Stores an Anthropic API key securely. The API key is encrypted in memory.
/// The object features a [`Display`] implementation that can be used to write
Expand Down Expand Up @@ -60,8 +63,9 @@ impl TryFrom<Vec<u8>> for Key {
fn try_from(mut v: Vec<u8>) -> Result<Self, Self::Error> {
let mut arr: Arr = [0; LEN];
if v.len() != LEN {
let actual = v.len();
v.zeroize();
return Err(InvalidKeyLength(v.len()));
return Err(InvalidKeyLength { actual });
}

arr.copy_from_slice(&v);
Expand Down Expand Up @@ -105,3 +109,27 @@ impl std::fmt::Display for Key {
write!(f, "{}", key_str)
}
}

#[cfg(test)]
mod tests {
use super::*;

// Note: This is a real key but it's been disabled. As is warned in the
// docs above, do not use a string literal for a real key. There is no
// TryFrom<&'static str> for Key for this reason.
const API_KEY: &str = "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";

#[test]
fn test_key() {
let key = Key::try_from(API_KEY.to_string()).unwrap();
let key_str = key.to_string();
assert_eq!(key_str, API_KEY);
}

#[test]
fn test_invalid_key_length() {
let key = "test_key".to_string();
let err = Key::try_from(key).unwrap_err();
assert_eq!(err.to_string(), "Invalid key length: 8 (expected 108)");
}
}
32 changes: 31 additions & 1 deletion src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl PartialEq<str> for Markdown {
pub trait ToMarkdown {
/// Render the type to a [`Markdown`] string with [`DEFAULT_OPTIONS`].
fn markdown(&self) -> Markdown {
self.markdown_custom(DEFAULT_OPTIONS_REF)
self.markdown_events().into()
}

/// Render the type to a [`Markdown`] string with custom [`Options`].
Expand Down Expand Up @@ -232,6 +232,8 @@ impl Default for Options {

#[cfg(test)]
mod tests {
use crate::request::{message::Role, Message};

use super::*;

use std::borrow::Borrow;
Expand All @@ -246,6 +248,21 @@ mod tests {
assert!(options == options2);
}

#[test]
fn test_options_from_pulldown() {
let inner = pulldown_cmark::Options::empty();
let options: Options = inner.into();
assert_eq!(options.inner, inner);
}

#[test]
fn test_options_verbose() {
let options = Options::verbose();
assert!(options.tool_use);
assert!(options.tool_results);
assert!(options.system);
}

#[test]
fn test_markdown() {
let expected = "Hello, **world**!";
Expand All @@ -257,4 +274,17 @@ mod tests {
let markdown: String = markdown.into();
assert_eq!(markdown, expected);
}

#[test]
fn test_message_markdown() {
let message = Message {
role: Role::User,
content: "Hello, **world**!".into(),
};

assert_eq!(
message.markdown().as_ref(),
"### User\n\nHello, **world**!"
);
}
}
4 changes: 4 additions & 0 deletions src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,10 @@ mod tests {
#[test]
#[cfg(feature = "prompt-caching")]
fn test_cache() {
// Test with nothing to cache. This should be a no-op.
let request = Request::default().cache();
assert!(request == Request::default());

// Test with no system prompt or messages that the call to cache affects
// the tools.
let request = Request::default().add_tool(Tool {
Expand Down
Loading

0 comments on commit 4ae9de8

Please sign in to comment.