Skip to content
Closed
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
5 changes: 5 additions & 0 deletions avro/src/serde/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ use crate::{
///
/// Use the schema of the inner field directly. Is only allowed on structs with only one unskipped field.
///
/// - `#[avro(tests = false)]`
///
/// To disable generating tests for derived schemas. This is useful if you use a different [validator](crate::validator)
/// or the `unnameable_test_items` lint is triggered because the type you are deriving is inside a function.
///
///
/// #### Variant attributes
///
Expand Down
4 changes: 4 additions & 0 deletions avro/tests/avro-rs-226.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ where
#[test]
fn avro_rs_226_index_out_of_bounds_with_serde_skip_serializing_skip_middle_field() -> TestResult {
#[derive(AvroSchema, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[avro(tests = false)]
struct T {
x: Option<i8>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -67,6 +68,7 @@ fn avro_rs_226_index_out_of_bounds_with_serde_skip_serializing_skip_middle_field
#[test]
fn avro_rs_226_index_out_of_bounds_with_serde_skip_serializing_skip_first_field() -> TestResult {
#[derive(AvroSchema, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[avro(tests = false)]
struct T {
#[serde(skip_serializing_if = "Option::is_none")]
#[avro(default = "null")]
Expand All @@ -88,6 +90,7 @@ fn avro_rs_226_index_out_of_bounds_with_serde_skip_serializing_skip_first_field(
#[test]
fn avro_rs_226_index_out_of_bounds_with_serde_skip_serializing_skip_last_field() -> TestResult {
#[derive(AvroSchema, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[avro(tests = false)]
struct T {
x: Option<i8>,
y: Option<String>,
Expand All @@ -109,6 +112,7 @@ fn avro_rs_226_index_out_of_bounds_with_serde_skip_serializing_skip_last_field()
#[test]
fn avro_rs_226_index_out_of_bounds_with_serde_skip_multiple_fields() -> TestResult {
#[derive(AvroSchema, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[avro(tests = false)]
struct T {
no_skip1: Option<i8>,
#[serde(skip_serializing)]
Expand Down
2 changes: 2 additions & 0 deletions avro/tests/get_record_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use apache_avro_test_helper::TestResult;
#[test]
fn avro_rs_448_default_get_record_fields_no_recursion() -> TestResult {
#[derive(apache_avro_derive::AvroSchema)]
#[avro(tests = false)]
struct Foo {
_a: i32,
_b: String,
Expand Down Expand Up @@ -67,6 +68,7 @@ fn avro_rs_448_default_get_record_fields_no_recursion() -> TestResult {
#[test]
fn avro_rs_448_default_get_record_fields_recursion() -> TestResult {
#[derive(apache_avro_derive::AvroSchema)]
#[avro(tests = false)]
struct Foo {
_a: i32,
_b: Option<Box<Foo>>,
Expand Down
3 changes: 3 additions & 0 deletions avro/tests/serde_human_readable_false.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ fn avro_rs_53_uuid_with_fixed() -> TestResult {
#[test]
fn avro_rs_440_uuid_string() -> TestResult {
#[derive(apache_avro_derive::AvroSchema, Serialize, Deserialize)]
#[avro(tests = false)]
#[serde(transparent)]
struct CustomUuid {
#[avro(with = || Schema::Uuid(UuidSchema::String))]
Expand All @@ -88,6 +89,7 @@ fn avro_rs_440_uuid_string() -> TestResult {
#[test]
fn avro_rs_440_uuid_bytes() -> TestResult {
#[derive(apache_avro_derive::AvroSchema, Serialize, Deserialize)]
#[avro(tests = false)]
#[serde(transparent)]
struct CustomUuid {
#[avro(with = || Schema::Uuid(UuidSchema::Bytes))]
Expand Down Expand Up @@ -116,6 +118,7 @@ fn avro_rs_440_uuid_bytes() -> TestResult {
#[test]
fn avro_rs_440_uuid_fixed() -> TestResult {
#[derive(apache_avro_derive::AvroSchema, Serialize, Deserialize)]
#[avro(tests = false)]
#[serde(transparent)]
struct CustomUuid {
inner: Uuid,
Expand Down
3 changes: 3 additions & 0 deletions avro/tests/serde_human_readable_true.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ fn avro_rs_53_uuid_with_string_true() -> TestResult {
#[test]
fn avro_rs_440_uuid_string() -> TestResult {
#[derive(apache_avro_derive::AvroSchema, Serialize, Deserialize)]
#[avro(tests = false)]
#[serde(transparent)]
struct CustomUuid {
#[avro(with = || Schema::Uuid(UuidSchema::String))]
Expand All @@ -89,6 +90,7 @@ fn avro_rs_440_uuid_string() -> TestResult {
#[test]
fn avro_rs_440_uuid_bytes() -> TestResult {
#[derive(apache_avro_derive::AvroSchema, Serialize, Deserialize)]
#[avro(tests = false)]
#[serde(transparent)]
struct CustomUuid {
#[avro(with = || Schema::Uuid(UuidSchema::Bytes))]
Expand All @@ -112,6 +114,7 @@ fn avro_rs_440_uuid_bytes() -> TestResult {
#[test]
fn avro_rs_440_uuid_fixed() -> TestResult {
#[derive(apache_avro_derive::AvroSchema, Serialize, Deserialize)]
#[avro(tests = false)]
#[serde(transparent)]
struct CustomUuid {
inner: Uuid,
Expand Down
3 changes: 3 additions & 0 deletions avro_derive/src/attributes/avro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ pub struct ContainerAttributes {
/// Force the generator to use a certain schema representation
#[darling(default)]
pub repr: Option<Repr>,
/// Disable the generation of tests
#[darling(default = || true)]
pub tests: bool,
}

impl ContainerAttributes {
Expand Down
2 changes: 2 additions & 0 deletions avro_derive/src/attributes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub struct NamedTypeOptions {
pub transparent: bool,
pub default: Option<Value>,
pub repr: Option<Repr>,
pub tests: bool,
}

impl NamedTypeOptions {
Expand Down Expand Up @@ -266,6 +267,7 @@ impl NamedTypeOptions {
transparent: serde.transparent,
default,
repr,
tests: avro.tests,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions avro_derive/src/enums/bare_union.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,6 @@ pub fn to_implementation(
.default
.map(json_value_expr)
.map(|t| quote! { ::std::option::Option::Some(#t)}),
container_attrs.tests,
))
}
1 change: 1 addition & 0 deletions avro_derive/src/enums/plain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub fn to_implementation(
.default
.map(json_value_expr)
.map(|t| quote! { ::std::option::Option::Some(#t)}),
container_attrs.tests,
))
}

Expand Down
1 change: 1 addition & 0 deletions avro_derive/src/enums/record_internally_tagged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,6 @@ pub fn to_implementation(
.default
.map(json_value_expr)
.map(|t| quote! { ::std::option::Option::Some(#t)}),
container_attrs.tests,
))
}
1 change: 1 addition & 0 deletions avro_derive/src/enums/record_tag_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,6 @@ pub fn to_implementation(
.default
.map(json_value_expr)
.map(|t| quote! { ::std::option::Option::Some(#t)}),
container_attrs.tests,
))
}
1 change: 1 addition & 0 deletions avro_derive/src/enums/union_of_records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,6 @@ pub fn to_implementation(
.default
.map(json_value_expr)
.map(|t| quote! { ::std::option::Option::Some(#t)}),
container_attrs.tests,
))
}
87 changes: 85 additions & 2 deletions avro_derive/src/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

use crate::utils::name_expr;
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use quote::{format_ident, quote};
use syn::Generics;

pub struct Implementation {
Expand All @@ -35,6 +35,8 @@ pub struct Implementation {
record_fields_expr: TokenStream,
/// An expression that resolves to a `Option<serde_json::Value>`
field_default_expr: TokenStream,
/// Tests that will be run if the type has no generics and they haven't been disabled via an attribute.
tests: Option<Vec<Test>>,
}

impl Implementation {
Expand All @@ -49,6 +51,7 @@ impl Implementation {
schema_expr: TokenStream,
record_fields_expr: Option<TokenStream>,
field_default_expr: Option<TokenStream>,
tests: bool,
) -> Implementation {
let name_expr = name_expr(name);
let schema_expr = quote! {
Expand All @@ -68,6 +71,7 @@ impl Implementation {
schema_expr,
record_fields_expr,
field_default_expr,
tests,
)
}

Expand All @@ -77,15 +81,46 @@ impl Implementation {
schema_expr: TokenStream,
record_fields_expr: Option<TokenStream>,
field_default_expr: Option<TokenStream>,
tests: bool,
) -> Implementation {
Self {
let mut this = Self {
ident,
generics,
schema_expr,
record_fields_expr: record_fields_expr
.unwrap_or_else(|| quote! { ::std::option::Option::None }),
field_default_expr: field_default_expr
.unwrap_or_else(|| quote! { ::std::option::Option::None }),
tests: tests.then(Vec::new),
};

this.add_schema_test();

this
}

pub fn add_test(&mut self, test: Test) {
if let Some(tests) = &mut self.tests {
tests.push(test);
}
}

fn add_schema_test(&mut self) {
// Only add the schema test if there are no generics to deal with. I think it should be possible
// to be able to generate the test if there are only lifetimes, but that needs more experimentation.
if self.generics.lifetimes().count() == 0
&& self.generics.type_params().count() == 0
&& self.generics.const_params().count() == 0
{
let ident = &self.ident;
self.add_test(Test {
name: "schema".to_string(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get the idea of this PR :-/
It is invasive by generating tests no one asked for.
The end user will have to opt-out instead of opt-in for those tests.

It is not clear what is being tested too.

  1. The name is always "schema". Is it really needed ?!
  2. The body always prints "Invalid schema" to stderr
  3. should_panic is always None
  4. The body just calls get_schema(), so it tests runtime construction of the derived Schema and validates its Name. But one could assume that the users make use of the derived schema too in their application and tests, so this is kinda already covered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think especially point 4 is a good point, so let's close this for now.

(1 and 3 are to allow generating other kinds of tests, 2 is so that the user gets some feedback when the test fails).

body: quote! {
eprintln!("Invalid schema, if this is because a different validator is used at runtime you can disable generating tests with `#[avro(tests = false)]`");
<#ident as ::apache_avro::serde::AvroSchema>::get_schema();
},
should_panic: None,
});
}
}

Expand All @@ -96,6 +131,24 @@ impl Implementation {
let record_fields_expr = self.record_fields_expr;
let field_default_expr = self.field_default_expr;

let tests = if let Some(tests) = self.tests {
let test_module_name = format!(
"_apache_avro_derive_tests_for_{}",
ident.to_string().to_lowercase()
);
let tests = tests
.into_iter()
.map(|t| t.into_token_stream(&test_module_name));
quote! {
#(
#[cfg(test)]
#tests
)*
}
} else {
TokenStream::new()
};

quote! {
#[automatically_derived]
impl #impl_generics ::apache_avro::AvroSchemaComponent for #ident #ty_generics #where_clause {
Expand All @@ -111,6 +164,36 @@ impl Implementation {
#field_default_expr
}
}

#tests
}
}
}

pub struct Test {
/// The name of the test.
pub name: String,
/// The body of the test function, so without `fn some_name() {`.
pub body: TokenStream,
/// If this test should panic, this should be the message.
pub should_panic: Option<String>,
}

impl Test {
pub fn into_token_stream(self, prefix: &str) -> TokenStream {
let name = format_ident!("{prefix}_{}", &self.name);
let body = &self.body;
let should_panic = if let Some(expected) = &self.should_panic {
quote! { #[should_panic(expected = #expected)] }
} else {
TokenStream::new()
};
quote! {
#[test]
#should_panic
fn #name() {
#body
}
}
}
}
5 changes: 4 additions & 1 deletion avro_derive/src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn to_implementation(
data: DataStruct,
) -> Result<Implementation, Vec<syn::Error>> {
if container_attrs.transparent {
transparent(input_span, ident, generics, data)
transparent(input_span, ident, generics, &container_attrs, data)
} else {
normal(ident, generics, container_attrs, data)
}
Expand Down Expand Up @@ -85,13 +85,15 @@ fn normal(
.default
.map(json_value_expr)
.map(|t| quote! { ::std::option::Option::Some(#t)}),
container_attrs.tests,
))
}

fn transparent(
input_span: Span,
ident: Ident,
generics: Generics,
container_attrs: &NamedTypeOptions,
data: DataStruct,
) -> Result<Implementation, Vec<syn::Error>> {
match data.fields {
Expand Down Expand Up @@ -119,6 +121,7 @@ fn transparent(
field_to_schema_expr(&field, &attrs.with)?,
Some(field_to_record_fields_expr(&field, &attrs.with)?),
Some(field_default_expr),
container_attrs.tests,
))
} else {
Err(vec![syn::Error::new(
Expand Down
Loading
Loading