<commit_before>use crate::utils::deserialize::deserialize_duration;
use elasticsearch::Elasticsearch;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
use url::Url;

pub mod configuration;
pub mod explain;
pub(super) mod internal;
pub mod list;
pub mod models;
pub mod query;
pub mod remote;
pub mod status;
pub mod storage;

/// A structure wrapping around the elasticsearch's client.
#[derive(Clone, Debug)]
pub struct ElasticsearchStorage {
    /// Elasticsearch client
    pub(crate) client: Elasticsearch,
    /// Client configuration
    pub config: ElasticsearchStorageConfig,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ElasticsearchStorageConfig {
    pub url: Url,
    #[serde(deserialize_with = "deserialize_duration")]
    pub timeout: Duration,
    pub version_req: String,
    pub scroll_chunk_size: u64,
    pub scroll_pit_alive: String,
    pub insertion_concurrent_requests: usize,
    pub insertion_chunk_size: usize,
    pub create: ElasticsearchStorageConfigCreate,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ElasticsearchStorageConfigCreate {
    pub wait_for_active_shards: String,
    pub number_of_shards: String,
}

impl Default for ElasticsearchStorageConfig {
    /// We retrieve the elasticsearch configuration from ./config/elasticsearch/default.
    fn default() -> Self {
        let config = common::config::config_from(
            &PathBuf::from("config"),
            &["elasticsearch"],
            None,
            None,
            None,
        );

        config
            .expect("cannot build the configuration for testing from config")
            .get("elasticsearch")
            .expect("expected elasticsearch section in configuration from config")
    }
}

impl ElasticsearchStorageConfig {
    pub fn default_testing() -> Self {
        let config_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../config");

        let config = common::config::config_from(
            config_dir.as_path(),
            &["elasticsearch"],
            "testing",
            "MIMIR_TEST",
            None,
        );

        config
            .unwrap_or_else(|_| {
                panic!(
                    "cannot build the configuration for testing from {}",
                    config_dir.display(),
                )
            })
            .get("elasticsearch")
            .unwrap_or_else(|_| {
                panic!(
                    "expected elasticsearch section in configuration from {}",
                    config_dir.display(),
                )
            })
    }
}

#[cfg(test)]
pub mod tests {

    use config::Config;
    use serde::{Deserialize, Serialize};
    use serial_test::serial;

    use super::*;

    use crate::domain::ports::secondary::remote::Remote;
    use crate::domain::ports::secondary::storage::Storage;
    use crate::utils::docker;
    use common::document::{ContainerDocument, Document};

    // In this test we present an invalid configuration (its actually empty) to the
    // create_container function, and expect the error message to be meaningful
    #[tokio::test]
    #[serial]
    async fn should_return_invalid_configuration() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");

        let config = Config::builder().build().expect("build empty config");

        let res = client.create_container(config).await;

        assert!(res
            .unwrap_err()
            .to_string()
            .contains("Container Creation Error: Invalid Configuration"));
    }

    #[tokio::test]
    #[serial]
    async fn should_connect_to_elasticsearch() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let _client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");
    }

    #[tokio::test]
    #[serial]
    async fn should_create_index_with_valid_configuration() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");
        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");
        let config = config::Config::builder()
            .add_source(config::File::from_str(
                r#"{
                    "container": {
                        "name": "foo",
                        "dataset": "bar"
                    },
                    "elasticsearch": {
                        "parameters": {
                            "timeout": "10s",
                            "force_merge": true,
                            "max_number_segments": 1,
                            "wait_for_active_shards": "1"
                        },
                        "settings": {
                            "index": {
                                "number_of_shards": 1,
                                "number_of_replicas": 1
                            }
                        },
                        "mappings": {
                            "properties": {
                                "value": {
                                    "type": "text"
                                }
                            }
                        }
                    }
                }"#,
                config::FileFormat::Json,
            ))
            .build()
            .expect("valid configuration");
        let res = client.create_container(config).await;
        assert!(res.is_ok());
    }

    #[tokio::test]
    #[serial]
    async fn should_correctly_report_invalid_configuration() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");
        let config = config::Config::builder()
            .add_source(config::File::from_str(
                r#"{
                    "container": {
                        "name": "foo",
                        "dataset": "bar"
                    },
                    "elasticsearch": {
                        "parameters": {
                            "timeout": "10s",
                            "wait_for_active_shards": "1"
                        },
                        "settings": {
                            "indx": {
                                "number_of_shards": 1,
                                "number_of_replicas": 1
                            }
                        },
                        "mappings": {
                            "properties": {
                                "value": {
                                    "type": "text"
                                }
                            }
                        }
                    }
                }"#,
                config::FileFormat::Json,
            ))
            .build()
            .expect("valid configuration");
        let res = client.create_container(config).await;
        assert!(res
            .unwrap_err()
            .to_string()
            .starts_with("Container Creation Error"));
    }

    #[derive(Deserialize, Serialize)]
    struct TestObj {
        value: String,
    }

    impl Document for TestObj {
        fn id(&self) -> String {
            self.value.clone()
        }
    }

    impl ContainerDocument for TestObj {
        fn static_doc_type() -> &'static str {
            "test-obj"
        }

        fn default_es_container_config() -> config::Config {
            config::Config::builder()
                .set_default("container.name", Self::static_doc_type())
                .unwrap()
                .set_default("container.dataset", "default")
                .unwrap()
                .set_default("elasticsearch.parameters.timeout", "10s")
                .unwrap()
                .set_default("elasticsearch.parameters.force_merge", true)
                .unwrap()
                .set_default("elasticsearch.parameters.max_number_segments", 1)
                .unwrap()
                .set_default("elasticsearch.parameters.wait_for_active_shards", "1")
                .unwrap()
                .add_source(config::File::from_str(
                    r#"{ "elasticsearch": { "settings": { "index": { "number_of_shards": 1, "number_of_replicas": 1 } } } }"#,
                    config::FileFormat::Json,
                ))
                .add_source(config::File::from_str(
                    r#"{ "elasticsearch": { "mappings": { "properties": { "value": { "type": "text" } } } } }"#,
                    config::FileFormat::Json,
                ))
                .build()
                .expect("invalid container configuration for TestObj")
        }
    }

    #[tokio::test]
    #[serial]
    async fn should_correctly_insert_multiple_documents() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");

        client
            .create_container(TestObj::default_es_container_config())
            .await
            .expect("container creation");

        let documents = vec![
            TestObj {
                value: String::from("obj1"),
            },
            TestObj {
                value: String::from("obj2"),
            },
            TestObj {
                value: String::from("obj3"),
            },
            TestObj {
                value: String::from("obj4"),
            },
            TestObj {
                value: String::from("obj5"),
            },
            TestObj {
                value: String::from("obj6"),
            },
        ];
        let documents = futures::stream::iter(documents);

        let res = client
            .insert_documents(
                String::from("root_obj_dataset_test-index-bulk-insert"),
                documents,
            )
            .await;

        assert_eq!(res.expect("insertion stats").created, 6);
    }

    #[tokio::test]
    #[serial]
    async fn should_detect_invalid_elasticsearch_version() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig {
                version_req: ">=9.99.99".to_string(),
                ..ElasticsearchStorageConfig::default_testing()
            })
            .await;

        assert!(client
            .unwrap_err()
            .to_string()
            .contains("Elasticsearch Invalid version"));
    }
}
<commit_msg>elasticsearch: fix tests for new typed container config<commit_after>use crate::utils::deserialize::deserialize_duration;
use elasticsearch::Elasticsearch;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
use url::Url;

pub mod configuration;
pub mod explain;
pub(super) mod internal;
pub mod list;
pub mod models;
pub mod query;
pub mod remote;
pub mod status;
pub mod storage;

/// A structure wrapping around the elasticsearch's client.
#[derive(Clone, Debug)]
pub struct ElasticsearchStorage {
    /// Elasticsearch client
    pub(crate) client: Elasticsearch,
    /// Client configuration
    pub config: ElasticsearchStorageConfig,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ElasticsearchStorageConfig {
    pub url: Url,
    #[serde(deserialize_with = "deserialize_duration")]
    pub timeout: Duration,
    pub version_req: String,
    pub scroll_chunk_size: u64,
    pub scroll_pit_alive: String,
    pub insertion_concurrent_requests: usize,
    pub insertion_chunk_size: usize,
    pub create: ElasticsearchStorageConfigCreate,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ElasticsearchStorageConfigCreate {
    pub wait_for_active_shards: String,
    pub number_of_shards: String,
}

impl Default for ElasticsearchStorageConfig {
    /// We retrieve the elasticsearch configuration from ./config/elasticsearch/default.
    fn default() -> Self {
        let config = common::config::config_from(
            &PathBuf::from("config"),
            &["elasticsearch"],
            None,
            None,
            None,
        );

        config
            .expect("cannot build the configuration for testing from config")
            .get("elasticsearch")
            .expect("expected elasticsearch section in configuration from config")
    }
}

impl ElasticsearchStorageConfig {
    pub fn default_testing() -> Self {
        let config_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../config");

        let config = common::config::config_from(
            config_dir.as_path(),
            &["elasticsearch"],
            "testing",
            "MIMIR_TEST",
            None,
        );

        config
            .unwrap_or_else(|_| {
                panic!(
                    "cannot build the configuration for testing from {}",
                    config_dir.display(),
                )
            })
            .get("elasticsearch")
            .unwrap_or_else(|_| {
                panic!(
                    "expected elasticsearch section in configuration from {}",
                    config_dir.display(),
                )
            })
    }
}

#[cfg(test)]
pub mod tests {

    use serde::{Deserialize, Serialize};
    use serial_test::serial;

    use super::*;

    use crate::domain::model::configuration::ContainerConfigForceMerge;
    use crate::domain::ports::secondary::storage::Storage;
    use crate::domain::{model::configuration::ContainerConfig, ports::secondary::remote::Remote};
    use crate::utils::docker;
    use common::document::{ContainerDocument, Document};

    #[tokio::test]
    #[serial]
    async fn should_connect_to_elasticsearch() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let _client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");
    }

    #[tokio::test]
    #[serial]
    async fn should_create_index() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");
        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");

        let config = ContainerConfig {
            name: "foo".to_string(),
            dataset: "bar".to_string(),
            force_merge: ContainerConfigForceMerge {
                enabled: true,
                max_number_segments: 1,
            },
        };

        let res = client.create_container(&config).await;
        assert!(res.is_ok());
    }

    #[derive(Deserialize, Serialize)]
    struct TestObj {
        value: String,
    }

    impl Document for TestObj {
        fn id(&self) -> String {
            self.value.clone()
        }
    }

    impl ContainerDocument for TestObj {
        fn static_doc_type() -> &'static str {
            "test-obj"
        }
    }

    #[tokio::test]
    #[serial]
    async fn should_correctly_insert_multiple_documents() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig::default_testing())
            .await
            .expect("Elasticsearch Connection Established");

        let config = ContainerConfig {
            name: TestObj::static_doc_type().to_string(),
            dataset: "default".to_string(),
            force_merge: ContainerConfigForceMerge {
                enabled: true,
                max_number_segments: 1,
            },
        };

        client
            .create_container(&config)
            .await
            .expect("container creation");

        let documents = vec![
            TestObj {
                value: String::from("obj1"),
            },
            TestObj {
                value: String::from("obj2"),
            },
            TestObj {
                value: String::from("obj3"),
            },
            TestObj {
                value: String::from("obj4"),
            },
            TestObj {
                value: String::from("obj5"),
            },
            TestObj {
                value: String::from("obj6"),
            },
        ];
        let documents = futures::stream::iter(documents);

        let res = client
            .insert_documents(
                String::from("root_obj_dataset_test-index-bulk-insert"),
                documents,
            )
            .await;

        assert_eq!(res.expect("insertion stats").created, 6);
    }

    #[tokio::test]
    #[serial]
    async fn should_detect_invalid_elasticsearch_version() {
        docker::initialize()
            .await
            .expect("elasticsearch docker initialization");

        let client = remote::connection_test_pool()
            .conn(ElasticsearchStorageConfig {
                version_req: ">=9.99.99".to_string(),
                ..ElasticsearchStorageConfig::default_testing()
            })
            .await;

        assert!(client
            .unwrap_err()
            .to_string()
            .contains("Elasticsearch Invalid version"));
    }
}
<|endoftext|>


<commit_before>/// Parser

use std::char;

/// A regular expression AST.
#[deriving(ToStr)]
pub enum Expr {
    Empty,
    Range(char, char),
    Concatenate(~[Expr]),
    Alternate(~[Expr]),
    Repeat(~Expr, uint, Option<uint>, Greedy)
}

#[deriving(ToStr)]
pub enum Greedy {
    NonGreedy,
    Greedy
}

/// Parse a regular expression into an AST.  Fails on invalid syntax.
pub fn parse(input: &str) -> Expr {
    let mut s = State::new(input);
    let e = p_alternate(&mut s);
    if s.has_input() {
        // p_alternate() only terminates on an empty string or an extra
        // paren.  Since the string isn't empty, we infer the latter.
        fail!("unbalanced parenthesis")
    } else {
        e
    }
}

/// The parser state.  This tracks the position in the input string.
#[deriving(Clone)]
struct State<'a> {
    input: &'a str,
    prev: Option<&'a str>  // See `State::retreat`
}

impl<'a> State<'a> {
    fn new<'a>(input: &'a str) -> State<'a> {
        State {
            input: input,
            prev: None
        }
    }

    /// Consume and return the next character in the input, returning
    /// `None` if empty.
    fn advance(&mut self) -> Option<char> {
        self.prev = Some(self.input);
        if self.has_input() {
            let (c, input_) = self.input.slice_shift_char();
            self.input = input_;
            Some(c)
        } else {
            None
        }
    }

    /// Push the previously read character back onto the input.  This
    /// can only be called immediately after `advance`.
    fn retreat(&mut self) {
        self.input = self.prev.expect("nowhere to retreat");
        self.prev = None;
    }

    /// Return `true` if there is input remaining.
    fn has_input(&self) -> bool {
        self.input.len() > 0
    }
}

/// Parse alternation, e.g. `ducks|geese|swans`.
///
/// An alternation consists of zero or more concatenations, separated by
/// vertical bars `|`.
///
fn p_alternate(s: &mut State) -> Expr {
    let mut items: ~[Expr] = ~[];

    loop {
        items.push(p_concatenate(s));
        match s.advance() {
            Some(c) => {
                match c {
                    ')' => { s.retreat(); break },
                    '|' => continue,
                    _ => fail!("something bad happened; it's really bad")
                }
            },
            None => break
        }
    }

    match items {
        [] => Empty,
        [e] => e,
        _ => Alternate(items)
    }
}

/// Parse concatenation, e.g. `abc`.
fn p_concatenate(s: &mut State) -> Expr {
    let mut items: ~[Expr] = ~[];

    loop {
        match s.advance() {
            Some(c) => match c {
                '|' | ')' => { s.retreat(); break },
                '(' => {
                    // Parse inside the parens
                    let e = p_alternate(s);
                    // Match the closing paren
                    match s.advance() {
                        Some(')') => push_ignore_empty(&mut items, e),
                        _ => fail!("mismatched parenthesis")
                    }
                },
                '.' => items.push(Range('\0', char::MAX)),
                '?' => {
                    let e = pop_expr(&mut items);
                    items.push(match e {
                        Repeat(_, _, _, NonGreedy) => fail!("multiple repeat"),
                        Repeat(inner, min, max, Greedy) =>
                            Repeat(inner, min, max, NonGreedy),
                        _ => Repeat(~e, 0, Some(1), Greedy)
                    })
                },
                '+' => add_repeat(&mut items, 1, None),
                '*' => add_repeat(&mut items, 0, None),
                '{' => match p_repetition(s) {
                    Some((min, max)) => add_repeat(&mut items, min, max),
                    None => items.push(Range(c, c))
                },
                _ => items.push(Range(c, c))
            },
            None => break
        }
    }

    match items {
        [] => Empty,
        [e] => e,
        _ => Concatenate(items)
    }
}

#[inline]
fn push_ignore_empty(items: &mut ~[Expr], e: Expr) {
    match e {
        Empty => {},
        _ => items.push(e)
    }
}

#[inline]
fn pop_expr(items: &mut ~[Expr]) -> Expr {
    items.pop_opt().expect("nothing to repeat")
}

#[inline]
fn add_repeat(items: &mut ~[Expr], min: uint, max: Option<uint>) {
    let e = pop_expr(items);
    items.push(match e {
        Repeat(..) => fail!("multiple repeat"),
        _ => Repeat(~e, min, max, Greedy)
    })
}

/// Parse a counted repetition (e.g. `a{2,3}`), sans the opening brace.
///
/// The following syntaxes are accepted:
///
/// * `{N}` – match exactly N repetitions;
/// * `{M,}` – at least M;
/// * `{M,N}` – from M to N inclusive;
/// * `{,N}` – at most N.
///
/// If parsing fails, return `None` without consuming input.  This
/// matches Python behavior, where invalid repetitions are ignored.
///
fn p_repetition(s_outer: &mut State) -> Option<(uint, Option<uint>)> {
    // Clone the parser state, so we can backtrack on failure
    let mut s = s_outer.clone();

    let result = {
        let min = p_number(&mut s);
        match s.advance() {
            Some(',') => {
                let max = p_number(&mut s);
                match s.advance() {
                    // {} or {N,} or {,M} or {N,M}
                    Some('}') => Some((min.unwrap_or(0), max)),
                    _ => None
                }
            },
            Some('}') => match min {
                // {N}
                Some(n) => Some((n, Some(n))),
                _ => None
            },
            _ => None
        }
    };

    if result.is_some() {
        // Only consume input if parsing was successful
        s_outer.clone_from(&s);
    }

    result
}

/// Parse a non-negative integer, returning `None` on failure.
fn p_number(s: &mut State) -> Option<uint> {
    let mut acc = None;
    loop {
        match s.advance() {
            Some(c) if '0' <= c && c <= '9' => {
                let digit = c as uint - '0' as uint;
                acc = Some(match acc {
                    Some(n) => 10 * n + digit,
                    None => digit
                });
            },
            _ => { s.retreat(); break }
        }
    }
    acc
}
<commit_msg>Document {,} repetition<commit_after>/// Parser

use std::char;

/// A regular expression AST.
#[deriving(ToStr)]
pub enum Expr {
    Empty,
    Range(char, char),
    Concatenate(~[Expr]),
    Alternate(~[Expr]),
    Repeat(~Expr, uint, Option<uint>, Greedy)
}

#[deriving(ToStr)]
pub enum Greedy {
    NonGreedy,
    Greedy
}

/// Parse a regular expression into an AST.  Fails on invalid syntax.
pub fn parse(input: &str) -> Expr {
    let mut s = State::new(input);
    let e = p_alternate(&mut s);
    if s.has_input() {
        // p_alternate() only terminates on an empty string or an extra
        // paren.  Since the string isn't empty, we infer the latter.
        fail!("unbalanced parenthesis")
    } else {
        e
    }
}

/// The parser state.  This tracks the position in the input string.
#[deriving(Clone)]
struct State<'a> {
    input: &'a str,
    prev: Option<&'a str>  // See `State::retreat`
}

impl<'a> State<'a> {
    fn new<'a>(input: &'a str) -> State<'a> {
        State {
            input: input,
            prev: None
        }
    }

    /// Consume and return the next character in the input, returning
    /// `None` if empty.
    fn advance(&mut self) -> Option<char> {
        self.prev = Some(self.input);
        if self.has_input() {
            let (c, input_) = self.input.slice_shift_char();
            self.input = input_;
            Some(c)
        } else {
            None
        }
    }

    /// Push the previously read character back onto the input.  This
    /// can only be called immediately after `advance`.
    fn retreat(&mut self) {
        self.input = self.prev.expect("nowhere to retreat");
        self.prev = None;
    }

    /// Return `true` if there is input remaining.
    fn has_input(&self) -> bool {
        self.input.len() > 0
    }
}

/// Parse alternation, e.g. `ducks|geese|swans`.
///
/// An alternation consists of zero or more concatenations, separated by
/// vertical bars `|`.
///
fn p_alternate(s: &mut State) -> Expr {
    let mut items: ~[Expr] = ~[];

    loop {
        items.push(p_concatenate(s));
        match s.advance() {
            Some(c) => {
                match c {
                    ')' => { s.retreat(); break },
                    '|' => continue,
                    _ => fail!("something bad happened; it's really bad")
                }
            },
            None => break
        }
    }

    match items {
        [] => Empty,
        [e] => e,
        _ => Alternate(items)
    }
}

/// Parse concatenation, e.g. `abc`.
fn p_concatenate(s: &mut State) -> Expr {
    let mut items: ~[Expr] = ~[];

    loop {
        match s.advance() {
            Some(c) => match c {
                '|' | ')' => { s.retreat(); break },
                '(' => {
                    // Parse inside the parens
                    let e = p_alternate(s);
                    // Match the closing paren
                    match s.advance() {
                        Some(')') => push_ignore_empty(&mut items, e),
                        _ => fail!("mismatched parenthesis")
                    }
                },
                '.' => items.push(Range('\0', char::MAX)),
                '?' => {
                    let e = pop_expr(&mut items);
                    items.push(match e {
                        Repeat(_, _, _, NonGreedy) => fail!("multiple repeat"),
                        Repeat(inner, min, max, Greedy) =>
                            Repeat(inner, min, max, NonGreedy),
                        _ => Repeat(~e, 0, Some(1), Greedy)
                    })
                },
                '+' => add_repeat(&mut items, 1, None),
                '*' => add_repeat(&mut items, 0, None),
                '{' => match p_repetition(s) {
                    Some((min, max)) => add_repeat(&mut items, min, max),
                    None => items.push(Range(c, c))
                },
                _ => items.push(Range(c, c))
            },
            None => break
        }
    }

    match items {
        [] => Empty,
        [e] => e,
        _ => Concatenate(items)
    }
}

#[inline]
fn push_ignore_empty(items: &mut ~[Expr], e: Expr) {
    match e {
        Empty => {},
        _ => items.push(e)
    }
}

#[inline]
fn pop_expr(items: &mut ~[Expr]) -> Expr {
    items.pop_opt().expect("nothing to repeat")
}

#[inline]
fn add_repeat(items: &mut ~[Expr], min: uint, max: Option<uint>) {
    let e = pop_expr(items);
    items.push(match e {
        Repeat(..) => fail!("multiple repeat"),
        _ => Repeat(~e, min, max, Greedy)
    })
}

/// Parse a counted repetition (e.g. `a{2,3}`), sans the opening brace.
///
/// The following syntaxes are accepted:
///
/// * `{N}` – match exactly N repetitions;
/// * `{M,}` – at least M;
/// * `{,N}` – at most N;
/// * `{M,N}` – from M to N inclusive;
/// * `{,}` – zero or more (synonymous with `*`).
///
/// If parsing fails, return `None` without consuming input.  This
/// matches Python behavior, where invalid repetitions are ignored.
///
fn p_repetition(s_outer: &mut State) -> Option<(uint, Option<uint>)> {
    // Clone the parser state, so we can backtrack on failure
    let mut s = s_outer.clone();

    let result = {
        let min = p_number(&mut s);
        match s.advance() {
            Some(',') => {
                let max = p_number(&mut s);
                match s.advance() {
                    // {} or {N,} or {,M} or {N,M}
                    Some('}') => Some((min.unwrap_or(0), max)),
                    _ => None
                }
            },
            Some('}') => match min {
                // {N}
                Some(n) => Some((n, Some(n))),
                _ => None
            },
            _ => None
        }
    };

    if result.is_some() {
        // Only consume input if parsing was successful
        s_outer.clone_from(&s);
    }

    result
}

/// Parse a non-negative integer, returning `None` on failure.
fn p_number(s: &mut State) -> Option<uint> {
    let mut acc = None;
    loop {
        match s.advance() {
            Some(c) if '0' <= c && c <= '9' => {
                let digit = c as uint - '0' as uint;
                acc = Some(match acc {
                    Some(n) => 10 * n + digit,
                    None => digit
                });
            },
            _ => { s.retreat(); break }
        }
    }
    acc
}
<|endoftext|>


<commit_before>extern crate twig;
extern crate serde_json;
extern crate difference;
extern crate term;

use std::collections::HashMap;
use std::io::{self, Read, BufReader, BufRead};
use std::env;
use std::io::Write;
use difference::Difference;
use std::fs::{self, DirEntry, File};
use std::path::Path;

use twig::loader::ArrayLoader;
use twig::{ Environment, Engine, Config };

#[test]
fn fixtures() {
    let errors = visit_fixtures(&env::current_dir().unwrap().join("tests").join("fixtures"), &|entry| {
        let f = match File::open(entry.path()) {
            Ok(f) => f,
            Err(e) => panic!("error opening fixture file {:?}, {:?}", entry.path(), e),
        };
        let fixture = match Fixture::new(f) {
            Err(e) => panic!("invalid test {:?}", e),
            Ok(f) => f,
        };

        let message = match fixture.message.clone() {
            Some(m) => m,
            None => panic!("fixture {:?} must have a message", entry.path()),
        };
        print_fixture_start(&message);

        let twig = Engine::new(ArrayLoader::new(
            vec![("index.twig".into(), fixture.template.expect("fixture must contain main template"))]
                .into_iter()
                .chain(fixture.templates.into_iter())
                .collect()
        ), match fixture.config {
            Some(config) => Environment::new(Config::from_hashmap(
                match serde_json::from_str(&config) {
                    Ok(map) => map,
                    Err(e) => panic!("failed to deserialize template config: {:#?}", e),
                }
            )),
            _ => Environment::default(),
        });

        let data = match fixture.data {
            Some(data) => match serde_json::from_str::<HashMap<String, String>>(&data) {
                Ok(map) => map,
                Err(e) => panic!("failed to deserialize template data: {:#?}", e),
            },
            None => HashMap::new(),
        };

        let res = match twig.get("index.twig", data.iter().map(|(k, v)| (&k[..], &v[..])).collect()) {
            Ok(res) => res,
            Err(e) => panic!("error executing template: {:#?}", e),
        };

        let expected = fixture.expect.expect("fixture must have expect block");

        if res != expected {
            print_fixture_result(false);

            let (_, changeset) = difference::diff(
                &res,
                &expected,
                "\n"
            );
            print_diff(&changeset);

            Some((
                entry.path().to_string_lossy().into_owned(),
                message,
                changeset
            ))
            //assert_eq!(res, expected);
        } else {
            print_fixture_result(true);
            None
        }
    }).unwrap();

    let num_errors = errors.len();
    if num_errors > 0 {
        for (file, name, changeset) in errors {
            println!("in {}", file);
            println!("testing {}", name);
            print_uncolored(&changeset);
        }
        //panic!("{} fixtures produced errors", num_errors);
    }
}

fn visit_fixtures(dir: &Path, cb: &Fn(&DirEntry) -> Option<(String, String, Vec<Difference>)>) -> io::Result<Vec<(String, String, Vec<Difference>)>> {
    let mut errors = Vec::new();
    if try!(fs::metadata(dir)).is_dir() {
        for entry in try!(fs::read_dir(dir)) {
            let entry = try!(entry);
            if try!(fs::metadata(entry.path())).is_dir() {
                for e in try!(visit_fixtures(&entry.path(), cb)) {
                    errors.push(e);
                }
            } else {
                if let Some(Some("test")) = entry.path().extension().map(|v| v.to_str()) {
                    if let Some(err) = cb(&entry) {
                        errors.push(err);
                    }
                }
            }
        }
    }
    Ok(errors)
}

#[derive(Debug)]
enum FixtureError {
    ExpectedBlockStart,
    IoError,
}

#[derive(Debug)]
enum TemplateName {
    Main,
    Other(String),
}

#[derive(Debug)]
enum ReadState {
    Message(String),
    Template((TemplateName, String)),
    Data(String),
    Config(String),
    Expect(String)
}

#[derive(Debug)]
struct Fixture {
    message: Option<String>,
    template: Option<String>,
    templates: Vec<(String, String)>,
    data: Option<String>,
    config: Option<String>,
    expect: Option<String>,
}

const TEMPLATE_NAME_START: &'static str = "--TEMPLATE(";
const TEMPLATE_NAME_END: &'static str = ")--";

fn check_for_new_state(line: &str) -> Option<ReadState> {
    match &line[..] {
        "--TEST--" => Some(ReadState::Message(String::new())),
        "--DATA--" => Some(ReadState::Data(String::new())),
        "--CONFIG--" => Some(ReadState::Config(String::new())),
        "--EXPECT--" => Some(ReadState::Expect(String::new())),
        other => if other.starts_with("--TEMPLATE(") {
            let name = other[TEMPLATE_NAME_START.len()..other.len()-TEMPLATE_NAME_END.len()].to_string();
            Some(ReadState::Template((TemplateName::Other(name), String::new())))
        } else if other.starts_with("--TEMPLATE--") {
            Some(ReadState::Template((TemplateName::Main, String::new())))
        } else {
            None
        },
    }
}

impl Fixture {
    pub fn new<R: Read>(input: R) -> Result<Fixture, FixtureError> {
        let mut state = None;
        let mut fixture = Fixture {
            message: None,
            template: None,
            templates: Vec::new(),
            data: None,
            config: None,
            expect: None,
        };

        for maybe_line in BufReader::new(input).lines() {
            let line = match maybe_line {
                Ok(l) => l,
                Err(_) => return Err(FixtureError::IoError),
            };

            state = match state {
                None => {
                    Some(try!(check_for_new_state(&line).ok_or(FixtureError::ExpectedBlockStart)))
                },
                Some(mut old) => match check_for_new_state(&line) {
                    Some(new_state) => {
                        fixture.collect(old);
                        Some(new_state)
                    },
                    None => {
                        match old {
                            ReadState::Message(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); },
                            ReadState::Template((_, ref mut m)) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Data(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Config(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Expect(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                        };
                        Some(old)
                    }
                },
            }
        }

        if let Some(leftover_state) = state {
            fixture.collect(leftover_state);
        }

        Ok(fixture)
    }

    fn collect(&mut self, state: ReadState) {
        match state {
            ReadState::Message(m) => self.message = Some(m),
            ReadState::Template((TemplateName::Main, m)) => self.template = Some(m),
            ReadState::Template((TemplateName::Other(name), m)) => {
                self.templates.push((name, m));
            },
            ReadState::Data(m) => self.data = Some(m),
            ReadState::Config(m) => self.config = Some(m),
            ReadState::Expect(m) => self.expect = Some(m),
        }
    }
}

fn print_fixture_start(message: &str) {
    let mut t = term::stdout().unwrap();
    write!(t, "fixture ");
    t.attr(term::Attr::Bold).unwrap();
    write!(t, "{}", message);
    t.reset().unwrap();
    write!(t, " ... ");
    t.flush().unwrap();
}

fn print_fixture_result(ok: bool) {
    let mut t = term::stdout().unwrap();
    if ok {
        t.fg(term::color::GREEN).unwrap();
        writeln!(t, "ok");
    } else {
        t.fg(term::color::RED).unwrap();
        writeln!(t, "ERROR!");
    }
    t.reset().unwrap();
    t.flush().unwrap();
}

fn print_diff(changeset: &Vec<Difference>) {
    let mut t = term::stdout().unwrap();

    for i in 0..changeset.len() {
        match changeset[i] {
            Difference::Same(ref x) => {
                t.reset().unwrap();
                writeln!(t, "  {}", x);
            },
            Difference::Add(ref x) => {
                t.fg(term::color::GREEN).unwrap();
                for line in x.lines() {
                    writeln!(t, "+ {}", line);
                }
            },
            Difference::Rem(ref x) => {
                t.fg(term::color::RED).unwrap();
                for line in x.lines() {
                    writeln!(t, "- {}", line);
                }
            }
        }
    }
    t.reset().unwrap();
    writeln!(t, "");
    t.flush().unwrap();
}

fn print_uncolored(changeset: &Vec<Difference>) {
    for i in 0..changeset.len() {
        match changeset[i] {
            Difference::Same(ref x) => {
                println!("  {}", x);
            },
            Difference::Add(ref x) => {
                for line in x.lines() {
                    println!("+ {}", line);
                }
            },
            Difference::Rem(ref x) => {
                for line in x.lines() {
                    println!("- {}", line);
                }
            }
        }
    }
    println!("");
}
<commit_msg>Formatting test.<commit_after>extern crate twig;
extern crate serde_json;
extern crate difference;
extern crate term;

use std::collections::HashMap;
use std::io::{self, Read, BufReader, BufRead};
use std::env;
use std::io::Write;
use difference::Difference;
use std::fs::{self, DirEntry, File};
use std::path::Path;

use twig::loader::ArrayLoader;
use twig::{ Environment, Engine, Config };

#[test]
fn fixtures() {
    let errors = visit_fixtures(&env::current_dir().unwrap().join("tests").join("fixtures"), &|entry| {
        let f = match File::open(entry.path()) {
            Ok(f) => f,
            Err(e) => panic!("error opening fixture file {:?}, {:?}", entry.path(), e),
        };
        let fixture = match Fixture::new(f) {
            Err(e) => panic!("invalid test {:?}", e),
            Ok(f) => f,
        };

        let message = match fixture.message.clone() {
            Some(m) => m,
            None => panic!("fixture {:?} must have a message", entry.path()),
        };
        print_fixture_start(&message);

        let twig = Engine::new(ArrayLoader::new(
            vec![("index.twig".into(), fixture.template.expect("fixture must contain main template"))]
                .into_iter()
                .chain(fixture.templates.into_iter())
                .collect()
        ), match fixture.config {
            Some(config) => Environment::new(Config::from_hashmap(
                match serde_json::from_str(&config) {
                    Ok(map) => map,
                    Err(e) => panic!("failed to deserialize template config: {:#?}", e),
                }
            )),
            _ => Environment::default(),
        });

        let data = match fixture.data {
            Some(data) => match serde_json::from_str::<HashMap<String, String>>(&data) {
                Ok(map) => map,
                Err(e) => panic!("failed to deserialize template data: {:#?}", e),
            },
            None => HashMap::new(),
        };

        let res = match twig.get("index.twig", data.iter().map(|(k, v)| (&k[..], &v[..])).collect()) {
            Ok(res) => res,
            Err(e) => panic!("error executing template: {:#?}", e),
        };

        let expected = fixture.expect.expect("fixture must have expect block");

        if res != expected {
            print_fixture_result(false);

            let (_, changeset) = difference::diff(
                &res,
                &expected,
                "\n"
            );
            print_diff(&changeset);

            Some((
                entry.path().to_string_lossy().into_owned(),
                message,
                changeset
            ))
            //assert_eq!(res, expected);
        } else {
            print_fixture_result(true);
            None
        }
    }).unwrap();

    let num_errors = errors.len();
    if num_errors > 0 {
        for (file, name, changeset) in errors {
            println!("in {}", file);
            println!("testing {}", name);
            print_uncolored(&changeset);
        }
        //panic!("{} fixtures produced errors", num_errors);
    }
}

fn visit_fixtures(dir: &Path, cb: &Fn(&DirEntry) -> Option<(String, String, Vec<Difference>)>) -> io::Result<Vec<(String, String, Vec<Difference>)>> {
    let mut errors = Vec::new();
    if try!(fs::metadata(dir)).is_dir() {
        for entry in try!(fs::read_dir(dir)) {
            let entry = try!(entry);
            if try!(fs::metadata(entry.path())).is_dir() {
                for e in try!(visit_fixtures(&entry.path(), cb)) {
                    errors.push(e);
                }
            } else {
                if let Some(Some("test")) = entry.path().extension().map(|v| v.to_str()) {
                    if let Some(err) = cb(&entry) {
                        errors.push(err);
                    }
                }
            }
        }
    }
    Ok(errors)
}

#[derive(Debug)]
enum FixtureError {
    ExpectedBlockStart,
    IoError,
}

#[derive(Debug)]
enum TemplateName {
    Main,
    Other(String),
}

#[derive(Debug)]
enum ReadState {
    Message(String),
    Template((TemplateName, String)),
    Data(String),
    Config(String),
    Expect(String)
}

#[derive(Debug)]
struct Fixture {
    message: Option<String>,
    template: Option<String>,
    templates: Vec<(String, String)>,
    data: Option<String>,
    config: Option<String>,
    expect: Option<String>,
}

const TEMPLATE_NAME_START: &'static str = "--TEMPLATE(";
const TEMPLATE_NAME_END: &'static str = ")--";

fn check_for_new_state(line: &str) -> Option<ReadState> {
    match &line[..] {
        "--TEST--" => Some(ReadState::Message(String::new())),
        "--DATA--" => Some(ReadState::Data(String::new())),
        "--CONFIG--" => Some(ReadState::Config(String::new())),
        "--EXPECT--" => Some(ReadState::Expect(String::new())),
        other => if other.starts_with("--TEMPLATE(") {
            let name = other[TEMPLATE_NAME_START.len()..other.len()-TEMPLATE_NAME_END.len()].to_string();
            Some(ReadState::Template((TemplateName::Other(name), String::new())))
        } else if other.starts_with("--TEMPLATE--") {
            Some(ReadState::Template((TemplateName::Main, String::new())))
        } else {
            None
        },
    }
}

impl Fixture {
    pub fn new<R: Read>(input: R) -> Result<Fixture, FixtureError> {
        let mut state = None;
        let mut fixture = Fixture {
            message: None,
            template: None,
            templates: Vec::new(),
            data: None,
            config: None,
            expect: None,
        };

        for maybe_line in BufReader::new(input).lines() {
            let line = match maybe_line {
                Ok(l) => l,
                Err(_) => return Err(FixtureError::IoError),
            };

            state = match state {
                None => {
                    Some(try!(check_for_new_state(&line).ok_or(FixtureError::ExpectedBlockStart)))
                },
                Some(mut old) => match check_for_new_state(&line) {
                    Some(new_state) => {
                        fixture.collect(old);
                        Some(new_state)
                    },
                    None => {
                        match old {
                            ReadState::Message(ref mut m) => { if m.len() > 0 { m.push_str("\n"); } m.push_str(&line); },
                            ReadState::Template((_, ref mut m)) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Data(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Config(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                            ReadState::Expect(ref mut m) => { if m.len() > 0 { m.push_str("\n"); }  m.push_str(&line); },
                        };
                        Some(old)
                    }
                },
            }
        }

        if let Some(leftover_state) = state {
            fixture.collect(leftover_state);
        }

        Ok(fixture)
    }

    fn collect(&mut self, state: ReadState) {
        match state {
            ReadState::Message(m) => self.message = Some(m),
            ReadState::Template((TemplateName::Main, m)) => self.template = Some(m),
            ReadState::Template((TemplateName::Other(name), m)) => {
                self.templates.push((name, m));
            },
            ReadState::Data(m) => self.data = Some(m),
            ReadState::Config(m) => self.config = Some(m),
            ReadState::Expect(m) => self.expect = Some(m),
        }
    }
}

fn print_fixture_start(message: &str) {
    let mut t = term::stdout().unwrap();
    write!(t, "fixture ");
    t.attr(term::Attr::Bold).unwrap();
    write!(t, "{}", message);
    t.reset().unwrap();
    write!(t, " ... ");
    t.flush().unwrap();
}

fn print_fixture_result(ok: bool) {
    let mut t = term::stdout().unwrap();
    if ok {
        t.fg(term::color::GREEN).unwrap();
        writeln!(t, "ok");
    } else {
        t.fg(term::color::RED).unwrap();
        writeln!(t, "ERROR!");
    }
    t.reset().unwrap();
    t.flush().unwrap();
}

fn print_diff(changeset: &Vec<Difference>) {
    let mut t = term::stdout().unwrap();

    for i in 0..changeset.len() {
        match changeset[i] {
            Difference::Same(ref x) => {
                t.reset().unwrap();
                writeln!(t, "  {}", x);
            },
            Difference::Add(ref x) => {
                for line in x.lines() {
                    t.fg(term::color::GREEN).unwrap();
                    writeln!(t, "+ {}", line);
                }
            },
            Difference::Rem(ref x) => {
                t.fg(term::color::RED).unwrap();
                for line in x.lines() {
                    writeln!(t, "- {}", line);
                }
            }
        }
    }
    t.reset().unwrap();
    writeln!(t, "");
    t.flush().unwrap();
}

fn print_uncolored(changeset: &Vec<Difference>) {
    for i in 0..changeset.len() {
        match changeset[i] {
            Difference::Same(ref x) => {
                println!("  {}", x);
            },
            Difference::Add(ref x) => {
                for line in x.lines() {
                    println!("+ {}", line);
                }
            },
            Difference::Rem(ref x) => {
                for line in x.lines() {
                    println!("- {}", line);
                }
            }
        }
    }
    println!("");
}
<|endoftext|>


<commit_before>use codespan::{FileId, Files, LineIndex, Span};
use std::io;
use termcolor::WriteColor;

use crate::diagnostic::Label;
use crate::term::Config;

use super::{Locus, NewLine};

mod border;
mod gutter;
mod note;
mod underline;

use self::border::{BorderLeft, BorderLeftBreak, BorderTop, BorderTopLeft};
use self::gutter::Gutter;
use self::note::Note;
use self::underline::{Underline, UnderlineBottom, UnderlineLeft, UnderlineTop, UnderlineTopLeft};

pub use self::underline::MarkStyle;

/// An underlined snippet of source code.
///
/// ```text
///   ┌── test:2:9 ───
///   │
/// 2 │ (+ test "")
///   │         ^^ expected `Int` but found `String`
///   │
///   = expected type `Int`
///        found type `String`
/// ```
pub struct SourceSnippet<'a> {
    file_id: FileId,
    spans: Vec<(&'a Label, MarkStyle)>,
    notes: &'a [String],
}

impl<'a> SourceSnippet<'a> {
    pub fn new(file_id: FileId, spans: Vec<(&'a Label, MarkStyle)>, notes: &'a [String]) -> Self {
        SourceSnippet {
            file_id,
            spans,
            notes,
        }
    }

    fn source_locus_spans(&self) -> (Span, Span) {
        let mut source_span = None;
        let mut locus_span = None;

        for (label, mark_style) in &self.spans {
            source_span =
                Some(source_span.map_or(label.span, |span| Span::merge(span, label.span)));
            if let MarkStyle::Primary(_) = mark_style {
                locus_span =
                    Some(locus_span.map_or(label.span, |span| Span::merge(span, label.span)));
            }
        }

        let source_span = source_span.unwrap_or(Span::initial());
        let locus_span = locus_span.unwrap_or(source_span);

        (source_span, locus_span)
    }

    pub fn emit(
        &self,
        files: &'a Files<impl AsRef<str>>,
        writer: &mut (impl WriteColor + ?Sized),
        config: &Config,
    ) -> io::Result<()> {
        // Note: All of the things we need with `files` is done here. Could this
        // help us decide on a decent trait for the file provider?
        let file_name = files.name(self.file_id);
        let location = |byte_index| files.location(self.file_id, byte_index);
        let line_span = |line_index| files.line_span(self.file_id, line_index);
        let source_slice = |span, tab| {
            // NOTE: Not sure if we can do this more efficiently? Perhaps a custom
            // writer might be better?
            files
                .source_slice(self.file_id, span)
                .map(|s| s.replace('\t', tab))
        };

        let (source_span, locus_span) = self.source_locus_spans();
        let locus_start = location(locus_span.start()).expect("locus_span_start");
        let source_end = location(source_span.end()).expect("source_span_end");

        // Use the length of the last line number as the gutter padding
        let gutter_padding = format!("{}", source_end.line.number()).len();
        // Cache the tabs we'll be using to pad the source strings.
        let tab = config.tab_padding();

        // Top left border and locus.
        //
        // ```text
        // ┌── test:2:9 ───
        // ```

        Gutter::new(None, gutter_padding).emit(writer, config)?;
        BorderTopLeft::new().emit(writer, config)?;
        BorderTop::new(2).emit(writer, config)?;
        write!(writer, " ")?;

        Locus::new(file_name, locus_start).emit(writer, config)?;

        write!(writer, " ")?;
        BorderTop::new(3).emit(writer, config)?;
        NewLine::new().emit(writer, config)?;

        // TODO: Better grouping
        for (i, (label, mark_style)) in self.spans.iter().enumerate() {
            let start = location(label.span.start()).expect("location_start");
            let end = location(label.span.end()).expect("location_end");
            let start_line_span = line_span(start.line).expect("start_line_span");
            let end_line_span = line_span(end.line).expect("end_line_span");

            let label_style = mark_style.label_style(config);

            // Code snippet
            //
            // ```text
            //   │
            // 2 │ (+ test "")
            //   │         ^^ expected `Int` but found `String`
            //   │
            // ```

            // Write initial border
            Gutter::new(None, gutter_padding).emit(writer, config)?;
            match i {
                0 => BorderLeft::new().emit(writer, config)?,
                _ => BorderLeftBreak::new().emit(writer, config)?,
            }
            NewLine::new().emit(writer, config)?;

            // Write underlined source section
            if start.line == end.line {
                // Single line
                //
                // ```text
                // 2 │ (+ test "")
                //   │         ^^ expected `Int` but found `String`
                // ```

                let prefix_source = {
                    let span = Span::new(start_line_span.start(), label.span.start());
                    source_slice(span, &tab).expect("prefix_source")
                };
                let highlighted_source = {
                    let span = label.span;
                    source_slice(span, &tab).expect("highlighted_source")
                };
                let suffix_source = {
                    let span = Span::new(label.span.end(), end_line_span.end());
                    source_slice(span, &tab).expect("suffix_source")
                };

                // Write line number and border
                Gutter::new(start.line.number(), gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;

                // Write line source
                write!(writer, " {}", prefix_source)?;
                writer.set_color(label_style)?;
                write!(writer, "{}", highlighted_source)?;
                writer.reset()?;
                write!(writer, "{}", suffix_source.trim_end())?;
                NewLine::new().emit(writer, config)?;

                // Write border, underline, and label
                Gutter::new(None, gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;
                Underline::new(
                    *mark_style,
                    &prefix_source,
                    &highlighted_source,
                    &label.message,
                )
                .emit(writer, config)?;
                NewLine::new().emit(writer, config)?;
            } else {
                // Multiple lines
                //
                // ```text
                // 4 │   fizz₁ num = case (mod num 5) (mod num 3) of
                //   │ ╭─────────────^
                // 5 │ │     0 0 => "FizzBuzz"
                // 6 │ │     0 _ => "Fizz"
                // 7 │ │     _ 0 => "Buzz"
                // 8 │ │     _ _ => num
                //   │ ╰──────────────^ `case` clauses have incompatible types
                // ```

                let prefix_source = {
                    let span = Span::new(start_line_span.start(), label.span.start());
                    source_slice(span, &tab).expect("prefix_source")
                };
                let highlighted_source = {
                    let span = Span::new(label.span.start(), start_line_span.end());
                    source_slice(span, &tab).expect("highlighted_source_1")
                };

                if prefix_source.trim().is_empty() {
                    // Section is prefixed by empty space, so we don't need to take
                    // up a new line.
                    //
                    // ```text
                    // 4 │ ╭     case (mod num 5) (mod num 3) of
                    // ```

                    // Write line number, border, and underline
                    Gutter::new(start.line.number(), gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;
                    UnderlineTopLeft::new(*mark_style).emit(writer, config)?;

                    // Write source line
                    write!(writer, " {}", prefix_source)?;
                    writer.set_color(&label_style)?;
                    write!(writer, "{}", highlighted_source.trim_end())?;
                    writer.reset()?;
                    NewLine::new().emit(writer, config)?;
                } else {
                    // There's source code in the prefix, so run an underline
                    // underneath it to get to the start of the span.
                    //
                    // ```text
                    // 4 │   fizz₁ num = case (mod num 5) (mod num 3) of
                    //   │ ╭─────────────^
                    // ```

                    // Write line number and border
                    Gutter::new(start.line.number(), gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;

                    // Write source line
                    write!(writer, "   {}", prefix_source)?;
                    writer.set_color(&label_style)?;
                    write!(writer, "{}", highlighted_source.trim_end())?;
                    writer.reset()?;
                    NewLine::new().emit(writer, config)?;

                    // Write border and underline
                    Gutter::new(None, gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;
                    UnderlineTop::new(*mark_style, &prefix_source).emit(writer, config)?;
                    NewLine::new().emit(writer, config)?;
                }

                // Write highlighted lines
                //
                // ```text
                // 5 │ │     0 0 => "FizzBuzz"
                // 6 │ │     0 _ => "Fizz"
                // 7 │ │     _ 0 => "Buzz"
                // ```
                for line_index in ((start.line.to_usize() + 1)..end.line.to_usize())
                    .map(|i| LineIndex::from(i as u32))
                {
                    let highlighted_source = {
                        let span = line_span(line_index).expect("highlighted_span");
                        source_slice(span, &tab).expect("highlighted_source_2")
                    };

                    // Write line number, border, and underline
                    Gutter::new(line_index.number(), gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;
                    UnderlineLeft::new(*mark_style).emit(writer, config)?;

                    // Write highlighted source
                    writer.set_color(label_style)?;
                    write!(writer, " {}", highlighted_source.trim_end())?;
                    writer.reset()?;
                    NewLine::new().emit(writer, config)?;
                }

                // Write last highlighted line
                //
                // ```text
                // 8 │ │     _ _ => num
                //   │ ╰──────────────^ `case` clauses have incompatible types
                // ```
                let highlighted_source = {
                    let span = Span::new(end_line_span.start(), label.span.end());
                    source_slice(span, &tab).expect("highlighted_source_3")
                };
                let suffix_source = {
                    let span = Span::new(label.span.end(), end_line_span.end());
                    source_slice(span, &tab).expect("suffix_source")
                };

                // Write line number, border, and underline
                Gutter::new(end.line.number(), gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;
                UnderlineLeft::new(*mark_style).emit(writer, config)?;

                // Write line source
                writer.set_color(label_style)?;
                write!(writer, " {}", highlighted_source)?;
                writer.reset()?;
                write!(writer, "{}", suffix_source.trim_end())?;
                NewLine::new().emit(writer, config)?;

                // Write border, underline, and label
                Gutter::new(None, gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;
                UnderlineBottom::new(*mark_style, &highlighted_source, &label.message)
                    .emit(writer, config)?;
                NewLine::new().emit(writer, config)?;
            }
        }

        // Write final border
        Gutter::new(None, gutter_padding).emit(writer, config)?;
        BorderLeft::new().emit(writer, config)?;
        NewLine::new().emit(writer, config)?;

        // Additional notes
        //
        // ```text
        // = expected type `Int`
        //      found type `String`
        // ```

        for note in self.notes {
            Note::new(gutter_padding, note).emit(writer, config)?;
        }

        Ok(())
    }
}
<commit_msg>Simplify usage of Files some more<commit_after>use codespan::{FileId, Files, LineIndex, Span};
use std::io;
use termcolor::WriteColor;

use crate::diagnostic::Label;
use crate::term::Config;

use super::{Locus, NewLine};

mod border;
mod gutter;
mod note;
mod underline;

use self::border::{BorderLeft, BorderLeftBreak, BorderTop, BorderTopLeft};
use self::gutter::Gutter;
use self::note::Note;
use self::underline::{Underline, UnderlineBottom, UnderlineLeft, UnderlineTop, UnderlineTopLeft};

pub use self::underline::MarkStyle;

/// An underlined snippet of source code.
///
/// ```text
///   ┌── test:2:9 ───
///   │
/// 2 │ (+ test "")
///   │         ^^ expected `Int` but found `String`
///   │
///   = expected type `Int`
///        found type `String`
/// ```
pub struct SourceSnippet<'a> {
    file_id: FileId,
    spans: Vec<(&'a Label, MarkStyle)>,
    notes: &'a [String],
}

impl<'a> SourceSnippet<'a> {
    pub fn new(file_id: FileId, spans: Vec<(&'a Label, MarkStyle)>, notes: &'a [String]) -> Self {
        SourceSnippet {
            file_id,
            spans,
            notes,
        }
    }

    fn source_locus_spans(&self) -> (Span, Span) {
        let mut source_span = None;
        let mut locus_span = None;

        for (label, mark_style) in &self.spans {
            source_span =
                Some(source_span.map_or(label.span, |span| Span::merge(span, label.span)));
            if let MarkStyle::Primary(_) = mark_style {
                locus_span =
                    Some(locus_span.map_or(label.span, |span| Span::merge(span, label.span)));
            }
        }

        let source_span = source_span.unwrap_or(Span::initial());
        let locus_span = locus_span.unwrap_or(source_span);

        (source_span, locus_span)
    }

    pub fn emit(
        &self,
        files: &'a Files<impl AsRef<str>>,
        writer: &mut (impl WriteColor + ?Sized),
        config: &Config,
    ) -> io::Result<()> {
        // NOTE: All of the things we need with `files` is done here. Could this
        // help us decide on a decent trait for the file provider?
        let file_name = files.name(self.file_id);
        let location = |byte_index| files.location(self.file_id, byte_index);
        let get_line = |line_index| {
            // NOTE: We could simplify this into a single `get_line` method
            let span = files.line_span(self.file_id, line_index).ok()?;
            let source = files.source_slice(self.file_id, span).ok()?;
            Some((span, source))
        };

        let (source_span, locus_span) = self.source_locus_spans();
        let locus_start = location(locus_span.start()).expect("locus_span_start");
        let source_end = location(source_span.end()).expect("source_span_end");

        // Use the length of the last line number as the gutter padding
        let gutter_padding = format!("{}", source_end.line.number()).len();
        // Cache the tabs we'll be using to pad the source strings.
        let tab = config.tab_padding();
        let replace_tabs = |source: &str| {
            // NOTE: Not sure if we can do this more efficiently?
            // Perhaps a custom writer might be better?
            source.replace('\t', &tab)
        };

        // Top left border and locus.
        //
        // ```text
        // ┌── test:2:9 ───
        // ```

        Gutter::new(None, gutter_padding).emit(writer, config)?;
        BorderTopLeft::new().emit(writer, config)?;
        BorderTop::new(2).emit(writer, config)?;
        write!(writer, " ")?;

        Locus::new(file_name, locus_start).emit(writer, config)?;

        write!(writer, " ")?;
        BorderTop::new(3).emit(writer, config)?;
        NewLine::new().emit(writer, config)?;

        // TODO: Better grouping
        for (i, (label, mark_style)) in self.spans.iter().enumerate() {
            let start = location(label.span.start()).expect("location_start");
            let end = location(label.span.end()).expect("location_end");
            let (start_line_span, start_line) = get_line(start.line).expect("start_line_span");
            let (end_line_span, end_line) = get_line(end.line).expect("end_line_span");

            let label_style = mark_style.label_style(config);

            // Code snippet
            //
            // ```text
            //   │
            // 2 │ (+ test "")
            //   │         ^^ expected `Int` but found `String`
            //   │
            // ```

            // Write initial border
            Gutter::new(None, gutter_padding).emit(writer, config)?;
            match i {
                0 => BorderLeft::new().emit(writer, config)?,
                _ => BorderLeftBreak::new().emit(writer, config)?,
            }
            NewLine::new().emit(writer, config)?;

            // Write underlined source section
            if start.line == end.line {
                // Single line
                //
                // ```text
                // 2 │ (+ test "")
                //   │         ^^ expected `Int` but found `String`
                // ```

                let highlight_start = (label.span.start() - start_line_span.start()).to_usize();
                let highlight_end = (label.span.end() - start_line_span.start()).to_usize();
                let prefix_source = replace_tabs(&start_line[..highlight_start]);
                let highlighted_source = replace_tabs(&start_line[highlight_start..highlight_end]);
                let suffix_source = replace_tabs(&start_line[highlight_end..]);

                // Write line number and border
                Gutter::new(start.line.number(), gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;

                // Write line source
                write!(writer, " {}", prefix_source)?;
                writer.set_color(label_style)?;
                write!(writer, "{}", highlighted_source)?;
                writer.reset()?;
                write!(writer, "{}", suffix_source.trim_end())?;
                NewLine::new().emit(writer, config)?;

                // Write border, underline, and label
                Gutter::new(None, gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;
                Underline::new(
                    *mark_style,
                    &prefix_source,
                    &highlighted_source,
                    &label.message,
                )
                .emit(writer, config)?;
                NewLine::new().emit(writer, config)?;
            } else {
                // Multiple lines
                //
                // ```text
                // 4 │   fizz₁ num = case (mod num 5) (mod num 3) of
                //   │ ╭─────────────^
                // 5 │ │     0 0 => "FizzBuzz"
                // 6 │ │     0 _ => "Fizz"
                // 7 │ │     _ 0 => "Buzz"
                // 8 │ │     _ _ => num
                //   │ ╰──────────────^ `case` clauses have incompatible types
                // ```

                let highlight_start = (label.span.start() - start_line_span.start()).to_usize();
                let prefix_source = replace_tabs(&start_line[..highlight_start]);
                let highlighted_source = replace_tabs(&start_line[highlight_start..]);

                if prefix_source.trim().is_empty() {
                    // Section is prefixed by empty space, so we don't need to take
                    // up a new line.
                    //
                    // ```text
                    // 4 │ ╭     case (mod num 5) (mod num 3) of
                    // ```

                    // Write line number, border, and underline
                    Gutter::new(start.line.number(), gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;
                    UnderlineTopLeft::new(*mark_style).emit(writer, config)?;

                    // Write source line
                    write!(writer, " {}", prefix_source)?;
                    writer.set_color(&label_style)?;
                    write!(writer, "{}", highlighted_source.trim_end())?;
                    writer.reset()?;
                    NewLine::new().emit(writer, config)?;
                } else {
                    // There's source code in the prefix, so run an underline
                    // underneath it to get to the start of the span.
                    //
                    // ```text
                    // 4 │   fizz₁ num = case (mod num 5) (mod num 3) of
                    //   │ ╭─────────────^
                    // ```

                    // Write line number and border
                    Gutter::new(start.line.number(), gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;

                    // Write source line
                    write!(writer, "   {}", prefix_source)?;
                    writer.set_color(&label_style)?;
                    write!(writer, "{}", highlighted_source.trim_end())?;
                    writer.reset()?;
                    NewLine::new().emit(writer, config)?;

                    // Write border and underline
                    Gutter::new(None, gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;
                    UnderlineTop::new(*mark_style, &prefix_source).emit(writer, config)?;
                    NewLine::new().emit(writer, config)?;
                }

                // Write highlighted lines
                //
                // ```text
                // 5 │ │     0 0 => "FizzBuzz"
                // 6 │ │     0 _ => "Fizz"
                // 7 │ │     _ 0 => "Buzz"
                // ```

                for line_index in ((start.line.to_usize() + 1)..end.line.to_usize())
                    .map(|i| LineIndex::from(i as u32))
                {
                    let (_, highlighted_source) =
                        get_line(line_index).expect("highlighted_source_2");

                    // Write line number, border, and underline
                    Gutter::new(line_index.number(), gutter_padding).emit(writer, config)?;
                    BorderLeft::new().emit(writer, config)?;
                    UnderlineLeft::new(*mark_style).emit(writer, config)?;

                    // Write highlighted source
                    writer.set_color(label_style)?;
                    write!(writer, " {}", highlighted_source.trim_end())?;
                    writer.reset()?;
                    NewLine::new().emit(writer, config)?;
                }

                // Write last highlighted line
                //
                // ```text
                // 8 │ │     _ _ => num
                //   │ ╰──────────────^ `case` clauses have incompatible types
                // ```

                let highlight_end = (label.span.end() - end_line_span.start()).to_usize();
                let highlighted_source = replace_tabs(&end_line[..highlight_end]);
                let suffix_source = replace_tabs(&end_line[highlight_end..]);

                // Write line number, border, and underline
                Gutter::new(end.line.number(), gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;
                UnderlineLeft::new(*mark_style).emit(writer, config)?;

                // Write line source
                writer.set_color(label_style)?;
                write!(writer, " {}", highlighted_source)?;
                writer.reset()?;
                write!(writer, "{}", suffix_source.trim_end())?;
                NewLine::new().emit(writer, config)?;

                // Write border, underline, and label
                Gutter::new(None, gutter_padding).emit(writer, config)?;
                BorderLeft::new().emit(writer, config)?;
                UnderlineBottom::new(*mark_style, &highlighted_source, &label.message)
                    .emit(writer, config)?;
                NewLine::new().emit(writer, config)?;
            }
        }

        // Write final border
        Gutter::new(None, gutter_padding).emit(writer, config)?;
        BorderLeft::new().emit(writer, config)?;
        NewLine::new().emit(writer, config)?;

        // Additional notes
        //
        // ```text
        // = expected type `Int`
        //      found type `String`
        // ```

        for note in self.notes {
            Note::new(gutter_padding, note).emit(writer, config)?;
        }

        Ok(())
    }
}
<|endoftext|>


<commit_before>//! Utilities for the reference image test suite.
//!
//! This module has utility functions that are used in the test suite
//! to compare rendered surfaces to reference images.

use cairo;

use std::convert::TryFrom;
use std::env;
use std::fs::{self, File};
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::Once;

use librsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};

use crate::compare_surfaces::{compare_surfaces, BufferDiff, Diff};

pub struct Reference(SharedImageSurface);

impl Reference {
    pub fn from_png<P>(path: P) -> Result<Self, cairo::IoError>
    where
        P: AsRef<Path>,
    {
        let file = File::open(path).map_err(|e| cairo::IoError::Io(e))?;
        let mut reader = BufReader::new(file);
        let surface = surface_from_png(&mut reader)?;
        Self::from_surface(surface)
    }

    pub fn from_surface(surface: cairo::ImageSurface) -> Result<Self, cairo::IoError> {
        let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?;
        Ok(Self(shared))
    }
}

pub trait Compare {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>;
}

impl Compare for &Reference {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
        compare_surfaces(&self.0, surface).map_err(cairo::IoError::from)
    }
}

impl Compare for Result<Reference, cairo::IoError> {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
        self.map(|reference| reference.compare(surface))
            .and_then(std::convert::identity)
    }
}

pub trait Evaluate {
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str);
}

impl Evaluate for BufferDiff {
    /// Evaluates a BufferDiff and panics if there are relevant differences
    ///
    /// The `output_base_name` is used to write test results if the
    /// surfaces are different.  If this is `foo`, this will write
    /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a
    /// visual diff between `output_surf` and the `Reference` that this
    /// diff was created from.
    ///
    /// # Panics
    ///
    /// Will panic if the surfaces are too different to be acceptable.
    fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) {
        match self {
            BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"),

            BufferDiff::Diff(diff) => {
                if diff.distinguishable() {
                    println!(
                        "{}: {} pixels changed with maximum difference of {}",
                        output_base_name, diff.num_pixels_changed, diff.max_diff,
                    );

                    write_to_file(output_surf, output_base_name, "out");
                    write_to_file(&diff.surface, output_base_name, "diff");

                    if diff.inacceptable() {
                        panic!("surfaces are too different");
                    }
                }
            }
        }
    }
}

impl Evaluate for Result<BufferDiff, cairo::IoError> {
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) {
        self.as_ref()
            .map(|diff| diff.evaluate(output_surface, output_base_name))
            .unwrap();
    }
}

fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) {
    let path = output_dir().join(&format!("{}-{}.png", output_base_name, suffix));
    println!("{}: {}", suffix, path.to_string_lossy());
    let mut output_file = File::create(path).unwrap();
    input
        .clone()
        .into_image_surface()
        .unwrap()
        .write_to_png(&mut output_file)
        .unwrap();
}

/// Creates a directory for test output and returns its path.
///
/// The location for the output directory is taken from the `OUT_DIR` environment
/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is
/// a platform dependent location for temporary files.
///
/// # Panics
///
/// Will panic if the output directory can not be created.
pub fn output_dir() -> PathBuf {
    let tempdir = || {
        let mut path = env::temp_dir();
        path.push("rsvg-test-output");
        path
    };
    let path = env::var_os("OUT_DIR").map_or_else(tempdir, PathBuf::from);

    fs::create_dir_all(&path).expect("could not create output directory for tests");

    path
}

fn tolerable_difference() -> u8 {
    static mut TOLERANCE: u8 = 2;

    static ONCE: Once = Once::new();
    ONCE.call_once(|| unsafe {
        if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") {
            let value: usize = str
                .parse()
                .expect("Can not parse RSVG_TEST_TOLERANCE as a number");
            TOLERANCE =
                u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255");
        }
    });

    unsafe { TOLERANCE }
}

pub trait Deviation {
    fn distinguishable(&self) -> bool;
    fn inacceptable(&self) -> bool;
}

impl Deviation for Diff {
    fn distinguishable(&self) -> bool {
        self.max_diff > 2
    }

    fn inacceptable(&self) -> bool {
        self.max_diff > tolerable_difference()
    }
}

/// Creates a cairo::ImageSurface from a stream of PNG data.
///
/// The surface is converted to ARGB if needed. Use this helper function with `Reference`.
pub fn surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError>
where
    R: Read,
{
    let png = cairo::ImageSurface::create_from_png(stream)?;
    let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?;
    {
        // convert to ARGB; the PNG may come as Rgb24
        let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context");
        cr.set_source_surface(&png, 0.0, 0.0).unwrap();
        cr.paint().unwrap();
    }
    Ok(argb)
}

/// Macro test that compares render outputs
///
/// Takes in SurfaceSize width and height, setting the cairo surface
#[macro_export]
macro_rules! test_compare_render_output {
    ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => {
        #[test]
        fn $test_name() {
            crate::utils::setup_font_map();

            let sx: i32 = $width;
            let sy: i32 = $height;
            let svg = load_svg($test).unwrap();
            let output_surf = render_document(
                &svg,
                SurfaceSize(sx, sy),
                |_| (),
                cairo::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: sx as f64,
                    height: sy as f64,
                },
            )
            .unwrap();

            let reference = load_svg($reference).unwrap();
            let reference_surf = render_document(
                &reference,
                SurfaceSize(sx, sy),
                |_| (),
                cairo::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: sx as f64,
                    height: sy as f64,
                },
            )
            .unwrap();

            Reference::from_surface(reference_surf.into_image_surface().unwrap())
                .compare(&output_surf)
                .evaluate(&output_surf, stringify!($test_name));
        }
    };
}
<commit_msg>test_svg_reference: new macro to have reftests from two SVG files<commit_after>//! Utilities for the reference image test suite.
//!
//! This module has utility functions that are used in the test suite
//! to compare rendered surfaces to reference images.

use cairo;

use std::convert::TryFrom;
use std::env;
use std::fs::{self, File};
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::Once;

use librsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};

use crate::compare_surfaces::{compare_surfaces, BufferDiff, Diff};

pub struct Reference(SharedImageSurface);

impl Reference {
    pub fn from_png<P>(path: P) -> Result<Self, cairo::IoError>
    where
        P: AsRef<Path>,
    {
        let file = File::open(path).map_err(|e| cairo::IoError::Io(e))?;
        let mut reader = BufReader::new(file);
        let surface = surface_from_png(&mut reader)?;
        Self::from_surface(surface)
    }

    pub fn from_surface(surface: cairo::ImageSurface) -> Result<Self, cairo::IoError> {
        let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?;
        Ok(Self(shared))
    }
}

pub trait Compare {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>;
}

impl Compare for &Reference {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
        compare_surfaces(&self.0, surface).map_err(cairo::IoError::from)
    }
}

impl Compare for Result<Reference, cairo::IoError> {
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
        self.map(|reference| reference.compare(surface))
            .and_then(std::convert::identity)
    }
}

pub trait Evaluate {
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str);
}

impl Evaluate for BufferDiff {
    /// Evaluates a BufferDiff and panics if there are relevant differences
    ///
    /// The `output_base_name` is used to write test results if the
    /// surfaces are different.  If this is `foo`, this will write
    /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a
    /// visual diff between `output_surf` and the `Reference` that this
    /// diff was created from.
    ///
    /// # Panics
    ///
    /// Will panic if the surfaces are too different to be acceptable.
    fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) {
        match self {
            BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"),

            BufferDiff::Diff(diff) => {
                if diff.distinguishable() {
                    println!(
                        "{}: {} pixels changed with maximum difference of {}",
                        output_base_name, diff.num_pixels_changed, diff.max_diff,
                    );

                    write_to_file(output_surf, output_base_name, "out");
                    write_to_file(&diff.surface, output_base_name, "diff");

                    if diff.inacceptable() {
                        panic!("surfaces are too different");
                    }
                }
            }
        }
    }
}

impl Evaluate for Result<BufferDiff, cairo::IoError> {
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) {
        self.as_ref()
            .map(|diff| diff.evaluate(output_surface, output_base_name))
            .unwrap();
    }
}

fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) {
    let path = output_dir().join(&format!("{}-{}.png", output_base_name, suffix));
    println!("{}: {}", suffix, path.to_string_lossy());
    let mut output_file = File::create(path).unwrap();
    input
        .clone()
        .into_image_surface()
        .unwrap()
        .write_to_png(&mut output_file)
        .unwrap();
}

/// Creates a directory for test output and returns its path.
///
/// The location for the output directory is taken from the `OUT_DIR` environment
/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is
/// a platform dependent location for temporary files.
///
/// # Panics
///
/// Will panic if the output directory can not be created.
pub fn output_dir() -> PathBuf {
    let tempdir = || {
        let mut path = env::temp_dir();
        path.push("rsvg-test-output");
        path
    };
    let path = env::var_os("OUT_DIR").map_or_else(tempdir, PathBuf::from);

    fs::create_dir_all(&path).expect("could not create output directory for tests");

    path
}

fn tolerable_difference() -> u8 {
    static mut TOLERANCE: u8 = 2;

    static ONCE: Once = Once::new();
    ONCE.call_once(|| unsafe {
        if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") {
            let value: usize = str
                .parse()
                .expect("Can not parse RSVG_TEST_TOLERANCE as a number");
            TOLERANCE =
                u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255");
        }
    });

    unsafe { TOLERANCE }
}

pub trait Deviation {
    fn distinguishable(&self) -> bool;
    fn inacceptable(&self) -> bool;
}

impl Deviation for Diff {
    fn distinguishable(&self) -> bool {
        self.max_diff > 2
    }

    fn inacceptable(&self) -> bool {
        self.max_diff > tolerable_difference()
    }
}

/// Creates a cairo::ImageSurface from a stream of PNG data.
///
/// The surface is converted to ARGB if needed. Use this helper function with `Reference`.
pub fn surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError>
where
    R: Read,
{
    let png = cairo::ImageSurface::create_from_png(stream)?;
    let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?;
    {
        // convert to ARGB; the PNG may come as Rgb24
        let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context");
        cr.set_source_surface(&png, 0.0, 0.0).unwrap();
        cr.paint().unwrap();
    }
    Ok(argb)
}

/// Macro test that compares render outputs
///
/// Takes in SurfaceSize width and height, setting the cairo surface
#[macro_export]
macro_rules! test_compare_render_output {
    ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => {
        #[test]
        fn $test_name() {
            crate::utils::setup_font_map();

            let sx: i32 = $width;
            let sy: i32 = $height;
            let svg = load_svg($test).unwrap();
            let output_surf = render_document(
                &svg,
                SurfaceSize(sx, sy),
                |_| (),
                cairo::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: sx as f64,
                    height: sy as f64,
                },
            )
            .unwrap();

            let reference = load_svg($reference).unwrap();
            let reference_surf = render_document(
                &reference,
                SurfaceSize(sx, sy),
                |_| (),
                cairo::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: sx as f64,
                    height: sy as f64,
                },
            )
            .unwrap();

            Reference::from_surface(reference_surf.into_image_surface().unwrap())
                .compare(&output_surf)
                .evaluate(&output_surf, stringify!($test_name));
        }
    };
}

/// Render two SVG files and compare them.
///
/// This is used to implement reference tests, or reftests.  Use it like this:
///
/// ```ignore
/// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg");
/// ```
///
/// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions,
/// and that they produce the same rendered output.
#[macro_export]
macro_rules! test_svg_reference {
    ($test_name:ident, $test_filename:expr, $reference_filename:expr) => {
        #[test]
        fn $test_name() {
            use crate::utils::setup_font_map;
            use librsvg::{CairoRenderer, Loader};

            setup_font_map();

            let svg = Loader::new()
                .read_path($test_filename)
                .expect("reading SVG test file");
            let reference = Loader::new()
                .read_path($reference_filename)
                .expect("reading reference file");

            let svg_renderer = CairoRenderer::new(&svg);
            let ref_renderer = CairoRenderer::new(&reference);

            let svg_dim = svg_renderer.intrinsic_dimensions();
            let ref_dim = ref_renderer.intrinsic_dimensions();

            assert_eq!(
                svg_dim, ref_dim,
                "sizes of SVG document and reference file are different"
            );

            let pixels = svg_renderer
                .intrinsic_size_in_pixels()
                .unwrap_or((100.0, 100.0));

            let output_surf = render_document(
                &svg,
                SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
                |_| (),
                cairo::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: pixels.0,
                    height: pixels.1,
                },
            )
            .unwrap();

            let reference_surf = render_document(
                &reference,
                SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
                |_| (),
                cairo::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: pixels.0,
                    height: pixels.1,
                },
            )
            .unwrap();

            Reference::from_surface(reference_surf.into_image_surface().unwrap())
                .compare(&output_surf)
                .evaluate(&output_surf, stringify!($test_name));
        }
    };
}
<|endoftext|>