p4cmd 2.9.1__tar.gz → 2.10.0__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.1 → p4cmd-2.10.0}/PKG-INFO +1 -1
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/__init__.py +1 -1
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/p4cmd.py +17 -1
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/p4errors.py +14 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd.egg-info/PKG-INFO +1 -1
- {p4cmd-2.9.1 → p4cmd-2.10.0}/tests/test_p4client.py +80 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/LICENSE +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/README.md +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/p4file.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/__init__.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_DEPOT_ONLY.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_MOVED.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_NEED_SYNC.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_OPEN_FOR_ADD.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_OPEN_FOR_DELETE.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_OPEN_FOR_EDIT.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_UNKNOWN.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_UNTRACKED.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/resources/perforce_UP_TO_DATE.png +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd/utils.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd.egg-info/SOURCES.txt +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd.egg-info/dependency_links.txt +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd.egg-info/requires.txt +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/p4cmd.egg-info/top_level.txt +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/setup.cfg +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/setup.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/tests/__init__.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/tests/conftest.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/tests/test_integration.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/tests/test_p4file.py +0 -0
- {p4cmd-2.9.1 → p4cmd-2.10.0}/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):
|
|
@@ -1231,6 +1245,8 @@ class P4Client(object):
|
|
|
1231
1245
|
:param changelist: *string* with cl description or *int* cl number
|
|
1232
1246
|
:return:
|
|
1233
1247
|
"""
|
|
1248
|
+
if changelist is None:
|
|
1249
|
+
return "default"
|
|
1234
1250
|
if isinstance(changelist, str):
|
|
1235
1251
|
try:
|
|
1236
1252
|
return int(changelist)
|
|
@@ -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
|
|
|
@@ -109,6 +111,10 @@ def test_ensure_changelist_float(p4client):
|
|
|
109
111
|
assert p4client._P4Client__ensure_changelist(1.5) == 1
|
|
110
112
|
|
|
111
113
|
|
|
114
|
+
def test_ensure_changelist_none(p4client):
|
|
115
|
+
assert p4client._P4Client__ensure_changelist(None) == "default"
|
|
116
|
+
|
|
117
|
+
|
|
112
118
|
def test_ensure_changelist_default_str(p4client):
|
|
113
119
|
# "default" triggers get_or_make_changelist which returns "default"
|
|
114
120
|
with patch.object(p4client, "get_or_make_changelist", return_value="default") as mock_gomc:
|
|
@@ -573,6 +579,80 @@ def test_get_depot_paths_missing_depot_file(p4client):
|
|
|
573
579
|
assert result == ["//depot/project/file.txt"]
|
|
574
580
|
|
|
575
581
|
|
|
582
|
+
# ---------------------------------------------------------------------------
|
|
583
|
+
# run_cmd error handling
|
|
584
|
+
# ---------------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
def test_run_cmd_logs_warning_on_error_dicts(p4client, caplog):
|
|
587
|
+
"""run_cmd logs a warning for each error dict returned by p4."""
|
|
588
|
+
error_dict = {b"code": b"error", b"data": b"//depot/file.txt - file(s) not on client.\n"}
|
|
589
|
+
ok_dict = {b"code": b"stat", b"depotFile": b"//depot/other.txt"}
|
|
590
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
591
|
+
import marshal as _m, io as _io
|
|
592
|
+
buf = _io.BytesIO()
|
|
593
|
+
_m.dump(error_dict, buf)
|
|
594
|
+
_m.dump(ok_dict, buf)
|
|
595
|
+
buf.seek(0)
|
|
596
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
597
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
598
|
+
mock_popen.return_value.stdout = buf
|
|
599
|
+
mock_popen.return_value.kill = MagicMock()
|
|
600
|
+
with caplog.at_level(logging.WARNING):
|
|
601
|
+
result = p4client.run_cmd("sync", file_list=["//depot/file.txt", "//depot/other.txt"])
|
|
602
|
+
assert len(result) == 2 # both dicts still returned
|
|
603
|
+
assert "file(s) not on client" in caplog.text
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def test_run_cmd_raise_on_errors(p4client):
|
|
607
|
+
"""run_cmd raises P4CommandError when raise_on_errors=True and errors are present."""
|
|
608
|
+
error_dict = {b"code": b"error", b"data": b"//depot/file.txt - file(s) not on client.\n"}
|
|
609
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
610
|
+
import marshal as _m, io as _io
|
|
611
|
+
buf = _io.BytesIO()
|
|
612
|
+
_m.dump(error_dict, buf)
|
|
613
|
+
buf.seek(0)
|
|
614
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
615
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
616
|
+
mock_popen.return_value.stdout = buf
|
|
617
|
+
mock_popen.return_value.kill = MagicMock()
|
|
618
|
+
with pytest.raises(P4CommandError, match="file\\(s\\) not on client"):
|
|
619
|
+
p4client.run_cmd("sync", file_list=["//depot/file.txt"], raise_on_errors=True)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def test_run_cmd_no_raise_without_errors(p4client):
|
|
623
|
+
"""run_cmd doesn't raise when raise_on_errors=True but no errors are present."""
|
|
624
|
+
ok_dict = {b"code": b"stat", b"depotFile": b"//depot/file.txt"}
|
|
625
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
626
|
+
import marshal as _m, io as _io
|
|
627
|
+
buf = _io.BytesIO()
|
|
628
|
+
_m.dump(ok_dict, buf)
|
|
629
|
+
buf.seek(0)
|
|
630
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
631
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
632
|
+
mock_popen.return_value.stdout = buf
|
|
633
|
+
mock_popen.return_value.kill = MagicMock()
|
|
634
|
+
result = p4client.run_cmd("sync", file_list=["//depot/file.txt"], raise_on_errors=True)
|
|
635
|
+
assert result == [ok_dict]
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def test_run_cmd_handles_string_key_error_dicts(p4client, caplog):
|
|
639
|
+
"""run_cmd handles error dicts with string keys (from ValueError fallback)."""
|
|
640
|
+
# String-keyed error dicts come from the ValueError handler in run_cmd
|
|
641
|
+
error_dict = {b"code": b"error", b"data": b"some error\n"}
|
|
642
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
643
|
+
import marshal as _m, io as _io
|
|
644
|
+
buf = _io.BytesIO()
|
|
645
|
+
_m.dump(error_dict, buf)
|
|
646
|
+
buf.seek(0)
|
|
647
|
+
mock_popen.return_value.__enter__ = lambda s: s
|
|
648
|
+
mock_popen.return_value.__exit__ = MagicMock(return_value=False)
|
|
649
|
+
mock_popen.return_value.stdout = buf
|
|
650
|
+
mock_popen.return_value.kill = MagicMock()
|
|
651
|
+
with caplog.at_level(logging.WARNING):
|
|
652
|
+
p4client.run_cmd("sync", file_list=["//depot/file.txt"])
|
|
653
|
+
assert "some error" in caplog.text
|
|
654
|
+
|
|
655
|
+
|
|
576
656
|
def test_get_local_paths_missing_path(p4client):
|
|
577
657
|
"""get_local_paths skips entries where path is missing (e.g. unmapped paths from p4 where)."""
|
|
578
658
|
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
|