[librsvg: 11/15] tests: Add tests for rsvg-convert setting the PDF CreationDate
- From: Federico Mena Quintero <federico src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [librsvg: 11/15] tests: Add tests for rsvg-convert setting the PDF CreationDate
- Date: Thu, 13 Feb 2020 01:12:57 +0000 (UTC)
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]