//! `flowsamples` is a set of sample flows, used to test and demonstrate flow, and different //! semantics and characteristics of flows that can be written. //! //! Each subdirectory holds a self-contained flow sample, with flow definition, docs etc and //! some of them provide their own function implementations. //! //! At the top-level there is a `build.rs` build script that iterates over all the sub-folders //! found and compiles the flow. That will include flow compilation to produce a `manifest.json` //! flow manifest. If any flow has provided function implementations then they will be compiled //! to WASM files. When the build has completed, all samples should be ready to be ran, using //! the `flowr` flow runner. //! //! This `flowsamples` binary provides a way for the user to run a sample manually and for //! automated test of all samples. //! //! See [main] for details on running the flow samples yourself directly. //! //! If `cargo test` is run on this crate, then all the samples will be ran using the provided //! test input and test args (see names of test files below) and the Stdout and File //! output will be compared to a set of predefined Stdout and File output to determine if //! the sample ran correctly. No Stderr output is expected so if any is detected the sample is //! deemed to have not ran correctly. use std::{env, fs, io}; use std::fs::File; use std::io::{BufRead, BufReader, ErrorKind}; use std::path::Path; use std::process::{Command, Stdio}; use tempdir::TempDir; /// Name of file where any Stdout will be written while executing a flowsample in test mode const TEST_STDOUT_FILENAME: &str = "test.stdout"; #[cfg(test)] /// Name of file where the Stdout is defined const EXPECTED_STDOUT_FILENAME : &str = "expected.stdout"; /// Name of file where any Stdin will be read from while executing a flowsample in test mode const TEST_STDIN_FILENAME : &str = "test.stdin"; /// Name of file where any Stderr will be written from while executing a flowsample in test mode const TEST_STDERR_FILENAME : &str = "test.stderr"; /// Name of file used for file output of a sample const TEST_FILE_FILENAME: &str = "test.file"; #[cfg(test)] /// Name of file where expected file output is defined const EXPECTED_FILE_FILENAME : &str = "expected.file"; /// Name of file where flow arguments for a flow sample test are read from const TEST_ARGS_FILENAME: &str = "test.args"; /// Run one or all of the flowsamples by typing `flowsamples` or `cargo run -p flowsamples` /// at the command line /// - If no additional argument is provided, then all flowsamples found are executed /// e.g. `flowsamples` or `cargo run -p flowsamples` /// - If the name of a flow sample is provided as an additional argument, then that sample will be run /// e.g. `flowsamples hello-world` or `cargo run -p flowsamples -- hello-world` fn main() -> io::Result<()> { println!("`flowsamples` version {}", env!("CARGO_PKG_VERSION")); println!( "Current Working Directory: `{}`", env::current_dir().expect("Could not get working directory").display() ); let home_dir_str = env::var("HOME").expect("Could not get $HOME"); let home_dir = Path::new(&home_dir_str); let samples_dir = home_dir.join(".flow/samples/flowsamples"); println!("Samples Root Directory: `{}`", samples_dir.display()); let samples_out_dir = TempDir::new("flowsamples").expect("Could not create temp directory"); let samples_out_dir = samples_out_dir.path(); println!("Samples Temp Directory used for input/output files: '{}'", samples_out_dir.display()); let args: Vec = env::args().collect(); match args.len() { 1 => { for entry in fs::read_dir(samples_dir)? { let e = entry?; if e.file_type()?.is_dir() && e.path().join("root.toml").exists() { run_sample(&e.path(), &samples_out_dir.join(e.file_name()), false, true)? } } } 2 => { run_sample(&samples_dir.join(&args[1]), &samples_out_dir.join(&args[1]), false, true)? } _ => eprintln!("Usage: {} ", args[0]), } Ok(()) } /// Run one specific flow sample fn run_sample(sample_dir: &Path, output_dir: &Path, flowrex: bool, native: bool) -> io::Result<()> { let manifest_path = sample_dir.join("manifest.json"); println!("\n\tRunning Sample: {:?}", sample_dir.file_name()); assert!(manifest_path.exists(), "Manifest not found at '{}'", manifest_path.display()); println!("\tOutput written to {}/", output_dir.display()); println!("\t\tSTDIN is read from {TEST_STDIN_FILENAME}"); println!("\t\tArguments are read from {TEST_ARGS_FILENAME}"); println!("\t\tSTDOUT is sent to {TEST_STDOUT_FILENAME}"); println!("\t\tSTDERR to {TEST_STDERR_FILENAME}"); println!("\t\tFile output to {TEST_FILE_FILENAME}"); let mut command_args: Vec = if native { vec!["--native".into()] } else { vec![] }; if flowrex { command_args.push("--context".into()) } command_args.push( manifest_path.display().to_string()); command_args.append(&mut args(sample_dir)?); fs::create_dir_all(output_dir).expect("Could not create output directory"); let output = File::create(output_dir.join(TEST_STDOUT_FILENAME)) .expect("Could not get directory as string"); let error = File::create(output_dir.join(TEST_STDERR_FILENAME)) .expect("Could not get directory as string"); let flowrex_child = if flowrex { match Command::new("flowrex").spawn() { Ok(child) => Some(child), Err(e) => return match e.kind() { ErrorKind::NotFound => Err(io::Error::new( ErrorKind::Other, format!("`flowrex` was not found! Check your $PATH. {e}"), )), _ => Err(io::Error::new( ErrorKind::Other, format!("Unexpected error running `flowrex`: {e}"), )), }, } } else { None }; println!("\tCommand line: 'flowr {}'", command_args.join(" ")); match Command::new("flowr") .args(command_args) .current_dir(output_dir.canonicalize()?) .stdin(Stdio::piped()) .stdout(Stdio::from(output)) .stderr(Stdio::from(error)) .spawn() { Ok(mut flowr_child) => { let stdin_file = sample_dir.join(TEST_STDIN_FILENAME); if stdin_file.exists() { let _ = Command::new("cat") .args(vec![stdin_file]) .stdout(flowr_child.stdin.take().ok_or_else(|| { io::Error::new( ErrorKind::Other, "Could not take STDIN of `flowr` process", ) })?) .spawn(); } flowr_child.wait_with_output()?; } Err(e) => return match e.kind() { ErrorKind::NotFound => Err(io::Error::new( ErrorKind::Other, format!("`flowr` was not found! Check your $PATH. {e}"), )), _ => Err(io::Error::new( ErrorKind::Other, format!("Unexpected error running `flowr`: {e}"), )), }, } // If flowrex was started - then kill it if let Some(mut child) = flowrex_child { println!("Killing 'flowrex'"); child.kill().expect("Failed to kill server child process"); } Ok(()) } /// Read the flow args from a file and return them as a Vector of Strings that will be passed to `flowr` fn args(sample_dir: &Path) -> io::Result> { let args_file = sample_dir.join(TEST_ARGS_FILENAME); let mut args = Vec::new(); // read args from the file if it exists, otherwise no args if let Ok(f) = File::open(args_file) { let f = BufReader::new(f); for line in f.lines() { args.push(line?); } } Ok(args) } #[cfg(test)] mod test { use std::{env, fs}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use serial_test::serial; use tempdir::TempDir; use crate::{EXPECTED_FILE_FILENAME, EXPECTED_STDOUT_FILENAME, TEST_FILE_FILENAME, TEST_STDERR_FILENAME, TEST_STDOUT_FILENAME}; fn test_sample(name: &str, flowrex: bool, native: bool) { let home_dir_str = env::var("HOME").expect("Could not get $HOME"); let home_dir = Path::new(&home_dir_str); let samples_dir = home_dir.join(".flow/samples/flowsamples"); println!("Samples Root Directory: `{}`", samples_dir.display()); let sample_dir = samples_dir.join(name); println!("'{}' Sample Directory: `{}`", name, sample_dir.display()); let samples_out_dir = TempDir::new("flowsamples").expect("Could not create temp directory"); let samples_out_dir = samples_out_dir.path(); let output_dir = samples_out_dir.join(name); println!("Temp directory used for input/output files: '{}'", output_dir.display()); // Remove any previous output let _ = fs::remove_file(output_dir.join(TEST_STDERR_FILENAME)); let _ = fs::remove_file(output_dir.join(TEST_FILE_FILENAME)); let _ = fs::remove_file(output_dir.join(TEST_STDOUT_FILENAME)); super::run_sample(&sample_dir, &output_dir, flowrex, native) .expect("Running of sample failed"); check_test_output(&sample_dir, &output_dir); // if test passed, remove output let _ = fs::remove_file(output_dir.join(TEST_STDERR_FILENAME)); let _ = fs::remove_file(output_dir.join(TEST_FILE_FILENAME)); let _ = fs::remove_file(output_dir.join(TEST_STDOUT_FILENAME)); } fn compare_and_fail(expected_path: PathBuf, actual_path: PathBuf) { if expected_path.exists() { let diff = Command::new("diff") .args(vec![&expected_path, &actual_path]) .stdin(Stdio::inherit()) .stderr(Stdio::inherit()) .stdout(Stdio::inherit()) .spawn() .expect("Could not get child process"); let output = diff.wait_with_output().expect("Could not get child process output"); if output.status.success() { return; } panic!("Contents of '{}' doesn't match the expected contents in '{}'", actual_path.display(), expected_path.display()); } } fn check_test_output(sample_dir: &Path, output_dir: &Path) { let error_output = output_dir.join(TEST_STDERR_FILENAME); if error_output.exists() { let contents = fs::read_to_string(&error_output).expect("Could not read from {STDERR_FILENAME} file"); if !contents.is_empty() { panic!( "Sample {:?} produced output to STDERR written to '{}'\n{contents}", sample_dir.file_name().expect("Could not get directory file name"), error_output.display()); } } compare_and_fail(sample_dir.join(EXPECTED_STDOUT_FILENAME), output_dir.join(TEST_STDOUT_FILENAME)); compare_and_fail(sample_dir.join(EXPECTED_FILE_FILENAME), output_dir.join(TEST_FILE_FILENAME)); } #[test] #[serial] fn test_args() { test_sample("args", false, true); } #[test] #[serial] fn test_arrays() { test_sample("arrays", false, true); } #[test] #[serial] fn test_factorial() { test_sample("factorial", false, true); } #[test] #[serial] fn test_fibonacci() { test_sample("fibonacci", false, true); } #[test] #[serial] fn test_fibonacci_wasm() { test_sample("fibonacci", false, false); } #[test] #[serial] fn test_fibonacci_flowrex() { test_sample("fibonacci", true, true); } #[test] #[serial] fn test_hello_world() { test_sample("hello-world", false, true); } #[test] #[serial] fn test_matrix_mult() { test_sample("matrix_mult", false, true); } #[test] #[serial] fn test_pipeline() { test_sample("pipeline", false, true); } #[test] #[serial] fn test_primitives() { test_sample("primitives", false, true); } #[test] #[serial] fn test_sequence() { test_sample("sequence", false, true); } #[test] #[serial] fn test_sequence_of_sequences() { test_sample("sequence-of-sequences", false, true); } #[test] #[serial] fn test_router() { test_sample("router", false, true); } #[test] #[serial] fn test_tokenizer() { test_sample("tokenizer", false, true); } // This sample uses provided implementations and hence is executing WASM #[test] #[serial] fn test_reverse_echo() { test_sample("reverse-echo", false, true); } // This sample uses provided implementations and hence is executing WASM #[test] #[serial] fn test_mandlebrot() { test_sample("mandlebrot", false, true); } #[test] #[serial] fn test_prime() { test_sample("prime", false, true); } }