Skip to content

Commit 59f5f9c

Browse files
committed
Refactor tests and update changelog; enhance test functionality and improve production handling
1 parent 8b46336 commit 59f5f9c

7 files changed

Lines changed: 118 additions & 55 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
### Changed
1616
- Refactor whole unittest and e2e test suite for better maintainability and reliability
17+
- Export production now returns Production name as key in the returned dict for better clarity
1718

1819
## [3.5.5] - 2026-01-23
1920
### Added

src/iop/cls/IOP/Service/Remote/Rest/v1.cls

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ ClassMethod PostTest() As %Status
276276

277277
// Optionally restart the target component before testing
278278
If restart {
279-
zw "Restarting target component: "_target
280279
$$$ThrowOnError(##class(Ens.Director).EnableConfigItem(target, 0, 1))
281280
$$$ThrowOnError(##class(Ens.Director).EnableConfigItem(target, 1, 1))
282281
}
@@ -304,11 +303,23 @@ ClassMethod PostTest() As %Status
304303
Set result.body = response.GetObjectJson(.atEnd)
305304
set result.classname = response.classname
306305
Set result.truncated = 'atEnd
306+
}
307+
Else {
308+
Set result.classname = response.%ClassName(1)
309+
// Use the ObjectScript XML Serializer so arbitrary objects and collections are captured.
310+
Try {
311+
Set xmlWriter = ##class(%XML.Writer).%New()
312+
Set xmlStream = ##class(%Stream.TmpCharacter).%New()
313+
$$$ThrowOnError(xmlWriter.OutputToStream(xmlStream))
314+
$$$ThrowOnError(xmlWriter.RootObject(response))
315+
Do xmlStream.Rewind()
316+
Set body = ""
317+
While 'xmlStream.AtEnd { Set body = body _ xmlStream.Read(32000) }
318+
Set result.body = body
319+
} Catch {
320+
Set result.body = ""
321+
}
307322
}
308-
} Else {
309-
Set result.classname = response.%ClassName(1)
310-
// TODO, try to use the ObjectScript XML Serializer to capture the response body as XML, which can handle arbitrary objects including collections. For now we'll just return an empty body for non-IOP.Message responses.
311-
Set result.body = ""
312323
}
313324
set sc = $$EndCapture^%SYS.Capture(msg,.msgArray)
314325
Return ..%WriteResponse(result.%ToJSON())

src/tests/e2e/local/bench/cls/Bench.Operation.cls

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,30 @@ Method Method(
2323
Quit tStatus
2424
}
2525

26+
/// Description
27+
Method MethodRandom(
28+
pRequest As Ens.StringRequest,
29+
Output pResponse As Ens.StringResponse) As %Status
30+
{
31+
set tStatus = $$$OK
32+
set pResponse = ##class(Ens.StringResponse).%New()
33+
34+
try{
35+
set pResponse.StringValue = $R(1000000) // Return a random number as string
36+
37+
}
38+
catch exp
39+
{
40+
set tStatus = exp.AsStatus()
41+
}
42+
Quit tStatus
43+
}
44+
2645
XData MessageMap
2746
{
2847
<MapItems>
2948
<MapItem MessageType="Ens.Request">
30-
<Method>Method</Method>
49+
<Method>MethodRandom</Method>
3150
</MapItem>
3251
<MapItem MessageType="IOP.Message">
3352
<Method>Method</Method>

src/tests/e2e/remote/test_rest_api.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,12 @@ class TestExport:
131131
def _need_production(self, remote_director):
132132
prod = remote_director.get_default_production()
133133
if prod in ("", "Not defined"):
134-
pytest.skip("No default production defined")
134+
# List productions and pick the first one if no default is set
135+
prods = remote_director.list_productions()
136+
if not prods:
137+
pytest.skip("No productions available to test export")
138+
# Prods is an object with production names as keys, so take the first key
139+
prod = next(iter(prods.keys()))
135140
self.production = prod
136141

137142
def test_export_returns_200(self, remote_director):
@@ -140,18 +145,17 @@ def test_export_returns_200(self, remote_director):
140145
namespace=remote_director._namespace)
141146
assert resp.status_code == 200
142147

143-
def test_export_body_has_xml_key(self, remote_director):
148+
def test_export_body_has_data(self, remote_director):
144149
data = remote_director._check_error(
145150
remote_director._get("/export", {"production": self.production})
146151
)
147-
assert "xml" in data
152+
assert data is not None
148153

149-
def test_export_xml_is_non_empty_string(self, remote_director):
154+
def test_export_json_is_non_empty_string(self, remote_director):
150155
data = remote_director._check_error(
151156
remote_director._get("/export", {"production": self.production})
152157
)
153-
assert isinstance(data["xml"], str)
154-
assert len(data["xml"]) > 0
158+
assert len(data) > 0
155159

156160

157161
# ---------------------------------------------------------------------------

src/tests/e2e/remote/test_test_component.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,48 @@
44
IOP_URL=http://localhost:52773 pytest src/tests/e2e/remote/
55
"""
66
import pytest
7+
import requests
8+
9+
10+
@pytest.fixture(scope="module")
11+
def default_production(remote_director):
12+
"""Default production name for the configured namespace.
13+
14+
Falls back to the first production found in the list; skips only when
15+
no production exists at all.
16+
"""
17+
prod = remote_director.get_default_production()
18+
if prod in ("", "Not defined"):
19+
prods = remote_director.list_productions()
20+
if not prods:
21+
pytest.skip("No productions available")
22+
prod = next(iter(prods))
23+
return prod
24+
25+
26+
@pytest.fixture(scope="module")
27+
def first_active_component(remote_director, default_production):
28+
"""Name of the first enabled component in the default production, or skip."""
29+
try:
30+
remote_director.start_production(default_production)
31+
except (RuntimeError, requests.exceptions.HTTPError):
32+
pass # already running
33+
34+
components = remote_director.export_production(default_production)
35+
production_data = list(components.values())[0]
36+
items = production_data.get("Item", [])
37+
if isinstance(items, dict):
38+
items = [items]
39+
active = [item for item in items if item.get("@Enabled", "1") == "1"]
40+
if not active:
41+
pytest.skip("No active components found in the default production")
42+
return active[0]["@Name"]
743

844

945
class TestComponentTesting:
10-
def test_test_component_returns_response(self, remote_director):
46+
def test_test_component_returns_response(self, remote_director, default_production):
1147
"""POST /test with a basic Ens.StringRequest should return a valid response."""
12-
default_target = remote_director.get_default_production()
13-
if default_target in ("", "Not defined"):
14-
pytest.skip("No default production defined")
15-
16-
# This test uses Ens.StringRequest as a generic smoke test.
48+
# Uses Ens.StringRequest as a generic smoke test.
1749
# Adjust target and classname to match your environment.
1850
try:
1951
result = remote_director.test_component(
@@ -35,38 +67,20 @@ def test_test_component_bad_target_raises(self, remote_director):
3567
body='{"StringValue": "ping"}',
3668
)
3769

38-
def test_test_component_bad_classname_raises(self, remote_director):
70+
def test_test_component_bad_classname_raises(self, remote_director, default_production):
3971
"""Sending with a non-existent classname should raise RuntimeError."""
40-
default_target = remote_director.get_default_production()
41-
if default_target in ("", "Not defined"):
42-
pytest.skip("No default production defined")
43-
4472
with pytest.raises(RuntimeError):
4573
remote_director.test_component(
4674
target=None,
4775
classname="This.Class.DoesNotExist",
4876
body='{"StringValue": "ping"}',
4977
)
5078

51-
def test_test_component_restart(self, remote_director):
79+
def test_test_component_restart(self, remote_director, first_active_component):
5280
"""Test that the restart option in test_component works without error."""
53-
default_target = remote_director.get_default_production()
54-
if default_target in ("", "Not defined"):
55-
pytest.skip("No default production defined")
56-
57-
# export the default production's components and pick one to target for this test
58-
components = remote_director.export_components()
59-
active_components = [c for c in components if c["active"]]
60-
61-
62-
if not active_components:
63-
pytest.skip("No active components found in the default production")
64-
65-
target_component = active_components[0]["name"]
66-
6781
result = remote_director.test_component(
68-
target=target_component,
69-
classname="Ens.StringRequest",
82+
target=first_active_component,
83+
classname=None,
7084
body='{"StringValue": "ping"}',
7185
restart=True,
7286
)

src/tests/unit/test_cli.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
class TestIOPCli(unittest.TestCase):
1212
"""Test cases for IOP CLI functionality."""
1313

14+
def setUp(self):
15+
# Force local mode regardless of any IOP_URL / IOP_SETTINGS env vars
16+
# that may be set by a parallel e2e test session.
17+
self._remote_patcher = patch('iop._cli.get_remote_settings', return_value=None)
18+
self._remote_patcher.start()
19+
20+
def tearDown(self):
21+
self._remote_patcher.stop()
22+
1423
def test_help_and_basic_commands(self):
1524
"""Test basic CLI commands like help and namespace."""
1625
# Test help
@@ -50,15 +59,18 @@ def test_namespace_with_value_prints_help(self):
5059
def test_default_settings(self):
5160
"""Test default production settings."""
5261
# Test with name
53-
with self.assertRaises(SystemExit) as cm:
54-
main(['-d', 'Bench.Production'])
55-
self.assertEqual(cm.exception.code, 0)
56-
self.assertEqual(_Director.get_default_production(), 'Bench.Production')
62+
with patch('iop._director._Director.set_default_production') as mock_set, \
63+
patch('iop._director._Director.get_default_production', return_value='Bench.Production'):
64+
with self.assertRaises(SystemExit) as cm:
65+
main(['-d', 'Bench.Production'])
66+
self.assertEqual(cm.exception.code, 0)
67+
mock_set.assert_called_once_with('Bench.Production')
5768

58-
# Test without name
59-
with self.assertRaises(SystemExit) as cm:
60-
main(['-d'])
61-
self.assertEqual(cm.exception.code, 0)
69+
# Test without name — just prints the current default
70+
with patch('iop._director._Director.get_default_production', return_value='Bench.Production'):
71+
with self.assertRaises(SystemExit) as cm:
72+
main(['-d'])
73+
self.assertEqual(cm.exception.code, 0)
6274

6375
def test_production_controls(self):
6476
"""Test production control commands (start, stop, restart, kill)."""
@@ -77,12 +89,13 @@ def test_production_controls(self):
7789

7890
# Test stop
7991
with patch('iop._director._Director.stop_production') as mock_stop:
80-
with patch('sys.stdout', new=StringIO()) as fake_out:
81-
with self.assertRaises(SystemExit) as cm:
82-
main(['-S'])
83-
self.assertEqual(cm.exception.code, 0)
84-
mock_stop.assert_called_once()
85-
self.assertEqual(fake_out.getvalue().strip(), 'Production Bench.Production stopped')
92+
with patch('iop._director._Director.get_default_production', return_value='Bench.Production'):
93+
with patch('sys.stdout', new=StringIO()) as fake_out:
94+
with self.assertRaises(SystemExit) as cm:
95+
main(['-S'])
96+
self.assertEqual(cm.exception.code, 0)
97+
mock_stop.assert_called_once()
98+
self.assertEqual(fake_out.getvalue().strip(), 'Production Bench.Production stopped')
8699

87100
# Test restart
88101
with patch('iop._director._Director.restart_production') as mock_restart:

src/tests/unit/test_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ def test_fallback_key_when_no_name_attr(self):
131131
assert "Production" in data
132132

133133

134-
class TestRemoteMigration: @patch('requests.put')
134+
class TestRemoteMigration:
135+
@patch('requests.put')
135136
@patch('iop._utils._Utils._load_settings')
136137
@patch('os.walk')
137138
def test_migrate_remote_verify_ssl_true(self, mock_walk, mock_load_settings, mock_put):

0 commit comments

Comments
 (0)