Skip to content

Commit d4b98d6

Browse files
PanGan21Eikix
authored andcommitted
feat(test-suite): add per-service override support to fhevm-cli (#2075)
* feat(test-suite): add per-service override support to fhevm-cli * feat(test-suite): warn user when selecting services that share a database
1 parent 34320f2 commit d4b98d6

File tree

7 files changed

+187
-29
lines changed

7 files changed

+187
-29
lines changed

test-suite/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,12 @@ cd test-suite/fhevm
5757
./fhevm-cli test public-decrypt-http-ebool
5858
./fhevm-cli test erc20
5959

60-
# Boot with a local coprocessor override
60+
# Boot with a local coprocessor override (all services)
6161
./fhevm-cli up --target latest-release --override coprocessor --profile local
6262

63+
# Boot with only specific coprocessor services built locally
64+
./fhevm-cli up --target latest-release --override coprocessor:host-listener,tfhe-worker --profile local
65+
6366
# View logs
6467
./fhevm-cli logs relayer
6568

@@ -74,11 +77,17 @@ For the local CLI entrypoint and architecture, see [test-suite/fhevm/README.md](
7477
To run one local component on top of an otherwise versioned stack, use `--override`:
7578

7679
```sh
80+
# Override an entire group (builds all services locally)
7781
./fhevm-cli up --target latest-release --override coprocessor --profile local
82+
83+
# Override specific services within a group (others pull from registry)
84+
./fhevm-cli up --target latest-release --override coprocessor:host-listener,tfhe-worker --profile local
7885
```
7986

8087
Supported override groups are `coprocessor`, `kms-connector`, `gateway-contracts`, `host-contracts`, and `test-suite`.
8188

89+
When specifying individual services, use the short suffix after the group prefix (e.g., `host-listener` not `coprocessor-host-listener`). Services that share a Docker image are automatically co-selected (e.g., `host-listener` includes `host-listener-poller`).
90+
8291
### Resuming a deployment
8392

8493
If a boot fails mid-way, you can resume from a specific step:

test-suite/fhevm/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ flowchart TD
3232
F8 --> F9["relayer"]
3333
F9 --> F10["test-suite"]
3434
35-
G["Local overrides"] --> E
35+
G["Local overrides (group or per-service)"] --> E
3636
H["Multicopro topology + per-instance overrides"] --> E
3737
I["Compatibility policy"] --> E
3838
I --> F7

test-suite/fhevm/README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,68 @@ Supported groups:
162162
- `host-contracts`
163163
- `test-suite`
164164

165-
Example:
165+
### Override an entire group
166166

167167
```sh
168168
./fhevm-cli up --target latest-release --override coprocessor --profile local
169169
```
170170

171+
### Override specific services
172+
173+
To build only specific services locally while pulling the rest from the registry:
174+
175+
```sh
176+
./fhevm-cli up --target latest-release --override coprocessor:host-listener,tfhe-worker --profile local
177+
```
178+
179+
Use the short service suffix after the colon (e.g., `host-listener` not `coprocessor-host-listener`).
180+
Multiple services are comma-separated. Services that share a Docker image are automatically
181+
co-selected (e.g., `host-listener` includes `host-listener-poller`).
182+
183+
> **Note:** `coprocessor` and `kms-connector` services share a database. Per-service overrides
184+
> work when your local changes don't include DB migrations. If your changes add or alter
185+
> migrations, non-overridden services will fail against the mismatched schema — use
186+
> `--override coprocessor` (full group) instead.
187+
188+
Available suffixes per group:
189+
190+
| Group | Suffixes |
191+
|-------|----------|
192+
| `coprocessor` | `db-migration`, `host-listener`, `host-listener-poller`, `gw-listener`, `tfhe-worker`, `zkproof-worker`, `sns-worker`, `transaction-sender` |
193+
| `kms-connector` | `db-migration`, `gw-listener`, `kms-worker`, `tx-sender` |
194+
| `gateway-contracts` | `deploy-mocked-zama-oft`, `set-relayer-mocked-payment`, `sc-deploy`, `sc-add-network`, `sc-add-pausers`, `sc-trigger-keygen`, `sc-trigger-crsgen` |
195+
| `host-contracts` | `sc-deploy`, `sc-add-pausers` |
196+
| `test-suite` | `e2e-debug` |
197+
198+
### Multiple overrides
199+
200+
Repeat `--override` to override several groups at once:
201+
202+
```sh
203+
# Two full groups
204+
./fhevm-cli up --target latest-release --override coprocessor --override gateway-contracts --profile local
205+
206+
# Per-service across groups
207+
./fhevm-cli up --target latest-release --override coprocessor:host-listener --override gateway-contracts:sc-deploy --profile local
208+
209+
# Mixed: per-service + full group
210+
./fhevm-cli up --target latest-release --override coprocessor:host-listener --override kms-connector --profile local
211+
```
212+
213+
The `--profile` flag applies to every override that doesn't specify its own profile.
214+
215+
### Combining with env var overrides
216+
217+
You can mix per-service local builds with registry tag overrides:
218+
219+
```sh
220+
COPROCESSOR_GW_LISTENER_VERSION=abc1234 \
221+
./fhevm-cli up --target latest-release --override coprocessor:host-listener --profile local
222+
```
223+
224+
This builds `host-listener` (and `host-listener-poller`) locally, pulls `gw-listener` at tag
225+
`abc1234`, and pulls all other coprocessor services at the resolved target version.
226+
171227
## Multicopro
172228

173229
Example:

test-suite/fhevm/src/artifacts.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ENV_DIR,
1414
GROUP_BUILD_COMPONENTS,
1515
GROUP_BUILD_SERVICES,
16+
GROUP_SERVICE_SUFFIXES,
1617
TEMPLATE_COMPOSE_DIR,
1718
TEMPLATE_ENV_DIR,
1819
TEMPLATE_RELAYER_CONFIG,
@@ -70,9 +71,11 @@ const loadComposeDoc = async (component: string) =>
7071

7172
const overriddenServicesForComponent = (state: State, component: string) =>
7273
new Set(
73-
state.overrides.flatMap((o) =>
74-
GROUP_BUILD_COMPONENTS[o.group].includes(component) ? GROUP_BUILD_SERVICES[o.group] : [],
75-
),
74+
state.overrides.flatMap((o) => {
75+
if (!GROUP_BUILD_COMPONENTS[o.group].includes(component)) return [];
76+
if (o.services?.length) return o.services;
77+
return GROUP_BUILD_SERVICES[o.group];
78+
}),
7679
);
7780

7881
const retagLocal = (image: unknown) =>
@@ -209,10 +212,10 @@ const buildCoprocessorOverride = async (state: State) => {
209212
const baseOverride = state.topology.instances["coprocessor-0"];
210213
const baseEnv = await readEnvFile(envPath("coprocessor"));
211214
const compat = compatPolicyForState(state);
212-
// Skip compat args for overridden services — local builds use HEAD code, not the resolved version.
213-
const compatArgs = overridden.size ? {} : compat.coprocessorArgs;
214215
for (const [name, service] of Object.entries(doc.services)) {
215-
const adjusted = applyInstanceAdjustments(service, envPath("coprocessor"), baseEnv, baseOverride, compatArgs);
216+
// Skip compat args for overridden services — local builds use HEAD code, not the resolved version.
217+
const serviceCompatArgs = overridden.has(name) ? {} : compat.coprocessorArgs;
218+
const adjusted = applyInstanceAdjustments(service, envPath("coprocessor"), baseEnv, baseOverride, serviceCompatArgs);
216219
applyBuildPolicy(adjusted, overridden.has(name));
217220
services[name] = adjusted;
218221
}
@@ -222,12 +225,13 @@ const buildCoprocessorOverride = async (state: State) => {
222225
const instanceEnv = await readEnvFile(envPath(`coprocessor.${index}`));
223226
for (const [name, service] of Object.entries(doc.services)) {
224227
const suffix = name.replace(/^coprocessor-/, "");
228+
const instanceCompatArgs = overridden.has(name) ? {} : compat.coprocessorArgs;
225229
const cloned = applyInstanceAdjustments(
226230
service,
227231
envPath(`coprocessor.${index}`),
228232
instanceEnv,
229233
override,
230-
compatArgs,
234+
instanceCompatArgs,
231235
);
232236
cloned.container_name = prefix + suffix;
233237
applyBuildPolicy(cloned, overridden.has(name));
@@ -480,7 +484,10 @@ const maybeBuild = async (
480484
if (GROUP_BUILD_COMPONENTS[override.group].includes(component)) {
481485
const doc = YAML.parse(await fs.readFile(composePath(component), "utf8")) as ComposeDoc;
482486
const available = new Set(Object.keys(doc.services));
483-
const services = GROUP_BUILD_SERVICES[override.group].filter((s) => available.has(s));
487+
const candidates = override.services?.length
488+
? override.services
489+
: GROUP_BUILD_SERVICES[override.group];
490+
const services = candidates.filter((s) => available.has(s));
484491
if (!services.length) {
485492
continue;
486493
}
@@ -549,16 +556,7 @@ export const serviceNameList = (state: State, component: string) => {
549556
if (component !== "coprocessor") {
550557
return [];
551558
}
552-
const suffixes = [
553-
"db-migration",
554-
"host-listener",
555-
"host-listener-poller",
556-
"gw-listener",
557-
"tfhe-worker",
558-
"zkproof-worker",
559-
"sns-worker",
560-
"transaction-sender",
561-
];
559+
const suffixes = GROUP_SERVICE_SUFFIXES["coprocessor"];
562560
const names: string[] = [];
563561
for (let index = 0; index < state.topology.count; index += 1) {
564562
for (const suffix of suffixes) {

test-suite/fhevm/src/layout.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,61 @@ export const GROUP_BUILD_SERVICES: Record<OverrideGroup, string[]> = {
9696
"test-suite": ["test-suite-e2e-debug"],
9797
};
9898

99+
const GROUP_PREFIX: Record<OverrideGroup, string> = {
100+
"coprocessor": "coprocessor-",
101+
"kms-connector": "kms-connector-",
102+
"gateway-contracts": "gateway-",
103+
"host-contracts": "host-",
104+
"test-suite": "test-suite-",
105+
};
106+
107+
/** Derived from GROUP_BUILD_SERVICES by stripping the group prefix. */
108+
export const GROUP_SERVICE_SUFFIXES: Record<OverrideGroup, string[]> = Object.fromEntries(
109+
Object.entries(GROUP_BUILD_SERVICES).map(([group, services]) => [
110+
group,
111+
services.map((s) => s.slice(GROUP_PREFIX[group as OverrideGroup].length)),
112+
]),
113+
) as Record<OverrideGroup, string[]>;
114+
115+
/** Canonical sibling groups — services sharing the same Docker image. */
116+
const SIBLING_GROUPS: string[][] = [
117+
["coprocessor-host-listener", "coprocessor-host-listener-poller"],
118+
];
119+
120+
/** Bidirectional lookup derived from SIBLING_GROUPS. */
121+
export const IMAGE_SIBLINGS: Record<string, string[]> = {};
122+
for (const group of SIBLING_GROUPS) {
123+
for (const member of group) {
124+
IMAGE_SIBLINGS[member] = group.filter((s) => s !== member);
125+
}
126+
}
127+
128+
/** Groups where all services share a database — per-service override requires schema compatibility. */
129+
export const SCHEMA_COUPLED_GROUPS: OverrideGroup[] = ["coprocessor", "kms-connector"];
130+
131+
export const suffixToServiceName = (group: OverrideGroup, suffix: string): string => {
132+
const fullName = GROUP_PREFIX[group] + suffix;
133+
if (!GROUP_BUILD_SERVICES[group].includes(fullName)) {
134+
throw new Error(
135+
`Unknown service "${suffix}" in group "${group}". Valid: ${GROUP_SERVICE_SUFFIXES[group].join(", ")}`,
136+
);
137+
}
138+
return fullName;
139+
};
140+
141+
/** Resolve suffixes to full service names, auto-including image siblings. */
142+
export const resolveServiceOverrides = (group: OverrideGroup, suffixes: string[]): string[] => {
143+
const names = new Set<string>();
144+
for (const suffix of suffixes) {
145+
const name = suffixToServiceName(group, suffix);
146+
names.add(name);
147+
for (const sibling of IMAGE_SIBLINGS[name] ?? []) {
148+
names.add(sibling);
149+
}
150+
}
151+
return [...names];
152+
};
153+
99154
export const TEST_GREP: Record<string, string> = {
100155
"paused-host-contracts": "test paused host user input|test paused host HTTP public decrypt|test paused host operators",
101156
"paused-gateway-contracts":

test-suite/fhevm/src/runtime.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { composeDown, composeUp, inspectImageId, regen, resolvedComposeEnv, serv
1616
import {
1717
COMPONENT_BY_STEP,
1818
COMPONENTS,
19+
GROUP_SERVICE_SUFFIXES,
20+
SCHEMA_COUPLED_GROUPS,
1921
LOCK_DIR,
2022
LOG_TARGETS,
2123
PORTS,
@@ -28,6 +30,7 @@ import {
2830
envPath,
2931
gatewayAddressesPath,
3032
hostAddressesPath,
33+
resolveServiceOverrides,
3134
} from "./layout";
3235
import type { Runner } from "./utils";
3336
import {
@@ -102,11 +105,34 @@ const createTopology = (count: number, threshold?: number, instances?: Record<st
102105
});
103106

104107
const parseLocalOverride = (value: string): LocalOverride => {
105-
const [group, profile] = value.split(":", 2);
108+
const colonIdx = value.indexOf(":");
109+
if (colonIdx < 0) {
110+
if (!OVERRIDE_GROUPS.includes(value as OverrideGroup)) {
111+
throw new Error(`Unsupported override ${value}`);
112+
}
113+
return { group: value as OverrideGroup };
114+
}
115+
const group = value.slice(0, colonIdx);
116+
const rest = value.slice(colonIdx + 1);
106117
if (!OVERRIDE_GROUPS.includes(group as OverrideGroup)) {
107-
throw new Error(`Unsupported override ${value}`);
118+
throw new Error(`Unsupported override group "${group}"`);
119+
}
120+
const g = group as OverrideGroup;
121+
const parts = rest.split(",").map((s) => s.trim()).filter(Boolean);
122+
const isServiceList = parts.length > 1 || GROUP_SERVICE_SUFFIXES[g].includes(parts[0]);
123+
if (isServiceList) {
124+
const services = resolveServiceOverrides(g, parts);
125+
return { group: g, services };
108126
}
109-
return { group: group as OverrideGroup, profile };
127+
// If the value contains a hyphen but isn't a known suffix, it's likely a typo.
128+
if (parts.length === 1 && parts[0].includes("-")) {
129+
throw new Error(
130+
`Unknown service "${parts[0]}" in group "${g}". ` +
131+
`Valid services: ${GROUP_SERVICE_SUFFIXES[g].join(", ")}. ` +
132+
`If this is a cargo profile, use --profile instead.`,
133+
);
134+
}
135+
return { group: g, profile: rest };
110136
};
111137

112138
const parseKeyValue = (value: string) => {
@@ -580,12 +606,25 @@ const printBundle = (bundle: VersionBundle) => {
580606
log(describeBundle(bundle));
581607
};
582608

609+
const describeOverride = (item: LocalOverride) => {
610+
const profile = item.profile ? `:${item.profile}` : "";
611+
const svc = item.services?.length ? `[${item.services.join(",")}]` : "";
612+
return `${item.group}${profile}${svc}`;
613+
};
614+
583615
const printPlan = (state: Pick<State, "target" | "overrides" | "topology">, fromStep?: StepName) => {
584616
log(`[plan] target=${state.target}`);
585617
if (state.overrides.length) {
586-
log(
587-
`[plan] overrides=${state.overrides.map((item) => `${item.group}${item.profile ? `:${item.profile}` : ""}`).join(", ")}`,
588-
);
618+
log(`[plan] overrides=${state.overrides.map(describeOverride).join(", ")}`);
619+
for (const o of state.overrides) {
620+
if (o.services?.length && SCHEMA_COUPLED_GROUPS.includes(o.group)) {
621+
log(
622+
`[warn] ${o.group}: per-service override with a shared database. ` +
623+
`If your changes include DB migrations, non-overridden services may fail. ` +
624+
`Use --override ${o.group} (full group) in that case.`,
625+
);
626+
}
627+
}
589628
}
590629
log(`[plan] topology=n${state.topology.count}/t${state.topology.threshold}`);
591630
log(`[plan] steps=${STEP_NAMES.slice(stateStepIndex(fromStep ?? STEP_NAMES[0])).join(" -> ")}`);
@@ -924,7 +963,7 @@ const runStatus = async (deps: RuntimeDeps) => {
924963
if (state) {
925964
log(`[target] ${state.target}`);
926965
if (state.overrides.length) {
927-
log(`[overrides] ${state.overrides.map((item) => `${item.group}${item.profile ? `:${item.profile}` : ""}`).join(", ")}`);
966+
log(`[overrides] ${state.overrides.map(describeOverride).join(", ")}`);
928967
}
929968
log(`[topology] n=${state.topology.count} t=${state.topology.threshold}`);
930969
log(`[steps] ${state.completedSteps.join(", ") || "none"}`);
@@ -1090,7 +1129,7 @@ Commands:
10901129
up options:
10911130
--target latest-main|latest-release|devnet|testnet|mainnet
10921131
--lock-file <path-to-bundle-json>
1093-
--override <group[:profile]> repeated; groups: ${OVERRIDE_GROUPS.join(", ")}
1132+
--override <group[:profile|svc1,svc2]> repeated; groups: ${OVERRIDE_GROUPS.join(", ")}
10941133
--profile <cargo-profile>
10951134
--coprocessors <n>
10961135
--threshold <t>

test-suite/fhevm/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type InstanceOverride = {
3838
export type LocalOverride = {
3939
group: OverrideGroup;
4040
profile?: string;
41+
services?: string[];
4142
};
4243

4344
export type Topology = {

0 commit comments

Comments
 (0)