blob: 3d30760fbe434998f469787745fb25f560f4a11c [file] [log] [blame] [edit]
use std::io::prelude::*;
use rayon::prelude::*;
#[derive(Debug)]
pub(crate) struct Runner {
cases: Vec<Case>,
}
impl Runner {
pub(crate) fn new() -> Self {
Self {
cases: Default::default(),
}
}
pub(crate) fn case(&mut self, case: Case) {
self.cases.push(case);
}
pub(crate) fn run(
&self,
mode: &Mode,
bins: &crate::BinRegistry,
substitutions: &crate::elide::Substitutions,
) {
let palette = crate::Palette::current();
if self.cases.is_empty() {
eprintln!(
"{}",
palette.warn.paint("There are no trycmd tests enabled yet")
);
} else {
let failures: Vec<_> = self
.cases
.par_iter()
.flat_map(|c| {
let results = c.run(mode, bins, substitutions);
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
results
.into_iter()
.filter_map(|s| {
debug!("Case: {:#?}", s);
match s {
Ok(status) => {
let _ = writeln!(
stderr,
"{} {} ... {}",
palette.hint.paint("Testing"),
status.name(),
status.spawn.status.summary()
);
if !status.is_ok() {
// Assuming `status` will print the newline
let _ = write!(stderr, "{}", &status);
}
None
}
Err(status) => {
let _ = writeln!(
stderr,
"{} {} ... {}",
palette.hint.paint("Testing"),
status.name(),
palette.error.paint("failed"),
);
// Assuming `status` will print the newline
let _ = write!(stderr, "{}", &status);
Some(status)
}
}
})
.collect::<Vec<_>>()
})
.collect();
if !failures.is_empty() {
let stderr = std::io::stderr();
let mut stderr = stderr.lock();
let _ = writeln!(
stderr,
"{}",
palette
.hint
.paint("Update snapshots with `TRYCMD=overwrite`"),
);
let _ = writeln!(
stderr,
"{}",
palette.hint.paint("Debug output with `TRYCMD=dump`"),
);
panic!("{} of {} tests failed", failures.len(), self.cases.len());
}
}
}
}
impl Default for Runner {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub(crate) struct Case {
pub(crate) path: std::path::PathBuf,
pub(crate) expected: Option<crate::schema::CommandStatus>,
pub(crate) timeout: Option<std::time::Duration>,
pub(crate) default_bin: Option<crate::schema::Bin>,
pub(crate) env: crate::schema::Env,
pub(crate) error: Option<SpawnStatus>,
}
impl Case {
pub(crate) fn with_error(path: std::path::PathBuf, error: crate::Error) -> Self {
Self {
path,
expected: None,
timeout: None,
default_bin: None,
env: Default::default(),
error: Some(SpawnStatus::Failure(error)),
}
}
pub(crate) fn run(
&self,
mode: &Mode,
bins: &crate::BinRegistry,
substitutions: &crate::elide::Substitutions,
) -> Vec<Result<Output, Output>> {
if self.expected == Some(crate::schema::CommandStatus::Skipped) {
let output = Output::sequence(self.path.clone());
assert_eq!(output.spawn.status, SpawnStatus::Skipped);
return vec![Ok(output)];
}
if let Some(err) = self.error.clone() {
let mut output = Output::step(self.path.clone(), "setup".into());
output.spawn.status = err;
return vec![Err(output)];
}
let mut sequence = match crate::schema::TryCmd::load(&self.path) {
Ok(sequence) => sequence,
Err(e) => {
let output = Output::step(self.path.clone(), "setup".into());
return vec![Err(output.error(e))];
}
};
if sequence.steps.is_empty() {
let output = Output::sequence(self.path.clone());
assert_eq!(output.spawn.status, SpawnStatus::Skipped);
return vec![Ok(output)];
}
let fs_context = match crate::FilesystemContext::new(
&self.path,
sequence.fs.base.as_deref(),
sequence.fs.sandbox(),
mode,
) {
Ok(fs_context) => fs_context,
Err(e) => {
let output = Output::step(self.path.clone(), "setup".into());
return vec![Err(
output.error(format!("Failed to initialize sandbox: {}", e).into())
)];
}
};
let cwd = match fs_context
.path()
.map(|p| {
sequence.fs.rel_cwd().map(|rel| {
let p = p.join(rel);
crate::filesystem::strip_trailing_slash(&p).to_owned()
})
})
.transpose()
{
Ok(cwd) => cwd.or_else(|| std::env::current_dir().ok()),
Err(e) => {
let output = Output::step(self.path.clone(), "setup".into());
return vec![Err(output.error(e))];
}
};
let mut substitutions = substitutions.clone();
if let Some(root) = fs_context.path() {
substitutions
.insert("[ROOT]", root.display().to_string())
.unwrap();
}
if let Some(cwd) = cwd.clone().or_else(|| std::env::current_dir().ok()) {
substitutions
.insert("[CWD]", cwd.display().to_string())
.unwrap();
}
substitutions
.insert("[EXE]", std::env::consts::EXE_SUFFIX)
.unwrap();
debug!("{:?}", substitutions);
let mut outputs = Vec::with_capacity(sequence.steps.len());
let mut prior_step_failed = false;
for step in &mut sequence.steps {
if prior_step_failed {
step.expected_status = Some(crate::schema::CommandStatus::Skipped);
}
let step_status = self.run_step(step, cwd.as_deref(), bins, &substitutions);
if fs_context.is_sandbox() && step_status.is_err() && *mode == Mode::Fail {
prior_step_failed = true;
}
outputs.push(step_status);
}
match mode {
Mode::Dump(root) => {
for output in &mut outputs {
let output = match output {
Ok(output) => output,
Err(output) => output,
};
output.stdout =
match self.dump_stream(root, output.id.as_deref(), output.stdout.take()) {
Ok(stream) => stream,
Err(stream) => stream,
};
output.stderr =
match self.dump_stream(root, output.id.as_deref(), output.stderr.take()) {
Ok(stream) => stream,
Err(stream) => stream,
};
}
}
Mode::Overwrite => {
// `rev()` to ensure we don't mess up our line number info
for output in outputs.iter().rev() {
if let Err(output) = output {
let _ = sequence.overwrite(
&self.path,
output.id.as_deref(),
output.stdout.as_ref().map(|s| &s.content),
output.stderr.as_ref().map(|s| &s.content),
);
}
}
}
Mode::Fail => {}
}
if sequence.fs.sandbox() {
let mut ok = true;
let mut output = Output::step(self.path.clone(), "teardown".into());
output.fs = match self.validate_fs(
fs_context.path().expect("sandbox must be filled"),
output.fs,
mode,
&substitutions,
) {
Ok(fs) => fs,
Err(fs) => {
ok = false;
fs
}
};
if let Err(err) = fs_context.close() {
ok = false;
output.fs.context.push(FileStatus::Failure(
format!("Failed to cleanup sandbox: {}", err).into(),
));
}
let output = if ok {
output.spawn.status = SpawnStatus::Ok;
Ok(output)
} else {
output.spawn.status = SpawnStatus::Failure("Files left in unexpected state".into());
Err(output)
};
outputs.push(output);
}
outputs
}
pub(crate) fn run_step(
&self,
step: &mut crate::schema::Step,
cwd: Option<&std::path::Path>,
bins: &crate::BinRegistry,
substitutions: &crate::elide::Substitutions,
) -> Result<Output, Output> {
let output = if let Some(id) = step.id.clone() {
Output::step(self.path.clone(), id)
} else {
Output::sequence(self.path.clone())
};
let mut bin = step.bin.take();
if bin.is_none() {
bin = self.default_bin.clone()
}
bin = bin
.map(|name| bins.resolve_bin(name))
.transpose()
.map_err(|e| output.clone().error(e))?;
step.bin = bin;
if step.timeout.is_none() {
step.timeout = self.timeout;
}
if self.expected.is_some() {
step.expected_status = self.expected;
}
step.env.update(&self.env);
if step.expected_status() == crate::schema::CommandStatus::Skipped {
assert_eq!(output.spawn.status, SpawnStatus::Skipped);
return Ok(output);
}
#[allow(unused_variables)]
match &step.bin {
Some(crate::schema::Bin::Path(_)) => {}
Some(crate::schema::Bin::Name(name)) => {
// Unhandled by resolve
debug!("bin={:?} not found", name);
assert_eq!(output.spawn.status, SpawnStatus::Skipped);
return Ok(output);
}
Some(crate::schema::Bin::Error(_)) => {}
// Unlike `Name`, this always represents a bug
None => {}
}
let cmd_output = step.to_output(cwd).map_err(|e| output.clone().error(e))?;
let output = output.output(cmd_output);
// For Mode::Dump's sake, allow running all
let output = self.validate_spawn(output, step.expected_status());
let output = self.validate_streams(output, step, substitutions);
if output.is_ok() {
Ok(output)
} else {
Err(output)
}
}
fn validate_spawn(&self, mut output: Output, expected: crate::schema::CommandStatus) -> Output {
let status = output.spawn.exit.expect("bale out before now");
match expected {
crate::schema::CommandStatus::Success => {
if !status.success() {
output.spawn.status = SpawnStatus::Expected("success".into());
}
}
crate::schema::CommandStatus::Failed => {
if status.success() || status.code().is_none() {
output.spawn.status = SpawnStatus::Expected("failure".into());
}
}
crate::schema::CommandStatus::Interrupted => {
if status.code().is_some() {
output.spawn.status = SpawnStatus::Expected("interrupted".into());
}
}
crate::schema::CommandStatus::Skipped => unreachable!("handled earlier"),
crate::schema::CommandStatus::Code(expected_code) => {
if Some(expected_code) != status.code() {
output.spawn.status = SpawnStatus::Expected(expected_code.to_string());
}
}
}
output
}
fn validate_streams(
&self,
mut output: Output,
step: &crate::schema::Step,
substitutions: &crate::elide::Substitutions,
) -> Output {
output.stdout = self.validate_stream(
output.stdout,
step.expected_stdout.as_ref(),
step.binary,
substitutions,
);
output.stderr = self.validate_stream(
output.stderr,
step.expected_stderr.as_ref(),
step.binary,
substitutions,
);
output
}
fn validate_stream(
&self,
stream: Option<Stream>,
expected_content: Option<&crate::File>,
binary: bool,
substitutions: &crate::elide::Substitutions,
) -> Option<Stream> {
let mut stream = stream?;
if !binary {
stream = stream.utf8();
if !stream.is_ok() {
return Some(stream);
}
}
if let Some(expected_content) = expected_content {
if let crate::File::Text(e) = &expected_content {
stream.content = stream
.content
.map_text(|t| crate::elide::normalize(t, e, substitutions));
}
if stream.content != *expected_content {
stream.status = StreamStatus::Expected(expected_content.clone());
return Some(stream);
}
}
Some(stream)
}
fn dump_stream(
&self,
root: &std::path::Path,
id: Option<&str>,
stream: Option<Stream>,
) -> Result<Option<Stream>, Option<Stream>> {
if let Some(stream) = stream {
let file_name = match id {
Some(id) => {
format!(
"{}-{}.{}",
self.path.file_stem().unwrap().to_string_lossy(),
id,
stream.stream.as_str(),
)
}
None => {
format!(
"{}.{}",
self.path.file_stem().unwrap().to_string_lossy(),
stream.stream.as_str(),
)
}
};
let stream_path = root.join(file_name);
stream.content.write_to(&stream_path).map_err(|e| {
let mut stream = stream.clone();
if stream.is_ok() {
stream.status = StreamStatus::Failure(e);
}
stream
})?;
Ok(Some(stream))
} else {
Ok(None)
}
}
fn validate_fs(
&self,
actual_root: &std::path::Path,
mut fs: Filesystem,
mode: &Mode,
substitutions: &crate::elide::Substitutions,
) -> Result<Filesystem, Filesystem> {
let mut ok = true;
if let Mode::Dump(_) = mode {
// Handled as part of FilesystemContext
} else {
let fixture_root = self.path.with_extension("out");
if fixture_root.exists() {
for expected_path in crate::FsIterate::new(&fixture_root) {
if expected_path
.as_deref()
.map(|p| p.is_dir())
.unwrap_or_default()
{
continue;
}
match self.validate_path(
expected_path,
&fixture_root,
actual_root,
substitutions,
) {
Ok(status) => {
fs.context.push(status);
}
Err(status) => {
let mut is_current_ok = false;
if *mode == Mode::Overwrite {
match &status {
FileStatus::TypeMismatch {
expected_path,
actual_path,
..
} => {
if crate::shallow_copy(expected_path, actual_path).is_ok() {
is_current_ok = true;
}
}
FileStatus::LinkMismatch {
expected_path,
actual_path,
..
} => {
if crate::shallow_copy(expected_path, actual_path).is_ok() {
is_current_ok = true;
}
}
FileStatus::ContentMismatch {
expected_path,
actual_content,
..
} => {
if actual_content.write_to(expected_path).is_ok() {
is_current_ok = true;
}
}
_ => {}
}
}
fs.context.push(status);
if !is_current_ok {
ok = false;
}
}
}
}
}
}
if ok {
Ok(fs)
} else {
Err(fs)
}
}
fn validate_path(
&self,
expected_path: Result<std::path::PathBuf, std::io::Error>,
fixture_root: &std::path::Path,
actual_root: &std::path::Path,
substitutions: &crate::elide::Substitutions,
) -> Result<FileStatus, FileStatus> {
let expected_path = expected_path.map_err(|e| FileStatus::Failure(e.to_string().into()))?;
let expected_meta = expected_path
.symlink_metadata()
.map_err(|e| FileStatus::Failure(e.to_string().into()))?;
let expected_target = std::fs::read_link(&expected_path).ok();
let rel = expected_path.strip_prefix(&fixture_root).unwrap();
let actual_path = actual_root.join(rel);
let actual_meta = actual_path.symlink_metadata().ok();
let actual_target = std::fs::read_link(&actual_path).ok();
let expected_type = if expected_meta.is_dir() {
FileType::Dir
} else if expected_meta.is_file() {
FileType::File
} else if expected_target.is_some() {
FileType::Symlink
} else {
FileType::Unknown
};
let actual_type = if let Some(actual_meta) = actual_meta {
if actual_meta.is_dir() {
FileType::Dir
} else if actual_meta.is_file() {
FileType::File
} else if actual_target.is_some() {
FileType::Symlink
} else {
FileType::Unknown
}
} else {
FileType::Missing
};
if expected_type != actual_type {
return Err(FileStatus::TypeMismatch {
expected_path,
actual_path,
expected_type,
actual_type,
});
}
match expected_type {
FileType::Symlink => {
if expected_target != actual_target {
return Err(FileStatus::LinkMismatch {
expected_path,
actual_path,
expected_target: expected_target.unwrap(),
actual_target: actual_target.unwrap(),
});
}
}
FileType::File => {
let expected_content = crate::File::read_from(&expected_path, None)
.map_err(FileStatus::Failure)?
.try_utf8();
let mut actual_content = crate::File::read_from(&actual_path, None)
.map_err(FileStatus::Failure)?
.try_utf8();
if let crate::File::Text(e) = &expected_content {
actual_content =
actual_content.map_text(|t| crate::elide::normalize(t, e, substitutions));
}
if expected_content != actual_content {
return Err(FileStatus::ContentMismatch {
expected_path,
actual_path,
expected_content,
actual_content,
});
}
}
FileType::Dir | FileType::Unknown | FileType::Missing => {}
}
Ok(FileStatus::Ok {
expected_path,
actual_path,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Output {
path: std::path::PathBuf,
id: Option<String>,
spawn: Spawn,
stdout: Option<Stream>,
stderr: Option<Stream>,
fs: Filesystem,
}
impl Output {
fn sequence(path: std::path::PathBuf) -> Self {
Self {
path,
id: None,
spawn: Spawn {
exit: None,
status: SpawnStatus::Skipped,
},
stdout: None,
stderr: None,
fs: Default::default(),
}
}
fn step(path: std::path::PathBuf, step: String) -> Self {
Self {
path,
id: Some(step),
spawn: Default::default(),
stdout: None,
stderr: None,
fs: Default::default(),
}
}
fn output(mut self, output: std::process::Output) -> Self {
self.spawn.exit = Some(output.status);
assert_eq!(self.spawn.status, SpawnStatus::Skipped);
self.spawn.status = SpawnStatus::Ok;
self.stdout = Some(Stream {
stream: Stdio::Stdout,
content: crate::File::Binary(output.stdout),
status: StreamStatus::Ok,
});
self.stderr = Some(Stream {
stream: Stdio::Stderr,
content: crate::File::Binary(output.stderr),
status: StreamStatus::Ok,
});
self
}
fn error(mut self, msg: crate::Error) -> Self {
self.spawn.status = SpawnStatus::Failure(msg);
self
}
fn is_ok(&self) -> bool {
self.spawn.is_ok()
&& self.stdout.as_ref().map(|s| s.is_ok()).unwrap_or(true)
&& self.stderr.as_ref().map(|s| s.is_ok()).unwrap_or(true)
&& self.fs.is_ok()
}
fn name(&self) -> String {
self.id
.as_deref()
.map(|id| format!("{}:{}", self.path.display(), id))
.unwrap_or_else(|| self.path.display().to_string())
}
}
impl std::fmt::Display for Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.spawn.fmt(f)?;
if let Some(stdout) = &self.stdout {
stdout.fmt(f)?;
}
if let Some(stderr) = &self.stderr {
stderr.fmt(f)?;
}
self.fs.fmt(f)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Spawn {
exit: Option<std::process::ExitStatus>,
status: SpawnStatus,
}
impl Spawn {
fn is_ok(&self) -> bool {
self.status.is_ok()
}
}
impl Default for Spawn {
fn default() -> Self {
Self {
exit: None,
status: SpawnStatus::Skipped,
}
}
}
impl std::fmt::Display for Spawn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let palette = crate::Palette::current();
match &self.status {
SpawnStatus::Ok => {
if let Some(exit) = self.exit {
if exit.success() {
writeln!(f, "Exit: {}", palette.info.paint("success"))?;
} else if let Some(code) = exit.code() {
writeln!(f, "Exit: {}", palette.error.paint(code))?;
} else {
writeln!(f, "Exit: {}", palette.error.paint("interrupted"))?;
}
}
}
SpawnStatus::Skipped => {
writeln!(f, "{}", palette.warn.paint("Skipped"))?;
}
SpawnStatus::Failure(msg) => {
writeln!(f, "Failed: {}", palette.error.paint(msg))?;
}
SpawnStatus::Expected(expected) => {
if let Some(exit) = self.exit {
if exit.success() {
writeln!(
f,
"Expected {}, was {}",
palette.info.paint(expected),
palette.error.paint("success")
)?;
} else if let Some(code) = exit.code() {
writeln!(
f,
"Expected {}, was {}",
palette.info.paint(expected),
palette.error.paint(code)
)?;
} else {
writeln!(
f,
"Expected {}, was {}",
palette.info.paint(expected),
palette.error.paint("interrupted")
)?;
}
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SpawnStatus {
Ok,
Skipped,
Failure(crate::Error),
Expected(String),
}
impl SpawnStatus {
fn is_ok(&self) -> bool {
match self {
Self::Ok | Self::Skipped => true,
Self::Failure(_) | Self::Expected(_) => false,
}
}
fn summary(&self) -> impl std::fmt::Display {
let palette = crate::Palette::current();
match self {
Self::Ok => palette.info.paint("ok"),
Self::Skipped => palette.warn.paint("ignored"),
Self::Failure(_) | Self::Expected(_) => palette.error.paint("failed"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Stream {
stream: Stdio,
content: crate::File,
status: StreamStatus,
}
impl Stream {
fn utf8(mut self) -> Self {
if self.content.utf8().is_err() {
self.status = StreamStatus::Failure("invalid UTF-8".into());
}
self
}
fn is_ok(&self) -> bool {
self.status.is_ok()
}
}
impl std::fmt::Display for Stream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let palette = crate::Palette::current();
match &self.status {
StreamStatus::Ok => {
writeln!(f, "{}:", self.stream)?;
writeln!(f, "{}", palette.info.paint(&self.content))?;
}
StreamStatus::Failure(msg) => {
writeln!(
f,
"{} {}:",
self.stream,
palette.error.paint(format_args!("({})", msg))
)?;
writeln!(f, "{}", palette.info.paint(&self.content))?;
}
StreamStatus::Expected(expected) => {
#[allow(unused_mut)]
let mut rendered = false;
#[cfg(feature = "diff")]
if let (crate::File::Text(expected), crate::File::Text(actual)) =
(&expected, &self.content)
{
let diff =
crate::diff::diff(expected, actual, self.stream, self.stream, palette);
writeln!(f, "{}", diff)?;
rendered = true;
}
if !rendered {
writeln!(f, "{} {}:", self.stream, palette.info.paint("(expected)"))?;
writeln!(f, "{}", palette.info.paint(&expected))?;
writeln!(f, "{} {}:", self.stream, palette.error.paint("(actual)"))?;
writeln!(f, "{}", palette.error.paint(&self.content))?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum StreamStatus {
Ok,
Failure(crate::Error),
Expected(crate::File),
}
impl StreamStatus {
fn is_ok(&self) -> bool {
match self {
Self::Ok => true,
Self::Failure(_) | Self::Expected(_) => false,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Stdio {
Stdout,
Stderr,
}
impl Stdio {
fn as_str(&self) -> &str {
match self {
Self::Stdout => "stdout",
Self::Stderr => "stderr",
}
}
}
impl std::fmt::Display for Stdio {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
struct Filesystem {
context: Vec<FileStatus>,
}
impl Filesystem {
fn is_ok(&self) -> bool {
if self.context.is_empty() {
true
} else {
self.context.iter().all(FileStatus::is_ok)
}
}
}
impl std::fmt::Display for Filesystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for status in &self.context {
status.fmt(f)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum FileStatus {
Ok {
expected_path: std::path::PathBuf,
actual_path: std::path::PathBuf,
},
Failure(crate::Error),
TypeMismatch {
expected_path: std::path::PathBuf,
actual_path: std::path::PathBuf,
expected_type: FileType,
actual_type: FileType,
},
LinkMismatch {
expected_path: std::path::PathBuf,
actual_path: std::path::PathBuf,
expected_target: std::path::PathBuf,
actual_target: std::path::PathBuf,
},
ContentMismatch {
expected_path: std::path::PathBuf,
actual_path: std::path::PathBuf,
expected_content: crate::File,
actual_content: crate::File,
},
}
impl FileStatus {
fn is_ok(&self) -> bool {
match self {
Self::Ok { .. } => true,
Self::Failure(_)
| Self::TypeMismatch { .. }
| Self::LinkMismatch { .. }
| Self::ContentMismatch { .. } => false,
}
}
}
impl std::fmt::Display for FileStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let palette = crate::Palette::current();
match &self {
FileStatus::Ok {
expected_path,
actual_path: _actual_path,
} => {
writeln!(
f,
"{}: is {}",
expected_path.display(),
palette.info.paint("good"),
)?;
}
FileStatus::Failure(msg) => {
writeln!(f, "{}", palette.error.paint(msg))?;
}
FileStatus::TypeMismatch {
expected_path,
actual_path: _actual_path,
expected_type,
actual_type,
} => {
writeln!(
f,
"{}: Expected {}, was {}",
expected_path.display(),
palette.info.paint(expected_type),
palette.error.paint(actual_type)
)?;
}
FileStatus::LinkMismatch {
expected_path,
actual_path: _actual_path,
expected_target,
actual_target,
} => {
writeln!(
f,
"{}: Expected {}, was {}",
expected_path.display(),
palette.info.paint(expected_target.display()),
palette.error.paint(actual_target.display())
)?;
}
FileStatus::ContentMismatch {
expected_path,
actual_path,
expected_content,
actual_content,
} => {
#[allow(unused_mut)]
let mut rendered = false;
#[cfg(feature = "diff")]
if let (crate::File::Text(expected), crate::File::Text(actual)) =
(&expected_content, &actual_content)
{
let diff = crate::diff::diff(
expected,
actual,
expected_path.display(),
actual_path.display(),
palette,
);
writeln!(f, "{}", diff)?;
rendered = true;
}
if !rendered {
writeln!(
f,
"{} {}:",
expected_path.display(),
palette.info.paint("(expected)")
)?;
writeln!(f, "{}", palette.info.paint(&expected_content))?;
writeln!(
f,
"{} {}:",
actual_path.display(),
palette.error.paint("(actual)")
)?;
writeln!(f, "{}", palette.error.paint(&actual_content))?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum FileType {
Dir,
File,
Symlink,
Unknown,
Missing,
}
impl FileType {
fn as_str(&self) -> &str {
match self {
Self::Dir => "dir",
Self::File => "file",
Self::Symlink => "symlink",
Self::Unknown => "unknown",
Self::Missing => "missing",
}
}
}
impl std::fmt::Display for FileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
Fail,
Overwrite,
Dump(std::path::PathBuf),
}
impl Mode {
pub(crate) fn initialize(&self) -> Result<(), std::io::Error> {
match self {
Self::Fail => {}
Self::Overwrite => {}
Self::Dump(root) => {
std::fs::create_dir_all(root)?;
let gitignore_path = root.join(".gitignore");
std::fs::write(gitignore_path, "*\n")?;
}
}
Ok(())
}
}