Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.idea/
.vs/
*.files
*.lscache
*.mdb
*.nupkg
*.orig
Expand Down
18 changes: 6 additions & 12 deletions src/LibChorus/VcsDrivers/Mercurial/HgRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,9 @@ public bool Pull(RepositoryAddress source, string targetUri)

public void PushToTarget(string targetLabel, string targetUri)
{
try
{
CheckAndUpdateHgrc();
Execute(false, _mercurialTwoCompatible,SecondsBeforeTimeoutOnRemoteOperation, "push --new-branch " + GetProxyConfigParameterString(targetUri), SurroundWithQuotes(targetUri));
}
catch (Exception err)
{
_progress.WriteMessageWithColor("OrangeRed",
$"Could not send to {ServerSettingsModel.RemovePasswordForLog(targetUri)}{Environment.NewLine}{err.Message}");
}
CheckAndUpdateHgrc();
Execute(false, _mercurialTwoCompatible, SecondsBeforeTimeoutOnRemoteOperation,
"push --new-branch " + GetProxyConfigParameterString(targetUri), SurroundWithQuotes(targetUri));

if (GetIsLocalUri(targetUri))
{
Expand All @@ -313,8 +306,9 @@ public void PushToTarget(string targetLabel, string targetUri)
}
catch (Exception err)
{
_progress.WriteMessageWithColor("OrangeRed",
$"Could not update the actual files after a pushing to {ServerSettingsModel.RemovePasswordForLog(targetUri)}{Environment.NewLine}{err.Message}");
throw new ApplicationException(
$"Could not update the actual files after pushing to {ServerSettingsModel.RemovePasswordForLog(targetUri)}",
err);
}
}
}
Expand Down
25 changes: 24 additions & 1 deletion src/LibChorus/sync/Synchronizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,16 +251,39 @@ public void SetIsOneOfDefaultSyncAddresses(RepositoryAddress address, bool enabl
private void SendToOthers(HgRepository repo, List<RepositoryAddress> sourcesToTry, Dictionary<RepositoryAddress, bool> connectionAttempt)
{
using var activity = LibChorusActivitySource.Value.StartActivity();
List<Exception> errors = new List<Exception>();
bool atLeastOneSucceeded = false;
foreach (RepositoryAddress address in sourcesToTry)
{
ThrowIfCancelPending();

if (!address.IsReadOnly)
{
SendToOneOther(address, connectionAttempt, repo);
try
{
SendToOneOther(address, connectionAttempt, repo);
atLeastOneSucceeded = true;
}
catch (UserCancelledException)
{
throw;
}
catch (Exception e)
{
_progress.WriteException(e);
_progress.WriteError(e.Message);
errors.Add(e);
}
}
}
ThrowIfCancelPending();

// Only throw and fail the sync if every writable target failed; partial success still counts as success.
if (errors.Any() && !atLeastOneSucceeded)
{
var error = errors.Count == 1 ? errors.Single() : new AggregateException(errors);
throw new SynchronizationException(error, WhatToDo.CheckAddressAndConnection, "Failed to send changes");
}
}

private void ThrowIfCancelPending()
Expand Down
100 changes: 99 additions & 1 deletion src/LibChorusTests/sync/Synchronizer.BadSituationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Chorus.merge;
using Chorus.sync;
using Chorus.Utilities;
using Chorus.VcsDrivers;
using Chorus.VcsDrivers.Mercurial;
using LibChorus.TestUtilities;
using NUnit.Framework;
Expand Down Expand Up @@ -55,6 +56,81 @@ public void Sync_HgrcInUseByOther_FailsGracefully()
}
}

[Test]
public void Sync_PushFails_ErrorEncounteredIsSet()
{
using (var sender = new RepositorySetup("sender"))
{
sender.AddAndCheckinFile("one.txt", "sender's content");

var options = new SyncOptions
{
DoMergeWithOthers = false,
DoPullFromOthers = false,
DoSendToOthers = true,
};
options.RepositorySourcesToTry.Add(new FailingConnectableHttpRepositoryAddress("test-repo"));

var result = sender.SyncWithOptions(options);
Assert.That(result.Succeeded, Is.False);
Assert.That(result.ErrorEncountered, Is.Not.Null);
Assert.That(result.ErrorEncountered.Message, Does.Contain("Failed to send changes"));
Assert.That(sender.GetProgressString(), Does.Contain("Failed to send to test-repo"));
}
}

[Test]
public void Sync_FirstPushFails_OtherSourcesStillTried()
{
using (var sender = new RepositorySetup("sender"))
{
sender.AddAndCheckinFile("one.txt", "sender's content");

var options = new SyncOptions
{
DoMergeWithOthers = false,
DoPullFromOthers = false,
DoSendToOthers = true,
};
options.RepositorySourcesToTry.Add(new FailingConnectableHttpRepositoryAddress("first-test-repo"));
options.RepositorySourcesToTry.Add(new FailingConnectableHttpRepositoryAddress("second-test-repo"));

var result = sender.SyncWithOptions(options);
Assert.That(result.Succeeded, Is.False);
Assert.That(result.ErrorEncountered, Is.Not.Null);
// Both targets should have been attempted even though the first failed.
var progress = sender.GetProgressString();
Assert.That(progress, Does.Contain("Failed to send to first-test-repo"));
Assert.That(progress, Does.Contain("Failed to send to second-test-repo"));
}
}

[Test]
public void Sync_OneOfMultiplePushTargetsFails_SyncStillSucceeds()
{
using (var sender = new RepositorySetup("sender"))
{
sender.AddAndCheckinFile("one.txt", "sender's content");

using (var receiver = new RepositorySetup("receiver", sender))
{
var options = new SyncOptions
{
DoMergeWithOthers = false,
DoPullFromOthers = false,
DoSendToOthers = true,
};
options.RepositorySourcesToTry.Add(new FailingConnectableHttpRepositoryAddress("test-repo"));
options.RepositorySourcesToTry.Add(receiver.GetRepositoryAddress());

var result = sender.SyncWithOptions(options);
Assert.That(result.Succeeded, Is.True);
Assert.That(result.ErrorEncountered, Is.Null);
Assert.That(sender.GetProgressString(), Does.Contain("Failed to send to test-repo"));
}
}
}

[Test]
public void Sync_ExceptionInMergeCode_LeftWith2HeadsAndErrorOutputToProgress()
{
Expand Down Expand Up @@ -518,5 +594,27 @@ public void Sync_NewFileWithNonAsciCharacters_FileAdded()
}
}

/// <summary>
/// Test double that bypasses HttpRepositoryPath's remote validation.
/// HttpRepositoryPath.CanConnect() validates the endpoint and aborts if invalid, preventing
/// sync operations from executing. This allows CanConnect() to return true while sync operations
/// fail, enabling tests of sync failure scenarios (push/pull/merge failures).
/// </summary>
private class FailingConnectableHttpRepositoryAddress : RepositoryAddress
{
public FailingConnectableHttpRepositoryAddress(string name) : base(name, "http://127.0.0.1:1/test-project", false)
{
}

public override bool CanConnect(HgRepository localRepository, string projectName, IProgress progress)
{
return true;
}

public override string GetPotentialRepoUri(string repoIdentifier, string projectName, IProgress progress)
{
return URI;
}
}
}
}
}
Loading