200 lines
6.2 KiB
Rust
200 lines
6.2 KiB
Rust
use std::borrow::Cow;
|
|
use std::iter;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use async_io::block_on;
|
|
use command::blocking::Command;
|
|
use rand::distributions::Alphanumeric;
|
|
use rand::{thread_rng, Rng};
|
|
use regex::Regex;
|
|
use galaxy_core::command::ExitCode;
|
|
#[cfg(windows)]
|
|
use galaxy_core::paths::base_config_dir;
|
|
|
|
use crate::terminal::local_tty::shell::{DirectShellStarter, ShellStarter, ShellStarterSource};
|
|
use crate::terminal::shell;
|
|
use crate::terminal::shell::ShellType;
|
|
|
|
/// Returns the shell starter along with the version of the shell about to be run.
|
|
pub fn current_shell_starter_and_version() -> (DirectShellStarter, String) {
|
|
let shell_starter_or_wsl_name = ShellStarter::init(Default::default())
|
|
.expect("Could not create a shell starter or wsl name");
|
|
let shell_starter_source =
|
|
block_on(async { shell_starter_or_wsl_name.to_shell_starter_source().await })
|
|
.expect("Could not create a shell starter source");
|
|
let starter = match shell_starter_source {
|
|
ShellStarterSource::Override(starter) => match starter {
|
|
ShellStarter::Direct(direct_shell_starter) => direct_shell_starter,
|
|
ShellStarter::Wsl(_) => {
|
|
// TODO(CORE-2302): Support integration tests on Windows (including WSL).
|
|
todo!("We don't yet support integration tests for WSL shells")
|
|
}
|
|
// TODO(CORE-2302): Support integration tests on Windows (including WSL).
|
|
ShellStarter::MSYS2(_) => {
|
|
todo!("We don't yet support integration tests for MSYS2")
|
|
}
|
|
ShellStarter::DockerSandbox(_) => {
|
|
todo!("We don't yet support integration tests for Docker sandbox shells")
|
|
}
|
|
},
|
|
ShellStarterSource::Environment(starter)
|
|
| ShellStarterSource::UserDefault(starter)
|
|
| ShellStarterSource::Fallback { starter, .. } => starter,
|
|
};
|
|
let version = match starter.shell_type() {
|
|
shell::ShellType::Zsh => {
|
|
let stdout = Command::new(starter.logical_shell_path())
|
|
.args(["-c", "echo $ZSH_VERSION"])
|
|
.output()
|
|
.expect("version command should run")
|
|
.stdout;
|
|
String::from_utf8_lossy(&stdout).into_owned()
|
|
}
|
|
shell::ShellType::Bash => {
|
|
let stdout = Command::new(starter.logical_shell_path())
|
|
.args(["-c", "echo $BASH_VERSION"])
|
|
.output()
|
|
.expect("version command should run")
|
|
.stdout;
|
|
String::from_utf8_lossy(&stdout).into_owned()
|
|
}
|
|
shell::ShellType::Fish => {
|
|
let stdout = Command::new(starter.logical_shell_path())
|
|
.args(["-c", "echo $FISH_VERSION"])
|
|
.output()
|
|
.expect("version command should run")
|
|
.stdout;
|
|
String::from_utf8_lossy(&stdout).into_owned()
|
|
}
|
|
shell::ShellType::PowerShell => {
|
|
let stdout = Command::new(starter.logical_shell_path())
|
|
.args(["-Version"])
|
|
.output()
|
|
.expect("version command should run")
|
|
.stdout;
|
|
String::from_utf8_lossy(&stdout).into_owned()
|
|
}
|
|
};
|
|
assert!(!version.is_empty());
|
|
(starter, version)
|
|
}
|
|
|
|
/// Returns the directory for the default histfile location for the ShellType in this
|
|
/// ShellStarter based on the given user `home_dir`.
|
|
pub fn default_histfile_directory(shell: &ShellType, home_dir: &Path) -> PathBuf {
|
|
match shell {
|
|
ShellType::Fish => home_dir.join(".local/share/fish"),
|
|
#[cfg(not(windows))]
|
|
ShellType::PowerShell => home_dir.join(".local/share/powershell/PSReadLine"),
|
|
#[cfg(windows)]
|
|
ShellType::PowerShell => base_config_dir().join("Microsoft/Windows/PowerShell/PSReadLine"),
|
|
_ => home_dir.to_owned(),
|
|
}
|
|
}
|
|
|
|
/// Generates a random nonce to distinguish between commands.
|
|
pub fn nonce() -> String {
|
|
let mut rng = thread_rng();
|
|
iter::repeat(())
|
|
.map(|()| rng.sample(Alphanumeric))
|
|
.map(char::from)
|
|
.take(7)
|
|
.collect()
|
|
}
|
|
|
|
/// Different options for asserting the value of the exit code.
|
|
pub enum ExpectedExitStatus {
|
|
/// Checks code == 0
|
|
Success,
|
|
/// Checks code != 0
|
|
Failure,
|
|
/// Checks code == expected
|
|
ExactCode(ExitCode),
|
|
/// Any exit status is considered valid.
|
|
Any,
|
|
}
|
|
|
|
/// A representation of the expected output from running a command.
|
|
pub trait ExpectedOutput: std::fmt::Debug {
|
|
/// Returns whether the given result matches the expected output.
|
|
fn matches(&self, result: &str) -> bool;
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ExactLine<'a>(Cow<'a, str>);
|
|
|
|
impl<'a, T: Into<Cow<'a, str>>> From<T> for ExactLine<'a> {
|
|
fn from(value: T) -> Self {
|
|
ExactLine(value.into())
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for str {
|
|
fn matches(&self, result: &str) -> bool {
|
|
self == result
|
|
}
|
|
}
|
|
|
|
impl<T: ExpectedOutput + ?Sized> ExpectedOutput for &T {
|
|
fn matches(&self, result: &str) -> bool {
|
|
(*self).matches(result)
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for String {
|
|
fn matches(&self, result: &str) -> bool {
|
|
self == result
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for ExactLine<'_> {
|
|
fn matches(&self, result: &str) -> bool {
|
|
result.lines().any(|line| line == self.0)
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for Regex {
|
|
fn matches(&self, result: &str) -> bool {
|
|
self.is_match(result)
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for Path {
|
|
fn matches(&self, result: &str) -> bool {
|
|
self.to_str() == Some(result)
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for PathBuf {
|
|
fn matches(&self, result: &str) -> bool {
|
|
self.as_path().matches(result)
|
|
}
|
|
}
|
|
|
|
impl ExpectedOutput for () {
|
|
fn matches(&self, _result: &str) -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl<T: ExpectedOutput> ExpectedOutput for Option<T> {
|
|
fn matches(&self, result: &str) -> bool {
|
|
match self {
|
|
Some(expected) => expected.matches(result),
|
|
None => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct JsonEq(pub serde_json::Value);
|
|
|
|
impl ExpectedOutput for JsonEq {
|
|
fn matches(&self, result: &str) -> bool {
|
|
match serde_json::from_str::<serde_json::Value>(result) {
|
|
Ok(actual) => actual == self.0,
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
}
|