Merged PR 548016: Initial set of CloudTest Tests for Gvfs

Initial set of CloudTest Tests for Gvfs

To prep your machine to run locally:
1) Install git 4 windows
2) Install Gvfs

To build:
```
bxl /f:output='out\bin\cloudtest\*'
```
To run, execute the following in an **elevated** shell:
```
Out\Bin\CloudTest\debug\Gvfs\RunTests.cmd
```
This commit is contained in:
Danny van Velzen 2020-04-28 16:51:39 +00:00 коммит произвёл Aleksandar Milicevic
Родитель 3a9dc95484
Коммит 776597080e
13 изменённых файлов: 436 добавлений и 64 удалений

Просмотреть файл

@ -9,18 +9,18 @@ namespace BuildXL.CloudTest.Gvfs
{
public class BasicFileOperationTests
{
/// <summary>
/// This test is a smoke test for the journal code
/// </summary>
[Fact]
public void EditFile()
public void CreateFile()
{
using (var helper = new JournalHelper())
{
var filePath = Path.Combine(helper.WorkingFolder, "a.txt");
var filePath = helper.GetPath("a.txt");
// Track
helper.TrackPath(filePath);
// $REview: @Iman: Why does tracking a non-existent file require adding the parent folder?
// If this is true, I'll implement it in the helper
helper.TrackPath(helper.WorkingFolder);
// Do
File.WriteAllText(filePath, "hi");
@ -31,5 +31,46 @@ namespace BuildXL.CloudTest.Gvfs
}
}
public void EditFile()
{
using (var helper = new JournalHelper())
{
var filePath = helper.GetPath("a.txt");
File.WriteAllText(filePath, "hi-old");
// Track
helper.TrackPath(filePath);
// Do
File.WriteAllText(filePath, "hi");
// Check
helper.SnapCheckPoint();
helper.AssertChangeFile(filePath);
}
}
public void DeleteFile()
{
using (var helper = new JournalHelper())
{
var filePath = helper.GetPath("a.txt");
File.WriteAllText(filePath, "hi-old");
// Track
helper.TrackPath(filePath);
// Do
File.WriteAllText(filePath, "hi");
// Check
helper.SnapCheckPoint();
helper.AssertDeleteFile(filePath);
}
}
}
}

Просмотреть файл

@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.IO;
using Xunit;
using Xunit.Abstractions;
using GVFS.FunctionalTests.Tools;
namespace BuildXL.CloudTest.Gvfs
{
// TODO: make this class public once there is at least one enabled test in it
class BasicGvfsperationTests : TestBase
{
public BasicGvfsperationTests(ITestOutputHelper testOutput)
: base(testOutput)
{
}
[Fact(Skip="Gvfs Bug: The change file notification is flacky, not 100% reliable...")]
public void ChangeFileWhereFolderIsNotMaterialized()
{
using (var helper = Clone())
{
// Setup
var file = helper.GetPath(@"src\files\changingFile.txt");
helper.TrackPath(file);
// Operation (no materializtion)
helper.GitCheckout("changingFile1");
// Check
helper.SnapCheckPoint();
helper.AssertDeleteFile(file); // Gvfs causes a journal delete and a journal change
helper.AssertChangeFile(file); // $$Bug: Gvfs should trigger this USN entry..
helper.AssertFileContents(file, "VersionA");
}
}
[Fact(Skip="Gvfs Bug: The change file notification is flacky, not 100% reliable...")]
public void ChangeFileWhereFileNotButFolderIsMaterialized()
{
using (var helper = Clone())
{
// Setup
var file = helper.GetPath(@"src\files\changingFile.txt");
helper.TrackPath(file);
// Operation (Materialize folder only)
helper.AssertFileOnDisk(file);
helper.GitCheckout("changingFile1");
// Check
helper.SnapCheckPoint();
helper.AssertDeleteFile(file); // $$BUG: Gvfs causes a journal delete and a journal change
helper.AssertChangeFile(file); // $$BUG: This does not always fire :(
helper.AssertFileContents(file, "VersionA");
}
}
[Fact(Skip="Gvfs Bug: The change file notification is flacky, not 100% reliable...")]
public void ChangeFileWhereFileIsMaterialized()
{
using (var helper = Clone())
{
// Setup
var file = helper.GetPath(@"src\files\changingFile.txt");
// Operation (Materialize File)
helper.AssertFileContents(file, "VersionB");
helper.TrackPath(file);
helper.GitCheckout("changingFile1");
// Check
helper.SnapCheckPoint();
System.Diagnostics.Debugger.Launch();
helper.AssertDeleteFile(file); // $$BUG: Gvfs causes a journal delete and a journal change
helper.AssertChangeFile(file); // $$BUG: This does not always fire :(
helper.AssertFileContents(file, "VersionA");
}
}
[Fact(Skip = "Gvfs bug - Fires too many directory deleted notifications")]
public void NewFileWhenParentFolderIsNotMaterialzedBeforeOperation()
{
using (var helper = Clone())
{
// Setup
var file = helper.GetPath(@"src\files\subfolder\newfile2.txt");
helper.TrackPath(file);
// Operation
helper.GitCheckout("newFileInSubfolder");
// Check
helper.SnapCheckPoint();
helper.AssertCreateFile(file); // This step is failing
// $$BUG: Gvfs fires lots of Folder Removed notifications and a metadatachange on the root
}
}
[Fact(Skip = "Gvfs bug - Fires too many directory deleted notifications")]
public void NewFileWhenParentFolderIsMaterialzedBeforeOperation()
{
using (var helper = Clone())
{
// Setup
var file = helper.GetPath(@"src\files\subfolder\newfile.txt");
var file2 = helper.GetPath(@"src\files\subfolder\newfile2.txt");
helper.TrackPath(file);
helper.TrackPath(file2);
// Operation (materializes parent folder but not files)
helper.AssertFileOnDisk(file);
helper.AssertFileOnDisk(file2, expectExists: false);
helper.GitCheckout("newFileInSubfolder");
// Check
helper.AssertFileOnDisk(file);
helper.AssertFileOnDisk(file2);
helper.SnapCheckPoint();
helper.AssertCreateFile(file2);
// $$BUG: Gvfs fires lots of Folder Removed notifications and a metadatachange on the root
}
}
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
import * as BuildXLSdk from "Sdk.BuildXL";
import {Transformer} from "Sdk.Transformers";
@@public
export interface GvfsTestQualifer extends Qualifier
@ -13,13 +14,10 @@ export interface GvfsTestQualifer extends Qualifier
export declare const qualifier : GvfsTestQualifer;
// If you want to execute the cloudtest tests from BuildXL you can do so by passing /p:[BuildXL.CloudTest]executeTests=1 to bxl.
const testFunction = Environment.getFlag("[BuildXL.CloudTest]executeTests")
? BuildXLSdk.test
: BuildXLSdk.library;
const gvfsPkg = importFrom("GvfsTestHelpersForBuildXL").Contents.all;
@@public
export const dll = testFunction({
export const dll = BuildXLSdk.library({
assemblyName: "BuildXL.CloudTest.Gvfs",
sources: globR(d`.`, "*.cs"),
skipDocumentationGeneration: true,
@ -30,10 +28,24 @@ export const dll = testFunction({
importFrom("BuildXL.Utilities.UnitTests").TestUtilities.XUnit.dll,
...importFrom("Sdk.Managed.Testing.XUnit").xunitReferences,
BuildXLSdk.Factory.createBinary(gvfsPkg, r`tools/GVFS.FunctionalTests.dll`),
BuildXLSdk.Factory.createBinary(gvfsPkg, r`tools/GVFS.Tests.dll`),
BuildXLSdk.Factory.createBinary(gvfsPkg, r`tools/nunit.framework.dll`),
],
runtimeContent: [
f`BuildXL.CloudTest.Gvfs.JobGroup.xml`,
f`setup.cmd`,
f`setup.cmd`, // This is called from cloudtest JobGroup.xml file
f`runTests.cmd`, // This is just for local testing.
...importFrom("Sdk.Managed.Testing.XUnit").additionalNetCoreRuntimeContent,
]
gvfsPkg,
],
tools: {
csc: {
noWarnings: [
// Gvfs test assemblies are not signed, ignore warning.
8002
]
},
},
});

Просмотреть файл

@ -0,0 +1,118 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Linq;
using BuildXL.Utilities;
using GVFS.FunctionalTests;
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using Xunit;
using Xunit.Abstractions;
namespace BuildXL.CloudTest.Gvfs
{
public class GvfsJournalHelper : JournalHelper
{
private readonly string m_repo = "https://mseng@dev.azure.com/mseng/Domino/_git/BuildXL.Test.Gvfs";
private GVFSFunctionalTestEnlistment m_enlistment;
public string RepoRoot => m_enlistment.RepoRoot;
private ITestOutputHelper m_testOutput;
private GvfsJournalHelper(ITestOutputHelper testOutput, string repo = null)
{
m_testOutput = testOutput;
if (repo != null)
{
m_repo = repo;
}
// Set values for the Gvfs test helpers
Settings.Default.Initialize();
Settings.Default.Commitish = @"master";
Settings.Default.EnlistmentRoot = WorkingFolder;
Settings.Default.PathToGVFS = Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "GVFS", "gvfs.exe");
Settings.Default.RepoToClone = m_repo;
GVFSTestConfig.RepoToClone = m_repo;
GVFSTestConfig.NoSharedCache = true;
GVFSTestConfig.TestGVFSOnPath = true;
GVFSTestConfig.DotGVFSRoot = ".gvfs";
Assert.True(File.Exists(Settings.Default.PathToGVFS), Settings.Default.PathToGVFS);
Assert.True(File.Exists(Settings.Default.PathToGit), Settings.Default.PathToGit);
}
public override string GetPath(string path)
{
return Path.Combine(RepoRoot, path);
}
private void Clone(string commitish = null)
{
Assert.True(CurrentProcess.IsElevated, "Tests must run elevated!!");
m_enlistment = GVFSFunctionalTestEnlistment.CloneAndMount(Settings.Default.PathToGVFS);
}
public static GvfsJournalHelper Clone(ITestOutputHelper m_testOutput, string repo = null)
{
var helper = new GvfsJournalHelper(m_testOutput, repo);
helper.Clone();
return helper;
}
public void Git(string command)
{
m_testOutput.WriteLine($"Executing Git command -- {command}");
var result = GitProcess.InvokeProcess(RepoRoot, command);
if (result.ExitCode != 0)
{
m_testOutput.WriteLine($"Failed executing Git command -- {command}");
m_testOutput.WriteLine(result.Output);
m_testOutput.WriteLine(result.Errors);
Assert.Equal(0, result.ExitCode);
}
}
public void GitCheckout(string testBranchName)
{
Git($"checkout -b tests/{testBranchName} origin/tests/{testBranchName}");
}
public void Gvfs(string command)
{
m_testOutput.WriteLine($"Executing Gvfs command -- {command}");
}
public void AssertFileOnDisk(string path, bool expectExists = true)
{
var fullPath = Path.Combine(RepoRoot, path);
var fileExists = File.Exists(fullPath);
if (expectExists != fileExists)
{
var allFiles = Directory.EnumerateFiles(RepoRoot);
var expectMsg = expectExists ? "to exist, but it wasn't found." : "not to exist, but it is found on disk.";
Assert.True(false, $"Expected file '{fullPath}' {expectMsg}. Found {allFiles.Count()} files in enlistment: {string.Join(", ", allFiles)} ");
}
}
public void AssertFileContents(string path, string contents)
{
var fullPath = Path.Combine(RepoRoot, path);
var fileExists = File.Exists(fullPath);
if (!fileExists)
{
var allFiles = Directory.EnumerateFiles(RepoRoot);
var expectMsg = "to exist, but it wasn't found.";
Assert.True(false, $"Expected file '{fullPath}' {expectMsg}. Found {allFiles.Count()} files in enlistment: {string.Join(", ", allFiles)} ");
}
var fileContents = File.ReadAllText(fullPath);
Assert.Equal(contents, fileContents);
}
}
}

Просмотреть файл

@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using BuildXL.Storage;
using BuildXL.Storage.ChangeJournalService;
@ -18,18 +20,25 @@ namespace BuildXL.CloudTest.Gvfs
{
public string WorkingFolder {get;}
private readonly FileChangeTrackingSet m_changeTracker;
public string TestFolder {get;}
private readonly IChangeJournalAccessor m_journal;
private FileChangeTrackingSet m_changeTracker;
private IChangeJournalAccessor m_journal;
private List<ChangedPathInfo> m_changes = new List<ChangedPathInfo>();
public JournalHelper()
{
TestFolder = Path.GetDirectoryName(Assembly.GetAssembly(typeof(JournalHelper)).Location);
WorkingFolder = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString());
var workingRoot = Path.GetPathRoot(WorkingFolder);
Directory.CreateDirectory(WorkingFolder);
}
public void StartTracking()
{
var loggingContext = new LoggingContext("JournalTesting", "Dummy");
VolumeMap volumeMap = JournalUtils.TryCreateMapOfAllLocalVolumes(loggingContext);
@ -46,8 +55,18 @@ namespace BuildXL.CloudTest.Gvfs
);
}
public virtual string GetPath(string path)
{
return Path.Combine(WorkingFolder, path);
}
public void TrackPath(string path)
{
if (m_changeTracker == null)
{
StartTracking();
}
Analysis.IgnoreResult(m_changeTracker.TryProbeAndTrackPath(path));
}
@ -61,11 +80,12 @@ namespace BuildXL.CloudTest.Gvfs
public void SnapCheckPoint(int? nrOfExpectedOperations = null)
{
XAssert.IsNotNull(m_changeTracker);
m_changes.Clear();
using (m_changeTracker.Subscribe(this))
{
// $REview: @Iman: Why does default null not mean infinite, I constatly get timeout errors...
var result = m_changeTracker.TryProcessChanges(m_journal, TimeSpan.FromMinutes(1));
var result = m_changeTracker.TryProcessChanges(m_journal, null);
XAssert.IsTrue(result.Succeeded);
}
@ -80,11 +100,21 @@ namespace BuildXL.CloudTest.Gvfs
AssertOperation(new ChangedPathInfo(filePath, PathChanges.NewlyPresentAsFile));
}
public void AssertChangeFile(string filePath)
{
AssertOperation(new ChangedPathInfo(filePath, PathChanges.DataOrMetadataChanged));
}
public void AssertDeleteFile(string filePath)
{
AssertOperation(new ChangedPathInfo(filePath, PathChanges.Removed));
}
private void AssertOperation(ChangedPathInfo expectedChange)
{
if (!m_changes.Remove(expectedChange))
{
XAssert.Fail(GetChangeReport($"Expected to find change {expectedChange.Path} -- {expectedChange.PathChanges.ToString()}"));
XAssert.Fail(GetChangeReport($"Expected to find change {PrintChange(expectedChange)}"));
}
}
@ -107,6 +137,11 @@ namespace BuildXL.CloudTest.Gvfs
}
}
private string PrintChange(ChangedPathInfo change)
{
return $"{change.Path} -- {change.PathChanges.ToString()}";
}
void IObserver<ChangedPathInfo>.OnCompleted()
{
}

Просмотреть файл

@ -0,0 +1 @@
dotnet %~dp0\xunit.console.dll %~dp0\BuildXL.CloudTest.Gvfs.dll -parallel none -noshadow -noappdomain -xml %~dp0\xunit.results.xml

Просмотреть файл

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Xunit;
using Xunit.Abstractions;
namespace BuildXL.CloudTest.Gvfs
{
public class TestBase
{
private ITestOutputHelper m_testOutput;
public TestBase(ITestOutputHelper testOutput)
{
m_testOutput = testOutput;
}
public GvfsJournalHelper Clone(string repo = null)
{
return GvfsJournalHelper.Clone(m_testOutput, repo);
}
}
}

Просмотреть файл

@ -221,24 +221,7 @@ export interface TestRunArguments {
* this is an optional settings for executing the test processs to
* allow for overidding the process execution settings
* */
exec?: {
/** Tools dependencies. */
dependencies?: Transformer.InputArtifact[];
/** Tool outputs */
outputs?: Transformer.Output[];
/** Regex that would be used to extract errors from the output. */
errorRegex?: string;
/** Regex that would be used to extract warnings from the output. */
warningRegex?: string;
/** Environment variables. */
environmentVariables?: Transformer.EnvironmentVariable[];
/** Unsafe arguments */
unsafe?: Transformer.UnsafeExecuteArguments;
/** Mutexes to avoid running certain tests simultaneously */
acquireMutexes?: string[];
/** Semaphores to acquire when running tests */
acquireSemaphores?: Transformer.SemaphoreInfo[];
};
exec?: Transformer.ExecuteArgumentsComposible;
/**
* Some test frameworks might want to wrap other test runners

Просмотреть файл

@ -315,32 +315,34 @@ export function runQTest(args: QTestArguments): Result {
let result = Transformer.execute(
Object.merge<Transformer.ExecuteArguments>(
{
tool: args.qTestTool ? args.qTestTool : qTestTool,
tags: args.tags,
description: args.description,
arguments: commandLineArgs,
consoleOutput: consolePath,
workingDirectory: sandboxDir,
tempDirectory: qtestRunTempDirectory,
weight: args.weight,
environmentVariables: envVars,
disableCacheLookup: Environment.getFlag("[Sdk.BuildXL]qTestForceTest"),
additionalTempDirectories : [sandboxDir],
privilegeLevel: args.privilegeLevel,
dependencies: [
//When there are test failures, and PDBs are looked up to generate the stack traces,
//the original location of PDBs is used instead of PDBs in test sandbox. This is
//a temporary solution until a permanent fix regarding the lookup is identified
...(args.qTestInputs || args.qTestDirToDeploy.contents),
...(args.qTestRuntimeDependencies || []),
],
unsafe: unsafeOptions,
retryExitCodes: [2],
acquireSemaphores: args.qTestAcquireSemaphores,
},
changeAffectedInputListWrittenFileArg
));
args.tools && args.tools.exec,
{
tool: args.qTestTool ? args.qTestTool : qTestTool,
tags: args.tags,
description: args.description,
arguments: commandLineArgs,
consoleOutput: consolePath,
workingDirectory: sandboxDir,
tempDirectory: qtestRunTempDirectory,
weight: args.weight,
environmentVariables: envVars,
disableCacheLookup: Environment.getFlag("[Sdk.BuildXL]qTestForceTest"),
additionalTempDirectories : [sandboxDir],
privilegeLevel: args.privilegeLevel,
dependencies: [
//When there are test failures, and PDBs are looked up to generate the stack traces,
//the original location of PDBs is used instead of PDBs in test sandbox. This is
//a temporary solution until a permanent fix regarding the lookup is identified
...(args.qTestInputs || args.qTestDirToDeploy.contents),
...(args.qTestRuntimeDependencies || []),
],
unsafe: unsafeOptions,
retryExitCodes: [2],
acquireSemaphores: args.qTestAcquireSemaphores,
},
changeAffectedInputListWrittenFileArg
)
);
const qTestLogsDir: StaticDirectory = result.getOutputDirectory(logDir);
@ -500,6 +502,15 @@ export interface QTestArguments extends Transformer.RunnerArguments {
qTestAcquireSemaphores?: Transformer.SemaphoreInfo[];
/** Overrides global setting to disable code coverage collection on this test binary */
qTestDisableCodeCoverage?: boolean;
/** Nested tool options */
tools?: {
/**
* Options for tool execution
* */
exec?: Transformer.ExecuteArgumentsComposible;
wrapExec?: (exec: Transformer.ExecuteArguments) => Transformer.ExecuteArguments;
};
}
/**

Просмотреть файл

@ -208,7 +208,8 @@ function runTest(args : TestRunArguments) : File[] {
qTestRuntimeDependencies: qTestRuntimeDependencies,
qTestEnvironmentVariables: qTestEnvironmentVariables,
qTestAcquireSemaphores: args.tools && args.tools.exec && args.tools.exec.acquireSemaphores,
qTestDisableCodeCoverage : args.disableCodeCoverage
qTestDisableCodeCoverage : args.disableCodeCoverage,
tools: args.tools,
});
return [

Просмотреть файл

@ -379,6 +379,15 @@
}
}
},
{
"Component": {
"Type": "NuGet",
"NuGet": {
"Name": "GvfsTestHelpersForBuildXL",
"Version": "0.1.0"
}
}
},
{
"Component": {
"Type": "NuGet",

Просмотреть файл

@ -11,10 +11,14 @@ config({
...globR(d`Public/Sdk/UnitTests`, "module.config.dsc"),
...globR(d`Private/Wdg`, "module.config.dsc"),
...globR(d`Private/QTest`, "module.config.dsc"),
...globR(d`Private/CloudTest`, "module.config.dsc"),
...globR(d`Private/InternalSdk`, "module.config.dsc"),
...globR(d`Private/Tools`, "module.config.dsc"),
...globR(d`Public/Sdk/SelfHost`, "module.config.dsc"),
// Internal only modules
...addIf(importFile(f`config.microsoftInternal.dsc`).isMicrosoftInternal,
...globR(d`Private/CloudTest`, "module.config.dsc")
),
],
frontEnd: {

Просмотреть файл

@ -41,6 +41,9 @@ export const pkgs = isMicrosoftInternal ? [
{ id: "Symbol.App.Core", version: "18.163.29708-buildid11260482", dependentPackageIdsToSkip: ["*"], dependentPackageIdsToIgnore: ["BuildXL.Cache.Hashing", "BuildXL.Cache.Interfaces", "BuildXL.Cache.Libraries", "BuildXL.library.forAzDev"] },
{ id: "Symbol.Client", version: "18.163.29708-buildid11260482", dependentPackageIdsToSkip: ["*"] },
// CloudTest internal dependencies
{ id: "GvfsTestHelpersForBuildXL", version: "0.1.0"},
// Internal pacakged version to avoid downloading from the web but the trusted stable internal feed:
{ id: "NodeJs", version: "13.3.0-noTest" },
{ id: "PowerShell.Core", version: "6.1.0", osSkip: [ "macOS", "unix" ] },