Parcourir la source

Merge pull request #510 from aws-lumberyard-dev/LYN-17649_tiaf_native_optimization_native

TIAF: Native Test Sharding
John il y a 2 ans
Parent
commit
b2395c3f18
31 fichiers modifiés avec 1661 ajouts et 172 suppressions
  1. 11 10
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Include/TestImpactFramework/Native/TestImpactNativeConfiguration.h
  2. 0 4
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Include/TestImpactFramework/Native/TestImpactNativeRuntime.h
  3. 17 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Include/TestImpactFramework/Native/TestImpactNativeRuntimeConfigurationFactory.h
  4. 8 3
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.cpp
  5. 4 1
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.h
  6. 1 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Artifact/Static/TestImpactNativeTestTargetMeta.h
  7. 42 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Platform/Windows/TestRunner/Native/Job/TestImpactWin32_NativeTestJobInfoUtils.cpp
  8. 1 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Platform/Windows/platform_windows_files.cmake
  9. 11 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Target/Native/TestImpactNativeTestTarget.cpp
  10. 3 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Target/Native/TestImpactNativeTestTarget.h
  11. 94 33
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestEngine/Native/TestImpactNativeTestEngine.cpp
  12. 18 5
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestEngine/Native/TestImpactNativeTestEngine.h
  13. 4 5
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestImpactNativeRuntime.cpp
  14. 127 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestImpactNativeRuntimeConfigurationFactory.cpp
  15. 106 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.cpp
  16. 332 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h
  17. 23 59
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.cpp
  18. 16 47
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h
  19. 16 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.cpp
  20. 25 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h
  21. 15 5
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestRunJobData.h
  22. 172 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Shard/TestImpactNativeShardedTestJob.h
  23. 308 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Shard/TestImpactNativeShardedTestRunnerBase.h
  24. 3 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h
  25. 3 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeRegularTestRunner.h
  26. 130 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.cpp
  27. 30 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.h
  28. 98 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedRegularTestRunner.cpp
  29. 30 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedRegularTestRunner.h
  30. 3 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeTestEnumerator.h
  31. 10 0
      Code/Tools/TestImpactFramework/Runtime/Native/Code/testimpactframework_runtime_native_files.cmake

+ 11 - 10
Code/Tools/TestImpactFramework/Runtime/Native/Code/Include/TestImpactFramework/Native/TestImpactNativeConfiguration.h

@@ -12,6 +12,14 @@
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
+    //! Temporary workspace configuration.
+    struct NativeShardedArtifactDir
+    {
+        RepoPath m_shardedTestRunArtifactDirectory; //!< Path to read and write sharded test run artifacts to and from.
+        RepoPath m_shardedCoverageArtifactDirectory; //!< Path to read and write coverage artifacts to and from.
+    };
+
+
     //! Test engine configuration.
     //! Test engine configuration.
     struct NativeTestEngineConfig
     struct NativeTestEngineConfig
     {
     {
@@ -40,23 +48,16 @@ namespace TestImpact
     //! Build target configuration.
     //! Build target configuration.
     struct NativeTargetConfig
     struct NativeTargetConfig
     {
     {
-        //! Test target sharding configuration.
-        struct ShardedTarget
-        {
-            AZStd::string m_name; //!< Name of test target this sharding configuration applies to.
-            ShardConfiguration m_configuration; //!< The shard configuration to use.
-        };
-
         RepoPath m_outputDirectory; //!< Path to the test target binary directory.
         RepoPath m_outputDirectory; //!< Path to the test target binary directory.
-        NativeExcludedTargets m_excludedTargets;
-        AZStd::vector<ShardedTarget> m_shardedTestTargets; //!< Test target shard configurations (opt-in).
+        NativeExcludedTargets m_excludedTargets; //!< Test targets to exclude from regular and/or instrumented runs.
     };
     };
 
 
     //! Native runtime configuration.
     //! Native runtime configuration.
     struct NativeRuntimeConfig
     struct NativeRuntimeConfig
     {
     {
         RuntimeConfig m_commonConfig;
         RuntimeConfig m_commonConfig;
-        WorkspaceConfig m_workspace;    
+        WorkspaceConfig m_workspace;
+        NativeShardedArtifactDir m_shardedArtifactDir;
         NativeTestEngineConfig m_testEngine;
         NativeTestEngineConfig m_testEngine;
         NativeTargetConfig m_target;
         NativeTargetConfig m_target;
     };
     };

+ 0 - 4
Code/Tools/TestImpactFramework/Runtime/Native/Code/Include/TestImpactFramework/Native/TestImpactNativeRuntime.h

@@ -56,7 +56,6 @@ namespace TestImpact
         //! @param executionFailureDraftingPolicy Determines how test targets that previously failed to execute are drafted into subsequent test sequences.
         //! @param executionFailureDraftingPolicy Determines how test targets that previously failed to execute are drafted into subsequent test sequences.
         //! @param testFailurePolicy Determines how to handle test targets that report test failures.
         //! @param testFailurePolicy Determines how to handle test targets that report test failures.
         //! @param integrationFailurePolicy Determines how to handle instances where the build system model and/or test impact analysis data is compromised.
         //! @param integrationFailurePolicy Determines how to handle instances where the build system model and/or test impact analysis data is compromised.
-        //! @param testShardingPolicy  Determines how to handle test targets that have opted in to test sharding.
         NativeRuntime(
         NativeRuntime(
             NativeRuntimeConfig&& config,
             NativeRuntimeConfig&& config,
             const AZStd::optional<RepoPath>& dataFile,
             const AZStd::optional<RepoPath>& dataFile,
@@ -68,7 +67,6 @@ namespace TestImpact
             Policy::FailedTestCoverage failedTestCoveragePolicy,
             Policy::FailedTestCoverage failedTestCoveragePolicy,
             Policy::TestFailure testFailurePolicy,
             Policy::TestFailure testFailurePolicy,
             Policy::IntegrityFailure integrationFailurePolicy,
             Policy::IntegrityFailure integrationFailurePolicy,
-            Policy::TestSharding testShardingPolicy,
             Policy::TargetOutputCapture targetOutputCapture,
             Policy::TargetOutputCapture targetOutputCapture,
             AZStd::optional<size_t> maxConcurrency = AZStd::nullopt);
             AZStd::optional<size_t> maxConcurrency = AZStd::nullopt);
 
 
@@ -161,7 +159,6 @@ namespace TestImpact
         Policy::FailedTestCoverage m_failedTestCoveragePolicy;
         Policy::FailedTestCoverage m_failedTestCoveragePolicy;
         Policy::TestFailure m_testFailurePolicy;
         Policy::TestFailure m_testFailurePolicy;
         Policy::IntegrityFailure m_integrationFailurePolicy;
         Policy::IntegrityFailure m_integrationFailurePolicy;
-        Policy::TestSharding m_testShardingPolicy;
         Policy::TargetOutputCapture m_targetOutputCapture;
         Policy::TargetOutputCapture m_targetOutputCapture;
         size_t m_maxConcurrency = 0;
         size_t m_maxConcurrency = 0;
         AZStd::unique_ptr<BuildTargetList<NativeProductionTarget, NativeTestTarget>> m_buildTargets;
         AZStd::unique_ptr<BuildTargetList<NativeProductionTarget, NativeTestTarget>> m_buildTargets;
@@ -170,7 +167,6 @@ namespace TestImpact
         AZStd::unique_ptr<TestEngine> m_testEngine;
         AZStd::unique_ptr<TestEngine> m_testEngine;
         AZStd::unique_ptr<TestTargetExclusionList<NativeTestTarget>> m_regularTestTargetExcludeList;
         AZStd::unique_ptr<TestTargetExclusionList<NativeTestTarget>> m_regularTestTargetExcludeList;
         AZStd::unique_ptr<TestTargetExclusionList<NativeTestTarget>> m_instrumentedTestTargetExcludeList;
         AZStd::unique_ptr<TestTargetExclusionList<NativeTestTarget>> m_instrumentedTestTargetExcludeList;
-        AZStd::unordered_set<const NativeTestTarget*> m_testTargetShardList;
         AZStd::unordered_set<const NativeTestTarget*> m_previouslyFailingTestTargets;
         AZStd::unordered_set<const NativeTestTarget*> m_previouslyFailingTestTargets;
         bool m_hasImpactAnalysisData = false;
         bool m_hasImpactAnalysisData = false;
     };
     };

+ 17 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Include/TestImpactFramework/Native/TestImpactNativeRuntimeConfigurationFactory.h

@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestImpactFramework/Native/TestImpactNativeConfiguration.h>
+
+namespace TestImpact
+{
+    //! Parses the native-specific configuration data (in JSON format) and returns the constructed runtime configuration.
+    NativeRuntimeConfig NativeRuntimeConfigurationFactory(const AZStd::string& configurationData);
+} // namespace TestImpact

+ 8 - 3
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.cpp

@@ -17,7 +17,9 @@
 namespace TestImpact
 namespace TestImpact
 {
 {
     NativeTestTargetMetaMap NativeTestTargetMetaMapFactory(
     NativeTestTargetMetaMap NativeTestTargetMetaMapFactory(
-        const AZStd::string& masterTestListData, const SuiteSet& suiteSet, const SuiteLabelExcludeSet& suiteLabelExcludeSet)
+        const AZStd::string& masterTestListData,
+        const SuiteSet& suiteSet,
+        const SuiteLabelExcludeSet& suiteLabelExcludeSet)
     {
     {
         // Keys for pertinent JSON node and attribute names
         // Keys for pertinent JSON node and attribute names
         constexpr const char* Keys[] =
         constexpr const char* Keys[] =
@@ -37,7 +39,7 @@ namespace TestImpact
             "labels"
             "labels"
         };
         };
 
 
-        enum
+        enum Fields
         {
         {
             GoogleKey,
             GoogleKey,
             TestKey,
             TestKey,
@@ -51,9 +53,12 @@ namespace TestImpact
             NameKey,
             NameKey,
             CommandKey,
             CommandKey,
             TimeoutKey,
             TimeoutKey,
-            SuiteLabelsKey
+            SuiteLabelsKey,
+            // Checksum
+            _CHECKSUM_
         };
         };
 
 
+        static_assert(Fields::_CHECKSUM_ == AZStd::size(Keys));
         AZ_TestImpact_Eval(!masterTestListData.empty(), ArtifactException, "Test meta-data cannot be empty");
         AZ_TestImpact_Eval(!masterTestListData.empty(), ArtifactException, "Test meta-data cannot be empty");
 
 
         NativeTestTargetMetaMap testMetas;
         NativeTestTargetMetaMap testMetas;

+ 4 - 1
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.h

@@ -9,6 +9,7 @@
 #pragma once
 #pragma once
 
 
 #include <TestImpactFramework/TestImpactTestSequence.h>
 #include <TestImpactFramework/TestImpactTestSequence.h>
+#include <TestImpactFramework/Native/TestImpactNativeConfiguration.h>
 
 
 #include <Artifact/Static/TestImpactNativeTestTargetMeta.h>
 #include <Artifact/Static/TestImpactNativeTestTargetMeta.h>
 
 
@@ -20,5 +21,7 @@ namespace TestImpact
     //! @param suiteLabelExcludeSet Any tests with suites that match a label from this set will be excluded.
     //! @param suiteLabelExcludeSet Any tests with suites that match a label from this set will be excluded.
     //! @return The constructed list of test target meta-data artifacts.
     //! @return The constructed list of test target meta-data artifacts.
     NativeTestTargetMetaMap NativeTestTargetMetaMapFactory(
     NativeTestTargetMetaMap NativeTestTargetMetaMapFactory(
-        const AZStd::string& masterTestListData, const SuiteSet& suiteSet, const SuiteLabelExcludeSet& suiteLabelExcludeSet);
+        const AZStd::string& masterTestListData,
+        const SuiteSet& suiteSet,
+        const SuiteLabelExcludeSet& suiteLabelExcludeSet);
 } // namespace TestImpact
 } // namespace TestImpact

+ 1 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Artifact/Static/TestImpactNativeTestTargetMeta.h

@@ -9,6 +9,7 @@
 #pragma once
 #pragma once
 
 
 #include <TestImpactFramework/TestImpactRepoPath.h>
 #include <TestImpactFramework/TestImpactRepoPath.h>
+#include <TestImpactFramework/TestImpactTestSequence.h>
 
 
 #include <Artifact/Static/TestImpactTestTargetMeta.h>
 #include <Artifact/Static/TestImpactTestTargetMeta.h>
 
 

+ 42 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Platform/Windows/TestRunner/Native/Job/TestImpactWin32_NativeTestJobInfoUtils.cpp

@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h>
+
+namespace TestImpact
+{
+    NativeInstrumentedTestRunner::Command GenerateInstrumentedTestJobInfoCommand(
+        const RepoPath& instrumentBindaryPath,
+        const RepoPath& coverageArtifactPath,
+        CoverageLevel coverageLevel,
+        const RepoPath& modulesPath,
+        const RepoPath& excludedModulesPath,
+        const RepoPath& sourcesPath,
+        const NativeRegularTestRunner::Command& testRunLaunchCommand)
+    {
+        return {
+            AZStd::string::format(
+                "\"%s\" " // 1. Instrumented test runner
+                "--coverage_level %s " // 2. Coverage level
+                "--export_type cobertura:\"%s\" " // 3. Test coverage artifact path
+                "--modules \"%s\" " // 4. Modules path
+                "--excluded_modules \"%s\" " // 5. Exclude modules
+                "--sources \"%s\" -- " // 6. Sources path
+                "%s ", // 7. Launch command
+
+                instrumentBindaryPath.c_str(), // 1. Instrumented test runner
+                (coverageLevel == CoverageLevel::Line ? "line" : "source"), // 2. Coverage level
+                coverageArtifactPath.c_str(), // 3. Test coverage artifact path
+                modulesPath.c_str(), // 4. Modules path
+                excludedModulesPath.c_str(), // 5. Exclude modules
+                sourcesPath.c_str(), // 6. Sources path
+                testRunLaunchCommand.m_args.c_str() // 7. Launch command
+            )
+        };
+    }
+} // namespace TestImpact

+ 1 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Platform/Windows/platform_windows_files.cmake

@@ -9,4 +9,5 @@
 set(FILES
 set(FILES
     TestEngine/Native/TestImpactWin32_NativeErrorCodeChecker.cpp
     TestEngine/Native/TestImpactWin32_NativeErrorCodeChecker.cpp
     TestEngine/Native/Job/TestImpactWin32_NativeTestTargetExtension.cpp
     TestEngine/Native/Job/TestImpactWin32_NativeTestTargetExtension.cpp
+    TestRunner/Native/Job/TestImpactWin32_NativeTestJobInfoUtils.cpp
 )
 )

+ 11 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Target/Native/TestImpactNativeTestTarget.cpp

@@ -10,6 +10,12 @@
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
+    namespace SupportedTestFrameworks
+    {
+        //! The CTest label for the GTest framework.
+        inline constexpr const char* GTest = "FRAMEWORK_googletest";
+    } // namespace SupportedTestFrameworks
+
     NativeTestTarget::NativeTestTarget(
     NativeTestTarget::NativeTestTarget(
         TargetDescriptor&& descriptor, NativeTestTargetMeta&& testMetaData)
         TargetDescriptor&& descriptor, NativeTestTargetMeta&& testMetaData)
         : TestTarget(AZStd::move(descriptor), AZStd::move(testMetaData.m_testTargetMeta))
         : TestTarget(AZStd::move(descriptor), AZStd::move(testMetaData.m_testTargetMeta))
@@ -26,4 +32,9 @@ namespace TestImpact
     {
     {
         return m_launchMeta.m_launchMethod;
         return m_launchMeta.m_launchMethod;
     }
     }
+
+    bool NativeTestTarget::CanEnumerate() const
+    {
+        return GetSuiteLabelSet().contains(SupportedTestFrameworks::GTest);
+    }
 } // namespace TestImpact
 } // namespace TestImpact

+ 3 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/Target/Native/TestImpactNativeTestTarget.h

@@ -28,6 +28,9 @@ namespace TestImpact
         //! Returns the test target launch method.
         //! Returns the test target launch method.
         LaunchMethod GetLaunchMethod() const;
         LaunchMethod GetLaunchMethod() const;
 
 
+        // TestTarget overrides ...
+        bool CanEnumerate() const override;
+
     private:
     private:
         NativeTargetLaunchMeta m_launchMeta;
         NativeTargetLaunchMeta m_launchMeta;
     };
     };

+ 94 - 33
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestEngine/Native/TestImpactNativeTestEngine.cpp

@@ -13,9 +13,10 @@
 #include <TestEngine/Native/TestImpactNativeTestEngine.h>
 #include <TestEngine/Native/TestImpactNativeTestEngine.h>
 #include <TestRunner/Native/TestImpactNativeErrorCodeChecker.h>
 #include <TestRunner/Native/TestImpactNativeErrorCodeChecker.h>
 #include <TestRunner/Native/TestImpactNativeTestEnumerator.h>
 #include <TestRunner/Native/TestImpactNativeTestEnumerator.h>
-#include <TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h>
-#include <TestRunner/Native/TestImpactNativeRegularTestRunner.h>
+#include <TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.h>
+#include <TestRunner/Native/TestImpactNativeShardedRegularTestRunner.h>
 #include <TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h>
 #include <TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h>
+#include <TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h>
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
@@ -121,7 +122,7 @@ namespace TestImpact
         using TestEngineJobType = TestEngineEnumeration<NativeTestTarget>;
         using TestEngineJobType = TestEngineEnumeration<NativeTestTarget>;
     };
     };
 
 
-    // Type trait for the test runner
+    // Type trait for the regular test runner
     template<>
     template<>
     struct TestJobRunnerTrait<NativeRegularTestRunner>
     struct TestJobRunnerTrait<NativeRegularTestRunner>
     {
     {
@@ -135,29 +136,66 @@ namespace TestImpact
         using TestEngineJobType = TestEngineInstrumentedRun<NativeTestTarget, TestCoverage>;
         using TestEngineJobType = TestEngineInstrumentedRun<NativeTestTarget, TestCoverage>;
     };
     };
 
 
+    // Type trait for the sharded regular test runner
+    template<>
+    struct TestJobRunnerTrait<NativeShardedRegularTestRunner>
+    {
+        using TestEngineJobType = TestEngineRegularRun<NativeTestTarget>;
+    };
+
+
+    // Type trait for the sharded instrumented test runner
+    template<>
+    struct TestJobRunnerTrait<NativeShardedInstrumentedTestRunner>
+    {
+        using TestEngineJobType = TestEngineInstrumentedRun<NativeTestTarget, TestCoverage>;
+    };
+
     NativeTestEngine::NativeTestEngine(
     NativeTestEngine::NativeTestEngine(
-        const RepoPath& sourceDir,
+        const RepoPath& repoRootDir,
         const RepoPath& targetBinaryDir,
         const RepoPath& targetBinaryDir,
-        [[maybe_unused]]const RepoPath& cacheDir,
         const ArtifactDir& artifactDir,
         const ArtifactDir& artifactDir,
+        const NativeShardedArtifactDir& shardedArtifactDir,
         const RepoPath& testRunnerBinary,
         const RepoPath& testRunnerBinary,
         const RepoPath& instrumentBinary,
         const RepoPath& instrumentBinary,
         size_t maxConcurrentRuns)
         size_t maxConcurrentRuns)
-        : m_regularTestJobInfoGenerator(AZStd::make_unique<NativeRegularTestRunJobInfoGenerator>(
-            sourceDir,
+        : m_enumerationTestJobInfoGenerator(AZStd::make_unique<NativeTestEnumerationJobInfoGenerator>(
+            targetBinaryDir,
+            artifactDir,
+            testRunnerBinary))
+        , m_regularTestJobInfoGenerator(AZStd::make_unique<NativeRegularTestRunJobInfoGenerator>(
+            repoRootDir,
             targetBinaryDir,
             targetBinaryDir,
             artifactDir,
             artifactDir,
             testRunnerBinary))
             testRunnerBinary))
         , m_instrumentedTestJobInfoGenerator(AZStd::make_unique<NativeInstrumentedTestRunJobInfoGenerator>(
         , m_instrumentedTestJobInfoGenerator(AZStd::make_unique<NativeInstrumentedTestRunJobInfoGenerator>(
-            sourceDir,
+            repoRootDir,
             targetBinaryDir,
             targetBinaryDir,
             artifactDir,
             artifactDir,
             testRunnerBinary,
             testRunnerBinary,
             instrumentBinary))
             instrumentBinary))
+        , m_shardedRegularTestJobInfoGenerator(AZStd::make_unique<NativeShardedRegularTestRunJobInfoGenerator>(
+            *m_regularTestJobInfoGenerator.get(),
+            maxConcurrentRuns,
+            repoRootDir,
+            targetBinaryDir,
+            shardedArtifactDir,
+            testRunnerBinary))
+        , m_shardedInstrumentedTestJobInfoGenerator(AZStd::make_unique<NativeShardedInstrumentedTestRunJobInfoGenerator>(
+            *m_instrumentedTestJobInfoGenerator.get(),
+            maxConcurrentRuns,
+            repoRootDir,
+            targetBinaryDir,
+            shardedArtifactDir,
+            testRunnerBinary,
+            instrumentBinary))
         , m_testEnumerator(AZStd::make_unique<NativeTestEnumerator>(maxConcurrentRuns))
         , m_testEnumerator(AZStd::make_unique<NativeTestEnumerator>(maxConcurrentRuns))
         , m_instrumentedTestRunner(AZStd::make_unique<NativeInstrumentedTestRunner>(maxConcurrentRuns))
         , m_instrumentedTestRunner(AZStd::make_unique<NativeInstrumentedTestRunner>(maxConcurrentRuns))
         , m_testRunner(AZStd::make_unique<NativeRegularTestRunner>(maxConcurrentRuns))
         , m_testRunner(AZStd::make_unique<NativeRegularTestRunner>(maxConcurrentRuns))
+        , m_shardedInstrumentedTestRunner(AZStd::make_unique<NativeShardedInstrumentedTestRunner>(*m_instrumentedTestRunner.get(), repoRootDir, artifactDir))
+        , m_shardedTestRunner(AZStd::make_unique<NativeShardedRegularTestRunner>(*m_testRunner.get(), repoRootDir, artifactDir))
         , m_artifactDir(artifactDir)
         , m_artifactDir(artifactDir)
+        , m_shardedArtifactDir(shardedArtifactDir)
     {
     {
     }
     }
 
 
@@ -167,6 +205,8 @@ namespace TestImpact
     {
     {
         DeleteFiles(m_artifactDir.m_testRunArtifactDirectory, "*.xml");
         DeleteFiles(m_artifactDir.m_testRunArtifactDirectory, "*.xml");
         DeleteFiles(m_artifactDir.m_coverageArtifactDirectory, "*.xml");
         DeleteFiles(m_artifactDir.m_coverageArtifactDirectory, "*.xml");
+        DeleteFiles(m_shardedArtifactDir.m_shardedTestRunArtifactDirectory, "*.xml");
+        DeleteFiles(m_shardedArtifactDir.m_shardedCoverageArtifactDirectory, "*.xml");
     }
     }
 
 
     TestEngineRegularRunResult<NativeTestTarget> NativeTestEngine::RegularRun(
     TestEngineRegularRunResult<NativeTestTarget> NativeTestEngine::RegularRun(
@@ -179,11 +219,12 @@ namespace TestImpact
     {
     {
         DeleteXmlArtifacts();
         DeleteXmlArtifacts();
 
 
-        const auto jobInfos = m_regularTestJobInfoGenerator->GenerateJobInfos(testTargets);
+        const auto shardedJobInfos =
+            m_shardedRegularTestJobInfoGenerator->GenerateJobInfos(GenerateTestTargetAndEnumerations(testTargets));
 
 
         return RunTests(
         return RunTests(
-            m_testRunner.get(),
-            jobInfos,
+            m_shardedTestRunner.get(),
+            shardedJobInfos,
             testTargets,
             testTargets,
             NativeRegularTestRunnerErrorCodeChecker,
             NativeRegularTestRunnerErrorCodeChecker,
             executionFailurePolicy,
             executionFailurePolicy,
@@ -204,30 +245,50 @@ namespace TestImpact
     {
     {
         DeleteXmlArtifacts();
         DeleteXmlArtifacts();
 
 
-        const auto jobInfos = m_instrumentedTestJobInfoGenerator->GenerateJobInfos(testTargets);
-
+        const auto shardedJobInfos =
+            m_shardedInstrumentedTestJobInfoGenerator->GenerateJobInfos(GenerateTestTargetAndEnumerations(testTargets));
+        
         const auto result = RunTests(
         const auto result = RunTests(
-                m_instrumentedTestRunner.get(),
-                jobInfos,
-                testTargets,
-                NativeInstrumentedTestRunnerErrorCodeChecker,
-                executionFailurePolicy,
-                testFailurePolicy,
-                targetOutputCapture,
-                testTargetTimeout,
-                globalTimeout);
-
-            if(const auto integrityErrors = GenerateIntegrityErrorString(result);
-                !integrityErrors.empty())
-            {
-                AZ_TestImpact_Eval(
-                        integrityFailurePolicy != Policy::IntegrityFailure::Abort,
-                        TestEngineException,
-                        integrityErrors);
+            m_shardedInstrumentedTestRunner.get(),
+            shardedJobInfos,
+            testTargets,
+            NativeInstrumentedTestRunnerErrorCodeChecker,
+            executionFailurePolicy,
+            testFailurePolicy,
+            targetOutputCapture,
+            testTargetTimeout,
+            globalTimeout);
+        
+        if (const auto integrityErrors = GenerateIntegrityErrorString(result); !integrityErrors.empty())
+        {
+            AZ_TestImpact_Eval(integrityFailurePolicy != Policy::IntegrityFailure::Abort, TestEngineException, integrityErrors);
+        
+            AZ_Error("InstrumentedRun", false, integrityErrors.c_str());
+        }
 
 
-                AZ_Error("InstrumentedRun", false, integrityErrors.c_str());
-            }
+        return result;
+    }
 
 
-            return result;
+    AZStd::vector<TestTargetAndEnumeration> NativeTestEngine::GenerateTestTargetAndEnumerations(
+        const AZStd::vector<const NativeTestTarget*>& testTargets) const
+    {
+        const auto enumerationJobInfos = m_enumerationTestJobInfoGenerator->GenerateJobInfos(testTargets);
+        auto [enumerationResult, enumerations] =
+            m_testEnumerator->Enumerate(enumerationJobInfos, StdOutputRouting::None, StdErrorRouting::None, AZStd::nullopt, AZStd::nullopt);
+
+        if (enumerationResult != ProcessSchedulerResult::Graceful)
+        {
+            return {};
+        }
+
+        AZStd::vector<TestTargetAndEnumeration> testTargetsAndEnumerations;
+        testTargetsAndEnumerations.reserve(enumerations.size());
+        for (auto&& enumeration : enumerations)
+        {
+            testTargetsAndEnumerations.emplace_back(
+                testTargets[enumeration.GetJobInfo().GetId().m_value], AZStd::move(enumeration.ReleasePayload()));
+        }
+
+        return testTargetsAndEnumerations;
     }
     }
 } // namespace TestImpact
 } // namespace TestImpact

+ 18 - 5
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestEngine/Native/TestImpactNativeTestEngine.h

@@ -23,29 +23,34 @@
 namespace TestImpact
 namespace TestImpact
 {
 {
     class NativeTestTarget;
     class NativeTestTarget;
+    class NativeTestEnumerationJobInfoGenerator;
     class NativeRegularTestRunJobInfoGenerator;
     class NativeRegularTestRunJobInfoGenerator;
     class NativeInstrumentedTestRunJobInfoGenerator;
     class NativeInstrumentedTestRunJobInfoGenerator;
+    class NativeShardedRegularTestRunJobInfoGenerator;
+    class NativeShardedInstrumentedTestRunJobInfoGenerator;
     class NativeTestEnumerator;
     class NativeTestEnumerator;
     class NativeInstrumentedTestRunner;
     class NativeInstrumentedTestRunner;
     class NativeRegularTestRunner;
     class NativeRegularTestRunner;
+    class NativeShardedInstrumentedTestRunner;
+    class NativeShardedRegularTestRunner;
 
 
     //! Provides the front end for performing test enumerations and test runs.
     //! Provides the front end for performing test enumerations and test runs.
     class NativeTestEngine
     class NativeTestEngine
     {
     {
     public:
     public:
         //! Configures the test engine with the necessary path information for launching test targets and managing the artifacts they produce.
         //! 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 repoRootDir Root path where source files are found (including subfolders).
         //! @param targetBinaryDir Path to where the test target binaries are found.
         //! @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 artifactDir Path to the transient directory where test artifacts are produced.
+        //! @param shardedArtifactDir Path to the transient directory where sharded test artifacts are produced.
         //! @param testRunnerBinary Path to the binary responsible for launching test targets that have the TestRunner launch method.
         //! @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 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.
         //! @param maxConcurrentRuns The maximum number of concurrent test targets that can be in flight at any given moment.
         NativeTestEngine(
         NativeTestEngine(
-            const RepoPath& sourceDir,
+            const RepoPath& repoRootDir,
             const RepoPath& targetBinaryDir,
             const RepoPath& targetBinaryDir,
-            const RepoPath& cacheDir,
             const ArtifactDir& artifactDir,
             const ArtifactDir& artifactDir,
+            const NativeShardedArtifactDir& shardedArtifactDir,
             const RepoPath& testRunnerBinary,
             const RepoPath& testRunnerBinary,
             const RepoPath& instrumentBinary,
             const RepoPath& instrumentBinary,
             size_t maxConcurrentRuns);
             size_t maxConcurrentRuns);
@@ -72,7 +77,6 @@ namespace TestImpact
 
 
         //! Performs a test run without any instrumentation and, for each test target, returns the test run results and metrics about the run.
         //! 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 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 executionFailurePolicy Policy for how test execution failures should be handled.
         //! @param testFailurePolicy Policy for how test targets with failing tests 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 targetOutputCapture Policy for how test target standard output should be captured and handled.
@@ -111,11 +115,20 @@ namespace TestImpact
         //! Cleans up the artifacts directory of any artifacts from previous runs.
         //! Cleans up the artifacts directory of any artifacts from previous runs.
         void DeleteXmlArtifacts() const;
         void DeleteXmlArtifacts() const;
 
 
+        //! Helper function to generate the test target and enumeration pairs for a given set of test targets.
+        AZStd::vector<TestTargetAndEnumeration> GenerateTestTargetAndEnumerations(const AZStd::vector<const NativeTestTarget*>& testTargets) const;
+
+        AZStd::unique_ptr<NativeTestEnumerationJobInfoGenerator> m_enumerationTestJobInfoGenerator;
         AZStd::unique_ptr<NativeRegularTestRunJobInfoGenerator> m_regularTestJobInfoGenerator;
         AZStd::unique_ptr<NativeRegularTestRunJobInfoGenerator> m_regularTestJobInfoGenerator;
         AZStd::unique_ptr<NativeInstrumentedTestRunJobInfoGenerator> m_instrumentedTestJobInfoGenerator;
         AZStd::unique_ptr<NativeInstrumentedTestRunJobInfoGenerator> m_instrumentedTestJobInfoGenerator;
+        AZStd::unique_ptr<NativeShardedRegularTestRunJobInfoGenerator> m_shardedRegularTestJobInfoGenerator;
+        AZStd::unique_ptr<NativeShardedInstrumentedTestRunJobInfoGenerator> m_shardedInstrumentedTestJobInfoGenerator;
         AZStd::unique_ptr<NativeTestEnumerator> m_testEnumerator;
         AZStd::unique_ptr<NativeTestEnumerator> m_testEnumerator;
         AZStd::unique_ptr<NativeInstrumentedTestRunner> m_instrumentedTestRunner;
         AZStd::unique_ptr<NativeInstrumentedTestRunner> m_instrumentedTestRunner;
         AZStd::unique_ptr<NativeRegularTestRunner> m_testRunner;
         AZStd::unique_ptr<NativeRegularTestRunner> m_testRunner;
+        AZStd::unique_ptr<NativeShardedInstrumentedTestRunner> m_shardedInstrumentedTestRunner;
+        AZStd::unique_ptr<NativeShardedRegularTestRunner> m_shardedTestRunner;
         ArtifactDir m_artifactDir;
         ArtifactDir m_artifactDir;
+        NativeShardedArtifactDir m_shardedArtifactDir;
     };
     };
 } // namespace TestImpact
 } // namespace TestImpact

+ 4 - 5
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestImpactNativeRuntime.cpp

@@ -26,7 +26,9 @@
 namespace TestImpact
 namespace TestImpact
 {
 {
     NativeTestTargetMetaMap ReadNativeTestTargetMetaMapFile(
     NativeTestTargetMetaMap ReadNativeTestTargetMetaMapFile(
-        const SuiteSet& suiteSet, const SuiteLabelExcludeSet& suiteLabelExcludeSet, const RepoPath& testTargetMetaConfigFile)
+        const SuiteSet& suiteSet,
+        const SuiteLabelExcludeSet& suiteLabelExcludeSet,
+        const RepoPath& testTargetMetaConfigFile)
     {
     {
         const auto masterTestListData = ReadFileContents<RuntimeException>(testTargetMetaConfigFile);
         const auto masterTestListData = ReadFileContents<RuntimeException>(testTargetMetaConfigFile);
         return NativeTestTargetMetaMapFactory(masterTestListData, suiteSet, suiteLabelExcludeSet);
         return NativeTestTargetMetaMapFactory(masterTestListData, suiteSet, suiteLabelExcludeSet);
@@ -43,7 +45,6 @@ namespace TestImpact
         Policy::FailedTestCoverage failedTestCoveragePolicy,
         Policy::FailedTestCoverage failedTestCoveragePolicy,
         Policy::TestFailure testFailurePolicy,
         Policy::TestFailure testFailurePolicy,
         Policy::IntegrityFailure integrationFailurePolicy,
         Policy::IntegrityFailure integrationFailurePolicy,
-        Policy::TestSharding testShardingPolicy,
         Policy::TargetOutputCapture targetOutputCapture,
         Policy::TargetOutputCapture targetOutputCapture,
         AZStd::optional<size_t> maxConcurrency)
         AZStd::optional<size_t> maxConcurrency)
         : m_config(AZStd::move(config))
         : m_config(AZStd::move(config))
@@ -53,7 +54,6 @@ namespace TestImpact
         , m_failedTestCoveragePolicy(failedTestCoveragePolicy)
         , m_failedTestCoveragePolicy(failedTestCoveragePolicy)
         , m_testFailurePolicy(testFailurePolicy)
         , m_testFailurePolicy(testFailurePolicy)
         , m_integrationFailurePolicy(integrationFailurePolicy)
         , m_integrationFailurePolicy(integrationFailurePolicy)
-        , m_testShardingPolicy(testShardingPolicy)
         , m_targetOutputCapture(targetOutputCapture)
         , m_targetOutputCapture(targetOutputCapture)
         , m_maxConcurrency(maxConcurrency.value_or(AZStd::thread::hardware_concurrency()))
         , m_maxConcurrency(maxConcurrency.value_or(AZStd::thread::hardware_concurrency()))
     {
     {
@@ -96,8 +96,8 @@ namespace TestImpact
         m_testEngine = AZStd::make_unique<TestEngine>(
         m_testEngine = AZStd::make_unique<TestEngine>(
             m_config.m_commonConfig.m_repo.m_root,
             m_config.m_commonConfig.m_repo.m_root,
             m_config.m_target.m_outputDirectory,
             m_config.m_target.m_outputDirectory,
-            m_config.m_workspace.m_temp.m_enumerationCacheDirectory,
             m_config.m_workspace.m_temp,
             m_config.m_workspace.m_temp,
+            m_config.m_shardedArtifactDir,
             m_config.m_testEngine.m_testRunner.m_binary,
             m_config.m_testEngine.m_testRunner.m_binary,
             m_config.m_testEngine.m_instrumentation.m_binary,
             m_config.m_testEngine.m_instrumentation.m_binary,
             m_maxConcurrency);
             m_maxConcurrency);
@@ -200,7 +200,6 @@ namespace TestImpact
         policyState.m_integrityFailurePolicy = m_integrationFailurePolicy;
         policyState.m_integrityFailurePolicy = m_integrationFailurePolicy;
         policyState.m_targetOutputCapture = m_targetOutputCapture;
         policyState.m_targetOutputCapture = m_targetOutputCapture;
         policyState.m_testFailurePolicy = m_testFailurePolicy;
         policyState.m_testFailurePolicy = m_testFailurePolicy;
-        policyState.m_testShardingPolicy = m_testShardingPolicy;
 
 
         return policyState;
         return policyState;
     }
     }

+ 127 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestImpactNativeRuntimeConfigurationFactory.cpp

@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TestImpactFramework/TestImpactConfigurationException.h>
+#include <TestImpactFramework/TestImpactUtils.h>
+#include <TestImpactFramework/Native/TestImpactNativeConfiguration.h>
+
+#include <TestImpactRuntimeConfigurationFactory.h>
+
+#include <AzCore/std/functional.h>
+#include <AzCore/std/optional.h>
+
+namespace TestImpact
+{
+    namespace NativeConfigFactory
+    {
+        // Keys for pertinent JSON elements
+        constexpr const char* Keys[] =
+        {
+            "native",
+            "test_engine",
+            "target",
+            "test_runner",
+            "bin",
+            "instrumentation",
+            "dir",
+            "exclude",
+            "regular",
+            "instrumented",
+            "target",
+            "workspace",
+            "temp",
+            "sharded_run_artifact_dir",
+            "sharded_coverage_artifact_dir"
+        };
+
+        enum Fields
+        {
+            Native,
+            TestEngine,
+            TargetConfig,
+            TestRunner,
+            BinaryFile,
+            TestInstrumentation,
+            Directory,
+            TargetExclude,
+            RegularTargetExcludeFilter,
+            InstrumentedTargetExcludeFilter,
+            TargetName,
+            Workspace,
+            TempWorkspace,
+            ShardedRunArtifactDir,
+            ShardedCoverageArtifactDir,
+            // Checksum
+            _CHECKSUM_
+        };
+    } // namespace NativeConfigFactory
+
+    NativeTestEngineConfig ParseTestEngineConfig(const rapidjson::Value& testEngine)
+    {
+        NativeTestEngineConfig testEngineConfig;
+        testEngineConfig.m_testRunner.m_binary =
+            testEngine[NativeConfigFactory::Keys[NativeConfigFactory::Fields::TestRunner]]
+                [NativeConfigFactory::Keys[NativeConfigFactory::Fields::BinaryFile]].GetString();
+        testEngineConfig.m_instrumentation.m_binary =
+            testEngine[NativeConfigFactory::Keys[NativeConfigFactory::Fields::TestInstrumentation]]
+                [NativeConfigFactory::Keys[NativeConfigFactory::Fields::BinaryFile]].GetString();
+        return testEngineConfig;
+    }
+
+    NativeTargetConfig ParseTargetConfig(const rapidjson::Value& target)
+    {
+        NativeTargetConfig targetConfig;
+        targetConfig.m_outputDirectory = target[NativeConfigFactory::Keys[NativeConfigFactory::Fields::Directory]].GetString();
+        const auto& testExcludes = target[NativeConfigFactory::Keys[NativeConfigFactory::Fields::TargetExclude]];
+        targetConfig.m_excludedTargets.m_excludedRegularTestTargets =
+            ParseTargetExcludeList(testExcludes[NativeConfigFactory::Keys[NativeConfigFactory::Fields::RegularTargetExcludeFilter]].GetArray());
+        targetConfig.m_excludedTargets.m_excludedInstrumentedTestTargets =
+            ParseTargetExcludeList(testExcludes[NativeConfigFactory::Keys[NativeConfigFactory::Fields::InstrumentedTargetExcludeFilter]].GetArray());
+
+        return targetConfig;
+    }
+
+    NativeShardedArtifactDir ParseShardedArtifactConfig(const rapidjson::Value& tempWorkspace)
+    {
+        NativeShardedArtifactDir shardedWorkspaceConfig;
+        shardedWorkspaceConfig.m_shardedTestRunArtifactDirectory =
+            tempWorkspace[NativeConfigFactory::Keys[NativeConfigFactory::Fields::ShardedRunArtifactDir]].GetString();
+        shardedWorkspaceConfig.m_shardedCoverageArtifactDirectory =
+            tempWorkspace[NativeConfigFactory::Keys[NativeConfigFactory::Fields::ShardedCoverageArtifactDir]].GetString();
+        return shardedWorkspaceConfig;
+    }
+
+    NativeRuntimeConfig NativeRuntimeConfigurationFactory(const AZStd::string& configurationData)
+    {
+        static_assert(NativeConfigFactory::Fields::_CHECKSUM_ == AZStd::size(NativeConfigFactory::Keys));
+        rapidjson::Document configurationFile;
+
+        if (configurationFile.Parse(configurationData.c_str()).HasParseError())
+        {
+            throw TestImpact::ConfigurationException("Could not parse runtimeConfig data, JSON has errors");
+        }
+
+        NativeRuntimeConfig runtimeConfig;
+        runtimeConfig.m_commonConfig = RuntimeConfigurationFactory(configurationData);
+        runtimeConfig.m_workspace =
+            ParseWorkspaceConfig(configurationFile[NativeConfigFactory::Keys[NativeConfigFactory::Fields::Native]]
+                [NativeConfigFactory::Keys[NativeConfigFactory::Fields::Workspace]]);
+        runtimeConfig.m_shardedArtifactDir = ParseShardedArtifactConfig(
+            configurationFile[NativeConfigFactory::Keys[NativeConfigFactory::Fields::Native]]
+                [NativeConfigFactory::Keys[NativeConfigFactory::Fields::Workspace]]
+                    [NativeConfigFactory::Keys[NativeConfigFactory::Fields::TempWorkspace]]);
+        runtimeConfig.m_testEngine =
+            ParseTestEngineConfig(configurationFile[NativeConfigFactory::Keys[NativeConfigFactory::Fields::Native]]
+                [NativeConfigFactory::Keys[NativeConfigFactory::Fields::TestEngine]]);
+        runtimeConfig.m_target =
+            ParseTargetConfig(configurationFile[NativeConfigFactory::Keys[NativeConfigFactory::Fields::Native]]
+                [NativeConfigFactory::Keys[NativeConfigFactory::Fields::TargetConfig]]);
+
+        return runtimeConfig;
+    }
+} // namespace TestImpact

+ 106 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.cpp

@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <TestImpactFramework/TestImpactTestSequence.h>
+
+#include <TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h>
+
+#include <AzCore/std/string/string_view.h>
+
+namespace TestImpact
+{
+    NativeShardedInstrumentedTestRunJobInfoGenerator::NativeShardedInstrumentedTestRunJobInfoGenerator(
+        const JobInfoGenerator& jobInfoGenerator,
+        size_t maxConcurrency,
+        const RepoPath& sourceDir,
+        const RepoPath& targetBinaryDir,
+        const NativeShardedArtifactDir& artifactDir,
+        const RepoPath& testRunnerBinary,
+        const RepoPath& instrumentBinary,
+        CoverageLevel coverageLevel)
+        : NativeShardedTestRunJobInfoGeneratorBase<NativeInstrumentedTestRunner>(
+            jobInfoGenerator,
+            maxConcurrency,
+            sourceDir,
+            targetBinaryDir,
+            artifactDir,
+            testRunnerBinary)
+        , m_instrumentBinary(instrumentBinary)
+        , m_coverageLevel(coverageLevel)
+    {
+    }
+
+    RepoPath NativeShardedInstrumentedTestRunJobInfoGenerator::GenerateShardedTargetCoverageArtifactFilePath(
+        const NativeTestTarget* testTarget, size_t shardNumber) const
+    {
+        auto artifactFilePath = GenerateTargetCoverageArtifactFilePath(testTarget, m_artifactDir.m_shardedCoverageArtifactDirectory);
+        return artifactFilePath.ReplaceExtension(
+            AZStd::string_view(AZStd::string::format("%zu%.*s", shardNumber, AZ_STRING_ARG(artifactFilePath.Extension().Native()))));
+    }
+
+    ShardedInstrumentedTestJobInfo NativeShardedInstrumentedTestRunJobInfoGenerator::GenerateJobInfoImpl(
+        const TestTargetAndEnumeration& testTargetAndEnumeration,
+        typename NativeInstrumentedTestRunner::JobInfo::Id startingId) const
+    {
+        const auto [testTarget, testEnumeration] = testTargetAndEnumeration;
+        const auto testFilters = TestListsToTestFilters(ShardTestInterleaved(testTargetAndEnumeration));
+        typename ShardedInstrumentedTestJobInfo::JobInfos jobInfos;
+        jobInfos.reserve(testFilters.size());
+
+        for (size_t i = 0; i < testFilters.size(); i++)
+        {
+            const auto shardedRunArtifact = GenerateShardedTargetRunArtifactFilePath(testTarget, i);
+            const auto shardedCoverageArtifact = GenerateShardedTargetCoverageArtifactFilePath(testTarget, i);
+            const RepoPath shardAdditionalArgsFile = GenerateShardedAdditionalArgsFilePath(testTarget, i);
+            const auto shardLaunchCommand = GenerateShardedLaunchCommand(testTarget, shardAdditionalArgsFile);
+            WriteFileContents<TestRunnerException>(testFilters[i], shardAdditionalArgsFile);
+
+            const auto command = GenerateInstrumentedTestJobInfoCommand(
+                m_instrumentBinary,
+                shardedCoverageArtifact,
+                m_coverageLevel,
+                m_targetBinaryDir,
+                m_testRunnerBinary,
+                m_sourceDir,
+                GenerateRegularTestJobInfoCommand(shardLaunchCommand, shardedRunArtifact));
+
+            jobInfos.emplace_back(
+                NativeInstrumentedTestRunner::JobInfo::Id{ startingId.m_value + i },
+                command,
+                NativeInstrumentedTestRunner::JobData(testTarget->GetLaunchMethod(), shardedRunArtifact, shardedCoverageArtifact));
+        }
+
+        return ShardedInstrumentedTestJobInfo(testTarget, AZStd::move(jobInfos));
+    }
+
+    ShardedRegularTestJobInfo NativeShardedRegularTestRunJobInfoGenerator::GenerateJobInfoImpl(
+        const TestTargetAndEnumeration& testTargetAndEnumeration,
+        typename NativeRegularTestRunner::JobInfo::Id startingId) const
+    {
+        const auto [testTarget, testEnumeration] = testTargetAndEnumeration;
+        const auto testFilters = TestListsToTestFilters(ShardTestInterleaved(testTargetAndEnumeration));
+        typename ShardedRegularTestJobInfo::JobInfos jobInfos;
+        jobInfos.reserve(testFilters.size());
+
+        for (size_t i = 0; i < testFilters.size(); i++)
+        {
+            const auto shardedRunArtifact = GenerateShardedTargetRunArtifactFilePath(testTarget,  i);
+            const RepoPath shardAdditionalArgsFile = GenerateShardedAdditionalArgsFilePath(testTarget, i);
+            const auto shardLaunchCommand = GenerateShardedLaunchCommand(testTarget, shardAdditionalArgsFile);
+            WriteFileContents<TestRunnerException>(testFilters[i], shardAdditionalArgsFile);
+            const auto command = GenerateRegularTestJobInfoCommand(shardLaunchCommand, shardedRunArtifact);
+
+            jobInfos.emplace_back(
+                NativeRegularTestRunner::JobInfo::Id{ startingId.m_value + i },
+                command,
+                NativeRegularTestRunner::JobData(testTarget->GetLaunchMethod(), shardedRunArtifact));
+        }
+
+        return ShardedRegularTestJobInfo(testTarget, AZStd::move(jobInfos));
+    }
+} // namespace TestImpact

+ 332 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h

@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestImpactFramework/Native/TestImpactNativeConfiguration.h>
+
+#include <Target/Native/TestImpactNativeTestTarget.h>
+#include <TestEngine/Common/Enumeration/TestImpactTestEngineEnumeration.h>
+#include <TestRunner/Common/Job/TestImpactTestJobInfoUtils.h>
+#include <TestRunner/Native/TestImpactNativeRegularTestRunner.h>
+#include <TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h>
+#include <TestRunner/Native/TestImpactNativeRegularTestRunner.h>
+#include <TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h>
+#include <TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h>
+
+namespace TestImpact
+{
+    //! Job info for all shards of a given test target.
+    template<typename TestJobRunner>
+    class ShardedTestJobInfo
+    {
+    public:
+        using Id = typename TestJobRunner::JobInfo::Id;
+        using IdType = typename TestJobRunner::JobInfo::IdType;
+        using JobInfos = typename TestJobRunner::JobInfos;
+
+        ShardedTestJobInfo(const NativeTestTarget* testTarget, JobInfos&& jobInfos)
+            : m_testTarget(testTarget)
+            , m_jobInfos(AZStd::move(jobInfos))
+        {
+            AZ_TestImpact_Eval(!m_jobInfos.empty(), TestRunnerException, "Attepted to instantiate a sharded test job info no sub job infos");
+        }
+
+        //! Returns the id of the job info (the first shard is considered the "parent" id of all shards).
+        Id GetId() const
+        {
+            return m_jobInfos.front().GetId();
+        }
+
+        //! Returns the test target that is sharded.
+        const NativeTestTarget* GetTestTarget() const
+        {
+            return m_testTarget;
+        }
+
+        // Returns the job infos of each shard for the test target.
+        const JobInfos& GetJobInfos() const
+        {
+            return m_jobInfos;
+        }
+
+    private:
+        const NativeTestTarget* m_testTarget = nullptr; //!< The sharded test target.
+        JobInfos m_jobInfos; //! The job infos for each shard of the test target.
+    };
+
+    //! Type alias for the instrumented test runner.
+    using ShardedInstrumentedTestJobInfo = ShardedTestJobInfo<NativeInstrumentedTestRunner>;
+
+    //! Type alias for the regular test runner.
+    using ShardedRegularTestJobInfo = ShardedTestJobInfo<NativeRegularTestRunner>;
+
+    //! Helper pair for a test target and its enumeration (if any).
+    using TestTargetAndEnumeration = AZStd::pair<const NativeTestTarget*, AZStd::optional<TestEnumeration>>;
+
+    //! Base class for the regular and instrumented sharded job info generators.
+    template<typename TestJobRunner>
+    class NativeShardedTestRunJobInfoGeneratorBase
+    {
+    public:
+        using JobInfoGenerator = typename TestJobRunner::JobInfoGenerator;
+
+        NativeShardedTestRunJobInfoGeneratorBase(
+            const JobInfoGenerator& jobInfoGenerator,
+            size_t maxConcurrency,
+            const RepoPath& sourceDir,
+            const RepoPath& targetBinaryDir,
+            const NativeShardedArtifactDir& artifactDir,
+            const RepoPath& testRunnerBinary);
+
+        virtual ~NativeShardedTestRunJobInfoGeneratorBase() = default;
+
+        //! Generates the sharded job info for a given test target based on its enumerated tests.
+        //! @note If no enumeration is provided, the standard job info generator for the underlying test runner will be used.
+        ShardedTestJobInfo<TestJobRunner> GenerateJobInfo(
+            const TestTargetAndEnumeration& testTargetAndEnumeration,
+            typename TestJobRunner::JobInfo::Id startingId);
+
+        //! Generates the sharded job infos for a set of test target based on their enumerated tests.
+        AZStd::vector<ShardedTestJobInfo<TestJobRunner>> GenerateJobInfos(
+            const AZStd::vector<TestTargetAndEnumeration>& testTargetsAndEnumerations);
+
+    protected:
+        //! The interleaved tests for a given set of shards.
+        using ShardedTestsList = AZStd::vector<AZStd::vector<AZStd::string>>;
+
+        //! The test framework specific string to filter the tests for a given shard.
+        using ShardedTestsFilter = AZStd::vector<AZStd::string>;
+
+        //! The specialized (instrumented or regular test runner) job generator.
+        virtual ShardedTestJobInfo<TestJobRunner> GenerateJobInfoImpl(
+            const TestTargetAndEnumeration& testTargetAndEnumeration, typename TestJobRunner::JobInfo::Id startingId) const = 0;
+
+        //! Interleaves the enumerated tests across the shards.
+        ShardedTestsList ShardTestInterleaved(const TestTargetAndEnumeration& testTargetAndEnumeration) const;
+
+        //! Converts the raw shard test lists to the test framework specific filters.
+        ShardedTestsFilter TestListsToTestFilters(const ShardedTestsList& shardedTestList) const;
+
+        //! Generates a sharded run artifact file path for a given test target's shard.
+        RepoPath GenerateShardedTargetRunArtifactFilePath(const NativeTestTarget* testTarget, size_t shardNumber) const;
+
+        //! Generates a sharded AzTestRunner additional arguments file path for a given test target's shard.
+        RepoPath GenerateShardedAdditionalArgsFilePath(const NativeTestTarget* testTarget, size_t shardNumber) const;
+
+        //! Generates the launch command for a given test target's shard.
+        AZStd::string GenerateShardedLaunchCommand(
+            const NativeTestTarget* testTarget, const RepoPath& shardAdditionalArgsFile) const;
+
+        const JobInfoGenerator* m_jobInfoGenerator = nullptr; //!< The standard job info generator to use if a target cannot be sharded.
+        size_t m_maxConcurrency; //!< The maximum number of shards to generate for a test target.
+        RepoPath m_sourceDir; //!< Path to the repository.
+        RepoPath m_targetBinaryDir; //!< Path to the test target binaries.
+        NativeShardedArtifactDir m_artifactDir; //!< Path to the sharded artifact directories.
+        RepoPath m_testRunnerBinary; //!< Path to the test runner binary.
+    };
+
+    //! Job info generator for the instrumented sharded test runner.
+    class NativeShardedInstrumentedTestRunJobInfoGenerator
+        : public NativeShardedTestRunJobInfoGeneratorBase<NativeInstrumentedTestRunner>
+    {
+    public:
+        NativeShardedInstrumentedTestRunJobInfoGenerator(
+            const JobInfoGenerator& jobInfoGenerator,
+            size_t maxConcurrency,
+            const RepoPath& sourceDir,
+            const RepoPath& targetBinaryDir,
+            const NativeShardedArtifactDir& artifactDir,
+            const RepoPath& testRunnerBinary,
+            const RepoPath& instrumentBinary,
+            CoverageLevel coverageLevel = CoverageLevel::Source);
+
+    protected:
+        // NativeShardedTestRunJobInfoGeneratorBase overrides ...
+        ShardedInstrumentedTestJobInfo GenerateJobInfoImpl(
+            const TestTargetAndEnumeration& testTargetAndEnumeration,
+            typename NativeInstrumentedTestRunner::JobInfo::Id startingId) const override;
+
+    private:
+        //! Generates a sharded coverage artifact file path for a given test target's shard.
+        RepoPath GenerateShardedTargetCoverageArtifactFilePath(const NativeTestTarget* testTarget, size_t shardNumber) const;
+
+        RepoPath m_instrumentBinary; //!< Path to the coverage instrumentation binary.
+        CoverageLevel m_coverageLevel; //!< Coverage level to use for instrumentation.
+    };
+
+    //! Job info generator for the regular sharded test runner.
+    class NativeShardedRegularTestRunJobInfoGenerator
+        :  public NativeShardedTestRunJobInfoGeneratorBase<NativeRegularTestRunner>
+    {
+    public:
+        using NativeShardedTestRunJobInfoGeneratorBase<NativeRegularTestRunner>::NativeShardedTestRunJobInfoGeneratorBase;
+
+    protected:
+        // NativeShardedTestRunJobInfoGeneratorBase overrides ...
+        ShardedRegularTestJobInfo GenerateJobInfoImpl(
+            const TestTargetAndEnumeration& testTargetAndEnumeration,
+            typename NativeRegularTestRunner::JobInfo::Id startingId) const override;
+    };
+
+    template<typename TestJobRunner>
+    NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::NativeShardedTestRunJobInfoGeneratorBase(
+        const JobInfoGenerator& jobInfoGenerator,
+        size_t maxConcurrency,
+        const RepoPath& sourceDir,
+        const RepoPath& targetBinaryDir,
+        const NativeShardedArtifactDir& artifactDir,
+        const RepoPath& testRunnerBinary)
+        : m_jobInfoGenerator(&jobInfoGenerator)
+        , m_maxConcurrency(maxConcurrency)
+        , m_sourceDir(sourceDir)
+        , m_targetBinaryDir(targetBinaryDir)
+        , m_artifactDir(artifactDir)
+        , m_testRunnerBinary(testRunnerBinary)
+    {
+        AZ_TestImpact_Eval(maxConcurrency != 0, TestRunnerException, "Max Number of concurrent processes in flight cannot be 0");
+    }
+
+    template<typename TestJobRunner>
+    ShardedTestJobInfo<TestJobRunner> NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::GenerateJobInfo(
+        const TestTargetAndEnumeration& testTargetAndEnumeration, typename TestJobRunner::JobInfo::Id startingId)
+    {
+        if (const auto [testTarget, testEnumeration] = testTargetAndEnumeration;
+            m_maxConcurrency > 1 && testEnumeration.has_value() && testEnumeration->GetNumEnabledTests() > 1)
+        {
+            return GenerateJobInfoImpl(testTargetAndEnumeration, startingId);
+        }
+        else
+        {
+            // Target cannot be sharded, use the standard job info generator.
+            return { testTarget, { m_jobInfoGenerator->GenerateJobInfo(testTarget, startingId) } };
+        }
+    }
+
+    template<typename TestJobRunner>
+    typename NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::ShardedTestsList NativeShardedTestRunJobInfoGeneratorBase<
+        TestJobRunner>::ShardTestInterleaved(const TestTargetAndEnumeration& testTargetAndEnumeration) const
+    {
+        const auto [testTarget, testEnumeration] = testTargetAndEnumeration;
+        const auto numTests = testEnumeration->GetNumEnabledTests();
+        const auto numShards = std::min(m_maxConcurrency, numTests);
+        ShardedTestsList shardTestList(numShards);
+        const auto testsPerShard = numTests / numShards;
+
+        size_t testIndex = 0;
+        for (const auto fixture : testEnumeration->GetTestSuites())
+        {
+            if (!fixture.m_enabled)
+            {
+                continue;
+            }
+
+            for (const auto test : fixture.m_tests)
+            {
+                if (!test.m_enabled)
+                {
+                    continue;
+                }
+
+                shardTestList[testIndex++ % numShards].emplace_back(
+                    AZStd::string::format("%s.%s", fixture.m_name.c_str(), test.m_name.c_str()));
+            }
+        }
+
+        return shardTestList;
+    }
+
+    template<typename TestJobRunner>
+    auto NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::TestListsToTestFilters(const ShardedTestsList& shardedTestList) const
+        -> ShardedTestsFilter
+    {
+        ShardedTestsFilter shardedTestFilter;
+        shardedTestFilter.reserve(shardedTestList.size());
+
+        for (const auto& shardTests : shardedTestList)
+        {
+            AZStd::string testFilter = "--gtest_filter=";
+            for (const auto& test : shardTests)
+            {
+                // The trailing colon added by the last test is still a valid GTest filter
+                testFilter += AZStd::string::format("%s:", test.c_str());
+            }
+
+            shardedTestFilter.emplace_back(testFilter);
+        }
+
+        return shardedTestFilter;
+    }
+
+    template<typename TestJobRunner>
+    auto NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::GenerateJobInfos(
+        const AZStd::vector<TestTargetAndEnumeration>& testTargetsAndEnumerations)
+        -> AZStd::vector<ShardedTestJobInfo<TestJobRunner>>
+    {
+        AZStd::vector<ShardedTestJobInfo<TestJobRunner>> jobInfos;
+        for (size_t testTargetIndex = 0, jobId = 0; testTargetIndex < testTargetsAndEnumerations.size(); testTargetIndex++)
+        {
+            jobInfos.push_back(GenerateJobInfo(testTargetsAndEnumerations[testTargetIndex], { jobId }));
+
+            // Increment the job id by the number of sharded job infos generated for the most recently added test target so that the next
+            // job id used is contiguous in seqeunce
+            jobId += jobInfos.back().GetJobInfos().size();
+        }
+
+        return jobInfos;
+    }
+
+    template<typename TestJobRunner>
+    RepoPath NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::GenerateShardedTargetRunArtifactFilePath(
+        const NativeTestTarget* testTarget, const size_t shardNumber) const
+    {
+        auto artifactFilePath = GenerateTargetRunArtifactFilePath(testTarget, m_artifactDir.m_shardedTestRunArtifactDirectory);
+        return artifactFilePath.ReplaceExtension(AZStd::string::format("%zu%s", shardNumber, artifactFilePath.Extension().String().c_str()).c_str());
+    }
+
+    template<typename TestJobRunner>
+    AZStd::string NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::GenerateShardedLaunchCommand(
+        const NativeTestTarget* testTarget, const RepoPath& shardAdditionalArgsFile) const
+    {
+        return AZStd::string::format(
+            "%s --args_from_file \"%s\"",
+            GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary).c_str(),
+            shardAdditionalArgsFile.c_str());
+    }
+
+    template<typename TestJobRunner>
+    RepoPath NativeShardedTestRunJobInfoGeneratorBase<TestJobRunner>::GenerateShardedAdditionalArgsFilePath(
+        const NativeTestTarget* testTarget, size_t shardNumber) const
+    {
+        return AZStd::string::format(
+            "%s.%zu.args", (m_artifactDir.m_shardedTestRunArtifactDirectory / RepoPath(testTarget->GetName())).c_str(), shardNumber);
+    }
+} // namespace TestImpact
+
+namespace AZStd
+{
+    //! Less function for TestTargetAndEnumeration types for use in maps and sets.
+    template<>
+    struct less<TestImpact::TestTargetAndEnumeration>
+    {
+        bool operator()(const TestImpact::TestTargetAndEnumeration& lhs, const TestImpact::TestTargetAndEnumeration& rhs) const
+        {
+            return reinterpret_cast<size_t>(lhs.first) < reinterpret_cast<size_t>(rhs.first);
+        }
+    };
+
+    //! Hash function for TestTargetAndEnumeration types for use in unordered maps and sets.
+    template<>
+    struct hash<TestImpact::TestTargetAndEnumeration>
+    {
+        size_t operator()(const TestImpact::TestTargetAndEnumeration& testTargetAndEnumeration) const noexcept
+        {
+            return reinterpret_cast<size_t>(testTargetAndEnumeration.first);
+        }
+    };
+} // namespace AZStd

+ 23 - 59
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.cpp

@@ -14,42 +14,19 @@
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
-    NativeTestEnumerationJobInfoGenerator::NativeTestEnumerationJobInfoGenerator(
-        const RepoPath& targetBinaryDir,
-        const RepoPath& cacheDir,
-        const ArtifactDir& artifactDir,
-        const RepoPath& testRunnerBinary)
-        : m_targetBinaryDir(targetBinaryDir)
-        , m_cacheDir(cacheDir)
-        , m_artifactDir(artifactDir)
-        , m_testRunnerBinary(testRunnerBinary)
-    {
-    }
-
-
-    NativeTestEnumerator::JobInfo NativeTestEnumerationJobInfoGenerator::GenerateJobInfo(
+    NativeTestEnumerator::JobInfo NativeTestEnumerationJobInfoGenerator::GenerateJobInfoImpl(
         const NativeTestTarget* testTarget, NativeTestEnumerator::JobInfo::Id jobId) const
         const NativeTestTarget* testTarget, NativeTestEnumerator::JobInfo::Id jobId) const
     {
     {
-        using Cache = NativeTestEnumerator::JobData::Cache;
-
         const auto enumerationArtifact = GenerateTargetEnumerationArtifactFilePath(testTarget, m_artifactDir.m_enumerationCacheDirectory);
         const auto enumerationArtifact = GenerateTargetEnumerationArtifactFilePath(testTarget, m_artifactDir.m_enumerationCacheDirectory);
-        const Command args = { AZStd::string::format(
-            "%s --gtest_list_tests --gtest_output=xml:\"%s\"",
-            GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary).c_str(), enumerationArtifact.c_str()) };
+        const auto launchArgument = GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary);
+        const auto command = GenerateTestEnumeratorJobInfoCommand(launchArgument, enumerationArtifact);
 
 
         return JobInfo(
         return JobInfo(
-            jobId, args,
-            JobData(enumerationArtifact, Cache{ m_cachePolicy, GenerateTargetEnumerationCacheFilePath(testTarget, m_cacheDir) }));
-    }
-
-    void NativeTestEnumerationJobInfoGenerator::SetCachePolicy(NativeTestEnumerator::JobInfo::CachePolicy cachePolicy)
-    {
-        m_cachePolicy = cachePolicy;
-    }
-
-    NativeTestEnumerator::JobInfo::CachePolicy NativeTestEnumerationJobInfoGenerator::GetCachePolicy() const
-    {
-        return m_cachePolicy;
+            jobId,
+            command,
+            JobData(
+                enumerationArtifact,
+                Cache{ GetCachePolicy(), GenerateTargetEnumerationCacheFilePath(testTarget, m_artifactDir.m_enumerationCacheDirectory) }));
     }
     }
 
 
     NativeRegularTestRunJobInfoGenerator::NativeRegularTestRunJobInfoGenerator(
     NativeRegularTestRunJobInfoGenerator::NativeRegularTestRunJobInfoGenerator(
@@ -64,12 +41,10 @@ namespace TestImpact
     NativeRegularTestRunner::JobInfo NativeRegularTestRunJobInfoGenerator::GenerateJobInfo(
     NativeRegularTestRunner::JobInfo NativeRegularTestRunJobInfoGenerator::GenerateJobInfo(
         const NativeTestTarget* testTarget, NativeRegularTestRunner::JobInfo::Id jobId) const
         const NativeTestTarget* testTarget, NativeRegularTestRunner::JobInfo::Id jobId) const
     {
     {
+        const auto launchArgument = GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary);
         const auto runArtifact = GenerateTargetRunArtifactFilePath(testTarget, m_artifactDir.m_testRunArtifactDirectory);
         const auto runArtifact = GenerateTargetRunArtifactFilePath(testTarget, m_artifactDir.m_testRunArtifactDirectory);
-        const Command args = { AZStd::string::format(
-            "%s --gtest_output=xml:\"%s\"", GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary).c_str(),
-            runArtifact.c_str()) };
-
-        return JobInfo(jobId, args, JobData(testTarget->GetLaunchMethod(), runArtifact));
+        const auto command = GenerateRegularTestJobInfoCommand(launchArgument, runArtifact);
+        return JobInfo(jobId, command, JobData(testTarget->GetLaunchMethod(), runArtifact));
     }
     }
 
 
     NativeInstrumentedTestRunJobInfoGenerator::NativeInstrumentedTestRunJobInfoGenerator(
     NativeInstrumentedTestRunJobInfoGenerator::NativeInstrumentedTestRunJobInfoGenerator(
@@ -91,30 +66,19 @@ namespace TestImpact
     NativeInstrumentedTestRunner::JobInfo NativeInstrumentedTestRunJobInfoGenerator::GenerateJobInfo(
     NativeInstrumentedTestRunner::JobInfo NativeInstrumentedTestRunJobInfoGenerator::GenerateJobInfo(
         const NativeTestTarget* testTarget, NativeInstrumentedTestRunner::JobInfo::Id jobId) const
         const NativeTestTarget* testTarget, NativeInstrumentedTestRunner::JobInfo::Id jobId) const
     {
     {
-        const auto coverageArtifact = GenerateTargetCoverageArtifactFilePath(testTarget, m_artifactDir.m_coverageArtifactDirectory);
+        const auto launchArgument = GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary);
         const auto runArtifact = GenerateTargetRunArtifactFilePath(testTarget, m_artifactDir.m_testRunArtifactDirectory);
         const auto runArtifact = GenerateTargetRunArtifactFilePath(testTarget, m_artifactDir.m_testRunArtifactDirectory);
-        const Command args = {
-            AZStd::string::format(
-                "\"%s\" " // 1. Instrumented test runner
-                "--coverage_level %s " // 2. Coverage level
-                "--export_type cobertura:\"%s\" " // 3. Test coverage artifact path
-                "--modules \"%s\" " // 4. Modules path
-                "--excluded_modules \"%s\" " // 5. Exclude modules
-                "--sources \"%s\" -- " // 6. Sources path
-                "%s " // 7. Launch command
-                "--gtest_output=xml:\"%s\"", // 8. Result artifact
-
-                m_instrumentBinary.c_str(), // 1. Instrumented test runner
-                (m_coverageLevel == CoverageLevel::Line ? "line" : "source"), // 2. Coverage level
-                coverageArtifact.c_str(), // 3. Test coverage artifact path
-                m_targetBinaryDir.c_str(), // 4. Modules path
-                m_testRunnerBinary.c_str(), // 5. Exclude modules
-                m_sourceDir.c_str(), // 6. Sources path
-                GenerateLaunchArgument(testTarget, m_targetBinaryDir, m_testRunnerBinary).c_str(), // 7. Launch command
-                runArtifact.c_str()) // 8. Result artifact
-        };
-
-        return JobInfo(jobId, args, JobData(testTarget->GetLaunchMethod(), runArtifact, coverageArtifact));
+        const auto coverageArtifact = GenerateTargetCoverageArtifactFilePath(testTarget, m_artifactDir.m_coverageArtifactDirectory);
+        const auto command = GenerateInstrumentedTestJobInfoCommand(
+            m_instrumentBinary,
+            coverageArtifact,
+            m_coverageLevel,
+            m_targetBinaryDir,
+            m_testRunnerBinary,
+            m_sourceDir,
+            GenerateRegularTestJobInfoCommand(launchArgument, runArtifact));
+
+        return JobInfo(jobId, command, JobData(testTarget->GetLaunchMethod(), runArtifact, coverageArtifact));
     }
     }
 
 
     void NativeInstrumentedTestRunJobInfoGenerator::SetCoverageLevel(CoverageLevel coverageLevel)
     void NativeInstrumentedTestRunJobInfoGenerator::SetCoverageLevel(CoverageLevel coverageLevel)

+ 16 - 47
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h

@@ -20,46 +20,20 @@ namespace TestImpact
 {
 {
     //! Generates job information for the different test job runner types.
     //! Generates job information for the different test job runner types.
     class NativeTestEnumerationJobInfoGenerator
     class NativeTestEnumerationJobInfoGenerator
-        : public TestJobInfoGenerator<NativeTestEnumerator, NativeTestTarget>
+        : public TestEnumerationJobInfoGeneratorBase<NativeTestEnumerator, NativeTestTarget>
     {
     {
     public:
     public:
-        //! Configures the test job info generator with the necessary path information for launching test targets.
-        //! @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.
-        NativeTestEnumerationJobInfoGenerator(
-            const RepoPath& targetBinaryDir,
-            const RepoPath& cacheDir,
-            const ArtifactDir& artifactDir,
-            const RepoPath& testRunnerBinary);
-
-        //! Generates the information for a test enumeration job.
-        //! @param testTarget The test target to generate the job information for.
-        //! @param jobId The id to assign for this job.
-        NativeTestEnumerator::JobInfo GenerateJobInfo(
-            const NativeTestTarget* testTarget,
-            NativeTestEnumerator::JobInfo::Id jobId) const override;
-
-        //!
-        //! @param cachePolicy The cache policy to use for job generation.
-        void SetCachePolicy(NativeTestEnumerator::JobInfo::CachePolicy cachePolicy);
-
-        //!
-        NativeTestEnumerator::JobInfo::CachePolicy GetCachePolicy() const;
+        using TestEnumerationJobInfoGeneratorBase<NativeTestEnumerator, NativeTestTarget>::TestEnumerationJobInfoGeneratorBase;
 
 
-    private:
-        RepoPath m_targetBinaryDir;
-        RepoPath m_cacheDir;
-        ArtifactDir m_artifactDir;
-        RepoPath m_testRunnerBinary;
-
-        NativeTestEnumerator::JobInfo::CachePolicy m_cachePolicy;
+    protected:
+        // TestEnumerationJobInfoGenerator overrides...
+        NativeTestEnumerator::JobInfo GenerateJobInfoImpl(
+            const NativeTestTarget* testTarget, NativeTestEnumerator::JobInfo::Id jobId) const override;
     };
     };
 
 
     //! Generates job information for the different test job runner types.
     //! Generates job information for the different test job runner types.
     class NativeRegularTestRunJobInfoGenerator
     class NativeRegularTestRunJobInfoGenerator
-        : public TestJobInfoGenerator<NativeRegularTestRunner, NativeTestTarget>
+        : public TestJobInfoGeneratorBase<NativeRegularTestRunner, NativeTestTarget>
     {
     {
     public:
     public:
         //! Configures the test job info generator with the necessary path information for launching test targets.
         //! Configures the test job info generator with the necessary path information for launching test targets.
@@ -70,11 +44,9 @@ namespace TestImpact
         NativeRegularTestRunJobInfoGenerator(
         NativeRegularTestRunJobInfoGenerator(
             const RepoPath& sourceDir, const RepoPath& targetBinaryDir, const ArtifactDir& artifactDir, const RepoPath& testRunnerBinary);
             const RepoPath& sourceDir, const RepoPath& targetBinaryDir, const ArtifactDir& artifactDir, const RepoPath& testRunnerBinary);
 
 
-        //! Generates the information for a test run job.
-        //! @param testTarget The test target to generate the job information for.
-        //! @param jobId The id to assign for this job.
+        // TestJobInfoGeneratorBase overrides...
         NativeRegularTestRunner::JobInfo GenerateJobInfo(
         NativeRegularTestRunner::JobInfo GenerateJobInfo(
-            const NativeTestTarget* testTarget, NativeRegularTestRunner::JobInfo::Id jobId) const;
+            const NativeTestTarget* testTarget, NativeRegularTestRunner::JobInfo::Id jobId) const override;
 
 
     private:
     private:
         RepoPath m_sourceDir;
         RepoPath m_sourceDir;
@@ -85,7 +57,7 @@ namespace TestImpact
 
 
     //! Generates job information for the different test job runner types.
     //! Generates job information for the different test job runner types.
     class NativeInstrumentedTestRunJobInfoGenerator
     class NativeInstrumentedTestRunJobInfoGenerator
-        : public TestJobInfoGenerator<NativeInstrumentedTestRunner, NativeTestTarget>
+        : public TestJobInfoGeneratorBase<NativeInstrumentedTestRunner, NativeTestTarget>
     {
     {
     public:
     public:
         //! Configures the test job info generator with the necessary path information for launching test targets.
         //! Configures the test job info generator with the necessary path information for launching test targets.
@@ -102,23 +74,20 @@ namespace TestImpact
             const RepoPath& instrumentBinary,
             const RepoPath& instrumentBinary,
             CoverageLevel coverageLevel = CoverageLevel::Source);
             CoverageLevel coverageLevel = CoverageLevel::Source);
 
 
-        //! Generates the information for a test run job.
-        //! @param testTarget The test target to generate the job information for.
-        //! @param jobId The id to assign for this job.
-        NativeInstrumentedTestRunner::JobInfo GenerateJobInfo(
-            const NativeTestTarget* testTarget, NativeInstrumentedTestRunner::JobInfo::Id jobId) const;
-
-        //!
+        //! Sets the coverage level of the job info generator.
         //! @param cachePolicy The cache policy to use for job generation.
         //! @param cachePolicy The cache policy to use for job generation.
         void SetCoverageLevel(CoverageLevel coverageLevel);
         void SetCoverageLevel(CoverageLevel coverageLevel);
 
 
-        //!
+        //! Returns the coverage level of the job info generator.
         CoverageLevel GetCoverageLevel() const;
         CoverageLevel GetCoverageLevel() const;
 
 
+        // TestJobInfoGeneratorBase overrides...
+        NativeInstrumentedTestRunner::JobInfo GenerateJobInfo(
+            const NativeTestTarget* testTarget, NativeInstrumentedTestRunner::JobInfo::Id jobId) const override;
+
     private:
     private:
         RepoPath m_sourceDir;
         RepoPath m_sourceDir;
         RepoPath m_targetBinaryDir;
         RepoPath m_targetBinaryDir;
-        RepoPath m_cacheDir;
         ArtifactDir m_artifactDir;
         ArtifactDir m_artifactDir;
         RepoPath m_testRunnerBinary;
         RepoPath m_testRunnerBinary;
         RepoPath m_instrumentBinary;
         RepoPath m_instrumentBinary;

+ 16 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.cpp

@@ -7,6 +7,7 @@
  */
  */
 
 
 #include <Target/Native/TestImpactNativeTestTarget.h>
 #include <Target/Native/TestImpactNativeTestTarget.h>
+#include <TestRunner/Common/Job/TestImpactTestJobInfoUtils.h>
 #include <TestEngine/Native/TestImpactNativeTestTargetExtension.h>
 #include <TestEngine/Native/TestImpactNativeTestTargetExtension.h>
 #include <TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h>
 #include <TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h>
 
 
@@ -31,4 +32,19 @@ namespace TestImpact
                 .c_str();
                 .c_str();
         }
         }
     }
     }
+
+    NativeTestEnumerator::Command GenerateTestEnumeratorJobInfoCommand(
+        const AZStd::string& launchArguement, const RepoPath& runArtifact)
+    {
+        const auto regularCommand = GenerateRegularTestJobInfoCommand(launchArguement, runArtifact);
+        return { AZStd::string::format("%s --gtest_list_tests", regularCommand.m_args.c_str()) };
+    }
+
+    NativeRegularTestRunner::Command GenerateRegularTestJobInfoCommand(
+        const AZStd::string& launchArguement,
+        const RepoPath& runArtifact
+    )
+    {
+        return { AZStd::string::format("%s --gtest_output=xml:\"%s\"", launchArguement.c_str(), runArtifact.c_str()) };
+    }
 } // namespace TestImpact
 } // namespace TestImpact

+ 25 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h

@@ -9,6 +9,11 @@
 #pragma once
 #pragma once
 
 
 #include <TestImpactFramework/TestImpactRepoPath.h>
 #include <TestImpactFramework/TestImpactRepoPath.h>
+#include <TestImpactFramework/TestImpactConfiguration.h>
+
+#include <TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h>
+#include <TestRunner/Native/TestImpactNativeRegularTestRunner.h>
+#include <TestRunner/Native/TestImpactNativeTestEnumerator.h>
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
@@ -17,4 +22,24 @@ namespace TestImpact
     //! Generates the command string to launch the specified test target.
     //! Generates the command string to launch the specified test target.
     AZStd::string GenerateLaunchArgument(
     AZStd::string GenerateLaunchArgument(
         const NativeTestTarget* testTarget, const RepoPath& targetBinaryDir, const RepoPath& testRunnerBinary);
         const NativeTestTarget* testTarget, const RepoPath& targetBinaryDir, const RepoPath& testRunnerBinary);
+
+    //! Generates a test enumeration job command.
+    NativeTestEnumerator::Command GenerateTestEnumeratorJobInfoCommand(
+        const AZStd::string& launchArguement, const RepoPath& runArtifact);
+
+    //! Generates a regular test run job command.
+    NativeRegularTestRunner::Command GenerateRegularTestJobInfoCommand(
+        const AZStd::string& launchArguement,
+        const RepoPath& runArtifact
+    );
+
+    //! Generates an instrumented test run job command.
+    NativeInstrumentedTestRunner::Command GenerateInstrumentedTestJobInfoCommand(
+        const RepoPath& instrumentBindaryPath,
+        const RepoPath& coverageArtifactPath,
+        CoverageLevel coverageLevel,
+        const RepoPath& modulesPath,
+        const RepoPath& excludedModulesPath,
+        const RepoPath& sourcesPath,
+        const typename NativeRegularTestRunner::Command& testRunLaunchCommand);
 } // namespace TestImpact
 } // namespace TestImpact

+ 15 - 5
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Job/TestImpactNativeTestRunJobData.h

@@ -19,17 +19,27 @@ namespace TestImpact
     public:
     public:
         template<typename... AdditionalInfoArgs>
         template<typename... AdditionalInfoArgs>
         NativeTestRunJobData(LaunchMethod launchMethod, AdditionalInfoArgs&&... additionalInfo)
         NativeTestRunJobData(LaunchMethod launchMethod, AdditionalInfoArgs&&... additionalInfo)
-            : Parent(std::forward<AdditionalInfoArgs>(additionalInfo)...)
+            : Parent(std::forward<AdditionalInfoArgs>(additionalInfo)...)   
             , m_launchMethod(launchMethod)
             , m_launchMethod(launchMethod)
         {
         {
         }
         }
 
 
-        LaunchMethod GetLaunchMethod() const
-        {
-            return m_launchMethod;
-        }
+        //! Copy and move constructors/assignment operators.
+        NativeTestRunJobData(const NativeTestRunJobData& other) = default;
+        NativeTestRunJobData(NativeTestRunJobData&& other) = default;
+        NativeTestRunJobData& operator=(const NativeTestRunJobData& other) = default;
+        NativeTestRunJobData& operator=(NativeTestRunJobData&& other) = default;
+
+        //! Returns the launch method used by this test target.
+        LaunchMethod GetLaunchMethod() const;
 
 
     private:
     private:
         LaunchMethod m_launchMethod = LaunchMethod::TestRunner;
         LaunchMethod m_launchMethod = LaunchMethod::TestRunner;
     };
     };
+
+    template<typename Parent>
+    LaunchMethod NativeTestRunJobData<Parent>::GetLaunchMethod() const
+    {
+        return m_launchMethod;
+    }
 } // namespace TestImpact
 } // namespace TestImpact

+ 172 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Shard/TestImpactNativeShardedTestJob.h

@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestImpactFramework/TestImpactUtils.h>
+
+#include <Process/JobRunner/TestImpactProcessJobInfo.h>
+#include <Process/JobRunner/TestImpactProcessJobMeta.h>
+#include <TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h>
+
+#include <AzCore/std/optional.h>
+#include <AzCore/std/utility/to_underlying.h>
+
+namespace TestImpact
+{
+    //! Composite test job for all test shards of a given test target.
+    template<typename TestRunnerType>
+    class ShardedTestJob
+    {
+    public:
+        using ShardedTestJobInfoType = ShardedTestJobInfo<TestRunnerType>;
+        using JobInfo = typename TestRunnerType::Job::Info;
+
+        //! Consolidated job data for all sharded sub jobs.
+        struct JobData;
+
+        //! Constructs a sharded test job from the specified sharded test job info.
+        ShardedTestJob(const ShardedTestJobInfoType& shardedTestJobInfo);
+
+        //! Returns `true` if all shards in this job have completed, otherwise `false`.
+        bool IsComplete() const;
+
+        //! Registers the specified sharded sub job as complete.
+        void RegisterCompletedSubJob(const JobInfo& jobInfo, const JobMeta& meta, const StdContent& std);
+
+        //! Returns the consolidated job data when all sharded sub jobs have completed, otherwise `AZStd::nullopt`.
+        const AZStd::optional<JobData>& GetConsolidatedJobData() const;
+
+        //! Returns the vector of sub job data that may or may not be complete.
+        const AZStd::vector<JobData>& GetSubJobs() const;
+
+        //! Resolves the test run results of each sharded sub job into one consolidated test run result.
+        static JobResult ResolveJobResult(const AZStd::optional<JobResult> jobResult, const JobResult subJobResult);
+
+    private:
+        const ShardedTestJobInfoType* m_shardedTestJobInfo = nullptr; //!< Pointer to the sharded test job info of this sharded job.
+        AZStd::vector<JobData> m_subJobs; //!< The sharded sub jobs that belong this job.
+        AZStd::optional<JobData> m_consolidatedJobData; //!< The consolidated sub job data.
+        Timer m_timer; //!< The timer to measure the total runtime of all ahrded sub jobs.
+    };
+
+    template<typename TestRunnerType>
+    struct ShardedTestJob<TestRunnerType>::JobData
+    {
+        JobData(const JobInfo& jobInfo)
+            : m_jobInfo(jobInfo)
+        {
+        }
+
+        JobData(const JobInfo& jobInfo, const JobMeta& meta, const StdContent& std)
+            : m_jobInfo(jobInfo)
+            , m_meta(meta)
+            , m_std(std)
+        {
+        }
+
+        JobInfo m_jobInfo; //!< The job info for this shard.
+        JobMeta m_meta; //<! The job meta for this shard.
+        StdContent m_std; //!< The standard output/error for this shard.
+    };
+
+    template<typename TestRunnerType>
+    JobResult ShardedTestJob<TestRunnerType>::ResolveJobResult(const AZStd::optional<JobResult> jobResult, const JobResult subJobResult)
+    {
+        if (jobResult.has_value())
+        {
+            // Unless the sub job result is not executed, take the job result in reverse order of precedence
+            // of the JobResult enumeration
+            if (AZStd::to_underlying(subJobResult) < AZStd::to_underlying(jobResult.value()))
+            {
+                if (subJobResult == JobResult::NotExecuted)
+                {
+                    return jobResult.value();
+                }
+
+                return subJobResult;
+            }
+        }
+
+        return subJobResult;
+    }
+
+    template<typename TestRunnerType>
+    ShardedTestJob<TestRunnerType>::ShardedTestJob(const ShardedTestJobInfoType& shardedTestJobInfo)
+        : m_shardedTestJobInfo(&shardedTestJobInfo)
+    {
+        m_subJobs.reserve(m_shardedTestJobInfo->GetJobInfos().size());
+    }
+
+    template<typename TestRunnerType>
+    bool ShardedTestJob<TestRunnerType>::IsComplete() const
+    {
+        return m_subJobs.size() == m_shardedTestJobInfo->GetJobInfos().size();
+    }
+
+    template<typename TestRunnerType>
+    void ShardedTestJob<TestRunnerType>::RegisterCompletedSubJob(const JobInfo& jobInfo, const JobMeta& meta, const StdContent& std)
+    {
+        m_subJobs.emplace_back(jobInfo, meta, std);
+
+        if (IsComplete())
+        {
+            // Take the first job to be scheduled as for no sharding this will be the actual job and for sharding it
+            // doesn't make a great deal of sense to try and consolidate the jobs at this level anyway (the completed
+            // jobs returned by the sharded test runner will present all shards as a single completed job)
+            m_consolidatedJobData = JobData(m_shardedTestJobInfo->GetJobInfos().front());
+
+            AZStd::optional<JobResult> consolidatedJobResult;
+            JobMeta& consolidatedMeta = m_consolidatedJobData->m_meta;
+
+            consolidatedMeta.m_startTime = m_timer.GetStartTimePoint();
+            consolidatedMeta.m_duration = m_timer.GetElapsedMs();
+
+            for (const auto& subJob : m_subJobs)
+            {
+                // Resolve consolidated job result from existing sub job results
+                consolidatedJobResult = ResolveJobResult(consolidatedJobResult, subJob.m_meta.m_result);
+
+                // Technically, it would be possible to consolidate return codes at the job level as we could use the
+                // platform/framework error code checkers that the test engine uses to determine what error codes map
+                // to what test run results but it's not worth it so just take the highest error code value
+                if (subJob.m_meta.m_returnCode.has_value() &&
+                    (!consolidatedMeta.m_returnCode.has_value() ||
+                     consolidatedMeta.m_returnCode.value() < subJob.m_meta.m_returnCode.value()))
+                {
+                    consolidatedMeta.m_returnCode = subJob.m_meta.m_returnCode;
+                }
+
+                // Accumulate the standard out/error of each sub job
+                const auto stdAccumulator = [](const AZStd::optional<AZStd::string>& source, AZStd::optional<AZStd::string>& dest)
+                {
+                    if (source.has_value())
+                    {
+                        dest = dest.has_value() ? dest.value() + source.value() : source.value();
+                    }
+                };
+                stdAccumulator(subJob.m_std.m_out, m_consolidatedJobData->m_std.m_out);
+                stdAccumulator(subJob.m_std.m_err, m_consolidatedJobData->m_std.m_err);
+            }
+
+            consolidatedMeta.m_result = consolidatedJobResult.value_or(JobResult::NotExecuted);
+        }
+    }
+
+    template<typename TestRunnerType>
+    auto ShardedTestJob<TestRunnerType>::GetConsolidatedJobData() const -> const AZStd::optional<JobData>&
+    {
+        return m_consolidatedJobData;
+    }
+
+    template<typename TestRunnerType>
+    auto ShardedTestJob<TestRunnerType>::GetSubJobs() const -> const AZStd::vector<JobData>&
+    {
+        return m_subJobs;
+    }
+} // namespace TestImpact

+ 308 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/Shard/TestImpactNativeShardedTestRunnerBase.h

@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestRunner/Common/Run/TestImpactTestCoverageSerializer.h>
+#include <TestRunner/Common/Run/TestImpactTestRunSerializer.h>
+#include <TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h>
+#include <TestRunner/Native/Shard/TestImpactNativeShardedTestJob.h>
+
+#include <AzCore/std/numeric.h>
+
+namespace TestImpact
+{
+    //! Base class for all sharded test runners.
+    template<typename TestRunnerType>
+    class NativeShardedTestRunnerBase
+    {
+    public:
+        //! Public-facing `JobInfo` for interoperability with the various helper functions and event handlers.
+        using JobInfo = typename TestRunnerType::JobInfo;
+
+        //! The actual collection of `JobInfo`s condumed by this runner.
+        using JobInfos = AZStd::vector<ShardedTestJobInfo<TestRunnerType>>;
+
+        using JobId = typename JobInfo::Id;
+        using Job = typename TestRunnerType::Job;
+
+        //! Constructs the sharded test system to wrap around the specified test runner.
+        NativeShardedTestRunnerBase(TestRunnerType& testRunner, const RepoPath& repoRoot, const ArtifactDir& artifactDir);
+        virtual ~NativeShardedTestRunnerBase() = default;
+
+        //! Wrapper around the test runner's `RunTests` method to present the sharded test running interface to the user.
+        [[nodiscard]] AZStd::pair<ProcessSchedulerResult, AZStd::vector<Job>> RunTests(
+            const JobInfos& shardedJobInfos,
+            StdOutputRouting stdOutRouting,
+            StdErrorRouting stdErrRouting,
+            AZStd::optional<AZStd::chrono::milliseconds> runTimeout,
+            AZStd::optional<AZStd::chrono::milliseconds> runnerTimeout);
+
+        //! Bus for native sharded test system notifications.
+        class Notifications
+            : public AZ::EBusTraits
+        {
+        public:
+            // EBusTraits overrides ...
+            static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
+            static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
+
+            //! Callback for sharded job completion/failure.
+            //! @param jobInfo The job information associated with this job.
+            //! @param meta The meta-data about the job run.
+            //! @param std The standard output and standard error of the process running the job.
+            virtual ProcessCallbackResult OnJobComplete(
+                [[maybe_unused]] const JobInfo& jobInfo,
+                [[maybe_unused]] const JobMeta& meta,
+                [[maybe_unused]] const StdContent& std)
+            {
+                return ProcessCallbackResult::Continue;
+            }
+
+            //! Callback for sharded sub-job completion/failure.
+            //! @param subJobCount The number of sub-jobs that make up this job.
+            //! @param jobId The id of the sharded job.
+            //! @param subJobInfo The job information associated with this sharded sub-job.
+            //! @param subJobMeta The meta-data about the sharded sub-job run.
+            //! @param subJobStd The standard output and standard error of the process running the sharded sub-job.
+            virtual ProcessCallbackResult OnShardedJobComplete(
+                [[maybe_unused]] JobId jobId,
+                [[maybe_unused]] size_t subJobCount,
+                [[maybe_unused]] const JobInfo& subJobInfo,
+                [[maybe_unused]] const JobMeta& subJobMeta,
+                [[maybe_unused]] const StdContent& subJobStd)
+            {
+                return ProcessCallbackResult::Continue;
+            }
+        };
+
+        using NotificationBus = AZ::EBus<Notifications>;
+
+    protected:
+        //! Map of sharded sub jobs to the parent sharded test job info.
+        using ShardToParentShardedJobMap = AZStd::unordered_map<typename JobId::IdType, const ShardedTestJobInfo<TestRunnerType>*>;
+
+        //! Map of sharded test job infos to their sharded test jobs.
+        using CompletedShardMap = AZStd::unordered_map<const ShardedTestJobInfo<TestRunnerType>*, ShardedTestJob<TestRunnerType>>;
+
+        //! Helper function to aid in debugging flakey test targets that may contain file race conditions when sharded.
+        //! @note It's actually impossible to definitively say at this level if a sharded sub job failed due to a race condition
+        //! but provide some helpful output to aid in debugging in any case. The idea behind this function is to print out the last
+        //! chunk of standard output produced by the offending sub job as if the output suddenly terminates, it means the shard 
+        //! crashed (possible due to file race conditions with other shards) but if the output ends gracefully (i.e. with a test
+        //! framework summary of the test run) then it failed to produce the requisite test run artifacts for unrelated reasons.
+        static void LogSuspectedShardFileRaceCondition(
+            const Job& subJob, const ShardToParentShardedJobMap& shardToParentShardedJobMap, const CompletedShardMap& completedShardMap);
+
+        //! Consolidates the sharded sub job artifacts into one set of artifacts per test target.
+        [[nodiscard]] virtual typename TestRunnerType::ResultType ConsolidateSubJobs(
+            const typename TestRunnerType::ResultType& result,
+            const ShardToParentShardedJobMap& shardToParentShardedJobMap,
+            const CompletedShardMap& completedShardMap) = 0;
+
+        RepoPath m_repoRoot; //!< Path to repo root.
+        ArtifactDir m_artifactDir; //!< Path to repo artifact directory.
+
+    private:
+        //! Handler for test job runner notifications.
+        class TestJobRunnerNotificationHandler;
+
+        TestRunnerType* m_testRunner = nullptr; //!< Pointer to the unerlying (non-sharded) test runner used by this sharded runner.
+    };
+
+    template<typename TestRunnerType>
+    class NativeShardedTestRunnerBase<TestRunnerType>::TestJobRunnerNotificationHandler
+        : private TestRunnerType::NotificationBus::Handler
+    {
+    public:
+        using JobInfo = typename TestRunnerType::JobInfo;
+
+        TestJobRunnerNotificationHandler(ShardToParentShardedJobMap& shardToParentShardedJobMap, CompletedShardMap& completedShardMap);
+        virtual ~TestJobRunnerNotificationHandler();
+
+    private:
+        // NotificationsBus overrides ...
+        ProcessCallbackResult OnJobComplete(
+            const typename TestRunnerType::Job::Info& jobInfo, const JobMeta& meta, const StdContent& std) override;
+
+        ShardToParentShardedJobMap* m_shardToParentShardedJobMap = nullptr;
+        CompletedShardMap* m_completedShardMap = nullptr;
+    };
+
+    template<typename TestRunnerType>
+    NativeShardedTestRunnerBase<TestRunnerType>::TestJobRunnerNotificationHandler::TestJobRunnerNotificationHandler(
+        ShardToParentShardedJobMap& shardToParentShardedJobMap, CompletedShardMap& completedShardMap)
+        : m_shardToParentShardedJobMap(&shardToParentShardedJobMap)
+        , m_completedShardMap(&completedShardMap)
+    {
+        TestRunnerType::NotificationBus::Handler::BusConnect();
+    }
+
+    template<typename TestRunnerType>
+    NativeShardedTestRunnerBase<TestRunnerType>::TestJobRunnerNotificationHandler::~TestJobRunnerNotificationHandler()
+    {
+        TestRunnerType::NotificationBus::Handler::BusDisconnect();
+    }
+
+    template<typename TestRunnerType>
+    ProcessCallbackResult NativeShardedTestRunnerBase<TestRunnerType>::TestJobRunnerNotificationHandler::OnJobComplete(
+        const typename TestRunnerType::Job::Info& jobInfo, const JobMeta& meta, const StdContent& std)
+    {
+        const auto& shardedJobInfo = m_shardToParentShardedJobMap->at(jobInfo.GetId().m_value);
+        auto& shardedTestJob = m_completedShardMap->at(shardedJobInfo);
+        
+        {
+            AZ::EBusAggregateResults<ProcessCallbackResult> results;
+            NotificationBus::BroadcastResult(
+                results,
+                &NotificationBus::Events::OnShardedJobComplete,
+                shardedJobInfo->GetJobInfos().begin()->GetId(),
+                shardedJobInfo->GetJobInfos().size(),
+                jobInfo,
+                meta,
+                std);
+
+            const auto result = GetAggregateProcessCallbackResult(results);
+            if (result == ProcessCallbackResult::Abort)
+            {
+                return result;
+            }
+        }
+
+        shardedTestJob.RegisterCompletedSubJob(jobInfo, meta, std);
+
+        if (shardedTestJob.IsComplete())
+        {
+            auto& consolidatedJobData = *shardedTestJob.GetConsolidatedJobData();
+            AZ::EBusAggregateResults<ProcessCallbackResult> results;
+            NotificationBus::BroadcastResult(
+                results,
+                &NotificationBus::Events::OnJobComplete,
+                consolidatedJobData.m_jobInfo,
+                consolidatedJobData.m_meta,
+                consolidatedJobData.m_std);
+            return GetAggregateProcessCallbackResult(results);
+        }
+
+        return ProcessCallbackResult::Continue;
+    }
+
+    template<typename TestRunnerType>
+    NativeShardedTestRunnerBase<TestRunnerType>::NativeShardedTestRunnerBase(
+        TestRunnerType& testRunner, const RepoPath& repoRoot, const ArtifactDir& artifactDir)
+        : m_testRunner(&testRunner)
+        , m_repoRoot(repoRoot)
+        , m_artifactDir(artifactDir)
+    {
+    }
+
+    template<typename TestRunnerType>
+    AZStd::pair<ProcessSchedulerResult, AZStd::vector<typename TestRunnerType::Job>> NativeShardedTestRunnerBase<TestRunnerType>::RunTests(
+        const JobInfos& shardedJobInfos,
+        StdOutputRouting stdOutRouting,
+        StdErrorRouting stdErrRouting,
+        AZStd::optional<AZStd::chrono::milliseconds> runTimeout,
+        AZStd::optional<AZStd::chrono::milliseconds> runnerTimeout)
+    {
+        ShardToParentShardedJobMap shardToParentShardedJobMap;
+        CompletedShardMap completedShardMap;
+
+        // Calculate the total number of shards across all test targets in this run
+        const auto totalJobShards = AZStd::accumulate(
+            shardedJobInfos.begin(),
+            shardedJobInfos.end(),
+            size_t{ 0 },
+            [](size_t sum, const ShardedTestJobInfo<TestRunnerType>& shardedJobInfo)
+            {
+                return sum + shardedJobInfo.GetJobInfos().size();
+            });
+
+        // Prepare the shard and completed job maps for this run
+        AZStd::vector<JobInfo> subJobInfos;
+        subJobInfos.reserve(totalJobShards);
+        for (const auto& shardedJobInfo : shardedJobInfos)
+        {
+            completedShardMap.emplace(
+                AZStd::piecewise_construct, AZStd::forward_as_tuple(&shardedJobInfo), std::forward_as_tuple(shardedJobInfo));
+            const auto& jobInfos = shardedJobInfo.GetJobInfos();
+            subJobInfos.insert(subJobInfos.end(), jobInfos.begin(), jobInfos.end());
+            for (const auto& jobInfo : jobInfos)
+            {
+                shardToParentShardedJobMap[jobInfo.GetId().m_value] = &shardedJobInfo;
+            }
+        }
+
+        // Run each shard as a test run in the underlying standard test runner for this sharded test runenr type
+        TestJobRunnerNotificationHandler handler(shardToParentShardedJobMap, completedShardMap);
+        const auto result = m_testRunner->RunTests(
+            subJobInfos,
+            stdOutRouting,
+            stdErrRouting,
+            runTimeout,
+            runnerTimeout);
+
+        // Return the consolidated test run jobs to transparently to the caller
+        return ConsolidateSubJobs(result, shardToParentShardedJobMap, completedShardMap);
+    }
+
+    template<typename TestRunnerType>
+    void NativeShardedTestRunnerBase<TestRunnerType>::LogSuspectedShardFileRaceCondition(
+        const Job& subJob, const ShardToParentShardedJobMap& shardToParentShardedJobMap, const CompletedShardMap& completedShardMap)
+    {
+        const auto jobId = subJob.GetJobInfo().GetId().m_value;
+        const auto shardedTestJobInfo = shardToParentShardedJobMap.at(jobId);
+        const auto& shardedTestJob = completedShardMap.at(shardedTestJobInfo);
+        const auto& shardedSubJobs = shardedTestJob.GetSubJobs();
+
+        // Try and find the offending sharded sub job
+        auto jobData = AZStd::find_if(
+            shardedSubJobs.begin(),
+            shardedSubJobs.end(),
+            [&](const typename ShardedTestJob<TestRunnerType>::JobData& jobData)
+            {
+                return jobData.m_jobInfo.GetId().m_value == jobId;
+            });
+
+        // This really should't fail but check anyway
+        if (jobData != shardedSubJobs.end())
+        {
+            const size_t shardNumber = jobData->m_jobInfo.GetId().m_value - shardedTestJobInfo->GetJobInfos().front().GetId().m_value;
+
+            if (jobData->m_std.m_out.has_value())
+            {
+                // Offending sub job has std output available, print a truncated summary of the last 500 characters of known output
+                constexpr size_t numCharsToPrint = 500;
+                const size_t subStringLength = AZStd::min(size_t{ numCharsToPrint }, jobData->m_std.m_out->length());
+                const auto subString = jobData->m_std.m_out->substr(jobData->m_std.m_out->length() - subStringLength);
+                AZ_Warning(
+                    "Shard",
+                    false,
+                    AZStd::string::format(
+                        "Possible file race condition detected for test target '%s' on shard '%zu', backtrace of std out for last %zu "
+                        "characters (check for properly terminated test log output):\n%s",
+                        shardedTestJobInfo->GetTestTarget()->GetName().c_str(),
+                        shardNumber,
+                        subStringLength,
+                        subString.c_str())
+                        .c_str());
+            }
+            else
+            {
+                // Offending sub job has no std output available, happy hunting!
+                AZ_Warning(
+                    "Shard",
+                    false,
+                    AZStd::string::format(
+                        "Possible race condition detected for test target '%s' on shard '%zu', backtrace of std out unavailable",
+                        shardedTestJobInfo->GetTestTarget()->GetName().c_str(),
+                        shardNumber)
+                        .c_str());
+            }
+        }
+    }
+} // namespace TestImpact

+ 3 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h

@@ -20,10 +20,13 @@
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
+    class NativeInstrumentedTestRunJobInfoGenerator;
+
     class NativeInstrumentedTestRunner
     class NativeInstrumentedTestRunner
         : public TestRunnerWithCoverage<NativeTestRunJobData<TestRunWithCoverageJobData>, TestCoverage>
         : public TestRunnerWithCoverage<NativeTestRunJobData<TestRunWithCoverageJobData>, TestCoverage>
     {
     {
     public:
     public:
+        using JobInfoGenerator = NativeInstrumentedTestRunJobInfoGenerator;
         using TestRunnerWithCoverage<NativeTestRunJobData<TestRunWithCoverageJobData>, TestCoverage>::TestRunnerWithCoverage;
         using TestRunnerWithCoverage<NativeTestRunJobData<TestRunWithCoverageJobData>, TestCoverage>::TestRunnerWithCoverage;
 
 
     protected:
     protected:

+ 3 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeRegularTestRunner.h

@@ -18,10 +18,13 @@
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
+    class NativeRegularTestRunJobInfoGenerator;
+
     class NativeRegularTestRunner
     class NativeRegularTestRunner
         : public TestRunner<NativeTestRunJobData<TestRunJobData>>
         : public TestRunner<NativeTestRunJobData<TestRunJobData>>
     {
     {
     public:
     public:
+        using JobInfoGenerator = NativeRegularTestRunJobInfoGenerator;
         using TestRunner<NativeTestRunJobData<TestRunJobData>>::TestRunner;
         using TestRunner<NativeTestRunJobData<TestRunJobData>>::TestRunner;
 
 
     protected:
     protected:

+ 130 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.cpp

@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestRunner/Common/Job/TestImpactTestJobInfoUtils.h>
+#include <TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.h>
+
+namespace TestImpact
+{
+    typename NativeInstrumentedTestRunner::ResultType NativeShardedInstrumentedTestRunner::ConsolidateSubJobs(
+        const typename NativeInstrumentedTestRunner::ResultType& result,
+        const ShardToParentShardedJobMap& shardToParentShardedJobMap,
+        const CompletedShardMap& completedShardMap)
+    {
+        const auto& [returnCode, subJobs] = result;
+        AZStd::unordered_map<
+            typename JobId::IdType,
+            std::pair<AZStd::unordered_map<AZStd::string, TestRunSuite>, AZStd::unordered_map<RepoPath, ModuleCoverage>>>
+            consolidatedJobArtifacts;
+
+        for (const auto& subJob : subJobs)
+        {
+            if (const auto payload = subJob.GetPayload();
+                payload.has_value())
+            {
+                const auto& [subTestRun, subTestCoverage] = payload.value();
+                const auto shardedTestJobInfo = shardToParentShardedJobMap.at(subJob.GetJobInfo().GetId().m_value);
+                const auto parentJobInfoId = shardedTestJobInfo->GetId();
+                auto& [testSuites, testCoverage] = consolidatedJobArtifacts[parentJobInfoId.m_value];
+
+                // Accumulate test results
+                if (subTestRun.has_value())
+                {
+                    for (const auto& subTestSuite : subTestRun->GetTestSuites())
+                    {
+                        auto& testSuite = testSuites[subTestSuite.m_name];
+                        if (testSuite.m_name.empty())
+                        {
+                            testSuite.m_name = subTestSuite.m_name;
+                        }
+
+                        testSuite.m_enabled = subTestSuite.m_enabled;
+                        testSuite.m_duration += subTestSuite.m_duration;
+                        testSuite.m_tests.insert(testSuite.m_tests.end(), subTestSuite.m_tests.begin(), subTestSuite.m_tests.end());
+                    }
+                }
+
+                // Accumulate test coverage
+                for (const auto& subModuleCoverage : subTestCoverage.GetModuleCoverages())
+                {
+                    auto& moduleCoverage = testCoverage[subModuleCoverage.m_path];
+                    if (moduleCoverage.m_path.empty())
+                    {
+                        moduleCoverage.m_path = subModuleCoverage.m_path;
+                        moduleCoverage.m_sources.insert(
+                            moduleCoverage.m_sources.end(), subModuleCoverage.m_sources.begin(), subModuleCoverage.m_sources.end());
+                    }
+                }
+            }
+            else
+            {
+                LogSuspectedShardFileRaceCondition(subJob, shardToParentShardedJobMap, completedShardMap);
+            }
+        }
+
+        AZStd::vector<typename NativeInstrumentedTestRunner::Job> consolidatedJobs;
+        consolidatedJobs.reserve(consolidatedJobArtifacts.size());
+
+        for (auto&& [jobId, artifacts] : consolidatedJobArtifacts)
+        {
+            auto&& [testSuites, testCoverage] = artifacts;
+            const auto shardedTestJobInfo = shardToParentShardedJobMap.at(jobId);
+            const auto& shardedTestJob = completedShardMap.at(shardedTestJobInfo);
+            const auto& jobData = shardedTestJob.GetConsolidatedJobData();
+
+            // Consolidate test runs
+            AZStd::optional<TestRun> run;
+            if (testSuites.size())
+            {
+                AZStd::vector<TestRunSuite> suites;
+                suites.reserve(testSuites.size());
+                for (auto&& [suiteName, suite] : testSuites)
+                {
+                    suites.emplace_back(AZStd::move(suite));
+                }
+
+                if (jobData.has_value())
+                {
+                    run = TestRun(AZStd::move(suites), jobData->m_meta.m_duration.value_or(AZStd::chrono::milliseconds{ 0 }));
+                }
+            }          
+
+            // Consolidate test coverages
+            AZStd::vector<ModuleCoverage> moduleCoverages;
+            moduleCoverages.reserve(testCoverage.size());
+            for (auto&& [modulePath, moduleCoverage] : testCoverage)
+            {
+                moduleCoverages.emplace_back(AZStd::move(moduleCoverage));
+            }
+
+            auto payload =
+                typename NativeInstrumentedTestRunner::JobPayload{ AZStd::move(run), TestCoverage(AZStd::move(moduleCoverages)) };
+
+            if (shardedTestJobInfo->GetJobInfos().size() > 1)
+            {
+                // Serialize the consolidated run and coverage as artifacts in the canonical run and coverage directories
+                WriteFileContents<TestRunnerException>(
+                    Cobertura::SerializeTestCoverage(payload.second, m_repoRoot),
+                    m_artifactDir.m_coverageArtifactDirectory / RepoPath(shardedTestJobInfo->GetTestTarget()->GetName() + ".xml"));
+                if (payload.first.has_value())
+                {
+                    WriteFileContents<TestRunnerException>(
+                        GTest::SerializeTestRun(payload.first.value()),
+                        m_artifactDir.m_testRunArtifactDirectory /
+                            RepoPath(GenerateFullQualifiedTargetNameStem(shardedTestJobInfo->GetTestTarget()).String() + ".xml"));
+                }
+            }
+
+            consolidatedJobs.emplace_back(jobData->m_jobInfo, JobMeta{ jobData->m_meta }, AZStd::move(payload));
+        }
+
+        return { result.first, consolidatedJobs };
+    }
+} // namespace TestImpact

+ 30 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h>
+#include <TestRunner/Native/Shard/TestImpactNativeShardedTestRunnerBase.h>
+
+namespace TestImpact
+{
+    //! Sharded test runner for instrumented tests.
+    class NativeShardedInstrumentedTestRunner
+        : public NativeShardedTestRunnerBase<NativeInstrumentedTestRunner>
+    {
+    public:
+        using NativeShardedTestRunnerBase<NativeInstrumentedTestRunner>::NativeShardedTestRunnerBase;
+
+    private:
+        // NativeShardedTestSystem overrides ...
+        [[nodiscard]] typename NativeInstrumentedTestRunner::ResultType ConsolidateSubJobs(
+            const typename NativeInstrumentedTestRunner::ResultType& result,
+            const ShardToParentShardedJobMap& shardToParentShardedJobMap,
+            const CompletedShardMap& completedShardMap) override;
+    };
+} // namespace TestImpact

+ 98 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedRegularTestRunner.cpp

@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestRunner/Common/Job/TestImpactTestJobInfoUtils.h>
+#include <TestRunner/Native/TestImpactNativeShardedRegularTestRunner.h>
+
+namespace TestImpact
+{
+    typename NativeRegularTestRunner::ResultType NativeShardedRegularTestRunner::ConsolidateSubJobs(
+        const typename NativeRegularTestRunner::ResultType& result,
+        const ShardToParentShardedJobMap& shardToParentShardedJobMap,
+        const CompletedShardMap& completedShardMap)
+    {
+        const auto& [returnCode, subJobs] = result;
+        AZStd::unordered_map<
+            typename JobId::IdType,
+            std::pair<AZStd::unordered_map<AZStd::string, TestRunSuite>, AZStd::unordered_map<RepoPath, ModuleCoverage>>>
+            consolidatedJobArtifacts;
+
+        for (const auto& subJob : subJobs)
+        {
+            if (const auto payload = subJob.GetPayload();
+                payload.has_value())
+            {
+                const auto& subTestRun = payload.value();
+                const auto shardedTestJobInfo = shardToParentShardedJobMap.at(subJob.GetJobInfo().GetId().m_value);
+                const auto parentJobInfoId = shardedTestJobInfo->GetId();
+                auto& [testSuites, testCoverage] = consolidatedJobArtifacts[parentJobInfoId.m_value];
+
+                // Accumulate test results
+                for (const auto& subTestSuite : subTestRun.GetTestSuites())
+                {
+                    auto& testSuite = testSuites[subTestSuite.m_name];
+                    if (testSuite.m_name.empty())
+                    {
+                        testSuite.m_name = subTestSuite.m_name;
+                    }
+
+                    testSuite.m_enabled = subTestSuite.m_enabled;
+                    testSuite.m_duration += subTestSuite.m_duration;
+                    testSuite.m_tests.insert(testSuite.m_tests.end(), subTestSuite.m_tests.begin(), subTestSuite.m_tests.end());
+                }
+            }
+            else
+            {
+                LogSuspectedShardFileRaceCondition(subJob, shardToParentShardedJobMap, completedShardMap);
+            }
+        }
+
+        AZStd::vector<typename NativeRegularTestRunner::Job> consolidatedJobs;
+        consolidatedJobs.reserve(consolidatedJobArtifacts.size());
+
+        for (auto&& [jobId, artifacts] : consolidatedJobArtifacts)
+        {
+            auto&& [testSuites, testCoverage] = artifacts;
+            const auto shardedTestJobInfo = shardToParentShardedJobMap.at(jobId);
+            const auto& shardedTestJob = completedShardMap.at(shardedTestJobInfo);
+            const auto& jobData = shardedTestJob.GetConsolidatedJobData();
+
+            // Consolidate test runs
+            AZStd::optional<TestRun> run;
+            if (testSuites.size())
+            {
+                AZStd::vector<TestRunSuite> suites;
+                suites.reserve(testSuites.size());
+                for (auto&& [suiteName, suite] : testSuites)
+                {
+                    suites.emplace_back(AZStd::move(suite));
+                }
+
+                if (jobData.has_value())
+                {
+                    run = TestRun(AZStd::move(suites), jobData->m_meta.m_duration.value_or(AZStd::chrono::milliseconds{ 0 }));
+                }
+            }
+
+            // Serialize the consolidated run as and artifact in the canonical run directory
+            if (run.has_value() && shardedTestJobInfo->GetJobInfos().size() > 1)
+            {
+                WriteFileContents<TestRunnerException>(
+                    GTest::SerializeTestRun(run.value()),
+                    m_artifactDir.m_testRunArtifactDirectory /
+                        RepoPath(GenerateFullQualifiedTargetNameStem(shardedTestJobInfo->GetTestTarget()).String() + ".xml"));
+            }
+
+            consolidatedJobs.emplace_back(jobData->m_jobInfo, JobMeta{ jobData->m_meta }, AZStd::move(run));
+        }
+
+        return { result.first, consolidatedJobs };
+    }
+} // namespace TestImpact

+ 30 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeShardedRegularTestRunner.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <TestRunner/Native/TestImpactNativeRegularTestRunner.h>
+#include <TestRunner/Native/Shard/TestImpactNativeShardedTestRunnerBase.h>
+
+namespace TestImpact
+{
+    //! Sharded test runner for regular tests.
+    class NativeShardedRegularTestRunner
+        : public NativeShardedTestRunnerBase<NativeRegularTestRunner>
+    {
+    public:
+        using NativeShardedTestRunnerBase<NativeRegularTestRunner>::NativeShardedTestRunnerBase;
+
+    private:
+        // NativeShardedTestSystem overrides ...
+        [[nodiscard]] NativeRegularTestRunner::ResultType ConsolidateSubJobs(
+            const NativeRegularTestRunner::ResultType& result,
+            const ShardToParentShardedJobMap& shardToParentShardedJobMap,
+            const CompletedShardMap& completedShardMap) override;
+    };
+} // namespace TestImpact

+ 3 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/Source/TestRunner/Native/TestImpactNativeTestEnumerator.h

@@ -18,6 +18,8 @@
 
 
 namespace TestImpact
 namespace TestImpact
 {
 {
+    class NativeTestEnumerationJobInfoGenerator;
+
     struct NativeTestEnumerationJobData
     struct NativeTestEnumerationJobData
         : public TestEnumerationJobData
         : public TestEnumerationJobData
     {
     {
@@ -28,6 +30,7 @@ namespace TestImpact
         : public TestEnumerator<NativeTestEnumerationJobData>
         : public TestEnumerator<NativeTestEnumerationJobData>
     {
     {
     public:
     public:
+        using JobInfoGenerator = NativeTestEnumerationJobInfoGenerator;
         using TestEnumerator<NativeTestEnumerationJobData>::TestEnumerator;
         using TestEnumerator<NativeTestEnumerationJobData>::TestEnumerator;
 
 
     protected:
     protected:

+ 10 - 0
Code/Tools/TestImpactFramework/Runtime/Native/Code/testimpactframework_runtime_native_files.cmake

@@ -9,6 +9,7 @@
 set(FILES
 set(FILES
     Include/TestImpactFramework/Native/TestImpactNativeRuntime.h
     Include/TestImpactFramework/Native/TestImpactNativeRuntime.h
     Include/TestImpactFramework/Native/TestImpactNativeConfiguration.h
     Include/TestImpactFramework/Native/TestImpactNativeConfiguration.h
+    Include/TestImpactFramework/Native/TestImpactNativeRuntimeConfigurationFactory.h
     Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.cpp
     Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.cpp
     Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.h
     Source/Artifact/Factory/TestImpactNativeTestTargetMetaMapFactory.h
     Source/Artifact/Static/TestImpactNativeTestTargetMeta.h
     Source/Artifact/Static/TestImpactNativeTestTargetMeta.h
@@ -21,14 +22,23 @@ set(FILES
     Source/TestRunner/Native/TestImpactNativeErrorCodeChecker.h
     Source/TestRunner/Native/TestImpactNativeErrorCodeChecker.h
     Source/TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h
     Source/TestRunner/Native/TestImpactNativeInstrumentedTestRunner.h
     Source/TestRunner/Native/TestImpactNativeRegularTestRunner.h
     Source/TestRunner/Native/TestImpactNativeRegularTestRunner.h
+    Source/TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.cpp
+    Source/TestRunner/Native/TestImpactNativeShardedInstrumentedTestRunner.h
+    Source/TestRunner/Native/TestImpactNativeShardedRegularTestRunner.cpp
+    Source/TestRunner/Native/TestImpactNativeShardedRegularTestRunner.h
     Source/TestRunner/Native/TestImpactNativeTestEnumerator.h
     Source/TestRunner/Native/TestImpactNativeTestEnumerator.h
+    Source/TestRunner/Native/Shard/TestImpactNativeShardedTestJob.h
+    Source/TestRunner/Native/Shard/TestImpactNativeShardedTestRunnerBase.h
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.cpp
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.cpp
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoGenerator.h
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.cpp
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.cpp
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h
     Source/TestRunner/Native/Job/TestImpactNativeTestJobInfoUtils.h
     Source/TestRunner/Native/Job/TestImpactNativeTestRunJobData.h
     Source/TestRunner/Native/Job/TestImpactNativeTestRunJobData.h
+    Source/TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.cpp
+    Source/TestRunner/Native/Job/TestImpactNativeShardedTestJobInfoGenerator.h
     Source/TestEngine/Native/TestImpactNativeTestEngine.cpp
     Source/TestEngine/Native/TestImpactNativeTestEngine.cpp
     Source/TestEngine/Native/TestImpactNativeTestEngine.h
     Source/TestEngine/Native/TestImpactNativeTestEngine.h
     Source/TestEngine/Native/TestImpactNativeTestTargetExtension.h
     Source/TestEngine/Native/TestImpactNativeTestTargetExtension.h
     Source/TestImpactNativeRuntime.cpp
     Source/TestImpactNativeRuntime.cpp
+    Source/TestImpactNativeRuntimeConfigurationFactory.cpp
 )
 )