Merge pull request #1648 from aws-lumberyard-dev/TIF/Runtime

Tif/runtime
monroegm-disable-blank-issue-2
jonawals 5 years ago committed by GitHub
commit 9e85caa352
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,3 +11,4 @@
add_subdirectory(Code)
add_subdirectory(PythonTests)
add_subdirectory(PythonCoverage)

@ -55,4 +55,5 @@ set(ENABLED_GEMS
AWSCore
AWSClientAuth
AWSMetrics
PythonCoverage
)

@ -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)

@ -0,0 +1,62 @@
#
# 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.
#
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})
include(${pal_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake)
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_CONFIG_FILE_PATH_DEFINITION}
BUILD_DEPENDENCIES
PUBLIC
AZ::AzToolsFramework
RUNTIME_DEPENDENCIES
Gem::EditorPythonBindings.Editor
)
ly_add_target(
NAME PythonCoverage.Editor GEM_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
)
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()

@ -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.
#
set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE)

@ -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.
#
set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE)

@ -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.
#
set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE)

@ -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.
#
set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED TRUE)

@ -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.
#
set(PAL_TRAIT_PYTHONCOVERAGE_SUPPORTED FALSE)

@ -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 "PythonCoverageEditorModule.h"
#include "PythonCoverageEditorSystemComponent.h"
namespace PythonCoverage
{
AZ_CLASS_ALLOCATOR_IMPL(PythonCoverageEditorModule, AZ::SystemAllocator, 0)
PythonCoverageEditorModule::PythonCoverageEditorModule()
{
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<PythonCoverageEditorSystemComponent>() };
}
} // namespace PythonCoverage
AZ_DECLARE_MODULE_CLASS(Gem_PythonCoverageEditor, PythonCoverage::PythonCoverageEditorModule)

@ -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 <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Module/Module.h>
namespace PythonCoverage
{
class PythonCoverageEditorModule
: public AZ::Module
{
public:
AZ_CLASS_ALLOCATOR_DECL
AZ_RTTI(PythonCoverageEditorModule, "{32C0FFEA-09A7-460F-9257-5BDEF74FCD5B}", AZ::Module);
PythonCoverageEditorModule();
~PythonCoverageEditorModule();
// PythonCoverageModule ...
AZ::ComponentTypeList GetRequiredSystemComponents() const override;
};
} // namespace WhiteBox

@ -0,0 +1,246 @@
/*
* 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 <AzCore/IO/Path/Path.h>
#include <AzCore/JSON/document.h>
#include <AzCore/Module/ModuleManagerBus.h>
#include <AzCore/Module/Module.h>
#include <AzCore/Module/DynamicModuleHandle.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <PythonCoverageEditorSystemComponent.h>
namespace PythonCoverage
{
static constexpr char* const LogCallSite = "PythonCoverageEditorSystemComponent";
void PythonCoverageEditorSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<PythonCoverageEditorSystemComponent, AZ::Component>()->Version(1);
}
}
void PythonCoverageEditorSystemComponent::Activate()
{
AzToolsFramework::EditorPythonScriptNotificationsBus::Handler::BusConnect();
AZ::EntitySystemBus::Handler::BusConnect();
// If no output directory discovered, coverage gathering will be disabled
if (ParseCoverageOutputDirectory() == CoverageState::Disabled)
{
return;
}
EnumerateAllModuleComponents();
}
void PythonCoverageEditorSystemComponent::Deactivate()
{
AZ::EntitySystemBus::Handler::BusDisconnect();
AzToolsFramework::EditorPythonScriptNotificationsBus::Handler::BusDisconnect();
}
void PythonCoverageEditorSystemComponent::OnEntityActivated(const AZ::EntityId& entityId)
{
if (m_coverageState == CoverageState::Disabled)
{
return;
}
EnumerateComponentsForEntity(entityId);
// 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();
}
}
PythonCoverageEditorSystemComponent::CoverageState PythonCoverageEditorSystemComponent::ParseCoverageOutputDirectory()
{
m_coverageState = CoverageState::Disabled;
const AZStd::string configFilePath = LY_TEST_IMPACT_DEFAULT_CONFIG_FILE;
if (configFilePath.empty())
{
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(LogCallSite, false, "Test impact analysis framework config file '%s' does not exist", configFilePath.c_str());
return m_coverageState;
}
AZStd::vector<char> buffer(fileSize + 1);
buffer[fileSize] = '\0';
if (!AZ::IO::SystemFile::Read(configFilePath.c_str(), buffer.data()))
{
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(LogCallSite, false, "Could not parse test impact analysis framework config file data, JSON has errors");
return m_coverageState;
}
const auto& tempConfig = configurationFile["workspace"]["temp"];
// Temp 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;
return m_coverageState;
}
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);
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<char> 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(LogCallSite, false, "Couldn't open file '%s' for writing", m_coverageFile.c_str());
return;
}
if (!file.Write(bytes.data(), bytes.size()))
{
AZ_Error(LogCallSite, 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)
{
// We can only enumerate shared libs, static libs are invisible to us
if (moduleData.GetDynamicModuleHandle())
{
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<AZStd::string> PythonCoverageEditorSystemComponent::GetParentComponentModulesForAllActivatedEntities(
const AZStd::unordered_map<AZ::Uuid, AZ::ComponentDescriptor*>& entityComponents) const
{
AZStd::unordered_set<AZStd::string> 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::OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector<AZStd::string_view>& 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(LogCallSite, 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

@ -0,0 +1,88 @@
/*
* 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 <AzCore/Component/Component.h>
#include <AzCore/Component/EntityBus.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/std/optional.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/API/EditorPythonScriptNotificationsBus.h>
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:
//! 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;
// AZ::EntitySystemBus overrides...
void OnEntityActivated(const AZ::EntityId& entityId) override;
// AZ::EditorPythonScriptNotificationsBus ...
void OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector<AZStd::string_view>& args) override;
//! 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.
AZStd::unordered_set<AZStd::string> GetParentComponentModulesForAllActivatedEntities(
const AZStd::unordered_map<AZ::Uuid, AZ::ComponentDescriptor*>& entityComponents) const;
//! Writes the current coverage data snapshot to disk.
void WriteCoverageFile();
CoverageState m_coverageState = CoverageState::Disabled; //!< Current coverage state.
AZStd::unordered_map<AZStd::string, AZStd::unordered_map<AZ::Uuid, AZ::ComponentDescriptor*>> m_entityComponentMap; //!< Map of
//!< component IDs to component descriptors for all activated entities, organized by test cases.
AZStd::unordered_map<AZ::Uuid, AZStd::string> 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

@ -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.
#
set(FILES
Source/PythonCoverageEditorSystemComponent.cpp
Source/PythonCoverageEditorSystemComponent.h
)

@ -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.
#
set(FILES
Source/PythonCoverageEditorModule.cpp
Source/PythonCoverageEditorModule.h
)

@ -0,0 +1,13 @@
{
"gem_name": "PythonCoverage",
"display_name": "PythonCoverage",
"summary": "A tool for generating gem coverage for Python tests.",
"canonical_tags": [
"Gem"
],
"user_tags": [
"PythonCoverage"
],
"icon_path": "preview.png",
"restricted_name": "gems"
}

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d6204c6730e5675791765ca194e9b1cbec282208e280507de830afc2805e5fa
size 41127

@ -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:

@ -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:

@ -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<AZStd::string_view>& args) { AZ_UNUSED(filename); AZ_UNUSED(args); }
virtual void ExecuteByFilenameWithArgs(
[[maybe_unused]] AZStd::string_view filename, [[maybe_unused]] const AZStd::vector<AZStd::string_view>& args) {}
//! executes a Python script as a test
virtual void ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& 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<AZStd::string_view>& args)
{
}
};
using EditorPythonRunnerRequestBus = AZ::EBus<EditorPythonRunnerRequests>;

@ -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 <AzCore/EBus/EBus.h>
#include <AzCore/std/string/string.h>
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 start of execution of a Python script using a string.
virtual void OnStartExecuteByString([[maybe_unused]] AZStd::string_view script) {}
//! Notifies the start of execution of a Python script using a filename.
virtual void OnStartExecuteByFilename([[maybe_unused]] AZStd::string_view filename) {}
//! 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<AZStd::string_view>& args) {}
//! 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<AZStd::string_view>& args) {}
};
using EditorPythonScriptNotificationsBus = AZ::EBus<EditorPythonScriptNotifications>;
}

@ -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

@ -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<std::pair<CommandLineStringOption, QString&> > 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);

@ -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

@ -39,6 +39,7 @@
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/API/EditorPythonConsoleBus.h>
#include <AzToolsFramework/API/EditorPythonScriptNotificationsBus.h>
namespace Platform
{
@ -579,6 +580,9 @@ namespace EditorPythonBindings
if (!script.empty())
{
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByString, script);
// Acquire GIL before calling Python code
AZStd::lock_guard<decltype(m_lock)> lock(m_lock);
pybind11::gil_scoped_acquire acquire;
@ -639,11 +643,15 @@ namespace EditorPythonBindings
void PythonSystemComponent::ExecuteByFilename(AZStd::string_view filename)
{
AZStd::vector<AZStd::string_view> args;
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilename, filename);
ExecuteByFilenameWithArgs(filename, args);
}
void PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
void PythonSystemComponent::ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector<AZStd::string_view>& args)
{
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameAsTest, 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<AZStd::string_view>& args)
{
AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
&AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameWithArgs, filename, args);
EvaluateFile(filename, args);
}

@ -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<AZStd::string_view>& args) override;
void ExecuteByFilenameAsTest(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args) override;
void ExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, const AZStd::vector<AZStd::string_view>& args) override;
////////////////////////////////////////////////////////////////////////
private:

@ -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"
}
]
}
}

@ -0,0 +1,7 @@
{
"gems": {
[
${enumerated_gem_targets}
]
}
}

@ -18,15 +18,21 @@ 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")
# 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")
@ -36,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.$<CONFIG>.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
@ -212,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)
@ -262,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
@ -273,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
@ -333,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})
@ -366,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}")
@ -389,12 +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}/$<TARGET_FILE_BASE_NAME:${LY_TEST_IMPACT_CONSOLE_TARGET}>.$<CONFIG>.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}/$<TARGET_FILE_BASE_NAME:${LY_TEST_IMPACT_CONSOLE_TARGET}>.$<CONFIG>.json\"")
message(DEBUG "Test impact framework post steps complete")
endfunction()
@ -404,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}/$<CONFIG>")
# 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(
@ -422,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}
)

@ -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": {

Loading…
Cancel
Save