Add tests for if (not) defined feature

This commit is contained in:
Guillaume Gomez 2024-07-15 16:19:27 +02:00
parent 0372dac003
commit fcf1a97d9d
4 changed files with 394 additions and 90 deletions

View File

@ -8,98 +8,106 @@ use similar::{Algorithm, ChangeTag, TextDiffConfig};
use crate::build_template;
// This function makes it much easier to compare expected code by adding the wrapping around
// the code we want to check.
#[track_caller]
fn compare(jinja: &str, expected: &str, fields: &[(&str, &str)], size_hint: usize) {
let jinja = format!(
r##"#[template(source = {jinja:?}, ext = "txt")]
struct Foo {{ {} }}"##,
fields
.iter()
.map(|(name, type_)| format!("{name}: {type_}"))
.collect::<Vec<_>>()
.join(","),
);
let generated = build_template(&syn::parse_str::<syn::DeriveInput>(&jinja).unwrap())
.unwrap()
.parse()
.unwrap();
let generated: syn::File = syn::parse2(generated).unwrap();
let size_hint = proc_macro2::Literal::usize_unsuffixed(size_hint);
let expected: proc_macro2::TokenStream = expected.parse().unwrap();
let expected: syn::File = syn::parse_quote! {
impl ::rinja::Template for Foo {
fn render_into<RinjaW>(&self, writer: &mut RinjaW) -> ::rinja::Result<()>
where
RinjaW: ::core::fmt::Write + ?::core::marker::Sized,
{
use ::rinja::filters::AutoEscape as _;
use ::core::fmt::Write as _;
#expected
::rinja::Result::Ok(())
}
const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = Some("txt");
const SIZE_HINT: ::std::primitive::usize = #size_hint;
const MIME_TYPE: &'static ::std::primitive::str = "text/plain; charset=utf-8";
}
impl ::std::fmt::Display for Foo {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::rinja::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})
}
}
};
if expected != generated {
let expected = prettyplease::unparse(&expected);
let generated = prettyplease::unparse(&generated);
struct Diff<'a>(&'a str, &'a str);
impl fmt::Display for Diff<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let diff = TextDiffConfig::default()
.algorithm(Algorithm::Patience)
.diff_lines(self.0, self.1);
for change in diff.iter_all_changes() {
let (change, line) = match change.tag() {
ChangeTag::Equal => (
style(" ").dim().bold(),
style(change.to_string_lossy()).dim(),
),
ChangeTag::Delete => (
style("-").red().bold(),
style(change.to_string_lossy()).red(),
),
ChangeTag::Insert => (
style("+").green().bold(),
style(change.to_string_lossy()).green(),
),
};
write!(f, "{change}{line}")?;
}
Ok(())
}
}
panic!(
"\n\
=== Expected ===\n\
\n\
{expected}\n\
\n\
=== Generated ===\n\
\n\
{generated}\n\
\n\
=== Diff ===\n\
\n\
{diff}\n\
\n\
=== FAILURE ===",
expected = style(&expected).red(),
generated = style(&generated).green(),
diff = Diff(&expected, &generated),
);
}
}
#[test]
fn check_if_let() {
// This function makes it much easier to compare expected code by adding the wrapping around
// the code we want to check.
#[track_caller]
fn compare(jinja: &str, expected: &str, size_hint: usize) {
let jinja = format!(r#"#[template(source = {jinja:?}, ext = "txt")] struct Foo;"#);
let generated = build_template(&syn::parse_str::<syn::DeriveInput>(&jinja).unwrap())
.unwrap()
.parse()
.unwrap();
let generated: syn::File = syn::parse2(generated).unwrap();
let size_hint = proc_macro2::Literal::usize_unsuffixed(size_hint);
let expected: proc_macro2::TokenStream = expected.parse().unwrap();
let expected: syn::File = syn::parse_quote! {
impl ::rinja::Template for Foo {
fn render_into<RinjaW>(&self, writer: &mut RinjaW) -> ::rinja::Result<()>
where
RinjaW: ::core::fmt::Write + ?::core::marker::Sized,
{
use ::rinja::filters::AutoEscape as _;
use ::core::fmt::Write as _;
#expected
::rinja::Result::Ok(())
}
const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = Some("txt");
const SIZE_HINT: ::std::primitive::usize = #size_hint;
const MIME_TYPE: &'static ::std::primitive::str = "text/plain; charset=utf-8";
}
impl ::std::fmt::Display for Foo {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::rinja::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})
}
}
};
if expected != generated {
let expected = prettyplease::unparse(&expected);
let generated = prettyplease::unparse(&generated);
struct Diff<'a>(&'a str, &'a str);
impl fmt::Display for Diff<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let diff = TextDiffConfig::default()
.algorithm(Algorithm::Patience)
.diff_lines(self.0, self.1);
for change in diff.iter_all_changes() {
let (change, line) = match change.tag() {
ChangeTag::Equal => (
style(" ").dim().bold(),
style(change.to_string_lossy()).dim(),
),
ChangeTag::Delete => (
style("-").red().bold(),
style(change.to_string_lossy()).red(),
),
ChangeTag::Insert => (
style("+").green().bold(),
style(change.to_string_lossy()).green(),
),
};
write!(f, "{change}{line}")?;
}
Ok(())
}
}
panic!(
"\n\
=== Expected ===\n\
\n\
{expected}\n\
\n\
=== Generated ===\n\
\n\
{generated}\n\
\n\
=== Diff ===\n\
\n\
{diff}\n\
\n\
=== FAILURE ===",
expected = style(&expected).red(),
generated = style(&generated).green(),
diff = Diff(&expected, &generated),
);
}
}
// In this test, we ensure that `query` never is `self.query`.
compare(
"{% if let Some(query) = s && !query.is_empty() %}{{query}}{% endif %}",
@ -110,6 +118,7 @@ fn check_if_let() {
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(query), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
&[],
3,
);
@ -124,6 +133,7 @@ fn check_if_let() {
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
&[],
3,
);
@ -138,6 +148,7 @@ fn check_if_let() {
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
&[],
3,
);
@ -157,6 +168,161 @@ fn check_if_let() {
writer.write_str("3")?;
writer.write_str("3")?;"#
),
&[],
4,
);
}
#[test]
fn check_is_defined() {
// Checks that it removes conditions if we know at compile-time that they always return false.
//
// We're forced to add `bla` otherwise `compare` assert fails in weird ways...
compare(
"{% if y is defined %}{{query}}{% endif %}bla",
r#"writer.write_str("bla")?;"#,
&[],
3,
);
compare(
"{% if x is not defined %}{{query}}{% endif %}bla",
r#"writer.write_str("bla")?;"#,
&[("x", "u32")],
3,
);
compare(
"{% if y is defined && x is not defined %}{{query}}{% endif %}bla",
r#"writer.write_str("bla")?;"#,
&[("x", "u32")],
3,
);
// Same with declared variables.
compare(
"{% set y = 12 %}
{%- if y is not defined %}{{query}}{% endif %}bla",
r#"let y = 12;
writer.write_str("bla")?;"#,
&[],
3,
);
compare(
"{% set y = 12 %}
{%- if y is not defined && x is defined %}{{query}}{% endif %}bla",
r#"let y = 12;
writer.write_str("bla")?;"#,
&[],
3,
);
// Checks that if the condition is always `true` at compile-time, then we keep the code but
// remove the condition.
compare(
"{% if y is defined %}bla{% endif %}",
r#"writer.write_str("bla")?;"#,
&[("y", "u32")],
3,
);
compare(
"{% if x is not defined %}bla{% endif %}",
r#"writer.write_str("bla")?;"#,
&[],
3,
);
// Same with declared variables.
compare(
"{% set y = 12 %}
{%- if y is defined %}bla{% endif %}",
r#"let y = 12;
writer.write_str("bla")?;"#,
&[],
3,
);
// If the always `true` condition is followed by more `else if`/`else`, check that they are
// removed as well.
compare(
"{% if x is defined %}bli
{%- else if x == 12 %}12{% endif %}bla",
r#"writer.write_str("bli")?;
writer.write_str("bla")?;"#,
&[("x", "u32")],
6,
);
compare(
"{% if x is defined %}bli
{%- else if x == 12 %}12
{%- else %}nope{% endif %}bla",
r#"writer.write_str("bli")?;
writer.write_str("bla")?;"#,
&[("x", "u32")],
6,
);
// If it's not the first one.
compare(
"{% if x == 12 %}bli
{%- else if x is defined %}12
{%- else %}nope{% endif %}",
r#"if *(&(self.x == 12) as &bool) {
writer.write_str("bli")?;
} else {
writer.write_str("12")?;
}"#,
&[("x", "u32")],
5,
);
// Checking that it doesn't remove the condition if other non-"if (not) defined" checks
// are present.
compare(
"{% if y is defined || x == 12 %}{{x}}{% endif %}",
r#"if *(&(false || self.x == 12) as &bool) {
::std::write!(writer, "{expr0}",
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(self.x), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}
"#,
&[("x", "u32")],
3,
);
compare(
"{% if y is defined || x == 12 %}{{x}}{% endif %}",
r#"if *(&(true || self.x == 12) as &bool) {
::std::write!(writer, "{expr0}",
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(self.x), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}
"#,
&[("y", "u32"), ("x", "u32")],
3,
);
// Checking some funny cases.
// This one is a bit useless because you can use `is not defined` but I suppose it's possible
// to encounter cases like that in the wild so better have a check.
compare(
"{% if !(y is defined) %}bla{% endif %}",
r#"writer.write_str("bla")?;"#,
&[],
3,
);
compare(
"{% if !(y is not defined) %}bli{% endif %}bla",
r#"writer.write_str("bla")?;"#,
&[],
3,
);
compare(
"{% if !(y is defined) %}bli{% endif %}bla",
r#"writer.write_str("bla")?;"#,
&[("y", "u32")],
3,
);
compare(
"{% if !(y is not defined) %}bla{% endif %}",
r#"writer.write_str("bla")?;"#,
&[("y", "u32")],
3,
);
}

View File

@ -0,0 +1,33 @@
use rinja::Template;
#[derive(Template)]
#[template(
source = r#"<script>
const x = {{ x is defined }};
const y = {{ y is not defined }};
const z = {{ y is defined }};
const w = {{ x is not defined }};
const v = {{ y }};
</script>"#,
ext = "html"
)]
struct IsDefined {
y: u32,
}
// This test ensures that `include` are correctly working inside filter blocks and that external
// variables are used correctly.
#[test]
fn is_defined_in_expr() {
let s = IsDefined { y: 0 };
assert_eq!(
s.render().unwrap(),
r#"<script>
const x = false;
const y = false;
const z = true;
const w = true;
const v = 0;
</script>"#
);
}

View File

@ -0,0 +1,46 @@
use rinja::Template;
#[derive(Template)]
#[template(
ext = "html",
source = r#"{% if x.y is defined %}{% endif %}"#,
)]
struct A;
#[derive(Template)]
#[template(
ext = "html",
source = r#"{% if true is defined %}{% endif %}"#,
)]
struct B;
#[derive(Template)]
#[template(
ext = "html",
source = r#"{% if true is %}{% endif %}"#,
)]
struct C;
#[derive(Template)]
#[template(
ext = "html",
source = r#"{% if x is %}{% endif %}"#,
)]
struct D;
#[derive(Template)]
#[template(
ext = "html",
source = r#"{% if x is blue %}{% endif %}"#,
)]
struct E;
#[derive(Template)]
#[template(
ext = "html",
source = r#"{% if x is blue.red %}{% endif %}"#,
)]
struct F;
fn main() {
}

View File

@ -0,0 +1,59 @@
error: `is defined` operator can only be used on variables, not on their fields
failed to parse template source at row 1, column 6 near:
"x.y is defined %}{% endif %}"
--> tests/ui/is_defined.rs:3:10
|
3 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
error: `is defined` operator can only be used on variables
failed to parse template source at row 1, column 6 near:
"true is defined %}{% endif %}"
--> tests/ui/is_defined.rs:10:10
|
10 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected `defined` or `not defined` after `is`
failed to parse template source at row 1, column 6 near:
"true is %}{% endif %}"
--> tests/ui/is_defined.rs:17:10
|
17 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected `defined` or `not defined` after `is`
failed to parse template source at row 1, column 6 near:
"x is %}{% endif %}"
--> tests/ui/is_defined.rs:24:10
|
24 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected `defined` or `not defined` after `is`
failed to parse template source at row 1, column 6 near:
"x is blue %}{% endif %}"
--> tests/ui/is_defined.rs:31:10
|
31 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected `defined` or `not defined` after `is`
failed to parse template source at row 1, column 6 near:
"x is blue.red %}{% endif %}"
--> tests/ui/is_defined.rs:38:10
|
38 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)