Skip to content

Commit 476ee00

Browse files
authored
Merge branch 'dev' into fix-exception-handling-and-logging
2 parents de1fcc3 + f011c27 commit 476ee00

9 files changed

Lines changed: 198 additions & 15 deletions

File tree

.github/workflows/tests.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main, dev]
6+
pull_request:
7+
branches: [main, dev]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12']
15+
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install -r requirements-dev.txt
29+
pip install pyzmq
30+
31+
- name: Run tests
32+
run: pytest -v

.gitignore

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
1-
**/.DS_Store
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.class
5+
*.so
6+
.Python
7+
venv/
8+
env/
9+
ENV/
10+
11+
# IDE
12+
.vscode/
13+
.idea/
14+
15+
# Testing
16+
.pytest_cache/
17+
htmlcov/
18+
.coverage
19+
20+
# Concore specific
21+
concorekill.bat

concore.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,24 @@ def terminate_zmq():
105105
logging.error(f"Error while terminating ZMQ port {port.address}: {e}")
106106
# --- ZeroMQ Integration End ---
107107

108+
109+
# NumPy Type Conversion Helper
110+
def convert_numpy_to_python(obj):
111+
#Recursively convert numpy types to native Python types.
112+
#This is necessary because literal_eval cannot parse numpy representations
113+
#like np.float64(1.0), but can parse native Python types like 1.0.
114+
if isinstance(obj, np.generic):
115+
# Convert numpy scalar types to Python native types
116+
return obj.item()
117+
elif isinstance(obj, list):
118+
return [convert_numpy_to_python(item) for item in obj]
119+
elif isinstance(obj, tuple):
120+
return tuple(convert_numpy_to_python(item) for item in obj)
121+
elif isinstance(obj, dict):
122+
return {key: convert_numpy_to_python(value) for key, value in obj.items()}
123+
else:
124+
return obj
125+
108126
# ===================================================================
109127
# File & Parameter Handling
110128
# ===================================================================
@@ -130,13 +148,15 @@ def safe_literal_eval(filename, defaultValue):
130148
inpath = "./in" #must be rel path for local
131149
outpath = "./out"
132150
simtime = 0
151+
concore_params_file = os.path.join(inpath + "1", "concore.params")
152+
concore_maxtime_file = os.path.join(inpath + "1", "concore.maxtime")
133153

134154
#9/21/22
135155
# ===================================================================
136156
# Parameter Parsing
137157
# ===================================================================
138158
try:
139-
sparams_path = os.path.join(inpath + "1", "concore.params")
159+
sparams_path = concore_params_file
140160
if os.path.exists(sparams_path):
141161
with open(sparams_path, "r") as f:
142162
sparams = f.read()
@@ -176,8 +196,7 @@ def tryparam(n, i):
176196
def default_maxtime(default):
177197
"""Read maximum simulation time from file or use default."""
178198
global maxtime
179-
maxtime_path = os.path.join(inpath + "1", "concore.maxtime")
180-
maxtime = safe_literal_eval(maxtime_path, default)
199+
maxtime = safe_literal_eval(concore_maxtime_file, default)
181200

182201
default_maxtime(100)
183202

@@ -225,14 +244,15 @@ def read(port_identifier, name, initstr_val):
225244
return default_return_val
226245

227246
time.sleep(delay)
228-
file_path = os.path.join(inpath+str(file_port_num), name)
247+
file_path = os.path.join(inpath + str(file_port_num), name)
229248
ins = ""
230249

231250
try:
232251
with open(file_path, "r") as infile:
233252
ins = infile.read()
234253
except FileNotFoundError:
235254
ins = str(initstr_val)
255+
s += ins # Update s to break unchanged() loop
236256
except Exception as e:
237257
logging.error(f"Error reading {file_path}: {e}. Using default value.")
238258
return default_return_val
@@ -291,11 +311,8 @@ def write(port_identifier, name, val, delta=0):
291311

292312
# Case 2: File-based port
293313
try:
294-
if isinstance(port_identifier, str) and port_identifier in zmq_ports:
295-
file_path = os.path.join("../"+port_identifier, name)
296-
else:
297-
file_port_num = int(port_identifier)
298-
file_path = os.path.join(outpath+str(file_port_num), name)
314+
file_port_num = int(port_identifier)
315+
file_path = os.path.join(outpath + str(file_port_num), name)
299316
except ValueError:
300317
logging.error(f"Error: Invalid port identifier '{port_identifier}' for file operation. Must be integer or ZMQ name.")
301318
return
@@ -310,7 +327,9 @@ def write(port_identifier, name, val, delta=0):
310327
try:
311328
with open(file_path, "w") as outfile:
312329
if isinstance(val, list):
313-
data_to_write = [simtime + delta] + val
330+
# Convert numpy types to native Python types
331+
val_converted = convert_numpy_to_python(val)
332+
data_to_write = [simtime + delta] + val_converted
314333
outfile.write(str(data_to_write))
315334
simtime += delta
316335
else:

concoredocker.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ def safe_literal_eval(filename, defaultValue):
2121
inpath = os.path.abspath("/in")
2222
outpath = os.path.abspath("/out")
2323
simtime = 0
24+
concore_params_file = os.path.join(inpath, "1", "concore.params")
25+
concore_maxtime_file = os.path.join(inpath, "1", "concore.maxtime")
2426

2527
#9/21/22
2628
try:
27-
sparams = open(inpath+"1/concore.params").read()
29+
sparams = open(concore_params_file).read()
2830
if sparams[0] == '"': #windows keeps "" need to remove
2931
sparams = sparams[1:]
3032
sparams = sparams[0:sparams.find('"')]
@@ -46,7 +48,7 @@ def tryparam(n, i):
4648
#9/12/21
4749
def default_maxtime(default):
4850
global maxtime
49-
maxtime = safe_literal_eval(os.path.join(inpath, "1", "concore.maxtime"), default)
51+
maxtime = safe_literal_eval(concore_maxtime_file, default)
5052

5153
default_maxtime(100)
5254

@@ -62,7 +64,7 @@ def read(port, name, initstr):
6264
global s, simtime, retrycount
6365
max_retries=5
6466
time.sleep(delay)
65-
file_path = os.path.join(inpath+str(port), name)
67+
file_path = os.path.join(inpath, str(port), name)
6668

6769
try:
6870
with open(file_path, "r") as infile:
@@ -101,7 +103,7 @@ def read(port, name, initstr):
101103

102104
def write(port, name, val, delta=0):
103105
global simtime
104-
file_path = os.path.join(outpath+str(port), name)
106+
file_path = os.path.join(outpath, str(port), name)
105107

106108
if isinstance(val, str):
107109
time.sleep(2 * delay)

pytest.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[pytest]
2+
testpaths = tests
3+
python_files = test_*.py
4+
python_classes = Test*
5+
python_functions = test_*
6+
addopts = -v --tb=short

requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pytest>=7.0.0
2+
pytest-cov>=4.0.0

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import pytest
2+
import os
3+
import sys
4+
import tempfile
5+
import shutil
6+
7+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8+
9+
10+
@pytest.fixture
11+
def temp_dir():
12+
dirpath = tempfile.mkdtemp()
13+
yield dirpath
14+
if os.path.exists(dirpath):
15+
shutil.rmtree(dirpath)

tests/test_concore.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import pytest
2+
import os
3+
4+
class TestSafeLiteralEval:
5+
6+
def test_reads_dictionary_from_file(self, temp_dir):
7+
test_file = os.path.join(temp_dir, "config.txt")
8+
with open(test_file, "w") as f:
9+
f.write("{'name': 'test', 'value': 123}")
10+
11+
from concore import safe_literal_eval
12+
result = safe_literal_eval(test_file, {})
13+
14+
assert result == {'name': 'test', 'value': 123}
15+
16+
def test_returns_default_when_file_missing(self):
17+
from concore import safe_literal_eval
18+
result = safe_literal_eval("nonexistent_file.txt", "fallback")
19+
20+
assert result == "fallback"
21+
22+
def test_returns_default_for_empty_file(self, temp_dir):
23+
test_file = os.path.join(temp_dir, "empty.txt")
24+
with open(test_file, "w") as f:
25+
pass
26+
27+
from concore import safe_literal_eval
28+
result = safe_literal_eval(test_file, "default")
29+
30+
assert result == "default"
31+
32+
33+
class TestTryparam:
34+
35+
@pytest.fixture(autouse=True)
36+
def reset_params(self):
37+
from concore import params
38+
original_params = params.copy()
39+
yield
40+
params.clear()
41+
params.update(original_params)
42+
43+
def test_returns_existing_parameter(self):
44+
from concore import tryparam, params
45+
params['my_setting'] = 'custom_value'
46+
47+
result = tryparam('my_setting', 'default_value')
48+
49+
assert result == 'custom_value'
50+
51+
def test_returns_default_for_missing_parameter(self):
52+
from concore import tryparam
53+
result = tryparam('missing_param', 'fallback')
54+
55+
assert result == 'fallback'
56+
57+
58+
class TestZeroMQPort:
59+
60+
def test_class_is_defined(self):
61+
from concore import ZeroMQPort
62+
assert ZeroMQPort is not None
63+
64+
65+
class TestDefaultConfiguration:
66+
67+
def test_default_input_path(self):
68+
from concore import inpath
69+
assert inpath == "./in"
70+
71+
def test_default_output_path(self):
72+
from concore import outpath
73+
assert outpath == "./out"
74+
75+
76+
class TestPublicAPI:
77+
78+
def test_module_imports_successfully(self):
79+
from concore import safe_literal_eval
80+
assert safe_literal_eval is not None
81+
82+
def test_core_functions_exist(self):
83+
from concore import safe_literal_eval, tryparam, default_maxtime
84+
85+
assert callable(safe_literal_eval)
86+
assert callable(tryparam)
87+
assert callable(default_maxtime)

0 commit comments

Comments
 (0)