<commit_before>extern crate regex;
use mysql::conn::pool::{MyPool,MyPooledConn};
use mysql::conn::Stmt;

use std::process::{Command, Stdio, Child};
use std::error::Error;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use lib::config::ConfigGen;
use std::convert::Into;

use lib::DownloadError;

use CONFIG;
use lib;

macro_rules! regex(
    ($s:expr) => (regex::Regex::new($s).unwrap());
);

#[derive(Clone)]
pub struct DownloadDB {
    pub url: String,
    pub quality: i16,
    pub playlist: bool,
    pub compress: bool,
    pub qid: i64,
    pub source_type: i16,
    pub from: i32,
    pub to: i32,
    pub folder: String, // download folder, changes for playlists
    pub pool: MyPool,
}

impl DownloadDB {
    pub fn update_video(&mut self,url: String, qid: i64) -> &mut DownloadDB{
        self.qid = qid;
        self.url = url;
        self
    }
    pub fn update_folder(&mut self, folder: String){
        self.folder = folder;
    }
}

pub struct Downloader<'a> {
    pub ddb: &'a DownloadDB,
    defaults: &'a ConfigGen,
}

impl<'a> Downloader<'a>{
    pub fn new(ddb: &'a DownloadDB, defaults: &'a ConfigGen) -> Downloader<'a>{
        Downloader {ddb: ddb, defaults: defaults}
    }

    //Regex matching: [download]  13.4% of 275.27MiB at 525.36KiB/s ETA 07:52
    /// Downloads the requested file.
    /// file_path specifies the download location.
    /// DMCA errors will get thrown.
    /// download_audio option: ignore the specified quality & download CONFIG.codecs.yt.audio_normal quality for split containers
    fn download_file_in(&self, file_path: &str, download_audio: bool) -> Result<bool,DownloadError> {
        trace!("{:?}", self.ddb.url);

        let curr_quality = if download_audio {
            &CONFIG.codecs.audio_raw
        }else{
            &self.ddb.quality
        };

        trace!("quality: {}",curr_quality);
        let mut child = try!(self.run_download_process(file_path,curr_quality));
        let stdout = BufReader::new(child.stdout.take().unwrap());

        let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

        let mut conn = self.ddb.pool.get_conn().unwrap();
        let mut statement = self.prepare_progress_updater(&mut conn);
        let re = regex!(r"(\d+\.\d)%");

        for line in stdout.lines(){
            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why)); panic!();},
                Ok(text) => {
                        trace!("Out: {}",text);
                        if !self.ddb.playlist {
                            match re.captures(&text) {
                                Some(cap) => { //println!("Match at {}", s.0);
                                            debug!("{}",  cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                            try!(self.update_progress(&mut statement, cap.at(1).unwrap()));
                                        },
                                None => (),
                            }
                        }
                },
            }
        }

        try!(child.wait()); // waits for finish & then exists zombi process, fixes #10

        let mut stderr: String = String::new();
        try!(stderr_buffer.read_to_string(&mut stderr));

        if stderr.is_empty() {
            Ok(true)
        } else if stderr.contains("requested format not available") {
            Err(DownloadError::QualityNotAvailable)
        } else if stderr.contains("ExtractorError") {
            Err(DownloadError::ExtractorError)
        } else {
            Err(DownloadError::InternalError(stderr))
        }

    }

    /// Wrapper for download_file_fn to retry on Extract Error's, which are appearing randomly.
    pub fn download_file(&self, file_path: &str, download_audio: bool) -> Result<bool,DownloadError> {
        for attempts in 0..2 {
            match self.download_file_in(file_path, download_audio) {
                Ok(v) => return Ok(v),
                Err(e) => {
                    match e {
                        DownloadError::ExtractorError =>  { warn!("download try no {}",attempts)},
                        _ => return Err(e),
                    }
                },
            }
        }
        Err(DownloadError::ExtractorError)
    }

    /// Trys to get the original name of a file, while checking for availability
    /// As an ExtractError can appear randomly, bug 11, we're retrying again 2 times if it should occour
    pub fn get_file_name(&self) -> Result<String,DownloadError> {
        for attempts in 0..2 {
            let mut child = try!(self.run_filename_process());
            let mut stdout_buffer = BufReader::new(child.stdout.take().unwrap());
            let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

            let mut stdout: String = String::new();
            try!(stdout_buffer.read_to_string(&mut stdout));
            let mut stderr: String = String::new();
            try!(stderr_buffer.read_to_string(&mut stderr));

            try!(child.wait());
            if stderr.is_empty() {
                trace!("get_file_name: {:?}", stdout);
                return Ok(stdout.trim().to_string());
            }else{
                if stderr.contains("not available in your country") || stderr.contains("contains content from") || stderr.contains("This video is available in") {
                    return Err(DownloadError::DMCAError);
                } else if stderr.contains("Please sign in to view this video") {
                    return Err(DownloadError::NotAvailable);
                } else if stderr.contains("ExtractorError") { // #11
                    info!("ExtractorError on attempt {}", attempts +1);
                } else {
                    return Err(DownloadError::DownloadError(stderr));
                }
            }
        }
        Err(DownloadError::ExtractorError)
    }

    /// Gets the playlist ids needed for furture download requests.
    /// The output is a vector of IDs
    pub fn get_playlist_ids(&self) -> Result<Vec<String>,DownloadError> {
        let mut child = try!(self.run_playlist_extract());
        trace!("retrieving playlist ids");
        let stdout = BufReader::new(child.stdout.take().unwrap());
        let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

        let re = regex!(r#""url": "([a-zA-Z0-9_-]+)""#);

        let mut id_list: Vec<String> = Vec::new();
        for line in stdout.lines(){
            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why)); panic!();},
                Ok(text) => {
                        trace!("Out: {}",text);
                        match re.captures(&text) {
                            Some(cap) => { //println!("Match at {}", s.0);
                                        debug!("{}", cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                        id_list.push(cap.at(1).unwrap().to_string());
                                    },
                            None => (),
                        }
                    },
            }
        }

        let mut stderr: String = String::new();
        try!(stderr_buffer.read_to_string(&mut stderr));

        try!(child.wait());

        if !stderr.is_empty() {
            warn!("stderr: {:?}", stderr);
            return Err(DownloadError::InternalError(stderr));
        }

        Ok(id_list)
    }

    /// Retrives the playlist name, will kill the process due to yt-dl starting detailed retrieval afterwards.
    pub fn get_playlist_name(&self) -> Result<String,DownloadError> {
        let mut child = try!(self.run_playlist_get_name());
        let stdout = BufReader::new(child.stdout.take().unwrap());

        let re = regex!(r"\[download\] Downloading playlist: (.*)");

        let name: String;
        for line in stdout.lines(){

            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why)); panic!();},
                Ok(text) => {
                        println!("Out: {}",text);
                        match re.captures(&text) {
                            Some(cap) => {
                                        trace!("{}", cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                        name = cap.at(1).unwrap().to_string();
                                        try!(child.kill());
                                        trace!("killed it");
                                        return Ok(name);
                                    },
                            None => (),
                        }
                    },
            }
        }

        try!(child.wait()); // waits for finish & then exists zombi process fixes #10

        Err(DownloadError::DownloadError("no playlist name".to_string()))
    }

    fn run_download_process(&self, file_path: &str, quality: &i16) -> Result<Child,DownloadError> {
        match Command::new("youtube-dl")
                                    .arg("--newline")
                                    .arg("--no-warnings")
                                    .arg("-r")
                                    .arg(format!("{}M",self.defaults.download_mbps))
                                    .arg("-f")
                                    .arg(quality.to_string())
                                    .arg("-o")
                                    .arg(file_path)
                                    .arg(&self.ddb.url)
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::piped())
                                    .stderr(Stdio::piped())
                                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    fn run_filename_process(&self) -> Result<Child,DownloadError> {
        match Command::new("youtube-dl")
                                    .arg("--get-filename")
                                    .arg("-o")
                                    .arg("%(title)s")
                                    .arg(&self.ddb.url)
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::piped())
                                    .stderr(Stdio::piped())
                                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    /// Runs the playlist extraction process.
    fn run_playlist_extract(&self) -> Result<Child,DownloadError> {
        let mut cmd = Command::new("youtube-dl");
        cmd.arg("-s").arg("--dump-json").arg("--flat-playlist");
        if self.ddb.from > 0 {
            cmd.arg("--playlist-start");
            cmd.arg(self.ddb.from.to_string());
        }
        if self.ddb.to > 0 {
            cmd.arg("--playlist-end");
            cmd.arg(self.ddb.to.to_string());
        }
        match cmd.arg(&self.ddb.url)
                    .stdin(Stdio::null())
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    /// Runs the playlist name retrival process.
    fn run_playlist_get_name(&self) -> Result<Child,DownloadError> {
        match Command::new("youtube-dl")
                                    .arg("-s")
                                    .arg("--no-warnings")
                                    .arg(&self.ddb.url)
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::piped())
                                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    /// Prepares the progress update statement.
    // MyPooledConn does only live when MyOpts is alive -> lifetime needs to be declared
    fn prepare_progress_updater(&'a self,conn: &'a mut MyPooledConn) -> Stmt<'a> { // no livetime needed: struct livetime used
        conn.prepare("UPDATE querydetails SET progress = ? WHERE qid = ?").unwrap()
    }

    /// Executes the progress update statement.
    fn update_progress(&self,stmt: &mut Stmt, progress: &str) -> Result<(),DownloadError>{
        try!(stmt.execute((progress,&self.ddb.qid)).map(|_| Ok(())))
        //-> only return errors, ignore the return value of stmt.execute
    }

    /// This function does a 3rd party binding in case it's needed
    /// due to the country restrictions
    /// The returned value has to contain the original video name, the lib has to download & save
    /// the file to the given location
    pub fn lib_request_video(&self, current_steps: i32,max_steps: i32, file_path: &String) -> Result<String,DownloadError> {
        let mut child = try!(self.lib_request_video_cmd(file_path));
        trace!("Requesting video via lib..");
        let stdout = BufReader::new(child.stdout.take().unwrap());
        let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

        let re = regex!(r"step (\d)");

        let mut last_line = String::new();
        for line in stdout.lines(){
            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why));panic!();},
                Ok(text) => {
                        trace!("Out: {}",text);
                        match re.captures(&text) {
                            Some(cap) => {
                                        debug!("Match: {}", cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                        if !self.ddb.playlist {
                                            lib::db::update_steps(&self.ddb.pool ,&self.ddb.qid, current_steps + &cap.at(1).unwrap().parse::<i32>().unwrap(), max_steps, false);
                                        }
                                    },
                            None => {last_line = text.clone()},
                        }
                    },
            }
        }

        trace!("reading stderr");

        let mut stderr: String = String::new();
        try!(stderr_buffer.read_to_string(&mut stderr));

        try!(child.wait());

        if !stderr.is_empty() {
            warn!("stderr: {:?}", stderr);
            return Err(DownloadError::InternalError(stderr));
        }
        //this ONLY works because `filename ` is ascii..
        let mut out = last_line[last_line.find("filename: ").unwrap()+9..].trim().to_string();
        out = lib::url_encode(&out);//stdout.trim();

        Ok(out)
    }

    /// Generate the lib command.
    fn lib_request_video_cmd(&self, file_path: &String) -> Result<Child,DownloadError> {
        let java_path = Path::new(&self.defaults.lib_dir);

        debug!("{} {:?} -q {} -f {} -v {} {}", self.defaults.lib_bin, self.defaults.lib_args, self.ddb.quality, file_path, !self.is_audio(), self.ddb.url);
        match Command::new(&self.defaults.lib_bin)
                                        .current_dir(&java_path)
                                        .args(&self.defaults.lib_args)
                                        .arg("-q")
                                        .arg(&self.ddb.quality.to_string())
                                        .arg("-f")
                                        .arg(file_path)
                                        .arg("-v")
                                        .arg((!self.is_audio()).to_string())
                                        .arg(&self.ddb.url)
                                        .stdin(Stdio::null())
                                        .stdout(Stdio::piped())
                                        .stderr(Stdio::piped())
                                        .spawn() {
                Err(why) => {warn!("{:?}",why); Err(DownloadError::InternalError(Error::description(&why).into()))},
                Ok(process) => Ok(process),
            }
    }

    /// Check if audio is requested or not.
    pub fn is_audio(&self) -> bool {
        if self.ddb.quality == CONFIG.codecs.audio_raw {
            true
        } else if self.ddb.quality == CONFIG.codecs.audio_source_hq {
            true
        } else if self.ddb.quality == CONFIG.codecs.audio_mp3 {
            true
        } else {
            false
        }
    }
}<commit_msg>added ffmpeg dir to yt-dl cmd, allowing twitch extractions<commit_after>extern crate regex;
use mysql::conn::pool::{MyPool,MyPooledConn};
use mysql::conn::Stmt;

use std::process::{Command, Stdio, Child};
use std::error::Error;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use lib::config::ConfigGen;
use std::convert::Into;

use lib::DownloadError;

use CONFIG;
use lib;

macro_rules! regex(
    ($s:expr) => (regex::Regex::new($s).unwrap());
);

#[derive(Clone)]
pub struct DownloadDB {
    pub url: String,
    pub quality: i16,
    pub playlist: bool,
    pub compress: bool,
    pub qid: i64,
    pub source_type: i16,
    pub from: i32,
    pub to: i32,
    pub folder: String, // download folder, changes for playlists
    pub pool: MyPool,
}

impl DownloadDB {
    pub fn update_video(&mut self,url: String, qid: i64) -> &mut DownloadDB{
        self.qid = qid;
        self.url = url;
        self
    }
    pub fn update_folder(&mut self, folder: String){
        self.folder = folder;
    }
}

pub struct Downloader<'a> {
    pub ddb: &'a DownloadDB,
    defaults: &'a ConfigGen,
}

impl<'a> Downloader<'a>{
    pub fn new(ddb: &'a DownloadDB, defaults: &'a ConfigGen) -> Downloader<'a>{
        Downloader {ddb: ddb, defaults: defaults}
    }

    //Regex matching: [download]  13.4% of 275.27MiB at 525.36KiB/s ETA 07:52
    /// Downloads the requested file.
    /// file_path specifies the download location.
    /// DMCA errors will get thrown.
    /// download_audio option: ignore the specified quality & download CONFIG.codecs.yt.audio_normal quality for split containers
    fn download_file_in(&self, file_path: &str, download_audio: bool) -> Result<bool,DownloadError> {
        trace!("{:?}", self.ddb.url);

        let curr_quality = if download_audio {
            &CONFIG.codecs.audio_raw
        }else{
            &self.ddb.quality
        };

        trace!("quality: {}",curr_quality);
        let mut child = try!(self.run_download_process(file_path,curr_quality));
        let stdout = BufReader::new(child.stdout.take().unwrap());

        let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

        let mut conn = self.ddb.pool.get_conn().unwrap();
        let mut statement = self.prepare_progress_updater(&mut conn);
        let re = regex!(r"(\d+\.\d)%");

        for line in stdout.lines(){
            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why)); panic!();},
                Ok(text) => {
                        trace!("Out: {}",text);
                        if !self.ddb.playlist {
                            match re.captures(&text) {
                                Some(cap) => { //println!("Match at {}", s.0);
                                            debug!("{}",  cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                            try!(self.update_progress(&mut statement, cap.at(1).unwrap()));
                                        },
                                None => (),
                            }
                        }
                },
            }
        }

        try!(child.wait()); // waits for finish & then exists zombi process, fixes #10

        let mut stderr: String = String::new();
        try!(stderr_buffer.read_to_string(&mut stderr));

        if stderr.is_empty() {
            Ok(true)
        } else if stderr.contains("requested format not available") {
            Err(DownloadError::QualityNotAvailable)
        } else if stderr.contains("ExtractorError") {
            Err(DownloadError::ExtractorError)
        } else {
            Err(DownloadError::InternalError(stderr))
        }

    }

    /// Wrapper for download_file_fn to retry on Extract Error's, which are appearing randomly.
    pub fn download_file(&self, file_path: &str, download_audio: bool) -> Result<bool,DownloadError> {
        for attempts in 0..2 {
            match self.download_file_in(file_path, download_audio) {
                Ok(v) => return Ok(v),
                Err(e) => {
                    match e {
                        DownloadError::ExtractorError =>  { warn!("download try no {}",attempts)},
                        _ => return Err(e),
                    }
                },
            }
        }
        Err(DownloadError::ExtractorError)
    }

    /// Trys to get the original name of a file, while checking for availability
    /// As an ExtractError can appear randomly, bug 11, we're retrying again 2 times if it should occour
    pub fn get_file_name(&self) -> Result<String,DownloadError> {
        for attempts in 0..2 {
            let mut child = try!(self.run_filename_process());
            let mut stdout_buffer = BufReader::new(child.stdout.take().unwrap());
            let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

            let mut stdout: String = String::new();
            try!(stdout_buffer.read_to_string(&mut stdout));
            let mut stderr: String = String::new();
            try!(stderr_buffer.read_to_string(&mut stderr));

            try!(child.wait());
            if stderr.is_empty() {
                trace!("get_file_name: {:?}", stdout);
                return Ok(stdout.trim().to_string());
            }else{
                if stderr.contains("not available in your country") || stderr.contains("contains content from") || stderr.contains("This video is available in") {
                    return Err(DownloadError::DMCAError);
                } else if stderr.contains("Please sign in to view this video") {
                    return Err(DownloadError::NotAvailable);
                } else if stderr.contains("ExtractorError") { // #11
                    info!("ExtractorError on attempt {}", attempts +1);
                } else {
                    return Err(DownloadError::DownloadError(stderr));
                }
            }
        }
        Err(DownloadError::ExtractorError)
    }

    /// Gets the playlist ids needed for furture download requests.
    /// The output is a vector of IDs
    pub fn get_playlist_ids(&self) -> Result<Vec<String>,DownloadError> {
        let mut child = try!(self.run_playlist_extract());
        trace!("retrieving playlist ids");
        let stdout = BufReader::new(child.stdout.take().unwrap());
        let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

        let re = regex!(r#""url": "([a-zA-Z0-9_-]+)""#);

        let mut id_list: Vec<String> = Vec::new();
        for line in stdout.lines(){
            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why)); panic!();},
                Ok(text) => {
                        trace!("Out: {}",text);
                        match re.captures(&text) {
                            Some(cap) => { //println!("Match at {}", s.0);
                                        debug!("{}", cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                        id_list.push(cap.at(1).unwrap().to_string());
                                    },
                            None => (),
                        }
                    },
            }
        }

        let mut stderr: String = String::new();
        try!(stderr_buffer.read_to_string(&mut stderr));

        try!(child.wait());

        if !stderr.is_empty() {
            warn!("stderr: {:?}", stderr);
            return Err(DownloadError::InternalError(stderr));
        }

        Ok(id_list)
    }

    /// Retrives the playlist name, will kill the process due to yt-dl starting detailed retrieval afterwards.
    pub fn get_playlist_name(&self) -> Result<String,DownloadError> {
        let mut child = try!(self.run_playlist_get_name());
        let stdout = BufReader::new(child.stdout.take().unwrap());

        let re = regex!(r"\[download\] Downloading playlist: (.*)");

        let name: String;
        for line in stdout.lines(){

            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why)); panic!();},
                Ok(text) => {
                        println!("Out: {}",text);
                        match re.captures(&text) {
                            Some(cap) => {
                                        trace!("{}", cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                        name = cap.at(1).unwrap().to_string();
                                        try!(child.kill());
                                        trace!("killed it");
                                        return Ok(name);
                                    },
                            None => (),
                        }
                    },
            }
        }

        try!(child.wait()); // waits for finish & then exists zombi process fixes #10

        Err(DownloadError::DownloadError("no playlist name".to_string()))
    }

	/// Formats the download command.
    fn run_download_process(&self, file_path: &str, quality: &i16) -> Result<Child,DownloadError> {
        match Command::new("youtube-dl")
                                    .arg("--newline")
                                    .arg("--no-warnings")
                                    .arg("-r")
                                    .arg(format!("{}M",self.defaults.download_mbps))
                                    .arg("-f")
                                    .arg(quality.to_string())
                                    .arg("-o")
                                    .arg(file_path)
                                    .arg("--ffmpeg-location") // this is needed for twitch extraction
                                    .arg(&CONFIG.general.ffmpeg_bin_dir)
                                    .arg(&self.ddb.url)
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::piped())
                                    .stderr(Stdio::piped())
                                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

	/// Runs the filename retrival process.
    fn run_filename_process(&self) -> Result<Child,DownloadError> {
        match Command::new("youtube-dl")
                                    .arg("--get-filename")
                                    .arg("-o")
                                    .arg("%(title)s")
                                    .arg(&self.ddb.url)
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::piped())
                                    .stderr(Stdio::piped())
                                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    /// Runs the playlist extraction process.
    fn run_playlist_extract(&self) -> Result<Child,DownloadError> {
        let mut cmd = Command::new("youtube-dl");
        cmd.arg("-s").arg("--dump-json").arg("--flat-playlist");
        if self.ddb.from > 0 {
            cmd.arg("--playlist-start");
            cmd.arg(self.ddb.from.to_string());
        }
        if self.ddb.to > 0 {
            cmd.arg("--playlist-end");
            cmd.arg(self.ddb.to.to_string());
        }
        match cmd.arg(&self.ddb.url)
                    .stdin(Stdio::null())
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    /// Runs the playlist name retrival process.
    fn run_playlist_get_name(&self) -> Result<Child,DownloadError> {
        match Command::new("youtube-dl")
                                    .arg("-s")
                                    .arg("--no-warnings")
                                    .arg(&self.ddb.url)
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::piped())
                                    .spawn() {
            Err(why) => Err(DownloadError::InternalError(Error::description(&why).into())),
            Ok(process) => Ok(process),
        }
    }

    /// Prepares the progress update statement.
    // MyPooledConn does only live when MyOpts is alive -> lifetime needs to be declared
    fn prepare_progress_updater(&'a self,conn: &'a mut MyPooledConn) -> Stmt<'a> { // no livetime needed: struct livetime used
        conn.prepare("UPDATE querydetails SET progress = ? WHERE qid = ?").unwrap()
    }

    /// Executes the progress update statement.
    fn update_progress(&self,stmt: &mut Stmt, progress: &str) -> Result<(),DownloadError>{
        try!(stmt.execute((progress,&self.ddb.qid)).map(|_| Ok(())))
        //-> only return errors, ignore the return value of stmt.execute
    }

    /// This function does a 3rd party binding in case it's needed
    /// due to the country restrictions
    /// The returned value has to contain the original video name, the lib has to download & save
    /// the file to the given location
    pub fn lib_request_video(&self, current_steps: i32,max_steps: i32, file_path: &String) -> Result<String,DownloadError> {
        let mut child = try!(self.lib_request_video_cmd(file_path));
        trace!("Requesting video via lib..");
        let stdout = BufReader::new(child.stdout.take().unwrap());
        let mut stderr_buffer = BufReader::new(child.stderr.take().unwrap());

        let re = regex!(r"step (\d)");

        let mut last_line = String::new();
        for line in stdout.lines(){
            match line{
                Err(why) => {error!("couldn't read cmd stdout: {}", Error::description(&why));panic!();},
                Ok(text) => {
                        trace!("Out: {}",text);
                        match re.captures(&text) {
                            Some(cap) => {
                                        debug!("Match: {}", cap.at(1).unwrap()); // ONLY with ASCII chars makeable!
                                        if !self.ddb.playlist {
                                            lib::db::update_steps(&self.ddb.pool ,&self.ddb.qid, current_steps + &cap.at(1).unwrap().parse::<i32>().unwrap(), max_steps, false);
                                        }
                                    },
                            None => {last_line = text.clone()},
                        }
                    },
            }
        }

        trace!("reading stderr");

        let mut stderr: String = String::new();
        try!(stderr_buffer.read_to_string(&mut stderr));

        try!(child.wait());

        if !stderr.is_empty() {
            warn!("stderr: {:?}", stderr);
            return Err(DownloadError::InternalError(stderr));
        }
        //this ONLY works because `filename ` is ascii..
        let mut out = last_line[last_line.find("filename: ").unwrap()+9..].trim().to_string();
        out = lib::url_encode(&out);//stdout.trim();

        Ok(out)
    }

    /// Generate the lib command.
    fn lib_request_video_cmd(&self, file_path: &String) -> Result<Child,DownloadError> {
        let java_path = Path::new(&self.defaults.lib_dir);

        debug!("{} {:?} -q {} -f {} -v {} {}", self.defaults.lib_bin, self.defaults.lib_args, self.ddb.quality, file_path, !self.is_audio(), self.ddb.url);
        match Command::new(&self.defaults.lib_bin)
                                        .current_dir(&java_path)
                                        .args(&self.defaults.lib_args)
                                        .arg("-q")
                                        .arg(&self.ddb.quality.to_string())
                                        .arg("-f")
                                        .arg(file_path)
                                        .arg("-v")
                                        .arg((!self.is_audio()).to_string())
                                        .arg(&self.ddb.url)
                                        .stdin(Stdio::null())
                                        .stdout(Stdio::piped())
                                        .stderr(Stdio::piped())
                                        .spawn() {
                Err(why) => {warn!("{:?}",why); Err(DownloadError::InternalError(Error::description(&why).into()))},
                Ok(process) => Ok(process),
            }
    }

    /// Check if audio is requested or not.
    pub fn is_audio(&self) -> bool {
        if self.ddb.quality == CONFIG.codecs.audio_raw {
            true
        } else if self.ddb.quality == CONFIG.codecs.audio_source_hq {
            true
        } else if self.ddb.quality == CONFIG.codecs.audio_mp3 {
            true
        } else {
            false
        }
    }
}<|endoftext|>


<commit_before>//! Execution of futures which perform no IO on a single thread

use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::boxed::Box;

use {Future, Async};

use executor::{self, Spawn};

/// Main loop object
pub struct Core {
    inner: Rc<RefCell<Inner>>,
    live: usize,
}

impl Core {
    /// Create a new `Core`.
    pub fn new() -> Self {
        Core {
            inner: Rc::new(RefCell::new(Inner::new())),
            live: 0,
        }
    }

    /// Spawn a future to be executed by a future call to `run`.
    pub fn spawn<F>(&mut self, f: F)
        where F: Future<Item=(), Error=()> + 'static
    {
        self.inner.borrow_mut().ready.push_back(Rc::new(RefCell::new(executor::spawn(Box::new(f)))));
        self.live += 1;
    }

    /// Run the loop until all futures complete.
    ///
    /// # Panics
    /// Panics if a future deadlocks.
    pub fn run(&mut self) {
        loop {
            let task = self.inner.borrow_mut().ready.pop_front();
            if let Some(task) = task {
                let unpark = Arc::new(Unpark { task: task.clone(), inner: self.inner.clone(), });
                match task.borrow_mut().poll_future(unpark) {
                    Ok(Async::Ready(())) => self.live -= 1,
                    Err(()) => self.live -= 1,
                    Ok(Async::NotReady) => {}
                }
            } else {
                break;
            }
        }
        if self.live != 0 {
            panic!("deadlock");
        }
    }
}

struct Inner {
    ready: VecDeque<Rc<RefCell<Spawn<Box<Future<Item=(), Error=()>>>>>>,
}

impl Inner {
    fn new() -> Self {
        Inner {
            ready: VecDeque::new(),
        }
    }
}

struct Unpark {
    task: Rc<RefCell<Spawn<Box<Future<Item=(), Error=()>>>>>,
    inner: Rc<RefCell<Inner>>,
}

// We will do neither of these things.
unsafe impl ::std::marker::Sync for Unpark {}
unsafe impl ::std::marker::Send for Unpark {}

impl executor::Unpark for Unpark {
    fn unpark(&self) {
        self.inner.borrow_mut().ready.push_back(self.task.clone())
    }
}
<commit_msg>Make local_executor thread-safe<commit_after>//! Execution of futures on a single thread
//!
//! This module has no special handling of any blocking operations other than
//! futures-aware inter-thread communications, and should therefore probably not
//! be used to manage IO.

use std::sync::{Arc, Mutex, mpsc};
use std::collections::HashMap;
use std::collections::hash_map;
use std::boxed::Box;

use {Future, Async};

use executor::{self, Spawn};

/// Main loop object
pub struct Core {
    unpark_send: mpsc::Sender<u64>,
    unpark: mpsc::Receiver<u64>,
    live: HashMap<u64, Spawn<Box<Future<Item=(), Error=()>>>>,
    next_id: u64,
}

impl Core {
    /// Create a new `Core`.
    pub fn new() -> Self {
        let (send, recv) = mpsc::channel();
        Core {
            unpark_send: send,
            unpark: recv,
            live: HashMap::new(),
            next_id: 0,
        }
    }

    /// Spawn a future to be executed by a future call to `run`.
    pub fn spawn<F>(&mut self, f: F)
        where F: Future<Item=(), Error=()> + 'static
    {
        self.live.insert(self.next_id, executor::spawn(Box::new(f)));
        self.unpark_send.send(self.next_id).unwrap();
        self.next_id += 1;
    }

    /// Run the loop until all futures previously passed to `spawn` complete.
    ///
    /// # Panics
    /// May, but is not guaranteed to, panic if a future that was passed to `spawn` deadlocks.
    pub fn run(&mut self) {
        while !self.live.is_empty() {
            if let Ok(task) = self.unpark.recv() {
                let unpark = Arc::new(Unpark { task: task, send: Mutex::new(self.unpark_send.clone()), });
                let mut task = if let hash_map::Entry::Occupied(x) = self.live.entry(task) { x } else { continue };
                let result = task.get_mut().poll_future(unpark);
                match result {
                    Ok(Async::Ready(())) => { task.remove_entry(); }
                    Err(()) => { task.remove_entry(); }
                    Ok(Async::NotReady) => {}
                }
            } else {
                panic!("deadlock: {} task(s) blocked", self.live.len());
            }
        }
    }
}

struct Unpark {
    task: u64,
    send: Mutex<mpsc::Sender<u64>>,
}

impl executor::Unpark for Unpark {
    fn unpark(&self) {
        let _ = self.send.lock().unwrap().send(self.task);
    }
}
<|endoftext|>


<commit_before>use codegen::x64::reg::*;
use mem;
use ctxt::Context;

use ast::*;
use ast::Stmt::*;
use ast::Expr::*;
use ast::visit::*;

pub fn generate<'a, 'ast>(ctxt: &'a Context<'a, 'ast>, fct: &'ast Function) {
    CodeGenInfo::new(ctxt, fct).generate();
}

struct CodeGenInfo<'a, 'ast: 'a> {
    ctxt: &'a Context<'a, 'ast>,
    fct: &'ast Function,

    stacksize: u32,
    param_offset: i32,
    contains_fct_invocation: bool,
}

impl<'a, 'ast> CodeGenInfo<'a, 'ast> {
    fn new(ctxt: &'a Context<'a, 'ast>, fct: &'ast Function) -> CodeGenInfo<'a, 'ast> {
        CodeGenInfo {
            ctxt: ctxt,
            fct: fct,

            stacksize: 0,

            // first param offset to rbp is +16,
            // rbp+0 -> saved rbp
            // rbp+8 -> return address
            param_offset: 16,

            contains_fct_invocation: false,
        }
    }

    fn generate(&mut self) {
        self.visit_fct(self.fct);

        self.ctxt.fct_info_mut(self.fct.id, |fct| {
            fct.stacksize = self.stacksize;
            fct.contains_fct_invocation = self.contains_fct_invocation;
        });
    }

    fn increase_stack(&mut self, id: NodeId) {
        self.ctxt.var_mut(id, |v, _| {
            let ty_size = v.data_type.size();
            self.stacksize = mem::align(self.stacksize + ty_size, ty_size);
            v.offset = -(self.stacksize as i32);
        });
    }
}

impl<'a, 'ast> Visitor<'ast> for CodeGenInfo<'a, 'ast> {
    fn visit_param(&mut self, p: &'ast Param) {
        // on x64 only the first 6 parameters are stored in registers
        if (p.idx as usize) < REG_PARAMS.len() {
            self.increase_stack(p.id);

        // the rest of the parameters need to be stored in the callers stack
        } else {
            self.ctxt.var_mut(p.id, |v, _| { v.offset = self.param_offset; });

            // all params on stack need size of 8
            self.param_offset += 8;
        }
    }

    fn visit_stmt(&mut self, s: &'ast Stmt) {
        if let StmtVar(ref var) = *s {
            self.increase_stack(var.id);
        }

        visit::walk_stmt(self, s);
    }

    fn visit_expr(&mut self, e: &'ast Expr) {
        if let ExprCall(_) = *e {
            self.contains_fct_invocation = true;
        }

        visit::walk_expr(self, e);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use ctxt::*;
    use test;

    fn info<F>(code: &'static str, f: F) where F: FnOnce(&Context, &FctInfo) {
        test::parse(code, |ctxt| {
            let fct = ctxt.ast.elements[0].to_function().unwrap();
            generate(ctxt, fct);

            ctxt.fct_info(fct.id, |fct| {
                f(ctxt, fct);
            });
        });
    }

    #[test]
    fn test_invocation_flag() {
        info("fn f() { g(); } fn g() { }", |_, fct| {
            assert!(fct.contains_fct_invocation);
        });

        info("fn f() { }", |_, fct| {
            assert!(!fct.contains_fct_invocation);
        });
    }

    #[test]
    fn test_param_offset() {
        info("fn f(a: bool, b: int) { var c = 1; }", |ctxt, fct| {
            assert_eq!(12, fct.stacksize);

            for (varid, offset) in fct.vars.iter().zip(&[-1, -8, -12]) {
                assert_eq!(*offset, ctxt.var_infos.borrow()[varid.0].offset);
            }
        });
    }

    #[test]
    fn test_params_over_6_offset() {
        info("fn f(a: int, b: int, c: int, d: int,
                   e: int, f: int, g: int, h: int) {
                  var i : int = 1;
              }", |ctxt, fct| {
            assert_eq!(28, fct.stacksize);
            let offsets = [-4, -8, -12, -16, -20, -24, 16, 24, -28];

            for (varid, offset) in fct.vars.iter().zip(&offsets) {
                assert_eq!(*offset, ctxt.var_infos.borrow()[varid.0].offset);
            }
        });
    }

    #[test]
    fn test_var_offset() {
        info("fn f() { var a = true; var b = false; var c = 2; var d = \"abc\"; }", |ctxt, fct| {
            assert_eq!(16, fct.stacksize);

            for (varid, offset) in fct.vars.iter().zip(&[-1, -2, -8, -16]) {
                assert_eq!(*offset, ctxt.var_infos.borrow()[varid.0].offset);
            }
        });
    }
}
<commit_msg>codegen: renamed stacksize to localsize<commit_after>use codegen::x64::reg::*;
use mem;
use ctxt::Context;

use ast::*;
use ast::Stmt::*;
use ast::Expr::*;
use ast::visit::*;

pub fn generate<'a, 'ast>(ctxt: &'a Context<'a, 'ast>, fct: &'ast Function) {
    CodeGenInfo::new(ctxt, fct).generate();
}

struct CodeGenInfo<'a, 'ast: 'a> {
    ctxt: &'a Context<'a, 'ast>,
    fct: &'ast Function,

    localsize: u32,
    param_offset: i32,
    contains_fct_invocation: bool,
}

impl<'a, 'ast> CodeGenInfo<'a, 'ast> {
    fn new(ctxt: &'a Context<'a, 'ast>, fct: &'ast Function) -> CodeGenInfo<'a, 'ast> {
        CodeGenInfo {
            ctxt: ctxt,
            fct: fct,

            localsize: 0,

            // first param offset to rbp is +16,
            // rbp+0 -> saved rbp
            // rbp+8 -> return address
            param_offset: 16,

            contains_fct_invocation: false,
        }
    }

    fn generate(&mut self) {
        self.visit_fct(self.fct);

        self.ctxt.fct_info_mut(self.fct.id, |fct| {
            fct.stacksize = self.localsize;
            fct.contains_fct_invocation = self.contains_fct_invocation;
        });
    }

    fn increase_stack(&mut self, id: NodeId) {
        self.ctxt.var_mut(id, |v, _| {
            let ty_size = v.data_type.size();
            self.localsize = mem::align(self.localsize + ty_size, ty_size);
            v.offset = -(self.localsize as i32);
        });
    }
}

impl<'a, 'ast> Visitor<'ast> for CodeGenInfo<'a, 'ast> {
    fn visit_param(&mut self, p: &'ast Param) {
        // on x64 only the first 6 parameters are stored in registers
        if (p.idx as usize) < REG_PARAMS.len() {
            self.increase_stack(p.id);

        // the rest of the parameters need to be stored in the callers stack
        } else {
            self.ctxt.var_mut(p.id, |v, _| { v.offset = self.param_offset; });

            // all params on stack need size of 8
            self.param_offset += 8;
        }
    }

    fn visit_stmt(&mut self, s: &'ast Stmt) {
        if let StmtVar(ref var) = *s {
            self.increase_stack(var.id);
        }

        visit::walk_stmt(self, s);
    }

    fn visit_expr(&mut self, e: &'ast Expr) {
        if let ExprCall(_) = *e {
            self.contains_fct_invocation = true;
        }

        visit::walk_expr(self, e);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use ctxt::*;
    use test;

    fn info<F>(code: &'static str, f: F) where F: FnOnce(&Context, &FctInfo) {
        test::parse(code, |ctxt| {
            let fct = ctxt.ast.elements[0].to_function().unwrap();
            generate(ctxt, fct);

            ctxt.fct_info(fct.id, |fct| {
                f(ctxt, fct);
            });
        });
    }

    #[test]
    fn test_invocation_flag() {
        info("fn f() { g(); } fn g() { }", |_, fct| {
            assert!(fct.contains_fct_invocation);
        });

        info("fn f() { }", |_, fct| {
            assert!(!fct.contains_fct_invocation);
        });
    }

    #[test]
    fn test_param_offset() {
        info("fn f(a: bool, b: int) { var c = 1; }", |ctxt, fct| {
            assert_eq!(12, fct.stacksize);

            for (varid, offset) in fct.vars.iter().zip(&[-1, -8, -12]) {
                assert_eq!(*offset, ctxt.var_infos.borrow()[varid.0].offset);
            }
        });
    }

    #[test]
    fn test_params_over_6_offset() {
        info("fn f(a: int, b: int, c: int, d: int,
                   e: int, f: int, g: int, h: int) {
                  var i : int = 1;
              }", |ctxt, fct| {
            assert_eq!(28, fct.stacksize);
            let offsets = [-4, -8, -12, -16, -20, -24, 16, 24, -28];

            for (varid, offset) in fct.vars.iter().zip(&offsets) {
                assert_eq!(*offset, ctxt.var_infos.borrow()[varid.0].offset);
            }
        });
    }

    #[test]
    fn test_var_offset() {
        info("fn f() { var a = true; var b = false; var c = 2; var d = \"abc\"; }", |ctxt, fct| {
            assert_eq!(16, fct.stacksize);

            for (varid, offset) in fct.vars.iter().zip(&[-1, -2, -8, -16]) {
                assert_eq!(*offset, ctxt.var_infos.borrow()[varid.0].offset);
            }
        });
    }
}
<|endoftext|>


<commit_before>extern crate hyper;
extern crate serde;
extern crate serde_json;
extern crate chrono;
extern crate uuid;
extern crate http_event_store as hes;

#[macro_use]
mod support;

use support::task_domain::*;

use hes::write::Event;
use hes::read::BodyEntry;
use hes::client::Client;
use hes::expected_version::ExpectedVersion;
use hes::error::ApiError::*;

#[test]
fn should_append_events_in_right_order() {
    let created_id = uuid::Uuid::new_v4();
    let updated_id = uuid::Uuid::new_v4();
    let events: Vec<Event> = vec![
        TaskCreated { name: format!("Created {:?}", chrono::UTC::now()), event_id: created_id }.into(),
        TaskRenamed { name: format!("Renamed {:?}", chrono::UTC::now()), event_id: updated_id }.into()
    ];

    let client = Client::default();
    let stream_name = test_stream_name();
    client.append_to_stream(&stream_name, ExpectedVersion::NoStream, events).unwrap();
    let stream = client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 2, true).unwrap();

    assert_eq!("task-renamed", stream.entries[0].event_type);
    assert_eq!("task-created", stream.entries[1].event_type);
    assert_eq!(updated_id, stream.entries[0].event_id);
    assert_eq!(created_id, stream.entries[1].event_id);
    assert_eq!(1, stream.entries[0].event_number);
    assert_eq!(0, stream.entries[1].event_number);
}

#[test]
fn should_require_expected_version_to_be_correct() {
    let client = Client::default();
    let stream_name = test_stream_name();

    let mut version = ExpectedVersion::NoStream;
    client.append_to_stream(&stream_name, version, vec![task_created_event().into()]).unwrap();
    assert_eq!(1, client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 3, true).unwrap().entries.len());

    version = ExpectedVersion::Number(0);
    client.append_to_stream(&stream_name, version, vec![task_renamed_event().into()]).unwrap();
    assert_eq!(2, client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 3, true).unwrap().entries.len());

    version = ExpectedVersion::Number(1);
    client.append_to_stream(&stream_name, version, vec![task_renamed_event().into()]).unwrap();
    assert_eq!(3, client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 3, true).unwrap().entries.len());
}

#[test]
fn should_return_wrong_expected_event_number_error_if_expected_version_is_wrong() {
    let client = Client::default();
    let stream_name = test_stream_name();

    let version = ExpectedVersion::Number(1);
    let result = client.append_to_stream(&stream_name, version, vec![task_created_event().into()]);

    assert_error!(WrongExpectedEventNumber(..), result.unwrap_err());
}

#[test]
fn should_fail_appending_with_any_expected_version_to_deleted_stream() {
    let client = Client::default();
    let stream_name = test_stream_name();

    client.append_to_stream(&stream_name, ExpectedVersion::NoStream, vec![task_created_event().into()]).unwrap();
    client.hard_delete_stream(&stream_name, ExpectedVersion::Any).unwrap();

    let result = client.append_to_stream(&stream_name, ExpectedVersion::Any, vec![task_renamed_event().into()]);

    assert_error!(StreamDeleted(..), result.unwrap_err());
}

//TODO
#[test]
fn should_cope_with_empty_event_collection() {}

fn test_stream_name() -> String {
    format!("task-{}", uuid::Uuid::new_v4().simple())
}

fn task_created_event() -> TaskCreated {
    TaskCreated { name: format!("Created {:?}", chrono::UTC::now()), event_id: uuid::Uuid::new_v4() }
}
fn task_renamed_event() -> TaskRenamed {
    TaskRenamed { name: format!("Renamed {:?}", chrono::UTC::now()), event_id: uuid::Uuid::new_v4() }
}
<commit_msg>Add a spec on attempt to append an empty collection of events<commit_after>extern crate hyper;
extern crate serde;
extern crate serde_json;
extern crate chrono;
extern crate uuid;
extern crate http_event_store as hes;

#[macro_use]
mod support;

use support::task_domain::*;

use hes::write::Event;
use hes::read::BodyEntry;
use hes::client::Client;
use hes::expected_version::ExpectedVersion;
use hes::error::ApiError::*;

#[test]
fn should_append_events_in_right_order() {
    let created_id = uuid::Uuid::new_v4();
    let updated_id = uuid::Uuid::new_v4();
    let events: Vec<Event> = vec![
        TaskCreated { name: format!("Created {:?}", chrono::UTC::now()), event_id: created_id }.into(),
        TaskRenamed { name: format!("Renamed {:?}", chrono::UTC::now()), event_id: updated_id }.into()
    ];

    let client = Client::default();
    let stream_name = test_stream_name();
    client.append_to_stream(&stream_name, ExpectedVersion::NoStream, events).unwrap();
    let stream = client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 2, true).unwrap();

    assert_eq!("task-renamed", stream.entries[0].event_type);
    assert_eq!("task-created", stream.entries[1].event_type);
    assert_eq!(updated_id, stream.entries[0].event_id);
    assert_eq!(created_id, stream.entries[1].event_id);
    assert_eq!(1, stream.entries[0].event_number);
    assert_eq!(0, stream.entries[1].event_number);
}

#[test]
fn should_require_expected_version_to_be_correct() {
    let client = Client::default();
    let stream_name = test_stream_name();

    let mut version = ExpectedVersion::NoStream;
    client.append_to_stream(&stream_name, version, vec![task_created_event().into()]).unwrap();
    assert_eq!(1, client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 3, true).unwrap().entries.len());

    version = ExpectedVersion::Number(0);
    client.append_to_stream(&stream_name, version, vec![task_renamed_event().into()]).unwrap();
    assert_eq!(2, client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 3, true).unwrap().entries.len());

    version = ExpectedVersion::Number(1);
    client.append_to_stream(&stream_name, version, vec![task_renamed_event().into()]).unwrap();
    assert_eq!(3, client.read_stream_events_forward::<BodyEntry>(&stream_name, 0, 3, true).unwrap().entries.len());
}

#[test]
fn should_return_wrong_expected_event_number_error_if_expected_version_is_wrong() {
    let client = Client::default();
    let stream_name = test_stream_name();

    let version = ExpectedVersion::Number(1);
    let result = client.append_to_stream(&stream_name, version, vec![task_created_event().into()]);

    assert_error!(WrongExpectedEventNumber(..), result.unwrap_err());
}

#[test]
fn should_fail_appending_with_any_expected_version_to_deleted_stream() {
    let client = Client::default();
    let stream_name = test_stream_name();

    client.append_to_stream(&stream_name, ExpectedVersion::NoStream, vec![task_created_event().into()]).unwrap();
    client.hard_delete_stream(&stream_name, ExpectedVersion::Any).unwrap();

    let result = client.append_to_stream(&stream_name, ExpectedVersion::Any, vec![task_renamed_event().into()]);

    assert_error!(StreamDeleted(..), result.unwrap_err());
}

#[test]
fn should_cope_with_empty_event_collection() {
    let client = Client::default();
    let result = client.append_to_stream(&test_stream_name(), ExpectedVersion::NoStream, vec![]);
    assert_error!(Restful(..), result.unwrap_err());
}

fn test_stream_name() -> String {
    format!("task-{}", uuid::Uuid::new_v4().simple())
}

fn task_created_event() -> TaskCreated {
    TaskCreated { name: format!("Created {:?}", chrono::UTC::now()), event_id: uuid::Uuid::new_v4() }
}

fn task_renamed_event() -> TaskRenamed {
    TaskRenamed { name: format!("Renamed {:?}", chrono::UTC::now()), event_id: uuid::Uuid::new_v4() }
}
<|endoftext|>


<commit_before>use std::marker::PhantomData;

use query::Operand;
use perlin_core::index::posting::DocId;

pub type Pipeline<Out, T> = Box<Fn(DocId, &mut T, &str) -> PhantomData<Out> + Sync + Send>;
pub type QueryPipeline<T> = Box<Fn(&T, &str) -> Operand>;

#[cfg(test)]
mod tests {
    use perlin_core::index::posting::DocId;
    use field::Field;

    use rust_stemmers::Algorithm;

    use test_utils::create_test_dir;

    // #[derive(PerlinDocument)]
    // #[ExternalId(usize)]
    pub struct Test {
        text: Field<String>,
        title: Field<String>,
        number: Field<u64>,
        emails: Field<usize>,
    }
    pub use self::perlin_impl::TestIndex;
    mod perlin_impl {
        use super::Test;
        use std::path::{Path, PathBuf};
        use std::borrow::Cow;
        use document_index::{QueryPipeline, Pipeline};
        use query::Operand;
        use perlin_core::index::posting::DocId;
        struct TestPipes {
            text: Option<Pipeline<String, Test>>,
            title: Option<Pipeline<String, Test>>,
            number: Option<Pipeline<u64, Test>>,
            emails: Option<Pipeline<usize, Test>>,
        }

        impl Default for TestPipes {
            fn default() -> Self {
                TestPipes {
                    text: None,
                    title: None,
                    number: None,
                    emails: None,
                }
            }
        }
        pub struct TestIndex {
            documents: Test,
            pipelines: TestPipes,
            query_pipeline: Option<QueryPipeline<Test>>,
            base_path: PathBuf,
            doc_counter: DocId,
            external_ids: Vec<(DocId, usize)>,
        }
        impl TestIndex {
            pub fn create(base_path: PathBuf) -> Self {
                TestIndex {
                    documents: Test::create(&base_path),
                    pipelines: TestPipes::default(),
                    query_pipeline: None,
                    base_path: base_path,
                    doc_counter: DocId::none(),
                    external_ids: Vec::new(),
                }
            }
            pub fn commit(&mut self) {
                self.documents.commit();
            }
            pub fn add_document(&mut self,
                                key_values: &[(Cow<str>, Cow<str>)],
                                external_id: usize) {
                self.doc_counter.inc();
                let doc_id = self.doc_counter;
                for &(ref key, ref value) in key_values {
                    match key.as_ref() {
                        "text" => {
                            if let Some(ref pipeline) = self.pipelines.text {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        "title" => {
                            if let Some(ref pipeline) = self.pipelines.title {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        "number" => {
                            if let Some(ref pipeline) = self.pipelines.number {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        "emails" => {
                            if let Some(ref pipeline) = self.pipelines.emails {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        _ => {}
                    }
                }
                self.external_ids.push((doc_id, external_id));
            }
            pub fn set_text_pipeline(&mut self, pipe: Pipeline<String, Test>) {
                self.pipelines.text = Some(pipe);
            }
            pub fn set_title_pipeline(&mut self, pipe: Pipeline<String, Test>) {
                self.pipelines.title = Some(pipe);
            }
            pub fn set_query_pipeline(&mut self, pipe: QueryPipeline<Test>) {
                self.query_pipeline = Some(pipe);
            }
            pub fn run_query<'a>(&'a self, query: &str) -> Operand<'a> {
                if let Some(ref query_pipe) = self.query_pipeline {
                    query_pipe(&self.documents, query)
                } else {
                    panic!();
                }
            }
        }
        impl Test {
            pub fn create(path: &Path) -> Self {
                use perlin_core::page_manager::{RamPageCache, FsPageManager};
                use perlin_core::index::vocabulary::SharedVocabulary;
                use perlin_core::index::Index;
                let text_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("text_page_cache")));
                let title_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("title_page_cache")));
                let number_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("number_page_cache")));
                let emails_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("emails_page_cache")));
                Test {
                    text: Index::new(text_page_cache, SharedVocabulary::new()),
                    title: Index::new(title_page_cache, SharedVocabulary::new()),
                    number: Index::new(number_page_cache, SharedVocabulary::new()),
                    emails: Index::new(emails_page_cache, SharedVocabulary::new()),
                }
            }
            pub fn commit(&mut self) {
                self.text.commit();
                self.title.commit();
                self.number.commit();
                self.emails.commit();
            }
        }
    }

    use language::{Stemmer, LowercaseFilter, WhitespaceTokenizer};
    use language::integers::NumberFilter;

    #[test]
    fn test() {
        use std::borrow::Cow;
        use query::{Funnel, Operator, ToOperand};
        let mut t = TestIndex::create(create_test_dir("doc_index/test"));
        t.set_text_pipeline(pipeline!(text
                           WhitespaceTokenizer
                           > NumberFilter
                           | [number]
                           > LowercaseFilter
                      > Stemmer(Algorithm::English)));
        t.set_title_pipeline(pipeline!(title
                      WhitespaceTokenizer
                      > LowercaseFilter
                                       > Stemmer(Algorithm::English)));
        t.set_query_pipeline(Box::new(|docs: &Test, query: &str| {
            let to_op = WhitespaceTokenizer::create(
                LowercaseFilter::create(
                    Stemmer::create(Algorithm::English,
                                    Funnel::create(Operator::And, &docs.text))));
            to_op.to_operand()
        }));

        t.add_document(&[(Cow::from("text"), Cow::from("10 birds flew over MT EVEREST"))],
                       10);
        t.add_document(&[(Cow::from("text"), Cow::from("125 birds flew accross THE ocean"))],
                       10);
        t.add_document(&[(Cow::from("text"),
                          Cow::from("2567 unicorns flew from phobos to deimos"))],
                       10);
        t.commit();
    }
}
<commit_msg>working query pipeline<commit_after>use std::marker::PhantomData;

use query::Operand;
use perlin_core::index::posting::DocId;

pub type Pipeline<Out, T> = Box<Fn(DocId, &mut T, &str) -> PhantomData<Out> + Sync + Send>;
pub type QueryPipeline<T> = Box<for<'r, 'x> Fn(&'r T, &'x str) -> Operand<'r>>;

#[cfg(test)]
mod tests {
    use perlin_core::index::posting::DocId;
    use field::Field;

    use rust_stemmers::Algorithm;

    use test_utils::create_test_dir;

    // #[derive(PerlinDocument)]
    // #[ExternalId(usize)]
    pub struct Test {
        text: Field<String>,
        title: Field<String>,
        number: Field<u64>,
        emails: Field<usize>,
    }
    pub use self::perlin_impl::TestIndex;
    mod perlin_impl {
        use super::Test;
        use std::path::{Path, PathBuf};
        use std::borrow::Cow;
        use document_index::{QueryPipeline, Pipeline};
        use query::Operand;
        use perlin_core::index::posting::DocId;
        struct TestPipes {
            text: Option<Pipeline<String, Test>>,
            title: Option<Pipeline<String, Test>>,
            number: Option<Pipeline<u64, Test>>,
            emails: Option<Pipeline<usize, Test>>,
        }

        impl Default for TestPipes {
            fn default() -> Self {
                TestPipes {
                    text: None,
                    title: None,
                    number: None,
                    emails: None,
                }
            }
        }
        pub struct TestIndex {
            documents: Test,
            pipelines: TestPipes,
            query_pipeline: Option<QueryPipeline<Test>>,
            base_path: PathBuf,
            doc_counter: DocId,
            external_ids: Vec<(DocId, usize)>,
        }
        impl TestIndex {
            pub fn create(base_path: PathBuf) -> Self {
                TestIndex {
                    documents: Test::create(&base_path),
                    pipelines: TestPipes::default(),
                    query_pipeline: None,
                    base_path: base_path,
                    doc_counter: DocId::none(),
                    external_ids: Vec::new(),
                }
            }
            pub fn commit(&mut self) {
                self.documents.commit();
            }
            pub fn add_document(&mut self,
                                key_values: &[(Cow<str>, Cow<str>)],
                                external_id: usize) {
                self.doc_counter.inc();
                let doc_id = self.doc_counter;
                for &(ref key, ref value) in key_values {
                    match key.as_ref() {
                        "text" => {
                            if let Some(ref pipeline) = self.pipelines.text {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        "title" => {
                            if let Some(ref pipeline) = self.pipelines.title {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        "number" => {
                            if let Some(ref pipeline) = self.pipelines.number {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        "emails" => {
                            if let Some(ref pipeline) = self.pipelines.emails {
                                pipeline(doc_id, &mut self.documents, value.as_ref());
                            } else {
                            }
                        }
                        _ => {}
                    }
                }
                self.external_ids.push((doc_id, external_id));
            }
            pub fn set_text_pipeline(&mut self, pipe: Pipeline<String, Test>) {
                self.pipelines.text = Some(pipe);
            }
            pub fn set_title_pipeline(&mut self, pipe: Pipeline<String, Test>) {
                self.pipelines.title = Some(pipe);
            }
            pub fn set_query_pipeline(&mut self, pipe: QueryPipeline<Test>) {
                self.query_pipeline = Some(pipe);
            }
            pub fn run_query<'a>(&'a self, query: &str) -> Operand<'a> {
                if let Some(ref query_pipe) = self.query_pipeline {
                    query_pipe(&self.documents, query)
                } else {
                    panic!();
                }
            }
        }
        impl Test {
            pub fn create(path: &Path) -> Self {
                use perlin_core::page_manager::{RamPageCache, FsPageManager};
                use perlin_core::index::vocabulary::SharedVocabulary;
                use perlin_core::index::Index;
                let text_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("text_page_cache")));
                let title_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("title_page_cache")));
                let number_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("number_page_cache")));
                let emails_page_cache =
                    RamPageCache::new(FsPageManager::new(&path.join("emails_page_cache")));
                Test {
                    text: Index::new(text_page_cache, SharedVocabulary::new()),
                    title: Index::new(title_page_cache, SharedVocabulary::new()),
                    number: Index::new(number_page_cache, SharedVocabulary::new()),
                    emails: Index::new(emails_page_cache, SharedVocabulary::new()),
                }
            }
            pub fn commit(&mut self) {
                self.text.commit();
                self.title.commit();
                self.number.commit();
                self.emails.commit();
            }
        }
    }

    use language::{Stemmer, LowercaseFilter, WhitespaceTokenizer};
    use language::integers::NumberFilter;

    #[test]
    fn test() {
        use std::borrow::Cow;
        use query::{Funnel, Operator, ToOperand};
        use perlin_core::index::posting::Posting;
        let mut t = TestIndex::create(create_test_dir("doc_index/test"));
        t.set_text_pipeline(pipeline!(text
                           WhitespaceTokenizer
                           > NumberFilter
                           | [number]
                           > LowercaseFilter
                      > Stemmer(Algorithm::English)));
        t.set_title_pipeline(pipeline!(title
                      WhitespaceTokenizer
                      > LowercaseFilter
                                       > Stemmer(Algorithm::English)));
        t.set_query_pipeline(Box::new(|docs, query| {
            use language::CanApply;
            let mut to_op = WhitespaceTokenizer::create(
                LowercaseFilter::create(
                    Stemmer::create(Algorithm::English,
                                    Funnel::create(Operator::And, &docs.text))));
            to_op.apply(query);
            to_op.to_operand()
        }));

        t.add_document(&[(Cow::from("text"), Cow::from("10 birds flew over MT EVEREST"))],
                       10);
        t.add_document(&[(Cow::from("text"), Cow::from("125 birds flew accross THE ocean"))],
                       10);
        t.add_document(&[(Cow::from("text"),
                          Cow::from("2567 unicorns flew from phobos to deimos"))],
                       10);
        t.commit();
        assert_eq!(t.run_query("deimos").collect::<Vec<_>>(), vec![Posting(DocId(2))]);
    }
}
<|endoftext|>


<commit_before>use std::collections::HashMap;
use nix::sys::wait::*;
use nix::sys::signal;
use nix::unistd::Pid;
use tracer::*;
use breakpoint::*;
use ptrace_control::*;
use config::Config;
///
/// So we are either:
///     Waiting for an signal
///     Handling a stop
///     Handling an exit
///     Handling being signalled
///     Handling ptrace events
///     Handling an error
///     Continuing execution
///     Handling ptrace error
///
/// So we wait -> other thing -> continuing
///
/// Cannot do wait -> wait (unless non-polling, timeout?)
///
/// Or wait -> other thing -> other thing -> continue
///
/// Or wait -> other thing -> continue -> continue

/// Possible states when executing an inferior process. This is an attempt at
/// a platform agnostic abstracting to provide the potential of future
/// implementations for other operating systems and provides the implementation
/// of the test running state machine
/// T is data used to store the necessary process information to enable tracing
pub enum TestState {
    /// Start state. Wait for test to appear and track time to enable timeout
    Start {
        start_time: u64,
    },
    /// Initialise: once test process appears instrument
    Initialise ,
    /// Waiting for breakpoint to be hit or test to end
    Waiting {
        start_time: u64,
    },
    /// Test process stopped, check coverage
    Stopped,
    /// Process timed out
    Timeout,
    /// Unrecoverable error occurred
    Unrecoverable,
    /// Test exited normally
    End,
}

/// Trait for state machines to implement
trait StateMachine<T> {
    /// Update the states
    fn step(self, data: &mut T, config: &Config) -> TestState;
}

/// Handle to linux process state
#[derive(Debug)]
struct LinuxData<'a> {
    wait: WaitStatus,
    current: Pid,
    parent: Pid,
    breakpoints: HashMap<u64, Breakpoint>,
    traces: &'a mut Vec<TracerData>,
}

impl <'a>LinuxData<'a> {
    fn new(traces: &'a mut Vec<TracerData>) -> LinuxData {
        LinuxData {
            wait: WaitStatus::StillAlive,
            current: Pid::from_raw(0),
            parent: Pid::from_raw(0),
            breakpoints: HashMap::new(),
            traces: traces
        }
    }

    fn check_for_tracee(&mut self) -> Option<TestState> {
        match waitpid(self.current, Some(WNOHANG)) {
            Ok(WaitStatus::StillAlive) => None,
            Ok(sig @ WaitStatus::Stopped(_, signal::SIGTRAP)) => {
                if let WaitStatus::Stopped(child, _) = sig {
                    self.current = child;
                }
                self.wait = sig;
                Some(TestState::Initialise)
            },
            Ok(s) => {
                println!("Unexpected signal when starting test");
                None
            },
            Err(e) => {
                println!("Error when starting test: {}", e);
                Some(TestState::Unrecoverable)
            },
        }
    }
}

impl <'a> StateMachine<LinuxData<'a>> for TestState {

    fn step(self, data: &mut LinuxData<'a>, config: &Config) -> TestState {
        match self {
            TestState::Start{start_time} => {
                if let Some(state) = data.check_for_tracee() {
                    state
                } else if start_time < 1000 {
                    TestState::Start{start_time: start_time + 1}
                } else {
                    TestState::Timeout
                }
            },
            TestState::Initialise => {
                if let Err(c) = trace_children(data.current) {
                    println!("Failed to trace child threads");
                }
                for trace in data.traces.iter() {
                    if let Some(addr) = trace.address {
                        match Breakpoint::new(data.current, addr) {
                            Ok(bp) => {
                                let _ = data.breakpoints.insert(addr, bp);
                            },
                            Err(e) => {
                                println!("Failed to instrument");
                            },
                        }
                    }
                }
                if let Ok(_) = continue_exec(data.parent, None) {
                    TestState::Waiting{start_time:0}
                } else {
                    TestState::Unrecoverable
                }
            },
            _ => TestState::Unrecoverable
        }
    }
}

<commit_msg>Implemented state for TestState::Waiting + tidy<commit_after>use std::collections::HashMap;
use nix::sys::wait::*;
use nix::sys::signal;
use nix::unistd::Pid;
use tracer::*;
use breakpoint::*;
use ptrace_control::*;
use config::Config;
///
/// So we are either:
///     Waiting for an signal
///     Handling a stop
///     Handling an exit
///     Handling being signalled
///     Handling ptrace events
///     Handling an error
///     Continuing execution
///     Handling ptrace error
///
/// So we wait -> other thing -> continuing
///
/// Cannot do wait -> wait (unless non-polling, timeout?)
///
/// Or wait -> other thing -> other thing -> continue
///
/// Or wait -> other thing -> continue -> continue

/// Possible states when executing an inferior process. This is an attempt at
/// a platform agnostic abstracting to provide the potential of future
/// implementations for other operating systems and provides the implementation
/// of the test running state machine
/// T is data used to store the necessary process information to enable tracing
pub enum TestState {
    /// Start state. Wait for test to appear and track time to enable timeout
    Start {
        start_time: u64,
    },
    /// Initialise: once test process appears instrument
    Initialise ,
    /// Waiting for breakpoint to be hit or test to end
    Waiting {
        start_time: u64,
    },
    /// Test process stopped, check coverage
    Stopped,
    /// Process timed out
    Timeout,
    /// Unrecoverable error occurred
    Unrecoverable,
    /// Test exited normally
    End,
}

/// Trait for state machines to implement
pub trait StateMachine<T> {
    /// Update the states
    fn step(self, data: &mut T, config: &Config) -> TestState;
}

/// Handle to linux process state
#[derive(Debug)]
pub struct LinuxData<'a> {
    wait: WaitStatus,
    current: Pid,
    parent: Pid,
    breakpoints: HashMap<u64, Breakpoint>,
    traces: &'a mut Vec<TracerData>,
}

impl <'a>LinuxData<'a> {
    pub fn new(traces: &'a mut Vec<TracerData>) -> LinuxData {
        LinuxData {
            wait: WaitStatus::StillAlive,
            current: Pid::from_raw(0),
            parent: Pid::from_raw(0),
            breakpoints: HashMap::new(),
            traces: traces
        }
    }

    fn check_for_tracee(&mut self) -> Option<TestState> {
        match waitpid(self.current, Some(WNOHANG)) {
            Ok(WaitStatus::StillAlive) => None,
            Ok(sig @ WaitStatus::Stopped(_, signal::SIGTRAP)) => {
                if let WaitStatus::Stopped(child, _) = sig {
                    self.current = child;
                }
                self.wait = sig;
                Some(TestState::Initialise)
            },
            Ok(s) => {
                println!("Unexpected signal when starting test");
                None
            },
            Err(e) => {
                println!("Error when starting test: {}", e);
                Some(TestState::Unrecoverable)
            },
        }
    }
}

impl <'a> StateMachine<LinuxData<'a>> for TestState {

    fn step(self, data: &mut LinuxData<'a>, config: &Config) -> TestState {
        match self {
            TestState::Start{start_time} => {
                if let Some(state) = data.check_for_tracee() {
                    state
                } else if start_time < 1000 {
                    TestState::Start{start_time: start_time + 1}
                } else {
                    TestState::Timeout
                }
            },
            TestState::Initialise => {
                if let Err(c) = trace_children(data.current) {
                    println!("Failed to trace child threads");
                }
                for trace in data.traces.iter() {
                    if let Some(addr) = trace.address {
                        match Breakpoint::new(data.current, addr) {
                            Ok(bp) => {
                                let _ = data.breakpoints.insert(addr, bp);
                            },
                            Err(e) => {
                                println!("Failed to instrument");
                            },
                        }
                    }
                }
                if let Ok(_) = continue_exec(data.parent, None) {
                    TestState::Waiting{start_time:0}
                } else {
                    TestState::Unrecoverable
                }
            },
            TestState::Waiting{start_time} => {
                if start_time > 100_000 {
                    TestState::Timeout
                } else {
                    let wait = waitpid(Pid::from_raw(-1), Some(WNOHANG | __WALL));
                    match wait {
                        Ok(WaitStatus::StillAlive) => {
                            data.wait = WaitStatus::StillAlive;
                            TestState::Waiting{start_time: start_time + 1}
                        },
                        Ok(s) => {
                            data.wait = s;
                            TestState::Stopped
                        },
                        Err(e) => {
                            println!("An error occurred");
                            TestState::Unrecoverable
                        },
                    }
                }
            },
            _ => TestState::Unrecoverable
        }
    }
}

<|endoftext|>


<commit_before>pub mod log_events;
use self::log_events::*;

pub fn parse_line(line: &String) -> Result<LogEvent, &'static str> {
    if line.contains("->") {
        let request = Request::new_from_log_line(line);

        return match request {
            Ok(request) => Ok(LogEvent::Request(request)),
            Err(err) => Err(err),
        };
    }

    if line.contains("<-") {
        let response = Response::new_from_log_line(line);

        return match response {
            Ok(response) => Ok(LogEvent::Response(response)),
            Err(err) => Err(err),
        };
    }

    Err("Line is neither a Request nor a Response")
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::log_events::*;

    #[test]
    fn test_parse_line_request() {
        let line = "08/Apr/2016:09:58:47 +0200 [02] -> GET /content/some/other.html HTTP/1.1"
            .to_string();

        let event = match parse_line(&line).unwrap() {
            LogEvent::Request(request) => request,
            LogEvent::Response(_) => unreachable!(),
        };

        assert_eq!(event.id, 2);
    }

    #[test]
    fn test_parse_line_response() {
        let line = "08/Apr/2016:09:58:48 +0200 [05] <- 200 text/html 10ms".to_string();

        let event = match parse_line(&line).unwrap() {
            LogEvent::Request(_) => unreachable!(),
            LogEvent::Response(response) => response,
        };

        assert_eq!(event.id, 5);
    }

    #[test]
    fn test_parse_line_unrecognized() {
        let line = "08/Apr/2016:09:58:48 +0200 [05] XY 200 text/html 10ms".to_string();

        let event = parse_line(&line);

        assert_eq!(event, Err("Line is neither a Request nor a Response"));
    }
}
<commit_msg>Test for magic "<-" marker in URL<commit_after>pub mod log_events;
use self::log_events::*;

pub fn parse_line(line: &String) -> Result<LogEvent, &'static str> {
    if line.contains("->") {
        let request = Request::new_from_log_line(line);

        return match request {
            Ok(request) => Ok(LogEvent::Request(request)),
            Err(err) => Err(err),
        };
    }

    if line.contains("<-") {
        let response = Response::new_from_log_line(line);

        return match response {
            Ok(response) => Ok(LogEvent::Response(response)),
            Err(err) => Err(err),
        };
    }

    Err("Line is neither a Request nor a Response")
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::log_events::*;

    #[test]
    fn test_parse_line_request() {
        let line = "08/Apr/2016:09:58:47 +0200 [02] -> GET /content/some/other.html HTTP/1.1"
            .to_string();

        let event = match parse_line(&line).unwrap() {
            LogEvent::Request(request) => request,
            LogEvent::Response(_) => unreachable!(),
        };

        assert_eq!(event.id, 2);
    }

    #[test]
    fn test_parse_line_response() {
        let line = "08/Apr/2016:09:58:48 +0200 [05] <- 200 text/html 10ms".to_string();

        let event = match parse_line(&line).unwrap() {
            LogEvent::Request(_) => unreachable!(),
            LogEvent::Response(response) => response,
        };

        assert_eq!(event.id, 5);
    }

    #[test]
    fn test_parse_line_unrecognized() {
        let line = "08/Apr/2016:09:58:48 +0200 [05] XY 200 text/html 10ms".to_string();

        let event = parse_line(&line);

        assert_eq!(event, Err("Line is neither a Request nor a Response"));
    }

    #[test]
    fn test_parse_line_with_response_arrow_in_url() {
        let line = "08/Apr/2016:09:58:47 +0200 [02] -> GET /content/<-.html HTTP/1.1".to_string();

        let event = match parse_line(&line).unwrap() {
            LogEvent::Request(request) => request,
            LogEvent::Response(_) => unreachable!(),
        };

        assert_eq!(event.url, "/content/<-.html");
    }
}
<|endoftext|>