1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
use std::{env, fs, io};
use std::fmt::Write;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};

/// Name of file where any Stdout will be written while executing an example
const TEST_STDOUT_FILENAME: &str = "test.stdout";

/// 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 am example
const TEST_STDIN_FILENAME : &str = "test.stdin";

/// Name of file where any Stderr will be written from while executing an example
const TEST_STDERR_FILENAME : &str = "test.stderr";

/// Name of file used for file output of a example
const TEST_FILE_FILENAME: &str = "test.file";

/// 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 example test are read from
const TEST_ARGS_FILENAME: &str = "test.args";

/// Run one specific flow example
pub fn run_example(source_file: &str, runner: &str, flowrex: bool, native: bool) {
    let mut sample_dir = PathBuf::from(source_file);
    sample_dir.pop();

    compile_example(&sample_dir, runner);

    println!("\n\tRunning example: {}", sample_dir.display());
    println!("\t\tRunner: {}", runner);
    println!("\t\tSTDIN is read from {TEST_STDIN_FILENAME}");
    println!("\t\tArguments are read from {TEST_ARGS_FILENAME}");
    println!("\t\tSTDOUT is saved in {TEST_STDOUT_FILENAME}");
    println!("\t\tSTDERR is saved in {TEST_STDERR_FILENAME}");
    println!("\t\tFile output is saved in {TEST_FILE_FILENAME}");

    // Remove any previous output
    let _ = fs::remove_file(sample_dir.join(TEST_STDERR_FILENAME));
    let _ = fs::remove_file(sample_dir.join(TEST_FILE_FILENAME));
    let _ = fs::remove_file(sample_dir.join(TEST_STDOUT_FILENAME));

    let mut runner_args: Vec<String> = if native {
        vec!["--native".into()]
    } else {
        vec![]
    };

    if runner == "flowrgui" {
        runner_args.push("--auto".into());
    }

    let flowrex_child = if flowrex {
        // set 0 executor threads in flowr coordinator, so that all job execution is done in flowrex
        runner_args.push("--threads".into());
        runner_args.push("0".into());
        Some(Command::new("flowrex").spawn().expect("Could not spawn flowrex"))
    } else {
        None
    };

    runner_args.push( "manifest.json".into());
    runner_args.append(&mut args(&sample_dir).expect("Could not get flow args"));

    let output = File::create(sample_dir.join(TEST_STDOUT_FILENAME))
        .expect("Could not create Test StdOutput File");
    let error = File::create(sample_dir.join(TEST_STDERR_FILENAME))
        .expect("Could not create Test StdError File ");

    println!("\tCommand line: '{} {}'", runner, runner_args.join(" "));
    let mut runner_child = Command::new(runner)
        .args(runner_args)
        .current_dir(sample_dir.canonicalize().expect("Could not canonicalize path"))
        .stdin(Stdio::piped())
        .stdout(Stdio::from(output))
        .stderr(Stdio::from(error))
        .spawn().expect("Could not spawn runner");

    let stdin_file = sample_dir.join(TEST_STDIN_FILENAME);
    if stdin_file.exists() {
        let _ = Command::new("cat")
            .args(vec![stdin_file])
            .stdout(runner_child.stdin.take().expect("Could not take STDIN"))
            .spawn();
    }

    runner_child.wait_with_output().expect("Could not get sub process output");

    // 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");
    }
}

/// Run an example and check the output matches the expected
pub fn test_example(source_file: &str, runner: &str, flowrex: bool, native: bool) {
    let _ = env::set_current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent().expect("Could not cd into flowr directory")
        .parent().expect("Could not cd into flow directory"));

    run_example(source_file, runner, flowrex, native);
    check_test_output(source_file);
}

/// Read the flow args from a file and return them as a Vector of Strings that will be passed in
fn args(sample_dir: &Path) -> io::Result<Vec<String>> {
    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)
}

/// Compile a flow example in-place in the `sample_dir` directory using flowc
pub fn compile_example(sample_path: &Path, runner: &str) {
    let sample_dir = sample_path.to_string_lossy();

    let mut command = Command::new("flowc");
    // -d for debug symbols
    // -g to dump graphs
    // -c to skip running and only compile the flow
    // -O to optimize the WASM files generated
    // -r <runner> to specify the runner to use
    // <sample_dir> is the path to the directory of the sample flow to compile
    let command_args = vec!["-d", "-g", "-c", "-O", "-r", runner, &sample_dir];

    let stat = command
        .args(&command_args)
        .status().expect("Could not get status of 'flowc' execution");

    if !stat.success() {
        panic!("Error building example, command line\n flowc {}", command_args.join(" "));
    }
}

pub fn check_test_output(source_file: &str) {
    let mut sample_dir = PathBuf::from(source_file);
    sample_dir.pop();

    let error_output = sample_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), sample_dir.join(TEST_STDOUT_FILENAME));
    compare_and_fail(sample_dir.join(EXPECTED_FILE_FILENAME), sample_dir.join(TEST_FILE_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 diff output");
        if output.status.success() {
            return;
        }
        panic!("Contents of '{}' doesn't match the expected contents in '{}'",
               actual_path.display(), expected_path.display());
    }
}

/// Execute a flow using separate server (coordinator) and client
pub fn execute_flow_client_server(example_name: &str, manifest: PathBuf) {
    let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let root_dir = crate_dir.parent().expect("Could not go to parent flowr dir");
    let samples_dir = root_dir.join("examples").join(example_name);

    let mut server_command = Command::new("flowrcli");

    // separate 'flowr' server process args: -n for native libs, -s to get a server process
    let server_args = vec!["-n", "-s"];

    println!("Starting 'flowrcli' as server with command line: 'flowrcli {}'", server_args.join(" "));

    // spawn the 'flowr' server process
    let mut server = server_command
        .args(server_args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("Failed to spawn flowrcli");

    // capture the discovery port by reading one line of stdout
    let stdout = server.stdout.as_mut().expect("Could not read stdout of server");
    let mut reader = BufReader::new(stdout);
    let mut discovery_port = String::new();
    reader.read_line(&mut discovery_port).expect("Could not read line");

    let mut client = Command::new("flowrcli");
    let manifest_str = manifest.to_string_lossy();
    let client_args =  vec!["-c", discovery_port.trim(), &manifest_str];
    println!("Starting 'flowrcli' client with command line: 'flowr {}'", client_args.join(" "));

    // spawn the 'flowrcli' client process
    let mut runner = client
        .args(client_args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("Could not spawn flowrcli process");

    // read it's stderr - don't fail, to ensure we kill the server
    let mut actual_stderr = String::new();
    if let Some(ref mut stderr) = runner.stderr {
        for line in BufReader::new(stderr).lines() {
            let _ = writeln!(actual_stderr, "{}", &line.expect("Could not read line"));
        }
    }

    // read it's stdout - don't fail, to ensure we kill the server
    let mut actual_stdout = String::new();
    if let Some(ref mut stdout) = runner.stdout {
        for line in BufReader::new(stdout).lines() {
            let _ = writeln!(actual_stdout, "{}", &line.expect("Could not read line"));
        }
    }

    println!("Killing 'flowr' server");
    server.kill().expect("Failed to kill server child process");

    if !actual_stderr.is_empty() {
        eprintln!("STDERR: {actual_stderr}");
        panic!("Failed due to STDERR output")
    }

    let expected_stdout = read_file(&samples_dir, "expected.stdout");
    if expected_stdout != actual_stdout {
        println!("Expected STDOUT:\n{expected_stdout}");
        println!("Actual STDOUT:\n{actual_stdout}");
        panic!("Actual STDOUT did not match expected.stdout");
    }
}

fn read_file(test_dir: &Path, file_name: &str) -> String {
    let expected_file = test_dir.join(file_name);
    if !expected_file.exists() {
        return "".into();
    }

    let mut f = File::open(&expected_file).expect("Could not open file");
    let mut buffer = Vec::new();
    f.read_to_end(&mut buffer).expect("Could not read from file");
    String::from_utf8(buffer).expect("Could not convert to String")
}