From 67c3bc55ba6f209be0b11d9bbdb24f60d2c63e8b Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Fri, 9 Oct 2026 23:29:23 +0800 Subject: [PATCH] chore(judge): document explicit managed runtime rollout --- README.md | 8 +- backend/src/kf_oj/modules/problems/README.md | 14 +- backend/tests/test_judging_v2_contracts.py | 32 ++++ deploy/README.md | 34 ++++ deploy/judge.admission.example.json | 185 +++++++++++++++++++ deploy/sandbox.managed.runtime.example.json | 111 +++++++++++ judge/README.md | 30 +++ judge/crates/sandbox/tests/runtime_policy.rs | 26 +++ 8 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 deploy/judge.admission.example.json create mode 100644 deploy/sandbox.managed.runtime.example.json diff --git a/README.md b/README.md index bc04ef8..ab503e5 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,12 @@ make sandbox-probe # 只读观察 Linux 环境,不创建隔离资源 开发中的规划文档仅在本地维护,不包含在远端仓库中。 单机提交服务的配置、最小权限、停止恢复与独立 VM 验收要求见 [判题说明](judge/README.md#单机持久化协调与常驻-worker)和[部署说明](deploy/README.md#单机提交服务的启动与停止)。 -当前范围为单机普通题、已有账号、固定 GNU C17/C++20 与精确字节比较;自助注册、 -自测、SPJ/交互题、Python/Rust 用户程序、分布式调度和学习能力仍待后续实现。 +当前普通题执行支持固定 GNU C17/C++20、CPython 3.13.16 和 OpenJDK 21.0.12.1+1-LTS, +使用精确字节比较。Python 固定单文件 main.py 与标准库;Java 固定默认包 Main.java/Main, +允许有界辅助/内部/匿名类。题目必须显式声明对应语言能力,旧题目不会自动获得新语言。 +新版源码与普通 Worker 的沙箱执行已在独立 VM 验收;完整平台/浏览器及故障验收按工程阶段 +分别记录,生产部署需另行进行。自助注册、自测、SPJ/交互题、Rust 用户程序、分布式调度 +和学习能力仍待后续实现。 ## 许可证 diff --git a/backend/src/kf_oj/modules/problems/README.md b/backend/src/kf_oj/modules/problems/README.md index 8e39814..206ced8 100644 --- a/backend/src/kf_oj/modules/problems/README.md +++ b/backend/src/kf_oj/modules/problems/README.md @@ -10,7 +10,7 @@ 要求配置 `KF_OJ_ARTIFACT_ROOT` 和可信 `KF_OJ_JUDGE_ADMISSION` JSON;同一准入配置应交付给节点, 不能由题目放宽。结构示例来自共享 `judge-admission.valid.json`,不是当前宿主隔离验收证明。 -清单固定 C17/C++20、工具链/镜像版本、精确字节比较、编译/运行/整个作业预算,并声明按序 +v1 清单固定 C17/C++20、工具链/镜像版本、精确字节比较、编译/运行/整个作业预算,并声明按序 测试点 `{key,input_file,expected_file}` 和最多十对公开样例 `{input_file,output_file}`。 题面以 `statement_file` 读取 UTF-8。每份样例至多 64 KiB,响应采用 base64 保留二进制数据。 同一 slug 受事务锁保护,每次发布递增 revision;发布后题面、限额、样例、测试点不可修改。 @@ -19,3 +19,15 @@ `GET /api/v1/problems` 以 slug 游标分页(`after` / `limit`,最多 100 项), `GET /api/v1/problems/versions/{uuid}` 读取已发布版本。禁用逻辑题目时其版本对公开读取不可用。 内部判题投影仅供授权应用编排使用;外部 Worker 读取由 submissions 的有效租约端口控制。 + + +v2 使用 `schema_version: 2` 与 `language_configurations`,每个配置包含 `language`、 +完整 `toolchain` 身份、`compile_limits`、`run_limits` 和 `job_wall_time_us`。 +可分别声明 `c17`、`cpp20`、`python313`、`java21`;不再同时提供 v1 顶层工具链或预算。 +所有语言按可信节点配置准入,缺失能力/错误补丁版本/镜像摘要/过宽预算在发布前拒绝。 +`judge.admission.example.json` 的镜像摘要为占位值,使用前替换为管理员制作镜像的实际摘要。 +公开题面仅显示固定语言、版本、入口与预算,不公布宿主镜像路径和隐藏工件。 + +导入同一 slug 的 v2 版本保持原 v1 版本与提交不变。已有提交按其保存的协议版本解析, +不能重新序列化旧 canonical 数据来补入新字段。Java 使用 Main.java 与默认包 Main; +Python 使用 main.py。每个新版本的语言集合独立,浏览器不得为旧版本补开新语言。 diff --git a/backend/tests/test_judging_v2_contracts.py b/backend/tests/test_judging_v2_contracts.py index b51dbff..541d9c0 100644 --- a/backend/tests/test_judging_v2_contracts.py +++ b/backend/tests/test_judging_v2_contracts.py @@ -235,3 +235,35 @@ def test_generic_admission_revalidates_constructed_values_in_both_generations(): with pytest.raises(ValueError): v2.admit_manifest(registry, wrong, 1) v2.admit_manifest(registry, legacy, 1) + + +def test_operator_registry_example_preserves_old_protocol_and_binds_managed_runtime(): + policy = v2.AdmissionRegistry.model_validate_json( + (ROOT / "deploy/judge.admission.example.json").read_bytes() + ) + old = v1.decode_wire( + v1.JobManifest, (ROOT / "tests/fixtures/contracts/judge-manifest.valid.json").read_bytes() + ) + policy.admit(old, 1) + assert policy.multi_runtime is not None + root = json.loads((ROOT / "deploy/sandbox.managed.runtime.example.json").read_text()) + for cap in policy.multi_runtime.capabilities: + selected = next( + item + for item in root["runtime_images"] + if item["runtime_identity"] == cap.toolchain.model_dump(mode="json") + ) + ceiling = v1.RunLimits.model_validate(selected["per_task_ceiling"]) + assert cap.compile_ceiling.fits(ceiling) and cap.run_ceiling.fits(ceiling) + manifest = fixture(f"judge-manifest.valid-{cap.language}", v2.JobManifest).model_copy( + update={"compile_limits": cap.compile_ceiling, "run_limits": cap.run_ceiling} + ) + policy.admit(manifest, 1) + changed = manifest.model_copy( + update={"toolchain": manifest.toolchain.model_copy(update={"image_digest": "f" * 64})} + ) + with pytest.raises(ValueError): + policy.admit(changed, 1) + legacy_only = v2.AdmissionRegistry(schema_version=2, legacy=policy.legacy, multi_runtime=None) + with pytest.raises(ValueError): + legacy_only.admit(fixture("judge-manifest.valid-java21", v2.JobManifest), 1) diff --git a/deploy/README.md b/deploy/README.md index a91c9e5..3927d0b 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -106,3 +106,37 @@ attempt 或强改状态。旧租约不能覆盖新结果,有限基础设施重 挂载、无业务凭据且整体上限已读回的独立测试 VM 内验收。开发 Compose 仍只提供可信 数据库和 broker,不在宿主或业务服务器运行用户代码。workflow 如需启用由指定 GitHub 私有仓库承载,并设置有限并发;Gitea 只保存逐阶段工程提交。 + + +## 四语言节点的受控升级 + +新增公开示例 `judge.admission.example.json` 为显式 v1/v2 注册表, +`sandbox.managed.runtime.example.json` 为一槽位的共享 Root 配置;原 v1 示例保持原语义。 +所有 `aaaaaaaa…` 镜像摘要均是无权限占位值,不能当作已验镜像。管理员制作固定镜像后, +对 manifest.json 的实际字节计算 SHA-256,逐文件验证版本/字节/权限,并将相同完整身份 +写入准入注册表与 Root runtime_images。Root 镜像路径仅在私有可信配置中保存。 +配置解析通过或 check-runtime 可见仍不允许跳过实际隔离门控。 + +1. 停止创建新提交/消费投递,向所有普通 Worker 发 SIGTERM;保留 DB、清单、工件和 journal。 +2. 取得工厂实际 stopped/cleanup_complete 证明,读回空任务 cgroup 和 owned 资源; + 未排空或存在未知资源时停止升级,保留证据,不递归清空任务目录。 +3. 使用独立迁移账号执行 Backend Alembic upgrade;新语言数据存在时旧迁移降级会拒绝, + 不删除数据以实现回退。平台与 Coordinator 继续使用各自最小权限角色,Worker 无 DB 账号。 +4. 安装固定版本的 Backend/Coordinator/Worker/Sandbox;同时更新三个普通服务的同一 + `judge_admission`/`admission` 注册表,Root 配置单独由管理员持有。旧消费者不混用新协议。 +5. 为整体节点、控制面各服务、工厂和任务设置有限 CPU/memory/swap/pids/文件/期限, + 读回内核值。节点至少预留一份512MiB编译任务以及独立控制面;Java运行示例总预算384MiB, + Xmx不能代替整体预算。并发1/prefetch1,共享UID池,不因第二Worker存在而扩大节点槽位。 +6. 在独立无NIC、无宿主共享挂载、无业务凭据 VM 用合成题目验证四语言、故障与实际排空, + 再恢复工厂/Coordinator/API/publisher/Worker。生产启用需单独安排,仓库不自动部署。 + +请求不在线安装 Python/Java 依赖,不运行 pip/uvx,不接收自定义解释器参数/JAR/agents。 +Python main.py 仅标准库;Java Main.java 使用默认包 Main,最多256个有界编译class。 +题目语言与预算以公开版本配置为准,旧题目和草稿保留,Rust继续明确不支持。 +新版检查阶段CE/运行RE/真实限额与SystemError的区别见 [判题说明](../judge/README.md#python--java-与明确协议版本)。 + +停止/故障恢复沿原单机流程:停止Worker使任务全树排空,旧租约/report拒绝后恢复; +publisher/broker中断保留outbox,有限重试而非清空记录。恢复旧软件前应先排空v2任务, +保留不可变v2版本和报告供兼容消费者读取;不能把旧配置静默转换为宽松新能力。 +这些公开配置不含业务令牌。数据库/AMQP/服务令牌分别在仓库外私下生成,Root不持有它们。 +需要CI时只在GitHub私有仓库 `KiriAky107/KF-OnlineJudge` 运行并设置有限并发,Gitea保存源码。 diff --git a/deploy/judge.admission.example.json b/deploy/judge.admission.example.json new file mode 100644 index 0000000..e791eda --- /dev/null +++ b/deploy/judge.admission.example.json @@ -0,0 +1,185 @@ +{ + "schema_version": 2, + "legacy": { + "max_concurrent_jobs": 1, + "prefetch": 1, + "max_retries": 2, + "max_job_wall_time_us": 60000000, + "max_package_bytes": 67108864, + "compile_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + }, + "run_ceiling": { + "cpu_time_us": 1000000, + "wall_time_us": 3000000, + "memory_bytes": 67108864, + "stack_bytes": 8388608, + "processes": 8, + "open_files": 64, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 16777216, + "workdir_bytes": 67108864 + }, + "toolchain": { + "toolchain_id": "gnu-cxx-v1", + "image_kind": "linux_gnu_cxx_x86_64_v1", + "image_revision": 1 + } + }, + "multi_runtime": { + "max_concurrent_jobs": 1, + "prefetch": 1, + "max_retries": 2, + "max_job_wall_time_us": 60000000, + "max_package_bytes": 67108864, + "schema_version": 2, + "capabilities": [ + { + "language": "c17", + "toolchain": { + "toolchain_id": "gnu-cxx-v1", + "image_kind": "linux_gnu_cxx_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "gnu-cxx-v1" + }, + "compile_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + }, + "run_ceiling": { + "cpu_time_us": 1000000, + "wall_time_us": 3000000, + "memory_bytes": 67108864, + "stack_bytes": 8388608, + "processes": 8, + "open_files": 64, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 16777216, + "workdir_bytes": 67108864 + } + }, + { + "language": "cpp20", + "toolchain": { + "toolchain_id": "gnu-cxx-v1", + "image_kind": "linux_gnu_cxx_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "gnu-cxx-v1" + }, + "compile_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + }, + "run_ceiling": { + "cpu_time_us": 1000000, + "wall_time_us": 3000000, + "memory_bytes": 67108864, + "stack_bytes": 8388608, + "processes": 8, + "open_files": 64, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 16777216, + "workdir_bytes": 67108864 + } + }, + { + "language": "python313", + "toolchain": { + "toolchain_id": "cpython313-v1", + "image_kind": "linux_cpython313_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "3.13.16" + }, + "compile_ceiling": { + "cpu_time_us": 1000000, + "wall_time_us": 3000000, + "memory_bytes": 67108864, + "stack_bytes": 8388608, + "processes": 8, + "open_files": 64, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 16777216, + "workdir_bytes": 67108864 + }, + "run_ceiling": { + "cpu_time_us": 1000000, + "wall_time_us": 3000000, + "memory_bytes": 67108864, + "stack_bytes": 8388608, + "processes": 8, + "open_files": 64, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 16777216, + "workdir_bytes": 67108864 + } + }, + { + "language": "java21", + "toolchain": { + "toolchain_id": "openjdk21-v1", + "image_kind": "linux_openjdk21_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "21.0.12.1+1-LTS" + }, + "compile_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + }, + "run_ceiling": { + "cpu_time_us": 1000000, + "wall_time_us": 3000000, + "memory_bytes": 402653184, + "stack_bytes": 8388608, + "processes": 32, + "open_files": 64, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 16777216, + "workdir_bytes": 67108864 + } + } + ] + } +} diff --git a/deploy/sandbox.managed.runtime.example.json b/deploy/sandbox.managed.runtime.example.json new file mode 100644 index 0000000..400a7af --- /dev/null +++ b/deploy/sandbox.managed.runtime.example.json @@ -0,0 +1,111 @@ +{ + "schema_version": 1, + "state_root": "/var/lib/kf-judge", + "tasks_root": "/var/lib/kf-judge/tasks", + "image_root": "/var/lib/kf-judge/images/gnu-cxx-x86_64-v1", + "image_kind": "linux_gnu_cxx_x86_64_v1", + "delegated_cgroup_root": "/sys/fs/cgroup/kf-judge.service", + "identities": { + "worker_uid": 1000, + "worker_gid": 1000, + "first_task_uid": 100000, + "first_task_gid": 100000, + "count": 1 + }, + "max_concurrent_runs": 1, + "per_task_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + }, + "aggregate": { + "memory_bytes": 805306368, + "supervisor_memory_bytes": 268435456, + "processes": 64, + "supervisor_processes": 32, + "workdir_bytes": 402653184, + "supervisor_workdir_bytes": 67108864, + "cpu_period_us": 100000, + "cpu_quota_us": 150000, + "task_cpu_quota_us": 100000, + "supervisor_cpu_quota_us": 50000 + }, + "runtime_images": [ + { + "image_root": "/var/lib/kf-judge/images/gnu-cxx-v2", + "image_kind": "linux_gnu_cxx_x86_64_v1", + "runtime_identity": { + "toolchain_id": "gnu-cxx-v1", + "image_kind": "linux_gnu_cxx_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "gnu-cxx-v1" + }, + "per_task_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + } + }, + { + "image_root": "/var/lib/kf-judge/images/cpython313-v1", + "image_kind": "linux_cpython313_x86_64_v1", + "runtime_identity": { + "toolchain_id": "cpython313-v1", + "image_kind": "linux_cpython313_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "3.13.16" + }, + "per_task_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + } + }, + { + "image_root": "/var/lib/kf-judge/images/openjdk21-v1", + "image_kind": "linux_openjdk21_x86_64_v1", + "runtime_identity": { + "toolchain_id": "openjdk21-v1", + "image_kind": "linux_openjdk21_x86_64_v1", + "image_revision": 1, + "image_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "runtime_version": "21.0.12.1+1-LTS" + }, + "per_task_ceiling": { + "cpu_time_us": 10000000, + "wall_time_us": 30000000, + "memory_bytes": 536870912, + "stack_bytes": 16777216, + "processes": 32, + "open_files": 128, + "stdout_bytes": 1048576, + "stderr_bytes": 1048576, + "file_bytes": 67108864, + "workdir_bytes": 268435456 + } + } + ] +} diff --git a/judge/README.md b/judge/README.md index ef63067..a18bc0e 100644 --- a/judge/README.md +++ b/judge/README.md @@ -152,3 +152,33 @@ Worker SIGTERM 取消当前执行后退出,SIGKILL 由 Root EOF 清理、租 Worker 的 `lapin 2.5.5` / `futures-lite 2.6.1` 负责 AMQP 通知,`reqwest 0.12.24` 负责有界 loopback 请求并禁用代理与重定向;`uuid 1.27.0` 生成身份,`sha2 0.10.9` 验证清单/工件/报告摘要。Engine 不引入数据库、HTTP、队列或权限依赖。 + + +## Python / Java 与明确协议版本 + +新语言使用 v2 清单、typed 程序工件、阶段和报告。旧 v1 清单/报告/canonical 摘要保持原样, +不默加新字段。需要同一节点处理两代任务时,管理员显式配置 `AdmissionRegistry` 的 +`legacy` 与 `multi_runtime`;仅配置旧能力的消费者继续拒绝 v2。两代配置必须共享 +并发/prefetch/重试预算,所有镜像共用一个 Root 工厂、UID 池和容量预留。 + +CPython 固定 3.13.16,仅 main.py/标准库。准备 profile 只分析/编译源码,不执行顶层代码, +不导入用户 pyc、marshal、pickle 或 ZIP;运行使用已检查的封印源码。 +OpenJDK 固定 Temurin 21.0.12.1+1-LTS,固定 Main.java/default-package Main;javac +使用固定 UTF-8 与禁用 annotation processing 的参数,允许最多256个有界平铺 class, +包括辅助/内部/匿名类,class 文件名仅允许受控 ASCII 字符且最长128字节。链接、目录替换、空类集、重复/超量/过长或不支持名称拒绝; +完整类集封印后逐点使用。同一题的每点都重新建立 helper、UID 与工作根。 + +每次检查、编译、运行均通过 Root 隔离门控,不在普通 Worker 执行解释器或 javac。 +请求不能指定运行镜像宿主路径、任意 flags、classpath、JAR、agent 或环境变量; +PYTHONPATH、JAVA_TOOL_OPTIONS、JDK_JAVA_OPTIONS 等不从宿主继承。 +语法/编译/入口错误为 CE,普通运行异常为 RE;真实可信 CPU/墙钟、cgroup OOM 和 +输出超限分别为 TLE/MLE/OLE。用户自抛 MemoryError/OOM、打印文字或 exit code +不能证明 MLE。JVM Xmx 仅限制堆,元空间、代码缓存、native、每个线程栈均包含在 +完整 cgroup 预算;低于启动/非堆预留的配置提前拒绝。镜像/门控/通信/清理故障为 +SystemError,未知计量不补零。 + +镜像制作使用 `scripts/build_judging_images.py`,固定来源/摘要与闭合动态库列表, +不复制平台 venv、site-packages、服务配置或凭据。产物经 root 所有权、不可写目录、 +逐文件摘要及 catalog 摘要核对后发布;路径只由可信 Root 配置决定。 +生产标准预算不使用 VM 模拟器验收时明确设置的较慢启动预算。 +新版节点配置与排空步骤见 [部署说明](../deploy/README.md#四语言节点的受控升级)。 diff --git a/judge/crates/sandbox/tests/runtime_policy.rs b/judge/crates/sandbox/tests/runtime_policy.rs index 4e7362b..580fa4f 100644 --- a/judge/crates/sandbox/tests/runtime_policy.rs +++ b/judge/crates/sandbox/tests/runtime_policy.rs @@ -336,3 +336,29 @@ fn task_and_image_roots_cannot_overlap_private_capture_storage() { } } } + +#[test] +fn managed_operator_example_shares_one_pool_and_rejects_unreserved_capacity() { + let config: RuntimeConfig = serde_json::from_str(include_str!( + "../../../../deploy/sandbox.managed.runtime.example.json" + )) + .unwrap(); + let validated = config.clone().validate().unwrap(); + assert_eq!(validated.task_identity(0), Some((100000, 100000))); + assert_eq!(validated.task_identity(1), None); + assert_eq!(config.runtime_images.len(), 3); + // Adding languages never creates independent identity or resource pools. + let mut unreserved = config.clone(); + unreserved.max_concurrent_runs = 2; + unreserved.identities.count = 2; + assert!(unreserved.validate().is_err()); + let mut unknown = config.clone(); + unknown.runtime_images[1].runtime_identity.runtime_version = + "3.14.7".to_string().try_into().unwrap(); + assert!(unknown.validate().is_err()); + let mut duplicate = config.clone(); + duplicate + .runtime_images + .push(config.runtime_images[0].clone()); + assert!(duplicate.validate().is_err()); +}