Repository: tafia/quick-xml
Pull request #773 (2024-06-27 07:18:08)

Fix incorrect end position in `read_to_end` and `read_text`
When working on #766 I found this bug. When `trim_text_start` is set to `true`, the trailing spaces could not be included in text returned by the `read_text` method and not counted in the `end` field of the returned span from `read_to_end` family of methods. If you read text content of such XML:
```xml
<tag>  <nested>  </tag>
```
then the trailing space would be dropped. This is because we detect end position of the span *before* we read `</tag>`. When start spaces are trimmed we have 3 events: `Start`, `Empty`, `End`, and `buffer_position()` after reading each of them are:
```xml
<tag>  <nested>  </tag>
     ^         ^ |     ^ - buffer_position() after each event
     ^           ^       - expected Span of the read_to_end() methods
```
Spaces just skipped and do not update `buffer_position()` which results to the incorrect end of the `Span`. This is happened only when the last meaningful event before the closing `End` event is not a `Text` event.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/tafia/quick-xml/pull/773?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 60.37%. Comparing base [(`7558577`)](https://app.codecov.io/gh/tafia/quick-xml/commit/7558577bc56e853db381910b4ab7a698cd0bbaba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`930d1b2`)](https://app.codecov.io/gh/tafia/quick-xml/commit/930d1b21f68022c2fdfbeee010aee3b22d67ea45?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> Report is 54 commits behind head on master.

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #773      +/-   ##
==========================================
- Coverage   61.81%   60.37%   -1.45%     
==========================================
  Files          41       41              
  Lines       16798    16229     -569     
==========================================
- Hits        10384     9798     -586     
- Misses       6414     6431      +17     
```

| [Flag](https://app.codecov.io/gh/tafia/quick-xml/pull/773/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/tafia/quick-xml/pull/773/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | `60.37% <ø> (-1.45%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tafia/quick-xml/pull/773?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/Changelog.md b/Changelog.md
index ab8548b..2d9f59a 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -19,9 +19,14 @@
 
 ### Bug Fixes
 
+- [#773]: Fixed reporting incorrect end position in `Reader::read_to_end` family
+  of methods and trimming of the trailing spaces in `Reader::read_text` when
+  `trim_text_start` is set and the last event is not a `Text` event.
+
 ### Misc Changes
 
 [#772]: https://github.com/tafia/quick-xml/pull/772
+[#773]: https://github.com/tafia/quick-xml/pull/773
 
 
 ## 0.34.0 -- 2024-06-25
diff --git a/src/reader/mod.rs b/src/reader/mod.rs
index 68853cb..63a4257 100644
--- a/src/reader/mod.rs
+++ b/src/reader/mod.rs
@@ -394,28 +394,52 @@ macro_rules! read_until_close {
 /// Generalization of `read_to_end` method for buffered and borrowed readers
 macro_rules! read_to_end {
     (
+        // $self: &mut Reader
         $self:expr, $end:expr, $buf:expr,
         $read_event:ident,
         // Code block that performs clearing of internal buffer after read of each event
         $clear:block
         $(, $await:ident)?
     ) => {{
+        // Because we take position after the event before the End event,
+        // it is important that this position indicates beginning of the End event.
+        // If between last event and the End event would be only spaces, then we
+        // take position before the spaces, but spaces would be skipped without
+        // generating event if `trim_text_start` is set to `true`. To prevent that
+        // we temporary disable start text trimming.
+        //
+        // We also cannot take position after getting End event, because if
+        // `trim_markup_names_in_closing_tags` is set to `true` (which is the default),
+        // we do not known the real size of the End event that it is occupies in
+        // the source and cannot correct the position after the End event.
+        // So, we in any case should tweak parser configuration.
+        let config = $self.config_mut();
+        let trim = config.trim_text_start;
+        config.trim_text_start = false;
+
         let start = $self.buffer_position();
         let mut depth = 0;
         loop {
             $clear
             let end = $self.buffer_position();
             match $self.$read_event($buf) $(.$await)? {
-                Err(e) => return Err(e),
+                Err(e) => {
+                    $self.config_mut().trim_text_start = trim;
+                    return Err(e);
+                }
 
                 Ok(Event::Start(e)) if e.name() == $end => depth += 1,
                 Ok(Event::End(e)) if e.name() == $end => {
                     if depth == 0 {
+                        $self.config_mut().trim_text_start = trim;
                         break start..end;
                     }
                     depth -= 1;
                 }
-                Ok(Event::Eof) => return Err(Error::missed_end($end, $self.decoder())),
+                Ok(Event::Eof) => {
+                    $self.config_mut().trim_text_start = trim;
+                    return Err(Error::missed_end($end, $self.decoder()));
+                }
                 _ => (),
             }
         }


Test patch:
diff --git a/tests/async-tokio.rs b/tests/async-tokio.rs
index 3d70c50..3c267d9 100644
--- a/tests/async-tokio.rs
+++ b/tests/async-tokio.rs
@@ -1,13 +1,13 @@
 use std::iter;
 
 use pretty_assertions::assert_eq;
-use quick_xml::events::Event::*;
+use quick_xml::events::{BytesStart, Event::*};
 use quick_xml::name::QName;
 use quick_xml::reader::Reader;
 
 // Import `small_buffers_tests!`
 #[macro_use]
-mod reader;
+mod helpers;
 
 small_buffers_tests!(
     #[tokio::test]
@@ -40,6 +40,53 @@ async fn test_sample() {
     assert_eq!((count, reads), (1247, 5245));
 }
 
+/// This tests checks that read_to_end() correctly returns span even when
+/// text is trimmed from both sides
+mod read_to_end {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[tokio::test]
+    async fn text() {
+        let mut r = Reader::from_str("<tag> text </tag>");
+        //                            ^0   ^5    ^11
+        r.config_mut().trim_text(true);
+
+        let mut buf = Vec::new();
+        assert_eq!(
+            r.read_event_into_async(&mut buf).await.unwrap(),
+            Start(BytesStart::new("tag"))
+        );
+        assert_eq!(
+            r.read_to_end_into_async(QName(b"tag"), &mut buf)
+                .await
+                .unwrap(),
+            5..11
+        );
+        assert_eq!(r.read_event_into_async(&mut buf).await.unwrap(), Eof);
+    }
+
+    #[tokio::test]
+    async fn tag() {
+        let mut r = Reader::from_str("<tag> <nested/> </tag>");
+        //                            ^0   ^5         ^16
+        r.config_mut().trim_text(true);
+
+        let mut buf = Vec::new();
+        assert_eq!(
+            r.read_event_into_async(&mut buf).await.unwrap(),
+            Start(BytesStart::new("tag"))
+        );
+        assert_eq!(
+            r.read_to_end_into_async(QName(b"tag"), &mut buf)
+                .await
+                .unwrap(),
+            5..16
+        );
+        assert_eq!(r.read_event_into_async(&mut buf).await.unwrap(), Eof);
+    }
+}
+
 /// Regression test for https://github.com/tafia/quick-xml/issues/751
 ///
 /// Actually, that error was not found in async reader, but we would to test it as well.
diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs
index 1af3ea7..bbc7c0c 100644
--- a/tests/helpers/mod.rs
+++ b/tests/helpers/mod.rs
@@ -1,24 +1,151 @@
 //! Utility functions for integration tests
 
-use quick_xml::de::Deserializer;
-use quick_xml::DeError;
-use serde::Deserialize;
-
-/// Deserialize an instance of type T from a string of XML text.
-/// If deserialization was succeeded checks that all XML events was consumed
-pub fn from_str<'de, T>(source: &'de str) -> Result<T, DeError>
-where
-    T: Deserialize<'de>,
-{
-    // Log XML that we try to deserialize to see it in the failed tests output
-    dbg!(source);
-    let mut de = Deserializer::from_str(source);
-    let result = T::deserialize(&mut de);
-
-    // If type was deserialized, the whole XML document should be consumed
-    if let Ok(_) = result {
-        assert!(de.is_empty(), "the whole XML document should be consumed");
-    }
-
-    result
+/// Tests for https://github.com/tafia/quick-xml/issues/469
+/// Exported to reuse in `async-tokio` tests.
+#[macro_export]
+macro_rules! small_buffers_tests {
+    (
+        #[$test:meta]
+        $read_event:ident: $BufReader:ty
+        $(, $async:ident, $await:ident)?
+    ) => {
+        mod small_buffers {
+            use quick_xml::events::{BytesCData, BytesDecl, BytesPI, BytesStart, BytesText, Event};
+            use quick_xml::reader::Reader;
+            use pretty_assertions::assert_eq;
+
+            #[$test]
+            $($async)? fn decl() {
+                let xml = "<?xml ?>";
+                //         ^^^^^^^ data that fit into buffer
+                let size = xml.match_indices("?>").next().unwrap().0 + 1;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml ", 3)))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+
+            #[$test]
+            $($async)? fn pi() {
+                let xml = "<?pi?>";
+                //         ^^^^^ data that fit into buffer
+                let size = xml.match_indices("?>").next().unwrap().0 + 1;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::PI(BytesPI::new("pi"))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+
+            #[$test]
+            $($async)? fn empty() {
+                let xml = "<empty/>";
+                //         ^^^^^^^ data that fit into buffer
+                let size = xml.match_indices("/>").next().unwrap().0 + 1;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Empty(BytesStart::new("empty"))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+
+            #[$test]
+            $($async)? fn cdata1() {
+                let xml = "<![CDATA[cdata]]>";
+                //         ^^^^^^^^^^^^^^^ data that fit into buffer
+                let size = xml.match_indices("]]>").next().unwrap().0 + 1;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::CData(BytesCData::new("cdata"))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+
+            #[$test]
+            $($async)? fn cdata2() {
+                let xml = "<![CDATA[cdata]]>";
+                //         ^^^^^^^^^^^^^^^^ data that fit into buffer
+                let size = xml.match_indices("]]>").next().unwrap().0 + 2;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::CData(BytesCData::new("cdata"))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+
+            #[$test]
+            $($async)? fn comment1() {
+                let xml = "<!--comment-->";
+                //         ^^^^^^^^^^^^ data that fit into buffer
+                let size = xml.match_indices("-->").next().unwrap().0 + 1;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Comment(BytesText::new("comment"))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+
+            #[$test]
+            $($async)? fn comment2() {
+                let xml = "<!--comment-->";
+                //         ^^^^^^^^^^^^^ data that fit into buffer
+                let size = xml.match_indices("-->").next().unwrap().0 + 2;
+                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
+                let mut reader = Reader::from_reader(br);
+                let mut buf = Vec::new();
+
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Comment(BytesText::new("comment"))
+                );
+                assert_eq!(
+                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
+                    Event::Eof
+                );
+            }
+        }
+    };
 }
diff --git a/tests/reader-errors.rs b/tests/reader-errors.rs
index e5d4bfc..18c08a2 100644
--- a/tests/reader-errors.rs
+++ b/tests/reader-errors.rs
@@ -5,7 +5,7 @@ use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesPI, BytesStart, By
 use quick_xml::reader::{NsReader, Reader};
 
 macro_rules! ok {
-    ($test:ident($xml:literal) => $event:expr) => {
+    ($test:ident($xml:literal) => $pos:literal : $event:expr) => {
         mod $test {
             use super::*;
 
@@ -18,6 +18,7 @@ macro_rules! ok {
                     let mut reader = Reader::from_str($xml);
                     reader.config_mut().enable_all_checks(true);
                     assert_eq!(reader.read_event().unwrap(), $event);
+                    assert_eq!(reader.buffer_position(), $pos);
                 }
 
                 #[test]
@@ -26,6 +27,7 @@ macro_rules! ok {
                     let mut reader = Reader::from_str($xml);
                     reader.config_mut().enable_all_checks(true);
                     assert_eq!(reader.read_event_into(&mut buf).unwrap(), $event);
+                    assert_eq!(reader.buffer_position(), $pos);
                 }
 
                 #[cfg(feature = "async-tokio")]
@@ -38,6 +40,7 @@ macro_rules! ok {
                         reader.read_event_into_async(&mut buf).await.unwrap(),
                         $event
                     );
+                    assert_eq!(reader.buffer_position(), $pos);
                 }
             }
 
@@ -50,6 +53,7 @@ macro_rules! ok {
                     let mut reader = NsReader::from_str($xml);
                     reader.config_mut().enable_all_checks(true);
                     assert_eq!(reader.read_resolved_event().unwrap().1, $event);
+                    assert_eq!(reader.buffer_position(), $pos);
                 }
 
                 #[test]
@@ -58,6 +62,7 @@ macro_rules! ok {
                     let mut reader = NsReader::from_str($xml);
                     reader.config_mut().enable_all_checks(true);
                     assert_eq!(reader.read_resolved_event_into(&mut buf).unwrap().1, $event);
+                    assert_eq!(reader.buffer_position(), $pos);
                 }
 
                 #[cfg(feature = "async-tokio")]
@@ -74,6 +79,7 @@ macro_rules! ok {
                             .1,
                         $event
                     );
+                    assert_eq!(reader.buffer_position(), $pos);
                 }
             }
         }
@@ -303,7 +309,8 @@ mod syntax {
         err!(unclosed09("<!--->") => SyntaxError::UnclosedComment);
         err!(unclosed10("<!---c") => SyntaxError::UnclosedComment);
 
-        ok!(normal("<!---->") => Event::Comment(BytesText::new("")));
+        ok!(normal1("<!---->")     => 7: Event::Comment(BytesText::new("")));
+        ok!(normal2("<!---->rest") => 7: Event::Comment(BytesText::new("")));
     }
 
     /// https://www.w3.org/TR/xml11/#NT-CDSect
@@ -336,7 +343,8 @@ mod syntax {
         err!(unclosed24("<![CDATA[]h") => SyntaxError::UnclosedCData);
         err!(unclosed25("<![CDATA[]>") => SyntaxError::UnclosedCData);
 
-        ok!(normal("<![CDATA[]]>") => Event::CData(BytesCData::new("")));
+        ok!(normal1("<![CDATA[]]>")     => 12: Event::CData(BytesCData::new("")));
+        ok!(normal2("<![CDATA[]]>rest") => 12: Event::CData(BytesCData::new("")));
     }
 
     /// According to the grammar, only upper-case letters allowed for DOCTYPE writing.
@@ -371,7 +379,8 @@ mod syntax {
 
         // According to the grammar, XML declaration MUST contain at least one space
         // and an element name, but we do not consider this as a _syntax_ error.
-        ok!(normal("<!DOCTYPE e>") => Event::DocType(BytesText::new("e")));
+        ok!(normal1("<!DOCTYPE e>")     => 12: Event::DocType(BytesText::new("e")));
+        ok!(normal2("<!DOCTYPE e>rest") => 12: Event::DocType(BytesText::new("e")));
     }
 
     /// https://www.w3.org/TR/xml11/#NT-PI
@@ -391,8 +400,10 @@ mod syntax {
 
         // According to the grammar, processing instruction MUST contain a non-empty
         // target name, but we do not consider this as a _syntax_ error.
-        ok!(normal_empty("<??>")    => Event::PI(BytesPI::new("")));
-        ok!(normal_xmlx("<?xmlx?>") => Event::PI(BytesPI::new("xmlx")));
+        ok!(normal_empty1("<??>")        => 4: Event::PI(BytesPI::new("")));
+        ok!(normal_empty2("<??>rest")    => 4: Event::PI(BytesPI::new("")));
+        ok!(normal_xmlx1("<?xmlx?>")     => 8: Event::PI(BytesPI::new("xmlx")));
+        ok!(normal_xmlx2("<?xmlx?>rest") => 8: Event::PI(BytesPI::new("xmlx")));
     }
 
     /// https://www.w3.org/TR/xml11/#NT-prolog
@@ -406,11 +417,12 @@ mod syntax {
 
         // According to the grammar, XML declaration MUST contain at least one space
         // and `version` attribute, but we do not consider this as a _syntax_ error.
-        ok!(normal1("<?xml?>")   => Event::Decl(BytesDecl::from_start(BytesStart::new("xml"))));
-        ok!(normal2("<?xml ?>")  => Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml ", 3))));
-        ok!(normal3("<?xml\t?>") => Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\t", 3))));
-        ok!(normal4("<?xml\r?>") => Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\r", 3))));
-        ok!(normal5("<?xml\n?>") => Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\n", 3))));
+        ok!(normal1("<?xml?>")       => 7: Event::Decl(BytesDecl::from_start(BytesStart::new("xml"))));
+        ok!(normal2("<?xml ?>")      => 8: Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml ", 3))));
+        ok!(normal3("<?xml\t?>")     => 8: Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\t", 3))));
+        ok!(normal4("<?xml\r?>")     => 8: Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\r", 3))));
+        ok!(normal5("<?xml\n?>")     => 8: Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\n", 3))));
+        ok!(normal6("<?xml\n?>rest") => 8: Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml\n", 3))));
     }
 }
 
@@ -811,13 +823,13 @@ mod ill_formed {
     //                                   ^= 9
     err!(missing_doctype_name2("<!DOCTYPE \t\r\n>") => 13: IllFormedError::MissingDoctypeName);
     //                                          ^= 13
-    ok!(missing_doctype_name3("<!DOCTYPE \t\r\nx>") => Event::DocType(BytesText::new("x")));
+    ok!(missing_doctype_name3("<!DOCTYPE \t\r\nx>") => 15: Event::DocType(BytesText::new("x")));
 
     err!(unmatched_end_tag1("</>") => 0: IllFormedError::UnmatchedEndTag("".to_string()));
     err!(unmatched_end_tag2("</end>") => 0: IllFormedError::UnmatchedEndTag("end".to_string()));
     err!(unmatched_end_tag3("</end >") => 0: IllFormedError::UnmatchedEndTag("end".to_string()));
 
-    ok!(mismatched_end_tag1("<start></start>") => Event::Start(BytesStart::new("start")));
+    ok!(mismatched_end_tag1("<start></start>") => 7: Event::Start(BytesStart::new("start")));
     err2!(mismatched_end_tag2("<start></>") => 7: IllFormedError::MismatchedEndTag {
         //                            ^= 7
         expected: "start".to_string(),
@@ -834,7 +846,7 @@ mod ill_formed {
         found: "end".to_string(),
     });
 
-    ok!(double_hyphen_in_comment1("<!---->") => Event::Comment(BytesText::new("")));
+    ok!(double_hyphen_in_comment1("<!---->") => 7: Event::Comment(BytesText::new("")));
     err!(double_hyphen_in_comment2("<!----->") => 4: IllFormedError::DoubleHyphenInComment);
     //                                  ^= 4
     err!(double_hyphen_in_comment3("<!-- --->") => 5: IllFormedError::DoubleHyphenInComment);
diff --git a/tests/reader.rs b/tests/reader.rs
index f1e58de..4921cd8 100644
--- a/tests/reader.rs
+++ b/tests/reader.rs
@@ -1,159 +1,14 @@
 use std::str::from_utf8;
 
 use quick_xml::events::{BytesCData, BytesEnd, BytesStart, BytesText, Event::*};
+use quick_xml::name::QName;
 use quick_xml::reader::Reader;
 
 use pretty_assertions::assert_eq;
 
-/// Tests for https://github.com/tafia/quick-xml/issues/469
-/// Exported to reuse in `async-tokio` tests.
-#[macro_export]
-macro_rules! small_buffers_tests {
-    (
-        #[$test:meta]
-        $read_event:ident: $BufReader:ty
-        $(, $async:ident, $await:ident)?
-    ) => {
-        mod small_buffers {
-            use quick_xml::events::{BytesCData, BytesDecl, BytesPI, BytesStart, BytesText, Event};
-            use quick_xml::reader::Reader;
-            use pretty_assertions::assert_eq;
-
-            #[$test]
-            $($async)? fn decl() {
-                let xml = "<?xml ?>";
-                //         ^^^^^^^ data that fit into buffer
-                let size = xml.match_indices("?>").next().unwrap().0 + 1;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Decl(BytesDecl::from_start(BytesStart::from_content("xml ", 3)))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-
-            #[$test]
-            $($async)? fn pi() {
-                let xml = "<?pi?>";
-                //         ^^^^^ data that fit into buffer
-                let size = xml.match_indices("?>").next().unwrap().0 + 1;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::PI(BytesPI::new("pi"))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-
-            #[$test]
-            $($async)? fn empty() {
-                let xml = "<empty/>";
-                //         ^^^^^^^ data that fit into buffer
-                let size = xml.match_indices("/>").next().unwrap().0 + 1;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Empty(BytesStart::new("empty"))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-
-            #[$test]
-            $($async)? fn cdata1() {
-                let xml = "<![CDATA[cdata]]>";
-                //         ^^^^^^^^^^^^^^^ data that fit into buffer
-                let size = xml.match_indices("]]>").next().unwrap().0 + 1;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::CData(BytesCData::new("cdata"))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-
-            #[$test]
-            $($async)? fn cdata2() {
-                let xml = "<![CDATA[cdata]]>";
-                //         ^^^^^^^^^^^^^^^^ data that fit into buffer
-                let size = xml.match_indices("]]>").next().unwrap().0 + 2;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::CData(BytesCData::new("cdata"))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-
-            #[$test]
-            $($async)? fn comment1() {
-                let xml = "<!--comment-->";
-                //         ^^^^^^^^^^^^ data that fit into buffer
-                let size = xml.match_indices("-->").next().unwrap().0 + 1;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Comment(BytesText::new("comment"))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-
-            #[$test]
-            $($async)? fn comment2() {
-                let xml = "<!--comment-->";
-                //         ^^^^^^^^^^^^^ data that fit into buffer
-                let size = xml.match_indices("-->").next().unwrap().0 + 2;
-                let br = <$BufReader>::with_capacity(size, xml.as_bytes());
-                let mut reader = Reader::from_reader(br);
-                let mut buf = Vec::new();
-
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Comment(BytesText::new("comment"))
-                );
-                assert_eq!(
-                    reader.$read_event(&mut buf) $(.$await)? .unwrap(),
-                    Event::Eof
-                );
-            }
-        }
-    };
-}
+// Import `small_buffers_tests!`
+#[macro_use]
+mod helpers;
 
 small_buffers_tests!(
     #[test]
@@ -411,3 +266,98 @@ mod double_dash {
         assert_eq!(r.read_event().unwrap(), End(BytesEnd::new("hello")));
     }
 }
+
+/// This tests checks that read_to_end() correctly returns span even when
+/// text is trimmed from both sides
+mod read_to_end {
+    use super::*;
+
+    mod borrowed {
+        use super::*;
+        use pretty_assertions::assert_eq;
+
+        #[test]
+        fn text() {
+            let mut r = Reader::from_str("<tag> text </tag>");
+            //                            ^0   ^5    ^11
+            r.config_mut().trim_text(true);
+
+            assert_eq!(r.read_event().unwrap(), Start(BytesStart::new("tag")));
+            assert_eq!(r.read_to_end(QName(b"tag")).unwrap(), 5..11);
+            assert_eq!(r.read_event().unwrap(), Eof);
+        }
+
+        #[test]
+        fn tag() {
+            let mut r = Reader::from_str("<tag> <nested/> </tag>");
+            //                            ^0   ^5         ^16
+            r.config_mut().trim_text(true);
+
+            assert_eq!(r.read_event().unwrap(), Start(BytesStart::new("tag")));
+            assert_eq!(r.read_to_end(QName(b"tag")).unwrap(), 5..16);
+            assert_eq!(r.read_event().unwrap(), Eof);
+        }
+    }
+
+    mod buffered {
+        use super::*;
+        use pretty_assertions::assert_eq;
+
+        #[test]
+        fn text() {
+            let mut r = Reader::from_str("<tag> text </tag>");
+            //                            ^0   ^5    ^11
+            r.config_mut().trim_text(true);
+
+            let mut buf = Vec::new();
+            assert_eq!(
+                r.read_event_into(&mut buf).unwrap(),
+                Start(BytesStart::new("tag"))
+            );
+            assert_eq!(r.read_to_end_into(QName(b"tag"), &mut buf).unwrap(), 5..11);
+            assert_eq!(r.read_event_into(&mut buf).unwrap(), Eof);
+        }
+
+        #[test]
+        fn tag() {
+            let mut r = Reader::from_str("<tag> <nested/> </tag>");
+            //                            ^0   ^5         ^16
+            r.config_mut().trim_text(true);
+
+            let mut buf = Vec::new();
+            assert_eq!(
+                r.read_event_into(&mut buf).unwrap(),
+                Start(BytesStart::new("tag"))
+            );
+            assert_eq!(r.read_to_end_into(QName(b"tag"), &mut buf).unwrap(), 5..16);
+            assert_eq!(r.read_event_into(&mut buf).unwrap(), Eof);
+        }
+    }
+}
+
+/// This tests checks that read_text() correctly returns text even when
+/// text is trimmed from both sides
+mod read_text {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn text() {
+        let mut r = Reader::from_str("<tag> text </tag>");
+        r.config_mut().trim_text(true);
+
+        assert_eq!(r.read_event().unwrap(), Start(BytesStart::new("tag")));
+        assert_eq!(r.read_text(QName(b"tag")).unwrap(), " text ");
+        assert_eq!(r.read_event().unwrap(), Eof);
+    }
+
+    #[test]
+    fn tag() {
+        let mut r = Reader::from_str("<tag> <nested/> </tag>");
+        r.config_mut().trim_text(true);
+
+        assert_eq!(r.read_event().unwrap(), Start(BytesStart::new("tag")));
+        assert_eq!(r.read_text(QName(b"tag")).unwrap(), " <nested/> ");
+        assert_eq!(r.read_event().unwrap(), Eof);
+    }
+}
diff --git a/tests/serde-de-enum.rs b/tests/serde-de-enum.rs
index 8202800..88832fd 100644
--- a/tests/serde-de-enum.rs
+++ b/tests/serde-de-enum.rs
@@ -3,8 +3,8 @@
 use quick_xml::DeError;
 use serde::Deserialize;
 
-mod helpers;
-use helpers::from_str;
+mod serde_helpers;
+use serde_helpers::from_str;
 
 #[derive(Debug, Deserialize, PartialEq)]
 struct Nested {
diff --git a/tests/serde-de-seq.rs b/tests/serde-de-seq.rs
index 24c5016..4ffa410 100644
--- a/tests/serde-de-seq.rs
+++ b/tests/serde-de-seq.rs
@@ -3,8 +3,8 @@
 use quick_xml::DeError;
 use serde::Deserialize;
 
-mod helpers;
-use helpers::from_str;
+mod serde_helpers;
+use serde_helpers::from_str;
 
 /// Check that top-level sequences can be deserialized from the multi-root XML documents
 mod top_level {
diff --git a/tests/serde-de.rs b/tests/serde-de.rs
index f0a4d07..75526b4 100644
--- a/tests/serde-de.rs
+++ b/tests/serde-de.rs
@@ -8,8 +8,8 @@ use serde::de::IgnoredAny;
 use serde::serde_if_integer128;
 use serde::Deserialize;
 
-mod helpers;
-use helpers::from_str;
+mod serde_helpers;
+use serde_helpers::from_str;
 
 /// Tests for deserializing into specially named field `$text` which represent
 /// textual content of an XML element
diff --git a/tests/serde_helpers/mod.rs b/tests/serde_helpers/mod.rs
new file mode 100644
index 0000000..fa6432f
--- /dev/null
+++ b/tests/serde_helpers/mod.rs
@@ -0,0 +1,24 @@
+//! Utility functions for serde integration tests
+
+use quick_xml::de::Deserializer;
+use quick_xml::DeError;
+use serde::Deserialize;
+
+/// Deserialize an instance of type T from a string of XML text.
+/// If deserialization was succeeded checks that all XML events was consumed
+pub fn from_str<'de, T>(source: &'de str) -> Result<T, DeError>
+where
+    T: Deserialize<'de>,
+{
+    // Log XML that we try to deserialize to see it in the failed tests output
+    dbg!(source);
+    let mut de = Deserializer::from_str(source);
+    let result = T::deserialize(&mut de);
+
+    // If type was deserialized, the whole XML document should be consumed
+    if let Ok(_) = result {
+        assert!(de.is_empty(), "the whole XML document should be consumed");
+    }
+
+    result
+}