/* * 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 #include #include #include #include #include #include #include #include #include namespace TestImpact { namespace { // Calculate the sequence result by analysing the state of the test targets that were run. template TestSequenceResult CalculateSequenceResult( ProcessSchedulerResult result, const AZStd::vector& 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()) { if (const auto result = CheckForAnyKnownErrorCode(meta.m_returnCode.value()); result != AZStd::nullopt) { return result.value(); } } 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(meta.m_result)))); } } // Map for storing the test engine job data of completed test target runs template using TestEngineJobMap = AZStd::unordered_map; // Helper trait for identifying the test engine job specialization for a given test job runner template struct TestJobRunnerTrait {}; // Helper function for getting the type directly of the test job runner trait template using TestEngineJobType = typename TestJobRunnerTrait::TestEngineJobType; // Type trait for the test enumerator template<> struct TestJobRunnerTrait { using TestEngineJobType = TestEngineEnumeration; }; // Type trait for the test runner template<> struct TestJobRunnerTrait { using TestEngineJobType = TestEngineRegularRun; }; // Type trait for the instrumented test runner template<> struct TestJobRunnerTrait { using TestEngineJobType = TestEngineInstrumentedRun; }; // Functor for handling test job runner callbacks template class TestJobRunnerCallbackHandler { using IdType = typename TestJobRunner::JobInfo::IdType; using JobInfo = typename TestJobRunner::JobInfo; public: TestJobRunnerCallbackHandler( const AZStd::vector& testTargets, TestEngineJobMap* engineJobs, Policy::ExecutionFailure executionFailurePolicy, Policy::TestFailure testFailurePolicy, AZStd::optional* callback) : m_testTargets(testTargets) , m_engineJobs(engineJobs) , m_executionFailurePolicy(executionFailurePolicy) , m_testFailurePolicy(testFailurePolicy) , m_callback(callback) { } [[nodiscard]] ProcessCallbackResult 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); } if ((result == Client::TestRunResult::FailedToExecute && m_executionFailurePolicy == Policy::ExecutionFailure::Abort) || (result == Client::TestRunResult::TestFailures && m_testFailurePolicy == Policy::TestFailure::Abort)) { return ProcessCallbackResult::Abort; } return ProcessCallbackResult::Continue; } private: const AZStd::vector& m_testTargets; TestEngineJobMap* m_engineJobs; Policy::ExecutionFailure m_executionFailurePolicy; Policy::TestFailure m_testFailurePolicy; AZStd::optional* m_callback; }; // Helper function to compile the run type specific test engine jobs from their associated jobs and payloads template AZStd::vector> CompileTestEngineRuns( const AZStd::vector& testTargets, AZStd::vector& runnerjobs, TestEngineJobMap&& engineJobs) { AZStd::vector> 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 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 run(TestEngineJob(target, args, {}, Client::TestRunResult::NotRun), {}); engineRuns.push_back(AZStd::move(run)); } } return engineRuns; } } 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( sourceDir, targetBinaryDir, cacheDir, artifactDir, testRunnerBinary, instrumentBinary)) , m_testEnumerator(AZStd::make_unique(maxConcurrentRuns)) , m_instrumentedTestRunner(AZStd::make_unique(maxConcurrentRuns)) , m_testRunner(AZStd::make_unique(maxConcurrentRuns)) { } TestEngine::~TestEngine() = default; AZStd::pair> TestEngine::UpdateEnumerationCache( const AZStd::vector& testTargets, Policy::ExecutionFailure executionFailurePolicy, Policy::TestFailure testFailurePolicy, AZStd::optional testTargetTimeout, AZStd::optional globalTimeout, AZStd::optional callback) { TestEngineJobMap engineJobs; const auto jobInfos = m_testJobInfoGenerator->GenerateTestEnumerationJobInfos(testTargets, TestEnumerator::JobInfo::CachePolicy::Write); auto [result, runnerJobs] = m_testEnumerator->Enumerate( jobInfos, testTargetTimeout, globalTimeout, TestJobRunnerCallbackHandler(testTargets, &engineJobs, executionFailurePolicy, testFailurePolicy, &callback)); auto engineRuns = CompileTestEngineRuns(testTargets, runnerJobs, AZStd::move(engineJobs)); return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) }; } AZStd::pair> TestEngine::RegularRun( const AZStd::vector& testTargets, [[maybe_unused]]Policy::TestSharding testShardingPolicy, Policy::ExecutionFailure executionFailurePolicy, Policy::TestFailure testFailurePolicy, [[maybe_unused]]Policy::TargetOutputCapture targetOutputCapture, AZStd::optional testTargetTimeout, AZStd::optional globalTimeout, AZStd::optional callback) { TestEngineJobMap engineJobs; const auto jobInfos = m_testJobInfoGenerator->GenerateRegularTestRunJobInfos(testTargets); TestJobRunnerCallbackHandler jobCallback(testTargets, &engineJobs, executionFailurePolicy, testFailurePolicy, &callback); auto [result, runnerJobs] = m_testRunner->RunTests( jobInfos, testTargetTimeout, globalTimeout, jobCallback); auto engineRuns = CompileTestEngineRuns(testTargets, runnerJobs, AZStd::move(engineJobs)); return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) }; } AZStd::pair> TestEngine::InstrumentedRun( const AZStd::vector& testTargets, [[maybe_unused]] Policy::TestSharding testShardingPolicy, Policy::ExecutionFailure executionFailurePolicy, Policy::IntegrityFailure integrityFailurePolicy, Policy::TestFailure testFailurePolicy, [[maybe_unused]]Policy::TargetOutputCapture targetOutputCapture, AZStd::optional testTargetTimeout, AZStd::optional globalTimeout, AZStd::optional callback) { TestEngineJobMap engineJobs; const auto jobInfos = m_testJobInfoGenerator->GenerateInstrumentedTestRunJobInfos(testTargets, CoverageLevel::Source); auto [result, runnerJobs] = m_instrumentedTestRunner->RunInstrumentedTests( jobInfos, testTargetTimeout, globalTimeout, TestJobRunnerCallbackHandler(testTargets, &engineJobs, executionFailurePolicy, testFailurePolicy, &callback)); auto engineRuns = CompileTestEngineRuns(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