Skip to content

Instantly share code, notes, and snippets.

@soareschen
Created December 25, 2024 20:17
Show Gist options
  • Save soareschen/fcde9d72f01509b4cee9c8d4bedbedf2 to your computer and use it in GitHub Desktop.
Save soareschen/fcde9d72f01509b4cee9c8d4bedbedf2 to your computer and use it in GitHub Desktop.
Scala Cake in CGP
// This is an example of how the Scala cake pattern can be used in CGP,
// following the example at https://www.baeldung.com/scala/cake-pattern
pub mod traits {
use std::collections::BTreeMap;
use anyhow::Error;
use cgp::prelude::*;
// For simplicity of the example, we use `dyn` trait to represent test cases
pub trait TestCase {
fn execute_test(&self, test_environment: &BTreeMap<String, String>) -> Result<(), Error>;
}
// This follows the TestEnvironmentComponent Scala trait
#[cgp_component {
name: TestEnvironmentComponent,
provider: TestEnvironmentGetter,
context: Context,
}]
pub trait HasTestEnvironment {
fn test_environment(&self) -> &BTreeMap<String, String>;
}
// This follows the TestExecutorComponent Scala trait
#[cgp_component {
name: TestExecutorComponent,
provider: TestExecutor,
context: Context,
}]
pub trait CanExecuteTests {
fn execute_tests(&self, tests: &[Box<dyn TestCase>]) -> Result<(), Error>;
}
// This follows the LoggingComponent Scala trait
#[cgp_component {
name: LoggerComponent,
provider: Logger,
context: Context,
}]
pub trait CanLog {
fn log(&self, level: &str, message: &str);
}
}
pub mod impls {
use core::marker::PhantomData;
use anyhow::Error;
use super::traits::*;
// We show an example test executor that runs all tests in serial.
// The name implies that we can also define a different test executor that runs all tests in parallel.
pub struct ExecuteAllTestsInSerial;
impl<Context> TestExecutor<Context> for ExecuteAllTestsInSerial
where
// We use dependency injection to require `Context` to implement `HasTestEnvironment`
Context: HasTestEnvironment,
{
fn execute_tests(context: &Context, tests: &[Box<dyn TestCase>]) -> Result<(), Error> {
let test_environment = context.test_environment();
for test in tests {
test.execute_test(test_environment)?;
}
Ok(())
}
}
// We have a middleware test executor that would log the test environment using the logger,
// and then use `InExecutor` to execute the tests
pub struct LogAndExecuteTests<InExecutor>(pub PhantomData<InExecutor>);
impl<Context, InHandler> TestExecutor<Context> for LogAndExecuteTests<InHandler>
where
// We use dependency injection to require `Context` to implement both `CanLog` and `HasTestEnvironment`
Context: CanLog + HasTestEnvironment,
InHandler: TestExecutor<Context>,
{
fn execute_tests(context: &Context, tests: &[Box<dyn TestCase>]) -> Result<(), Error> {
context.log(
"INFO",
&format!(
"Executing tests with environments: {:?}",
context.test_environment()
),
);
InHandler::execute_tests(context, tests)
}
}
// A simple logger implementation that logs using `prinln!`.
// We could later replace this with more sophisticated loggers, such as `TracingLogger`.
pub struct PrintLoggger;
impl<Context> Logger<Context> for PrintLoggger {
fn log(_context: &Context, level: &str, message: &str) {
println!("[{level}] {message}");
}
}
}
pub mod contexts {
use std::collections::BTreeMap;
use cgp::prelude::*;
use super::impls::*;
use super::traits::*;
pub struct MyTestContext {
pub test_environment: BTreeMap<String, String>,
}
pub struct MyTestContextComponents;
impl HasComponents for MyTestContext {
type Components = MyTestContextComponents;
}
// We can easily customize how we want to execute the test and display logs,
// by choosing the appropriate providers.
delegate_components! {
MyTestContextComponents {
TestExecutorComponent: LogAndExecuteTests<ExecuteAllTestsInSerial>,
LoggerComponent: PrintLoggger,
}
}
// Define a custom `TestEnvironmentGetter` implementation for `MyTestContext`.
// It is also possible to have more general getter implementation, but we'd
// skip that inside this example.
impl TestEnvironmentGetter<MyTestContext> for MyTestContextComponents {
fn test_environment(context: &MyTestContext) -> &BTreeMap<String, String> {
&context.test_environment
}
}
}
#[cfg(test)]
pub mod tests {
use std::collections::BTreeMap;
use anyhow::Error;
use super::contexts::MyTestContext;
use super::traits::*;
pub struct DummyTest;
impl TestCase for DummyTest {
fn execute_test(
&self,
_test_environment: &std::collections::BTreeMap<String, String>,
) -> Result<(), anyhow::Error> {
Ok(())
}
}
#[test]
fn run_dummy_test() -> Result<(), Error> {
let test_context = MyTestContext {
test_environment: BTreeMap::from([("foo".into(), "bar".into())]),
};
test_context.execute_tests(&[Box::new(DummyTest)])?;
Ok(())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment