From 1d95ad76a1df4063690f90b68bf8e8da5518f874 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 6 Sep 2023 16:41:01 -0600 Subject: [PATCH] feat: delete query (#2) --- Cargo.lock | 23 +++ Makefile.toml | 4 +- example/Cargo.toml | 3 + example/src/entities/person/model.rs | 194 +++++++++---------------- example/src/entities/person/queries.rs | 66 ++++++++- example/src/main.rs | 21 ++- scyllax-macros/src/entity.rs | 35 ++++- scyllax-macros/src/lib.rs | 14 ++ scyllax-macros/src/queries/delete.rs | 65 +++++++++ scyllax-macros/src/queries/mod.rs | 1 + scyllax-macros/src/queries/select.rs | 4 +- scyllax-macros/src/queries/upsert.rs | 2 +- src/executor.rs | 14 +- src/lib.rs | 15 ++ src/prelude.rs | 4 +- src/util.rs | 7 + 16 files changed, 330 insertions(+), 142 deletions(-) create mode 100644 scyllax-macros/src/queries/delete.rs diff --git a/Cargo.lock b/Cargo.lock index b197af4..fcd415c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "either" version = "1.9.0" @@ -224,6 +230,7 @@ name = "example" version = "0.1.1-alpha" dependencies = [ "anyhow", + "pretty_assertions", "scylla", "scyllax", "tokio", @@ -671,6 +678,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1386,3 +1403,9 @@ checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Makefile.toml b/Makefile.toml index 7951212..b5553c6 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -69,12 +69,12 @@ args = ["nextest", "run", "--workspace"] [tasks.cov] command = "cargo" env = { "RUN_MODE" = "test" } -args = ["llvm-cov", "nextest", "${@}"] +args = ["llvm-cov", "nextest", "--workspace", "--exclude", "scyllax-macros", "${@}"] [tasks.cov-ci] command = "cargo" env = { "RUN_MODE" = "ci" } -args = ["llvm-cov", "nextest", "--lcov", "--output-path", "lcov.info"] +args = ["llvm-cov", "nextest", "--workspace", "--exclude", "scyllax-macros", "--lcov", "--output-path", "lcov.info"] [tasks.integration] env = { "RUN_MODE" = "test", "RUST_LOG" = "info", "RUST_BACKTRACE" = 1 } diff --git a/example/Cargo.toml b/example/Cargo.toml index a8fe6c7..e8a8561 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -17,3 +17,6 @@ tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = "1" diff --git a/example/src/entities/person/model.rs b/example/src/entities/person/model.rs index b6eddfa..5629900 100644 --- a/example/src/entities/person/model.rs +++ b/example/src/entities/person/model.rs @@ -16,136 +16,82 @@ pub struct PersonEntity { pub created_at: i64, } -// struct UpsertPerson { -// pub id: uuid::Uuid, -// pub email: MaybeUnset, -// pub age: MaybeUnset>, -// pub first_name: MaybeUnset, -// } +#[cfg(test)] +mod test { + use super::PersonEntity; + use crate::entities::person::model::UpsertPerson; + use pretty_assertions::assert_eq; + use scyllax::prelude::*; -// TODO: macroify -// TODO: use insert if every field is a PK -// #[scyllax::async_trait] -// impl UpsertQuery for UpsertPerson { -// fn query( -// &self, -// ) -> Result<(String, scylla::frame::value::SerializedValues), BuildUpsertQueryError> { -// let mut query = String::from("update person set "); -// let mut variables = scylla::frame::value::SerializedValues::new(); + #[test] + fn test_pks() { + assert_eq!(PersonEntity::pks(), vec!["id".to_string()]); + } -// if let MaybeUnset::Set(first_name) = &self.first_name { -// query.push_str(&format!(r##"first_name = ?, "##)); + #[test] + fn test_keys() { + assert_eq!( + PersonEntity::keys(), + vec![ + "id".to_string(), + "email".to_string(), + "age".to_string(), + "\"createdAt\"".to_string() + ] + ); + } -// match variables.add_value(first_name) { -// Ok(_) => (), -// Err(SerializeValuesError::TooManyValues) => { -// return Err(BuildUpsertQueryError::TooManyValues { -// field: "first_name".to_string(), -// }) -// } -// Err(SerializeValuesError::MixingNamedAndNotNamedValues) => { -// return Err(BuildUpsertQueryError::MixingNamedAndNotNamedValues) -// } -// Err(SerializeValuesError::ValueTooBig(_)) => { -// return Err(BuildUpsertQueryError::ValueTooBig { -// field: "first_name".to_string(), -// }) -// } -// Err(SerializeValuesError::ParseError) => { -// return Err(BuildUpsertQueryError::ParseError { -// field: "first_name".to_string(), -// }) -// } -// } -// } + #[test] + fn test_upsert_v1() { + let upsert = UpsertPerson { + id: v1_uuid(), + email: MaybeUnset::Set("foo21@scyllax.local".to_string()), + age: MaybeUnset::Set(Some(21)), + created_at: MaybeUnset::Unset, + }; -// if let MaybeUnset::Set(email) = &self.email { -// query.push_str(r##"email = ?, "##); -// match variables.add_value(email) { -// Ok(_) => (), -// Err(SerializeValuesError::TooManyValues) => { -// return Err(BuildUpsertQueryError::TooManyValues { -// field: "email".to_string(), -// }) -// } -// Err(SerializeValuesError::MixingNamedAndNotNamedValues) => { -// return Err(BuildUpsertQueryError::MixingNamedAndNotNamedValues) -// } -// Err(SerializeValuesError::ValueTooBig(_)) => { -// return Err(BuildUpsertQueryError::ValueTooBig { -// field: "email".to_string(), -// }) -// } -// Err(SerializeValuesError::ParseError) => { -// return Err(BuildUpsertQueryError::ParseError { -// field: "email".to_string(), -// }) -// } -// } -// } + let (query, values) = upsert.query().expect("failed to parse into query"); -// if let MaybeUnset::Set(age) = &self.age { -// // age is also optional, so we have to unwrap it -// if let Some(age) = age { -// query.push_str("age = ?, "); -// match variables.add_value(age) { -// Ok(_) => (), -// Err(SerializeValuesError::TooManyValues) => { -// return Err(BuildUpsertQueryError::TooManyValues { -// field: "age".to_string(), -// }) -// } -// Err(SerializeValuesError::MixingNamedAndNotNamedValues) => { -// return Err(BuildUpsertQueryError::MixingNamedAndNotNamedValues) -// } -// Err(SerializeValuesError::ValueTooBig(_)) => { -// return Err(BuildUpsertQueryError::ValueTooBig { -// field: "age".to_string(), -// }) -// } -// Err(SerializeValuesError::ParseError) => { -// return Err(BuildUpsertQueryError::ParseError { -// field: "age".to_string(), -// }) -// } -// } -// } -// } + assert_eq!( + query, + r#"update "person" set "email" = ?, "age" = ? where "id" = ?;"# + ); -// query.pop(); -// query.pop(); -// query.push_str(" where id = ?;"); -// match variables.add_value(&self.id) { -// Ok(_) => (), -// Err(SerializeValuesError::TooManyValues) => { -// return Err(BuildUpsertQueryError::TooManyValues { -// field: "id".to_string(), -// }) -// } -// Err(SerializeValuesError::MixingNamedAndNotNamedValues) => { -// return Err(BuildUpsertQueryError::MixingNamedAndNotNamedValues) -// } -// Err(SerializeValuesError::ValueTooBig(_)) => { -// return Err(BuildUpsertQueryError::ValueTooBig { -// field: "id".to_string(), -// }) -// } -// Err(SerializeValuesError::ParseError) => { -// return Err(BuildUpsertQueryError::ParseError { -// field: "id".to_string(), -// }) -// } -// } + let mut result_values = SerializedValues::new(); + result_values + .add_value(&upsert.email) + .expect("failed to add value"); + result_values + .add_value(&upsert.age) + .expect("failed to add value"); + result_values + .add_value(&upsert.id) + .expect("failed to add value"); -// Ok((query, variables)) -// } + assert_eq!(values, result_values); + } -// async fn execute( -// self, -// db: &scyllax::Executor, -// ) -> anyhow::Result { -// let (query, values) = self.query()?; + #[test] + fn test_upsert_v2() { + let upsert = UpsertPerson { + id: v1_uuid(), + email: MaybeUnset::Set("foo21@scyllax.local".to_string()), + age: MaybeUnset::Unset, + created_at: MaybeUnset::Unset, + }; -// db.session.execute(query, values).await.map_err(|e| e.into()) -// } -// } + let (query, values) = upsert.query().expect("failed to parse into query"); + + assert_eq!(query, r#"update "person" set "email" = ? where "id" = ?;"#); + + let mut result_values = SerializedValues::new(); + result_values + .add_value(&upsert.email) + .expect("failed to add value"); + result_values + .add_value(&upsert.id) + .expect("failed to add value"); + + assert_eq!(values, result_values); + } +} diff --git a/example/src/entities/person/queries.rs b/example/src/entities/person/queries.rs index 764f0af..148a46d 100644 --- a/example/src/entities/person/queries.rs +++ b/example/src/entities/person/queries.rs @@ -1,10 +1,13 @@ -use scyllax::prelude::*; +use scyllax::{delete_query, prelude::*}; use uuid::Uuid; /// Load all queries for this entity #[tracing::instrument(skip(db))] pub async fn load(db: &mut Executor) -> anyhow::Result<()> { let _ = GetPersonById::prepare(db).await; + let _ = GetPeopleByIds::prepare(db).await; + let _ = GetPersonByEmail::prepare(db).await; + let _ = DeletePersonById::prepare(db).await; Ok(()) } @@ -40,3 +43,64 @@ pub struct GetPersonByEmail { /// The email address of the [`super::model::PersonEntity`] to get pub email: String, } + +/// Get a [`super::model::PersonEntity`] by its [`uuid::Uuid`] +#[delete_query( + query = "delete from person where id = ?", + entity_type = "super::model::PersonEntity" +)] +pub struct DeletePersonById { + /// The [`uuid::Uuid`] of the [`super::model::PersonEntity`] to get + pub id: Uuid, +} + +#[cfg(test)] +mod test { + use super::*; + use scyllax::prelude::*; + + #[test] + fn test_get_person_by_id() { + let _query = GetPersonById { id: v1_uuid() }; + + assert_eq!( + GetPersonById::query(), + r#"select id, email, age, "createdAt" from person where id = ? limit 1"# + ); + } + + #[test] + fn test_get_people_by_ids() { + let _query = GetPeopleByIds { + ids: vec![v1_uuid(), v1_uuid()], + limit: 10, + }; + + assert_eq!( + GetPeopleByIds::query(), + r#"select id, email, age, "createdAt" from person where id in ? limit ?"# + ); + } + + #[test] + fn test_get_person_by_email() { + let _query = GetPersonByEmail { + email: "foo@scyllax.com".to_string(), + }; + + assert_eq!( + GetPersonByEmail::query(), + r#"select id, email, age, "createdAt" from person_by_email where email = ? limit 1"# + ); + } + + #[test] + fn test_delete_person_by_id() { + let _query = DeletePersonById { id: v1_uuid() }; + + assert_eq!( + DeletePersonById::query(), + r#"delete from person where id = ?"# + ); + } +} diff --git a/example/src/main.rs b/example/src/main.rs index 79fe1d6..9738e0a 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,7 +1,7 @@ //! Example use entities::person::{ model::UpsertPerson, - queries::{GetPeopleByIds, GetPersonByEmail, GetPersonById}, + queries::{load, DeletePersonById, GetPeopleByIds, GetPersonByEmail, GetPersonById}, }; use scyllax::prelude::*; use scyllax::{executor::create_session, util::v1_uuid}; @@ -22,7 +22,9 @@ async fn main() -> anyhow::Result<()> { let default_keyspace = std::env::var("SCYLLA_DEFAULT_KEYSPACE").ok(); let session = create_session(known_nodes, default_keyspace).await?; - let executor = Executor::with_session(session); + let mut executor = Executor::with_session(session); + + load(&mut executor).await?; let query = GetPersonByEmail { email: "foo11@scyllax.local".to_string(), @@ -31,14 +33,14 @@ async fn main() -> anyhow::Result<()> { .execute_select(query) .await? .expect("person not found"); - tracing::debug!("query 1: {:?}", res_one); + tracing::info!("GetPersonByEmail returned: {:?}", res_one); let query = GetPersonById { id: res_one.id }; let res_two = executor .execute_select(query) .await? .expect("person not found"); - tracing::debug!("query 2: {:?}", res_two); + tracing::info!("GetPersonById returned: {:?}", res_two); assert_eq!(res_one, res_two); let ids = [ @@ -53,16 +55,21 @@ async fn main() -> anyhow::Result<()> { ids, }; let res = executor.execute_select(query).await?; - tracing::debug!("query 3: {:?}", res); + tracing::info!("GetPeopleByIds returned: {:?}", res); + let upsert_id = v1_uuid(); let query = UpsertPerson { - id: v1_uuid(), + id: upsert_id, email: MaybeUnset::Set("foo21@scyllax.local".to_string()), age: MaybeUnset::Set(Some(21)), created_at: MaybeUnset::Unset, }; let res = executor.execute_upsert(query).await?; - tracing::debug!("query 4: {:?}", res); + tracing::info!("UpsertPerson returned: {:?}", res); + + let delete = DeletePersonById { id: upsert_id }; + let res = executor.execute_delete(delete).await?; + tracing::info!("DeletePersonById returned: {:?}", res); Ok(()) } diff --git a/scyllax-macros/src/entity.rs b/scyllax-macros/src/entity.rs index 55845f5..9aa1caf 100644 --- a/scyllax-macros/src/entity.rs +++ b/scyllax-macros/src/entity.rs @@ -5,7 +5,7 @@ use syn::{Expr, Field, ItemStruct}; /// Attribute expand /// Just adds the dervie macro to the struct. -pub fn expand(input: TokenStream) -> TokenStream { +pub(crate) fn expand(input: TokenStream) -> TokenStream { let input: ItemStruct = match syn::parse2(input.clone()) { Ok(it) => it, Err(e) => return token_stream_with_error(input, e), @@ -51,7 +51,7 @@ fn entity_impl(input: &ItemStruct, pks: &[&Field]) -> TokenStream { } } -/// This is used to get the name of a field, taking into account the `#[rename]` attribute. +/// This is used to get the name of a field, taking into account the `#[rename]` attribute. /// /// Rename is usually used to support camelCase keys, which need to be wrapped /// in quotes or scylla will snake_ify it. @@ -72,3 +72,34 @@ pub fn get_field_name(field: &Field) -> String { .expect("Expected field to have a name") .to_string() } + +#[cfg(test)] +mod tests { + use super::*; + + fn find_field(fields: &syn::Fields, name: &str) -> Field { + fields + .iter() + .find(|f| f.ident.as_ref().unwrap() == name) + .unwrap() + .clone() + } + + #[test] + fn test_get_field_name() { + let example_struct = r#" + struct Example { + foo: String, + #[rename = "bAr"] + bar: String, + } + "#; + let parsed = syn::parse_str::(example_struct).unwrap(); + + let foo = find_field(&parsed.fields, "foo"); + assert_eq!(get_field_name(&foo), "foo"); + + let bar = find_field(&parsed.fields, "bar"); + assert_eq!(get_field_name(&bar), r#""bAr""#); + } +} diff --git a/scyllax-macros/src/lib.rs b/scyllax-macros/src/lib.rs index d6ee4c7..bab5b10 100644 --- a/scyllax-macros/src/lib.rs +++ b/scyllax-macros/src/lib.rs @@ -41,6 +41,20 @@ pub fn select_query(args: TokenStream, input: TokenStream) -> TokenStream { queries::select::expand(args.into(), input.into()).into() } +/// Apply this attribute to a struct to generate a delete query. +/// ```rust,ignore +/// #[delete_query( +/// query = "delete from person where id = ?", +/// )] +/// pub struct DeletePersonById { +/// pub id: Uuid, +/// } +/// ``` +#[proc_macro_attribute] +pub fn delete_query(args: TokenStream, input: TokenStream) -> TokenStream { + queries::delete::expand(args.into(), input.into()).into() +} + /// Apply this attribute to a entity struct to generate an upsert query. /// ```rust,ignore /// #[upsert_query(table = "person", name = UpsertPerson)] diff --git a/scyllax-macros/src/queries/delete.rs b/scyllax-macros/src/queries/delete.rs new file mode 100644 index 0000000..792aff2 --- /dev/null +++ b/scyllax-macros/src/queries/delete.rs @@ -0,0 +1,65 @@ +use darling::{export::NestedMeta, FromMeta}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ItemStruct; + +use crate::token_stream_with_error; + +#[derive(FromMeta)] +pub(crate) struct SelectQueryOptions { + query: String, + entity_type: syn::Type, +} + +pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream { + let attr_args = match NestedMeta::parse_meta_list(args.clone()) { + Ok(args) => args, + Err(e) => return darling::Error::from(e).write_errors(), + }; + + let args = match SelectQueryOptions::from_list(&attr_args) { + Ok(o) => o, + Err(e) => return e.write_errors(), + }; + + let entity_type = args.entity_type; + let query = args.query.clone(); + + let input: ItemStruct = match syn::parse2(item.clone()) { + Ok(it) => it, + Err(e) => return token_stream_with_error(item, e), + }; + let struct_ident = &input.ident; + + quote! { + #[derive(scylla::ValueList, std::fmt::Debug, std::clone::Clone, PartialEq, Hash)] + #input + + #[scyllax::async_trait] + impl scyllax::DeleteQuery<#entity_type> for #struct_ident { + fn query() -> String { + #query.to_string() + } + + async fn prepare(db: &Executor) -> Result { + let query = Self::query(); + tracing::debug!{ + target = stringify!(#struct_ident), + query, + "preparing query" + }; + db.session.add_prepared_statement(&scylla::query::Query::new(query)).await + } + + async fn execute(self, db: &scyllax::Executor) -> anyhow::Result { + let query = Self::query(); + tracing::debug! { + query, + "executing delete" + }; + + db.session.execute(query, self).await + } + } + } +} diff --git a/scyllax-macros/src/queries/mod.rs b/scyllax-macros/src/queries/mod.rs index fb200bc..3a29e55 100644 --- a/scyllax-macros/src/queries/mod.rs +++ b/scyllax-macros/src/queries/mod.rs @@ -1,2 +1,3 @@ +pub mod delete; pub mod select; pub mod upsert; diff --git a/scyllax-macros/src/queries/select.rs b/scyllax-macros/src/queries/select.rs index 18a1a9b..7ba5af8 100644 --- a/scyllax-macros/src/queries/select.rs +++ b/scyllax-macros/src/queries/select.rs @@ -6,12 +6,12 @@ use syn::ItemStruct; use crate::token_stream_with_error; #[derive(FromMeta)] -pub struct SelectQueryOptions { +pub(crate) struct SelectQueryOptions { query: String, entity_type: syn::Type, } -pub fn expand(args: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream { let attr_args = match NestedMeta::parse_meta_list(args.clone()) { Ok(args) => args, Err(e) => return darling::Error::from(e).write_errors(), diff --git a/scyllax-macros/src/queries/upsert.rs b/scyllax-macros/src/queries/upsert.rs index f7307d1..35a8aa1 100644 --- a/scyllax-macros/src/queries/upsert.rs +++ b/scyllax-macros/src/queries/upsert.rs @@ -13,7 +13,7 @@ pub(crate) struct UpsertQueryOptions { /// Attribute expand /// Just adds the dervie macro to the struct. -pub fn expand(args: TokenStream, input: TokenStream) -> TokenStream { +pub(crate) fn expand(args: TokenStream, input: TokenStream) -> TokenStream { let attr_args = match NestedMeta::parse_meta_list(args.clone()) { Ok(args) => args, Err(e) => return darling::Error::from(e).write_errors(), diff --git a/src/executor.rs b/src/executor.rs index a77dcb3..b085ccd 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,6 +1,8 @@ //! The `scyllax` [`Executor`] processes queries. -use crate::{error::ScyllaxError, EntityExt, FromRow, ImplValueList, SelectQuery, UpsertQuery}; +use crate::{ + error::ScyllaxError, DeleteQuery, EntityExt, FromRow, ImplValueList, SelectQuery, UpsertQuery, +}; use scylla::{ prepared_statement::PreparedStatement, query::Query, transport::errors::QueryError, CachingSession, QueryResult, SessionBuilder, @@ -58,6 +60,16 @@ impl Executor { E::parse_response(res).await } + /// Executes a [`DeleteQuery`] and returns the result + pub async fn execute_delete + FromRow + ImplValueList, E: DeleteQuery>( + &self, + query: E, + ) -> Result { + let res = query.execute(self).await?; + + Ok(res) + } + /// Executes a [`UpsertQuery`] and returns the result pub async fn execute_upsert + FromRow + ImplValueList, E: UpsertQuery>( &self, diff --git a/src/lib.rs b/src/lib.rs index 4ebdd58..2c9852a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,3 +95,18 @@ pub trait UpsertQuery + ImplValueList + FromRow> { /// Executes the query async fn execute(self, db: &Executor) -> Result; } + +/// The trait that's implemented on delete queries +// R is the return type of the query +// It can be either Option or Vec +#[async_trait] +pub trait DeleteQuery + ImplValueList + FromRow> { + /// Returns the query as a string + fn query() -> String; + + /// Prepares the query + async fn prepare(db: &Executor) -> Result; + + /// Executes the query + async fn execute(self, db: &Executor) -> Result; +} diff --git a/src/prelude.rs b/src/prelude.rs index 01b9209..3402e7a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,8 +1,8 @@ //! Re-exports of the most commonly used types and traits. pub use crate::{ error::BuildUpsertQueryError, executor::Executor, maybe_unset::MaybeUnset, select_query, - upsert_query, Entity, EntityExt, FromRow, ImplValueList, ScyllaxError, SelectQuery, - UpsertQuery, + upsert_query, util::v1_uuid, DeleteQuery, Entity, EntityExt, FromRow, ImplValueList, + ScyllaxError, SelectQuery, UpsertQuery, }; pub use scylla::frame::value::SerializeValuesError; diff --git a/src/util.rs b/src/util.rs index 098b43e..c31a448 100644 --- a/src/util.rs +++ b/src/util.rs @@ -30,4 +30,11 @@ mod tests { assert_eq!(uuid.get_version(), Some(uuid::Version::Mac)); assert_eq!(uuid.get_variant(), uuid::Variant::RFC4122); } + + #[test] + fn test_get_mac_address() { + let mac = get_mac_address(); + + assert_eq!(mac.len(), 6); + } }