adding the testing files for testing for python asset building and scripting
the scene_api gets a small update for mesh_group_add_advanced_coordinate_system(self,main
parent
0997a2cfbf
commit
f0cf27b8d3
@ -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.
|
||||||
|
"""
|
||||||
@ -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.
|
||||||
|
"""
|
||||||
|
import sys, os
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../../PythonTests')
|
||||||
|
from PythonAssetBuilder import bootstrap_tests
|
||||||
@ -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.
|
||||||
|
"""
|
||||||
|
#
|
||||||
|
# This launches the AssetProcessor and Editor then attempts to find the expected
|
||||||
|
# assets created by a Python Asset Builder and the output of a scene pipeline script
|
||||||
|
#
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
pytest.importorskip('ly_test_tools')
|
||||||
|
|
||||||
|
import ly_test_tools.environment.file_system as file_system
|
||||||
|
import ly_test_tools.log.log_monitor
|
||||||
|
import ly_test_tools.environment.waiter as waiter
|
||||||
|
|
||||||
|
@pytest.mark.SUITE_sandbox
|
||||||
|
@pytest.mark.parametrize('launcher_platform', ['windows_editor'])
|
||||||
|
@pytest.mark.parametrize('project', ['AutomatedTesting'])
|
||||||
|
@pytest.mark.parametrize('level', ['auto_test'])
|
||||||
|
class TestPythonAssetProcessing(object):
|
||||||
|
def test_DetectPythonCreatedAsset(self, request, editor, level, launcher_platform):
|
||||||
|
unexpected_lines = []
|
||||||
|
expected_lines = [
|
||||||
|
'Mock asset exists',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_negative.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_positive.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found',
|
||||||
|
'Expected subId for asset (gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel) found'
|
||||||
|
]
|
||||||
|
timeout = 180
|
||||||
|
halt_on_unexpected = False
|
||||||
|
test_directory = os.path.join(os.path.dirname(__file__))
|
||||||
|
testFile = os.path.join(test_directory, 'AssetBuilder_test_case.py')
|
||||||
|
editor.args.extend(['-NullRenderer', "--skipWelcomeScreenDialog", "--autotest_mode", "--runpythontest", testFile])
|
||||||
|
|
||||||
|
with editor.start():
|
||||||
|
editorlog_file = os.path.join(editor.workspace.paths.project_log(), 'Editor.log')
|
||||||
|
log_monitor = ly_test_tools.log.log_monitor.LogMonitor(editor, editorlog_file)
|
||||||
|
waiter.wait_for(
|
||||||
|
lambda: editor.is_alive(),
|
||||||
|
timeout,
|
||||||
|
exc=("Log file '{}' was never opened by another process.".format(editorlog_file)),
|
||||||
|
interval=1)
|
||||||
|
log_monitor.monitor_log_for_lines(expected_lines, unexpected_lines, halt_on_unexpected, timeout)
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||||
|
its licensors.
|
||||||
|
|
||||||
|
For complete copyright and license terms please see the LICENSE at the root of this
|
||||||
|
distribution (the "License"). All use of this software is governed by the License,
|
||||||
|
or, if provided, by the license below or the license accompanying this file. Do not
|
||||||
|
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
"""
|
||||||
|
import azlmbr.bus
|
||||||
|
import azlmbr.asset
|
||||||
|
import azlmbr.editor
|
||||||
|
import azlmbr.math
|
||||||
|
import azlmbr.legacy.general
|
||||||
|
|
||||||
|
def raise_and_stop(msg):
|
||||||
|
print (msg)
|
||||||
|
azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
|
||||||
|
|
||||||
|
# These tests are meant to check that the test_asset.mock source asset turned into
|
||||||
|
# a test_asset.mock_asset product asset via the Python asset builder system
|
||||||
|
mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0)
|
||||||
|
mockAssetPath = 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset'
|
||||||
|
assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', mockAssetPath, mockAssetType, False)
|
||||||
|
if (assetId.is_valid() is False):
|
||||||
|
raise_and_stop(f'Mock AssetId is not valid!')
|
||||||
|
|
||||||
|
if (assetId.to_string().endswith(':54c06b89') is False):
|
||||||
|
raise_and_stop(f'Mock AssetId has unexpected sub-id for {mockAssetPath}!')
|
||||||
|
|
||||||
|
print ('Mock asset exists')
|
||||||
|
|
||||||
|
# These tests detect if the geom_group.fbx file turns into a number of azmodel product assets
|
||||||
|
def test_azmodel_product(generatedModelAssetPath, expectedSubId):
|
||||||
|
azModelAssetType = azlmbr.math.Uuid_CreateString('{2C7477B6-69C5-45BE-8163-BCD6A275B6D8}', 0)
|
||||||
|
assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', generatedModelAssetPath, azModelAssetType, False)
|
||||||
|
assetIdString = assetId.to_string()
|
||||||
|
if (assetIdString.endswith(':' + expectedSubId) is False):
|
||||||
|
raise_and_stop(f'Asset has unexpected asset ID ({assetIdString}) for ({generatedModelAssetPath})!')
|
||||||
|
else:
|
||||||
|
print(f'Expected subId for asset ({generatedModelAssetPath}) found')
|
||||||
|
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center.azmodel', '10412075')
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_positive.azmodel', '10d16e68')
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_X_negative.azmodel', '10a71973')
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Y_positive.azmodel', '10130556')
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Y_negative.azmodel', '1065724d')
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Z_positive.azmodel', '1024be55')
|
||||||
|
test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_Z_negative.azmodel', '1052c94e')
|
||||||
|
|
||||||
|
azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
|
||||||
@ -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.
|
||||||
|
"""
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import mock_asset_builder
|
||||||
|
except:
|
||||||
|
print ('skipping asset builder testing via mock_asset_builder')
|
||||||
@ -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.
|
||||||
|
"""
|
||||||
|
import uuid, os
|
||||||
|
import azlmbr.scene as sceneApi
|
||||||
|
import azlmbr.scene.graph
|
||||||
|
from scene_api import scene_data as sceneData
|
||||||
|
|
||||||
|
def get_mesh_node_names(sceneGraph):
|
||||||
|
meshDataList = []
|
||||||
|
node = sceneGraph.get_root()
|
||||||
|
children = []
|
||||||
|
|
||||||
|
while node.IsValid():
|
||||||
|
# store children to process after siblings
|
||||||
|
if sceneGraph.has_node_child(node):
|
||||||
|
children.append(sceneGraph.get_node_child(node))
|
||||||
|
|
||||||
|
# store any node that has mesh data content
|
||||||
|
nodeContent = sceneGraph.get_node_content(node)
|
||||||
|
if nodeContent is not None and nodeContent.CastWithTypeName('MeshData'):
|
||||||
|
if sceneGraph.is_node_end_point(node) is False:
|
||||||
|
meshDataList.append(sceneData.SceneGraphName(sceneGraph.get_node_name(node)))
|
||||||
|
|
||||||
|
# advance to next node
|
||||||
|
if sceneGraph.has_node_sibling(node):
|
||||||
|
node = sceneGraph.get_node_sibling(node)
|
||||||
|
elif children:
|
||||||
|
node = children.pop()
|
||||||
|
else:
|
||||||
|
node = azlmbr.scene.graph.NodeIndex()
|
||||||
|
|
||||||
|
return meshDataList
|
||||||
|
|
||||||
|
def update_manifest(scene):
|
||||||
|
graph = sceneData.SceneGraph(scene.graph)
|
||||||
|
meshNameList = get_mesh_node_names(graph)
|
||||||
|
sceneManifest = sceneData.SceneManifest()
|
||||||
|
sourceFilenameOnly = os.path.basename(scene.sourceFilename)
|
||||||
|
sourceFilenameOnly = sourceFilenameOnly.replace('.','_')
|
||||||
|
|
||||||
|
for activeMeshIndex in range(len(meshNameList)):
|
||||||
|
chunkName = meshNameList[activeMeshIndex]
|
||||||
|
chunkPath = chunkName.get_path()
|
||||||
|
meshGroupName = '{}_{}'.format(sourceFilenameOnly, chunkName.get_name())
|
||||||
|
meshGroup = sceneManifest.add_mesh_group(meshGroupName)
|
||||||
|
meshGroup['id'] = '{' + str(uuid.uuid5(uuid.NAMESPACE_DNS, sourceFilenameOnly + chunkPath)) + '}'
|
||||||
|
sceneManifest.mesh_group_add_comment(meshGroup, 'auto generated by scene manifest')
|
||||||
|
sceneManifest.mesh_group_add_advanced_coordinate_system(meshGroup, None, None, None, 1.0)
|
||||||
|
|
||||||
|
# create selection node list
|
||||||
|
pathSet = set()
|
||||||
|
for meshIndex in range(len(meshNameList)):
|
||||||
|
targetPath = meshNameList[meshIndex].get_path()
|
||||||
|
if (activeMeshIndex == meshIndex):
|
||||||
|
sceneManifest.mesh_group_select_node(meshGroup, targetPath)
|
||||||
|
else:
|
||||||
|
if targetPath not in pathSet:
|
||||||
|
pathSet.update(targetPath)
|
||||||
|
sceneManifest.mesh_group_unselect_node(meshGroup, targetPath)
|
||||||
|
|
||||||
|
return sceneManifest.export()
|
||||||
|
|
||||||
|
mySceneJobHandler = None
|
||||||
|
|
||||||
|
def on_update_manifest(args):
|
||||||
|
scene = args[0]
|
||||||
|
result = update_manifest(scene)
|
||||||
|
global mySceneJobHandler
|
||||||
|
mySceneJobHandler.disconnect()
|
||||||
|
mySceneJobHandler = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global mySceneJobHandler
|
||||||
|
mySceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
|
||||||
|
mySceneJobHandler.connect()
|
||||||
|
mySceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:66d38948309ef273adf74b63eaa38f8fc2e2bdfbab3933d2ee082ce6a8cb108e
|
||||||
|
size 30496
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"values":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"$type": "ScriptProcessorRule",
|
||||||
|
"scriptFilename": "Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
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 azlmbr.asset
|
||||||
|
import azlmbr.asset.builder
|
||||||
|
import azlmbr.bus
|
||||||
|
import azlmbr.math
|
||||||
|
import os, traceback, binascii, sys
|
||||||
|
|
||||||
|
jobKeyName = 'Mock Asset'
|
||||||
|
|
||||||
|
def log_exception_traceback():
|
||||||
|
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||||
|
data = traceback.format_exception(exc_type, exc_value, exc_tb)
|
||||||
|
print(str(data))
|
||||||
|
|
||||||
|
# creates a single job to compile for each platform
|
||||||
|
def create_jobs(request):
|
||||||
|
# create job descriptor for each platform
|
||||||
|
jobDescriptorList = []
|
||||||
|
for platformInfo in request.enabledPlatforms:
|
||||||
|
jobDesc = azlmbr.asset.builder.JobDescriptor()
|
||||||
|
jobDesc.jobKey = jobKeyName
|
||||||
|
jobDesc.set_platform_identifier(platformInfo.identifier)
|
||||||
|
jobDescriptorList.append(jobDesc)
|
||||||
|
|
||||||
|
response = azlmbr.asset.builder.CreateJobsResponse()
|
||||||
|
response.result = azlmbr.asset.builder.CreateJobsResponse_ResultSuccess
|
||||||
|
response.createJobOutputs = jobDescriptorList
|
||||||
|
return response
|
||||||
|
|
||||||
|
def on_create_jobs(args):
|
||||||
|
try:
|
||||||
|
request = args[0]
|
||||||
|
return create_jobs(request)
|
||||||
|
except:
|
||||||
|
log_exception_traceback()
|
||||||
|
# returing back a default CreateJobsResponse() records an asset error
|
||||||
|
return azlmbr.asset.builder.CreateJobsResponse()
|
||||||
|
|
||||||
|
def process_file(request):
|
||||||
|
# prepare output folder
|
||||||
|
basePath, _ = os.path.split(request.sourceFile)
|
||||||
|
outputPath = os.path.join(request.tempDirPath, basePath)
|
||||||
|
os.makedirs(outputPath, exist_ok=True)
|
||||||
|
|
||||||
|
# write out a mock file
|
||||||
|
basePath, sourceFile = os.path.split(request.sourceFile)
|
||||||
|
mockFilename = os.path.splitext(sourceFile)[0] + '.mock_asset'
|
||||||
|
mockFilename = os.path.join(basePath, mockFilename)
|
||||||
|
mockFilename = mockFilename.replace('\\', '/').lower()
|
||||||
|
tempFilename = os.path.join(request.tempDirPath, mockFilename)
|
||||||
|
|
||||||
|
# write out a tempFilename like a JSON or something?
|
||||||
|
fileOutput = open(tempFilename, "w")
|
||||||
|
fileOutput.write('{}')
|
||||||
|
fileOutput.close()
|
||||||
|
|
||||||
|
# generate a product asset file entry
|
||||||
|
subId = binascii.crc32(mockFilename.encode())
|
||||||
|
mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0)
|
||||||
|
product = azlmbr.asset.builder.JobProduct(mockFilename, mockAssetType, subId)
|
||||||
|
product.dependenciesHandled = True
|
||||||
|
productOutputs = []
|
||||||
|
productOutputs.append(product)
|
||||||
|
|
||||||
|
# fill out response object
|
||||||
|
response = azlmbr.asset.builder.ProcessJobResponse()
|
||||||
|
response.outputProducts = productOutputs
|
||||||
|
response.resultCode = azlmbr.asset.builder.ProcessJobResponse_Success
|
||||||
|
response.dependenciesHandled = True
|
||||||
|
return response
|
||||||
|
|
||||||
|
# using the incoming 'request' find the type of job via 'jobKey' to determine what to do
|
||||||
|
def on_process_job(args):
|
||||||
|
try:
|
||||||
|
request = args[0]
|
||||||
|
if (request.jobDescription.jobKey.startswith(jobKeyName)):
|
||||||
|
return process_file(request)
|
||||||
|
except:
|
||||||
|
log_exception_traceback()
|
||||||
|
# returning back an empty ProcessJobResponse() will record an error
|
||||||
|
return azlmbr.asset.builder.ProcessJobResponse()
|
||||||
|
|
||||||
|
# register asset builder
|
||||||
|
def register_asset_builder(busId):
|
||||||
|
assetPattern = azlmbr.asset.builder.AssetBuilderPattern()
|
||||||
|
assetPattern.pattern = '*.mock'
|
||||||
|
assetPattern.type = azlmbr.asset.builder.AssetBuilderPattern_Wildcard
|
||||||
|
|
||||||
|
builderDescriptor = azlmbr.asset.builder.AssetBuilderDesc()
|
||||||
|
builderDescriptor.name = "Mock Builder"
|
||||||
|
builderDescriptor.patterns = [assetPattern]
|
||||||
|
builderDescriptor.busId = busId
|
||||||
|
builderDescriptor.version = 1
|
||||||
|
|
||||||
|
outcome = azlmbr.asset.builder.PythonAssetBuilderRequestBus(azlmbr.bus.Broadcast, 'RegisterAssetBuilder', builderDescriptor)
|
||||||
|
if outcome.IsSuccess():
|
||||||
|
# created the asset builder to hook into the notification bus
|
||||||
|
handler = azlmbr.asset.builder.PythonBuilderNotificationBusHandler()
|
||||||
|
handler.connect(busId)
|
||||||
|
handler.add_callback('OnCreateJobsRequest', on_create_jobs)
|
||||||
|
handler.add_callback('OnProcessJobRequest', on_process_job)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
# create the asset builder handler
|
||||||
|
busIdString = '{CF5C74C1-9ED4-5851-95B1-0B15090DBEC7}'
|
||||||
|
busId = azlmbr.math.Uuid_CreateString(busIdString, 0)
|
||||||
|
handler = None
|
||||||
|
try:
|
||||||
|
handler = register_asset_builder(busId)
|
||||||
|
except:
|
||||||
|
handler = None
|
||||||
|
log_exception_traceback()
|
||||||
@ -0,0 +1 @@
|
|||||||
|
mock data
|
||||||
Loading…
Reference in New Issue