From 5d4c244d5c8d6c19aec9a6c54fd97a85e7c9c9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Gari=C3=A9py?= Date: Wed, 30 Nov 2022 19:04:33 -0500 Subject: [PATCH] Improve `test_integration` internal organization. --- .../fixtures/codegen/benchmarks.toml | 4 +- .../fixtures/codegen/examples.toml | 6 +- .../fixtures/codegen/test_codegen.toml | 4 +- test_integration/src/codegen.rs | 93 ++++++ test_integration/src/errors.rs | 96 ++++++ test_integration/src/fixtures.rs | 86 ++++++ test_integration/src/main.rs | 281 ++---------------- test_integration/src/utils.rs | 35 +++ 8 files changed, 334 insertions(+), 271 deletions(-) create mode 100644 test_integration/src/codegen.rs create mode 100644 test_integration/src/errors.rs create mode 100644 test_integration/src/fixtures.rs create mode 100644 test_integration/src/utils.rs diff --git a/test_integration/fixtures/codegen/benchmarks.toml b/test_integration/fixtures/codegen/benchmarks.toml index 829ca3b6..e88023d4 100644 --- a/test_integration/fixtures/codegen/benchmarks.toml +++ b/test_integration/fixtures/codegen/benchmarks.toml @@ -1,10 +1,10 @@ -[[codegen]] +[[test]] name = "Sync" base_path = "benches/execution/cornucopia_benches" destination = "generated_sync.rs" sync = true -[[codegen]] +[[test]] name = "Async" base_path = "benches/execution/cornucopia_benches" destination = "generated_async.rs" diff --git a/test_integration/fixtures/codegen/examples.toml b/test_integration/fixtures/codegen/examples.toml index 1f1d538d..da1972b7 100644 --- a/test_integration/fixtures/codegen/examples.toml +++ b/test_integration/fixtures/codegen/examples.toml @@ -1,15 +1,15 @@ -[[codegen]] +[[test]] name = "Auto build" base_path = "examples/auto_build" run = true -[[codegen]] +[[test]] name = "Basic sync" base_path = "examples/basic_sync" sync = true run = true -[[codegen]] +[[test]] name = "Basic async" base_path = "examples/basic_async" run = true diff --git a/test_integration/fixtures/codegen/test_codegen.toml b/test_integration/fixtures/codegen/test_codegen.toml index 3a56d174..48f502b6 100644 --- a/test_integration/fixtures/codegen/test_codegen.toml +++ b/test_integration/fixtures/codegen/test_codegen.toml @@ -1,4 +1,4 @@ -[[codegen]] +[[test]] name = "Sync" base_path = "test_codegen" destination = "src/cornucopia_sync.rs" @@ -6,7 +6,7 @@ derive_ser = true sync = true run = true -[[codegen]] +[[test]] name = "Async" base_path = "test_codegen" destination = "src/cornucopia_async.rs" diff --git a/test_integration/src/codegen.rs b/test_integration/src/codegen.rs new file mode 100644 index 00000000..5a937e5e --- /dev/null +++ b/test_integration/src/codegen.rs @@ -0,0 +1,93 @@ +use crate::{ + fixtures::{CodegenTest, TestSuite}, + utils::{reset_db, rustfmt_file, rustfmt_string}, +}; + +use cornucopia::{CodegenSettings, Error}; +use owo_colors::OwoColorize; +use std::{env::set_current_dir, process::Command}; + +// Run codegen test, return true if all test are successful +pub(crate) fn run_codegen_test( + client: &mut postgres::Client, + apply: bool, +) -> Result> { + let mut successful = true; + let original_pwd = std::env::current_dir()?; + let fixture_path = "fixtures/codegen"; + + let test_suites = TestSuite::::read(fixture_path); + for suite in test_suites { + println!("{}", format!("[codegen] {}", suite.name).magenta()); + for test in suite.tests { + set_current_dir(format!("../{}", test.base_path))?; + + // Load schema + reset_db(client)?; + cornucopia::load_schema(client, vec!["schema.sql".to_string()])?; + + // If `--apply`, then the code will be regenerated. + // Otherwise, it is only checked. + if apply { + // Generate + cornucopia::generate_live( + client, + test.queries_path.to_str().unwrap(), // TODO: Update this once our API accepts paths + Some(test.destination.to_str().unwrap()), // TODO: Update this once our API accepts paths + CodegenSettings::from(&test), + ) + .map_err(Error::report)?; + // Format the generated file + rustfmt_file(&test.destination); + } else { + // Get currently checked-in generate file + let old_codegen = std::fs::read_to_string(&test.destination).unwrap(); + // Generate new file + let new_codegen = cornucopia::generate_live( + client, + test.queries_path.to_str().unwrap(), // TODO: Update this once our API accepts paths + None, + CodegenSettings::from(&test), + ) + .map_err(Error::report)?; + // Format the generated code string by piping to rustfmt + let new_codegen_formatted = rustfmt_string(&new_codegen); + + // If the newly generated file differs from + // the currently checked in one, return an error. + if old_codegen != new_codegen_formatted { + Err(format!( + "\"{}\" is outdated", + test.destination.to_str().unwrap() + ))?; + } + } + println!("(generate) {} {}", test.name, "OK".green()); + + if test.run { + // Change current directory + std::env::set_current_dir(&original_pwd)?; + std::env::set_current_dir(&format!("../{}", test.base_path))?; + // Run + let result = Command::new("cargo").arg("run").output()?; + if result.status.success() { + println!("(run) {} {}", test.name, "OK".green()); + } else { + successful = false; + println!( + " {}\n{}", + "ERR".red(), + String::from_utf8_lossy(&result.stderr) + .as_ref() + .bright_black() + ); + } + } + + // Move back to original directory + std::env::set_current_dir(&original_pwd)?; + } + } + + Ok(successful) +} diff --git a/test_integration/src/errors.rs b/test_integration/src/errors.rs new file mode 100644 index 00000000..752e7831 --- /dev/null +++ b/test_integration/src/errors.rs @@ -0,0 +1,96 @@ +use cornucopia::{CodegenSettings, Error}; +use owo_colors::OwoColorize; + +use crate::{ + fixtures::{ErrorTest, TestSuite}, + utils::reset_db, +}; + +/// Run errors test, return true if all test are successful +pub(crate) fn run_errors_test( + client: &mut postgres::Client, + apply: bool, +) -> Result> { + let mut successful = true; + let original_pwd = std::env::current_dir().unwrap(); + let test_suites = TestSuite::::read("fixtures/errors"); + + for mut suite in test_suites { + println!("{} {}", "[error]".magenta(), suite.name.magenta()); + for test in suite.tests.iter_mut() { + // Generate file tree path + let temp_dir = tempfile::tempdir()?; + + // We need to change current dir for error path to always be the same + std::env::set_current_dir(&temp_dir)?; + + // Generate schema + std::fs::write( + "schema.sql", + [ + "CREATE TABLE author (id SERIAL, name TEXT);\n", + &test.schema, + ] + .concat(), + )?; + + // Generate queries files + std::fs::create_dir("queries")?; + std::fs::write("queries/test.sql", &test.query)?; + + // Reset db + reset_db(client)?; + + // Run codegen + let result = cornucopia::load_schema(client, vec!["schema.sql".into()]) + .map_err(Error::from) + .and_then(|_| { + cornucopia::generate_live( + client, + "queries", + None, + CodegenSettings::from(&*test), + ) + }); + + let err = result.unwrap_err().report(); + let err_trimmed = err.trim(); + if err_trimmed == test.error.trim() { + println!("{} {}", test.name, "OK".green()); + } else { + let got_msg = if apply { + "Apply:".bright_black() + } else { + "Got:".bright_black() + }; + let expected_msg = if apply { + "Previous:".bright_black() + } else { + "Expected:".bright_black() + }; + successful = false; + println!( + "{} {}\n{}\n{}\n{}\n{}\n", + test.name, + "ERR".red(), + expected_msg, + test.error, + got_msg, + err, + ); + } + if apply { + test.error = err_trimmed.into(); + } + std::env::set_current_dir(&original_pwd)?; + } + + if apply { + // Format test descriptor and update error message if needed + let edited = toml::to_string_pretty(&suite.tests)?; + std::fs::write(suite.path, edited)?; + } + } + + Ok(successful) +} diff --git a/test_integration/src/fixtures.rs b/test_integration/src/fixtures.rs new file mode 100644 index 00000000..aa7b91f6 --- /dev/null +++ b/test_integration/src/fixtures.rs @@ -0,0 +1,86 @@ +use std::path::{Path, PathBuf}; + +use cornucopia::CodegenSettings; +use serde::{de::DeserializeOwned, Deserialize}; + +#[derive(Deserialize)] +struct TestSuiteDeserializer { + test: Vec, +} + +pub struct TestSuite { + pub(crate) name: String, + pub(crate) path: PathBuf, + pub(crate) tests: Vec, +} + +impl TestSuite { + pub(crate) fn read>(fixtures_path: P) -> impl Iterator> { + std::fs::read_dir(fixtures_path).unwrap().map(|file| { + let file = file.unwrap(); + let name = file.file_name().to_string_lossy().to_string(); + let path = file.path(); + let content = std::fs::read_to_string(&path).unwrap(); + let tests: TestSuiteDeserializer = toml::from_str(&content).unwrap(); + TestSuite { + name, + tests: tests.test, + path, + } + }) + } +} + +/// Codegen test case +#[derive(Debug, serde::Deserialize)] +pub(crate) struct CodegenTest { + pub(crate) name: String, + pub(crate) base_path: String, + #[serde(default = "default_queries_path")] + pub(crate) queries_path: PathBuf, + #[serde(default = "default_destination_path")] + pub(crate) destination: PathBuf, + #[serde(default)] + pub(crate) sync: bool, + #[serde(default)] + pub(crate) derive_ser: bool, + #[serde(default)] + pub(crate) run: bool, +} + +fn default_queries_path() -> PathBuf { + PathBuf::from("queries") +} + +fn default_destination_path() -> PathBuf { + PathBuf::from("src/cornucopia.rs") +} + +impl From<&CodegenTest> for CodegenSettings { + fn from(codegen_test: &CodegenTest) -> Self { + Self { + is_async: !codegen_test.sync, + derive_ser: codegen_test.derive_ser, + } + } +} + +/// Error test case +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct ErrorTest { + pub(crate) name: String, + #[serde(default)] + pub(crate) query: String, + #[serde(default)] + pub(crate) schema: String, + pub(crate) error: String, +} + +impl From<&ErrorTest> for CodegenSettings { + fn from(_error_test: &ErrorTest) -> Self { + Self { + is_async: false, + derive_ser: false, + } + } +} diff --git a/test_integration/src/main.rs b/test_integration/src/main.rs index 0d07e574..0758ac8f 100644 --- a/test_integration/src/main.rs +++ b/test_integration/src/main.rs @@ -1,15 +1,15 @@ -use std::{ - borrow::Cow, - fmt::Display, - io::Write, - process::{Command, ExitCode, Stdio}, -}; +use std::{fmt::Display, process::ExitCode}; +use crate::{codegen::run_codegen_test, errors::run_errors_test}; use clap::Parser; -use cornucopia::{container, CodegenSettings, Error}; -use owo_colors::OwoColorize; +use cornucopia::container; -/// Start cornucopia test runner +mod codegen; +mod errors; +mod fixtures; +mod utils; + +/// Integration test CLI arguments #[derive(Parser, Debug)] #[clap(version)] struct Args { @@ -24,51 +24,10 @@ struct Args { podman: bool, } -#[derive(serde::Deserialize, serde::Serialize)] -struct ErrorTestSuite<'a> { - #[serde(borrow)] - test: Vec>, -} - -#[derive(serde::Deserialize, serde::Serialize)] -struct ErrorTest<'a> { - name: &'a str, - query: Option<&'a str>, - schema: Option<&'a str>, - query_name: Option<&'a str>, - error: Cow<'a, str>, -} - -#[derive(serde::Deserialize)] -struct CodegenTestSuite<'a> { - #[serde(borrow)] - codegen: Vec>, -} - -#[derive(serde::Deserialize)] -struct CodegenTest<'a> { - name: &'a str, - base_path: &'a str, - queries: Option<&'a str>, - destination: Option<&'a str>, - sync: Option, - derive_ser: Option, - run: Option, -} - -fn main() -> ExitCode { - let args = Args::parse(); - if test(args) { - ExitCode::SUCCESS - } else { - ExitCode::FAILURE - } -} - /// Print error to stderr fn display(result: Result) -> Result { if let Err(err) = &result { - eprintln!("{}", err); + eprintln!("{err}"); } result } @@ -93,220 +52,14 @@ fn test( successful.unwrap() } -/// Reset the current database -fn reset_db(client: &mut postgres::Client) -> Result<(), postgres::Error> { - client.batch_execute("DROP SCHEMA public CASCADE;CREATE SCHEMA public;") -} - -// Common schema to all error tests -const SCHEMA_BASE: &str = "CREATE TABLE author (id SERIAL, name TEXT);\n"; - -/// Run errors test, return true if all test are successful -fn run_errors_test( - client: &mut postgres::Client, - apply: bool, -) -> Result> { - let mut successful = true; - - let got_msg = if apply { - "Apply:".bright_black() - } else { - "Got:".bright_black() - }; - let expected_msg = if apply { - "Previous:".bright_black() +/// Main entry point +fn main() -> ExitCode { + let args = Args::parse(); + if test(args) { + ExitCode::SUCCESS } else { - "Expected:".bright_black() - }; - - let original_pwd = std::env::current_dir().unwrap(); - for file in std::fs::read_dir("fixtures/errors")? { - let file = file?; - let name = file.file_name().to_string_lossy().to_string(); - let content = std::fs::read_to_string(file.path())?; - let mut suite: ErrorTestSuite = toml::from_str(&content)?; - - println!("{} {}", "[error]".magenta(), name.magenta()); - for test in &mut suite.test { - // Generate file tree path - let temp_dir = tempfile::tempdir()?; - - // Reset db - reset_db(client)?; - - // We need to change current dir for error path to always be the same - std::env::set_current_dir(&temp_dir)?; - - // Generate schema - std::fs::write( - "schema.sql", - [SCHEMA_BASE, test.schema.unwrap_or_default()].concat(), - )?; - - // Generate queries files - std::fs::create_dir("queries")?; - let name = test.query_name.unwrap_or("test.sql"); - std::fs::write(&format!("queries/{name}"), test.query.unwrap_or_default())?; - - // Run codegen - let result: Result<(), cornucopia::Error> = (|| { - cornucopia::load_schema(client, vec!["schema.sql".into()])?; - cornucopia::generate_live( - client, - "queries", - None, - CodegenSettings { - is_async: false, - derive_ser: false, - }, - )?; - Ok(()) - })(); - - let err = result.err().map(Error::report).unwrap_or_default(); - if err.trim() == test.error.trim() { - println!("{} {}", test.name, "OK".green()); - } else { - successful = false; - println!( - "{} {}\n{}\n{}\n{}\n{}", - test.name, - "ERR".red(), - expected_msg, - test.error, - got_msg, - err, - ); - } - if apply { - test.error = Cow::Owned(err.trim().to_string()); - } - std::env::set_current_dir(&original_pwd)?; - } - - if apply { - // Format test descriptor and update error message if needed - let edited = toml::to_string_pretty(&suite)?; - std::fs::write(file.path(), edited)?; - } - } - Ok(successful) -} - -// Run codegen test, return true if all test are successful -fn run_codegen_test( - client: &mut postgres::Client, - apply: bool, -) -> Result> { - let mut successful = true; - let original_pwd = std::env::current_dir()?; - - for file in std::fs::read_dir("fixtures/codegen")? { - let file = file?; - let name = file.file_name().to_string_lossy().to_string(); - let content = std::fs::read_to_string(file.path())?; - println!("{} {}", "[codegen]".magenta(), name.magenta()); - - let suite: CodegenTestSuite = toml::from_str(&content)?; - - for codegen_test in suite.codegen { - std::env::set_current_dir(format!("../{}", codegen_test.base_path))?; - let queries_path = codegen_test.queries.unwrap_or("queries"); - let schema_path = "schema.sql"; - let destination = codegen_test.destination.unwrap_or("src/cornucopia.rs"); - let is_async = !codegen_test.sync.unwrap_or(false); - let derive_ser = codegen_test.derive_ser.unwrap_or(false); - - // Load schema - reset_db(client)?; - cornucopia::load_schema(client, vec![schema_path.to_string()])?; - - // If `--apply`, then the code will be regenerated. - // Otherwise, it is only checked. - if apply { - // Generate - cornucopia::generate_live( - client, - queries_path, - Some(destination), - CodegenSettings { - is_async, - derive_ser, - }, - ) - .map_err(Error::report)?; - // Format the generated file - Command::new("rustfmt") - .args(["--edition", "2021"]) - .arg(destination) - .output()?; - } else { - // Get currently checked-in generate file - let old_codegen = std::fs::read_to_string(destination).unwrap_or_default(); - // Generate new file - let new_codegen = cornucopia::generate_live( - client, - queries_path, - None, - CodegenSettings { - is_async, - derive_ser, - }, - ) - .map_err(Error::report)?; - // Format the generated code string by piping to rustfmt - let mut rustfmt = Command::new("rustfmt") - .args(["--edition", "2021"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; - rustfmt - .stdin - .as_mut() - .unwrap() - .write_all(new_codegen.as_bytes())?; - let formated_new_codegen = - String::from_utf8(rustfmt.wait_with_output()?.stdout).unwrap(); - - // If the newly generated file differs from - // the currently checked in one, return an error. - if old_codegen != formated_new_codegen { - Err(format!("\"{destination}\" is outdated"))?; - } - } - println!("(generate) {} {}", codegen_test.name, "OK".green()); - - // Use the base path as run path if `run` `Some(true)`. - let run_path = match codegen_test.run { - Some(true) => Some(codegen_test.base_path), - _ => None, - }; - if let Some(path) = run_path { - // Change current directory - std::env::set_current_dir(&original_pwd)?; - std::env::set_current_dir(&format!("../{}", path))?; - // Run - let result = Command::new("cargo").arg("run").output()?; - if result.status.success() { - println!("(run) {} {}", codegen_test.name, "OK".green()); - } else { - successful = false; - println!( - " {}\n{}", - "ERR".red(), - String::from_utf8_lossy(&result.stderr) - .as_ref() - .bright_black() - ); - } - } - - // Move back to original directory - std::env::set_current_dir(&original_pwd)?; - } + ExitCode::FAILURE } - - Ok(successful) } #[cfg(test)] @@ -318,7 +71,7 @@ mod test { assert!(test(crate::Args { apply_errors: false, apply_codegen: false, - podman: false + podman: true })) } } diff --git a/test_integration/src/utils.rs b/test_integration/src/utils.rs new file mode 100644 index 00000000..1ba234b1 --- /dev/null +++ b/test_integration/src/utils.rs @@ -0,0 +1,35 @@ +use std::{ + io::Write, + path::Path, + process::{Command, Stdio}, +}; + +/// Reset the current database +pub(crate) fn reset_db(client: &mut postgres::Client) -> Result<(), postgres::Error> { + client.batch_execute("DROP SCHEMA public CASCADE;CREATE SCHEMA public;") +} + +pub(crate) fn rustfmt_file(path: &Path) { + Command::new("rustfmt") + .args(["--edition", "2021"]) + .arg(path) + .output() + .unwrap(); +} + +pub(crate) fn rustfmt_string(string: &str) -> String { + // Format the generated code string by piping to rustfmt + let mut rustfmt = Command::new("rustfmt") + .args(["--edition", "2021"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + rustfmt + .stdin + .as_mut() + .unwrap() + .write_all(string.as_bytes()) + .unwrap(); + String::from_utf8(rustfmt.wait_with_output().unwrap().stdout).unwrap() +}