add basic integration test

This commit is contained in:
Zeyi Fan 2019-08-13 00:04:18 -07:00
parent bbb7d97fe1
commit e9b0b5557a
13 changed files with 290 additions and 178 deletions

9
.travis.yml Normal file
View file

@ -0,0 +1,9 @@
language: rust
cache: cargo
rust:
- stable
- beta
- nightly
script:
- cargo build --verbose
- cargo test --test-threads=1

3
fixtures/hello.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
println!("Hello World!");
}

4
src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
mod cargo;
mod errors;
pub mod opt;
pub mod steps;

View file

@ -1,182 +1,15 @@
mod cargo;
mod errors;
mod opt;
mod steps;
use log::debug;
use pathdiff::diff_paths;
use std::env;
use std::fs::File;
use std::io::{Read, Write};
use std::iter::Iterator;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::process::{Command, Stdio};
use std::vec::Vec;
use crate::cargo::CargoManifest;
use crate::errors::CargoPlayError;
use crate::opt::{Opt, RustEdition};
fn parse_inputs(inputs: &Vec<PathBuf>) -> Result<Vec<String>, CargoPlayError> {
inputs
.into_iter()
.map(File::open)
.map(|res| match res {
Ok(mut fp) => {
let mut buf = String::new();
fp.read_to_string(&mut buf)?;
Ok(buf)
}
Err(e) => Err(CargoPlayError::from(e)),
})
.collect()
}
fn extract_headers(files: &Vec<String>) -> Vec<String> {
files
.iter()
.map(|file: &String| -> Vec<String> {
file.lines()
.skip_while(|line| line.starts_with("#!") || line.is_empty())
.take_while(|line| line.starts_with("//#"))
.map(|line| line[3..].trim_start().into())
.filter(|s: &String| !s.is_empty())
.collect()
})
.flatten()
.collect()
}
fn temp_dir(name: PathBuf) -> PathBuf {
let mut temp = PathBuf::new();
temp.push(env::temp_dir());
temp.push(name);
temp
}
/// This function ignores the error intentionally.
fn rmtemp(temp: &PathBuf) {
debug!("Cleaning temporary folder at: {:?}", temp);
let _ = std::fs::remove_dir_all(temp);
}
fn mktemp(temp: &PathBuf) {
debug!("Creating temporary building folder at: {:?}", temp);
if let Err(_) = std::fs::create_dir(temp) {
debug!("Temporary directory already exists.");
}
}
fn write_cargo_toml(
dir: &PathBuf,
name: String,
dependencies: Vec<String>,
edition: RustEdition,
) -> Result<(), CargoPlayError> {
let manifest = CargoManifest::new(name, dependencies, edition)?;
let mut cargo = File::create(dir.join("Cargo.toml"))?;
cargo.write_all(&toml::to_vec(&manifest).map_err(CargoPlayError::from_serde)?)?;
Ok(())
}
/// Copy all the passed in sources to the temporary directory. The first in the list will be
/// treated as main.rs.
fn copy_sources(temp: &PathBuf, sources: &Vec<PathBuf>) -> Result<(), CargoPlayError> {
let destination = temp.join("src");
std::fs::create_dir_all(&destination)?;
let mut files = sources.iter();
let base = if let Some(first) = files.next() {
let dst = destination.join("main.rs");
debug!("Copying {:?} => {:?}", first, dst);
std::fs::copy(first, dst)?;
first.parent()
} else {
None
};
if let Some(base) = base {
files
.map(|file| -> Result<(), CargoPlayError> {
let part =
diff_paths(file, base).ok_or(CargoPlayError::DiffPathError(file.to_owned()))?;
let dst = destination.join(part);
// ensure the parent folder all exists
if let Some(parent) = dst.parent() {
let _ = std::fs::create_dir_all(&parent);
}
debug!("Copying {:?} => {:?}", file, dst);
std::fs::copy(file, dst).map(|_| ()).map_err(From::from)
})
.collect::<Result<Vec<_>, _>>()?;
}
Ok(())
}
fn run_cargo_build(
toolchain: Option<String>,
project: &PathBuf,
release: bool,
cargo_option: Option<String>,
) -> Result<ExitStatus, CargoPlayError> {
let mut cargo = Command::new("cargo");
if let Some(toolchain) = toolchain {
cargo.arg(format!("+{}", toolchain));
}
cargo
.arg("run")
.arg("--manifest-path")
.arg(project.join("Cargo.toml"));
if let Some(cargo_option) = cargo_option {
// FIXME: proper escaping
cargo.args(cargo_option.split_ascii_whitespace());
}
if release {
cargo.arg("--release");
}
cargo
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.status()
.map_err(From::from)
}
fn copy_project<T: AsRef<Path>, U: AsRef<Path>>(
from: T,
to: U,
) -> Result<ExitStatus, CargoPlayError> {
let to = to.as_ref();
if to.is_dir() {
return Err(CargoPlayError::PathExistError(to.to_path_buf()));
}
Command::new("cp")
.arg("-R")
.arg(from.as_ref())
.arg(&to)
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.status()
.map(|x| {
// At this point we are certain the `to` path exists
println!(
"Generated project at {}",
to.canonicalize().unwrap().display()
);
x
})
.map_err(From::from)
}
use crate::opt::Opt;
use crate::steps::*;
fn main() -> Result<(), CargoPlayError> {
let args = std::env::args().collect::<Vec<_>>();
@ -216,11 +49,11 @@ fn main() -> Result<(), CargoPlayError> {
if opt.clean {
rmtemp(&temp);
}
dbg!(mktemp(&temp));
mktemp(&temp);
write_cargo_toml(&temp, src_hash.clone(), dependencies, opt.edition)?;
copy_sources(&temp, &opt.src)?;
let end = if let Some(save) = dbg!(opt.save) {
let end = if let Some(save) = opt.save {
copy_project(&temp, &save)?
} else {
run_cargo_build(opt.toolchain, &temp, opt.release, opt.cargo_option)?
@ -247,7 +80,7 @@ mod tests {
.into_iter()
.map(Into::into)
.collect();
let result = dbg!(extract_headers(&inputs));
let result = extract_headers(&inputs);
assert_eq!(result.len(), 2);
assert_eq!(result[0], String::from("line 1"));

View file

@ -8,7 +8,7 @@ use structopt::StructOpt;
use crate::errors::CargoPlayError;
#[derive(Debug)]
pub(crate) enum RustEdition {
pub enum RustEdition {
E2015,
E2018,
}
@ -36,14 +36,20 @@ impl Into<String> for RustEdition {
}
}
#[derive(Debug, StructOpt)]
impl Default for RustEdition {
fn default() -> Self {
RustEdition::E2018
}
}
#[derive(Debug, StructOpt, Default)]
#[structopt(
name = "cargo-play",
about = "Run your Rust program without Cargo.toml"
)]
pub(crate) struct Opt {
pub struct Opt {
#[structopt(short = "d", long = "debug", hidden = true)]
debug: bool,
pub debug: bool,
#[structopt(short = "c", long = "clean")]
/// Rebuild the cargo project without the cache from previous run
pub clean: bool,

175
src/steps.rs Normal file
View file

@ -0,0 +1,175 @@
use log::debug;
use pathdiff::diff_paths;
use std::env;
use std::fs::File;
use std::io::{Read, Write};
use std::iter::Iterator;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::vec::Vec;
use crate::cargo::CargoManifest;
use crate::errors::CargoPlayError;
use crate::opt::RustEdition;
pub fn parse_inputs(inputs: &Vec<PathBuf>) -> Result<Vec<String>, CargoPlayError> {
inputs
.into_iter()
.map(File::open)
.map(|res| match res {
Ok(mut fp) => {
let mut buf = String::new();
fp.read_to_string(&mut buf)?;
Ok(buf)
}
Err(e) => Err(CargoPlayError::from(e)),
})
.collect()
}
pub fn extract_headers(files: &Vec<String>) -> Vec<String> {
files
.iter()
.map(|file: &String| -> Vec<String> {
file.lines()
.skip_while(|line| line.starts_with("#!") || line.is_empty())
.take_while(|line| line.starts_with("//#"))
.map(|line| line[3..].trim_start().into())
.filter(|s: &String| !s.is_empty())
.collect()
})
.flatten()
.collect()
}
pub fn temp_dir(name: PathBuf) -> PathBuf {
let mut temp = PathBuf::new();
temp.push(env::temp_dir());
temp.push(name);
temp
}
/// This function ignores the error intentionally.
pub fn rmtemp(temp: &PathBuf) {
debug!("Cleaning temporary folder at: {:?}", temp);
let _ = std::fs::remove_dir_all(temp);
}
pub fn mktemp(temp: &PathBuf) {
debug!("Creating temporary building folder at: {:?}", temp);
if let Err(_) = std::fs::create_dir(temp) {
debug!("Temporary directory already exists.");
}
}
pub fn write_cargo_toml(
dir: &PathBuf,
name: String,
dependencies: Vec<String>,
edition: RustEdition,
) -> Result<(), CargoPlayError> {
let manifest = CargoManifest::new(name, dependencies, edition)?;
let mut cargo = File::create(dir.join("Cargo.toml"))?;
cargo.write_all(&toml::to_vec(&manifest).map_err(CargoPlayError::from_serde)?)?;
Ok(())
}
/// Copy all the passed in sources to the temporary directory. The first in the list will be
/// treated as main.rs.
pub fn copy_sources(temp: &PathBuf, sources: &Vec<PathBuf>) -> Result<(), CargoPlayError> {
let destination = temp.join("src");
std::fs::create_dir_all(&destination)?;
let mut files = sources.iter();
let base = if let Some(first) = files.next() {
let dst = destination.join("main.rs");
debug!("Copying {:?} => {:?}", first, dst);
std::fs::copy(first, dst)?;
first.parent()
} else {
None
};
if let Some(base) = base {
files
.map(|file| -> Result<(), CargoPlayError> {
let part =
diff_paths(file, base).ok_or(CargoPlayError::DiffPathError(file.to_owned()))?;
let dst = destination.join(part);
// ensure the parent folder all exists
if let Some(parent) = dst.parent() {
let _ = std::fs::create_dir_all(&parent);
}
debug!("Copying {:?} => {:?}", file, dst);
std::fs::copy(file, dst).map(|_| ()).map_err(From::from)
})
.collect::<Result<Vec<_>, _>>()?;
}
Ok(())
}
pub fn run_cargo_build(
toolchain: Option<String>,
project: &PathBuf,
release: bool,
cargo_option: Option<String>,
) -> Result<ExitStatus, CargoPlayError> {
let mut cargo = Command::new("cargo");
if let Some(toolchain) = toolchain {
cargo.arg(format!("+{}", toolchain));
}
cargo
.arg("run")
.arg("--manifest-path")
.arg(project.join("Cargo.toml"));
if let Some(cargo_option) = cargo_option {
// FIXME: proper escaping
cargo.args(cargo_option.split_ascii_whitespace());
}
if release {
cargo.arg("--release");
}
cargo
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.status()
.map_err(From::from)
}
pub fn copy_project<T: AsRef<Path>, U: AsRef<Path>>(
from: T,
to: U,
) -> Result<ExitStatus, CargoPlayError> {
let to = to.as_ref();
if to.is_dir() {
return Err(CargoPlayError::PathExistError(to.to_path_buf()));
}
Command::new("cp")
.arg("-R")
.arg(from.as_ref())
.arg(&to)
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.status()
.map(|x| {
// At this point we are certain the `to` path exists
println!(
"Generated project at {}",
to.canonicalize().unwrap().display()
);
x
})
.map_err(From::from)
}

82
tests/integration_test.rs Normal file
View file

@ -0,0 +1,82 @@
use cargo_play::opt::Opt;
use cargo_play::steps;
use std::env;
use std::ffi::OsStr;
use std::io::Result;
use std::path::PathBuf;
use std::process::{ExitStatus, Output, Stdio};
fn cargo_play_binary_path() -> PathBuf {
let mut path = env::current_exe().unwrap();
path.pop();
if path.ends_with("deps") {
path.pop();
}
let exe = String::from("cargo-play") + env::consts::EXE_SUFFIX;
path.push(exe);
path
}
#[derive(Debug)]
struct StringOutput {
pub status: ExitStatus,
pub stdout: String,
pub stderr: String,
}
impl From<std::process::Output> for StringOutput {
fn from(v: Output) -> Self {
StringOutput {
status: v.status,
stdout: String::from_utf8_lossy(&v.stdout).to_string(),
stderr: String::from_utf8_lossy(&v.stderr).to_string(),
}
}
}
fn cargo_play<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
args: I,
) -> std::io::Result<StringOutput> {
let mut play = std::process::Command::new(cargo_play_binary_path());
play.args(args)
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.output()
.map(From::from)
}
#[test]
fn basic_compile() -> Result<()> {
let output = cargo_play(&["-c", "fixtures/hello.rs"])?;
assert_eq!(output.status.code().unwrap(), 0);
assert_eq!(output.stdout, "Hello World!\n");
Ok(())
}
#[test]
fn clean() -> Result<()> {
let opt = Opt {
src: vec![PathBuf::from("fixtures/hello.rs").canonicalize()?],
..Default::default()
};
let path = steps::temp_dir(opt.temp_dirname());
let canary = path.clone().join("canary");
if path.exists() {
std::fs::remove_dir_all(&path)?;
}
println!("{:?}", path);
let _ = dbg!(cargo_play(&["fixtures/hello.rs"])?);
assert!(path.exists());
std::fs::write(&canary, "I_AM_CANARY")?;
assert!(canary.exists());
let _ = cargo_play(&["--clean", "fixtures/hello.rs"])?;
assert!(!canary.exists());
Ok(())
}