Skip to content

Commit 9709747

Browse files
authored
Merge pull request #1748 from Bastian-Krause/bst/acquire-all-remoteplaces
remote/client: make acquire/release operate on all RemotePlaces in env
2 parents aa9550d + 829079f commit 9709747

File tree

3 files changed

+123
-18
lines changed

3 files changed

+123
-18
lines changed

labgrid/remote/client.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import attr
2929
import grpc
3030

31+
# TODO: drop if Python >= 3.11 guaranteed
32+
from exceptiongroup import ExceptionGroup # pylint: disable=redefined-builtin
33+
3134
from .common import (
3235
ResourceEntry,
3336
ResourceMatch,
@@ -57,7 +60,8 @@
5760

5861

5962
class Error(Exception):
60-
pass
63+
def __str__(self):
64+
return f"Error: {' '.join(self.args)}"
6165

6266

6367
class UserError(Error):
@@ -72,6 +76,13 @@ class InteractiveCommandError(Error):
7276
pass
7377

7478

79+
class ErrorGroup(ExceptionGroup):
80+
def __str__(self):
81+
# TODO: drop pylint disable once https://github.com/pylint-dev/pylint/issues/8985 is fixed
82+
errors_combined = "\n".join(f"- {' '.join(e.args)}" for e in self.exceptions) # pylint: disable=not-an-iterable
83+
return f"{self.message}:\n{errors_combined}"
84+
85+
7586
@attr.s(eq=False)
7687
class ClientSession:
7788
"""The ClientSession encapsulates all the actions a Client can invoke on
@@ -481,6 +492,17 @@ def get_place(self, place=None):
481492
raise UserError(f"pattern {pattern} matches multiple places ({', '.join(places)})")
482493
return self.places[places[0]]
483494

495+
def get_place_names_from_env(self):
496+
"""Returns a list of RemotePlace names found in the environment config."""
497+
places = []
498+
for role_config in self.env.config.get_targets().values():
499+
resources, _ = target_factory.normalize_config(role_config)
500+
remote_places = resources.get("RemotePlace", [])
501+
for place in remote_places:
502+
places.append(place)
503+
504+
return places
505+
484506
def get_idle_place(self, place=None):
485507
place = self.get_place(place)
486508
if place.acquired:
@@ -684,17 +706,31 @@ def check_matches(self, place):
684706
raise UserError(f"Match {match} has no matching remote resource")
685707

686708
async def acquire(self):
709+
errors = []
710+
places = self.get_place_names_from_env() if self.env else [self.args.place]
711+
for place in places:
712+
try:
713+
await self._acquire_place(place)
714+
except Error as e:
715+
errors.append(e)
716+
717+
if errors:
718+
if len(errors) == 1:
719+
raise errors[0]
720+
raise ErrorGroup("Multiple errors occurred during acquire", errors)
721+
722+
async def _acquire_place(self, place):
687723
"""Acquire a place, marking it unavailable for other clients"""
688-
place = self.get_place()
724+
place = self.get_place(place)
689725
if place.acquired:
690726
host, user = place.acquired.split("/")
691727
allowhelp = f"'labgrid-client -p {place.name} allow {self.gethostname()}/{self.getuser()}' on {host}."
692728
if self.getuser() == user:
693729
if self.gethostname() == host:
694-
raise UserError("You have already acquired this place.")
730+
raise UserError(f"You have already acquired place {place.name}.")
695731
else:
696732
raise UserError(
697-
f"You have already acquired this place on {host}. To work simultaneously, execute {allowhelp}"
733+
f"You have already acquired place {place.name} on {host}. To work simultaneously, execute {allowhelp}"
698734
)
699735
else:
700736
raise UserError(
@@ -730,8 +766,22 @@ async def acquire(self):
730766
raise ServerError(e.details())
731767

732768
async def release(self):
769+
errors = []
770+
places = self.get_place_names_from_env() if self.env else [self.args.place]
771+
for place in places:
772+
try:
773+
await self._release_place(place)
774+
except Error as e:
775+
errors.append(e)
776+
777+
if errors:
778+
if len(errors) == 1:
779+
raise errors[0]
780+
raise ErrorGroup("Multiple errors occurred during release", errors)
781+
782+
async def _release_place(self, place):
733783
"""Release a previously acquired place"""
734-
place = self.get_place()
784+
place = self.get_place(place)
735785
if not place.acquired:
736786
raise UserError(f"place {place.name} is not acquired")
737787
_, user = place.acquired.split("/")
@@ -2215,11 +2265,11 @@ def main():
22152265
if args.debug:
22162266
traceback.print_exc(file=sys.stderr)
22172267
exitcode = e.exitcode
2218-
except Error as e:
2268+
except (Error, ErrorGroup) as e:
22192269
if args.debug:
22202270
traceback.print_exc(file=sys.stderr)
22212271
else:
2222-
print(f"{parser.prog}: error: {e}", file=sys.stderr)
2272+
print(f"{parser.prog}: {e}", file=sys.stderr)
22232273
exitcode = 1
22242274
except KeyboardInterrupt:
22252275
exitcode = 1

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ classifiers = [
3434
]
3535
dependencies = [
3636
"attrs>=21.4.0",
37+
"exceptiongroup>=1.3.0", # TODO: drop if Python >= 3.11 guaranteed
3738
"grpcio>=1.64.1, <2.0.0",
3839
"grpcio-reflection>=1.64.1, <2.0.0",
3940
"protobuf>=5.27.0",

tests/test_client.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,33 @@ def test_startup(coordinator):
99
pass
1010

1111
@pytest.fixture(scope='function')
12-
def place(coordinator):
13-
with pexpect.spawn('python -m labgrid.remote.client -p test create') as spawn:
14-
spawn.expect(pexpect.EOF)
15-
spawn.close()
12+
def place(create_place):
13+
create_place('test')
14+
15+
@pytest.fixture(scope='function')
16+
def create_place(coordinator):
17+
place_names = []
18+
19+
def _create_place(place_name):
20+
with pexpect.spawn(f'python -m labgrid.remote.client -p {place_name} create') as spawn:
21+
spawn.expect(pexpect.EOF)
1622
assert spawn.exitstatus == 0, spawn.before.strip()
1723

18-
with pexpect.spawn('python -m labgrid.remote.client -p test set-tags board=123board') as spawn:
19-
spawn.expect(pexpect.EOF)
20-
spawn.close()
24+
place_names.append(place_name)
25+
26+
with pexpect.spawn(f'python -m labgrid.remote.client -p {place_name} set-tags board=123board') as spawn:
27+
spawn.expect(pexpect.EOF)
2128
assert spawn.exitstatus == 0, spawn.before.strip()
2229

23-
yield
30+
yield _create_place
2431

25-
with pexpect.spawn('python -m labgrid.remote.client -p test delete') as spawn:
26-
spawn.expect(pexpect.EOF)
27-
spawn.close()
32+
for place_name in place_names:
33+
# clean up
34+
with pexpect.spawn(f'python -m labgrid.remote.client -p {place_name} release') as spawn:
35+
spawn.expect(pexpect.EOF)
36+
37+
with pexpect.spawn(f'python -m labgrid.remote.client -p {place_name} delete') as spawn:
38+
spawn.expect(pexpect.EOF)
2839
assert spawn.exitstatus == 0, spawn.before.strip()
2940

3041
@pytest.fixture(scope='function')
@@ -151,6 +162,49 @@ def test_place_acquire(place):
151162
spawn.close()
152163
assert spawn.exitstatus == 0, spawn.before.strip()
153164

165+
def test_place_acquire_multiple(create_place, tmpdir):
166+
# create multiple places
167+
place_names = ['test1', 'test2']
168+
for place_name in place_names:
169+
create_place(place_name)
170+
171+
# create env config with multiple RemotePlaces
172+
p = tmpdir.join('config.yaml')
173+
p.write('targets:')
174+
for place_name in place_names:
175+
p.write(
176+
f"""
177+
{place_name}:
178+
resources:
179+
RemotePlace:
180+
name: {place_name}
181+
""",
182+
mode='a',
183+
)
184+
185+
# acquire all places in env config
186+
with pexpect.spawn(f'python -m labgrid.remote.client -c {p} acquire') as spawn:
187+
spawn.expect(pexpect.EOF)
188+
assert spawn.exitstatus == 0, spawn.before.strip()
189+
190+
# check 'who'
191+
with pexpect.spawn('python -m labgrid.remote.client who') as spawn:
192+
spawn.expect(pexpect.EOF)
193+
for place_name in place_names:
194+
assert place_name.encode('utf-8') in spawn.before
195+
196+
assert spawn.exitstatus == 0, spawn.before.strip()
197+
198+
# release all places in env config
199+
with pexpect.spawn(f'python -m labgrid.remote.client -c {p} release') as spawn:
200+
spawn.expect(pexpect.EOF)
201+
assert spawn.exitstatus == 0, spawn.before.strip()
202+
203+
# check 'who' again
204+
with pexpect.spawn('python -m labgrid.remote.client who') as spawn:
205+
spawn.expect('User.*Host.*Place.*Changed\r\n')
206+
assert not spawn.before, spawn.before
207+
154208
def test_place_acquire_enforce(place):
155209
with pexpect.spawn('python -m labgrid.remote.client -p test add-match does/not/exist') as spawn:
156210
spawn.expect(pexpect.EOF)

0 commit comments

Comments
 (0)