Skip to content

Commit

Permalink
Add PgConfig module
Browse files Browse the repository at this point in the history
It takes a path to `pg_config`, executes it, and stores all the
configuration values. A `get` method provides access to configurations
(using a lowercase key name), while `iter` and `into_iter` allow
iteration over all the configurations. The module is crate-only for now,
and not yet used, but will be soon.

In order to support tests on multiple OSes, add `mocks/pg_config.rs` and
`mocks/exit_err.rs`. The PgConfig tests use `rustc` to compile apps for
the current OS that mock the output of `pg_config` and that exit with an
error. This eliminates the need for a bunch of conditional code to build
shell scripts or batch files; we're testing Rust, so Rust is available,
so just use it to build the apps and ensure consistent behavior.

Tweak the handling of Command errors by stringifying a Command before
creating the error object. This avoids a bunch of ownership issues. The
code does the work only in conditional blocks that execute when an error
is needed, so the overhead is minimal.
  • Loading branch information
theory committed Nov 27, 2024
1 parent ac2d7ef commit dbbab62
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 5 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: Run pre-commit
uses: pre-commit/[email protected]
- uses: actions-rust-lang/audit@v1
name: Audit Rust Dependencies
- name: Audit Rust Dependencies
uses: actions-rust-lang/audit@v1
- name: Generate Coverage
run: make cover RUST_BACKTRACE=1
- name: Publish Coverage
Expand Down
Binary file added exit_err
Binary file not shown.
6 changes: 6 additions & 0 deletions mocks/exit_err.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Simple app that returns an error.
fn main() {
let args: Vec<String> = std::env::args().collect();
println!("DED: {}", &args[1..].join(" "));
std::process::exit(2)
}
10 changes: 10 additions & 0 deletions mocks/pg_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Mock pg_config.

fn main() {
println!("BINDIR = /opt/data/pgsql-17.2/bin");
println!("MANDIR = /opt/data/pgsql-17.2/share/man");
println!("PGXS = /opt/data/pgsql-17.2/lib/pgxs/src/makefiles/pgxs.mk");
println!("CFLAGS_SL = ");
println!("LIBS = -lpgcommon -lpgport -lxml2 -lssl -lcrypto -lz -lreadline -lm ");
println!("VERSION = PostgreSQL 17.2");
}
4 changes: 2 additions & 2 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ pub enum BuildError {
MissingFile(&'static str),

/// Command execution failure.
#[error("executing `{0:?}`: {1}")]
Command(std::process::Command, io::ErrorKind),
#[error("executing `{0}`: {1}")]
Command(String, String),
}

impl From<ureq::Error> for BuildError {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ versions.
*/
pub mod api;
pub mod error;
mod pg_config;
mod pgrx;
mod pgxs;
mod pipeline;

use crate::{error::BuildError, pgrx::Pgrx, pgxs::Pgxs, pipeline::Pipeline};
use pg_config::PgConfig;
use pgxn_meta::{dist, release::Release};
use std::path::Path;

Expand Down
72 changes: 72 additions & 0 deletions src/pg_config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::{
collections::{self, HashMap},
io::{BufRead, BufReader},
path::Path,
process::Command,
};

use crate::error::BuildError;

pub(crate) struct PgConfig(HashMap<String, String>);

impl PgConfig {
/// Executes `pg_config`, parses the output, and returns a `PgConfig`
/// containing its key/value pairs.
pub fn new<P: AsRef<Path>>(pg_config: P) -> Result<Self, BuildError> {
// Execute pg_config.
let mut cmd = Command::new(pg_config.as_ref().as_os_str());

let out = cmd
.output()
.map_err(|e| BuildError::Command(format!("{:?}", cmd), e.kind().to_string()))?;
if !out.status.success() {
return Err(BuildError::Command(
format!("{:?}", cmd),
String::from_utf8_lossy(&out.stdout).to_string(),
));
}

// Parse each line, splitting on " = ".
let reader = BufReader::new(out.stdout.as_slice());
let mut cfg = HashMap::new();
for line in reader.lines().map_while(Result::ok) {
let mut split = line.splitn(2, " = ");
if let Some(key) = split.nth(0) {
if let Some(val) = split.last() {
cfg.insert(key.to_ascii_lowercase(), val.to_string());
}
}
}

Ok(PgConfig(cfg))
}

/// Returns the `pg_config` value for `cfg`, which should be a lowercase
/// string.
pub fn get(&mut self, cfg: &str) -> Option<&str> {
match self.0.get(cfg) {
Some(c) => Some(c.as_str()),
None => None,
}
}

/// An iterator visiting all `pg_config` key-value pairs in arbitrary
/// order. The iterator element type is `(&'a str, &'a str)`.
pub fn iter(&self) -> collections::hash_map::Iter<'_, String, String> {
self.0.iter()
}
}

impl<'h> IntoIterator for &'h PgConfig {
type Item = <&'h HashMap<String, String> as IntoIterator>::Item;
type IntoIter = <&'h HashMap<String, String> as IntoIterator>::IntoIter;

/// Convert into an iterator visiting all `pg_config` key-value pairs in
/// arbitrary order. The iterator element type is `(&'a str, &'a str)`.
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}

#[cfg(test)]
mod tests;
125 changes: 125 additions & 0 deletions src/pg_config/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use super::*;
use assertables::*;
use tempfile::tempdir;

#[test]
fn pg_config() -> Result<(), BuildError> {
// Build a mock pg_config.
let tmp = tempdir()?;
let path = tmp.path().join("pg_config").display().to_string();
compile_mock("pg_config", &path);

let exp = HashMap::from([
("bindir".to_string(), "/opt/data/pgsql-17.2/bin".to_string()),
(
"mandir".to_string(),
"/opt/data/pgsql-17.2/share/man".to_string(),
),
(
"pgxs".to_string(),
"/opt/data/pgsql-17.2/lib/pgxs/src/makefiles/pgxs.mk".to_string(),
),
("cflags_sl".to_string(), "".to_string()),
(
"libs".to_string(),
"-lpgcommon -lpgport -lxml2 -lssl -lcrypto -lz -lreadline -lm ".to_string(),
),
("version".to_string(), "PostgreSQL 17.2".to_string()),
]);

// Parse its output.
let mut cfg = PgConfig::new(&path)?;
assert_eq!(&exp, &cfg.0);

// Get lowercase.
assert_eq!(
cfg.get("bindir"),
Some("/opt/data/pgsql-17.2/bin"),
"bindir"
);
assert_eq!(
cfg.get("mandir"),
Some("/opt/data/pgsql-17.2/share/man"),
"mandir"
);
assert_eq!(
cfg.get("pgxs"),
Some("/opt/data/pgsql-17.2/lib/pgxs/src/makefiles/pgxs.mk"),
"pgxs",
);
assert_eq!(cfg.get("cflags_sl"), Some(""));
assert_eq!(
cfg.get("libs"),
Some("-lpgcommon -lpgport -lxml2 -lssl -lcrypto -lz -lreadline -lm "),
"libs",
);
assert_eq!(cfg.get("version"), Some("PostgreSQL 17.2"), "version");

// Uppercase and unknown keys ignored.
for name in [
"BINDIR",
"MANDIR",
"PGXS",
"CFLAGS_SL",
"LIBS",
"VERSION",
"nonesuch",
] {
assert_eq!(cfg.get(name), None, "{name}");
}

// Test iter.
let mut all = HashMap::new();
for (k, v) in cfg.iter() {
all.insert(k.to_string(), v.to_string());
}
assert_eq!(&exp, &all);

// Test into_iter.
let mut all = HashMap::new();
for (k, v) in &cfg {
all.insert(k.to_string(), v.to_string());
}
assert_eq!(&exp, &all);

Ok(())
}

#[test]
fn pg_config_err() {
// Build a mock pg_config that exits with an error.
let tmp = tempdir().unwrap();
let path = tmp.path().join("exit_err").display().to_string();
compile_mock("exit_err", &path);

// Get the error.
match PgConfig::new(&path) {
Ok(_) => panic!("exit_err unexpectedly succeeded"),
Err(e) => {
assert_starts_with!(e.to_string(), "executing");
assert_ends_with!(e.to_string(), " DED: \n");
}
}

// Try executing a nonexistent file.
let path = tmp.path().join("nonesuch").display().to_string();
match PgConfig::new(&path) {
Ok(_) => panic!("nonesuch unexpectedly succeeded"),
Err(e) => {
assert_starts_with!(e.to_string(), "executing");
assert_ends_with!(e.to_string(), "nonesuch\"`: entity not found");
}
}
}

fn compile_mock(name: &str, dest: &str) {
let src = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("mocks")
.join(format!("{name}.rs"))
.display()
.to_string();
Command::new("rustc")
.args([&src, "-o", dest])
.output()
.unwrap();
}
5 changes: 4 additions & 1 deletion src/pipeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ pub(crate) trait Pipeline<P: AsRef<Path>> {
cmd.current_dir(self.dir());
match cmd.output() {
Ok(_) => Ok(()),
Err(e) => Err(BuildError::Command(cmd, e.kind())),
Err(e) => Err(BuildError::Command(
format!("{:?}", cmd),
e.kind().to_string(),
)),
}
}
}
Expand Down

0 comments on commit dbbab62

Please sign in to comment.