iris-devtester 1.8.1__py3-none-any.whl → 1.9.1__py3-none-any.whl
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.
- iris_devtester/__init__.py +3 -2
- iris_devtester/cli/__init__.py +4 -2
- iris_devtester/cli/__main__.py +1 -1
- iris_devtester/cli/connection_commands.py +31 -51
- iris_devtester/cli/container.py +42 -113
- iris_devtester/cli/container_commands.py +6 -4
- iris_devtester/cli/fixture_commands.py +97 -73
- iris_devtester/config/auto_discovery.py +8 -20
- iris_devtester/config/container_config.py +24 -35
- iris_devtester/config/container_state.py +19 -43
- iris_devtester/config/discovery.py +10 -10
- iris_devtester/config/presets.py +3 -10
- iris_devtester/config/yaml_loader.py +3 -2
- iris_devtester/connections/__init__.py +25 -30
- iris_devtester/connections/connection.py +4 -3
- iris_devtester/connections/dbapi.py +5 -1
- iris_devtester/connections/jdbc.py +2 -6
- iris_devtester/connections/manager.py +1 -1
- iris_devtester/connections/retry.py +2 -5
- iris_devtester/containers/__init__.py +6 -6
- iris_devtester/containers/cpf_manager.py +13 -12
- iris_devtester/containers/iris_container.py +268 -436
- iris_devtester/containers/models.py +18 -43
- iris_devtester/containers/monitor_utils.py +1 -3
- iris_devtester/containers/monitoring.py +31 -46
- iris_devtester/containers/performance.py +5 -5
- iris_devtester/containers/validation.py +27 -60
- iris_devtester/containers/wait_strategies.py +13 -4
- iris_devtester/fixtures/__init__.py +14 -13
- iris_devtester/fixtures/creator.py +127 -555
- iris_devtester/fixtures/loader.py +221 -78
- iris_devtester/fixtures/manifest.py +8 -6
- iris_devtester/fixtures/obj_export.py +45 -35
- iris_devtester/fixtures/validator.py +4 -7
- iris_devtester/integrations/langchain.py +2 -6
- iris_devtester/ports/registry.py +5 -4
- iris_devtester/testing/__init__.py +3 -0
- iris_devtester/testing/fixtures.py +10 -1
- iris_devtester/testing/helpers.py +5 -12
- iris_devtester/testing/models.py +3 -2
- iris_devtester/testing/schema_reset.py +1 -3
- iris_devtester/utils/__init__.py +20 -5
- iris_devtester/utils/container_port.py +2 -6
- iris_devtester/utils/container_status.py +2 -6
- iris_devtester/utils/dbapi_compat.py +29 -14
- iris_devtester/utils/enable_callin.py +5 -7
- iris_devtester/utils/health_checks.py +18 -33
- iris_devtester/utils/iris_container_adapter.py +27 -26
- iris_devtester/utils/password.py +673 -0
- iris_devtester/utils/progress.py +1 -1
- iris_devtester/utils/test_connection.py +4 -6
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/METADATA +7 -7
- iris_devtester-1.9.1.dist-info/RECORD +66 -0
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/WHEEL +1 -1
- iris_devtester/utils/password_reset.py +0 -594
- iris_devtester/utils/password_verification.py +0 -350
- iris_devtester/utils/unexpire_passwords.py +0 -168
- iris_devtester-1.8.1.dist-info/RECORD +0 -68
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/entry_points.txt +0 -0
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/licenses/LICENSE +0 -0
- {iris_devtester-1.8.1.dist-info → iris_devtester-1.9.1.dist-info}/top_level.txt +0 -0
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import time
|
|
3
2
|
import subprocess
|
|
3
|
+
import time
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import List, Optional
|
|
6
5
|
from subprocess import TimeoutExpired
|
|
6
|
+
from typing import List, Optional
|
|
7
7
|
|
|
8
8
|
from iris_devtester.containers.iris_container import IRISContainer
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
from .manifest import FixtureLoadError, FixtureManifest, FixtureValidationError, LoadResult
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
class DATFixtureLoader:
|
|
14
16
|
|
|
15
17
|
def __init__(self, container: Optional[IRISContainer] = None, **kwargs):
|
|
16
18
|
self.container = container
|
|
17
|
-
self.connection_config = kwargs.get(
|
|
19
|
+
self.connection_config = kwargs.get("connection_config")
|
|
18
20
|
self._owns_container = False
|
|
19
21
|
|
|
20
|
-
def validate_fixture(
|
|
21
|
-
|
|
22
|
+
def validate_fixture(
|
|
23
|
+
self, fixture_path: str, validate_checksum: bool = True
|
|
24
|
+
) -> FixtureManifest:
|
|
22
25
|
from .validator import FixtureValidator
|
|
26
|
+
|
|
23
27
|
validator = FixtureValidator()
|
|
24
28
|
result = validator.validate_fixture(fixture_path, validate_checksum=validate_checksum)
|
|
25
29
|
if not result.valid or result.manifest is None:
|
|
@@ -56,95 +60,232 @@ class DATFixtureLoader:
|
|
|
56
60
|
|
|
57
61
|
if validate_checksum:
|
|
58
62
|
from .validator import FixtureValidator
|
|
63
|
+
|
|
59
64
|
validator = FixtureValidator()
|
|
60
65
|
validation = validator.validate_fixture(fixture_path)
|
|
61
66
|
if not validation.valid:
|
|
62
|
-
raise FixtureValidationError(
|
|
63
|
-
f"Checksum validation failed: {validation.errors}"
|
|
64
|
-
)
|
|
67
|
+
raise FixtureValidationError(f"Checksum validation failed: {validation.errors}")
|
|
65
68
|
|
|
66
69
|
container_name = self.container.get_container_name()
|
|
67
|
-
|
|
70
|
+
container_gof_path = f"/tmp/RESTORE_{namespace}.gof"
|
|
71
|
+
container_cls_path = f"/tmp/RESTORE_{namespace}.xml"
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
# Step 1: Copy fixture files to container
|
|
74
|
+
# The fixture package has globals.gof and optionally classes.xml
|
|
75
|
+
fixture_base = Path(fixture_path)
|
|
76
|
+
gof_file = fixture_base / "globals.gof"
|
|
77
|
+
cls_file = fixture_base / "classes.xml"
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
refresh_script = f"""
|
|
80
|
-
Set nsName = "{namespace}"
|
|
81
|
-
Set dbName = "{db_name}"
|
|
82
|
-
If ##class(Config.Namespaces).Exists(nsName,.obj) Do ##class(Config.Namespaces).Delete(nsName)
|
|
83
|
-
If ##class(Config.Databases).Exists(dbName,.obj) {{
|
|
84
|
-
Set dir = obj.Directory
|
|
85
|
-
Do ##class(SYS.Database).DismountDatabase(dir)
|
|
86
|
-
Do ##class(Config.Databases).Delete(dbName)
|
|
87
|
-
}}
|
|
88
|
-
"""
|
|
79
|
+
if gof_file.exists():
|
|
80
|
+
subprocess.run(
|
|
81
|
+
["docker", "cp", str(gof_file), f"{container_name}:{container_gof_path}"],
|
|
82
|
+
check=True,
|
|
83
|
+
)
|
|
89
84
|
else:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
raise FixtureLoadError(f"globals.gof not found in {fixture_path}")
|
|
86
|
+
|
|
87
|
+
has_classes = cls_file.exists()
|
|
88
|
+
if has_classes:
|
|
89
|
+
subprocess.run(
|
|
90
|
+
["docker", "cp", str(cls_file), f"{container_name}:{container_cls_path}"],
|
|
91
|
+
check=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Fix permissions on the copied files
|
|
95
|
+
subprocess.run(
|
|
96
|
+
[
|
|
97
|
+
"docker",
|
|
98
|
+
"exec",
|
|
99
|
+
"-u",
|
|
100
|
+
"root",
|
|
101
|
+
container_name,
|
|
102
|
+
"chmod",
|
|
103
|
+
"644",
|
|
104
|
+
container_gof_path,
|
|
105
|
+
],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
timeout=30,
|
|
108
|
+
)
|
|
109
|
+
if has_classes:
|
|
110
|
+
subprocess.run(
|
|
111
|
+
[
|
|
112
|
+
"docker",
|
|
113
|
+
"exec",
|
|
114
|
+
"-u",
|
|
115
|
+
"root",
|
|
116
|
+
container_name,
|
|
117
|
+
"chmod",
|
|
118
|
+
"644",
|
|
119
|
+
container_cls_path,
|
|
120
|
+
],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
timeout=30,
|
|
123
|
+
)
|
|
94
124
|
|
|
95
|
-
|
|
125
|
+
# Step 2: Create namespace if it doesn't exist, then import globals
|
|
126
|
+
# First, ensure the namespace exists (create with default database structure)
|
|
127
|
+
db_dir = f"/usr/irissys/mgr/db_{namespace.lower()}"
|
|
128
|
+
create_ns_script = f"""
|
|
129
|
+
Set ns = "{namespace}"
|
|
96
130
|
Set dbDir = "{db_dir}"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{refresh_script}
|
|
100
|
-
|
|
131
|
+
If ##class(Config.Namespaces).Exists(ns) Write "NS_READY" Halt
|
|
101
132
|
If '##class(%File).DirectoryExists(dbDir) Do ##class(%File).CreateDirectoryChain(dbDir)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Set
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Set
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
Set db = ##class(SYS.Database).%New()
|
|
134
|
+
Set db.Directory = dbDir
|
|
135
|
+
Set sc = db.%Save()
|
|
136
|
+
If 'sc Write "ERR_DB:",$System.Status.GetErrorText(sc) Halt
|
|
137
|
+
Set sc = ##class(Config.Databases).Create(ns, dbDir)
|
|
138
|
+
If 'sc Write "ERR_DBCFG:",$System.Status.GetErrorText(sc) Halt
|
|
139
|
+
Kill p Set p("Globals") = ns, p("Routines") = ns
|
|
140
|
+
Set sc = ##class(Config.Namespaces).Create(ns, .p)
|
|
141
|
+
If 'sc Write "ERR_NS:",$System.Status.GetErrorText(sc) Halt
|
|
142
|
+
Write "NS_READY"
|
|
143
|
+
Halt
|
|
144
|
+
"""
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
["docker", "exec", "-i", container_name, "iris", "session", "IRIS", "-U", "%SYS"],
|
|
147
|
+
input=create_ns_script.encode("utf-8"),
|
|
148
|
+
capture_output=True,
|
|
149
|
+
timeout=60,
|
|
150
|
+
)
|
|
151
|
+
stdout = result.stdout.decode("utf-8", errors="replace")
|
|
152
|
+
if "NS_READY" not in stdout:
|
|
153
|
+
raise FixtureLoadError(f"Namespace creation failed: {stdout}")
|
|
154
|
+
|
|
155
|
+
# Step 3a: Import classes FIRST (if available) - this creates SQL table metadata
|
|
156
|
+
if has_classes:
|
|
157
|
+
import_cls_script = f"""
|
|
158
|
+
Set clsFile = "{container_cls_path}"
|
|
159
|
+
Set sc = $SYSTEM.OBJ.Load(clsFile, "ck")
|
|
160
|
+
If 'sc Write "ERR_CLS:",$System.Status.GetErrorText(sc) Halt
|
|
161
|
+
Write "CLASSES_LOADED"
|
|
114
162
|
Halt
|
|
115
|
-
"""
|
|
163
|
+
"""
|
|
164
|
+
result = subprocess.run(
|
|
165
|
+
[
|
|
166
|
+
"docker",
|
|
167
|
+
"exec",
|
|
168
|
+
"-i",
|
|
169
|
+
container_name,
|
|
170
|
+
"iris",
|
|
171
|
+
"session",
|
|
172
|
+
"IRIS",
|
|
173
|
+
"-U",
|
|
174
|
+
namespace,
|
|
175
|
+
],
|
|
176
|
+
input=import_cls_script.encode("utf-8"),
|
|
177
|
+
capture_output=True,
|
|
178
|
+
timeout=120,
|
|
179
|
+
)
|
|
180
|
+
stdout = result.stdout.decode("utf-8", errors="replace")
|
|
181
|
+
logger.info(f"Class import output: {stdout}")
|
|
182
|
+
if "CLASSES_LOADED" not in stdout and "ERR_CLS" in stdout:
|
|
183
|
+
raise FixtureLoadError(f"Class import failed: {stdout}")
|
|
116
184
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
185
|
+
# Step 3b: Import globals (data)
|
|
186
|
+
# Signature: Import(Nsp, GlobalList, FileName, InputFormat)
|
|
187
|
+
# InputFormat=7 for GOF (block format)
|
|
188
|
+
import_gof_script = f"""
|
|
189
|
+
Set file = "{container_gof_path}"
|
|
190
|
+
Set sc = ##class(%Library.Global).Import($Namespace, "*", file, 7)
|
|
191
|
+
If 'sc Write "ERR_IMPORT:",$System.Status.GetErrorText(sc) Halt
|
|
192
|
+
Write "SUCCESS"
|
|
193
|
+
Halt
|
|
194
|
+
"""
|
|
195
|
+
# Run import in the TARGET namespace so globals go to the right place
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
[
|
|
198
|
+
"docker",
|
|
199
|
+
"exec",
|
|
200
|
+
"-i",
|
|
201
|
+
container_name,
|
|
202
|
+
"iris",
|
|
203
|
+
"session",
|
|
204
|
+
"IRIS",
|
|
205
|
+
"-U",
|
|
206
|
+
namespace,
|
|
207
|
+
],
|
|
208
|
+
input=import_gof_script.encode("utf-8"),
|
|
209
|
+
capture_output=True,
|
|
210
|
+
timeout=120,
|
|
211
|
+
)
|
|
121
212
|
|
|
122
|
-
stdout = result.stdout.decode(
|
|
213
|
+
stdout = result.stdout.decode("utf-8", errors="replace")
|
|
214
|
+
stderr = result.stderr.decode("utf-8", errors="replace")
|
|
215
|
+
logger.info(f"GOF import output: {stdout}")
|
|
216
|
+
if stderr:
|
|
217
|
+
logger.warning(f"GOF import stderr: {stderr}")
|
|
123
218
|
if "SUCCESS" not in stdout:
|
|
124
219
|
raise FixtureLoadError(f"Restore failed: {stdout}")
|
|
125
220
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
221
|
+
# Step 3: CRITICAL - Ensure test credentials still work after restore
|
|
222
|
+
# Restoration can sometimes overwrite security settings or trigger flags.
|
|
223
|
+
ensure_user_script = """
|
|
224
|
+
Set user="testuser",pass="testpassword"
|
|
225
|
+
Kill p
|
|
226
|
+
Set p("PasswordExternal")=pass,p("Roles")="%ALL",p("ChangePassword")=0,p("PasswordNeverExpires")=1
|
|
227
|
+
If ##class(Security.Users).Exists(user) Do ##class(Security.Users).Delete(user)
|
|
228
|
+
Set sc=##class(Security.Users).Create(user,.p)
|
|
229
|
+
Do ##class(Security.Services).Get("%Service_CallIn",.svcP)
|
|
230
|
+
Set svcP("Enabled")=1
|
|
231
|
+
Do ##class(Security.Services).Modify("%Service_CallIn",.svcP)
|
|
232
|
+
If $$$ISERR(sc) Write "ERR:",$System.Status.GetErrorText(sc) Halt
|
|
233
|
+
Write "SUCCESS" Halt
|
|
234
|
+
"""
|
|
235
|
+
subprocess.run(
|
|
236
|
+
["docker", "exec", "-i", container_name, "iris", "session", "IRIS", "-U", "%SYS"],
|
|
237
|
+
input=ensure_user_script.encode("utf-8"),
|
|
238
|
+
capture_output=True,
|
|
239
|
+
timeout=30,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Also unexpire everything else just in case
|
|
243
|
+
from iris_devtester.utils.password import unexpire_all_passwords
|
|
244
|
+
|
|
245
|
+
unexpire_all_passwords(container_name)
|
|
246
|
+
|
|
247
|
+
time.sleep(5) # Give IRIS a moment to stabilize security changes
|
|
132
248
|
|
|
133
|
-
except TimeoutExpired:
|
|
134
|
-
raise FixtureLoadError("Restore timed out")
|
|
135
249
|
except Exception as e:
|
|
136
|
-
if isinstance(e, FixtureLoadError):
|
|
250
|
+
if isinstance(e, FixtureLoadError):
|
|
251
|
+
raise
|
|
137
252
|
raise FixtureLoadError(f"Restore failed: {e}")
|
|
138
253
|
|
|
254
|
+
return self._verify_load(namespace, manifest, start_time)
|
|
255
|
+
|
|
256
|
+
def _verify_load(
|
|
257
|
+
self, namespace: str, manifest: FixtureManifest, start_time: float
|
|
258
|
+
) -> LoadResult:
|
|
259
|
+
if not self.container:
|
|
260
|
+
raise RuntimeError("IRIS container required for verification")
|
|
261
|
+
|
|
139
262
|
try:
|
|
140
|
-
|
|
263
|
+
# Use the modern connection manager which has automatic password reset remediation
|
|
141
264
|
from iris_devtester.config import IRISConfig
|
|
265
|
+
from iris_devtester.connections.connection import (
|
|
266
|
+
get_connection as get_modern_connection,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# CRITICAL: Use the provided connection_config if available (has verified testuser credentials)
|
|
270
|
+
# Otherwise fall back to container's config
|
|
271
|
+
if self.connection_config:
|
|
272
|
+
config = self.connection_config
|
|
273
|
+
else:
|
|
274
|
+
config = self.container.get_config()
|
|
142
275
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
276
|
+
# Use the actual container name for remediation if needed
|
|
277
|
+
container_name = self.container.get_container_name()
|
|
278
|
+
|
|
279
|
+
conn = get_modern_connection(
|
|
280
|
+
IRISConfig(
|
|
281
|
+
host=config.host,
|
|
282
|
+
port=config.port,
|
|
283
|
+
namespace=namespace,
|
|
284
|
+
username=config.username,
|
|
285
|
+
password=config.password,
|
|
286
|
+
container_name=container_name,
|
|
287
|
+
)
|
|
288
|
+
)
|
|
148
289
|
cursor = conn.cursor()
|
|
149
290
|
verified_tables = []
|
|
150
291
|
for table_info in manifest.tables:
|
|
@@ -155,24 +296,26 @@ class DATFixtureLoader:
|
|
|
155
296
|
conn.close()
|
|
156
297
|
|
|
157
298
|
return LoadResult(
|
|
158
|
-
success=True,
|
|
159
|
-
|
|
160
|
-
|
|
299
|
+
success=True,
|
|
300
|
+
manifest=manifest,
|
|
301
|
+
namespace=namespace,
|
|
302
|
+
tables_loaded=verified_tables,
|
|
303
|
+
elapsed_seconds=time.time() - start_time,
|
|
161
304
|
)
|
|
162
305
|
except Exception as e:
|
|
163
306
|
raise FixtureLoadError(f"Table verification failed: {e}")
|
|
164
307
|
|
|
165
308
|
def cleanup_fixture(self, namespace: str, delete_namespace: bool = True):
|
|
166
|
-
"""Contract‑compatible cleanup wrapper."""
|
|
167
309
|
if not namespace:
|
|
168
310
|
raise ValueError("Namespace is required")
|
|
169
311
|
if not self.container:
|
|
170
312
|
raise RuntimeError("IRIS container required for cleanup")
|
|
171
|
-
|
|
172
313
|
if delete_namespace:
|
|
173
314
|
self.container.delete_namespace(namespace)
|
|
174
315
|
|
|
175
316
|
def get_connection(self):
|
|
176
317
|
"""Contract‑compatible connection getter."""
|
|
177
|
-
|
|
178
|
-
|
|
318
|
+
# Use the modern connection manager which has automatic password reset remediation
|
|
319
|
+
from iris_devtester.connections.connection import get_connection as get_modern_connection
|
|
320
|
+
|
|
321
|
+
return get_modern_connection(config=self.connection_config)
|
|
@@ -4,35 +4,40 @@ This module defines the data structures for IRIS .DAT fixture manifests,
|
|
|
4
4
|
including FixtureManifest, TableInfo, ValidationResult, and LoadResult.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from dataclasses import dataclass, field, asdict
|
|
8
|
-
from typing import List, Dict, Any, Optional
|
|
9
7
|
import json
|
|
8
|
+
from dataclasses import asdict, dataclass, field
|
|
10
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# Custom Exceptions
|
|
14
14
|
class FixtureError(Exception):
|
|
15
15
|
"""Base exception for fixture operations."""
|
|
16
|
+
|
|
16
17
|
pass
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class FixtureValidationError(FixtureError):
|
|
20
21
|
"""Raised when fixture validation fails."""
|
|
22
|
+
|
|
21
23
|
pass
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class FixtureLoadError(FixtureError):
|
|
25
27
|
"""Raised when fixture loading fails."""
|
|
28
|
+
|
|
26
29
|
pass
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class FixtureCreateError(FixtureError):
|
|
30
33
|
"""Raised when fixture creation fails."""
|
|
34
|
+
|
|
31
35
|
pass
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
class ChecksumMismatchError(FixtureValidationError):
|
|
35
39
|
"""Raised when file checksum doesn't match manifest."""
|
|
40
|
+
|
|
36
41
|
pass
|
|
37
42
|
|
|
38
43
|
|
|
@@ -218,10 +223,7 @@ class FixtureManifest:
|
|
|
218
223
|
errors.append("Duplicate table names found")
|
|
219
224
|
|
|
220
225
|
return ValidationResult(
|
|
221
|
-
valid=len(errors) == 0,
|
|
222
|
-
errors=errors,
|
|
223
|
-
warnings=warnings,
|
|
224
|
-
manifest=self
|
|
226
|
+
valid=len(errors) == 0, errors=errors, warnings=warnings, manifest=self
|
|
225
227
|
)
|
|
226
228
|
|
|
227
229
|
|
|
@@ -86,24 +86,26 @@ def export_classes(
|
|
|
86
86
|
"""
|
|
87
87
|
qualifiers = "/displaylog" + ("/compile" if compile else "")
|
|
88
88
|
|
|
89
|
-
objectscript = f
|
|
89
|
+
objectscript = f"""
|
|
90
90
|
ZN "{namespace}"
|
|
91
91
|
Set sc = $SYSTEM.OBJ.Export("{pattern}", "{output_file}", "{qualifiers}")
|
|
92
92
|
Write $Select(sc=1:1,1:0)
|
|
93
93
|
Halt
|
|
94
|
-
|
|
94
|
+
"""
|
|
95
95
|
|
|
96
96
|
try:
|
|
97
97
|
container_name = container.get_container_name()
|
|
98
98
|
|
|
99
99
|
cmd = [
|
|
100
|
-
"docker",
|
|
101
|
-
|
|
100
|
+
"docker",
|
|
101
|
+
"exec",
|
|
102
|
+
container_name,
|
|
103
|
+
"sh",
|
|
104
|
+
"-c",
|
|
105
|
+
f'iris session IRIS -U %SYS << "EOF"\n{objectscript}\nEOF',
|
|
102
106
|
]
|
|
103
107
|
|
|
104
|
-
result = subprocess.run(
|
|
105
|
-
cmd, capture_output=True, text=True, timeout=120
|
|
106
|
-
)
|
|
108
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
107
109
|
|
|
108
110
|
raw_output = result.stdout + result.stderr
|
|
109
111
|
success = result.returncode == 0 and "1" in result.stdout
|
|
@@ -173,24 +175,26 @@ def import_classes(
|
|
|
173
175
|
"""
|
|
174
176
|
qualifiers = "/displaylog" + ("/compile" if compile else "")
|
|
175
177
|
|
|
176
|
-
objectscript = f
|
|
178
|
+
objectscript = f"""
|
|
177
179
|
ZN "{namespace}"
|
|
178
180
|
Set sc = $SYSTEM.OBJ.Import("{input_file}", "{qualifiers}")
|
|
179
181
|
Write $Select(sc=1:1,1:0)
|
|
180
182
|
Halt
|
|
181
|
-
|
|
183
|
+
"""
|
|
182
184
|
|
|
183
185
|
try:
|
|
184
186
|
container_name = container.get_container_name()
|
|
185
187
|
|
|
186
188
|
cmd = [
|
|
187
|
-
"docker",
|
|
188
|
-
|
|
189
|
+
"docker",
|
|
190
|
+
"exec",
|
|
191
|
+
container_name,
|
|
192
|
+
"sh",
|
|
193
|
+
"-c",
|
|
194
|
+
f'iris session IRIS -U %SYS << "EOF"\n{objectscript}\nEOF',
|
|
189
195
|
]
|
|
190
196
|
|
|
191
|
-
result = subprocess.run(
|
|
192
|
-
cmd, capture_output=True, text=True, timeout=120
|
|
193
|
-
)
|
|
197
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
194
198
|
|
|
195
199
|
raw_output = result.stdout + result.stderr
|
|
196
200
|
success = result.returncode == 0 and "1" in result.stdout
|
|
@@ -260,23 +264,25 @@ def export_global(
|
|
|
260
264
|
# Normalize global name (remove leading ^ if present for comparison)
|
|
261
265
|
clean_global = global_name.lstrip("^")
|
|
262
266
|
|
|
263
|
-
objectscript = f
|
|
267
|
+
objectscript = f"""
|
|
264
268
|
Set sc = ##class(%Library.Global).Export("{namespace}", "^{clean_global}", "{output_file}")
|
|
265
269
|
Write $Select(sc=1:1,1:0)
|
|
266
270
|
Halt
|
|
267
|
-
|
|
271
|
+
"""
|
|
268
272
|
|
|
269
273
|
try:
|
|
270
274
|
container_name = container.get_container_name()
|
|
271
275
|
|
|
272
276
|
cmd = [
|
|
273
|
-
"docker",
|
|
274
|
-
|
|
277
|
+
"docker",
|
|
278
|
+
"exec",
|
|
279
|
+
container_name,
|
|
280
|
+
"sh",
|
|
281
|
+
"-c",
|
|
282
|
+
f'iris session IRIS -U %SYS << "EOF"\n{objectscript}\nEOF',
|
|
275
283
|
]
|
|
276
284
|
|
|
277
|
-
result = subprocess.run(
|
|
278
|
-
cmd, capture_output=True, text=True, timeout=120
|
|
279
|
-
)
|
|
285
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
280
286
|
|
|
281
287
|
raw_output = result.stdout + result.stderr
|
|
282
288
|
success = result.returncode == 0 and "1" in result.stdout
|
|
@@ -342,23 +348,25 @@ def import_global(
|
|
|
342
348
|
... if result.success:
|
|
343
349
|
... print("Imported global data")
|
|
344
350
|
"""
|
|
345
|
-
objectscript = f
|
|
351
|
+
objectscript = f"""
|
|
346
352
|
Set sc = ##class(%Library.Global).Import("{namespace}", "{input_file}")
|
|
347
353
|
Write $Select(sc=1:1,1:0)
|
|
348
354
|
Halt
|
|
349
|
-
|
|
355
|
+
"""
|
|
350
356
|
|
|
351
357
|
try:
|
|
352
358
|
container_name = container.get_container_name()
|
|
353
359
|
|
|
354
360
|
cmd = [
|
|
355
|
-
"docker",
|
|
356
|
-
|
|
361
|
+
"docker",
|
|
362
|
+
"exec",
|
|
363
|
+
container_name,
|
|
364
|
+
"sh",
|
|
365
|
+
"-c",
|
|
366
|
+
f'iris session IRIS -U %SYS << "EOF"\n{objectscript}\nEOF',
|
|
357
367
|
]
|
|
358
368
|
|
|
359
|
-
result = subprocess.run(
|
|
360
|
-
cmd, capture_output=True, text=True, timeout=120
|
|
361
|
-
)
|
|
369
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
362
370
|
|
|
363
371
|
raw_output = result.stdout + result.stderr
|
|
364
372
|
success = result.returncode == 0 and "1" in result.stdout
|
|
@@ -422,24 +430,26 @@ def export_package(
|
|
|
422
430
|
... if result.success:
|
|
423
431
|
... print(f"Exported package to {result.output_file}")
|
|
424
432
|
"""
|
|
425
|
-
objectscript = f
|
|
433
|
+
objectscript = f"""
|
|
426
434
|
ZN "{namespace}"
|
|
427
435
|
Set sc = $SYSTEM.OBJ.ExportPackage("{package_name}", "{output_file}")
|
|
428
436
|
Write $Select(sc=1:1,1:0)
|
|
429
437
|
Halt
|
|
430
|
-
|
|
438
|
+
"""
|
|
431
439
|
|
|
432
440
|
try:
|
|
433
441
|
container_name = container.get_container_name()
|
|
434
442
|
|
|
435
443
|
cmd = [
|
|
436
|
-
"docker",
|
|
437
|
-
|
|
444
|
+
"docker",
|
|
445
|
+
"exec",
|
|
446
|
+
container_name,
|
|
447
|
+
"sh",
|
|
448
|
+
"-c",
|
|
449
|
+
f'iris session IRIS -U %SYS << "EOF"\n{objectscript}\nEOF',
|
|
438
450
|
]
|
|
439
451
|
|
|
440
|
-
result = subprocess.run(
|
|
441
|
-
cmd, capture_output=True, text=True, timeout=120
|
|
442
|
-
)
|
|
452
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
443
453
|
|
|
444
454
|
raw_output = result.stdout + result.stderr
|
|
445
455
|
success = result.returncode == 0 and "1" in result.stdout
|
|
@@ -9,10 +9,10 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Optional
|
|
10
10
|
|
|
11
11
|
from .manifest import (
|
|
12
|
+
ChecksumMismatchError,
|
|
12
13
|
FixtureManifest,
|
|
13
|
-
ValidationResult,
|
|
14
14
|
FixtureValidationError,
|
|
15
|
-
|
|
15
|
+
ValidationResult,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
|
|
@@ -104,8 +104,7 @@ class FixtureValidator:
|
|
|
104
104
|
"""
|
|
105
105
|
if not expected_checksum.startswith("sha256:"):
|
|
106
106
|
raise ValueError(
|
|
107
|
-
f"Invalid checksum format: {expected_checksum}. "
|
|
108
|
-
"Must start with 'sha256:'"
|
|
107
|
+
f"Invalid checksum format: {expected_checksum}. " "Must start with 'sha256:'"
|
|
109
108
|
)
|
|
110
109
|
|
|
111
110
|
actual_checksum = self.calculate_sha256(file_path, chunk_size)
|
|
@@ -212,9 +211,7 @@ class FixtureValidator:
|
|
|
212
211
|
# Validate checksum if requested
|
|
213
212
|
if validate_checksum:
|
|
214
213
|
try:
|
|
215
|
-
self.validate_checksum(
|
|
216
|
-
str(dat_file), manifest.checksum, chunk_size
|
|
217
|
-
)
|
|
214
|
+
self.validate_checksum(str(dat_file), manifest.checksum, chunk_size)
|
|
218
215
|
except ChecksumMismatchError:
|
|
219
216
|
# Re-raise ChecksumMismatchError - it's a critical failure
|
|
220
217
|
# that requires immediate attention (Constitutional Principle #5)
|
|
@@ -178,9 +178,7 @@ class LangChainIRISContainer(IRISContainer):
|
|
|
178
178
|
password = getattr(self, "password", "SYS")
|
|
179
179
|
namespace = getattr(self, "namespace", "USER")
|
|
180
180
|
|
|
181
|
-
connection_string =
|
|
182
|
-
f"iris://{username}:{password}@{host}:{port}/{namespace}"
|
|
183
|
-
)
|
|
181
|
+
connection_string = f"iris://{username}:{password}@{host}:{port}/{namespace}"
|
|
184
182
|
|
|
185
183
|
return connection_string
|
|
186
184
|
|
|
@@ -247,9 +245,7 @@ class LangChainIRISContainer(IRISContainer):
|
|
|
247
245
|
|
|
248
246
|
connection_string = self.get_connection_string()
|
|
249
247
|
|
|
250
|
-
logger.info(
|
|
251
|
-
f"Creating LangChain chat history for session '{session_id}'"
|
|
252
|
-
)
|
|
248
|
+
logger.info(f"Creating LangChain chat history for session '{session_id}'")
|
|
253
249
|
|
|
254
250
|
history = IRISChatMessageHistory(
|
|
255
251
|
connection_string=connection_string,
|
iris_devtester/ports/registry.py
CHANGED
|
@@ -192,7 +192,10 @@ class PortRegistry:
|
|
|
192
192
|
# Find stale assignments (container doesn't exist)
|
|
193
193
|
active_assignments = []
|
|
194
194
|
for assignment in assignments:
|
|
195
|
-
if
|
|
195
|
+
if (
|
|
196
|
+
assignment.container_name
|
|
197
|
+
and assignment.container_name not in container_names
|
|
198
|
+
):
|
|
196
199
|
# Container removed - mark as stale
|
|
197
200
|
assignment.status = "stale"
|
|
198
201
|
released.append(assignment)
|
|
@@ -298,9 +301,7 @@ class PortRegistry:
|
|
|
298
301
|
return port
|
|
299
302
|
|
|
300
303
|
# All ports exhausted
|
|
301
|
-
raise PortExhaustedError(
|
|
302
|
-
port_range=self.port_range, current_assignments=assignments
|
|
303
|
-
)
|
|
304
|
+
raise PortExhaustedError(port_range=self.port_range, current_assignments=assignments)
|
|
304
305
|
|
|
305
306
|
def _get_docker_bound_ports(self) -> set:
|
|
306
307
|
"""
|