p4cmd 2.9.2__tar.gz → 2.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {p4cmd-2.9.2 → p4cmd-2.10.1}/PKG-INFO +1 -1
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/__init__.py +1 -1
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/p4cmd.py +16 -2
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/p4errors.py +14 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd.egg-info/PKG-INFO +1 -1
- {p4cmd-2.9.2 → p4cmd-2.10.1}/tests/test_p4client.py +90 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/LICENSE +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/README.md +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/p4file.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/__init__.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_MOVED.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd/utils.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd.egg-info/SOURCES.txt +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd.egg-info/dependency_links.txt +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd.egg-info/requires.txt +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/p4cmd.egg-info/top_level.txt +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/setup.cfg +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/setup.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/tests/__init__.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/tests/conftest.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/tests/test_integration.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/tests/test_p4file.py +0 -0
- {p4cmd-2.9.2 → p4cmd-2.10.1}/tests/test_utils.py +0 -0
|
@@ -84,7 +84,7 @@ class P4Client(object):
|
|
|
84
84
|
"""
|
|
85
85
|
self.max_parallel_connections = value
|
|
86
86
|
|
|
87
|
-
def run_cmd(self, cmd, args=None, file_list=None, use_global_options=True, online_check=True):
|
|
87
|
+
def run_cmd(self, cmd, args=None, file_list=None, use_global_options=True, online_check=True, raise_on_errors=False):
|
|
88
88
|
"""
|
|
89
89
|
Reads the output stream of the command and returns it as a marshaled dict.
|
|
90
90
|
|
|
@@ -145,6 +145,20 @@ class P4Client(object):
|
|
|
145
145
|
dict_list.append(output_dict)
|
|
146
146
|
pipe.kill()
|
|
147
147
|
|
|
148
|
+
# Check for error dicts in the results
|
|
149
|
+
errors = [d for d in dict_list if d.get(b"code") == b"error" or d.get("code") == "error"]
|
|
150
|
+
if errors:
|
|
151
|
+
messages = []
|
|
152
|
+
for e in errors:
|
|
153
|
+
msg = e.get(b"data") or e.get("data") or e.get(b"error") or e.get("error") or "unknown error"
|
|
154
|
+
if isinstance(msg, bytes):
|
|
155
|
+
msg = msg.decode("utf-8", errors="replace")
|
|
156
|
+
messages.append(msg.strip())
|
|
157
|
+
for msg in messages:
|
|
158
|
+
logging.warning(f"p4 {cmd}: {msg}")
|
|
159
|
+
if raise_on_errors:
|
|
160
|
+
raise p4errors.P4CommandError(cmd, messages)
|
|
161
|
+
|
|
148
162
|
return dict_list
|
|
149
163
|
|
|
150
164
|
def __replace_description_in_template(self, template, new_description):
|
|
@@ -1288,7 +1302,7 @@ class P4Client(object):
|
|
|
1288
1302
|
if p4file.local_file_path is None:
|
|
1289
1303
|
data = self.__get_dict_value(file_dict, "data") or ""
|
|
1290
1304
|
if " - " in data:
|
|
1291
|
-
p4file.local_file_path = data.
|
|
1305
|
+
p4file.local_file_path = data.rsplit(" - ", 1)[0].strip()
|
|
1292
1306
|
p4file.have_revision = self.__get_dict_value(file_dict, "haveRev")
|
|
1293
1307
|
p4file.head_revision = self.__get_dict_value(file_dict, "headRev")
|
|
1294
1308
|
p4file.last_submit_time = self.__get_dict_value(file_dict, "headTime")
|
|
@@ -15,6 +15,20 @@ class WorkSpaceError(P4cmdError):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class P4CommandError(P4cmdError):
|
|
19
|
+
"""Raised when a P4 command returns one or more error dicts.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
cmd: The P4 command that failed (e.g. "sync", "edit").
|
|
23
|
+
messages: List of error message strings from the error dicts.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, cmd, messages):
|
|
26
|
+
self.cmd = cmd
|
|
27
|
+
self.messages = messages
|
|
28
|
+
joined = "; ".join(str(m).strip() for m in messages)
|
|
29
|
+
super().__init__(f"p4 {cmd}: {joined}")
|
|
30
|
+
|
|
31
|
+
|
|
18
32
|
class ServerOffline(P4cmdError):
|
|
19
33
|
"""Raised when the Perforce server is unreachable.
|
|
20
34
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Tests for p4cmd/p4cmd.py — P4Client with run_cmd mocked."""
|
|
2
|
+
import logging
|
|
2
3
|
import socket
|
|
3
4
|
from unittest.mock import MagicMock, patch
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
7
8
|
from p4cmd.p4cmd import P4Client
|
|
9
|
+
from p4cmd.p4errors import P4CommandError
|
|
8
10
|
from p4cmd.p4file import P4File, Status
|
|
9
11
|
from tests.conftest import make_fstat_dict
|
|
10
12
|
|
|
@@ -210,6 +212,20 @@ def test_fstat_actionowner_field(p4client):
|
|
|
210
212
|
assert any("charlie@testclient" in entry for entry in files[0].checked_out_by)
|
|
211
213
|
|
|
212
214
|
|
|
215
|
+
def test_fstat_untracked_file_with_dash_in_name(p4client):
|
|
216
|
+
"""fstat_to_p4_files correctly parses untracked file paths containing ' - '."""
|
|
217
|
+
fstat = [
|
|
218
|
+
{
|
|
219
|
+
b"code": b"error",
|
|
220
|
+
b"data": b"D:/p4/games/Animations/my_clip - Copy.pose - no such file(s).",
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
with patch.object(p4client, "find_p4_client", return_value="testclient"):
|
|
224
|
+
files = p4client.fstat_to_p4_files(fstat)
|
|
225
|
+
assert len(files) == 1
|
|
226
|
+
assert files[0].local_file_path == "D:/p4/games/Animations/my_clip - Copy.pose"
|
|
227
|
+
|
|
228
|
+
|
|
213
229
|
def test_fstat_invalid_revision_becomes_none(p4client):
|
|
214
230
|
fstat = [
|
|
215
231
|
make_fstat_dict(
|
|
@@ -577,6 +593,80 @@ def test_get_depot_paths_missing_depot_file(p4client):
|
|
|
577
593
|
assert result == ["//depot/project/file.txt"]
|
|
578
594
|
|
|
579
595
|
|
|
596
|
+
# ---------------------------------------------------------------------------
|
|
597
|
+
# run_cmd error handling
|
|
598
|
+
# ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
def test_run_cmd_logs_warning_on_error_dicts(p4client, caplog):
|
|
601
|
+
"""run_cmd logs a warning for each error dict returned by p4."""
|
|
602
|
+
error_dict = {b"code": b"error", b"data": b"//depot/file.txt - file(s) not on client.\n"}
|
|
603
|
+
ok_dict = {b"code": b"stat", b"depotFile": b"//depot/other.txt"}
|
|
604
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
605
|
+
import marshal as _m, io as _io
|
|
606
|
+
buf = _io.BytesIO()
|
|
607
|
+
_m.dump(error_dict, buf)
|
|
608
|
+
_m.dump(ok_dict, buf)
|
|
609
|
+
buf.seek(0)
|
|
610
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
611
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
612
|
+
mock_popen.return_value.stdout = buf
|
|
613
|
+
mock_popen.return_value.kill = MagicMock()
|
|
614
|
+
with caplog.at_level(logging.WARNING):
|
|
615
|
+
result = p4client.run_cmd("sync", file_list=["//depot/file.txt", "//depot/other.txt"])
|
|
616
|
+
assert len(result) == 2 # both dicts still returned
|
|
617
|
+
assert "file(s) not on client" in caplog.text
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_run_cmd_raise_on_errors(p4client):
|
|
621
|
+
"""run_cmd raises P4CommandError when raise_on_errors=True and errors are present."""
|
|
622
|
+
error_dict = {b"code": b"error", b"data": b"//depot/file.txt - file(s) not on client.\n"}
|
|
623
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
624
|
+
import marshal as _m, io as _io
|
|
625
|
+
buf = _io.BytesIO()
|
|
626
|
+
_m.dump(error_dict, buf)
|
|
627
|
+
buf.seek(0)
|
|
628
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
629
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
630
|
+
mock_popen.return_value.stdout = buf
|
|
631
|
+
mock_popen.return_value.kill = MagicMock()
|
|
632
|
+
with pytest.raises(P4CommandError, match="file\\(s\\) not on client"):
|
|
633
|
+
p4client.run_cmd("sync", file_list=["//depot/file.txt"], raise_on_errors=True)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def test_run_cmd_no_raise_without_errors(p4client):
|
|
637
|
+
"""run_cmd doesn't raise when raise_on_errors=True but no errors are present."""
|
|
638
|
+
ok_dict = {b"code": b"stat", b"depotFile": b"//depot/file.txt"}
|
|
639
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
640
|
+
import marshal as _m, io as _io
|
|
641
|
+
buf = _io.BytesIO()
|
|
642
|
+
_m.dump(ok_dict, buf)
|
|
643
|
+
buf.seek(0)
|
|
644
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
645
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
646
|
+
mock_popen.return_value.stdout = buf
|
|
647
|
+
mock_popen.return_value.kill = MagicMock()
|
|
648
|
+
result = p4client.run_cmd("sync", file_list=["//depot/file.txt"], raise_on_errors=True)
|
|
649
|
+
assert result == [ok_dict]
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def test_run_cmd_handles_string_key_error_dicts(p4client, caplog):
|
|
653
|
+
"""run_cmd handles error dicts with string keys (from ValueError fallback)."""
|
|
654
|
+
# String-keyed error dicts come from the ValueError handler in run_cmd
|
|
655
|
+
error_dict = {b"code": b"error", b"data": b"some error\n"}
|
|
656
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
657
|
+
import marshal as _m, io as _io
|
|
658
|
+
buf = _io.BytesIO()
|
|
659
|
+
_m.dump(error_dict, buf)
|
|
660
|
+
buf.seek(0)
|
|
661
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
662
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
663
|
+
mock_popen.return_value.stdout = buf
|
|
664
|
+
mock_popen.return_value.kill = MagicMock()
|
|
665
|
+
with caplog.at_level(logging.WARNING):
|
|
666
|
+
p4client.run_cmd("sync", file_list=["//depot/file.txt"])
|
|
667
|
+
assert "some error" in caplog.text
|
|
668
|
+
|
|
669
|
+
|
|
580
670
|
def test_get_local_paths_missing_path(p4client):
|
|
581
671
|
"""get_local_paths skips entries where path is missing (e.g. unmapped paths from p4 where)."""
|
|
582
672
|
info_dicts = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|