From b3d41e7dad79e04db1d264d494144d33848be472 Mon Sep 17 00:00:00 2001 From: Carl Johnson Date: Mon, 21 Sep 2020 11:20:17 +0100 Subject: [PATCH] Allow stream-switching when re-using a client on new host (#189) --- python/fixture/server.zip | Bin 892309 -> 896055 bytes python/perforce.py | 19 +++++++++++++++++- python/test_perforce.py | 41 ++++++++++++++++++++++++++++++++++---- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/python/fixture/server.zip b/python/fixture/server.zip index 23b7c4816497f3eb2dfde442179de6f9db39210b..2a8fb0f41e408db0a08e691ec936d9d7e15fc515 100644 GIT binary patch delta 6005 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 diff --git a/python/perforce.py b/python/perforce.py index fd27b0c..496879a 100644 --- a/python/perforce.py +++ b/python/perforce.py @@ -77,6 +77,22 @@ def insert_clientname(mapping): return '%s //%s/%s' % (depot, clientname, local) return [insert_clientname(mapping) for mapping in view] + def _flush_to_previous_client(self, current_client, prev_clientname): + """Flush a new client to match existing workspace data from a previous client""" + prev_client = self.perforce.fetch_client(prev_clientname) + stream_switch = self.stream and prev_client._stream != self.stream + if stream_switch: + self.perforce.logger.info("previous client stream %s does not match %s, switching stream temporarily to flush" % (prev_client._stream, self.stream)) + current_client._stream = prev_client._stream + self.perforce.save_client(current_client) + + self.perforce.run_flush(['//...@%s' % prev_clientname]) + + if stream_switch: + self.perforce.logger.info("switching stream back to %s" % self.stream) + current_client._stream = self.stream + self.perforce.save_client(current_client) + def _setup_client(self): """Creates or re-uses the client workspace for this machine""" # pylint: disable=protected-access @@ -107,7 +123,8 @@ def _setup_client(self): if line.startswith('P4CLIENT=')) if prev_clientname != clientname: self.perforce.logger.warning("p4config last client was %s, flushing workspace to match" % prev_clientname) - self.perforce.run_flush(['//...@%s' % prev_clientname]) + self._flush_to_previous_client(client, prev_clientname) + elif 'Update' in client: # client was accessed previously self.perforce.logger.warning("p4config missing for previously accessed client workspace. flushing to revision zero") self.perforce.run_flush(['//...@0']) diff --git a/python/test_perforce.py b/python/test_perforce.py index 54c67b7..c64a6a4 100644 --- a/python/test_perforce.py +++ b/python/test_perforce.py @@ -106,6 +106,7 @@ def test_fixture(capsys, server): assert depotfile_to_content == { "//depot/file.txt": "Hello World\n", "//stream-depot/main/file.txt": "Hello Stream World\n", + "//stream-depot/main/file_2.txt": "file_2\n", "//stream-depot/dev/file.txt": "Hello Stream World (dev)\n", } @@ -143,6 +144,11 @@ def test_fixture(capsys, server): 'action': ['edit'], 'depotFile': ['//stream-depot/dev/file.txt'], 'desc': 'Update contents of //stream-depot/dev/file.txt\n' + }, + '9': { + 'action': ['add'], + 'depotFile': ['//stream-depot/main/file_2.txt'], + 'desc': 'file_2.txt - exists in main but not dev\n' } } @@ -188,11 +194,12 @@ def test_fixture(capsys, server): def test_head(server, tmpdir): """Test resolve of HEAD changelist""" - repo = P4Repo(root=tmpdir) - assert repo.head() == "@6", "Unexpected global HEAD revision" + # workspace with no changes in view defaults to global view + repo = P4Repo(root=tmpdir, view="//depot/empty_dir/... empty_dir/...") + assert repo.head() == "@9", "Unexpected global HEAD revision" - repo = P4Repo(root=tmpdir, stream='//stream-depot/main') - assert repo.head() == "@2", "Unexpected HEAD revision for stream" + repo = P4Repo(root=tmpdir, stream='//stream-depot/dev') + assert repo.head() == "@8", "Unexpected HEAD revision for stream" repo = P4Repo(root=tmpdir, stream='//stream-depot/idontexist') with pytest.raises(Exception, match=r"Stream '//stream-depot/idontexist' doesn't exist."): @@ -413,6 +420,8 @@ def test_stream_switching(server, tmpdir): repo = P4Repo(root=tmpdir, stream='//stream-depot/main') synced = repo.sync() assert len(synced) > 0, "Didn't sync any files" + assert set(os.listdir(tmpdir)) == set([ + "file.txt", "file_2.txt", "p4config"]) with open(os.path.join(tmpdir, "file.txt")) as content: assert content.read() == "Hello Stream World\n", "Unexpected content in workspace file" @@ -420,9 +429,33 @@ def test_stream_switching(server, tmpdir): repo = P4Repo(root=tmpdir, stream='//stream-depot/dev') repo.sync() assert len(synced) > 0, "Didn't sync any files" + assert set(os.listdir(tmpdir)) == set([ + "file.txt", "p4config"]) # file_2.txt was de-synced with open(os.path.join(tmpdir, "file.txt")) as content: assert content.read() == "Hello Stream World (dev)\n", "Unexpected content in workspace file" +def test_stream_switching_migration(server, tmpdir): + """Test stream-switching and client migration simultaneously""" + repo = P4Repo(root=tmpdir, stream='//stream-depot/main') + synced = repo.sync() + assert len(synced) > 0, "Didn't sync any files" + assert set(os.listdir(tmpdir)) == set([ + "file.txt", "file_2.txt", "p4config"]) + with open(os.path.join(tmpdir, "file.txt")) as content: + assert content.read() == "Hello Stream World\n", "Unexpected content in workspace file" + + with tempfile.TemporaryDirectory(prefix="bk-p4-test-") as second_client: + copytree(tmpdir, second_client) + # Client names include path on disk, so this creates a new unique client + # Re-use the same checkout directory and switch streams at the same time + repo = P4Repo(root=second_client, stream='//stream-depot/dev') + repo.sync() + assert len(synced) > 0, "Didn't sync any files" + assert set(os.listdir(second_client)) == set([ + "file.txt", "p4config"]) # file_2.txt was de-synced + with open(os.path.join(second_client, "file.txt")) as content: + assert content.read() == "Hello Stream World (dev)\n", "Unexpected content in workspace file" + # def test_live_server(): # """Reproduce production issues quickly by writing tests which run against a real server"""