[librsvg: 11/15] tests: Add tests for rsvg-convert setting the PDF CreationDate



commit b21b06ab5840e35f68b992d1a225ce8edeaafd1e
Author: Sven Neumann <sven svenfoo org>
Date:   Mon Feb 10 23:07:14 2020 +0100

    tests: Add tests for rsvg-convert setting the PDF CreationDate
    
    Found issues in cairo and in the lopdf crate and reported them.

 Cargo.lock                        |   1 +
 tests/Cargo.toml                  |   1 +
 tests/src/cmdline/predicates.rs   | 117 +++++++++++++++++++++++++++++++++-----
 tests/src/cmdline/rsvg_convert.rs |  69 +++++++++++++++++++++-
 4 files changed, 171 insertions(+), 17 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 14b11e89..82f8b877 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -715,6 +715,7 @@ name = "librsvg-tests"
 version = "0.1.0"
 dependencies = [
  "assert_cmd 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "lopdf 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "png 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "predicates 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/tests/Cargo.toml b/tests/Cargo.toml
index 1643d410..f8d780a1 100644
--- a/tests/Cargo.toml
+++ b/tests/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2018"
 
 [dev-dependencies]
 assert_cmd = "0.12"
+chrono = "0.3"
 lopdf = "0.23.0"
 png = "0.15.3"
 predicates = "1.0.2"
diff --git a/tests/src/cmdline/predicates.rs b/tests/src/cmdline/predicates.rs
index b0505a9b..f2c170d4 100644
--- a/tests/src/cmdline/predicates.rs
+++ b/tests/src/cmdline/predicates.rs
@@ -1,9 +1,12 @@
+extern crate chrono;
 extern crate lopdf;
 extern crate png;
 extern crate predicates;
 
 pub mod file {
 
+    use chrono::{DateTime, FixedOffset, UTC};
+
     use predicates::boolean::AndPredicate;
     use predicates::prelude::*;
     use predicates::reflection::{Case, Child, PredicateReflection, Product};
@@ -16,8 +19,18 @@ pub mod file {
     pub struct PdfPredicate {}
 
     impl PdfPredicate {
-        pub fn with_page_count(self: Self, num_pages: usize) -> PageCountPredicate<Self> {
-            PageCountPredicate::<Self> { p: self, n: num_pages }
+        pub fn with_page_count(self: Self, num_pages: usize) -> DetailPredicate<Self> {
+            DetailPredicate::<Self> {
+                p: self,
+                d: Detail::PageCount(num_pages),
+            }
+        }
+
+        pub fn with_creation_date(self: Self, when: DateTime<UTC>) -> DetailPredicate<Self> {
+            DetailPredicate::<Self> {
+                p: self,
+                d: Detail::CreationDate(when),
+            }
         }
     }
 
@@ -29,7 +42,7 @@ pub mod file {
         fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option<Case<'a>> {
             match lopdf::Document::load_mem(data) {
                 Ok(_) => None,
-                Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e)))
+                Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
             }
         }
     }
@@ -42,16 +55,30 @@ pub mod file {
         }
     }
 
-    /// Extends a PdfPredicate by a check for a given number of pages.
+    /// Extends a PdfPredicate by a check for page count or creation date.
     #[derive(Debug)]
-    pub struct PageCountPredicate<PdfPredicate> {
+    pub struct DetailPredicate<PdfPredicate> {
         p: PdfPredicate,
-        n: usize
+        d: Detail,
+    }
+
+    #[derive(Debug)]
+    enum Detail {
+        PageCount(usize),
+        CreationDate(DateTime<UTC>),
+    }
+
+    trait Details {
+        fn get_num_pages(&self) -> usize;
+        fn get_creation_date(&self) -> Option<DateTime<UTC>>;
     }
 
-    impl PageCountPredicate<PdfPredicate> {
+    impl DetailPredicate<PdfPredicate> {
         fn eval_doc(&self, doc: &lopdf::Document) -> bool {
-            doc.get_pages().len() == self.n
+            match self.d {
+                Detail::PageCount(n) => n == doc.get_num_pages(),
+                Detail::CreationDate(d) => doc.get_creation_date().map_or(false, |date| date == d),
+            }
         }
 
         fn find_case_for_doc<'a>(
@@ -68,12 +95,71 @@ pub mod file {
         }
 
         fn product_for_doc(&self, doc: &lopdf::Document) -> Product {
-            let actual_count = format!("{} page(s)", doc.get_pages().len());
-            Product::new("actual page count", actual_count)
+            match self.d {
+                Detail::PageCount(_) => Product::new(
+                    "actual page count",
+                    format!("{} page(s)", doc.get_num_pages()),
+                ),
+                Detail::CreationDate(_) => Product::new(
+                    "actual creation date",
+                    format!("{:?}", doc.get_creation_date()),
+                ),
+            }
         }
     }
 
-    impl Predicate<[u8]> for PageCountPredicate<PdfPredicate> {
+    impl Details for lopdf::Document {
+        fn get_creation_date(self: &Self) -> Option<DateTime<UTC>> {
+            fn get_from_trailer<'a>(
+                doc: &'a lopdf::Document,
+                key: &[u8],
+            ) -> lopdf::Result<&'a lopdf::Object> {
+                let id = doc.trailer.get(b"Info")?.as_reference()?;
+                doc.get_object(id)?.as_dict()?.get(key)
+            }
+
+            if let Ok(obj) = get_from_trailer(self, b"CreationDate") {
+                // Now this should actually be as simple as returning obj.as_datetime().
+                // However there are bugs that need to be worked around here:
+                //
+                // First of all cairo inadvertently truncates the timezone offset,
+                // see https://gitlab.freedesktop.org/cairo/cairo/issues/392
+                //
+                // On top of that the lopdf::Object::as_datetime() method has issues
+                // and can not be used, see https://github.com/J-F-Liu/lopdf/issues/88
+                //
+                // So here's our implentation instead.
+
+                fn as_datetime(str: &str) -> Option<DateTime<FixedOffset>> {
+                    if str.ends_with("0000") {
+                        DateTime::parse_from_str(str, "%Y%m%d%H%M%S%z").ok()
+                    } else {
+                        let str = String::from(str) + "00";
+                        as_datetime(&str)
+                    }
+                }
+
+                if let lopdf::Object::String(ref bytes, _) = obj {
+                    if let Ok(str) = String::from_utf8(
+                        bytes
+                            .iter()
+                            .filter(|b| ![b'D', b':', b'\''].contains(b))
+                            .cloned()
+                            .collect(),
+                    ) {
+                        return as_datetime(&str).map(|date| date.with_timezone(&UTC));
+                    }
+                }
+            }
+            None
+        }
+
+        fn get_num_pages(self: &Self) -> usize {
+            self.get_pages().len()
+        }
+    }
+
+    impl Predicate<[u8]> for DetailPredicate<PdfPredicate> {
         fn eval(&self, data: &[u8]) -> bool {
             match lopdf::Document::load_mem(data) {
                 Ok(doc) => self.eval_doc(&doc),
@@ -89,16 +175,19 @@ pub mod file {
         }
     }
 
-    impl PredicateReflection for PageCountPredicate<PdfPredicate> {
+    impl PredicateReflection for DetailPredicate<PdfPredicate> {
         fn children<'a>(&'a self) -> Box<dyn Iterator<Item = Child<'a>> + 'a> {
             let params = vec![Child::new("predicate", &self.p)];
             Box::new(params.into_iter())
         }
     }
 
-    impl fmt::Display for PageCountPredicate<PdfPredicate> {
+    impl fmt::Display for DetailPredicate<PdfPredicate> {
         fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-            write!(f, "is a PDF with {} page(s)", self.n)
+            match self.d {
+                Detail::PageCount(n) => write!(f, "is a PDF with {} page(s)", n),
+                Detail::CreationDate(d) => write!(f, "is a PDF created {:?}", d),
+            }
         }
     }
 
diff --git a/tests/src/cmdline/rsvg_convert.rs b/tests/src/cmdline/rsvg_convert.rs
index 9f482d14..ef0bec83 100644
--- a/tests/src/cmdline/rsvg_convert.rs
+++ b/tests/src/cmdline/rsvg_convert.rs
@@ -1,9 +1,11 @@
 extern crate assert_cmd;
+extern crate chrono;
 extern crate predicates;
 
-use crate::cmdline::predicates::file;
+use super::predicates::file;
 
 use assert_cmd::Command;
+use chrono::{TimeZone, UTC};
 use predicates::prelude::*;
 use std::path::Path;
 
@@ -16,7 +18,7 @@ use std::path::Path;
 //  - limit on output size (32767 pixels) ✔
 //  - output formats (PNG, PDF, PS, EPS, SVG), okay to ignore XML and recording ✔
 //  - multi-page output (for PDF) ✔
-//  - handling of SOURCE_DATA_EPOCH environment variable for PDF output
+//  - handling of SOURCE_DATA_EPOCH environment variable for PDF output ✔
 //  - handling of background color option
 //  - support for optional CSS stylesheet
 //  - error handling for missing SVG dimensions ✔
@@ -37,6 +39,7 @@ impl RsvgConvert {
         let path = Self::binary_location().join("rsvg-convert");
         let mut command = Command::new(path);
         command.env_clear();
+        command.env("TZ", "Berlin");
         command
     }
 
@@ -190,13 +193,73 @@ fn multiple_input_files_accepted_for_ps_output() {
 fn multiple_input_files_create_multi_page_pdf_output() {
     let one = Path::new("fixtures/dimensions/521-with-viewbox.svg");
     let two = Path::new("fixtures/dimensions/sub-rect-no-unit.svg");
+    let three = Path::new("fixtures/api/example.svg");
     RsvgConvert::new()
         .arg("--format=pdf")
         .arg(one)
         .arg(two)
+        .arg(three)
         .assert()
         .success()
-        .stdout(file::is_pdf().with_page_count(2));
+        .stdout(file::is_pdf().with_page_count(3));
+}
+
+#[test]
+fn env_source_data_epoch_controls_pdf_creation_date() {
+    let input = Path::new("fixtures/dimensions/521-with-viewbox.svg");
+    let date = 1581411039; // seconds since epoch
+    RsvgConvert::new()
+        .env("SOURCE_DATE_EPOCH", format!("{}", date))
+        .arg("--format=pdf")
+        .arg(input)
+        .assert()
+        .success()
+        .stdout(file::is_pdf().with_creation_date(UTC.timestamp(date, 0)));
+}
+
+#[test]
+fn env_source_data_epoch_no_digits() {
+    // intentionally not testing for the full error string here
+    let input = Path::new("fixtures/dimensions/521-with-viewbox.svg");
+    RsvgConvert::new()
+        .env("SOURCE_DATE_EPOCH", "foobar")
+        .arg("--format=pdf")
+        .arg(input)
+        .assert()
+        .failure()
+        .stderr(predicates::str::starts_with(
+            "Environment variable $SOURCE_DATE_EPOCH",
+        ));
+}
+
+#[test]
+fn env_source_data_epoch_trailing_garbage() {
+    // intentionally not testing for the full error string here
+    let input = Path::new("fixtures/dimensions/521-with-viewbox.svg");
+    RsvgConvert::new()
+        .arg("--format=pdf")
+        .env("SOURCE_DATE_EPOCH", "1234556+")
+        .arg(input)
+        .assert()
+        .failure()
+        .stderr(predicates::str::starts_with(
+            "Environment variable $SOURCE_DATE_EPOCH",
+        ));
+}
+
+#[test]
+fn env_source_data_epoch_empty() {
+    // intentionally not testing for the full error string here
+    let input = Path::new("fixtures/dimensions/521-with-viewbox.svg");
+    RsvgConvert::new()
+        .arg("--format=pdf")
+        .env("SOURCE_DATE_EPOCH", "")
+        .arg(input)
+        .assert()
+        .failure()
+        .stderr(predicates::str::starts_with(
+            "Environment variable $SOURCE_DATE_EPOCH",
+        ));
 }
 
 #[test]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]