Add test engine

This commit is contained in:
jonawals
2021-05-24 15:00:21 +01:00
parent d77c82e52d
commit bdbbedda71
18 changed files with 820 additions and 106 deletions
@@ -10,7 +10,7 @@
*
*/
#include <TestEngine/Enumeration/TestImpactTestEnumerationException.h>
#include <TestEngine/TestImpactTestEngineException.h>
#include <TestEngine/Enumeration/TestImpactTestEnumerationSerializer.h>
#include <AzCore/JSON/document.h>
@@ -82,7 +82,7 @@ namespace TestImpact
if (doc.Parse<0>(testEnumString.c_str()).HasParseError())
{
throw TestEnumerationException("Could not parse enumeration data");
throw TestEngineException("Could not parse enumeration data");
}
for (const auto& suite : doc[TestEnumFields::Keys[TestEnumFields::SuitesKey]].GetArray())
@@ -13,7 +13,7 @@
#include <TestImpactFramework/TestImpactUtils.h>
#include <Artifact/Factory/TestImpactTestEnumerationSuiteFactory.h>
#include <TestEngine/Enumeration/TestImpactTestEnumerationException.h>
#include <TestEngine/TestImpactTestEngineException.h>
#include <TestEngine/Enumeration/TestImpactTestEnumerationSerializer.h>
#include <TestEngine/Enumeration/TestImpactTestEnumerator.h>
#include <TestEngine/Enumeration/TestImpactTestEnumeration.h>
@@ -36,7 +36,7 @@ namespace TestImpact
AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY))
{
AZ_TestImpact_Eval(
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheWriteFailure), TestEnumerationException,
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheWriteFailure), TestEngineException,
"Couldn't open cache file for writing");
return;
}
@@ -44,50 +44,16 @@ namespace TestImpact
if (cacheFile.Write(cacheBytes.data(), cacheBytes.size()) == 0)
{
AZ_TestImpact_Eval(
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheWriteFailure), TestEnumerationException,
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheWriteFailure), TestEngineException,
"Couldn't write cache file data");
return;
}
}
AZStd::optional<TestEnumeration> ReadCacheFile(const RepoPath& path, Bitwise::CacheExceptionPolicy cacheExceptionPolicy)
{
AZ::IO::SystemFile cacheFile;
AZStd::string cacheJSON;
if (!cacheFile.Open(path.c_str(), AZ::IO::SystemFile::SF_OPEN_READ_ONLY))
{
AZ_TestImpact_Eval(
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheNotExist), TestEnumerationException,
"Couldn't locate cache file");
return AZStd::nullopt;
}
const AZ::IO::SystemFile::SizeType length = cacheFile.Length();
if (length == 0)
{
AZ_TestImpact_Eval(
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheReadFailure), TestEnumerationException,
"Cache file is empty");
return AZStd::nullopt;
}
cacheFile.Seek(0, AZ::IO::SystemFile::SF_SEEK_BEGIN);
cacheJSON.resize(length);
if (cacheFile.Read(length, cacheJSON.data()) != length)
{
AZ_TestImpact_Eval(
!IsFlagSet(cacheExceptionPolicy, Bitwise::CacheExceptionPolicy::OnCacheReadFailure), TestEnumerationException,
"Couldn't read cache file");
return AZStd::nullopt;
}
return DeserializeTestEnumeration(cacheJSON);
}
} // namespace
TestEnumeration ParseTestEnumerationFile(const RepoPath& enumerationFile)
{
return TestEnumeration(GTest::TestEnumerationSuitesFactory(ReadFileContents<TestEnumerationException>(enumerationFile)));
return TestEnumeration(GTest::TestEnumerationSuitesFactory(ReadFileContents<TestEngineException>(enumerationFile)));
}
TestEnumerationJobData::TestEnumerationJobData(const RepoPath& enumerationArtifact, AZStd::optional<Cache>&& cache)
@@ -128,7 +94,16 @@ namespace TestImpact
if (jobInfo.GetCache().has_value() && jobInfo.GetCache()->m_policy == JobData::CachePolicy::Read)
{
JobMeta meta;
auto enumeration = ReadCacheFile(jobInfo.GetCache()->m_file, cacheExceptionPolicy);
AZStd::optional<TestEnumeration> enumeration;
try
{
enumeration = TestEnumeration(DeserializeTestEnumeration(ReadFileContents<TestEngineException>(jobInfo.GetCache()->m_file)));
}
catch (const TestEngineException& e)
{
AZ_Printf("Enumerate", "Enumeration cache error: %s", e.what());
}
// Even though cached jobs don't get executed we still give the client the opportunity to handle the job state
// change in order to make the caching process transparent to the client
@@ -164,12 +139,20 @@ namespace TestImpact
const auto& [meta, jobInfo] = jobData;
if (meta.m_result == JobResult::ExecutedWithSuccess)
{
const auto& enumeration = (enumerations[jobId] = ParseTestEnumerationFile(jobInfo->GetEnumerationArtifactPath()));
// Write out the enumeration to a cache file if we have a cache write policy for this job
if (jobInfo->GetCache().has_value() && jobInfo->GetCache()->m_policy == JobData::CachePolicy::Write)
try
{
WriteCacheFile(enumeration.value(), jobInfo->GetCache()->m_file, cacheExceptionPolicy);
const auto& enumeration = (enumerations[jobId] = ParseTestEnumerationFile(jobInfo->GetEnumerationArtifactPath()));
// Write out the enumeration to a cache file if we have a cache write policy for this job
if (jobInfo->GetCache().has_value() && jobInfo->GetCache()->m_policy == JobData::CachePolicy::Write)
{
WriteCacheFile(enumeration.value(), jobInfo->GetCache()->m_file, cacheExceptionPolicy);
}
}
catch (const Exception& e)
{
AZ_Warning("Enumerate", false, e.what());
enumerations[jobId] = AZStd::nullopt;
}
}
}
@@ -57,9 +57,7 @@ namespace TestImpact
enum class CacheExceptionPolicy
{
Never = 0, //! Never throw.
OnCacheNotExist = 1, //! Throw when a cache read policy was in place but a cache file for this job doesn't exist.
OnCacheReadFailure = 1 << 1, //! Throw when a cache read policy is in place but the cache file could not be read.
OnCacheWriteFailure = 1 << 2 //! Throw when a cache write policy is in place but the cache file could not be written.
OnCacheWriteFailure = 1 //! Throw when a cache write policy is in place but the cache file could not be written.
};
} // namespace Bitwise
@@ -14,8 +14,8 @@
#include <Artifact/Factory/TestImpactModuleCoverageFactory.h>
#include <Artifact/Factory/TestImpactTestRunSuiteFactory.h>
#include <TestEngine/TestImpactTestEngineException.h>
#include <TestEngine/Run/TestImpactInstrumentedTestRunner.h>
#include <TestEngine/Run/TestImpactTestRunException.h>
#include <TestEngine/Run/TestImpactTestRunSerializer.h>
#include <AzCore/IO/SystemFile.h>
@@ -36,18 +36,10 @@ namespace TestImpact
InstrumentedTestRunner::JobPayload ParseTestRunAndCoverageFiles(
const RepoPath& runFile,
const RepoPath& coverageFile,
AZStd::chrono::milliseconds duration,
InstrumentedTestRunner::CoverageExceptionPolicy coverageExceptionPolicy)
AZStd::chrono::milliseconds duration)
{
TestRun run(GTest::TestRunSuitesFactory(ReadFileContents<TestRunException>(runFile)), duration);
AZStd::vector<ModuleCoverage> moduleCoverages = Cobertura::ModuleCoveragesFactory(ReadFileContents<TestRunException>(coverageFile));
if (moduleCoverages.empty())
{
AZ_TestImpact_Eval(
!IsFlagSet(coverageExceptionPolicy, Bitwise::CoverageExceptionPolicy::OnEmptyCoverage), TestRunException,
AZStd::string::format("No coverage data generated for '%s'", coverageFile.c_str()));
}
TestRun run(GTest::TestRunSuitesFactory(ReadFileContents<TestEngineException>(runFile)), duration);
AZStd::vector<ModuleCoverage> moduleCoverages = Cobertura::ModuleCoveragesFactory(ReadFileContents<TestEngineException>(coverageFile));
TestCoverage coverage(AZStd::move(moduleCoverages));
return {AZStd::move(run), AZStd::move(coverage)};
}
@@ -59,13 +51,12 @@ namespace TestImpact
AZStd::pair<ProcessSchedulerResult, AZStd::vector<InstrumentedTestRunner::Job>> InstrumentedTestRunner::RunInstrumentedTests(
const AZStd::vector<JobInfo>& jobInfos,
CoverageExceptionPolicy coverageExceptionPolicy,
JobExceptionPolicy jobExceptionPolicy,
AZStd::optional<AZStd::chrono::milliseconds> runTimeout,
AZStd::optional<AZStd::chrono::milliseconds> runnerTimeout,
AZStd::optional<ClientJobCallback> clientCallback)
{
const auto payloadGenerator = [this, coverageExceptionPolicy](const JobDataMap& jobDataMap)
const auto payloadGenerator = [this](const JobDataMap& jobDataMap)
{
PayloadMap<Job> runs;
for (const auto& [jobId, jobData] : jobDataMap)
@@ -78,18 +69,12 @@ namespace TestImpact
runs[jobId] = ParseTestRunAndCoverageFiles(
jobInfo->GetRunArtifactPath(),
jobInfo->GetCoverageArtifactPath(),
meta.m_duration.value(),
coverageExceptionPolicy);
meta.m_duration.value());
}
catch (const Exception& e)
{
AZ_Warning("RunInstrumentedTests", false, e.what());
AZ_Printf("RunInstrumentedTests", e.what());
runs[jobId] = AZStd::nullopt;
if (coverageExceptionPolicy == CoverageExceptionPolicy::OnEmptyCoverage)
{
break;
}
}
}
}
@@ -33,16 +33,6 @@ namespace TestImpact
RepoPath m_coverageArtifact; //!< Path to coverage data.
};
namespace Bitwise
{
//! Exception policy for test coverage artifacts.
enum class CoverageExceptionPolicy
{
Never = 0, //! Never throw.
OnEmptyCoverage = 1 //! Throw when no coverage data was produced.
};
} // namespace Bitwise
//! Runs a batch of test targets to determine the test coverage and passes/failures.
class InstrumentedTestRunner
: public TestJobRunner<InstrumentedTestRunJobData, AZStd::pair<TestRun, TestCoverage>>
@@ -50,15 +40,12 @@ namespace TestImpact
using JobRunner = TestJobRunner<InstrumentedTestRunJobData, AZStd::pair<TestRun, TestCoverage>>;
public:
using CoverageExceptionPolicy = Bitwise::CoverageExceptionPolicy;
//! Constructs an instrumented test runner with the specified parameters common to all job runs of this runner.
//! @param maxConcurrentRuns The maximum number of runs to be in flight at any given time.
explicit InstrumentedTestRunner(size_t maxConcurrentRuns);
//! Executes the specified instrumented test run jobs according to the specified job exception policies.
//! @param jobInfos The test run jobs to execute.
//! @param CoverageExceptionPolicy The coverage exception policy to be used for this run.
//! @param jobExceptionPolicy The test run job exception policy to be used for this run (use
//! TestJobExceptionPolicy::OnFailedToExecute to throw on test failures).
//! @param runTimeout The maximum duration a run may be in-flight for before being forcefully terminated.
@@ -67,7 +54,6 @@ namespace TestImpact
//! @return The result of the run sequence and the instrumented run jobs with their associated test run and coverage payloads.
AZStd::pair<ProcessSchedulerResult, AZStd::vector<Job>> RunInstrumentedTests(
const AZStd::vector<JobInfo>& jobInfos,
CoverageExceptionPolicy coverageExceptionPolicy,
JobExceptionPolicy jobExceptionPolicy,
AZStd::optional<AZStd::chrono::milliseconds> runTimeout,
AZStd::optional<AZStd::chrono::milliseconds> runnerTimeout,
@@ -10,7 +10,7 @@
*
*/
#include <TestEngine/Run/TestImpactTestRunException.h>
#include <TestEngine/TestImpactTestEngineException.h>
#include <TestEngine/Run/TestImpactTestRunSerializer.h>
#include <AzCore/JSON/document.h>
@@ -142,7 +142,7 @@ namespace TestImpact
if (doc.Parse<0>(testEnumString.c_str()).HasParseError())
{
throw TestRunException("Could not parse enumeration data");
throw TestEngineException("Could not parse enumeration data");
}
// Run duration
@@ -13,7 +13,7 @@
#include <TestImpactFramework/TestImpactUtils.h>
#include <Artifact/Factory/TestImpactTestRunSuiteFactory.h>
#include <TestEngine/Run/TestImpactTestRunException.h>
#include <TestEngine/TestImpactTestEngineException.h>
#include <TestEngine/Run/TestImpactTestRunSerializer.h>
#include <TestEngine/Run/TestImpactTestRunner.h>
@@ -21,11 +21,6 @@
namespace TestImpact
{
TestRun ParseTestRunFile(const RepoPath& runFile, AZStd::chrono::milliseconds duration)
{
return TestRun(GTest::TestRunSuitesFactory(ReadFileContents<TestRunException>(runFile)), duration);
}
TestRunner::TestRunner(size_t maxConcurrentRuns)
: JobRunner(maxConcurrentRuns)
{
@@ -48,11 +43,11 @@ namespace TestImpact
{
try
{
runs[jobId] = ParseTestRunFile(jobInfo->GetRunArtifactPath(), meta.m_duration.value());
runs[jobId] = TestRun(GTest::TestRunSuitesFactory(ReadFileContents<TestEngineException>(jobInfo->GetRunArtifactPath())), meta.m_duration.value());
}
catch (const Exception& e)
{
AZ_Warning("RunTests", false, e.what());
AZ_Printf("RunTests", e.what());
runs[jobId] = AZStd::nullopt;
}
}
@@ -0,0 +1,382 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <Target/TestImpactTestTarget.h>
#include <TestEngine/TestImpactTestEngineException.h>
#include <TestEngine/TestImpactTestEngine.h>
#include <TestEngine/Enumeration/TestImpactTestEnumerator.h>
#include <TestEngine/Run/TestImpactInstrumentedTestRunner.h>
#include <TestEngine/Run/TestImpactTestRunner.h>
#include <TestEngine/JobRunner/TestImpactTestJobInfoGenerator.h>
#include <AzCore/std/containers/unordered_map.h>
namespace TestImpact
{
// Known error codes for test instrumentation, test runner and unit test library
// This could be refactored into a generic solution agnostic of the tool and library specific details
namespace ErrorCodes
{
namespace OpenCppCoverage
{
static constexpr ReturnCode InvalidArgs = -1618178468;
}
namespace GTest
{
static constexpr ReturnCode Unsuccessful = 1;
}
namespace AZTestRunner
{
static constexpr ReturnCode InvalidArgs = 101;
static constexpr ReturnCode FailedToFindTargetBinary = 102;
static constexpr ReturnCode SymbolNotFound = 103;
static constexpr ReturnCode ModuleSkipped = 104;
}
}
namespace
{
// Calculate the sequence result by analysing the state of the test targets that were run.
template<typename TestEngineJobType>
TestSequenceResult CalculateSequenceResult(
ProcessSchedulerResult result,
const AZStd::vector<TestEngineJobType>& engineJobs,
Policy::ExecutionFailure executionFailurePolicy)
{
if (result == ProcessSchedulerResult::Timeout)
{
// Test job runner timing out overrules all other possible sequence results
return TestSequenceResult::Timeout;
}
bool hasExecutionFailures = false;
bool hasTestFailures = false;
for (const auto& engineJob : engineJobs)
{
switch (engineJob.GetTestResult())
{
case Client::TestRunResult::FailedToExecute:
{
hasExecutionFailures = true;
break;
}
case Client::TestRunResult::Timeout:
case Client::TestRunResult::TestFailures:
{
hasTestFailures = true;
break;
}
default:
{
continue;
}
}
}
// Execution failure can be considered test passes if a permissive execution failure policy is used, otherwise they are failures
if ((hasExecutionFailures && executionFailurePolicy != Policy::ExecutionFailure::Ignore) || hasTestFailures)
{
return TestSequenceResult::Failure;
}
else
{
return TestSequenceResult::Success;
}
}
// Deduces the run result for a given test target based on how the process exited and known return values
Client::TestRunResult GetClientTestRunResultForMeta(const JobMeta& meta)
{
// Attempt to determine why a given test target executed successfully but return with an error code
if (meta.m_returnCode.has_value())
{
switch (meta.m_returnCode.value())
{
// We will consider test targets that technically execute but their launcher or unit test library return a know error
// code that pertains to incorrect argument usage as test targets that failed to execute
case ErrorCodes::OpenCppCoverage::InvalidArgs:
case ErrorCodes::AZTestRunner::InvalidArgs:
case ErrorCodes::AZTestRunner::FailedToFindTargetBinary:
case ErrorCodes::AZTestRunner::ModuleSkipped:
case ErrorCodes::AZTestRunner::SymbolNotFound:
return Client::TestRunResult::FailedToExecute;
// The trivial case: the test target has failing tests
case ErrorCodes::GTest::Unsuccessful:
return Client::TestRunResult::TestFailures;
default:
break;
}
}
switch (meta.m_result)
{
// If the test target executed successfully but returned in an unknown abnormal state it's probably because a test caused
// an unhandled exception, segfault or any other of the weird and wonderful ways a badly behaving test can terminate
case JobResult::ExecutedWithFailure:
return Client::TestRunResult::TestFailures;
// The trivial case: all of the tests in the test target passed
case JobResult::ExecutedWithSuccess:
return Client::TestRunResult::AllTestsPass;
// NotExecuted happens when a test is queued for launch but the test runner terminates the sequence (either due to client abort
// or due to the sequence timer expiring) whereas Terminated happens when the aforementioned scenarios happen when the test target
// is in flight
case JobResult::NotExecuted:
case JobResult::Terminated:
return Client::TestRunResult::NotRun;
// The individual timer for the test target expired
case JobResult::Timeout:
return Client::TestRunResult::Timeout;
default:
throw(TestEngineException(AZStd::string::format("Unexpected job result: %u", static_cast<unsigned int>(meta.m_result))));
}
}
// Map for storing the test engine job data of completed test target runs
template<typename IdType>
using TestEngineJobMap = AZStd::unordered_map<IdType, TestEngineJob>;
// Helper trait for identifying the test engine job specialization for a given test job runner
template<typename TestJobRunner>
struct TestJobRunnerTrait
{};
// Helper function for getting the type directly of the test job runner trait
template<typename TestJobRunner>
using TestEngineJobType = typename TestJobRunnerTrait<TestJobRunner>::TestEngineJobType;
// Type trait for the test enumerator
template<>
struct TestJobRunnerTrait<TestEnumerator>
{
using TestEngineJobType = TestEngineEnumeration;
};
// Type trait for the test runner
template<>
struct TestJobRunnerTrait<TestRunner>
{
using TestEngineJobType = TestEngineRegularRun;
};
// Type trait for the instrumented test runner
template<>
struct TestJobRunnerTrait<InstrumentedTestRunner>
{
using TestEngineJobType = TestEngineInstrumentedRun;
};
// Functor for handling test job runner callbacks
template<typename TestJobRunner>
class TestJobRunnerCallbackHandler
{
using IdType = typename TestJobRunner::JobInfo::IdType;
using JobInfo = typename TestJobRunner::JobInfo;
public:
TestJobRunnerCallbackHandler(
const AZStd::vector<const TestTarget*>& testTargets,
TestEngineJobMap<IdType>* engineJobs,
AZStd::optional<TestEngineJobCompleteCallback>* callback)
: m_testTargets(testTargets)
, m_engineJobs(engineJobs)
, m_callback(callback)
{
}
void operator()(const typename JobInfo& jobInfo, const TestImpact::JobMeta& meta)
{
const auto id = jobInfo.GetId().m_value;
const auto& args = jobInfo.GetCommand().m_args;
const auto* target = m_testTargets[id];
const auto result = GetClientTestRunResultForMeta(meta);
// Place the test engine job associated with this test run into the map along with its client test run result so
// that it can be retrieved when the sequence has ended (and any associated artifacts processed)
const auto& [it, success] = m_engineJobs->emplace(id, TestEngineJob(target, args, meta, result));
if (m_callback->has_value())
{
(*m_callback).value()(it->second);
}
}
private:
const AZStd::vector<const TestTarget*>& m_testTargets;
TestEngineJobMap<typename IdType>* m_engineJobs;
AZStd::optional<TestEngineJobCompleteCallback>* m_callback;
};
// Helper function to compile the run type specific test engine jobs from their associated jobs and payloads
template<typename TestJobRunner>
AZStd::vector<TestEngineJobType<TestJobRunner>> CompileTestEngineRuns(
const AZStd::vector<const TestTarget*>& testTargets,
AZStd::vector<typename TestJobRunner::Job>& runnerjobs,
TestEngineJobMap<typename TestJobRunner::JobInfo::IdType>&& engineJobs)
{
AZStd::vector<TestEngineJobType<TestJobRunner>> engineRuns;
engineRuns.reserve(testTargets.size());
for (auto& job : runnerjobs)
{
const auto id = job.GetJobInfo().GetId().m_value;
if (auto it = engineJobs.find(id);
it != engineJobs.end())
{
// An entry in the test engine job map means that this job was acted upon (an attempt to execute, successful or otherwise)
auto& engineJob = it->second;
TestEngineJobType<TestJobRunner> run(AZStd::move(engineJob), job.ReleasePayload());
engineRuns.push_back(AZStd::move(run));
}
else
{
// No entry in the test engine job map means that this job never had the opportunity to be acted upon (the sequence
// was terminated whilst this job was still queued up for execution)
const auto& args = job.GetJobInfo().GetCommand().m_args;
const auto* target = testTargets[id];
TestEngineJobType<TestJobRunner> run(TestEngineJob(target, args, {}, Client::TestRunResult::NotRun), {});
engineRuns.push_back(AZStd::move(run));
}
}
return engineRuns;
}
Bitwise::TestJobExceptionPolicy GetTestJobExceptionPolicy(
Policy::ExecutionFailure executionFailurePolicy, Policy::TestFailure testFailurePolicy)
{
auto jobExecutionPolicy = Bitwise::TestJobExceptionPolicy::Never;
if (executionFailurePolicy == Policy::ExecutionFailure::Abort)
{
jobExecutionPolicy |= Bitwise::TestJobExceptionPolicy::OnFailedToExecute;
}
if (testFailurePolicy == Policy::TestFailure::Abort)
{
jobExecutionPolicy |= Bitwise::TestJobExceptionPolicy::OnExecutedWithFailure;
}
return jobExecutionPolicy;
}
}
TestEngine::TestEngine(
const RepoPath& sourceDir,
const RepoPath& targetBinaryDir,
const RepoPath& cacheDir,
const RepoPath& artifactDir,
const RepoPath& testRunnerBinary,
const RepoPath& instrumentBinary,
size_t maxConcurrentRuns)
: m_maxConcurrentRuns(maxConcurrentRuns)
, m_testJobInfoGenerator(AZStd::make_unique<TestJobInfoGenerator>(
sourceDir, targetBinaryDir, cacheDir, artifactDir, testRunnerBinary, instrumentBinary))
, m_testEnumerator(AZStd::make_unique<TestEnumerator>(maxConcurrentRuns))
, m_instrumentedTestRunner(AZStd::make_unique<InstrumentedTestRunner>(maxConcurrentRuns))
, m_testRunner(AZStd::make_unique<TestRunner>(maxConcurrentRuns))
{
}
TestEngine::~TestEngine() = default;
AZStd::pair<TestSequenceResult, AZStd::vector<TestEngineEnumeration>> TestEngine::UpdateEnumerationCache(
const AZStd::vector<const TestTarget*>& testTargets,
Policy::ExecutionFailure executionFailurePolicy,
AZStd::optional<AZStd::chrono::milliseconds> testTargetTimeout,
AZStd::optional<AZStd::chrono::milliseconds> globalTimeout,
AZStd::optional<TestEngineJobCompleteCallback> callback)
{
TestEngineJobMap<TestEnumerator::JobInfo::IdType> engineJobs;
const auto jobInfos = m_testJobInfoGenerator->GenerateTestEnumerationJobInfos(testTargets, TestEnumerator::JobInfo::CachePolicy::Write);
const auto jobExecutionPolicy = executionFailurePolicy == Policy::ExecutionFailure::Abort
? (TestEnumerator::JobExceptionPolicy::OnExecutedWithFailure | TestEnumerator::JobExceptionPolicy::OnFailedToExecute)
: TestEnumerator::JobExceptionPolicy::Never;
auto [result, runnerJobs] = m_testEnumerator->Enumerate(
jobInfos,
TestEnumerator::CacheExceptionPolicy::OnCacheWriteFailure,
jobExecutionPolicy,
testTargetTimeout,
globalTimeout,
TestJobRunnerCallbackHandler<TestEnumerator>(testTargets, &engineJobs, &callback));
auto engineRuns = CompileTestEngineRuns<TestEnumerator>(testTargets, runnerJobs, AZStd::move(engineJobs));
return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) };
}
AZStd::pair<TestSequenceResult, AZStd::vector<TestEngineRegularRun>> TestEngine::RegularRun(
const AZStd::vector<const TestTarget*>& testTargets,
[[maybe_unused]]Policy::TestSharding testShardingPolicy,
Policy::ExecutionFailure executionFailurePolicy,
Policy::TestFailure testFailurePolicy,
[[maybe_unused]]Policy::TargetOutputCapture targetOutputCapture,
AZStd::optional<AZStd::chrono::milliseconds> testTargetTimeout,
AZStd::optional<AZStd::chrono::milliseconds> globalTimeout,
AZStd::optional<TestEngineJobCompleteCallback> callback)
{
TestEngineJobMap<TestRunner::JobInfo::IdType> engineJobs;
const auto jobInfos = m_testJobInfoGenerator->GenerateRegularTestRunJobInfos(testTargets);
TestJobRunnerCallbackHandler<TestRunner> jobCallback(testTargets, &engineJobs, &callback);
auto [result, runnerJobs] = m_testRunner->RunTests(
jobInfos,
GetTestJobExceptionPolicy(executionFailurePolicy, testFailurePolicy),
testTargetTimeout,
globalTimeout,
jobCallback);
auto engineRuns = CompileTestEngineRuns<TestRunner>(testTargets, runnerJobs, AZStd::move(engineJobs));
return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) };
}
AZStd::pair<TestSequenceResult, AZStd::vector<TestEngineInstrumentedRun>> TestEngine::InstrumentedRun(
const AZStd::vector<const TestTarget*>& testTargets,
[[maybe_unused]] Policy::TestSharding testShardingPolicy,
Policy::ExecutionFailure executionFailurePolicy,
Policy::IntegrityFailure integrityFailurePolicy,
Policy::TestFailure testFailurePolicy,
[[maybe_unused]]Policy::TargetOutputCapture targetOutputCapture,
AZStd::optional<AZStd::chrono::milliseconds> testTargetTimeout,
AZStd::optional<AZStd::chrono::milliseconds> globalTimeout,
AZStd::optional<TestEngineJobCompleteCallback> callback)
{
TestEngineJobMap<InstrumentedTestRunner::JobInfo::IdType> engineJobs;
const auto jobInfos = m_testJobInfoGenerator->GenerateInstrumentedTestRunJobInfos(testTargets, CoverageLevel::Source);
auto [result, runnerJobs] = m_instrumentedTestRunner->RunInstrumentedTests(
jobInfos,
GetTestJobExceptionPolicy(executionFailurePolicy, testFailurePolicy),
testTargetTimeout,
globalTimeout,
TestJobRunnerCallbackHandler<InstrumentedTestRunner>(testTargets, &engineJobs, &callback));
auto engineRuns = CompileTestEngineRuns<InstrumentedTestRunner>(testTargets, runnerJobs, AZStd::move(engineJobs));
// Now that we know the true result of successful jobs that return non-zero we can deduce if we have any integrity failures
// where a test target ran and completed its tests without incident yet failed to produce coverage data
if (integrityFailurePolicy == Policy::IntegrityFailure::Abort)
{
for (const auto& engineRun : engineRuns)
{
if (const auto testResult = engineRun.GetTestResult();
testResult == Client::TestRunResult::AllTestsPass || testResult == Client::TestRunResult::TestFailures)
{
AZ_TestImpact_Eval(engineRun.GetTestCoverge().has_value(), TestEngineException, AZStd::string::format(
"Test target %s completed its test run but failed to produce coverage data", engineRun.GetTestTarget()->GetName().c_str()));
}
}
}
return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) };
}
} // namespace TestImpact
@@ -0,0 +1,126 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <TestImpactFramework/TestImpactTestSequence.h>
#include <TestImpactFramework/TestImpactClientTestRun.h>
#include <TestImpactFramework/TestImpactRuntime.h>
#include <TestEngine/TestImpactTestEngineEnumeration.h>
#include <TestEngine/TestImpactTestEngineInstrumentedRun.h>
#include <TestEngine/TestImpactTestEngineRegularRun.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
namespace TestImpact
{
class TestTarget;
class TestJobInfoGenerator;
class TestEnumerator;
class InstrumentedTestRunner;
class TestRunner;
//! Callback for when a given test engine job completes.
using TestEngineJobCompleteCallback = AZStd::function<void(const TestEngineJob& testJob)>;
//! Provides the front end for performing test enumerations and test runs.
class TestEngine
{
public:
//! Configures the test engine with the necessary path information for launching test targets and managing the artifacts they produce.
//! @param sourceDir Root path where source files are found (including subfolders).
//! @param targetBinaryDir Path to where the test target binaries are found.
//! @param cacheDir Path to the persistent folder where test target enumerations are cached.
//! @param artifactDir Path to the transient directory where test artifacts are produced.
//! @param testRunnerBinary Path to the binary responsible for launching test targets that have the TestRunner launch method.
//! @param instrumentBinary Path to the binary responsible for launching test targets with test coverage instrumentation.
//! @param maxConcurrentRuns The maximum number of concurrent test targets that can be in flight at any given moment.
TestEngine(
const RepoPath& sourceDir,
const RepoPath& targetBinaryDir,
const RepoPath& cacheDir,
const RepoPath& artifactDir,
const RepoPath& testRunnerBinary,
const RepoPath& instrumentBinary,
size_t maxConcurrentRuns);
~TestEngine();
//! Updates the cached enumerations for the specified test targets.
//! @note Whilst test runs will make use of this cache for test target sharding it is the responsibility of the client to
//! ensure any stale caches are up to date by calling this function. No attempt to maintain internal consistency will be made
//! by the test engine itself.
//! @param testTargets The test targets to enumerate.
//! @param executionFailurePolicy The policy for how enumeration execution failures should be handled.
//! @param testTargetTimeout The maximum duration a test target may be in-flight for before being forcefully terminated (infinite if empty).
//! @param globalTimeout The maximum duration the enumeration sequence may run before being forcefully terminated (infinite if empty).
//! @param callback The client callback function to handle completed test target enumerations.
//! @ returns The sequence result and the enumerations for the target that were enumerated.
AZStd::pair < TestSequenceResult, AZStd::vector<TestEngineEnumeration>> UpdateEnumerationCache(
const AZStd::vector<const TestTarget*>& testTargets,
Policy::ExecutionFailure executionFailurePolicy,
AZStd::optional<AZStd::chrono::milliseconds> testTargetTimeout,
AZStd::optional<AZStd::chrono::milliseconds> globalTimeout,
AZStd::optional<TestEngineJobCompleteCallback> callback);
//! Performs a test run without any instrumentation and, for each test target, returns the test run results and metrics about the run.
//! @param testTargets The test targets to run.
//! @param testShardingPolicy Test sharding policy to use for test targets in this run.
//! @param executionFailurePolicy Policy for how test execution failures should be handled.
//! @param testFailurePolicy Policy for how test targets with failing tests should be handled.
//! @param targetOutputCapture Policy for how test target standard output should be captured and handled.
//! @param testTargetTimeout The maximum duration a test target may be in-flight for before being forcefully terminated (infinite if empty).
//! @param globalTimeout The maximum duration the enumeration sequence may run before being forcefully terminated (infinite if empty).
//! @param callback The client callback function to handle completed test target runs.
//! @ returns The sequence result and the test run results for the test targets that were run.
[[nodiscard]]AZStd::pair<TestSequenceResult, AZStd::vector<TestEngineRegularRun>> RegularRun(
const AZStd::vector<const TestTarget*>& testTargets,
Policy::TestSharding testShardingPolicy,
Policy::ExecutionFailure executionFailurePolicy,
Policy::TestFailure testFailurePolicy,
Policy::TargetOutputCapture targetOutputCapture,
AZStd::optional<AZStd::chrono::milliseconds> testTargetTimeout,
AZStd::optional<AZStd::chrono::milliseconds> globalTimeout,
AZStd::optional<TestEngineJobCompleteCallback> callback);
//! Performs a test run with instrumentation and, for each test target, returns the test run results, coverage data and metrics about the run.
//! @param testTargets The test targets to run.
//! @param testShardingPolicy Test sharding policy to use for test targets in this run.
//! @param executionFailurePolicy Policy for how test execution failures should be handled.
//! @param integrityFailurePolicy Policy for how integrty failures of the test impact data and source tree model should be handled.
//! @param testFailurePolicy Policy for how test targets with failing tests should be handled.
//! @param targetOutputCapture Policy for how test target standard output should be captured and handled.
//! @param testTargetTimeout The maximum duration a test target may be in-flight for before being forcefully terminated (infinite if empty).
//! @param globalTimeout The maximum duration the enumeration sequence may run before being forcefully terminated (infinite if empty).
//! @param callback The client callback function to handle completed test target runs.
//! @ returns The sequence result and the test run results and test coverages for the test targets that were run.
[[nodiscard]]AZStd::pair<TestSequenceResult, AZStd::vector<TestEngineInstrumentedRun>> InstrumentedRun(
const AZStd::vector<const TestTarget*>& testTargets,
Policy::TestSharding testShardingPolicy,
Policy::ExecutionFailure executionFailurePolicy,
Policy::IntegrityFailure integrityFailurePolicy,
Policy::TestFailure testFailurePolicy,
Policy::TargetOutputCapture targetOutputCapture,
AZStd::optional<AZStd::chrono::milliseconds> testTargetTimeout,
AZStd::optional<AZStd::chrono::milliseconds> globalTimeout,
AZStd::optional<TestEngineJobCompleteCallback> callback);
private:
size_t m_maxConcurrentRuns = 0;
AZStd::unique_ptr<TestJobInfoGenerator> m_testJobInfoGenerator;
AZStd::unique_ptr<TestEnumerator> m_testEnumerator;
AZStd::unique_ptr<InstrumentedTestRunner> m_instrumentedTestRunner;
AZStd::unique_ptr<TestRunner> m_testRunner;
};
} // namespace TestImpact
@@ -0,0 +1,27 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <TestEngine/TestImpactTestEngineEnumeration.h>
namespace TestImpact
{
TestEngineEnumeration::TestEngineEnumeration(TestEngineJob&& job, AZStd::optional<TestEnumeration>&& enumeration)
: TestEngineJob(AZStd::move(job))
, m_enumeration(AZStd::move(enumeration))
{
}
const AZStd::optional<TestEnumeration>& TestEngineEnumeration::GetTestEnumeration() const
{
return m_enumeration;
}
} // namespace TestImpact
@@ -0,0 +1,32 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <TestEngine/TestImpactTestEngineJob.h>
#include <TestEngine/Enumeration/TestImpactTestEnumeration.h>
namespace TestImpact
{
//! Represents the generated test enumeration data for a test engine enumeration.
class TestEngineEnumeration
: public TestEngineJob
{
public:
TestEngineEnumeration(TestEngineJob&& job, AZStd::optional<TestEnumeration>&& enumeration);
//! Returns the test enumeration payload for this job (if any).
const AZStd::optional<TestEnumeration>& GetTestEnumeration() const;
private:
AZStd::optional<TestEnumeration> m_enumeration;
};
} // namespace TestImpact
@@ -16,8 +16,8 @@
namespace TestImpact
{
//! Exception for test runs and test run related operations.
class TestRunException : public Exception
//! Exception for test engine runs and related operations.
class TestEngineException : public Exception
{
public:
using Exception::Exception;
@@ -0,0 +1,52 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <TestEngine/TestImpactTestEngineInstrumentedRun.h>
namespace TestImpact
{
namespace
{
AZStd::optional<TestRun> ReleaseTestRun(AZStd::optional<AZStd::pair<TestRun, TestCoverage>>&& testRunAndCoverage)
{
if (testRunAndCoverage.has_value())
{
return AZStd::move(testRunAndCoverage.value().first);
}
return AZStd::nullopt;
}
AZStd::optional<TestCoverage> ReleaseTestCoverage(AZStd::optional<AZStd::pair<TestRun, TestCoverage>>&& testRunAndCoverage)
{
if (testRunAndCoverage.has_value())
{
return AZStd::move(testRunAndCoverage.value().second);
}
return AZStd::nullopt;
}
}
TestEngineInstrumentedRun::TestEngineInstrumentedRun(TestEngineJob&& testJob, AZStd::optional<AZStd::pair<TestRun, TestCoverage>>&& testRunAndCoverage)
: TestEngineRegularRun(AZStd::move(testJob), ReleaseTestRun(AZStd::move(testRunAndCoverage)))
, m_testCoverage(ReleaseTestCoverage(AZStd::move(testRunAndCoverage)))
{
}
const AZStd::optional<TestCoverage>& TestEngineInstrumentedRun::GetTestCoverge() const
{
return m_testCoverage;
}
} // namespace TestImpact
@@ -0,0 +1,32 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <TestEngine/TestImpactTestEngineRegularRun.h>
#include <TestEngine/Run/TestImpactTestCoverage.h>
namespace TestImpact
{
//! Represents the generated test run and coverage data for an instrumented regular test engine run.
class TestEngineInstrumentedRun
: public TestEngineRegularRun
{
public:
TestEngineInstrumentedRun(TestEngineJob&& testJob, AZStd::optional<AZStd::pair<TestRun, TestCoverage>>&& testRunAndCoverage);
//! Returns the test coverage payload for this job (if any).
const AZStd::optional<TestCoverage>& GetTestCoverge() const;
private:
AZStd::optional<TestCoverage> m_testCoverage;
};
} // namespace TestImpact
@@ -0,0 +1,40 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <Target/TestImpactTestTarget.h>
#include <TestEngine/TestImpactTestEngineJob.h>
namespace TestImpact
{
TestEngineJob::TestEngineJob(const TestTarget* testTarget, const AZStd::string& commandString, const JobMeta& jobMeta, Client::TestRunResult testResult)
: JobMetaWrapper(jobMeta)
, m_testTarget(testTarget)
, m_commandString(commandString)
, m_testResult(testResult)
{
}
const TestTarget* TestEngineJob::GetTestTarget() const
{
return m_testTarget;
}
const AZStd::string& TestEngineJob::GetCommandString() const
{
return m_commandString;
}
Client::TestRunResult TestEngineJob::GetTestResult() const
{
return m_testResult;
}
} // namespace TestImpact
@@ -0,0 +1,41 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <Process/JobRunner/TestImpactProcessJobMeta.h>
namespace TestImpact
{
class TestTarget;
//! Represents the meta-data describing a test engine run.
class TestEngineJob
: public JobMetaWrapper
{
public:
TestEngineJob(const TestTarget* testTarget, const AZStd::string& commandString, const JobMeta& jobMeta, Client::TestRunResult testResult);
//! Returns the test target that was run for this job.
const TestTarget* GetTestTarget() const;
//! Returns the result of the job that was run.
Client::TestRunResult GetTestResult() const;
//! Returns the command string that was used to execute this job.
const AZStd::string& GetCommandString() const;
private:
const TestTarget* m_testTarget;
AZStd::string m_commandString;
Client::TestRunResult m_testResult;
};
} // namespace TestImpact
@@ -10,17 +10,18 @@
*
*/
#pragma once
#include <TestImpactFramework/TestImpactException.h>
#include <TestEngine/TestImpactTestEngineRegularRun.h>
namespace TestImpact
{
//! Exception for test enumerations and test enumeration related operations.
class TestEnumerationException
: public Exception
TestEngineRegularRun::TestEngineRegularRun(TestEngineJob&& testJob, AZStd::optional<TestRun>&& testRun)
: TestEngineJob(AZStd::move(testJob))
, m_testRun(AZStd::move(testRun))
{
public:
using Exception::Exception;
};
}
const AZStd::optional<TestRun>& TestEngineRegularRun::GetTestRun() const
{
return m_testRun;
}
} // namespace TestImpact
@@ -0,0 +1,34 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <TestImpactFramework/TestImpactClientTestRun.h>
#include <TestEngine/TestImpactTestEngineJob.h>
#include <TestEngine/Run/TestImpactTestRun.h>
namespace TestImpact
{
//! Represents the generated test run data for a regular test engine run.
class TestEngineRegularRun
: public TestEngineJob
{
public:
TestEngineRegularRun(TestEngineJob&& testJob, AZStd::optional<TestRun>&& testRun);
//! Returns the test run payload for this job (if any).
const AZStd::optional<TestRun>& GetTestRun() const;
private:
AZStd::optional<TestRun> m_testRun;
};
} // namespace TestImpact