Skip to content

Commit c388eaf

Browse files
committed
feat(coprocessor): parallelize CI tests across services using cargo-nextest
1 parent bfc89a5 commit c388eaf

File tree

3 files changed

+161
-58
lines changed

3 files changed

+161
-58
lines changed

.github/workflows/coprocessor-cargo-tests.yml

Lines changed: 122 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,67 @@ jobs:
3232
- coprocessor/fhevm-engine/**
3333
- coprocessor/proto/**
3434
cargo-tests:
35-
name: coprocessor-cargo-test/cargo-tests (bpr)
35+
name: coprocessor-cargo-test/${{ matrix.service }}
3636
needs: check-changes
3737
if: ${{ needs.check-changes.outputs.changes-rust-files == 'true' }}
3838
permissions:
3939
contents: 'read' # Required to checkout repository code
4040
checks: 'write' # Required to create GitHub checks for test results
4141
packages: 'read' # Required to read GitHub packages/container registry
42-
pull-requests: 'write' # Required to post coverage comment on PR
43-
runs-on: large_ubuntu_16
42+
runs-on: ${{ matrix.runner }}
43+
strategy:
44+
fail-fast: false
45+
matrix:
46+
include:
47+
- service: tfhe-worker
48+
package: tfhe-worker
49+
needs_db: true
50+
needs_db_reset: false
51+
needs_localstack: false
52+
needs_foundry: false
53+
runner: large_ubuntu_16
54+
- service: sns-worker
55+
package: sns-worker
56+
needs_db: true
57+
needs_db_reset: true
58+
needs_localstack: true
59+
needs_foundry: false
60+
runner: large_ubuntu_16
61+
- service: zkproof-worker
62+
package: zkproof-worker
63+
needs_db: true
64+
needs_db_reset: true
65+
needs_localstack: false
66+
needs_foundry: false
67+
runner: large_ubuntu_16
68+
- service: transaction-sender
69+
package: transaction-sender
70+
needs_db: true
71+
needs_db_reset: true
72+
needs_localstack: true
73+
needs_foundry: true
74+
runner: large_ubuntu_16
75+
- service: gw-listener
76+
package: gw-listener
77+
needs_db: true
78+
needs_db_reset: true
79+
needs_localstack: false
80+
needs_foundry: true
81+
runner: large_ubuntu_16
82+
- service: host-listener
83+
package: host-listener
84+
needs_db: true
85+
needs_db_reset: true
86+
needs_localstack: false
87+
needs_foundry: true
88+
runner: large_ubuntu_16
89+
- service: common
90+
package: fhevm-engine-common
91+
needs_db: false
92+
needs_db_reset: false
93+
needs_localstack: false
94+
needs_foundry: false
95+
runner: ubuntu-latest
4496
steps:
4597
- name: Checkout code
4698
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -65,10 +117,13 @@ jobs:
65117
- name: Install cargo dependencies
66118
run: |
67119
sudo apt-get update
68-
sudo apt-get install -y protobuf-compiler && \
69-
cargo install sqlx-cli --version 0.7.2 --no-default-features --features postgres --locked
120+
sudo apt-get install -y protobuf-compiler
121+
- name: Install sqlx-cli
122+
if: ${{ matrix.needs_db }}
123+
run: cargo install sqlx-cli --version 0.7.2 --no-default-features --features postgres --locked
70124

71125
- name: Install foundry
126+
if: ${{ matrix.needs_foundry }}
72127
uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c
73128

74129
- name: Cache cargo
@@ -78,24 +133,27 @@ jobs:
78133
~/.cargo/registry
79134
~/.cargo/git
80135
target
81-
key: ${{ runner.os }}-cargo-coverage-${{ hashFiles('**/Cargo.lock') }}
82-
restore-keys: ${{ runner.os }}-cargo-coverage-
136+
key: ${{ runner.os }}-cargo-coverage-${{ matrix.service }}-${{ hashFiles('**/Cargo.lock') }}
137+
restore-keys: ${{ runner.os }}-cargo-coverage-${{ matrix.service }}-
83138

84139
- name: Login to GitHub Container Registry
140+
if: ${{ matrix.needs_db }}
85141
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
86142
with:
87143
registry: ghcr.io
88144
username: ${{ github.actor }}
89145
password: ${{ secrets.GITHUB_TOKEN }}
90146

91147
- name: Login to GitHub Chainguard Registry
148+
if: ${{ matrix.needs_db }}
92149
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
93150
with:
94151
registry: cgr.dev
95152
username: ${{ secrets.CGR_USERNAME }}
96153
password: ${{ secrets.CGR_PASSWORD }}
97154

98155
- name: Init database
156+
if: ${{ matrix.needs_db }}
99157
run: make init_db
100158
working-directory: coprocessor/fhevm-engine/tfhe-worker
101159

@@ -105,47 +163,82 @@ jobs:
105163
node-version: 20.x
106164

107165
- name: Start localstack
166+
if: ${{ matrix.needs_localstack }}
108167
run: |
109168
docker run --rm -d -p 4566:4566 --name localstack localstack/localstack:4.14.0
110169
111-
- name: Clean previous coverage data
112-
run: cargo llvm-cov clean --workspace --profile coverage
113-
working-directory: coprocessor/fhevm-engine
114-
115170
- name: Run tests with coverage
116171
run: |
172+
cargo llvm-cov clean --workspace --profile coverage
117173
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/coprocessor \
118-
TEST_GLOBAL_LOCALSTACK=1 \
119-
cargo llvm-cov --no-report --workspace --profile coverage
174+
COPROCESSOR_TEST_LOCALHOST_RESET=${{ matrix.needs_db_reset && '1' || '0' }} \
175+
TEST_GLOBAL_LOCALSTACK=${{ matrix.needs_localstack && '1' || '0' }} \
176+
cargo llvm-cov --no-report -p ${{ matrix.package }} --profile coverage
120177
working-directory: coprocessor/fhevm-engine
121178

122-
- name: Generate coverage report
179+
- name: Export LCOV coverage data
123180
if: ${{ !cancelled() }}
181+
run: cargo llvm-cov report --lcov --profile coverage --output-path /tmp/lcov.info || true
182+
working-directory: coprocessor/fhevm-engine
183+
184+
- name: Upload coverage artifact
185+
if: ${{ !cancelled() }}
186+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
187+
with:
188+
name: lcov-${{ matrix.service }}
189+
path: /tmp/lcov.info
190+
retention-days: 1
191+
if-no-files-found: ignore
192+
193+
coverage-report:
194+
name: coprocessor-cargo-test/coverage-report
195+
needs: [check-changes, cargo-tests]
196+
if: ${{ !cancelled() && needs.check-changes.outputs.changes-rust-files == 'true' }}
197+
permissions:
198+
contents: 'read' # Required to checkout repository code
199+
pull-requests: 'write' # Required to post coverage comment on PR
200+
runs-on: ubuntu-latest
201+
steps:
202+
- name: Checkout code
203+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
204+
with:
205+
persist-credentials: 'false'
206+
207+
- name: Install lcov
208+
run: sudo apt-get update && sudo apt-get install -y lcov
209+
210+
- name: Download all coverage artifacts
211+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
212+
with:
213+
pattern: lcov-*
214+
path: /tmp/coverage
215+
216+
- name: Merge LCOV files
124217
run: |
125-
if cargo llvm-cov report --profile coverage > /tmp/cov-report.txt 2>&1; then
126-
REPORT=$(cat /tmp/cov-report.txt)
127-
else
128-
echo "cargo llvm-cov report failed:"
129-
cat /tmp/cov-report.txt
130-
REPORT=""
218+
LCOV_FILES=$(find /tmp/coverage -name 'lcov.info' -size +0c)
219+
if [ -z "$LCOV_FILES" ]; then
220+
echo "No coverage data found"
221+
exit 0
131222
fi
223+
LCOV_ARGS=""
224+
for f in $LCOV_FILES; do
225+
LCOV_ARGS="$LCOV_ARGS -a $f"
226+
done
227+
lcov $LCOV_ARGS -o /tmp/lcov.info
228+
229+
- name: Generate coverage summary
230+
if: ${{ !cancelled() }}
231+
run: |
132232
{
133233
echo '## Coverage: coprocessor/fhevm-engine'
134-
if [ -n "$REPORT" ]; then
234+
if [ -f /tmp/lcov.info ]; then
135235
echo '```'
136-
echo "$REPORT"
236+
lcov --summary /tmp/lcov.info 2>&1 || true
137237
echo '```'
138238
else
139239
echo '*No coverage data available (tests may have failed before producing profiling data).*'
140240
fi
141241
} >> "$GITHUB_STEP_SUMMARY"
142-
echo "$REPORT"
143-
working-directory: coprocessor/fhevm-engine
144-
145-
- name: Export LCOV coverage data
146-
if: ${{ !cancelled() }}
147-
run: cargo llvm-cov report --lcov --profile coverage --output-path /tmp/lcov.info || true
148-
working-directory: coprocessor/fhevm-engine
149242
150243
- name: Diff coverage of changed lines
151244
if: ${{ !cancelled() }}

coprocessor/fhevm-engine/test-harness/src/instance.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ pub async fn setup_test_db(mode: ImportMode) -> Result<DBInstance, Box<dyn std::
5555
}
5656
}
5757

58+
// Extracts the database name from a PostgreSQL URL.
59+
// e.g. "postgresql://user:pass@host:port/mydb?opt=val" -> "mydb"
60+
// Panics if the extracted name contains characters other than alphanumeric, underscore, or hyphen.
61+
fn extract_db_name(db_url: &str) -> &str {
62+
let after_slash = db_url
63+
.rsplit('/')
64+
.next()
65+
.expect("database URL must contain /");
66+
let name = after_slash
67+
.split('?')
68+
.next()
69+
.expect("split always yields at least one element");
70+
assert!(
71+
!name.is_empty()
72+
&& name
73+
.chars()
74+
.all(|c| c.is_alphanumeric() || c == '_' || c == '-'),
75+
"invalid database name extracted from URL: {name}"
76+
);
77+
name
78+
}
79+
80+
fn admin_url_from(db_url: &str) -> String {
81+
let last_slash = db_url.rfind('/').expect("database URL must contain /");
82+
format!("{}postgres", &db_url[..=last_slash])
83+
}
84+
5885
async fn setup_test_app_existing_localhost(
5986
with_reset: bool,
6087
mode: ImportMode,
@@ -63,7 +90,7 @@ async fn setup_test_app_existing_localhost(
6390

6491
if with_reset {
6592
info!("Resetting local database at {db_url}");
66-
let admin_db_url = db_url.as_str().replace("coprocessor", "postgres");
93+
let admin_db_url = admin_url_from(db_url.as_str());
6794
create_database(&admin_db_url, db_url.as_str(), mode).await?;
6895
}
6996

@@ -122,17 +149,18 @@ async fn create_database(
122149
db_url: &str,
123150
mode: ImportMode,
124151
) -> Result<(), Box<dyn std::error::Error>> {
125-
info!("Creating coprocessor db...");
152+
let db_name = extract_db_name(db_url);
153+
info!(db_name, "Creating database...");
126154
let admin_pool = sqlx::postgres::PgPoolOptions::new()
127155
.max_connections(1)
128156
.connect(admin_db_url)
129157
.await?;
130158

131-
sqlx::query!("DROP DATABASE IF EXISTS coprocessor;")
159+
sqlx::query(&format!("DROP DATABASE IF EXISTS \"{db_name}\""))
132160
.execute(&admin_pool)
133161
.await?;
134162

135-
sqlx::query!("CREATE DATABASE coprocessor;")
163+
sqlx::query(&format!("CREATE DATABASE \"{db_name}\""))
136164
.execute(&admin_pool)
137165
.await?;
138166

coprocessor/fhevm-engine/transaction-sender/tests/common.rs

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use alloy::{
1111
sol,
1212
transports::http::reqwest::Url,
1313
};
14-
use fhevm_engine_common::utils::DatabaseURL;
1514
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
15+
use test_harness::instance::{setup_test_db, DBInstance, ImportMode};
1616
use test_harness::localstack::{
1717
create_aws_aws_kms_client, create_localstack_kms_signing_key, start_localstack,
1818
LocalstackContainer, LOCALSTACK_PORT,
@@ -53,7 +53,7 @@ pub struct TestEnvironment {
5353
pub user_address: Address,
5454
anvil: Option<AnvilInstance>,
5555
pub wallet: EthereumWallet,
56-
// Just keep the handle to destroy the container when it is dropped.
56+
_db_instance: DBInstance,
5757
_localstack: Option<LocalstackContainer>,
5858
}
5959

@@ -80,25 +80,14 @@ impl TestEnvironment {
8080
.with_test_writer()
8181
.try_init();
8282

83+
let db_instance = setup_test_db(ImportMode::None)
84+
.await
85+
.map_err(|e| anyhow::anyhow!("{e}"))?;
8386
let db_pool = PgPoolOptions::new()
8487
.max_connections(10)
85-
.connect(DatabaseURL::default().as_str())
88+
.connect(db_instance.db_url())
8689
.await?;
8790

88-
Self::truncate_tables(
89-
&db_pool,
90-
vec![
91-
"verify_proofs",
92-
"ciphertext_digest",
93-
"allowed_handles",
94-
"delegate_user_decrypt",
95-
"keys",
96-
"crs",
97-
"host_chains",
98-
],
99-
)
100-
.await?;
101-
10291
let anvil = Self::new_anvil()?;
10392
let chain_id =
10493
get_chain_id(anvil.ws_endpoint_url(), std::time::Duration::from_secs(1)).await;
@@ -141,6 +130,7 @@ impl TestEnvironment {
141130
user_address: PrivateKeySigner::random().address(),
142131
anvil: Some(anvil),
143132
wallet,
133+
_db_instance: db_instance,
144134
_localstack: localstack,
145135
})
146136
}
@@ -177,12 +167,4 @@ impl TestEnvironment {
177167
fn new_anvil_with_port(port: u16) -> anyhow::Result<AnvilInstance> {
178168
Ok(Anvil::new().block_time(1).port(port).try_spawn()?)
179169
}
180-
181-
async fn truncate_tables(db_pool: &sqlx::PgPool, tables: Vec<&str>) -> Result<(), sqlx::Error> {
182-
for table in tables {
183-
let query = format!("TRUNCATE {}", table);
184-
sqlx::query(&query).execute(db_pool).await?;
185-
}
186-
Ok(())
187-
}
188170
}

0 commit comments

Comments
 (0)