Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ jobs:
path: MicrosoftGit

build:
if: false
runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-2025' }}
name: Build and Unit Test
needs: validate
Expand Down Expand Up @@ -329,12 +330,15 @@ jobs:

functional_tests:
name: Functional Tests
needs: [validate, build]
needs: [validate]
uses: ./.github/workflows/functional-tests.yaml
with:
skip: ${{ needs.validate.outputs.skip }}
vfs_run_id: '39636821560'
git_run_id: '39636821560'

upgrade_tests:
if: false
name: Upgrade Tests
needs: [validate, build]
uses: ./.github/workflows/upgrade-tests.yaml
Expand Down
54 changes: 46 additions & 8 deletions .github/workflows/functional-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
matrix:
configuration: [ Release ]
architecture: [ x64, arm64 ]
nr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # 12 parallel jobs to speed up the tests
nr: [0, 1, 2, 3, 4]
fail-fast: false # most failures are flaky tests, no need to stop the other jobs from succeeding

steps:
Expand Down Expand Up @@ -197,15 +197,53 @@ jobs:
git\logs
gvfs\logs

- name: Run functional tests
- name: Repeat reset placeholder test
if: steps.skip.outputs.result != 'true'
shell: cmd
shell: pwsh
timeout-minutes: 60
run: |
SET PATH=C:\Program Files\VFS for Git;%PATH%
SET GIT_TRACE2_PERF=C:\temp\git-trace2.log
SET GVFS_TEST_DIAGNOSTICS_DIR=C:\temp\gvfs-ft-diagnostics
ft\GVFS.FunctionalTests.exe /result:TestResult.xml --ci --slice=${{ matrix.nr }},12
$env:PATH = "C:\Program Files\VFS for Git;$env:PATH"
$env:GIT_TRACE2_PERF = "C:\temp\git-trace2.log"
$env:GVFS_TEST_DIAGNOSTICS_DIR = "C:\temp\gvfs-ft-diagnostics"
@(
'GVFS.FunctionalTests.Tests.GitCommands.CreatePlaceholderTests(None).BlocksPlaceholderCreationWhileGitCommandIsRunning("reset --stdin")'
'GVFS.FunctionalTests.Tests.GitCommands.CreatePlaceholderTests(Full).BlocksPlaceholderCreationWhileGitCommandIsRunning("reset --stdin")'
'GVFS.FunctionalTests.Tests.GitCommands.CreatePlaceholderTests(SparseMode).BlocksPlaceholderCreationWhileGitCommandIsRunning("reset --stdin")'
) | Set-Content repro-testlist.txt
New-Item -ItemType Directory -Force results | Out-Null
& ft\GVFS.FunctionalTests.exe --explore=SelectedTests.xml --full-suite --workers=4 --prefilter=GVFS.FunctionalTests --testlist=repro-testlist.txt
if ($LASTEXITCODE -ne 0) {
throw "Test discovery failed."
}
[xml]$selection = Get-Content SelectedTests.xml -Raw
if ($selection.SelectNodes('//test-case').Count -ne 3) {
throw "Test discovery did not select exactly three reset cases."
}
$failures = 0
for ($attempt = 1; $attempt -le 10; $attempt++) {
$resultPath = "results\TestResult-$attempt.xml"
& ft\GVFS.FunctionalTests.exe "/result:$resultPath" --full-suite --workers=4 --prefilter=GVFS.FunctionalTests --testlist=repro-testlist.txt
$exitCode = $LASTEXITCODE
if (!(Test-Path $resultPath)) {
throw "Attempt $attempt produced no test result."
}
[xml]$result = Get-Content $resultPath -Raw
$cases = @($result.SelectNodes('//test-case'))
if ($cases.Count -ne 3) {
throw "Attempt $attempt selected $($cases.Count) target cases instead of three."
}
if ($exitCode -ne 0 -or @($cases | Where-Object result -ne 'Passed').Count -ne 0) {
$failures++
}
foreach ($case in $cases) {
"Attempt $attempt`: exit=$exitCode result=$($case.result) test=$($case.fullname)" | Tee-Object -FilePath results\attempts.txt -Append
}
}
"Failures: $failures / 10" | Tee-Object -FilePath results\attempts.txt -Append
Get-Content results\attempts.txt >> $env:GITHUB_STEP_SUMMARY
if ($failures -ne 0) {
throw "$failures of 10 reset placeholder attempts failed."
}

- name: Upload failure diagnostics (mount dumps + logs)
if: always() && steps.skip.outputs.result != 'true'
Expand All @@ -221,7 +259,7 @@ jobs:
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.ARTIFACT_PREFIX }}FunctionalTests_Results_${{ env.FT_MATRIX_NAME }}
path: TestResult.xml
path: results

- name: Upload Git trace2 output
if: always() && steps.skip.outputs.result != 'true'
Expand Down
27 changes: 26 additions & 1 deletion GVFS/GVFS.FunctionalTests.LockHolder/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
using System.CommandLine;
using System;
using System.CommandLine;
using System.Threading;

namespace GVFS.FunctionalTests.LockHolder
{
public class Program
{
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "--wait-for-event")
{
string eventName = Environment.GetEnvironmentVariable("GVFS_TEST_HOOK_WAIT_EVENT_NAME");
if (string.IsNullOrEmpty(eventName))
{
Console.Error.WriteLine("The test did not supply the hook event name.");
Environment.Exit(1);
}

using (EventWaitHandle ready = EventWaitHandle.OpenExisting(eventName + ".Ready"))
using (EventWaitHandle release = EventWaitHandle.OpenExisting(eventName + ".Release"))
{
ready.Set();
if (!release.WaitOne(TimeSpan.FromSeconds(30)))
{
Console.Error.WriteLine("Timed out before the test released the command hook.");
Environment.Exit(1);
}
}

return;
}

RootCommand rootCommand = AcquireGVFSLockVerb.BuildRootCommand();
rootCommand.Parse(args).Invoke();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
Expand Down Expand Up @@ -48,14 +49,82 @@ public void GitBranch()
"remotes/origin/" + Properties.Settings.Default.Commitish);
}

[TestCase, Order(4)]
public void GitCommandWaitsWhileAnotherIsRunning()
[TestCase("status")]
[TestCase("for-each-repo")]
[Order(4)]
public void GitCommandWaitsWhileAnotherIsRunning(string command)
{
int pid;
GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: 3000);
string emptyConfigPath = null;
Dictionary<string, string> environmentVariables = null;
try
{
if (command == "for-each-repo")
{
emptyConfigPath = Path.Combine(this.Enlistment.EnlistmentRoot, "empty-git-config-" + Guid.NewGuid().ToString("N"));
File.WriteAllText(emptyConfigPath, string.Empty);
environmentVariables = new Dictionary<string, string>
{
{ "GIT_CONFIG_GLOBAL", emptyConfigPath },
{ "GIT_CONFIG_NOSYSTEM", "1" },
{ "GIT_CONFIG_COUNT", "0" },
{ "GIT_CONFIG_PARAMETERS", string.Empty },
};

ProcessResult alias = GitProcess.InvokeProcess(
this.Enlistment.RepoRoot,
"config --get alias.for-each-repo",
environmentVariables);
alias.ExitCode.ShouldEqual(1);
alias.Output.ShouldBeEmpty();

command = "for-each-repo --config=gvfs.test-lock-policy-repos-" + Guid.NewGuid().ToString("N");
}

ProcessResult statusWait = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, "status", removeWaitingMessages: false);
statusWait.Errors.ShouldContain(ExpectedStatusWaitingText);
int pid;
GitHelpers.AcquireGVFSLock(this.Enlistment, out pid, resetTimeout: 3000);

ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo(
this.Enlistment.RepoRoot,
command,
environmentVariables,
removeWaitingMessages: false);
result.ExitCode.ShouldEqual(0);
result.Errors.ShouldContain(ExpectedStatusWaitingText);
}
finally
{
if (emptyConfigPath != null)
{
File.Delete(emptyConfigPath);
}
}
}

[TestCase("worktree list")]
[TestCase("worktree list --porcelain")]
[TestCase("worktree list --porcelain -z")]
[TestCase("annotate HEAD -- Readme.md")]
[TestCase("check-ref-format --branch topic")]
[TestCase("cherry HEAD HEAD")]
[TestCase("count-objects -vH")]
[TestCase("ls-remote --get-url origin")]
[TestCase("range-diff HEAD~1..HEAD HEAD~1..HEAD")]
[TestCase("shortlog -sn HEAD")]
[TestCase("show-branch --list")]
public void ReadOnlyCommandDoesNotWaitForLock(string command)
{
GitHelpers.RunWithGVFSLock(
this.Enlistment,
() =>
{
ProcessResult result = GitProcess.InvokeProcess(
this.Enlistment.RepoRoot,
command,
timeoutMs: AcquireGVFSLockTimeout);

result.ExitCode.ShouldEqual(0);
result.Errors.ShouldNotContain(false, ExpectedStatusWaitingText);
});
}

[TestCase, Order(5)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Should;
using GVFS.FunctionalTests.Tools;
using GVFS.Tests.Should;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
Expand All @@ -19,6 +22,18 @@ public CreatePlaceholderTests(Settings.ValidateWorkingTreeMode validateWorkingTr
{
}

[TestCase]
public void AllowsPlaceholderCreationForDefaultAnnotate()
{
ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo(
this.Enlistment.RepoRoot,
$"annotate -- \"{GitHelpers.ConvertPathToGitFormat(FileToRead)}\"");

result.ExitCode.ShouldEqual(0);
result.Errors.ShouldBeEmpty();
string.IsNullOrWhiteSpace(result.Output).ShouldBeFalse("Annotate did not return file annotations.");
}

[TestCase("check-attr --stdin --all")]
[TestCase("check-ignore --stdin")]
[TestCase("check-mailmap --stdin")]
Expand All @@ -44,6 +59,67 @@ public void BlocksPlaceholderCreationWhileGitCommandIsRunning(string commandToRu
this.CheckPlaceholderCreation(commandToRun, shouldAllow: false);
}

[TestCase("worktree list")]
[TestCase("worktree list --porcelain")]
[TestCase("worktree list --porcelain -z")]
public void AllowsPlaceholderCreationWhileWorktreeListIsRunning(string command)
{
string hookListPath = this.Enlistment.GetVirtualPathTo(".git", "hooks", "post-command.hooks");
string originalHookList = File.ReadAllText(hookListPath);
string eventName = "GVFS.WorktreeList." + Guid.NewGuid().ToString("N");
string expectedContent = File.ReadAllText(Path.Combine(this.ControlGitRepo.RootPath, FileToRead));

using (EventWaitHandle ready = new EventWaitHandle(false, EventResetMode.ManualReset, eventName + ".Ready"))
using (EventWaitHandle release = new EventWaitHandle(false, EventResetMode.ManualReset, eventName + ".Release"))
using (Process git = new Process())
{
git.StartInfo = new ProcessStartInfo(Settings.Default.PathToGit, command)
{
WorkingDirectory = this.Enlistment.RepoRoot,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
git.StartInfo.Environment["GVFS_TEST_HOOK_WAIT_EVENT_NAME"] = eventName;

bool started = false;
try
{
// Pause the post-command chain before GVFS can release an incorrectly acquired lock.
string waitHook = $"\"{GitHelpers.LockHolderCommandPath}\" --wait-for-event";
File.WriteAllText(hookListPath, waitHook + Environment.NewLine + originalHookList);

started = git.Start();
started.ShouldBeTrue();
bool reachedHook = ready.WaitOne(TimeSpan.FromSeconds(30));
if (!reachedHook && git.HasExited)
{
Assert.Fail($"Git exited before the wait hook. Exit code: {git.ExitCode}. Errors: {git.StandardError.ReadToEnd()}");
}

reachedHook.ShouldBeTrue("Git did not reach the post-command hook.");

File.ReadAllText(this.Enlistment.GetVirtualPathTo(FileToRead)).ShouldEqual(expectedContent);

release.Set();
git.WaitForExit(10000).ShouldBeTrue("Git did not exit after the test released the hook.");
git.ExitCode.ShouldEqual(0);
git.StandardError.ReadToEnd().ShouldBeEmpty();
}
finally
{
release.Set();
if (started && !git.WaitForExit(10000))
{
git.Kill(entireProcessTree: true);
git.WaitForExit();
}

File.WriteAllText(hookListPath, originalHookList);
}
}
}

private void CheckPlaceholderCreation(string command, bool shouldAllow)
{
string eofCharacter = "\x04";
Expand Down
42 changes: 41 additions & 1 deletion GVFS/GVFS.FunctionalTests/Tools/GitHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static class GitHelpers
private const string WindowsPathSeparator = "\\";
private const string GitPathSeparator = "/";

private static string LockHolderCommandPath
public static string LockHolderCommandPath
{
get
{
Expand Down Expand Up @@ -189,6 +189,46 @@ public static ManualResetEventSlim AcquireGVFSLock(
out processId);
}

public static void RunWithGVFSLock(GVFSFunctionalTestEnlistment enlistment, Action action)
{
ArgumentNullException.ThrowIfNull(enlistment);
ArgumentNullException.ThrowIfNull(action);

ProcessStartInfo processInfo = new ProcessStartInfo(LockHolderCommandPath)
{
WorkingDirectory = enlistment.RepoRoot,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};

using (Process holdingProcess = Process.Start(processInfo))
{
try
{
enlistment.WaitForLock(LockHolderCommandName)
.ShouldBeTrue("The test lock holder did not acquire the GVFS lock.");
action();
}
finally
{
holdingProcess.StandardInput.WriteLine("done");
holdingProcess.StandardInput.Close();

bool exited = holdingProcess.WaitForExit(10000);
if (!exited)
{
holdingProcess.Kill(entireProcessTree: true);
holdingProcess.WaitForExit();
}

exited.ShouldBeTrue("The lock holder did not exit after the test released it.");
holdingProcess.ExitCode.ShouldEqual(0);
}
}
}

/// <summary>
/// Run the specified Git command. This method will return once the GVFSLock has been acquired.
/// </summary>
Expand Down
Loading
Loading