398 lines
12 KiB
Rust
398 lines
12 KiB
Rust
#![feature(round_char_boundary)]
|
|
#![feature(exact_size_is_empty)]
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::process::ExitCode;
|
|
|
|
use futures::future::BoxFuture;
|
|
use futures::future::FutureExt;
|
|
use organic::compare::silent_compare_on_file;
|
|
use tokio::sync::Semaphore;
|
|
use tokio::task::JoinError;
|
|
use walkdir::WalkDir;
|
|
|
|
#[cfg(feature = "tracing")]
|
|
use crate::init_tracing::init_telemetry;
|
|
#[cfg(feature = "tracing")]
|
|
use crate::init_tracing::shutdown_telemetry;
|
|
#[cfg(feature = "tracing")]
|
|
mod init_tracing;
|
|
|
|
#[cfg(not(feature = "tracing"))]
|
|
fn main() -> Result<ExitCode, Box<dyn std::error::Error>> {
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
rt.block_on(async {
|
|
let main_body_result = main_body().await;
|
|
main_body_result
|
|
})
|
|
}
|
|
|
|
#[cfg(feature = "tracing")]
|
|
fn main() -> Result<ExitCode, Box<dyn std::error::Error>> {
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
rt.block_on(async {
|
|
init_telemetry()?;
|
|
let main_body_result = main_body().await;
|
|
shutdown_telemetry()?;
|
|
main_body_result
|
|
})
|
|
}
|
|
|
|
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
|
async fn main_body() -> Result<ExitCode, Box<dyn std::error::Error>> {
|
|
let layer = compare_group("org-mode", || {
|
|
compare_all_org_document("/foreign_documents/org-mode")
|
|
});
|
|
let layer = layer.chain(compare_group("emacs", || {
|
|
compare_all_org_document("/foreign_documents/emacs")
|
|
}));
|
|
let layer = layer.chain(compare_group("worg", || {
|
|
compare_all_org_document("/foreign_documents/worg")
|
|
}));
|
|
let layer = layer.chain(compare_group("howard_abrams", compare_howard_abrams));
|
|
let layer = layer.chain(compare_group("doomemacs", || {
|
|
compare_all_org_document("/foreign_documents/doomemacs")
|
|
}));
|
|
let layer = layer.chain(compare_group("literate_build_emacs", || {
|
|
compare_all_org_document("/foreign_documents/literate_build_emacs")
|
|
}));
|
|
|
|
let running_tests: Vec<_> = layer.map(|c| tokio::spawn(c.run_test())).collect();
|
|
let mut any_failed = false;
|
|
for test in running_tests.into_iter() {
|
|
let test_result = test.await??;
|
|
if test_result.is_immediately_bad() || test_result.has_bad_children() {
|
|
any_failed = true;
|
|
}
|
|
test_result.print();
|
|
}
|
|
|
|
if any_failed {
|
|
println!(
|
|
"{color}Some tests failed.{reset}",
|
|
color = TestResult::foreground_color(255, 0, 0),
|
|
reset = TestResult::reset_color(),
|
|
);
|
|
Ok(ExitCode::FAILURE)
|
|
} else {
|
|
println!(
|
|
"{color}All tests passed.{reset}",
|
|
color = TestResult::foreground_color(0, 255, 0),
|
|
reset = TestResult::reset_color(),
|
|
);
|
|
Ok(ExitCode::SUCCESS)
|
|
}
|
|
}
|
|
|
|
fn compare_howard_abrams() -> impl Iterator<Item = TestConfig> {
|
|
let layer = compare_group("dot-files", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/dot-files")
|
|
});
|
|
let layer = layer.chain(compare_group("hamacs", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/hamacs")
|
|
}));
|
|
let layer = layer.chain(compare_group("demo-it", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/demo-it")
|
|
}));
|
|
let layer = layer.chain(compare_group("magit-demo", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/magit-demo")
|
|
}));
|
|
let layer = layer.chain(compare_group("pdx-emacs-hackers", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/pdx-emacs-hackers")
|
|
}));
|
|
let layer = layer.chain(compare_group("flora-simulator", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/flora-simulator")
|
|
}));
|
|
let layer = layer.chain(compare_group("literate-devops-demo", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/literate-devops-demo")
|
|
}));
|
|
let layer = layer.chain(compare_group("clojure-yesql-xp", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/clojure-yesql-xp")
|
|
}));
|
|
layer.chain(compare_group("veep", || {
|
|
compare_all_org_document("/foreign_documents/howardabrams/veep")
|
|
}))
|
|
}
|
|
|
|
fn compare_group<N: Into<String>, F: Fn() -> I, I: Iterator<Item = TestConfig>>(
|
|
name: N,
|
|
inner: F,
|
|
) -> impl Iterator<Item = TestConfig> {
|
|
std::iter::once(TestConfig::TestLayer(TestLayer {
|
|
name: name.into(),
|
|
children: inner().collect(),
|
|
}))
|
|
}
|
|
|
|
fn compare_all_org_document<P: AsRef<Path>>(root_dir: P) -> impl Iterator<Item = TestConfig> {
|
|
let root_dir = root_dir.as_ref();
|
|
let mut test_files = WalkDir::new(root_dir)
|
|
.into_iter()
|
|
.filter(|e| match e {
|
|
Ok(dir_entry) => {
|
|
dir_entry.file_type().is_file()
|
|
&& Path::new(dir_entry.file_name())
|
|
.extension()
|
|
.map(|ext| ext.to_ascii_lowercase() == "org")
|
|
.unwrap_or(false)
|
|
}
|
|
Err(_) => true,
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap();
|
|
test_files.sort_by_cached_key(|test_file| PathBuf::from(test_file.path()));
|
|
let test_configs: Vec<_> = test_files
|
|
.into_iter()
|
|
.map(|test_file| {
|
|
let name = test_file
|
|
.path()
|
|
.strip_prefix(root_dir)
|
|
.expect("Result is from walkdir so it must be below the root directory.")
|
|
.as_os_str()
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
TestConfig::SingleFile(SingleFile {
|
|
name,
|
|
file_path: test_file.into_path(),
|
|
})
|
|
})
|
|
.collect();
|
|
test_configs.into_iter()
|
|
}
|
|
|
|
static TEST_PERMITS: Semaphore = Semaphore::const_new(8);
|
|
|
|
#[derive(Debug)]
|
|
enum TestConfig {
|
|
TestLayer(TestLayer),
|
|
SingleFile(SingleFile),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TestLayer {
|
|
name: String,
|
|
children: Vec<TestConfig>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SingleFile {
|
|
name: String,
|
|
file_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum TestResult {
|
|
ResultLayer(ResultLayer),
|
|
SingleFileResult(SingleFileResult),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ResultLayer {
|
|
name: String,
|
|
children: Vec<TestResult>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SingleFileResult {
|
|
name: String,
|
|
status: TestStatus,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum TestStatus {
|
|
Pass,
|
|
Fail,
|
|
}
|
|
|
|
impl TestConfig {
|
|
fn run_test(self) -> BoxFuture<'static, Result<TestResult, JoinError>> {
|
|
async move {
|
|
match self {
|
|
TestConfig::TestLayer(test) => Ok(TestResult::ResultLayer(test.run_test().await?)),
|
|
TestConfig::SingleFile(test) => {
|
|
Ok(TestResult::SingleFileResult(test.run_test().await?))
|
|
}
|
|
}
|
|
}
|
|
.boxed()
|
|
}
|
|
}
|
|
|
|
impl SingleFile {
|
|
async fn run_test(self) -> Result<SingleFileResult, JoinError> {
|
|
let _permit = TEST_PERMITS.acquire().await.unwrap();
|
|
let result = silent_compare_on_file(&self.file_path).await;
|
|
Ok(SingleFileResult {
|
|
name: self.name,
|
|
status: if let Ok(true) = result {
|
|
TestStatus::Pass
|
|
} else {
|
|
TestStatus::Fail
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TestLayer {
|
|
async fn run_test(self) -> Result<ResultLayer, JoinError> {
|
|
let running_children: Vec<_> = self
|
|
.children
|
|
.into_iter()
|
|
.map(|c| tokio::spawn(c.run_test()))
|
|
.collect();
|
|
let mut children = Vec::with_capacity(running_children.len());
|
|
for c in running_children {
|
|
children.push(c.await??);
|
|
}
|
|
Ok(ResultLayer {
|
|
name: self.name,
|
|
children,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TestResult {
|
|
pub fn print(&self) {
|
|
self.print_indented(0);
|
|
}
|
|
|
|
fn print_indented(&self, indentation: usize) {
|
|
match self {
|
|
TestResult::ResultLayer(result) => result.print_indented(indentation),
|
|
TestResult::SingleFileResult(result) => result.print_indented(indentation),
|
|
}
|
|
}
|
|
|
|
fn has_bad_children(&self) -> bool {
|
|
match self {
|
|
TestResult::ResultLayer(result) => result.has_bad_children(),
|
|
TestResult::SingleFileResult(result) => result.has_bad_children(),
|
|
}
|
|
}
|
|
|
|
fn is_immediately_bad(&self) -> bool {
|
|
match self {
|
|
TestResult::ResultLayer(result) => result.is_immediately_bad(),
|
|
TestResult::SingleFileResult(result) => result.is_immediately_bad(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn foreground_color(red: u8, green: u8, blue: u8) -> String {
|
|
if TestResult::should_use_color() {
|
|
format!(
|
|
"\x1b[38;2;{red};{green};{blue}m",
|
|
red = red,
|
|
green = green,
|
|
blue = blue
|
|
)
|
|
} else {
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub(crate) fn background_color(red: u8, green: u8, blue: u8) -> String {
|
|
if TestResult::should_use_color() {
|
|
format!(
|
|
"\x1b[48;2;{red};{green};{blue}m",
|
|
red = red,
|
|
green = green,
|
|
blue = blue
|
|
)
|
|
} else {
|
|
String::new()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn reset_color() -> &'static str {
|
|
if TestResult::should_use_color() {
|
|
"\x1b[0m"
|
|
} else {
|
|
""
|
|
}
|
|
}
|
|
|
|
fn should_use_color() -> bool {
|
|
!std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty())
|
|
}
|
|
}
|
|
|
|
impl SingleFileResult {
|
|
fn print_indented(&self, indentation: usize) {
|
|
match self.status {
|
|
TestStatus::Pass => {
|
|
println!(
|
|
"{indentation}{color}PASS{reset} {name}",
|
|
indentation = " ".repeat(indentation),
|
|
color = TestResult::foreground_color(0, 255, 0),
|
|
reset = TestResult::reset_color(),
|
|
name = self.name
|
|
);
|
|
}
|
|
TestStatus::Fail => {
|
|
println!(
|
|
"{indentation}{color}FAIL{reset} {name}",
|
|
indentation = " ".repeat(indentation),
|
|
color = TestResult::foreground_color(255, 0, 0),
|
|
reset = TestResult::reset_color(),
|
|
name = self.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn has_bad_children(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn is_immediately_bad(&self) -> bool {
|
|
match self.status {
|
|
TestStatus::Pass => false,
|
|
TestStatus::Fail => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ResultLayer {
|
|
fn print_indented(&self, indentation: usize) {
|
|
if self.is_immediately_bad() {
|
|
println!(
|
|
"{indentation}{color}FAIL{reset} {name}",
|
|
indentation = " ".repeat(indentation),
|
|
color = TestResult::foreground_color(255, 0, 0),
|
|
reset = TestResult::reset_color(),
|
|
name = self.name
|
|
);
|
|
} else if self.has_bad_children() {
|
|
println!(
|
|
"{indentation}{color}BADCHILD{reset} {name}",
|
|
indentation = " ".repeat(indentation),
|
|
color = TestResult::foreground_color(255, 255, 0),
|
|
reset = TestResult::reset_color(),
|
|
name = self.name
|
|
);
|
|
} else {
|
|
println!(
|
|
"{indentation}{color}PASS{reset} {name}",
|
|
indentation = " ".repeat(indentation),
|
|
color = TestResult::foreground_color(0, 255, 0),
|
|
reset = TestResult::reset_color(),
|
|
name = self.name
|
|
);
|
|
}
|
|
self.children
|
|
.iter()
|
|
.for_each(|result| result.print_indented(indentation + 1));
|
|
}
|
|
|
|
fn has_bad_children(&self) -> bool {
|
|
self.children
|
|
.iter()
|
|
.any(|result| result.is_immediately_bad() || result.has_bad_children())
|
|
}
|
|
|
|
fn is_immediately_bad(&self) -> bool {
|
|
false
|
|
}
|
|
}
|