Files
UnrealEngineUWP/Engine/Source/Developer/AutomationController/Private/AutomationControllerManger.cpp
Andrew Grant c60ffc4e26 Copying //UE4/Orion-Staging to //UE4/Main (Source: //Orion/Dev-General @ 3316439)
#lockdown Nick.Penwarden

Change 3315047 on 2017/02/21 by Mieszko.Zielinski@mieszko.zielinski_T4675_Orion

	HTN code first check in #UE4

	#rb none
	#test currently unused

Change 3314042 on 2017/02/21 by Jason.Bestimt@Jason.Bestimt_Dev-General

	#ORION_DG - DAILY Main @ CL 3313484

	#RB:none
	#Tests:none

Change 3313355 on 2017/02/20 by Uriel.Doyon@uriel.doyon_PC2_Orion

	Changed the preliminary GPU benchmark workloads to take into account the target workload.
	This is to prevent running the last test with poor performance, risking a driver reset.
	#jira OR-29915
	#rb marcus.wassmer
	#test Run the game triggering benchmarks

Change 3312553 on 2017/02/20 by Mieszko.Zielinski@mieszko.zielinski_T4675_Orion

	Implemented a simple AITask for running EQS queries #UE4

	#rb Lukasz.Furman
	#test golden path

Change 3311661 on 2017/02/20 by Jason.Bestimt@Jason.Bestimt_Dev-General

	#ORION_DG - Merge MAIN @ CL 3311631

	#RB:none
	#Tests:none

Change 3310392 on 2017/02/17 by Daniel.Lamb@daniel.lamb_T3905_6612

	Unreal pak now outputs to named log files instead of timestamps.
	#rb Trivial
	#test Cook deploy paragon
	#jira OR-36057

Change 3310196 on 2017/02/17 by Clayton.Langford@RDU-WD-8359_3635_Paragon_DevGen

	Created an event to be fired whenever a GameplayCue is routed that passes all relevant info about that GC. Added a listener in OrionPhasedFunctionalTest that parses that event into a string and stores it in an array to be accessed from a test phase later.

	#test PIE
	#rb Ben.Salem, Adric.Worley

Change 3308437 on 2017/02/16 by Jason.Bestimt@Jason.Bestimt_Dev-General

	#ORION_DG - Merge MAIN @ CL 3308413
	(Prep for Merge up)

	#RB:none
	#Tests:none

Change 3306497 on 2017/02/16 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Fix for compilation issue with USE_MALLOC_STOMP
	#rb none
	#tests compiled with malloc_stomp

Change 3306468 on 2017/02/16 by Cody.Haskell@OrionStream

	#Orion

	- Text popup work for Shield. If you click on an OrionEditableTextBox while running the game with -gfn, a special popup is called. Should do nothing normally.

	#rb none
	#tests PIE, golden path.

Change 3305945 on 2017/02/16 by David.Ratti@David.Ratti_G6218_Orion.Dev-General

	Remove unused/deprecated UGameplayEffectExtension class
	#rb #tests none

Change 3304630 on 2017/02/15 by Jason.Bestimt@Jason.Bestimt_Dev-General

	#ORION_DG - Merge Mieszko stuff from MAIN to DG

	#RB:none
	#TestS:none

	#!codereview: mieszko.zielinski

Change 3303785 on 2017/02/15 by jason.bestimt@Jason.Bestimt_Dev-General

	#ORION_MAIN - Merge 38.3 @ CL 3303224

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3303718 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

	#!ROBOMERGE-SAYS: Unresolved conflicts. jason.bestimt, please merge this change by hand.
	//Orion/Dev-General/OrionGame/Content/UI/DeckBuilder/DeckBuilderRoot.uasset - can't integrate exclusive file already opened
	//Orion/Dev-General/OrionGame/Content/UI/Master_Layouts/FrontEnd.uasset - can't integrate exclusive file already opened
	#!codereview: jason.bestimt

Change 3302382 on 2017/02/14 by Alexis.Matte@amatte-orion-dev-general

	Fix import of morph target when there is no animation
	#jira UE-41383
	#jira OR-35859
	#rb none
	#test none

Change 3301538 on 2017/02/14 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 38.3 @ CL 3301392

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3301481 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3299985 on 2017/02/13 by Lukasz.Furman@Lukasz.Furman_T7320_OrionStream

	added time limit to "get out of overlap" move for minons to avoid getting stuck in moving to inaccessbile spots
	#jira OR-35834
	#rb Mieszko.Zielinski
	#tests PIE

Change 3299732 on 2017/02/13 by Mieszko.Zielinski@mieszko.zielinski_T4675_Orion

	Tweaked the way EQS tests of negative score get normalized #UE4

	#rb none
	#test golden path + math
	#!codereview Lukasz.Furman, John.Abercrombie

Change 3299724 on 2017/02/13 by Mieszko.Zielinski@mieszko.zielinski_T4675_Orion

	Generic AI interface extensions #UE4

	Mostly getters

	#rb none
	#test golden path

Change 3299717 on 2017/02/13 by Mieszko.Zielinski@mieszko.zielinski_T4675_Orion

	A little tweak to VisLog's point labels drawing - if there's only one point in a set it will no longer append '_0' to the label #UE4

	#rb none
	#test PIE

Change 3299527 on 2017/02/13 by Paul.Moore@OrionWorkspace_Dev-General

	#orion #mms
	- Update libWebSockets binaries to fix Linux server web socket connections.
	#tests matchmaking, mms
	#rb none

Change 3299278 on 2017/02/13 by David.Ratti@David.Ratti_G6218_Orion.Dev-General

	Ability Task Pass: tasks should not broadcast out (back into ability graph) if the owning ability has completed EndAbility.
	#rb none
	#tests pie, golden path

Change 3297884 on 2017/02/10 by Paul.Moore@OrionWorkspace_Dev-General

	#mms
	- Enable SSL module for PS4 (needed by OpenSSL when using WebSockets).
	- Turn on verbose logging for WebSockets module for initial MMS debugging.
	#tests PS4
	#rb none

Change 3296911 on 2017/02/10 by John.Pollard@John.Pollard_T2802_Orion_DevGeneral

	Encode user search string so we support special characters

	#rb RyanG
	#tests Replays

Change 3296746 on 2017/02/10 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 38.3 @ CL 3296659

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3296735 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3296705 on 2017/02/10 by Daniel.Lamb@daniel.lamb_T3905_6612

	Added support to the cooker for iterating shared builds.
	#rb Not used yet
	#test Fast cook paragon

Change 3295747 on 2017/02/09 by Paul.Moore@OrionWorkspace_Dev-General

	#orion #mms
	- Integrated WS upgrade header functionality with latest Fortnite libws changes.
	- Added "ws" and "wss" protocols to web socket manager context.
	#rb rob.cannaday
	#!codereview rob.cannaday, james.hopkin
	#tests win64, ps4

Change 3295579 on 2017/02/09 by John.Pollard@John.Pollard_T2802_Orion_DevGeneral

	Fix for replay backward compatibility from John.Pollard
	#tests #rb na

	Merging using OrionScratchReleaseMapping

Change 3295506 on 2017/02/09 by Rolando.Caloca@rolando.caloca_T3903_OrionMainS

	O - Added option for force recompute tangents using skin cache
	#rb none
	#jira UE-41541
	#tests Editor run, toggle, restart

Change 3295461 on 2017/02/09 by Lukasz.Furman@Lukasz.Furman_T7320_OrionStream

	fixed huge interpolation times for linear network smoothing on stationary characters,
	fixed mismatch in movement Base between NavWalking server and Walking client, causing some stationary characters to float in midair

	copy of CL# 3295439
	#jira OR-35664, OR-35572
	#rb none
	#tests game

Change 3294954 on 2017/02/09 by Paul.Moore@OrionWorkspace_Dev-General

	#orion #mms

	- Integrating Fortnite WebSocket changes into Orion that fixes some win10 issues.

	#!codereview rob.cannaday, james.hopkin
	#tests compile ps4, linux, win64
	#rb none

Change 3294947 on 2017/02/09 by Daniel.Lamb@daniel.lamb_T3905_6612

	The generate stub return result is considered as success when saving cooked packages.
	Fixes bug with cooking blueprint nativized packages.
	#rb Trivial
	#test Cook paragon

Change 3293307 on 2017/02/08 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Fix for issue in last checkin - need to clear activecontext regardless
	#rb none
	#tests solo smoke with nullrhi

Change 3293284 on 2017/02/08 by Ryan.Gerleve@Ryan.Gerleve_T3703_Orion

	Allow setting the per-frame time limit for processing queued bunches separately for instant replays, since they may have more strict timing/framerate requirements.

	#rb john.pollard
	#tests golden path

Change 3293148 on 2017/02/08 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Fixed invalid memory access* with nullrhi and suppressed IME warning if no valid window handle exists

	(*Likely only an issue when running with memory validation)

	#rb none
	#tests verified invalid access exception no longer occurs with nullrhi
	#!review-3293149 @Matt.Khulenschmidt

Change 3293103 on 2017/02/08 by Max.Chen@Max.Chen_T4664_Orion_Main

	Sequencer: Fix build

	#jira OR-34918
	#rb none
	#tests none

Change 3292921 on 2017/02/08 by Max.Chen@Max.Chen_T4664_Orion_Main

	Sequencer: Force local player to maintain x fov axis.

	#jira OR-34918
	#rb david.ratti
	#tests Render/PIE a level sequence and test that the camera isn't zoomed in.

Change 3292869 on 2017/02/08 by David.Ratti@David.Ratti_G6218_Orion.Dev-General

	Yet more logging for OR-35448
	#rb #tests none

Change 3292821 on 2017/02/08 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: rob.cannaday
	PS4 libwebsockets build fix
	Update build cs files to point to PS4 file location
	Copy libwebsocket include directory from Fortnite to Orion
	#rb paul.moore
	#tests compile/link Win64 Development Editor, PS4 Debug, Linux Development Server

	#!ROBOMERGE-SOURCE: CL 3292820 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3292277 on 2017/02/08 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge DMM @ CL 3292219

	#RB:none
	#Tests:none

	[CODEREVIEW] paul.moore, benjamin.crocker
	#QAReview

	#!ROBOMERGE-SOURCE: CL 3292276 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3292211 on 2017/02/08 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Pulling new ags library from Release-4.15 and reverting hack that disabled feature for AMD users
	#rb Marcus.Wassmer
	#tests compiled

Change 3292167 on 2017/02/08 by David.Ratti@David.Ratti_G6218_Orion.Dev-General

	Additional logging for OR-35448
	#rb none
	#tests pie

Change 3289462 on 2017/02/06 by Ben.Salem@ben.salem_OrionMain

	Adding priority filters to Automation tests, also commands to filter on priority levels.
	#rb adric worley
	#tests Compiled, ran a few commands to verify it works.

Change 3288801 on 2017/02/06 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 (38.3) @ CL 3288681

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3288800 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3288750 on 2017/02/06 by Daniel.Lamb@daniel.lamb_T3905_6612

	Fixed issue when cooking client and server platforms in single cook some packages would be marked incorrectly because they would be stripped when from client / server.
	#rb Andrew.Grant
	#test Cook paragon

Change 3288624 on 2017/02/06 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Unlocked network version
	#rb #tests na
	OR-35603

Change 3288612 on 2017/02/06 by Daniel.Lamb@daniel.lamb_T3905_6612

	Added more ini settings to the iterative ini blacklist.
	#rb Trivial
	#test Iterative Cook Paragon

Change 3288184 on 2017/02/06 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Downgraded warning to display
	#!review-3288185 @David.Ratti
	#rb none
	#tests none

Change 3287634 on 2017/02/06 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ 3287498

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3287619 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3286668 on 2017/02/03 by Alexis.Matte@amatte-orion-dev-general

	Fix a crash when importing a LOD containing different material with less sections
	#rb none
	#test none

Change 3286112 on 2017/02/03 by Alexis.Matte@amatte-orion-dev-general

	Fix the re-import skeletal mesh regression, where all material disapear.
	#jira UE-41294
	#rb matt.kuhlenschmidt
	#test see the jira

Change 3285859 on 2017/02/03 by Daniel.Lamb@daniel.lamb_T3905_6612

	Fixed merge error from last checkin with the DDC commandlet
	#!codereview Matthew.Griffin
	#test DDC commandlet paragon
	#rb None

Change 3285637 on 2017/02/03 by Ryan.Gerleve@Ryan.Gerleve_T3703_Orion

	Pass in the DemoNetDriver pointer to the ConcurrentWithSlateTickTask instead of accessing it from the world in the task itself.

	#rb john.pollard
	#tests golden path

Change 3285479 on 2017/02/03 by Mieszko.Zielinski@mieszko.zielinski_T4675_Orion

	Made bot communicate ults when they're up, not when they're using it #Orion

	CL also contains a bit of code shuffling around, preparing ground for HTN plug in

	#rb none
	#test golden path

Change 3285125 on 2017/02/03 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ CL 3285078

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3285124 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3283996 on 2017/02/02 by Michael.Trepka@Michael.Trepka_PC_Orion-Dev-General

	Added UGameUserSettings::GetRecommandedResolutionScale() to replace UOrionGameUserSettings::GetDefaultResolutionScale(). This makes things less confusing (UGameUserSettings::GetRecommandedResolutionScale() returns scale recommended based on results of the benchmark and UGameUserSettings::GetDefaultResolutionScale() returns scale based on user settings) and fixes a regression introduced in 3257936 (OR-35544)

	#rb Cody.Haskell
	#tests Tested on PC

Change 3283951 on 2017/02/02 by Daniel.Lamb@daniel.lamb_T3905_6612

	Ensure DDC commandlet calls begincacheforcookedplatformdata correctly.
	#rb None
	#!codereview Matthew.Griffin
	#test DDC commandlet paragon.

Change 3283874 on 2017/02/02 by Lina.Halper@Lina.Halper_Orion

	fix for invalid resource issue

	#rb: none
	#code review: Daniel.Wright
	#tests: compile and editor with wolf

Change 3283621 on 2017/02/02 by Laurent.Delayen@laurent.delayen_Work2016_Orion

	Femme WIP whip aiming for Q ability.

	#rb none
	#tests Femme

Change 3283216 on 2017/02/02 by jason.bestimt@Jason.Bestimt_Dev-General

	#ORION_MAIN - Merge 37.2 @ CL 3282900

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3283199 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3282954 on 2017/02/02 by Lina.Halper@Lina.Halper_Orion

	It becomes invalid on the resource, so checking null, but still wip on verifying this with Daniel Wright. He's sick out.

	#rb:none
	#tests: compile
	#code review:Daniel.Wright
	#Jira: OR-35418

Change 3281993 on 2017/02/01 by Daniel.Lamb@daniel.lamb_T3905_6612

	Removed default unattended flag.
	#rb Trivial
	#test PS4 cook run paragon.

Change 3281990 on 2017/02/01 by Daniel.Lamb@daniel.lamb_T3905_6612

	Potential fix for deterministic cooking issue with UMovieSceneSignedObjects.
	#rb Andrew.Grant
	#!codereview Max.Preussner
	#test Cook and run paragon ps4.

Change 3281610 on 2017/02/01 by Laurent.Delayen@laurent.delayen_Work2016_Orion

	AimOffsetLookAt is now thread safe.

	#rb lina.halper
	#tests femme

Change 3281609 on 2017/02/01 by Laurent.Delayen@laurent.delayen_Work2016_Orion

	Fixed 'Convert to AimOffset LookAt' option being broken in Persona.

	#rb lina.halper
	#tests works for Femme now.

Change 3281019 on 2017/02/01 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ CL 3280498

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3281018 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3280813 on 2017/02/01 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: matthew.griffin
	Prevent inclusion of NotForLicensees files when staging CrashReportClient config files
	#rb none
	#tests none

	#!ROBOMERGE-SOURCE: CL 3280812 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3279921 on 2017/01/31 by Yanni.Tripolitis@yanni.tripolitis_Dev_General_Cary

	Fixed an error in the Round MF, that was somehow "leaked" into Paragon from Odin.

	#lockdown Billy.Rivers, Adam.Bellefeuil

	#!codereview Tim.Elek

Change 3279178 on 2017/01/31 by Daniel.Lamb@daniel.lamb_T3905_6612

	Fixed up diff files commandlet stack information
	#rb Joe.Conley
	#test Diff cooked packages

Change 3279084 on 2017/01/31 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Merging //UE4/Main at 3276432  through Orion-Staging
	#rb #tests na

Change 3279078 on 2017/01/31 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ CL 3279032

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3279077 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3277908 on 2017/01/30 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_37 - Fix for "-game" crash with missing meta data

	#RB:none
	#Tests:none

	[CodeReviewed]: andrew.grant, jamie.dale, mieszko.zielinski

	#!ROBOMERGE-SOURCE: CL 3277901 in //Orion/Release-37/... via CL 3277902 via CL 3277904 via CL 3277905
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3277520 on 2017/01/30 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Workaround for OR-35418
	#!ROBOMERGE: Main
	#rb none
	#tests verified ShortSoloGame test completes without a crash

Change 3277357 on 2017/01/30 by Daniel.Lamb@daniel.lamb_T3905_6612

	Fixed the rebuild lighting commandlet.
	#rb Trivial
	#test Rebuild lighting dev general

Change 3277322 on 2017/01/30 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ CL 3277275

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3277296 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3277210 on 2017/01/30 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: andrew.grant
	Non-shipping test changes:

	Fixed issue where with -stdout messages would be duplicated due to FeedbackContextAnsi echoing to stdout by default
	Changed stdout output to postfix instead of trail newlines
	Firstpass of finding and displaying crash callstacks in Orion Test Framework.

	#rb none
	#tests ran test framework with tests that purposefully crashed/checked

	#!ROBOMERGE-SOURCE: CL 3276889 in //Orion/Release-37/... via CL 3277207 via CL 3277208 via CL 3277209
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3276774 on 2017/01/29 by Andrew.Grant@andrew.grant.T6730.orion.floating

	Fix for non-unity issue.
	#tests compiled
	#rb none
	#!ROBOMERGE: Main, DUI

Change 3276594 on 2017/01/28 by Lina.Halper@Lina.Halper_Orion

	Checked in potential fix for nonunity build issue

	#rb:none
	#tests:compile

Change 3275806 on 2017/01/27 by Ben.Salem@ben.salem_OrionMain

	Adding in a checkpointing system for automated test passes where, if a client crashes while running a pass, on reboot and reissue of the automation command the test pass will start off where it left off, skipping the crashing test.
	#rb clayton.langford
	#tests Ran several dozen test passses. Seriously.
	#!codereview steve.white, bob.ferreira, clayton.langford, adric.worley

Change 3275803 on 2017/01/27 by Shaun.Kime@shaun.kime_RDU-WD-9788_oriondevgen

	Paragon has retainer widgets with no World set. When encountered, they can cause the scene list to be desynchronized with the rendering thread.
	This logic resolves the issue by registering a null scene in this case, properly setting the slate scene index for subsequent slate draw calls.

	#rb nick.darnell
	#jira OR-34919
	#TESTS na

Change 3275533 on 2017/01/27 by Max.Chen@Max.Chen_T4664_Orion_Main

	Sequencer: Switch to static pointer to fix crash when tearing down curve editor.

	#jira UE-40796
	#rb andrew.rodham
	#tests none

Change 3275093 on 2017/01/27 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ CL 3273298

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3273417 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3274700 on 2017/01/26 by Lina.Halper@Lina.Halper_Orion

	#Anim curve crash on cooking
	- fixed crash during cooking while accessing default value of material
	- this code doesn't have to run during cooking with inactive world, so I'm checking that

	#code review: Daniel.Wright, Chris.Bunner, Jurre.DeBaare
	#rb: none
	#tests: cooking

Change 3274129 on 2017/01/26 by Lina.Halper@Lina.Halper_Orion

	Fixed safer to get featurelevel

	#rb: Daniel.Wright
	#tests: compile/wolf

Change 3274012 on 2017/01/26 by Lukasz.Furman@Lukasz.Furman_T7320_OrionStream

	fixed crash in navigation grids
	#jira OR-35356
	#rb none
	#tests PIE

Change 3273803 on 2017/01/26 by Lina.Halper@Lina.Halper_Orion

	Fixed issue with animation curve getting reset to 0.f
	- the issue is that skeleton contains material flag types, so now it just keeps setting the value
	- even after I fix validation check, it still cleared it due to the material curve not found anymore, so added to support default value setting

	#jira: OR-34563
	#rb: Martin.Wilson, Chris.Bunner, Benn.Gallagher
	#code review: Martin.Wilson, Daniel.Wright
	#tests: wolf, coil

Change 3273257 on 2017/01/26 by Alexis.Matte@amatte-orion-dev-general

	Isolate by material slot instead of section index. Add UI to isolate and highlight material in the material panel
	#rb matt.kuhlenschmidt
	#jira UE-41131
	#tests none

Change 3272527 on 2017/01/25 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: chris.bunner
	Ensure FSceneRenderTargets snapshot copies default clear colors.
	#tests Golden path on lowest and high settings
	#rb None
	#lockdown Jason.Bestimt
	#jira OR-34905

	#!ROBOMERGE-SOURCE: CL 3272507 in //Orion/Release-37.1/... via CL 3272521 via CL 3272525
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3272244 on 2017/01/25 by Rolando.Caloca@rolando.caloca_T3903_OrionMainS

	Show more info when a material instance failed to compile
	#jira OR-34626
	#tests Forced crash in the debugger
	#rb Daniel.Wright

Change 3272109 on 2017/01/25 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: philip.buuck
	Fix bad merge from Main

	#rb Dan.Hertzka
	#tests PIE
	[CodeReviewed] Andrew.Grant
	#lockdown Andrew.Grant

	#!ROBOMERGE-SOURCE: CL 3272106 in //Orion/Release-37.1/... via CL 3272107 via CL 3272108
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

Change 3271721 on 2017/01/25 by Lukasz.Furman@Lukasz.Furman_T7320_OrionStream

	jungle minions will spawn navigation obstacles when they are stuck in static geometry, fixed issues with falling off cliffs
	#jira OR-35054
	#rb Mieszko.Zielinski
	#tests PIE

Change 3271432 on 2017/01/25 by Jason.Bestimt@ROBOMERGE_ORION_Dev_General

	#!ROBOMERGE-AUTHOR: jason.bestimt
	#ORION_MAIN - Merge 37.2 @ CL 3271043

	#RB:none
	#Tests:none

	#!ROBOMERGE-SOURCE: CL 3271429 in //Orion/Main/...
	#!ROBOMERGE-BOT: ORION (Main -> Dev-General)

[CL 3322856 by Andrew Grant in Main branch]
2017-02-25 19:37:22 -05:00

1206 lines
40 KiB
C++

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "CoreMinimal.h"
#include "HAL/FileManager.h"
#include "Misc/CommandLine.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/AutomationTest.h"
#include "Misc/App.h"
#include "Interfaces/IAutomationReport.h"
#include "AutomationWorkerMessages.h"
#include "IMessageContext.h"
#include "Helpers/MessageEndpoint.h"
#include "Modules/ModuleManager.h"
#include "Helpers/MessageEndpointBuilder.h"
#include "AssetEditorMessages.h"
#include "ImageComparer.h"
#include "AutomationControllerManager.h"
#include "Interfaces/IScreenShotToolsModule.h"
#include "Serialization/JsonSerializer.h"
#include "JsonObjectConverter.h"
#include "Misc/EngineVersion.h"
#include "Misc/FileHelper.h"
#include "PlatformHttp.h"
#if WITH_EDITOR
#include "Logging/MessageLog.h"
#endif
namespace AutomationControllerConstants
{
const FString HistoryConfigSectionName = TEXT("AutomationController.History");
}
FAutomationControllerManager::FAutomationControllerManager()
{
FParse::Value(FCommandLine::Get(), TEXT("ReportOutputPath="), ReportOutputPathOverride, false);
CheckpointFile = nullptr;
}
void FAutomationControllerManager::RequestAvailableWorkers(const FGuid& SessionId)
{
//invalidate previous tests
++ExecutionCount;
DeviceClusterManager.Reset();
ControllerResetDelegate.Broadcast();
// Don't allow reports to be exported
bTestResultsAvailable = false;
//store off active session ID to reject messages that come in from different sessions
ActiveSessionId = SessionId;
//TODO AUTOMATION - include change list, game, etc, or remove when launcher is integrated
int32 ChangelistNumber = 10000;
FString ProcessName = TEXT("instance_name");
MessageEndpoint->Publish(new FAutomationWorkerFindWorkers(ChangelistNumber, FApp::GetGameName(), ProcessName, SessionId), EMessageScope::Network);
// Reset the check test timers
LastTimeUpdateTicked = FPlatformTime::Seconds();
CheckTestTimer = 0.f;
IScreenShotToolsModule& ScreenShotModule = FModuleManager::LoadModuleChecked<IScreenShotToolsModule>("ScreenShotComparisonTools");
ScreenshotManager = ScreenShotModule.GetScreenShotManager();
}
void FAutomationControllerManager::RequestTests()
{
//invalidate incoming results
ExecutionCount++;
//reset the number of responses we have received
RefreshTestResponses = 0;
ReportManager.Empty();
for ( int32 ClusterIndex = 0; ClusterIndex < DeviceClusterManager.GetNumClusters(); ++ClusterIndex )
{
int32 DevicesInCluster = DeviceClusterManager.GetNumDevicesInCluster(ClusterIndex);
if ( DevicesInCluster > 0 )
{
FMessageAddress MessageAddress = DeviceClusterManager.GetDeviceMessageAddress(ClusterIndex, 0);
ResetIntermediateTestData();
//issue tests on appropriate platforms
MessageEndpoint->Send(new FAutomationWorkerRequestTests(bDeveloperDirectoryIncluded, RequestedTestFlags), MessageAddress);
}
}
}
void FAutomationControllerManager::RunTests(const bool bInIsLocalSession)
{
ExecutionCount++;
CurrentTestPass = 0;
ReportManager.SetCurrentTestPass(CurrentTestPass);
ClusterDistributionMask = 0;
bTestResultsAvailable = false;
TestRunningArray.Empty();
bIsLocalSession = bInIsLocalSession;
// Reset the check test timers
LastTimeUpdateTicked = FPlatformTime::Seconds();
CheckTestTimer = 0.f;
#if WITH_EDITOR
FMessageLog AutomationTestingLog("AutomationTestingLog");
FString NewPageName = FString::Printf(TEXT("-----Test Run %d----"), ExecutionCount);
FText NewPageNameText = FText::FromString(*NewPageName);
AutomationTestingLog.Open();
AutomationTestingLog.NewPage(NewPageNameText);
AutomationTestingLog.Info(NewPageNameText);
#endif
//reset all tests
ReportManager.ResetForExecution(NumTestPasses);
for ( int32 ClusterIndex = 0; ClusterIndex < DeviceClusterManager.GetNumClusters(); ++ClusterIndex )
{
//enable each device cluster
ClusterDistributionMask |= ( 1 << ClusterIndex );
//for each device in this cluster
for ( int32 DeviceIndex = 0; DeviceIndex < DeviceClusterManager.GetNumDevicesInCluster(ClusterIndex); ++DeviceIndex )
{
//mark the device as idle
DeviceClusterManager.SetTest(ClusterIndex, DeviceIndex, NULL);
// Send command to reset tests (delete local files, etc)
FMessageAddress MessageAddress = DeviceClusterManager.GetDeviceMessageAddress(ClusterIndex, DeviceIndex);
MessageEndpoint->Send(new FAutomationWorkerResetTests(), MessageAddress);
}
}
// Inform the UI we are running tests
if ( ClusterDistributionMask != 0 )
{
SetControllerStatus(EAutomationControllerModuleState::Running);
}
}
void FAutomationControllerManager::StopTests()
{
bTestResultsAvailable = false;
ClusterDistributionMask = 0;
ReportManager.StopRunningTests();
// Inform the UI we have stopped running tests
if ( DeviceClusterManager.HasActiveDevice() )
{
SetControllerStatus(EAutomationControllerModuleState::Ready);
}
else
{
SetControllerStatus(EAutomationControllerModuleState::Disabled);
}
TestRunningArray.Empty();
}
void FAutomationControllerManager::Init()
{
extern void EmptyLinkFunctionForStaticInitializationAutomationExecCmd();
EmptyLinkFunctionForStaticInitializationAutomationExecCmd();
AutomationTestState = EAutomationControllerModuleState::Disabled;
bTestResultsAvailable = false;
bScreenshotsEnabled = true;
bSendAnalytics = FParse::Param(FCommandLine::Get(), TEXT("SendAutomationAnalytics"));
// Update the ini with the settings
bTrackHistory = false;
GConfig->GetBool(*AutomationControllerConstants::HistoryConfigSectionName, TEXT("bTrackHistory"), bTrackHistory, GEngineIni);
// Default num of items to track
NumberOfHistoryItemsTracked = 5;
GConfig->GetInt(*AutomationControllerConstants::HistoryConfigSectionName, TEXT("NumberOfHistoryItemsTracked"), NumberOfHistoryItemsTracked, GEngineIni);
}
void FAutomationControllerManager::RequestLoadAsset(const FString& InAssetName)
{
MessageEndpoint->Publish(new FAssetEditorRequestOpenAsset(InAssetName), EMessageScope::Process);
}
void FAutomationControllerManager::Tick()
{
ProcessAvailableTasks();
ProcessComparisonQueue();
}
void FAutomationControllerManager::ProcessComparisonQueue()
{
TSharedPtr<FComparisonEntry> Entry;
if ( ComparisonQueue.Peek(Entry) )
{
if ( Entry->PendingComparison.IsReady() )
{
const bool Dequeued = ComparisonQueue.Dequeue(Entry);
check(Dequeued);
FImageComparisonResult Result = Entry->PendingComparison.Get();
const bool bIsNew = Result.IsNew();
const bool bAreSimilar = Result.AreSimilar();
// Issue tests on appropriate platforms
MessageEndpoint->Send(new FAutomationWorkerImageComparisonResults(bIsNew, bAreSimilar), Entry->Sender);
// Record the metadata for the test that needed the screenshots compared.
for ( int32 Index = 0; Index < TestRunningArray.Num(); Index++ )
{
// Find the game session instance info
int32 ClusterIndex;
int32 DeviceIndex;
verify(DeviceClusterManager.FindDevice(Entry->Sender, ClusterIndex, DeviceIndex));
TSharedPtr<IAutomationReport> Report = DeviceClusterManager.GetTest(ClusterIndex, DeviceIndex);
check(Report.IsValid());
FString ApprovedFolder = ScreenshotManager->GetLocalApprovedFolder();
FString UnapprovedFolder = ScreenshotManager->GetLocalUnapprovedFolder();
FString ComparisonFolder = ScreenshotManager->GetLocalComparisonFolder();
TArray<FString> Files;
Files.Add(ApprovedFolder / Result.ApprovedFile);
Files.Add(UnapprovedFolder / Result.IncomingFile);
Files.Add(ComparisonFolder / Result.ComparisonFile);
Report->AddArtifact(ClusterIndex, CurrentTestPass, FAutomationArtifact(Entry->Name, EAutomationArtifactType::Comparison, Files));
}
}
}
}
void FAutomationControllerManager::ProcessAvailableTasks()
{
// Distribute tasks
if ( ClusterDistributionMask != 0 )
{
// For each device cluster
for ( int32 ClusterIndex = 0; ClusterIndex < DeviceClusterManager.GetNumClusters(); ++ClusterIndex )
{
bool bAllTestsComplete = true;
// If any of the devices were valid
if ( ( ClusterDistributionMask & ( 1 << ClusterIndex ) ) && DeviceClusterManager.GetNumDevicesInCluster(ClusterIndex) > 0 )
{
ExecuteNextTask(ClusterIndex, bAllTestsComplete);
}
//if we're all done running our tests
if ( bAllTestsComplete )
{
//we don't need to test this cluster anymore
ClusterDistributionMask &= ~( 1 << ClusterIndex );
if ( ClusterDistributionMask == 0 )
{
ProcessResults();
//Notify the graphical layout we are done processing results.
TestsCompleteDelegate.Broadcast();
}
}
}
}
if ( bIsLocalSession == false )
{
// Update the test status for timeouts if this is not a local session
UpdateTests();
}
}
void FAutomationControllerManager::ReportTestResults()
{
GLog->Logf(TEXT("Test Pass Results:"));
for ( int32 i = 0; i < OurPassResults.TestInformation.Num(); i++ )
{
GLog->Logf(TEXT("%s: %s"), *OurPassResults.TestInformation[i].TestDisplayName, ToString(OurPassResults.TestInformation[i].State));
}
}
void FAutomationControllerManager::CollectTestResults(TSharedPtr<IAutomationReport> Report, const FAutomationTestResults& Results)
{
// TODO This is slow, change to a map.
for ( int32 i = 0; i < OurPassResults.TestInformation.Num(); i++ )
{
FAutomatedTestResult& ReportResult = OurPassResults.TestInformation[i];
if ( ReportResult.FullTestPath == Report->GetFullTestPath() )
{
ReportResult.Logs = Results.Logs;
ReportResult.Warnings = Results.Warnings;
for ( int j = 0; j < Results.Errors.Num(); j++ )
{
ReportResult.Errors.Add(Results.Errors[j].Message);
}
ReportResult.State = Results.State;
ReportResult.Artifacts = Results.Artifacts;
switch ( Results.State )
{
case EAutomationState::Success:
OurPassResults.NumSucceeded++;
break;
case EAutomationState::Fail:
OurPassResults.NumFailed++;
break;
default:
OurPassResults.NumNotRun++;
break;
}
return;
}
}
}
void FAutomationControllerManager::GenerateJsonTestPassSummary(FDateTime Timestamp)
{
if (!OurPassResults.TestInformation.Num())
{
return;
}
const FAutomatedTestPassResults SerializedPassResults = OurPassResults;
TSharedPtr<FJsonObject> ReportJson = FJsonObjectConverter::UStructToJsonObject(SerializedPassResults);
if (ReportJson.IsValid())
{
FString ReportOutputPath = GetReportPath(Timestamp);
FString ReportFileName = FString::Printf(TEXT("%s/index.json"), *ReportOutputPath);
FArchive* ReportFileWriter = IFileManager::Get().CreateFileWriter(*ReportFileName);
if (ReportFileWriter != nullptr)
{
TSharedRef<TJsonWriter<> > JsonWriter = TJsonWriterFactory<>::Create(ReportFileWriter, 0);
FJsonSerializer::Serialize(ReportJson.ToSharedRef(), JsonWriter);
delete ReportFileWriter;
}
}
else
{
GLog->Logf(ELogVerbosity::Error, TEXT("Test Report Json is invalid - report not generated."));
}
}
void FAutomationControllerManager::GenerateHtmlTestPassSummary(FDateTime Timestamp)
{
if ( !OurPassResults.TestInformation.Num() )
{
return;
}
FString ReportOutputPath = GetReportPath(Timestamp);
FScreenshotExportResults ExportResults = ScreenshotManager->ExportComparisonResultsAsync(ReportOutputPath).Get();
FAutomatedTestPassResults SerializedPassResults = OurPassResults;
SerializedPassResults.TestInformation.StableSort([] (const FAutomatedTestResult& A, const FAutomatedTestResult& B) {
if ( A.Errors.Num() > 0 )
{
if ( B.Errors.Num() > 0 )
return ( A.TestDisplayName < B.TestDisplayName );
else
return true;
}
else if ( B.Errors.Num() > 0 )
{
return false;
}
if ( A.Warnings.Num() > 0 )
{
if ( B.Warnings.Num() > 0 )
return ( A.TestDisplayName < B.TestDisplayName );
else
return true;
}
else if ( B.Warnings.Num() > 0 )
{
return false;
}
return A.TestDisplayName < B.TestDisplayName;
});
FString MasterTemplate, ResultTemplate, LogTemplate, ArtifactCompareTemplate, ArtifactImageTemplate;
const bool bLoadedMaster = FFileHelper::LoadFileToString(MasterTemplate, *( FPaths::EngineContentDir() / TEXT("Automation/Report-Master-Template.html") ));
const bool bLoadedResult = FFileHelper::LoadFileToString(ResultTemplate, *( FPaths::EngineContentDir() / TEXT("Automation/Report-Result-Template.html") ));
const bool bLoadedLog = FFileHelper::LoadFileToString(LogTemplate, *( FPaths::EngineContentDir() / TEXT("Automation/Report-Log-Template.html") ));
const bool bLoadedCompareArtifact = FFileHelper::LoadFileToString(ArtifactCompareTemplate, *( FPaths::EngineContentDir() / TEXT("Automation/Report-Artifact-Compare-Template.html") ));
const bool bLoadedImageArtifact = FFileHelper::LoadFileToString(ArtifactImageTemplate, *( FPaths::EngineContentDir() / TEXT("Automation/Report-Artifact-Image-Template.html") ));
check(bLoadedMaster && bLoadedResult && bLoadedLog && bLoadedCompareArtifact && bLoadedImageArtifact);
FString ReportState = TEXT("success");
FString ReportIcon = TEXT("heartbeat");
if ( SerializedPassResults.TestInformation.Num() > 0 )
{
const FAutomatedTestResult& FirstTest = SerializedPassResults.TestInformation[0];
if ( FirstTest.Errors.Num() > 0 )
{
ReportState = TEXT("error");
ReportIcon = TEXT("bomb");
}
else if ( FirstTest.Warnings.Num() > 0 )
{
ReportState = TEXT("warning");
ReportIcon = TEXT("exclamation-triangle");
}
}
FString HtmlResults;
for ( const FAutomatedTestResult& Test : SerializedPassResults.TestInformation )
{
FString TestState = Test.Errors.Num() > 0 ? TEXT("error") : Test.Warnings.Num() > 0 ? TEXT("warning") : TEXT("success");
FString Logs = TEXT("");
for ( const FString& LogItem : Test.Errors )
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("Statement"), FPlatformHttp::HtmlEncode(LogItem));
Logs += FString::Format(*LogTemplate, Args);
}
for ( const FString& LogItem : Test.Warnings )
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("Statement"), FPlatformHttp::HtmlEncode(LogItem));
Logs += FString::Format(*LogTemplate, Args);
}
for ( const FString& LogItem : Test.Logs )
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("Statement"), FPlatformHttp::HtmlEncode(LogItem));
Logs += FString::Format(*LogTemplate, Args);
}
for ( const FAutomationArtifact& Artifact : Test.Artifacts )
{
if ( Artifact.Type == EAutomationArtifactType::Comparison )
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("Name"), FPlatformHttp::HtmlEncode(Artifact.Name));
Args.Add(TEXT("Approved"), CopyArtifact(ReportOutputPath, Artifact.FilePaths[0]));
Args.Add(TEXT("Unapproved"), CopyArtifact(ReportOutputPath, Artifact.FilePaths[1]));
Args.Add(TEXT("Difference"), CopyArtifact(ReportOutputPath, Artifact.FilePaths[2]));
Logs += FString::Format(*ArtifactCompareTemplate, Args);
}
else if ( Artifact.Type == EAutomationArtifactType::Image )
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("Name"), FPlatformHttp::HtmlEncode(Artifact.Name));
Args.Add(TEXT("File"), CopyArtifact(ReportOutputPath, Artifact.FilePaths[0]));
Logs += FString::Format(*ArtifactCompareTemplate, Args);
}
else
{
check(false);
}
}
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("TestState"), TestState);
Args.Add(TEXT("TestName"), Test.TestDisplayName);
Args.Add(TEXT("TestPath"), Test.FullTestPath);
Args.Add(TEXT("Logs"), Logs);
HtmlResults += FString::Format(*ResultTemplate, Args);
}
}
{
TMap<FString, FStringFormatArg> Args;
Args.Add(TEXT("Title"), TEXT("Automation Test Results"));
Args.Add(TEXT("ReportState"), ReportState);
Args.Add(TEXT("ReportIcon"), ReportIcon);
Args.Add(TEXT("ComparisonExportDirectory"), ExportResults.ExportPath);
Args.Add(TEXT("Results"), HtmlResults);
FString Html = FString::Format(*MasterTemplate, Args);
FString ReportFileName = FString::Printf(TEXT("%s/index.html"), *ReportOutputPath);
if ( !FFileHelper::SaveStringToFile(Html, *ReportFileName, FFileHelper::EEncodingOptions::ForceUTF8) )
{
GLog->Logf(ELogVerbosity::Error, TEXT("Test Report Html is invalid - report not generated."));
}
}
}
FString FAutomationControllerManager::CopyArtifact(const FString& DestFolder, const FString& SourceFile) const
{
FString AritfactDirectory = FString::Printf(TEXT("ReportArtifacts-%d"), FEngineVersion::Current().GetChangelist());
FString ArtifactFile = AritfactDirectory / FGuid::NewGuid().ToString(EGuidFormats::Digits) + FPaths::GetExtension(SourceFile, true);
FString ArtifactDestination = DestFolder / ArtifactFile;
IFileManager::Get().Copy(*ArtifactDestination, *SourceFile, true, true);
return ArtifactFile;
}
FString FAutomationControllerManager::GetReportPath(FDateTime Timestamp) const
{
return ReportOutputPathOverride.IsEmpty() ? FString::Printf(TEXT("%s/Report-%d-%s"), *FPaths::AutomationLogDir(), FEngineVersion::Current().GetChangelist(), *Timestamp.ToString()) : ReportOutputPathOverride;
}
void FAutomationControllerManager::ExecuteNextTask( int32 ClusterIndex, OUT bool& bAllTestsCompleted )
{
bool bTestThatRequiresMultiplePraticipantsHadEnoughParticipants = false;
TArray< IAutomationReportPtr > TestsRunThisPass;
// For each device in this cluster
int32 NumDevicesInCluster = DeviceClusterManager.GetNumDevicesInCluster( ClusterIndex );
for ( int32 DeviceIndex = 0; DeviceIndex < NumDevicesInCluster; ++DeviceIndex )
{
// If this device is idle
if ( !DeviceClusterManager.GetTest(ClusterIndex, DeviceIndex).IsValid() && DeviceClusterManager.DeviceEnabled(ClusterIndex, DeviceIndex) )
{
// Get the next test that should be worked on
TSharedPtr< IAutomationReport > NextTest = ReportManager.GetNextReportToExecute(bAllTestsCompleted, ClusterIndex, CurrentTestPass, NumDevicesInCluster);
if ( NextTest.IsValid() )
{
// Get the status of the test
EAutomationState TestState = NextTest->GetState(ClusterIndex, CurrentTestPass);
if ( TestState == EAutomationState::NotRun )
{
// Reserve this device for the test
DeviceClusterManager.SetTest(ClusterIndex, DeviceIndex, NextTest);
TestsRunThisPass.Add(NextTest);
// Register this as a test we'll need to report on.
FAutomatedTestResult tempresult;
tempresult.Test = NextTest;
tempresult.TestDisplayName = NextTest->GetDisplayName();
tempresult.FullTestPath = NextTest->GetFullTestPath();
OurPassResults.TestInformation.Add(tempresult);
// If we now have enough devices reserved for the test, run it!
TArray<FMessageAddress> DeviceAddresses = DeviceClusterManager.GetDevicesReservedForTest(ClusterIndex, NextTest);
if ( DeviceAddresses.Num() == NextTest->GetNumParticipantsRequired() )
{
// Send it to each device
for ( int32 AddressIndex = 0; AddressIndex < DeviceAddresses.Num(); ++AddressIndex )
{
FAutomationTestResults TestResults;
GLog->Logf(ELogVerbosity::Display, TEXT("Running Automation: '%s' (Class Name: '%s')"), *TestsRunThisPass[AddressIndex]->GetFullTestPath(), *TestsRunThisPass[AddressIndex]->GetCommand());
TestResults.State = EAutomationState::InProcess;
if (CheckpointFile)
{
WriteLineToCheckpointFile(NextTest->GetFullTestPath());
}
TestResults.GameInstance = DeviceClusterManager.GetClusterDeviceName(ClusterIndex, DeviceIndex);
NextTest->SetResults(ClusterIndex, CurrentTestPass, TestResults);
NextTest->ResetNetworkCommandResponses();
// Mark the device as busy
FMessageAddress DeviceAddress = DeviceAddresses[AddressIndex];
// Send the test to the device for execution!
MessageEndpoint->Send(new FAutomationWorkerRunTests(ExecutionCount, AddressIndex, NextTest->GetCommand(), NextTest->GetDisplayName(), bScreenshotsEnabled, bSendAnalytics), DeviceAddress);
// Add a test so we can check later if the device is still active
TestRunningArray.Add(FTestRunningInfo(DeviceAddress));
}
}
}
}
}
else
{
// At least one device is still working
bAllTestsCompleted = false;
}
}
// Ensure any tests we have attempted to run on this pass had enough participants to successfully run.
for ( int32 TestIndex = 0; TestIndex < TestsRunThisPass.Num(); TestIndex++ )
{
IAutomationReportPtr CurrentTest = TestsRunThisPass[TestIndex];
if ( CurrentTest->GetNumDevicesRunningTest() != CurrentTest->GetNumParticipantsRequired() )
{
if ( GetNumDevicesInCluster(ClusterIndex) < CurrentTest->GetNumParticipantsRequired() )
{
float EmptyDuration = 0.0f;
TArray<FString> EmptyStringArray;
TArray<FString> AutomationsWarnings;
AutomationsWarnings.Add(FString::Printf(TEXT("Needed %d devices to participate, Only had %d available."), CurrentTest->GetNumParticipantsRequired(), DeviceClusterManager.GetNumDevicesInCluster(ClusterIndex)));
FAutomationTestResults TestResults;
TestResults.State = EAutomationState::NotEnoughParticipants;
TestResults.GameInstance = DeviceClusterManager.GetClusterDeviceName(ClusterIndex, 0);
TestResults.Warnings.Append(AutomationsWarnings);
CurrentTest->SetResults(ClusterIndex, CurrentTestPass, TestResults);
DeviceClusterManager.ResetAllDevicesRunningTest(ClusterIndex, CurrentTest);
}
}
}
//Check to see if we finished a pass
if ( bAllTestsCompleted && CurrentTestPass < NumTestPasses - 1 )
{
CurrentTestPass++;
ReportManager.SetCurrentTestPass(CurrentTestPass);
bAllTestsCompleted = false;
}
}
void FAutomationControllerManager::Startup()
{
MessageEndpoint = FMessageEndpoint::Builder("FAutomationControllerModule")
.Handling<FAutomationWorkerFindWorkersResponse>(this, &FAutomationControllerManager::HandleFindWorkersResponseMessage)
.Handling<FAutomationWorkerPong>(this, &FAutomationControllerManager::HandlePongMessage)
.Handling<FAutomationWorkerRequestNextNetworkCommand>(this, &FAutomationControllerManager::HandleRequestNextNetworkCommandMessage)
.Handling<FAutomationWorkerRequestTestsReply>(this, &FAutomationControllerManager::HandleRequestTestsReplyMessage)
.Handling<FAutomationWorkerRequestTestsReplyComplete>(this, &FAutomationControllerManager::HandleRequestTestsReplyCompleteMessage)
.Handling<FAutomationWorkerRunTestsReply>(this, &FAutomationControllerManager::HandleRunTestsReplyMessage)
.Handling<FAutomationWorkerScreenImage>(this, &FAutomationControllerManager::HandleReceivedScreenShot)
.Handling<FAutomationWorkerWorkerOffline>(this, &FAutomationControllerManager::HandleWorkerOfflineMessage);
if ( MessageEndpoint.IsValid() )
{
MessageEndpoint->Subscribe<FAutomationWorkerWorkerOffline>();
}
ClusterDistributionMask = 0;
ExecutionCount = 0;
bDeveloperDirectoryIncluded = false;
RequestedTestFlags = EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::EngineFilter | EAutomationTestFlags::ProductFilter | EAutomationTestFlags::PerfFilter;
NumOfTestsToReceive = 0;
NumTestPasses = 1;
//Default to machine name
DeviceGroupFlags = 0;
ToggleDeviceGroupFlag(EAutomationDeviceGroupTypes::MachineName);
}
void FAutomationControllerManager::Shutdown()
{
MessageEndpoint.Reset();
ShutdownDelegate.Broadcast();
RemoveCallbacks();
}
void FAutomationControllerManager::RemoveCallbacks()
{
ShutdownDelegate.Clear();
TestsAvailableDelegate.Clear();
TestsRefreshedDelegate.Clear();
TestsCompleteDelegate.Clear();
}
void FAutomationControllerManager::SetTestNames(const FMessageAddress& AutomationWorkerAddress)
{
int32 DeviceClusterIndex = INDEX_NONE;
int32 DeviceIndex = INDEX_NONE;
// Find the device that requested these tests
if ( DeviceClusterManager.FindDevice(AutomationWorkerAddress, DeviceClusterIndex, DeviceIndex) )
{
// Sort tests by display name
struct FCompareAutomationTestInfo
{
FORCEINLINE bool operator()(const FAutomationTestInfo& A, const FAutomationTestInfo& B) const
{
return A.GetDisplayName() < B.GetDisplayName();
}
};
TestInfo.Sort(FCompareAutomationTestInfo());
// Add each test to the collection
for ( int32 TestIndex = 0; TestIndex < TestInfo.Num(); ++TestIndex )
{
// Ensure our test exists. If not, add it
ReportManager.EnsureReportExists(TestInfo[TestIndex], DeviceClusterIndex, NumTestPasses);
}
// Clear any intermediate data we had associated with the tests whilst building the full list of tests
ResetIntermediateTestData();
}
else
{
//todo automation - make sure to report error if the device was not discovered correctly
}
// Note the response
RefreshTestResponses++;
// If we have received all the responses we expect to
if ( RefreshTestResponses == DeviceClusterManager.GetNumClusters() )
{
TestsRefreshedDelegate.Broadcast();
// Update the tests with tracking details
ReportManager.TrackHistory(bTrackHistory, NumberOfHistoryItemsTracked);
}
}
void FAutomationControllerManager::ProcessResults()
{
bHasErrors = false;
bHasWarning = false;
bHasLogs = false;
TArray< TSharedPtr< IAutomationReport > >& TestReports = GetReports();
if ( TestReports.Num() )
{
bTestResultsAvailable = true;
for ( int32 Index = 0; Index < TestReports.Num(); Index++ )
{
CheckChildResult(TestReports[Index]);
}
}
if ( !ReportOutputPathOverride.IsEmpty() )
{
FDateTime Timestamp = FDateTime::Now();
// Generate Html
GenerateHtmlTestPassSummary(Timestamp);
// Generate Json
GenerateJsonTestPassSummary(Timestamp);
}
// Then clean our array for the next pass.
OurPassResults.ClearAllEntries();
CleanUpCheckpointFile();
SetControllerStatus(EAutomationControllerModuleState::Ready);
}
void FAutomationControllerManager::CheckChildResult(TSharedPtr<IAutomationReport> InReport)
{
TArray<TSharedPtr<IAutomationReport> >& ChildReports = InReport->GetChildReports();
if ( ChildReports.Num() > 0 )
{
for ( int32 Index = 0; Index < ChildReports.Num(); Index++ )
{
CheckChildResult(ChildReports[Index]);
}
}
else if ( ( bHasErrors && bHasWarning && bHasLogs ) == false && InReport->IsEnabled() )
{
for ( int32 ClusterIndex = 0; ClusterIndex < GetNumDeviceClusters(); ++ClusterIndex )
{
FAutomationTestResults TestResults = InReport->GetResults(ClusterIndex, CurrentTestPass);
if ( TestResults.Errors.Num() )
{
bHasErrors = true;
}
if ( TestResults.Warnings.Num() )
{
bHasWarning = true;
}
if ( TestResults.Logs.Num() )
{
bHasLogs = true;
}
}
}
}
void FAutomationControllerManager::SetControllerStatus(EAutomationControllerModuleState::Type InAutomationTestState)
{
if ( InAutomationTestState != AutomationTestState )
{
// Inform the UI if the test state has changed
AutomationTestState = InAutomationTestState;
TestsAvailableDelegate.Broadcast(AutomationTestState);
}
}
void FAutomationControllerManager::RemoveTestRunning(const FMessageAddress& TestAddressToRemove)
{
for ( int32 Index = 0; Index < TestRunningArray.Num(); Index++ )
{
if ( TestRunningArray[Index].OwnerMessageAddress == TestAddressToRemove )
{
TestRunningArray.RemoveAt(Index);
break;
}
}
}
void FAutomationControllerManager::AddPingResult(const FMessageAddress& ResponderAddress)
{
for ( int32 Index = 0; Index < TestRunningArray.Num(); Index++ )
{
if ( TestRunningArray[Index].OwnerMessageAddress == ResponderAddress )
{
TestRunningArray[Index].LastPingTime = 0;
break;
}
}
}
void FAutomationControllerManager::UpdateTests()
{
static const float CheckTestInterval = 1.0f;
static const float GameInstanceLostTimer = 200.0f;
CheckTestTimer += FPlatformTime::Seconds() - LastTimeUpdateTicked;
LastTimeUpdateTicked = FPlatformTime::Seconds();
if ( CheckTestTimer > CheckTestInterval )
{
for ( int32 Index = 0; Index < TestRunningArray.Num(); Index++ )
{
TestRunningArray[Index].LastPingTime += CheckTestTimer;
if ( TestRunningArray[Index].LastPingTime > GameInstanceLostTimer )
{
// Find the game session instance info
int32 ClusterIndex;
int32 DeviceIndex;
verify(DeviceClusterManager.FindDevice(TestRunningArray[Index].OwnerMessageAddress, ClusterIndex, DeviceIndex));
//verify this device thought it was busy
TSharedPtr <IAutomationReport> Report = DeviceClusterManager.GetTest(ClusterIndex, DeviceIndex);
check(Report.IsValid());
// A dummy array used to report the result
TArray<FString> EmptyStringArray;
TArray<FString> ErrorStringArray;
ErrorStringArray.Add(FString(TEXT("Failed")));
bHasErrors = true;
GLog->Logf(ELogVerbosity::Display, TEXT("Timeout hit. Nooooooo."));
FAutomationTestResults TestResults;
TestResults.State = EAutomationState::Fail;
TestResults.GameInstance = DeviceClusterManager.GetClusterDeviceName(ClusterIndex, DeviceIndex);
// Set the results
Report->SetResults(ClusterIndex, CurrentTestPass, TestResults);
bTestResultsAvailable = true;
// Disable the device in the cluster so it is not used again
DeviceClusterManager.DisableDevice(ClusterIndex, DeviceIndex);
// Remove the running test
TestRunningArray.RemoveAt(Index--);
// If there are no more devices, set the module state to disabled
if ( DeviceClusterManager.HasActiveDevice() == false )
{
GLog->Logf(ELogVerbosity::Display, TEXT("Module disabled"));
SetControllerStatus(EAutomationControllerModuleState::Disabled);
ClusterDistributionMask = 0;
}
else
{
GLog->Logf(ELogVerbosity::Display, TEXT("Module not disabled. Keep looking."));
// Remove the cluster from the mask if there are no active devices left
if ( DeviceClusterManager.GetNumActiveDevicesInCluster(ClusterIndex) == 0 )
{
ClusterDistributionMask &= ~( 1 << ClusterIndex );
}
if ( TestRunningArray.Num() == 0 )
{
SetControllerStatus(EAutomationControllerModuleState::Ready);
}
}
}
else
{
MessageEndpoint->Send(new FAutomationWorkerPing(), TestRunningArray[Index].OwnerMessageAddress);
}
}
CheckTestTimer = 0.f;
}
}
const bool FAutomationControllerManager::ExportReport(uint32 FileExportTypeMask)
{
return ReportManager.ExportReport(FileExportTypeMask, GetNumDeviceClusters());
}
bool FAutomationControllerManager::IsTestRunnable(IAutomationReportPtr InReport) const
{
bool bIsRunnable = false;
for ( int32 ClusterIndex = 0; ClusterIndex < GetNumDeviceClusters(); ++ClusterIndex )
{
if ( InReport->IsSupported(ClusterIndex) )
{
if ( GetNumDevicesInCluster(ClusterIndex) >= InReport->GetNumParticipantsRequired() )
{
bIsRunnable = true;
break;
}
}
}
return bIsRunnable;
}
/* FAutomationControllerModule callbacks
*****************************************************************************/
void FAutomationControllerManager::HandleFindWorkersResponseMessage(const FAutomationWorkerFindWorkersResponse& Message, const IMessageContextRef& Context)
{
if ( Message.SessionId == ActiveSessionId )
{
DeviceClusterManager.AddDeviceFromMessage(Context->GetSender(), Message, DeviceGroupFlags);
}
RequestTests();
SetControllerStatus(EAutomationControllerModuleState::Ready);
}
void FAutomationControllerManager::HandlePongMessage( const FAutomationWorkerPong& Message, const IMessageContextRef& Context )
{
AddPingResult(Context->GetSender());
}
void FAutomationControllerManager::HandleReceivedScreenShot(const FAutomationWorkerScreenImage& Message, const IMessageContextRef& Context)
{
FString ScreenshotIncomingFolder = FPaths::GameSavedDir() / TEXT("Automation/Incoming/");
bool bTree = true;
FString FileName = ScreenshotIncomingFolder / Message.ScreenShotName;
IFileManager::Get().MakeDirectory(*FPaths::GetPath(FileName), bTree);
FFileHelper::SaveArrayToFile(Message.ScreenImage, *FileName);
// TODO Automation There is identical code in, Engine\Source\Runtime\AutomationWorker\Private\AutomationWorkerModule.cpp,
// need to move this code into common area.
FString Json;
if ( FJsonObjectConverter::UStructToJsonObjectString(Message.Metadata, Json) )
{
FString MetadataPath = FPaths::ChangeExtension(FileName, TEXT("json"));
FFileHelper::SaveStringToFile(Json, *MetadataPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
}
TSharedRef<FComparisonEntry> Comparison = MakeShareable(new FComparisonEntry());
Comparison->Sender = Context->GetSender();
Comparison->Name = Message.Metadata.Name;
Comparison->PendingComparison = ScreenshotManager->CompareScreensotAsync(Message.ScreenShotName);
ComparisonQueue.Enqueue(Comparison);
}
void FAutomationControllerManager::HandleRequestNextNetworkCommandMessage(const FAutomationWorkerRequestNextNetworkCommand& Message, const IMessageContextRef& Context)
{
// Harvest iteration of running the tests this result came from (stops stale results from being committed to subsequent runs)
if ( Message.ExecutionCount == ExecutionCount )
{
// Find the device id for the address
int32 ClusterIndex;
int32 DeviceIndex;
verify(DeviceClusterManager.FindDevice(Context->GetSender(), ClusterIndex, DeviceIndex));
// Verify this device thought it was busy
TSharedPtr<IAutomationReport> Report = DeviceClusterManager.GetTest(ClusterIndex, DeviceIndex);
check(Report.IsValid());
// Increment network command responses
bool bAllResponsesReceived = Report->IncrementNetworkCommandResponses();
// Test if we've accumulated all responses AND this was the result for the round of test running AND we're still running tests
if ( bAllResponsesReceived && ( ClusterDistributionMask & ( 1 << ClusterIndex ) ) )
{
// Reset the counter
Report->ResetNetworkCommandResponses();
// For every device in this networked test
TArray<FMessageAddress> DeviceAddresses = DeviceClusterManager.GetDevicesReservedForTest(ClusterIndex, Report);
check(DeviceAddresses.Num() == Report->GetNumParticipantsRequired());
// Send it to each device
for ( int32 AddressIndex = 0; AddressIndex < DeviceAddresses.Num(); ++AddressIndex )
{
//send "next command message" to worker
MessageEndpoint->Send(new FAutomationWorkerNextNetworkCommandReply(), DeviceAddresses[AddressIndex]);
}
}
}
}
void FAutomationControllerManager::HandleRequestTestsReplyMessage(const FAutomationWorkerRequestTestsReply& Message, const IMessageContextRef& Context)
{
FAutomationTestInfo NewTest = Message.GetTestInfo();
TestInfo.Add(NewTest);
}
void FAutomationControllerManager::HandleRequestTestsReplyCompleteMessage(const FAutomationWorkerRequestTestsReplyComplete& Message, const IMessageContextRef& Context)
{
SetTestNames(Context->GetSender());
}
void FAutomationControllerManager::HandleRunTestsReplyMessage(const FAutomationWorkerRunTestsReply& Message, const IMessageContextRef& Context)
{
// If we should commit these results
if ( Message.ExecutionCount == ExecutionCount )
{
FAutomationTestResults TestResults;
TestResults.State = Message.Success ? EAutomationState::Success : EAutomationState::Fail;
TestResults.Duration = Message.Duration;
// Mark device as back on the market
int32 ClusterIndex;
int32 DeviceIndex;
verify(DeviceClusterManager.FindDevice(Context->GetSender(), ClusterIndex, DeviceIndex));
TestResults.GameInstance = DeviceClusterManager.GetClusterDeviceName(ClusterIndex, DeviceIndex);
for ( auto& Error : Message.Errors )
{
TestResults.Errors.Add(Error.ToAutomationEvent());
}
TestResults.Logs = Message.Logs;
TestResults.Warnings = Message.Warnings;
// Verify this device thought it was busy
TSharedPtr<IAutomationReport> Report = DeviceClusterManager.GetTest(ClusterIndex, DeviceIndex);
check(Report.IsValid());
Report->SetResults(ClusterIndex, CurrentTestPass, TestResults);
const FAutomationTestResults& FinalResults = Report->GetResults(ClusterIndex, CurrentTestPass);
// Gather all of the data relevant to this test for our json reporting.
CollectTestResults(Report, FinalResults);
#if WITH_EDITOR
FMessageLog AutomationTestingLog("AutomationTestingLog");
AutomationTestingLog.Open();
#endif
for ( TArray<FAutomationEvent>::TConstIterator ErrorIter(TestResults.Errors); ErrorIter; ++ErrorIter )
{
// FAutomationTestFramework::Get().LogTestMessage(**ErrorIter, ELogVerbosity::Error);
GLog->Logf(ELogVerbosity::Error, TEXT("%s"), *( *ErrorIter ).ToString());
#if WITH_EDITOR
AutomationTestingLog.Error(FText::FromString(( *ErrorIter ).ToString()));
#endif
}
for ( TArray<FString>::TConstIterator WarningIter(Message.Warnings); WarningIter; ++WarningIter )
{
GLog->Logf(ELogVerbosity::Warning, TEXT("%s"), **WarningIter);
#if WITH_EDITOR
AutomationTestingLog.Warning(FText::FromString(*WarningIter));
#endif
}
for ( TArray<FString>::TConstIterator LogItemIter(Message.Logs); LogItemIter; ++LogItemIter )
{
GLog->Logf(ELogVerbosity::Log, TEXT("%s"), **LogItemIter);
#if WITH_EDITOR
AutomationTestingLog.Info(FText::FromString(*LogItemIter));
#endif
}
if ( TestResults.State == EAutomationState::Success )
{
FString SuccessString = FString::Printf(TEXT("...Automation Test Succeeded (%s)"), *Report->GetDisplayName());
GLog->Logf(ELogVerbosity::Log, TEXT("%s"), *SuccessString);
#if WITH_EDITOR
AutomationTestingLog.Info(FText::FromString(*SuccessString));
#endif
}
else
{
FString FailureString = FString::Printf(TEXT("...Automation Test Failed (%s)"), *Report->GetDisplayName());
GLog->Logf(ELogVerbosity::Log, TEXT("%s"), *FailureString);
#if WITH_EDITOR
AutomationTestingLog.Error(FText::FromString(*FailureString));
#endif
//FAutomationTestFramework::Get().Lo
}
// const bool TestSucceeded = (TestResults.State == EAutomationState::Success);
//FAutomationTestFramework::Get().LogEndTestMessage(Report->GetDisplayName(), TestSucceeded);
// Device is now good to go
DeviceClusterManager.SetTest(ClusterIndex, DeviceIndex, NULL);
}
// Remove the running test
RemoveTestRunning(Context->GetSender());
}
void FAutomationControllerManager::HandleWorkerOfflineMessage( const FAutomationWorkerWorkerOffline& Message, const IMessageContextRef& Context )
{
FMessageAddress DeviceMessageAddress = Context->GetSender();
DeviceClusterManager.Remove(DeviceMessageAddress);
}
bool FAutomationControllerManager::IsDeviceGroupFlagSet( EAutomationDeviceGroupTypes::Type InDeviceGroup ) const
{
const uint32 FlagMask = 1 << InDeviceGroup;
return (DeviceGroupFlags & FlagMask) > 0;
}
void FAutomationControllerManager::ToggleDeviceGroupFlag( EAutomationDeviceGroupTypes::Type InDeviceGroup )
{
const uint32 FlagMask = 1 << InDeviceGroup;
DeviceGroupFlags = DeviceGroupFlags ^ FlagMask;
}
void FAutomationControllerManager::UpdateDeviceGroups( )
{
DeviceClusterManager.ReGroupDevices( DeviceGroupFlags );
// Update the reports in case the number of clusters changed
int32 NumOfClusters = DeviceClusterManager.GetNumClusters();
ReportManager.ClustersUpdated(NumOfClusters);
}
void FAutomationControllerManager::TrackReportHistory(const bool bShouldTrack, const int32 NumReportsToTrack)
{
bTrackHistory = bShouldTrack;
NumberOfHistoryItemsTracked = NumReportsToTrack;
// Update the ini with the settings
GConfig->SetBool(*AutomationControllerConstants::HistoryConfigSectionName, TEXT("bTrackHistory"), bTrackHistory, GEngineIni);
GConfig->SetInt(*AutomationControllerConstants::HistoryConfigSectionName, TEXT("NumberOfHistoryItemsTracked"), NumberOfHistoryItemsTracked, GEngineIni);
ReportManager.TrackHistory(bTrackHistory, NumberOfHistoryItemsTracked);
}
const bool FAutomationControllerManager::IsTrackingHistory() const
{
return bTrackHistory;
}
const int32 FAutomationControllerManager::GetNumberHistoryItemsTracking() const
{
return NumberOfHistoryItemsTracked;
}
TArray<FString> FAutomationControllerManager::GetCheckpointFileContents()
{
TestsRun.Empty();
FString CheckpointFileName = FString::Printf(TEXT("%sautomationcheckpoint.txt"), *FPaths::AutomationDir());
if (IFileManager::Get().FileExists(*CheckpointFileName))
{
FString FileData;
FFileHelper::LoadFileToString(FileData, *CheckpointFileName);
FileData.ParseIntoArrayLines(TestsRun);
}
return TestsRun;
}
FArchive* FAutomationControllerManager::GetCheckpointFileForWrite()
{
if (!CheckpointFile)
{
FString CheckpointFileName = FString::Printf(TEXT("%sautomationcheckpoint.txt"), *FPaths::AutomationDir());
CheckpointFile = IFileManager::Get().CreateFileWriter(*CheckpointFileName, 8);
}
return CheckpointFile;
}
void FAutomationControllerManager::CleanUpCheckpointFile()
{
if (CheckpointFile)
{
CheckpointFile->Close();
CheckpointFile = nullptr;
}
FString CheckpointFileName = FString::Printf(TEXT("%sautomationcheckpoint.txt"), *FPaths::AutomationDir());
if (IFileManager::Get().FileExists(*CheckpointFileName))
{
IFileManager::Get().Delete(*CheckpointFileName);
}
}
void FAutomationControllerManager::WriteLoadedCheckpointDataToFile()
{
GetCheckpointFileForWrite();
for (int i = 0; i < TestsRun.Num(); i++)
{
FString LineToWrite = FString::Printf(TEXT("%s\n"), *TestsRun[i]);
CheckpointFile->Serialize(TCHAR_TO_ANSI(*LineToWrite), LineToWrite.Len());
}
}
void FAutomationControllerManager::WriteLineToCheckpointFile(FString StringToWrite)
{
GetCheckpointFileForWrite();
if (CheckpointFile)
{
FString LineToWrite = FString::Printf(TEXT("%s\n"), *StringToWrite);
CheckpointFile->Serialize(TCHAR_TO_ANSI(*LineToWrite), LineToWrite.Len());
CheckpointFile->Flush();
}
}