Fix coverage clear bug and other minor refactors

This commit is contained in:
jonawals
2021-06-07 17:45:48 +01:00
parent 796b5be286
commit cc9e8a72e4
11 changed files with 132 additions and 97 deletions
@@ -51,7 +51,7 @@ namespace TestImpact
Ignore, Ignore,
StdOut, StdOut,
File, File,
Remove, Discard,
Keep Keep
}; };
@@ -87,7 +87,7 @@ namespace TestImpact
"ignore", "ignore",
"stdout", "stdout",
"file", "file",
"remove", "discard",
"keep" "keep"
}; };
@@ -147,7 +147,7 @@ namespace TestImpact
{ {
const AZStd::vector<AZStd::pair<AZStd::string, Policy::FailedTestCoverage>> states = const AZStd::vector<AZStd::pair<AZStd::string, Policy::FailedTestCoverage>> states =
{ {
{OptionKeys[Remove], Policy::FailedTestCoverage::Remove}, {OptionKeys[Discard], Policy::FailedTestCoverage::Discard},
{OptionKeys[Keep], Policy::FailedTestCoverage::Keep} {OptionKeys[Keep], Policy::FailedTestCoverage::Keep}
}; };
@@ -405,11 +405,11 @@ namespace TestImpact
" tests are run regardless).\n" " tests are run regardless).\n"
" -shard=<on,off> Break any test targets with a sharding policy into the number of \n" " -shard=<on,off> Break any test targets with a sharding policy into the number of \n"
" shards according to the maximum concurrency value.\n" " shards according to the maximum concurrency value.\n"
" -cpolicy=<remove, keep> Policy for handling the coverage data of failed tests (both tests that \n" " -cpolicy=<remove, keep> Policy for handling the coverage data of failing tests, where discard \n"
" failed to execute and tests that ran but failed), where remove will \n" " will discard the coverage data produced by the failing tests, causing \n"
" remove the failed tests from the all coverage data (causing them to be \n" " them to be drafted into future test runs and keep will keep any existing \n"
" drafted into future test runs) and keep will keep any existing coverage \n" " coverage data and update the coverage data for failed tests that produce \n"
" data and update the coverage data for failed tests that produce coverage.\n" " coverage.\n"
" -targetout=<sdtout, file> Capture of individual test run stdout, where stdout will capture \n" " -targetout=<sdtout, file> Capture of individual test run stdout, where stdout will capture \n"
" each individual test target's stdout and output each one to stdout \n" " each individual test target's stdout and output each one to stdout \n"
" and file will capture each individual test target's stdout and output \n" " and file will capture each individual test target's stdout and output \n"
@@ -81,7 +81,8 @@ namespace TestImpact
using SafeTestSequenceCompleteCallback = AZStd::function<void( using SafeTestSequenceCompleteCallback = AZStd::function<void(
Client::SequenceFailure&& selectedFailureReport, Client::SequenceFailure&& selectedFailureReport,
Client::SequenceFailure&& discardedFailureReport, Client::SequenceFailure&& discardedFailureReport,
AZStd::chrono::milliseconds duration)>; AZStd::chrono::milliseconds selectedDuration,
AZStd::chrono::milliseconds discardedDuration)>;
//! Callback for test runs that have completed for any reason. //! Callback for test runs that have completed for any reason.
//! @param selectedTests The test that has completed. //! @param selectedTests The test that has completed.
@@ -201,6 +202,10 @@ namespace TestImpact
AZStd::pair<AZStd::vector<const TestTarget*>, AZStd::vector<const TestTarget*>> SelectTestTargetsByExcludeList( AZStd::pair<AZStd::vector<const TestTarget*>, AZStd::vector<const TestTarget*>> SelectTestTargetsByExcludeList(
AZStd::vector<const TestTarget*> testTargets) const; AZStd::vector<const TestTarget*> testTargets) const;
//! Prunes the existing coverage for the specified jobs and creates the consolidates source covering tests list from the
//! test engine instrumented run jobs.
SourceCoveringTestsList CreateSourceCoveringTestFromTestCoverages(const AZStd::vector<TestEngineInstrumentedRun>& jobs);
//! Prepares the dynamic dependency map for a seed update by clearing all existing data and deleting the file that will be serialized. //! Prepares the dynamic dependency map for a seed update by clearing all existing data and deleting the file that will be serialized.
void ClearDynamicDependencyMapAndRemoveExistingFile(); void ClearDynamicDependencyMapAndRemoveExistingFile();
@@ -34,7 +34,7 @@ namespace TestImpact
//! Policy for handling the coverage data of failed tests targets (both test that failed to execute and tests that ran but failed). //! Policy for handling the coverage data of failed tests targets (both test that failed to execute and tests that ran but failed).
enum class FailedTestCoverage enum class FailedTestCoverage
{ {
Remove, //!< Remove the failed test targets from the all coverage data (causing them to be drafted into future test runs). Discard, //!< Discard the coverage data produced by the failing tests, causing them to be drafted into future test runs.
Keep //!< Keep any existing coverage data and update the coverage data for failed test targetss that produce coverage. Keep //!< Keep any existing coverage data and update the coverage data for failed test targetss that produce coverage.
}; };
@@ -133,8 +133,9 @@ namespace TestImpact
return buildTarget; return buildTarget;
} }
void DynamicDependencyMap::ReplaceSourceCoverage(const SourceCoveringTestsList& sourceCoverageDelta) void DynamicDependencyMap::ReplaceSourceCoverageInternal(const SourceCoveringTestsList& sourceCoverageDelta, bool pruneIfNoParentsOrCoverage)
{ {
AZStd::vector<AZStd::string> killList;
for (const auto& sourceCoverage : sourceCoverageDelta.GetCoverage()) for (const auto& sourceCoverage : sourceCoverageDelta.GetCoverage())
{ {
// Autogen input files are not compiled sources and thus supplying coverage data for them makes no sense // Autogen input files are not compiled sources and thus supplying coverage data for them makes no sense
@@ -146,7 +147,12 @@ namespace TestImpact
auto [sourceDependencyIt, inserted] = m_sourceDependencyMap.insert(sourceCoverage.GetPath().String()); auto [sourceDependencyIt, inserted] = m_sourceDependencyMap.insert(sourceCoverage.GetPath().String());
auto& [source, sourceDependency] = *sourceDependencyIt; auto& [source, sourceDependency] = *sourceDependencyIt;
// Remove the source from the test target covering sources map and clear any existing coverage for the delta // Before we can replace the coverage for this source dependency, we must:
// 1. Remove the source from the test target covering sources map
// 2. Prune the covered targets for the parent test target(s) of this source dependency
// 3. Clear any existing coverage for the delta
// 1.
for (const auto& testTarget : sourceDependency.m_coveringTestTargets) for (const auto& testTarget : sourceDependency.m_coveringTestTargets)
{ {
if (auto coveringTestTargetIt = m_testTargetSourceCoverage.find(testTarget); if (auto coveringTestTargetIt = m_testTargetSourceCoverage.find(testTarget);
@@ -154,7 +160,20 @@ namespace TestImpact
{ {
coveringTestTargetIt->second.erase(source); coveringTestTargetIt->second.erase(source);
} }
} }
// 2.
// This step is prohibitively expensive as it requires iterating over all of the sources of the build targets covered by
// the parent test targets of this source dependency to ensure that this is in fact the last source being cleared and thus
// it can be determined that the parent test target is no longer covering the given build target
//
// The implications of this are that multiple calls to the test selector and priritizor's SelectTestTargets method will end
// up pulling in more test targets than needed for newly-created production sources until the next time the dynamic dependency
// map reconstructed, however until this use case materializes the implications described will not be addressed
// 3.
sourceDependency.m_coveringTestTargets.clear(); sourceDependency.m_coveringTestTargets.clear();
// Update the dependency with any new coverage data // Update the dependency with any new coverage data
@@ -183,13 +202,18 @@ namespace TestImpact
} }
// If the new coverage data results in a parentless and coverageless entry, consider it a dead entry and remove accordingly // If the new coverage data results in a parentless and coverageless entry, consider it a dead entry and remove accordingly
if (sourceDependency.m_coveringTestTargets.empty() && sourceDependency.m_parentTargets.empty()) if (sourceDependency.m_coveringTestTargets.empty() && sourceDependency.m_parentTargets.empty() && pruneIfNoParentsOrCoverage)
{ {
m_sourceDependencyMap.erase(sourceDependencyIt); m_sourceDependencyMap.erase(sourceDependencyIt);
} }
} }
} }
void DynamicDependencyMap::ReplaceSourceCoverage(const SourceCoveringTestsList& sourceCoverageDelta)
{
ReplaceSourceCoverageInternal(sourceCoverageDelta, true);
}
void DynamicDependencyMap::ClearSourceCoverage(const AZStd::vector<RepoPath>& paths) void DynamicDependencyMap::ClearSourceCoverage(const AZStd::vector<RepoPath>& paths)
{ {
for (const auto& path : paths) for (const auto& path : paths)
@@ -212,9 +236,14 @@ namespace TestImpact
void DynamicDependencyMap::ClearAllSourceCoverage() void DynamicDependencyMap::ClearAllSourceCoverage()
{ {
for (const auto& [path, coverage] : m_sourceDependencyMap) for (auto it = m_sourceDependencyMap.begin(); it != m_sourceDependencyMap.end(); ++it)
{ {
ReplaceSourceCoverage(SourceCoveringTestsList(AZStd::vector<SourceCoveringTests>{ SourceCoveringTests(RepoPath(path)) })); const auto& [path, coverage] = *it;
ReplaceSourceCoverageInternal(SourceCoveringTestsList(AZStd::vector<SourceCoveringTests>{ SourceCoveringTests(RepoPath(path)) }), false);
if (coverage.m_coveringTestTargets.empty() && coverage.m_parentTargets.empty())
{
it = m_sourceDependencyMap.erase(it);
}
} }
} }
@@ -81,7 +81,6 @@ namespace TestImpact
SourceDependency GetSourceDependencyOrThrow(const RepoPath& path) const; SourceDependency GetSourceDependencyOrThrow(const RepoPath& path) const;
//! Replaces the source coverage of the specified sources with the specified source coverage. //! Replaces the source coverage of the specified sources with the specified source coverage.
//! @note The covering targets for the parent test target(s) will not be pruned if those covering targets are removed.
//! @param sourceCoverageDelta The source coverage delta to replace in the dependency map. //! @param sourceCoverageDelta The source coverage delta to replace in the dependency map.
void ReplaceSourceCoverage(const SourceCoveringTestsList& sourceCoverageDelta); void ReplaceSourceCoverage(const SourceCoveringTestsList& sourceCoverageDelta);
@@ -110,6 +109,13 @@ namespace TestImpact
AZStd::vector<const TestTarget*> GetNotCoveringTests() const; AZStd::vector<const TestTarget*> GetNotCoveringTests() const;
private: private:
//! Internal handler for ReplaceSourceCoverage where the pruning of parentless and coverageless source depenencies after the
//! source coverage has been replaced must be explicitly stated.
//! @note The covered targets for the source dependency's parent test target(s) will not be pruned if those covering targets are removed.
//! @param sourceCoverageDelta The source coverage delta to replace in the dependency map.
//! @param pruneIfNoParentsOrCoverage Flag to specify whether or not newly parentless and coverageless dependencies will be removed.
void ReplaceSourceCoverageInternal(const SourceCoveringTestsList& sourceCoverageDelta, bool pruneIfNoParentsOrCoverage);
//! Clears the source coverage of the specified sources. //! Clears the source coverage of the specified sources.
//! @note The covering targets for the parent test target(s) will not be pruned if those covering targets are removed. //! @note The covering targets for the parent test target(s) will not be pruned if those covering targets are removed.
void ClearSourceCoverage(const AZStd::vector<RepoPath>& paths); void ClearSourceCoverage(const AZStd::vector<RepoPath>& paths);
@@ -127,6 +133,7 @@ namespace TestImpact
AZStd::unordered_map<const TestTarget*, AZStd::unordered_set<AZStd::string>> m_testTargetSourceCoverage; AZStd::unordered_map<const TestTarget*, AZStd::unordered_set<AZStd::string>> m_testTargetSourceCoverage;
//! The map of build targets and their covering test targets. //! The map of build targets and their covering test targets.
//! @note As per the note for ReplaceSourceCoverageInternal, this map is currently not pruned when source coverage is replaced.
AZStd::unordered_map<const BuildTarget*, AZStd::unordered_set<const TestTarget*>> m_buildTargetCoverage; AZStd::unordered_map<const BuildTarget*, AZStd::unordered_set<const TestTarget*>> m_buildTargetCoverage;
//! Mapping of autogen input sources to their generated output sources. //! Mapping of autogen input sources to their generated output sources.
@@ -73,7 +73,7 @@ namespace TestImpact
} }
catch (const TestEngineException& e) catch (const TestEngineException& e)
{ {
AZ_Printf("Enumerate", "Enumeration cache error: %s", e.what()); AZ_Printf("Enumerate", AZStd::string::format("Enumeration cache error: %s\n", e.what()).c_str());
DeleteFile(jobInfo->GetCache()->m_file); DeleteFile(jobInfo->GetCache()->m_file);
} }
@@ -72,7 +72,7 @@ namespace TestImpact
} }
catch (const Exception& e) catch (const Exception& e)
{ {
AZ_Printf("RunInstrumentedTests", e.what()); AZ_Printf("RunInstrumentedTests", AZStd::string::format("%s\n", e.what()).c_str());
runs[jobId] = AZStd::nullopt; runs[jobId] = AZStd::nullopt;
} }
} }
@@ -46,7 +46,7 @@ namespace TestImpact
} }
catch (const Exception& e) catch (const Exception& e)
{ {
AZ_Printf("RunTests", e.what()); AZ_Printf("RunTests", AZStd::string::format("%s\n", e.what()).c_str());
runs[jobId] = AZStd::nullopt; runs[jobId] = AZStd::nullopt;
} }
} }
@@ -256,32 +256,77 @@ namespace TestImpact
void Runtime::ClearDynamicDependencyMapAndRemoveExistingFile() void Runtime::ClearDynamicDependencyMapAndRemoveExistingFile()
{ {
DeleteFile(m_sparTIAFile);
m_dynamicDependencyMap->ClearAllSourceCoverage(); m_dynamicDependencyMap->ClearAllSourceCoverage();
DeleteFile(m_sparTIAFile);
}
SourceCoveringTestsList Runtime::CreateSourceCoveringTestFromTestCoverages(const AZStd::vector<TestEngineInstrumentedRun>& jobs)
{
AZStd::unordered_map<AZStd::string, AZStd::unordered_set<AZStd::string>> coverage;
for (const auto& job : jobs)
{
// First we must remove any existing coverage for the test target so as to not end up with source remnants from previous
// coverage that is no longer covered by this revision of the test target
m_dynamicDependencyMap->RemoveTestTargetFromSourceCoverage(job.GetTestTarget());
// Next we will update the coverage of test targets that completed (with or without failures), unless the failed test coverage
// policy dictates we should instead discard the coverage of test targets with failing tests
if (const auto testResult = job.GetTestResult();
testResult == Client::TestRunResult::AllTestsPass ||
(m_failedTestCoveragePolicy == Policy::FailedTestCoverage::Keep && testResult == Client::TestRunResult::TestFailures))
{
if (testResult == Client::TestRunResult::AllTestsPass)
{
// Passing tests should have coverage data, otherwise something is very wrong
AZ_TestImpact_Eval(
job.GetTestCoverge().has_value(),
RuntimeException,
AZStd::string::format(
"Test target '%s' completed its test run successfully but produced no coverage data",
job.GetTestTarget()->GetName().c_str()));
}
else if (!job.GetTestCoverge().has_value())
{
// When a test run completes with failing tests but produces no coverage artifact that's typically a sign of the
// test aborting due to an unhandled exception, in which case ignore it and let it be picked up in the failure report
continue;
}
for (const auto& source : job.GetTestCoverge().value().GetSourcesCovered())
{
coverage[source.String()].insert(job.GetTestTarget()->GetName());
}
}
}
AZStd::vector<SourceCoveringTests> sourceCoveringTests;
sourceCoveringTests.reserve(coverage.size());
for (auto&& [source, testTargets] : coverage)
{
if (const auto sourcePath = RepoPath(source);
sourcePath.IsRelativeTo(m_config.m_repo.m_root))
{
sourceCoveringTests.push_back(
SourceCoveringTests(RepoPath(sourcePath.LexicallyRelative(m_config.m_repo.m_root)), AZStd::move(testTargets)));
}
else
{
AZ_Warning("TestImpact", false, "Ignoring source, source it outside of repo: '%s'", sourcePath.c_str());
}
}
return SourceCoveringTestsList(AZStd::move(sourceCoveringTests));
} }
void Runtime::UpdateAndSerializeDynamicDependencyMap(const AZStd::vector<TestEngineInstrumentedRun>& jobs) void Runtime::UpdateAndSerializeDynamicDependencyMap(const AZStd::vector<TestEngineInstrumentedRun>& jobs)
{ {
const auto sourceCoverageTestsList = CreateSourceCoveringTestFromTestCoverages(jobs, m_config.m_repo.m_root); const auto sourceCoverageTestsList = CreateSourceCoveringTestFromTestCoverages(jobs);
if (!sourceCoverageTestsList.GetNumSources()) if (!sourceCoverageTestsList.GetNumSources())
{ {
return; return;
} }
m_dynamicDependencyMap->ReplaceSourceCoverage(sourceCoverageTestsList); m_dynamicDependencyMap->ReplaceSourceCoverage(sourceCoverageTestsList);
if (m_failedTestCoveragePolicy == Policy::FailedTestCoverage::Remove)
{
for (const auto& job : jobs)
{
if (job.GetTestResult() != Client::TestRunResult::AllTestsPass ||
!job.GetTestCoverge().has_value())
{
m_dynamicDependencyMap->RemoveTestTargetFromSourceCoverage(job.GetTestTarget());
}
}
}
const auto sparTIA = m_dynamicDependencyMap->ExportSourceCoverage(); const auto sparTIA = m_dynamicDependencyMap->ExportSourceCoverage();
const auto sparTIAData = SerializeSourceCoveringTestsList(sparTIA); const auto sparTIAData = SerializeSourceCoveringTestsList(sparTIA);
WriteFileContents<RuntimeException>(sparTIAData, m_sparTIAFile); WriteFileContents<RuntimeException>(sparTIAData, m_sparTIAFile);
@@ -422,7 +467,7 @@ namespace TestImpact
AZStd::optional<SafeTestSequenceCompleteCallback> testSequenceEndCallback, AZStd::optional<SafeTestSequenceCompleteCallback> testSequenceEndCallback,
AZStd::optional<TestRunCompleteCallback> testCompleteCallback) AZStd::optional<TestRunCompleteCallback> testCompleteCallback)
{ {
Timer timer; Timer timer;
// Draft in the test targets that have no coverage entries in the dynamic dependency map // Draft in the test targets that have no coverage entries in the dynamic dependency map
AZStd::vector<const TestTarget*> draftedTestTargets = m_dynamicDependencyMap->GetNotCoveringTests(); AZStd::vector<const TestTarget*> draftedTestTargets = m_dynamicDependencyMap->GetNotCoveringTests();
@@ -459,6 +504,8 @@ namespace TestImpact
testTargetTimeout, testTargetTimeout,
globalTimeout, globalTimeout,
TestRunCompleteCallbackHandler(testCompleteCallback)); TestRunCompleteCallbackHandler(testCompleteCallback));
const auto selectedDuraton = timer.Elapsed();
// Carry the remaining global sequence time over to the discarded test run // Carry the remaining global sequence time over to the discarded test run
if (globalTimeout.has_value()) if (globalTimeout.has_value())
@@ -478,16 +525,18 @@ namespace TestImpact
globalTimeout, globalTimeout,
TestRunCompleteCallbackHandler(testCompleteCallback)); TestRunCompleteCallbackHandler(testCompleteCallback));
UpdateAndSerializeDynamicDependencyMap(selectedTestJobs); const auto discardedDuraton = timer.Elapsed();
if (testSequenceEndCallback.has_value()) if (testSequenceEndCallback.has_value())
{ {
(*testSequenceEndCallback)( (*testSequenceEndCallback)(
GenerateSequenceFailureReport(selectedTestJobs), GenerateSequenceFailureReport(selectedTestJobs),
GenerateSequenceFailureReport(discardedTestJobs), GenerateSequenceFailureReport(discardedTestJobs),
timer.Elapsed()); selectedDuraton,
discardedDuraton);
} }
UpdateAndSerializeDynamicDependencyMap(selectedTestJobs);
return { selectedResult, discardedResult }; return { selectedResult, discardedResult };
} }
@@ -530,14 +579,14 @@ namespace TestImpact
globalTimeout, globalTimeout,
TestRunCompleteCallbackHandler(testCompleteCallback)); TestRunCompleteCallbackHandler(testCompleteCallback));
ClearDynamicDependencyMapAndRemoveExistingFile();
UpdateAndSerializeDynamicDependencyMap(testJobs);
if (testSequenceEndCallback.has_value()) if (testSequenceEndCallback.has_value())
{ {
(*testSequenceEndCallback)(GenerateSequenceFailureReport(testJobs), timer.Elapsed()); (*testSequenceEndCallback)(GenerateSequenceFailureReport(testJobs), timer.Elapsed());
} }
ClearDynamicDependencyMapAndRemoveExistingFile();
UpdateAndSerializeDynamicDependencyMap(testJobs);
return result; return result;
} }
@@ -82,54 +82,4 @@ namespace TestImpact
return testNames; return testNames;
} }
} // namespace TestImpact
SourceCoveringTestsList CreateSourceCoveringTestFromTestCoverages(const AZStd::vector<TestEngineInstrumentedRun>& jobs, const RepoPath& root)
{
AZStd::unordered_map<AZStd::string, AZStd::unordered_set<AZStd::string>> coverage;
for (const auto& job : jobs)
{
if (const auto testResult = job.GetTestResult();
testResult == Client::TestRunResult::AllTestsPass || testResult == Client::TestRunResult::TestFailures)
{
if (testResult == Client::TestRunResult::AllTestsPass)
{
// Passing tests should have coverage data, otherwise something is very wrong
AZ_TestImpact_Eval(
job.GetTestCoverge().has_value(),
RuntimeException,
AZStd::string::format(
"Test target '%s' completed its test run successfully but produced no coverage data",
job.GetTestTarget()->GetName().c_str()));
}
else if (!job.GetTestCoverge().has_value())
{
// When a test run completes with failing tests but produces no coverage artifact that's typically a sign of the
// test aborting due to an unhandled exception, in which case ignore it and let it be picked up in the failure report
continue;
}
for (const auto& source : job.GetTestCoverge().value().GetSourcesCovered())
{
coverage[source.String()].insert(job.GetTestTarget()->GetName());
}
}
}
AZStd::vector<SourceCoveringTests> sourceCoveringTests;
sourceCoveringTests.reserve(coverage.size());
for (auto&& [source, testTargets] : coverage)
{
if (const auto sourcePath = RepoPath(source);
sourcePath.IsRelativeTo(root))
{
sourceCoveringTests.push_back(SourceCoveringTests(RepoPath(sourcePath.LexicallyRelative(root)), AZStd::move(testTargets)));
}
else
{
AZ_Warning("TestImpact", false, "Ignoring source, source it outside of repo: %s", sourcePath.c_str());
}
}
return SourceCoveringTestsList(AZStd::move(sourceCoveringTests));
}
}
@@ -40,12 +40,7 @@ namespace TestImpact
const AZStd::vector<AZStd::string>& excludedTestTargets); const AZStd::vector<AZStd::string>& excludedTestTargets);
//! Extracts the name information from the specified test targets. //! Extracts the name information from the specified test targets.
AZStd::vector<AZStd::string> ExtractTestTargetNames(const AZStd::vector<const TestTarget*> testTargets); AZStd::vector<AZStd::string> ExtractTestTargetNames(const AZStd::vector<const TestTarget*> testTargets);
//! Creates the consolidates source covering tests list from the test engine instrumented run jobs.
SourceCoveringTestsList CreateSourceCoveringTestFromTestCoverages(
const AZStd::vector<TestEngineInstrumentedRun>& jobs,
const RepoPath& root);
//! Generates a test run failure report from the specified test engine job information. //! Generates a test run failure report from the specified test engine job information.
//! @tparam TestJob The test engine job type. //! @tparam TestJob The test engine job type.