cuopt-server-cu13 26.8.0__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.
Files changed (67) hide show
  1. cuopt_server/GIT_COMMIT +1 -0
  2. cuopt_server/VERSION +1 -0
  3. cuopt_server/__init__.py +9 -0
  4. cuopt_server/_version.py +14 -0
  5. cuopt_server/cuopt_service.py +446 -0
  6. cuopt_server/tests/__init__.py +2 -0
  7. cuopt_server/tests/test_bill_logging.py +128 -0
  8. cuopt_server/tests/test_cuopt_amr_service.py +26 -0
  9. cuopt_server/tests/test_file_path_validation.py +97 -0
  10. cuopt_server/tests/test_grpc_server_entry_point.py +52 -0
  11. cuopt_server/tests/test_incumbents.py +88 -0
  12. cuopt_server/tests/test_initial_solutions.py +40 -0
  13. cuopt_server/tests/test_job_abort.py +230 -0
  14. cuopt_server/tests/test_job_cache.py +103 -0
  15. cuopt_server/tests/test_lp.py +241 -0
  16. cuopt_server/tests/test_multi_cost.py +97 -0
  17. cuopt_server/tests/test_pdlp_warmstart.py +90 -0
  18. cuopt_server/tests/test_server.py +1364 -0
  19. cuopt_server/tests/test_server_health.py +63 -0
  20. cuopt_server/tests/test_set_cost_matrix.py +115 -0
  21. cuopt_server/tests/test_set_cost_waypoint_graph.py +128 -0
  22. cuopt_server/tests/test_set_fleet_data.py +582 -0
  23. cuopt_server/tests/test_set_solver_config.py +113 -0
  24. cuopt_server/tests/test_set_task_data.py +293 -0
  25. cuopt_server/tests/test_set_travel_time_waypoint_graph.py +111 -0
  26. cuopt_server/tests/test_solver_logging.py +59 -0
  27. cuopt_server/tests/utils/__init__.py +2 -0
  28. cuopt_server/tests/utils/sdk_tester.py +18 -0
  29. cuopt_server/tests/utils/utils.py +452 -0
  30. cuopt_server/utils/__init__.py +4 -0
  31. cuopt_server/utils/billing_data.py +94 -0
  32. cuopt_server/utils/data_definition.py +374 -0
  33. cuopt_server/utils/exceptions.py +79 -0
  34. cuopt_server/utils/health_check.py +48 -0
  35. cuopt_server/utils/job_queue.py +1760 -0
  36. cuopt_server/utils/linear_programming/__init__.py +2 -0
  37. cuopt_server/utils/linear_programming/data_definition.py +997 -0
  38. cuopt_server/utils/linear_programming/data_validation.py +129 -0
  39. cuopt_server/utils/linear_programming/generate_metadata.py +2 -0
  40. cuopt_server/utils/linear_programming/solver.py +412 -0
  41. cuopt_server/utils/logutil.py +65 -0
  42. cuopt_server/utils/mock_store.py +76 -0
  43. cuopt_server/utils/process_handler.py +142 -0
  44. cuopt_server/utils/request_filter.py +163 -0
  45. cuopt_server/utils/response_util.py +39 -0
  46. cuopt_server/utils/result_store.py +60 -0
  47. cuopt_server/utils/routing/__init__.py +2 -0
  48. cuopt_server/utils/routing/data_definition.py +1055 -0
  49. cuopt_server/utils/routing/generate_metadata.py +113 -0
  50. cuopt_server/utils/routing/initial_solution.py +42 -0
  51. cuopt_server/utils/routing/optimization_data_model.py +1048 -0
  52. cuopt_server/utils/routing/solver.py +571 -0
  53. cuopt_server/utils/routing/validation_cost_matrix.py +53 -0
  54. cuopt_server/utils/routing/validation_fleet_data.py +235 -0
  55. cuopt_server/utils/routing/validation_solver_config.py +29 -0
  56. cuopt_server/utils/routing/validation_task_data.py +158 -0
  57. cuopt_server/utils/routing/validation_waypoint_graph.py +96 -0
  58. cuopt_server/utils/sema.py +38 -0
  59. cuopt_server/utils/settings.py +64 -0
  60. cuopt_server/utils/solver.py +506 -0
  61. cuopt_server/utils/utils.py +87 -0
  62. cuopt_server/webserver.py +1651 -0
  63. cuopt_server_cu13-26.8.0.dist-info/METADATA +40 -0
  64. cuopt_server_cu13-26.8.0.dist-info/RECORD +67 -0
  65. cuopt_server_cu13-26.8.0.dist-info/WHEEL +5 -0
  66. cuopt_server_cu13-26.8.0.dist-info/licenses/LICENSE +201 -0
  67. cuopt_server_cu13-26.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ 400863c1a92914bd47f5d68245f9a00e2d0d2df4
cuopt_server/VERSION ADDED
@@ -0,0 +1 @@
1
+ 26.08.00
@@ -0,0 +1,9 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from cuopt_server import cuopt_service
5
+ from cuopt_server._version import (
6
+ __git_commit__,
7
+ __version__,
8
+ __version_major_minor__,
9
+ )
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import importlib.resources
5
+
6
+ __version__ = (
7
+ importlib.resources.files("cuopt_server")
8
+ .joinpath("VERSION")
9
+ .read_text()
10
+ .strip()
11
+ )
12
+ __git_commit__ = ""
13
+
14
+ __version_major_minor__ = ".".join(__version__.split(".")[:2])
@@ -0,0 +1,446 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import argparse
5
+ import logging
6
+ import os
7
+ import signal
8
+ import sys
9
+ from multiprocessing import get_context
10
+
11
+ import psutil
12
+
13
+ import cuopt_server.utils.process_handler as process_handler
14
+ import cuopt_server.utils.request_filter as request_filter
15
+ import cuopt_server.utils.settings as settings
16
+ from cuopt_server._version import __version__
17
+ from cuopt_server.utils.job_queue import create_abort_list
18
+ from cuopt_server.utils.logutil import (
19
+ get_ncaid,
20
+ get_requestid,
21
+ get_solverid,
22
+ message_init,
23
+ )
24
+
25
+ log_fmt = "%(ncaid)s%(requestid)s%(asctime)s.%(msecs)03d %(levelname)s %(message)s%(solverid)s" # noqa
26
+ date_fmt = "%Y-%m-%d %H:%M:%S"
27
+
28
+
29
+ def watcher(app_exit, results_queue, job_queue, abort_queue, abort_list):
30
+ try:
31
+ process_handler.watch_solvers(
32
+ app_exit, job_queue, results_queue, abort_queue, abort_list
33
+ )
34
+ logging.info("app.exit set, watcher exiting")
35
+ except Exception:
36
+ logging.info("exception, watcher exiting")
37
+
38
+
39
+ if __name__ == "__main__":
40
+ import subprocess
41
+
42
+ # Do this before setting the signal handler,
43
+ # because sigchld from the subprocess will terminate us :)
44
+ try:
45
+ result = subprocess.run(
46
+ ["nvidia-smi", "-L"], capture_output=True, text=True
47
+ )
48
+ gpu_string = [r for r in result.stdout.split("\n") if r]
49
+
50
+ # extract GPU id number from each line (looks like "GPU 0: ..."
51
+ gpu_ids = [s.split(" ")[1][0:-1] for s in gpu_string]
52
+
53
+ # if for some reason the parse failed (nvidia-smi output changed)
54
+ gpu_ids = [s for s in gpu_ids if s.isnumeric()]
55
+ except Exception:
56
+ gpu_string = None
57
+ gpu_ids = []
58
+
59
+ try:
60
+ cuda_string = None
61
+ result = subprocess.run(["nvidia-smi"], capture_output=True, text=True)
62
+ for r in result.stdout.split("\n"):
63
+ if "CUDA Version" in r:
64
+ cuda_string = r
65
+ break
66
+ except Exception:
67
+ pass
68
+
69
+ ctx = get_context("fork")
70
+
71
+ # Flag for this process that says we have already run the
72
+ # exit handler
73
+ terminated = ctx.Event()
74
+
75
+ # Flag for all processes that the app is shutting down
76
+ app_exit = ctx.Event()
77
+
78
+ # Flag set by results thread when all jobs have been
79
+ # marked done, to give a chance for anyone actively
80
+ # waiting to get a graceful response
81
+ jobs_marked_done = ctx.Event()
82
+
83
+ job_queue = ctx.Queue()
84
+ abort_queue = ctx.Queue()
85
+ results_queue = ctx.Queue()
86
+
87
+ w = None
88
+
89
+ def handle_exit(signame, frame):
90
+ if terminated.is_set():
91
+ return
92
+ if signame:
93
+ logging.info(f"cuopt received signal {signame}")
94
+ if signame == 17:
95
+ return
96
+ terminated.set()
97
+ app_exit.set()
98
+ process_handler.terminate(
99
+ job_queue, results_queue, abort_queue, signame
100
+ )
101
+ logging.info("handle_exit complete")
102
+
103
+ signal.signal(signal.SIGINT, handle_exit)
104
+ signal.signal(signal.SIGTERM, handle_exit)
105
+ signal.signal(signal.SIGCHLD, handle_exit)
106
+
107
+ # Allow defaults to be overridden from the environment.
108
+ # Commandline arguments have the highest precedence.
109
+ # These values all have commandline analogs below.
110
+ ip = os.environ.get("CUOPT_SERVER_IP", "0.0.0.0")
111
+ port = os.environ.get("CUOPT_SERVER_PORT", 5000)
112
+ log_level = os.environ.get("CUOPT_SERVER_LOG_LEVEL", "debug")
113
+ log_file = os.environ.get("CUOPT_SERVER_LOG_FILE", "")
114
+ log_max = os.environ.get("CUOPT_SERVER_LOG_MAX", 10485760)
115
+ log_backup = os.environ.get("CUOPT_SERVER_LOG_BACKUP", 10)
116
+ tier = os.environ.get("CUOPT_SERVER_TIER", "managed_default")
117
+ datadir = os.environ.get("CUOPT_DATA_DIR", "")
118
+ resultdir = os.environ.get("CUOPT_RESULT_DIR", "")
119
+ maxresult = os.environ.get("CUOPT_MAX_RESULT", 250)
120
+ resultmode = os.environ.get("CUOPT_RESULT_MODE", "644")
121
+ ssl_certfile = os.environ.get("CUOPT_SSL_CERTFILE", "")
122
+ ssl_keyfile = os.environ.get("CUOPT_SSL_KEYFILE", "")
123
+ gpu_count = os.environ.get("CUOPT_GPU_COUNT", 1)
124
+
125
+ # This setting is experimental, and so is only settable via an
126
+ # env var. This setting allows multiple solver processes per GPU
127
+ # to be allocated, so that problems may be solved in parallel on
128
+ # the same GPU. Additional work may be needed on the best way to
129
+ # set up rmm in this case.
130
+ procs_per_gpu = os.environ.get("CUOPT_PROCS_PER_GPU", 1)
131
+
132
+ if not isinstance(procs_per_gpu, int):
133
+ raise ValueError(
134
+ "Process per GPU should be an integer value, "
135
+ f"current value is of type : {type(procs_per_gpu)}"
136
+ )
137
+
138
+ levels = {
139
+ "critical": logging.CRITICAL,
140
+ "error": logging.ERROR,
141
+ "warning": logging.WARNING,
142
+ "info": logging.INFO,
143
+ "debug": logging.DEBUG,
144
+ }
145
+
146
+ parser = argparse.ArgumentParser()
147
+ parser.add_argument(
148
+ "-i",
149
+ "--ip",
150
+ type=str,
151
+ help=(
152
+ "IP to the server, also can be set by environment "
153
+ "variable CUOPT_SERVER_IP"
154
+ ),
155
+ default=ip,
156
+ )
157
+ parser.add_argument(
158
+ "-p",
159
+ "--port",
160
+ type=int,
161
+ help=(
162
+ "Port of the server, also can be set by environment "
163
+ "variable CUOPT_SERVER_PORT"
164
+ ),
165
+ default=port,
166
+ )
167
+ parser.add_argument(
168
+ "-d",
169
+ "--datadir",
170
+ type=str,
171
+ help=(
172
+ "Data directory used to optionally pass cuopt problem data files "
173
+ "to /cuopt/request endpoint via the filesystem instead of over "
174
+ "http, also can be set by environment variable CUOPT_DATA_DIR"
175
+ ),
176
+ default=datadir,
177
+ )
178
+ parser.add_argument(
179
+ "-r",
180
+ "--resultdir",
181
+ type=str,
182
+ help=(
183
+ "Output directory used to optionally pass cuopt result files from "
184
+ "/cuopt/request endpoint via the filesystem instead of over http, "
185
+ "also can be set by environment variable CUOPT_RESULT_DIR"
186
+ ),
187
+ default=resultdir,
188
+ )
189
+ parser.add_argument(
190
+ "-mr",
191
+ "--maxresult",
192
+ type=int,
193
+ help=(
194
+ "Maximum size (kilobytes) of a result returned over http from "
195
+ "/cuopt/request endpoint when RESULTDIR is set. Set to 0 to have "
196
+ "all results written to RESULTDIR (default 250), also can be "
197
+ "set by environment variable CUOPT_MAX_RESULT"
198
+ ),
199
+ default=maxresult,
200
+ )
201
+ parser.add_argument(
202
+ "-mo",
203
+ "--mode",
204
+ type=str,
205
+ help=(
206
+ "Mode (octal) of result files from /cuopt/request endpoint "
207
+ "(default is 644), also can be set by environment variable "
208
+ "CUOPT_RESULT_MODE"
209
+ ),
210
+ default=resultmode,
211
+ )
212
+ parser.add_argument(
213
+ "-l",
214
+ "--log-level",
215
+ type=str,
216
+ choices=list(levels.keys()),
217
+ help=(
218
+ "Log level, also can be set by environment variable "
219
+ "CUOPT_SERVER_LOG_LEVEL"
220
+ ),
221
+ default=log_level,
222
+ )
223
+ parser.add_argument(
224
+ "-f",
225
+ "--log-file",
226
+ type=str,
227
+ help=(
228
+ "Log filename (by default logs to stdout), also can be set by "
229
+ "environment variable CUOPT_SERVER_LOG_FILE"
230
+ ),
231
+ default=log_file,
232
+ )
233
+ parser.add_argument(
234
+ "-lm",
235
+ "--log-max",
236
+ type=int,
237
+ help=(
238
+ "Max bytes for a log file, also can be set by environment "
239
+ "variable CUOPT_SERVER_LOG_MAX"
240
+ ),
241
+ default=log_max,
242
+ )
243
+ parser.add_argument(
244
+ "-b",
245
+ "--log-backup",
246
+ type=int,
247
+ help=(
248
+ "Number of backup log files, also can be set by environment "
249
+ "variable CUOPT_SERVER_LOG_BACKUP"
250
+ ),
251
+ default=log_backup,
252
+ )
253
+ parser.add_argument(
254
+ "--ssl-certfile",
255
+ type=str,
256
+ help=(
257
+ "SSL certificate file with complete path, also can be set by "
258
+ "environment variable CUOPT_SSL_CERTFILE"
259
+ ),
260
+ default=ssl_certfile,
261
+ )
262
+ parser.add_argument(
263
+ "--ssl-keyfile",
264
+ type=str,
265
+ help=(
266
+ "SSL key file with complete path, also can be set by environment "
267
+ "variable CUOPT_SSL_KEYFILE"
268
+ ),
269
+ default=ssl_keyfile,
270
+ )
271
+ parser.add_argument(
272
+ "-g",
273
+ "--gpu-count",
274
+ type=int,
275
+ help=(
276
+ "Number of available GPUs to use for solver processes "
277
+ "(the available GPUs can be affected by arguments to 'docker' "
278
+ "when using a container and by the CUDA_VISIBLE_DEVICES "
279
+ "environment variable). If more GPUs are requested than are "
280
+ "available, cuopt will use all available. Default is 1, zero "
281
+ "will be ignored, also can be set by environment variable "
282
+ "CUOPT_GPU_COUNT"
283
+ ),
284
+ default=gpu_count,
285
+ )
286
+ parser.add_argument(
287
+ "-t",
288
+ "--tier",
289
+ type=str,
290
+ choices=request_filter.tier_list(),
291
+ help=(
292
+ "Feature tier for /cuopt/cuopt endpoint. This names a feature "
293
+ "tier defined in request_filter.py which restricts allowed "
294
+ "features or values for datasets in a request. Not currently "
295
+ "implemented for the /cuopt/request endpoint. "
296
+ "See request_filter.py for details. "
297
+ "Also can be set by environment variable CUOPT_TIER"
298
+ ),
299
+ default=tier,
300
+ )
301
+
302
+ args = parser.parse_args()
303
+ if args.log_file:
304
+ if args.log_backup < 1:
305
+ parser.error(
306
+ "Minimum number of backup log files is 1, also can be set by "
307
+ "environment variable CUOPT_SERVER_LOG_BACKUP"
308
+ )
309
+ handlers = [
310
+ logging.handlers.RotatingFileHandler(
311
+ args.log_file, args.log_max, args.log_backup
312
+ )
313
+ ]
314
+ else:
315
+ handlers = [logging.StreamHandler(sys.stdout)]
316
+
317
+ logging.basicConfig(
318
+ handlers=handlers,
319
+ level=levels[args.log_level],
320
+ format=log_fmt,
321
+ datefmt=date_fmt,
322
+ )
323
+ log_factory = logging.getLogRecordFactory()
324
+
325
+ def record_factory(*args, **kwargs):
326
+ record = log_factory(*args, **kwargs)
327
+ record.ncaid = get_ncaid()
328
+ record.requestid = get_requestid()
329
+ record.solverid = get_solverid()
330
+ if record.ncaid:
331
+ record.ncaid = f"NCA_ID={record.ncaid} "
332
+ if record.requestid:
333
+ record.requestid = f"NVCF_REQID={record.requestid} "
334
+ if record.solverid:
335
+ record.solverid = f" (GPU {record.solverid})"
336
+ return record
337
+
338
+ logging.setLogRecordFactory(record_factory)
339
+
340
+ message_init()
341
+ logging.info(f"cuopt server version {__version__}")
342
+
343
+ request_filter.set_tier(args.tier)
344
+ if args.datadir:
345
+ settings.set_data_dir(args.datadir)
346
+ if args.resultdir:
347
+ settings.set_result_dir(args.resultdir, args.maxresult, args.mode)
348
+
349
+ if not terminated.is_set():
350
+ if cuda_string:
351
+ logging.info(cuda_string)
352
+ if gpu_string is None:
353
+ logging.info("failed to read GPU list with nvidia-smi")
354
+ else:
355
+ logging.info(f"{', '.join(gpu_string)}")
356
+ if not gpu_ids:
357
+ logging.debug("failed to determine gpu ids")
358
+ else:
359
+ # validate visible setting if we can
360
+ visible = os.environ.get("CUDA_VISIBLE_DEVICES", "")
361
+ if visible:
362
+ filtered = list(set(visible.split(",")).intersection(gpu_ids))
363
+ if not filtered:
364
+ logging.error(
365
+ "CUDA_VISIBLE_DEVICES does not allow "
366
+ f"any available GPUs {gpu_ids}"
367
+ )
368
+ sys.exit(1)
369
+ gpu_ids = filtered
370
+ logging.debug(
371
+ f"gpu_ids after CUDA_VISIBLE_DEVICES filter {gpu_ids}"
372
+ )
373
+
374
+ gpu_count = args.gpu_count
375
+ if not isinstance(gpu_count, int):
376
+ gpu_count = 1
377
+ if not gpu_ids:
378
+ # We failed to read the list, skip the id
379
+ gpu_ids = [None]
380
+ else:
381
+ if gpu_count < 1:
382
+ gpu_count = 1
383
+ logging.warning("GPU count cannot be less than 1")
384
+ if gpu_count < len(gpu_ids):
385
+ gpu_ids = gpu_ids[0:gpu_count]
386
+
387
+ abort_list = create_abort_list()
388
+
389
+ for id in gpu_ids:
390
+ for p in range(int(procs_per_gpu)):
391
+ process_handler.create_process(
392
+ app_exit, job_queue, results_queue, abort_list, id
393
+ )
394
+
395
+ # For whatever reason, if we put this before the solver process
396
+ # then the cuda health checks fail. Check into this.
397
+ import socket
398
+
399
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as skt:
400
+ if skt.connect_ex(("localhost", args.port)) == 0:
401
+ logging.info(f"Port {args.port} already in use")
402
+ handle_exit("", "")
403
+
404
+ from cuopt_server.webserver import run_server
405
+
406
+ w = ctx.Process(
407
+ target=run_server,
408
+ args=(
409
+ app_exit,
410
+ results_queue,
411
+ job_queue,
412
+ abort_queue,
413
+ abort_list,
414
+ jobs_marked_done,
415
+ args.ip,
416
+ args.port,
417
+ args.log_level,
418
+ args.log_file,
419
+ args.log_max,
420
+ args.log_backup,
421
+ args.ssl_certfile,
422
+ args.ssl_keyfile,
423
+ ),
424
+ )
425
+
426
+ w.start()
427
+ watcher(app_exit, results_queue, job_queue, abort_queue, abort_list)
428
+ jobs_marked_done.wait(timeout=1)
429
+ for _, s in process_handler.get_solver_processes().items():
430
+ s.process.join()
431
+
432
+ else:
433
+ logging.info("calling handle_exit")
434
+ handle_exit("", "")
435
+
436
+ w.join()
437
+
438
+ # Check for leftover threads. Sometimes queue feeder threads do not exit
439
+ import threading
440
+
441
+ thread_names = {
442
+ t.name for t in threading.enumerate() if t.name != "MainThread"
443
+ }
444
+ if len(thread_names) > 0:
445
+ logging.info("not all threads exited in main process, killing")
446
+ psutil.Process(os.getpid()).kill()
@@ -0,0 +1,2 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,128 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import json
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import signal
9
+
10
+ import pexpect
11
+ import pytest
12
+
13
+ from cuopt_server import cuopt_service
14
+ from cuopt_server.tests.utils import utils
15
+ from cuopt_server.tests.utils.utils import RequestClient
16
+
17
+ # Find where is server script
18
+ server_script = cuopt_service.__file__
19
+ python_path = shutil.which("python")
20
+
21
+
22
+ @pytest.mark.skipif(
23
+ "x86" not in platform.uname().machine,
24
+ reason="Billing logs are not currently supported on non-x86_64 architectures (they take too long to run)", # noqa: E501
25
+ )
26
+ def test_time_limit_logs():
27
+ dataset_path = (
28
+ utils.RAPIDS_DATASET_ROOT_DIR
29
+ + "/cuopt_service_data/cuopt_problem_data.json"
30
+ )
31
+ dataset = json.load(open(dataset_path))
32
+ client = RequestClient(port=5010)
33
+ env = os.environ.copy()
34
+ env["CUOPT_MAX_SOLVE_TIME_LIMIT"] = "5"
35
+
36
+ url = "/cuopt/"
37
+
38
+ cmd = python_path + " " + server_script + " -i 127.0.0.1 -p 5010 -l debug"
39
+ process = pexpect.spawn(cmd, env=env)
40
+
41
+ try:
42
+ process.expect("Uvicorn running on", timeout=60, searchwindowsize=500)
43
+ except Exception:
44
+ pass
45
+ finally:
46
+ assert client.get(url + "health").status_code == 200
47
+
48
+ # /cuopt/cuopt endpoint, time limit omitted
49
+ dataset["solver_config"]["time_limit"] = None
50
+ client.post(
51
+ url + "cuopt",
52
+ json={"action": "cuOpt_OptimizedRouting", "data": dataset},
53
+ )
54
+ process.expect(
55
+ "Solver time limit not specified, setting to", timeout=60
56
+ )
57
+
58
+ # /cuopt/cuopt endpoint, time limit modified (too high)
59
+ dataset["solver_config"]["time_limit"] = 100
60
+ client.post(
61
+ url + "cuopt",
62
+ json={"action": "cuOpt_OptimizedRouting", "data": dataset},
63
+ )
64
+ process.expect("Solver time modified to", timeout=60)
65
+
66
+ # /cuopt/cuopt endpoint, use specified (low)
67
+ dataset["solver_config"]["time_limit"] = 0.1
68
+ client.post(
69
+ url + "cuopt",
70
+ json={"action": "cuOpt_OptimizedRouting", "data": dataset},
71
+ )
72
+ process.expect("Using specified solver time 0.1", timeout=60)
73
+
74
+ # /cuopt/cuopt endpoint, solver_config omitted
75
+ save_config = dataset["solver_config"]
76
+ del dataset["solver_config"]
77
+ client.post(
78
+ url + "cuopt",
79
+ json={"action": "cuOpt_OptimizedRouting", "data": dataset},
80
+ )
81
+ process.expect("Creating default solver config")
82
+ process.expect(
83
+ "Solver time limit not specified, setting to", timeout=60
84
+ )
85
+
86
+ # /cuopt/cuopt solver_config none
87
+ dataset["solver_config"] = None
88
+ client.post(
89
+ url + "cuopt",
90
+ json={"action": "cuOpt_OptimizedRouting", "data": dataset},
91
+ )
92
+ process.expect("Creating default solver config")
93
+ process.expect(
94
+ "Solver time limit not specified, setting to", timeout=60
95
+ )
96
+
97
+ # /cuopt/request, time limit omitted
98
+ dataset["solver_config"] = save_config
99
+ dataset["solver_config"]["time_limit"] = None
100
+ client.post(url + "request", json=dataset)
101
+ process.expect(
102
+ "Solver time limit not specified, setting to", timeout=60
103
+ )
104
+
105
+ # /cuopt/request, use specified time
106
+ dataset["solver_config"]["time_limit"] = 0.1
107
+ client.post(url + "request", json=dataset)
108
+ process.expect("Using specified solver time 0.1", timeout=60)
109
+
110
+ # /cuopt/request, solver config omitted
111
+ del dataset["solver_config"]
112
+ client.post(url + "request", json=dataset)
113
+ process.expect("Creating default solver config")
114
+ process.expect(
115
+ "Solver time limit not specified, setting to", timeout=60
116
+ )
117
+
118
+ # /cuopt/request, solver config omitted
119
+ dataset["solver_config"] = None
120
+ client.post(url + "request", json=dataset)
121
+ process.expect("Creating default solver config")
122
+ process.expect(
123
+ "Solver time limit not specified, setting to", timeout=60
124
+ )
125
+ process.kill(signal.SIGINT)
126
+ process.wait()
127
+ if process.isalive():
128
+ process.kill(signal.SIGTERM)
@@ -0,0 +1,26 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from cuopt_server.tests.utils.utils import RequestClient, cuoptproc # noqa
5
+
6
+ client = RequestClient()
7
+
8
+
9
+ def test_health(cuoptproc): # noqa
10
+ # Normal health check
11
+ response = client.get("/cuopt/health")
12
+ assert response.status_code == 200
13
+
14
+ # health check with root path
15
+ response = client.get("/")
16
+ assert response.status_code == 200
17
+
18
+
19
+ def test_readiness(cuoptproc): # noqa
20
+ response = client.get("/v2/health/ready")
21
+ assert response.status_code == 200
22
+
23
+
24
+ def test_liveness(cuoptproc): # noqa
25
+ response = client.get("/v2/health/live")
26
+ assert response.status_code == 200