Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 22 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
[workspace]
resolver = "3"
members = ["ephemeral-postgres", "ephemeral-postgres-tests"]
members = ["ephemeral-postgres", "ephemeral-postgres-run", "ephemeral-postgres-tests"]

[workspace.dependencies]
ephemeral-postgres = { path = "ephemeral-postgres" }
ephemeral-postgres = { path = "ephemeral-postgres", version = "0.4.0" }
ephemeral-postgres-tests = { path = "ephemeral-postgres-tests" }
sqlx = { version = "0.9", features = ["chrono", "derive", "json", "migrate", "postgres", "runtime-tokio", "tls-native-tls", "uuid"] }
testcontainers-modules = { version = "0.15", default-features = false, features = ["postgres", "watchdog"] }
thiserror = "2.0"
tokio = { version = "1.53", features = ["time"] }
uuid = { version = "1.24", features = ["serde", "v4"] }

[workspace.lints.rust]
warnings = "deny"

[workspace.lints.clippy]
ignored_unit_patterns = "warn"
must_use_candidate = "warn"
all = { level = "deny", priority = -1 }
nursery = { level = "deny", priority = -1 }
pedantic = { level = "deny", priority = -1 }

expect_used = "deny"
panic = "deny"
unwrap_used = "deny"

[workspace.metadata.heisenberg]
test_crates = ["ephemeral-postgres-tests"]

[[workspace.metadata.heisenberg.configurations]]
name = "x86_64-unknown-linux-gnu"
target = "x86_64-unknown-linux-gnu"
default_features = true
features = []
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
CARGO_HOME ?= $(HOME)/.cargo

# -----------------------------------------------------------------------------
# Real targets
# -----------------------------------------------------------------------------
Expand All @@ -15,7 +17,7 @@ package-lock.json: package.json

.PHONY: clippy
clippy:
cargo clippy --workspace --tests -- -D warnings
cargo clippy --workspace --tests

.PHONY: coverage
coverage: node_modules
Expand All @@ -24,12 +26,22 @@ coverage: node_modules
cargo llvm-cov report --json --output-path target/llvm-cov.json
npx rust-coverage-check target/llvm-cov.json \
--workspace-root $(CURDIR) \
--gated ephemeral-postgres=100
--gated ephemeral-postgres=100 \
--gated ephemeral-postgres-run=100

.PHONY: fmt
fmt:
cargo fmt

.PHONY: heisenberg
heisenberg:
cargo fetch
heisenberg check \
--cargo-home "$(CARGO_HOME)" \
--sysroot "$(shell rustc --print sysroot)" \
--configuration x86_64-unknown-linux-gnu \
--workspace "$(CURDIR)"

.PHONY: rust-test
rust-test:
cargo test --workspace
4 changes: 4 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
allow-expect-in-tests = true
allow-panic-in-tests = true
allow-unwrap-in-tests = true
avoid-breaking-exported-api = false
27 changes: 27 additions & 0 deletions ephemeral-postgres-run/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "ephemeral-postgres-run"
version = "0.4.0"
edition = "2024"
description = "Runs a command next to one ephemeral PostgreSQL server that all of its processes share."
license = "Apache-2.0"
repository = "https://github.com/intentee/ephemeral-postgres"
documentation = "https://docs.rs/ephemeral-postgres-run"
readme = "../README.md"
keywords = ["postgres", "postgresql", "testing", "testcontainers"]
categories = ["development-tools::testing", "database"]
autotests = false

[dependencies]
ephemeral-postgres = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "process", "rt"] }

[dev-dependencies]
ephemeral-postgres-tests = { workspace = true }

[lints]
workspace = true

[[test]]
name = "integration"
path = "tests/main.rs"
1 change: 1 addition & 0 deletions ephemeral-postgres-run/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const SERVER_URL_VARIABLE: &str = "EPHEMERAL_POSTGRES_URL";
41 changes: 41 additions & 0 deletions ephemeral-postgres-run/src/ephemeral_postgres_run_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::process::ExitStatus;

use ephemeral_postgres::ephemeral_postgres_error::EphemeralPostgresError;

#[derive(Debug, thiserror::Error)]
pub enum EphemeralPostgresRunError {
#[error("failed to start the shared postgres cluster")]
ClusterStart {
#[source]
source: EphemeralPostgresError,
},

#[error("failed to run `{command}`")]
CommandSpawn {
command: String,
#[source]
source: std::io::Error,
},

#[error("`{command}` ended without an exit code ({status})")]
CommandTerminated { command: String, status: ExitStatus },

#[error("missing the command to run after `--`")]
MissingCommand,

#[error(
"missing the postgres image name; usage: ephemeral-postgres-run <image-name> <image-tag> -- <command> [arguments...]"
)]
MissingImageName,

#[error(
"missing the postgres image tag; usage: ephemeral-postgres-run <image-name> <image-tag> -- <command> [arguments...]"
)]
MissingImageTag,

#[error("missing `--` between the postgres image and the command")]
MissingSeparator,

#[error("expected `--` after the postgres image, found `{found}`")]
UnexpectedArgument { found: String },
}
4 changes: 4 additions & 0 deletions ephemeral-postgres-run/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod constants;
pub mod ephemeral_postgres_run_error;
pub mod run;
pub mod run_arguments;
27 changes: 27 additions & 0 deletions ephemeral-postgres-run/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use std::env;
use std::error::Error;
use std::process;

use ephemeral_postgres_run::run::run;

fn report(error: &dyn Error) {
eprintln!("ephemeral-postgres-run: {error}");

let mut source = error.source();

while let Some(cause) = source {
eprintln!(" caused by: {cause}");
source = cause.source();
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
match run(env::args().skip(1).collect()).await {
Ok(code) => process::exit(code),
Err(error) => {
report(&error);
process::exit(1);
}
}
}
36 changes: 36 additions & 0 deletions ephemeral-postgres-run/src/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use tokio::process::Command;

use ephemeral_postgres::cluster::Cluster;
use ephemeral_postgres::cluster_params::ClusterParams;

use crate::constants::SERVER_URL_VARIABLE;
use crate::ephemeral_postgres_run_error::EphemeralPostgresRunError;
use crate::run_arguments::RunArguments;

/// # Errors
///
/// Fails when the arguments are invalid, the cluster cannot start, or the command cannot run or
/// ends without an exit code.
pub async fn run(arguments: Vec<String>) -> Result<i32, EphemeralPostgresRunError> {
let RunArguments {
command,
command_arguments,
image,
} = RunArguments::parse(arguments)?;
let cluster = Cluster::start(ClusterParams::new(image))
.await
.map_err(|source| EphemeralPostgresRunError::ClusterStart { source })?;
let status = Command::new(&command)
.args(&command_arguments)
.env(SERVER_URL_VARIABLE, cluster.base_url())
.status()
.await
.map_err(|source| EphemeralPostgresRunError::CommandSpawn {
command: command.clone(),
source,
})?;

status
.code()
.ok_or(EphemeralPostgresRunError::CommandTerminated { command, status })
}
44 changes: 44 additions & 0 deletions ephemeral-postgres-run/src/run_arguments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use ephemeral_postgres::postgres_image::PostgresImage;

use crate::ephemeral_postgres_run_error::EphemeralPostgresRunError;

const SEPARATOR: &str = "--";

pub struct RunArguments {
pub command: String,
pub command_arguments: Vec<String>,
pub image: PostgresImage,
}

impl RunArguments {
/// # Errors
///
/// Fails when the image name, the image tag, the `--` separator or the command is missing, or
/// when another argument stands in place of the separator.
pub fn parse(arguments: Vec<String>) -> Result<Self, EphemeralPostgresRunError> {
let mut arguments = arguments.into_iter();
let image_name = arguments
.next()
.ok_or(EphemeralPostgresRunError::MissingImageName)?;
let image_tag = arguments
.next()
.ok_or(EphemeralPostgresRunError::MissingImageTag)?;
let separator = arguments
.next()
.ok_or(EphemeralPostgresRunError::MissingSeparator)?;

if separator != SEPARATOR {
return Err(EphemeralPostgresRunError::UnexpectedArgument { found: separator });
}

let command = arguments
.next()
.ok_or(EphemeralPostgresRunError::MissingCommand)?;

Ok(Self {
command,
command_arguments: arguments.collect(),
image: PostgresImage::new(image_name, image_tag),
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::process::Command;

use ephemeral_postgres::postgres_image::PostgresImage;
use ephemeral_postgres_tests::postgres_test_image::postgres_test_image;

#[test]
fn exits_with_the_exit_code_of_the_command() {
let PostgresImage { name, tag } = postgres_test_image();

let status = Command::new(env!("CARGO_BIN_EXE_ephemeral-postgres-run"))
.args([name.as_str(), tag.as_str(), "--", "sh", "-c", "exit 5"])
.status()
.unwrap();

assert_eq!(status.code(), Some(5));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use std::process::Command;

use ephemeral_postgres::postgres_image::PostgresImage;
use ephemeral_postgres_tests::postgres_test_image::postgres_test_image;

#[test]
fn exits_with_a_failure_when_the_command_cannot_run() {
let PostgresImage { name, tag } = postgres_test_image();

let output = Command::new(env!("CARGO_BIN_EXE_ephemeral-postgres-run"))
.args([name.as_str(), tag.as_str(), "--", "/nonexistent/command"])
.output()
.unwrap();

assert_eq!(output.status.code(), Some(1));
assert_eq!(
String::from_utf8(output.stderr).unwrap(),
"ephemeral-postgres-run: failed to run `/nonexistent/command`\n caused by: No such file or directory (os error 2)\n"
);
}
14 changes: 14 additions & 0 deletions ephemeral-postgres-run/tests/binary_reports_a_usage_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::process::Command;

#[test]
fn exits_with_a_failure_and_the_usage_when_arguments_are_missing() {
let output = Command::new(env!("CARGO_BIN_EXE_ephemeral-postgres-run"))
.output()
.unwrap();

assert_eq!(output.status.code(), Some(1));
assert_eq!(
String::from_utf8(output.stderr).unwrap(),
"ephemeral-postgres-run: missing the postgres image name; usage: ephemeral-postgres-run <image-name> <image-tag> -- <command> [arguments...]\n"
);
}
12 changes: 12 additions & 0 deletions ephemeral-postgres-run/tests/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mod binary_exits_with_the_command_exit_code;
mod binary_reports_a_failure_with_its_causes;
mod binary_reports_a_usage_error;
mod run_arguments_reject_a_missing_command;
mod run_arguments_reject_a_missing_image_name;
mod run_arguments_reject_a_missing_image_tag;
mod run_arguments_reject_a_missing_separator;
mod run_arguments_reject_an_argument_in_place_of_the_separator;
mod run_reports_a_cluster_that_cannot_start;
mod run_reports_a_command_terminated_by_a_signal;
mod run_reports_a_command_that_cannot_be_spawned;
mod run_returns_the_command_exit_code_with_the_server_url;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use ephemeral_postgres_run::ephemeral_postgres_run_error::EphemeralPostgresRunError;
use ephemeral_postgres_run::run_arguments::RunArguments;

#[test]
fn rejects_a_separator_without_a_command() {
assert!(matches!(
RunArguments::parse(vec![
"postgres".to_owned(),
"18".to_owned(),
"--".to_owned(),
]),
Err(EphemeralPostgresRunError::MissingCommand)
));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use ephemeral_postgres_run::ephemeral_postgres_run_error::EphemeralPostgresRunError;
use ephemeral_postgres_run::run_arguments::RunArguments;

#[test]
fn rejects_arguments_without_an_image_name() {
assert!(matches!(
RunArguments::parse(Vec::new()),
Err(EphemeralPostgresRunError::MissingImageName)
));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use ephemeral_postgres_run::ephemeral_postgres_run_error::EphemeralPostgresRunError;
use ephemeral_postgres_run::run_arguments::RunArguments;

#[test]
fn rejects_arguments_without_an_image_tag() {
assert!(matches!(
RunArguments::parse(vec!["postgres".to_owned()]),
Err(EphemeralPostgresRunError::MissingImageTag)
));
}
Loading
Loading