Files
UnrealEngine/Engine/Source/Programs/Shared/EpicGames.Perforce.Managed.Tests/ManagedWorkspaceTest.cs
2025-05-18 13:04:45 +08:00

385 lines
15 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EpicGames.Core;
using EpicGames.Perforce.Fixture;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenTelemetry.Trace;
namespace EpicGames.Perforce.Managed.Tests;
[TestClass]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
public class ManagedWorkspaceTest : BasePerforceFixtureTest
{
private string SyncDir => Path.Join(TempDir.FullName, "Sync");
private string CacheDir => Path.Join(TempDir.FullName, "Cache");
private string StreamName => Fixture.StreamFooMain.Root;
private StreamFixture Stream => Fixture.StreamFooMain;
private readonly ILogger<ManagedWorkspace> _mwLogger;
public ManagedWorkspaceTest()
{
_mwLogger = LoggerFactory.CreateLogger<ManagedWorkspace>();
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task SyncSingleChangelistAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
await AssertHaveTableFileCountAsync(0);
await SyncAsync(ws, 6);
Stream.GetChangelist(6).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(6).AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
private class CleanCounter
{
public int NumCleans { get; private set; } = 0;
public int NumFilesDeleted { get; private set; } = 0;
public int NumDirsDeleted { get; private set; } = 0;
public void OnClean(int numFiles, int numDirs)
{
NumCleans += 1;
NumFilesDeleted += numFiles;
NumDirsDeleted += numDirs;
Console.WriteLine($"Clean performed: numFiles={numFiles} numDirs={numDirs}");
}
public CleanCounter Reset()
{
NumCleans = 0;
NumFilesDeleted = 0;
NumDirsDeleted = 0;
return this;
}
public CleanCounter Assert(int expectedCount, int expectedNumFilesCleaned, int expectedNumDirsCleaned)
{
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(expectedCount, NumCleans);
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(expectedNumFilesCleaned, NumFilesDeleted);
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(expectedNumDirsCleaned, NumDirsDeleted);
return this;
}
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
public async Task SyncSkipCleanIfPossibleAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
await AssertHaveTableFileCountAsync(0);
CleanCounter cc = new();
ws.OnClean += cc.OnClean;
// Sync first time, clean is expected
await SyncAsync(ws, 6, assertFiles: true, useHaveTable: useHaveTable);
cc.Assert(1, 0, 0).Reset();
// Simulate some build output inside workspace
await File.WriteAllTextAsync(Path.Join(SyncDir, "build-output.obj"), "somecontent");
// Finalize and clean up is called when job finishes
await ws.CleanAsync(PerforceConnection, true, CancellationToken.None);
cc.Assert(1, 1, 0).Reset();
// Syncing again should not result in a clean up, as it was called during finalize and we're using the same P4 client
await SyncAsync(ws, 7, assertFiles: true, useHaveTable: useHaveTable);
cc.Assert(0, 0, 0).Reset();
}
[TestMethod]
public async Task SyncBackwardsToOlderChangelistRemoveUntrackedAsync()
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(true);
await SyncAsync(ws, 6, removeUntracked: false);
Stream.GetChangelist(6).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(6).AssertHaveTableAsync(PerforceConnection);
await SyncAsync(ws, 7, removeUntracked: false);
Stream.GetChangelist(7).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(7).AssertHaveTableAsync(PerforceConnection);
await SyncAsync(ws, 6, removeUntracked: false);
Stream.GetChangelist(6).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(6).AssertHaveTableAsync(PerforceConnection);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task SyncBackwardsToOlderChangelistAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
await SyncAsync(ws, 6);
Stream.GetChangelist(6).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(6).AssertHaveTableAsync(PerforceConnection, useHaveTable);
await SyncAsync(ws, 7);
Stream.GetChangelist(7).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(7).AssertHaveTableAsync(PerforceConnection, useHaveTable);
// Go back one changelist
await SyncAsync(ws, 6);
Stream.GetChangelist(6).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(6).AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task SyncUsingCacheFilesAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
FileReference GetCacheFilePath(int changeNumber)
{
return new FileReference(Path.Join(TempDir.FullName, $"CacheFile-{changeNumber}.bin"));
}
// Sync and create a new cache file per change number
foreach (ChangelistFixture cl in Stream.Changelists)
{
await SyncAsync(ws, cl.Number, cacheFile: GetCacheFilePath(cl.Number));
cl.AssertDepotFiles(SyncDir);
await cl.AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
// Sync again but using the cache files created above
foreach (ChangelistFixture cl in Stream.Changelists.Reverse())
{
await SyncAsync(ws, cl.Number, cacheFile: GetCacheFilePath(cl.Number));
cl.AssertDepotFiles(SyncDir);
await cl.AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task SyncWithViewExclusivePathFirstAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
ChangelistFixture cl = Stream.GetChangelist(6);
List<string> view = new() { "-/Data/...", "-/shared.h", };
await ws.SyncAsync(PerforceConnection, StreamName, cl.Number, view, true, false, null, CancellationToken.None);
List<DepotFileFixture> filtered = cl.StreamFiles
.Where(x => !x.DepotFile.Contains("shared.h", StringComparison.Ordinal))
.Where(x => !x.DepotFile.Contains("Data/", StringComparison.Ordinal)).ToList();
ChangelistFixture clViewApplied = new(cl.Number, cl.Description, filtered, cl.IsShelved);
clViewApplied.AssertDepotFiles(SyncDir);
await clViewApplied.AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task SyncWithViewInclusivePathFirstAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
ChangelistFixture cl = Stream.GetChangelist(6);
List<string> view = new() { "/...", "-/Data/..." };
await ws.SyncAsync(PerforceConnection, StreamName, cl.Number, view, true, false, null, CancellationToken.None);
List<DepotFileFixture> filtered = cl.StreamFiles
.Where(x => !x.DepotFile.Contains("Data/", StringComparison.Ordinal)).ToList();
ChangelistFixture clViewApplied = new(cl.Number, cl.Description, filtered, cl.IsShelved);
clViewApplied.AssertDepotFiles(SyncDir);
await clViewApplied.AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task PopulateAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
List<PopulateRequest> populateRequests = new() { new PopulateRequest(PerforceConnection, StreamName, new List<string>()) };
await ws.PopulateAsync(populateRequests, false, CancellationToken.None);
Stream.LatestChangelist.AssertDepotFiles(SyncDir);
await Stream.LatestChangelist.AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task PopulateWithViewAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
List<string> view = new() { "-/Data/...", "-/shared.h" };
ChangelistFixture cl = Stream.LatestChangelist;
List<DepotFileFixture> filtered = cl.StreamFiles
.Where(x => !x.DepotFile.Contains("shared.h", StringComparison.Ordinal))
.Where(x => !x.DepotFile.Contains("Data/", StringComparison.Ordinal)).ToList();
List<PopulateRequest> populateRequests = new() { new PopulateRequest(PerforceConnection, StreamName, view) };
await ws.PopulateAsync(populateRequests, false, CancellationToken.None);
ChangelistFixture clViewApplied = new(cl.Number, cl.Description, filtered, cl.IsShelved);
clViewApplied.AssertDepotFiles(SyncDir);
await clViewApplied.AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task UnshelveAsync(bool useHaveTable)
{
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
await SyncAsync(ws, 7);
await ws.UnshelveAsync(PerforceConnection, 8, CancellationToken.None);
Stream.GetChangelist(8).AssertDepotFiles(SyncDir);
// Have-table still correspond to CL 7 as CL 8 is shelved, only p4 printed to workspace
await Stream.GetChangelist(7).AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task Caching_SyncingNewChangelist_UnusedFilesMovedToCacheAsync(bool useHaveTable)
{
// Arrange
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
await SyncAsync(ws, 7);
// Act
await SyncAsync(ws, 1);
// Assert - all files from CL 7 are moved to the cache
PerforceFixture.AssertCacheEquals(CacheDir, Stream.GetChangelist(7).StreamFiles.Select(x => x.Digest).ToArray());
}
[TestMethod]
[DataRow(true, DisplayName = "With have-table")]
[DataRow(false, DisplayName = "Without have-table")]
public async Task Caching_SyncingWithCachedData_FilesPopulatedFromCacheAsync(bool useHaveTable)
{
// Arrange
ManagedWorkspace ws = await CreateManagedWorkspaceAsync(useHaveTable);
await SyncAsync(ws, 7);
await SyncAsync(ws, 1);
// Act
await SyncAsync(ws, 7);
// Assert - cache becomes empty
PerforceFixture.AssertCacheEquals(CacheDir, Array.Empty<string>());
}
[TestMethod]
public async Task FixtureMetadataMatchesServerMetadataAsync()
{
FStatOptions options = FStatOptions.IncludeFileSizes;
string fileSpec = "//Foo/Main/...";
List<ChangesRecord> submittedChanges = await PerforceConnection.GetChangesAsync(ChangesOptions.None, -1, ChangeStatus.Submitted, fileSpec);
List<ChangesRecord> shelvedChanges = await PerforceConnection.GetChangesAsync(ChangesOptions.None, -1, ChangeStatus.Shelved, fileSpec);
foreach (ChangesRecord cr in submittedChanges.Concat(shelvedChanges).OrderBy(x => x.Number))
{
HashSet<(string clientFile, int rev, long size, string digest)> depotFiles = new();
List<FStatRecord> fstatRecords = await PerforceConnection.FStatAsync(options, fileSpec + "@" + cr.Number).ToListAsync();
foreach (FStatRecord fsr in fstatRecords)
{
if (fsr.HeadAction is FileAction.Add or FileAction.MoveAdd or FileAction.Edit)
{
depotFiles.Add((fsr.DepotFile!, fsr.HeadRevision, fsr.FileSize, fsr.Digest!));
}
}
HashSet<(string clientFile, int rev, long size, string digest)> fixtureFiles = new();
ChangelistFixture changelist = Stream.GetChangelist(cr.Number);
if (changelist.IsShelved)
{
continue;
}
foreach (DepotFileFixture depotFile in changelist.StreamFiles)
{
fixtureFiles.Add((depotFile.DepotFile, depotFile.Revision, depotFile.Size, depotFile.Digest));
}
Assert.AreEqual(depotFiles.Count, fixtureFiles.Count);
foreach ((string clientFile, int rev, long size, string digest) tuple in fixtureFiles)
{
if (!depotFiles.Contains(tuple))
{
Assert.Fail("File in fixtures does not exist in depot: " + tuple);
}
}
}
}
[TestMethod]
public async Task ReusePerforceClientWithoutHaveTableAsync()
{
// Sync with have-table as normal
ManagedWorkspace wsWithHave = await CreateManagedWorkspaceAsync(true);
await wsWithHave.SetupAsync(PerforceConnection, StreamName, CancellationToken.None);
ChangelistFixture cl = Stream.GetChangelist(6);
await SyncAsync(wsWithHave, cl.Number);
cl.AssertDepotFiles(SyncDir);
await cl.AssertHaveTableAsync(PerforceConnection);
// Create a new ManagedWorkspace without have-table but re-use same Perforce connection
// The have-table remnants from above should not interfere with this workspace
ManagedWorkspace wsWithoutHave = await CreateManagedWorkspaceAsync(false);
await wsWithoutHave.SetupAsync(PerforceConnection, StreamName, CancellationToken.None);
await SyncAsync(wsWithoutHave, cl.Number);
await AssertHaveTableFileCountAsync(0);
await cl.AssertHaveTableAsync(PerforceConnection, useHaveTable: false);
cl.AssertDepotFiles(SyncDir);
}
private async Task<ManagedWorkspace> CreateManagedWorkspaceAsync(bool useHaveTable)
{
ManagedWorkspaceOptions options = new() { UseHaveTable = useHaveTable };
ManagedWorkspace ws = await ManagedWorkspace.CreateAsync(Environment.MachineName, TempDir, options, TracerProvider.Default.GetTracer("Test"), _mwLogger, CancellationToken.None);
await ws.SetupAsync(PerforceConnection, StreamName, CancellationToken.None);
return ws;
}
private async Task SyncAsync(ManagedWorkspace managedWorkspace, int changeNumber, FileReference? cacheFile = null, bool removeUntracked = true, bool assertFiles = false, bool useHaveTable = true)
{
await managedWorkspace.SyncAsync(PerforceConnection, StreamName, changeNumber, Array.Empty<string>(), removeUntracked, false, cacheFile, CancellationToken.None);
if (assertFiles)
{
Stream.GetChangelist(changeNumber).AssertDepotFiles(SyncDir);
await Stream.GetChangelist(changeNumber).AssertHaveTableAsync(PerforceConnection, useHaveTable);
}
}
private async Task AssertHaveTableFileCountAsync(int expected)
{
List<HaveRecord> haveRecords = await PerforceConnection.HaveAsync(new FileSpecList(), CancellationToken.None).ToListAsync();
if (haveRecords.Count != expected)
{
Console.WriteLine("Have table contains:");
foreach (HaveRecord haveRecord in haveRecords)
{
Console.WriteLine(haveRecord.DepotFile + "#" + haveRecord.HaveRev);
}
Assert.Fail($"Actual have table file count does not match expected count. Actual={haveRecords.Count} Expected={expected}");
}
}
}