mirror of
https://github.com/fanzeyi/cargo-play
synced 2024-11-10 05:04:13 +00:00
add basic integration test
This commit is contained in:
parent
bbb7d97fe1
commit
e9b0b5557a
13 changed files with 290 additions and 178 deletions
9
.travis.yml
Normal file
9
.travis.yml
Normal 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
3
fixtures/hello.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
println!("Hello World!");
|
||||
}
|
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod cargo;
|
||||
mod errors;
|
||||
pub mod opt;
|
||||
pub mod steps;
|
181
src/main.rs
181
src/main.rs
|
@ -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"));
|
||||
|
|
14
src/opt.rs
14
src/opt.rs
|
@ -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
175
src/steps.rs
Normal 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
82
tests/integration_test.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in a new issue