TSharedRef<SDockTab> FMerge::GenerateMergeWidget(const UBlueprint& Object, TSharedRef<FBlueprintEditor> Editor) { auto ActiveTabPtr = ActiveTab.Pin(); if( ActiveTabPtr.IsValid() ) { // just bring the tab to the foreground: auto CurrentTab = FGlobalTabmanager::Get()->InvokeTab(MergeToolTabId); check( CurrentTab == ActiveTabPtr ); return ActiveTabPtr.ToSharedRef(); } // merge the local asset with the depot, SCC provides us with the last common revision as // a basis for the merge: TSharedPtr<SWidget> Contents; if (!PendingMerge(Object)) { // this should load up the merge-tool, with an asset picker, where they // can pick the asset/revisions to merge against Contents = GenerateMergeTabContents(Editor, nullptr, FRevisionInfo::InvalidRevision(), nullptr, FRevisionInfo::InvalidRevision(), &Object, FOnMergeResolved()); } else { // @todo DO: this will probably need to be async.. pulling down some old versions of assets: const FString& PackageName = Object.GetOutermost()->GetName(); const FString& AssetName = Object.GetName(); FSourceControlStatePtr SourceControlState = FMergeToolUtils::GetSourceControlState(PackageName); if (!SourceControlState.IsValid()) { DisplayErrorMessage( FText::Format( LOCTEXT("MergeFailedNoSourceControl", "Aborted Load of {0} from {1} because the source control state was invalidated") , FText::FromString(AssetName) , FText::FromString(PackageName) ) ); Contents = SNew(SHorizontalBox); } else { ISourceControlState const& SourceControlStateRef = *SourceControlState; FRevisionInfo CurrentRevInfo = FRevisionInfo::InvalidRevision(); const UBlueprint* RemoteBlueprint = Cast< UBlueprint >(LoadHeadRev(PackageName, AssetName, SourceControlStateRef, CurrentRevInfo)); FRevisionInfo BaseRevInfo = FRevisionInfo::InvalidRevision(); const UBlueprint* BaseBlueprint = Cast< UBlueprint >(LoadBaseRev(PackageName, AssetName, SourceControlStateRef, BaseRevInfo)); Contents = GenerateMergeTabContents(Editor, BaseBlueprint, BaseRevInfo, RemoteBlueprint, CurrentRevInfo, &Object, FOnMergeResolved()); } } TSharedRef<SDockTab> Tab = FGlobalTabmanager::Get()->InvokeTab(MergeToolTabId); Tab->SetContent(Contents.ToSharedRef()); ActiveTab = Tab; return Tab; }
/** * Simulates the user pressing the blueprint's compile button (will load the * blueprint first if it isn't already). * * @param BlueprintAssetPath The asset object path that you wish to compile. * @return False if we failed to load the blueprint, true otherwise */ static bool CompileBlueprint(const FString& BlueprintAssetPath) { UBlueprint* BlueprintObj = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *BlueprintAssetPath)); if (!BlueprintObj || !BlueprintObj->ParentClass) { UE_LOG(LogBlueprintAutomationTests, Error, TEXT("Failed to compile invalid blueprint, or blueprint parent no longer exists.")); return false; } UPackage* const BlueprintPackage = Cast<UPackage>(BlueprintObj->GetOutermost()); // compiling the blueprint will inherently dirty the package, but if there // weren't any changes to save before, there shouldn't be after bool const bStartedWithUnsavedChanges = (BlueprintPackage != nullptr) ? BlueprintPackage->IsDirty() : true; bool bIsRegeneratingOnLoad = false; bool bSkipGarbageCollection = true; FBlueprintEditorUtils::RefreshAllNodes(BlueprintObj); FKismetEditorUtilities::CompileBlueprint(BlueprintObj, bIsRegeneratingOnLoad, bSkipGarbageCollection); if (BlueprintPackage != nullptr) { BlueprintPackage->SetDirtyFlag(bStartedWithUnsavedChanges); } return true; }
bool FMerge::PendingMerge(const UBlueprint& BlueprintObj) const { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); bool bPendingMerge = false; if( SourceControlProvider.IsEnabled() ) { FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(BlueprintObj.GetOutermost(), EStateCacheUsage::Use); bPendingMerge = SourceControlState.IsValid() && SourceControlState->IsConflicted(); } return bPendingMerge; }
/** * Runs compile-on-load test against all unloaded, and optionally loaded, blueprints * See the TestAllBlueprints config key in the [Automation.Blueprint] config sections */ bool FBlueprintCompileOnLoadTest::RunTest(const FString& BlueprintAssetPath) { FCompilerResultsLog Results; UBlueprint* ExistingBP = nullptr; // if this blueprint was already loaded, then these tests are invalidated // (because dependencies have already been loaded) if (FBlueprintAutomationTestUtilities::IsBlueprintLoaded(BlueprintAssetPath, &ExistingBP)) { if (FBlueprintAutomationTestUtilities::IsAssetUnsaved(BlueprintAssetPath)) { AddError(FString::Printf(TEXT("You have unsaved changes made to '%s', please save them before running this test."), *BlueprintAssetPath)); return false; } else { AddWarning(FString::Printf(TEXT("Test may be invalid (the blueprint is already loaded): '%s'"), *BlueprintAssetPath)); FBlueprintAutomationTestUtilities::UnloadBlueprint(ExistingBP); } } // tracks blueprints that were already loaded (and cleans up any that were // loaded in its lifetime, once it is destroyed) FScopedBlueprintUnloader NewBlueprintUnloader(/*bAutoOpenScope =*/true, /*bRunGCOnCloseIn =*/true); // We load the blueprint twice and compare the two for discrepancies. This is // to bring dependency load issues to light (among other things). If a blueprint's // dependencies are loaded too late, then this first object is the degenerate one. UBlueprint* InitialBlueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *BlueprintAssetPath)); // if we failed to load it the first time, then there is no need to make a // second attempt, leave them to fix up this issue first if (InitialBlueprint == NULL) { AddError(*FString::Printf(TEXT("Unable to load blueprint for: '%s'"), *BlueprintAssetPath)); return false; } if (!InitialBlueprint->SkeletonGeneratedClass || !InitialBlueprint->GeneratedClass) { AddError(*FString::Printf(TEXT("Unable to load blueprint for: '%s'. Probably it derives from an invalid class."), *BlueprintAssetPath)); return false; } // GATHER SUBOBJECTS TArray<TWeakObjectPtr<UObject>> InitialBlueprintSubobjects; { TArray<UObject*> InitialBlueprintSubobjectsPtr; GetObjectsWithOuter(InitialBlueprint, InitialBlueprintSubobjectsPtr); for (auto Obj : InitialBlueprintSubobjectsPtr) { InitialBlueprintSubobjects.Add(Obj); } } // GATHER DEPENDENCIES TSet<TWeakObjectPtr<UBlueprint>> BlueprintDependencies; { TArray<UBlueprint*> DependentBlueprints; FBlueprintEditorUtils::GetDependentBlueprints(InitialBlueprint, DependentBlueprints); for (auto BP : DependentBlueprints) { BlueprintDependencies.Add(BP); } } BlueprintDependencies.Add(InitialBlueprint); // GATHER DEPENDENCIES PERSISTENT DATA struct FReplaceInnerData { TWeakObjectPtr<UClass> Class; FStringAssetReference BlueprintAsset; }; TArray<FReplaceInnerData> ReplaceInnerData; for (auto BPToUnloadWP : BlueprintDependencies) { auto BPToUnload = BPToUnloadWP.Get(); auto OldClass = BPToUnload ? *BPToUnload->GeneratedClass : NULL; if (OldClass) { FReplaceInnerData Data; Data.Class = OldClass; Data.BlueprintAsset = FStringAssetReference(BPToUnload); ReplaceInnerData.Add(Data); } } // store off data for the initial blueprint so we can unload it (and reconstruct // later to compare it with a second one) TArray<uint8> InitialLoadData; FObjectWriter(InitialBlueprint, InitialLoadData); // grab the name before we unload the blueprint FName const BlueprintName = InitialBlueprint->GetFName(); // unload the blueprint so we can reload it (to catch any differences, now // that all its dependencies should be loaded as well) //UNLOAD DEPENDENCIES, all circular dependencies will be loaded again // unload the blueprint so we can reload it (to catch any differences, now // that all its dependencies should be loaded as well) for (auto BPToUnloadWP : BlueprintDependencies) { if (auto BPToUnload = BPToUnloadWP.Get()) { FBlueprintAutomationTestUtilities::UnloadBlueprint(BPToUnload); } } // this blueprint is now dead (will be destroyed next garbage-collection pass) UBlueprint* UnloadedBlueprint = InitialBlueprint; InitialBlueprint = NULL; // load the blueprint a second time; if the two separately loaded blueprints // are different, then this one is most likely the choice one (it has all its // dependencies loaded) UBlueprint* ReloadedBlueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *BlueprintAssetPath)); UPackage* TransientPackage = GetTransientPackage(); FName ReconstructedName = MakeUniqueObjectName(TransientPackage, UBlueprint::StaticClass(), BlueprintName); // reconstruct the initial blueprint (using the serialized data from its initial load) EObjectFlags const StandardBlueprintFlags = RF_Public | RF_Standalone | RF_Transactional; InitialBlueprint = ConstructObject<UBlueprint>(UBlueprint::StaticClass(), TransientPackage, ReconstructedName, StandardBlueprintFlags | RF_Transient); FObjectReader(InitialBlueprint, InitialLoadData); { TMap<UObject*, UObject*> ClassRedirects; for (auto& Data : ReplaceInnerData) { UClass* OriginalClass = Data.Class.Get(); UBlueprint* NewBlueprint = Cast<UBlueprint>(Data.BlueprintAsset.ResolveObject()); UClass* NewClass = NewBlueprint ? *NewBlueprint->GeneratedClass : NULL; if (OriginalClass && NewClass) { ClassRedirects.Add(OriginalClass, NewClass); } } // REPLACE OLD DATA FArchiveReplaceObjectRef<UObject>(InitialBlueprint, ClassRedirects, /*bNullPrivateRefs=*/false, /*bIgnoreOuterRef=*/true, /*bIgnoreArchetypeRef=*/false); for (auto SubobjWP : InitialBlueprintSubobjects) { if (auto Subobj = SubobjWP.Get()) { FArchiveReplaceObjectRef<UObject>(Subobj, ClassRedirects, /*bNullPrivateRefs=*/false, /*bIgnoreOuterRef=*/true, /*bIgnoreArchetypeRef=*/false); } } UPackage* AssetPackage = ReloadedBlueprint->GetOutermost(); bool bHasUnsavedChanges = AssetPackage->IsDirty(); FBlueprintEditorUtils::RefreshAllNodes(ReloadedBlueprint); AssetPackage->SetDirtyFlag(bHasUnsavedChanges); } // look for diffs between subsequent loads and log them as errors TArray<FDiffSingleResult> BlueprintDiffs; bool bDiffsFound = FBlueprintAutomationTestUtilities::DiffBlueprints(InitialBlueprint, ReloadedBlueprint, BlueprintDiffs); if (bDiffsFound) { FBlueprintAutomationTestUtilities::ResolveCircularDependencyDiffs(ReloadedBlueprint, BlueprintDiffs); // if there are still diffs after resolving any the could have been from unloaded circular dependencies if (BlueprintDiffs.Num() > 0) { AddError(FString::Printf(TEXT("Inconsistencies between subsequent blueprint loads for: '%s' (was a dependency not preloaded?)"), *BlueprintAssetPath)); } else { bDiffsFound = false; } // list all the differences (so as to help identify what dependency was missing) for (auto DiffIt(BlueprintDiffs.CreateIterator()); DiffIt; ++DiffIt) { // will be presented in the context of "what changed between the initial load and the second?" FString DiffDescription = DiffIt->ToolTip; if (DiffDescription != DiffIt->DisplayString) { DiffDescription = FString::Printf(TEXT("%s (%s)"), *DiffDescription, *DiffIt->DisplayString); } const UEdGraphNode* NodeFromPin = DiffIt->Pin1 ? Cast<const UEdGraphNode>(DiffIt->Pin1->GetOuter()) : NULL; const UEdGraphNode* Node = DiffIt->Node1 ? DiffIt->Node1 : NodeFromPin; const UEdGraph* Graph = Node ? Node->GetGraph() : NULL; const FString GraphName = Graph ? Graph->GetName() : FString(TEXT("Unknown Graph")); AddError(FString::Printf(TEXT("%s.%s differs between subsequent loads: %s"), *BlueprintName.ToString(), *GraphName, *DiffDescription)); } } // At the close of this function, the FScopedBlueprintUnloader should prep // for following tests by unloading any blueprint dependencies that were // loaded for this one (should catch InitialBlueprint and ReloadedBlueprint) // // The FScopedBlueprintUnloader should also run garbage-collection after, // in hopes that the imports for this blueprint get destroyed so that they // don't invalidate other tests that share the same dependencies return !bDiffsFound; }