Skip to content

Commit ef1f220

Browse files
Merge pull request #178 from boschresearch/174-odb-version-mismatch
Check for version mismatch between odbclient and odbserver
2 parents e2e0f55 + 2d81b35 commit ef1f220

File tree

8 files changed

+168
-22
lines changed

8 files changed

+168
-22
lines changed

tools/odbclient/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,23 @@ So far only names made of `ascii` strings are supported. That means that
7777
instance names, node that names and the like containing non-ascii characters
7878
like German umlauts will not work.
7979

80+
81+
## Development
82+
83+
Due to the server client architechture running the unit tests is not completely
84+
trivial. Here are some instructions on how to get them running.
85+
86+
### Setting up the environments
87+
88+
As of now, we are assuming that conda is used to setup the server
89+
environments. Probably we will change for `uv` in the future.
90+
91+
We provide a bash script in `tests/create_server_envs.sh` that you can run from
92+
within the root folder of `odbclient` (the folder this `README.md` resides
93+
in). Then it should generate all the necessary environments. You will have to
94+
run the script again, if there has been a release update in between.
95+
96+
The script is not well tested. So please be prepared for some manual steps.
97+
8098
___
8199
[1]: https://pylife.readthedocs.io/en/latest/tools/odbclient/odbclient.html

tools/odbclient/src/odbclient/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
try:
2525
# Change here if project is renamed and does not equal the package name
26-
dist_name = __name__
26+
dist_name = "pylife-odbclient"
2727
__version__ = version(dist_name)
2828
except PackageNotFoundError: # pragma: no cover
2929
__version__ = "unknown"

tools/odbclient/src/odbclient/odbclient.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import numpy as np
3232
import pandas as pd
3333

34+
import odbclient
35+
3436

3537
class OdbServerError(Exception):
3638
"""Raised when the ODB Server launch fails."""
@@ -148,7 +150,13 @@ def __init__(self, odb_file, abaqus_bin=None, python_env_path=None):
148150
if lock_file_exists:
149151
self._gulp_lock_file_warning()
150152

151-
self._wait_for_server_ready_sign()
153+
server_version, server_python_version = self._wait_for_server_ready_sign()
154+
_raise_if_version_mismatch(server_version)
155+
if server_python_version == "2":
156+
self._parse_response = self._parse_response_py2
157+
else:
158+
self._parse_response = self._parse_response_py3
159+
152160

153161
def _gulp_lock_file_warning(self):
154162
self._proc.stdout.readline()
@@ -158,6 +166,8 @@ def _wait_for_server_ready_sign(self):
158166
def wait_for_input(stdout, queue):
159167
sign = stdout.read(5)
160168
queue.put(sign)
169+
version_sign = stdout.readline()
170+
queue.put(version_sign)
161171

162172
queue = QU.Queue()
163173
thread = THR.Thread(target=wait_for_input, args=(self._proc.stdout, queue))
@@ -173,6 +183,12 @@ def wait_for_input(stdout, queue):
173183
else:
174184
if sign != b'ready':
175185
raise OdbServerError("Expected ready sign from server, received %s" % sign)
186+
187+
try:
188+
sign = queue.get_nowait()
189+
return _ascii(_decode, sign).strip().split()
190+
except QU.Empty:
191+
return "unannounced", "2"
176192
return
177193

178194
def instance_names(self):
@@ -203,8 +219,11 @@ def node_coordinates(self, instance_name, nset_name=''):
203219
"""
204220
self._fail_if_instance_invalid(instance_name)
205221
index, node_data = self._query('get_nodes', (instance_name, nset_name))
206-
return pd.DataFrame(data=node_data, columns=['x', 'y', 'z'],
207-
index=pd.Index(index, name='node_id', dtype=np.int64))
222+
return pd.DataFrame(
223+
data=node_data,
224+
columns=['x', 'y', 'z'],
225+
index=pd.Index(index, name='node_id', dtype=np.int64),
226+
)
208227

209228
def element_connectivity(self, instance_name, elset_name=''):
210229
"""Query the element connectivity of an instance.
@@ -447,6 +466,7 @@ def history_region_description(self, step_name, history_region_name):
447466
"""Query the description of a history Regions of a given step.
448467
449468
Parameters
469+
450470
----------
451471
step_name : string
452472
The name of the step
@@ -491,11 +511,21 @@ def _send_command(self, command, args=None):
491511
pickle.dump((command, args), self._proc.stdin, protocol=2)
492512
self._proc.stdin.flush()
493513

494-
def _parse_response(self):
495-
expected_size, = struct.unpack("Q", self._proc.stdout.read(8))
496-
pickle_data = self._proc.stdout.read(expected_size)
514+
def _parse_response_py2(self):
515+
pickle_data = b''
516+
while True:
517+
line = self._proc.stdout.readline().rstrip() + b'\n'
518+
pickle_data += line
519+
if line == b'.\n':
520+
break
497521
return pickle.loads(pickle_data, encoding='bytes')
498522

523+
def _parse_response_py3(self):
524+
msg = self._proc.stdout.read(8)
525+
expected_size, = struct.unpack("Q", msg)
526+
pickle_data = self._proc.stdout.read(expected_size)
527+
return pickle.loads(pickle_data)
528+
499529
def __del__(self):
500530
if self._proc is not None:
501531
self._send_command('QUIT')
@@ -536,6 +566,7 @@ def _decode(arg):
536566
return [_decode(element) for element in arg]
537567
return arg
538568

569+
539570
def _guess_abaqus_bin():
540571
if sys.platform == 'win32':
541572
return _guess_abaqus_bin_windows()
@@ -582,9 +613,25 @@ def _determine_server_python_version(abaqus_bin):
582613
return version_string[:version_string.rfind(".")]
583614

584615

585-
586616
def _guess_python_env_path(python_env_path):
587617
cand = python_env_path or os.path.join(os.environ['HOME'], '.conda', 'envs', 'odbserver')
588618
if os.path.exists(cand):
589619
return cand
590620
return None
621+
622+
623+
def _raise_if_version_mismatch(server_version):
624+
def strip_version(version):
625+
pos = version.find(".post")
626+
if pos == -1:
627+
return version
628+
return version[:pos]
629+
630+
server_version = strip_version(server_version)
631+
client_version = strip_version(odbclient.__version__)
632+
633+
if client_version != server_version:
634+
raise RuntimeError(
635+
"Version mismatch: "
636+
f"odbserver version {server_version} != odbclient version {client_version}"
637+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/bash
2+
3+
4+
if [ "$(uname)" == "Linux" ]; then
5+
CONDABASE=`conda info --base`
6+
CONDA_SH=${CONDABASE}"/etc/profile.d/conda.sh"
7+
source $CONDA_SH
8+
else
9+
eval "$('/c/Program Files/Anaconda3/Scripts/conda.exe' 'shell.bash' 'hook')"
10+
fi
11+
12+
conda create -n odbserver-2022 python=2.7 --yes
13+
conda activate odbserver-2022
14+
pip install -e ../odbserver
15+
16+
conda create -n odbserver-2023 python=2.7 --yes
17+
conda activate odbserver-2023
18+
pip install -e ../odbserver
19+
20+
conda create -n odbserver-2024 python=3.10 --yes
21+
conda activate odbserver-2024
22+
pip install -e ../odbserver
23+
24+
conda create -n odbserver-2022-version-mismatch python=2.7 --yes
25+
conda activate odbserver-2022-version-mismatch
26+
pip install pylife-odbserver
27+
28+
conda create -n odbserver-2023-version-mismatch python=2.7 --yes
29+
conda activate odbserver-2023-version-mismatch
30+
pip install pylife-odbserver
31+
32+
conda create -n odbserver-2024-version-mismatch python=3.10 --yes
33+
conda activate odbserver-2024-version-mismatch
34+
pip install pylife-odbserver==2.2.0a6

tools/odbclient/tests/test_odbclient.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
import pytest
2626
import json
2727
import shutil
28+
29+
import subprocess
30+
2831
from pathlib import Path
32+
import pylife
2933

3034
import numpy as np
3135
import pandas as pd
@@ -34,6 +38,7 @@
3438

3539
from odbclient.odbclient import OdbServerError
3640

41+
3742
@pytest.fixture
3843
def datapath():
3944
base = os.path.dirname(__file__)
@@ -44,11 +49,16 @@ def join_path(filename):
4449
return join_path
4550

4651

47-
@pytest.fixture(params=["2022", "2023", "2024"])
52+
@pytest.fixture(params=["2022", "2023", "2024"], scope="session")
4853
def abaqus_version(request):
4954
return request.param
5055

5156

57+
@pytest.fixture(scope="session")
58+
def pyenvs(abaqus_version):
59+
return os.path.join(Path.home(), ".conda", "envs", f"odbserver-{abaqus_version}")
60+
61+
5262
@pytest.fixture
5363
def abaqus_bin(abaqus_version):
5464
if sys.platform == 'win32':
@@ -57,12 +67,16 @@ def abaqus_bin(abaqus_version):
5767

5868

5969
@pytest.fixture
60-
def client(datapath, abaqus_version, abaqus_bin):
61-
python_path = os.path.join(Path.home(), ".conda", "envs", f"odbserver-{abaqus_version}")
70+
def client(datapath, pyenvs, abaqus_version, abaqus_bin):
71+
python_path = pyenvs
6272
odb_file = datapath(f"beam_3d_hex_quad-{abaqus_version}.odb")
6373
return odbclient.OdbClient(odb_file, abaqus_bin=abaqus_bin, python_env_path=python_path)
6474

6575

76+
def test_odbclient_version():
77+
assert odbclient.__version__ == pylife.__version__
78+
79+
6680
def test_not_existing_odbserver_env():
6781
with pytest.raises(OSError, match="No odbserver environment found."):
6882
odbclient.OdbClient('foo.odb', python_env_path='/foo/bar/env')
@@ -73,6 +87,15 @@ def test_not_existing_abaqus_path():
7387
odbclient.OdbClient('foo.odb', abaqus_bin='/foo/bar/abaqus')
7488

7589

90+
91+
def test_version_mismatch(datapath, abaqus_version, abaqus_bin):
92+
python_path = os.path.join(Path.home(), ".conda", "envs", f"odbserver-{abaqus_version}-version-mismatch")
93+
odb_file = datapath(f"beam_3d_hex_quad-{abaqus_version}.odb")
94+
with pytest.raises(RuntimeError, match="Version mismatch"):
95+
odbclient.OdbClient(odb_file, abaqus_bin=abaqus_bin, python_env_path=python_path)
96+
97+
98+
7699
def test_odbclient_instances(client):
77100
np.testing.assert_array_equal(client.instance_names(), ['PART-1-1'])
78101

@@ -84,7 +107,10 @@ def test_odbclient_invalid_instance(client):
84107

85108
def test_odbclient_node_coordinates(client, datapath):
86109
expected = pd.read_csv(datapath('node_coordinates.csv'), index_col='node_id')
87-
pd.testing.assert_frame_equal(client.node_coordinates('PART-1-1'), expected)
110+
result = client.node_coordinates('PART-1-1')
111+
print("comparing")
112+
pd.testing.assert_frame_equal(result, expected)
113+
print("test finished")
88114

89115

90116
def test_odbclient_node_ids(client):
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pkg_resources
22

33
try:
4-
__version__ = pkg_resources.get_distribution(__name__).version
4+
__version__ = pkg_resources.get_distribution("pylife-odbserver").version
55
except:
66
__version__ = 'unknown'

tools/odbserver/odbserver/__main__.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import numpy as np
2828

2929
from .interface import OdbInterface
30+
import odbserver
3031

3132
class OdbServer:
3233

@@ -124,12 +125,24 @@ def history_info(self, args):
124125
_send_response(self._odb.history_info())
125126

126127

127-
def _send_response(pickle_data, numpy_arrays=None):
128-
stdout = sys.stdout if sys.version_info.major == 2 else sys.stdout.buffer
128+
def _send_response_py2(pickle_data, numpy_arrays=None):
129+
numpy_arrays = numpy_arrays or []
130+
s = pickle.dumps((len(numpy_arrays), pickle_data))
131+
sys.stdout.write(s + '\n')
132+
sys.stdout.flush()
133+
for nparr in numpy_arrays:
134+
np.lib.format.write_array(sys.stdout, nparr)
135+
136+
137+
def _send_response_py3(pickle_data, numpy_arrays=None):
138+
stdout = os.fdopen(sys.stdout.fileno(), 'wb', closefd=False)
129139
numpy_arrays = numpy_arrays or []
130140

131-
message = pickle.dumps((len(numpy_arrays), pickle_data), protocol=2)
132-
data_size_8_bytes = struct.pack("Q", len(message))
141+
message = pickle.dumps((len(numpy_arrays), pickle_data))
142+
143+
data_size = len(message)
144+
145+
data_size_8_bytes = struct.pack("Q", data_size)
133146

134147
stdout.write(data_size_8_bytes)
135148
stdout.write(message)
@@ -138,6 +151,13 @@ def _send_response(pickle_data, numpy_arrays=None):
138151
np.lib.format.write_array(sys.stdout, nparr)
139152

140153

154+
if sys.version_info.major == 2:
155+
_send_response = _send_response_py2
156+
else:
157+
_send_response = _send_response_py3
158+
159+
160+
141161
def main():
142162

143163
def decode_strings_if_not_on_python_2(parameters):
@@ -164,7 +184,8 @@ def pickle_load_3():
164184
print(str(e), file=sys.stderr)
165185
sys.exit(1)
166186

167-
sys.stdout.write('ready')
187+
ready_message = "ready %s %s\n" % (odbserver.__version__, sys.version_info.major)
188+
sys.stdout.write(ready_message)
168189
sys.stdout.flush()
169190

170191
command = ''
@@ -176,10 +197,9 @@ def pickle_load_3():
176197
if command == 'QUIT':
177198
break
178199

179-
func = server.command_dict.get(command)
180-
if func is not None:
181-
func(parameters)
182-
sys.stdout.flush()
200+
func = server.command_dict.get(command, )
201+
func(parameters)
202+
sys.stdout.flush()
183203

184204

185205
if __name__ == "__main__":

tools/odbserver/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def setup_package():
1919
scm_version_setup = {
2020
"root": "../..",
2121
"relative_to": __file__,
22+
"version_scheme": "no-guess-dev",
2223
}
2324
if os.environ.get("CI") == "true":
2425
scm_version_setup.update({"local_scheme": "no-local-version"})

0 commit comments

Comments
 (0)