Skip to content

Commit 9b0e4a2

Browse files
authored
Merge pull request #2119 from Jamesbarford/infra/add-database-testing-capability
Infra - wire up database for test
2 parents f25b1ac + c01ac41 commit 9b0e4a2

File tree

11 files changed

+262
-4
lines changed

11 files changed

+262
-4
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ jobs:
1414
test_and_deploy:
1515
name: Test and deploy
1616
runs-on: ubuntu-22.04
17+
env:
18+
TEST_DB_URL: postgres://postgres:testpass@localhost:5432/postgres
19+
services:
20+
postgres:
21+
image: postgres:16-alpine
22+
env:
23+
POSTGRES_USER: postgres
24+
POSTGRES_PASSWORD: testpass
25+
POSTGRES_DB: postgres
26+
ports:
27+
- 5432:5432
1728
steps:
1829
- name: Checkout the source code
1930
uses: actions/checkout@v4

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.PHONY: test start-postgres
2+
3+
# Helper to spin up postgres
4+
start-postgres:
5+
docker compose up -d pg_test
6+
7+
# Helper to stop postgres & destroy the volume (ensures a clean rebuild on `start`)
8+
stop-postgres:
9+
docker compose down -v
10+
11+
# Run the tests with the database purring along in the background
12+
test: start-postgres
13+
TEST_DB_URL="postgres://postgres:testpass@localhost/postgres" cargo test

REUSE.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ path = [
2525
"Cargo.lock",
2626
"Cargo.toml",
2727
"Dockerfile",
28+
"docker-compose.yml",
2829
"LICENSE.md",
30+
"Makefile",
2931
"README.md",
3032
"REUSE.toml",
3133
".gitignore",

collector/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,22 @@ compilation of the final/leaf crate. Cargo only passes arguments after `--` to t
556556
therefore this does not affect the compilation of dependencies.
557557
2) Profiling/benchmarking - `cargo` is invoked with `--wrap-rustc-with <TOOL>`, which executes the
558558
specified profiling tool by `rustc-fake`.
559+
560+
## How to test
561+
Run `make test`; in the root of the project there is a `Makefile` which
562+
presently exists to spin up/down a Postgres database, from a
563+
`docker-compose.yml`, then run `cargo test` with a `TEST_DB_URL` set. In
564+
concrete terms `make test` is a convenience for running;
565+
566+
```bash
567+
docker compose up -d pg_test && \
568+
TEST_DB_URL="postgres://postgres:testpass@localhost/postgres" cargo test
569+
```
570+
571+
The above becomes cumbersome to type and easy to forget both how to setup the
572+
database and set the `TEST_DB_URL` environment variable.
573+
574+
**Note: Windows**
575+
The tests for the database are disabled and will skip. This is due, at the time
576+
of writing (May 2025), to limitations with the GitHub Ci runner not supporting
577+
docker, hence unable to start the database for the tests.

database/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ csv = "1"
2626
x509-cert = { version = "0.2.5", features = ["pem"] }
2727

2828
intern = { path = "../intern" }
29+
30+
[dev-dependencies]
31+
uuid = "1.16.0"

database/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub mod metric;
1414
pub mod pool;
1515
pub mod selector;
1616

17+
#[cfg(test)]
18+
mod tests;
19+
1720
pub use pool::{Connection, Pool};
1821

1922
intern!(pub struct Metric);

database/src/pool.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,80 @@ impl Pool {
294294
}
295295
}
296296
}
297+
298+
#[cfg(test)]
299+
mod tests {
300+
use chrono::Utc;
301+
use std::str::FromStr;
302+
303+
use super::*;
304+
use crate::{tests::run_db_test, Commit, CommitType, Date};
305+
306+
/// Create a Commit
307+
fn create_commit(commit_sha: &str, time: chrono::DateTime<Utc>, r#type: CommitType) -> Commit {
308+
Commit {
309+
sha: commit_sha.into(),
310+
date: Date(time),
311+
r#type,
312+
}
313+
}
314+
315+
#[tokio::test]
316+
async fn pstat_returns_empty_vector_when_empty() {
317+
run_db_test(|ctx| async {
318+
// This is essentially testing the database testing framework is
319+
// wired up correctly. Though makes sense that there should be
320+
// an empty vector returned if there are no pstats.
321+
let db = ctx.db_client();
322+
let result = db.connection().await.get_pstats(&vec![], &vec![]).await;
323+
let expected: Vec<Vec<Option<f64>>> = vec![];
324+
325+
assert_eq!(result, expected);
326+
Ok(ctx)
327+
})
328+
.await;
329+
}
330+
331+
#[tokio::test]
332+
async fn artifact_storage() {
333+
run_db_test(|ctx| async {
334+
let db = ctx.db_client();
335+
let time = chrono::DateTime::from_str("2021-09-01T00:00:00.000Z").unwrap();
336+
337+
let artifact_one = ArtifactId::from(create_commit("abc", time, CommitType::Master));
338+
let artifact_two = ArtifactId::Tag("nightly-2025-05-14".to_string());
339+
340+
let artifact_one_id_number = db.connection().await.artifact_id(&artifact_one).await;
341+
let artifact_two_id_number = db.connection().await.artifact_id(&artifact_two).await;
342+
343+
// We cannot arbitrarily add random sizes to the artifact size
344+
// table, as there is a constraint that the artifact must actually
345+
// exist before attaching something to it.
346+
347+
let db = db.connection().await;
348+
349+
// Artifact one inserts
350+
db.record_artifact_size(artifact_one_id_number, "llvm.so", 32)
351+
.await;
352+
db.record_artifact_size(artifact_one_id_number, "llvm.a", 64)
353+
.await;
354+
355+
// Artifact two inserts
356+
db.record_artifact_size(artifact_two_id_number, "another-llvm.a", 128)
357+
.await;
358+
359+
let result_one = db.get_artifact_size(artifact_one_id_number).await;
360+
let result_two = db.get_artifact_size(artifact_two_id_number).await;
361+
362+
// artifact one
363+
assert_eq!(Some(32u64), result_one.get("llvm.so").copied());
364+
assert_eq!(Some(64u64), result_one.get("llvm.a").copied());
365+
assert_eq!(None, result_one.get("another-llvm.a").copied());
366+
367+
// artifact two
368+
assert_eq!(Some(128), result_two.get("another-llvm.a").copied());
369+
Ok(ctx)
370+
})
371+
.await;
372+
}
373+
}

database/src/pool/postgres.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl Postgres {
2525

2626
const CERT_URL: &str = "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem";
2727

28-
async fn make_client(db_url: &str) -> anyhow::Result<tokio_postgres::Client> {
28+
pub async fn make_client(db_url: &str) -> anyhow::Result<tokio_postgres::Client> {
2929
if db_url.contains("rds.amazonaws.com") {
3030
let mut builder = TlsConnector::builder();
3131
for cert in make_certificates().await {

database/src/tests/mod.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
use std::future::Future;
2+
use tokio_postgres::config::Host;
3+
use tokio_postgres::Config;
4+
5+
use crate::pool::postgres::make_client;
6+
use crate::Pool;
7+
8+
/// Represents a connection to a Postgres database that can be
9+
/// used in integration tests to test logic that interacts with
10+
/// a database.
11+
pub(crate) struct TestContext {
12+
db_name: String,
13+
original_db_url: String,
14+
// Pre-cached client to avoid creating unnecessary connections in tests
15+
client: Pool,
16+
}
17+
18+
impl TestContext {
19+
async fn new(db_url: &str) -> Self {
20+
let config: Config = db_url.parse().expect("Cannot parse connection string");
21+
22+
// Create a new database that will be used for this specific test
23+
let client = make_client(&db_url)
24+
.await
25+
.expect("Cannot connect to database");
26+
let db_name = format!("db{}", uuid::Uuid::new_v4().to_string().replace("-", ""));
27+
client
28+
.execute(&format!("CREATE DATABASE {db_name}"), &[])
29+
.await
30+
.expect("Cannot create database");
31+
drop(client);
32+
33+
let host = match &config
34+
.get_hosts()
35+
.first()
36+
.expect("connection string must contain at least one host")
37+
{
38+
Host::Tcp(host) => host,
39+
40+
// This variant only exists on Unix targets, so the arm itself is
41+
// cfg-gated to keep non-unix builds happy.
42+
#[cfg(unix)]
43+
Host::Unix(_) => panic!("Unix sockets in Postgres connection string are not supported"),
44+
45+
// On non-unix targets the enum has no other variants.
46+
#[cfg(not(unix))]
47+
_ => unreachable!("non-TCP hosts cannot appear on this platform"),
48+
};
49+
50+
// We need to connect to the database against, because Postgres doesn't allow
51+
// changing the active database mid-connection.
52+
// There does not seem to be a way to turn the config back into a connection
53+
// string, so construct it manually.
54+
let test_db_url = format!(
55+
"postgresql://{}:{}@{}:{}/{}",
56+
config.get_user().unwrap(),
57+
String::from_utf8(config.get_password().unwrap().to_vec()).unwrap(),
58+
host,
59+
&config.get_ports()[0],
60+
db_name
61+
);
62+
let pool = Pool::open(test_db_url.as_str());
63+
64+
Self {
65+
db_name,
66+
original_db_url: db_url.to_string(),
67+
client: pool,
68+
}
69+
}
70+
71+
pub(crate) fn db_client(&self) -> &Pool {
72+
&self.client
73+
}
74+
75+
async fn finish(self) {
76+
// Cleanup the test database
77+
// First, we need to stop using the database
78+
drop(self.client);
79+
80+
// Then we need to connect to the default database and drop our test DB
81+
let client = make_client(&self.original_db_url)
82+
.await
83+
.expect("Cannot connect to database");
84+
client
85+
.execute(&format!("DROP DATABASE {}", self.db_name), &[])
86+
.await
87+
.unwrap();
88+
}
89+
}
90+
91+
pub(crate) async fn run_db_test<F, Fut>(f: F)
92+
where
93+
F: FnOnce(TestContext) -> Fut,
94+
Fut: Future<Output = anyhow::Result<TestContext>>,
95+
{
96+
if let Ok(db_url) = std::env::var("TEST_DB_URL") {
97+
let ctx = TestContext::new(&db_url).await;
98+
let ctx = f(ctx).await.expect("Test failed");
99+
ctx.finish().await;
100+
} else {
101+
// The github CI does not yet support running containers on Windows,
102+
// meaning that the test suite would fail.
103+
if cfg!(unix) {
104+
panic!("Aborting; `TEST_DB_URL` was not passed");
105+
} else {
106+
eprintln!(
107+
"Skipping database test on platform {} `TEST_DB_URL` was not passed",
108+
std::env::consts::OS
109+
);
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)