diff options
author | Ævar Arnfjörð Bjarmason <avarab@gmail.com> | 2020-12-17 17:39:25 +0300 |
---|---|---|
committer | Ævar Arnfjörð Bjarmason <avarab@gmail.com> | 2021-01-12 18:00:54 +0300 |
commit | ea0e36b456646d87e88b3b966aeacdd48e71affe (patch) | |
tree | 10ff2218b68572db5da8815c8e3addfd97652bed | |
parent | 01ba5b2c1c0719d27aced511bd9497b56cd9384a (diff) |
updater: optionally fake the oldvalue for testing
This API allows us to easily inject fake oldvalues into update-ref,
the purpose of which is to easily test ref race conditions without
carefully needing to orchestrate race conditions.
Patrick suggested I could use something like
testhelper.CaptureHookEnv() for this. For update-ref we'd need to have
that more complex since it would need to munge stdin, but more
specifically we'd need to get some out-of-bounds message to it (via a
file) to ask it to map key->value. Not such a big deal now, we could
write this in $PATH:
#!/bin/sh
sed 's/EXPECTED/CHANGED/' | git update-ref "$@"
But once we're going to tread update-ref like "cat-file --batch" (via
the transactions it supports) that becomes painful. I.e. a follow-up
to 154dbc1a2 (updateref: Safeguard against accidentally committing
updates, 2020-12-15) with the new interface in Git 2.30.
So I think doing it this way sucks the least, it's obvious from the
variable name that you shouldn't use it for testing, and we could die
on some AreWeTesting() I don't know about in
fakeOldValueForTesting(). The performance hit of checking a hash map
for whether it has any keys is trivial.
-rw-r--r-- | internal/git/updateref/updateref.go | 29 | ||||
-rw-r--r-- | internal/git/updateref/updateref_test.go | 54 |
2 files changed, 81 insertions, 2 deletions
diff --git a/internal/git/updateref/updateref.go b/internal/git/updateref/updateref.go index 688042925..4ce9d0c8a 100644 --- a/internal/git/updateref/updateref.go +++ b/internal/git/updateref/updateref.go @@ -77,10 +77,35 @@ func (u *Updater) Create(ref, value string) error { return err } +var mungeMapForTesting = make(map[string]string) + +// MungeMapForTestingAdd is a test-only interface to the private +// mungeMapForTesting. Use it for ad-hoc mapping the Update() +// "oldvalue" to some fake value. The munged value will only be used +// once, it's delete()-ed on retrieval. +func MungeMapForTestingAdd(key, value string) { + mungeMapForTesting[key] = value +} + +func fakeOldValueForTesting(oldValue string) string { + fakeOldValue := mungeMapForTesting[oldValue] + if len(fakeOldValue) != 0 { + // Don't persist the value, it's one-use. If we had a + // Del() interface and the test died and forgot to + // cleanup later tests might die at a distance. + delete(mungeMapForTesting, oldValue) + return fakeOldValue + } + return oldValue +} + // Update commands the reference to be updated to point at the sha specified in // newvalue -func (u *Updater) Update(ref, newvalue, oldvalue string) error { - _, err := fmt.Fprintf(u.cmd, "update %s\x00%s\x00%s\x00", ref, newvalue, oldvalue) +func (u *Updater) Update(ref, newvalue, oldValue string) error { + if len(mungeMapForTesting) != 0 { + oldValue = fakeOldValueForTesting(oldValue) + } + _, err := fmt.Fprintf(u.cmd, "update %s\x00%s\x00%s\x00", ref, newvalue, oldValue) return err } diff --git a/internal/git/updateref/updateref_test.go b/internal/git/updateref/updateref_test.go index 905b9e470..1586b1ece 100644 --- a/internal/git/updateref/updateref_test.go +++ b/internal/git/updateref/updateref_test.go @@ -99,6 +99,60 @@ func TestUpdate(t *testing.T) { require.NotEqual(t, commit.Id, parentCommit.Id, "reference was updated when it shouldn't have been") } +func TestUpdateFake(t *testing.T) { + ctx, testRepo, _, teardown := setup(t) + defer teardown() + + locator := config.NewLocator(config.Config) + masterCommit, err := log.GetCommit(ctx, locator, testRepo, "refs/heads/master") + require.NoError(t, err) + + updater, err := New(ctx, testRepo) + require.NoError(t, err) + + ref := "refs/heads/fix" + + // Sanity check: ensure the ref exists before we start + fixCommit, logErr := log.GetCommit(ctx, locator, testRepo, ref) + require.NoError(t, logErr) + require.NotEqual(t, masterCommit.Id, fixCommit.Id, "%s points to %s in the test repository", ref, fixCommit.Id) + + // Updating it from fix->master with a bad oldvalue fails + updateErr := updater.Update(ref, masterCommit.Id, masterCommit.Id) + require.NoError(t, updateErr) + require.Error(t, updater.Wait()) // the error is deferred + updater, err = New(ctx, testRepo) // .Wait() closes it, create a new one + require.NoError(t, err) + + // check that the ref was not updated + fixCommitTwo, logErr := log.GetCommit(ctx, locator, testRepo, ref) + require.NoError(t, logErr) + require.Equal(t, fixCommit.Id, fixCommitTwo.Id) + + // Now fake up the oldvalue + MungeMapForTestingAdd(masterCommit.Id, fixCommit.Id) + + // Updating it from fix->master with a bad oldvalue works, + // since we're lying via MungeMapForTesting + updateErr = updater.Update(ref, masterCommit.Id, masterCommit.Id) + require.NoError(t, updateErr) + require.NoError(t, updater.Wait()) // the error is deferred + updater, err = New(ctx, testRepo) // .Wait() closes it, create a new one + require.NoError(t, err) + + // check that the ref was updated + fixCommitThree, logErr := log.GetCommit(ctx, locator, testRepo, ref) + require.NoError(t, logErr) + require.Equal(t, masterCommit.Id, fixCommitThree.Id) + + // Updating it with masterCommit.Id does not error, since the + // MungeMapForTesting is one-use (it deleted the key after it + // was used) (it would if Del() wasn't called) + updateErr = updater.Update(ref, fixCommit.Id, masterCommit.Id) + require.NoError(t, updateErr) + require.NoError(t, updater.Wait()) // the error is deferred +} + func TestDelete(t *testing.T) { ctx, testRepo, _, teardown := setup(t) defer teardown() |