From 3e659b51070e0cb5e8cf734bcc6c3f591e79a45f Mon Sep 17 00:00:00 2001 From: jromnoa Date: Wed, 23 Jun 2021 14:49:18 -0700 Subject: [PATCH 01/29] fixes the nightly GPU test Signed-off-by: John --- .../atom_hydra_scripts/hydra_GPUTest_BasicLevelSetup.py | 4 ++-- .../Gem/PythonTests/atom_renderer/test_Atom_GPUTests.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_GPUTest_BasicLevelSetup.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_GPUTest_BasicLevelSetup.py index 7f63d1f2bc..f37519f420 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_GPUTest_BasicLevelSetup.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_GPUTest_BasicLevelSetup.py @@ -99,8 +99,8 @@ def run(): # Wait for Editor idle loop before executing Python hydra scripts. general.idle_enable(True) - # Create a new level. - new_level_name = "all_components_indepth_level" # Specified in class TestAllComponentsIndepthTests() + # Open the auto_test level. + new_level_name = "auto_test" # Specified in class TestAllComponentsIndepthTests() heightmap_resolution = 512 heightmap_meters_per_pixel = 1 terrain_texture_resolution = 412 diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_GPUTests.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_GPUTests.py index 1371fed4ad..1a4843af66 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_GPUTests.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_GPUTests.py @@ -17,6 +17,7 @@ import os import pytest import ly_test_tools.environment.file_system as file_system +from ly_test_tools.image.screenshot_compare_qssim import qssim as compare_screenshots import editor_python_test_tools.hydra_test_utils as hydra logger = logging.getLogger(__name__) @@ -81,7 +82,9 @@ class TestAllComponentsIndepthTests(object): unexpected_lines=unexpected_lines, halt_on_unexpected=True, cfg_args=[level], + auto_test_mode=False, + null_renderer=False, ) for test_screenshot, golden_screenshot in zip(test_screenshots, golden_images): - self.compare_screenshots(test_screenshot, golden_screenshot) + compare_screenshots(test_screenshot, golden_screenshot) From 497a93014b20649f093c14f76af43d945bdd7e4a Mon Sep 17 00:00:00 2001 From: John Jones-Steele Date: Thu, 24 Jun 2021 11:59:44 +0100 Subject: [PATCH 02/29] Entity Outliner - Sort Order options don't show current selection Also fixes the menus that have checkmark and icon Signed-off-by: John --- .../AzQtComponents/Components/Style.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp index 47c0e66ff0..c94eb59ca6 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp @@ -514,6 +514,20 @@ namespace AzQtComponents } } } + // Implement checkmark with icon in menu. + QStyleOptionMenuItem myOpt = *qstyleoption_cast(option); + bool checkable = myOpt.checkType != QStyleOptionMenuItem::NotCheckable; + bool checked = checkable ? myOpt.checked : false; + + if (!myOpt.icon.isNull() && checked) + { + const int iconSize{ 18 }; + int topPadding{ AZStd::max( 0 , (myOpt.rect.height() - iconSize) / 2) - 1}; + + QProxyStyle::drawControl(element, &myOpt, painter, widget); + myOpt.rect.adjust(0, topPadding, iconSize - myOpt.rect.width(), iconSize - myOpt.rect.height()); + return drawPrimitive(PE_IndicatorMenuCheckMark, &myOpt, painter, widget); + } } break; } From 4c65fecb575de8363559f2e81df19db4043a8ed8 Mon Sep 17 00:00:00 2001 From: Vincent Liu <5900509+onecent1101@users.noreply.github.com> Date: Thu, 24 Jun 2021 09:13:35 -0700 Subject: [PATCH 03/29] Merge 'gameliftfeature' branch into 'development' branch (#1534) Signed-off-by: John --- .../Session/ISessionHandlingRequests.h | 1 + Gems/AWSGameLift/CMakeLists.txt | 12 + .../Code/AWSGameLiftClient/CMakeLists.txt | 89 ++ .../AWSGameLiftCreateSessionOnQueueRequest.h | 39 + .../Request/AWSGameLiftCreateSessionRequest.h | 42 + .../Request/AWSGameLiftJoinSessionRequest.h | 33 + .../AWSGameLiftSearchSessionsRequest.h | 42 + .../Include/Request/IAWSGameLiftRequests.h | 78 ++ .../Source/AWSGameLiftClientManager.cpp | 360 ++++++++ .../Source/AWSGameLiftClientManager.h | 122 +++ .../Source/AWSGameLiftClientModule.cpp | 49 ++ .../AWSGameLiftClientSystemComponent.cpp | 186 +++++ .../Source/AWSGameLiftClientSystemComponent.h | 56 ++ .../AWSGameLiftCreateSessionActivity.cpp | 90 ++ .../AWSGameLiftCreateSessionActivity.h | 39 + ...WSGameLiftCreateSessionOnQueueActivity.cpp | 80 ++ .../AWSGameLiftCreateSessionOnQueueActivity.h | 40 + .../AWSGameLiftJoinSessionActivity.cpp | 112 +++ .../Activity/AWSGameLiftJoinSessionActivity.h | 54 ++ .../AWSGameLiftLeaveSessionActivity.cpp | 38 + .../AWSGameLiftLeaveSessionActivity.h | 27 + .../AWSGameLiftSearchSessionsActivity.cpp | 130 +++ .../AWSGameLiftSearchSessionsActivity.h | 45 + ...AWSGameLiftCreateSessionOnQueueRequest.cpp | 58 ++ .../AWSGameLiftCreateSessionRequest.cpp | 63 ++ .../Request/AWSGameLiftJoinSessionRequest.cpp | 54 ++ .../AWSGameLiftSearchSessionsRequest.cpp | 69 ++ .../Tests/AWSGameLiftClientFixture.h | 63 ++ .../Tests/AWSGameLiftClientManagerTest.cpp | 773 ++++++++++++++++++ .../Tests/AWSGameLiftClientMocks.h | 86 ++ .../AWSGameLiftClientSystemComponentTest.cpp | 156 ++++ .../Tests/AWSGameLiftClientTest.cpp | 15 + .../AWSGameLiftCreateSessionActivityTest.cpp | 81 ++ ...meLiftCreateSessionOnQueueActivityTest.cpp | 79 ++ .../AWSGameLiftJoinSessionActivityTest.cpp | 84 ++ .../AWSGameLiftSearchSessionsActivityTest.cpp | 126 +++ .../awsgamelift_client_files.cmake | 36 + .../awsgamelift_client_shared_files.cmake | 14 + .../awsgamelift_client_tests_files.cmake | 22 + .../Source/AWSGameLiftSessionConstants.h | 24 + .../Code/AWSGameLiftServer/CMakeLists.txt | 72 ++ .../Source/AWSGameLiftServerManager.cpp | 319 ++++++++ .../Source/AWSGameLiftServerManager.h | 128 +++ .../Source/AWSGameLiftServerModule.cpp | 49 ++ .../AWSGameLiftServerSystemComponent.cpp | 129 +++ .../Source/AWSGameLiftServerSystemComponent.h | 57 ++ .../Source/GameLiftServerSDKWrapper.cpp | 72 ++ .../Source/GameLiftServerSDKWrapper.h | 64 ++ .../Tests/AWSGameLiftServerFixture.h | 58 ++ .../Tests/AWSGameLiftServerManagerTest.cpp | 421 ++++++++++ .../Tests/AWSGameLiftServerMocks.h | 127 +++ .../AWSGameLiftServerSystemComponentTest.cpp | 99 +++ .../Tests/AWSGameLiftServerTest.cpp | 15 + .../awsgamelift_server_files.cmake | 20 + .../awsgamelift_server_shared_files.cmake | 14 + .../awsgamelift_server_tests_files.cmake | 18 + Gems/AWSGameLift/Code/CMakeLists.txt | 13 + Gems/AWSGameLift/cdk/.gitignore | 10 + Gems/AWSGameLift/cdk/README.md | 99 +++ Gems/AWSGameLift/cdk/app.py | 53 ++ Gems/AWSGameLift/cdk/aws_gamelift/__init__.py | 10 + .../aws_gamelift/aws_gamelift_construct.py | 59 ++ .../cdk/aws_gamelift/fleet_configurations.py | 147 ++++ .../cdk/aws_gamelift/gamelift_stack.py | 184 +++++ .../cdk/aws_gamelift/support_stack.py | 71 ++ Gems/AWSGameLift/cdk/cdk.json | 17 + Gems/AWSGameLift/cdk/requirements.txt | 4 + Gems/AWSGameLift/gem.json | 14 + Gems/AWSGameLift/preview.png | 3 + engine.json | 1 + 70 files changed, 5914 insertions(+) create mode 100644 Gems/AWSGameLift/CMakeLists.txt create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionOnQueueRequest.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionRequest.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftJoinSessionRequest.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSearchSessionsRequest.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientModule.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionOnQueueRequest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionRequest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftJoinSessionRequest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftSearchSessionsRequest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientFixture.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientSystemComponentTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionActivityTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionOnQueueActivityTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftJoinSessionActivityTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftSearchSessionsActivityTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_shared_files.cmake create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_tests_files.cmake create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerModule.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerFixture.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerSystemComponentTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerTest.cpp create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_shared_files.cmake create mode 100644 Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_tests_files.cmake create mode 100644 Gems/AWSGameLift/Code/CMakeLists.txt create mode 100644 Gems/AWSGameLift/cdk/.gitignore create mode 100644 Gems/AWSGameLift/cdk/README.md create mode 100644 Gems/AWSGameLift/cdk/app.py create mode 100644 Gems/AWSGameLift/cdk/aws_gamelift/__init__.py create mode 100644 Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py create mode 100644 Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py create mode 100644 Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py create mode 100644 Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py create mode 100644 Gems/AWSGameLift/cdk/cdk.json create mode 100644 Gems/AWSGameLift/cdk/requirements.txt create mode 100644 Gems/AWSGameLift/gem.json create mode 100644 Gems/AWSGameLift/preview.png diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h index d55f38f65c..e7d13504cd 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h @@ -12,6 +12,7 @@ #pragma once +#include #include #include diff --git a/Gems/AWSGameLift/CMakeLists.txt b/Gems/AWSGameLift/CMakeLists.txt new file mode 100644 index 0000000000..cb6bd1dc1e --- /dev/null +++ b/Gems/AWSGameLift/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +add_subdirectory(Code) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt b/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt new file mode 100644 index 0000000000..754bcf2436 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/CMakeLists.txt @@ -0,0 +1,89 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(awsgameliftclient_compile_definition $,AWSGAMELIFT_RELEASE,AWSGAMELIFT_DEV>) + +ly_add_target( + NAME AWSGameLift.Client.Static STATIC + NAMESPACE Gem + FILES_CMAKE + awsgamelift_client_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + ../AWSGameLiftCommon/Source + COMPILE_DEFINITIONS + PRIVATE + ${awsgameliftclient_compile_definition} + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + AZ::AzFramework + Gem::AWSCore + 3rdParty::AWSNativeSDK::GameLiftClient +) + +ly_add_target( + NAME AWSGameLift.Clients ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + awsgamelift_client_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + 3rdParty::AWSNativeSDK::GameLiftClient + PUBLIC + Gem::AWSGameLift.Client.Static + RUNTIME_DEPENDENCIES + Gem::AWSCore +) + +# Load the "Gem::AWSGameLift" module in all types of applications. +if (PAL_TRAIT_BUILD_HOST_TOOLS) + ly_create_alias(NAME AWSGameLift.Tools NAMESPACE Gem TARGETS Gem::AWSGameLift.Clients) + ly_create_alias(NAME AWSGameLift.Builders NAMESPACE Gem TARGETS Gem::AWSGameLift.Clients) +endif() + +################################################################################ +# Tests +################################################################################ +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME AWSGameLift.Client.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + awsgamelift_client_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Include + Tests + Source + ../AWSGameLiftCommon/Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + AZ::AzFramework + AZ::AzTest + Gem::AWSCore + Gem::AWSGameLift.Client.Static + 3rdParty::AWSNativeSDK::GameLiftClient + AZ::AWSNativeSDKInit + ) + # Add AWSGameLift.Client.Tests to googletest + ly_add_googletest( + NAME Gem::AWSGameLift.Client.Tests + ) +endif() diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionOnQueueRequest.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionOnQueueRequest.h new file mode 100644 index 0000000000..f0ae90ef94 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionOnQueueRequest.h @@ -0,0 +1,39 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AWSGameLift +{ + //! AWSGameLiftCreateSessionOnQueueRequest + //! GameLift create session on queue request which corresponds to Amazon GameLift + //! StartGameSessionPlacement + struct AWSGameLiftCreateSessionOnQueueRequest + : public AzFramework::CreateSessionRequest + { + public: + AZ_RTTI(AWSGameLiftCreateSessionOnQueueRequest, "{2B99E594-CE81-4EB0-8888-74EF4242B59F}", AzFramework::CreateSessionRequest); + static void Reflect(AZ::ReflectContext* context); + + AWSGameLiftCreateSessionOnQueueRequest() = default; + virtual ~AWSGameLiftCreateSessionOnQueueRequest() = default; + + // Name of the queue to use to place the new game session. You can use either the queue name or ARN value. + AZStd::string m_queueName; + + // A unique identifier to assign to the new game session placement. This value is developer-defined. + // The value must be unique across all Regions and cannot be reused unless you are resubmitting a canceled or timed-out placement request. + AZStd::string m_placementId; + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionRequest.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionRequest.h new file mode 100644 index 0000000000..f1bafd83a7 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftCreateSessionRequest.h @@ -0,0 +1,42 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AWSGameLift +{ + //! AWSGameLiftCreateSessionRequest + //! GameLift create session on fleet request which corresponds to Amazon GameLift + //! CreateGameSessionRequest + struct AWSGameLiftCreateSessionRequest + : public AzFramework::CreateSessionRequest + { + public: + AZ_RTTI(AWSGameLiftCreateSessionRequest, "{69612D5D-F899-4DEB-AD63-4C497ABC5C0D}", AzFramework::CreateSessionRequest); + static void Reflect(AZ::ReflectContext* context); + + AWSGameLiftCreateSessionRequest() = default; + virtual ~AWSGameLiftCreateSessionRequest() = default; + + // A unique identifier for the alias associated with the fleet to create a game session in. + AZStd::string m_aliasId; + + // A unique identifier for the fleet to create a game session in. + AZStd::string m_fleetId; + + // Custom string that uniquely identifies the new game session request. + // This is useful for ensuring that game session requests with the same idempotency token are processed only once. + AZStd::string m_idempotencyToken; + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftJoinSessionRequest.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftJoinSessionRequest.h new file mode 100644 index 0000000000..7bcbbf692b --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftJoinSessionRequest.h @@ -0,0 +1,33 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AWSGameLift +{ + //! AWSGameLiftJoinSessionRequest + //! GameLift join session request which corresponds to Amazon GameLift CreatePlayerSessionRequest. + //! Once player session has been created successfully in game session, gamelift client manager will + //! signal Multiplayer Gem to setup networking connection. + struct AWSGameLiftJoinSessionRequest + : public AzFramework::JoinSessionRequest + { + public: + AZ_RTTI(AWSGameLiftJoinSessionRequest, "{6EED6D15-531A-4956-90D0-2EDA31AC9CBA}", AzFramework::JoinSessionRequest); + static void Reflect(AZ::ReflectContext* context); + + AWSGameLiftJoinSessionRequest() = default; + virtual ~AWSGameLiftJoinSessionRequest() = default; + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSearchSessionsRequest.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSearchSessionsRequest.h new file mode 100644 index 0000000000..5d510a0312 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSearchSessionsRequest.h @@ -0,0 +1,42 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AWSGameLift +{ + //! AWSGameLiftSearchSessionsRequest + //! GameLift search sessions request which corresponds to Amazon GameLift + //! SearchSessionsRequest + struct AWSGameLiftSearchSessionsRequest + : public AzFramework::SearchSessionsRequest + { + public: + AZ_RTTI(AWSGameLiftSearchSessionsRequest, "{864C91C0-CA53-4585-BF07-066C0DF3E198}", AzFramework::SearchSessionsRequest); + static void Reflect(AZ::ReflectContext* context); + + AWSGameLiftSearchSessionsRequest() = default; + virtual ~AWSGameLiftSearchSessionsRequest() = default; + + // A unique identifier for the alias associated with the fleet to search for active game sessions. + AZStd::string m_aliasId; + + // A unique identifier for the fleet to search for active game sessions. + AZStd::string m_fleetId; + + // A fleet location to search for game sessions. + AZStd::string m_location; + }; +} // namespace AWSGameLift + diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h new file mode 100644 index 0000000000..07ac4a3de0 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h @@ -0,0 +1,78 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include + +namespace AWSGameLift +{ + //! IAWSGameLiftRequests + //! GameLift Gem interfaces to configure client manager + class IAWSGameLiftRequests + { + public: + AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}"); + + IAWSGameLiftRequests() = default; + virtual ~IAWSGameLiftRequests() = default; + + //! ConfigureGameLiftClient + //! Configure GameLift client to interact with Amazon GameLift service + //! @param region Specifies the AWS region to use + //! @return True if client configuration succeeds, false otherwise + virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0; + + //! CreatePlayerId + //! Create a new, random ID number for every player in every new game session. + //! @param includeBrackets Whether includes brackets in player id + //! @param includeDashes Whether includes dashes in player id + //! @return The player id to use in game session + virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0; + }; + + // IAWSGameLiftRequests EBus wrapper for scripting + class AWSGameLiftRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftRequestBus = AZ::EBus; + + // ISessionAsyncRequests EBus wrapper for scripting + class AWSGameLiftSessionAsyncRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftSessionAsyncRequestBus = AZ::EBus; + + // ISessionRequests EBus wrapper for scripting + class AWSGameLiftSessionRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftSessionRequestBus = AZ::EBus; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp new file mode 100644 index 0000000000..34c7ab56f9 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp @@ -0,0 +1,360 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace AWSGameLift +{ +#if defined(AWSGAMELIFT_DEV) + AZ_CVAR(AZ::CVarFixedString, cl_gameliftLocalEndpoint, "", nullptr, AZ::ConsoleFunctorFlags::Null, "The local endpoint to test with GameLiftLocal SDK."); +#endif + + AWSGameLiftClientManager::AWSGameLiftClientManager() + { + m_gameliftClient.reset(); + } + + void AWSGameLiftClientManager::ActivateManager() + { + AZ::Interface::Register(this); + AWSGameLiftRequestBus::Handler::BusConnect(); + + AZ::Interface::Register(this); + AWSGameLiftSessionAsyncRequestBus::Handler::BusConnect(); + + AZ::Interface::Register(this); + AWSGameLiftSessionRequestBus::Handler::BusConnect(); + } + + void AWSGameLiftClientManager::DeactivateManager() + { + AWSGameLiftSessionRequestBus::Handler::BusDisconnect(); + AZ::Interface::Unregister(this); + + AWSGameLiftSessionAsyncRequestBus::Handler::BusDisconnect(); + AZ::Interface::Unregister(this); + + AWSGameLiftRequestBus::Handler::BusDisconnect(); + AZ::Interface::Unregister(this); + } + + bool AWSGameLiftClientManager::ConfigureGameLiftClient(const AZStd::string& region) + { + m_gameliftClient.reset(); + + Aws::Client::ClientConfiguration clientConfig; + // Set up client endpoint or region + AZStd::string localEndpoint = ""; +#if defined(AWSGAMELIFT_DEV) + localEndpoint = static_cast(cl_gameliftLocalEndpoint); +#endif + if (!localEndpoint.empty()) + { + // The attribute needs to override to interact with GameLiftLocal + clientConfig.endpointOverride = localEndpoint.c_str(); + } + else if (!region.empty()) + { + clientConfig.region = region.c_str(); + } + else + { + AZStd::string clientRegion; + AWSCore::AWSResourceMappingRequestBus::BroadcastResult(clientRegion, &AWSCore::AWSResourceMappingRequests::GetDefaultRegion); + if (clientRegion.empty()) + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientRegionMissingErrorMessage); + return false; + } + clientConfig.region = clientRegion.c_str(); + } + + // Fetch AWS credential for client + AWSCore::AWSCredentialResult credentialResult; + AWSCore::AWSCredentialRequestBus::BroadcastResult(credentialResult, &AWSCore::AWSCredentialRequests::GetCredentialsProvider); + if (!localEndpoint.empty()) + { + credentialResult.result = std::make_shared(); + } + else if (!credentialResult.result) + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientCredentialMissingErrorMessage); + return false; + } + m_gameliftClient = AZStd::make_shared(credentialResult.result, clientConfig); + return true; + } + + AZStd::string AWSGameLiftClientManager::CreatePlayerId(bool includeBrackets, bool includeDashes) + { + return AZ::Uuid::CreateRandom().ToString(includeBrackets, includeDashes); + } + + AZStd::string AWSGameLiftClientManager::CreateSession(const AzFramework::CreateSessionRequest& createSessionRequest) + { + AZStd::string result = ""; + if (CreateSessionActivity::ValidateCreateSessionRequest(createSessionRequest)) + { + const AWSGameLiftCreateSessionRequest& gameliftCreateSessionRequest = + static_cast(createSessionRequest); + result = CreateSessionHelper(gameliftCreateSessionRequest); + } + else if (CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(createSessionRequest)) + { + const AWSGameLiftCreateSessionOnQueueRequest& gameliftCreateSessionOnQueueRequest = + static_cast(createSessionRequest); + result = CreateSessionOnQueueHelper(gameliftCreateSessionOnQueueRequest); + } + else + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftCreateSessionRequestInvalidErrorMessage); + } + + return result; + } + + void AWSGameLiftClientManager::CreateSessionAsync(const AzFramework::CreateSessionRequest& createSessionRequest) + { + if (CreateSessionActivity::ValidateCreateSessionRequest(createSessionRequest)) + { + const AWSGameLiftCreateSessionRequest& gameliftCreateSessionRequest = + static_cast(createSessionRequest); + + AZ::JobContext* jobContext = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(jobContext, &AWSCore::AWSCoreRequests::GetDefaultJobContext); + AZ::Job* createSessionJob = AZ::CreateJobFunction( + [this, gameliftCreateSessionRequest]() + { + AZStd::string result = CreateSessionHelper(gameliftCreateSessionRequest); + + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnCreateSessionAsyncComplete, result); + }, + true, jobContext); + createSessionJob->Start(); + } + else if (CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(createSessionRequest)) + { + const AWSGameLiftCreateSessionOnQueueRequest& gameliftCreateSessionOnQueueRequest = + static_cast(createSessionRequest); + + AZ::JobContext* jobContext = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(jobContext, &AWSCore::AWSCoreRequests::GetDefaultJobContext); + AZ::Job* createSessionOnQueueJob = AZ::CreateJobFunction( + [this, gameliftCreateSessionOnQueueRequest]() + { + AZStd::string result = CreateSessionOnQueueHelper(gameliftCreateSessionOnQueueRequest); + + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnCreateSessionAsyncComplete, result); + }, + true, jobContext); + createSessionOnQueueJob->Start(); + } + else + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftCreateSessionRequestInvalidErrorMessage); + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnCreateSessionAsyncComplete, ""); + } + } + + AZStd::string AWSGameLiftClientManager::CreateSessionHelper( + const AWSGameLiftCreateSessionRequest& createSessionRequest) + { + AZStd::shared_ptr gameLiftClient = m_gameliftClient; + AZStd::string result = ""; + if (!gameLiftClient) + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientMissingErrorMessage); + } + else + { + result = CreateSessionActivity::CreateSession(*gameLiftClient, createSessionRequest); + } + return result; + } + + AZStd::string AWSGameLiftClientManager::CreateSessionOnQueueHelper( + const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest) + { + AZStd::shared_ptr gameliftClient = m_gameliftClient; + AZStd::string result; + if (!gameliftClient) + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientMissingErrorMessage); + } + else + { + result = CreateSessionOnQueueActivity::CreateSessionOnQueue(*gameliftClient, createSessionOnQueueRequest); + } + return result; + } + + bool AWSGameLiftClientManager::JoinSession(const AzFramework::JoinSessionRequest& joinSessionRequest) + { + bool result = false; + if (JoinSessionActivity::ValidateJoinSessionRequest(joinSessionRequest)) + { + const AWSGameLiftJoinSessionRequest& gameliftJoinSessionRequest = + static_cast(joinSessionRequest); + result = JoinSessionHelper(gameliftJoinSessionRequest); + } + + return result; + } + + void AWSGameLiftClientManager::JoinSessionAsync(const AzFramework::JoinSessionRequest& joinSessionRequest) + { + if (!JoinSessionActivity::ValidateJoinSessionRequest(joinSessionRequest)) + { + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnJoinSessionAsyncComplete, false); + return; + } + + const AWSGameLiftJoinSessionRequest& gameliftJoinSessionRequest = + static_cast(joinSessionRequest); + + AZ::JobContext* jobContext = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(jobContext, &AWSCore::AWSCoreRequests::GetDefaultJobContext); + AZ::Job* joinSessionJob = AZ::CreateJobFunction( + [this, gameliftJoinSessionRequest]() + { + bool result = JoinSessionHelper(gameliftJoinSessionRequest); + + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnJoinSessionAsyncComplete, result); + }, + true, jobContext); + + joinSessionJob->Start(); + } + + bool AWSGameLiftClientManager::JoinSessionHelper(const AWSGameLiftJoinSessionRequest& joinSessionRequest) + { + AZStd::shared_ptr gameliftClient = m_gameliftClient; + bool result = false; + if (!gameliftClient) + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientMissingErrorMessage); + } + else + { + auto createPlayerSessionOutcome = JoinSessionActivity::CreatePlayerSession(*gameliftClient, joinSessionRequest); + + result = JoinSessionActivity::RequestPlayerJoinSession(createPlayerSessionOutcome); + } + return result; + } + + void AWSGameLiftClientManager::LeaveSession() + { + AWSGameLift::LeaveSessionActivity::LeaveSession(); + } + + void AWSGameLiftClientManager::LeaveSessionAsync() + { + AZ::JobContext* jobContext = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(jobContext, &AWSCore::AWSCoreRequests::GetDefaultJobContext); + AZ::Job* leaveSessionJob = AZ::CreateJobFunction( + [this]() + { + LeaveSession(); + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnLeaveSessionAsyncComplete); + }, + true, jobContext); + + leaveSessionJob->Start(); + } + + AzFramework::SearchSessionsResponse AWSGameLiftClientManager::SearchSessions( + const AzFramework::SearchSessionsRequest& searchSessionsRequest) const + { + AzFramework::SearchSessionsResponse response; + if (SearchSessionsActivity::ValidateSearchSessionsRequest(searchSessionsRequest)) + { + const AWSGameLiftSearchSessionsRequest& gameliftSearchSessionsRequest = + static_cast(searchSessionsRequest); + response = SearchSessionsHelper(gameliftSearchSessionsRequest); + } + + return response; + } + + void AWSGameLiftClientManager::SearchSessionsAsync(const AzFramework::SearchSessionsRequest& searchSessionsRequest) const + { + if (!SearchSessionsActivity::ValidateSearchSessionsRequest(searchSessionsRequest)) + { + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnSearchSessionsAsyncComplete, AzFramework::SearchSessionsResponse()); + return; + } + + const AWSGameLiftSearchSessionsRequest& gameliftSearchSessionsRequest = + static_cast(searchSessionsRequest); + + AZ::JobContext* jobContext = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(jobContext, &AWSCore::AWSCoreRequests::GetDefaultJobContext); + AZ::Job* searchSessionsJob = AZ::CreateJobFunction( + [this, gameliftSearchSessionsRequest]() + { + AzFramework::SearchSessionsResponse response = SearchSessionsHelper(gameliftSearchSessionsRequest); + + AzFramework::SessionAsyncRequestNotificationBus::Broadcast( + &AzFramework::SessionAsyncRequestNotifications::OnSearchSessionsAsyncComplete, response); + }, + true, jobContext); + + searchSessionsJob->Start(); + } + + AzFramework::SearchSessionsResponse AWSGameLiftClientManager::SearchSessionsHelper( + const AWSGameLiftSearchSessionsRequest& searchSessionsRequest) const + { + AZStd::shared_ptr gameliftClient = m_gameliftClient; + + AzFramework::SearchSessionsResponse response; + if (!gameliftClient) + { + AZ_Error(AWSGameLiftClientManagerName, false, AWSGameLiftClientMissingErrorMessage); + } + else + { + response = SearchSessionsActivity::SearchSessions(*gameliftClient, searchSessionsRequest); + } + return response; + } + + void AWSGameLiftClientManager::SetGameLiftClient(AZStd::shared_ptr gameliftClient) + { + m_gameliftClient.swap(gameliftClient); + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h new file mode 100644 index 0000000000..ef128b6562 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h @@ -0,0 +1,122 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include + +namespace Aws +{ + namespace GameLift + { + class GameLiftClient; + } +} + +namespace AWSGameLift +{ + struct AWSGameLiftCreateSessionRequest; + struct AWSGameLiftCreateSessionOnQueueRequest; + struct AWSGameLiftJoinSessionRequest; + struct AWSGameLiftSearchSessionsRequest; + + // SessionAsyncRequestNotificationBus EBus handler for scripting + class AWSGameLiftSessionAsyncRequestNotificationBusHandler + : public AzFramework::SessionAsyncRequestNotificationBus::Handler + , public AZ::BehaviorEBusHandler + { + public: + AZ_EBUS_BEHAVIOR_BINDER( + AWSGameLiftSessionAsyncRequestNotificationBusHandler, + "{6E13FC73-53DC-4B6B-AEA7-9038DE4C9635}", + AZ::SystemAllocator, + OnCreateSessionAsyncComplete, + OnSearchSessionsAsyncComplete, + OnJoinSessionAsyncComplete, + OnLeaveSessionAsyncComplete); + + void OnCreateSessionAsyncComplete(const AZStd::string& createSessionReponse) override + { + Call(FN_OnCreateSessionAsyncComplete, createSessionReponse); + } + + void OnSearchSessionsAsyncComplete(const AzFramework::SearchSessionsResponse& searchSessionsResponse) override + { + Call(FN_OnSearchSessionsAsyncComplete, searchSessionsResponse); + } + + void OnJoinSessionAsyncComplete(bool joinSessionsResponse) override + { + Call(FN_OnJoinSessionAsyncComplete, joinSessionsResponse); + } + + void OnLeaveSessionAsyncComplete() override + { + Call(FN_OnLeaveSessionAsyncComplete); + } + }; + + //! AWSGameLiftClientManager + //! GameLift client manager to support game and player session related client requests + class AWSGameLiftClientManager + : public AWSGameLiftRequestBus::Handler + , public AWSGameLiftSessionAsyncRequestBus::Handler + , public AWSGameLiftSessionRequestBus::Handler + { + public: + static constexpr const char AWSGameLiftClientManagerName[] = "AWSGameLiftClientManager"; + static constexpr const char AWSGameLiftClientRegionMissingErrorMessage[] = + "Missing AWS region for GameLift client."; + static constexpr const char AWSGameLiftClientCredentialMissingErrorMessage[] = + "Missing AWS credential for GameLift client."; + static constexpr const char AWSGameLiftClientMissingErrorMessage[] = + "GameLift client is not configured yet."; + + static constexpr const char AWSGameLiftCreateSessionRequestInvalidErrorMessage[] = + "Invalid GameLift CreateSession or CreateSessionOnQueue request."; + + AWSGameLiftClientManager(); + virtual ~AWSGameLiftClientManager() = default; + + virtual void ActivateManager(); + virtual void DeactivateManager(); + + // AWSGameLiftRequestBus interface implementation + bool ConfigureGameLiftClient(const AZStd::string& region) override; + AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) override; + + // AWSGameLiftSessionAsyncRequestBus interface implementation + void CreateSessionAsync(const AzFramework::CreateSessionRequest& createSessionRequest) override; + void JoinSessionAsync(const AzFramework::JoinSessionRequest& joinSessionRequest) override; + void SearchSessionsAsync(const AzFramework::SearchSessionsRequest& searchSessionsRequest) const override; + void LeaveSessionAsync() override; + + // AWSGameLiftSessionRequestBus interface implementation + AZStd::string CreateSession(const AzFramework::CreateSessionRequest& createSessionRequest) override; + bool JoinSession(const AzFramework::JoinSessionRequest& joinSessionRequest) override; + AzFramework::SearchSessionsResponse SearchSessions(const AzFramework::SearchSessionsRequest& searchSessionsRequest) const override; + void LeaveSession() override; + + protected: + // Use for automation tests only to inject mock objects. + void SetGameLiftClient(AZStd::shared_ptr gameliftClient); + + private: + AZStd::string CreateSessionHelper(const AWSGameLiftCreateSessionRequest& createSessionRequest); + AZStd::string CreateSessionOnQueueHelper(const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest); + bool JoinSessionHelper(const AWSGameLiftJoinSessionRequest& joinSessionRequest); + AzFramework::SearchSessionsResponse SearchSessionsHelper(const AWSGameLiftSearchSessionsRequest& searchSessionsRequest) const; + + AZStd::shared_ptr m_gameliftClient; + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientModule.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientModule.cpp new file mode 100644 index 0000000000..dc3c8b80f0 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientModule.cpp @@ -0,0 +1,49 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include + +namespace AWSGameLift +{ + //! Provide the entry point for the gem and register the system component. + class AWSGameLiftClientModule + : public AZ::Module + { + public: + AZ_RTTI(AWSGameLiftClientModule, "{7b920f3e-2b23-482e-a1b6-16bd278d126c}", AZ::Module); + AZ_CLASS_ALLOCATOR(AWSGameLiftClientModule, AZ::SystemAllocator, 0); + + AWSGameLiftClientModule() + : AZ::Module() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + m_descriptors.insert(m_descriptors.end(), { + AWSGameLiftClientSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList { + azrtti_typeid(), + }; + } + }; +}// namespace AWSGameLift + +AZ_DECLARE_MODULE_CLASS(Gem_AWSGameLift_Client, AWSGameLift::AWSGameLiftClientModule) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp new file mode 100644 index 0000000000..18a0ae739e --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp @@ -0,0 +1,186 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace AWSGameLift +{ + AWSGameLiftClientSystemComponent::AWSGameLiftClientSystemComponent() + { + m_gameliftClientManager = AZStd::make_unique(); + } + + void AWSGameLiftClientSystemComponent::Reflect(AZ::ReflectContext* context) + { + ReflectCreateSessionRequest(context); + AWSGameLiftCreateSessionOnQueueRequest::Reflect(context); + AWSGameLiftCreateSessionRequest::Reflect(context); + AWSGameLiftJoinSessionRequest::Reflect(context); + AWSGameLiftSearchSessionsRequest::Reflect(context); + ReflectSearchSessionsResponse(context); + + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ; + + if (AZ::EditContext* editContext = serialize->GetEditContext()) + { + editContext + ->Class( + "AWSGameLiftClient", + "Create the GameLift client manager that handles communication between game clients and the GameLift service.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ; + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->EBus("AWSGameLiftRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Event("ConfigureGameLiftClient", &AWSGameLiftRequestBus::Events::ConfigureGameLiftClient, + {{{"Region", ""}}}) + ->Event("CreatePlayerId", &AWSGameLiftRequestBus::Events::CreatePlayerId, + {{{"IncludeBrackets", ""}, + {"IncludeDashes", ""}}}) + ; + behaviorContext->EBus("AWSGameLiftSessionAsyncRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Event("CreateSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::CreateSessionAsync, + {{{"CreateSessionRequest", ""}}}) + ->Event("JoinSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::JoinSessionAsync, + {{{"JoinSessionRequest", ""}}}) + ->Event("SearchSessionsAsync", &AWSGameLiftSessionAsyncRequestBus::Events::SearchSessionsAsync, + {{{"SearchSessionsRequest", ""}}}) + ->Event("LeaveSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::LeaveSessionAsync) + ; + behaviorContext + ->EBus("AWSGameLiftSessionAsyncRequestNotificationBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Handler() + ; + behaviorContext->EBus("AWSGameLiftSessionRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Event("CreateSession", &AWSGameLiftSessionRequestBus::Events::CreateSession, + {{{"CreateSessionRequest", ""}}}) + ->Event("JoinSession", &AWSGameLiftSessionRequestBus::Events::JoinSession, + {{{"JoinSessionRequest", ""}}}) + ->Event("SearchSessions", &AWSGameLiftSessionRequestBus::Events::SearchSessions, + {{{"SearchSessionsRequest", ""}}}) + ->Event("LeaveSession", &AWSGameLiftSessionRequestBus::Events::LeaveSession) + ; + } + } + + void AWSGameLiftClientSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("AWSGameLiftClientService")); + } + + void AWSGameLiftClientSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("AWSGameLiftClientService")); + } + + void AWSGameLiftClientSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("AWSCoreService")); + } + + void AWSGameLiftClientSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + AZ_UNUSED(dependent); + } + + void AWSGameLiftClientSystemComponent::Init() + { + } + + void AWSGameLiftClientSystemComponent::Activate() + { + m_gameliftClientManager->ActivateManager(); + } + + void AWSGameLiftClientSystemComponent::Deactivate() + { + m_gameliftClientManager->DeactivateManager(); + } + + void AWSGameLiftClientSystemComponent::ReflectCreateSessionRequest(AZ::ReflectContext* context) + { + AzFramework::CreateSessionRequest::Reflect(context); + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("CreateSessionRequest") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + // Expose base type to BehaviorContext, but hide it to be used directly + ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All) + ; + } + } + + void AWSGameLiftClientSystemComponent::ReflectSearchSessionsResponse(AZ::ReflectContext* context) + { + // As it is a common response type, reflection could be moved to AzFramework to avoid duplication + AzFramework::SessionConfig::Reflect(context); + AzFramework::SearchSessionsResponse::Reflect(context); + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("SessionConfig") + ->Attribute(AZ::Script::Attributes::Category, "Session") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("CreationTime", BehaviorValueProperty(&AzFramework::SessionConfig::m_creationTime)) + ->Property("CreatorId", BehaviorValueProperty(&AzFramework::SessionConfig::m_creatorId)) + ->Property("CurrentPlayer", BehaviorValueProperty(&AzFramework::SessionConfig::m_currentPlayer)) + ->Property("DnsName", BehaviorValueProperty(&AzFramework::SessionConfig::m_dnsName)) + ->Property("IpAddress", BehaviorValueProperty(&AzFramework::SessionConfig::m_ipAddress)) + ->Property("MaxPlayer", BehaviorValueProperty(&AzFramework::SessionConfig::m_maxPlayer)) + ->Property("Port", BehaviorValueProperty(&AzFramework::SessionConfig::m_port)) + ->Property("SessionId", BehaviorValueProperty(&AzFramework::SessionConfig::m_sessionId)) + ->Property("SessionName", BehaviorValueProperty(&AzFramework::SessionConfig::m_sessionName)) + ->Property("SessionProperties", BehaviorValueProperty(&AzFramework::SessionConfig::m_sessionProperties)) + ->Property("Status", BehaviorValueProperty(&AzFramework::SessionConfig::m_status)) + ->Property("StatusReason", BehaviorValueProperty(&AzFramework::SessionConfig::m_statusReason)) + ->Property("TerminationTime", BehaviorValueProperty(&AzFramework::SessionConfig::m_terminationTime)) + ; + behaviorContext->Class("SearchSessionsResponse") + ->Attribute(AZ::Script::Attributes::Category, "Session") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("NextToken", BehaviorValueProperty(&AzFramework::SearchSessionsResponse::m_nextToken)) + ->Property("SessionConfigs", BehaviorValueProperty(&AzFramework::SearchSessionsResponse::m_sessionConfigs)) + ; + } + } + + void AWSGameLiftClientSystemComponent::SetGameLiftClientManager(AZStd::unique_ptr gameliftClientManager) + { + m_gameliftClientManager.reset(); + m_gameliftClientManager = AZStd::move(gameliftClientManager); + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.h new file mode 100644 index 0000000000..220eb00ba6 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.h @@ -0,0 +1,56 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include + +namespace AWSGameLift +{ + class AWSGameLiftClientManager; + + //! Gem client system component. Responsible for creating the gamelift client manager. + class AWSGameLiftClientSystemComponent + : public AZ::Component + { + public: + AZ_COMPONENT(AWSGameLiftClientSystemComponent, "{d481c15c-732a-4eea-9853-4965ed1bc2be}"); + + AWSGameLiftClientSystemComponent(); + virtual ~AWSGameLiftClientSystemComponent() = default; + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + protected: + //////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + void Deactivate() override; + //////////////////////////////////////////////////////////////////////// + + void SetGameLiftClientManager(AZStd::unique_ptr gameliftClientManager); + + private: + static void ReflectCreateSessionRequest(AZ::ReflectContext* context); + static void ReflectSearchSessionsResponse(AZ::ReflectContext* context); + + AZStd::unique_ptr m_gameliftClientManager; + }; + +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.cpp new file mode 100644 index 0000000000..67b110cf33 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.cpp @@ -0,0 +1,90 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +namespace AWSGameLift +{ + namespace CreateSessionActivity + { + Aws::GameLift::Model::CreateGameSessionRequest BuildAWSGameLiftCreateGameSessionRequest( + const AWSGameLiftCreateSessionRequest& createSessionRequest) + { + Aws::GameLift::Model::CreateGameSessionRequest request; + // Optional attributes + if (!createSessionRequest.m_creatorId.empty()) + { + request.SetCreatorId(createSessionRequest.m_creatorId.c_str()); + } + if (!createSessionRequest.m_sessionName.empty()) + { + request.SetName(createSessionRequest.m_sessionName.c_str()); + } + if (!createSessionRequest.m_idempotencyToken.empty()) + { + request.SetIdempotencyToken(createSessionRequest.m_idempotencyToken.c_str()); + } + for (auto iter = createSessionRequest.m_sessionProperties.begin(); + iter != createSessionRequest.m_sessionProperties.end(); iter++) + { + Aws::GameLift::Model::GameProperty sessionProperty; + sessionProperty.SetKey(iter->first.c_str()); + sessionProperty.SetValue(iter->second.c_str()); + request.AddGameProperties(sessionProperty); + } + + // Required attributes + if (!createSessionRequest.m_aliasId.empty()) + { + request.SetAliasId(createSessionRequest.m_aliasId.c_str()); + } + if (!createSessionRequest.m_fleetId.empty()) + { + request.SetFleetId(createSessionRequest.m_fleetId.c_str()); + } + request.SetMaximumPlayerSessionCount(createSessionRequest.m_maxPlayer); + + return request; + } + + AZStd::string CreateSession( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftCreateSessionRequest& createSessionRequest) + { + AZ_TracePrintf(AWSGameLiftCreateSessionActivityName, "Requesting CreateGameSession against Amazon GameLift service ..."); + + AZStd::string result = ""; + Aws::GameLift::Model::CreateGameSessionRequest request = BuildAWSGameLiftCreateGameSessionRequest(createSessionRequest); + auto createSessionOutcome = gameliftClient.CreateGameSession(request); + + if (createSessionOutcome.IsSuccess()) + { + result = AZStd::string(createSessionOutcome.GetResult().GetGameSession().GetGameSessionId().c_str()); + } + else + { + AZ_Error(AWSGameLiftCreateSessionActivityName, false, AWSGameLiftErrorMessageTemplate, + createSessionOutcome.GetError().GetExceptionName().c_str(), createSessionOutcome.GetError().GetMessage().c_str()); + } + return result; + } + + bool ValidateCreateSessionRequest(const AzFramework::CreateSessionRequest& createSessionRequest) + { + auto gameliftCreateSessionRequest = azrtti_cast(&createSessionRequest); + + return gameliftCreateSessionRequest && gameliftCreateSessionRequest->m_maxPlayer >= 0 && + (!gameliftCreateSessionRequest->m_aliasId.empty() || !gameliftCreateSessionRequest->m_fleetId.empty()); + } + } // namespace CreateSessionActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.h new file mode 100644 index 0000000000..236ae3f08d --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionActivity.h @@ -0,0 +1,39 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include +#include +#include + +namespace AWSGameLift +{ + namespace CreateSessionActivity + { + static constexpr const char AWSGameLiftCreateSessionActivityName[] = "AWSGameLiftCreateSessionActivity"; + + // Build AWS GameLift CreateGameSessionRequest by using AWSGameLiftCreateSessionRequest + Aws::GameLift::Model::CreateGameSessionRequest BuildAWSGameLiftCreateGameSessionRequest(const AWSGameLiftCreateSessionRequest& createSessionRequest); + + // Create CreateGameSessionRequest and make a CreateGameSession call through GameLift client + AZStd::string CreateSession( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftCreateSessionRequest& createSessionRequest); + + // Validate CreateSessionRequest and check required request parameters + bool ValidateCreateSessionRequest(const AzFramework::CreateSessionRequest& createSessionRequest); + + } // namespace CreateSessionActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.cpp new file mode 100644 index 0000000000..994d5e0281 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.cpp @@ -0,0 +1,80 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +namespace AWSGameLift +{ + namespace CreateSessionOnQueueActivity + { + Aws::GameLift::Model::StartGameSessionPlacementRequest BuildAWSGameLiftStartGameSessionPlacementRequest( + const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest) + { + Aws::GameLift::Model::StartGameSessionPlacementRequest request; + // Optional attributes + if (!createSessionOnQueueRequest.m_sessionName.empty()) + { + request.SetGameSessionName(createSessionOnQueueRequest.m_sessionName.c_str()); + } + for (auto iter = createSessionOnQueueRequest.m_sessionProperties.begin(); + iter != createSessionOnQueueRequest.m_sessionProperties.end(); iter++) + { + Aws::GameLift::Model::GameProperty sessionProperty; + sessionProperty.SetKey(iter->first.c_str()); + sessionProperty.SetValue(iter->second.c_str()); + request.AddGameProperties(sessionProperty); + } + + // Required attributes + request.SetGameSessionQueueName(createSessionOnQueueRequest.m_queueName.c_str()); + request.SetMaximumPlayerSessionCount(createSessionOnQueueRequest.m_maxPlayer); + request.SetPlacementId(createSessionOnQueueRequest.m_placementId.c_str()); + + return request; + } + + AZStd::string CreateSessionOnQueue( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest) + { + AZ_TracePrintf(AWSGameLiftCreateSessionOnQueueActivityName, + "Requesting StartGameSessionPlacement against Amazon GameLift service ..."); + + AZStd::string result = ""; + Aws::GameLift::Model::StartGameSessionPlacementRequest request = + BuildAWSGameLiftStartGameSessionPlacementRequest(createSessionOnQueueRequest); + auto createSessionOnQueueOutcome = gameliftClient.StartGameSessionPlacement(request); + + if (createSessionOnQueueOutcome.IsSuccess()) + { + result = AZStd::string(createSessionOnQueueOutcome.GetResult().GetGameSessionPlacement().GetPlacementId().c_str()); + } + else + { + AZ_Error(AWSGameLiftCreateSessionOnQueueActivityName, false, AWSGameLiftErrorMessageTemplate, + createSessionOnQueueOutcome.GetError().GetExceptionName().c_str(), + createSessionOnQueueOutcome.GetError().GetMessage().c_str()); + } + return result; + } + + bool ValidateCreateSessionOnQueueRequest(const AzFramework::CreateSessionRequest& createSessionRequest) + { + auto gameliftCreateSessionOnQueueRequest = + azrtti_cast(&createSessionRequest); + + return gameliftCreateSessionOnQueueRequest && gameliftCreateSessionOnQueueRequest->m_maxPlayer >= 0 && + !gameliftCreateSessionOnQueueRequest->m_queueName.empty() && !gameliftCreateSessionOnQueueRequest->m_placementId.empty(); + } + } // namespace CreateSessionOnQueueActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.h new file mode 100644 index 0000000000..1cb337650d --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.h @@ -0,0 +1,40 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include +#include +#include + +namespace AWSGameLift +{ + namespace CreateSessionOnQueueActivity + { + static constexpr const char AWSGameLiftCreateSessionOnQueueActivityName[] = "AWSGameLiftCreateSessionOnQueueActivity"; + + // Build AWS GameLift StartGameSessionPlacementRequest by using AWSGameLiftCreateSessionOnQueueRequest + Aws::GameLift::Model::StartGameSessionPlacementRequest BuildAWSGameLiftStartGameSessionPlacementRequest( + const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest); + + // Create StartGameSessionPlacementRequest and make a CreateGameSession call through GameLift client + AZStd::string CreateSessionOnQueue( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftCreateSessionOnQueueRequest& createSessionOnQueueRequest); + + // Validate CreateSessionOnQueueRequest and check required request parameters + bool ValidateCreateSessionOnQueueRequest(const AzFramework::CreateSessionRequest& createSessionRequest); + + } // namespace CreateSessionOnQueueActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.cpp new file mode 100644 index 0000000000..f4deebd8e6 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.cpp @@ -0,0 +1,112 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include + +namespace AWSGameLift +{ + namespace JoinSessionActivity + { + Aws::GameLift::Model::CreatePlayerSessionRequest BuildAWSGameLiftCreatePlayerSessionRequest( + const AWSGameLiftJoinSessionRequest& joinSessionRequest) + { + Aws::GameLift::Model::CreatePlayerSessionRequest request; + // Optional attributes + if (!joinSessionRequest.m_playerData.empty()) + { + request.SetPlayerData(joinSessionRequest.m_playerData.c_str()); + } + // Required attributes + request.SetPlayerId(joinSessionRequest.m_playerId.c_str()); + request.SetGameSessionId(joinSessionRequest.m_sessionId.c_str()); + return request; + } + + AzFramework::SessionConnectionConfig BuildSessionConnectionConfig( + const Aws::GameLift::Model::CreatePlayerSessionOutcome& createPlayerSessionOutcome) + { + AzFramework::SessionConnectionConfig sessionConnectionConfig; + auto createPlayerSessionResult = createPlayerSessionOutcome.GetResult(); + // TODO: AWSNativeSDK needs to be updated to support this attribute, and it is a must have for TLS certificate enabled fleet + //sessionConnectionConfig.m_dnsName = createPlayerSessionResult.GetPlayerSession().GetDnsName().c_str(); + sessionConnectionConfig.m_ipAddress = createPlayerSessionResult.GetPlayerSession().GetIpAddress().c_str(); + sessionConnectionConfig.m_playerSessionId = createPlayerSessionResult.GetPlayerSession().GetPlayerSessionId().c_str(); + sessionConnectionConfig.m_port = createPlayerSessionResult.GetPlayerSession().GetPort(); + return sessionConnectionConfig; + } + + Aws::GameLift::Model::CreatePlayerSessionOutcome CreatePlayerSession( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftJoinSessionRequest& joinSessionRequest) + { + AZ_TracePrintf(AWSGameLiftJoinSessionActivityName, + "Requesting CreatePlayerSession for player %s against Amazon GameLift service ...", + joinSessionRequest.m_playerId.c_str()); + + Aws::GameLift::Model::CreatePlayerSessionRequest request = + BuildAWSGameLiftCreatePlayerSessionRequest(joinSessionRequest); + auto createPlayerSessionOutcome = gameliftClient.CreatePlayerSession(request); + + if (!createPlayerSessionOutcome.IsSuccess()) + { + AZ_Error(AWSGameLiftJoinSessionActivityName, false, AWSGameLiftErrorMessageTemplate, + createPlayerSessionOutcome.GetError().GetExceptionName().c_str(), + createPlayerSessionOutcome.GetError().GetMessage().c_str()); + } + return createPlayerSessionOutcome; + } + + bool RequestPlayerJoinSession(const Aws::GameLift::Model::CreatePlayerSessionOutcome& createPlayerSessionOutcome) + { + bool result = false; + if (createPlayerSessionOutcome.IsSuccess()) + { + auto clientRequestHandler = AZ::Interface::Get(); + if (clientRequestHandler) + { + AZ_TracePrintf(AWSGameLiftJoinSessionActivityName, "Requesting player to connect to game session ..."); + + AzFramework::SessionConnectionConfig sessionConnectionConfig = + BuildSessionConnectionConfig(createPlayerSessionOutcome); + result = clientRequestHandler->RequestPlayerJoinSession(sessionConnectionConfig); + } + else + { + AZ_Error(AWSGameLiftJoinSessionActivityName, false, AWSGameLiftJoinSessionMissingRequestHandlerErrorMessage); + } + } + return result; + } + + bool ValidateJoinSessionRequest(const AzFramework::JoinSessionRequest& joinSessionRequest) + { + auto gameliftJoinSessionRequest = azrtti_cast(&joinSessionRequest); + + if (gameliftJoinSessionRequest && + !gameliftJoinSessionRequest->m_playerId.empty() && + !gameliftJoinSessionRequest->m_sessionId.empty()) + { + return true; + } + else + { + AZ_Error(AWSGameLiftJoinSessionActivityName, false, AWSGameLiftJoinSessionRequestInvalidErrorMessage); + + return false; + } + } + } // namespace JoinSessionActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.h new file mode 100644 index 0000000000..abb372df16 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftJoinSessionActivity.h @@ -0,0 +1,54 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include + +#include +#include +#include + +namespace AWSGameLift +{ + namespace JoinSessionActivity + { + static constexpr const char AWSGameLiftJoinSessionActivityName[] = "AWSGameLiftJoinSessionActivity"; + static constexpr const char AWSGameLiftJoinSessionRequestInvalidErrorMessage[] = + "Invalid GameLift JoinSession request."; + static constexpr const char AWSGameLiftJoinSessionMissingRequestHandlerErrorMessage[] = + "Missing GameLift JoinSession request handler, please make sure Multiplayer Gem is enabled and registered as handler."; + + // Build AWS GameLift CreatePlayerSessionRequest by using AWSGameLiftJoinSessionRequest + Aws::GameLift::Model::CreatePlayerSessionRequest BuildAWSGameLiftCreatePlayerSessionRequest( + const AWSGameLiftJoinSessionRequest& joinSessionRequest); + + // Build session connection config by using CreatePlayerSessionOutcome + AzFramework::SessionConnectionConfig BuildSessionConnectionConfig( + const Aws::GameLift::Model::CreatePlayerSessionOutcome& createPlayerSessionOutcome); + + // Create CreatePlayerSessionRequest and make a CreatePlayerSession call through GameLift client + Aws::GameLift::Model::CreatePlayerSessionOutcome CreatePlayerSession( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftJoinSessionRequest& joinSessionRequest); + + // Request to setup networking connection for player + bool RequestPlayerJoinSession( + const Aws::GameLift::Model::CreatePlayerSessionOutcome& createPlayerSessionOutcome); + + // Validate JoinSessionRequest and check required request parameters + bool ValidateJoinSessionRequest(const AzFramework::JoinSessionRequest& joinSessionRequest); + + } // namespace JoinSessionActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.cpp new file mode 100644 index 0000000000..b6d4697a7d --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.cpp @@ -0,0 +1,38 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include + +namespace AWSGameLift +{ + namespace LeaveSessionActivity + { + void LeaveSession() + { + auto clientRequestHandler = AZ::Interface::Get(); + if (clientRequestHandler) + { + AZ_TracePrintf(AWSGameLiftLeaveSessionActivityName, "Requesting to leave the current session..."); + + clientRequestHandler->RequestPlayerLeaveSession(); + } + else + { + AZ_Error(AWSGameLiftLeaveSessionActivityName, false, AWSGameLiftLeaveSessionMissingRequestHandlerErrorMessage); + } + } + + } // namespace LeaveSessionActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.h new file mode 100644 index 0000000000..59b9fea055 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftLeaveSessionActivity.h @@ -0,0 +1,27 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +namespace AWSGameLift +{ + namespace LeaveSessionActivity + { + static constexpr const char AWSGameLiftLeaveSessionActivityName[] = "AWSGameLiftLeaveSessionActivity"; + static constexpr const char AWSGameLiftLeaveSessionMissingRequestHandlerErrorMessage[] = + "Missing GameLift LeaveSession request handler, please make sure Multiplayer Gem is enabled and registered as handler."; + + // Request to leave the current session + void LeaveSession(); + } // namespace LeaveSessionActivity +} // namespace AWSGameLift + diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp new file mode 100644 index 0000000000..3d64c72f8d --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.cpp @@ -0,0 +1,130 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include + +namespace AWSGameLift +{ + namespace SearchSessionsActivity + { + Aws::GameLift::Model::SearchGameSessionsRequest BuildAWSGameLiftSearchGameSessionsRequest( + const AWSGameLiftSearchSessionsRequest& searchSessionsRequest) + { + Aws::GameLift::Model::SearchGameSessionsRequest request; + // Optional attributes + if (!searchSessionsRequest.m_filterExpression.empty()) + { + request.SetFilterExpression(searchSessionsRequest.m_filterExpression.c_str()); + } + if (!searchSessionsRequest.m_sortExpression.empty()) + { + request.SetSortExpression(searchSessionsRequest.m_sortExpression.c_str()); + } + if (searchSessionsRequest.m_maxResult > 0) + { + request.SetLimit(searchSessionsRequest.m_maxResult); + } + if (!searchSessionsRequest.m_nextToken.empty()) + { + request.SetNextToken(searchSessionsRequest.m_nextToken.c_str()); + } + // Required attributes + if (!searchSessionsRequest.m_aliasId.empty()) + { + request.SetAliasId(searchSessionsRequest.m_aliasId.c_str()); + } + if (!searchSessionsRequest.m_fleetId.empty()) + { + request.SetFleetId(searchSessionsRequest.m_fleetId.c_str()); + } + // TODO: Update the AWS Native SDK to accept the new request parameter. + //request.SetLocation(searchSessionsRequest.m_location.c_str()); + return request; + } + + AzFramework::SearchSessionsResponse SearchSessions( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftSearchSessionsRequest& searchSessionsRequest) + { + AZ_TracePrintf(AWSGameLiftSearchSessionsActivityName, "Requesting SearchGameSessions against Amazon GameLift service ..."); + + AzFramework::SearchSessionsResponse response; + Aws::GameLift::Model::SearchGameSessionsRequest request = BuildAWSGameLiftSearchGameSessionsRequest(searchSessionsRequest); + Aws::GameLift::Model::SearchGameSessionsOutcome outcome = gameliftClient.SearchGameSessions(request); + + if (outcome.IsSuccess()) + { + response = SearchSessionsActivity::ParseResponse(outcome.GetResult()); + } + else + { + AZ_Error(AWSGameLiftSearchSessionsActivityName, false, AWSGameLiftErrorMessageTemplate, + outcome.GetError().GetExceptionName().c_str(), outcome.GetError().GetMessage().c_str()); + } + + return response; + } + + AzFramework::SearchSessionsResponse ParseResponse( + const Aws::GameLift::Model::SearchGameSessionsResult& gameLiftSearchSessionsResult) + { + AzFramework::SearchSessionsResponse response; + response.m_nextToken = gameLiftSearchSessionsResult.GetNextToken().c_str(); + + for (const Aws::GameLift::Model::GameSession& gameSession : gameLiftSearchSessionsResult.GetGameSessions()) + { + AzFramework::SessionConfig session; + session.m_creationTime = gameSession.GetCreationTime().Millis(); + session.m_creatorId = gameSession.GetCreatorId().c_str(); + session.m_currentPlayer = gameSession.GetCurrentPlayerSessionCount(); + session.m_ipAddress = gameSession.GetIpAddress().c_str(); + session.m_maxPlayer = gameSession.GetMaximumPlayerSessionCount(); + session.m_port = gameSession.GetPort(); + session.m_sessionId = gameSession.GetGameSessionId().c_str(); + session.m_sessionName = gameSession.GetName().c_str(); + session.m_status = AWSGameLiftSessionStatusNames[(int)gameSession.GetStatus()]; + session.m_statusReason = AWSGameLiftSessionStatusReasons[(int)gameSession.GetStatusReason()]; + session.m_terminationTime = gameSession.GetTerminationTime().Millis(); + // TODO: Update the AWS Native SDK to get the new game session attributes. + //session.m_dnsName = gameSession.GetDnsName(); + + for (const auto& gameProperty : gameSession.GetGameProperties()) + { + session.m_sessionProperties[gameProperty.GetKey().c_str()] = gameProperty.GetValue().c_str(); + } + + response.m_sessionConfigs.emplace_back(AZStd::move(session)); + } + + return response; + }; + + bool ValidateSearchSessionsRequest(const AzFramework::SearchSessionsRequest& searchSessionsRequest) + { + auto gameliftSearchSessionsRequest = azrtti_cast(&searchSessionsRequest); + if (gameliftSearchSessionsRequest && + (!gameliftSearchSessionsRequest->m_aliasId.empty() || !gameliftSearchSessionsRequest->m_fleetId.empty())) + { + return true; + } + else + { + AZ_Error(AWSGameLiftSearchSessionsActivityName, false, AWSGameLiftSearchSessionsRequestInvalidErrorMessage); + + return false; + } + } + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.h new file mode 100644 index 0000000000..da755bda71 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Activity/AWSGameLiftSearchSessionsActivity.h @@ -0,0 +1,45 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include +#include +#include + +namespace AWSGameLift +{ + namespace SearchSessionsActivity + { + static constexpr const char AWSGameLiftSearchSessionsActivityName[] = "AWSGameLiftSearchSessionsActivity"; + static constexpr const char AWSGameLiftSearchSessionsRequestInvalidErrorMessage[] = + "Invalid GameLift SearchSessions request."; + + // Build AWS GameLift SearchGameSessionsRequest by using AWSGameLiftSearchSessionsRequest + Aws::GameLift::Model::SearchGameSessionsRequest BuildAWSGameLiftSearchGameSessionsRequest( + const AWSGameLiftSearchSessionsRequest& searchSessionsRequest); + + // Create SearchGameSessionsRequest and make a SeachGameSessions call through GameLift client + AzFramework::SearchSessionsResponse SearchSessions( + const Aws::GameLift::GameLiftClient& gameliftClient, + const AWSGameLiftSearchSessionsRequest& searchSessionsRequest); + + // Convert from Aws::GameLift::Model::SearchGameSessionsResult to AzFramework::SearchSessionsResponse. + AzFramework::SearchSessionsResponse ParseResponse( + const Aws::GameLift::Model::SearchGameSessionsResult& gameLiftSearchSessionsResult); + + // Validate SearchSessionsRequest and check required request parameters + bool ValidateSearchSessionsRequest(const AzFramework::SearchSessionsRequest& searchSessionsRequest); + } // namespace SearchSessionsActivity +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionOnQueueRequest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionOnQueueRequest.cpp new file mode 100644 index 0000000000..d3a2b88dbe --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionOnQueueRequest.cpp @@ -0,0 +1,58 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include + +namespace AWSGameLift +{ + void AWSGameLiftCreateSessionOnQueueRequest::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("queueName", &AWSGameLiftCreateSessionOnQueueRequest::m_queueName) + ->Field("placementId", &AWSGameLiftCreateSessionOnQueueRequest::m_placementId) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("AWSGameLiftCreateSessionOnQueueRequest", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftCreateSessionOnQueueRequest::m_queueName, "QueueName (Required)", + "Name of the queue to use to place the new game session") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftCreateSessionOnQueueRequest::m_placementId, "PlacementId (Required)", + "A unique identifier to assign to the new game session placement") + ; + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("AWSGameLiftCreateSessionOnQueueRequest") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("CreatorId", BehaviorValueProperty(&AWSGameLiftCreateSessionOnQueueRequest::m_creatorId)) + ->Property("SessionProperties", BehaviorValueProperty(&AWSGameLiftCreateSessionOnQueueRequest::m_sessionProperties)) + ->Property("SessionName", BehaviorValueProperty(&AWSGameLiftCreateSessionOnQueueRequest::m_sessionName)) + ->Property("MaxPlayer", BehaviorValueProperty(&AWSGameLiftCreateSessionOnQueueRequest::m_maxPlayer)) + ->Property("QueueName", BehaviorValueProperty(&AWSGameLiftCreateSessionOnQueueRequest::m_queueName)) + ->Property("PlacementId", BehaviorValueProperty(&AWSGameLiftCreateSessionOnQueueRequest::m_placementId)) + ; + } + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionRequest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionRequest.cpp new file mode 100644 index 0000000000..3c5939f98f --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftCreateSessionRequest.cpp @@ -0,0 +1,63 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include + +namespace AWSGameLift +{ + void AWSGameLiftCreateSessionRequest::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("aliasId", &AWSGameLiftCreateSessionRequest::m_aliasId) + ->Field("fleetId", &AWSGameLiftCreateSessionRequest::m_fleetId) + ->Field("idempotencyToken", &AWSGameLiftCreateSessionRequest::m_idempotencyToken) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("AWSGameLiftCreateSessionRequest", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftCreateSessionRequest::m_aliasId, "AliasId (Required, or FleetId)", + "A unique identifier for the alias associated with the fleet to create a game session in") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftCreateSessionRequest::m_fleetId, "FleetId (Required, or AliasId)", + "A unique identifier for the fleet to create a game session in") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftCreateSessionRequest::m_idempotencyToken, "IdempotencyToken", + "Custom string that uniquely identifies the new game session request") + ; + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("AWSGameLiftCreateSessionRequest") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("CreatorId", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_creatorId)) + ->Property("SessionProperties", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_sessionProperties)) + ->Property("SessionName", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_sessionName)) + ->Property("MaxPlayer", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_maxPlayer)) + ->Property("AliasId", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_aliasId)) + ->Property("FleetId", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_fleetId)) + ->Property("IdempotencyToken", BehaviorValueProperty(&AWSGameLiftCreateSessionRequest::m_idempotencyToken)) + ; + } + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftJoinSessionRequest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftJoinSessionRequest.cpp new file mode 100644 index 0000000000..8c7165a095 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftJoinSessionRequest.cpp @@ -0,0 +1,54 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include + +namespace AWSGameLift +{ + void AWSGameLiftJoinSessionRequest::Reflect(AZ::ReflectContext* context) + { + AzFramework::JoinSessionRequest::Reflect(context); + + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("AWSGameLiftJoinSessionRequest", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ; + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("JoinSessionRequest") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + // Expose base type to BehaviorContext, but hide it to be used directly + ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All) + ; + behaviorContext->Class("AWSGameLiftJoinSessionRequest") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("PlayerData", BehaviorValueProperty(&AWSGameLiftJoinSessionRequest::m_playerData)) + ->Property("PlayerId", BehaviorValueProperty(&AWSGameLiftJoinSessionRequest::m_playerId)) + ->Property("SessionId", BehaviorValueProperty(&AWSGameLiftJoinSessionRequest::m_sessionId)) + ; + } + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftSearchSessionsRequest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftSearchSessionsRequest.cpp new file mode 100644 index 0000000000..3e13793b37 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/AWSGameLiftSearchSessionsRequest.cpp @@ -0,0 +1,69 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include + +#include +#include + +namespace AWSGameLift +{ + void AWSGameLiftSearchSessionsRequest::Reflect(AZ::ReflectContext* context) + { + AzFramework::SearchSessionsRequest::Reflect(context); + + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("aliasId", &AWSGameLiftSearchSessionsRequest::m_aliasId) + ->Field("fleetId", &AWSGameLiftSearchSessionsRequest::m_fleetId) + ->Field("location", &AWSGameLiftSearchSessionsRequest::m_location); + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("AWSGameLiftSearchSessionsRequest", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftSearchSessionsRequest::m_aliasId, "AliasId (Required, or FleetId)", + "A unique identifier for the alias associated with the fleet to search for active game sessions.") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftSearchSessionsRequest::m_fleetId, "FleetId (Required, or AliasId)", + "A unique identifier for the fleet to search for active game sessions.") + ->DataElement( + AZ::Edit::UIHandlers::Default, &AWSGameLiftSearchSessionsRequest::m_location, "Location", + "A fleet location to search for game sessions."); + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("SearchSessionsRequest") + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + // Expose base type to BehaviorContext, but hide it to be used directly + ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All); + behaviorContext->Class("AWSGameLiftSearchSessionsRequest") + + ->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value) + ->Property("FilterExpression", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_filterExpression)) + ->Property("SortExpression", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_sortExpression)) + ->Property("MaxResult", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_maxResult)) + ->Property("NextToken", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_nextToken)) + ->Property("AliasId", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_aliasId)) + ->Property("FleetId", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_fleetId)) + ->Property("Location", BehaviorValueProperty(&AWSGameLiftSearchSessionsRequest::m_location)); + } + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientFixture.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientFixture.h new file mode 100644 index 0000000000..5fefa34a58 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientFixture.h @@ -0,0 +1,63 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class AWSGameLiftClientFixture + : public UnitTest::ScopedAllocatorSetupFixture +{ +public: + AWSGameLiftClientFixture() {} + virtual ~AWSGameLiftClientFixture() = default; + + void SetUp() override + { + AZ::AllocatorInstance::Create(); + AZ::AllocatorInstance::Create(); + + AZ::JobManagerDesc jobManagerDesc; + AZ::JobManagerThreadDesc threadDesc; + + m_jobManager.reset(aznew AZ::JobManager(jobManagerDesc)); + m_jobCancelGroup.reset(aznew AZ::JobCancelGroup()); + jobManagerDesc.m_workerThreads.push_back(threadDesc); + jobManagerDesc.m_workerThreads.push_back(threadDesc); + jobManagerDesc.m_workerThreads.push_back(threadDesc); + m_jobContext.reset(aznew AZ::JobContext(*m_jobManager, *m_jobCancelGroup)); + AZ::JobContext::SetGlobalContext(m_jobContext.get()); + + AWSNativeSDKInit::InitializationManager::InitAwsApi(); + } + + void TearDown() override + { + AWSNativeSDKInit::InitializationManager::Shutdown(); + + AZ::JobContext::SetGlobalContext(nullptr); + m_jobContext.reset(); + m_jobCancelGroup.reset(); + m_jobManager.reset(); + AZ::AllocatorInstance::Destroy(); + AZ::AllocatorInstance::Destroy(); + } + + AZStd::unique_ptr m_jobContext; + AZStd::unique_ptr m_jobCancelGroup; + AZStd::unique_ptr m_jobManager; +}; diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp new file mode 100644 index 0000000000..90e058f21c --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp @@ -0,0 +1,773 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +using namespace AWSGameLift; + +MATCHER_P(SearchSessionsResponseMatcher, expectedResponse, "") +{ + // Custome matcher for checking the SearchSessionsResponse type argument. + AZ_UNUSED(result_listener); + + bool result = arg.m_nextToken == expectedResponse.m_nextToken; + result &= arg.m_sessionConfigs.size() == expectedResponse.m_sessionConfigs.size(); + + for (int index = 0; index < arg.m_sessionConfigs.size(); ++index) + { + result &= arg.m_sessionConfigs[index].m_creationTime == expectedResponse.m_sessionConfigs[index].m_creationTime; + result &= arg.m_sessionConfigs[index].m_terminationTime == expectedResponse.m_sessionConfigs[index].m_terminationTime; + result &= arg.m_sessionConfigs[index].m_creatorId == expectedResponse.m_sessionConfigs[index].m_creatorId; + result &= arg.m_sessionConfigs[index].m_sessionProperties == expectedResponse.m_sessionConfigs[index].m_sessionProperties; + result &= arg.m_sessionConfigs[index].m_sessionId == expectedResponse.m_sessionConfigs[index].m_sessionId; + result &= arg.m_sessionConfigs[index].m_sessionName == expectedResponse.m_sessionConfigs[index].m_sessionName; + result &= arg.m_sessionConfigs[index].m_dnsName == expectedResponse.m_sessionConfigs[index].m_dnsName; + result &= arg.m_sessionConfigs[index].m_ipAddress == expectedResponse.m_sessionConfigs[index].m_ipAddress; + result &= arg.m_sessionConfigs[index].m_port == expectedResponse.m_sessionConfigs[index].m_port; + result &= arg.m_sessionConfigs[index].m_maxPlayer == expectedResponse.m_sessionConfigs[index].m_maxPlayer; + result &= arg.m_sessionConfigs[index].m_currentPlayer == expectedResponse.m_sessionConfigs[index].m_currentPlayer; + result &= arg.m_sessionConfigs[index].m_status == expectedResponse.m_sessionConfigs[index].m_status; + result &= arg.m_sessionConfigs[index].m_statusReason == expectedResponse.m_sessionConfigs[index].m_statusReason; + } + + return result; +} + +class AWSResourceMappingRequestsHandlerMock + : public AWSCore::AWSResourceMappingRequestBus::Handler +{ +public: + AWSResourceMappingRequestsHandlerMock() + { + AWSCore::AWSResourceMappingRequestBus::Handler::BusConnect(); + } + + ~ AWSResourceMappingRequestsHandlerMock() + { + AWSCore::AWSResourceMappingRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD0(GetDefaultRegion, AZStd::string()); + MOCK_CONST_METHOD0(GetDefaultAccountId, AZStd::string()); + MOCK_CONST_METHOD1(GetResourceAccountId, AZStd::string(const AZStd::string&)); + MOCK_CONST_METHOD1(GetResourceNameId, AZStd::string(const AZStd::string&)); + MOCK_CONST_METHOD1(GetResourceRegion, AZStd::string(const AZStd::string&)); + MOCK_CONST_METHOD1(GetResourceType, AZStd::string(const AZStd::string&)); + MOCK_CONST_METHOD1(GetServiceUrlByServiceName, AZStd::string(const AZStd::string&)); + MOCK_CONST_METHOD2(GetServiceUrlByRESTApiIdAndStage, AZStd::string(const AZStd::string&, const AZStd::string&)); + MOCK_METHOD1(ReloadConfigFile, void(bool)); +}; + +class AWSCredentialRequestsHandlerMock + : public AWSCore::AWSCredentialRequestBus::Handler +{ +public: + AWSCredentialRequestsHandlerMock() + { + AWSCore::AWSCredentialRequestBus::Handler::BusConnect(); + } + + ~AWSCredentialRequestsHandlerMock() + { + AWSCore::AWSCredentialRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD0(GetCredentialHandlerOrder, int()); + MOCK_METHOD0(GetCredentialsProvider, std::shared_ptr()); +}; + +class AWSCoreRequestsHandlerMock + : public AWSCore::AWSCoreRequestBus::Handler +{ +public: + AWSCoreRequestsHandlerMock() + { + AWSCore::AWSCoreRequestBus::Handler::BusConnect(); + } + + ~AWSCoreRequestsHandlerMock() + { + AWSCore::AWSCoreRequestBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(GetDefaultJobContext, AZ::JobContext*()); + MOCK_METHOD0(GetDefaultConfig, AWSCore::AwsApiJobConfig*()); +}; + +class TestAWSGameLiftClientManager + : public AWSGameLiftClientManager +{ +public: + TestAWSGameLiftClientManager() + { + m_gameliftClientMockPtr = nullptr; + } + ~TestAWSGameLiftClientManager() + { + m_gameliftClientMockPtr = nullptr; + } + + void SetUpMockClient() + { + m_gameliftClientMockPtr = AZStd::make_shared(); + SetGameLiftClient(m_gameliftClientMockPtr); + } + + AZStd::shared_ptr m_gameliftClientMockPtr; +}; + +class AWSGameLiftClientManagerTest + : public AWSGameLiftClientFixture +{ +protected: + void SetUp() override + { + AWSGameLiftClientFixture::SetUp(); + + m_gameliftClientManager = AZStd::make_unique(); + m_gameliftClientManager->SetUpMockClient(); + m_gameliftClientManager->ActivateManager(); + } + + void TearDown() override + { + m_gameliftClientManager->DeactivateManager(); + m_gameliftClientManager.reset(); + + AWSGameLiftClientFixture::TearDown(); + } + + AWSGameLiftSearchSessionsRequest GetValidSearchSessionsRequest() + { + AWSGameLiftSearchSessionsRequest request; + request.m_aliasId = "dummyAliasId"; + request.m_fleetId = "dummyFleetId"; + request.m_location = "dummyLocation"; + request.m_filterExpression = "dummyFilterExpression"; + request.m_sortExpression = "dummySortExpression"; + request.m_maxResult = 1; + request.m_nextToken = "dummyNextToken"; + + return request; + } + + Aws::GameLift::Model::SearchGameSessionsOutcome GetValidSearchGameSessionsOutcome() + { + Aws::GameLift::Model::GameProperty gameProperty; + gameProperty.SetKey("dummyKey"); + gameProperty.SetValue("dummyValue"); + Aws::Vector gameProperties = { gameProperty }; + + Aws::GameLift::Model::GameSession gameSession; + gameSession.SetCreationTime(Aws::Utils::DateTime(0.0)); + gameSession.SetTerminationTime(Aws::Utils::DateTime(0.0)); + gameSession.SetCreatorId("dummyCreatorId"); + gameSession.SetGameProperties(gameProperties); + gameSession.SetGameSessionId("dummyGameSessionId"); + gameSession.SetName("dummyGameSessionName"); + gameSession.SetIpAddress("dummyIpAddress"); + gameSession.SetPort(0); + gameSession.SetMaximumPlayerSessionCount(2); + gameSession.SetCurrentPlayerSessionCount(1); + gameSession.SetStatus(Aws::GameLift::Model::GameSessionStatus::TERMINATED); + gameSession.SetStatusReason(Aws::GameLift::Model::GameSessionStatusReason::INTERRUPTED); + // TODO: Update the AWS Native SDK to set the new game session attributes. + // gameSession.SetDnsName("dummyDnsName"); + + Aws::GameLift::Model::SearchGameSessionsResult result; + result.SetNextToken("dummyNextToken"); + result.SetGameSessions({ gameSession }); + + return Aws::GameLift::Model::SearchGameSessionsOutcome(result); + } + + AzFramework::SearchSessionsResponse GetValidSearchSessionsResponse() + { + AzFramework::SessionConfig sessionConfig; + sessionConfig.m_creationTime = 0; + sessionConfig.m_terminationTime = 0; + sessionConfig.m_creatorId = "dummyCreatorId"; + sessionConfig.m_sessionProperties["dummyKey"] = "dummyValue"; + sessionConfig.m_sessionId = "dummyGameSessionId"; + sessionConfig.m_sessionName = "dummyGameSessionName"; + sessionConfig.m_ipAddress = "dummyIpAddress"; + sessionConfig.m_port = 0; + sessionConfig.m_maxPlayer = 2; + sessionConfig.m_currentPlayer = 1; + sessionConfig.m_status = "Terminated"; + sessionConfig.m_statusReason = "Interrupted"; + // TODO: Update the AWS Native SDK to set the new game session attributes. + // sessionConfig.m_dnsName = "dummyDnsName"; + + AzFramework::SearchSessionsResponse response; + response.m_nextToken = "dummyNextToken"; + response.m_sessionConfigs = { sessionConfig }; + + return response; + } + +public: + AZStd::unique_ptr m_gameliftClientManager; +}; + +TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_GetFalseAsResult) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_gameliftClientManager->ConfigureGameLiftClient(""); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredential_GetFalseAsResult) +{ + AWSResourceMappingRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultRegion()).Times(1).WillOnce(::testing::Return("us-west-2")); + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_gameliftClientManager->ConfigureGameLiftClient(""); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithRegionAndCredential_GetTrueAsResult) +{ + AWSCredentialRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetCredentialsProvider()) + .Times(1) + .WillOnce(::testing::Return(std::make_shared("dummyAccess", "dummySecret", ""))); + auto result = m_gameliftClientManager->ConfigureGameLiftClient("us-west-2"); + EXPECT_TRUE(result); +} + +TEST_F(AWSGameLiftClientManagerTest, CreatePlayerId_CreateWithoutBracketsOrDashes_GetExpectedResult) +{ + auto result = m_gameliftClientManager->CreatePlayerId(false, false); + EXPECT_FALSE(result.starts_with("{")); + EXPECT_FALSE(result.ends_with("}")); + EXPECT_FALSE(result.contains("-")); +} + +TEST_F(AWSGameLiftClientManagerTest, CreatePlayerId_CreateWithBrackets_GetExpectedResult) +{ + auto result = m_gameliftClientManager->CreatePlayerId(true, false); + EXPECT_TRUE(result.starts_with("{")); + EXPECT_TRUE(result.ends_with("}")); + EXPECT_FALSE(result.contains("-")); +} + +TEST_F(AWSGameLiftClientManagerTest, CreatePlayerId_CreateWithDashes_GetExpectedResult) +{ + auto result = m_gameliftClientManager->CreatePlayerId(false, true); + EXPECT_FALSE(result.starts_with("{")); + EXPECT_FALSE(result.ends_with("}")); + EXPECT_TRUE(result.contains("-")); +} + +TEST_F(AWSGameLiftClientManagerTest, CreatePlayerId_CreateWithBracketsAndDashes_GetExpectedResult) +{ + auto result = m_gameliftClientManager->CreatePlayerId(true, true); + EXPECT_TRUE(result.starts_with("{")); + EXPECT_TRUE(result.ends_with("}")); + EXPECT_TRUE(result.contains("-")); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSession_CallWithoutClientSetup_GetEmptyResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->ConfigureGameLiftClient(""); + AWSGameLiftCreateSessionRequest request; + request.m_aliasId = "dummyAlias"; + auto response = m_gameliftClientManager->CreateSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(2); // capture 2 error message + EXPECT_TRUE(response == ""); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSession_CallWithInvalidRequest_GetEmptyResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + auto response = m_gameliftClientManager->CreateSession(AzFramework::CreateSessionRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_TRUE(response == ""); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSession_CallWithValidRequest_GetSuccessOutcome) +{ + AWSGameLiftCreateSessionRequest request; + request.m_aliasId = "dummyAlias"; + Aws::GameLift::Model::CreateGameSessionResult result; + result.SetGameSession(Aws::GameLift::Model::GameSession()); + Aws::GameLift::Model::CreateGameSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreateGameSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + m_gameliftClientManager->CreateSession(request); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSession_CallWithValidRequest_GetErrorOutcome) +{ + AWSGameLiftCreateSessionRequest request; + request.m_aliasId = "dummyAlias"; + Aws::Client::AWSError error; + Aws::GameLift::Model::CreateGameSessionOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreateGameSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->CreateSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionAsync_CallWithInvalidRequest_GetNotificationWithEmptyResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnCreateSessionAsyncComplete(AZStd::string())).Times(1); + m_gameliftClientManager->CreateSessionAsync(AzFramework::CreateSessionRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionAsync_CallWithValidRequest_GetNotificationWithSuccessOutcome) +{ + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + AWSGameLiftCreateSessionRequest request; + request.m_aliasId = "dummyAlias"; + Aws::GameLift::Model::CreateGameSessionResult result; + result.SetGameSession(Aws::GameLift::Model::GameSession()); + Aws::GameLift::Model::CreateGameSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreateGameSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnCreateSessionAsyncComplete(::testing::_)).Times(1); + m_gameliftClientManager->CreateSessionAsync(request); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionAsync_CallWithValidRequest_GetNotificationWithErrorOutcome) +{ + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + AWSGameLiftCreateSessionRequest request; + request.m_aliasId = "dummyAlias"; + Aws::Client::AWSError error; + Aws::GameLift::Model::CreateGameSessionOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreateGameSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnCreateSessionAsyncComplete(AZStd::string(""))).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->CreateSessionAsync(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionOnQueue_CallWithoutClientSetup_GetEmptyResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->ConfigureGameLiftClient(""); + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_queueName = "dummyQueue"; + request.m_placementId = "dummyPlacementId"; + auto response = m_gameliftClientManager->CreateSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(2); // capture 2 error message + EXPECT_TRUE(response == ""); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionOnQueue_CallWithValidRequest_GetSuccessOutcome) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_queueName = "dummyQueue"; + request.m_placementId = "dummyPlacementId"; + Aws::GameLift::Model::StartGameSessionPlacementResult result; + result.SetGameSessionPlacement(Aws::GameLift::Model::GameSessionPlacement()); + Aws::GameLift::Model::StartGameSessionPlacementOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), StartGameSessionPlacement(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + m_gameliftClientManager->CreateSession(request); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionOnQueue_CallWithValidRequest_GetErrorOutcome) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_queueName = "dummyQueue"; + request.m_placementId = "dummyPlacementId"; + Aws::Client::AWSError error; + Aws::GameLift::Model::StartGameSessionPlacementOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), StartGameSessionPlacement(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->CreateSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionOnQueueAsync_CallWithValidRequest_GetNotificationWithSuccessOutcome) +{ + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_queueName = "dummyQueue"; + request.m_placementId = "dummyPlacementId"; + Aws::GameLift::Model::StartGameSessionPlacementResult result; + result.SetGameSessionPlacement(Aws::GameLift::Model::GameSessionPlacement()); + Aws::GameLift::Model::StartGameSessionPlacementOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), StartGameSessionPlacement(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnCreateSessionAsyncComplete(::testing::_)).Times(1); + m_gameliftClientManager->CreateSessionAsync(request); +} + +TEST_F(AWSGameLiftClientManagerTest, CreateSessionOnQueueAsync_CallWithValidRequest_GetNotificationWithErrorOutcome) +{ + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_queueName = "dummyQueue"; + request.m_placementId = "dummyPlacementId"; + Aws::Client::AWSError error; + Aws::GameLift::Model::StartGameSessionPlacementOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), StartGameSessionPlacement(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnCreateSessionAsyncComplete(AZStd::string(""))).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->CreateSessionAsync(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSession_CallWithoutClientSetup_GetFalseResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->ConfigureGameLiftClient(""); + AWSGameLiftJoinSessionRequest request; + request.m_playerId = "dummyPlayerId"; + request.m_sessionId = "dummySessionId"; + auto response = m_gameliftClientManager->JoinSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(2); // capture 2 error message + EXPECT_FALSE(response); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSession_CallWithInvalidRequest_GetFalseResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + auto response = m_gameliftClientManager->JoinSession(AzFramework::JoinSessionRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(response); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSession_CallWithValidRequestButNoRequestHandler_GetSuccessOutcomeButFalseResponse) +{ + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::GameLift::Model::CreatePlayerSessionResult result; + result.SetPlayerSession(Aws::GameLift::Model::PlayerSession()); + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + AZ_TEST_START_TRACE_SUPPRESSION; + auto response = m_gameliftClientManager->JoinSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(response); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSession_CallWithValidRequest_GetErrorOutcomeAndFalseResponse) +{ + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::Client::AWSError error; + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + AZ_TEST_START_TRACE_SUPPRESSION; + auto response = m_gameliftClientManager->JoinSession(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(response); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSession_CallWithValidRequestAndRequestHandler_GetSuccessOutcomeButFalseResponse) +{ + SessionHandlingClientRequestsMock handlerMock; + EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(false)); + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::GameLift::Model::CreatePlayerSessionResult result; + result.SetPlayerSession(Aws::GameLift::Model::PlayerSession()); + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + auto response = m_gameliftClientManager->JoinSession(request); + EXPECT_FALSE(response); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSession_CallWithValidRequestAndRequestHandler_GetSuccessOutcomeAndTrueResponse) +{ + SessionHandlingClientRequestsMock handlerMock; + EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(true)); + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::GameLift::Model::CreatePlayerSessionResult result; + result.SetPlayerSession(Aws::GameLift::Model::PlayerSession()); + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + auto response = m_gameliftClientManager->JoinSession(request); + EXPECT_TRUE(response); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSessionAsync_CallWithInvalidRequest_GetNotificationWithFalseResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnJoinSessionAsyncComplete(false)).Times(1); + m_gameliftClientManager->JoinSessionAsync(AzFramework::JoinSessionRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSessionAsync_CallWithValidRequestButNoRequestHandler_GetSuccessOutcomeButNotificationWithFalseResponse) +{ + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::GameLift::Model::CreatePlayerSessionResult result; + result.SetPlayerSession(Aws::GameLift::Model::PlayerSession()); + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnJoinSessionAsyncComplete(false)).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->JoinSessionAsync(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSessionAsync_CallWithValidRequest_GetErrorOutcomeAndNotificationWithFalseResponse) +{ + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::Client::AWSError error; + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnJoinSessionAsyncComplete(false)).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->JoinSessionAsync(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSessionAsync_CallWithValidRequestAndRequestHandler_GetSuccessOutcomeButNotificationWithFalseResponse) +{ + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + SessionHandlingClientRequestsMock handlerMock; + EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(false)); + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::GameLift::Model::CreatePlayerSessionResult result; + result.SetPlayerSession(Aws::GameLift::Model::PlayerSession()); + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnJoinSessionAsyncComplete(false)).Times(1); + m_gameliftClientManager->JoinSessionAsync(request); +} + +TEST_F(AWSGameLiftClientManagerTest, JoinSessionAsync_CallWithValidRequestAndRequestHandler_GetSuccessOutcomeAndNotificationWithTrueResponse) +{ + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + SessionHandlingClientRequestsMock handlerMock; + EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(true)); + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + request.m_playerId = "dummyPlayerId"; + Aws::GameLift::Model::CreatePlayerSessionResult result; + result.SetPlayerSession(Aws::GameLift::Model::PlayerSession()); + Aws::GameLift::Model::CreatePlayerSessionOutcome outcome(result); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), CreatePlayerSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnJoinSessionAsyncComplete(true)).Times(1); + m_gameliftClientManager->JoinSessionAsync(request); +} + +TEST_F(AWSGameLiftClientManagerTest, SearchSessions_CallWithValidRequestAndErrorOutcome_GetErrorWithEmptyResponse) +{ + AWSGameLiftSearchSessionsRequest request = GetValidSearchSessionsRequest(); + + Aws::Client::AWSError error; + Aws::GameLift::Model::SearchGameSessionsOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), SearchGameSessions(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_gameliftClientManager->SearchSessions(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_TRUE(result.m_sessionConfigs.size() == 0); +} + +TEST_F(AWSGameLiftClientManagerTest, SearchSessions_CallWithValidRequestAndSuccessOutcome_GetNotificationWithValidResponse) +{ + AWSGameLiftSearchSessionsRequest request = GetValidSearchSessionsRequest(); + + Aws::GameLift::Model::SearchGameSessionsOutcome outcome = GetValidSearchGameSessionsOutcome(); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), SearchGameSessions(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + + AzFramework::SearchSessionsResponse expectedResponse = GetValidSearchSessionsResponse(); + auto result = m_gameliftClientManager->SearchSessions(request); + EXPECT_TRUE(result.m_sessionConfigs.size() != 0); +} + +TEST_F(AWSGameLiftClientManagerTest, SearchSessionsAsync_CallWithoutClientSetup_GetErrorWithEmptyResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + EXPECT_FALSE(m_gameliftClientManager->ConfigureGameLiftClient("")); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + + AWSGameLiftSearchSessionsRequest request = GetValidSearchSessionsRequest(); + + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, + OnSearchSessionsAsyncComplete(SearchSessionsResponseMatcher(AzFramework::SearchSessionsResponse()))).Times(1); + + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->SearchSessionsAsync(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, SearchSessionsAsync_CallWithInvalidRequest_GetErrorWithEmptyResponse) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, + OnSearchSessionsAsyncComplete(SearchSessionsResponseMatcher(AzFramework::SearchSessionsResponse()))).Times(1); + + m_gameliftClientManager->SearchSessionsAsync(AzFramework::SearchSessionsRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, SearchSessionsAsync_CallWithValidRequestAndErrorOutcome_GetErrorWithEmptyResponse) +{ + AWSGameLiftSearchSessionsRequest request = GetValidSearchSessionsRequest(); + + AWSCoreRequestsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + + Aws::Client::AWSError error; + Aws::GameLift::Model::SearchGameSessionsOutcome outcome(error); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), SearchGameSessions(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, + OnSearchSessionsAsyncComplete(SearchSessionsResponseMatcher(AzFramework::SearchSessionsResponse()))).Times(1); + + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->SearchSessionsAsync(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, SearchSessionsAsync_CallWithValidRequestAndSuccessOutcome_GetNotificationWithValidResponse) +{ + AWSGameLiftSearchSessionsRequest request = GetValidSearchSessionsRequest(); + + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultJobContext()).Times(1).WillOnce(::testing::Return(m_jobContext.get())); + + Aws::GameLift::Model::SearchGameSessionsOutcome outcome = GetValidSearchGameSessionsOutcome(); + EXPECT_CALL(*(m_gameliftClientManager->m_gameliftClientMockPtr), SearchGameSessions(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(outcome)); + + AzFramework::SearchSessionsResponse expectedResponse = GetValidSearchSessionsResponse(); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, + OnSearchSessionsAsyncComplete(SearchSessionsResponseMatcher(expectedResponse))).Times(1); + + m_gameliftClientManager->SearchSessionsAsync(request); +} + +TEST_F(AWSGameLiftClientManagerTest, LeaveSession_CallWithInterfaceNotRegistered_GetExpectedError) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->LeaveSession(); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, LeaveSession_CallWithInterfaceRegistered_LeaveSessionRequestSent) +{ + SessionHandlingClientRequestsMock handlerMock; + EXPECT_CALL(handlerMock, RequestPlayerLeaveSession).Times(1); + + m_gameliftClientManager->LeaveSession(); +} + +TEST_F(AWSGameLiftClientManagerTest, LeaveSessionAsync_CallWithInterfaceNotRegistered_GetExpectedError) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + m_gameliftClientManager->LeaveSessionAsync(); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message +} + +TEST_F(AWSGameLiftClientManagerTest, LeaveSessionAsync_CallWithInterfaceRegistered_LeaveSessionAsyncRequestSentAndGetNotification) +{ + SessionHandlingClientRequestsMock handlerMock; + EXPECT_CALL(handlerMock, RequestPlayerLeaveSession).Times(1); + SessionAsyncRequestNotificationsHandlerMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, OnLeaveSessionAsyncComplete()).Times(1); + + m_gameliftClientManager->LeaveSessionAsync(); +} diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h new file mode 100644 index 0000000000..e8699a5221 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h @@ -0,0 +1,86 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Aws::GameLift; + +class GameLiftClientMock + : public GameLiftClient +{ +public: + GameLiftClientMock() + : GameLiftClient(Aws::Auth::AWSCredentials()) + { + } + + MOCK_CONST_METHOD1(CreateGameSession, Model::CreateGameSessionOutcome(const Model::CreateGameSessionRequest&)); + MOCK_CONST_METHOD1(CreatePlayerSession, Model::CreatePlayerSessionOutcome(const Model::CreatePlayerSessionRequest&)); + MOCK_CONST_METHOD1(SearchGameSessions, Model::SearchGameSessionsOutcome(const Model::SearchGameSessionsRequest&)); + MOCK_CONST_METHOD1(StartGameSessionPlacement, Model::StartGameSessionPlacementOutcome(const Model::StartGameSessionPlacementRequest&)); +}; + +class SessionAsyncRequestNotificationsHandlerMock + : public AzFramework::SessionAsyncRequestNotificationBus::Handler +{ +public: + SessionAsyncRequestNotificationsHandlerMock() + { + AzFramework::SessionAsyncRequestNotificationBus::Handler::BusConnect(); + } + + ~SessionAsyncRequestNotificationsHandlerMock() + { + AzFramework::SessionAsyncRequestNotificationBus::Handler::BusDisconnect(); + } + + MOCK_METHOD1(OnCreateSessionAsyncComplete, void(const AZStd::string&)); + MOCK_METHOD1(OnSearchSessionsAsyncComplete, void(const AzFramework::SearchSessionsResponse&)); + MOCK_METHOD1(OnJoinSessionAsyncComplete, void(bool)); + MOCK_METHOD0(OnLeaveSessionAsyncComplete, void()); +}; + +class SessionHandlingClientRequestsMock + : public AzFramework::ISessionHandlingClientRequests +{ +public: + SessionHandlingClientRequestsMock() + { + AZ::Interface::Register(this); + } + + virtual ~SessionHandlingClientRequestsMock() + { + AZ::Interface::Unregister(this); + } + + MOCK_METHOD1(RequestPlayerJoinSession, bool(const AzFramework::SessionConnectionConfig&)); + MOCK_METHOD0(RequestPlayerLeaveSession, void()); +}; diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientSystemComponentTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientSystemComponentTest.cpp new file mode 100644 index 0000000000..f47a2cc60c --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientSystemComponentTest.cpp @@ -0,0 +1,156 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace AWSGameLift; + +class AWSGameLiftClientManagerMock + : public AWSGameLiftClientManager +{ +public: + AWSGameLiftClientManagerMock() = default; + ~AWSGameLiftClientManagerMock() = default; + + MOCK_METHOD0(ActivateManager, void()); + MOCK_METHOD0(DeactivateManager, void()); +}; + +class TestAWSGameLiftClientSystemComponent + : public AWSGameLiftClientSystemComponent +{ +public: + TestAWSGameLiftClientSystemComponent() + { + m_gameliftClientManagerMockPtr = nullptr; + } + ~TestAWSGameLiftClientSystemComponent() + { + m_gameliftClientManagerMockPtr = nullptr; + } + + void SetUpMockManager() + { + AZStd::unique_ptr gameliftClientManagerMock = AZStd::make_unique(); + m_gameliftClientManagerMockPtr = gameliftClientManagerMock.get(); + SetGameLiftClientManager(AZStd::move(gameliftClientManagerMock)); + } + + AWSGameLiftClientManagerMock* m_gameliftClientManagerMockPtr; +}; + +class AWSCoreSystemComponentMock + : public AZ::Component +{ +public: + AZ_COMPONENT(AWSCoreSystemComponentMock, "{52DB1342-30C6-412F-B7CC-B23F8B0629EA}"); + + static void Reflect(AZ::ReflectContext* context) + { + AZ_UNUSED(context); + } + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("AWSCoreService")); + } + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + AZ_UNUSED(incompatible); + } + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + AZ_UNUSED(required); + } + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + AZ_UNUSED(dependent); + } + + AWSCoreSystemComponentMock() = default; + ~AWSCoreSystemComponentMock() = default; + + void Init() override {} + void Activate() override {} + void Deactivate() override {} +}; + +class AWSGameLiftClientSystemComponentTest + : public AWSGameLiftClientFixture +{ +protected: + AZStd::unique_ptr m_serializeContext; + AZStd::unique_ptr m_behaviorContext; + AZStd::unique_ptr m_coreComponentDescriptor; + AZStd::unique_ptr m_gameliftClientComponentDescriptor; + + void SetUp() override + { + AWSGameLiftClientFixture::SetUp(); + + m_serializeContext = AZStd::make_unique(); + m_serializeContext->CreateEditContext(); + m_behaviorContext = AZStd::make_unique(); + m_coreComponentDescriptor.reset(AWSCoreSystemComponentMock::CreateDescriptor()); + m_gameliftClientComponentDescriptor.reset(TestAWSGameLiftClientSystemComponent::CreateDescriptor()); + m_gameliftClientComponentDescriptor->Reflect(m_serializeContext.get()); + m_gameliftClientComponentDescriptor->Reflect(m_behaviorContext.get()); + + m_entity = aznew AZ::Entity(); + m_coreSystemComponent = AZStd::make_unique(); + m_entity->AddComponent(m_coreSystemComponent.get()); + m_gameliftClientSystemComponent = AZStd::make_unique(); + m_gameliftClientSystemComponent->SetUpMockManager(); + m_entity->AddComponent(m_gameliftClientSystemComponent.get()); + } + + void TearDown() override + { + m_entity->RemoveComponent(m_gameliftClientSystemComponent.get()); + m_gameliftClientSystemComponent.reset(); + m_entity->RemoveComponent(m_coreSystemComponent.get()); + m_coreSystemComponent.reset(); + delete m_entity; + m_entity = nullptr; + + m_gameliftClientComponentDescriptor.reset(); + m_coreComponentDescriptor.reset(); + m_behaviorContext.reset(); + m_serializeContext.reset(); + + AWSGameLiftClientFixture::TearDown(); + } + +public: + AZStd::unique_ptr m_coreSystemComponent; + AZStd::unique_ptr m_gameliftClientSystemComponent; + AZ::Entity* m_entity; +}; + +TEST_F(AWSGameLiftClientSystemComponentTest, ActivateDeactivate_Call_GameLiftClientManagerGetsInvoked) +{ + m_entity->Init(); + EXPECT_CALL(*(m_gameliftClientSystemComponent->m_gameliftClientManagerMockPtr), ActivateManager()).Times(1); + m_entity->Activate(); + + EXPECT_CALL(*(m_gameliftClientSystemComponent->m_gameliftClientManagerMockPtr), DeactivateManager()).Times(1); + m_entity->Deactivate(); +} diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientTest.cpp new file mode 100644 index 0000000000..dfe3edaa96 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientTest.cpp @@ -0,0 +1,15 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionActivityTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionActivityTest.cpp new file mode 100644 index 0000000000..4ae89eada7 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionActivityTest.cpp @@ -0,0 +1,81 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +using namespace AWSGameLift; + +using AWSGameLiftCreateSessionActivityTest = AWSGameLiftClientFixture; + +TEST_F(AWSGameLiftCreateSessionActivityTest, BuildAWSGameLiftCreateGameSessionRequest_Call_GetExpectedResult) +{ + AWSGameLiftCreateSessionRequest request; + request.m_creatorId = "dummyCreatorId"; + request.m_sessionName = "dummySessionName"; + request.m_maxPlayer = 1; + request.m_sessionProperties.emplace("dummyKey", "dummyValue"); + request.m_aliasId = "dummyAliasId"; + request.m_fleetId = "dummyFleetId"; + request.m_idempotencyToken = "dummyIdempotencyToken"; + auto awsRequest = CreateSessionActivity::BuildAWSGameLiftCreateGameSessionRequest(request); + + EXPECT_TRUE(strcmp(awsRequest.GetCreatorId().c_str(), request.m_creatorId.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetName().c_str(), request.m_sessionName.c_str()) == 0); + EXPECT_TRUE(awsRequest.GetMaximumPlayerSessionCount() == request.m_maxPlayer); + EXPECT_TRUE(strcmp(awsRequest.GetGameProperties()[0].GetKey().c_str(), request.m_sessionProperties.begin()->first.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetGameProperties()[0].GetValue().c_str(), request.m_sessionProperties.begin()->second.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetAliasId().c_str(), request.m_aliasId.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetFleetId().c_str(), request.m_fleetId.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetIdempotencyToken().c_str(), request.m_idempotencyToken.c_str()) == 0); +} + +TEST_F(AWSGameLiftCreateSessionActivityTest, ValidateCreateSessionRequest_CallWithBaseType_GetFalseResult) +{ + auto result = CreateSessionActivity::ValidateCreateSessionRequest(AzFramework::CreateSessionRequest()); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionActivityTest, ValidateCreateSessionRequest_CallWithNegativeMaxPlayer_GetFalseResult) +{ + AWSGameLiftCreateSessionRequest request; + request.m_maxPlayer = -1; + + auto result = CreateSessionActivity::ValidateCreateSessionRequest(request); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionActivityTest, ValidateCreateSessionRequest_CallWithoutAliasOrFleetId_GetFalseResult) +{ + AWSGameLiftCreateSessionRequest request; + request.m_maxPlayer = 1; + auto result = CreateSessionActivity::ValidateCreateSessionRequest(request); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionActivityTest, ValidateCreateSessionRequest_CallWithAliasId_GetTrueResult) +{ + AWSGameLiftCreateSessionRequest request; + request.m_maxPlayer = 1; + request.m_aliasId = "dummyAliasId"; + auto result = CreateSessionActivity::ValidateCreateSessionRequest(request); + EXPECT_TRUE(result); +} + +TEST_F(AWSGameLiftCreateSessionActivityTest, ValidateCreateSessionRequest_CallWithFleetId_GetTrueResult) +{ + AWSGameLiftCreateSessionRequest request; + request.m_maxPlayer = 1; + request.m_fleetId = "dummyFleetId"; + auto result = CreateSessionActivity::ValidateCreateSessionRequest(request); + EXPECT_TRUE(result); +} diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionOnQueueActivityTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionOnQueueActivityTest.cpp new file mode 100644 index 0000000000..98eebfca77 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftCreateSessionOnQueueActivityTest.cpp @@ -0,0 +1,79 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +using namespace AWSGameLift; + +using AWSGameLiftCreateSessionOnQueueActivityTest = AWSGameLiftClientFixture; + +TEST_F(AWSGameLiftCreateSessionOnQueueActivityTest, BuildAWSGameLiftCreateGameSessionRequest_Call_GetExpectedResult) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_sessionName = "dummySessionName"; + request.m_maxPlayer = 1; + request.m_sessionProperties.emplace("dummyKey", "dummyValue"); + request.m_queueName = "dummyQueueName"; + request.m_placementId = "dummyPlacementId"; + auto awsRequest = CreateSessionOnQueueActivity::BuildAWSGameLiftStartGameSessionPlacementRequest(request); + + EXPECT_TRUE(strcmp(awsRequest.GetGameSessionName().c_str(), request.m_sessionName.c_str()) == 0); + EXPECT_TRUE(awsRequest.GetMaximumPlayerSessionCount() == request.m_maxPlayer); + EXPECT_TRUE(strcmp(awsRequest.GetGameProperties()[0].GetKey().c_str(), request.m_sessionProperties.begin()->first.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetGameProperties()[0].GetValue().c_str(), request.m_sessionProperties.begin()->second.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetGameSessionQueueName().c_str(), request.m_queueName.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetPlacementId().c_str(), request.m_placementId.c_str()) == 0); +} + +TEST_F(AWSGameLiftCreateSessionOnQueueActivityTest, ValidateCreateSessionOnQueueRequest_CallWithBaseType_GetFalseResult) +{ + auto result = CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(AzFramework::CreateSessionRequest()); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionOnQueueActivityTest, ValidateCreateSessionOnQueueRequest_CallWithNegativeMaxPlayer_GetFalseResult) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_maxPlayer = -1; + + auto result = CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(request); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionOnQueueActivityTest, ValidateCreateSessionOnQueueRequest_CallWithoutQueueName_GetFalseResult) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_maxPlayer = 1; + request.m_placementId = "dummyPlacementId"; + auto result = CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(request); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionOnQueueActivityTest, ValidateCreateSessionOnQueueRequest_CallWithoutPlacementId_GetFalseResult) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_maxPlayer = 1; + request.m_queueName = "dummyQueueName"; + auto result = CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(request); + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftCreateSessionOnQueueActivityTest, ValidateCreateSessionOnQueueRequest_CallWithValidRequest_GetTrueResult) +{ + AWSGameLiftCreateSessionOnQueueRequest request; + request.m_maxPlayer = 1; + request.m_queueName = "dummyQueueName"; + request.m_placementId = "dummyPlacementId"; + auto result = CreateSessionOnQueueActivity::ValidateCreateSessionOnQueueRequest(request); + EXPECT_TRUE(result); +} diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftJoinSessionActivityTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftJoinSessionActivityTest.cpp new file mode 100644 index 0000000000..8dcafe18aa --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftJoinSessionActivityTest.cpp @@ -0,0 +1,84 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +using namespace AWSGameLift; + +using AWSGameLiftJoinSessionActivityTest = AWSGameLiftClientFixture; + +TEST_F(AWSGameLiftJoinSessionActivityTest, BuildAWSGameLiftCreatePlayerSessionRequest_Call_GetExpectedResult) +{ + AWSGameLiftJoinSessionRequest request; + request.m_playerData = "dummyPlayerData"; + request.m_playerId = "dummyPlayerId"; + request.m_sessionId = "dummySessionId"; + auto awsRequest = JoinSessionActivity::BuildAWSGameLiftCreatePlayerSessionRequest(request); + + EXPECT_TRUE(strcmp(awsRequest.GetPlayerData().c_str(), request.m_playerData.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetPlayerId().c_str(), request.m_playerId.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetGameSessionId().c_str(), request.m_sessionId.c_str()) == 0); +} + +TEST_F(AWSGameLiftJoinSessionActivityTest, BuildSessionConnectionConfig_Call_GetExpectedResult) +{ + Aws::GameLift::Model::PlayerSession playerSession; + playerSession.SetIpAddress("dummyIpAddress"); + playerSession.SetPlayerSessionId("dummyPlayerSessionId"); + playerSession.SetPort(123); + Aws::GameLift::Model::CreatePlayerSessionResult createPlayerSessionResult; + createPlayerSessionResult.SetPlayerSession(playerSession); + Aws::GameLift::Model::CreatePlayerSessionOutcome createPlayerSessionOutcome(createPlayerSessionResult); + auto connectionConfig = JoinSessionActivity::BuildSessionConnectionConfig(createPlayerSessionOutcome); + + EXPECT_TRUE(strcmp(connectionConfig.m_ipAddress.c_str(), playerSession.GetIpAddress().c_str()) == 0); + EXPECT_TRUE(strcmp(connectionConfig.m_playerSessionId.c_str(), playerSession.GetPlayerSessionId().c_str()) == 0); + EXPECT_TRUE(connectionConfig.m_port == playerSession.GetPort()); +} + +TEST_F(AWSGameLiftJoinSessionActivityTest, ValidateJoinSessionRequest_CallWithBaseType_GetFalseResult) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = JoinSessionActivity::ValidateJoinSessionRequest(AzFramework::JoinSessionRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftJoinSessionActivityTest, ValidateJoinSessionRequest_CallWithEmptyPlayerId_GetFalseResult) +{ + AWSGameLiftJoinSessionRequest request; + request.m_sessionId = "dummySessionId"; + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = JoinSessionActivity::ValidateJoinSessionRequest(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftJoinSessionActivityTest, ValidateJoinSessionRequest_CallWithEmptySessionId_GetFalseResult) +{ + AWSGameLiftJoinSessionRequest request; + request.m_playerId = "dummyPlayerId"; + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = JoinSessionActivity::ValidateJoinSessionRequest(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftJoinSessionActivityTest, ValidateJoinSessionRequest_CallWithPlayerAndSessionId_GetTrueResult) +{ + AWSGameLiftJoinSessionRequest request; + request.m_playerId = "dummyPlayerId"; + request.m_sessionId = "dummySessionId"; + auto result = JoinSessionActivity::ValidateJoinSessionRequest(request); + EXPECT_TRUE(result); +} diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftSearchSessionsActivityTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftSearchSessionsActivityTest.cpp new file mode 100644 index 0000000000..8ca2727dec --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/Activity/AWSGameLiftSearchSessionsActivityTest.cpp @@ -0,0 +1,126 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include +#include + +using namespace AWSGameLift; + +using AWSGameLiftSearchSessionsActivityTest = AWSGameLiftClientFixture; + +TEST_F(AWSGameLiftSearchSessionsActivityTest, BuildAWSGameLiftSearchGameSessionsRequest_Call_GetExpectedResult) +{ + AWSGameLiftSearchSessionsRequest request; + request.m_aliasId = "dummyAliasId"; + request.m_fleetId = "dummyFleetId"; + request.m_location = "dummyLocation"; + request.m_filterExpression = "dummyFilterExpression"; + request.m_sortExpression = "dummySortExpression"; + request.m_maxResult = 1; + request.m_nextToken = "dummyNextToken"; + + auto awsRequest = SearchSessionsActivity::BuildAWSGameLiftSearchGameSessionsRequest(request); + + EXPECT_TRUE(strcmp(awsRequest.GetFleetId().c_str(), request.m_fleetId.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetAliasId().c_str(), request.m_aliasId.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetFilterExpression().c_str(), request.m_filterExpression.c_str()) == 0); + EXPECT_TRUE(strcmp(awsRequest.GetSortExpression().c_str(), request.m_sortExpression.c_str()) == 0); + EXPECT_TRUE(awsRequest.GetLimit() == request.m_maxResult); + EXPECT_TRUE(strcmp(awsRequest.GetNextToken().c_str(), request.m_nextToken.c_str()) == 0); + // TODO: Update the AWS Native SDK to get the new request attributes. + //EXPECT_TRUE(strcmp(awsRequest.GetLocation().c_str(), request.m_location.c_str()) == 0); +} + +TEST_F(AWSGameLiftSearchSessionsActivityTest, ValidateSearchSessionsRequest_CallWithBaseType_GetFalseResult) +{ + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = SearchSessionsActivity::ValidateSearchSessionsRequest(AzFramework::SearchSessionsRequest()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftSearchSessionsActivityTest, ValidateSearchSessionsRequest_CallWithoutAliasOrFleetId_GetFalseResult) +{ + AWSGameLiftSearchSessionsRequest request; + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = SearchSessionsActivity::ValidateSearchSessionsRequest(request); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message + EXPECT_FALSE(result); +} + +TEST_F(AWSGameLiftSearchSessionsActivityTest, ValidateSearchSessionsRequest_CallWithAliasId_GetTrueResult) +{ + AWSGameLiftSearchSessionsRequest request; + request.m_aliasId = "dummyAliasId"; + auto result = SearchSessionsActivity::ValidateSearchSessionsRequest(request); + EXPECT_TRUE(result); +} + +TEST_F(AWSGameLiftSearchSessionsActivityTest, ValidateSearchSessionsRequest_CallWithFleetId_GetTrueResult) +{ + AWSGameLiftSearchSessionsRequest request; + request.m_fleetId = "dummyFleetId"; + auto result = SearchSessionsActivity::ValidateSearchSessionsRequest(request); + EXPECT_TRUE(result); +} + +TEST_F(AWSGameLiftSearchSessionsActivityTest, ParseResponse_Call_GetExpectedResult) +{ + Aws::GameLift::Model::GameProperty gameProperty; + gameProperty.SetKey("dummyKey"); + gameProperty.SetValue("dummyValue"); + Aws::Vector gameProperties = { gameProperty }; + + Aws::GameLift::Model::GameSession gameSession; + gameSession.SetCreationTime(Aws::Utils::DateTime(0.0)); + gameSession.SetTerminationTime(Aws::Utils::DateTime(0.0)); + gameSession.SetCreatorId("dummyCreatorId"); + gameSession.SetGameProperties(gameProperties); + gameSession.SetGameSessionId("dummyGameSessionId"); + gameSession.SetName("dummyGameSessionName"); + gameSession.SetIpAddress("dummyIpAddress"); + gameSession.SetPort(0); + gameSession.SetMaximumPlayerSessionCount(2); + gameSession.SetCurrentPlayerSessionCount(1); + gameSession.SetStatus(Aws::GameLift::Model::GameSessionStatus::TERMINATED); + gameSession.SetStatusReason(Aws::GameLift::Model::GameSessionStatusReason::INTERRUPTED); + // TODO: Update the AWS Native SDK to set the new game session attributes. + //gameSession.SetDnsName("dummyDnsName"); + Aws::Vector gameSessions = { gameSession }; + + Aws::GameLift::Model::SearchGameSessionsResult result; + result.SetNextToken("dummyNextToken"); + result.SetGameSessions(gameSessions); + + auto response = SearchSessionsActivity::ParseResponse(result); + + EXPECT_TRUE(strcmp(response.m_nextToken.c_str(), result.GetNextToken().c_str()) == 0); + EXPECT_EQ(response.m_sessionConfigs.size(), 1); + + const auto& sessionConfig = response.m_sessionConfigs[0]; + EXPECT_EQ(gameSession.GetCreationTime().Millis(), sessionConfig.m_creationTime); + EXPECT_EQ(gameSession.GetTerminationTime().Millis(), sessionConfig.m_terminationTime); + EXPECT_TRUE(strcmp(gameSession.GetCreatorId().c_str(), sessionConfig.m_creatorId.c_str()) == 0); + EXPECT_TRUE(strcmp(gameSession.GetGameSessionId().c_str(), sessionConfig.m_sessionId.c_str()) == 0); + EXPECT_TRUE(strcmp(gameSession.GetName().c_str(), sessionConfig.m_sessionName.c_str()) == 0); + EXPECT_TRUE(strcmp(gameSession.GetIpAddress().c_str(), sessionConfig.m_ipAddress.c_str()) == 0); + EXPECT_EQ(gameSession.GetPort(), 0); + EXPECT_EQ(gameSession.GetMaximumPlayerSessionCount(), 2); + EXPECT_EQ(gameSession.GetCurrentPlayerSessionCount(), 1); + EXPECT_TRUE(strcmp(AWSGameLiftSessionStatusNames[(int)gameSession.GetStatus()], sessionConfig.m_status.c_str()) == 0); + EXPECT_TRUE(strcmp(AWSGameLiftSessionStatusReasons[(int)gameSession.GetStatusReason()], sessionConfig.m_statusReason.c_str()) == 0); + // TODO: Update the AWS Native SDK to get the new game session attributes. + // EXPECT_TRUE(strcmp(gameSession.GetDnsName().c_str(), sessionConfig.m_dnsName.c_str()) == 0); +} diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake new file mode 100644 index 0000000000..78a69268f4 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake @@ -0,0 +1,36 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + Include/Request/AWSGameLiftCreateSessionOnQueueRequest.h + Include/Request/AWSGameLiftCreateSessionRequest.h + Include/Request/AWSGameLiftJoinSessionRequest.h + Include/Request/AWSGameLiftSearchSessionsRequest.h + Include/Request/IAWSGameLiftRequests.h + Source/Activity/AWSGameLiftCreateSessionActivity.cpp + Source/Activity/AWSGameLiftCreateSessionActivity.h + Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.cpp + Source/Activity/AWSGameLiftCreateSessionOnQueueActivity.h + Source/Activity/AWSGameLiftJoinSessionActivity.cpp + Source/Activity/AWSGameLiftJoinSessionActivity.h + Source/Activity/AWSGameLiftLeaveSessionActivity.cpp + Source/Activity/AWSGameLiftLeaveSessionActivity.h + Source/Activity/AWSGameLiftSearchSessionsActivity.cpp + Source/Activity/AWSGameLiftSearchSessionsActivity.h + Source/AWSGameLiftClientManager.cpp + Source/AWSGameLiftClientManager.h + Source/AWSGameLiftClientSystemComponent.cpp + Source/AWSGameLiftClientSystemComponent.h + Source/Request/AWSGameLiftCreateSessionOnQueueRequest.cpp + Source/Request/AWSGameLiftCreateSessionRequest.cpp + Source/Request/AWSGameLiftJoinSessionRequest.cpp + Source/Request/AWSGameLiftSearchSessionsRequest.cpp +) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_shared_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_shared_files.cmake new file mode 100644 index 0000000000..73caae35e1 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_shared_files.cmake @@ -0,0 +1,14 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + Source/AWSGameLiftClientModule.cpp +) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_tests_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_tests_files.cmake new file mode 100644 index 0000000000..5cd34f02d3 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_tests_files.cmake @@ -0,0 +1,22 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + Tests/Activity/AWSGameLiftCreateSessionActivityTest.cpp + Tests/Activity/AWSGameLiftCreateSessionOnQueueActivityTest.cpp + Tests/Activity/AWSGameLiftJoinSessionActivityTest.cpp + Tests/Activity/AWSGameLiftSearchSessionsActivityTest.cpp + Tests/AWSGameLiftClientFixture.h + Tests/AWSGameLiftClientManagerTest.cpp + Tests/AWSGameLiftClientMocks.h + Tests/AWSGameLiftClientSystemComponentTest.cpp + Tests/AWSGameLiftClientTest.cpp +) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h b/Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h new file mode 100644 index 0000000000..527c939b8c --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h @@ -0,0 +1,24 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +namespace AWSGameLift +{ + // Reference https://sdk.amazonaws.com/cpp/api/LATEST/_game_session_status_8h_source.html + static const char* AWSGameLiftSessionStatusNames[6] = { "NotSet", "Active", "Activating", "Terminated", "Terminating", "Error"}; + + // Reference https://sdk.amazonaws.com/cpp/api/LATEST/_game_session_status_reason_8h.html + static const char* AWSGameLiftSessionStatusReasons[2] = { "NotSet", "Interrupted" }; + + static constexpr const char AWSGameLiftErrorMessageTemplate[] = "Exception: %s, Message: %s"; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt b/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt new file mode 100644 index 0000000000..b527010ab9 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/CMakeLists.txt @@ -0,0 +1,72 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +if (NOT PAL_TRAIT_BUILD_SERVER_SUPPORTED) + return() +endif() + +ly_add_target( + NAME AWSGameLift.Server.Static STATIC + NAMESPACE Gem + FILES_CMAKE + awsgamelift_server_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + ../AWSGameLiftCommon/Source + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + AZ::AzFramework + 3rdParty::AWSGameLiftServerSDK + ) + +ly_add_target( + NAME AWSGameLift.Servers ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + awsgamelift_server_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + Gem::AWSGameLift.Server.Static + 3rdParty::AWSGameLiftServerSDK +) + +################################################################################ +# Tests +################################################################################ +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME AWSGameLift.Server.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + awsgamelift_server_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + AZ::AzFramework + AZ::AzTest + Gem::AWSGameLift.Server.Static + 3rdParty::AWSGameLiftServerSDK + ) + # Add AWSGameLift.Server.Tests to googletest + ly_add_googletest( + NAME Gem::AWSGameLift.Server.Tests + ) +endif() diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp new file mode 100644 index 0000000000..0ac3326757 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.cpp @@ -0,0 +1,319 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace AWSGameLift +{ + AWSGameLiftServerManager::AWSGameLiftServerManager() + : m_serverSDKInitialized(false) + , m_gameLiftServerSDKWrapper(AZStd::make_unique()) + , m_connectedPlayers() + { + } + + AWSGameLiftServerManager::~AWSGameLiftServerManager() + { + m_gameLiftServerSDKWrapper.reset(); + m_connectedPlayers.clear(); + } + + bool AWSGameLiftServerManager::AddConnectedPlayer(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) + { + AZStd::lock_guard lock(m_gameliftMutex); + if (m_connectedPlayers.contains(playerConnectionConfig.m_playerConnectionId)) + { + if (m_connectedPlayers[playerConnectionConfig.m_playerConnectionId] != playerConnectionConfig.m_playerSessionId) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerPlayerConnectionRegisteredErrorMessage, + playerConnectionConfig.m_playerConnectionId, playerConnectionConfig.m_playerSessionId.c_str()); + } + return false; + } + else + { + m_connectedPlayers.emplace(playerConnectionConfig.m_playerConnectionId, playerConnectionConfig.m_playerSessionId); + return true; + } + } + + AzFramework::SessionConfig AWSGameLiftServerManager::BuildSessionConfig(const Aws::GameLift::Server::Model::GameSession& gameSession) + { + AzFramework::SessionConfig sessionConfig; + + sessionConfig.m_dnsName = gameSession.GetDnsName().c_str(); + AZStd::string propertiesOutput = ""; + for (const auto& gameProperty : gameSession.GetGameProperties()) + { + sessionConfig.m_sessionProperties.emplace(gameProperty.GetKey().c_str(), gameProperty.GetValue().c_str()); + propertiesOutput += AZStd::string::format("{Key=%s,Value=%s},", gameProperty.GetKey().c_str(), gameProperty.GetValue().c_str()); + } + if (!propertiesOutput.empty()) + { + propertiesOutput = propertiesOutput.substr(0, propertiesOutput.size() - 1); // Trim last comma to fit array format + } + sessionConfig.m_sessionId = gameSession.GetGameSessionId().c_str(); + sessionConfig.m_ipAddress = gameSession.GetIpAddress().c_str(); + sessionConfig.m_maxPlayer = gameSession.GetMaximumPlayerSessionCount(); + sessionConfig.m_sessionName = gameSession.GetName().c_str(); + sessionConfig.m_port = gameSession.GetPort(); + sessionConfig.m_status = AWSGameLiftSessionStatusNames[(int)gameSession.GetStatus()]; + + AZ_TracePrintf(AWSGameLiftServerManagerName, + "Built SessionConfig with Name=%s, Id=%s, Status=%s, DnsName=%s, IpAddress=%s, Port=%d, MaxPlayer=%d and Properties=%s", + sessionConfig.m_sessionName.c_str(), + sessionConfig.m_sessionId.c_str(), + sessionConfig.m_status.c_str(), + sessionConfig.m_dnsName.c_str(), + sessionConfig.m_ipAddress.c_str(), + sessionConfig.m_port, + sessionConfig.m_maxPlayer, + AZStd::string::format("[%s]", propertiesOutput.c_str()).c_str()); + + return sessionConfig; + } + + AZ::IO::Path AWSGameLiftServerManager::GetExternalSessionCertificate() + { + // TODO: Add support to get TLS cert file path + return AZ::IO::Path(); + } + + AZ::IO::Path AWSGameLiftServerManager::GetInternalSessionCertificate() + { + // GameLift doesn't support it, return empty path + return AZ::IO::Path(); + } + + bool AWSGameLiftServerManager::InitializeGameLiftServerSDK() + { + if (m_serverSDKInitialized) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerSDKAlreadyInitErrorMessage); + return false; + } + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Initiating Amazon GameLift Server SDK..."); + Aws::GameLift::Server::InitSDKOutcome initOutcome = m_gameLiftServerSDKWrapper->InitSDK(); + m_serverSDKInitialized = initOutcome.IsSuccess(); + + AZ_Error(AWSGameLiftServerManagerName, m_serverSDKInitialized, + AWSGameLiftServerInitSDKErrorMessage, initOutcome.GetError().GetErrorMessage().c_str()); + + return m_serverSDKInitialized; + } + + void AWSGameLiftServerManager::HandleDestroySession() + { + // No further request should be handled by GameLift server manager at this point + if (AZ::Interface::Get()) + { + AZ::Interface::Unregister(this); + } + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Server process is scheduled to be shut down at %s", + m_gameLiftServerSDKWrapper->GetTerminationTime().c_str()); + + // Send notifications to handler(s) to gracefully shut down the server process. + bool destroySessionResult = true; + AZ::EBusReduceResult> result(destroySessionResult); + AzFramework::SessionNotificationBus::BroadcastResult(result, &AzFramework::SessionNotifications::OnDestroySessionBegin); + + if (!destroySessionResult) + { + AZ_Error("AWSGameLift", destroySessionResult, AWSGameLiftServerGameSessionDestroyErrorMessage); + return; + } + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Notifying GameLift server process is ending..."); + Aws::GameLift::GenericOutcome processEndingOutcome = m_gameLiftServerSDKWrapper->ProcessEnding(); + bool processEndingIsSuccess = processEndingOutcome.IsSuccess(); + + AZ_Error(AWSGameLiftServerManagerName, processEndingIsSuccess, AWSGameLiftServerProcessEndingErrorMessage, + processEndingOutcome.GetError().GetErrorMessage().c_str()); + } + + void AWSGameLiftServerManager::HandlePlayerLeaveSession(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) + { + AZStd::string playerSessionId = ""; + RemoveConnectedPlayer(playerConnectionConfig.m_playerConnectionId, playerSessionId); + if (playerSessionId.empty()) + { + return; + } + + Aws::GameLift::GenericOutcome disconnectOutcome = m_gameLiftServerSDKWrapper->RemovePlayerSession(playerSessionId); + AZ_Error(AWSGameLiftServerManagerName, disconnectOutcome.IsSuccess(), AWSGameLiftServerRemovePlayerSessionErrorMessage, + playerSessionId.c_str(), disconnectOutcome.GetError().GetErrorMessage().c_str()); + } + + bool AWSGameLiftServerManager::NotifyGameLiftProcessReady(const GameLiftServerProcessDesc& desc) + { + if (!m_serverSDKInitialized) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerSDKNotInitErrorMessage); + return false; + } + + AZ_Warning(AWSGameLiftServerManagerName, desc.m_port != 0, AWSGameLiftServerTempPortErrorMessage); + + AZ::JobContext* jobContext = nullptr; + AZ::JobManagerBus::BroadcastResult(jobContext, &AZ::JobManagerEvents::GetGlobalContext); + AZ::Job* processReadyJob = AZ::CreateJobFunction( + [this, desc]() { + // The GameLift ProcessParameters object expects an vector (std::vector) of standard strings (std::string) as the log paths. + std::vector logPaths; + for (const AZStd::string& path : desc.m_logPaths) + { + logPaths.push_back(path.c_str()); + } + + Aws::GameLift::Server::ProcessParameters processReadyParameter = Aws::GameLift::Server::ProcessParameters( + AZStd::bind(&AWSGameLiftServerManager::OnStartGameSession, this, AZStd::placeholders::_1), + AZStd::bind(&AWSGameLiftServerManager::OnUpdateGameSession, this), + AZStd::bind(&AWSGameLiftServerManager::OnProcessTerminate, this), + AZStd::bind(&AWSGameLiftServerManager::OnHealthCheck, this), desc.m_port, + Aws::GameLift::Server::LogParameters(logPaths)); + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Notifying GameLift server process is ready..."); + auto processReadyOutcome = m_gameLiftServerSDKWrapper->ProcessReady(processReadyParameter); + + if (!processReadyOutcome.IsSuccess()) + { + AZ_Error(AWSGameLiftServerManagerName, false, + AWSGameLiftServerProcessReadyErrorMessage, processReadyOutcome.GetError().GetErrorMessage().c_str()); + this->HandleDestroySession(); + } + }, true, jobContext); + processReadyJob->Start(); + return true; + } + + void AWSGameLiftServerManager::OnStartGameSession(const Aws::GameLift::Server::Model::GameSession& gameSession) + { + AzFramework::SessionConfig sessionConfig = BuildSessionConfig(gameSession); + + bool createSessionResult = true; + AZ::EBusReduceResult> result(createSessionResult); + AzFramework::SessionNotificationBus::BroadcastResult( + result, &AzFramework::SessionNotifications::OnCreateSessionBegin, sessionConfig); + + if (createSessionResult) + { + AZ_TracePrintf(AWSGameLiftServerManagerName, "Activating GameLift game session..."); + Aws::GameLift::GenericOutcome activationOutcome = m_gameLiftServerSDKWrapper->ActivateGameSession(); + + if (activationOutcome.IsSuccess()) + { + // Register server manager as handler once game session has been activated + if (!AZ::Interface::Get()) + { + AZ::Interface::Register(this); + } + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerActivateGameSessionErrorMessage, + activationOutcome.GetError().GetErrorMessage().c_str()); + HandleDestroySession(); + } + } + else + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerGameInitErrorMessage); + HandleDestroySession(); + } + } + + void AWSGameLiftServerManager::OnProcessTerminate() + { + AZ_TracePrintf(AWSGameLiftServerManagerName, "GameLift is shutting down server process..."); + + HandleDestroySession(); + } + + bool AWSGameLiftServerManager::OnHealthCheck() + { + bool healthCheckResult = true; + AZ::EBusReduceResult> result(healthCheckResult); + AzFramework::SessionNotificationBus::BroadcastResult(result, &AzFramework::SessionNotifications::OnSessionHealthCheck); + + return m_serverSDKInitialized && healthCheckResult; + } + + void AWSGameLiftServerManager::OnUpdateGameSession() + { + // TODO: Perform game-specific tasks to prep for newly matched players + return; + } + + bool AWSGameLiftServerManager::RemoveConnectedPlayer(uint32_t playerConnectionId, AZStd::string& outPlayerSessionId) + { + AZStd::lock_guard lock(m_gameliftMutex); + if (m_connectedPlayers.contains(playerConnectionId)) + { + outPlayerSessionId = m_connectedPlayers[playerConnectionId]; + m_connectedPlayers.erase(playerConnectionId); + return true; + } + else + { + outPlayerSessionId = ""; + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerPlayerConnectionMissingErrorMessage, playerConnectionId); + return false; + } + } + + void AWSGameLiftServerManager::SetGameLiftServerSDKWrapper(AZStd::unique_ptr gameLiftServerSDKWrapper) + { + m_gameLiftServerSDKWrapper.reset(); + m_gameLiftServerSDKWrapper = AZStd::move(gameLiftServerSDKWrapper); + } + + bool AWSGameLiftServerManager::ValidatePlayerJoinSession(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) + { + uint32_t playerConnectionId = playerConnectionConfig.m_playerConnectionId; + AZStd::string playerSessionId = playerConnectionConfig.m_playerSessionId; + if (playerSessionId.empty()) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerInvalidConnectionConfigErrorMessage, + playerConnectionId, playerSessionId.c_str()); + return false; + } + + if (!AddConnectedPlayer(playerConnectionConfig)) + { + return false; + } + + AZ_TracePrintf(AWSGameLiftServerManagerName, "Attempting to accept player session connection with Amazon GameLift service..."); + auto acceptPlayerSessionOutcome = m_gameLiftServerSDKWrapper->AcceptPlayerSession(playerSessionId.c_str()); + + if (!acceptPlayerSessionOutcome.IsSuccess()) + { + AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerAcceptPlayerSessionErrorMessage, + playerSessionId.c_str(), acceptPlayerSessionOutcome.GetError().GetErrorMessage().c_str()); + RemoveConnectedPlayer(playerConnectionId, playerSessionId); + return false; + } + return true; + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h new file mode 100644 index 0000000000..2457425ab1 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h @@ -0,0 +1,128 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace AWSGameLift +{ + class GameLiftServerSDKWrapper; + + //! GameLift server process settings. + struct GameLiftServerProcessDesc + { + AZStd::vector m_logPaths; //!< Log paths the servers will write to. Both relative to the game root folder and absolute paths supported. + + uint16_t m_port = 0; //!< The port the server will be listening on. + }; + + //! Manage the server process for hosting game sessions via GameLiftServerSDK. + class AWSGameLiftServerManager + : public AzFramework::ISessionHandlingProviderRequests + { + public: + static constexpr const char AWSGameLiftServerManagerName[] = "AWSGameLiftServerManager"; + static constexpr const char AWSGameLiftServerSDKNotInitErrorMessage[] = + "Amazon GameLift Server SDK is not initialized yet."; + static constexpr const char AWSGameLiftServerSDKAlreadyInitErrorMessage[] = + "Amazon GameLift Server SDK has already been initialized."; + static constexpr const char AWSGameLiftServerTempPortErrorMessage[] = + "No server port specified, server will be listening on ephemeral port."; + static constexpr const char AWSGameLiftServerGameInitErrorMessage[] = + "Failed to process game dependent initialization during OnStartGameSession."; + static constexpr const char AWSGameLiftServerGameSessionDestroyErrorMessage[] = + "Failed to destroy game session during OnProcessTerminate."; + static constexpr const char AWSGameLiftServerPlayerConnectionRegisteredErrorMessage[] = + "Player connection id %d is already registered to player session id %s. Remove connected player first."; + static constexpr const char AWSGameLiftServerPlayerConnectionMissingErrorMessage[] = + "Player connection id %d does not exist."; + + static constexpr const char AWSGameLiftServerInitSDKErrorMessage[] = + "Failed to initialize Amazon GameLift Server SDK. ErrorMessage: %s"; + static constexpr const char AWSGameLiftServerProcessReadyErrorMessage[] = + "Failed to notify GameLift server process ready. ErrorMessage: %s"; + static constexpr const char AWSGameLiftServerActivateGameSessionErrorMessage[] = + "Failed to activate GameLift game session. ErrorMessage: %s"; + static constexpr const char AWSGameLiftServerProcessEndingErrorMessage[] = + "Failed to notify GameLift server process ending. ErrorMessage: %s"; + static constexpr const char AWSGameLiftServerAcceptPlayerSessionErrorMessage[] = + "Failed to validate player session connection with id %s. ErrorMessage: %s"; + static constexpr const char AWSGameLiftServerInvalidConnectionConfigErrorMessage[] = + "Invalid player connection config, player connection id: %d, player session id: %s"; + static constexpr const char AWSGameLiftServerRemovePlayerSessionErrorMessage[] = + "Failed to notify GameLift that the player with the player session id %s has disconnected from the server process. ErrorMessage: %s"; + + AWSGameLiftServerManager(); + virtual ~AWSGameLiftServerManager(); + + //! Initialize GameLift API client by calling InitSDK(). + //! @return Whether the initialization is successful. + bool InitializeGameLiftServerSDK(); + + //! Notify GameLift that the server process is ready to host a game session. + //! @param desc GameLift server process settings. + //! @return Whether the ProcessReady notification is sent to GameLift. + bool NotifyGameLiftProcessReady(const GameLiftServerProcessDesc& desc); + + // ISessionHandlingProviderRequests interface implementation + void HandleDestroySession() override; + bool ValidatePlayerJoinSession(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) override; + void HandlePlayerLeaveSession(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) override; + AZ::IO::Path GetExternalSessionCertificate() override; + AZ::IO::Path GetInternalSessionCertificate() override; + + protected: + void SetGameLiftServerSDKWrapper(AZStd::unique_ptr gameLiftServerSDKWrapper); + + //! Add connected player session id. + bool AddConnectedPlayer(const AzFramework::PlayerConnectionConfig& playerConnectionConfig); + + private: + //! Build session config by using AWS GameLift Server GameSession Model. + AzFramework::SessionConfig BuildSessionConfig(const Aws::GameLift::Server::Model::GameSession& gameSession); + + //! Callback function that the GameLift service invokes to activate a new game session. + void OnStartGameSession(const Aws::GameLift::Server::Model::GameSession& gameSession); + + //! Callback function that the GameLift service invokes to pass an updated game session object to the server process. + void OnUpdateGameSession(); + + //! Callback function that the server process or GameLift service invokes to force the server process to shut down. + void OnProcessTerminate(); + + //! Callback function that the GameLift service invokes to request a health status report from the server process. + //! @return Whether the server process is healthy. + bool OnHealthCheck(); + + //! Remove connected player session id. + //! @param playerConnectionId Connection id of the player to remove. + //! @param outPlayerSessionId Session id of the removed player. Empty if the player cannot be removed. + //! @return Whether the player is removed successfully. + bool RemoveConnectedPlayer(uint32_t playerConnectionId, AZStd::string& outPlayerSessionId); + + AZStd::unique_ptr m_gameLiftServerSDKWrapper; + bool m_serverSDKInitialized; + + AZStd::mutex m_gameliftMutex; + using PlayerConnectionId = uint32_t; + using PlayerSessionId = AZStd::string; + AZStd::unordered_map m_connectedPlayers; + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerModule.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerModule.cpp new file mode 100644 index 0000000000..32368efdad --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerModule.cpp @@ -0,0 +1,49 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include + +namespace AWSGameLift +{ + //! Provide the entry point for the gem and register the system component. + class AWSGameLiftServerModule + : public AZ::Module + { + public: + AZ_RTTI(AWSGameLiftServerModule, "{898416ca-dc11-4731-87de-afe285aedb04}", AZ::Module); + AZ_CLASS_ALLOCATOR(AWSGameLiftServerModule, AZ::SystemAllocator, 0); + + AWSGameLiftServerModule() + : AZ::Module() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + m_descriptors.insert(m_descriptors.end(), { + AWSGameLiftServerSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList { + azrtti_typeid(), + }; + } + }; +}// namespace AWSGameLift + +AZ_DECLARE_MODULE_CLASS(Gem_AWSGameLift_Server, AWSGameLift::AWSGameLiftServerModule) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.cpp new file mode 100644 index 0000000000..fce8c883f3 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.cpp @@ -0,0 +1,129 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace AWSGameLift +{ + AWSGameLiftServerSystemComponent::AWSGameLiftServerSystemComponent() + : m_gameLiftServerManager(AZStd::make_unique()) + { + } + + AWSGameLiftServerSystemComponent::~AWSGameLiftServerSystemComponent() + { + m_gameLiftServerManager.reset(); + } + + void AWSGameLiftServerSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ; + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("AWSGameLiftServer", "Create the GameLift server manager which manages the server process for hosting a game session via GameLiftServerSDK.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ; + } + } + } + + void AWSGameLiftServerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("AWSGameLiftServerService")); + } + + void AWSGameLiftServerSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("AWSGameLiftServerService")); + } + + void AWSGameLiftServerSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + AZ_UNUSED(required); + } + + void AWSGameLiftServerSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + AZ_UNUSED(dependent); + } + + void AWSGameLiftServerSystemComponent::Init() + { + } + + void AWSGameLiftServerSystemComponent::Activate() + { + if (m_gameLiftServerManager->InitializeGameLiftServerSDK()) + { + GameLiftServerProcessDesc serverProcessDesc; + UpdateGameLiftServerProcessDesc(serverProcessDesc); + m_gameLiftServerManager->NotifyGameLiftProcessReady(serverProcessDesc); + } + } + + void AWSGameLiftServerSystemComponent::Deactivate() + { + m_gameLiftServerManager->HandleDestroySession(); + } + + void AWSGameLiftServerSystemComponent::UpdateGameLiftServerProcessDesc(GameLiftServerProcessDesc& serverProcessDesc) + { + AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetDirectInstance(); + if (fileIO) + { + const char pathToLogFolder[] = "@log@/"; + char resolvedPath[AZ_MAX_PATH_LEN]; + if (fileIO->ResolvePath(pathToLogFolder, resolvedPath, AZ_ARRAY_SIZE(resolvedPath))) + { + serverProcessDesc.m_logPaths.push_back(resolvedPath); + } + else + { + AZ_Error("AWSGameLift", false, "Failed to resolve the path to the log folder."); + } + } + else + { + AZ_Error("AWSGameLift", false, "Failed to get File IO."); + } + + if (auto console = AZ::Interface::Get(); console != nullptr) + { + AZ::GetValueResult getCvarResult = console->GetCvarValue("sv_port", serverProcessDesc.m_port); + AZ_Error( + "AWSGameLift", getCvarResult == AZ::GetValueResult::Success, "Lookup of 'sv_port' console variable failed with error %s", + AZ::GetEnumString(getCvarResult)); + } + } + + void AWSGameLiftServerSystemComponent::SetGameLiftServerManager(AZStd::unique_ptr gameLiftServerManager) + { + m_gameLiftServerManager.reset(); + m_gameLiftServerManager = AZStd::move(gameLiftServerManager); + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.h new file mode 100644 index 0000000000..910d6907aa --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerSystemComponent.h @@ -0,0 +1,57 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AWSGameLift +{ + struct GameLiftServerProcessDesc; + class AWSGameLiftServerManager; + + //! Gem server system component. Responsible for managing the server process for hosting game sessions via the GameLift server manager. + class AWSGameLiftServerSystemComponent + : public AZ::Component + { + public: + AZ_COMPONENT(AWSGameLiftServerSystemComponent, "{fa2b46d6-82a9-408d-abab-62bae5ab38c9}"); + + AWSGameLiftServerSystemComponent(); + virtual ~AWSGameLiftServerSystemComponent(); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + protected: + //////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + void Deactivate() override; + //////////////////////////////////////////////////////////////////////// + + void SetGameLiftServerManager(AZStd::unique_ptr gameLiftServerManager); + + private: + //! Update the serverProcessDesc with appropriate server port number and log paths. + //! @param serverProcessDesc Desc object to update. + void UpdateGameLiftServerProcessDesc(GameLiftServerProcessDesc& serverProcessDesc); + + AZStd::unique_ptr m_gameLiftServerManager; + }; + +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp new file mode 100644 index 0000000000..407914164e --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.cpp @@ -0,0 +1,72 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include + +#pragma warning(disable : 4996) + +namespace AWSGameLift +{ + Aws::GameLift::GenericOutcome GameLiftServerSDKWrapper::AcceptPlayerSession(const std::string& playerSessionId) + { + return Aws::GameLift::Server::AcceptPlayerSession(playerSessionId); + } + + Aws::GameLift::GenericOutcome GameLiftServerSDKWrapper::ActivateGameSession() + { + return Aws::GameLift::Server::ActivateGameSession(); + } + + Aws::GameLift::Server::InitSDKOutcome GameLiftServerSDKWrapper::InitSDK() + { + return Aws::GameLift::Server::InitSDK(); + } + + Aws::GameLift::GenericOutcome GameLiftServerSDKWrapper::ProcessReady( + const Aws::GameLift::Server::ProcessParameters& processParameters) + { + return Aws::GameLift::Server::ProcessReady(processParameters); + } + + Aws::GameLift::GenericOutcome GameLiftServerSDKWrapper::ProcessEnding() + { + return Aws::GameLift::Server::ProcessEnding(); + } + + AZStd::string GameLiftServerSDKWrapper::GetTerminationTime() + { + // Timestamp format is using the UTC ISO8601 format + std::time_t terminationTime; + Aws::GameLift::AwsLongOutcome GetTerminationTimeOutcome = Aws::GameLift::Server::GetTerminationTime(); + if (GetTerminationTimeOutcome.IsSuccess()) + { + terminationTime = GetTerminationTimeOutcome.GetResult(); + } + else + { + // Use the current system time if the termination time is not available from GameLift. + time(&terminationTime); + } + + char buffer[50]; + strftime(buffer, sizeof(buffer), "%FT%TZ", gmtime(&terminationTime)); + + return AZStd::string(buffer); + } + + Aws::GameLift::GenericOutcome GameLiftServerSDKWrapper::RemovePlayerSession(const AZStd::string& playerSessionId) + { + return Aws::GameLift::Server::RemovePlayerSession(playerSessionId.c_str()); + } +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h new file mode 100644 index 0000000000..8188b69f50 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/GameLiftServerSDKWrapper.h @@ -0,0 +1,64 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include + +namespace AWSGameLift +{ + /* Wrapper to use to GameLift Server SDK. + */ + class GameLiftServerSDKWrapper + { + public: + GameLiftServerSDKWrapper() = default; + virtual ~GameLiftServerSDKWrapper() = default; + + //! Processes and validates a player session connection. + //! This method should be called when a client requests a connection to the server. + //! @param playerSessionId the ID of the joining player's session. + //! @return Returns a generic outcome consisting of success or failure with an error message. + virtual Aws::GameLift::GenericOutcome AcceptPlayerSession(const std::string& playerSessionId); + + //! Reports to GameLift that the server process is now ready to receive player sessions. + //! Should be called once all GameSession initialization has finished. + //! @return Returns a generic outcome consisting of success or failure with an error message. + virtual Aws::GameLift::GenericOutcome ActivateGameSession(); + + //! Initializes the GameLift SDK. + //! Should be called when the server starts, before any GameLift-dependent initialization happens. + //! @return If successful, returns an InitSdkOutcome object indicating that the server process is ready to call ProcessReady(). + virtual Aws::GameLift::Server::InitSDKOutcome InitSDK(); + + //! Notifies the GameLift service that the server process is ready to host game sessions. + //! @param processParameters A ProcessParameters object communicating the names of callback methods, port number and game + //! session-specific log files about the server process. + //! @return Returns a generic outcome consisting of success or failure with an error message. + virtual Aws::GameLift::GenericOutcome ProcessReady(const Aws::GameLift::Server::ProcessParameters& processParameters); + + //! Notifies the GameLift service that the server process is shutting down. + //! @return Returns a generic outcome consisting of success or failure with an error message. + virtual Aws::GameLift::GenericOutcome ProcessEnding(); + + //! Returns the time that a server process is scheduled to be shut down. + //! @return Timestamp using the UTC ISO8601 format. + virtual AZStd::string GetTerminationTime(); + + //! Notifies the GameLift service that a player with the specified player session ID has disconnected from the server process. + //! @param playerSessionId Unique ID issued by the Amazon GameLift service in response to a call to the AWS SDK Amazon GameLift API action CreatePlayerSession. + //! @return Returns a generic outcome consisting of success or failure with an error message. + virtual Aws::GameLift::GenericOutcome RemovePlayerSession(const AZStd::string& playerSessionId); + }; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerFixture.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerFixture.h new file mode 100644 index 0000000000..fb1811d483 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerFixture.h @@ -0,0 +1,58 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +class AWSGameLiftServerFixture + : public UnitTest::ScopedAllocatorSetupFixture +{ +public: + AWSGameLiftServerFixture() {} + virtual ~AWSGameLiftServerFixture() = default; + + void SetUp() override + { + AZ::AllocatorInstance::Create(); + AZ::AllocatorInstance::Create(); + + AZ::JobManagerDesc jobManagerDesc; + AZ::JobManagerThreadDesc threadDesc; + + m_jobManager.reset(aznew AZ::JobManager(jobManagerDesc)); + m_jobCancelGroup.reset(aznew AZ::JobCancelGroup()); + jobManagerDesc.m_workerThreads.push_back(threadDesc); + jobManagerDesc.m_workerThreads.push_back(threadDesc); + jobManagerDesc.m_workerThreads.push_back(threadDesc); + m_jobContext.reset(aznew AZ::JobContext(*m_jobManager, *m_jobCancelGroup)); + AZ::JobContext::SetGlobalContext(m_jobContext.get()); + } + + void TearDown() override + { + AZ::JobContext::SetGlobalContext(nullptr); + m_jobContext.reset(); + m_jobCancelGroup.reset(); + m_jobManager.reset(); + AZ::AllocatorInstance::Destroy(); + AZ::AllocatorInstance::Destroy(); + } + + AZStd::unique_ptr m_jobContext; + AZStd::unique_ptr m_jobCancelGroup; + AZStd::unique_ptr m_jobManager; +}; diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp new file mode 100644 index 0000000000..54d318f195 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerManagerTest.cpp @@ -0,0 +1,421 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include + +namespace UnitTest +{ + class SessionNotificationsHandlerMock + : public AzFramework::SessionNotificationBus::Handler + { + public: + SessionNotificationsHandlerMock() + { + AzFramework::SessionNotificationBus::Handler::BusConnect(); + } + + ~SessionNotificationsHandlerMock() + { + AzFramework::SessionNotificationBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(OnSessionHealthCheck, bool()); + MOCK_METHOD1(OnCreateSessionBegin, bool(const AzFramework::SessionConfig&)); + MOCK_METHOD0(OnDestroySessionBegin, bool()); + }; + + class GameLiftServerManagerTest + : public AWSGameLiftServerFixture + { + public: + void SetUp() override + { + AWSGameLiftServerFixture::SetUp(); + + GameLiftServerProcessDesc serverDesc; + m_serverManager = AZStd::make_unique>(); + } + + void TearDown() override + { + m_serverManager.reset(); + + AWSGameLiftServerFixture::TearDown(); + } + + AZStd::unique_ptr> m_serverManager; + }; + + TEST_F(GameLiftServerManagerTest, InitializeGameLiftServerSDK_InitializeTwice_InitSDKCalledOnce) + { + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), InitSDK()).Times(1); + + EXPECT_TRUE(m_serverManager->InitializeGameLiftServerSDK()); + + AZ_TEST_START_TRACE_SUPPRESSION; + EXPECT_FALSE(m_serverManager->InitializeGameLiftServerSDK()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, NotifyGameLiftProcessReady_SDKNotInitialized_FailToNotifyGameLift) + { + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessReady(testing::_)).Times(0); + + AZ_TEST_START_TRACE_SUPPRESSION; + EXPECT_FALSE(m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc())); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, NotifyGameLiftProcessReady_SDKInitialized_ProcessReadyNotificationSent) + { + EXPECT_TRUE(m_serverManager->InitializeGameLiftServerSDK()); + + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessReady(testing::_)).Times(1); + + EXPECT_TRUE(m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc())); + } + + TEST_F(GameLiftServerManagerTest, NotifyGameLiftProcessReady_ProcessReadyFails_TerminationNotificationSent) + { + EXPECT_TRUE(m_serverManager->InitializeGameLiftServerSDK()); + + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessReady(testing::_)) + .Times(1) + .WillOnce(testing::Return(Aws::GameLift::GenericOutcome())); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + EXPECT_TRUE(m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc())); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, OnProcessTerminate_OnDestroySessionBeginReturnsFalse_FailToNotifyGameLift) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + if (!AZ::Interface::Get()) + { + AZ::Interface::Register(m_serverManager.get()); + } + + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(false)); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), GetTerminationTime()).Times(1); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(0); + + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onProcessTerminateFunc(); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + + EXPECT_FALSE(AZ::Interface::Get()); + } + + TEST_F(GameLiftServerManagerTest, OnProcessTerminate_OnDestroySessionBeginReturnsTrue_TerminationNotificationSent) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + if (!AZ::Interface::Get()) + { + AZ::Interface::Register(m_serverManager.get()); + } + + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true)); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), GetTerminationTime()).Times(1); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(1); + + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onProcessTerminateFunc(); + + EXPECT_FALSE(AZ::Interface::Get()); + } + + TEST_F(GameLiftServerManagerTest, OnHealthCheck_OnSessionHealthCheckReturnsTrue_CallbackFunctionReturnsTrue) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnSessionHealthCheck()).Times(1).WillOnce(testing::Return(true)); + EXPECT_TRUE(m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_healthCheckFunc()); + } + + TEST_F(GameLiftServerManagerTest, OnHealthCheck_OnSessionHealthCheckReturnsFalseAndTrue_CallbackFunctionReturnsFalse) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + SessionNotificationsHandlerMock handlerMock1; + EXPECT_CALL(handlerMock1, OnSessionHealthCheck()).Times(1).WillOnce(testing::Return(false)); + SessionNotificationsHandlerMock handlerMock2; + EXPECT_CALL(handlerMock2, OnSessionHealthCheck()).Times(1).WillOnce(testing::Return(true)); + EXPECT_FALSE(m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_healthCheckFunc()); + } + + TEST_F(GameLiftServerManagerTest, OnHealthCheck_OnSessionHealthCheckReturnsFalse_CallbackFunctionReturnsFalse) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnSessionHealthCheck()).Times(1).WillOnce(testing::Return(false)); + EXPECT_FALSE(m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_healthCheckFunc()); + } + + TEST_F(GameLiftServerManagerTest, OnStartGameSession_OnCreateSessionBeginReturnsFalse_TerminationNotificationSent) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnCreateSessionBegin(testing::_)).Times(1).WillOnce(testing::Return(false)); + EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true)); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onStartGameSessionFunc(Aws::GameLift::Server::Model::GameSession()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, OnStartGameSession_ActivateGameSessionSucceeds_RegisterAsHandler) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnCreateSessionBegin(testing::_)).Times(1).WillOnce(testing::Return(true)); + EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true)); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ActivateGameSession()) + .Times(1) + .WillOnce(testing::Return(Aws::GameLift::GenericOutcome(nullptr))); + Aws::GameLift::Server::Model::GameSession testSession; + Aws::GameLift::Server::Model::GameProperty testProperty; + testProperty.SetKey("testKey"); + testProperty.SetValue("testValue"); + testSession.AddGameProperties(testProperty); + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onStartGameSessionFunc(testSession); + EXPECT_TRUE(AZ::Interface::Get()); + m_serverManager->HandleDestroySession(); + } + + TEST_F(GameLiftServerManagerTest, OnStartGameSession_ActivateGameSessionFails_TerminationNotificationSent) + { + m_serverManager->InitializeGameLiftServerSDK(); + m_serverManager->NotifyGameLiftProcessReady(GameLiftServerProcessDesc()); + SessionNotificationsHandlerMock handlerMock; + EXPECT_CALL(handlerMock, OnCreateSessionBegin(testing::_)).Times(1).WillOnce(testing::Return(true)); + EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true)); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ActivateGameSession()) + .Times(1) + .WillOnce(testing::Return(Aws::GameLift::GenericOutcome())); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onStartGameSessionFunc(Aws::GameLift::Server::Model::GameSession()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithInvalidConnectionConfig_GetFalseResultAndExpectedErrorLog) + { + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_serverManager->ValidatePlayerJoinSession(AzFramework::PlayerConnectionConfig()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(result); + } + + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithDuplicatedConnectionId_GetFalseResultAndExpectedErrorLog) + { + AzFramework::PlayerConnectionConfig connectionConfig1; + connectionConfig1.m_playerConnectionId = 123; + connectionConfig1.m_playerSessionId = "dummyPlayerSessionId1"; + GenericOutcome successOutcome(nullptr); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), AcceptPlayerSession(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + m_serverManager->ValidatePlayerJoinSession(connectionConfig1); + AzFramework::PlayerConnectionConfig connectionConfig2; + connectionConfig2.m_playerConnectionId = 123; + connectionConfig2.m_playerSessionId = "dummyPlayerSessionId2"; + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_serverManager->ValidatePlayerJoinSession(connectionConfig2); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(result); + } + + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithValidConnectionConfigButErrorOutcome_GetFalseResultAndExpectedErrorLog) + { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId1"; + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), AcceptPlayerSession(testing::_)).Times(1); + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_serverManager->ValidatePlayerJoinSession(connectionConfig); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(result); + } + + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithValidConnectionConfigAndSuccessOutcome_GetTrueResult) + { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId1"; + GenericOutcome successOutcome(nullptr); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), AcceptPlayerSession(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + auto result = m_serverManager->ValidatePlayerJoinSession(connectionConfig); + EXPECT_TRUE(result); + } + + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithFirstErrorSecondSuccess_GetFirstFalseSecondTrueResult) + { + AzFramework::PlayerConnectionConfig connectionConfig1; + connectionConfig1.m_playerConnectionId = 123; + connectionConfig1.m_playerSessionId = "dummyPlayerSessionId1"; + GenericOutcome successOutcome(nullptr); + Aws::GameLift::GameLiftError error; + GenericOutcome errorOutcome(error); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), AcceptPlayerSession(testing::_)) + .Times(2) + .WillOnce(Return(errorOutcome)) + .WillOnce(Return(successOutcome)); + AZ_TEST_START_TRACE_SUPPRESSION; + auto result = m_serverManager->ValidatePlayerJoinSession(connectionConfig1); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + EXPECT_FALSE(result); + AzFramework::PlayerConnectionConfig connectionConfig2; + connectionConfig2.m_playerConnectionId = 123; + connectionConfig2.m_playerSessionId = "dummyPlayerSessionId2"; + result = m_serverManager->ValidatePlayerJoinSession(connectionConfig2); + EXPECT_TRUE(result); + } + + TEST_F(GameLiftServerManagerTest, ValidatePlayerJoinSession_CallWithMultithread_GetFirstTrueAndRestFalse) + { + int testThreadNumber = 5; + GenericOutcome successOutcome(nullptr); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), AcceptPlayerSession(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + AZStd::vector testThreadPool; + AZStd::atomic trueCount = 0; + for (int index = 0; index < testThreadNumber; index++) + { + testThreadPool.emplace_back(AZStd::thread([&]() { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId"; + auto result = m_serverManager->ValidatePlayerJoinSession(connectionConfig); + if (result) + { + trueCount++; + } + })); + } + for (auto& testThread : testThreadPool) + { + testThread.join(); + } + EXPECT_TRUE(trueCount == 1); + } + + TEST_F(GameLiftServerManagerTest, HandlePlayerLeaveSession_CallWithInvalidConnectionConfig_GetExpectedErrorLog) + { + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), RemovePlayerSession(testing::_)).Times(0); + + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->HandlePlayerLeaveSession(AzFramework::PlayerConnectionConfig()); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, HandlePlayerLeaveSession_CallWithNonExistentPlayerConnectionId_GetExpectedErrorLog) + { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId"; + auto result = m_serverManager->AddConnectedTestPlayer(connectionConfig); + EXPECT_TRUE(result); + + AzFramework::PlayerConnectionConfig connectionConfig1; + connectionConfig1.m_playerConnectionId = 456; + connectionConfig1.m_playerSessionId = "dummyPlayerSessionId"; + + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), RemovePlayerSession(testing::_)).Times(0); + + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->HandlePlayerLeaveSession(connectionConfig1); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, HandlePlayerLeaveSession_CallWithValidConnectionConfigButErrorOutcome_GetExpectedErrorLog) + { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId"; + auto result = m_serverManager->AddConnectedTestPlayer(connectionConfig); + EXPECT_TRUE(result); + + Aws::GameLift::GameLiftError error; + GenericOutcome errorOutcome(error); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), RemovePlayerSession(testing::_)) + .Times(1) + .WillOnce(Return(errorOutcome)); + + AZ_TEST_START_TRACE_SUPPRESSION; + m_serverManager->HandlePlayerLeaveSession(connectionConfig); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + } + + TEST_F(GameLiftServerManagerTest, HandlePlayerLeaveSession_CallWithValidConnectionConfigAndSuccessOutcome_RemovePlayerSessionNotificationSent) + { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId"; + auto result = m_serverManager->AddConnectedTestPlayer(connectionConfig); + EXPECT_TRUE(result); + + GenericOutcome successOutcome(nullptr); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), RemovePlayerSession(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + + m_serverManager->HandlePlayerLeaveSession(connectionConfig); + } + + TEST_F(GameLiftServerManagerTest, HandlePlayerLeaveSession_CallWithMultithread_OnlyOneNotificationIsSent) + { + AzFramework::PlayerConnectionConfig connectionConfig; + connectionConfig.m_playerConnectionId = 123; + connectionConfig.m_playerSessionId = "dummyPlayerSessionId"; + auto result = m_serverManager->AddConnectedTestPlayer(connectionConfig); + EXPECT_TRUE(result); + + int testThreadNumber = 5; + GenericOutcome successOutcome(nullptr); + EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), RemovePlayerSession(testing::_)) + .Times(1) + .WillOnce(Return(successOutcome)); + + AZStd::vector testThreadPool; + AZStd::atomic trueCount = 0; + AZ_TEST_START_TRACE_SUPPRESSION; + for (int index = 0; index < testThreadNumber; index++) + { + testThreadPool.emplace_back(AZStd::thread( + [&]() + { + m_serverManager->HandlePlayerLeaveSession(connectionConfig); + })); + } + for (auto& testThread : testThreadPool) + { + testThread.join(); + } + AZ_TEST_STOP_TRACE_SUPPRESSION(testThreadNumber - 1); // The player is only disconnected once. + } +} // namespace UnitTest diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h new file mode 100644 index 0000000000..f5d4d794d5 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerMocks.h @@ -0,0 +1,127 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +using namespace Aws::GameLift; +using namespace AWSGameLift; +using testing::_; +using testing::Invoke; +using testing::Return; +using testing::NiceMock; +using testing::Eq; + +namespace UnitTest +{ + class GameLiftServerSDKWrapperMock + : public GameLiftServerSDKWrapper + { + public: + GameLiftServerSDKWrapperMock() + { + GenericOutcome successOutcome(nullptr); + Server::InitSDKOutcome sdkOutcome(nullptr); + + ON_CALL(*this, InitSDK()).WillByDefault(Return(sdkOutcome)); + ON_CALL(*this, ProcessReady(_)).WillByDefault(Invoke(this, &GameLiftServerSDKWrapperMock::ProcessReadyMock)); + ON_CALL(*this, ProcessEnding()).WillByDefault(Return(successOutcome)); + } + + MOCK_METHOD1(AcceptPlayerSession, GenericOutcome(const std::string&)); + MOCK_METHOD0(ActivateGameSession, GenericOutcome()); + MOCK_METHOD0(InitSDK, Server::InitSDKOutcome()); + MOCK_METHOD1(ProcessReady, GenericOutcome(const Server::ProcessParameters& processParameters)); + MOCK_METHOD0(ProcessEnding, GenericOutcome()); + MOCK_METHOD1(RemovePlayerSession, GenericOutcome(const AZStd::string& playerSessionId)); + MOCK_METHOD0(GetTerminationTime, AZStd::string()); + + GenericOutcome ProcessReadyMock(const Server::ProcessParameters& processParameters) + { + m_healthCheckFunc = processParameters.getOnHealthCheck(); + m_onStartGameSessionFunc = processParameters.getOnStartGameSession(); + m_onProcessTerminateFunc = processParameters.getOnProcessTerminate(); + + GenericOutcome successOutcome(nullptr); + return successOutcome; + } + + AZStd::function m_healthCheckFunc; + AZStd::function m_onProcessTerminateFunc; + AZStd::function m_onStartGameSessionFunc; + }; + + class AWSGameLiftServerManagerMock + : public AWSGameLiftServerManager + { + public: + AWSGameLiftServerManagerMock() + { + AZStd::unique_ptr> gameLiftServerSDKWrapper = + AZStd::make_unique>(); + m_gameLiftServerSDKWrapperMockPtr = gameLiftServerSDKWrapper.get(); + SetGameLiftServerSDKWrapper(AZStd::move(gameLiftServerSDKWrapper)); + } + + ~AWSGameLiftServerManagerMock() + { + m_gameLiftServerSDKWrapperMockPtr = nullptr; + } + + bool AddConnectedTestPlayer(const AzFramework::PlayerConnectionConfig& playerConnectionConfig) + { + return AddConnectedPlayer(playerConnectionConfig); + } + + NiceMock* m_gameLiftServerSDKWrapperMockPtr; + }; + + class AWSGameLiftServerSystemComponentMock + : public AWSGameLift::AWSGameLiftServerSystemComponent + { + public: + AWSGameLiftServerSystemComponentMock() + { + SetGameLiftServerManager(AZStd::make_unique>()); + + ON_CALL(*this, Init()).WillByDefault(testing::Invoke(this, &AWSGameLiftServerSystemComponentMock::InitMock)); + ON_CALL(*this, Activate()).WillByDefault(testing::Invoke(this, &AWSGameLiftServerSystemComponentMock::ActivateMock)); + ON_CALL(*this, Deactivate()).WillByDefault(testing::Invoke(this, &AWSGameLiftServerSystemComponentMock::DeactivateMock)); + } + + void InitMock() + { + AWSGameLift::AWSGameLiftServerSystemComponent::Init(); + } + + void ActivateMock() + { + AWSGameLift::AWSGameLiftServerSystemComponent::Activate(); + } + + void DeactivateMock() + { + AWSGameLift::AWSGameLiftServerSystemComponent::Deactivate(); + } + + MOCK_METHOD0(Init, void()); + MOCK_METHOD0(Activate, void()); + MOCK_METHOD0(Deactivate, void()); + + GameLiftServerProcessDesc m_serverProcessDesc; + }; +}; diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerSystemComponentTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerSystemComponentTest.cpp new file mode 100644 index 0000000000..805b386320 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerSystemComponentTest.cpp @@ -0,0 +1,99 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace UnitTest +{ + class AWSGameLiftServerSystemComponentTest + : public AWSGameLiftServerFixture + { + public: + void SetUp() override + { + AWSGameLiftServerFixture::SetUp(); + + m_serializeContext = AZStd::make_unique(); + m_serializeContext->CreateEditContext(); + m_behaviorContext = AZStd::make_unique(); + m_componentDescriptor.reset(AWSGameLift::AWSGameLiftServerSystemComponent::CreateDescriptor()); + m_componentDescriptor->Reflect(m_serializeContext.get()); + m_componentDescriptor->Reflect(m_behaviorContext.get()); + + m_entity = aznew AZ::Entity(); + + m_AWSGameLiftServerSystemsComponent = aznew NiceMock(); + m_entity->AddComponent(m_AWSGameLiftServerSystemsComponent); + + // Set up the file IO and alias + m_localFileIO = aznew AZ::IO::LocalFileIO(); + m_priorFileIO = AZ::IO::FileIOBase::GetInstance(); + + AZ::IO::FileIOBase::SetInstance(nullptr); + AZ::IO::FileIOBase::SetInstance(m_localFileIO); + m_localFileIO->SetAlias("@log@", AZ_TRAIT_TEST_ROOT_FOLDER); + } + + void TearDown() override + { + AZ::IO::FileIOBase::SetInstance(nullptr); + delete m_localFileIO; + AZ::IO::FileIOBase::SetInstance(m_priorFileIO); + + m_entity->RemoveComponent(m_AWSGameLiftServerSystemsComponent); + delete m_AWSGameLiftServerSystemsComponent; + delete m_entity; + + m_componentDescriptor.reset(); + m_behaviorContext.reset(); + m_serializeContext.reset(); + + AWSGameLiftServerFixture::TearDown(); + } + + AZStd::unique_ptr m_componentDescriptor; + AZStd::unique_ptr m_serializeContext; + AZStd::unique_ptr m_behaviorContext; + + AZ::Entity* m_entity; + NiceMock* m_AWSGameLiftServerSystemsComponent; + + AZ::IO::FileIOBase* m_priorFileIO; + AZ::IO::FileIOBase* m_localFileIO; + }; + + TEST_F(AWSGameLiftServerSystemComponentTest, ActivateDeactivateComponent_ExecuteInOrder_Success) + { + testing::Sequence s1, s2; + + EXPECT_CALL(*m_AWSGameLiftServerSystemsComponent, Init()).Times(1).InSequence(s1); + EXPECT_CALL(*m_AWSGameLiftServerSystemsComponent, Activate()).Times(1).InSequence(s1); + + EXPECT_CALL(*m_AWSGameLiftServerSystemsComponent, Deactivate()).Times(1).InSequence(s2); + + // activate component + m_entity->Init(); + m_entity->Activate(); + + // deactivate component + m_entity->Deactivate(); + } + +} // namespace UnitTest diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerTest.cpp new file mode 100644 index 0000000000..dfe3edaa96 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Tests/AWSGameLiftServerTest.cpp @@ -0,0 +1,15 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake new file mode 100644 index 0000000000..3a2b3b6295 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake @@ -0,0 +1,20 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + ../AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h + Source/AWSGameLiftServerManager.cpp + Source/AWSGameLiftServerManager.h + Source/AWSGameLiftServerSystemComponent.cpp + Source/AWSGameLiftServerSystemComponent.h + Source/GameLiftServerSDKWrapper.cpp + Source/GameLiftServerSDKWrapper.h +) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_shared_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_shared_files.cmake new file mode 100644 index 0000000000..4182aeae26 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_shared_files.cmake @@ -0,0 +1,14 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + Source/AWSGameLiftServerModule.cpp +) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_tests_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_tests_files.cmake new file mode 100644 index 0000000000..cffec47098 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_tests_files.cmake @@ -0,0 +1,18 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + Tests/AWSGameLiftServerFixture.h + Tests/AWSGameLiftServerMocks.h + Tests/AWSGameLiftServerManagerTest.cpp + Tests/AWSGameLiftServerTest.cpp + Tests/AWSGameLiftServerSystemComponentTest.cpp +) diff --git a/Gems/AWSGameLift/Code/CMakeLists.txt b/Gems/AWSGameLift/Code/CMakeLists.txt new file mode 100644 index 0000000000..972a42c7e2 --- /dev/null +++ b/Gems/AWSGameLift/Code/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +add_subdirectory(AWSGameLiftClient) +add_subdirectory(AWSGameLiftServer) diff --git a/Gems/AWSGameLift/cdk/.gitignore b/Gems/AWSGameLift/cdk/.gitignore new file mode 100644 index 0000000000..383cdd5040 --- /dev/null +++ b/Gems/AWSGameLift/cdk/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.env +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/Gems/AWSGameLift/cdk/README.md b/Gems/AWSGameLift/cdk/README.md new file mode 100644 index 0000000000..34933d3651 --- /dev/null +++ b/Gems/AWSGameLift/cdk/README.md @@ -0,0 +1,99 @@ + +# Welcome to O3DE GameLift Sample Project! + +This is an optional CDK application that provides two stacks: + + * A GameLift stack that contains all the GameLift resources required to host game servers + * An optional support stack which is used to upload local build files and create GameLift builds + +The `cdk.json` file tells the CDK Toolkit how to execute this application. + +This project is set up like a standard Python project. The initialization +process also creates a virtualenv within this project, stored under the `.env` +directory. To create the virtualenv it assumes that there is a `python3` +(or `python` for Windows) (Python 3.7+) executable in your path with access to the `venv` +package. If for any reason the automatic creation of the virtualenv fails, +you can create the virtualenv manually. + +See https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html about for information about how to set up +the prerequisites for CDK development. + +To manually create a virtualenv on MacOS and Linux: + +``` +$ python -m venv .env +``` + +Once the virtualenv is created, you can use the following step to activate your virtualenv. + +``` +$ source .env/bin/activate +``` + +If you are a Windows platform, you would activate the virtualenv like this: + +``` +% .env\Scripts\activate.bat +``` + +Once the virtualenv is activated, you can install the required dependencies. + +``` +$ pip install -r requirements.txt +``` + +## Set environment variables or accept defaults + +* O3DE_AWS_DEPLOY_REGION*: The region to deploy the stacks into, will default to CDK_DEFAULT_REGION +* O3DE_AWS_DEPLOY_ACCOUNT*: The account to deploy stacks into, will default to CDK_DEFAULT_ACCOUNT +* O3DE_AWS_PROJECT_NAME*: The name of the O3DE project stacks should be deployed for will default to AWS-PROJECT + +See https://docs.aws.amazon.com/cdk/latest/guide/environments.html for more information including how to pass parameters +to use for environment variables. + +## Edit the sample fleet configurations + +Before deploy the CDK application, please update the sample fleet configurations defined in the +[sample fleet configurations](aws_gamelift/fleet_configurations.py) + with project specific settings. + +## Synthesize the project + +At this point you can now synthesize the CloudFormation template for this code. + +``` +$ cdk synth +``` + +## Optional features +To create a game session queue using this CDK application, provide the following context variable +when synthesize the CloudFormation template or deploy the application: + +``` +$ cdk deploy -c create_game_session_queue=true +``` + +You can also deploy a support stack which is used to upload local build files to S3 and provide GameLift access +to the S3 objects when create GameLift builds: + +``` +$ cdk deploy -c upload-with-support-stack=true --all +``` + +You may need todo a one time bootstrap, once per account, per region. The CDK application will prompt you on this. + +To add additional dependencies, for example other CDK libraries, just add +them to your `setup.py` file and rerun the `pip install -r requirements.txt` +command. + +## Useful commands + + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +## GameLift reference and best practise +https://docs.aws.amazon.com/gamelift/index.html + diff --git a/Gems/AWSGameLift/cdk/app.py b/Gems/AWSGameLift/cdk/app.py new file mode 100644 index 0000000000..5b94bdbba9 --- /dev/null +++ b/Gems/AWSGameLift/cdk/app.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import os + +from aws_cdk import core + +from aws_gamelift.aws_gamelift_construct import AWSGameLift + +"""Configuration""" +REGION = os.environ.get('O3DE_AWS_DEPLOY_REGION', os.environ.get('CDK_DEFAULT_REGION')) +ACCOUNT = os.environ.get('O3DE_AWS_DEPLOY_ACCOUNT', os.environ.get('CDK_DEFAULT_ACCOUNT')) + +# Set the common prefix to group stacks in a project together. +PROJECT_NAME = os.environ.get('O3DE_AWS_PROJECT_NAME', f'O3DE-AWS-PROJECT').upper() + +# The name of this feature +FEATURE_NAME = 'AWSGameLift' + +# The name of this CDK application +PROJECT_FEATURE_NAME = f'{PROJECT_NAME}-{FEATURE_NAME}' + +# Standard Tag Key for project based tags +O3DE_PROJECT_TAG_NAME = 'O3DEProject' +# Standard Tag Key for feature based tags +O3DE_FEATURE_TAG_NAME = 'O3DEFeature' + +"""End of Configuration""" + +# Set-up regions to deploy stack to, or use default if not set +env = core.Environment( + account=ACCOUNT, + region=REGION) + +app = core.App() +feature_struct = AWSGameLift( + app, + id_=PROJECT_FEATURE_NAME, + project_name=PROJECT_NAME, + feature_name=FEATURE_NAME, + tags={O3DE_PROJECT_TAG_NAME: PROJECT_NAME, O3DE_FEATURE_TAG_NAME: FEATURE_NAME}, + env=env +) +app.synth() diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/__init__.py b/Gems/AWSGameLift/cdk/aws_gamelift/__init__.py new file mode 100644 index 0000000000..cdee4b5a56 --- /dev/null +++ b/Gems/AWSGameLift/cdk/aws_gamelift/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py b/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py new file mode 100644 index 0000000000..a9935b72eb --- /dev/null +++ b/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py @@ -0,0 +1,59 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import copy + +from aws_cdk import core + +from aws_gamelift.fleet_configurations import FLEET_CONFIGURATIONS +from aws_gamelift.gamelift_stack import GameLiftStack +from aws_gamelift.support_stack import SupportStack + + +class AWSGameLift(core.Construct): + """ + Orchestrates setting up the AWS GameLift Stack(s) + """ + def __init__(self, + scope: core.Construct, + id_: str, + project_name: str, + feature_name: str, + tags: dict, + env: core.Environment) -> None: + super().__init__(scope, id_) + + stack_name = f'{project_name}-{feature_name}-{env.region}' + + fleet_configurations = copy.deepcopy(FLEET_CONFIGURATIONS) + if self.node.try_get_context('upload-with-support-stack') == 'true': + # Create an optional support stack for generating GameLift builds with local build files + self._support_stack = SupportStack( + scope, + f'{stack_name}-Support', + stack_name=stack_name, + fleet_configurations=fleet_configurations, + description='(Optional) Contains resources for creating GameLift builds with local files', + tags=tags, + env=env + ) + + # Create the GameLift Stack + self._feature_stack = GameLiftStack( + scope, + f'{stack_name}', + stack_name=stack_name, + fleet_configurations=fleet_configurations, + create_game_session_queue=self.node.try_get_context('create_game_session_queue') == 'true', + description=f'Contains resources for the AWS GameLift Gem stack as part of the {project_name} project', + tags=tags, + env=env + ) diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py b/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py new file mode 100644 index 0000000000..d5390ae7b3 --- /dev/null +++ b/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py @@ -0,0 +1,147 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +# Configurations for the fleets to deploy. +# Modify the fleet configuration fields below before deploying the CDK application. +# To select the right combination of hosting resources and learn how to configure them to best suit to your application, +# please check: https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-design.html +FLEET_CONFIGURATIONS = [ + { + # (Optional) An alias for an Amazon GameLift fleet destination. + # By using aliases instead of specific fleet IDs, customers can more easily and seamlessly switch + # player traffic from one fleet to another by changing the alias's target location. + 'alias_configuration': { + # (Required) A descriptive label that is associated with an alias. Alias names do not need to be unique. + 'name': '', + # (Conditional) A type of routing strategy for the GameLift fleet alias if exists. + # Required if alias_configuration is provided. + 'routing_strategy': { + # The message text to be used with a terminal routing strategy. + # If you specify TERMINAL for the Type property, you must specify this property. + # Required if specify TERMINAL for the Type property, + 'message': '', + # (Required) A type of routing strategy. + 'type': 'SIMPLE | TERMINAL' + } + }, + # (Required) Information about a game server build that is installed and + # run on instances in an Amazon GameLift fleet. + 'build_configuration': { + # (Conditional) A unique identifier for a build to be deployed on the new fleet. + # This parameter is required unless the parameters build_path and operating_system are defined and + # the conditional variable upload-with-support-stack is set to true + 'build_id': '', + # (Conditional) The disk location of the local build file(zip). + # This parameter is required unless the parameter build_id is defined. + 'build_path': '', + # (Conditional) The operating system that the game server binaries are built to run on. + # This parameter is required if the parameter build_path is defined. + 'operating_system': 'AMAZON_LINUX | AMAZON_LINUX_2 | WINDOWS_2012' + }, + # (Optional) Information about the use of a TLS/SSL certificate for a fleet. + 'certificate_configuration': { + # (Required) Indicates whether a TLS/SSL certificate is generated for the fleet. + 'certificate_type': 'DISABLED | GENERATED', + }, + # A human-readable description of the fleet. + 'description': 'Amazon GameLift fleet to host game servers.', + # (Optional) A range of IP addresses and port settings that allow inbound traffic to connect to + # server processes on an Amazon GameLift server. + # This should be the same port range as the server is configured for. + 'ec2_inbound_permissions': [ + { + # (Required) A starting value for a range of allowed port numbers. + # 30090 is the default server port defined by the Multiplayer Gem. + 'from_port': 30090, + # (Required) A range of allowed IP addresses. + 'ip_range': '', + # (Required) The network communication protocol used by the fleet. + 'protocol': 'UDP', + # (Required) An ending value for a range of allowed port numbers. + 'to_port': 30090 + }, + { + # Open the debug port for remote into a Windows fleet. + 'from_port': 3389, + 'ip_range': '', + 'protocol': 'TCP', + 'to_port': 3389 + }, + { + # Open the debug port for remote into a Linux fleet. + 'from_port': 22, + 'ip_range': '', + 'protocol': 'TCP', + 'to_port': 22 + } + ], + # (Optional) The GameLift-supported EC2 instance type to use for all fleet instances. + 'ec2_instance_type': 'c3.2xlarge | c3.4xlarge | c3.8xlarge | c3.large | c3.xlarge | c4.2xlarge | c4.4xlarge |' + ' c4.8xlarge | c4.large | c4.xlarge | c5.12xlarge | c5.18xlarge | c5.24xlarge |' + ' c5.2xlarge | c5.4xlarge | c5.9xlarge | c5.large | c5.xlarge | c5a.12xlarge |' + ' c5a.16xlarge | c5a.24xlarge | c5a.2xlarge | c5a.4xlarge | c5a.8xlarge | c5a.large |' + ' c5a.xlarge | m3.2xlarge | m3.large | m3.medium | m3.xlarge | m4.10xlarge | m4.2xlarge |' + ' m4.4xlarge | m4.large | m4.xlarge | m5.12xlarge | m5.16xlarge | m5.24xlarge |' + ' m5.2xlarge | m5.4xlarge | m5.8xlarge | m5.large | m5.xlarge | m5a.12xlarge |' + ' m5a.16xlarge | m5a.24xlarge | m5a.2xlarge | m5a.4xlarge | m5a.8xlarge | m5a.large |' + ' m5a.xlarge | r3.2xlarge | r3.4xlarge | r3.8xlarge | r3.large | r3.xlarge | r4.16xlarge |' + ' r4.2xlarge | r4.4xlarge | r4.8xlarge | r4.large | r4.xlarge | r5.12xlarge |' + ' r5.16xlarge | r5.24xlarge | r5.2xlarge | r5.4xlarge | r5.8xlarge | r5.large |' + ' r5.xlarge | r5a.12xlarge | r5a.16xlarge | r5a.24xlarge | r5a.2xlarge | r5a.4xlarge |' + ' r5a.8xlarge | r5a.large | r5a.xlarge | t2.large | t2.medium | t2.micro | t2.small', + # (Optional) Indicates whether to use On-Demand or Spot instances for this fleet. + 'fleet_type': 'ON_DEMAND | SPOT', + # (Optional) A game session protection policy to apply to all game sessions hosted on instances in this fleet. + 'new_game_session_protection_policy': 'FullProtection | NoProtection', + # (Optional) A policy that limits the number of game sessions that an individual player + # can create on instances in this fleet within a specified span of time. + 'resource_creation_limit_policy': { + # (Optional) The maximum number of game sessions that an individual can create during the policy period. + # Provide any integer not less than 0. + 'new_game_sessions_per_creator': 3, + # (Optional) The time span used in evaluating the resource creation limit policy. + # Provide any integer not less than 0. + 'policy_period_in_minutes': 15 + }, + # (Conditional) Instructions for launching server processes on each instance in the fleet. + # This parameter is required unless the parameters ServerLaunchPath and ServerLaunchParameters are defined. + 'runtime_configuration': { + # (Optional) The maximum amount of time (in seconds) allowed to launch a new game session and + # have it report ready to host players. + # Provide an integer from 1 to 600. + 'game_session_activation_timeout_seconds': 300, + # (Optional) The number of game sessions in status ACTIVATING to allow on an instance. + # Provide an integer from 1 to 2147483647. + 'max_concurrent_game_session_activations': 2, + # (Optional) A collection of server process configurations that identify what server processes + # to run on each instance in a fleet. To set up a fleet's runtime configuration to + # run multiple game server processes per instance, please check the following document: + # https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-multiprocess.html + 'server_processes': [ + { + # (Required) The number of server processes using this configuration that + # run concurrently on each instance. + # Provide any integer not less than 1. + 'concurrent_executions': 1, + # (Required) The location of a game build executable or the Realtime script file that + # contains the Init() function. + 'launch_path': '(Windows) | ' + '(Linux) /local/game/MyGame/', + # (Optional) An optional list of parameters to pass to the server executable + # or Realtime script on launch. + 'parameters': '' + } + ] + } + # For additional fleet configurations, please check: + # # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_GameLift.html + } +] diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py b/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py new file mode 100644 index 0000000000..99702f054a --- /dev/null +++ b/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py @@ -0,0 +1,184 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import typing + +from aws_cdk import core +from aws_cdk import aws_gamelift as gamelift + + +class GameLiftStack(core.Stack): + """ + The AWS GameLift stack + + Defines GameLift resources to use in project + """ + def __init__(self, scope: core.Construct, id_: str, + stack_name: str, fleet_configurations: dict, + create_game_session_queue: bool, **kwargs) -> None: + super().__init__(scope, id_, **kwargs) + + self._stack_name = stack_name + + fleet_ids = [] + queue_destinations = [] + for index in range(len(fleet_configurations)): + fleet_configuration = fleet_configurations[index] + # Create a new GameLift fleet using the configuration + fleet_ids.append(self._create_fleet(fleet_configuration, index).attr_fleet_id) + destination_arn = core.Fn.sub( + body='arn:${AWS::Partition}:gamelift:${AWS::Region}::fleet/${FleetId}', + variables={ + 'FleetId': fleet_ids[index], + } + ) + + if fleet_configuration.get('alias_configuration'): + # Create an alias for the fleet if the alias configuration is provided + alias = self._create_alias(fleet_configuration['alias_configuration'], fleet_ids[index]) + destination_arn = core.Fn.sub( + body='arn:${AWS::Partition}:gamelift:${AWS::Region}::alias/${AliasId}', + variables={ + 'AliasId': alias.attr_alias_id, + } + ) + + queue_destinations.append(destination_arn) + + # Export the GameLift fleet ids as a stack output + fleets_output = core.CfnOutput( + self, + id='GameLiftFleets', + description='List of GameLift fleet ids', + export_name=f'{self._stack_name}:GameLiftFleets', + value=','.join(fleet_ids) + ) + + if create_game_session_queue: + # Create a game session queue which fulfills game session placement requests using the fleets + game_session_queue = self._create_game_session_queue(queue_destinations) + + # Export the game session queue name as a stack output + game_session_queue_output = core.CfnOutput( + self, + id='GameSessionQueue', + description='Name of the game session queue', + export_name=f'{self._stack_name}:GameSessionQueue', + value=game_session_queue.name) + + def _create_fleet(self, fleet_configuration: dict, identifier: int) -> gamelift.CfnFleet: + """ + Create an Amazon GameLift fleet to host game servers. + :param fleet_configuration: Configuration of the fleet. + :param identifier: Unique identifier of the fleet which will be included in the resource id. + :return: Generated GameLift fleet. + """ + fleet = gamelift.CfnFleet( + self, + id=f'{self._stack_name}-GameLiftFleet{identifier}', + build_id=self._get_gamelift_build_id(fleet_configuration.get('build_configuration', {}), identifier), + certificate_configuration=gamelift.CfnFleet.CertificateConfigurationProperty( + certificate_type=fleet_configuration['certificate_configuration'].get('certificate_type') + ) if fleet_configuration.get('certificate_configuration') else None, + description=fleet_configuration.get('description'), + ec2_inbound_permissions=[ + gamelift.CfnFleet.IpPermissionProperty( + **inbound_permission + ) for inbound_permission in fleet_configuration.get('ec2_inbound_permissions', []) + ], + ec2_instance_type=fleet_configuration.get('ec2_instance_type'), + fleet_type=fleet_configuration.get('fleet_type'), + name=f'{self._stack_name}-GameLiftFleet{identifier}', + new_game_session_protection_policy=fleet_configuration.get('new_game_session_protection_policy'), + resource_creation_limit_policy=gamelift.CfnFleet.ResourceCreationLimitPolicyProperty( + **fleet_configuration['resource_creation_limit_policy'] + ) if fleet_configuration.get('resource_creation_limit_policy') else None, + runtime_configuration=gamelift.CfnFleet.RuntimeConfigurationProperty( + game_session_activation_timeout_seconds=fleet_configuration['runtime_configuration'].get( + 'game_session_activation_timeout_seconds'), + max_concurrent_game_session_activations=fleet_configuration['runtime_configuration'].get( + 'max_concurrent_game_session_activations'), + server_processes=[ + gamelift.CfnFleet.ServerProcessProperty( + **server_process + ) for server_process in fleet_configuration['runtime_configuration'].get('server_processes', []) + ] + ) if fleet_configuration.get('runtime_configuration') else None, + ) + + return fleet + + def _get_gamelift_build_id(self, build_configuration: dict, identifier: int) -> str: + """ + Get the GameLift build id. + Create the GameLift build from the storage location information if the build doesn't exist. + :param build_configuration: Configuration of the GameLift build. + :param identifier: Unique identifier of the build which will be included in the resource id. + :return: Build id. + """ + if build_configuration.get('build_id'): + # GameLift build already exists + return build_configuration['build_id'] + elif build_configuration.get('storage_location'): + # Create the GameLift build using the storage location information. + build = gamelift.CfnBuild( + self, + id=f'{self._stack_name}-GameLiftBuild{identifier}', + name=f'{self._stack_name}-GameLiftBuild{identifier}', + operating_system=build_configuration.get('operating_system'), + storage_location=gamelift.CfnBuild.S3LocationProperty( + **build_configuration['storage_location'] + ) + ) + return build.ref + + return '' + + def _create_alias(self, alias_configuration: dict, fleet_id: str) -> gamelift.CfnAlias: + """ + Create an alias for an Amazon GameLift fleet destination. + :param alias_configuration: Configuration of the alias + :param fleet_id: Fleet id that the alias points to. + :return: Generated GameLift fleet alias. + """ + alias = gamelift.CfnAlias( + self, + id=f'{self._stack_name}-GameLiftAlias', + name=alias_configuration.get('name'), + routing_strategy=gamelift.CfnAlias.RoutingStrategyProperty( + **alias_configuration.get('routing_strategy', {}), + fleet_id=fleet_id + ) + ) + + return alias + + def _create_game_session_queue(self, destinations: typing.List) -> gamelift.CfnGameSessionQueue: + """ + Create a placement queue that processes requests for new game sessions. + :param destinations: Destinations of the queue. + :return: Generated GameLift game session queue. + """ + game_session_queue = gamelift.CfnGameSessionQueue( + self, + id=f'{self._stack_name}-GameLiftQueue', + name=f'{self._stack_name}-game-session-queue', + destinations=[ + gamelift.CfnGameSessionQueue.DestinationProperty( + destination_arn=resource_arn + ) for resource_arn in destinations + ] + ) + + return game_session_queue + + + diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py b/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py new file mode 100644 index 0000000000..717b220ca7 --- /dev/null +++ b/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py @@ -0,0 +1,71 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from aws_cdk import aws_iam as iam +from aws_cdk import aws_s3_assets as assets +from aws_cdk import core + + +class SupportStack(core.Stack): + """ + The support stack + + Defines AWS resources that help to create GameLift builds from local files + """ + def __init__(self, scope: core.Construct, id_: str, + stack_name: str, fleet_configurations: dict, **kwargs) -> None: + super().__init__(scope, id_, **kwargs) + self._stack_name = stack_name + self._support_iam_role = self._create_support_iam_role() + + for index in range(len(fleet_configurations)): + # Update the fleet configuration to include the corresponding build id + fleet_configurations[index]['build_configuration']['storage_location'] = self._upload_build_asset( + fleet_configurations[index].get('build_configuration', {}), index) + + def _create_support_iam_role(self) -> iam.Role: + """ + Create an IAM role for GameLift to read build files stored in S3. + :return: Generated IAM role. + """ + support_role = iam.Role( + self, + id=f'{self._stack_name}-SupportRole', + assumed_by=iam.ServicePrincipal( + service='gamelift.amazonaws.com' + ) + ) + + return support_role + + def _upload_build_asset(self, build_configuration: dict, identifier: int) -> dict: + """ + Upload the local build files to S3 for a creating GameLift build. + :param build_configuration: Configuration of the GameLift build. + :param identifier: Unique identifier of the asset which will be included in the resource id. + :return: Storage location of the S3 object. + """ + build_asset = assets.Asset( + self, + id=f'{self._stack_name}-Asset{identifier}', + path=build_configuration.get('build_path') + ) + # Grant the support IAM role permission to read the asset + build_asset.grant_read(self._support_iam_role) + + storage_location = { + 'bucket': build_asset.s3_bucket_name, + 'key': build_asset.s3_object_key, + 'role_arn': self._support_iam_role.role_arn + } + + return storage_location + diff --git a/Gems/AWSGameLift/cdk/cdk.json b/Gems/AWSGameLift/cdk/cdk.json new file mode 100644 index 0000000000..ff5bd91e35 --- /dev/null +++ b/Gems/AWSGameLift/cdk/cdk.json @@ -0,0 +1,17 @@ +{ + "app": "python app.py", + "context": { + "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, + "@aws-cdk/core:enableStackNameDuplicates": "true", + "aws-cdk:enableDiffNoFail": "true", + "@aws-cdk/core:stackRelativeExports": "true", + "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, + "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, + "@aws-cdk/aws-kms:defaultKeyPolicies": true, + "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, + "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, + "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, + "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, + "@aws-cdk/aws-lambda:recognizeVersionProps": true + } +} diff --git a/Gems/AWSGameLift/cdk/requirements.txt b/Gems/AWSGameLift/cdk/requirements.txt new file mode 100644 index 0000000000..f04d2b9aa4 --- /dev/null +++ b/Gems/AWSGameLift/cdk/requirements.txt @@ -0,0 +1,4 @@ +aws-cdk.core>=1.91.0 +aws-cdk.aws_gamelift>=1.91.0 +aws-cdk.aws_iam>=1.91.0 +aws-cdk.aws_s3_assets>=1.91.0 diff --git a/Gems/AWSGameLift/gem.json b/Gems/AWSGameLift/gem.json new file mode 100644 index 0000000000..586d09968b --- /dev/null +++ b/Gems/AWSGameLift/gem.json @@ -0,0 +1,14 @@ +{ + "gem_name": "AWSGameLift", + "origin": "The primary repo for AWSGameLift goes here: i.e. http://www.mydomain.com", + "license": "What license AWSGameLift uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "AWSGameLift", + "summary": "The AWSGameLift Gem provides a framework to extend O3DE networking layer to work with GameLift resources via GameLift server and client SDK.", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "AWSGameLift" + ], + "icon_path": "preview.png" +} diff --git a/Gems/AWSGameLift/preview.png b/Gems/AWSGameLift/preview.png new file mode 100644 index 0000000000..fc6c77b3dd --- /dev/null +++ b/Gems/AWSGameLift/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7aca75b5e82f55478f4f8c575d384ff3ef67260dde2107bd45d377516e6104f7 +size 22196 diff --git a/engine.json b/engine.json index 8449e763a9..e1ba5e8a5d 100644 --- a/engine.json +++ b/engine.json @@ -18,6 +18,7 @@ "Gems/AutomatedLauncherTesting", "Gems/AWSClientAuth", "Gems/AWSCore", + "Gems/AWSGameLift", "Gems/AWSMetrics", "Gems/Blast", "Gems/Camera", From 0a2b51c32d2634ece206b2378f7495a512629a23 Mon Sep 17 00:00:00 2001 From: Junbo Liang <68558268+junbo75@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:50:56 -0700 Subject: [PATCH 04/29] Address the comments on the CDK application (#1560) [LYN-3649] Address the comments on the CDK application Signed-off-by: John --- Gems/AWSGameLift/cdk/.gitignore | 1 + Gems/AWSGameLift/cdk/README.md | 11 ++-- .../aws_gamelift/aws_gamelift_construct.py | 7 ++- .../cdk/aws_gamelift/fleet_configurations.py | 59 +++++++++---------- .../cdk/aws_gamelift/gamelift_stack.py | 3 +- .../cdk/aws_gamelift/support_stack.py | 2 +- 6 files changed, 44 insertions(+), 39 deletions(-) diff --git a/Gems/AWSGameLift/cdk/.gitignore b/Gems/AWSGameLift/cdk/.gitignore index 383cdd5040..58505a0211 100644 --- a/Gems/AWSGameLift/cdk/.gitignore +++ b/Gems/AWSGameLift/cdk/.gitignore @@ -3,6 +3,7 @@ package-lock.json __pycache__ .pytest_cache .env +.venv *.egg-info # CDK asset staging directory diff --git a/Gems/AWSGameLift/cdk/README.md b/Gems/AWSGameLift/cdk/README.md index 34933d3651..9baaec0f26 100644 --- a/Gems/AWSGameLift/cdk/README.md +++ b/Gems/AWSGameLift/cdk/README.md @@ -53,9 +53,10 @@ to use for environment variables. ## Edit the sample fleet configurations -Before deploy the CDK application, please update the sample fleet configurations defined in the -[sample fleet configurations](aws_gamelift/fleet_configurations.py) - with project specific settings. +Before deploy the CDK application, please update the fleet configurations defined in the +[sample fleet configurations](aws_gamelift/fleet_configurations.py) with project specific settings. +You can either use an existing GameLift build id for creating a fleet or provide the local server package path +for creating a new GameLift build. ## Synthesize the project @@ -74,7 +75,9 @@ $ cdk deploy -c create_game_session_queue=true ``` You can also deploy a support stack which is used to upload local build files to S3 and provide GameLift access -to the S3 objects when create GameLift builds: +to the S3 objects when create GameLift builds. The local build path needs to be specified in the +[sample fleet configurations](aws_gamelift/fleet_configurations.py) if the feature is enabled. Otherwise an existing +build id is required. ``` $ cdk deploy -c upload-with-support-stack=true --all diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py b/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py index a9935b72eb..8eaa7b5dac 100644 --- a/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py +++ b/Gems/AWSGameLift/cdk/aws_gamelift/aws_gamelift_construct.py @@ -20,7 +20,10 @@ from aws_gamelift.support_stack import SupportStack class AWSGameLift(core.Construct): """ - Orchestrates setting up the AWS GameLift Stack(s) + Orchestrates setting up the AWS GameLift Stack(s). + + This construct uses the fleet configurations defined in + aws_gamelift/fleet_configurations.py to set up the GameLift stacks. """ def __init__(self, scope: core.Construct, @@ -41,7 +44,7 @@ class AWSGameLift(core.Construct): f'{stack_name}-Support', stack_name=stack_name, fleet_configurations=fleet_configurations, - description='(Optional) Contains resources for creating GameLift builds with local files', + description='Contains resources for creating GameLift builds with local files', tags=tags, env=env ) diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py b/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py index d5390ae7b3..f13df250a0 100644 --- a/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py +++ b/Gems/AWSGameLift/cdk/aws_gamelift/fleet_configurations.py @@ -11,6 +11,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # Configurations for the fleets to deploy. # Modify the fleet configuration fields below before deploying the CDK application. +# Customers can define multiple fleets by copying the existing configuration template below and +# append the new fleet configuration to the FLEET_CONFIGURATIONS list. All the fleets in the list +# will be deployed automatically by this CDK application. # To select the right combination of hosting resources and learn how to configure them to best suit to your application, # please check: https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-design.html FLEET_CONFIGURATIONS = [ @@ -29,7 +32,8 @@ FLEET_CONFIGURATIONS = [ # Required if specify TERMINAL for the Type property, 'message': '', # (Required) A type of routing strategy. - 'type': 'SIMPLE | TERMINAL' + # Choose from SIMPLE or TERMINAL. + 'type': 'SIMPLE' } }, # (Required) Information about a game server build that is installed and @@ -39,17 +43,19 @@ FLEET_CONFIGURATIONS = [ # This parameter is required unless the parameters build_path and operating_system are defined and # the conditional variable upload-with-support-stack is set to true 'build_id': '', - # (Conditional) The disk location of the local build file(zip). + # (Conditional) The disk location of the local build file(.zip). # This parameter is required unless the parameter build_id is defined. 'build_path': '', # (Conditional) The operating system that the game server binaries are built to run on. # This parameter is required if the parameter build_path is defined. - 'operating_system': 'AMAZON_LINUX | AMAZON_LINUX_2 | WINDOWS_2012' + # Choose from AMAZON_LINUX, AMAZON_LINUX or WINDOWS_2012. + 'operating_system': 'WINDOWS_2012' }, # (Optional) Information about the use of a TLS/SSL certificate for a fleet. 'certificate_configuration': { # (Required) Indicates whether a TLS/SSL certificate is generated for the fleet. - 'certificate_type': 'DISABLED | GENERATED', + # Choose from DISABLED or GENERATED. + 'certificate_type': 'DISABLED', }, # A human-readable description of the fleet. 'description': 'Amazon GameLift fleet to host game servers.', @@ -59,14 +65,14 @@ FLEET_CONFIGURATIONS = [ 'ec2_inbound_permissions': [ { # (Required) A starting value for a range of allowed port numbers. - # 30090 is the default server port defined by the Multiplayer Gem. - 'from_port': 30090, + # 33450 is the default server port defined by the Multiplayer Gem. + 'from_port': 33450, # (Required) A range of allowed IP addresses. 'ip_range': '', # (Required) The network communication protocol used by the fleet. 'protocol': 'UDP', # (Required) An ending value for a range of allowed port numbers. - 'to_port': 30090 + 'to_port': 33450 }, { # Open the debug port for remote into a Windows fleet. @@ -84,23 +90,14 @@ FLEET_CONFIGURATIONS = [ } ], # (Optional) The GameLift-supported EC2 instance type to use for all fleet instances. - 'ec2_instance_type': 'c3.2xlarge | c3.4xlarge | c3.8xlarge | c3.large | c3.xlarge | c4.2xlarge | c4.4xlarge |' - ' c4.8xlarge | c4.large | c4.xlarge | c5.12xlarge | c5.18xlarge | c5.24xlarge |' - ' c5.2xlarge | c5.4xlarge | c5.9xlarge | c5.large | c5.xlarge | c5a.12xlarge |' - ' c5a.16xlarge | c5a.24xlarge | c5a.2xlarge | c5a.4xlarge | c5a.8xlarge | c5a.large |' - ' c5a.xlarge | m3.2xlarge | m3.large | m3.medium | m3.xlarge | m4.10xlarge | m4.2xlarge |' - ' m4.4xlarge | m4.large | m4.xlarge | m5.12xlarge | m5.16xlarge | m5.24xlarge |' - ' m5.2xlarge | m5.4xlarge | m5.8xlarge | m5.large | m5.xlarge | m5a.12xlarge |' - ' m5a.16xlarge | m5a.24xlarge | m5a.2xlarge | m5a.4xlarge | m5a.8xlarge | m5a.large |' - ' m5a.xlarge | r3.2xlarge | r3.4xlarge | r3.8xlarge | r3.large | r3.xlarge | r4.16xlarge |' - ' r4.2xlarge | r4.4xlarge | r4.8xlarge | r4.large | r4.xlarge | r5.12xlarge |' - ' r5.16xlarge | r5.24xlarge | r5.2xlarge | r5.4xlarge | r5.8xlarge | r5.large |' - ' r5.xlarge | r5a.12xlarge | r5a.16xlarge | r5a.24xlarge | r5a.2xlarge | r5a.4xlarge |' - ' r5a.8xlarge | r5a.large | r5a.xlarge | t2.large | t2.medium | t2.micro | t2.small', + # Choose from the available EC2 instance type list: https://aws.amazon.com/ec2/instance-types/ + 'ec2_instance_type': 'c5.large', # (Optional) Indicates whether to use On-Demand or Spot instances for this fleet. - 'fleet_type': 'ON_DEMAND | SPOT', + # Choose from ON_DEMAND or SPOT + 'fleet_type': 'ON_DEMAND', # (Optional) A game session protection policy to apply to all game sessions hosted on instances in this fleet. - 'new_game_session_protection_policy': 'FullProtection | NoProtection', + # Choose from FullProtection or NoProtection + 'new_game_session_protection_policy': 'NoProtection', # (Optional) A policy that limits the number of game sessions that an individual player # can create on instances in this fleet within a specified span of time. 'resource_creation_limit_policy': { @@ -131,17 +128,19 @@ FLEET_CONFIGURATIONS = [ # run concurrently on each instance. # Provide any integer not less than 1. 'concurrent_executions': 1, - # (Required) The location of a game build executable or the Realtime script file that - # contains the Init() function. - 'launch_path': '(Windows) | ' - '(Linux) /local/game/MyGame/', - # (Optional) An optional list of parameters to pass to the server executable - # or Realtime script on launch. - 'parameters': '' + # (Required) The location of a game build executable that contains the Init() function. + # Game builds are installed on instances at the root: + # Windows (custom game builds only): C:\game. + # Linux: /local/game. + 'launch_path': 'C:\\game\\bin\\server.exe', + # (Optional) An optional list of parameters to pass to the server executable on launch. + 'parameters': '--sv_port 33450 --project-path=C:\\game ' + '--project-cache-path=C:\\game\\assets --engine-path=C:\\game ' + '-bg_ConnectToAssetProcessor=0' } ] } # For additional fleet configurations, please check: - # # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_GameLift.html + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_GameLift.html } ] diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py b/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py index 99702f054a..28a5552578 100644 --- a/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py +++ b/Gems/AWSGameLift/cdk/aws_gamelift/gamelift_stack.py @@ -118,8 +118,7 @@ class GameLiftStack(core.Stack): def _get_gamelift_build_id(self, build_configuration: dict, identifier: int) -> str: """ - Get the GameLift build id. - Create the GameLift build from the storage location information if the build doesn't exist. + Create a GameLift build using the storage location if the build doesn't exist and return the build id. :param build_configuration: Configuration of the GameLift build. :param identifier: Unique identifier of the build which will be included in the resource id. :return: Build id. diff --git a/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py b/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py index 717b220ca7..a4fc134cec 100644 --- a/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py +++ b/Gems/AWSGameLift/cdk/aws_gamelift/support_stack.py @@ -16,7 +16,7 @@ from aws_cdk import core class SupportStack(core.Stack): """ - The support stack + The Build support stack Defines AWS resources that help to create GameLift builds from local files """ From 0fb23a9708babadacccfa216575e643c64111b3f Mon Sep 17 00:00:00 2001 From: puvvadar Date: Thu, 24 Jun 2021 13:56:16 -0700 Subject: [PATCH 05/29] Fix order of ops issue in MP Session Termination Signed-off-by: John --- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 9c2fa2f828..afb963804a 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -219,7 +219,8 @@ namespace Multiplayer // Cleanup connections, fire events and uninitialize state auto visitor = [reason](IConnection& connection) { connection.Disconnect(reason, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); - if (GetAgentType() == MultiplayerAgentType::DedicatedServer || GetAgentType() == MultiplayerAgentType::ClientServer) + MultiplayerAgentType agentType = GetAgentType(); + if (agentType == MultiplayerAgentType::DedicatedServer || agentType == MultiplayerAgentType::ClientServer) { m_networkInterface->StopListening(); m_shutdownEvent.Signal(m_networkInterface); @@ -227,7 +228,7 @@ namespace Multiplayer InitializeMultiplayer(MultiplayerAgentType::Uninitialized); // Signal session management, do this after uninitializing state - if (GetAgentType() == MultiplayerAgentType::DedicatedServer || GetAgentType() == MultiplayerAgentType::ClientServer) + if (agentType == MultiplayerAgentType::DedicatedServer || agentType == MultiplayerAgentType::ClientServer) { if (AZ::Interface::Get() != nullptr) { From 9f034b90a2f405748926d480d1101d2f48e27d08 Mon Sep 17 00:00:00 2001 From: yuriy0 Date: Fri, 25 Jun 2021 12:43:16 -0400 Subject: [PATCH 06/29] Generate texture thumbnails in a job thread (#1571) Signed-off-by: John --- .../ImageThumbnailSystemComponent.cpp | 96 +++++++++++-------- .../Thumbnail/ImageThumbnailSystemComponent.h | 5 +- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.cpp b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.cpp index 1af6563577..5a8bd4df14 100644 --- a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.cpp +++ b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -115,8 +116,7 @@ namespace ImageProcessingAtom void ImageThumbnailSystemComponent::RenderThumbnail( AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize) { - auto sourceKey = azrtti_cast(thumbnailKey.data()); - if (sourceKey) + if (auto sourceKey = azrtti_cast(thumbnailKey.data())) { bool foundIt = false; AZ::Data::AssetInfo assetInfo; @@ -129,52 +129,72 @@ namespace ImageProcessingAtom { AZStd::string fullPath; AZ::StringFunc::Path::Join(watchFolder.c_str(), assetInfo.m_relativePath.c_str(), fullPath); - if (RenderThumbnailFromImage(thumbnailKey, thumbnailSize, IImageObjectPtr(LoadImageFromFile(fullPath)))) - { - return; - } + RenderThumbnailFromImage(thumbnailKey, thumbnailSize, + [fullPath]() { return IImageObjectPtr(LoadImageFromFile(fullPath)); } + ); } } - - auto productKey = azrtti_cast(thumbnailKey.data()); - if (productKey) + else if (auto productKey = azrtti_cast(thumbnailKey.data())) { - if (RenderThumbnailFromImage(thumbnailKey, thumbnailSize, Utils::LoadImageFromImageAsset(productKey->GetAssetId()))) - { - return; - } + RenderThumbnailFromImage(thumbnailKey, thumbnailSize, + [assetId = productKey->GetAssetId()]() { return Utils::LoadImageFromImageAsset(assetId); } + ); + } + else + { + AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Event( + thumbnailKey, &AzToolsFramework::Thumbnailer::ThumbnailerRendererNotifications::ThumbnailFailedToRender); } - - AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Event( - thumbnailKey, &AzToolsFramework::Thumbnailer::ThumbnailerRendererNotifications::ThumbnailFailedToRender); } - bool ImageThumbnailSystemComponent::RenderThumbnailFromImage( - AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize, IImageObjectPtr previewImage) const + template + void ImageThumbnailSystemComponent::RenderThumbnailFromImage( + AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize, MkImageFn mkPreviewImage) const { - if (!previewImage) + const auto JobRunner = [mkPreviewImage, thumbnailKey, thumbnailSize]() mutable { - return false; - } - - ImageToProcess imageToProcess(previewImage); - imageToProcess.ConvertFormat(ePixelFormat_R8G8B8A8); - previewImage = imageToProcess.Get(); - - AZ::u8* imageBuf = nullptr; - AZ::u32 mip = 0; - AZ::u32 pitch = 0; - previewImage->GetImagePointer(mip, imageBuf, pitch); - const AZ::u32 width = previewImage->GetWidth(mip); - const AZ::u32 height = previewImage->GetHeight(mip); - - QImage image(imageBuf, width, height, pitch, QImage::Format_RGBA8888); + IImageObjectPtr previewImage = mkPreviewImage(); + if (!previewImage) + { + AZ::SystemTickBus::QueueFunction( + [ + thumbnailKey + ]() + { + AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Event( + thumbnailKey, &AzToolsFramework::Thumbnailer::ThumbnailerRendererNotifications::ThumbnailFailedToRender); + }); - AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Event( - thumbnailKey, &AzToolsFramework::Thumbnailer::ThumbnailerRendererNotifications::ThumbnailRendered, - QPixmap::fromImage(image.scaled(QSize(thumbnailSize, thumbnailSize), Qt::KeepAspectRatio, Qt::SmoothTransformation))); + return; + } - return true; + ImageToProcess imageToProcess(previewImage); + imageToProcess.ConvertFormat(ePixelFormat_R8G8B8A8); + previewImage = imageToProcess.Get(); + + AZ::u8* imageBuf = nullptr; + AZ::u32 mip = 0; + AZ::u32 pitch = 0; + previewImage->GetImagePointer(mip, imageBuf, pitch); + const AZ::u32 width = previewImage->GetWidth(mip); + const AZ::u32 height = previewImage->GetHeight(mip); + + // Note that this image holds a non-owning pointer to the `previewImage' raw data buffer + const QImage image(imageBuf, width, height, pitch, QImage::Format_RGBA8888); + + // Dispatch event on main thread + AZ::SystemTickBus::QueueFunction( + [ + thumbnailKey, thumbnailSize, + pixmap = QPixmap::fromImage(image.scaled(QSize(thumbnailSize, thumbnailSize), Qt::KeepAspectRatio, Qt::SmoothTransformation)) + ]() mutable + { + AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Event( + thumbnailKey, &AzToolsFramework::Thumbnailer::ThumbnailerRendererNotifications::ThumbnailRendered, + pixmap); + }); + }; + AZ::CreateJobFunction(JobRunner, true)->Start(); } } // namespace Thumbnails } // namespace ImageProcessingAtom diff --git a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.h b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.h index ca2453b532..e051b9e880 100644 --- a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.h +++ b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnailSystemComponent.h @@ -52,8 +52,9 @@ namespace ImageProcessingAtom bool Installed() const override; void RenderThumbnail(AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize) override; - bool RenderThumbnailFromImage( - AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize, IImageObjectPtr previewImage) const; + template + void RenderThumbnailFromImage( + AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize, MkImageFn mkPreviewImage) const; }; } // namespace Thumbnails } // namespace ImageProcessingAtom From fd4f363fe2f4c1e675cfa30b1d7d9c777e7d410f Mon Sep 17 00:00:00 2001 From: yuriy0 Date: Fri, 25 Jun 2021 18:32:27 -0400 Subject: [PATCH 07/29] Don't give a broken copy constructor to BehaviorContextObject. (#1002) * Don't give a broken copy constructor to BehaviorContextObject. This used to be required by the serialization system, now it handles non-copyable and non-moveable types. * Explicitly delete the copy constructor instead of defaulting, which implicitly deletes it. Some compilers complain about such an implicit deletion, and we should be explicit about our intent anyways Signed-off-by: John --- .../Include/ScriptCanvas/Data/BehaviorContextObject.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Data/BehaviorContextObject.h b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Data/BehaviorContextObject.h index a6c7c33663..882d05e227 100644 --- a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Data/BehaviorContextObject.h +++ b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Data/BehaviorContextObject.h @@ -120,6 +120,7 @@ namespace ScriptCanvas AZ_FORCE_INLINE BehaviorContextObject() = default; BehaviorContextObject& operator=(const BehaviorContextObject&) = delete; + BehaviorContextObject(const BehaviorContextObject&) = delete; // copy ctor AZ_FORCE_INLINE BehaviorContextObject(const void* source, const AnyTypeInfo& typeInfo, AZ::u32 flags); @@ -138,13 +139,6 @@ namespace ScriptCanvas AZ_FORCE_INLINE void add_ref(); void release(); - - public: - // no copying allowed, this is here to allow compile time compatibility with storage in of BehaviorContextObjectPtr AZStd::any, only - AZ_FORCE_INLINE BehaviorContextObject(const BehaviorContextObject&) - { - AZ_Assert(false, "no copying allowed, this is here to allow storage in of BehaviorContextObjectPtr AZStd::any, only"); - } }; AZ_FORCE_INLINE BehaviorContextObject::BehaviorContextObject(const void* value, const AnyTypeInfo& typeInfo, AZ::u32 flags) From 592a8cad6d111a23baf6b2bd3fe79c9806e74e4a Mon Sep 17 00:00:00 2001 From: Yuriy Toporovskyy Date: Fri, 25 Jun 2021 09:36:47 -0400 Subject: [PATCH 08/29] Don't block main thread to load data from file Signed-off-by: John --- .../Code/Source/Previewer/ImagePreviewer.cpp | 87 ++++++++++++++----- .../Code/Source/Previewer/ImagePreviewer.h | 15 ++++ 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.cpp b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.cpp index fcdc9cd0d3..3cdfb1d15c 100644 --- a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.cpp +++ b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.cpp @@ -51,6 +51,12 @@ namespace ImageProcessingAtom ImagePreviewer::~ImagePreviewer() { + AZ::SystemTickBus::Handler::BusDisconnect(); + + if (m_createDisplayTextureResult.isRunning()) + { + m_createDisplayTextureResult.waitForFinished(); + } } void ImagePreviewer::Clear() const @@ -214,21 +220,30 @@ namespace ImageProcessingAtom m_ui->m_fileInfoCtrl->show(); m_fileinfo = QString::fromUtf8(product->GetName().c_str()); m_fileinfo += GetFileSize(product->GetRelativePath().c_str()); - - AZ::Data::Asset imageAsset = Utils::LoadImageAsset(product->GetAssetId()); - IImageObjectPtr image = Utils::LoadImageFromImageAsset(imageAsset); - if (image) + CreateAndDisplayTextureItemAsync( + [assetId = product->GetAssetId()] + () -> CreateDisplayTextureResult { - // Add product image info - AZStd::string productInfo; - GetImageInfoString(imageAsset, productInfo); + AZ::Data::Asset imageAsset = Utils::LoadImageAsset(assetId); + IImageObjectPtr image = Utils::LoadImageFromImageAsset(imageAsset); - m_fileinfo += QStringLiteral("\r\n"); - m_fileinfo += productInfo.c_str(); + if (image) + { + // Add product image info + AZStd::string productInfo; + GetImageInfoString(imageAsset, productInfo); - m_previewImageObject = ConvertImageForPreview(image); - } + QString fileInfo = QStringLiteral("\r\n"); + fileInfo += productInfo.c_str(); + + return { ConvertImageForPreview(image), fileInfo }; + } + else + { + return { nullptr, "" }; + } + }); DisplayTextureItem(); } @@ -239,19 +254,28 @@ namespace ImageProcessingAtom m_fileinfo = QString::fromUtf8(source->GetName().c_str()); m_fileinfo += GetFileSize(source->GetFullPath().c_str()); - IImageObjectPtr image = IImageObjectPtr(LoadImageFromFile(source->GetFullPath())); - - if (image) + CreateAndDisplayTextureItemAsync( + [fullPath = source->GetFullPath()] + () -> CreateDisplayTextureResult { - // Add source image info - AZStd::string sourceInfo; - GetImageInfoString(image, sourceInfo); + IImageObjectPtr image = IImageObjectPtr(LoadImageFromFile(fullPath)); - m_fileinfo += QStringLiteral("\r\n"); - m_fileinfo += sourceInfo.c_str(); + if (image) + { + // Add source image info + AZStd::string sourceInfo; + GetImageInfoString(image, sourceInfo); - m_previewImageObject = ConvertImageForPreview(image); - } + QString fileInfo = QStringLiteral("\r\n"); + fileInfo += sourceInfo.c_str(); + + return { ConvertImageForPreview(image), fileInfo }; + } + else + { + return { nullptr, "" }; + } + }); DisplayTextureItem(); } @@ -289,6 +313,27 @@ namespace ImageProcessingAtom updateGeometry(); } + template + void ImagePreviewer::CreateAndDisplayTextureItemAsync(CreateFn create) + { + AZ::SystemTickBus::Handler::BusConnect(); + m_createDisplayTextureResult = QtConcurrent::run(AZStd::move(create)); + } + + void ImagePreviewer::OnSystemTick() + { + if (m_createDisplayTextureResult.isFinished()) + { + CreateDisplayTextureResult result = m_createDisplayTextureResult.result(); + m_previewImageObject = AZStd::move(result.first); + m_fileinfo += result.second; + + AZ::SystemTickBus::Handler::BusDisconnect(); + + DisplayTextureItem(); + } + } + void ImagePreviewer::PreviewSubImage(uint32_t mip) { QImage previewImage = GetSubImagePreview(m_previewImageObject, mip); diff --git a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.h b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.h index 3b487f3d95..9e7066c20c 100644 --- a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.h +++ b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Previewer/ImagePreviewer.h @@ -13,12 +13,15 @@ #if !defined(Q_MOC_RUN) #include +#include #include #include #include #include +#include +#include #endif namespace Ui @@ -42,6 +45,7 @@ namespace ImageProcessingAtom { class ImagePreviewer : public AzToolsFramework::AssetBrowser::Previewer + , private AZ::SystemTickBus::Handler { Q_OBJECT public: @@ -65,16 +69,27 @@ namespace ImageProcessingAtom QString GetFileSize(const char* path); void DisplayTextureItem(); + template + void CreateAndDisplayTextureItemAsync(CreateFn create); + void PreviewSubImage(uint32_t mip); // QLabel word wrap does not break long words such as filenames, so manual word wrap needed static QString WordWrap(const QString& string, int maxLength); + // SystemTickBus + void OnSystemTick() override; + QScopedPointer m_ui; QString m_fileinfo; QString m_name = "ImagePreviewer"; // Decompressed image in preview. Cache it so we can preview its sub images IImageObjectPtr m_previewImageObject; + + // Properties for tracking the status of an asynchronous request to display an asset browser entry + using CreateDisplayTextureResult = AZStd::pair; + + QFuture m_createDisplayTextureResult; }; }//namespace ImageProcessingAtom From 41a5a5b4f40364c77a4f2fc8c5468defa80e645e Mon Sep 17 00:00:00 2001 From: Yuriy Toporovskyy Date: Fri, 25 Jun 2021 09:49:13 -0400 Subject: [PATCH 09/29] Avoid a somewhat costly call to ThumbnailerRequestsBus::IsLoading when the information is already manifest Signed-off-by: John --- .../AssetBrowser/Views/EntryDelegate.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp index d0ac05020d..bf9a68b6b2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp @@ -126,9 +126,9 @@ namespace AzToolsFramework { return 0; } - bool thumbnailLoading; - ThumbnailerRequestsBus::BroadcastResult(thumbnailLoading, &ThumbnailerRequests::IsLoading, thumbnailKey, m_thumbnailContext.c_str()); - if (thumbnailLoading) + + const Thumbnail::State thumbnailState = thumbnail->GetState(); + if (thumbnailState == Thumbnail::State::Loading) { AzQtComponents::StyledBusyLabel* busyLabel; AssetBrowserComponentRequestBus::BroadcastResult(busyLabel , &AssetBrowserComponentRequests::GetStyledBusyLabel); @@ -137,7 +137,7 @@ namespace AzToolsFramework busyLabel->DrawTo(painter, QRectF(point.x(), point.y(), size.width(), size.height())); } } - else + else if (thumbnailState == Thumbnail::State::Ready) { // Scaling and centering pixmap within bounds to preserve aspect ratio const QPixmap pixmap = thumbnail->GetPixmap().scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); @@ -145,6 +145,10 @@ namespace AzToolsFramework const QPoint pointDelta = QPoint(sizeDelta.width() / 2, sizeDelta.height() / 2); painter->drawPixmap(point + pointDelta, pixmap); } + else + { + AZ_Assert(false, "Thumbnail state %d unexpected here", int(thumbnailState)); + } return m_iconSize; } From 992e8500eadc7fb29523239da2df40654d699d49 Mon Sep 17 00:00:00 2001 From: John Date: Thu, 24 Jun 2021 18:39:03 +0100 Subject: [PATCH 10/29] Add snapshot selection to PR and non-PR builds. PR builds will detect the destination branch and check if that branch is one of the snapshot branches, otherwise it defaults to the 'development' snapshot. Non-PR builds use the user-selected snapshot from the list of available snapshots. Signed-off-by: John --- scripts/build/Jenkins/Jenkinsfile | 27 ++++++++++++++----- .../build/bootstrap/incremental_build_util.py | 23 ++++++++-------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index c52f432bed..80205e4dd6 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -18,6 +18,8 @@ EMPTY_JSON = readJSON text: '{}' ENGINE_REPOSITORY_NAME = 'o3de' +def buildSnapshots = ['development', 'stabilization/2106'] +def defaultBuildSnapshot = buildSnapshots.get(0) def pipelineProperties = [] def pipelineParameters = [ @@ -26,7 +28,8 @@ def pipelineParameters = [ booleanParam(defaultValue: false, description: 'Deletes the contents of the output directory before building. This will cause a \"clean\" build. NOTE: does not imply CLEAN_ASSETS', name: 'CLEAN_OUTPUT_DIRECTORY'), booleanParam(defaultValue: false, description: 'Deletes the contents of the output directories of the AssetProcessor before building.', name: 'CLEAN_ASSETS'), booleanParam(defaultValue: false, description: 'Deletes the contents of the workspace and forces a complete pull.', name: 'CLEAN_WORKSPACE'), - booleanParam(defaultValue: false, description: 'Recreates the volume used for the workspace. The volume will be created out of a snapshot taken from main.', name: 'RECREATE_VOLUME') + booleanParam(defaultValue: false, description: 'Recreates the volume used for the workspace. The volume will be created out of a snapshot taken from main.', name: 'RECREATE_VOLUME'), + choice(defaultValue: defaultBuildSnapshot, name: 'SNAPSHOT', choices: buildSnapshots, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.') ] def palSh(cmd, lbl = '', winSlashReplacement = true) { @@ -258,7 +261,7 @@ def CheckoutRepo(boolean disableSubmodules = false) { palRm('commitid') } -def PreBuildCommonSteps(Map pipelineConfig, String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean mount = true, boolean disableSubmodules = false) { +def PreBuildCommonSteps(Map pipelineConfig, String snapshot, String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean mount = true, boolean disableSubmodules = false) { echo 'Starting pre-build common steps...' if (mount) { @@ -272,7 +275,7 @@ def PreBuildCommonSteps(Map pipelineConfig, String repositoryName, String projec palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action delete --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Deleting volume', winSlashReplacement=false) } timeout(5) { - palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action mount --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Mounting volume', winSlashReplacement=false) + palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action mount --snapshot ${snapshot} --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Mounting volume', winSlashReplacement=false) } if(env.IS_UNIX) { @@ -391,10 +394,10 @@ def PostBuildCommonSteps(String workspace, boolean mount = true) { } } -def CreateSetupStage(Map pipelineConfig, String repositoryName, String projectName, String pipelineName, String branchName, String platformName, String jobName, Map environmentVars) { +def CreateSetupStage(Map pipelineConfig, String snapshot, String repositoryName, String projectName, String pipelineName, String branchName, String platformName, String jobName, Map environmentVars) { return { stage('Setup') { - PreBuildCommonSteps(pipelineConfig, repositoryName, projectName, pipelineName, branchName, platformName, jobName, environmentVars['WORKSPACE'], environmentVars['MOUNT_VOLUME']) + PreBuildCommonSteps(pipelineConfig, snapshot, repositoryName, projectName, pipelineName, branchName, platformName, jobName, environmentVars['WORKSPACE'], environmentVars['MOUNT_VOLUME']) } } } @@ -457,6 +460,18 @@ try { branchName = scm.branches[0].name // for non-multibranch pipelines env.BRANCH_NAME = branchName // so scripts that read this environment have it (e.g. incremental_build_util.py) } + if(env.CHANGE_TARGET) { + if(buildSnapshots.contains(env.CHANGE_TARGET)) { + snapshot = defaultBuildSnapshot + echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" found." + } else { + snapshot = env.CHANGE_TARGET + echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" does not exist, defaulting to snapshot \"${snapshot}\"" + } + } else { + snapshot = env.SNAPSHOT + echo "Snapshot \"${snapshot}\" selected." + } pipelineProperties.add(disableConcurrentBuilds()) echo "Running \"${pipelineName}\" for \"${branchName}\"..." @@ -514,7 +529,7 @@ try { withEnv(GetEnvStringList(envVars)) { def build_job_name = build_job.key try { - CreateSetupStage(pipelineConfig, repositoryName, projectName, pipelineName, branchName, platform.key, build_job.key, envVars).call() + CreateSetupStage(pipelineConfig, snapshot, repositoryName, projectName, pipelineName, branchName, platform.key, build_job.key, envVars).call() if(build_job.value.steps) { //this is a pipe with many steps so create all the build stages build_job.value.steps.each { build_step -> diff --git a/scripts/build/bootstrap/incremental_build_util.py b/scripts/build/bootstrap/incremental_build_util.py index a331cf3bb8..410746953f 100755 --- a/scripts/build/bootstrap/incremental_build_util.py +++ b/scripts/build/bootstrap/incremental_build_util.py @@ -94,6 +94,7 @@ def error(message): def parse_args(): parser = argparse.ArgumentParser() parser.add_argument('-a', '--action', dest="action", help="Action (mount|unmount|delete)") + parser.add_argument('-snapshot', '--snapshot', dest="snapshot", help="Build snapshot") parser.add_argument('-repository_name', '--repository_name', dest="repository_name", help="Repository name") parser.add_argument('-project', '--project', dest="project", help="Project") parser.add_argument('-pipe', '--pipeline', dest="pipeline", help="Pipeline") @@ -177,8 +178,8 @@ def delete_volume(ec2_client, volume_id): response = ec2_client.delete_volume(VolumeId=volume_id) print 'Volume {} deleted'.format(volume_id) -def find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, build_type, disk_size): - mount_name = get_mount_name(repository_name, project, pipeline, 'main', platform, build_type) # we take snapshots out of main +def find_snapshot_id(ec2_client, snapshot, repository_name, project, pipeline, platform, build_type, disk_size): + mount_name = get_mount_name(repository_name, project, pipeline, snapshot, platform, build_type) # we take snapshots out of main response = ec2_client.describe_snapshots(Filters= [{ 'Name': 'tag:Name', 'Values': [mount_name] }]) @@ -194,7 +195,7 @@ def find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, b snapshot_id = snapshot['SnapshotId'] return snapshot_id -def create_volume(ec2_client, availability_zone, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): +def create_volume(ec2_client, availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): # The actual EBS default calculation for IOps is a floating point number, the closest approxmiation is 4x of the disk size for simplicity mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type) pipeline_and_branch = get_pipeline_and_branch(pipeline, branch) @@ -218,7 +219,7 @@ def create_volume(ec2_client, availability_zone, repository_name, project, pipel if 'io1' in disk_type.lower(): parameters['Iops'] = (4 * disk_size) - snapshot_id = find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, build_type, disk_size) + snapshot_id = find_snapshot_id(ec2_client, snapshot, repository_name, project, pipeline, platform, build_type, disk_size) if snapshot_id: parameters['SnapshotId'] = snapshot_id created = False @@ -370,7 +371,7 @@ def attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_i mount_volume(created) attempt += 1 -def mount_ebs(repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): +def mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): session = boto3.session.Session() region = session.region_name if region is None: @@ -399,7 +400,7 @@ def mount_ebs(repository_name, project, pipeline, branch, platform, build_type, if 'Volumes' in response and not len(response['Volumes']): print 'Volume for {} doesn\'t exist creating it...'.format(mount_name) # volume doesn't exist, create it - volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) + volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) else: volume = response['Volumes'][0] volume_id = volume['VolumeId'] @@ -407,7 +408,7 @@ def mount_ebs(repository_name, project, pipeline, branch, platform, build_type, if (volume['Size'] != disk_size or volume['VolumeType'] != disk_type): print 'Override disk attributes does not match the existing volume, deleting {} and replacing the volume'.format(volume_id) delete_volume(ec2_client, volume_id) - volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) + volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) if len(volume['Attachments']): # this is bad we shouldn't be attached, we should have detached at the end of a build attachment = volume['Attachments'][0] @@ -437,7 +438,7 @@ def mount_ebs(repository_name, project, pipeline, branch, platform, build_type, print 'Error: EBS disk size reached to the allowed maximum disk size {}MB, please contact ly-infra@ and ly-build@ to investigate.'.format(MAX_EBS_DISK_SIZE) exit(1) print 'Recreating the EBS with disk size {}'.format(new_disk_size) - volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline, branch, platform, build_type, new_disk_size, disk_type) + volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, new_disk_size, disk_type) volume = ec2_resource.Volume(volume_id) attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_id, created) @@ -492,9 +493,9 @@ def delete_ebs(repository_name, project, pipeline, branch, platform, build_type) delete_volume(ec2_client, volume_id) -def main(action, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): +def main(action, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): if action == 'mount': - mount_ebs(repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) + mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) elif action == 'unmount': unmount_ebs() elif action == 'delete': @@ -502,5 +503,5 @@ def main(action, repository_name, project, pipeline, branch, platform, build_typ if __name__ == "__main__": args = parse_args() - ret = main(args.action, args.repository_name, args.project, args.pipeline, args.branch, args.platform, args.build_type, args.disk_size, args.disk_type) + ret = main(args.action, args.snapshot, args.repository_name, args.project, args.pipeline, args.branch, args.platform, args.build_type, args.disk_size, args.disk_type) sys.exit(ret) \ No newline at end of file From 3c31424e928a32bb99d4c694e73b342a19427603 Mon Sep 17 00:00:00 2001 From: John Date: Thu, 24 Jun 2021 19:31:53 +0100 Subject: [PATCH 11/29] Address PR comments Signed-off-by: John --- scripts/build/Jenkins/Jenkinsfile | 15 ++++++------ .../build/bootstrap/incremental_build_util.py | 24 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index 80205e4dd6..5d0f968c25 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -18,8 +18,9 @@ EMPTY_JSON = readJSON text: '{}' ENGINE_REPOSITORY_NAME = 'o3de' -def buildSnapshots = ['development', 'stabilization/2106'] -def defaultBuildSnapshot = buildSnapshots.get(0) +BUILD_SNAPSHOTS = ['development', 'stabilization/2106'] +DEFAULT_BUILD_SNAPSHOT = BUILD_SNAPSHOTS.get(0) + def pipelineProperties = [] def pipelineParameters = [ @@ -28,8 +29,7 @@ def pipelineParameters = [ booleanParam(defaultValue: false, description: 'Deletes the contents of the output directory before building. This will cause a \"clean\" build. NOTE: does not imply CLEAN_ASSETS', name: 'CLEAN_OUTPUT_DIRECTORY'), booleanParam(defaultValue: false, description: 'Deletes the contents of the output directories of the AssetProcessor before building.', name: 'CLEAN_ASSETS'), booleanParam(defaultValue: false, description: 'Deletes the contents of the workspace and forces a complete pull.', name: 'CLEAN_WORKSPACE'), - booleanParam(defaultValue: false, description: 'Recreates the volume used for the workspace. The volume will be created out of a snapshot taken from main.', name: 'RECREATE_VOLUME'), - choice(defaultValue: defaultBuildSnapshot, name: 'SNAPSHOT', choices: buildSnapshots, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.') + booleanParam(defaultValue: false, description: 'Recreates the volume used for the workspace. The volume will be created out of a snapshot taken from main.', name: 'RECREATE_VOLUME') ] def palSh(cmd, lbl = '', winSlashReplacement = true) { @@ -459,13 +459,14 @@ try { } else { branchName = scm.branches[0].name // for non-multibranch pipelines env.BRANCH_NAME = branchName // so scripts that read this environment have it (e.g. incremental_build_util.py) + choice(defaultValue: DEFAULT_BUILD_SNAPSHOT, name: 'SNAPSHOT', choices: BUILD_SNAPSHOTS, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.') } if(env.CHANGE_TARGET) { - if(buildSnapshots.contains(env.CHANGE_TARGET)) { - snapshot = defaultBuildSnapshot + if(BUILD_SNAPSHOTS.contains(env.CHANGE_TARGET)) { + snapshot = env.CHANGE_TARGET echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" found." } else { - snapshot = env.CHANGE_TARGET + snapshot = DEFAULT_BUILD_SNAPSHOT echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" does not exist, defaulting to snapshot \"${snapshot}\"" } } else { diff --git a/scripts/build/bootstrap/incremental_build_util.py b/scripts/build/bootstrap/incremental_build_util.py index 410746953f..2e57a5750a 100755 --- a/scripts/build/bootstrap/incremental_build_util.py +++ b/scripts/build/bootstrap/incremental_build_util.py @@ -94,7 +94,7 @@ def error(message): def parse_args(): parser = argparse.ArgumentParser() parser.add_argument('-a', '--action', dest="action", help="Action (mount|unmount|delete)") - parser.add_argument('-snapshot', '--snapshot', dest="snapshot", help="Build snapshot") + parser.add_argument('-snapshot-hint', '--snapshot-hint', dest="snapshot_hint", help="Build snapshot to attempt to use") parser.add_argument('-repository_name', '--repository_name', dest="repository_name", help="Repository name") parser.add_argument('-project', '--project', dest="project", help="Project") parser.add_argument('-pipe', '--pipeline', dest="pipeline", help="Pipeline") @@ -178,8 +178,8 @@ def delete_volume(ec2_client, volume_id): response = ec2_client.delete_volume(VolumeId=volume_id) print 'Volume {} deleted'.format(volume_id) -def find_snapshot_id(ec2_client, snapshot, repository_name, project, pipeline, platform, build_type, disk_size): - mount_name = get_mount_name(repository_name, project, pipeline, snapshot, platform, build_type) # we take snapshots out of main +def find_snapshot_id(ec2_client, snapshot_hint, repository_name, project, pipeline, platform, build_type, disk_size): + mount_name = get_mount_name(repository_name, project, pipeline, snapshot_hint, platform, build_type) response = ec2_client.describe_snapshots(Filters= [{ 'Name': 'tag:Name', 'Values': [mount_name] }]) @@ -195,7 +195,7 @@ def find_snapshot_id(ec2_client, snapshot, repository_name, project, pipeline, p snapshot_id = snapshot['SnapshotId'] return snapshot_id -def create_volume(ec2_client, availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): +def create_volume(ec2_client, availability_zone, snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): # The actual EBS default calculation for IOps is a floating point number, the closest approxmiation is 4x of the disk size for simplicity mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type) pipeline_and_branch = get_pipeline_and_branch(pipeline, branch) @@ -219,7 +219,7 @@ def create_volume(ec2_client, availability_zone, snapshot, repository_name, proj if 'io1' in disk_type.lower(): parameters['Iops'] = (4 * disk_size) - snapshot_id = find_snapshot_id(ec2_client, snapshot, repository_name, project, pipeline, platform, build_type, disk_size) + snapshot_id = find_snapshot_id(ec2_client, snapshot_hint, repository_name, project, pipeline, platform, build_type, disk_size) if snapshot_id: parameters['SnapshotId'] = snapshot_id created = False @@ -371,7 +371,7 @@ def attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_i mount_volume(created) attempt += 1 -def mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): +def mount_ebs(snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): session = boto3.session.Session() region = session.region_name if region is None: @@ -400,7 +400,7 @@ def mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, bu if 'Volumes' in response and not len(response['Volumes']): print 'Volume for {} doesn\'t exist creating it...'.format(mount_name) # volume doesn't exist, create it - volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) + volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) else: volume = response['Volumes'][0] volume_id = volume['VolumeId'] @@ -408,7 +408,7 @@ def mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, bu if (volume['Size'] != disk_size or volume['VolumeType'] != disk_type): print 'Override disk attributes does not match the existing volume, deleting {} and replacing the volume'.format(volume_id) delete_volume(ec2_client, volume_id) - volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) + volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) if len(volume['Attachments']): # this is bad we shouldn't be attached, we should have detached at the end of a build attachment = volume['Attachments'][0] @@ -438,7 +438,7 @@ def mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, bu print 'Error: EBS disk size reached to the allowed maximum disk size {}MB, please contact ly-infra@ and ly-build@ to investigate.'.format(MAX_EBS_DISK_SIZE) exit(1) print 'Recreating the EBS with disk size {}'.format(new_disk_size) - volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot, repository_name, project, pipeline, branch, platform, build_type, new_disk_size, disk_type) + volume_id, created = create_volume(ec2_client, ec2_availability_zone, snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, new_disk_size, disk_type) volume = ec2_resource.Volume(volume_id) attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_id, created) @@ -493,9 +493,9 @@ def delete_ebs(repository_name, project, pipeline, branch, platform, build_type) delete_volume(ec2_client, volume_id) -def main(action, snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): +def main(action, snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): if action == 'mount': - mount_ebs(snapshot, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) + mount_ebs(snapshot_hint, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) elif action == 'unmount': unmount_ebs() elif action == 'delete': @@ -503,5 +503,5 @@ def main(action, snapshot, repository_name, project, pipeline, branch, platform, if __name__ == "__main__": args = parse_args() - ret = main(args.action, args.snapshot, args.repository_name, args.project, args.pipeline, args.branch, args.platform, args.build_type, args.disk_size, args.disk_type) + ret = main(args.action, args.snapshot_hint, args.repository_name, args.project, args.pipeline, args.branch, args.platform, args.build_type, args.disk_size, args.disk_type) sys.exit(ret) \ No newline at end of file From cfe4a4913663b21f9360e4ac6d29464de38050d0 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 25 Jun 2021 10:18:29 +0100 Subject: [PATCH 12/29] Address PR comments Signed-off-by: John --- scripts/build/Jenkins/Jenkinsfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index 5d0f968c25..0f5d5638be 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -18,7 +18,7 @@ EMPTY_JSON = readJSON text: '{}' ENGINE_REPOSITORY_NAME = 'o3de' -BUILD_SNAPSHOTS = ['development', 'stabilization/2106'] +BUILD_SNAPSHOTS = ['development', 'stabilization/2106', ''] DEFAULT_BUILD_SNAPSHOT = BUILD_SNAPSHOTS.get(0) def pipelineProperties = [] @@ -458,10 +458,10 @@ try { branchName = env.BRANCH_NAME } else { branchName = scm.branches[0].name // for non-multibranch pipelines - env.BRANCH_NAME = branchName // so scripts that read this environment have it (e.g. incremental_build_util.py) - choice(defaultValue: DEFAULT_BUILD_SNAPSHOT, name: 'SNAPSHOT', choices: BUILD_SNAPSHOTS, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.') + env.BRANCH_NAME = branchName // so scripts that read this environment have it (e.g. incremental_build_util.py) } if(env.CHANGE_TARGET) { + // PR builds if(BUILD_SNAPSHOTS.contains(env.CHANGE_TARGET)) { snapshot = env.CHANGE_TARGET echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" found." @@ -470,6 +470,8 @@ try { echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" does not exist, defaulting to snapshot \"${snapshot}\"" } } else { + // Non-PR builds + choice(defaultValue: DEFAULT_BUILD_SNAPSHOT, name: 'SNAPSHOT', choices: BUILD_SNAPSHOTS, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.') snapshot = env.SNAPSHOT echo "Snapshot \"${snapshot}\" selected." } From 0e0f266fdd248268a5bfdf3d721bc830c9063712 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jun 2021 16:21:47 +0100 Subject: [PATCH 13/29] Provisional implementation of PythonCoverage gem Signed-off-by: John --- AutomatedTesting/Gem/CMakeLists.txt | 1 + AutomatedTesting/Gem/Code/enabled_gems.cmake | 1 + .../Gem/PythonCoverage/CMakeLists.txt | 21 ++ .../Gem/PythonCoverage/Code/CMakeLists.txt | 168 +++++++++++++ .../Code/Platform/Android/PAL_android.cmake | 4 + .../pythoncoverage_android_files.cmake | 8 + ...e_editor_shared_android_files - Copy.cmake | 8 + .../pythoncoverage_shared_android_files.cmake | 8 + .../Code/Platform/Linux/PAL_linux.cmake | 4 + ...oncoverage_editor_shared_linux_files.cmake | 8 + .../Linux/pythoncoverage_linux_files.cmake | 8 + .../pythoncoverage_shared_linux_files.cmake | 8 + .../Code/Platform/Mac/PAL_mac.cmake | 4 + ...thoncoverage_editor_shared_mac_files.cmake | 8 + .../Mac/pythoncoverage_mac_files.cmake | 8 + .../Mac/pythoncoverage_shared_mac_files.cmake | 8 + .../Code/Platform/Windows/PAL_windows.cmake | 4 + .../pythoncoverage_editor_windows_files.cmake | 8 + .../pythoncoverage_shared_windows_files.cmake | 8 + .../pythoncoverage_windows_files.cmake | 8 + .../Code/Platform/iOS/PAL_ios.cmake | 4 + ...thoncoverage_editor_shared_ios_files.cmake | 8 + .../iOS/pythoncoverage_ios_files.cmake | 8 + .../iOS/pythoncoverage_shared_ios_files.cmake | 8 + .../Source/PythonCoverageEditorModule.cpp | 40 +++ .../Code/Source/PythonCoverageEditorModule.h | 31 +++ .../PythonCoverageEditorSystemComponent.cpp | 228 ++++++++++++++++++ .../PythonCoverageEditorSystemComponent.h | 87 +++++++ .../Code/Source/PythonCoverageModule.cpp | 26 ++ .../Code/Source/PythonCoverageModule.h | 26 ++ .../Source/PythonCoverageSystemComponent.cpp | 70 ++++++ .../Source/PythonCoverageSystemComponent.h | 44 ++++ .../Code/Tests/PythonCoverageEditorTest.cpp | 24 ++ .../Code/Tests/PythonCoverageTest.cpp | 24 ++ .../Code/pythoncoverage_editor_files.cmake | 5 + .../pythoncoverage_editor_shared_files.cmake | 7 + .../pythoncoverage_editor_tests_files.cmake | 4 + .../Code/pythoncoverage_files.cmake | 5 + .../Code/pythoncoverage_shared_files.cmake | 5 + .../Code/pythoncoverage_tests_files.cmake | 4 + .../Platform/Android/android_gem.cmake | 1 + .../Platform/Android/android_gem.json | 3 + .../Platform/Linux/linux_gem.cmake | 1 + .../Platform/Linux/linux_gem.json | 3 + .../PythonCoverage/Platform/Mac/mac_gem.cmake | 1 + .../PythonCoverage/Platform/Mac/mac_gem.json | 3 + .../Platform/Windows/windows_gem.cmake | 1 + .../Platform/Windows/windows_gem.json | 3 + .../PythonCoverage/Platform/iOS/ios_gem.cmake | 1 + .../PythonCoverage/Platform/iOS/ios_gem.json | 3 + AutomatedTesting/Gem/PythonCoverage/gem.json | 15 ++ .../Gem/PythonCoverage/preview.png | 3 + .../automatedtesting_shared/base.py | 2 +- .../API/EditorPythonRunnerRequestsBus.h | 14 +- .../API/EditorPythonScriptNotificationsBus.h | 45 ++++ .../aztoolsframework_files.cmake | 1 + Code/Sandbox/Editor/CryEdit.cpp | 10 +- .../Code/Source/PythonSystemComponent.cpp | 12 +- .../Code/Source/PythonSystemComponent.h | 2 +- 59 files changed, 1077 insertions(+), 8 deletions(-) create mode 100644 AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake create mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json create mode 100644 AutomatedTesting/Gem/PythonCoverage/gem.json create mode 100644 AutomatedTesting/Gem/PythonCoverage/preview.png create mode 100644 Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h diff --git a/AutomatedTesting/Gem/CMakeLists.txt b/AutomatedTesting/Gem/CMakeLists.txt index 5d881f3f95..990fa72222 100644 --- a/AutomatedTesting/Gem/CMakeLists.txt +++ b/AutomatedTesting/Gem/CMakeLists.txt @@ -11,3 +11,4 @@ add_subdirectory(Code) add_subdirectory(PythonTests) +add_subdirectory(PythonCoverage) diff --git a/AutomatedTesting/Gem/Code/enabled_gems.cmake b/AutomatedTesting/Gem/Code/enabled_gems.cmake index d99d17b55e..4f04e61f2a 100644 --- a/AutomatedTesting/Gem/Code/enabled_gems.cmake +++ b/AutomatedTesting/Gem/Code/enabled_gems.cmake @@ -55,4 +55,5 @@ set(ENABLED_GEMS AWSCore AWSClientAuth AWSMetrics + PythonCoverage ) diff --git a/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt b/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt new file mode 100644 index 0000000000..a753914c4b --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt @@ -0,0 +1,21 @@ + +set(o3de_gem_path ${CMAKE_CURRENT_LIST_DIR}) +set(o3de_gem_json ${o3de_gem_path}/gem.json) +o3de_read_json_key(o3de_gem_name ${o3de_gem_json} "gem_name") +o3de_restricted_path(${o3de_gem_json} o3de_gem_restricted_path) + +# Currently we are in the DefaultProjectSource folder: ${CMAKE_CURRENT_LIST_DIR} +# Get the platform specific folder ${pal_dir} for the current folder: ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} +# Note: ly_get_list_relative_pal_filename will take care of the details for us, as this may be a restricted platform +# in which case it will see if that platform is present here or in the restricted folder. +# i.e. It could here: DefaultProjectSource/Platform/ or +# //DefaultProjectSource +ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} ${o3de_gem_restricted_path} ${o3de_gem_path} ${o3de_gem_name}) + +# Now that we have the platform abstraction layer (PAL) folder for this folder, thats where we will find the +# project cmake for this platform. +include(${pal_dir}/${PAL_PLATFORM_NAME_LOWERCASE}_gem.cmake) + +ly_add_external_target_path(${CMAKE_CURRENT_LIST_DIR}/3rdParty) + +add_subdirectory(Code) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt new file mode 100644 index 0000000000..c288377483 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt @@ -0,0 +1,168 @@ + +# Currently we are in the Code folder: ${CMAKE_CURRENT_LIST_DIR} +# Get the platform specific folder ${pal_dir} for the current folder: ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} +# Note: ly_get_list_relative_pal_filename will take care of the details for us, as this may be a restricted platform +# in which case it will see if that platform is present here or in the restricted folder. +# i.e. It could here in our gem : Gems/PythonCoverage/Code/Platform/ or +# //Gems/PythonCoverage/Code +ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} ${o3de_gem_restricted_path} ${o3de_gem_path} ${o3de_gem_name}) + +# Now that we have the platform abstraction layer (PAL) folder for this folder, thats where we will find the +# traits for this platform. Traits for a platform are defines for things like whether or not something in this gem +# is supported by this platform. +include(${pal_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) + +# Add the PythonCoverage.Static target +# Note: We include the common files and the platform specific files which are set in pythoncoverage_common_files.cmake +# and in ${pal_dir}/pythoncoverage_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake +ly_add_target( + NAME PythonCoverage.Static STATIC + NAMESPACE Gem + FILES_CMAKE + pythoncoverage_files.cmake + ${pal_dir}/pythoncoverage_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework +) + +# Here add PythonCoverage target, it depends on the PythonCoverage.Static +ly_add_target( + NAME PythonCoverage ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + pythoncoverage_shared_files.cmake + ${pal_dir}/pythoncoverage_shared_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::PythonCoverage.Static +) + +# By default, we will specify that the above target PythonCoverage would be used by +# Client and Server type targets when this gem is enabled. If you don't want it +# active in Clients or Servers by default, delete one of both of the following lines: +ly_create_alias(NAME PythonCoverage.Clients NAMESPACE Gem TARGETS Gem::PythonCoverage) +ly_create_alias(NAME PythonCoverage.Servers NAMESPACE Gem TARGETS Gem::PythonCoverage) + +# If we are on a host platform, we want to add the host tools targets like the PythonCoverage.Editor target which +# will also depend on PythonCoverage.Static +if(PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_target( + NAME PythonCoverage.Editor.Static STATIC + NAMESPACE Gem + FILES_CMAKE + pythoncoverage_editor_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + COMPILE_DEFINITIONS + PUBLIC + PYTHON_COVERAGE_EDITOR + PRIVATE + LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"\" + BUILD_DEPENDENCIES + PUBLIC + AZ::AzToolsFramework + Gem::PythonCoverage.Static + ) + + ly_add_target( + NAME PythonCoverage.Editor MODULE + NAMESPACE Gem + AUTOMOC + OUTPUT_NAME Gem.PythonCoverage.Editor + FILES_CMAKE + pythoncoverage_editor_shared_files.cmake + COMPILE_DEFINITIONS + PRIVATE + PYTHON_COVERAGE_EDITOR + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + Gem::PythonCoverage.Editor.Static + ) + + # By default, we will specify that the above target PythonCoverage would be used by + # Tool and Builder type targets when this gem is enabled. If you don't want it + # active in Tools or Builders by default, delete one of both of the following lines: + ly_create_alias(NAME PythonCoverage.Tools NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) + ly_create_alias(NAME PythonCoverage.Builders NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) + + +endif() + +################################################################################ +# Tests +################################################################################ +# See if globally, tests are supported +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + # We globally support tests, see if we support tests on this platform for PythonCoverage.Static + if(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED) + # We support PythonCoverage.Tests on this platform, add PythonCoverage.Tests target which depends on PythonCoverage.Static + ly_add_target( + NAME PythonCoverage.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + pythoncoverage_files.cmake + pythoncoverage_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzFramework + Gem::PythonCoverage.Static + ) + + # Add PythonCoverage.Tests to googletest + ly_add_googletest( + NAME Gem::PythonCoverage.Tests + ) + endif() + + # If we are a host platform we want to add tools test like editor tests here + if(PAL_TRAIT_BUILD_HOST_TOOLS) + # We are a host platform, see if Editor tests are supported on this platform + if(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED) + # We support PythonCoverage.Editor.Tests on this platform, add PythonCoverage.Editor.Tests target which depends on PythonCoverage.Editor + ly_add_target( + NAME PythonCoverage.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + pythoncoverage_editor_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + Gem::PythonCoverage.Editor + ) + + # Add PythonCoverage.Editor.Tests to googletest + ly_add_googletest( + NAME Gem::PythonCoverage.Editor.Tests + ) + endif() + endif() +endif() diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake new file mode 100644 index 0000000000..1be29fc854 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake new file mode 100644 index 0000000000..d337110ed9 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Android +# i.e. ../Source/Android/PythonCoverageAndroid.cpp +# ../Source/Android/PythonCoverageAndroid.h +# ../Include/Android/PythonCoverageAndroid.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake new file mode 100644 index 0000000000..d337110ed9 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Android +# i.e. ../Source/Android/PythonCoverageAndroid.cpp +# ../Source/Android/PythonCoverageAndroid.h +# ../Include/Android/PythonCoverageAndroid.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake new file mode 100644 index 0000000000..d337110ed9 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Android +# i.e. ../Source/Android/PythonCoverageAndroid.cpp +# ../Source/Android/PythonCoverageAndroid.h +# ../Include/Android/PythonCoverageAndroid.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake new file mode 100644 index 0000000000..bf0d788480 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake new file mode 100644 index 0000000000..7c08fd3b2f --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Linux +# i.e. ../Source/Linux/PythonCoverageLinux.cpp +# ../Source/Linux/PythonCoverageLinux.h +# ../Include/Linux/PythonCoverageLinux.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake new file mode 100644 index 0000000000..7c08fd3b2f --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Linux +# i.e. ../Source/Linux/PythonCoverageLinux.cpp +# ../Source/Linux/PythonCoverageLinux.h +# ../Include/Linux/PythonCoverageLinux.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake new file mode 100644 index 0000000000..7c08fd3b2f --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Linux +# i.e. ../Source/Linux/PythonCoverageLinux.cpp +# ../Source/Linux/PythonCoverageLinux.h +# ../Include/Linux/PythonCoverageLinux.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake new file mode 100644 index 0000000000..bf0d788480 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake new file mode 100644 index 0000000000..1a11934818 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Mac +# i.e. ../Source/Mac/PythonCoverageMac.cpp +# ../Source/Mac/PythonCoverageMac.h +# ../Include/Mac/PythonCoverageMac.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake new file mode 100644 index 0000000000..1a11934818 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Mac +# i.e. ../Source/Mac/PythonCoverageMac.cpp +# ../Source/Mac/PythonCoverageMac.h +# ../Include/Mac/PythonCoverageMac.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake new file mode 100644 index 0000000000..1a11934818 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Mac +# i.e. ../Source/Mac/PythonCoverageMac.cpp +# ../Source/Mac/PythonCoverageMac.h +# ../Include/Mac/PythonCoverageMac.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake new file mode 100644 index 0000000000..bf0d788480 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake new file mode 100644 index 0000000000..b4c8689f5f --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Windows +# i.e. ../Source/Windows/PythonCoverageWindows.cpp +# ../Source/Windows/PythonCoverageWindows.h +# ../Include/Windows/PythonCoverageWindows.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake new file mode 100644 index 0000000000..b4c8689f5f --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Windows +# i.e. ../Source/Windows/PythonCoverageWindows.cpp +# ../Source/Windows/PythonCoverageWindows.h +# ../Include/Windows/PythonCoverageWindows.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake new file mode 100644 index 0000000000..b4c8689f5f --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Windows +# i.e. ../Source/Windows/PythonCoverageWindows.cpp +# ../Source/Windows/PythonCoverageWindows.h +# ../Include/Windows/PythonCoverageWindows.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake new file mode 100644 index 0000000000..bf0d788480 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake new file mode 100644 index 0000000000..d21ab15181 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for iOS +# i.e. ../Source/iOS/PythonCoverageiOS.cpp +# ../Source/iOS/PythonCoverageiOS.h +# ../Include/iOS/PythonCoverageiOS.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake new file mode 100644 index 0000000000..d21ab15181 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for iOS +# i.e. ../Source/iOS/PythonCoverageiOS.cpp +# ../Source/iOS/PythonCoverageiOS.h +# ../Include/iOS/PythonCoverageiOS.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake new file mode 100644 index 0000000000..d21ab15181 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for iOS +# i.e. ../Source/iOS/PythonCoverageiOS.cpp +# ../Source/iOS/PythonCoverageiOS.h +# ../Include/iOS/PythonCoverageiOS.h + +set(FILES +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp new file mode 100644 index 0000000000..60c7d8463e --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp @@ -0,0 +1,40 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include "PythonCoverageEditorModule.h" +#include "PythonCoverageEditorSystemComponent.h" + +namespace PythonCoverage +{ + AZ_CLASS_ALLOCATOR_IMPL(PythonCoverageEditorModule, AZ::SystemAllocator, 0) + + PythonCoverageEditorModule::PythonCoverageEditorModule() + : PythonCoverageModule() + { + // push results of [MyComponent]::CreateDescriptor() into m_descriptors here + m_descriptors.insert( + m_descriptors.end(), + { + PythonCoverageEditorSystemComponent::CreateDescriptor() + }); + } + + PythonCoverageEditorModule::~PythonCoverageEditorModule() = default; + + AZ::ComponentTypeList PythonCoverageEditorModule::GetRequiredSystemComponents() const + { + // add required SystemComponents to the SystemEntity + return AZ::ComponentTypeList{ azrtti_typeid() }; + } +} // namespace PythonCoverage + +AZ_DECLARE_MODULE_CLASS(Gem_PythonCoverageEditor, PythonCoverage::PythonCoverageEditorModule) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h new file mode 100644 index 0000000000..669673ec49 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h @@ -0,0 +1,31 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include "PythonCoverageModule.h" + +namespace PythonCoverage +{ + class PythonCoverageEditorModule : public PythonCoverageModule + { + public: + AZ_CLASS_ALLOCATOR_DECL + AZ_RTTI(PythonCoverageEditorModule, "{32C0FFEA-09A7-460F-9257-5BDEF74FCD5B}", PythonCoverageModule); + + PythonCoverageEditorModule(); + ~PythonCoverageEditorModule(); + + // PythonCoverageModule ... + AZ::ComponentTypeList GetRequiredSystemComponents() const override; + }; +} // namespace WhiteBox diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp new file mode 100644 index 0000000000..26522ebf08 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp @@ -0,0 +1,228 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include +#include +#include + +#include + +#pragma optimize("", off) + +namespace PythonCoverage +{ + constexpr char* const Caller = "PythonCoverageEditorSystemComponent"; + + void PythonCoverageEditorSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class()->Version(1); + } + } + + void PythonCoverageEditorSystemComponent::Activate() + { + AzToolsFramework::EditorPythonScriptNotificationsBus::Handler::BusConnect(); + AZ::EntitySystemBus::Handler::BusConnect(); + ParseCoverageOutputDirectory(); + EnumerateAllModuleComponents(); + } + + void PythonCoverageEditorSystemComponent::Deactivate() + { + AZ::EntitySystemBus::Handler::BusDisconnect(); + AzToolsFramework::EditorPythonScriptNotificationsBus::Handler::BusDisconnect(); + } + + void PythonCoverageEditorSystemComponent::OnEntityActivated(const AZ::EntityId& entityId) + { + EnumerateComponentsForEntity(entityId); + WriteCoverageFile(); + } + + void PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory() + { + m_coverageState = CoverageState::Disabled; + const AZStd::string configFilePath = LY_TEST_IMPACT_DEFAULT_CONFIG_FILE; + if (configFilePath.empty()) + { + AZ_Warning(Caller, false, "No test impact analysis framework config found."); + return; + } + + const auto fileSize = AZ::IO::SystemFile::Length(configFilePath.c_str()); + if(!fileSize) + { + AZ_Error(Caller, false, "File %s does not exist", configFilePath.c_str()); + return; + } + + AZStd::vector buffer(fileSize + 1); + buffer[fileSize] = '\0'; + if (!AZ::IO::SystemFile::Read(configFilePath.c_str(), buffer.data())) + { + AZ_Error(Caller, false, "Could not read contents of file %s", configFilePath.c_str()); + return; + } + + const AZStd::string configurationData = AZStd::string(buffer.begin(), buffer.end()); + rapidjson::Document configurationFile; + if (configurationFile.Parse(configurationData.c_str()).HasParseError()) + { + AZ_Error(Caller, false, "Could not parse runtimeConfig data, JSON has errors"); + return; + } + + const AZ::IO::Path tempWorkspaceRootDir = configurationFile["workspace"]["temp"]["root"].GetString(); + const AZ::IO::Path artifactRelativeDir = configurationFile["workspace"]["temp"]["relative_paths"]["artifact_dir"].GetString(); + m_coverageDir = tempWorkspaceRootDir / artifactRelativeDir; + m_coverageState = CoverageState::Idle; + } + + void PythonCoverageEditorSystemComponent::WriteCoverageFile() + { + // Yes, we're doing blocking file operations on the main thread... If this becomes an issue this can be offloaded + // to a worker thread + if (m_coverageState == CoverageState::Gathering) + { + AZStd::string contents; + for (const auto& [testCase, entityComponents] : m_entityComponentMap) + { + const auto coveringModules = GetParentComponentModulesForAllActivatedEntities(entityComponents); + if (coveringModules.empty()) + { + return; + } + + contents = testCase + "\n"; + for (const auto& coveringModule : coveringModules) + { + contents += AZStd::string::format(" %s\n", coveringModule.c_str()); + } + } + + AZ::IO::SystemFile file; + const AZStd::vector bytes(contents.begin(), contents.end()); + if (!file.Open( + m_coverageFile.c_str(), + AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY)) + { + AZ_Error( + Caller, false, + "Couldn't open file %s for writing", m_coverageFile.c_str()); + return; + } + + if (!file.Write(bytes.data(), bytes.size())) + { + AZ_Error( + Caller, false, + "Couldn't write contents for file %s", m_coverageFile.c_str()); + return; + } + } + } + + void PythonCoverageEditorSystemComponent::EnumerateAllModuleComponents() + { + AZ::ModuleManagerRequestBus::Broadcast( + &AZ::ModuleManagerRequestBus::Events::EnumerateModules, + [this](const AZ::ModuleData& moduleData) + { + const AZStd::string moduleName = moduleData.GetDebugName(); + if (moduleData.GetDynamicModuleHandle()) + { + const auto fileName = moduleData.GetDynamicModuleHandle()->GetFilename(); + for (const auto* moduleComponentDescriptor : moduleData.GetModule()->GetComponentDescriptors()) + { + m_moduleComponents[moduleComponentDescriptor->GetUuid()] = moduleData.GetDebugName(); + } + } + + return true; + }); + } + + void PythonCoverageEditorSystemComponent::EnumerateComponentsForEntity(const AZ::EntityId& entityId) + { + AZ::Entity* entity = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, AZ::EntityId(entityId)); + + if (entity) + { + auto& entityComponents = m_entityComponentMap[m_testCase]; + for (const auto& entityComponent : entity->GetComponents()) + { + const auto componentTypeId = entityComponent->GetUnderlyingComponentType(); + AZ::ComponentDescriptor* componentDescriptor = nullptr; + AZ::ComponentDescriptorBus::EventResult( + componentDescriptor, componentTypeId, &AZ::ComponentDescriptorBus::Events::GetDescriptor); + entityComponents[componentTypeId] = componentDescriptor; + } + } + } + + AZStd::unordered_set PythonCoverageEditorSystemComponent::GetParentComponentModulesForAllActivatedEntities( + const AZStd::unordered_map& entityComponents) const + { + AZStd::unordered_set coveringModuleOutputNames; + for (const auto& [uuid, componentDescriptor] : entityComponents) + { + if (const auto moduleComponent = m_moduleComponents.find(uuid); moduleComponent != m_moduleComponents.end()) + { + coveringModuleOutputNames.insert(moduleComponent->second); + } + } + + return coveringModuleOutputNames; + } + + void PythonCoverageEditorSystemComponent::OnExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) + { + if (m_coverageState == CoverageState::Disabled) + { + return; + } + + if (m_coverageState == CoverageState::Gathering) + { + // Dump any existing coverage data to disk + WriteCoverageFile(); + m_coverageState = CoverageState::Idle; + } + + if (testCase.empty()) + { + // We need to be able to pinpoint the coverage data to the specific test case names otherwise we will not be able + // to specify which specific tests should be run in the future (filename does not necessarily equate to test case name) + AZ_Error(Caller, false, "No test case specified, coverage data gathering will be disabled for this test"); + return; + } + + const AZStd::string scriptName = AZ::IO::Path(filename).Stem().Native(); + const auto coverageFile = m_coverageDir / AZStd::string::format("%s.pycoverage", scriptName.c_str()); + + // If this is a different python script we clear the existing entity components and start afresh + if (m_coverageFile != coverageFile) + { + m_entityComponentMap.clear(); + m_coverageFile = coverageFile; + } + + m_testCase = testCase; + m_coverageState = CoverageState::Gathering; + } +} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h new file mode 100644 index 0000000000..0494cf1778 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h @@ -0,0 +1,87 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace AZ +{ + class ComponentDescriptor; +} + +namespace PythonCoverage +{ + //! System component for PythonCoverage editor. + class PythonCoverageEditorSystemComponent + : public AZ::Component + , private AZ::EntitySystemBus::Handler + , private AzToolsFramework::EditorPythonScriptNotificationsBus::Handler + { + public: + AZ_COMPONENT(PythonCoverageEditorSystemComponent, "{33370075-3aea-49c4-823d-476f8ac95b6f}"); + static void Reflect(AZ::ReflectContext* context); + + PythonCoverageEditorSystemComponent() = default; + + private: + // AZ::Component + void Activate() override; + void Deactivate() override; + + // AZ::EntitySystemBus ... + void OnEntityActivated(const AZ::EntityId& entityId) override; + + // AZ::EditorPythonScriptNotificationsBus ... + void OnExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) override; + + //! Attempts to parse the test impact analysis framework configuration file. + //! If either the test impact analysis framework is disabled or the configuration file cannot be parsed, python coverage + //! is disabled. + void ParseCoverageOutputDirectory(); + + //! Enumerates all of the loaded shared library modules and the component descriptors that belong to them. + void EnumerateAllModuleComponents(); + + //! Enumerates all of the component descriptors for the specified entity. + void EnumerateComponentsForEntity(const AZ::EntityId& entityId); + + //! Returns all of the shared library modules that parent the component descriptors of the specified set of activated entities. + //! @note Entity component descriptors are still retrieved even if the entity in question has since been deactivated. + //! @param entityComponents The set of activated entities and their component descriptors to get the parent modules for. + AZStd::unordered_set GetParentComponentModulesForAllActivatedEntities( + const AZStd::unordered_map& entityComponents) const; + + //! + void WriteCoverageFile(); + + enum class CoverageState : AZ::u8 + { + Disabled, //!< Python coverage is disabled. + Idle, //!< Python coverage is enabled but not actively gathering coverage data. + Gathering //!< Python coverage is enabled and actively gathering coverage data. + }; + + CoverageState m_coverageState = CoverageState::Disabled; //!< Current coverage state. + AZStd::unordered_map> m_entityComponentMap; //!< Map of + //!< component IDs to component descriptors for all activated entities, organized by test cases. + AZStd::unordered_map m_moduleComponents; //!< Map of component IDs to module names for all modules. + AZ::IO::Path m_coverageDir; //!< Directory to write coverage data to. + AZ::IO::Path m_coverageFile; //!< Full file path to write coverage data to. + AZStd::string m_testCase; //!< Name of current test case that coverage data is being gathered for. + }; +} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp new file mode 100644 index 0000000000..2c56987482 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp @@ -0,0 +1,26 @@ +#include + +#pragma optimize("", off) + +namespace PythonCoverage +{ + PythonCoverageModule::PythonCoverageModule() + : AZ::Module() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + m_descriptors.insert( + m_descriptors.end(), + { PythonCoverageSystemComponent::CreateDescriptor() }); + } + + AZ::ComponentTypeList PythonCoverageModule::GetRequiredSystemComponents() const + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } +}// namespace PythonCoverage + +#if !defined(PYTHON_COVERAGE_EDITOR) +AZ_DECLARE_MODULE_CLASS(Gem_PythonCoverage, PythonCoverage::PythonCoverageModule) +#endif // !defined(PYTHON_COVERAGE_EDITOR) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h new file mode 100644 index 0000000000..4ec6184906 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h @@ -0,0 +1,26 @@ + +#include +#include + +#include + +#pragma once + +#pragma optimize("", off) + +namespace PythonCoverage +{ + class PythonCoverageModule : public AZ::Module + { + public: + AZ_RTTI(PythonCoverageModule, "{dc706de0-22c4-4b05-9b99-438692afc082}", AZ::Module); + AZ_CLASS_ALLOCATOR(PythonCoverageModule, AZ::SystemAllocator, 0); + + PythonCoverageModule(); + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override; + }; +} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp new file mode 100644 index 0000000000..46ffdfdca8 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp @@ -0,0 +1,70 @@ + +#include + +#include +#include +#include + +namespace PythonCoverage +{ + void PythonCoverageSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ; + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("PythonCoverage", "[Description of functionality provided by this System Component]") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ; + } + } + } + + void PythonCoverageSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC("PythonCoverageService")); + } + + void PythonCoverageSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC("PythonCoverageService")); + } + + void PythonCoverageSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + AZ_UNUSED(required); + } + + void PythonCoverageSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + AZ_UNUSED(dependent); + } + + void PythonCoverageSystemComponent::Init() + { + } + + void PythonCoverageSystemComponent::Activate() + { + PythonCoverageRequestBus::Handler::BusConnect(); + AZ::TickBus::Handler::BusConnect(); + } + + void PythonCoverageSystemComponent::Deactivate() + { + AZ::TickBus::Handler::BusDisconnect(); + PythonCoverageRequestBus::Handler::BusDisconnect(); + } + + void PythonCoverageSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) + { + + } + +} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h new file mode 100644 index 0000000000..9ca1f60c74 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h @@ -0,0 +1,44 @@ + +#pragma once + +#include +#include +#include + +namespace PythonCoverage +{ + class PythonCoverageSystemComponent + : public AZ::Component + , protected PythonCoverageRequestBus::Handler + , public AZ::TickBus::Handler + { + public: + AZ_COMPONENT(PythonCoverageSystemComponent, "{b2f692ae-1047-4a6d-a4ed-27b1aac40ba5}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + protected: + //////////////////////////////////////////////////////////////////////// + // PythonCoverageRequestBus interface implementation + + //////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + void Deactivate() override; + //////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // AZTickBus interface implementation + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + //////////////////////////////////////////////////////////////////////// + }; + +} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp new file mode 100644 index 0000000000..2c97ecaf73 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp @@ -0,0 +1,24 @@ + +#include + +class PythonCoverageEditorTest + : public ::testing::Test +{ +protected: + void SetUp() override + { + + } + + void TearDown() override + { + + } +}; + +TEST_F(PythonCoverageEditorTest, SanityTest) +{ + ASSERT_TRUE(true); +} + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp new file mode 100644 index 0000000000..b1060df364 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp @@ -0,0 +1,24 @@ + +#include + +class PythonCoverageTest + : public ::testing::Test +{ +protected: + void SetUp() override + { + + } + + void TearDown() override + { + + } +}; + +TEST_F(PythonCoverageTest, SanityTest) +{ + ASSERT_TRUE(true); +} + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake new file mode 100644 index 0000000000..83fc442299 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake @@ -0,0 +1,5 @@ + +set(FILES + Source/PythonCoverageEditorSystemComponent.cpp + Source/PythonCoverageEditorSystemComponent.h +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake new file mode 100644 index 0000000000..33f8f244c6 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake @@ -0,0 +1,7 @@ + +set(FILES +Source/PythonCoverageModule.cpp +Source/PythonCoverageModule.h +Source/PythonCoverageEditorModule.cpp +Source/PythonCoverageEditorModule.h +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake new file mode 100644 index 0000000000..551b0bf062 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake @@ -0,0 +1,4 @@ + +set(FILES + Tests/PythonCoverageEditorTest.cpp +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake new file mode 100644 index 0000000000..bf6453e4f5 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake @@ -0,0 +1,5 @@ + +set(FILES + Source/PythonCoverageSystemComponent.cpp + Source/PythonCoverageSystemComponent.h +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake new file mode 100644 index 0000000000..3e614ee802 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake @@ -0,0 +1,5 @@ + +set(FILES + Source/PythonCoverageModule.cpp + Source/PythonCoverageModule.h +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake new file mode 100644 index 0000000000..55101af371 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake @@ -0,0 +1,4 @@ + +set(FILES + Tests/PythonCoverageTest.cpp +) diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json new file mode 100644 index 0000000000..23bbb28e66 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json @@ -0,0 +1,3 @@ +{ + "Tags": ["Android"], +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json new file mode 100644 index 0000000000..d08fbf53ba --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json @@ -0,0 +1,3 @@ +{ + "Tags": ["Linux"] +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json new file mode 100644 index 0000000000..d42b6f8186 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json @@ -0,0 +1,3 @@ +{ + "Tags": ["Mac"] +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json new file mode 100644 index 0000000000..a052f1e05a --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json @@ -0,0 +1,3 @@ +{ + "Tags": ["Windows"] +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json new file mode 100644 index 0000000000..b2dab56d05 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json @@ -0,0 +1,3 @@ +{ + "Tags": ["iOS"] +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/gem.json b/AutomatedTesting/Gem/PythonCoverage/gem.json new file mode 100644 index 0000000000..3ec79ac633 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/gem.json @@ -0,0 +1,15 @@ +{ + "gem_name": "PythonCoverage", + "origin": "The primary repo for PythonCoverage goes here: i.e. http://www.mydomain.com", + "license": "What license PythonCoverage uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "PythonCoverage", + "summary": "A short description of PythonCoverage.", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "PythonCoverage" + ], + "icon_path": "preview.png", + "restricted_name": "gems" +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/preview.png b/AutomatedTesting/Gem/PythonCoverage/preview.png new file mode 100644 index 0000000000..2f1ed47754 --- /dev/null +++ b/AutomatedTesting/Gem/PythonCoverage/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d6204c6730e5675791765ca194e9b1cbec282208e280507de830afc2805e5fa +size 41127 diff --git a/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py b/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py index f00227c47f..9d183ea326 100755 --- a/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py +++ b/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py @@ -94,7 +94,7 @@ class TestAutomationBase: editor_starttime = time.time() self.logger.debug("Running automated test") testcase_module_filepath = self._get_testcase_module_filepath(testcase_module) - pycmd = ["--runpythontest", testcase_module_filepath, "-BatchMode", "-autotest_mode", "-rhi=null"] + extra_cmdline_args + pycmd = ["--runpythontest", testcase_module_filepath, "-BatchMode", "-autotest_mode", "-rhi=null", f"-pythontestcase={request.node.originalname}"] + extra_cmdline_args editor.args.extend(pycmd) # args are added to the WinLauncher start command editor.start(backupFiles = False, launch_ap = False) try: diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerRequestsBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerRequestsBus.h index 6a2553f90b..1b423621e1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerRequestsBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerRequestsBus.h @@ -29,16 +29,22 @@ namespace AzToolsFramework ////////////////////////////////////////////////////////////////////////// //! executes a Python script using a string, prints the result if printResult is true and script is an expression - virtual void ExecuteByString(AZStd::string_view script, bool printResult) { AZ_UNUSED(script); AZ_UNUSED(printResult); } + virtual void ExecuteByString([[maybe_unused]] AZStd::string_view script, [[maybe_unused]] bool printResult) {} //! executes a Python script using a filename - virtual void ExecuteByFilename(AZStd::string_view filename) { AZ_UNUSED(filename); } + virtual void ExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {} //! executes a Python script using a filename and args - virtual void ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector& args) { AZ_UNUSED(filename); AZ_UNUSED(args); } + virtual void ExecuteByFilenameWithArgs( + [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} //! executes a Python script as a test - virtual void ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector& args) { AZ_UNUSED(filename); AZ_UNUSED(args); } + virtual void ExecuteByFilenameAsTest( + [[maybe_unused]] AZStd::string_view filename, + [[maybe_unused]] AZStd::string_view testCase, + [[maybe_unused]] const AZStd::vector& args) + { + } }; using EditorPythonRunnerRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h new file mode 100644 index 0000000000..586a348e76 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h @@ -0,0 +1,45 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ +#pragma once + +#include +#include + +namespace AzToolsFramework +{ + //! Provides a bus to notify when Python scripts are about to run. + class EditorPythonScriptNotifications + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + + //! Notifies the execution of a Python script using a string. + virtual void OnExecuteByString([[maybe_unused]] AZStd::string_view script) {} + + //! Notifies the execution of a Python script using a filename. + virtual void OnExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {} + + //! Notifies the execution of a Python script using a filename and args. + virtual void OnExecuteByFilenameWithArgs( + [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} + + //! Notifies the execution of a Python script as a test. + virtual void OnExecuteByFilenameAsTest( + [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) {} + }; + using EditorPythonScriptNotificationsBus = AZ::EBus; +} diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index 9ec0deaed6..bdb1ab6f0a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -41,6 +41,7 @@ set(FILES API/EditorVegetationRequestsBus.h API/EditorPythonConsoleBus.h API/EditorPythonRunnerRequestsBus.h + API/EditorPythonScriptNotificationsBus.h API/EntityPropertyEditorRequestsBus.h API/EditorWindowRequestBus.h API/EntityCompositionRequestBus.h diff --git a/Code/Sandbox/Editor/CryEdit.cpp b/Code/Sandbox/Editor/CryEdit.cpp index 792f817362..ae93ef04a4 100644 --- a/Code/Sandbox/Editor/CryEdit.cpp +++ b/Code/Sandbox/Editor/CryEdit.cpp @@ -542,6 +542,7 @@ public: QString m_appRoot; QString m_logFile; QString m_pythonArgs; + QString m_pythontTestCase; QString m_execFile; QString m_execLineCmd; @@ -589,6 +590,7 @@ public: const std::vector > stringOptions = { {{"logfile", "File name of the log file to write out to.", "logfile"}, m_logFile}, {{"runpythonargs", "Command-line argument string to pass to the python script if --runpython or --runpythontest was used.", "runpythonargs"}, m_pythonArgs}, + {{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythontTestCase}, {{"exec", "cfg file to run on startup, used for systems like automation", "exec"}, m_execFile}, {{"rhi", "Command-line argument to force which rhi to use", "dummyString"}, dummyString }, {{"rhi-device-validation", "Command-line argument to configure rhi validation", "dummyString"}, dummyString }, @@ -1527,7 +1529,13 @@ void CCryEditApp::RunInitPythonScript(CEditCommandLineInfo& cmdInfo) std::transform(tokens.begin(), tokens.end(), std::back_inserter(pythonArgs), [](auto& tokenData) { return tokenData.c_str(); }); if (cmdInfo.m_bRunPythonTestScript) { - EditorPythonRunnerRequestBus::Broadcast(&EditorPythonRunnerRequestBus::Events::ExecuteByFilenameAsTest, cmdInfo.m_strFileName.toUtf8().constData(), pythonArgs); + AZStd::string pythonTestCase; + if (!cmdInfo.m_pythontTestCase.isEmpty()) + { + pythonTestCase = cmdInfo.m_pythontTestCase.toUtf8().constData(); + } + + EditorPythonRunnerRequestBus::Broadcast(&EditorPythonRunnerRequestBus::Events::ExecuteByFilenameAsTest, cmdInfo.m_strFileName.toUtf8().constData(), pythonTestCase, pythonArgs); // Close the editor gracefully as the test has completed GetIEditor()->GetDocument()->SetModifiedFlag(false); diff --git a/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp b/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp index 00dbca12c9..e9e75b966f 100644 --- a/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp +++ b/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp @@ -39,6 +39,7 @@ #include #include +#include namespace Platform { @@ -579,6 +580,9 @@ namespace EditorPythonBindings if (!script.empty()) { + AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByString, script); + // Acquire GIL before calling Python code AZStd::lock_guard lock(m_lock); pybind11::gil_scoped_acquire acquire; @@ -639,11 +643,15 @@ namespace EditorPythonBindings void PythonSystemComponent::ExecuteByFilename(AZStd::string_view filename) { AZStd::vector args; + AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByFilename, filename); ExecuteByFilenameWithArgs(filename, args); } - void PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector& args) + void PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) { + AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByFilenameAsTest, filename, testCase, args); const Result evalResult = EvaluateFile(filename, args); if (evalResult == Result::Okay) { @@ -659,6 +667,8 @@ namespace EditorPythonBindings void PythonSystemComponent::ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector& args) { + AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByFilenameWithArgs, filename, args); EvaluateFile(filename, args); } diff --git a/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.h b/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.h index d5d1634c9d..69882e7d57 100644 --- a/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.h +++ b/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.h @@ -58,7 +58,7 @@ namespace EditorPythonBindings void ExecuteByString(AZStd::string_view script, bool printResult) override; void ExecuteByFilename(AZStd::string_view filename) override; void ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector& args) override; - void ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector& args) override; + void ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) override; //////////////////////////////////////////////////////////////////////// private: From 478ffeeac6932e194db8a60d557668a4c9fd8962 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 22 Jun 2021 14:49:08 +0100 Subject: [PATCH 14/29] Remove PythonCoverage runtime component Signed-off-by: John --- .../Gem/PythonCoverage/CMakeLists.txt | 29 +-- .../Gem/PythonCoverage/Code/CMakeLists.txt | 206 ++++++------------ .../Code/Platform/Android/PAL_android.cmake | 14 +- .../pythoncoverage_android_files.cmake | 8 - ...e_editor_shared_android_files - Copy.cmake | 8 - .../pythoncoverage_shared_android_files.cmake | 8 - .../Code/Platform/Linux/PAL_linux.cmake | 14 +- ...oncoverage_editor_shared_linux_files.cmake | 8 - .../Linux/pythoncoverage_linux_files.cmake | 8 - .../pythoncoverage_shared_linux_files.cmake | 8 - .../Code/Platform/Mac/PAL_mac.cmake | 14 +- ...thoncoverage_editor_shared_mac_files.cmake | 8 - .../Mac/pythoncoverage_mac_files.cmake | 8 - .../Mac/pythoncoverage_shared_mac_files.cmake | 8 - .../Code/Platform/Windows/PAL_windows.cmake | 12 +- .../pythoncoverage_editor_windows_files.cmake | 8 - .../pythoncoverage_shared_windows_files.cmake | 8 - .../pythoncoverage_windows_files.cmake | 8 - .../Code/Platform/iOS/PAL_ios.cmake | 14 +- ...thoncoverage_editor_shared_ios_files.cmake | 8 - .../iOS/pythoncoverage_ios_files.cmake | 8 - .../iOS/pythoncoverage_shared_ios_files.cmake | 8 - .../Source/PythonCoverageEditorModule.cpp | 2 - .../Code/Source/PythonCoverageEditorModule.h | 8 +- .../PythonCoverageEditorSystemComponent.cpp | 94 ++++---- .../Code/Source/PythonCoverageModule.cpp | 26 --- .../Code/Source/PythonCoverageModule.h | 26 --- .../Source/PythonCoverageSystemComponent.cpp | 70 ------ .../Source/PythonCoverageSystemComponent.h | 44 ---- .../Code/Tests/PythonCoverageTest.cpp | 24 -- .../Code/pythoncoverage_editor_files.cmake | 10 + .../pythoncoverage_editor_shared_files.cmake | 16 +- .../pythoncoverage_editor_tests_files.cmake | 10 + .../Code/pythoncoverage_files.cmake | 5 - .../Code/pythoncoverage_shared_files.cmake | 5 - .../Code/pythoncoverage_tests_files.cmake | 4 - .../Platform/Android/android_gem.cmake | 1 - .../Platform/Android/android_gem.json | 3 - .../Platform/Linux/linux_gem.cmake | 1 - .../Platform/Linux/linux_gem.json | 3 - .../PythonCoverage/Platform/Mac/mac_gem.cmake | 1 - .../PythonCoverage/Platform/Mac/mac_gem.json | 3 - .../Platform/Windows/windows_gem.cmake | 1 - .../Platform/Windows/windows_gem.json | 3 - .../PythonCoverage/Platform/iOS/ios_gem.cmake | 1 - .../PythonCoverage/Platform/iOS/ios_gem.json | 3 - .../API/EditorPythonRunnerNotificationBus.h | 46 ++++ .../LYTestImpactFramework.cmake | 4 + 48 files changed, 269 insertions(+), 568 deletions(-) delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json create mode 100644 Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h diff --git a/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt b/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt index a753914c4b..20a680bce9 100644 --- a/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonCoverage/CMakeLists.txt @@ -1,21 +1,12 @@ - -set(o3de_gem_path ${CMAKE_CURRENT_LIST_DIR}) -set(o3de_gem_json ${o3de_gem_path}/gem.json) -o3de_read_json_key(o3de_gem_name ${o3de_gem_json} "gem_name") -o3de_restricted_path(${o3de_gem_json} o3de_gem_restricted_path) - -# Currently we are in the DefaultProjectSource folder: ${CMAKE_CURRENT_LIST_DIR} -# Get the platform specific folder ${pal_dir} for the current folder: ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} -# Note: ly_get_list_relative_pal_filename will take care of the details for us, as this may be a restricted platform -# in which case it will see if that platform is present here or in the restricted folder. -# i.e. It could here: DefaultProjectSource/Platform/ or -# //DefaultProjectSource -ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} ${o3de_gem_restricted_path} ${o3de_gem_path} ${o3de_gem_name}) - -# Now that we have the platform abstraction layer (PAL) folder for this folder, thats where we will find the -# project cmake for this platform. -include(${pal_dir}/${PAL_PLATFORM_NAME_LOWERCASE}_gem.cmake) - -ly_add_external_target_path(${CMAKE_CURRENT_LIST_DIR}/3rdParty) +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# add_subdirectory(Code) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt index c288377483..570150758d 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt @@ -1,127 +1,74 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# -# Currently we are in the Code folder: ${CMAKE_CURRENT_LIST_DIR} -# Get the platform specific folder ${pal_dir} for the current folder: ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} -# Note: ly_get_list_relative_pal_filename will take care of the details for us, as this may be a restricted platform -# in which case it will see if that platform is present here or in the restricted folder. -# i.e. It could here in our gem : Gems/PythonCoverage/Code/Platform/ or -# //Gems/PythonCoverage/Code ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} ${o3de_gem_restricted_path} ${o3de_gem_path} ${o3de_gem_name}) - -# Now that we have the platform abstraction layer (PAL) folder for this folder, thats where we will find the -# traits for this platform. Traits for a platform are defines for things like whether or not something in this gem -# is supported by this platform. include(${pal_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) -# Add the PythonCoverage.Static target -# Note: We include the common files and the platform specific files which are set in pythoncoverage_common_files.cmake -# and in ${pal_dir}/pythoncoverage_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake -ly_add_target( - NAME PythonCoverage.Static STATIC - NAMESPACE Gem - FILES_CMAKE - pythoncoverage_files.cmake - ${pal_dir}/pythoncoverage_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake - INCLUDE_DIRECTORIES - PUBLIC - Include - PRIVATE - Source - BUILD_DEPENDENCIES - PUBLIC - AZ::AzCore - AZ::AzFramework -) - -# Here add PythonCoverage target, it depends on the PythonCoverage.Static -ly_add_target( - NAME PythonCoverage ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} - NAMESPACE Gem - FILES_CMAKE - pythoncoverage_shared_files.cmake - ${pal_dir}/pythoncoverage_shared_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake - INCLUDE_DIRECTORIES - PUBLIC - Include - PRIVATE - Source - BUILD_DEPENDENCIES - PRIVATE - Gem::PythonCoverage.Static -) - -# By default, we will specify that the above target PythonCoverage would be used by -# Client and Server type targets when this gem is enabled. If you don't want it -# active in Clients or Servers by default, delete one of both of the following lines: -ly_create_alias(NAME PythonCoverage.Clients NAMESPACE Gem TARGETS Gem::PythonCoverage) -ly_create_alias(NAME PythonCoverage.Servers NAMESPACE Gem TARGETS Gem::PythonCoverage) - -# If we are on a host platform, we want to add the host tools targets like the PythonCoverage.Editor target which -# will also depend on PythonCoverage.Static -if(PAL_TRAIT_BUILD_HOST_TOOLS) - ly_add_target( - NAME PythonCoverage.Editor.Static STATIC - NAMESPACE Gem - FILES_CMAKE - pythoncoverage_editor_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - Source - PUBLIC - Include - COMPILE_DEFINITIONS - PUBLIC - PYTHON_COVERAGE_EDITOR - PRIVATE - LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"\" - BUILD_DEPENDENCIES - PUBLIC - AZ::AzToolsFramework - Gem::PythonCoverage.Static - ) - - ly_add_target( - NAME PythonCoverage.Editor MODULE - NAMESPACE Gem - AUTOMOC - OUTPUT_NAME Gem.PythonCoverage.Editor - FILES_CMAKE - pythoncoverage_editor_shared_files.cmake - COMPILE_DEFINITIONS - PRIVATE - PYTHON_COVERAGE_EDITOR - INCLUDE_DIRECTORIES - PRIVATE - Source - PUBLIC - Include - BUILD_DEPENDENCIES - PUBLIC - Gem::PythonCoverage.Editor.Static - ) - - # By default, we will specify that the above target PythonCoverage would be used by - # Tool and Builder type targets when this gem is enabled. If you don't want it - # active in Tools or Builders by default, delete one of both of the following lines: - ly_create_alias(NAME PythonCoverage.Tools NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) - ly_create_alias(NAME PythonCoverage.Builders NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) +if(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED) + if(PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_target( + NAME PythonCoverage.Editor.Static STATIC + NAMESPACE Gem + FILES_CMAKE + pythoncoverage_editor_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + COMPILE_DEFINITIONS + PUBLIC + PYTHON_COVERAGE_EDITOR + PRIVATE + LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"\" + BUILD_DEPENDENCIES + PUBLIC + AZ::AzToolsFramework + ) + ly_add_target( + NAME PythonCoverage.Editor MODULE + NAMESPACE Gem + AUTOMOC + OUTPUT_NAME Gem.PythonCoverage.Editor + FILES_CMAKE + pythoncoverage_editor_shared_files.cmake + COMPILE_DEFINITIONS + PRIVATE + PYTHON_COVERAGE_EDITOR + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + Gem::PythonCoverage.Editor.Static + ) + # By default, we will specify that the above target PythonCoverage would be used by + # Tool and Builder type targets when this gem is enabled. If you don't want it + # active in Tools or Builders by default, delete one of both of the following lines: + ly_create_alias(NAME PythonCoverage.Tools NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) + ly_create_alias(NAME PythonCoverage.Builders NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) + endif() endif() -################################################################################ -# Tests -################################################################################ -# See if globally, tests are supported if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) - # We globally support tests, see if we support tests on this platform for PythonCoverage.Static - if(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED) - # We support PythonCoverage.Tests on this platform, add PythonCoverage.Tests target which depends on PythonCoverage.Static + if(PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( - NAME PythonCoverage.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAME PythonCoverage.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem FILES_CMAKE - pythoncoverage_files.cmake - pythoncoverage_tests_files.cmake + pythoncoverage_editor_tests_files.cmake INCLUDE_DIRECTORIES PRIVATE Tests @@ -129,40 +76,11 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) BUILD_DEPENDENCIES PRIVATE AZ::AzTest - AZ::AzFramework - Gem::PythonCoverage.Static + Gem::PythonCoverage.Editor ) - # Add PythonCoverage.Tests to googletest ly_add_googletest( - NAME Gem::PythonCoverage.Tests + NAME Gem::PythonCoverage.Editor.Tests ) endif() - - # If we are a host platform we want to add tools test like editor tests here - if(PAL_TRAIT_BUILD_HOST_TOOLS) - # We are a host platform, see if Editor tests are supported on this platform - if(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED) - # We support PythonCoverage.Editor.Tests on this platform, add PythonCoverage.Editor.Tests target which depends on PythonCoverage.Editor - ly_add_target( - NAME PythonCoverage.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} - NAMESPACE Gem - FILES_CMAKE - pythoncoverage_editor_tests_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - Tests - Source - BUILD_DEPENDENCIES - PRIVATE - AZ::AzTest - Gem::PythonCoverage.Editor - ) - - # Add PythonCoverage.Editor.Tests to googletest - ly_add_googletest( - NAME Gem::PythonCoverage.Editor.Tests - ) - endif() - endif() -endif() +endif() \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake index 1be29fc854..7385380639 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/PAL_android.cmake @@ -1,4 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# -set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake deleted file mode 100644 index d337110ed9..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_android_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Android -# i.e. ../Source/Android/PythonCoverageAndroid.cpp -# ../Source/Android/PythonCoverageAndroid.h -# ../Include/Android/PythonCoverageAndroid.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake deleted file mode 100644 index d337110ed9..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_editor_shared_android_files - Copy.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Android -# i.e. ../Source/Android/PythonCoverageAndroid.cpp -# ../Source/Android/PythonCoverageAndroid.h -# ../Include/Android/PythonCoverageAndroid.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake deleted file mode 100644 index d337110ed9..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Android/pythoncoverage_shared_android_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Android -# i.e. ../Source/Android/PythonCoverageAndroid.cpp -# ../Source/Android/PythonCoverageAndroid.h -# ../Include/Android/PythonCoverageAndroid.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake index bf0d788480..7385380639 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/PAL_linux.cmake @@ -1,4 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# -set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake deleted file mode 100644 index 7c08fd3b2f..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_editor_shared_linux_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Linux -# i.e. ../Source/Linux/PythonCoverageLinux.cpp -# ../Source/Linux/PythonCoverageLinux.h -# ../Include/Linux/PythonCoverageLinux.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake deleted file mode 100644 index 7c08fd3b2f..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_linux_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Linux -# i.e. ../Source/Linux/PythonCoverageLinux.cpp -# ../Source/Linux/PythonCoverageLinux.h -# ../Include/Linux/PythonCoverageLinux.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake deleted file mode 100644 index 7c08fd3b2f..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Linux/pythoncoverage_shared_linux_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Linux -# i.e. ../Source/Linux/PythonCoverageLinux.cpp -# ../Source/Linux/PythonCoverageLinux.h -# ../Include/Linux/PythonCoverageLinux.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake index bf0d788480..7385380639 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/PAL_mac.cmake @@ -1,4 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# -set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake deleted file mode 100644 index 1a11934818..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_editor_shared_mac_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Mac -# i.e. ../Source/Mac/PythonCoverageMac.cpp -# ../Source/Mac/PythonCoverageMac.h -# ../Include/Mac/PythonCoverageMac.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake deleted file mode 100644 index 1a11934818..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_mac_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Mac -# i.e. ../Source/Mac/PythonCoverageMac.cpp -# ../Source/Mac/PythonCoverageMac.h -# ../Include/Mac/PythonCoverageMac.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake deleted file mode 100644 index 1a11934818..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Mac/pythoncoverage_shared_mac_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Mac -# i.e. ../Source/Mac/PythonCoverageMac.cpp -# ../Source/Mac/PythonCoverageMac.h -# ../Include/Mac/PythonCoverageMac.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake index bf0d788480..99b5ab4780 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/PAL_windows.cmake @@ -1,4 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake deleted file mode 100644 index b4c8689f5f..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_editor_windows_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Windows -# i.e. ../Source/Windows/PythonCoverageWindows.cpp -# ../Source/Windows/PythonCoverageWindows.h -# ../Include/Windows/PythonCoverageWindows.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake deleted file mode 100644 index b4c8689f5f..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_shared_windows_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Windows -# i.e. ../Source/Windows/PythonCoverageWindows.cpp -# ../Source/Windows/PythonCoverageWindows.h -# ../Include/Windows/PythonCoverageWindows.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake deleted file mode 100644 index b4c8689f5f..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/Windows/pythoncoverage_windows_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for Windows -# i.e. ../Source/Windows/PythonCoverageWindows.cpp -# ../Source/Windows/PythonCoverageWindows.h -# ../Include/Windows/PythonCoverageWindows.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake index bf0d788480..7385380639 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/PAL_ios.cmake @@ -1,4 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# -set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_TEST_SUPPORTED TRUE) -set(PAL_TRAIT_PYTHONCOVERAGE_EDITOR_TEST_SUPPORTED TRUE) \ No newline at end of file +set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake deleted file mode 100644 index d21ab15181..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_editor_shared_ios_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for iOS -# i.e. ../Source/iOS/PythonCoverageiOS.cpp -# ../Source/iOS/PythonCoverageiOS.h -# ../Include/iOS/PythonCoverageiOS.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake deleted file mode 100644 index d21ab15181..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_ios_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for iOS -# i.e. ../Source/iOS/PythonCoverageiOS.cpp -# ../Source/iOS/PythonCoverageiOS.h -# ../Include/iOS/PythonCoverageiOS.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake deleted file mode 100644 index d21ab15181..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Platform/iOS/pythoncoverage_shared_ios_files.cmake +++ /dev/null @@ -1,8 +0,0 @@ - -# Platform specific files for iOS -# i.e. ../Source/iOS/PythonCoverageiOS.cpp -# ../Source/iOS/PythonCoverageiOS.h -# ../Include/iOS/PythonCoverageiOS.h - -set(FILES -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp index 60c7d8463e..fd3dce4cde 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.cpp @@ -18,9 +18,7 @@ namespace PythonCoverage AZ_CLASS_ALLOCATOR_IMPL(PythonCoverageEditorModule, AZ::SystemAllocator, 0) PythonCoverageEditorModule::PythonCoverageEditorModule() - : PythonCoverageModule() { - // push results of [MyComponent]::CreateDescriptor() into m_descriptors here m_descriptors.insert( m_descriptors.end(), { diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h index 669673ec49..180ed79656 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h @@ -12,15 +12,17 @@ #pragma once -#include "PythonCoverageModule.h" +#include +#include namespace PythonCoverage { - class PythonCoverageEditorModule : public PythonCoverageModule + class PythonCoverageEditorModule + : public AZ::Module { public: AZ_CLASS_ALLOCATOR_DECL - AZ_RTTI(PythonCoverageEditorModule, "{32C0FFEA-09A7-460F-9257-5BDEF74FCD5B}", PythonCoverageModule); + AZ_RTTI(PythonCoverageEditorModule, "{32C0FFEA-09A7-460F-9257-5BDEF74FCD5B}"); PythonCoverageEditorModule(); ~PythonCoverageEditorModule(); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp index 26522ebf08..674e8684a5 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp @@ -37,7 +37,15 @@ namespace PythonCoverage { AzToolsFramework::EditorPythonScriptNotificationsBus::Handler::BusConnect(); AZ::EntitySystemBus::Handler::BusConnect(); + + // Attempt to discover the output directory for the test coverage files ParseCoverageOutputDirectory(); + + if (m_coverageState == CoverageState::Disabled) + { + return; + } + EnumerateAllModuleComponents(); } @@ -49,16 +57,31 @@ namespace PythonCoverage void PythonCoverageEditorSystemComponent::OnEntityActivated(const AZ::EntityId& entityId) { + if (m_coverageState == CoverageState::Disabled) + { + return; + } + EnumerateComponentsForEntity(entityId); - WriteCoverageFile(); + + // There is currently no way to receive a graceful exit signal in order to properly handle the coverage end of life so + // instead we have to serialize the data on-the-fly with blocking disk writes on the main thread... if this adversely + // affects performance in a measurable way then this could potentially be put on a worker thread, although it remains to + // be seen whether the asynchronous nature of such a thread results in queued up coverage being lost due to the hard exit + if (m_coverageState == CoverageState::Gathering) + { + WriteCoverageFile(); + } } void PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory() { m_coverageState = CoverageState::Disabled; const AZStd::string configFilePath = LY_TEST_IMPACT_DEFAULT_CONFIG_FILE; + if (configFilePath.empty()) { + // Config file path will be empty if test impact analysis framework is disabled AZ_Warning(Caller, false, "No test impact analysis framework config found."); return; } @@ -86,54 +109,50 @@ namespace PythonCoverage return; } - const AZ::IO::Path tempWorkspaceRootDir = configurationFile["workspace"]["temp"]["root"].GetString(); - const AZ::IO::Path artifactRelativeDir = configurationFile["workspace"]["temp"]["relative_paths"]["artifact_dir"].GetString(); + const auto& tempConfig = configurationFile["workspace"]["temp"]; + const AZ::IO::Path tempWorkspaceRootDir = tempConfig["root"].GetString(); + const AZ::IO::Path artifactRelativeDir = tempConfig["relative_paths"]["artifact_dir"].GetString(); m_coverageDir = tempWorkspaceRootDir / artifactRelativeDir; m_coverageState = CoverageState::Idle; } void PythonCoverageEditorSystemComponent::WriteCoverageFile() { - // Yes, we're doing blocking file operations on the main thread... If this becomes an issue this can be offloaded - // to a worker thread - if (m_coverageState == CoverageState::Gathering) + AZStd::string contents; + for (const auto& [testCase, entityComponents] : m_entityComponentMap) { - AZStd::string contents; - for (const auto& [testCase, entityComponents] : m_entityComponentMap) + const auto coveringModules = GetParentComponentModulesForAllActivatedEntities(entityComponents); + if (coveringModules.empty()) { - const auto coveringModules = GetParentComponentModulesForAllActivatedEntities(entityComponents); - if (coveringModules.empty()) - { - return; - } - - contents = testCase + "\n"; - for (const auto& coveringModule : coveringModules) - { - contents += AZStd::string::format(" %s\n", coveringModule.c_str()); - } - } - - AZ::IO::SystemFile file; - const AZStd::vector bytes(contents.begin(), contents.end()); - if (!file.Open( - m_coverageFile.c_str(), - AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY)) - { - AZ_Error( - Caller, false, - "Couldn't open file %s for writing", m_coverageFile.c_str()); return; } - - if (!file.Write(bytes.data(), bytes.size())) + + contents = testCase + "\n"; + for (const auto& coveringModule : coveringModules) { - AZ_Error( - Caller, false, - "Couldn't write contents for file %s", m_coverageFile.c_str()); - return; + contents += AZStd::string::format(" %s\n", coveringModule.c_str()); } } + + AZ::IO::SystemFile file; + const AZStd::vector bytes(contents.begin(), contents.end()); + if (!file.Open( + m_coverageFile.c_str(), + AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY)) + { + AZ_Error( + Caller, false, + "Couldn't open file %s for writing", m_coverageFile.c_str()); + return; + } + + if (!file.Write(bytes.data(), bytes.size())) + { + AZ_Error( + Caller, false, + "Couldn't write contents for file %s", m_coverageFile.c_str()); + return; + } } void PythonCoverageEditorSystemComponent::EnumerateAllModuleComponents() @@ -142,10 +161,9 @@ namespace PythonCoverage &AZ::ModuleManagerRequestBus::Events::EnumerateModules, [this](const AZ::ModuleData& moduleData) { - const AZStd::string moduleName = moduleData.GetDebugName(); + // We can only enumerate shared libs, static libs are invisible to us if (moduleData.GetDynamicModuleHandle()) { - const auto fileName = moduleData.GetDynamicModuleHandle()->GetFilename(); for (const auto* moduleComponentDescriptor : moduleData.GetModule()->GetComponentDescriptors()) { m_moduleComponents[moduleComponentDescriptor->GetUuid()] = moduleData.GetDebugName(); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp deleted file mode 100644 index 2c56987482..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include - -#pragma optimize("", off) - -namespace PythonCoverage -{ - PythonCoverageModule::PythonCoverageModule() - : AZ::Module() - { - // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. - m_descriptors.insert( - m_descriptors.end(), - { PythonCoverageSystemComponent::CreateDescriptor() }); - } - - AZ::ComponentTypeList PythonCoverageModule::GetRequiredSystemComponents() const - { - return AZ::ComponentTypeList{ - azrtti_typeid(), - }; - } -}// namespace PythonCoverage - -#if !defined(PYTHON_COVERAGE_EDITOR) -AZ_DECLARE_MODULE_CLASS(Gem_PythonCoverage, PythonCoverage::PythonCoverageModule) -#endif // !defined(PYTHON_COVERAGE_EDITOR) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h deleted file mode 100644 index 4ec6184906..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageModule.h +++ /dev/null @@ -1,26 +0,0 @@ - -#include -#include - -#include - -#pragma once - -#pragma optimize("", off) - -namespace PythonCoverage -{ - class PythonCoverageModule : public AZ::Module - { - public: - AZ_RTTI(PythonCoverageModule, "{dc706de0-22c4-4b05-9b99-438692afc082}", AZ::Module); - AZ_CLASS_ALLOCATOR(PythonCoverageModule, AZ::SystemAllocator, 0); - - PythonCoverageModule(); - - /** - * Add required SystemComponents to the SystemEntity. - */ - AZ::ComponentTypeList GetRequiredSystemComponents() const override; - }; -} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp deleted file mode 100644 index 46ffdfdca8..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.cpp +++ /dev/null @@ -1,70 +0,0 @@ - -#include - -#include -#include -#include - -namespace PythonCoverage -{ - void PythonCoverageSystemComponent::Reflect(AZ::ReflectContext* context) - { - if (AZ::SerializeContext* serialize = azrtti_cast(context)) - { - serialize->Class() - ->Version(0) - ; - - if (AZ::EditContext* ec = serialize->GetEditContext()) - { - ec->Class("PythonCoverage", "[Description of functionality provided by this System Component]") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("System")) - ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ; - } - } - } - - void PythonCoverageSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) - { - provided.push_back(AZ_CRC("PythonCoverageService")); - } - - void PythonCoverageSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) - { - incompatible.push_back(AZ_CRC("PythonCoverageService")); - } - - void PythonCoverageSystemComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) - { - AZ_UNUSED(required); - } - - void PythonCoverageSystemComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) - { - AZ_UNUSED(dependent); - } - - void PythonCoverageSystemComponent::Init() - { - } - - void PythonCoverageSystemComponent::Activate() - { - PythonCoverageRequestBus::Handler::BusConnect(); - AZ::TickBus::Handler::BusConnect(); - } - - void PythonCoverageSystemComponent::Deactivate() - { - AZ::TickBus::Handler::BusDisconnect(); - PythonCoverageRequestBus::Handler::BusDisconnect(); - } - - void PythonCoverageSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) - { - - } - -} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h deleted file mode 100644 index 9ca1f60c74..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageSystemComponent.h +++ /dev/null @@ -1,44 +0,0 @@ - -#pragma once - -#include -#include -#include - -namespace PythonCoverage -{ - class PythonCoverageSystemComponent - : public AZ::Component - , protected PythonCoverageRequestBus::Handler - , public AZ::TickBus::Handler - { - public: - AZ_COMPONENT(PythonCoverageSystemComponent, "{b2f692ae-1047-4a6d-a4ed-27b1aac40ba5}"); - - static void Reflect(AZ::ReflectContext* context); - - static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); - static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); - static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); - - protected: - //////////////////////////////////////////////////////////////////////// - // PythonCoverageRequestBus interface implementation - - //////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////// - // AZ::Component interface implementation - void Init() override; - void Activate() override; - void Deactivate() override; - //////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////// - // AZTickBus interface implementation - void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; - //////////////////////////////////////////////////////////////////////// - }; - -} // namespace PythonCoverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp deleted file mode 100644 index b1060df364..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageTest.cpp +++ /dev/null @@ -1,24 +0,0 @@ - -#include - -class PythonCoverageTest - : public ::testing::Test -{ -protected: - void SetUp() override - { - - } - - void TearDown() override - { - - } -}; - -TEST_F(PythonCoverageTest, SanityTest) -{ - ASSERT_TRUE(true); -} - -AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake index 83fc442299..ffdc071eb8 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_files.cmake @@ -1,3 +1,13 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# set(FILES Source/PythonCoverageEditorSystemComponent.cpp diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake index 33f8f244c6..d1bb21b43e 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_shared_files.cmake @@ -1,7 +1,15 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# set(FILES -Source/PythonCoverageModule.cpp -Source/PythonCoverageModule.h -Source/PythonCoverageEditorModule.cpp -Source/PythonCoverageEditorModule.h + Source/PythonCoverageEditorModule.cpp + Source/PythonCoverageEditorModule.h ) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake index 551b0bf062..f1912e743a 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake +++ b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake @@ -1,3 +1,13 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# set(FILES Tests/PythonCoverageEditorTest.cpp diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake deleted file mode 100644 index bf6453e4f5..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_files.cmake +++ /dev/null @@ -1,5 +0,0 @@ - -set(FILES - Source/PythonCoverageSystemComponent.cpp - Source/PythonCoverageSystemComponent.h -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake deleted file mode 100644 index 3e614ee802..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_shared_files.cmake +++ /dev/null @@ -1,5 +0,0 @@ - -set(FILES - Source/PythonCoverageModule.cpp - Source/PythonCoverageModule.h -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake deleted file mode 100644 index 55101af371..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_tests_files.cmake +++ /dev/null @@ -1,4 +0,0 @@ - -set(FILES - Tests/PythonCoverageTest.cpp -) diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake deleted file mode 100644 index 8b13789179..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.cmake +++ /dev/null @@ -1 +0,0 @@ - diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json deleted file mode 100644 index 23bbb28e66..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Android/android_gem.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Tags": ["Android"], -} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake deleted file mode 100644 index 8b13789179..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.cmake +++ /dev/null @@ -1 +0,0 @@ - diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json deleted file mode 100644 index d08fbf53ba..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Linux/linux_gem.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Tags": ["Linux"] -} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake deleted file mode 100644 index 8b13789179..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.cmake +++ /dev/null @@ -1 +0,0 @@ - diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json deleted file mode 100644 index d42b6f8186..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Mac/mac_gem.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Tags": ["Mac"] -} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake deleted file mode 100644 index 8b13789179..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.cmake +++ /dev/null @@ -1 +0,0 @@ - diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json deleted file mode 100644 index a052f1e05a..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/Windows/windows_gem.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Tags": ["Windows"] -} \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake b/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake deleted file mode 100644 index 8b13789179..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.cmake +++ /dev/null @@ -1 +0,0 @@ - diff --git a/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json b/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json deleted file mode 100644 index b2dab56d05..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Platform/iOS/ios_gem.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Tags": ["iOS"] -} \ No newline at end of file diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h new file mode 100644 index 0000000000..ffd161d295 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h @@ -0,0 +1,46 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ +#pragma once + +#include + +namespace AzToolsFramework +{ + //! Provides a bus to notify when Python scripts are about to run. + class EditorPythonRunnerNotification + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + + //! Notifies the execution of a Python script using a string. + virtual void ExecuteByString([[maybe_unused]] AZStd::string_view script) {} + + //! Notifies the execution of a Python script using a filename. + virtual void ExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {} + + //! Notifies the execution of a Python script using a filename and args. + virtual void ExecuteByFilenameWithArgs( + [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} + + //! Notifies the execution of a Python script as a test. + virtual void ExecuteByFilenameAsTest( + [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} + }; + using EditorPythonRunnerNotificationBus = AZ::EBus; + +} // namespace AzToolsFramework + diff --git a/cmake/TestImpactFramework/LYTestImpactFramework.cmake b/cmake/TestImpactFramework/LYTestImpactFramework.cmake index 9789ce187f..bdd580f37e 100644 --- a/cmake/TestImpactFramework/LYTestImpactFramework.cmake +++ b/cmake/TestImpactFramework/LYTestImpactFramework.cmake @@ -18,6 +18,9 @@ option(LY_TEST_IMPACT_INSTRUMENTATION_BIN "Path to test impact framework instrum # Name of test impact framework console static library target set(LY_TEST_IMPACT_CONSOLE_STATIC_TARGET "TestImpact.Frontend.Console.Static") +# Name of test impact framework python coverage gem target +set(LY_TEST_IMPACT_PYTHON_COVERAGE_STATIC_TARGET "PythonCoverage.Editor.Static") + # Name of test impact framework console target set(LY_TEST_IMPACT_CONSOLE_TARGET "TestImpact.Frontend.Console") @@ -395,6 +398,7 @@ function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE PERSISTENT_DATA_D # Set the above config file as the default config file to use for the test impact framework console target target_compile_definitions(${LY_TEST_IMPACT_CONSOLE_STATIC_TARGET} PUBLIC "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"${PERSISTENT_DATA_DIR}/$.$.json\"") + target_compile_definitions(${LY_TEST_IMPACT_PYTHON_COVERAGE_STATIC_TARGET} PRIVATE "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"${PERSISTENT_DATA_DIR}/$.$.json\"") message(DEBUG "Test impact framework post steps complete") endfunction() From 1cf1301a07fc05ce9806f58ee2eb4969818fde13 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 22 Jun 2021 15:45:32 +0100 Subject: [PATCH 15/29] Add extra comments Signed-off-by: John --- .../Gem/PythonCoverage/Code/CMakeLists.txt | 3 -- .../PythonCoverageEditorSystemComponent.cpp | 30 +++++++++++-------- .../PythonCoverageEditorSystemComponent.h | 6 ++-- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt index 570150758d..65b6d7509f 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt @@ -54,9 +54,6 @@ if(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED) Gem::PythonCoverage.Editor.Static ) - # By default, we will specify that the above target PythonCoverage would be used by - # Tool and Builder type targets when this gem is enabled. If you don't want it - # active in Tools or Builders by default, delete one of both of the following lines: ly_create_alias(NAME PythonCoverage.Tools NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) ly_create_alias(NAME PythonCoverage.Builders NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) endif() diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp index 674e8684a5..95a084570c 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp @@ -10,17 +10,15 @@ * */ +#include +#include #include #include #include #include -#include -#include #include -#pragma optimize("", off) - namespace PythonCoverage { constexpr char* const Caller = "PythonCoverageEditorSystemComponent"; @@ -41,6 +39,7 @@ namespace PythonCoverage // Attempt to discover the output directory for the test coverage files ParseCoverageOutputDirectory(); + // If no output directory discovered, coverage gathering will be disabled if (m_coverageState == CoverageState::Disabled) { return; @@ -77,11 +76,12 @@ namespace PythonCoverage void PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory() { m_coverageState = CoverageState::Disabled; + + // Config file path will be empty if test impact analysis framework is disabled at the build config level const AZStd::string configFilePath = LY_TEST_IMPACT_DEFAULT_CONFIG_FILE; if (configFilePath.empty()) { - // Config file path will be empty if test impact analysis framework is disabled AZ_Warning(Caller, false, "No test impact analysis framework config found."); return; } @@ -89,7 +89,7 @@ namespace PythonCoverage const auto fileSize = AZ::IO::SystemFile::Length(configFilePath.c_str()); if(!fileSize) { - AZ_Error(Caller, false, "File %s does not exist", configFilePath.c_str()); + AZ_Error(Caller, false, "File '%s' does not exist", configFilePath.c_str()); return; } @@ -97,7 +97,7 @@ namespace PythonCoverage buffer[fileSize] = '\0'; if (!AZ::IO::SystemFile::Read(configFilePath.c_str(), buffer.data())) { - AZ_Error(Caller, false, "Could not read contents of file %s", configFilePath.c_str()); + AZ_Error(Caller, false, "Could not read contents of file '%s'", configFilePath.c_str()); return; } @@ -110,15 +110,23 @@ namespace PythonCoverage } const auto& tempConfig = configurationFile["workspace"]["temp"]; + + // Temo directory root path is absolute const AZ::IO::Path tempWorkspaceRootDir = tempConfig["root"].GetString(); + + // Artifact directory is relative to temp directory root const AZ::IO::Path artifactRelativeDir = tempConfig["relative_paths"]["artifact_dir"].GetString(); m_coverageDir = tempWorkspaceRootDir / artifactRelativeDir; + + // Everything is good to go, await the first python test case m_coverageState = CoverageState::Idle; } void PythonCoverageEditorSystemComponent::WriteCoverageFile() { AZStd::string contents; + + // Compile the coverage for all test cases in this script for (const auto& [testCase, entityComponents] : m_entityComponentMap) { const auto coveringModules = GetParentComponentModulesForAllActivatedEntities(entityComponents); @@ -140,17 +148,13 @@ namespace PythonCoverage m_coverageFile.c_str(), AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY)) { - AZ_Error( - Caller, false, - "Couldn't open file %s for writing", m_coverageFile.c_str()); + AZ_Error(Caller, false, "Couldn't open file '%s' for writing", m_coverageFile.c_str()); return; } if (!file.Write(bytes.data(), bytes.size())) { - AZ_Error( - Caller, false, - "Couldn't write contents for file %s", m_coverageFile.c_str()); + AZ_Error(Caller, false, "Couldn't write contents for file '%s'", m_coverageFile.c_str()); return; } } diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h index 0494cf1778..13101ff19c 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h @@ -39,11 +39,11 @@ namespace PythonCoverage PythonCoverageEditorSystemComponent() = default; private: - // AZ::Component + // AZ::Component overrides... void Activate() override; void Deactivate() override; - // AZ::EntitySystemBus ... + // AZ::EntitySystemBus overrides... void OnEntityActivated(const AZ::EntityId& entityId) override; // AZ::EditorPythonScriptNotificationsBus ... @@ -66,7 +66,7 @@ namespace PythonCoverage AZStd::unordered_set GetParentComponentModulesForAllActivatedEntities( const AZStd::unordered_map& entityComponents) const; - //! + //! Writes the current coverage data snapshot to disk. void WriteCoverageFile(); enum class CoverageState : AZ::u8 From fd1bb483c030f16711ea26b82072baf07eab4a1b Mon Sep 17 00:00:00 2001 From: John Date: Wed, 23 Jun 2021 16:47:40 +0100 Subject: [PATCH 16/29] Address PR comments Signed-off-by: John --- .../Gem/PythonCoverage/Code/CMakeLists.txt | 29 ++---------- .../PythonCoverageEditorSystemComponent.cpp | 14 +++--- .../PythonCoverageEditorSystemComponent.h | 2 +- .../Code/Tests/PythonCoverageEditorTest.cpp | 24 ---------- .../pythoncoverage_editor_tests_files.cmake | 14 ------ AutomatedTesting/Gem/PythonCoverage/gem.json | 4 +- .../API/EditorPythonRunnerNotificationBus.h | 46 ------------------- .../API/EditorPythonScriptNotificationsBus.h | 16 +++---- .../Code/Source/PythonSystemComponent.cpp | 8 ++-- 9 files changed, 24 insertions(+), 133 deletions(-) delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp delete mode 100644 AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake delete mode 100644 Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt index 65b6d7509f..6d0242d02e 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonCoverage/Code/CMakeLists.txt @@ -28,14 +28,16 @@ if(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED) PUBLIC PYTHON_COVERAGE_EDITOR PRIVATE - LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"\" + ${LY_TEST_IMPACT_CONFIG_FILE_PATH_DEFINITION} BUILD_DEPENDENCIES PUBLIC AZ::AzToolsFramework + RUNTIME_DEPENDENCIES + Gem::EditorPythonBindings.Editor ) ly_add_target( - NAME PythonCoverage.Editor MODULE + NAME PythonCoverage.Editor GEM_MODULE NAMESPACE Gem AUTOMOC OUTPUT_NAME Gem.PythonCoverage.Editor @@ -58,26 +60,3 @@ if(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED) ly_create_alias(NAME PythonCoverage.Builders NAMESPACE Gem TARGETS Gem::PythonCoverage.Editor) endif() endif() - -if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) - if(PAL_TRAIT_BUILD_HOST_TOOLS) - ly_add_target( - NAME PythonCoverage.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} - NAMESPACE Gem - FILES_CMAKE - pythoncoverage_editor_tests_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - Tests - Source - BUILD_DEPENDENCIES - PRIVATE - AZ::AzTest - Gem::PythonCoverage.Editor - ) - - ly_add_googletest( - NAME Gem::PythonCoverage.Editor.Tests - ) - endif() -endif() \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp index 95a084570c..c33986192a 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp @@ -76,20 +76,18 @@ namespace PythonCoverage void PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory() { m_coverageState = CoverageState::Disabled; - - // Config file path will be empty if test impact analysis framework is disabled at the build config level const AZStd::string configFilePath = LY_TEST_IMPACT_DEFAULT_CONFIG_FILE; if (configFilePath.empty()) { - AZ_Warning(Caller, false, "No test impact analysis framework config found."); + AZ_Warning(Caller, false, "No test impact analysis framework config file specified."); return; } const auto fileSize = AZ::IO::SystemFile::Length(configFilePath.c_str()); if(!fileSize) { - AZ_Error(Caller, false, "File '%s' does not exist", configFilePath.c_str()); + AZ_Error(Caller, false, "Test impact analysis framework config file '%s' does not exist", configFilePath.c_str()); return; } @@ -97,7 +95,7 @@ namespace PythonCoverage buffer[fileSize] = '\0'; if (!AZ::IO::SystemFile::Read(configFilePath.c_str(), buffer.data())) { - AZ_Error(Caller, false, "Could not read contents of file '%s'", configFilePath.c_str()); + AZ_Error(Caller, false, "Could not read contents of test impact analysis framework config file '%s'", configFilePath.c_str()); return; } @@ -105,13 +103,13 @@ namespace PythonCoverage rapidjson::Document configurationFile; if (configurationFile.Parse(configurationData.c_str()).HasParseError()) { - AZ_Error(Caller, false, "Could not parse runtimeConfig data, JSON has errors"); + AZ_Error(Caller, false, "Could not parse test impact analysis framework config file data, JSON has errors"); return; } const auto& tempConfig = configurationFile["workspace"]["temp"]; - // Temo directory root path is absolute + // Temp directory root path is absolute const AZ::IO::Path tempWorkspaceRootDir = tempConfig["root"].GetString(); // Artifact directory is relative to temp directory root @@ -212,7 +210,7 @@ namespace PythonCoverage return coveringModuleOutputNames; } - void PythonCoverageEditorSystemComponent::OnExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) + void PythonCoverageEditorSystemComponent::OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) { if (m_coverageState == CoverageState::Disabled) { diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h index 13101ff19c..68f9299bcc 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h @@ -47,7 +47,7 @@ namespace PythonCoverage void OnEntityActivated(const AZ::EntityId& entityId) override; // AZ::EditorPythonScriptNotificationsBus ... - void OnExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) override; + void OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) override; //! Attempts to parse the test impact analysis framework configuration file. //! If either the test impact analysis framework is disabled or the configuration file cannot be parsed, python coverage diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp deleted file mode 100644 index 2c97ecaf73..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Tests/PythonCoverageEditorTest.cpp +++ /dev/null @@ -1,24 +0,0 @@ - -#include - -class PythonCoverageEditorTest - : public ::testing::Test -{ -protected: - void SetUp() override - { - - } - - void TearDown() override - { - - } -}; - -TEST_F(PythonCoverageEditorTest, SanityTest) -{ - ASSERT_TRUE(true); -} - -AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake b/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake deleted file mode 100644 index f1912e743a..0000000000 --- a/AutomatedTesting/Gem/PythonCoverage/Code/pythoncoverage_editor_tests_files.cmake +++ /dev/null @@ -1,14 +0,0 @@ -# -# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -# its licensors. -# -# For complete copyright and license terms please see the LICENSE at the root of this -# distribution (the "License"). All use of this software is governed by the License, -# or, if provided, by the license below or the license accompanying this file. Do not -# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# - -set(FILES - Tests/PythonCoverageEditorTest.cpp -) diff --git a/AutomatedTesting/Gem/PythonCoverage/gem.json b/AutomatedTesting/Gem/PythonCoverage/gem.json index 3ec79ac633..a31ee8adcb 100644 --- a/AutomatedTesting/Gem/PythonCoverage/gem.json +++ b/AutomatedTesting/Gem/PythonCoverage/gem.json @@ -1,9 +1,7 @@ { "gem_name": "PythonCoverage", - "origin": "The primary repo for PythonCoverage goes here: i.e. http://www.mydomain.com", - "license": "What license PythonCoverage uses goes here: i.e. https://opensource.org/licenses/MIT", "display_name": "PythonCoverage", - "summary": "A short description of PythonCoverage.", + "summary": "A tool for generating gem coverage for Python tests.", "canonical_tags": [ "Gem" ], diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h deleted file mode 100644 index ffd161d295..0000000000 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonRunnerNotificationBus.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or - * its licensors. - * - * For complete copyright and license terms please see the LICENSE at the root of this - * distribution (the "License"). All use of this software is governed by the License, - * or, if provided, by the license below or the license accompanying this file. Do not - * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - */ -#pragma once - -#include - -namespace AzToolsFramework -{ - //! Provides a bus to notify when Python scripts are about to run. - class EditorPythonRunnerNotification - : public AZ::EBusTraits - { - public: - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - //! Notifies the execution of a Python script using a string. - virtual void ExecuteByString([[maybe_unused]] AZStd::string_view script) {} - - //! Notifies the execution of a Python script using a filename. - virtual void ExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {} - - //! Notifies the execution of a Python script using a filename and args. - virtual void ExecuteByFilenameWithArgs( - [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} - - //! Notifies the execution of a Python script as a test. - virtual void ExecuteByFilenameAsTest( - [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} - }; - using EditorPythonRunnerNotificationBus = AZ::EBus; - -} // namespace AzToolsFramework - diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h index 586a348e76..7586293a38 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorPythonScriptNotificationsBus.h @@ -27,18 +27,18 @@ namespace AzToolsFramework static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; ////////////////////////////////////////////////////////////////////////// - //! Notifies the execution of a Python script using a string. - virtual void OnExecuteByString([[maybe_unused]] AZStd::string_view script) {} + //! Notifies the start of execution of a Python script using a string. + virtual void OnStartExecuteByString([[maybe_unused]] AZStd::string_view script) {} - //! Notifies the execution of a Python script using a filename. - virtual void OnExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {} + //! Notifies the start of execution of a Python script using a filename. + virtual void OnStartExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {} - //! Notifies the execution of a Python script using a filename and args. - virtual void OnExecuteByFilenameWithArgs( + //! Notifies the start of execution of a Python script using a filename and args. + virtual void OnStartExecuteByFilenameWithArgs( [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector& args) {} - //! Notifies the execution of a Python script as a test. - virtual void OnExecuteByFilenameAsTest( + //! Notifies the start of execution of a Python script as a test. + virtual void OnStartExecuteByFilenameAsTest( [[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) {} }; using EditorPythonScriptNotificationsBus = AZ::EBus; diff --git a/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp b/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp index e9e75b966f..e390578818 100644 --- a/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp +++ b/Gems/EditorPythonBindings/Code/Source/PythonSystemComponent.cpp @@ -581,7 +581,7 @@ namespace EditorPythonBindings if (!script.empty()) { AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( - &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByString, script); + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByString, script); // Acquire GIL before calling Python code AZStd::lock_guard lock(m_lock); @@ -644,14 +644,14 @@ namespace EditorPythonBindings { AZStd::vector args; AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( - &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByFilename, filename); + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilename, filename); ExecuteByFilenameWithArgs(filename, args); } void PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) { AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( - &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByFilenameAsTest, filename, testCase, args); + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameAsTest, filename, testCase, args); const Result evalResult = EvaluateFile(filename, args); if (evalResult == Result::Okay) { @@ -668,7 +668,7 @@ namespace EditorPythonBindings void PythonSystemComponent::ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector& args) { AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast( - &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnExecuteByFilenameWithArgs, filename, args); + &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameWithArgs, filename, args); EvaluateFile(filename, args); } From 773fb4ea74521aec0b215b40bce5ab176eed8572 Mon Sep 17 00:00:00 2001 From: John Date: Thu, 24 Jun 2021 15:56:59 +0100 Subject: [PATCH 17/29] Add python test case to hydra test utils editor launcher Signed-off-by: John --- .../editor_python_test_tools/hydra_test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py index 9f498bbf50..4ea7362083 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py @@ -54,7 +54,7 @@ def launch_and_validate_results(request, test_directory, editor, editor_script, logger.debug("Running automated test: {}".format(editor_script)) editor.args.extend(["--skipWelcomeScreenDialog", "--regset=/Amazon/Settings/EnableSourceControl=false", "--regset=/Amazon/Preferences/EnablePrefabSystem=false", run_python, test_case, - "--runpythonargs", " ".join(cfg_args)]) + f"--pythontestcase={request.node.originalname}", "--runpythonargs", " ".join(cfg_args)]) if auto_test_mode: editor.args.extend(["--autotest_mode"]) if null_renderer: From ee537ae8fa0b3dc4e01b133bd3351c477043fd32 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 28 Jun 2021 13:01:57 +0100 Subject: [PATCH 18/29] Address PR comments Signed-off-by: John --- .../Code/Source/PythonCoverageEditorModule.h | 2 +- .../PythonCoverageEditorSystemComponent.cpp | 32 +++++++++---------- .../PythonCoverageEditorSystemComponent.h | 25 ++++++++------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h index 180ed79656..2295d20f3e 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorModule.h @@ -22,7 +22,7 @@ namespace PythonCoverage { public: AZ_CLASS_ALLOCATOR_DECL - AZ_RTTI(PythonCoverageEditorModule, "{32C0FFEA-09A7-460F-9257-5BDEF74FCD5B}"); + AZ_RTTI(PythonCoverageEditorModule, "{32C0FFEA-09A7-460F-9257-5BDEF74FCD5B}", AZ::Module); PythonCoverageEditorModule(); ~PythonCoverageEditorModule(); diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp index c33986192a..dec9581296 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp @@ -21,7 +21,7 @@ namespace PythonCoverage { - constexpr char* const Caller = "PythonCoverageEditorSystemComponent"; + static constexpr char* const LogCallSite = "PythonCoverageEditorSystemComponent"; void PythonCoverageEditorSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -36,11 +36,8 @@ namespace PythonCoverage AzToolsFramework::EditorPythonScriptNotificationsBus::Handler::BusConnect(); AZ::EntitySystemBus::Handler::BusConnect(); - // Attempt to discover the output directory for the test coverage files - ParseCoverageOutputDirectory(); - // If no output directory discovered, coverage gathering will be disabled - if (m_coverageState == CoverageState::Disabled) + if (ParseCoverageOutputDirectory() == CoverageState::Disabled) { return; } @@ -73,38 +70,38 @@ namespace PythonCoverage } } - void PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory() + PythonCoverageEditorSystemComponent::CoverageState PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory() { m_coverageState = CoverageState::Disabled; const AZStd::string configFilePath = LY_TEST_IMPACT_DEFAULT_CONFIG_FILE; if (configFilePath.empty()) { - AZ_Warning(Caller, false, "No test impact analysis framework config file specified."); - return; + AZ_Warning(LogCallSite, false, "No test impact analysis framework config file specified."); + return m_coverageState; } const auto fileSize = AZ::IO::SystemFile::Length(configFilePath.c_str()); if(!fileSize) { - AZ_Error(Caller, false, "Test impact analysis framework config file '%s' does not exist", configFilePath.c_str()); - return; + AZ_Error(LogCallSite, false, "Test impact analysis framework config file '%s' does not exist", configFilePath.c_str()); + return m_coverageState; } AZStd::vector buffer(fileSize + 1); buffer[fileSize] = '\0'; if (!AZ::IO::SystemFile::Read(configFilePath.c_str(), buffer.data())) { - AZ_Error(Caller, false, "Could not read contents of test impact analysis framework config file '%s'", configFilePath.c_str()); - return; + AZ_Error(LogCallSite, false, "Could not read contents of test impact analysis framework config file '%s'", configFilePath.c_str()); + return m_coverageState; } const AZStd::string configurationData = AZStd::string(buffer.begin(), buffer.end()); rapidjson::Document configurationFile; if (configurationFile.Parse(configurationData.c_str()).HasParseError()) { - AZ_Error(Caller, false, "Could not parse test impact analysis framework config file data, JSON has errors"); - return; + AZ_Error(LogCallSite, false, "Could not parse test impact analysis framework config file data, JSON has errors"); + return m_coverageState; } const auto& tempConfig = configurationFile["workspace"]["temp"]; @@ -118,6 +115,7 @@ namespace PythonCoverage // Everything is good to go, await the first python test case m_coverageState = CoverageState::Idle; + return m_coverageState; } void PythonCoverageEditorSystemComponent::WriteCoverageFile() @@ -146,13 +144,13 @@ namespace PythonCoverage m_coverageFile.c_str(), AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY)) { - AZ_Error(Caller, false, "Couldn't open file '%s' for writing", m_coverageFile.c_str()); + AZ_Error(LogCallSite, false, "Couldn't open file '%s' for writing", m_coverageFile.c_str()); return; } if (!file.Write(bytes.data(), bytes.size())) { - AZ_Error(Caller, false, "Couldn't write contents for file '%s'", m_coverageFile.c_str()); + AZ_Error(LogCallSite, false, "Couldn't write contents for file '%s'", m_coverageFile.c_str()); return; } } @@ -228,7 +226,7 @@ namespace PythonCoverage { // We need to be able to pinpoint the coverage data to the specific test case names otherwise we will not be able // to specify which specific tests should be run in the future (filename does not necessarily equate to test case name) - AZ_Error(Caller, false, "No test case specified, coverage data gathering will be disabled for this test"); + AZ_Error(LogCallSite, false, "No test case specified, coverage data gathering will be disabled for this test"); return; } diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h index 68f9299bcc..c2d6950ade 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.h @@ -39,6 +39,14 @@ namespace PythonCoverage PythonCoverageEditorSystemComponent() = default; private: + //! The coverage state for Python tests. + enum class CoverageState : AZ::u8 + { + Disabled, //!< Python coverage is disabled. + Idle, //!< Python coverage is enabled but not actively gathering coverage data. + Gathering //!< Python coverage is enabled and actively gathering coverage data. + }; + // AZ::Component overrides... void Activate() override; void Deactivate() override; @@ -49,17 +57,17 @@ namespace PythonCoverage // AZ::EditorPythonScriptNotificationsBus ... void OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector& args) override; - //! Attempts to parse the test impact analysis framework configuration file. - //! If either the test impact analysis framework is disabled or the configuration file cannot be parsed, python coverage - //! is disabled. - void ParseCoverageOutputDirectory(); - //! Enumerates all of the loaded shared library modules and the component descriptors that belong to them. void EnumerateAllModuleComponents(); //! Enumerates all of the component descriptors for the specified entity. void EnumerateComponentsForEntity(const AZ::EntityId& entityId); + //! Attempts to parse the test impact analysis framework configuration file. + //! If either the test impact analysis framework is disabled or the configuration file cannot be parsed, python coverage is disabled. + //! @returns The coverage state after the parsing attempt. + CoverageState ParseCoverageOutputDirectory(); + //! Returns all of the shared library modules that parent the component descriptors of the specified set of activated entities. //! @note Entity component descriptors are still retrieved even if the entity in question has since been deactivated. //! @param entityComponents The set of activated entities and their component descriptors to get the parent modules for. @@ -69,13 +77,6 @@ namespace PythonCoverage //! Writes the current coverage data snapshot to disk. void WriteCoverageFile(); - enum class CoverageState : AZ::u8 - { - Disabled, //!< Python coverage is disabled. - Idle, //!< Python coverage is enabled but not actively gathering coverage data. - Gathering //!< Python coverage is enabled and actively gathering coverage data. - }; - CoverageState m_coverageState = CoverageState::Disabled; //!< Current coverage state. AZStd::unordered_map> m_entityComponentMap; //!< Map of //!< component IDs to component descriptors for all activated entities, organized by test cases. From e310581e596427ac2bd82ac4c18f6d1e06ced291 Mon Sep 17 00:00:00 2001 From: Alex Peterson <26804013+AMZN-alexpete@users.noreply.github.com> Date: Mon, 28 Jun 2021 11:15:04 -0700 Subject: [PATCH 19/29] Updating LFS config to new endpoint (#1623) Signed-off-by: AMZN-alexpete Signed-off-by: John --- .lfsconfig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.lfsconfig b/.lfsconfig index 5b405115eb..e51eb5e7c4 100644 --- a/.lfsconfig +++ b/.lfsconfig @@ -1,2 +1,11 @@ [lfs] -url=https://dsasbkzux9giw.cloudfront.net/api/v1 +# Default LFS endpoint for this repository +url=https://d3df09qsjufr6g.cloudfront.net/api/v1 + +# To use the endpoint with your fork: +# 1. uncomment the url line below by removing the '#' +# 2. replace 'owner' with the username or organization that owns the fork +# 3. have git ignore your local modification of this file by running +# git update-index --skip-worktree .lfsconfig + +# url=https://d3df09qsjufr6g.cloudfront.net/api/v1/fork/owner From 0193062de45b03aba283f0fb3c5240539e609587 Mon Sep 17 00:00:00 2001 From: jiaweig Date: Wed, 23 Jun 2021 17:26:52 -0700 Subject: [PATCH 20/29] Fix validation errors Signed-off-by: John --- .../RHI/Vulkan/Code/Source/RHI/Device.cpp | 60 ++++++++++--------- Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h | 2 + .../Vulkan/Code/Source/RHI/PhysicalDevice.cpp | 60 ++++++++++++++++++- .../Vulkan/Code/Source/RHI/PhysicalDevice.h | 27 +++++++++ 4 files changed, 120 insertions(+), 29 deletions(-) diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp index 04f2465e78..6a29c398d8 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp @@ -66,30 +66,7 @@ namespace AZ RawStringList requiredLayers = GetRequiredLayers(); RawStringList requiredExtensions = GetRequiredExtensions(); - StringList deviceExtensions = physicalDevice.GetDeviceExtensionNames(); - RawStringList optionalDeviceExtensions = { { - VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME, - VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME, - VK_EXT_MEMORY_BUDGET_EXTENSION_NAME, - VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME, - VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME, - VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME, - VK_KHR_RELAXED_BLOCK_LAYOUT_EXTENSION_NAME, - VK_EXT_MEMORY_BUDGET_EXTENSION_NAME, - VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, - VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME, - - // ray tracing extensions - VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, - VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME, - VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, - VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME, - VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME, - VK_KHR_SPIRV_1_4_EXTENSION_NAME, - VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME - } }; - - RawStringList optionalExtensions = FilterList(optionalDeviceExtensions, deviceExtensions); + RawStringList optionalExtensions = physicalDevice.FilterSupportedOptionalExtensions(); requiredExtensions.insert(requiredExtensions.end(), optionalExtensions.begin(), optionalExtensions.end()); //We now need to find the queues that the physical device has available and make sure @@ -164,7 +141,7 @@ namespace AZ // unbounded array functionality VkPhysicalDeviceDescriptorIndexingFeaturesEXT descriptorIndexingFeatures = {}; - descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES; + descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT; const VkPhysicalDeviceDescriptorIndexingFeaturesEXT& physicalDeviceDescriptorIndexingFeatures = physicalDevice.GetPhysicalDeviceDescriptorIndexingFeatures(); descriptorIndexingFeatures.shaderInputAttachmentArrayDynamicIndexing = physicalDeviceDescriptorIndexingFeatures.shaderInputAttachmentArrayDynamicIndexing; @@ -181,6 +158,15 @@ namespace AZ descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = physicalDeviceDescriptorIndexingFeatures.descriptorBindingVariableDescriptorCount; descriptorIndexingFeatures.runtimeDescriptorArray = physicalDeviceDescriptorIndexingFeatures.runtimeDescriptorArray; + VkPhysicalDeviceBufferDeviceAddressFeaturesEXT bufferDeviceAddressFeatures = {}; + bufferDeviceAddressFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES_EXT; + const VkPhysicalDeviceBufferDeviceAddressFeaturesEXT& physicalDeviceBufferDeviceAddressFeatures = + physicalDevice.GetPhysicalDeviceBufferDeviceAddressFeatures(); + bufferDeviceAddressFeatures.bufferDeviceAddress = physicalDeviceBufferDeviceAddressFeatures.bufferDeviceAddress; + bufferDeviceAddressFeatures.bufferDeviceAddressCaptureReplay = physicalDeviceBufferDeviceAddressFeatures.bufferDeviceAddressCaptureReplay; + bufferDeviceAddressFeatures.bufferDeviceAddressMultiDevice = physicalDeviceBufferDeviceAddressFeatures.bufferDeviceAddressMultiDevice; + descriptorIndexingFeatures.pNext = &bufferDeviceAddressFeatures; + VkPhysicalDeviceDepthClipEnableFeaturesEXT depthClipEnabled = {}; depthClipEnabled.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_ENABLE_FEATURES_EXT; depthClipEnabled.depthClipEnable = physicalDevice.GetPhysicalDeviceDepthClipEnableFeatures().depthClipEnable; @@ -189,7 +175,7 @@ namespace AZ VkPhysicalDeviceRobustness2FeaturesEXT robustness2 = {}; robustness2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT; robustness2.nullDescriptor = physicalDevice.GetPhysicalDeviceRobutness2Features().nullDescriptor; - depthClipEnabled.pNext = &robustness2; + bufferDeviceAddressFeatures.pNext = &robustness2; VkPhysicalDeviceVulkan12Features vulkan12Features = {}; VkPhysicalDeviceShaderFloat16Int8FeaturesKHR float16Int8 = {}; @@ -200,6 +186,7 @@ namespace AZ if (majorVersion >= 1 && minorVersion >= 2) { vulkan12Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; + VkPhysicalDeviceVulkan12Features physicalDeviceVulkan12Features = physicalDevice.GetPhysicalDeviceVulkan12Features(); vulkan12Features.drawIndirectCount = physicalDevice.GetPhysicalDeviceVulkan12Features().drawIndirectCount; vulkan12Features.shaderFloat16 = physicalDevice.GetPhysicalDeviceVulkan12Features().shaderFloat16; vulkan12Features.shaderInt8 = physicalDevice.GetPhysicalDeviceVulkan12Features().shaderInt8; @@ -800,6 +787,23 @@ namespace AZ return *m_nullDescriptorManager; } + VkBufferUsageFlags Device::ValidateBufferUsageFlagsByFeatures(const VkBufferUsageFlags& bufferUsageFlags) const + { + const auto& physicalDevice = static_cast(GetPhysicalDevice()); + + VkBufferUsageFlags result = bufferUsageFlags; + if (!physicalDevice.GetPhysicalDeviceBufferDeviceAddressFeatures().bufferDeviceAddress) + { + result &= ~VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + } + if (!physicalDevice.IsOptionalDeviceExtensionSupported(OptionalDeviceExtension::AccelerationStructure)) + { + result &= ~VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR; + } + + return result; + } + VkBuffer Device::CreateBufferResouce(const RHI::BufferDescriptor& descriptor) const { AZ_Assert(descriptor.m_sharedQueueMask != RHI::HardwareQueueClassMask::None, "Invalid shared queue mask"); @@ -811,9 +815,9 @@ namespace AZ createInfo.flags = 0; createInfo.size = descriptor.m_byteCount; createInfo.usage = GetBufferUsageFlagBits(descriptor.m_bindFlags); + createInfo.usage = ValidateBufferUsageFlagsByFeatures(createInfo.usage); // Trying to guess here if the buffers are going to be used as attachments. Maybe it would be better to add an explicit flag in the descriptor. - createInfo.sharingMode = RHI::CheckBitsAny(descriptor.m_bindFlags, RHI::BufferBindFlags::ShaderWrite | RHI::BufferBindFlags::Predication | RHI::BufferBindFlags::Indirect) ? VK_SHARING_MODE_EXCLUSIVE : VK_SHARING_MODE_CONCURRENT; - createInfo.queueFamilyIndexCount = static_cast(queueFamilies.size()); + createInfo.sharingMode = RHI::CheckBitsAny(descriptor.m_bindFlags, RHI::BufferBindFlags::ShaderWrite | RHI::BufferBindFlags::Predication | RHI::BufferBindFlags::Indirect) ? VK_SHARING_MODE_EXCLUSIVE : VK_SHARING_MODE_CONCURRENT; createInfo.queueFamilyIndexCount = static_cast(queueFamilies.size()); createInfo.pQueueFamilyIndices = queueFamilies.empty() ? nullptr : queueFamilies.data(); VkBuffer vkBuffer = VK_NULL_HANDLE; diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h index d79b937d17..94e78afc0c 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h @@ -149,6 +149,8 @@ namespace AZ template RHI::Ptr AcquireObjectFromCache(ObjectCache& cache, const size_t hash, Args... args); + VkBufferUsageFlags ValidateBufferUsageFlagsByFeatures(const VkBufferUsageFlags& bufferUsageFlags) const; + VkDevice m_nativeDevice = VK_NULL_HANDLE; VkPhysicalDeviceFeatures m_enabledDeviceFeatures{}; VkPipelineStageFlags m_supportedPipelineStageFlagsMask = ~0; diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp index 9fd38aab93..e597ef30b8 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp @@ -133,6 +133,11 @@ namespace AZ return m_descriptorIndexingFeatures; } + const VkPhysicalDeviceBufferDeviceAddressFeaturesEXT& PhysicalDevice::GetPhysicalDeviceBufferDeviceAddressFeatures() const + { + return m_bufferDeviceAddressFeatures; + } + const VkPhysicalDeviceVulkan12Features& PhysicalDevice::GetPhysicalDeviceVulkan12Features() const { return m_vulkan12Features; @@ -234,6 +239,48 @@ namespace AZ (m_separateDepthStencilFeatures.separateDepthStencilLayouts && VK_DEVICE_EXTENSION_SUPPORTED(KHR_separate_depth_stencil_layouts)) || (m_vulkan12Features.separateDepthStencilLayouts)); m_features.set(static_cast(DeviceFeature::DescriptorIndexing), VK_DEVICE_EXTENSION_SUPPORTED(EXT_descriptor_indexing)); + m_features.set(static_cast(DeviceFeature::BufferDeviceAddress), VK_DEVICE_EXTENSION_SUPPORTED(EXT_buffer_device_address)); + } + + RawStringList PhysicalDevice::FilterSupportedOptionalExtensions() + { + // The order must match the enum OptionalDeviceExtensions + RawStringList optionalExtensions = { { + VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME, + VK_EXT_CONDITIONAL_RENDERING_EXTENSION_NAME, + VK_EXT_MEMORY_BUDGET_EXTENSION_NAME, + VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME, + VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME, + VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME, + VK_KHR_RELAXED_BLOCK_LAYOUT_EXTENSION_NAME, + VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, + VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME, + + // ray tracing extensions + VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, + VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME, + VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, + VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME, + VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME, + VK_KHR_SPIRV_1_4_EXTENSION_NAME, + VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME + } }; + + StringList deviceExtensions = GetDeviceExtensionNames(); + RawStringList filteredOptionalExtensions = FilterList(optionalExtensions, deviceExtensions); + + uint32_t originalIndex = 0; + for (const auto& extension : filteredOptionalExtensions) + { + while (strcmp(extension, optionalExtensions[originalIndex]) != 0) + { + ++originalIndex; + } + m_optionalExtensions.set(originalIndex); + ++originalIndex; + } + + return filteredOptionalExtensions; } void PhysicalDevice::CompileMemoryStatistics(RHI::MemoryStatisticsBuilder& builder) const @@ -271,10 +318,15 @@ namespace AZ descriptorIndexingFeatures = {}; descriptorIndexingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT; + VkPhysicalDeviceBufferDeviceAddressFeaturesEXT& bufferDeviceAddressFeatures = m_bufferDeviceAddressFeatures; + bufferDeviceAddressFeatures = {}; + bufferDeviceAddressFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES_EXT; + descriptorIndexingFeatures.pNext = &bufferDeviceAddressFeatures; + VkPhysicalDeviceDepthClipEnableFeaturesEXT& dephClipEnableFeatures = m_dephClipEnableFeatures; dephClipEnableFeatures = {}; dephClipEnableFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_ENABLE_FEATURES_EXT; - descriptorIndexingFeatures.pNext = &dephClipEnableFeatures; + bufferDeviceAddressFeatures.pNext = &dephClipEnableFeatures; VkPhysicalDeviceRobustness2FeaturesEXT& robustness2Feature = m_robutness2Features; robustness2Feature = {}; @@ -402,5 +454,11 @@ namespace AZ return m_features.test(index); } + bool PhysicalDevice::IsOptionalDeviceExtensionSupported(OptionalDeviceExtension optionalDeviceExtension) const + { + uint32_t index = static_cast(optionalDeviceExtension); + AZ_Assert(index < m_optionalExtensions.size(), "Invalid feature %d", index); + return m_optionalExtensions.test(index); + } } } diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h index 9667e03d3b..0b84f42e0d 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h @@ -35,9 +35,31 @@ namespace AZ NullDescriptor, SeparateDepthStencil, DescriptorIndexing, + BufferDeviceAddress, Count // Must be last }; + enum class OptionalDeviceExtension : uint32_t + { + SampleLocation = 0, + ConditionalRendering, + MemoryBudget, + DepthClipEnable, + ConservativeRasterization, + DrawIndirectCount, + RelaxedBlockLayout, + Robustness2, + ShaderFloat16Int8, + AccelerationStructure, + RayTracingPipeline, + BufferDeviceAddress, + DeferredHostOperations, + DescriptorIndexing, + Spirv14, + ShaderFloatControls, + Count + }; + class PhysicalDevice final : public RHI::PhysicalDevice { @@ -51,6 +73,7 @@ namespace AZ const VkPhysicalDevice& GetNativePhysicalDevice() const; const VkPhysicalDeviceMemoryProperties& GetMemoryProperties() const; bool IsFeatureSupported(DeviceFeature feature) const; + bool IsOptionalDeviceExtensionSupported(OptionalDeviceExtension optionalDeviceExtension) const; const VkPhysicalDeviceLimits& GetDeviceLimits() const; const VkPhysicalDeviceFeatures& GetPhysicalDeviceFeatures() const; const VkPhysicalDeviceProperties& GetPhysicalDeviceProperties() const; @@ -59,6 +82,7 @@ namespace AZ const VkPhysicalDeviceRobustness2FeaturesEXT& GetPhysicalDeviceRobutness2Features() const; const VkPhysicalDeviceShaderFloat16Int8FeaturesKHR& GetPhysicalDeviceFloat16Int8Features() const; const VkPhysicalDeviceDescriptorIndexingFeaturesEXT& GetPhysicalDeviceDescriptorIndexingFeatures() const; + const VkPhysicalDeviceBufferDeviceAddressFeaturesEXT& GetPhysicalDeviceBufferDeviceAddressFeatures() const; const VkPhysicalDeviceVulkan12Features& GetPhysicalDeviceVulkan12Features() const; const VkPhysicalDeviceSeparateDepthStencilLayoutsFeaturesKHR& GetPhysicalDeviceSeparateDepthStencilFeatures() const; const VkPhysicalDeviceAccelerationStructurePropertiesKHR& GetPhysicalDeviceAccelerationStructureProperties() const; @@ -68,6 +92,7 @@ namespace AZ StringList GetDeviceExtensionNames(const char* layerName = nullptr) const; bool IsFormatSupported(RHI::Format format, VkImageTiling tiling, VkFormatFeatureFlags features) const; void LoadSupportedFeatures(); + RawStringList FilterSupportedOptionalExtensions(); void CompileMemoryStatistics(RHI::MemoryStatisticsBuilder& builder) const; private: @@ -82,6 +107,7 @@ namespace AZ VkPhysicalDevice m_vkPhysicalDevice = VK_NULL_HANDLE; VkPhysicalDeviceMemoryProperties m_memoryProperty{}; + AZStd::bitset(OptionalDeviceExtension::Count)> m_optionalExtensions; AZStd::bitset(DeviceFeature::Count)> m_features; VkPhysicalDeviceFeatures m_deviceFeatures{}; VkPhysicalDeviceProperties m_deviceProperties{}; @@ -90,6 +116,7 @@ namespace AZ VkPhysicalDeviceRobustness2FeaturesEXT m_robutness2Features{}; VkPhysicalDeviceShaderFloat16Int8FeaturesKHR m_float16Int8Features{}; VkPhysicalDeviceDescriptorIndexingFeaturesEXT m_descriptorIndexingFeatures{}; + VkPhysicalDeviceBufferDeviceAddressFeaturesEXT m_bufferDeviceAddressFeatures{}; VkPhysicalDeviceSeparateDepthStencilLayoutsFeaturesKHR m_separateDepthStencilFeatures{}; VkPhysicalDeviceAccelerationStructurePropertiesKHR m_accelerationStructureProperties{}; VkPhysicalDeviceRayTracingPipelinePropertiesKHR m_rayTracingPipelineProperties{}; From c246aa074d3728fedf93233ecba14d5de3814e1d Mon Sep 17 00:00:00 2001 From: jiaweig Date: Wed, 23 Jun 2021 17:40:27 -0700 Subject: [PATCH 21/29] Break long line Signed-off-by: John --- Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp index 6a29c398d8..213f8f332f 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp @@ -817,7 +817,13 @@ namespace AZ createInfo.usage = GetBufferUsageFlagBits(descriptor.m_bindFlags); createInfo.usage = ValidateBufferUsageFlagsByFeatures(createInfo.usage); // Trying to guess here if the buffers are going to be used as attachments. Maybe it would be better to add an explicit flag in the descriptor. - createInfo.sharingMode = RHI::CheckBitsAny(descriptor.m_bindFlags, RHI::BufferBindFlags::ShaderWrite | RHI::BufferBindFlags::Predication | RHI::BufferBindFlags::Indirect) ? VK_SHARING_MODE_EXCLUSIVE : VK_SHARING_MODE_CONCURRENT; createInfo.queueFamilyIndexCount = static_cast(queueFamilies.size()); + createInfo.sharingMode = + RHI::CheckBitsAny( + descriptor.m_bindFlags, + RHI::BufferBindFlags::ShaderWrite | RHI::BufferBindFlags::Predication | RHI::BufferBindFlags::Indirect) + ? VK_SHARING_MODE_EXCLUSIVE + : VK_SHARING_MODE_CONCURRENT; + createInfo.queueFamilyIndexCount = static_cast(queueFamilies.size()); createInfo.pQueueFamilyIndices = queueFamilies.empty() ? nullptr : queueFamilies.data(); VkBuffer vkBuffer = VK_NULL_HANDLE; From 1e8465f1cc55fcf491eda0307c757e2e1b9f9d9e Mon Sep 17 00:00:00 2001 From: jiaweig Date: Fri, 25 Jun 2021 14:17:17 -0700 Subject: [PATCH 22/29] address comments Signed-off-by: John --- Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp | 17 ++++++++++------- Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h | 4 +++- .../Vulkan/Code/Source/RHI/PhysicalDevice.cpp | 8 ++++++++ .../RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h | 1 + 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp index 213f8f332f..0f3d8fa5e5 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.cpp @@ -787,21 +787,25 @@ namespace AZ return *m_nullDescriptorManager; } - VkBufferUsageFlags Device::ValidateBufferUsageFlagsByFeatures(const VkBufferUsageFlags& bufferUsageFlags) const + VkBufferUsageFlags Device::GetBufferUsageFlagBitsUnderRestrictions(RHI::BufferBindFlags bindFlags) const { + VkBufferUsageFlags bufferUsageFlags = GetBufferUsageFlagBits(bindFlags); + const auto& physicalDevice = static_cast(GetPhysicalDevice()); - VkBufferUsageFlags result = bufferUsageFlags; + // VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT require bufferDeviceAddress enabled. if (!physicalDevice.GetPhysicalDeviceBufferDeviceAddressFeatures().bufferDeviceAddress) { - result &= ~VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + bufferUsageFlags &= ~VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; } + // VK_KHR_acceleration_structure provides VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR + // Otherwise unrecognized flag. if (!physicalDevice.IsOptionalDeviceExtensionSupported(OptionalDeviceExtension::AccelerationStructure)) { - result &= ~VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR; + bufferUsageFlags &= ~VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR; } - return result; + return bufferUsageFlags; } VkBuffer Device::CreateBufferResouce(const RHI::BufferDescriptor& descriptor) const @@ -814,8 +818,7 @@ namespace AZ createInfo.pNext = nullptr; createInfo.flags = 0; createInfo.size = descriptor.m_byteCount; - createInfo.usage = GetBufferUsageFlagBits(descriptor.m_bindFlags); - createInfo.usage = ValidateBufferUsageFlagsByFeatures(createInfo.usage); + createInfo.usage = GetBufferUsageFlagBitsUnderRestrictions(descriptor.m_bindFlags); // Trying to guess here if the buffers are going to be used as attachments. Maybe it would be better to add an explicit flag in the descriptor. createInfo.sharingMode = RHI::CheckBitsAny( diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h index 94e78afc0c..234f87e8e9 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Device.h @@ -149,7 +149,9 @@ namespace AZ template RHI::Ptr AcquireObjectFromCache(ObjectCache& cache, const size_t hash, Args... args); - VkBufferUsageFlags ValidateBufferUsageFlagsByFeatures(const VkBufferUsageFlags& bufferUsageFlags) const; + //! Get the vulkan buffer usage flags from buffer bind flags. + //! Flags will be corrected if required features or extensions are not enabled. + VkBufferUsageFlags GetBufferUsageFlagBitsUnderRestrictions(RHI::BufferBindFlags bindFlags) const; VkDevice m_nativeDevice = VK_NULL_HANDLE; VkPhysicalDeviceFeatures m_enabledDeviceFeatures{}; diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp index e597ef30b8..c391a15006 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.cpp @@ -266,12 +266,20 @@ namespace AZ VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME } }; + uint32_t optionalExtensionCount = sizeof(optionalExtensions) / sizeof(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); + + AZ_Assert(optionalExtensionCount == static_cast(OptionalDeviceExtension::Count), "The order and size must match the enum OptionalDeviceExtensions."); + + // Optional device extensions are filtered based on what the device support. + // It returns in the same order as in the original list. StringList deviceExtensions = GetDeviceExtensionNames(); RawStringList filteredOptionalExtensions = FilterList(optionalExtensions, deviceExtensions); + // Mark the supported optional extensions in the bitset for faster look up compared to string search. uint32_t originalIndex = 0; for (const auto& extension : filteredOptionalExtensions) { + AZ_Assert(originalIndex < optionalExtensionCount, "Out of range index. Check FilterList algorithm if list is returned in the original order."); while (strcmp(extension, optionalExtensions[originalIndex]) != 0) { ++originalIndex; diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h index 0b84f42e0d..7a06e08e56 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/PhysicalDevice.h @@ -92,6 +92,7 @@ namespace AZ StringList GetDeviceExtensionNames(const char* layerName = nullptr) const; bool IsFormatSupported(RHI::Format format, VkImageTiling tiling, VkFormatFeatureFlags features) const; void LoadSupportedFeatures(); + //! Filter optional extensions based on what the physics device support. RawStringList FilterSupportedOptionalExtensions(); void CompileMemoryStatistics(RHI::MemoryStatisticsBuilder& builder) const; From b87e3eb1d6cc9a83c6074ef2ce044204eac88cea Mon Sep 17 00:00:00 2001 From: IgnacioMartinezGarrido <82394219+IgnacioMartinezGarrido@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:41:35 +0100 Subject: [PATCH 23/29] LYN-4863: Item data not copied on drag-n-drop if column is hidden in Asset Browser Tree View. (#1616) * Fixed: Item returning null in drag and drop when column is hidden * Added explanation about why this approach is needed Signed-off-by: John --- .../AssetBrowser/Views/AssetBrowserTreeView.cpp | 15 ++++++++++++--- .../AssetBrowser/Views/AssetBrowserTreeView.h | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp index 026343c2a4..8956d072ad 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp @@ -185,10 +185,11 @@ namespace AzToolsFramework void AssetBrowserTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { - // if selected entry is being removed, clear selection so not to select (and attempt to preview) other entries potentially marked for deletion - if (selectionModel() && selectionModel()->selectedIndexes().size() == 1) + // if selected entry is being removed, clear selection so not to select (and attempt to preview) other entries potentially + // marked for deletion + if (selectionModel() && selectedIndexes().size() == 1) { - QModelIndex selectedIndex = selectionModel()->selectedIndexes().first(); + QModelIndex selectedIndex = selectedIndexes().first(); QModelIndex parentSelectedIndex = selectedIndex.parent(); if (parentSelectedIndex == parent && selectedIndex.row() >= start && selectedIndex.row() <= end) { @@ -198,6 +199,14 @@ namespace AzToolsFramework QTreeView::rowsAboutToBeRemoved(parent, start, end); } + // Item data for hidden columns normally isn't copied by Qt during drag-and-drop (see QTBUG-30242). + // However, for the AssetBrowser, the hidden columns should get copied. By overriding selectedIndexes() to + // include all selected indices, not just the visible ones, we can get the behavior we're looking for. + QModelIndexList AssetBrowserTreeView::selectedIndexes() const + { + return selectionModel()->selectedIndexes(); + } + void AssetBrowserTreeView::SetThumbnailContext(const char* thumbnailContext) const { m_delegate->SetThumbnailContext(thumbnailContext); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.h b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.h index 19cbd3745a..867e01d80d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.h @@ -95,6 +95,9 @@ namespace AzToolsFramework void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; + protected: + QModelIndexList selectedIndexes() const override; + private: QPointer m_assetBrowserModel = nullptr; QPointer m_assetBrowserSortFilterProxyModel = nullptr; From aa94effd26cb2b12577450d19af2c964e6171588 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 28 Jun 2021 10:28:57 +0100 Subject: [PATCH 24/29] Fix snapshot choice that was incorrectly added Signed-off-by: John --- scripts/build/Jenkins/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index 0f5d5638be..f9d4262f54 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -471,7 +471,7 @@ try { } } else { // Non-PR builds - choice(defaultValue: DEFAULT_BUILD_SNAPSHOT, name: 'SNAPSHOT', choices: BUILD_SNAPSHOTS, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.') + pipelineParameters.add(choice(defaultValue: DEFAULT_BUILD_SNAPSHOT, name: 'SNAPSHOT', choices: BUILD_SNAPSHOTS, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.')) snapshot = env.SNAPSHOT echo "Snapshot \"${snapshot}\" selected." } From 6db3650d45da1df9879bd780dba6d8da2795dae9 Mon Sep 17 00:00:00 2001 From: amzn-sean <75276488+amzn-sean@users.noreply.github.com> Date: Wed, 23 Jun 2021 19:04:45 +0100 Subject: [PATCH 25/29] fix possible double connect of the Collider m_debugDisplayDataChangedEvent (#1518) Signed-off-by: John --- Gems/PhysX/Code/Editor/DebugDraw.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Gems/PhysX/Code/Editor/DebugDraw.cpp b/Gems/PhysX/Code/Editor/DebugDraw.cpp index 1f1c24b9ad..bd805cab4b 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.cpp +++ b/Gems/PhysX/Code/Editor/DebugDraw.cpp @@ -200,10 +200,8 @@ namespace PhysX void Collider::Disconnect() { - if (AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusIsConnected()) - { - AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusDisconnect(); - } + m_debugDisplayDataChangedEvent.Disconnect(); + AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusDisconnect(); AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusDisconnect(); AzFramework::EntityDebugDisplayEventBus::Handler::BusDisconnect(); m_displayCallback = nullptr; From 4f45211c85c533907b96aefbde7de4b0b94c7ac5 Mon Sep 17 00:00:00 2001 From: Royal OBrien Date: Tue, 29 Jun 2021 08:17:41 -0700 Subject: [PATCH 26/29] Migrated templates Signed-off-by: John --- .github/ISSUE_TEMPLATE/rfc-feature.md | 73 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/rfc-suggestion.md | 61 ++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/rfc-feature.md create mode 100644 .github/ISSUE_TEMPLATE/rfc-suggestion.md diff --git a/.github/ISSUE_TEMPLATE/rfc-feature.md b/.github/ISSUE_TEMPLATE/rfc-feature.md new file mode 100644 index 0000000000..31a195f803 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc-feature.md @@ -0,0 +1,73 @@ +--- +name: RFC Feature request +about: Create Feature RFC for this project +title: Proposed RFC Feature =description= +labels: 'rfc-feature' +assignees: '' + +--- + +# O3DE RFC Feature Template + +### When using this template, you do not have to fill out every question below. The questions are there for guidance. + +This RFC feature template should be used for any feature that is not a bug or a substantial reorganization of the O3DE product. + +If you submit a pull request to implement a new feature without going through the RFC process, it may be closed with a polite request to submit an RFC first. + +A hastily proposed RFC can hurt its chances of acceptance. Low-quality proposals, proposals for previously-rejected features, or those that don't fit into the near-term roadmap may be quickly rejected, demotivating the unprepared contributor. Laying some groundwork ahead of the RFC can make the process smoother. + +Although there is no single way to prepare for submitting an RFC, it is generally a good idea to pursue feedback from other project developers beforehand to ascertain that the RFC may be desirable; having a consistent impact on the project requires concerted effort toward consensus-building. + +The most common preparations for writing and submitting an RFC include: +- Talking the idea over on our Discord server. +- Discussing the topic on our GitHub RFCs discussions page. +- Occasionally posting "pre-RFCs" on the GitHub RFCs discussion page. +You may file issues in the RFCs repo for discussion, but these are not actively looked at by the teams. + +As a rule of thumb, receiving encouraging feedback from long-standing project developers, and particularly members of the relevant sub-team, is a good indication that the RFC is worth pursuing. + +# ----- DELETE EVERYTHING FROM THE TOP TO THE SUMMARY LINE BELOW WHEN USING TEMPLATE ----- # + +### Summary: +Single paragraph explanation of the feature + +### What is the relevance of this feature? +Why is this important? What are the use cases? What will it do once completed? + +### Feature design description: +- Explain the design of the feature with enough detail that someone familiar with the environment and framework can understand the concept and explain it to others. +- It should include at least one end-to-end example of how a developer will use it along with specific details, including outlying use cases. + +- If there is any new terminology, it should be defined here. + +### Technical design description: +- Explain the technical portion of the work in enough detail that members can implement the feature. + +- Explain any API or process changes required to implement this feature + +- This section should relate to the feature design description by reference and explain in greater detail how it makes the feature design examples work. + +- This should also provide detailed information on compatibility with different hardware platforms. + + +### What are the advantages of the feature? +- Explain the advantages for someone to use this feature + +### What are the disadvantages of the feature? +- Explain any disadvantages for someone to use this feature + +### How will this be implemented or integrated into the O3DE environment? +- Explain how a developer will integrate this into the codebase of O3DE and provide any specific library or technical stack requirements. + +### Are there any alternatives to this feature? +- Provide any other designs that have been considered. Explain what the impact might be of not doing this. +- If there is any prior art or approaches with other frameworks in the same domain, explain how they may have solved this problem or implemented this feature. + +### How will users learn this feature? +- Detail how it can be best presented and how it is used as an extension or a standalone tool used with O3DE. +- Explain if and how it may change how individuals would use the platform and if any documentation must be changed or reorganized. +- Explain how it would be taught to new and existing O3DE users. + +### Are there any open questions? +- What are some of the open questions and potential scenarios that should be considered? diff --git a/.github/ISSUE_TEMPLATE/rfc-suggestion.md b/.github/ISSUE_TEMPLATE/rfc-suggestion.md new file mode 100644 index 0000000000..7c51bbc918 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc-suggestion.md @@ -0,0 +1,61 @@ +--- +name: RFC Suggestion request +about: Create Suggestion RFC for this project +title: Proposed RFC Suggestion =description= +labels: 'rfc-suggestion' +assignees: '' + +--- + +# O3DE Suggestion RFC Template + +### When using this template, you do not have to fill out every question below. The questions are there for guidance. + +This RFC template should be used for any suggestion that is not based upon code or content related to the O3DE product itself. This template is for proposing new process models, approaches, or ideas to improve the O3DE community. + +A hastily proposed RFC can hurt its chances of acceptance. Low-quality proposals, proposals for previously rejected features, or those that do not have any substantive value to the project may be quickly rejected, demotivating the unprepared contributor. Laying some groundwork with others in the community ahead of the RFC can make the process much smoother. + +Although there is no single way to prepare for submitting an RFC, it is generally a good idea to pursue feedback from other project members beforehand. Keep in mind that you want other members to contribute and back your suggestion, which can drastically improve the chances of implementation. + +The most common preparations for writing and submitting an RFC include: +- Talking the idea over on our Discord server. +- Creating a discussion on our GitHub RFCs discussions page. +- Occasionally posting "pre-RFCs" on the GitHub RFCs discussion page. +You may file issues in the RFCs repo for discussion, but these are not actively looked at by the teams. + +As a rule of thumb, receiving encouraging feedback from long-standing community members is a good indication that the RFC is worth pursuing. + +# ----- DELETE EVERYTHING FROM THE TOP TO THE SUMMARY LINE BELOW WHEN USING TEMPLATE ----- # + +### Summary: +Single paragraph explanation of the suggestion + +### What is the motivation for this suggestion? +Why is this important? +What are the use cases for this suggestion? +What should the outcome be if this suggestion is implemented? + +### Suggestion design description: +- Explain the suggestion with enough detail that someone familiar with the process and environment of the project can understand the suggestion and explain it to others. +- It should include at least one end-to-end example of how the community will use it along with the specific details with outlying use cases. + +- If there is any new terminology, it should be defined here. + +### What are the advantages of the suggestion? +- Explain the advantages of using this suggestion + +### What are the disadvantages of the suggestion? +- Explain any disadvantages or trade-offs to using this suggestion + +### How will this be work within the O3DE project? +- Explain how this suggestion will be work within the O3DE project. + +### Are there any alternatives to this suggestion? +- Provide any other alternative ways that have been considered. +- Explain what the impact might be of not implementing this suggestion. +- If there are other similar suggestions previously used, list them and explain which parts may have solved some or all of this problem. + +### What is the strategy for adoption? +- Explain how new and existing users will adopt this suggestion. +- Point out any efforts needed if it requires coordination with multiple SIGs or other projects. +- Explain how it would be taught to new and existing O3DE users. From 6067ab38c81583fca925c61c83d3fe4a260a2012 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 28 Jun 2021 15:05:17 +0100 Subject: [PATCH 27/29] Parameterize build dir and make job explicit profile Signed-off-by: John --- scripts/build/Platform/Windows/build_config.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/build/Platform/Windows/build_config.json b/scripts/build/Platform/Windows/build_config.json index e37e467fa0..38392d4491 100644 --- a/scripts/build/Platform/Windows/build_config.json +++ b/scripts/build/Platform/Windows/build_config.json @@ -33,7 +33,7 @@ ], "steps": [ "profile_vs2019", - "test_impact_analysis", + "test_impact_analysis_profile_vs2019", "asset_profile_vs2019", "test_cpu_profile_vs2019" ] @@ -82,13 +82,15 @@ "SCRIPT_PARAMETERS": "--platform 3rdParty --type 3rdParty_all" } }, - "test_impact_analysis": { + "test_impact_analysis_profile_vs2019": { "TAGS": [ ], "COMMAND": "python_windows.cmd", "PARAMETERS": { + "OUTPUT_DIRECTORY": "build\\windows_vs2019", + "CONFIGURATION": "profile", "SCRIPT_PATH": "scripts/build/TestImpactAnalysis/tiaf_driver.py", - "SCRIPT_PARAMETERS": "--testFailurePolicy=continue --suite main --pipeline !PIPELINE_NAME! --destCommit !CHANGE_ID! --config \"build\\windows_vs2019\\bin\\TestImpactFramework\\persistent\\tiaf.profile.json\"" + "SCRIPT_PARAMETERS": "--testFailurePolicy=continue --suite main --pipeline !PIPELINE_NAME! --destCommit !CHANGE_ID! --config \"!OUTPUT_DIRECTORY!/bin/TestImpactFramework/persistent/tiaf.profile.json\"" } }, "debug_vs2019": { From ea2115089b759c25a557ae9a2381bcd0c41afa18 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 28 Jun 2021 16:43:54 +0100 Subject: [PATCH 28/29] Correct tiaf output path to use foward slash dirs Signed-off-by: John --- scripts/build/Platform/Windows/build_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/Platform/Windows/build_config.json b/scripts/build/Platform/Windows/build_config.json index 38392d4491..5499dbef84 100644 --- a/scripts/build/Platform/Windows/build_config.json +++ b/scripts/build/Platform/Windows/build_config.json @@ -87,7 +87,7 @@ ], "COMMAND": "python_windows.cmd", "PARAMETERS": { - "OUTPUT_DIRECTORY": "build\\windows_vs2019", + "OUTPUT_DIRECTORY": "build/windows_vs2019", "CONFIGURATION": "profile", "SCRIPT_PATH": "scripts/build/TestImpactAnalysis/tiaf_driver.py", "SCRIPT_PARAMETERS": "--testFailurePolicy=continue --suite main --pipeline !PIPELINE_NAME! --destCommit !CHANGE_ID! --config \"!OUTPUT_DIRECTORY!/bin/TestImpactFramework/persistent/tiaf.profile.json\"" From c145a9a74a4c9f6c45e229f419ee27112c9672ff Mon Sep 17 00:00:00 2001 From: John Date: Tue, 29 Jun 2021 19:44:28 +0100 Subject: [PATCH 29/29] Add missing files from addressing PR comments Signed-off-by: John --- .../Frontend/Console/Code/CMakeLists.txt | 3 + .../ConsoleFrontendConfig.in | 119 +----------------- .../EnumeratedGemTargets.in | 7 ++ .../LYTestImpactFramework.cmake | 73 ++++++++--- 4 files changed, 70 insertions(+), 132 deletions(-) create mode 100644 cmake/TestImpactFramework/EnumeratedGemTargets.in diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt b/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt index a26814fa51..bbc7dbde19 100644 --- a/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt @@ -19,6 +19,9 @@ ly_add_target( Include PRIVATE Source + COMPILE_DEFINITIONS + PRIVATE + ${LY_TEST_IMPACT_CONFIG_FILE_PATH_DEFINITION} BUILD_DEPENDENCIES PUBLIC AZ::TestImpact.Runtime.Static diff --git a/cmake/TestImpactFramework/ConsoleFrontendConfig.in b/cmake/TestImpactFramework/ConsoleFrontendConfig.in index 9338672fbd..ffd24ffb72 100644 --- a/cmake/TestImpactFramework/ConsoleFrontendConfig.in +++ b/cmake/TestImpactFramework/ConsoleFrontendConfig.in @@ -70,6 +70,9 @@ }, "test_target_meta": { "file": "${test_target_type_file}" + }, + "gem_target": { + "file": "${gem_target_file}" } } }, @@ -87,122 +90,6 @@ ], "shard": [ - { - "policy": "fixture_contiguous", - "target": "AzCore.Tests" - }, - { - "policy": "fixture_contiguous", - "target": "AzToolsFramework.Tests" - }, - { - "policy": "test_interleaved", - "target": "Framework.Tests" - }, - { - "policy": "test_interleaved", - "target": "LmbrCentral.Editor.Tests" - }, - { - "policy": "test_interleaved", - "target": "EditorLib.Tests" - }, - { - "policy": "test_interleaved", - "target": "PhysX.Tests" - }, - { - "policy": "test_interleaved", - "target": "ImageProcessing.Tests" - }, - { - "policy": "test_interleaved", - "target": "Atom_RPI.Tests" - }, - { - "policy": "test_interleaved", - "target": "Atom_RHI.Tests" - }, - { - "policy": "test_interleaved", - "target": "AzManipulatorFramework.Tests" - }, - { - "policy": "test_interleaved", - "target": "WhiteBox.Editor.Tests" - }, - { - "policy": "test_interleaved", - "target": "ImageProcessing.Tests" - }, - { - "policy": "test_interleaved", - "target": "AzManipulatorTestFramework.Tests" - }, - { - "policy": "test_interleaved", - "target": "AtomCore.Tests" - }, - { - "policy": "test_interleaved", - "target": "ImageProcessingAtom.Editor.Tests" - }, - { - "policy": "test_interleaved", - "target": "EditorPythonBindings.Tests" - }, - { - "policy": "test_interleaved", - "target": "Atom_Utils.Tests" - }, - { - "policy": "test_interleaved", - "target": "AudioEngineWwise.Editor.Tests" - }, - { - "policy": "test_interleaved", - "target": "Multiplayer.Tests" - }, - { - "policy": "test_interleaved", - "target": "LmbrCentral.Tests" - }, - { - "policy": "fixture_contiguous", - "target": "LyMetricsShared.Tests" - }, - { - "policy": "test_interleaved", - "target": "PhysX.Editor.Tests" - }, - { - "policy": "test_interleaved", - "target": "ComponentEntityEditorPlugin.Tests" - }, - { - "policy": "test_interleaved", - "target": "DeltaCataloger.Tests" - }, - { - "policy": "test_interleaved", - "target": "GradientSignal.Tests" - }, - { - "policy": "test_interleaved", - "target": "LyShine.Tests" - }, - { - "policy": "test_interleaved", - "target": "EMotionFX.Editor.Tests" - }, - { - "policy": "test_interleaved", - "target": "EMotionFX.Tests" - }, - { - "policy": "test_interleaved", - "target": "CrySystem.Tests" - } ] } } diff --git a/cmake/TestImpactFramework/EnumeratedGemTargets.in b/cmake/TestImpactFramework/EnumeratedGemTargets.in new file mode 100644 index 0000000000..183e8ff3bd --- /dev/null +++ b/cmake/TestImpactFramework/EnumeratedGemTargets.in @@ -0,0 +1,7 @@ +{ + "gems": { + [ +${enumerated_gem_targets} + ] + } +} \ No newline at end of file diff --git a/cmake/TestImpactFramework/LYTestImpactFramework.cmake b/cmake/TestImpactFramework/LYTestImpactFramework.cmake index bdd580f37e..a1114ea77d 100644 --- a/cmake/TestImpactFramework/LYTestImpactFramework.cmake +++ b/cmake/TestImpactFramework/LYTestImpactFramework.cmake @@ -24,12 +24,15 @@ set(LY_TEST_IMPACT_PYTHON_COVERAGE_STATIC_TARGET "PythonCoverage.Editor.Static") # Name of test impact framework console target set(LY_TEST_IMPACT_CONSOLE_TARGET "TestImpact.Frontend.Console") -# Directory for non-persistent test impact data trashed with each generation of build system +# Directory for test impact artifacts and data set(LY_TEST_IMPACT_WORKING_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/TestImpactFramework") -# Directory for temporary files generated at runtime +# Directory for artifacts generated at runtime set(LY_TEST_IMPACT_TEMP_DIR "${LY_TEST_IMPACT_WORKING_DIR}/Temp") +# Directory for files that persist between runtime runs +set(LY_TEST_IMPACT_PERSISTENT_DIR "${LY_TEST_IMPACT_WORKING_DIR}/Persistent") + # Directory for static artifacts produced as part of the build system generation process set(LY_TEST_IMPACT_ARTIFACT_DIR "${LY_TEST_IMPACT_WORKING_DIR}/Artifact") @@ -39,9 +42,18 @@ set(LY_TEST_IMPACT_SOURCE_TARGET_MAPPING_DIR "${LY_TEST_IMPACT_ARTIFACT_DIR}/Map # Directory for build target dependency/depender graphs set(LY_TEST_IMPACT_TARGET_DEPENDENCY_DIR "${LY_TEST_IMPACT_ARTIFACT_DIR}/Dependency") -# Master test enumeration file for all test types +# Main test enumeration file for all test types set(LY_TEST_IMPACT_TEST_TYPE_FILE "${LY_TEST_IMPACT_ARTIFACT_DIR}/TestType/All.tests") +# Main gem target file for all shared library gems +set(LY_TEST_IMPACT_GEM_TARGET_FILE "${LY_TEST_IMPACT_ARTIFACT_DIR}/BuildType/All.gems") + +# Path to the config file for each build configuration +set(LY_TEST_IMPACT_CONFIG_FILE_PATH "${LY_TEST_IMPACT_PERSISTENT_DIR}/tiaf.$.json") + +# Preprocessor directive for the config file path +set(LY_TEST_IMPACT_CONFIG_FILE_PATH_DEFINITION "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"${LY_TEST_IMPACT_CONFIG_FILE_PATH}\"") + #! ly_test_impact_rebase_file_to_repo_root: rebases the relative and/or absolute path to be relative to repo root directory and places the resulting path in quotes. # # \arg:INPUT_FILE the file to rebase @@ -215,7 +227,7 @@ function(ly_test_impact_extract_python_test_params COMPOSITE_TEST COMPOSITE_SUIT set(${TEST_SUITES} ${test_suites} PARENT_SCOPE) endfunction() -#! ly_test_impact_write_test_enumeration_file: exports the master test lists to file. +#! ly_test_impact_write_test_enumeration_file: exports the main test list to file. # # \arg:TEST_ENUMERATION_TEMPLATE_FILE path to test enumeration template file function(ly_test_impact_write_test_enumeration_file TEST_ENUMERATION_TEMPLATE_FILE) @@ -265,6 +277,33 @@ function(ly_test_impact_write_test_enumeration_file TEST_ENUMERATION_TEMPLATE_FI configure_file(${TEST_ENUMERATION_TEMPLATE_FILE} ${LY_TEST_IMPACT_TEST_TYPE_FILE}) endfunction() +#! ly_test_impact_write_gem_target_enumeration_file: exports the main gem target list to file. +# +# \arg:GEM_TARGET_TEMPLATE_FILE path to source to gem target template file +function(ly_test_impact_write_gem_target_enumeration_file GEM_TARGET_TEMPLATE_FILE) + get_property(LY_ALL_TARGETS GLOBAL PROPERTY LY_ALL_TARGETS) + + set(enumerated_gem_targets "") + # Walk the build targets + foreach(aliased_target ${LY_ALL_TARGETS}) + + unset(target) + ly_de_alias_target(${aliased_target} target) + + get_target_property(gem_module ${target} GEM_MODULE) + get_target_property(target_type ${target} TYPE) + if("${gem_module}" STREQUAL "TRUE") + if("${target_type}" STREQUAL "SHARED_LIBRARY" OR "${target_type}" STREQUAL "MODULE_LIBRARY") + list(APPEND enumerated_gem_targets " \"${target}\"") + endif() + endif() + endforeach() + string (REPLACE ";" ",\n" enumerated_gem_targets "${enumerated_gem_targets}") + # Write out source to target mapping file + set(mapping_path "${LY_TEST_IMPACT_GEM_TARGET_FILE}") + configure_file(${GEM_TARGET_TEMPLATE_FILE} ${mapping_path}) +endfunction() + #! ly_test_impact_export_source_target_mappings: exports the static source to target mappings to file. # # \arg:MAPPING_TEMPLATE_FILE path to source to target template file @@ -276,6 +315,7 @@ function(ly_test_impact_export_source_target_mappings MAPPING_TEMPLATE_FILE) unset(target) ly_de_alias_target(${aliased_target} target) + message(TRACE "Exporting static source file mappings for ${target}") # Target name and path relative to root @@ -336,9 +376,8 @@ endfunction() #! ly_test_impact_write_config_file: writes out the test impact framework config file using the data derived from the build generation process. # # \arg:CONFIG_TEMPLATE_FILE path to the runtime configuration template file -# \arg:PERSISTENT_DATA_DIR path to the test impact framework persistent data directory # \arg:BIN_DIR path to repo binary output directory -function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE PERSISTENT_DATA_DIR BIN_DIR) +function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE BIN_DIR) # Platform this config file is being generated for set(platform ${PAL_PLATFORM_NAME}) @@ -369,16 +408,19 @@ function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE PERSISTENT_DATA_D set(temp_dir "${LY_TEST_IMPACT_TEMP_DIR}") # Active persistent data dir - set(active_dir "${PERSISTENT_DATA_DIR}/active") + set(active_dir "${LY_TEST_IMPACT_PERSISTENT_DIR}/active") # Historic persistent data dir - set(historic_dir "${PERSISTENT_DATA_DIR}/historic") + set(historic_dir "${LY_TEST_IMPACT_PERSISTENT_DIR}/historic") # Source to target mappings dir set(source_target_mapping_dir "${LY_TEST_IMPACT_SOURCE_TARGET_MAPPING_DIR}") # Test type artifact file set(test_target_type_file "${LY_TEST_IMPACT_TEST_TYPE_FILE}") + + # Gem target file + set(gem_target_file "${LY_TEST_IMPACT_GEM_TARGET_FILE}") # Build dependency artifact dir set(target_dependency_dir "${LY_TEST_IMPACT_TARGET_DEPENDENCY_DIR}") @@ -392,13 +434,10 @@ function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE PERSISTENT_DATA_D # Write out entire config contents to a file in the build directory of the test impact framework console target file(GENERATE - OUTPUT "${PERSISTENT_DATA_DIR}/$.$.json" + OUTPUT "${LY_TEST_IMPACT_CONFIG_FILE_PATH}" CONTENT ${config_file} ) - # Set the above config file as the default config file to use for the test impact framework console target - target_compile_definitions(${LY_TEST_IMPACT_CONSOLE_STATIC_TARGET} PUBLIC "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"${PERSISTENT_DATA_DIR}/$.$.json\"") - target_compile_definitions(${LY_TEST_IMPACT_PYTHON_COVERAGE_STATIC_TARGET} PRIVATE "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"${PERSISTENT_DATA_DIR}/$.$.json\"") message(DEBUG "Test impact framework post steps complete") endfunction() @@ -408,13 +447,11 @@ function(ly_test_impact_post_step) return() endif() - # Directory per build config for persistent test impact data (to be checked in) - set(persistent_data_dir "${LY_TEST_IMPACT_WORKING_DIR}/persistent") # Directory for binaries built for this profile set(bin_dir "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") # Erase any existing non-persistent data to avoid getting test impact framework out of sync with current repo state - file(REMOVE_RECURSE "${LY_TEST_IMPACT_WORKING_DIR}") + file(REMOVE_RECURSE "${LY_TEST_IMPACT_TEMP_DIR}") # Export the soruce to target mapping files ly_test_impact_export_source_target_mappings( @@ -426,10 +463,14 @@ function(ly_test_impact_post_step) "cmake/TestImpactFramework/EnumeratedTests.in" ) + # Export the enumerated gems + ly_test_impact_write_gem_target_enumeration_file( + "cmake/TestImpactFramework/EnumeratedGemTargets.in" + ) + # Write out the configuration file ly_test_impact_write_config_file( "cmake/TestImpactFramework/ConsoleFrontendConfig.in" - ${persistent_data_dir} ${bin_dir} )