use anyhow::{Result, anyhow};
use log::{debug, info};
use named_lock::NamedLock;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::LazyLock;
use tempfile::TempDir;
use testtrim::cmd::cli::{GetTestIdentifierMode, PlatformTaggingMode, SourceMode};
use testtrim::cmd::get_test_identifiers::{self, AncestorSearchMode, get_target_test_cases};
use testtrim::cmd::run_tests::run_tests;
use testtrim::coverage::{CoverageDatabase, create_test_db};
use testtrim::errors::{RunTestsCommandErrors, RunTestsErrors};
use testtrim::platform::{ConcreteTestIdentifier as _, TestIdentifierCore as _, TestPlatform};
use testtrim::scm::git::GitScm;
use tokio::sync::MutexGuard;
use crate::util::ChangeWorkingDirectory;
use crate::{CWD_MUTEX, copy_dir_all, git_checkout, git_clone};
mod dotnet_test;
mod golang_test;
mod javascript_test;
mod rust_test;
struct CommitTestData<'a> {
test_commit: &'a str,
all_test_cases: Vec<&'a str>,
relevant_test_cases: Vec<&'a str>,
expected_failing_test_cases: Vec<&'a str>,
}
static LINEARCOMMITS_CACHE: LazyLock<NamedLock> =
LazyLock::new(|| NamedLock::create("testtrim-linearcommits-cache").unwrap());
async fn setup_test<TP: TestPlatform>(
test_project: &str,
) -> Result<(
TempDir,
ChangeWorkingDirectory,
MutexGuard<'_, i32>,
impl CoverageDatabase,
)> {
simplelog::SimpleLogger::init(simplelog::LevelFilter::Debug, simplelog::Config::default())?;
let _cwd_mutex = CWD_MUTEX.lock().await;
let tmp_dir = tempfile::Builder::new().prefix("testtrim-test").tempdir()?;
let cache_path = env::current_dir()?
.join(".linearcommits")
.join(test_project);
let project_path = tmp_dir.path().join(test_project); {
let _cache_lock = LINEARCOMMITS_CACHE.lock();
if std::fs::exists(&cache_path)? {
copy_dir_all(&cache_path, &project_path)?;
} else {
let _tmp_dir_cwd = ChangeWorkingDirectory::new(tmp_dir.path());
git_clone(test_project)?;
copy_dir_all(&project_path, cache_path)?;
drop(_tmp_dir_cwd);
}
}
let _tmp_dir_cwd = ChangeWorkingDirectory::new(&project_path);
let coverage_db = create_test_db()?;
coverage_db.clear_project_data::<TP>(test_project).await?;
Ok((tmp_dir, _tmp_dir_cwd, _cwd_mutex, coverage_db))
}
async fn execute_test<TP: TestPlatform>(
commit_test_data: &CommitTestData<'_>,
coverage_db: &impl CoverageDatabase,
) -> Result<()> {
let project_dir = fs::canonicalize(PathBuf::from("."))?;
let scm = GitScm::new(project_dir.clone());
let tags = &get_test_identifiers::tags::<TP>(&Vec::new(), PlatformTaggingMode::Automatic);
info!("checking out {}", commit_test_data.test_commit);
git_checkout(commit_test_data.test_commit)?;
let all_test_cases = get_target_test_cases::<_, _, _, _, _, _, TP>(
&project_dir,
GetTestIdentifierMode::All,
&scm,
AncestorSearchMode::AllCommits,
tags,
coverage_db,
None,
)
.await?
.target_test_cases;
debug!(
"expected all_test_cases: {:?}",
commit_test_data.all_test_cases
);
debug!("calculated actual all_test_cases: {all_test_cases:?}");
assert_eq!(
all_test_cases.len(),
commit_test_data.all_test_cases.len(),
"unexpected count of all tests in {} commit",
commit_test_data.test_commit,
);
for expected_test_name in commit_test_data.all_test_cases.iter() {
assert_eq!(
all_test_cases
.keys()
.filter(|tc| tc.test_identifier().lightly_unique_name() == *expected_test_name)
.count(),
1,
"couldn't find test named {expected_test_name}"
);
}
let relevant_test_cases = get_target_test_cases::<_, _, _, _, _, _, TP>(
&project_dir,
GetTestIdentifierMode::Relevant,
&scm,
AncestorSearchMode::AllCommits,
tags,
coverage_db,
None,
)
.await?
.target_test_cases;
debug!(
"expected relevant_test_cases: {:?}",
commit_test_data.relevant_test_cases
);
debug!("calculated actual relevant_test_cases: {relevant_test_cases:?}");
assert_eq!(
relevant_test_cases.len(),
commit_test_data.relevant_test_cases.len(),
"unexpected count of tests-to-run in {} commit",
commit_test_data.test_commit,
);
for expected_test_name in commit_test_data.relevant_test_cases.iter() {
assert_eq!(
relevant_test_cases
.keys()
.filter(|tc| tc.test_identifier().lightly_unique_name() == *expected_test_name)
.count(),
1
);
}
println!(
"starting cmd::run_tests for commit {}",
commit_test_data.test_commit
);
match run_tests::<_, _, _, _, _, _, TP>(
&project_dir,
GetTestIdentifierMode::Relevant,
&scm,
SourceMode::CleanCommit,
0,
tags,
coverage_db,
None,
)
.await
{
Ok(_) if commit_test_data.expected_failing_test_cases.is_empty() => Ok(()),
Ok(_) => Err(anyhow!(
"expected {} failed tests in {} commit, but had zero",
commit_test_data.expected_failing_test_cases.len(),
commit_test_data.test_commit
)),
Err(RunTestsCommandErrors::RunTestsErrors(RunTestsErrors::TestExecutionFailures(
failures,
))) => {
let mut expected = commit_test_data
.expected_failing_test_cases
.iter()
.map(|s| String::from(*s))
.collect::<HashSet<_>>();
for failure in failures {
let test_name = failure.test_identifier.lightly_unique_name();
if !expected.remove(&test_name) {
return Err(anyhow!(
"test {test_name} failed in commit {}, but wasn't expected to fail: {failure:?}",
commit_test_data.test_commit
));
}
}
if !expected.is_empty() {
Err(anyhow!(
"tests were expected to fail in commit {} but did not fail: {expected:?}",
commit_test_data.test_commit
))
} else {
Ok(())
}
}
Err(e) => Err(e.into()),
}
}