utilities/
lib.rs

1use std::{env, fs, io};
2use std::fmt::Write;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use std::io::Read;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8
9/// Name of file where any Stdout will be written while executing an example
10const TEST_STDOUT_FILENAME: &str = "test.stdout";
11
12/// Name of file where the Stdout is defined
13const EXPECTED_STDOUT_FILENAME : &str = "expected.stdout";
14
15/// Name of file where any Stdin will be read from while executing am example
16const TEST_STDIN_FILENAME : &str = "test.stdin";
17
18/// Name of file where any Stderr will be written from while executing an example
19const TEST_STDERR_FILENAME : &str = "test.stderr";
20
21/// Name of file used for file output of a example
22const TEST_FILE_FILENAME: &str = "test.file";
23
24/// Name of file where expected file output is defined
25const EXPECTED_FILE_FILENAME : &str = "expected.file";
26
27/// Name of file where flow arguments for a flow example test are read from
28const TEST_ARGS_FILENAME: &str = "test.args";
29
30/// Run one specific flow example
31pub fn run_example(source_file: &str, runner: &str, flowrex: bool, native: bool) {
32    let mut sample_dir = PathBuf::from(source_file);
33    sample_dir.pop();
34
35    compile_example(&sample_dir, runner);
36
37    println!("\n\tRunning example: {}", sample_dir.display());
38    println!("\t\tRunner: {}", runner);
39    println!("\t\tSTDIN is read from {TEST_STDIN_FILENAME}");
40    println!("\t\tArguments are read from {TEST_ARGS_FILENAME}");
41    println!("\t\tSTDOUT is saved in {TEST_STDOUT_FILENAME}");
42    println!("\t\tSTDERR is saved in {TEST_STDERR_FILENAME}");
43    println!("\t\tFile output is saved in {TEST_FILE_FILENAME}");
44
45    // Remove any previous output
46    let _ = fs::remove_file(sample_dir.join(TEST_STDERR_FILENAME));
47    let _ = fs::remove_file(sample_dir.join(TEST_FILE_FILENAME));
48    let _ = fs::remove_file(sample_dir.join(TEST_STDOUT_FILENAME));
49
50    let mut runner_args: Vec<String> = if native {
51        vec!["--native".into()]
52    } else {
53        vec![]
54    };
55
56    if runner == "flowrgui" {
57        runner_args.push("--auto".into());
58    }
59
60    let flowrex_child = if flowrex {
61        // set 0 executor threads in flowr coordinator, so that all job execution is done in flowrex
62        runner_args.push("--threads".into());
63        runner_args.push("0".into());
64        Some(Command::new("flowrex").spawn().expect("Could not spawn flowrex"))
65    } else {
66        None
67    };
68
69    runner_args.push( "manifest.json".into());
70    runner_args.append(&mut args(&sample_dir).expect("Could not get flow args"));
71
72    let output = File::create(sample_dir.join(TEST_STDOUT_FILENAME))
73        .expect("Could not create Test StdOutput File");
74    let error = File::create(sample_dir.join(TEST_STDERR_FILENAME))
75        .expect("Could not create Test StdError File ");
76
77    println!("\tCommand line: '{} {}'", runner, runner_args.join(" "));
78    let mut runner_child = Command::new(runner)
79        .args(runner_args)
80        .current_dir(sample_dir.canonicalize().expect("Could not canonicalize path"))
81        .stdin(Stdio::piped())
82        .stdout(Stdio::from(output))
83        .stderr(Stdio::from(error))
84        .spawn().expect("Could not spawn runner");
85
86    let stdin_file = sample_dir.join(TEST_STDIN_FILENAME);
87    if stdin_file.exists() {
88        let _ = Command::new("cat")
89            .args(vec![stdin_file])
90            .stdout(runner_child.stdin.take().expect("Could not take STDIN"))
91            .spawn();
92    }
93
94    runner_child.wait_with_output().expect("Could not get sub process output");
95
96    // If flowrex was started - then kill it
97    if let Some(mut child) = flowrex_child {
98        println!("Killing 'flowrex'");
99        child.kill().expect("Failed to kill server child process");
100    }
101}
102
103/// Run an example and check the output matches the expected
104pub fn test_example(source_file: &str, runner: &str, flowrex: bool, native: bool) {
105    let _ = env::set_current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR"))
106        .parent().expect("Could not cd into flowr directory")
107        .parent().expect("Could not cd into flow directory"));
108
109    run_example(source_file, runner, flowrex, native);
110    check_test_output(source_file);
111}
112
113/// Read the flow args from a file and return them as a Vector of Strings that will be passed in
114fn args(sample_dir: &Path) -> io::Result<Vec<String>> {
115    let args_file = sample_dir.join(TEST_ARGS_FILENAME);
116
117    let mut args = Vec::new();
118
119    // read args from the file if it exists, otherwise no args
120    if let Ok(f) = File::open(args_file) {
121        let f = BufReader::new(f);
122
123        for line in f.lines() {
124            args.push(line?);
125        }
126    }
127
128    Ok(args)
129}
130
131/// Compile a flow example in-place in the `sample_dir` directory using flowc
132pub fn compile_example(sample_path: &Path, runner: &str) {
133    let sample_dir = sample_path.to_string_lossy();
134
135    let mut command = Command::new("flowc");
136    // -d for debug symbols
137    // -g to dump graphs
138    // -c to skip running and only compile the flow
139    // -O to optimize the WASM files generated
140    // -r <runner> to specify the runner to use
141    // <sample_dir> is the path to the directory of the sample flow to compile
142    let command_args = vec!["-d", "-g", "-c", "-O", "-r", runner, &sample_dir];
143
144    let stat = command
145        .args(&command_args)
146        .status().expect("Could not get status of 'flowc' execution");
147
148    if !stat.success() {
149        panic!("Error building example, command line\n flowc {}", command_args.join(" "));
150    }
151}
152
153pub fn check_test_output(source_file: &str) {
154    let mut sample_dir = PathBuf::from(source_file);
155    sample_dir.pop();
156
157    let error_output = sample_dir.join(TEST_STDERR_FILENAME);
158    if error_output.exists() {
159        let contents = fs::read_to_string(&error_output).expect("Could not read from {STDERR_FILENAME} file");
160
161        if !contents.is_empty() {
162            panic!(
163                "Sample {:?} produced output to STDERR written to '{}'\n{contents}",
164                sample_dir.file_name().expect("Could not get directory file name"),
165                error_output.display());
166        }
167    }
168
169    compare_and_fail(sample_dir.join(EXPECTED_STDOUT_FILENAME), sample_dir.join(TEST_STDOUT_FILENAME));
170    compare_and_fail(sample_dir.join(EXPECTED_FILE_FILENAME), sample_dir.join(TEST_FILE_FILENAME));
171}
172
173fn compare_and_fail(expected_path: PathBuf, actual_path: PathBuf) {
174    if expected_path.exists() {
175        let diff = Command::new("diff")
176            .args(vec![&expected_path, &actual_path])
177            .stdin(Stdio::inherit())
178            .stderr(Stdio::inherit())
179            .stdout(Stdio::inherit())
180            .spawn()
181            .expect("Could not get child process");
182        let output = diff.wait_with_output().expect("Could not get diff output");
183        if output.status.success() {
184            return;
185        }
186        panic!("Contents of '{}' doesn't match the expected contents in '{}'",
187               actual_path.display(), expected_path.display());
188    }
189}
190
191/// Execute a flow using separate server (coordinator) and client
192pub fn execute_flow_client_server(example_name: &str, manifest: PathBuf) {
193    let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
194    let root_dir = crate_dir.parent().expect("Could not go to parent flowr dir");
195    let samples_dir = root_dir.join("examples").join(example_name);
196
197    let mut server_command = Command::new("flowrcli");
198
199    // separate 'flowr' server process args: -n for native libs, -s to get a server process
200    let server_args = vec!["-n", "-s"];
201
202    println!("Starting 'flowrcli' as server with command line: 'flowrcli {}'", server_args.join(" "));
203
204    // spawn the 'flowr' server process
205    let mut server = server_command
206        .args(server_args)
207        .stdin(Stdio::piped())
208        .stdout(Stdio::piped())
209        .stderr(Stdio::piped())
210        .spawn()
211        .expect("Failed to spawn flowrcli");
212
213    // capture the discovery port by reading one line of stdout
214    let stdout = server.stdout.as_mut().expect("Could not read stdout of server");
215    let mut reader = BufReader::new(stdout);
216    let mut discovery_port = String::new();
217    reader.read_line(&mut discovery_port).expect("Could not read line");
218
219    let mut client = Command::new("flowrcli");
220    let manifest_str = manifest.to_string_lossy();
221    let client_args =  vec!["-c", discovery_port.trim(), &manifest_str];
222    println!("Starting 'flowrcli' client with command line: 'flowr {}'", client_args.join(" "));
223
224    // spawn the 'flowrcli' client process
225    let mut runner = client
226        .args(client_args)
227        .stdin(Stdio::piped())
228        .stdout(Stdio::piped())
229        .stderr(Stdio::piped())
230        .spawn()
231        .expect("Could not spawn flowrcli process");
232
233    // read it's stderr - don't fail, to ensure we kill the server
234    let mut actual_stderr = String::new();
235    if let Some(ref mut stderr) = runner.stderr {
236        for line in BufReader::new(stderr).lines() {
237            let _ = writeln!(actual_stderr, "{}", &line.expect("Could not read line"));
238        }
239    }
240
241    // read it's stdout - don't fail, to ensure we kill the server
242    let mut actual_stdout = String::new();
243    if let Some(ref mut stdout) = runner.stdout {
244        for line in BufReader::new(stdout).lines() {
245            let _ = writeln!(actual_stdout, "{}", &line.expect("Could not read line"));
246        }
247    }
248
249    println!("Killing 'flowr' server");
250    server.kill().expect("Failed to kill server child process");
251
252    if !actual_stderr.is_empty() {
253        eprintln!("STDERR: {actual_stderr}");
254        panic!("Failed due to STDERR output")
255    }
256
257    let expected_stdout = read_file(&samples_dir, "expected.stdout");
258    if expected_stdout != actual_stdout {
259        println!("Expected STDOUT:\n{expected_stdout}");
260        println!("Actual STDOUT:\n{actual_stdout}");
261        panic!("Actual STDOUT did not match expected.stdout");
262    }
263}
264
265fn read_file(test_dir: &Path, file_name: &str) -> String {
266    let expected_file = test_dir.join(file_name);
267    if !expected_file.exists() {
268        return "".into();
269    }
270
271    let mut f = File::open(&expected_file).expect("Could not open file");
272    let mut buffer = Vec::new();
273    f.read_to_end(&mut buffer).expect("Could not read from file");
274    String::from_utf8(buffer).expect("Could not convert to String")
275}