Add initial nu-test-support port (#913)

* Add initial nu-test-support port

* finish changing binary name

* Oops, these aren't Windows-safe tests
This commit is contained in:
JT
2022-02-02 15:59:01 -05:00
committed by GitHub
parent cbdc0e2010
commit cc1b784e3d
169 changed files with 7276 additions and 56 deletions

View File

@ -0,0 +1,163 @@
use super::nu_process::*;
use super::EnvironmentVariable;
use std::ffi::OsString;
use std::fmt;
#[derive(Default, Debug)]
pub struct Director {
pub cwd: Option<OsString>,
pub environment_vars: Vec<EnvironmentVariable>,
pub config: Option<OsString>,
pub pipeline: Option<Vec<String>>,
pub executable: Option<NuProcess>,
}
impl Director {
pub fn cococo(&self, arg: &str) -> Self {
let mut process = NuProcess {
environment_vars: self.environment_vars.clone(),
..Default::default()
};
process.args(&["--testbin", "cococo", arg]);
Director {
config: self.config.clone(),
executable: Some(process),
environment_vars: self.environment_vars.clone(),
..Default::default()
}
}
pub fn and_then(&mut self, commands: &str) -> &mut Self {
let commands = commands.to_string();
if let Some(ref mut pipeline) = self.pipeline {
pipeline.push(commands);
} else {
self.pipeline = Some(vec![commands]);
}
self
}
pub fn pipeline(&self, commands: &str) -> Self {
let mut director = Director {
pipeline: if commands.is_empty() {
None
} else {
Some(vec![commands.to_string()])
},
..Default::default()
};
let mut process = NuProcess {
environment_vars: self.environment_vars.clone(),
..Default::default()
};
if let Some(working_directory) = &self.cwd {
process.cwd(working_directory);
}
process.arg("--skip-plugins");
process.arg("--no-history");
if let Some(config_file) = self.config.as_ref() {
process.args(&[
"--config-file",
config_file.to_str().expect("failed to convert."),
]);
}
process.arg("--perf");
director.executable = Some(process);
director
}
pub fn executable(&self) -> Option<&NuProcess> {
if let Some(binary) = &self.executable {
Some(binary)
} else {
None
}
}
}
impl Executable for Director {
fn execute(&mut self) -> NuResult {
use std::io::Write;
use std::process::Stdio;
match self.executable() {
Some(binary) => {
let mut process = match binary
.construct()
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(why) => panic!("Can't run test {}", why),
};
if let Some(pipelines) = &self.pipeline {
let child = process.stdin.as_mut().expect("Failed to open stdin");
for pipeline in pipelines {
child
.write_all(format!("{}\n", pipeline).as_bytes())
.expect("Could not write to");
}
child.write_all(b"exit\n").expect("Could not write to");
}
process
.wait_with_output()
.map_err(|_| {
let reason = format!(
"could not execute process {} ({})",
binary, "No execution took place"
);
NuError {
desc: reason,
exit: None,
output: None,
}
})
.and_then(|process| {
let out =
Outcome::new(&read_std(&process.stdout), &read_std(&process.stderr));
match process.status.success() {
true => Ok(out),
false => Err(NuError {
desc: String::new(),
exit: Some(process.status),
output: Some(out),
}),
}
})
}
None => Err(NuError {
desc: String::from("err"),
exit: None,
output: None,
}),
}
}
}
impl fmt::Display for Director {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "director")
}
}
fn read_std(std: &[u8]) -> Vec<u8> {
let out = String::from_utf8_lossy(std);
let out = out.lines().collect::<Vec<_>>().join("\n");
let out = out.replace("\r\n", "");
out.replace("\n", "").into_bytes()
}

View File

@ -0,0 +1,105 @@
use hamcrest2::core::{MatchResult, Matcher};
use std::fmt;
use std::str;
use super::nu_process::Outcome;
use super::{Director, Executable};
#[derive(Clone)]
pub struct Play {
stdout_expectation: Option<String>,
}
impl fmt::Display for Play {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "play")
}
}
impl fmt::Debug for Play {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "play")
}
}
pub fn says() -> Play {
Play {
stdout_expectation: None,
}
}
trait CheckerMatchers {
fn output(&self, actual: &Outcome) -> MatchResult;
fn std(&self, actual: &[u8], expected: Option<&String>, description: &str) -> MatchResult;
fn stdout(&self, actual: &Outcome) -> MatchResult;
}
impl CheckerMatchers for Play {
fn output(&self, actual: &Outcome) -> MatchResult {
self.stdout(actual)
}
fn stdout(&self, actual: &Outcome) -> MatchResult {
self.std(&actual.out, self.stdout_expectation.as_ref(), "stdout")
}
fn std(&self, actual: &[u8], expected: Option<&String>, description: &str) -> MatchResult {
let out = match expected {
Some(out) => out,
None => return Ok(()),
};
let actual = match str::from_utf8(actual) {
Err(..) => return Err(format!("{} was not utf8 encoded", description)),
Ok(actual) => actual,
};
if actual != *out {
return Err(format!(
"not equal:\n actual: {}\n expected: {}\n\n",
actual, out
));
}
Ok(())
}
}
impl Matcher<Outcome> for Play {
fn matches(&self, output: Outcome) -> MatchResult {
self.output(&output)
}
}
impl Matcher<Director> for Play {
fn matches(&self, mut director: Director) -> MatchResult {
self.matches(&mut director)
}
}
impl<'a> Matcher<&'a mut Director> for Play {
fn matches(&self, director: &'a mut Director) -> MatchResult {
if director.executable().is_none() {
return Err(format!("no such process {}", director));
}
let res = director.execute();
match res {
Ok(out) => self.output(&out),
Err(err) => {
if let Some(out) = &err.output {
return self.output(out);
}
Err(format!("could not exec process {}: {:?}", director, err))
}
}
}
}
impl Play {
pub fn stdout(mut self, expected: &str) -> Self {
self.stdout_expectation = Some(expected.to_string());
self
}
}

View File

@ -0,0 +1,104 @@
use super::EnvironmentVariable;
use crate::fs::{binaries as test_bins_path, executable_path};
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::path::Path;
use std::process::{Command, ExitStatus};
pub trait Executable {
fn execute(&mut self) -> NuResult;
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub out: Vec<u8>,
pub err: Vec<u8>,
}
impl Outcome {
pub fn new(out: &[u8], err: &[u8]) -> Outcome {
Outcome {
out: out.to_vec(),
err: err.to_vec(),
}
}
}
pub type NuResult = Result<Outcome, NuError>;
#[derive(Debug)]
pub struct NuError {
pub desc: String,
pub exit: Option<ExitStatus>,
pub output: Option<Outcome>,
}
#[derive(Clone, Debug, Default)]
pub struct NuProcess {
pub arguments: Vec<OsString>,
pub environment_vars: Vec<EnvironmentVariable>,
pub cwd: Option<OsString>,
}
impl fmt::Display for NuProcess {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "`nu")?;
for arg in &self.arguments {
write!(f, " {}", arg.to_string_lossy())?;
}
write!(f, "`")
}
}
impl NuProcess {
pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut Self {
self.arguments.push(arg.as_ref().to_os_string());
self
}
pub fn args<T: AsRef<OsStr>>(&mut self, arguments: &[T]) -> &mut NuProcess {
self.arguments
.extend(arguments.iter().map(|t| t.as_ref().to_os_string()));
self
}
pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut NuProcess {
self.cwd = Some(path.as_ref().to_os_string());
self
}
pub fn get_cwd(&self) -> Option<&Path> {
self.cwd.as_ref().map(Path::new)
}
pub fn construct(&self) -> Command {
let mut command = Command::new(&executable_path());
if let Some(cwd) = self.get_cwd() {
command.current_dir(cwd);
}
command.env_clear();
let paths = vec![test_bins_path()];
let paths_joined = match std::env::join_paths(&paths) {
Ok(all) => all,
Err(_) => panic!("Couldn't join paths for PATH var."),
};
command.env(crate::NATIVE_PATH_ENV_VAR, paths_joined);
for env_var in &self.environment_vars {
command.env(&env_var.name, &env_var.value);
}
for arg in &self.arguments {
command.arg(arg);
}
command
}
}

View File

@ -0,0 +1,248 @@
use super::Director;
use crate::fs;
use crate::fs::Stub;
use getset::Getters;
use glob::glob;
use std::path::{Path, PathBuf};
use std::str;
use tempfile::{tempdir, TempDir};
#[derive(Default, Clone, Debug)]
pub struct EnvironmentVariable {
pub name: String,
pub value: String,
}
impl EnvironmentVariable {
fn new(name: &str, value: &str) -> Self {
Self {
name: name.to_string(),
value: value.to_string(),
}
}
}
pub struct Playground<'a> {
root: TempDir,
tests: String,
cwd: PathBuf,
config: PathBuf,
environment_vars: Vec<EnvironmentVariable>,
dirs: &'a Dirs,
}
#[derive(Default, Getters, Clone)]
#[get = "pub"]
pub struct Dirs {
pub root: PathBuf,
pub test: PathBuf,
pub fixtures: PathBuf,
}
impl Dirs {
pub fn formats(&self) -> PathBuf {
self.fixtures.join("formats")
}
pub fn config_fixtures(&self) -> PathBuf {
self.fixtures.join("playground/config")
}
}
impl<'a> Playground<'a> {
pub fn root(&self) -> &Path {
self.root.path()
}
pub fn cwd(&self) -> &Path {
&self.cwd
}
pub fn back_to_playground(&mut self) -> &mut Self {
self.cwd = PathBuf::from(self.root()).join(self.tests.clone());
self
}
pub fn play(&mut self) -> &mut Self {
self
}
pub fn setup(topic: &str, block: impl FnOnce(Dirs, &mut Playground)) {
let root = tempdir().expect("Couldn't create a tempdir");
let nuplay_dir = root.path().join(topic);
if PathBuf::from(&nuplay_dir).exists() {
std::fs::remove_dir_all(PathBuf::from(&nuplay_dir)).expect("can not remove directory");
}
std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory");
let fixtures = fs::fixtures();
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let fixtures = nu_path::canonicalize_with(fixtures.clone(), cwd).unwrap_or_else(|e| {
panic!(
"Couldn't canonicalize fixtures path {}: {:?}",
fixtures.display(),
e
)
});
let mut playground = Playground {
root,
tests: topic.to_string(),
cwd: nuplay_dir,
config: fixtures.join("playground/config/default.toml"),
environment_vars: Vec::default(),
dirs: &Dirs::default(),
};
let playground_root = playground.root.path();
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let test =
nu_path::canonicalize_with(playground_root.join(topic), cwd).unwrap_or_else(|e| {
panic!(
"Couldn't canonicalize test path {}: {:?}",
playground_root.join(topic).display(),
e
)
});
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let root = nu_path::canonicalize_with(playground_root, cwd).unwrap_or_else(|e| {
panic!(
"Couldn't canonicalize tests root path {}: {:?}",
playground_root.display(),
e
)
});
let dirs = Dirs {
root,
test,
fixtures,
};
playground.dirs = &dirs;
block(dirs.clone(), &mut playground);
}
pub fn with_config(&mut self, source_file: impl AsRef<Path>) -> &mut Self {
self.config = source_file.as_ref().to_path_buf();
self
}
pub fn with_env(&mut self, name: &str, value: &str) -> &mut Self {
self.environment_vars
.push(EnvironmentVariable::new(name, value));
self
}
pub fn get_config(&self) -> &str {
self.config.to_str().expect("could not convert path.")
}
pub fn build(&mut self) -> Director {
Director {
cwd: Some(self.dirs.test().into()),
config: Some(self.config.clone().into()),
environment_vars: self.environment_vars.clone(),
..Default::default()
}
}
pub fn cococo(&mut self, arg: &str) -> Director {
self.build().cococo(arg)
}
pub fn pipeline(&mut self, commands: &str) -> Director {
self.build().pipeline(commands)
}
pub fn mkdir(&mut self, directory: &str) -> &mut Self {
self.cwd.push(directory);
std::fs::create_dir_all(&self.cwd).expect("can not create directory");
self.back_to_playground();
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn symlink(&mut self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> &mut Self {
let from = self.cwd.join(from);
let to = self.cwd.join(to);
let create_symlink = {
#[cfg(unix)]
{
std::os::unix::fs::symlink
}
#[cfg(windows)]
{
if from.is_file() {
std::os::windows::fs::symlink_file
} else if from.is_dir() {
std::os::windows::fs::symlink_dir
} else {
panic!("symlink from must be a file or dir")
}
}
};
create_symlink(from, to).expect("can not create symlink");
self.back_to_playground();
self
}
pub fn with_files(&mut self, files: Vec<Stub>) -> &mut Self {
let endl = fs::line_ending();
files
.iter()
.map(|f| {
let mut path = PathBuf::from(&self.cwd);
let (file_name, contents) = match *f {
Stub::EmptyFile(name) => (name, "fake data".to_string()),
Stub::FileWithContent(name, content) => (name, content.to_string()),
Stub::FileWithContentToBeTrimmed(name, content) => (
name,
content
.lines()
.skip(1)
.map(|line| line.trim())
.collect::<Vec<&str>>()
.join(&endl),
),
};
path.push(file_name);
std::fs::write(path, contents.as_bytes()).expect("can not create file");
})
.for_each(drop);
self.back_to_playground();
self
}
pub fn within(&mut self, directory: &str) -> &mut Self {
self.cwd.push(directory);
std::fs::create_dir(&self.cwd).expect("can not create directory");
self
}
pub fn glob_vec(pattern: &str) -> Vec<PathBuf> {
let glob = glob(pattern);
glob.expect("invalid pattern")
.map(|path| {
if let Ok(path) = path {
path
} else {
unreachable!()
}
})
.collect()
}
}

View File

@ -0,0 +1,41 @@
use crate::playground::Playground;
use std::path::{Path, PathBuf};
use super::matchers::says;
use hamcrest2::assert_that;
use hamcrest2::prelude::*;
fn path(p: &Path) -> PathBuf {
let cwd = std::env::current_dir().expect("Could not get current working directory.");
nu_path::canonicalize_with(p, cwd)
.unwrap_or_else(|e| panic!("Couldn't canonicalize path {}: {:?}", p.display(), e))
}
#[test]
fn asserts_standard_out_expectation_from_nu_executable() {
Playground::setup("topic", |_, nu| {
assert_that!(nu.cococo("andres"), says().stdout("andres"));
})
}
#[test]
fn current_working_directory_in_sandbox_directory_created() {
Playground::setup("topic", |dirs, nu| {
let original_cwd = dirs.test();
nu.within("some_directory_within");
assert_eq!(path(nu.cwd()), original_cwd.join("some_directory_within"));
})
}
#[test]
fn current_working_directory_back_to_root_from_anywhere() {
Playground::setup("topic", |dirs, nu| {
let original_cwd = dirs.test();
nu.within("some_directory_within");
nu.back_to_playground();
assert_eq!(path(nu.cwd()), *original_cwd);
})
}